From 7ed8fe4ef34cd19d057dae31901fa96cdd8eaeb7 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:34:23 +0100 Subject: [PATCH 01/41] feat(learn): add Package Configuration article --- apps/site/navigation.json | 4 + .../en/learn/modules/package-configuration.md | 405 ++++++++++++++++++ packages/i18n/locales/en.json | 1 + 3 files changed, 410 insertions(+) create mode 100644 apps/site/pages/en/learn/modules/package-configuration.md diff --git a/apps/site/navigation.json b/apps/site/navigation.json index 6168c078438d5..bde3ceac1c91b 100644 --- a/apps/site/navigation.json +++ b/apps/site/navigation.json @@ -308,6 +308,10 @@ "modules": { "label": "components.navigation.learn.modules.links.modules", "items": { + "packageConfiguration": { + "link": "/learn/modules/package-configuration", + "label": "components.navigation.learn.modules.links.packageConfiguration" + }, "publishingNodeApiModules": { "link": "/learn/modules/publishing-node-api-modules", "label": "components.navigation.learn.modules.links.publishingNodeApiModules" diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md new file mode 100644 index 0000000000000..1e6e61ee3cdb9 --- /dev/null +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -0,0 +1,405 @@ +--- +title: Package Configuration +layout: learn +--- + +# Package configuration + +Configuration is always a chore, but an unfortunately necessary evil. And configuring a package for CommonJS (CJS) and ES Modules (ESM) can be a waking nightmare—not least because it has changed a dozen times in half as many years. + +A frequent question is “how do I make this work!?” (often with angry tears); but yet more frequently we come across packages that are just misconfigured. + +All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[1](#footnotes), and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). + +For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your poison](#pick-your-poison) for the TLDR. + +## Preamble: How did we get here + +CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official (TC39) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. + +An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. + +## Pick your poison + +This article covers configuration of all possible combinations in modern Node.js (v12+). If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: + +- ESM source and distribution +- CJS source and distribution with good/specific `module.exports` + +| You as a package author write | Consumers of your package write their code in | Your options | +| :-------------------------------- | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | +| CJS source code using `require()` | CJS: consumers `require()` your package | [CJS source and distribution](#cjs-source-and-distribution) | +| CJS source code using `require()` | ESM: consumers `import` your package | [CJS source and only ESM distribution](#cjs-source-and-only-esm-distribution) | +| CJS source code using `require()` | CJS & ESM: consumers either `require()` or `import` your package | [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution) | +| ESM source code using `import` | CJS: consumers `require()` your package | [ESM source with only CJS distribution](#esm-source-with-only-cjs-distribution) | +| ESM source code using `import` | ESM: consumers `import` your package | [ESM source and distribution](#esm-source-and-distribution) | +| ESM: source code uses `import` | CJS & ESM: consumers either `require()` or `import` your package | [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution) | + +### CJS source and distribution + +This the "Rum & Coke" of packages: pretty difficult to mess up. Essentially just declare the package’s exports via the [`"exports"`](https://nodejs.org/api/packages.html#conditional-exports) field/field-set. + +**Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) + +```json +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": "PATH/TO/DIST/CODE/ENTRYPOINT.js", // ex "./dist/index.js" + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +Note that `packageJson.exports["."] = filepath` is shorthand for `packageJson.exports["."].default = filepath` + +### CJS source and only ESM distribution + +The "Gin & Tonic" of packages: This takes a small bit of finesse but is also pretty straight-forward. + +**Working example**: [cjs-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/esm-distro) + +```json +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": "PATH/TO/DIST/CODE/ENTRYPOINT.mjs", // ex "./dist/index.mjs" + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +The [`.mjs`](https://nodejs.org/api/esm.html#enabling) file extension is a trump-card: it will override **any** other configuration and the file will be treated as ESM. Using this file extension is necessary because `packageJson.exports.import` does **NOT** signify that the file is ESM (contrary to common, if not universal, misperception), only that it is the file to be used when the package is imported (ESM _can_ import CJS. See [Gotchas](#gotchas) below). + +The [`"engines"`](https://nodejs.dev/learn/the-package-json-guide#engines) field provides both a human-friendly and a machine-friendly indication of with which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field here will save a lot of headache for consumers with an older version of Node.js who cannot use the package. + +### CJS source and both CJS & ESM distribution + +You have a few options: + +#### Attach named exports directly onto `exports` + +The "French 75" of packages: Classic but takes some sophistication and finesse. + +Pros: + +- Smaller package weight +- Easy and simple (probably least effort if you don't mind keeping to a minor syntax stipulation) +- Precludes the Dual-Package Hazard + +Cons: + +- Hacky-ish: Leverages non-explicitly documented behaviour in Node.js's algorithm (it _can_ but is very unlikely to change). +- Requires very specific syntax (either in source code and/or bundler gymnastics). + +**Working example**: [cjs-with-dual-distro (properties)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/property-distro) + +```json +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +Typically, you would see `module.exports` assigned to something (be it an object or a function) like this: + +```js +const someObject = { + foo() {}, + bar() {}, + qux() {}, +}; + +module.exports = someObject; +``` + +Instead, do this: + +```js +module.exports.foo = function foo() {}; +module.exports.foo = function bar() {}; +module.exports.foo = function qux() {}; +``` + +#### Use a simple ESM wrapper + +The "Piña Colada" of packages: Complicated setup and difficult to get the balance right. + +Pros: + +- Smaller package weight + +Cons: + +- Likely requires complicated bundler gymnastics (we could not find any existing option to automate this in Webpack). + +**Working example**: [cjs-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/wrapper-distro) + +```json +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.mjs", // ex "./dist/es/wrapper.mjs" + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" + "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" // ex "./dist/cjs/index.js" + }, + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +In order to support named exports from the CJS bundle for an ESM consumer, this will need a bit of gymnastics from a bundler but is conceptually very simple. + +In certain conditions, CJS exports an object (which gets aliased to ESM's `default`); that object, like any object, is destructure-able. You can leverage that to pluck all the members of the object out, and then re-export them so the ESM consumer is none the wiser. + +```js +// ./dist/es/wrapper.mjs + +import cjs from '../cjs/index.js'; + +const { a, b, c /* … */ } = cjs; + +export { a, b, c /* … */ }; +``` + +#### Two full distributions + +The "Long Island Ice Tea" of packages: Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. + +Pros: + +- Simple bundler configuration + +Cons: + +- Larger package weight (basically double) + +**Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) + +```json +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.mjs", // ex "./dist/es/index.mjs" + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" + "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" // ex "./dist/cjs/index.js" + }, + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +### ESM source and distribution + +The wine of packages: Simple, tried, and true. + +This is almost exactly the same as the CJS-CJS configuration above with 1 small difference: the [`"type"`](https://nodejs.org/api/packages.html#type) field. + +**Working example**: [esm-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/esm-distro) + +```json +{ + "type": "module", + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": "PATH/TO/DIST/CODE/ENTRYPOINT.js", // ex "./dist/index.js" + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +Note that ESM is not “backwards” compatible with CJS: a CJS module cannot `require()` an ES Module; it is possible to use a dynamic import (`await import()`), but this is likely not what consumers expect (and, unlike ESM, CJS does not support [Top-Level Await](https://github.com/tc39/proposal-top-level-await/)). + +### ESM source with only CJS distribution + +We're not in Kansas anymore, Toto. + +The configurations (there are 2 options) are nearly the same as [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution), just exclude `packageJson.exports.import`. + +💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. + +**Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/cjs-distro) + +### ESM source and both CJS & ESM distribution + +These are "mixologist" territory. + +When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there is often no `.mjs` equivalent[3](#footnotes). + +Similar to [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution), you have the same options. + +There is also a 4th option of publishing only an ESM distribution and forcing consumers to use a dynamic import (`await import()`), but that is not quite the same and will likely lead to angry consumers, so it is not covered here. + +#### Publish only a CJS distribution with property exports + +The "Mojito" of packages: Tricky to make and needs good ingredients. + +This option is almost identical to the [CJS source with CJS & ESM distribution's property exports](#attach-named-exports-directly-onto-raw-exports-endraw-) above. The only difference is in package.json: `"type": "module"`. + +Only some build tools support generating this output. [Rollup](https://www.rollupjs.org/) produces compatible output out of the box when targetting commonjs. Webpack as of [v5.66.0+](https://github.com/webpack/webpack/releases/tag/v5.66.0) does with the new [`commonjs-static`](https://webpack.js.org/configuration/output/#type-commonjs-static) output type, (prior to this no commonjs options produces compatible output). It is not currently possible with [esbuild](https://esbuild.github.io/) (which produces a non-static `exports`). + +The working example below was created prior to Webpack's recent release, so it uses Rollup (I'll get around to adding a Webpack option too). + +**Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/property-distro) + +```json +{ + "type": "module", + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs", // ex "./dist/index.cjs" + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. + +#### Publish a CJS distribution with an ESM wrapper + +The "Pornstar Martini" of packages: There's a lot going on here. + +This is also almost identical to the [CJS source and dual distribution using an ESM wrapper](#use-a-simple-esm-wrapper), but with subtle differences `"type": "module"` and some `.cjs` file extenions in package.json. + +**Working example**: [esm-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/wrapper-distro) + +```json +{ + "type": "module", + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", // ex "./dist/es/wrapper.js" + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs", // ex "./dist/cjs/index.cjs" + "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs" // ex "./dist/cjs/index.cjs" + }, + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. + +#### Publish both full CJS & ESM distributions + +The "Tokyo Tea" of packages: Chuck in a bunch of stuff (with a surprise) and hope for the best. This is probably the most common and easiest of the ESM to CJS & ESM options, but you pay for it. + +In terms of package configuration, there are a few options that differ mostly in personal preference. + +##### Mark the whole package as ESM and specifically mark the CJS exports as CJS via the `.cjs` file extension + +This option has the least burden on development/developer experience. + +This also means that whatever build tooling must produce the distribution file with a `.cjs` file extension. This might necessitate chaining multiple build tools or adding a subsequent step to move/rename the file to have the `.cjs` file extension (ex `mv ./dist/index.js ./dist/index.cjs`)[3](#footnotes). This can be worked around by adding a subsequent step to move/rename those outputted files (ex [Rollup](https://rollupjs.org/) or [a simple shell script](https://stackoverflow.com/q/21985492)). + +Support for the `.cjs` file extension was added in 12.0.0, and using it will cause ESM to properly recognised a file as commonjs (`import { foo } from './foo.cjs` works). However, `require()` does not auto-resolve `.cjs` like it does for `.js`, so file extension cannot be omitted as is commonplace in commonjs: `require('./foo')` will fail, but `require('./foo.cjs')` works. Using it in your package's exports has no drawbacks: `packageJson.exports` (and `packageJson.main`) requires a file extension regardless, and consumers reference your package by the `"name"` field of your package.json (so they're blissfully unaware). + +**Working example**: [esm-with-dual-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/double-distro) + +```json +{ + "type": "module", + "engines": { "node": ">=12.22.7" }, // optional, but kind + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", // ex "./dist/es/index.js" + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs" // ex "./dist/index.cjs" + }, + "./package.json": "./package.json" // ensure this file is importable + } +} +``` + +💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. + +##### Use the `.mjs` (or equivalent) file extension for all source code files + +The configuration for this is the same as [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution). + +**Non-JavaScript source code**: The non-JavaScript language’s own configuration needs to recognise/specify that the input files are ESM. + +#### Node.js before 12.22.x + +🛑 You should not do this: Versions of Node.js prior to 12.x are End of Life and are now vulnerable to serious security exploits. + +If you're a security researcher needing to investigate Node.js prior to v12.22.x, feel free to contact me for help configuring. + +## Down the rabbit-hole + +Specifically in relation to Node.js, there are 4 problems to solve: + +- Determining format of source code files (author running her/his own code) +- Determining format of distribution files (code consumers will receive) + +- Publicising distribution code for when it is `require()`’d (consumer expects CJS) +- Publicising distribution code for when it is `import`’d (consumer probably wants ESM) + +⚠️ The first 2 are **independent** of the last 2. + +The method of loading does NOT determine the format the file is interpreted as: + +- **package.json’s** **`exports.require`** **≠** **`CJS`**. `require()` does NOT and cannot blindly interpret the file as CJS; for instance, `require('foo.json')` correctly interprets the file as JSON, not CJS. The module containing the `require()` call of course must be CJS, but what it is loading is not necessarily also CJS. +- **package.json’s** **`exports.import`** **≠** **`ESM`**. `import` similarly does NOT and cannot blindly interpret the file as ESM; `import` can load CJS, JSON, and WASM, as well as ESM. The module containing the `import` statement of course must be ESM, but what it is loading is not necessarily also ESM. + +So when you see configuration options citing or named with `require` or `import`, resist the urge to assume they are for _determining_ CJS vs ES Modules. + +⚠️ Adding an `"exports"` field/field-set to a package’s configuration effectively [blocks deep pathing into the package](https://nodejs.org/api/packages.html#package-entry-points) for anything not explicitly listed in the exports’ subpathing. This means it can be a breaking change. + +⚠️ Consider carefully whether to distribute both CJS and ESM: It creates the potential for the [Dual Package Hazard](https://nodejs.org/api/packages.html#dual-package-hazard) (especially if misconfigured and the consumer tries to get clever). This can lead to an extremely confusing bug in consuming projects, especially when your package is not perfectly configured. Consumers can even be blind-sided by an intermediary package that uses the "other" format of your package (eg consumer uses the ESM distribution, and some other package the consumer is also using itself uses the CJS distribution). If your package is in any way stateful, consuming both the CJS and ESM distributions will result in parallel states (which is almost surely unintentional). + +## Gotchas + +The `package.json`'s `"type"` field changes the `.js` file extension to mean either `commonjs` or ES `module` respectively. It is very common in dual/mixed packages (that contain both CJS and ESM) to use this field incorrectly. + +```json +// ⚠️ THIS DOES NOT WORK +{ + "type": "module", + "main": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" + }, + "./package.json": "./package.json" + } +} +``` + +This does not work because `"type": "module"` causes `packageJson.main`, `packageJson.exports["."].require`, and `packageJson.exports["."].default` to get interpreted as ESM (but they’re actually CJS). + +Excluding `"type": "module"` produces the opposite problem: + +```json +// ⚠️ THIS DOES NOT WORK +{ + "main": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "exports": { + ".": { + "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", + "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" + }, + "./package.json": "./package.json" + } +} +``` + +This does not work because `packageJson.exports["."].import` will get interpreted as CJS (but it’s actually ESM). + +## Footnotes + +1. There was a bug in Node.js v13.0–13.6 where `packageJson.exports["."]` had to be an array with verbose config options as the first item (as an object) and the “default” as the second item (as a string). See [nodejs/modules#446](https://github.com/nodejs/modules/issues/446). +2. The `"type"` field in package.json changes what the `.js` file extension means, similar to to an [HTML script element’s type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type). +3. TypeScript has experimental support for the [package.json `"type"` field](https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions) and [`.cts` and `.mts` file extensions](https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions). diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index 025560058e919..71e466c094a61 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -90,6 +90,7 @@ "modules": { "links": { "modules": "Modules", + "packageConfiguration": "Package configuration", "publishingNodeApiModules": "How to publish a Node-API package", "anatomyOfAnHttpTransaction": "Anatomy of an HTTP Transaction", "abiStability": "ABI Stability", From 5a607be9d354cf4a59d658c471e9d892fe70c1a1 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Thu, 14 Nov 2024 22:43:15 +0100 Subject: [PATCH 02/41] fixup!: add authorship --- apps/site/pages/en/learn/modules/package-configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 1e6e61ee3cdb9..6b64adb8b654b 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -1,6 +1,7 @@ --- title: Package Configuration layout: learn +authors: JakobJingleheimer --- # Package configuration From b5f72d0f7edb480790fcbd8bcbc4b3526e5b421f Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Fri, 15 Nov 2024 23:09:04 +0100 Subject: [PATCH 03/41] fixup!: makeshift footnotes to md footnotes --- .../en/learn/modules/package-configuration.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 6b64adb8b654b..fa2b3c09efe6e 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -10,7 +10,7 @@ Configuration is always a chore, but an unfortunately necessary evil. And config A frequent question is “how do I make this work!?” (often with angry tears); but yet more frequently we come across packages that are just misconfigured. -All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[1](#footnotes), and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). +All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your poison](#pick-your-poison) for the TLDR. @@ -227,7 +227,7 @@ We're not in Kansas anymore, Toto. The configurations (there are 2 options) are nearly the same as [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution), just exclude `packageJson.exports.import`. -💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. **Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/cjs-distro) @@ -235,7 +235,7 @@ The configurations (there are 2 options) are nearly the same as [ESM source and These are "mixologist" territory. -When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there is often no `.mjs` equivalent[3](#footnotes). +When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there is often no `.mjs` equivalent[^3]. Similar to [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution), you have the same options. @@ -264,7 +264,7 @@ The working example below was created prior to Webpack's recent release, so it u } ``` -💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. #### Publish a CJS distribution with an ESM wrapper @@ -401,6 +401,8 @@ This does not work because `packageJson.exports["."].import` will get interprete ## Footnotes -1. There was a bug in Node.js v13.0–13.6 where `packageJson.exports["."]` had to be an array with verbose config options as the first item (as an object) and the “default” as the second item (as a string). See [nodejs/modules#446](https://github.com/nodejs/modules/issues/446). -2. The `"type"` field in package.json changes what the `.js` file extension means, similar to to an [HTML script element’s type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type). -3. TypeScript has experimental support for the [package.json `"type"` field](https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions) and [`.cts` and `.mts` file extensions](https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions). +[^1]: There was a bug in Node.js v13.0–13.6 where `packageJson.exports["."]` had to be an array with verbose config options as the first item (as an object) and the “default” as the second item (as a string). See [nodejs/modules#446](https://github.com/nodejs/modules/issues/446). + +[^2]: The `"type"` field in package.json changes what the `.js` file extension means, similar to to an [HTML script element’s type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type). + +[^3]: TypeScript has experimental support for the [package.json `"type"` field](https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions) and [`.cts` and `.mts` file extensions](https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions). From 7e233cebc91365a01aaa08f1f64bf1a077b3dff4 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:28:04 +0100 Subject: [PATCH 04/41] fixup!: add a "general notes" section --- .../pages/en/learn/modules/package-configuration.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index fa2b3c09efe6e..3207c83748e10 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -14,6 +14,16 @@ All the provided `package.json` configurations (not specifically marked “does For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your poison](#pick-your-poison) for the TLDR. +## General notes + +[Syntax detection](https://nodejs.org/api/packages.html#syntax-detection) is _**not**_ a replacement for proper package configuration; syntax detection is not fool-proof and it has [significant performance cost](https://github.com/nodejs/node/pull/55238). + +When using [`"exports"`](https://nodejs.org/api/packages.html#conditional-exports) in package.json, it is generally a good idea to include `"./package.json": "./package.json"` so that it can be imported ([`module.findPackageJSON`](https://nodejs.org/api/module.html#modulefindpackagejsonspecifier-base) is not affected by this limitation, but `import` may be more convenient). + +`"exports"` can be advisable over [`"main"`](https://nodejs.org/api/packages.html#main) because it prevents external access to internal code (so you can be relatively sure users are not depending on things they shouldn't). If you don't need that, `"main"` is simpler and may be a better option for you. + +The `"engines"` field provides both a human-friendly and a machine-friendly indication of with which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. + ## Preamble: How did we get here CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official (TC39) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. @@ -74,8 +84,6 @@ The "Gin & Tonic" of packages: This takes a small bit of finesse but is also pre The [`.mjs`](https://nodejs.org/api/esm.html#enabling) file extension is a trump-card: it will override **any** other configuration and the file will be treated as ESM. Using this file extension is necessary because `packageJson.exports.import` does **NOT** signify that the file is ESM (contrary to common, if not universal, misperception), only that it is the file to be used when the package is imported (ESM _can_ import CJS. See [Gotchas](#gotchas) below). -The [`"engines"`](https://nodejs.dev/learn/the-package-json-guide#engines) field provides both a human-friendly and a machine-friendly indication of with which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field here will save a lot of headache for consumers with an older version of Node.js who cannot use the package. - ### CJS source and both CJS & ESM distribution You have a few options: From 4b957824a0dc459427b1044953c33fe47df2f473 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:29:14 +0100 Subject: [PATCH 05/41] fixup!: remove/update outdated info --- apps/site/pages/en/learn/modules/package-configuration.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 3207c83748e10..ca26ceaa71568 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -227,7 +227,7 @@ This is almost exactly the same as the CJS-CJS configuration above with 1 small } ``` -Note that ESM is not “backwards” compatible with CJS: a CJS module cannot `require()` an ES Module; it is possible to use a dynamic import (`await import()`), but this is likely not what consumers expect (and, unlike ESM, CJS does not support [Top-Level Await](https://github.com/tc39/proposal-top-level-await/)). +Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _can_ [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) without a flag as of 23.0.0. ### ESM source with only CJS distribution @@ -243,12 +243,10 @@ The configurations (there are 2 options) are nearly the same as [ESM source and These are "mixologist" territory. -When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there is often no `.mjs` equivalent[^3]. +When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there may be no `.mjs` equivalent. Similar to [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution), you have the same options. -There is also a 4th option of publishing only an ESM distribution and forcing consumers to use a dynamic import (`await import()`), but that is not quite the same and will likely lead to angry consumers, so it is not covered here. - #### Publish only a CJS distribution with property exports The "Mojito" of packages: Tricky to make and needs good ingredients. @@ -412,5 +410,3 @@ This does not work because `packageJson.exports["."].import` will get interprete [^1]: There was a bug in Node.js v13.0–13.6 where `packageJson.exports["."]` had to be an array with verbose config options as the first item (as an object) and the “default” as the second item (as a string). See [nodejs/modules#446](https://github.com/nodejs/modules/issues/446). [^2]: The `"type"` field in package.json changes what the `.js` file extension means, similar to to an [HTML script element’s type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type). - -[^3]: TypeScript has experimental support for the [package.json `"type"` field](https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions) and [`.cts` and `.mts` file extensions](https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions). From 506dcfc06f2f7e9fd2f22082fdeef99f6dfec9d0 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:30:24 +0100 Subject: [PATCH 06/41] fixup!: tidy & simplify examples --- .../en/learn/modules/package-configuration.md | 125 +++++++++--------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index ca26ceaa71568..2ed0430b6a069 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -52,14 +52,14 @@ This the "Rum & Coke" of packages: pretty difficult to mess up. Essentially just **Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) -```json +```jsonc { "type": "commonjs", // current default, but may change - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { - ".": "PATH/TO/DIST/CODE/ENTRYPOINT.js", // ex "./dist/index.js" - "./package.json": "./package.json" // ensure this file is importable - } + ".": "./dist/index.js", + "./package.json": "./package.json", + }, } ``` @@ -71,14 +71,14 @@ The "Gin & Tonic" of packages: This takes a small bit of finesse but is also pre **Working example**: [cjs-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/esm-distro) -```json +```jsonc { "type": "commonjs", // current default, but may change - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { - ".": "PATH/TO/DIST/CODE/ENTRYPOINT.mjs", // ex "./dist/index.mjs" - "./package.json": "./package.json" // ensure this file is importable - } + ".": "./dist/index.mjs", + "./package.json": "./package.json", + }, } ``` @@ -100,25 +100,24 @@ Pros: Cons: -- Hacky-ish: Leverages non-explicitly documented behaviour in Node.js's algorithm (it _can_ but is very unlikely to change). - Requires very specific syntax (either in source code and/or bundler gymnastics). **Working example**: [cjs-with-dual-distro (properties)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/property-distro) -```json +```jsonc { "type": "commonjs", // current default, but may change - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { - ".": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" - "./package.json": "./package.json" // ensure this file is importable - } + ".": "./dist/cjs/index.js", + "./package.json": "./package.json", + }, } ``` Typically, you would see `module.exports` assigned to something (be it an object or a function) like this: -```js +```cjs const someObject = { foo() {}, bar() {}, @@ -130,7 +129,7 @@ module.exports = someObject; Instead, do this: -```js +```cjs module.exports.foo = function foo() {}; module.exports.foo = function bar() {}; module.exports.foo = function qux() {}; @@ -150,18 +149,18 @@ Cons: **Working example**: [cjs-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/wrapper-distro) -```json +```jsonc { "type": "commonjs", // current default, but may change - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.mjs", // ex "./dist/es/wrapper.mjs" - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" - "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" // ex "./dist/cjs/index.js" + "import": "./dist/esm/wrapper.mjs", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js", }, - "./package.json": "./package.json" // ensure this file is importable - } + "./package.json": "./package.json", + }, } ``` @@ -169,9 +168,7 @@ In order to support named exports from the CJS bundle for an ESM consumer, this In certain conditions, CJS exports an object (which gets aliased to ESM's `default`); that object, like any object, is destructure-able. You can leverage that to pluck all the members of the object out, and then re-export them so the ESM consumer is none the wiser. -```js -// ./dist/es/wrapper.mjs - +```js displayName="./dist/esm/wrapper.mjs" import cjs from '../cjs/index.js'; const { a, b, c /* … */ } = cjs; @@ -193,18 +190,18 @@ Cons: **Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) -```json +```jsonc { "type": "commonjs", // current default, but may change - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.mjs", // ex "./dist/es/index.mjs" - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", // ex "./dist/cjs/index.js" - "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" // ex "./dist/cjs/index.js" + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js", }, - "./package.json": "./package.json" // ensure this file is importable - } + "./package.json": "./package.json", + }, } ``` @@ -219,10 +216,10 @@ This is almost exactly the same as the CJS-CJS configuration above with 1 small ```json { "type": "module", - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { - ".": "PATH/TO/DIST/CODE/ENTRYPOINT.js", // ex "./dist/index.js" - "./package.json": "./package.json" // ensure this file is importable + ".": "./dist/index.js", + "./package.json": "./package.json" } } ``` @@ -262,10 +259,10 @@ The working example below was created prior to Webpack's recent release, so it u ```json { "type": "module", - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { - ".": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs", // ex "./dist/index.cjs" - "./package.json": "./package.json" // ensure this file is importable + ".": "./dist/index.cjs", + "./package.json": "./package.json" } } ``` @@ -280,18 +277,18 @@ This is also almost identical to the [CJS source and dual distribution using an **Working example**: [esm-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/wrapper-distro) -```json +```jsonc { "type": "module", - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", // ex "./dist/es/wrapper.js" - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs", // ex "./dist/cjs/index.cjs" - "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs" // ex "./dist/cjs/index.cjs" + "import": "./dist/esm/wrapper.js", + "require": "./dist/cjs/index.cjs", + "default": "./dist/cjs/index.cjs", }, - "./package.json": "./package.json" // ensure this file is importable - } + "./package.json": "./package.json", + }, } ``` @@ -309,20 +306,20 @@ This option has the least burden on development/developer experience. This also means that whatever build tooling must produce the distribution file with a `.cjs` file extension. This might necessitate chaining multiple build tools or adding a subsequent step to move/rename the file to have the `.cjs` file extension (ex `mv ./dist/index.js ./dist/index.cjs`)[3](#footnotes). This can be worked around by adding a subsequent step to move/rename those outputted files (ex [Rollup](https://rollupjs.org/) or [a simple shell script](https://stackoverflow.com/q/21985492)). -Support for the `.cjs` file extension was added in 12.0.0, and using it will cause ESM to properly recognised a file as commonjs (`import { foo } from './foo.cjs` works). However, `require()` does not auto-resolve `.cjs` like it does for `.js`, so file extension cannot be omitted as is commonplace in commonjs: `require('./foo')` will fail, but `require('./foo.cjs')` works. Using it in your package's exports has no drawbacks: `packageJson.exports` (and `packageJson.main`) requires a file extension regardless, and consumers reference your package by the `"name"` field of your package.json (so they're blissfully unaware). +Support for the `.cjs` file extension was added in 12.0.0, and using it will cause ESM to properly recognised a file as commonjs (`import { foo } from './foo.cjs'` works). However, `require()` does not auto-resolve `.cjs` like it does for `.js`, so file extension cannot be omitted as is commonplace in commonjs: `require('./foo')` will fail, but `require('./foo.cjs')` works. Using it in your package's exports has no drawbacks: `packageJson.exports` (and `packageJson.main`) requires a file extension regardless, and consumers reference your package by the `"name"` field of your package.json (so they're blissfully unaware). **Working example**: [esm-with-dual-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/double-distro) ```json { "type": "module", - "engines": { "node": ">=12.22.7" }, // optional, but kind + "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", // ex "./dist/es/index.js" - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.cjs" // ex "./dist/index.cjs" + "import": "./dist/esm/index.js", + "require": "./dist/index.cjs" }, - "./package.json": "./package.json" // ensure this file is importable + "./package.json": "./package.json" } } ``` @@ -368,16 +365,15 @@ So when you see configuration options citing or named with `require` or `import` The `package.json`'s `"type"` field changes the `.js` file extension to mean either `commonjs` or ES `module` respectively. It is very common in dual/mixed packages (that contain both CJS and ESM) to use this field incorrectly. -```json -// ⚠️ THIS DOES NOT WORK +```json displayName="⚠️ THIS DOES NOT WORK" { "type": "module", - "main": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "main": "./dist/CJS/index.js", "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", - "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" }, "./package.json": "./package.json" } @@ -388,15 +384,14 @@ This does not work because `"type": "module"` causes `packageJson.main`, `packag Excluding `"type": "module"` produces the opposite problem: -```json -// ⚠️ THIS DOES NOT WORK +```json displayName="⚠️ THIS DOES NOT WORK" { - "main": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", + "main": "./dist/CJS/index.js", "exports": { ".": { - "import": "PATH/TO/DIST/ESM-CODE/ENTRYPOINT.js", - "require": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js", - "default": "PATH/TO/DIST/CJS-CODE/ENTRYPOINT.js" + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" }, "./package.json": "./package.json" } From 6b379c30ca728dfd72faad46f65a21bcf3ac7b23 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:30:49 +0100 Subject: [PATCH 07/41] fixup!: "This is rarely a good idea" --- apps/site/pages/en/learn/modules/package-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 2ed0430b6a069..60edad033ec4f 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -178,7 +178,7 @@ export { a, b, c /* … */ }; #### Two full distributions -The "Long Island Ice Tea" of packages: Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. +The "Long Island Ice Tea" of packages: Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. This is rarely a good idea. Pros: @@ -296,7 +296,7 @@ This is also almost identical to the [CJS source and dual distribution using an #### Publish both full CJS & ESM distributions -The "Tokyo Tea" of packages: Chuck in a bunch of stuff (with a surprise) and hope for the best. This is probably the most common and easiest of the ESM to CJS & ESM options, but you pay for it. +The "Tokyo Tea" of packages: Chuck in a bunch of stuff (with a surprise) and hope for the best. This is probably the most common and easiest of the ESM to CJS & ESM options, but you pay for it. This is rarely a good idea. In terms of package configuration, there are a few options that differ mostly in personal preference. From 5f2e342135e42c36841dd3847fd23b71e9c37937 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:31:21 +0100 Subject: [PATCH 08/41] fixup!: add missing con for dual-package hazard --- apps/site/pages/en/learn/modules/package-configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 60edad033ec4f..c262cf929e90b 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -187,6 +187,7 @@ Pros: Cons: - Larger package weight (basically double) +- Vulnerable to the Dual-Package Hazard **Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) From 2850a1972721ee32c96e01c44bbc6cf45dca4861 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:31:40 +0100 Subject: [PATCH 09/41] fixup!: add note about potentially no need for package config --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index c262cf929e90b..feb3c292727fd 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -48,7 +48,7 @@ This article covers configuration of all possible combinations in modern Node.js ### CJS source and distribution -This the "Rum & Coke" of packages: pretty difficult to mess up. Essentially just declare the package’s exports via the [`"exports"`](https://nodejs.org/api/packages.html#conditional-exports) field/field-set. +This the "Rum & Coke" of packages: pretty difficult to mess up. You _technially_ may not need _any_ package configuration beyond [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. **Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) From 00a395d293939125e14f8e67d4e605d5263dca57 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:32:01 +0100 Subject: [PATCH 10/41] =?UTF-8?q?fixup!:=20"me"=20=E2=86=92=20"us"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index feb3c292727fd..12b0c8e1650d2 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -337,7 +337,7 @@ The configuration for this is the same as [CJS source and both CJS & ESM distrib 🛑 You should not do this: Versions of Node.js prior to 12.x are End of Life and are now vulnerable to serious security exploits. -If you're a security researcher needing to investigate Node.js prior to v12.22.x, feel free to contact me for help configuring. +If you're a security researcher needing to investigate Node.js prior to v12.22.x, feel free to contact us for help configuring. ## Down the rabbit-hole From faed17472c0d01840bacca15e95d989086c6c30a Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:42:58 +0100 Subject: [PATCH 11/41] fixup!: add `"default"` & `"node"` alternatives --- .../en/learn/modules/package-configuration.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 12b0c8e1650d2..9cf52eab28f78 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -206,6 +206,22 @@ Cons: } ``` +Alternatively, you can use `"default"` and `"node"` keys, which are less counter-intuitive: Node.js will always choose the `"node"` option (which always works), and non-Node.js tooling will choose `"default"` when configured to target something other than node. **This precludes the dual-package hazard.** + +```jsonc +{ + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, + "exports": { + ".": { + "default": "./dist/esm/index.mjs", + "node": "./dist/cjs/index.js", + }, + "./package.json": "./package.json", + }, +} +``` + ### ESM source and distribution The wine of packages: Simple, tried, and true. @@ -311,7 +327,7 @@ Support for the `.cjs` file extension was added in 12.0.0, and using it will cau **Working example**: [esm-with-dual-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/double-distro) -```json +```json displayName="import & require" { "type": "module", "engines": { "node": ">=12.22.7" }, @@ -325,6 +341,22 @@ Support for the `.cjs` file extension was added in 12.0.0, and using it will cau } ``` +Alternatively, you can use `"default"` and `"node"` keys, which are less counter-intuitive: Node.js will always choose the `"node"` option (which always works), and non-Node.js tooling will choose `"default"` when configured to target something other than node. **This precludes the dual-package hazard.** + +```json displayName="default & node" +{ + "type": "module", + "engines": { "node": ">=12.22.7" }, + "exports": { + ".": { + "default": "./dist/esm/index.js", + "node": "./dist/index.cjs" + }, + "./package.json": "./package.json" + } +} +``` + 💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. ##### Use the `.mjs` (or equivalent) file extension for all source code files From ae48eb8e8f4095b7d3e59bd204bfa907dc46c3dd Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:44:40 +0100 Subject: [PATCH 12/41] fixup!: correct footnote refs --- apps/site/pages/en/learn/modules/package-configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 9cf52eab28f78..b941e18aac7c5 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -309,7 +309,7 @@ This is also almost identical to the [CJS source and dual distribution using an } ``` -💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. #### Publish both full CJS & ESM distributions @@ -321,7 +321,7 @@ In terms of package configuration, there are a few options that differ mostly in This option has the least burden on development/developer experience. -This also means that whatever build tooling must produce the distribution file with a `.cjs` file extension. This might necessitate chaining multiple build tools or adding a subsequent step to move/rename the file to have the `.cjs` file extension (ex `mv ./dist/index.js ./dist/index.cjs`)[3](#footnotes). This can be worked around by adding a subsequent step to move/rename those outputted files (ex [Rollup](https://rollupjs.org/) or [a simple shell script](https://stackoverflow.com/q/21985492)). +This also means that whatever build tooling must produce the distribution file with a `.cjs` file extension. This might necessitate chaining multiple build tools or adding a subsequent step to move/rename the file to have the `.cjs` file extension (ex `mv ./dist/index.js ./dist/index.cjs`). This can be worked around by adding a subsequent step to move/rename those outputted files (ex [Rollup](https://rollupjs.org/) or [a simple shell script](https://stackoverflow.com/q/21985492)). Support for the `.cjs` file extension was added in 12.0.0, and using it will cause ESM to properly recognised a file as commonjs (`import { foo } from './foo.cjs'` works). However, `require()` does not auto-resolve `.cjs` like it does for `.js`, so file extension cannot be omitted as is commonplace in commonjs: `require('./foo')` will fail, but `require('./foo.cjs')` works. Using it in your package's exports has no drawbacks: `packageJson.exports` (and `packageJson.main`) requires a file extension regardless, and consumers reference your package by the `"name"` field of your package.json (so they're blissfully unaware). @@ -357,7 +357,7 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter } ``` -💡 Using `"type": "module"`[2](#footnotes) paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. +💡 Using `"type": "module"`[^2] paired with the `.cjs` file extension (for commonjs files) yields best results. For more information on why, see [Down the rabbit-hole](#down-the-rabbithole) and [Gotchas](#gotchas) below. ##### Use the `.mjs` (or equivalent) file extension for all source code files From 6d008da56db7133ee11e3e7de517a35f753ac573 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:05:15 +0100 Subject: [PATCH 13/41] fixup!: list esm-only dist for consumption via `require()` add note about require(esm) --- .../en/learn/modules/package-configuration.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index b941e18aac7c5..06cb83a0720b7 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -37,14 +37,14 @@ This article covers configuration of all possible combinations in modern Node.js - ESM source and distribution - CJS source and distribution with good/specific `module.exports` -| You as a package author write | Consumers of your package write their code in | Your options | -| :-------------------------------- | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | -| CJS source code using `require()` | CJS: consumers `require()` your package | [CJS source and distribution](#cjs-source-and-distribution) | -| CJS source code using `require()` | ESM: consumers `import` your package | [CJS source and only ESM distribution](#cjs-source-and-only-esm-distribution) | -| CJS source code using `require()` | CJS & ESM: consumers either `require()` or `import` your package | [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution) | -| ESM source code using `import` | CJS: consumers `require()` your package | [ESM source with only CJS distribution](#esm-source-with-only-cjs-distribution) | -| ESM source code using `import` | ESM: consumers `import` your package | [ESM source and distribution](#esm-source-and-distribution) | -| ESM: source code uses `import` | CJS & ESM: consumers either `require()` or `import` your package | [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution) | +| You as a package author write | Consumers of your package write their code in | Your options | +| :-------------------------------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CJS source code using `require()` | CJS: consumers `require()` your package | [CJS source and distribution](#cjs-source-and-distribution) | +| CJS source code using `require()` | ESM: consumers `import` your package | [CJS source and only ESM distribution](#cjs-source-and-only-esm-distribution) | +| CJS source code using `require()` | CJS & ESM: consumers either `require()` or `import` your package | [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution) | +| ESM source code using `import` | CJS: consumers `require()` your package | [ESM source with only CJS distribution](#esm-source-with-only-cjs-distribution)
[ESM source and distribution](#esm-source-and-distribution) | +| ESM source code using `import` | ESM: consumers `import` your package | [ESM source and distribution](#esm-source-and-distribution) | +| ESM: source code uses `import` | CJS & ESM: consumers either `require()` or `import` your package | [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution)
[ESM source and distribution](#esm-source-and-distribution) | ### CJS source and distribution @@ -226,6 +226,8 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter The wine of packages: Simple, tried, and true. +Note that since Node.js v23.0.0, it is possible to `require` static ESM (code that does not use top-level `await`). See [Loading ECMAScript modules using `require()`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) for details. + This is almost exactly the same as the CJS-CJS configuration above with 1 small difference: the [`"type"`](https://nodejs.org/api/packages.html#type) field. **Working example**: [esm-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/esm-distro) From 27d5f2118d794d5ad3278fc7ca7f2c3376f09c17 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:14:53 +0100 Subject: [PATCH 14/41] =?UTF-8?q?fixup!:=20"Pick=20your=20poison"=20?= =?UTF-8?q?=E2=86=92=20"Pick=20your=20fix"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/site/pages/en/learn/modules/package-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 06cb83a0720b7..6c3e054ca66b2 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -12,7 +12,7 @@ A frequent question is “how do I make this work!?” (often with angry tears); All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). -For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your poison](#pick-your-poison) for the TLDR. +For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your fix](#pick-your-fix) for the TLDR. ## General notes @@ -30,7 +30,7 @@ CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when Jav An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. -## Pick your poison +## Pick your fix This article covers configuration of all possible combinations in modern Node.js (v12+). If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: From 7eccac2cd14fb11cb794adcb855ea28ad5185c89 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:16:54 +0100 Subject: [PATCH 15/41] fixup!: remove errant "with" --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 6c3e054ca66b2..2a8206f39a508 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -22,7 +22,7 @@ When using [`"exports"`](https://nodejs.org/api/packages.html#conditional-export `"exports"` can be advisable over [`"main"`](https://nodejs.org/api/packages.html#main) because it prevents external access to internal code (so you can be relatively sure users are not depending on things they shouldn't). If you don't need that, `"main"` is simpler and may be a better option for you. -The `"engines"` field provides both a human-friendly and a machine-friendly indication of with which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. +The `"engines"` field provides both a human-friendly and a machine-friendly indication of which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. ## Preamble: How did we get here From 555b8b55ce22d28609aae76d9d2f2917b57a4371 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:41:24 +0100 Subject: [PATCH 16/41] fixup!: alphabetical order unordered items --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 2a8206f39a508..8441360cbbb8b 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -34,8 +34,8 @@ An additional complication is bundlers, which historically managed much of this This article covers configuration of all possible combinations in modern Node.js (v12+). If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: -- ESM source and distribution - CJS source and distribution with good/specific `module.exports` +- ESM source and distribution | You as a package author write | Consumers of your package write their code in | Your options | | :-------------------------------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | From a4f9dab75e8c6dc68d8b9546fe4f38ffad8cfafc Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:42:46 +0100 Subject: [PATCH 17/41] fixup!: include `22.12.0` in support for `require(esm)` --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 8441360cbbb8b..49c9654080eec 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -243,7 +243,7 @@ This is almost exactly the same as the CJS-CJS configuration above with 1 small } ``` -Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _can_ [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) without a flag as of 23.0.0. +Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _can_ [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) without a flag as of 23.0.0 and 22.12.0. ### ESM source with only CJS distribution From d66ba9124b666ea75e278e1abaf5566b7381ceec Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:43:42 +0100 Subject: [PATCH 18/41] fixup!: add not recommended note to cjs with esm wrapper --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 49c9654080eec..c0376d4971f6c 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -290,7 +290,7 @@ The working example below was created prior to Webpack's recent release, so it u #### Publish a CJS distribution with an ESM wrapper -The "Pornstar Martini" of packages: There's a lot going on here. +The "Pornstar Martini" of packages: There's a lot going on here. Unlike the cocktail, this is usually not the best. This is also almost identical to the [CJS source and dual distribution using an ESM wrapper](#use-a-simple-esm-wrapper), but with subtle differences `"type": "module"` and some `.cjs` file extenions in package.json. From dde81b2dc7a3e581e3a141df0cd9314374be9145 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:45:25 +0100 Subject: [PATCH 19/41] fixup!: add minimal vs advanced/verbose examples --- .../en/learn/modules/package-configuration.md | 186 +++++++++++++++--- 1 file changed, 154 insertions(+), 32 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index c0376d4971f6c..e7c20732f4338 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -52,14 +52,22 @@ This the "Rum & Coke" of packages: pretty difficult to mess up. You _technially_ **Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) -```jsonc +```json displayName="Minimal package.json" { + "name": "cjs-source-and-distribution" + // "main": "./index.js" +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-source-and-distribution", "type": "commonjs", // current default, but may change "engines": { "node": ">=12.22.7" }, "exports": { ".": "./dist/index.js", - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -71,14 +79,22 @@ The "Gin & Tonic" of packages: This takes a small bit of finesse but is also pre **Working example**: [cjs-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/esm-distro) -```jsonc +```json displayName="Minimal package.json" { + "name": "cjs-source-with-esm-distribution", + "main": "./dist/index.mjs" +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-source-with-esm-distribution", "type": "commonjs", // current default, but may change "engines": { "node": ">=12.22.7" }, "exports": { ".": "./dist/index.mjs", - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -104,14 +120,22 @@ Cons: **Working example**: [cjs-with-dual-distro (properties)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/property-distro) -```jsonc +```json displayName="Minimal package.json" { + "name": "cjs-source-with-esm-via-properties-distribution", + "main": "./dist/cjs/index.js" +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-source-with-esm-via-properties-distribution", "type": "commonjs", // current default, but may change "engines": { "node": ">=12.22.7" }, "exports": { ".": "./dist/cjs/index.js", - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -149,18 +173,32 @@ Cons: **Working example**: [cjs-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/wrapper-distro) -```jsonc +```json displayName="Minimal package.json" { - "type": "commonjs", // current default, but may change + "name": "cjs-with-wrapper-dual-distro", + "exports": { + ".": { + "import": "./dist/esm/wrapper.mjs", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" + } + } +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-with-wrapper-dual-distro", + "type": "commonjs", "engines": { "node": ">=12.22.7" }, "exports": { ".": { "import": "./dist/esm/wrapper.mjs", "require": "./dist/cjs/index.js", - "default": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" }, - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -191,34 +229,61 @@ Cons: **Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) -```jsonc +```json displayName="Minimal package.json" { - "type": "commonjs", // current default, but may change + "name": "cjs-with-full-dual-distro", + "exports": { + ".": { + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" + } + } +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-with-full-dual-distro", + "type": "commonjs", "engines": { "node": ">=12.22.7" }, "exports": { ".": { "import": "./dist/esm/index.mjs", "require": "./dist/cjs/index.js", - "default": "./dist/cjs/index.js", + "default": "./dist/cjs/index.js" }, - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` Alternatively, you can use `"default"` and `"node"` keys, which are less counter-intuitive: Node.js will always choose the `"node"` option (which always works), and non-Node.js tooling will choose `"default"` when configured to target something other than node. **This precludes the dual-package hazard.** -```jsonc +```json displayName="Minimal package.json" { - "type": "commonjs", // current default, but may change + "name": "cjs-with-alt-full-dual-distro", + "exports": { + ".": { + "default": "./dist/esm/index.mjs", + "node": "./dist/cjs/index.js" + } + } +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-with-alt-full-dual-distro", + "type": "commonjs", "engines": { "node": ">=12.22.7" }, "exports": { ".": { "default": "./dist/esm/index.mjs", - "node": "./dist/cjs/index.js", + "node": "./dist/cjs/index.js" }, - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -232,8 +297,17 @@ This is almost exactly the same as the CJS-CJS configuration above with 1 small **Working example**: [esm-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/esm-distro) -```json +```json displayName="Minimal package.json" +{ + "name": "esm-source-and-distribution", + "type": "module" + // "main": "./index.js" +} +``` + +```json displayName="Advanced (verbose) package.json" { + "name": "esm-source-and-distribution", "type": "module", "engines": { "node": ">=12.22.7" }, "exports": { @@ -275,8 +349,17 @@ The working example below was created prior to Webpack's recent release, so it u **Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/property-distro) -```json +```json displayName="Minimal package.json" { + "name": "esm-with-cjs-distribution", + "type": "module", + "main": "./dist/index.cjs" +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "esm-with-cjs-distribution", "type": "module", "engines": { "node": ">=12.22.7" }, "exports": { @@ -296,18 +379,33 @@ This is also almost identical to the [CJS source and dual distribution using an **Working example**: [esm-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/wrapper-distro) -```jsonc +```json displayName="Minimal package.json" { + "name": "esm-with-cjs-and-esm-wrapper-distribution", + "type": "module", + "exports": { + ".": { + "import": "./dist/esm/wrapper.js", + "require": "./dist/cjs/index.cjs", + "default": "./dist/cjs/index.cjs" + } + } +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "esm-with-cjs-and-esm-wrapper-distribution", "type": "module", "engines": { "node": ">=12.22.7" }, "exports": { ".": { "import": "./dist/esm/wrapper.js", "require": "./dist/cjs/index.cjs", - "default": "./dist/cjs/index.cjs", + "default": "./dist/cjs/index.cjs" }, - "./package.json": "./package.json", - }, + "./package.json": "./package.json" + } } ``` @@ -329,7 +427,19 @@ Support for the `.cjs` file extension was added in 12.0.0, and using it will cau **Working example**: [esm-with-dual-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/double-distro) -```json displayName="import & require" +```json displayName="Minimal import & require package.json" +{ + "type": "module", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/index.cjs" + } + } +} +``` + +```json displayName="Advanced (verbose) import & require package.json" { "type": "module", "engines": { "node": ">=12.22.7" }, @@ -345,7 +455,19 @@ Support for the `.cjs` file extension was added in 12.0.0, and using it will cau Alternatively, you can use `"default"` and `"node"` keys, which are less counter-intuitive: Node.js will always choose the `"node"` option (which always works), and non-Node.js tooling will choose `"default"` when configured to target something other than node. **This precludes the dual-package hazard.** -```json displayName="default & node" +```json displayName="Minimal default & node package.json" +{ + "type": "module", + "exports": { + ".": { + "default": "./dist/esm/index.js", + "node": "./dist/index.cjs" + } + } +} +``` + +```json displayName="Advanced (verbose) default & node package.json" { "type": "module", "engines": { "node": ">=12.22.7" }, From 7038a7d4924911b411a9a835841591641f63f51c Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:50:16 +0100 Subject: [PATCH 20/41] fixup!: add note to top "pick your fix" section for `require(esm)` --- apps/site/pages/en/learn/modules/package-configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index e7c20732f4338..e621a84529daf 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -36,6 +36,7 @@ This article covers configuration of all possible combinations in modern Node.js - CJS source and distribution with good/specific `module.exports` - ESM source and distribution + - If your source-code does _not_ contain any top-level `await`, your ESM likely can be `required()`, meaning it can be consumed by a project using CJS. See [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require). | You as a package author write | Consumers of your package write their code in | Your options | | :-------------------------------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | From 7d6e239ec331f02e36221f283846bc3cf1074b61 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 17:05:51 +0100 Subject: [PATCH 21/41] fixup!: remove "footnotes" (it gets automatically added?) --- apps/site/pages/en/learn/modules/package-configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index e621a84529daf..0f6465639cb93 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -558,8 +558,6 @@ Excluding `"type": "module"` produces the opposite problem: This does not work because `packageJson.exports["."].import` will get interpreted as CJS (but it’s actually ESM). -## Footnotes - [^1]: There was a bug in Node.js v13.0–13.6 where `packageJson.exports["."]` had to be an array with verbose config options as the first item (as an object) and the “default” as the second item (as a string). See [nodejs/modules#446](https://github.com/nodejs/modules/issues/446). [^2]: The `"type"` field in package.json changes what the `.js` file extension means, similar to to an [HTML script element’s type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type). From 3836e3bfad7c187d609313c99f10f475b013fbe5 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 22:39:48 +0100 Subject: [PATCH 22/41] fixup!: correct sequence of `"default"` within `"exports"` --- .../en/learn/modules/package-configuration.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 0f6465639cb93..67e7d78d78dbd 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -266,8 +266,8 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter "name": "cjs-with-alt-full-dual-distro", "exports": { ".": { - "default": "./dist/esm/index.mjs", - "node": "./dist/cjs/index.js" + "node": "./dist/cjs/index.js", + "default": "./dist/esm/index.mjs" } } } @@ -280,8 +280,8 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "default": "./dist/esm/index.mjs", - "node": "./dist/cjs/index.js" + "node": "./dist/cjs/index.js", + "default": "./dist/esm/index.mjs" }, "./package.json": "./package.json" } @@ -461,8 +461,8 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter "type": "module", "exports": { ".": { - "default": "./dist/esm/index.js", - "node": "./dist/index.cjs" + "node": "./dist/index.cjs", + "default": "./dist/esm/index.js" } } } @@ -474,8 +474,8 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter "engines": { "node": ">=12.22.7" }, "exports": { ".": { - "default": "./dist/esm/index.js", - "node": "./dist/index.cjs" + "node": "./dist/index.cjs", + "default": "./dist/esm/index.js" }, "./package.json": "./package.json" } From ca1db99cef6a8a5e933af4552828e4945b430f31 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:47:10 -0500 Subject: [PATCH 23/41] fixup!: apply suggestions from code review Co-authored-by: Augustin Mauroy Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 67e7d78d78dbd..f7dda67e946d9 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -26,13 +26,13 @@ The `"engines"` field provides both a human-friendly and a machine-friendly indi ## Preamble: How did we get here -CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official (TC39) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. +CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official ([TC39](https://tc39.es)) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. ## Pick your fix -This article covers configuration of all possible combinations in modern Node.js (v12+). If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: +This article covers configuration of all possible combinations in currently supported Node.js versions. If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: - CJS source and distribution with good/specific `module.exports` - ESM source and distribution From fd41776bece4d2e260016430b58624b249f69f1e Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:33:28 -0500 Subject: [PATCH 24/41] fixup!: correct example Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index f7dda67e946d9..c7083d792a014 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -156,8 +156,8 @@ Instead, do this: ```cjs module.exports.foo = function foo() {}; -module.exports.foo = function bar() {}; -module.exports.foo = function qux() {}; +module.exports.bar = function bar() {}; +module.exports.qux = function qux() {}; ``` #### Use a simple ESM wrapper From 3dd8124a26a3bcffabc9cd514a82c327278df070 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:42:04 -0500 Subject: [PATCH 25/41] fixup!: wordsmith note about cjs exports reassignment Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index c7083d792a014..7529e9489107f 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -140,7 +140,7 @@ Cons: } ``` -Typically, you would see `module.exports` assigned to something (be it an object or a function) like this: +Sometimes, a CJS module may re-assign `module.exports` to something else (be it an object or a function) like this: ```cjs const someObject = { From aa130313095a3cfc99b914f8a29cfb6876ff2239 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:43:49 -0500 Subject: [PATCH 26/41] fixup!: provide explanation about CJS exports static analysis Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 7529e9489107f..db47cd577a6af 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -152,7 +152,7 @@ const someObject = { module.exports = someObject; ``` -Instead, do this: +Node.js detects the named exports in CJS via [static analysis that look for certain patterns](https://github.com/nodejs/cjs-module-lexer/tree/main?tab=readme-ov-file#parsing-examples), which the example above evades. To make the named exports detectable, do this: ```cjs module.exports.foo = function foo() {}; From 736dee8b7687bf41673b6c510ecf3f4cac520c6d Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:48:24 -0500 Subject: [PATCH 27/41] fixup!: wordsmith `cjs-with-wrapper-dual-distro` explanation Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index db47cd577a6af..2b83a2ef45cf8 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -205,7 +205,7 @@ Cons: In order to support named exports from the CJS bundle for an ESM consumer, this will need a bit of gymnastics from a bundler but is conceptually very simple. -In certain conditions, CJS exports an object (which gets aliased to ESM's `default`); that object, like any object, is destructure-able. You can leverage that to pluck all the members of the object out, and then re-export them so the ESM consumer is none the wiser. +When CJS exports an object (which gets aliased to ESM's `default`), you can save references to all the members of the object locally in the wrapper, and then re-export them so the ESM consumer can access all of them by name. ```js displayName="./dist/esm/wrapper.mjs" import cjs from '../cjs/index.js'; From de80adcd5bde1de2e6dc6723951bbb1a9aabe917 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:03:37 -0500 Subject: [PATCH 28/41] fixup!: clarify ESM wrapper usefulness Co-authored-by: Joyee Cheung Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 2b83a2ef45cf8..275c04caef7fa 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -203,7 +203,7 @@ Cons: } ``` -In order to support named exports from the CJS bundle for an ESM consumer, this will need a bit of gymnastics from a bundler but is conceptually very simple. +When the CJS output from the bundler evades the named exports detection in Node.js, a ESM wrapper can be used to explicitly re-export the known named exports for ESM consumers. When CJS exports an object (which gets aliased to ESM's `default`), you can save references to all the members of the object locally in the wrapper, and then re-export them so the ESM consumer can access all of them by name. From 87d8bf9e7ae1ff1e25b64ac31fb2ea80b93a6289 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:53:10 +0100 Subject: [PATCH 29/41] fixup!: link to commonjs wiki --- apps/site/pages/en/learn/modules/package-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 275c04caef7fa..77fe722ff4490 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -26,7 +26,7 @@ The `"engines"` field provides both a human-friendly and a machine-friendly indi ## Preamble: How did we get here -CommonJS (CJS) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official ([TC39](https://tc39.es)) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. +[CommonJS (CJS)](https://wiki.commonjs.org/wiki/Modules) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official ([TC39](https://tc39.es)) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. From bfc381210f9eced35e715bead06339297c97e291 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:53:27 +0100 Subject: [PATCH 30/41] fixup!: add live bindings caveat --- apps/site/pages/en/learn/modules/package-configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 77fe722ff4490..3f5fb2c27437b 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -215,6 +215,8 @@ const { a, b, c /* … */ } = cjs; export { a, b, c /* … */ }; ``` +**However**, this does break live bindings: a reassignment to `cjs.a` will not reflect in `esmWrapper.a`. + #### Two full distributions The "Long Island Ice Tea" of packages: Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. This is rarely a good idea. From b849f5814a9f2647989b7284c92da73a2f4cec84 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:02:29 +0100 Subject: [PATCH 31/41] fixup!: document the dual-package hazard --- .../pages/en/learn/modules/package-configuration.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 3f5fb2c27437b..8af638c99ae53 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -113,7 +113,7 @@ Pros: - Smaller package weight - Easy and simple (probably least effort if you don't mind keeping to a minor syntax stipulation) -- Precludes the Dual-Package Hazard +- Precludes [the Dual-Package Hazard](#the-dual-package-hazard) Cons: @@ -228,7 +228,7 @@ Pros: Cons: - Larger package weight (basically double) -- Vulnerable to the Dual-Package Hazard +- Vulnerable to [the Dual-Package Hazard](#the-dual-package-hazard) **Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) @@ -519,7 +519,13 @@ So when you see configuration options citing or named with `require` or `import` ⚠️ Adding an `"exports"` field/field-set to a package’s configuration effectively [blocks deep pathing into the package](https://nodejs.org/api/packages.html#package-entry-points) for anything not explicitly listed in the exports’ subpathing. This means it can be a breaking change. -⚠️ Consider carefully whether to distribute both CJS and ESM: It creates the potential for the [Dual Package Hazard](https://nodejs.org/api/packages.html#dual-package-hazard) (especially if misconfigured and the consumer tries to get clever). This can lead to an extremely confusing bug in consuming projects, especially when your package is not perfectly configured. Consumers can even be blind-sided by an intermediary package that uses the "other" format of your package (eg consumer uses the ESM distribution, and some other package the consumer is also using itself uses the CJS distribution). If your package is in any way stateful, consuming both the CJS and ESM distributions will result in parallel states (which is almost surely unintentional). +⚠️ Consider carefully whether to distribute both CJS and ESM: It creates the potential for the [Dual Package Hazard](#the-dual-package-hazard) (especially if misconfigured and the consumer tries to get clever). This can lead to an extremely confusing bug in consuming projects, especially when your package is not perfectly configured. Consumers can even be blind-sided by an intermediary package that uses the "other" format of your package (eg consumer uses the ESM distribution, and some other package the consumer is also using itself uses the CJS distribution). If your package is in any way stateful, consuming both the CJS and ESM distributions will result in parallel states (which is almost surely unintentional). + +### The dual-package hazard + +When an application is using a package that provides both CommonJS and ES module sources, there is a risk of certain bugs if both instances of the package get loaded. This potential comes from the fact that the `pkgInstance` created by `const pkgInstance = require('pkg')` is not the same as the `pkgInstance` created by `import pkgInstance from 'pkg'` (or an alternative main path like `'pkg/module'`). This is the “dual package hazard”, where two instances of the same package can be loaded within the same runtime environment. While it is unlikely that an application or package would intentionally load both instances directly, it is common for an application to load one copy while a dependency of the application loads the other copy. This hazard can happen because Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected and confusing behavior. + +If the package main export is a constructor, an `instanceof` comparison of instances created by the two copies returns `false`, and if the export is an object, properties added to one (like `pkgInstance.foo = 3`) are not present on the other. This differs from how `import` and `require` statements work in all-CommonJS or all-ES module environments, respectively, and therefore is surprising to users. It also differs from the behavior users are familiar with when using transpilation via tools like [Babel](https://babeljs.io/) or [`esm`](https://github.com/standard-things/esm#readme). ## Gotchas From 932229ed7f4b944526be7566d5b50b34faf59b09 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:13:46 +0100 Subject: [PATCH 32/41] fixup!: explain pjson `"type"` --- .../site/pages/en/learn/modules/package-configuration.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index 8af638c99ae53..dead75b7d98a5 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -346,10 +346,17 @@ The "Mojito" of packages: Tricky to make and needs good ingredients. This option is almost identical to the [CJS source with CJS & ESM distribution's property exports](#attach-named-exports-directly-onto-raw-exports-endraw-) above. The only difference is in package.json: `"type": "module"`. -Only some build tools support generating this output. [Rollup](https://www.rollupjs.org/) produces compatible output out of the box when targetting commonjs. Webpack as of [v5.66.0+](https://github.com/webpack/webpack/releases/tag/v5.66.0) does with the new [`commonjs-static`](https://webpack.js.org/configuration/output/#type-commonjs-static) output type, (prior to this no commonjs options produces compatible output). It is not currently possible with [esbuild](https://esbuild.github.io/) (which produces a non-static `exports`). +Only some build tools support generating this output. [Rollup](https://www.rollupjs.org/) produces compatible output out of the box when targetting commonjs. Webpack as of [v5.66.0+](https://github.com/webpack/webpack/releases/tag/v5.66.0) does with the new [`commonjs-static`](https://webpack.js.org/configuration/output/#type-commonjs-static) output type, (prior to this, no commonjs options produces compatible output). It is not currently possible with [esbuild](https://esbuild.github.io/) (which produces a non-static `exports`). The working example below was created prior to Webpack's recent release, so it uses Rollup (I'll get around to adding a Webpack option too). +These examples assume javascript files within use the extension `.js`; `"type"` in `package.json` controls how those are interpreted: + +`"type":"commonjs"` + `.js` → `cjs`
+`"type":"module"` + `.js` → `mjs` + +If your files explicitly _all_ use `.cjs` and/or `.mjs` file extensions (none use `.js`), `"type"` is superfluous. + **Working example**: [esm-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/dual/property-distro) ```json displayName="Minimal package.json" From 03255d0f6135ca44a86c64896c9896f4b40b4160 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:54:05 +0100 Subject: [PATCH 33/41] fixup!: re-organise article & remove drink references --- .../en/learn/modules/package-configuration.md | 153 ++++++++++-------- 1 file changed, 86 insertions(+), 67 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.md index dead75b7d98a5..6e60f228acba9 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.md @@ -6,15 +6,53 @@ authors: JakobJingleheimer # Package configuration -Configuration is always a chore, but an unfortunately necessary evil. And configuring a package for CommonJS (CJS) and ES Modules (ESM) can be a waking nightmare—not least because it has changed a dozen times in half as many years. - -A frequent question is “how do I make this work!?” (often with angry tears); but yet more frequently we come across packages that are just misconfigured. - All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). -For curious cats, [Preamble: How did we get here](#preamble-how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. If you're just looking for a solution, jump to [Pick your fix](#pick-your-fix) for the TLDR. +For curious cats, [How did we get here](#how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations. -## General notes +## Pick your fix + +There are 2 main options, which cover almost all use-cases: + +- Write source code and publish in CJS (you use `require()`); CJS is consumable by both CJS and ESM (in all versions of node). Skip to [CJS source and distribution](#cjs-source-and-distribution). +- Write source code and publish in ESM (you use `import`, and don't use top-level `await`); ESM is consumable by both ESM and CJS (in node 22.x and 23.x; see [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require)). Skip to [ESM source and distribution](#esm-source-and-distribution). + +It's generally best to publish only 1 format, either CJS _or_ ESM. Not both. Publishing multiple formats can result in the [dual-package hazard](#the-dual-package-hazard), as well as other drawbacks. + +There are other options available, mostly for historical purposes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
You as a package author writeConsumers of your package write their code inYour options
CJS source code using require()ESM: consumers import your packageCJS source and only ESM distribution
CJS & ESM: consumers either require() or import your packageCJS source and both CJS & ESM distribution
ESM source code using importCJS: consumers require() your package (and you use top-level await)ESM source with only CJS distribution
CJS & ESM: consumers either require() or import your packageESM source and both CJS & ESM distribution
+ +### General notes [Syntax detection](https://nodejs.org/api/packages.html#syntax-detection) is _**not**_ a replacement for proper package configuration; syntax detection is not fool-proof and it has [significant performance cost](https://github.com/nodejs/node/pull/55238). @@ -24,46 +62,55 @@ When using [`"exports"`](https://nodejs.org/api/packages.html#conditional-export The `"engines"` field provides both a human-friendly and a machine-friendly indication of which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. -## Preamble: How did we get here +### CJS source and distribution -[CommonJS (CJS)](https://wiki.commonjs.org/wiki/Modules) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official ([TC39](https://tc39.es)) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. +You _technially_ may not need _any_ package configuration beyond [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. -An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. +**Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) -## Pick your fix +```json displayName="Minimal package.json" +{ + "name": "cjs-source-and-distribution" + // "main": "./index.js" +} +``` + +```json displayName="Advanced (verbose) package.json" +{ + "name": "cjs-source-and-distribution", + "type": "commonjs", // current default, but may change + "engines": { "node": ">=12.22.7" }, + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + } +} +``` -This article covers configuration of all possible combinations in currently supported Node.js versions. If you are trying to decide which options are ideal, it is better to avoid dual packages, so either: +Note that `packageJson.exports["."] = filepath` is shorthand for `packageJson.exports["."].default = filepath` -- CJS source and distribution with good/specific `module.exports` -- ESM source and distribution - - If your source-code does _not_ contain any top-level `await`, your ESM likely can be `required()`, meaning it can be consumed by a project using CJS. See [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require). +### ESM source and distribution -| You as a package author write | Consumers of your package write their code in | Your options | -| :-------------------------------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CJS source code using `require()` | CJS: consumers `require()` your package | [CJS source and distribution](#cjs-source-and-distribution) | -| CJS source code using `require()` | ESM: consumers `import` your package | [CJS source and only ESM distribution](#cjs-source-and-only-esm-distribution) | -| CJS source code using `require()` | CJS & ESM: consumers either `require()` or `import` your package | [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution) | -| ESM source code using `import` | CJS: consumers `require()` your package | [ESM source with only CJS distribution](#esm-source-with-only-cjs-distribution)
[ESM source and distribution](#esm-source-and-distribution) | -| ESM source code using `import` | ESM: consumers `import` your package | [ESM source and distribution](#esm-source-and-distribution) | -| ESM: source code uses `import` | CJS & ESM: consumers either `require()` or `import` your package | [ESM source and both CJS & ESM distribution](#esm-source-and-both-cjs-amp-esm-distribution)
[ESM source and distribution](#esm-source-and-distribution) | +Simple, tried, and true. -### CJS source and distribution +Note that since Node.js v23.0.0, it is possible to `require` static ESM (code that does not use top-level `await`). See [Loading ECMAScript modules using `require()`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) for details. -This the "Rum & Coke" of packages: pretty difficult to mess up. You _technially_ may not need _any_ package configuration beyond [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. +This is almost exactly the same as the CJS-CJS configuration above with 1 small difference: the [`"type"`](https://nodejs.org/api/packages.html#type) field. -**Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) +**Working example**: [esm-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/esm-distro) ```json displayName="Minimal package.json" { - "name": "cjs-source-and-distribution" + "name": "esm-source-and-distribution", + "type": "module" // "main": "./index.js" } ``` ```json displayName="Advanced (verbose) package.json" { - "name": "cjs-source-and-distribution", - "type": "commonjs", // current default, but may change + "name": "esm-source-and-distribution", + "type": "module", "engines": { "node": ">=12.22.7" }, "exports": { ".": "./dist/index.js", @@ -72,11 +119,11 @@ This the "Rum & Coke" of packages: pretty difficult to mess up. You _technially_ } ``` -Note that `packageJson.exports["."] = filepath` is shorthand for `packageJson.exports["."].default = filepath` +Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _can_ [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) without a flag as of 23.0.0 and 22.12.0. ### CJS source and only ESM distribution -The "Gin & Tonic" of packages: This takes a small bit of finesse but is also pretty straight-forward. +This takes a small bit of finesse but is also pretty straight-forward. **Working example**: [cjs-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/esm-distro) @@ -107,7 +154,7 @@ You have a few options: #### Attach named exports directly onto `exports` -The "French 75" of packages: Classic but takes some sophistication and finesse. +Classic but takes some sophistication and finesse. Pros: @@ -162,7 +209,7 @@ module.exports.qux = function qux() {}; #### Use a simple ESM wrapper -The "Piña Colada" of packages: Complicated setup and difficult to get the balance right. +Complicated setup and difficult to get the balance right. Pros: @@ -219,7 +266,7 @@ export { a, b, c /* … */ }; #### Two full distributions -The "Long Island Ice Tea" of packages: Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. This is rarely a good idea. +Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. This is rarely a good idea. Pros: @@ -290,38 +337,6 @@ Alternatively, you can use `"default"` and `"node"` keys, which are less counter } ``` -### ESM source and distribution - -The wine of packages: Simple, tried, and true. - -Note that since Node.js v23.0.0, it is possible to `require` static ESM (code that does not use top-level `await`). See [Loading ECMAScript modules using `require()`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) for details. - -This is almost exactly the same as the CJS-CJS configuration above with 1 small difference: the [`"type"`](https://nodejs.org/api/packages.html#type) field. - -**Working example**: [esm-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/esm/esm-distro) - -```json displayName="Minimal package.json" -{ - "name": "esm-source-and-distribution", - "type": "module" - // "main": "./index.js" -} -``` - -```json displayName="Advanced (verbose) package.json" -{ - "name": "esm-source-and-distribution", - "type": "module", - "engines": { "node": ">=12.22.7" }, - "exports": { - ".": "./dist/index.js", - "./package.json": "./package.json" - } -} -``` - -Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _can_ [`require()` an ES Module](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require) without a flag as of 23.0.0 and 22.12.0. - ### ESM source with only CJS distribution We're not in Kansas anymore, Toto. @@ -334,8 +349,6 @@ The configurations (there are 2 options) are nearly the same as [ESM source and ### ESM source and both CJS & ESM distribution -These are "mixologist" territory. - When source code is written in non-JavaScript (ex TypeScript), options can be limited due to needing to use file extension(s) specific to that language (ex `.ts`) and there may be no `.mjs` equivalent. Similar to [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs-amp-esm-distribution), you have the same options. @@ -534,6 +547,12 @@ When an application is using a package that provides both CommonJS and ES module If the package main export is a constructor, an `instanceof` comparison of instances created by the two copies returns `false`, and if the export is an object, properties added to one (like `pkgInstance.foo = 3`) are not present on the other. This differs from how `import` and `require` statements work in all-CommonJS or all-ES module environments, respectively, and therefore is surprising to users. It also differs from the behavior users are familiar with when using transpilation via tools like [Babel](https://babeljs.io/) or [`esm`](https://github.com/standard-things/esm#readme). +### How did we get here + +[CommonJS (CJS)](https://wiki.commonjs.org/wiki/Modules) was created _long_ before ECMAScript Modules (ESM), back when JavaScript was still adolescent—CJS and jQuery were created just 3 years apart. CJS is not an official ([TC39](https://tc39.es)) standard and is supported by a limited few platforms (most notably, Node.js). ESM as a standard has been incoming for several years; it is currently supported by all major platforms (browsers, Deno, Node.js, etc), meaning it will run pretty much everywhere. As it became clear ESM would effectively succeed CJS (which is still very popular and widespread), many attempted to adopt early on, often before a particular aspect of the ESM specification was finalised. Because of this, those changed over time as better information became available (often informed by learnings/experiences of those eager beavers), going from best-guess to the aligning with the specification. + +An additional complication is bundlers, which historically managed much of this territory. However, much of what we previously needed bundle(r)s to manage is now native functionality; yet bundlers are still (and likely always will be) necessary for some things. Unfortunately, functionality bundlers no-longer need to provide is deeply ingrained in older bundlers’ implementations, so they can at times be too helpful, and in some cases, anti-pattern (bundling a library is often not recommended by bundler authors themselves). The hows and whys of that are an article unto itself. + ## Gotchas The `package.json`'s `"type"` field changes the `.js` file extension to mean either `commonjs` or ES `module` respectively. It is very common in dual/mixed packages (that contain both CJS and ESM) to use this field incorrectly. From 1bd407703b1c08909fca8a44bad1f2f354bb3af8 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:18:37 +0100 Subject: [PATCH 34/41] fixup!: convert to mdx --- .../{package-configuration.md => package-configuration.mdx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/site/pages/en/learn/modules/{package-configuration.md => package-configuration.mdx} (99%) diff --git a/apps/site/pages/en/learn/modules/package-configuration.md b/apps/site/pages/en/learn/modules/package-configuration.mdx similarity index 99% rename from apps/site/pages/en/learn/modules/package-configuration.md rename to apps/site/pages/en/learn/modules/package-configuration.mdx index 6e60f228acba9..c4fb787f319ab 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.md +++ b/apps/site/pages/en/learn/modules/package-configuration.mdx @@ -32,7 +32,7 @@ There are other options available, mostly for historical purposes. - CJS source code using require() + CJS source code using require() ESM: consumers import your package CJS source and only ESM distribution @@ -41,7 +41,7 @@ There are other options available, mostly for historical purposes. CJS source and both CJS & ESM distribution - ESM source code using import + ESM source code using import CJS: consumers require() your package (and you use top-level await) ESM source with only CJS distribution From b5085f2412ef371a47292061e8f2697f0508eaa5 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:58:15 +0100 Subject: [PATCH 35/41] fixup!: remove missed drinks --- apps/site/pages/en/learn/modules/package-configuration.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/site/pages/en/learn/modules/package-configuration.mdx b/apps/site/pages/en/learn/modules/package-configuration.mdx index c4fb787f319ab..b9421bc825287 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.mdx +++ b/apps/site/pages/en/learn/modules/package-configuration.mdx @@ -355,7 +355,7 @@ Similar to [CJS source and both CJS & ESM distribution](#cjs-source-and-both-cjs #### Publish only a CJS distribution with property exports -The "Mojito" of packages: Tricky to make and needs good ingredients. +Tricky to make and needs good ingredients. This option is almost identical to the [CJS source with CJS & ESM distribution's property exports](#attach-named-exports-directly-onto-raw-exports-endraw-) above. The only difference is in package.json: `"type": "module"`. @@ -396,7 +396,7 @@ If your files explicitly _all_ use `.cjs` and/or `.mjs` file extensions (none us #### Publish a CJS distribution with an ESM wrapper -The "Pornstar Martini" of packages: There's a lot going on here. Unlike the cocktail, this is usually not the best. +There's a lot going on here, and this is usually not the best. This is also almost identical to the [CJS source and dual distribution using an ESM wrapper](#use-a-simple-esm-wrapper), but with subtle differences `"type": "module"` and some `.cjs` file extenions in package.json. @@ -436,7 +436,7 @@ This is also almost identical to the [CJS source and dual distribution using an #### Publish both full CJS & ESM distributions -The "Tokyo Tea" of packages: Chuck in a bunch of stuff (with a surprise) and hope for the best. This is probably the most common and easiest of the ESM to CJS & ESM options, but you pay for it. This is rarely a good idea. +Chuck in a bunch of stuff (with a surprise) and hope for the best. This is probably the most common and easiest of the ESM to CJS & ESM options, but you pay for it. This is rarely a good idea. In terms of package configuration, there are a few options that differ mostly in personal preference. From d34fd0b6dba565cfb8355d6cbc2c36c4a6c0e4c3 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:49:04 +0100 Subject: [PATCH 36/41] fixup!: rename to "publishing a package" --- apps/site/navigation.json | 6 +++--- .../{package-configuration.mdx => publishing-a-package.mdx} | 4 ++-- packages/i18n/locales/en.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename apps/site/pages/en/learn/modules/{package-configuration.mdx => publishing-a-package.mdx} (99%) diff --git a/apps/site/navigation.json b/apps/site/navigation.json index 570387e158cec..39eb9df019b4c 100644 --- a/apps/site/navigation.json +++ b/apps/site/navigation.json @@ -313,9 +313,9 @@ "modules": { "label": "components.navigation.learn.modules.links.modules", "items": { - "packageConfiguration": { - "link": "/learn/modules/package-configuration", - "label": "components.navigation.learn.modules.links.packageConfiguration" + "publishingAPackage": { + "link": "/learn/modules/publishing-a-package", + "label": "components.navigation.learn.modules.links.publishingAPackage" }, "publishingNodeApiModules": { "link": "/learn/modules/publishing-node-api-modules", diff --git a/apps/site/pages/en/learn/modules/package-configuration.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx similarity index 99% rename from apps/site/pages/en/learn/modules/package-configuration.mdx rename to apps/site/pages/en/learn/modules/publishing-a-package.mdx index b9421bc825287..2eeacf2a231fb 100644 --- a/apps/site/pages/en/learn/modules/package-configuration.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -1,10 +1,10 @@ --- -title: Package Configuration +title: Publishing a package layout: learn authors: JakobJingleheimer --- -# Package configuration +# Publishing a package All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index 71e466c094a61..6a07b3f089af0 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -90,7 +90,7 @@ "modules": { "links": { "modules": "Modules", - "packageConfiguration": "Package configuration", + "publishingAPackage": "Publishing a package", "publishingNodeApiModules": "How to publish a Node-API package", "anatomyOfAnHttpTransaction": "Anatomy of an HTTP Transaction", "abiStability": "ABI Stability", From 4bd694ecb80149d2da5e54a445a21ca33d9b4c1f Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:50:16 +0100 Subject: [PATCH 37/41] fixup!: move "general notes" to bottom --- .../en/learn/modules/publishing-a-package.mdx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index 2eeacf2a231fb..e598f5363f2e5 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -52,16 +52,6 @@ There are other options available, mostly for historical purposes. -### General notes - -[Syntax detection](https://nodejs.org/api/packages.html#syntax-detection) is _**not**_ a replacement for proper package configuration; syntax detection is not fool-proof and it has [significant performance cost](https://github.com/nodejs/node/pull/55238). - -When using [`"exports"`](https://nodejs.org/api/packages.html#conditional-exports) in package.json, it is generally a good idea to include `"./package.json": "./package.json"` so that it can be imported ([`module.findPackageJSON`](https://nodejs.org/api/module.html#modulefindpackagejsonspecifier-base) is not affected by this limitation, but `import` may be more convenient). - -`"exports"` can be advisable over [`"main"`](https://nodejs.org/api/packages.html#main) because it prevents external access to internal code (so you can be relatively sure users are not depending on things they shouldn't). If you don't need that, `"main"` is simpler and may be a better option for you. - -The `"engines"` field provides both a human-friendly and a machine-friendly indication of which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. - ### CJS source and distribution You _technially_ may not need _any_ package configuration beyond [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. @@ -518,6 +508,16 @@ The configuration for this is the same as [CJS source and both CJS & ESM distrib If you're a security researcher needing to investigate Node.js prior to v12.22.x, feel free to contact us for help configuring. +## General notes + +[Syntax detection](https://nodejs.org/api/packages.html#syntax-detection) is _**not**_ a replacement for proper package configuration; syntax detection is not fool-proof and it has [significant performance cost](https://github.com/nodejs/node/pull/55238). + +When using [`"exports"`](https://nodejs.org/api/packages.html#conditional-exports) in package.json, it is generally a good idea to include `"./package.json": "./package.json"` so that it can be imported ([`module.findPackageJSON`](https://nodejs.org/api/module.html#modulefindpackagejsonspecifier-base) is not affected by this limitation, but `import` may be more convenient). + +`"exports"` can be advisable over [`"main"`](https://nodejs.org/api/packages.html#main) because it prevents external access to internal code (so you can be relatively sure users are not depending on things they shouldn't). If you don't need that, `"main"` is simpler and may be a better option for you. + +The `"engines"` field provides both a human-friendly and a machine-friendly indication of which version(s) of Node.js the package is compatible. Depending on the package manager used, an exception may be thrown causing the installation to fail when the consumer is using an incompatible version of Node.js (which can be very helpful to consumers). Including this field will save a lot of headache for consumers with an older version of Node.js who cannot use the package. + ## Down the rabbit-hole Specifically in relation to Node.js, there are 4 problems to solve: From 64ed71944741e08f10e2e78b9c1a420efcb433d8 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:15:26 +0100 Subject: [PATCH 38/41] fixup!: wordsmith --- apps/site/pages/en/learn/modules/publishing-a-package.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index e598f5363f2e5..19405f31f4f3f 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -54,7 +54,7 @@ There are other options available, mostly for historical purposes. ### CJS source and distribution -You _technially_ may not need _any_ package configuration beyond [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. +The most minimal configuration may be only [`"name"`](https://nodejs.org/api/packages.html#name). But the less arcane, the better: Essentially just declare the package’s exports via the `"exports"` field/field-set. **Working example**: [cjs-with-cjs-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/cjs-distro) @@ -113,7 +113,7 @@ Note that ESM now _is_ “backwards” compatible with CJS: a CJS module now _ca ### CJS source and only ESM distribution -This takes a small bit of finesse but is also pretty straight-forward. +This takes a small bit of finesse but is also pretty straight-forward. This may be the choice pick of older projects targetting newer standards, or authors who merely prefer CJS but are publishing for a different environment. **Working example**: [cjs-with-esm-distro](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/esm-distro) @@ -140,7 +140,7 @@ The [`.mjs`](https://nodejs.org/api/esm.html#enabling) file extension is a trump ### CJS source and both CJS & ESM distribution -You have a few options: +In order to _directly_ supply both audiences (so that your distribution works "natively" in either), you have a few options: #### Attach named exports directly onto `exports` From 7b692f979c5859f872b25a033f999854d1b30474 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:17:10 +0100 Subject: [PATCH 39/41] fixup!: move pros/cons after examples --- .../en/learn/modules/publishing-a-package.mdx | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index 19405f31f4f3f..420784a2c1c8e 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -146,16 +146,6 @@ In order to _directly_ supply both audiences (so that your distribution works "n Classic but takes some sophistication and finesse. -Pros: - -- Smaller package weight -- Easy and simple (probably least effort if you don't mind keeping to a minor syntax stipulation) -- Precludes [the Dual-Package Hazard](#the-dual-package-hazard) - -Cons: - -- Requires very specific syntax (either in source code and/or bundler gymnastics). - **Working example**: [cjs-with-dual-distro (properties)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/property-distro) ```json displayName="Minimal package.json" @@ -177,6 +167,16 @@ Cons: } ``` +Pros: + +- Smaller package weight +- Easy and simple (probably least effort if you don't mind keeping to a minor syntax stipulation) +- Precludes [the Dual-Package Hazard](#the-dual-package-hazard) + +Cons: + +- Requires very specific syntax (either in source code and/or bundler gymnastics). + Sometimes, a CJS module may re-assign `module.exports` to something else (be it an object or a function) like this: ```cjs @@ -201,14 +201,6 @@ module.exports.qux = function qux() {}; Complicated setup and difficult to get the balance right. -Pros: - -- Smaller package weight - -Cons: - -- Likely requires complicated bundler gymnastics (we could not find any existing option to automate this in Webpack). - **Working example**: [cjs-with-dual-distro (wrapper)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/wrapper-distro) ```json displayName="Minimal package.json" @@ -240,6 +232,14 @@ Cons: } ``` +Pros: + +- Smaller package weight + +Cons: + +- Likely requires complicated bundler gymnastics (we could not find any existing option to automate this in Webpack). + When the CJS output from the bundler evades the named exports detection in Node.js, a ESM wrapper can be used to explicitly re-export the known named exports for ESM consumers. When CJS exports an object (which gets aliased to ESM's `default`), you can save references to all the members of the object locally in the wrapper, and then re-export them so the ESM consumer can access all of them by name. @@ -258,15 +258,6 @@ export { a, b, c /* … */ }; Chuck in a bunch of stuff and hope for the best. This is probably the most common and easiest of the CJS to CJS & ESM options, but you pay for it. This is rarely a good idea. -Pros: - -- Simple bundler configuration - -Cons: - -- Larger package weight (basically double) -- Vulnerable to [the Dual-Package Hazard](#the-dual-package-hazard) - **Working example**: [cjs-with-dual-distro (double)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/double-distro) ```json displayName="Minimal package.json" @@ -298,6 +289,15 @@ Cons: } ``` +Pros: + +- Simple bundler configuration + +Cons: + +- Larger package weight (basically double) +- Vulnerable to [the Dual-Package Hazard](#the-dual-package-hazard) + Alternatively, you can use `"default"` and `"node"` keys, which are less counter-intuitive: Node.js will always choose the `"node"` option (which always works), and non-Node.js tooling will choose `"default"` when configured to target something other than node. **This precludes the dual-package hazard.** ```json displayName="Minimal package.json" From 1ffb083659074ea763ec0760271fc73a07a702cf Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:20:07 +0100 Subject: [PATCH 40/41] fixup!: explain "attach onto `exports`" --- apps/site/pages/en/learn/modules/publishing-a-package.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index 420784a2c1c8e..4e22abff552ea 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -144,7 +144,7 @@ In order to _directly_ supply both audiences (so that your distribution works "n #### Attach named exports directly onto `exports` -Classic but takes some sophistication and finesse. +Classic but takes some sophistication and finesse. This means adding properties onto the existing `module.exports` (instead of re-assigning `module.exports` as a whole). **Working example**: [cjs-with-dual-distro (properties)](https://github.com/JakobJingleheimer/nodejs-module-config-examples/tree/main/packages/cjs/dual/property-distro) From cda5e86dd90a579f00c899b9b8fee10ee7ad6fae Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:21:48 +0100 Subject: [PATCH 41/41] fixup!: temp restore examples repo --- apps/site/pages/en/learn/modules/publishing-a-package.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/pages/en/learn/modules/publishing-a-package.mdx b/apps/site/pages/en/learn/modules/publishing-a-package.mdx index 4e22abff552ea..5c0b82e6f89dc 100644 --- a/apps/site/pages/en/learn/modules/publishing-a-package.mdx +++ b/apps/site/pages/en/learn/modules/publishing-a-package.mdx @@ -6,7 +6,7 @@ authors: JakobJingleheimer # Publishing a package -All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [nodejs/package-examples](https://github.com/nodejs/package-examples/blob/main/config). +All the provided `package.json` configurations (not specifically marked “does not work”) work in Node.js 12.22.x (v12 latest, the oldest supported line) and 17.2.0 (current latest at the time)[^1], and for grins, with webpack 5.53.0 and 5.63.0 respectively. These are available: [JakobJingleheimer/nodejs-module-config-examples](https://github.com/JakobJingleheimer/nodejs-module-config-examples). For curious cats, [How did we get here](#how-did-we-get-here) and [Down the rabbit-hole](#down-the-rabbithole) provide background and deeper explanations.