Import from subfolder of npm package - javascript

I've been working on creating a small library of React components for use in several other projects. I am publishing the package internally (using a private GitHub repository) and then including in another project. However, when I go to import from a subdirectory of the package I am not able to do so as the paths don't match.
The projects using the package all utilize webpack to bundle/transpile code as I am trying to avoid doing any building in the component library if possible.
Directory Structure
- package.json
- src/
- index.js
- Button/
- index.js
- Button.jsx
- ButtonGroup.jsx
- Header/
- index.js
- Header.jsx (default export)
package.json
...
"main": "./src/index.js",
"scripts": "",
...
src/Button/index.js
import Button from './Button';
import ButtonGroup from './ButtonGroup';
export default Button;
export { Button, ButtonGroup};
src/index.js
Is this file actually necessary if only importing from subdirectories?
import Button from './Button';
import ButtonGroup from './Button/ButtonGroup';
import Header from './Header';
export { Button, ButtonGroup, Header };
Other Project
// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';
Example
Material-UI is a library of React components that is used by requiring in the following fashion: import { RadioButtonGroup } from 'material-ui/RadioButton. I've tried to figure out how this works for them but to no avail yet.
Similar Questions
How would I import a module within an npm package subfolder with webpack?
This is very nearly the correct approach I require, except that the import path used there involved the src/ directory, which I am trying to avoid (should be component-library/item, not component-library/src/item (which does work currently though))
Publishing Flat NPM Packages
This is exactly what I want except that I was hoping to not have a "build" phase in the package (rely on importing locations to build/transpile)
Questions
Can I skip the src/ directory somehow in the import path?
Can I skip any type of build phase in the package (so developers don't have to build before committing)?
How does a package similar to material-ui handle this?

Can I skip the src/ directory somehow in the import path?
Yes. Using the package.json "exports" field, which should be supported by Webpack in a near future (see this issue), but has already been supported by Node since Node 12 LTS following the Bare Module Specifier Resolution proposal:
package.json
...
"main": "./src/index.js",
"type": "module",
...
"exports": {
"./Button": "./src/Button/index.js",
"./Header": "./src/Header/index.js"
},
...
Now, the following code:
// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';
should be translated to:
import { Button, ButtonGroup } from 'components-library/src/Button/index.js';
which should correctly import the requested modules.
Caveat
Now, it would certainly be tempting to try a simpler version like:
...
"exports": {
"./Button": "./src/Button/",
"./Header": "./src/Header/"
},
...
so as the usual import statement
import { ... } from 'components-library/Button';
gets translated to
import { ... } from 'components-library/src/Button';
This looks nice, but it will not work in this case, because your submodules don't have each their own package.json file but rely on their index.js file to be found.
/!\ Unlike in CommonJS, there is no automatic searching for index.js or index.mjs or for file extensions.
src/index.js - Is this file actually necessary if only importing from subdirectories?
I don't think so, but you can keep it if you want.
Can I skip any type of build phase in the package?
Using the "exports" field does not require you to transpile your code.

The answer may depend on how you installed your components library. If you did it via either npm install <git-host>:<git-user>/<repo-name> or npm install <git repo url>,
You should be able to import {Button} from 'component-library/Button' as is, according to your first linked question. Similar to Node's require() resolution, Webpack should resolve subdirectories within component-library relative to component-library's entry point. You can find the docs on customizing the resolution behavior via the webpack.config.resolve property. material-ui seems to rely on resolving subdirectory imports from the module entry directory.
To distribute an ES module library, there's no need for building before distribution. However, projects such as create-react-app may need a pre-transpiled version.
Alternately, you can write import {Button} from 'components-library'.
Webpack will trace the dependencies back through each index without a fuss.

you have to install babel-plugin-module-resolver package
Specify the package relative path in your .babelrc file alias like this
{
"plugins": [
["module-resolver", {
"alias": {
"components-library": "./node_module/components-library"
}
}]
]
}
then you can import subdir of npm package like this
import { Button, ButtonGroup } from 'components-library/Button';

One of the possible solutions there is webpack aliasing system.
You can create another project, call it for example 'app-aliases', so your aliases will be reusable.
This project will has one js file with all of your packages paths:
const path = require('path');
module.exports = {
'#components': path.resolve(__dirname, 'node_modules/components-library/src'),
'#another': path.resolve(__dirname, 'node_modules/any/path/you/want'),
}
And then add it to the webpack configuration in any project which will be responsible for building/transpiling:
webpack.config.js
const appAliases = require('app-aliases');
const config = {
...
resolve: {
alias: {
...appAlises
}
}
}
In the runtime code you will be able to use it like this:
import {Button} from '#components/Button';
import {Something} from '#another'
If you are using typescript you will need to add the same aliases to the paths tsconfig property.
So answers to your questions are:
Yes, you can use any path in aliases
Yes, it is not necessary to build all of your projects
I see that now mui uses imports from directi packages (core for example), see https://material-ui.com/components/radio-buttons/ there is import Radio from '#material-ui/core/Radio';. But I hope they using re-export that I described below.
Also about node.js resolution mechanism.
When you import some library it tries to find node_modules/some-library/package.json and then main property inside it. This property should lead to your main entry point. Usually it is src/index.js (you should set it in package.json if it is no there yet). In this file you can re-export anything you want from internals file structure and will be able to use it without the full path.
Please see this repo for some examples.

I'am an angular developer never used react but what I could tell that material-ui are using monorepo where same concept exists in angular where we create one workspace and this workspace hold multiple project/packages as named in react. for more info Workspaces with Yarn
Material-ui using fake paths in tsconfig to make it appears like src folder doesn't exists this from the git you provided: tsconfig.json

This is possible but requires publishing a curated dist folder rather then the root of your project.
The whole thing is rather simple if you understand how module resolution works, and you just need a small script to prepare your distribution.
Lest I repeat all the details here, please see my answer for Importing from subfolders for a javascript package.

Related

Export shareable eslint config from a React library's dist folder for other projects

I have a published React library with a dist folder where I export numerous default React components for other projects and I would like to export my eslint config from there.
I've seen people do it in basic npm packages in the main index.js file, but I would like to export everything from one package and not rely on a second package for the eslint config.
I added all my rules to /src/components/eslint/index.js - this then gets translated into /dist/eslint/index.js
Is there any way to import this specific index.js with my eslint rules as eslint config in another project?
In another project I already tried linking it in package.json
and also in the .eslintrc.json file
without luck.
Any ideas?

Solve having more than one copy of React in the same app

I'm developing a React module locally. For that, I'm linking my module using npm link.
The module is imported successfully but hooks are failing inside the module. It's throwing the following error:
Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons: 1. You might have mismatching versions of React and the
renderer (such as React DOM) 2. You might be breaking the Rules of
Hooks 3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to
debug and fix this problem.
Checking the suggestions at React docs, I can confirm my app is using duplicate versions of React since the following code returns false:
// node_modules/mymodule/src/index.js
export { default as ReactFromModule } from 'react'
// src/index.js
import React from 'react'
import { ReactFromModule } from 'mymodule'
console.log(React === ReactFromModule) //false
This issue is full of suggestions but they are confusing. How can I solve it?
Note: Im not breaking rules of hooks, the error appears only when importing the module from an application.
In the module you are developing, add the conflicting packages to peerDependencies (and remove them from dependencies or devDependencies):
// package.json
"peerDependencies": {
"react": "16.13.1",
"react-dom": "16.13.1"
},
Execute npm install in your module.
Now add react and react-dom to the webpack configuration of your module as externals. These packages shouldnt be included in the bundle of the module (the app that uses the module will provide them):
// webpack.config.js
module.exports = {
/*
rest of config...
*/
output: {
filename: "index.js",
pathinfo: false,
libraryTarget: 'umd', // In my case, I use libraryTarget as 'umd'. Not sure if relevant
},
externals: {
// Use external version of React
"react": {
"commonjs": "react",
"commonjs2": "react",
"amd": "react",
"root": "React"
},
"react-dom": {
"commonjs": "react-dom",
"commonjs2": "react-dom",
"amd": "react-dom",
"root": "ReactDOM"
}
},
};
Then, after building your module, in your application you can check that both versions are now the same:
// node_modules/mymodule/src/index.js
export { default as ReactFromModule } from 'react'
// src/index.js
import React from 'react'
import { ReactFromModule } from 'mymodule'
console.log(React === ReactFromModule) // true :)
Adding react and react-dom as peerDependencies in the package.json didn't work for me.
I had to add an alias to the webpack configuration file:
// webpack.config.js
resolve: {
alias: {
react: path.resolve('./node_modules/react'),
}
In response to another comment, merely moving React to peerDependencies does not adequately resolve the issue in all cases. I would reply to that comment directly, but StackOverflow requires more reputation to respond to wrong answers than it does to post them.
I have a shared React component module built using Webpack and have run into the same issue. I've outlined one possible fix in this comment below which requires modifying peerDependencies and using npm link in a fashion similar to the answer shared by mtkopone.
https://github.com/facebook/react/issues/13991#issuecomment-841509933
My solution is a bit hacky and I wouldn't recommend it for long-term use. If you are using Webpack (which you tagged this question as), this article may detail a more permanent solution (https://medium.com/codex/duplicate-copy-of-react-errors-when-using-npm-link-e5011de0995d). I haven't tried it yet, but the author seems to have tried all the (incorrect) solutions out there and is also running into the hooks issue while trying to build shared component libraries.
The author of that article is trying to debug a Create-React-App app. While CRA uses webpack under the hood, you can't access the webpack.config directly, so the author has to perform some workarounds to do so. If you aren't using CRA, but just plain Webpack, then you could consider using the resolve.alias section of webpack.config to ensure there are no duplicate copies of React (see: https://blog.maximeheckel.com/posts/duplicate-dependencies-npm-link/)
I was attempting to use the peerDependencies and removal of the devDependencies and it was failing.
It turned out I had a node_modules folder in one of the parent folders of the library I was working on and the duplicate version of React was being loaded from there instead of the tool that was trying to use the React library.
Rather than editing the devDependencies to remove react I just wrote a small script to delete anything that's in the peerDependencies from the node_modules folder.
npm view --json=true . peerDependencies | jq -r 'keys | .[] | #text' | while read dep; do rm -r ./node_modules/${dep} && echo Removed ${dep}; done
In my case I was also missing import React from 'react' from couple of files.
check this

import the current package export by the package.json name

I'm interested to know if there's a convention that allows a person to test the expected usage of a package from within the package. Consider the following package.json:
{
"name": "#place/fn",
"version: "1.0.0"
}
From within this very package, I'd like to have a test.js file, with exactly following code:
import mainThing, { anotherThing } from '#place/fn';
Is this possible with the right directory structure or package.json configuration? I'm also fine with CommonJS syntax if that would work better.
Ok, found an answer on this but there's a ton of other related things I found in the process that could probably help others.
First, now I understand what I wanted to do is install the current package as a local dependency. That can be done in the package.json using the following syntax found in this answer.
{
"name": "#place/fn",
"dependencies": {
"#place/fn": "file:./"
}
}
Now, I can run npm i to install that change and code the following test file:
import mainThing, { anotherThing } from '#place/fn';
When running the test file, the import will act as if it was installed into another package; better mimicking the intended use.
Another thing I discovered is that you can alias packages installed from npm using the syntax found in this answer.
"dependencies": {
"case-1.5.3": "npm:case#^1.5.3",
"kool": "npm:case#^1.6.1"
}
Which then allows you to do the following:
let Case = require('case-1.5.3');
let Kool = require('kool');
And finally, another thing I found is that Node#13 allows for a new key on the package.json which maps directories found in this answer. There are other packages (like module-alias) that do something similar with the big difference being the installed solutions (ie: module-alias) only work from within the package you are working with. It seems like the new exports key on package.json will do the mapping for use in other packages.
// ./node_modules/es-module-package/package.json
{
"name": "es-module-package",
"exports": {
"./my/": "./src/js/lib/my/"
}
}
import thing from 'es-module-package/my/thing.js';
// Loads ./node_modules/es-module-package/src/js/lib/my/thing.js
Ultimately the purpose of all of this was to test that the style of syntaxes I provide outside the package could be tested from within the package instead of installing into another package and testing there. Now I can check that a common lodash import syntax works for my package.
import get from 'lodash/get'
(I wish I knew what the terminology was for requiring a clean, perhaps aliased, path of a package instead of destructuring the main export.)
To access the main export of a package from withing the package itself, add a main entry point export to package.json:
{
"name": "#place/fn",
"version: "1.0.0",
"exports": {
".": "./index.js"
}
}
Replace ./index.js with the path to the main module.
Then, in test.js, you can import it with the expected syntax:
import mainThing, { anotherThing } from '#place/fn';
Be careful that when the exports field is defined, all subpaths of the package are no longer available to external importers unless explicitly declared in exports. Typically, you will want at least package.json to be exportable beside the main module:
"exports": {
".": "./index.js",
"./package.json": "./package.json"
}
When you import a package, it's up to the package.json file to tell you where to go... Say you had the following file structure
project
demo
demo-index.js
dist
project.cjs.js
project.es.js
project.umd.js
src
index.js
package.json
You want to code your package inside src, build it (using something like Vite or Bili) and then inside your demo folder, see if the built files (inside dist) work as intended.
So your package.json should have:
{
"name": "project",
"version": "1.0.0",
"main": "dist/project.cjs.js",
"module": "dist/project.es.js",
...
}
Now, inside demo/demo-index.js you could just import from ../src/index.js and use the uncompiled version.
What you can also do is import from your project root directory and get the compiled version:
// demo/demo-index.js
import Project from "../";
This will import via the package.json file and give you the file listed under "module", i.e. dist/project.es.js.

Allow auto import my React library on vscode

I'm trying to do a library of components for React and publish on npm using webpack and babel to compile to Es5.
Almost everything worked, but for some reason, the project that consumes this lib cant auto import their components
I have a project on github with the setup I used:
https://github.com/dattebayorob/react-loading
//webpack.config.js
https://github.com/dattebayorob/react-loading/blob/master/webpack.config.js
//.babelrc
https://github.com/dattebayorob/react-loading/blob/master/.babelrc
//package.json
https://github.com/dattebayorob/react-loading/blob/master/package.json
I'm expecting to import components from my lib with 'CTRL+space' when typing then
Now, I can import from my lib manualy with import { Component } from 'my-react-lib'
Sometimes, when using Typescript in VSCode, you have to run the Typescript: Restart TS Server command in your command palette for auto import to work after creating new files. It's a bug.
On dattebayorob/react-loading/index.d.ts try:
export * from './src/components'
In package.json, you have "main": "./index.d.ts", but that's not a valid JS file, as it does not contain actual code, only type definitions.
In a library, usually you need to have an src/index.js file that imports / exports all components and in package.json you add the build artifact as main: "main": "dist/index.js".
Also, don't forget to explicitly specify the files: ["dist"] attribute in package.json so the src folder is not downloaded when your package is installed.

How to export multiple library variants from a single NPM package

I'm creating a node library that could be partially used in web browsers.
My current structure is something like:
package.json
lib/
index.js
node.js
web.js
modules/
some-function.js
some-node-function.js
some-web-function.js
...
"lib/index.js" is specified as "main" in package.json
I'm using each file inside lib/ to re-export functions from modules/ filtered by target (all/node/web)
And I would like to use it like this:
import fullLibrary from "my-library";
import {someFunction} from "my-library";
import webOnlyLibrary from "my-library/web";
import {someWebFunction} from "my-library/web";
import nodeOnlyLibrary from "my-library/node";
import {someNodeFunction} from "my-library/node";
BUT keeping index.js, node.js and web.js inside /lib!
Currently only the first two import statements work (because index.js is pointed from package.json).
I know that I could place those files in the root of the package and it would work as expected when importing, but I was wondering if there was a way to do the same while keeping the files in /lib for cleanness.
In any case, is this the recommended way to expose multiple variants of a same library from a single NPM package?
Just for anyone that is having similar questions regarding libraries structure and organization.
Back then I started working with monorepos using Lerna and Yarn Workspaces and I haven't regret it. I think this is the most efficient way to go in terms of modularity, readability, scalability and maintainability.
Specifically, the question code would be traduced into this:
import webOnlyLibrary from "#my-library/web";
import {someWebFunction} from "#my-library/web";
import nodeOnlyLibrary from "#my-library/node";
import {someNodeFunction} from "#my-library/node";

Categories

Resources