@@ -902,273 +902,7 @@ $ node other.js |
902 | 902 |
|
903 | 903 | ## Dual CommonJS/ES module packages |
904 | 904 |
|
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. |
1172 | 906 |
|
1173 | 907 | ## Node.js `package.json` field definitions |
1174 | 908 |
|
@@ -1412,7 +1146,6 @@ Package imports permit mapping to external packages. |
1412 | 1146 |
|
1413 | 1147 | This field defines [subpath imports][] for the current package. |
1414 | 1148 |
|
1415 | | -[Babel]: https://babeljs.io/ |
1416 | 1149 | [CommonJS]: modules.md |
1417 | 1150 | [Conditional exports]: #conditional-exports |
1418 | 1151 | [Corepack]: corepack.md |
@@ -1432,7 +1165,6 @@ This field defines [subpath imports][] for the current package. |
1432 | 1165 | [`--experimental-default-type`]: cli.md#--experimental-default-typetype |
1433 | 1166 | [`--no-addons` flag]: cli.md#--no-addons |
1434 | 1167 | [`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported |
1435 | | -[`esm`]: https://github.com/standard-things/esm#readme |
1436 | 1168 | [`package.json`]: #nodejs-packagejson-field-definitions |
1437 | 1169 | [entry points]: #package-entry-points |
1438 | 1170 | [folders as modules]: modules.md#folders-as-modules |
@@ -1446,3 +1178,4 @@ This field defines [subpath imports][] for the current package. |
1446 | 1178 | [supported package managers]: corepack.md#supported-package-managers |
1447 | 1179 | [the dual CommonJS/ES module packages section]: #dual-commonjses-module-packages |
1448 | 1180 | [the full specifier path]: esm.md#mandatory-file-extensions |
| 1181 | +[the package examples repository]: https://github.com/nodejs/package-examples |