Yarn won't publish/add both ESM and CJS versions of package? - javascript

We have an application bundled with Parcel that consumes a UI library (react components). The UI library is bundled with Rollup and published as a private package on NPM.
I've been trying to migrate our application to use Parcel 2, but Parcel complains that it cannot locate the ESM version of the UI library within the dist folder. Sure enough, when I check my node_modules directory, the dist folder for the UI lib includes only one file: index.cjs.js.
The weird part is that the UI lib is set up to build in both CJS and ESM formats with sourcemaps. When I build the project locally, Rollup produces both CJS and ESM files and their sourcemaps: index.cjs.js, index.cjs.js.map, index.esm.js, and index.esm.js.map. So, somehow, it seems that either: (1) Yarn is only publishing the CJS version of the library to NPM or (2) When the UI lib is added to the application, only the CJS version is being built. Neither of those situations makes sense to me.
Here's the relevant sections of our package.json and rollup.config.js files:
{
"name": "#thecb/components",
"version": "4.0.23",
"description": "Common lib for CityBase react components",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"source": "src/index.js",
"scripts": {
"storybook": "start-storybook",
"build": "rollup -cm"
},
import resolve from "rollup-plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";
import babel from "rollup-plugin-babel";
import json from "rollup-plugin-json";
import visualizer from "rollup-plugin-visualizer";
import pkg from "./package.json";
import * as formattedInput from "formatted-input";
const globals = {
react: "React",
"react-dom": "ReactDOM"
};
const plugins = [
resolve({ preferBuiltins: false }),
babel({
exclude: "node_modules/**",
presets: ["#babel/env", "#babel/preset-react"]
}),
json(),
commonjs({
include: "node_modules/**",
namedExports: {
"formatted-input": Object.keys(formattedInput)
}
}),
visualizer()
];
const external = [...Object.keys(pkg.peerDependencies || {})];
const output_data = [
{
file: pkg.main,
format: "cjs"
},
{
file: pkg.module,
format: "esm"
}
];
export default output_data.map(({ file, format }) => ({
input: "src/index.js",
output: {
file,
format,
globals
},
plugins,
external
}));
Anyone have any idea why the ESM version of this lib either wouldn't be published or installed?

Late reply but I ran into something very similar today. Not using rollup but rather tsc directly to output dist/cjs/* and dist/esm/*.
I found that my build process locally produced both outputs but the tarball produced by yarn publish only contained dist/cjs/*. TBH I'm not sure why; my current theory is that yarn is somehow using the "main": "dist/cjs/index.js" and inferring some defaults for package inclusion from that.
What I can tell you is that by adding "files": "dist" to my package.json I got yarn to behave itself and add both dist/cjs/* and dist/esm/* to the package tarball as I initially expected.
https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files

Related

Why importing from a linked local ES modules package using the package name works with "main" property but fails with "module"

Question
Why does importing from a linked local NPM pacakage (built as an ES module) using the pacakge name works when the pacakge.json has the "main" property, but fails when it has the "module" property?
Setup
More spicifically, if we have the following setup:
exporter: A Node.js package which is an ES module that exports something (e.g., using export default).
importer: A Node.js module that tries to import what exporter exports, using import something from 'exporter'.
Using npm link to locally link exporter to importer.
Then:
The setup runs successfully if exporter's package.json uses the main property.
The setup run fails if exporter's package.json uses the module property.
This failure can be "fixed" by using import something from 'exporter/dist/bundle.js', but that's unacceptable.
Why is that? I guess I'm doing something wrong?
For some more info about this setup, see my other question
Code
exporter
|- webpack.config.js
|- package.json
|- /src
|- index.js
|- /dist
|- bundle.js
webpack.config.js:
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
library: {
type: "module",
},
},
experiments: {
outputModule: true,
},
};
package.json:
{
"name": "exporter",
"version": "1.0.0",
"main": "dist/bundle.js", <-- *** NOTE THIS LINE ***
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0"
},
"type": "module"
}
index.js:
function util() {
return "I'm a util!";
}
export default util;
importer
|- package.json
|- /src
|- index.js
package.json
{
"name": "importer",
"version": "1.0.0",
"type": "module"
}
index.js
import util from 'exporter';
console.log(util());
Then:
Linking:
⚡ cd exporter
⚡ npm link
⚡ cd importer
⚡ npm link exporter
Executing:
⚡ node importer.js
I'm a util!
However, if exporter's package.json is changed to:
{
"name": "exporter",
"version": "1.0.0",
"module": "dist/bundle.js", <-- *** NOTE THIS LINE ***
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0"
},
"type": "module"
}
Then:
Executing:
⚡ node importer.js
Fails:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'importer\node_modules\exporter\' imported from importer\src\index.js
But Why?
When resolving a node_modules package, Node.js first checks if there's a "exports" field in the package.json, if there's none it looks for a "main" field, and if it's. also not there, it checks if there's a file named index.js – although this behavior is deprecated and may be removed in a later version of Node.js, I would advise against relying on it and always specify "exports" and/or "main" fields. You can check out Package entry points section of Node.js docs to get more info on that.
"module" is simply not a field Node.js uses, it's used by some other tools so it's certainly okay to have it defined in your package.json, but you should also have "main" and/or "exports" fields. Node.js will use the file extension to determine if the file is an ES module (dist/bundle.js is using .js as extension and you have "type": "module" in your package.json, so you're all set).

Configure a js library to tell webpack to use the ES6 module dist file

I maintain a library that has has a /dist folder with versions for CommonJS, ES6 modules, and direct <script> loading.
The package.json file is configured to identify each version:
"type": "module",
"main": "dist/pretty-print-json.umd.cjs",
"module": "dist/pretty-print-json.esm.js",
"browser": "dist/pretty-print-json.min.js",
When the library is installed:
$ npm install pretty-print-json
and imported:
import { prettyPrintJson } from 'pretty-print-json';
into a project with webpack, IDEs correctly interpret the ES6 module version plus the TypeScript declaration file (dist/pretty-print-json.d.ts).
However, the build process fails:
./src/app/app.component.ts:74:13-35 - Error: export 'prettyPrintJson' (imported
as 'prettyPrintJson') was not found in 'pretty-print-json' (module has no
exports)
The ES6 module version has an export statement:
export { prettyPrintJson };
After a bunch of experiments with a simple Angular project, I figured out that webpack is using the "browser" version instead of the "module" version.
How do you configure a library so that webpack correctly picks up the "module" version without breaking support for the other versions?
The browser field takes priority over module and main if your target is the browser.
I found this solution for your problem:
{
"name": "main-module-browser",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"browser": {
"./dist/index.js": "./dist/index.browser.js",
"./dist/index.esm.js": "./dist/index.browser.esm.js"
}
}

Rollup bundle node_modules into app when using Typescript

I have a Typescript application. I want to bundle the dependencies inside node_modules into the resulting bundle. Here's what I've tried so far based on these posts (Does rollup bundle node_modules into bundle.js?, Does rollup bundle node_modules into bundle.js?).
Here's my rollup config and the simple file I've tried bundling:
import typescript from "#rollup/plugin-typescript";
import resolve from "#rollup/plugin-node-resolve";
import commonjs from "#rollup/plugin-commonjs";
import globals from "rollup-plugin-node-globals";
import builtins from "rollup-plugin-node-builtins";
export default {
input: "src/test.ts",
output: {
dir: "dist/",
format: "iife",
},
plugins: [
resolve({jsnext: true}),
commonjs({ include: ["src/test.ts", "node_modules/**"] }),
// I need to polyfill some node stuff
globals(),
builtins(),
typescript({ noEmitOnError: false, outDir: "dist/" }),
],
};
The file:
import * as obj from 'some-module'
console.log(obj)
How can I get dependencies in node_modules to be bundled into the final output when using rollup + typescript? I suspect the issue is that the Typescript plugin is outputting files which use require and rollup cannot recognize require.
In package.json, make sure the dependencies you want to include is registered in dependencies property not peerDependencies. Rollup will bundled them in the output then.
You need to configure TypeScript to output ES modules for it to work. Set module to "ESNext" in your tsconfig.json:
{
"compilerOptions": {
"module": "ESNext"
}
}

How to deal with external modules when compiling TypeScript?

I am very new to TypeScript and I am trying out many things and got stuck with external modules after compilation.
I started with simple TypeScript project in Visual Studio Code set to target ES2015 and module to es2015 (because I want to use native stuff as much as possible) and I wanted to try Strongly Type Events (STE) which I installed using npm.
By changing module resolution to node and setting baseUrl in tsconfig.json, TypeScript has no problem in using STE with non-relative import:
import { SimpleEventDispatcher } from "ste-simple-events";
However, when I compile TypeScript, resulting JavaScript file has exact same import statement and when loading html which is including this module, I get an error that module cannot be found.
I can not figure out how to solve this.
Should TypeScript somehow change import statement to exact location of STE?
Perhaps, but TypeScript team says TypeScript compilation will never change code in import statements.
Or should I somehow compile external modules as well, so that they get included in output?
Or should default module resolution in ES2015 standard implemented in browsers do the job - for which I have no idea how it works and how should external ES2015 modules be imported in JavaScript?
Any help or a nudge in the right direction would be greatly appreciated.
Thanks,
Mario
For any TypeScript beginner scratching their head over this, the answer is called JavaScript bundlers.
Since I am using ES6, I opted for RollupJs bundler combined with following plugins (use them in this order):
rollup-plugin-resolve - required to resolve node_modules
rollup-plugin-commonjs - required to transpile CommonJS modules in node_modules to ES6
rollup-plugin-typescript2 - optional, you can have it in the process or you can use tsc manually before you run rollup - just make sure you use version 2 (first version is not maintained any more)
rollup-plugin-terser - minifier and obfuscator
You can install all of those with npm:
npm install rollup rollup-plugin-resolve rollup-plugin-commonjs rollup-plugin-typescript2 rollup-plugin-terser
Add rollup.config.js to the root of your project, mine looked like this:
import typescript from "rollup-plugin-typescript2"
import commonjs from "rollup-plugin-commonjs";
import resolve from "rollup-plugin-node-resolve";
import { terser } from "rollup-plugin-terser";
import pkg from "./package.json"
export default {
input: "./wwwroot/js/svgts.js",
output: [
{
file: pkg.module,
format: "esm",
},
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
], plugins: [
resolve({
mainFields: ["module"], // Default: ["module", "main"]
}),
commonjs({
include: "node_modules/**"
}),
typescript({
typescript: require("typescript"),
tsconfig: "./tsconfig.json"
}),
(process.env.NODE_ENV === "production" && terser({
mangle: { reserved: ['svg'] }
}))
],
}
Rollup supports environment variables which I use here with line:
process.env.NODE_ENV === "production"
This allows you to create npm scripts in package.json to easily include minification or not, for example:
"scripts": {
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"rollup": "rollup -c",
"rollupw": "rollup -cw",
"start": "concurrently \"npm run rollupw\" \"npm run lite\"",
"startprod": "NODE_ENV=production concurrently \"npm run rollupw\" \"npm run lite\"",
"production": "NODE_ENV=production npm run rollup"
},
Then you can run in terminal npm run production for instance to build minified bundle.
You can find more details on GitHub of each project.

How can I export a React Component as an NPM package to use in separate projects?

I have created a React component inside a project that I'd like to use in multiple projects. At the moment, I only care about doing this locally and for development. The React Component is rendered into the root div, the project uses webpack and babel to transpile JSX, ES6 and some ES7 features into a bundle.
I thought it would be simple to export this component such that I can simply run npm install MyComponent and begin using it in a fresh project. However, I find it isn't so straight forward. In particular, I've been reading for hours and hours and only seem to be getting more confused.
If my end goal is to keep developing 'MyComponent' in its containing project, while using 'MyComponent' in any number of other local projects, what are my options? The first thing I did was change the main key of my package.json to /src/components/MyComponent and run npm pack. This produces a tgz file I can install via its absolute filepath in other projects. However, I found that the es6 and jsx was not being transpiled and so my client projects would be unable to parse MyComponent. I then used webpack to transpile into lib/MyComponent, but when I have import MyComponent from './path/to/MyComponent-1.0.0.tgz I'd only see {} (an empty object) in the console.
Searching for solutions to my problem turn up many different approaches pulling together NPM, Grunt, Gulp, Babel, Webpack, etc.. And I am worried it will be many many more hours (days?) before I can grind that down to something understandable.
Given my requirements, what is the simplest solution I can implement to 1) compile down my React Component to the simplest to import module 2) import it into any local projects 3) continue to develop the package in the original host project and have changes easily propagate to client projects.
In general, if you're going to begin creating React components as separated packages (which is a great pattern, for all the reasons you've already mentioned) - you're going to need to get at least a bit familiar with webpack and babel. There's a ton to learn here, but let me try to point you in the right direction:
// webpack.config.js
/* eslint-disable */
const path = require('path')
const webpack = require('webpack')
const ENVIRONMENT = process.env.NODE_ENV
const PRODUCTION = ENVIRONMENT === 'production'
const SOURCEMAP = !PRODUCTION || process.env.SOURCEMAP
const library = 'your-lib-name' // << RENAME THIS <<
const filename = PRODUCTION ? `${library}.min.js` : `${library}.js`
const plugins = []
if (PRODUCTION) {
plugins.push(
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(ENVIRONMENT),
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.optimize.UglifyJsPlugin({
minimize: true,
output: { comments: false, semicolons: false },
sourceMap: SOURCEMAP,
})
)
}
module.exports = {
devtool: SOURCEMAP ? 'source-map' : 'none',
entry: `${__dirname}/path/to/your/component.js`, // << RENAME THIS <<
externals: {
'react': 'react',
'react-dom': 'react-dom',
},
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
}],
},
output: {
filename,
library,
path: `${__dirname}/lib`,
libraryTarget: 'umd',
umdNamedDefine: true,
},
plugins,
}
I know that looks like a bunch - but it handles the majority of what you're going to want. In specific:
If you specify NODE_ENV=production when building, this will uglify/minify your package, and do some other trimming which you may want later.
Building with this script will output a sourcemap, which you can use with dev tools to inspect your minified code in the debugger window, among other things.
This marks react and react-dom as externals - which means they won't get bundled up and packaged inside your bundle. This is great - because it means you won't get 2+ copies of react's filesize just because you've imported your own component!
To use it, though, you now need some package.json love.
package.json
{
"name": "Your Name",
"version": "0.0.1",
"description": "This is my awesome react package!",
"main": "path/to/your/component.js",
"author": "Your Name",
"license": "MIT",
"repository": { /* Your Repo Info Here */ },
"dependencies": {
"any-packages-you-need-included-in-builds": "^1.0.0"
},
"devDependencies": {
"babel-cli": "^6.22.2",
"babel-loader": "^7.1.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.22.0",
"prop-types": "^15.5.10",
"react-dom": "^15.6.1",
"webpack": "^3.0.0"
},
"scripts": {
"build": "yarn prebuild && NODE_ENV=production webpack",
"prebuild": "mkdir -p ./lib && rm -rf ./lib/*"
}
}
Obviously, you can have a lot more here if you need it - such as other babel-plugin-* plugins that you use in your transpilation, other packages, etc.But this set will let your webpack build run. Note that the scripts here assume you're using yarn - so that you can run yarn build, and that you're on a posix system, for mkdir to work. If you're on windows or not using yarn, just update the scripts accordingly.
The rest is just learning to publish your package to npm or another package repository. Primarily, that's just setting the version number in package.json to something new (npm version) and then publishing (npm publish). You will have to have an npm account for this, of course - and be logged in (npm login).
Once you've published to npm you can just yarn add your-package-name.
Remember, though - we marked react and react-dom as external - so in the consuming package, you'll need to make sure they're available as window.React and window.ReactDOM - or you'll need to include the component directly from node_modules/your-package-name/path/to/your/component.js
You don't need to npm pack a package to use it. If you make your component into a git repo and put it on Github, you can use NPM to install it directly from Github by using npm install alex/mycomponent where alex is your github username and mycomponent is the repo name. Re-running that command will re-install from Github, in case you make changes to the repo.
Once you're happy with the component, you can upload it to the NPM registry to install like any other package (npm install name). Using Github at first makes it a bit easier to develop.
Webpack might not compile things from node_modules by default. Usually, packages are pre-compiled before being published anyway, but you should be able to configure webpack to build your 'packaged' component, along with the rest of your app. Maybe this will help: https://stackoverflow.com/a/38008149/7486612
In order to push react libraries into NPM, you may need some boilerplate which will install and convert many things for you (and you can still use your current react module as the main source, just follow the guides at the end of my answer, then you will surely get all the ideas)
Or you can also refer to my previous answer to a similar question:
Issue with publishing to npm
=====
I've also pushed several react libraries successfully into NPM:
https://www.npmjs.com/~thinhvo0108
=====
Your github repositories' folder structure should also look like mine:
https://github.com/thinhvo0108/react-paypal-express-checkout
=====
Useful tutorial below here:
(boilerplate source) https://github.com/juliancwirko/react-npm-boilerplate
(author's article) http://julian.io/creating-react-npm-packages-with-es2015/
Start by looking at existing component library, eg Material UI.
Specifically check out npm scripts they have (see package.json):
"build:es2015": "cross-env NODE_ENV=production babel ./src --ignore *.spec.js --out-dir ./build",
"build:es2015modules": "cross-env NODE_ENV=production BABEL_ENV=modules babel ./src/index.js --out-file ./build/index.es.js",
"build:copy-files": "babel-node ./scripts/copy-files.js",
"build:umd:dev": "webpack --config scripts/umd.webpack.config.js",
"build:umd:prod": "cross-env NODE_ENV=production webpack --config scripts/umd.webpack.config.js",
"build": "npm run build:es2015 && npm run build:es2015modules && npm run build:copy-files && npm run build:umd:dev && npm run build:umd:prod",
That's example of very involved and high quality component library, that makes sure that regardless of your build pipeline you'll be able to use it.
Setting up build process with webpack might be cumbersome, but don't concentrate on that too much from the begining, and cover cases that are most straight forward to you.
Also check out https://storybook.js.org/ while working on your components.

Categories