@@ -219,50 +219,6 @@ The `"main"` field can point to exactly one file, regardless of whether the
219
219
package is referenced via ` require ` (in a CommonJS context) or ` import ` (in an
220
220
ES module context).
221
221
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
-
266
222
### Package Exports
267
223
268
224
By default, all subpaths from a package can be imported (` import 'pkg/x.js' ` ).
@@ -422,9 +378,9 @@ can be written:
422
378
}
423
379
```
424
380
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.
428
384
429
385
<!-- eslint-skip -->
430
386
``` js
@@ -465,6 +421,257 @@ thrown:
465
421
}
466
422
```
467
423
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
+
468
675
## `import` Specifiers
469
676
470
677
### Terminology
@@ -1152,6 +1359,7 @@ $ node --experimental-modules --es-module-specifier-resolution=node index
1152
1359
success!
1153
1360
` ` `
1154
1361
1362
+ [Babel]: https://babeljs.io/
1155
1363
[CommonJS]: modules.html
1156
1364
[Conditional Exports]: #esm_conditional_exports
1157
1365
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
@@ -1160,13 +1368,13 @@ success!
1160
1368
[Terminology]: #esm_terminology
1161
1369
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
1162
1370
[` data: ` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
1371
+ [` esm` ]: https://github.com/standard-things/esm#readme
1163
1372
[` export ` ]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
1164
1373
[` import ()` ]: #esm_import-expressions
1165
1374
[` import .meta .url ` ]: #esm_import_meta
1166
1375
[` import ` ]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
1167
1376
[` module .createRequire ()` ]: modules.html#modules_module_createrequire_filename
1168
1377
[` module .syncBuiltinESMExports ()` ]: modules.html#modules_module_syncbuiltinesmexports
1169
- [package exports]: #esm_package_exports
1170
1378
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
1171
1379
[special scheme]: https://url.spec.whatwg.org/#special-scheme
1172
1380
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
0 commit comments