| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent bb57e90 commit 8e76cc6
1 file changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -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 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments