doc: move dual package shipping docs to separate repo · nodejs/node@8e76cc6
@@ -902,273 +902,7 @@ $ node other.js
902902903903## Dual CommonJS/ES module packages
904904905-<!-- This section should not be in the API documentation:
906-907-1. It teaches opinionated practices that some consider dangerous, see
908- https://github.com/nodejs/node/issues/52174
909-2. It will soon be obsolete when we unflag --experimental-require-module.
910-3. It's difficult to understand a multi-file structure via long texts and snippets in
911- a markdown document.
912-913-TODO(?): Move this section to its own repository with example folders.
914--->
915-916-Prior to the introduction of support for ES modules in Node.js, it was a common
917-pattern for package authors to include both CommonJS and ES module JavaScript
918-sources in their package, with `package.json` [`"main"`][] specifying the
919-CommonJS entry point and `package.json` `"module"` specifying the ES module
920-entry point.
921-This enabled Node.js to run the CommonJS entry point while build tools such as
922-bundlers used the ES module entry point, since Node.js ignored (and still
923-ignores) the top-level `"module"` field.
924-925-Node.js can now run ES module entry points, and a package can contain both
926-CommonJS and ES module entry points (either via separate specifiers such as
927-`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional
928-exports][]). Unlike in the scenario where top-level `"module"` field is only used by bundlers,
929-or ES module files are transpiled into CommonJS on the fly before evaluation by
930-Node.js, the files referenced by the ES module entry point are evaluated as ES
931-modules.
932-933-### Dual package hazard
934-935-When an application is using a package that provides both CommonJS and ES module
936-sources, there is a risk of certain bugs if both versions of the package get
937-loaded. This potential comes from the fact that the `pkgInstance` created by
938-`const pkgInstance = require('pkg')` is not the same as the `pkgInstance`
939-created by `import pkgInstance from 'pkg'` (or an alternative main path like
940-`'pkg/module'`). This is the “dual package hazard,” where two versions of the
941-same package can be loaded within the same runtime environment. While it is
942-unlikely that an application or package would intentionally load both versions
943-directly, it is common for an application to load one version while a dependency
944-of the application loads the other version. This hazard can happen because
945-Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected
946-behavior.
947-948-If the package main export is a constructor, an `instanceof` comparison of
949-instances created by the two versions returns `false`, and if the export is an
950-object, properties added to one (like `pkgInstance.foo = 3`) are not present on
951-the other. This differs from how `import` and `require` statements work in
952-all-CommonJS or all-ES module environments, respectively, and therefore is
953-surprising to users. It also differs from the behavior users are familiar with
954-when using transpilation via tools like [Babel][] or [`esm`][].
955-956-### Writing dual packages while avoiding or minimizing hazards
957-958-First, the hazard described in the previous section occurs when a package
959-contains both CommonJS and ES module sources and both sources are provided for
960-use in Node.js, either via separate main entry points or exported paths. A
961-package might instead be written where any version of Node.js receives only
962-CommonJS sources, and any separate ES module sources the package might contain
963-are intended only for other environments such as browsers. Such a package
964-would be usable by any version of Node.js, since `import` can refer to CommonJS
965-files; but it would not provide any of the advantages of using ES module syntax.
966-967-A package might also switch from CommonJS to ES module syntax in a [breaking
968-change](https://semver.org/) version bump. This has the disadvantage that the
969-newest version of the package would only be usable in ES module-supporting
970-versions of Node.js.
971-972-Every pattern has tradeoffs, but there are two broad approaches that satisfy the
973-following conditions:
974-975-1. The package is usable via both `require` and `import`.
976-2. The package is usable in both current Node.js and older versions of Node.js
977- that lack support for ES modules.
978-3. The package main entry point, e.g. `'pkg'` can be used by both `require` to
979- resolve to a CommonJS file and by `import` to resolve to an ES module file.
980- (And likewise for exported paths, e.g. `'pkg/feature'`.)
981-4. The package provides named exports, e.g. `import { name } from 'pkg'` rather
982- than `import pkg from 'pkg'; pkg.name`.
983-5. The package is potentially usable in other ES module environments such as
984- browsers.
985-6. The hazards described in the previous section are avoided or minimized.
986-987-#### Approach #1: Use an ES module wrapper
988-989-Write the package in CommonJS or transpile ES module sources into CommonJS, and
990-create an ES module wrapper file that defines the named exports. Using
991-[Conditional exports][], the ES module wrapper is used for `import` and the
992-CommonJS entry point for `require`.
993-994-```json
995-// ./node_modules/pkg/package.json
996-{
997-"type": "module",
998-"exports": {
999-"import": "./wrapper.mjs",
1000-"require": "./index.cjs"
1001- }
1002-}
1003-```
1004-1005-The preceding example uses explicit extensions `.mjs` and `.cjs`.
1006-If your files use the `.js` extension, `"type": "module"` will cause such files
1007-to be treated as ES modules, just as `"type": "commonjs"` would cause them
1008-to be treated as CommonJS.
1009-See [Enabling](esm.md#enabling).
1010-1011-```cjs
1012-// ./node_modules/pkg/index.cjs
1013-exports.name = 'value';
1014-```
1015-1016-```js
1017-// ./node_modules/pkg/wrapper.mjs
1018-import cjsModule from './index.cjs';
1019-export const name = cjsModule.name;
1020-```
1021-1022-In this example, the `name` from `import { name } from 'pkg'` is the same
1023-singleton as the `name` from `const { name } = require('pkg')`. Therefore `===`
1024-returns `true` when comparing the two `name`s and the divergent specifier hazard
1025-is avoided.
1026-1027-If the module is not simply a list of named exports, but rather contains a
1028-unique function or object export like `module.exports = function () { ... }`,
1029-or if support in the wrapper for the `import pkg from 'pkg'` pattern is desired,
1030-then the wrapper would instead be written to export the default optionally
1031-along with any named exports as well:
1032-1033-```js
1034-import cjsModule from './index.cjs';
1035-export const name = cjsModule.name;
1036-export default cjsModule;
1037-```
1038-1039-This approach is appropriate for any of the following use cases:
1040-1041-* The package is currently written in CommonJS and the author would prefer not
1042- to refactor it into ES module syntax, but wishes to provide named exports for
1043- ES module consumers.
1044-* The package has other packages that depend on it, and the end user might
1045- install both this package and those other packages. For example a `utilities`
1046- package is used directly in an application, and a `utilities-plus` package
1047- adds a few more functions to `utilities`. Because the wrapper exports
1048- underlying CommonJS files, it doesn't matter if `utilities-plus` is written in
1049- CommonJS or ES module syntax; it will work either way.
1050-* The package stores internal state, and the package author would prefer not to
1051- refactor the package to isolate its state management. See the next section.
1052-1053-A variant of this approach not requiring conditional exports for consumers could
1054-be to add an export, e.g. `"./module"`, to point to an all-ES module-syntax
1055-version of the package. This could be used via `import 'pkg/module'` by users
1056-who are certain that the CommonJS version will not be loaded anywhere in the
1057-application, such as by dependencies; or if the CommonJS version can be loaded
1058-but doesn't affect the ES module version (for example, because the package is
1059-stateless):
1060-1061-```json
1062-// ./node_modules/pkg/package.json
1063-{
1064-"type": "module",
1065-"exports": {
1066-".": "./index.cjs",
1067-"./module": "./wrapper.mjs"
1068- }
1069-}
1070-```
1071-1072-#### Approach #2: Isolate state
1073-1074-A [`package.json`][] file can define the separate CommonJS and ES module entry
1075-points directly:
1076-1077-```json
1078-// ./node_modules/pkg/package.json
1079-{
1080-"type": "module",
1081-"exports": {
1082-"import": "./index.mjs",
1083-"require": "./index.cjs"
1084- }
1085-}
1086-```
1087-1088-This can be done if both the CommonJS and ES module versions of the package are
1089-equivalent, for example because one is the transpiled output of the other; and
1090-the package's management of state is carefully isolated (or the package is
1091-stateless).
1092-1093-The reason that state is an issue is because both the CommonJS and ES module
1094-versions of the package might get used within an application; for example, the
1095-user's application code could `import` the ES module version while a dependency
1096-`require`s the CommonJS version. If that were to occur, two copies of the
1097-package would be loaded in memory and therefore two separate states would be
1098-present. This would likely cause hard-to-troubleshoot bugs.
1099-1100-Aside from writing a stateless package (if JavaScript's `Math` were a package,
1101-for example, it would be stateless as all of its methods are static), there are
1102-some ways to isolate state so that it's shared between the potentially loaded
1103-CommonJS and ES module instances of the package:
1104-1105-1. If possible, contain all state within an instantiated object. JavaScript's
1106-`Date`, for example, needs to be instantiated to contain state; if it were a
1107- package, it would be used like this:
1108-1109-```js
1110-import Date from 'date';
1111-const someDate = new Date();
1112-// someDate contains state; Date does not
1113-```
1114-1115- The `new` keyword isn't required; a package's function can return a new
1116- object, or modify a passed-in object, to keep the state external to the
1117- package.
1118-1119-2. Isolate the state in one or more CommonJS files that are shared between the
1120- CommonJS and ES module versions of the package. For example, if the CommonJS
1121- and ES module entry points are `index.cjs` and `index.mjs`, respectively:
1122-1123-```cjs
1124-// ./node_modules/pkg/index.cjs
1125-const state = require('./state.cjs');
1126-module.exports.state = state;
1127-```
1128-1129-```js
1130-// ./node_modules/pkg/index.mjs
1131-import state from './state.cjs';
1132-export {
1133-state,
1134- };
1135-```
1136-1137- Even if `pkg` is used via both `require` and `import` in an application (for
1138- example, via `import` in application code and via `require` by a dependency)
1139- each reference of `pkg` will contain the same state; and modifying that
1140- state from either module system will apply to both.
1141-1142-Any plugins that attach to the package's singleton would need to separately
1143-attach to both the CommonJS and ES module singletons.
1144-1145-This approach is appropriate for any of the following use cases:
1146-1147-* The package is currently written in ES module syntax and the package author
1148- wants that version to be used wherever such syntax is supported.
1149-* The package is stateless or its state can be isolated without too much
1150- difficulty.
1151-* The package is unlikely to have other public packages that depend on it, or if
1152- it does, the package is stateless or has state that need not be shared between
1153- dependencies or with the overall application.
1154-1155-Even with isolated state, there is still the cost of possible extra code
1156-execution between the CommonJS and ES module versions of a package.
1157-1158-As with the previous approach, a variant of this approach not requiring
1159-conditional exports for consumers could be to add an export, e.g.
1160-`"./module"`, to point to an all-ES module-syntax version of the package:
1161-1162-```json
1163-// ./node_modules/pkg/package.json
1164-{
1165-"type": "module",
1166-"exports": {
1167-".": "./index.cjs",
1168-"./module": "./index.mjs"
1169- }
1170-}
1171-```
905+See [the package examples repository][] for details.
11729061173907## Node.js `package.json` field definitions
1174908@@ -1412,7 +1146,6 @@ Package imports permit mapping to external packages.
1412114614131147This field defines [subpath imports][] for the current package.
141411481415-[Babel]: https://babeljs.io/
14161149[CommonJS]: modules.md
14171150[Conditional exports]: #conditional-exports
14181151[Corepack]: corepack.md
@@ -1432,7 +1165,6 @@ This field defines [subpath imports][] for the current package.
14321165[`--experimental-default-type`]: cli.md#--experimental-default-typetype
14331166[`--no-addons` flag]: cli.md#--no-addons
14341167[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
1435-[`esm`]: https://github.com/standard-things/esm#readme
14361168[`package.json`]: #nodejs-packagejson-field-definitions
14371169[entry points]: #package-entry-points
14381170[folders as modules]: modules.md#folders-as-modules
@@ -1446,3 +1178,4 @@ This field defines [subpath imports][] for the current package.
14461178[supported package managers]: corepack.md#supported-package-managers
14471179[the dual CommonJS/ES module packages section]: #dual-commonjses-module-packages
14481180[the full specifier path]: esm.md#mandatory-file-extensions
1181+[the package examples repository]: https://github.com/nodejs/package-examples