Skip to content

Commit b4c186a

Browse files
joyeecheungtpoisseau
authored andcommitted
doc: move dual package shipping docs to separate repo
Refs: nodejs/admin#917 Refs: nodejs/package-examples#1 Refs: nodejs#54648 PR-URL: nodejs#55444 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 00c6652 commit b4c186a

File tree

1 file changed

+2
-269
lines changed

1 file changed

+2
-269
lines changed

doc/api/packages.md

+2-269
Original file line numberDiff line numberDiff line change
@@ -902,273 +902,7 @@ $ node other.js
902902

903903
## Dual CommonJS/ES module packages
904904

905-
<!-- 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.
1172906

1173907
## Node.js `package.json` field definitions
1174908

@@ -1412,7 +1146,6 @@ Package imports permit mapping to external packages.
14121146

14131147
This field defines [subpath imports][] for the current package.
14141148

1415-
[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

0 commit comments

Comments
 (0)