@@ -219,46 +219,8 @@ The `"main"` field can point to exactly one file, regardless of whether the
219219package is referenced via ` require ` (in a CommonJS context) or ` import ` (in an
220220ES module context).
221221
222- #### Compatibility with CommonJS-Only Versions of Node.js
223-
224- Prior to the introduction of support for ES modules in Node.js, it was a common
225- pattern for package authors to include both CommonJS and ES module JavaScript
226- sources in their package, with ` package.json ` ` "main" ` specifying the CommonJS
227- entry point and ` package.json ` ` "module" ` specifying the ES module entry point.
228- This enabled Node.js to run the CommonJS entry point while build tools such as
229- bundlers used the ES module entry point, since Node.js ignored (and still
230- ignores) ` "module" ` .
231-
232- Node.js can now run ES module entry points, but it remains impossible for a
233- package to define separate CommonJS and ES module entry points. This is for good
234- reason: the ` pkg ` variable created from ` import pkg from 'pkg' ` is not the same
235- singleton as the ` pkg ` variable created from ` const pkg = require('pkg') ` , so if
236- both are referenced within the same app (including dependencies), unexpected
237- behavior might occur.
238-
239- There are two general approaches to addressing this limitation while still
240- publishing a package that contains both CommonJS and ES module sources:
241-
242- 1 . Document a new ES module entry point that’s not the package ` "main" ` , e.g.
243- ` import pkg from 'pkg/module.mjs' ` (or ` import 'pkg/esm' ` , if using [ package
244- exports] [ ] ). The package ` "main" ` would still point to a CommonJS file, and
245- thus the package would remain compatible with older versions of Node.js that
246- lack support for ES modules.
247-
248- 1 . Switch the package ` "main" ` entry point to an ES module file as part of a
249- breaking change version bump. This version and above would only be usable on
250- ES module-supporting versions of Node.js. If the package still contains a
251- CommonJS version, it would be accessible via a path within the package, e.g.
252- ` require('pkg/commonjs') ` ; this is essentially the inverse of the previous
253- approach. Package consumers who are using CommonJS-only versions of Node.js
254- would need to update their code from ` require('pkg') ` to e.g.
255- ` require('pkg/commonjs') ` .
256-
257- Of course, a package could also include only CommonJS or only ES module sources.
258- An existing package could make a semver major bump to an ES module-only version,
259- that would only be supported in ES module-supporting versions of Node.js (and
260- other runtimes). New packages could be published containing only ES module
261- sources, and would be compatible only with ES module-supporting runtimes.
222+ To define separate package entry points for use by ` require ` and by ` import ` ,
223+ see [ Conditional Exports] [ ] .
262224
263225### Package Exports
264226
@@ -395,6 +357,269 @@ package in use in an application, which can cause a number of bugs.
395357Other conditions such as ` "browser" ` , ` "electron" ` , ` "deno" ` , ` "react-native" `
396358etc. could be defined in other runtimes or tools.
397359
360+ ### Dual CommonJS/ES Module Packages and the Divergent Specifier Hazard
361+
362+ Prior to the introduction of support for ES modules in Node.js, it was a common
363+ pattern for package authors to include both CommonJS and ES module JavaScript
364+ sources in their package, with ` package.json ` ` "main" ` specifying the CommonJS
365+ entry point and ` package.json ` ` "module" ` specifying the ES module entry point.
366+ This enabled Node.js to run the CommonJS entry point while build tools such as
367+ bundlers used the ES module entry point, since Node.js ignored (and still
368+ ignores) the top-level ` "module" ` field.
369+
370+ Node.js can now run ES module entry points, and using [ conditional exports] [ ] it
371+ is possible to define separate package entry points for CommonJS and ES module
372+ consumers. Unlike in the scenario where ` "module" ` is only used by bundlers, or
373+ ES module files are transpiled into CommonJS on the fly before evaluation by
374+ Node.js, the files referenced by the ES module entry point are evaluated as ES
375+ modules. When a specifier such as ` 'pkg' ` resolves to different files when
376+ referenced via ` require ` and ` import ` , there is a risk of certain bugs that only
377+ occur under these conditions. This risk is the divergent specifier hazard, and
378+ it is only possible when a package entry point or exported path is defined via
379+ [ conditional exports] [ ] to point to different files for CommonJS and ES module
380+ consumers. For example:
381+
382+ <!-- eslint-skip -->
383+ ``` js
384+ // ./node_modules/pkg/package.json
385+ {
386+ " type" : " module" ,
387+ " main" : " ./pkg.cjs" ,
388+ " exports" : {
389+ " ." : {
390+ " module" : " ./pkg.mjs" ,
391+ " node" : " ./pkg.cjs"
392+ }
393+ }
394+ }
395+ ```
396+
397+ In this example, ` require('pkg') ` always resolves to ` pkg.cjs ` , including in
398+ versions of Node.js where ES modules are unsupported. In Node.js where ES
399+ modules are supported, ` import 'pkg' ` references ` pkg.mjs ` .
400+
401+ The hazard is that the ` pkg ` created by ` const pkg = require('pkg') ` is not the
402+ same as the ` pkg ` created by ` import pkg from 'pkg' ` . An ` instanceof ` comparison
403+ of the two returns ` false ` , and properties added to one (like ` pkg.foo = 3 ` ) are
404+ not present on the other. This differs from how ` import ` and ` require `
405+ statements work in all-CommonJS or all-ES module environments, respectively, and
406+ therefore is surprising to users.
407+
408+ Essentially, the ` pkg ` in each environment is a separate _ singleton._ Whereas in
409+ one ES module file you can have ` import a from 'pkg' ` and in another you can
410+ write ` import b from 'pkg' ` and ` a instanceof b ` returns ` true ` , that would not
411+ be the case for ` const b = require('pkg') ` .
412+
413+ The ES module syntax that users have been writing for use in Node.js via Babel
414+ or [ ` esm ` ] ( https://github.com/standard-things/esm#readme ) for the last several
415+ years does not behave this way, because Babel or ` esm ` have been transpiling
416+ everything into CommonJS before evaluation. In the previous example, `import a
417+ from 'pkg'` would be converted into ` const a = require('pkg')` and then ` a
418+ instanceof b` (where ` b` comes from ` const b = require('pkg')`) would return
419+ ` true ` .
420+
421+ Looked at another way, ` import pkg from 'pkg' ` is a shorthand for `import pkg
422+ from './node_modules/pkg/pkg.mjs'` and ` const pkg = require('pkg')` is a
423+ shorthand for ` const pkg = require('./node_modules/pkg/pkg.cjs') ` . Because the
424+ file paths in the two statements are different, the two ` pkg ` singletons are
425+ different.
426+
427+ It’s not enough to refer to ` 'pkg' ` using only ` require ` or only ` import ` within
428+ an application; all of that application’s dependencies also need to use the same
429+ method or the hazard is present. For example, if an application uses ` pkg ` and
430+ ` pkg-plugin ` , and the application references ` pkg ` via ` import ` and ` pkg-plugin `
431+ references ` pkg ` via ` require ` , both versions of ` pkg ` are therefore loaded.
432+
433+ #### Preventing the Divergent Specifier Hazard
434+
435+ To avoid the bugs that can occur when the divergent specifier hazard is present,
436+ one approach is to simply prevent the conditions under which the hazard can
437+ occur. This can be achieved in the following ways:
438+
439+ 1 . Publish a package that doesn’t use [ conditional exports] [ ] ; the package entry
440+ point and all exported paths would not resolve differently based on whether
441+ they’re referenced via ` require ` or ` import ` . This would therefore mean that
442+ if the export is CommonJS, it would lack the benefits of ES module syntax
443+ such as the ability to potentially run unmodified in browser environments;
444+ and if the export is an ES module, it would not be able to run in older
445+ versions of Node.js that lack support for ES module syntax.
446+
447+ 1 . Define separate exported paths for separate environments, for example:
448+
449+ ``` json
450+ {
451+ "type" : " module" ,
452+ "main" : " ./pkg.cjs" ,
453+ "exports" : {
454+ "." : " ./pkg.cjs" ,
455+ "./module" : " ./pkg.mjs"
456+ }
457+ }
458+ ```
459+
460+ In this example, the package `"main"` would point to a CommonJS file, and
461+ thus the package would remain compatible with older versions of Node.js that
462+ lack support for ES modules; but the ES module version of the package is
463+ available for supported environments via `'pkg/module'`. Users would need to
464+ refer to the package’s documentation (or the `package.json "module"` field,
465+ if the package supports bundlers) to know to use `'pkg/module'` with
466+ `import`; using `'pkg'` with `import` would cause the CommonJS version to be
467+ loaded.
468+
469+ 1 . Change the package `"main"` entry point to point to an ES module file as part
470+ of a breaking change version bump. This version and above would only be
471+ usable on ES module-supporting versions of Node.js. If the package still
472+ contains a CommonJS version, it would be accessible via a path within the
473+ package, e.g. `require('pkg/commonjs')`; this is essentially the inverse of
474+ the previous approach. Package consumers who are using CommonJS-only versions
475+ of Node.js would need to update their code from `require('pkg')` to e.g.
476+ `require('pkg/commonjs')`.
477+
478+ #### Avoiding Bugs Caused by the Divergent Specifier Hazard
479+
480+ Package authors may wish to make available at the same specifier both the
481+ CommonJS and ES module versions of a dual CommonJS/ES module package. This would
482+ allow consumers to `require('pkg')` and `import 'pkg'` and receive the CommonJS
483+ and ES module versions, respectively. When authoring a package that provides
484+ this, care must be taken to avoid the bugs which may occur due to the divergent
485+ specifier hazard.
486+
487+ 1 . Write the package in CommonJS or transpile the package into CommonJS, and
488+ create an ES module wrapper file to provide support for named exports (for
489+ example `import { name } from 'pkg'` instead of `import pkg from 'pkg';
490+ pkg.name`). Using conditional exports, the ES module wrapper is used for
491+ `import` and the CommonJS entry point for `require`.
492+
493+ _pkg/package.json_
494+ ```json
495+ {
496+ "exports" : {
497+ "." : {
498+ "module" : " ./wrapper.mjs" ,
499+ "node" : " ./index.cjs"
500+ }
501+ }
502+ }
503+ ```
504+
505+ _pkg/index.cjs_
506+ ```js
507+ exports.name = 'value';
508+ ```
509+
510+ _pkg/wrapper.mjs_
511+ ```js
512+ import cjsModule from './index.cjs';
513+ export const name = cjsModule.name;
514+ ```
515+
516+ In this example, the `name` from `import { name } from 'pkg'` is the same
517+ singleton as the `name` from `const { name } = require('pkg')`. Therefore
518+ `instanceof` returns `true` when comparing the two `name`s and the hazard is
519+ avoided. This wrapper approach, however, means that mostly CommonJS files are
520+ loaded for the package, even in Node.js ES module environments; and a
521+ separate version of the package would need to be created for environments
522+ such as browsers that don’t support CommonJS.
523+
524+ 1 . If possible, write a package that is stateless or stores its state outside of
525+ the package. A package that is entirely static methods, for example, is
526+ stateless; if JavaScript’s `Math` were a package, it would be stateless as
527+ all of its methods (`max`, etc.) are static. JavaScript’s `Date` needs to be
528+ instantiated to contain state; if it were a package, it would be used like
529+ this:
530+
531+ ```js
532+ import date from 'date';
533+ const someDate = new date();
534+ // someDate contains state; date does not
535+ ```
536+
537+ Since the state is contained within an object instantiated from the package
538+ (`someDate` in this example) rather than the package itself, an application
539+ using this package would pass around references to the instantiated object
540+ when an object with that state is desired. In other words, this file would
541+ `export` `someDate`, and other files in the application would `import` that
542+ rather than the package `date`, unless those other files wanted to create new
543+ objects with separate states. Note also that `new` isn’t required; a
544+ package’s function can also return a new object, or modify a passed-in
545+ object, to keep the state external to the package.
546+
547+ 1 . Write a package where the state is isolated in one or more CommonJS files
548+ that are shared between the CommonJS and ES module versions of the package.
549+ This is essentially a combination of the previous two approaches. For
550+ example, if the CommonJS and ES module entry points are `index.cjs` and
551+ `index.mjs`, respectively:
552+
553+ _pkg/state.cjs_
554+ ```js
555+ module.exports = {
556+ cache: []
557+ };
558+ ```
559+
560+ _pkg/index.cjs_
561+ ```js
562+ const state = require('./state.cjs');
563+ module.exports.state = state;
564+ ```
565+
566+ _pkg/index.mjs_
567+ ```js
568+ export * as state from './state.cjs';
569+ ```
570+
571+ Even if `pkg` is used via both `require` and `import` in an application (for
572+ example, via `import` in application code and via `require` by a dependency)
573+ each reference of `pkg` will contain the same state; and modifying that
574+ state from either module system will apply to both.
575+
576+ A package utilizing this pattern would not be usable as is in browsers or
577+ other environments that lack support for CommonJS.
578+
579+ 1 . Write a package where state is stored globally. This is similar to the
580+ previous approach, but instead of isolating state within a shared CommonJS
581+ file it is attached to the global object, e.g.
582+ `globalThis[Symbol.for('[email protected] ')]`. For example, if the CommonJS and ES 583+ module entry points are `index.cjs` and `index.mjs`, respectively:
584+
585+ _pkg/index.cjs_
586+ ```js
587+ const state = globalThis[Symbol.for('[email protected] ')]; 588+ module.exports.state = state;
589+ ```
590+
591+ _pkg/index.mjs_
592+ ```js
593+ export const state = globalThis[Symbol.for('[email protected] ')]; 594+ ```
595+
596+ Like the previous approach, if `pkg` is used via both `require` and `import`
597+ in an application (for example, via `import` in application code and via
598+ `require` by a dependency) each reference of `pkg` will contain the same
599+ state; and modifying that state from either module system will apply to both.
600+
601+ This has the disadvantage of polluting the global namespace, but it is
602+ compatible with non-CommonJS environments such as browsers.
603+
604+ For all approaches other than the first, an `instanceof` comparison would return
605+ `false` when comparing the CommonJS and ES module versions of such packages, or
606+ objects instantiated from each version; end users of such packages need to be
607+ aware of this and avoid comparing identity in mixed-module system environments,
608+ or check against both versions:
609+
610+ ```js
611+ import { createRequire } from 'module';
612+ const require = createRequire(import.meta.url);
613+
614+ import pkgEsModule from 'pkg';
615+ const pkgCommonJs = require('pkg');
616+
617+ export const instanceofPkg = (instantiatedPkg) => {
618+ return instantiatedPkg instanceof pkgEsModule ||
619+ instantiatedPkg instanceof pkgCommonJs;
620+ };
621+ ```
622+
398623## <code >import</code > Specifiers
399624
400625### Terminology
@@ -1074,7 +1299,7 @@ success!
10741299[` import ` ]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
10751300[` module .createRequire ()` ]: modules.html#modules_module_createrequire_filename
10761301[` module .syncBuiltinESMExports ()` ]: modules.html#modules_module_syncbuiltinesmexports
1077- [package exports]: #esm_package_exports
1302+ [conditional exports]: #esm_conditional_exports
10781303[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
10791304[special scheme]: https://url.spec.whatwg.org/#special-scheme
10801305[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
0 commit comments