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