@@ -219,50 +219,6 @@ 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.
262-
263- To define separate package entry points for use by ` require ` and by ` import ` ,
264- see [ Conditional Exports] [ ] .
265-
266222### Package Exports
267223
268224By default, all subpaths from a package can be imported (` import 'pkg/x.js' ` ).
@@ -422,9 +378,9 @@ can be written:
422378}
423379```
424380
425- When using conditional exports , the rule is that all keys in the object mapping
426- must not start with a ` "." ` otherwise they would be indistinguishable from
427- exports subpaths.
381+ When using [ Conditional Exports ] [ ] , the rule is that all keys in the object
382+ mapping must not start with a ` "." ` otherwise they would be indistinguishable
383+ from exports subpaths.
428384
429385<!-- eslint-skip -->
430386``` js
@@ -465,6 +421,257 @@ thrown:
465421}
466422```
467423
424+ ### Dual CommonJS/ES Module Packages
425+
426+ _ These patterns are currently experimental and only work under the
427+ ` --experimental-conditional-exports ` flag._
428+
429+ Prior to the introduction of support for ES modules in Node.js, it was a common
430+ pattern for package authors to include both CommonJS and ES module JavaScript
431+ sources in their package, with ` package.json ` ` "main" ` specifying the CommonJS
432+ entry point and ` package.json ` ` "module" ` specifying the ES module entry point.
433+ This enabled Node.js to run the CommonJS entry point while build tools such as
434+ bundlers used the ES module entry point, since Node.js ignored (and still
435+ ignores) the top-level ` "module" ` field.
436+
437+ Node.js can now run ES module entry points, and using [ Conditional Exports] [ ]
438+ with the ` --experimental-conditional-exports ` flag it is possible to define
439+ separate package entry points for CommonJS and ES module consumers. Unlike in
440+ the scenario where ` "module" ` is only used by bundlers, or ES module files are
441+ transpiled into CommonJS on the fly before evaluation by Node.js, the files
442+ referenced by the ES module entry point are evaluated as ES modules.
443+
444+ #### Divergent Specifier Hazard
445+
446+ When an application is using a package that provides both CommonJS and ES module
447+ sources, there is a risk of certain bugs if both versions of the package get
448+ loaded (for example, because one version is imported by the application and the
449+ other version is required by one of the application’s dependencies). Such a
450+ package might look like this:
451+
452+ <!-- eslint-skip -->
453+ ``` js
454+ // ./node_modules/pkg/package.json
455+ {
456+ " type" : " module" ,
457+ " main" : " ./pkg.cjs" ,
458+ " exports" : {
459+ " require" : " ./pkg.cjs" ,
460+ " default" : " ./pkg.mjs"
461+ }
462+ }
463+ ```
464+
465+ In this example, ` require('pkg') ` always resolves to ` pkg.cjs ` , including in
466+ versions of Node.js where ES modules are unsupported. In Node.js where ES
467+ modules are supported, ` import 'pkg' ` references ` pkg.mjs ` .
468+
469+ The potential for bugs comes from the fact that the ` pkg ` created by `const pkg
470+ = require('pkg')` is not the same as the ` pkg` created by ` import pkg from
471+ 'pkg'` . This is the “divergent specifier hazard,” where one specifer ( ` 'pkg'`)
472+ resolves to separate files (` pkg.cjs ` and ` pkg.mjs ` ) in separate module systems,
473+ yet both versions might get loaded within an application because Node.js
474+ supports intermixing CommonJS and ES modules.
475+
476+ If the export is a constructor, an ` instanceof ` comparison of instances created
477+ by the two returns ` false ` , and if the export is an object, properties added to
478+ one (like ` pkg.foo = 3 ` ) are not present on the other. This differs from how
479+ ` import ` and ` require ` statements work in all-CommonJS or all-ES module
480+ environments, respectively, and therefore is surprising to users. It also
481+ differs from the behavior users are familiar with when using transpilation via
482+ tools like [ Babel] [ ] or [ ` esm ` ] [ ] .
483+
484+ Even if the user consistently uses either ` require ` or ` import ` to refer to
485+ ` pkg ` , if any dependencies of the application use the other method the hazard is
486+ still present.
487+
488+ The ` --experimental-conditional-exports ` flag should be set for modern Node.js
489+ for this behavior to work out. If it is not set, only the ES module version can
490+ be used in modern Node.js and the package will throw when accessed via
491+ ` require() ` .
492+
493+ #### Writing Dual Packages While Avoiding or Minimizing Hazards
494+
495+ First, the hazard described in the previous section occurs when a package
496+ contains both CommonJS and ES module sources and both sources are provided for
497+ use in Node.js, either via separate main entry points or exported paths. A
498+ package could instead be written where any version of Node.js receives only
499+ CommonJS sources, and any separate ES module sources the package may contain
500+ could be intended only for other environments such as browsers. Such a package
501+ would be usable by any version of Node.js, since ` import ` can refer to CommonJS
502+ files; but it would not provide any of the advantages of using ES module syntax.
503+
504+ A package could also switch from CommonJS to ES module syntax in a breaking
505+ change version bump. This has the obvious disadvantage that the newest version
506+ of the package would only be usable in ES module-supporting versions of Node.js.
507+
508+ Every pattern has tradeoffs, but there are two broad approaches that satisfy the
509+ following conditions:
510+
511+ 1 . The package is usable via both ` require ` and ` import ` .
512+ 1 . The package is usable in both current Node.js and older versions of Node.js
513+ that lack support for ES modules.
514+ 1 . The package main entry point, e.g. ` 'pkg' ` can be used by both ` require ` to
515+ resolve to a CommonJS file and by ` import ` to resolve to an ES module file.
516+ (And likewise for exported paths, e.g. ` 'pkg/feature' ` .)
517+ 1 . The package provides named exports, e.g. ` import { name } from 'pkg' ` rather
518+ than ` import pkg from 'pkg'; pkg.name ` .
519+ 1 . The package is potentially usable in other ES module environments such as
520+ browsers.
521+ 1 . The hazards described in the previous section are avoided or minimized.
522+
523+ ##### Approach #1 : Use an ES Module Wrapper
524+
525+ Write the package in CommonJS or transpile ES module sources into CommonJS, and
526+ create an ES module wrapper file that defines the named exports. Using
527+ [ Conditional Exports] [ ] , the ES module wrapper is used for ` import ` and the
528+ CommonJS entry point for ` require ` .
529+
530+ <!-- eslint-skip -->
531+ ``` js
532+ // ./node_modules/pkg/package.json
533+ {
534+ " type" : " module" ,
535+ " main" : " ./index.cjs" ,
536+ " exports" : {
537+ " require" : " ./index.cjs" ,
538+ " default" : " ./wrapper.mjs"
539+ }
540+ }
541+ ```
542+
543+ ``` js
544+ // ./node_modules/pkg/index.cjs
545+ exports .name = ' value' ;
546+ ```
547+
548+ ``` js
549+ // ./node_modules/pkg/wrapper.mjs
550+ import cjsModule from ' ./index.cjs' ;
551+ export const name = cjsModule .name ;
552+ ```
553+
554+ In this example, the ` name ` from ` import { name } from 'pkg' ` is the same
555+ singleton as the ` name ` from ` const { name } = require('pkg') ` . Therefore ` === `
556+ returns ` true ` when comparing the two ` name ` s and the divergent specifier hazard
557+ is avoided.
558+
559+ If the module is not simply a list of named exports, but rather contains a
560+ unique function or object export like ` module.exports = function () { ... } ` ,
561+ or if support in the wrapper for the ` import pkg from 'pkg' ` pattern is desired,
562+ then the wrapper would instead be written to export the default optionally
563+ along with any named exports as well:
564+
565+ ``` js
566+ import cjsModule from ' ./index.cjs' ;
567+ export const name = cjsModule .name ;
568+ export default cjsModule ;
569+ ```
570+
571+ This approach is appropriate for any of the following use cases:
572+ * The package is currently written in CommonJS and the author would prefer not
573+ to refactor it into ES module syntax, but wishes to provide named exports for
574+ ES module consumers.
575+ * The package has other packages that depend on it, and the end user might
576+ install both this package and those other packages. For example a ` utilities `
577+ package is used directly in an application, and a ` utilities-plus ` package
578+ adds a few more functions to ` utilities ` . Because the wrapper exports
579+ underlying CommonJS files, it doesn’t matter if ` utilities-plus ` is written in
580+ CommonJS or ES module syntax; it will work either way.
581+ * The package stores internal state, and the package author would prefer not to
582+ refactor the package to isolate its state management. See the next section.
583+
584+ A variant of this approach would add an export, e.g. ` "./module" ` , to point to
585+ an all-ES module-syntax version the package. This could be used via `import
586+ 'pkg/module'` by users who are certain that the CommonJS version will not be
587+ loaded anywhere in the application, such as by dependencies; or if the CommonJS
588+ version can be loaded but doesn’t affect the ES module version (for example,
589+ because the package is stateless).
590+
591+ ##### Approach #2 : Isolate State
592+
593+ The most straightforward ` package.json ` would be one that defines the separate
594+ CommonJS and ES module entry points directly:
595+
596+ <!-- eslint-skip -->
597+ ``` js
598+ // ./node_modules/pkg/package.json
599+ {
600+ " type" : " module" ,
601+ " main" : " ./index.cjs" ,
602+ " exports" : {
603+ " require" : " ./index.cjs" ,
604+ " default" : " ./index.mjs"
605+ }
606+ }
607+ ```
608+
609+ This can be done if both the CommonJS and ES module versions of the package are
610+ equivalent, for example because one is the transpiled output of the other; and
611+ the package’s management of state is carefully isolated (or the package is
612+ stateless).
613+
614+ The reason that state is an issue is because both the CommonJS and ES module
615+ versions of the package may get used within an application; for example, the
616+ user’s application code could ` import ` the ES module version while a dependency
617+ ` require ` s the CommonJS version. If that were to occur, two copies of the
618+ package would be loaded in memory and therefore two separate states would be
619+ present. This would likely cause hard-to-troubleshoot bugs.
620+
621+ Aside from writing a stateless package (if JavaScript’s ` Math ` were a package,
622+ for example, it would be stateless as all of its methods are static), there are
623+ some ways to isolate state so that it’s shared between the potentially loaded
624+ CommonJS and ES module instances of the package:
625+
626+ 1 . If possible, contain all state within an instantiated object. JavaScript’s
627+ ` Date ` , for example, needs to be instantiated to contain state; if it were a
628+ package, it would be used like this:
629+
630+ ``` js
631+ import date from ' date' ;
632+ const someDate = new date ();
633+ // someDate contains state; date does not
634+ ```
635+
636+ The ` new` keyword isn’t required; a package ’s function can return a new
637+ object, or modify a passed-in object, to keep the state external to the
638+ package.
639+
640+ 1. Isolate the state in one or more CommonJS files that are shared between the
641+ CommonJS and ES module versions of the package. For example, if the CommonJS
642+ and ES module entry points are `index.cjs` and `index.mjs`, respectively:
643+
644+ ```js
645+ // ./node_modules/pkg/index.cjs
646+ const state = require (' ./state.cjs' );
647+ module.exports.state = state;
648+ ```
649+
650+ ```js
651+ // ./node_modules/pkg/index.mjs
652+ export state from './state.cjs';
653+ ```
654+
655+ Even if `pkg` is used via both `require` and `import` in an application (for
656+ example, via `import` in application code and via `require` by a dependency)
657+ each reference of `pkg` will contain the same state; and modifying that
658+ state from either module system will apply to both.
659+
660+ Any plugins that attach to the package’s singleton would need to separately
661+ attach to both the CommonJS and ES module singletons.
662+
663+ This approach is appropriate for any of the following use cases:
664+ * The package is currently written in ES module syntax and the package author
665+ wants that version to be used wherever such syntax is supported.
666+ * The package is stateless or its state can be isolated without too much
667+ difficulty.
668+ * The package is unlikely to have other public packages that depend on it, or if
669+ it does, the package is stateless or has state that need not be shared between
670+ dependencies or with the overall application.
671+
672+ Even with isolated state, there is still the cost of possible extra code
673+ execution between the CommonJS and ES module versions of a package.
674+
468675## <code>import</code> Specifiers
469676
470677### Terminology
@@ -1153,6 +1360,7 @@ $ node --experimental-modules --es-module-specifier-resolution=node index
11531360success!
11541361` ` `
11551362
1363+ [Babel]: https://babeljs.io/
11561364[CommonJS]: modules.html
11571365[Conditional Exports]: #esm_conditional_exports
11581366[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
@@ -1161,13 +1369,13 @@ success!
11611369[Terminology]: #esm_terminology
11621370[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
11631371[` data: ` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
1372+ [` esm` ]: https://github.com/standard-things/esm#readme
11641373[` export ` ]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
11651374[` import ()` ]: #esm_import-expressions
11661375[` import .meta .url ` ]: #esm_import_meta
11671376[` import ` ]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
11681377[` module .createRequire ()` ]: modules.html#modules_module_createrequire_filename
11691378[` module .syncBuiltinESMExports ()` ]: modules.html#modules_module_syncbuiltinesmexports
1170- [package exports]: #esm_package_exports
11711379[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
11721380[special scheme]: https://url.spec.whatwg.org/#special-scheme
11731381[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
0 commit comments