| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -184,6 +184,19 @@ unspecified. | |||
| 184 | 184 | ||
| 185 | 185 | ### Package Entry Points | |
| 186 | 186 | ||
| 187 | + There are two fields that can define entry points for a package: `"main"` and | ||
| 188 | + `"exports"`. The `"main"` field is supported in all versions of Node.js, but its | ||
| 189 | + capabilities are limited: it only defines the main entry point of the package. | ||
| 190 | + The `"exports"` field, part of [Package Exports][], provides an alternative to | ||
| 191 | + `"main"` where the package main entry point can be defined while also | ||
| 192 | + encapsulating the package, preventing any other entry points besides those | ||
| 193 | + defined in `"exports"`. If package entry points are defined in both `"main"` and | ||
| 194 | + `"exports"`, the latter takes precedence in versions of Node.js that support | ||
| 195 | + `"exports"`. [Conditional Exports][] can also be used within `"exports"` to | ||
| 196 | + define different package entry points per environment. | ||
| 197 | + | ||
| 198 | + #### <code>package.json</code> <code>"main"</code> | ||
| 199 | + | ||
| 187 | 200 | The `package.json` `"main"` field defines the entry point for a package, | |
| 188 | 201 | whether the package is included into CommonJS via `require` or into an ES | |
| 189 | 202 | module via `import`. | |
@@ -219,7 +232,7 @@ The `"main"` field can point to exactly one file, regardless of whether the | |||
| 219 | 232 | package is referenced via `require` (in a CommonJS context) or `import` (in an | |
| 220 | 233 | ES module context). | |
| 221 | 234 | ||
| 222 | - ### Package Exports | ||
| 235 | + #### Package Exports | ||
| 223 | 236 | ||
| 224 | 237 | By default, all subpaths from a package can be imported (`import 'pkg/x.js'`). | |
| 225 | 238 | Custom subpath aliasing and encapsulation can be provided through the | |
@@ -423,9 +436,6 @@ thrown: | |||
| 423 | 436 | ||
| 424 | 437 | ### Dual CommonJS/ES Module Packages | |
| 425 | 438 | ||
| 426 | - _These patterns are currently experimental and only work under the | ||
| 427 | - `--experimental-conditional-exports` flag._ | ||
| 428 | - | ||
| 429 | 439 | Prior to the introduction of support for ES modules in Node.js, it was a common | |
| 430 | 440 | pattern for package authors to include both CommonJS and ES module JavaScript | |
| 431 | 441 | sources in their package, with `package.json` `"main"` specifying the CommonJS | |
@@ -434,61 +444,36 @@ This enabled Node.js to run the CommonJS entry point while build tools such as | |||
| 434 | 444 | bundlers used the ES module entry point, since Node.js ignored (and still | |
| 435 | 445 | ignores) the top-level `"module"` field. | |
| 436 | 446 | ||
| 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 | ||
| 447 | + Node.js can now run ES module entry points, and a package can contain both | ||
| 448 | + CommonJS and ES module entry points (either via separate specifiers such as | ||
| 449 | + `'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional | ||
| 450 | + Exports][] with the `--experimental-conditional-exports` flag). Unlike in the | ||
| 451 | + scenario where `"module"` is only used by bundlers, or ES module files are | ||
| 441 | 452 | transpiled into CommonJS on the fly before evaluation by Node.js, the files | |
| 442 | 453 | referenced by the ES module entry point are evaluated as ES modules. | |
| 443 | 454 | ||
| 444 | - #### Divergent Specifier Hazard | ||
| 455 | + #### Dual Package Hazard | ||
| 445 | 456 | ||
| 446 | 457 | When an application is using a package that provides both CommonJS and ES module | |
| 447 | 458 | 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()`. | ||
| 459 | + loaded. This potential comes from the fact that the `pkgInstance` created by | ||
| 460 | + `const pkgInstance = require('pkg')` is not the same as the `pkgInstance` | ||
| 461 | + created by `import pkgInstance from 'pkg'` (or an alternative main path like | ||
| 462 | + `'pkg/module'`). This is the “dual package hazard,” where two versions of the | ||
| 463 | + same package can be loaded within the same runtime environment. While it is | ||
| 464 | + unlikely that an application or package would intentionally load both versions | ||
| 465 | + directly, it is common for an application to load one version while a dependency | ||
| 466 | + of the application loads the other version. This hazard can happen because | ||
| 467 | + Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected | ||
| 468 | + behavior. | ||
| 469 | + | ||
| 470 | + If the package main export is a constructor, an `instanceof` comparison of | ||
| 471 | + instances created by the two versions returns `false`, and if the export is an | ||
| 472 | + object, properties added to one (like `pkgInstance.foo = 3`) are not present on | ||
| 473 | + the other. This differs from how `import` and `require` statements work in | ||
| 474 | + all-CommonJS or all-ES module environments, respectively, and therefore is | ||
| 475 | + surprising to users. It also differs from the behavior users are familiar with | ||
| 476 | + when using transpilation via tools like [Babel][] or [`esm`][]. | ||
| 492 | 477 | ||
| 493 | 478 | #### Writing Dual Packages While Avoiding or Minimizing Hazards | |
| 494 | 479 | ||
@@ -524,8 +509,14 @@ following conditions: | |||
| 524 | 509 | ||
| 525 | 510 | Write the package in CommonJS or transpile ES module sources into CommonJS, and | |
| 526 | 511 | 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`. | ||
| 512 | + [Conditional Exports][] via the `--experimental-conditional-exports` flag, the | ||
| 513 | + ES module wrapper is used for `import` and the CommonJS entry point for | ||
| 514 | + `require`. | ||
| 515 | + | ||
| 516 | + > Note: While `--experimental-conditional-exports` is flagged, a package | ||
| 517 | + > using this pattern will throw when loaded via `require()` in modern | ||
| 518 | + > Node.js, unless package consumers use the `--experimental-conditional-exports` | ||
| 519 | + > flag. | ||
| 529 | 520 | ||
| 530 | 521 | <!-- eslint-skip --> | |
| 531 | 522 | ```js | |
@@ -581,17 +572,37 @@ This approach is appropriate for any of the following use cases: | |||
| 581 | 572 | * The package stores internal state, and the package author would prefer not to | |
| 582 | 573 | refactor the package to isolate its state management. See the next section. | |
| 583 | 574 | ||
| 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 | ||
| 575 | + A variant of this approach not requiring `--experimental-conditional-exports` | ||
| 576 | + for consumers could be to add an export, e.g. `"./module"`, to point to an | ||
| 577 | + all-ES module-syntax version of the package. This could be used via `import | ||
| 586 | 578 | 'pkg/module'` by users who are certain that the CommonJS version will not be | |
| 587 | 579 | loaded anywhere in the application, such as by dependencies; or if the CommonJS | |
| 588 | 580 | version can be loaded but doesn’t affect the ES module version (for example, | |
| 589 | - because the package is stateless). | ||
| 581 | + because the package is stateless): | ||
| 582 | + | ||
| 583 | + <!-- eslint-skip --> | ||
| 584 | + ```js | ||
| 585 | + // ./node_modules/pkg/package.json | ||
| 586 | + { | ||
| 587 | + "type": "module", | ||
| 588 | + "main": "./index.cjs", | ||
| 589 | + "exports": { | ||
| 590 | + ".": "./index.cjs", | ||
| 591 | + "./module": "./wrapper.mjs" | ||
| 592 | + } | ||
| 593 | + } | ||
| 594 | + ``` | ||
| 595 | + | ||
| 596 | + If the `--experimental-conditional-exports` flag is dropped and therefore | ||
| 597 | + [Conditional Exports][] become available without a flag, this variant could be | ||
| 598 | + easily updated to use conditional exports by adding conditions to the `"."` | ||
| 599 | + path; while keeping `"./module"` for backward compatibility. | ||
| 590 | 600 | ||
| 591 | 601 | ##### Approach #2: Isolate State | |
| 592 | 602 | ||
| 593 | 603 | The most straightforward `package.json` would be one that defines the separate | |
| 594 | - CommonJS and ES module entry points directly: | ||
| 604 | + CommonJS and ES module entry points directly (requires | ||
| 605 | + `--experimental-conditional-exports`): | ||
| 595 | 606 | ||
| 596 | 607 | <!-- eslint-skip --> | |
| 597 | 608 | ```js | |
@@ -672,6 +683,28 @@ This approach is appropriate for any of the following use cases: | |||
| 672 | 683 | Even with isolated state, there is still the cost of possible extra code | |
| 673 | 684 | execution between the CommonJS and ES module versions of a package. | |
| 674 | 685 | ||
| 686 | + As with the previous approach, a variant of this approach not requiring | ||
| 687 | + `--experimental-conditional-exports` for consumers could be to add an export, | ||
| 688 | + e.g. `"./module"`, to point to an all-ES module-syntax version of the package: | ||
| 689 | + | ||
| 690 | + <!-- eslint-skip --> | ||
| 691 | + ```js | ||
| 692 | + // ./node_modules/pkg/package.json | ||
| 693 | + { | ||
| 694 | + "type": "module", | ||
| 695 | + "main": "./index.cjs", | ||
| 696 | + "exports": { | ||
| 697 | + ".": "./index.cjs", | ||
| 698 | + "./module": "./index.mjs" | ||
| 699 | + } | ||
| 700 | + } | ||
| 701 | + ``` | ||
| 702 | + | ||
| 703 | + If the `--experimental-conditional-exports` flag is dropped and therefore | ||
| 704 | + [Conditional Exports][] become available without a flag, this variant could be | ||
| 705 | + easily updated to use conditional exports by adding conditions to the `"."` | ||
| 706 | + path; while keeping `"./module"` for backward compatibility. | ||
| 707 | + | ||
| 675 | 708 | ## `import` Specifiers | |
| 676 | 709 | ||
| 677 | 710 | ### Terminology | |
@@ -1363,6 +1396,7 @@ success! | |||
| 1363 | 1396 | [ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md | |
| 1364 | 1397 | [ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration | |
| 1365 | 1398 | [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md | |
| 1399 | + [Package Exports]: #esm_package_exports | ||
| 1366 | 1400 | [Terminology]: #esm_terminology | |
| 1367 | 1401 | [WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script | |
| 1368 | 1402 | [`"exports"` field]: #esm_package_exports | |
| Back | FazBrowse Home | New Git URL |
0 commit comments