| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -178,6 +178,19 @@ unspecified. | |||
| 178 | 178 | ||
| 179 | 179 | ### Package Entry Points | |
| 180 | 180 | ||
| 181 | + There are two fields that can define entry points for a package: `"main"` and | ||
| 182 | + `"exports"`. The `"main"` field is supported in all versions of Node.js, but its | ||
| 183 | + capabilities are limited: it only defines the main entry point of the package. | ||
| 184 | + The `"exports"` field, part of [Package Exports][], provides an alternative to | ||
| 185 | + `"main"` where the package main entry point can be defined while also | ||
| 186 | + encapsulating the package, preventing any other entry points besides those | ||
| 187 | + defined in `"exports"`. If package entry points are defined in both `"main"` and | ||
| 188 | + `"exports"`, the latter takes precedence in versions of Node.js that support | ||
| 189 | + `"exports"`. [Conditional Exports][] can also be used within `"exports"` to | ||
| 190 | + define different package entry points per environment. | ||
| 191 | + | ||
| 192 | + #### <code>package.json</code> <code>"main"</code> | ||
| 193 | + | ||
| 181 | 194 | The `package.json` `"main"` field defines the entry point for a package, | |
| 182 | 195 | whether the package is included into CommonJS via `require` or into an ES | |
| 183 | 196 | module via `import`. | |
@@ -213,7 +226,7 @@ The `"main"` field can point to exactly one file, regardless of whether the | |||
| 213 | 226 | package is referenced via `require` (in a CommonJS context) or `import` (in an | |
| 214 | 227 | ES module context). | |
| 215 | 228 | ||
| 216 | - ### Package Exports | ||
| 229 | + #### Package Exports | ||
| 217 | 230 | ||
| 218 | 231 | By default, all subpaths from a package can be imported (`import 'pkg/x.js'`). | |
| 219 | 232 | Custom subpath aliasing and encapsulation can be provided through the | |
@@ -417,9 +430,6 @@ thrown: | |||
| 417 | 430 | ||
| 418 | 431 | ### Dual CommonJS/ES Module Packages | |
| 419 | 432 | ||
| 420 | - _These patterns are currently experimental and only work under the | ||
| 421 | - `--experimental-conditional-exports` flag._ | ||
| 422 | - | ||
| 423 | 433 | Prior to the introduction of support for ES modules in Node.js, it was a common | |
| 424 | 434 | pattern for package authors to include both CommonJS and ES module JavaScript | |
| 425 | 435 | sources in their package, with `package.json` `"main"` specifying the CommonJS | |
@@ -428,61 +438,36 @@ This enabled Node.js to run the CommonJS entry point while build tools such as | |||
| 428 | 438 | bundlers used the ES module entry point, since Node.js ignored (and still | |
| 429 | 439 | ignores) the top-level `"module"` field. | |
| 430 | 440 | ||
| 431 | - Node.js can now run ES module entry points, and using [Conditional Exports][] | ||
| 432 | - with the `--experimental-conditional-exports` flag it is possible to define | ||
| 433 | - separate package entry points for CommonJS and ES module consumers. Unlike in | ||
| 434 | - the scenario where `"module"` is only used by bundlers, or ES module files are | ||
| 441 | + Node.js can now run ES module entry points, and a package can contain both | ||
| 442 | + CommonJS and ES module entry points (either via separate specifiers such as | ||
| 443 | + `'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional | ||
| 444 | + Exports][] with the `--experimental-conditional-exports` flag). Unlike in the | ||
| 445 | + scenario where `"module"` is only used by bundlers, or ES module files are | ||
| 435 | 446 | transpiled into CommonJS on the fly before evaluation by Node.js, the files | |
| 436 | 447 | referenced by the ES module entry point are evaluated as ES modules. | |
| 437 | 448 | ||
| 438 | - #### Divergent Specifier Hazard | ||
| 449 | + #### Dual Package Hazard | ||
| 439 | 450 | ||
| 440 | 451 | When an application is using a package that provides both CommonJS and ES module | |
| 441 | 452 | sources, there is a risk of certain bugs if both versions of the package get | |
| 442 | - loaded (for example, because one version is imported by the application and the | ||
| 443 | - other version is required by one of the application’s dependencies). Such a | ||
| 444 | - package might look like this: | ||
| 445 | - | ||
| 446 | - <!-- eslint-skip --> | ||
| 447 | - ```js | ||
| 448 | - // ./node_modules/pkg/package.json | ||
| 449 | - { | ||
| 450 | - "type": "module", | ||
| 451 | - "main": "./pkg.cjs", | ||
| 452 | - "exports": { | ||
| 453 | - "require": "./pkg.cjs", | ||
| 454 | - "default": "./pkg.mjs" | ||
| 455 | - } | ||
| 456 | - } | ||
| 457 | - ``` | ||
| 458 | - | ||
| 459 | - In this example, `require('pkg')` always resolves to `pkg.cjs`, including in | ||
| 460 | - versions of Node.js where ES modules are unsupported. In Node.js where ES | ||
| 461 | - modules are supported, `import 'pkg'` references `pkg.mjs`. | ||
| 462 | - | ||
| 463 | - The potential for bugs comes from the fact that the `pkg` created by `const pkg | ||
| 464 | - = require('pkg')` is not the same as the `pkg` created by `import pkg from | ||
| 465 | - 'pkg'`. This is the “divergent specifier hazard,” where one specifer (`'pkg'`) | ||
| 466 | - resolves to separate files (`pkg.cjs` and `pkg.mjs`) in separate module systems, | ||
| 467 | - yet both versions might get loaded within an application because Node.js | ||
| 468 | - supports intermixing CommonJS and ES modules. | ||
| 469 | - | ||
| 470 | - If the export is a constructor, an `instanceof` comparison of instances created | ||
| 471 | - by the two returns `false`, and if the export is an object, properties added to | ||
| 472 | - one (like `pkg.foo = 3`) are not present on the other. This differs from how | ||
| 473 | - `import` and `require` statements work in all-CommonJS or all-ES module | ||
| 474 | - environments, respectively, and therefore is surprising to users. It also | ||
| 475 | - differs from the behavior users are familiar with when using transpilation via | ||
| 476 | - tools like [Babel][] or [`esm`][]. | ||
| 477 | - | ||
| 478 | - Even if the user consistently uses either `require` or `import` to refer to | ||
| 479 | - `pkg`, if any dependencies of the application use the other method the hazard is | ||
| 480 | - still present. | ||
| 481 | - | ||
| 482 | - The `--experimental-conditional-exports` flag should be set for modern Node.js | ||
| 483 | - for this behavior to work out. If it is not set, only the ES module version can | ||
| 484 | - be used in modern Node.js and the package will throw when accessed via | ||
| 485 | - `require()`. | ||
| 453 | + loaded. This potential comes from the fact that the `pkgInstance` created by | ||
| 454 | + `const pkgInstance = require('pkg')` is not the same as the `pkgInstance` | ||
| 455 | + created by `import pkgInstance from 'pkg'` (or an alternative main path like | ||
| 456 | + `'pkg/module'`). This is the “dual package hazard,” where two versions of the | ||
| 457 | + same package can be loaded within the same runtime environment. While it is | ||
| 458 | + unlikely that an application or package would intentionally load both versions | ||
| 459 | + directly, it is common for an application to load one version while a dependency | ||
| 460 | + of the application loads the other version. This hazard can happen because | ||
| 461 | + Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected | ||
| 462 | + behavior. | ||
| 463 | + | ||
| 464 | + If the package main export is a constructor, an `instanceof` comparison of | ||
| 465 | + instances created by the two versions returns `false`, and if the export is an | ||
| 466 | + object, properties added to one (like `pkgInstance.foo = 3`) are not present on | ||
| 467 | + the other. This differs from how `import` and `require` statements work in | ||
| 468 | + all-CommonJS or all-ES module environments, respectively, and therefore is | ||
| 469 | + surprising to users. It also differs from the behavior users are familiar with | ||
| 470 | + when using transpilation via tools like [Babel][] or [`esm`][]. | ||
| 486 | 471 | ||
| 487 | 472 | #### Writing Dual Packages While Avoiding or Minimizing Hazards | |
| 488 | 473 | ||
@@ -518,8 +503,14 @@ following conditions: | |||
| 518 | 503 | ||
| 519 | 504 | Write the package in CommonJS or transpile ES module sources into CommonJS, and | |
| 520 | 505 | create an ES module wrapper file that defines the named exports. Using | |
| 521 | - [Conditional Exports][], the ES module wrapper is used for `import` and the | ||
| 522 | - CommonJS entry point for `require`. | ||
| 506 | + [Conditional Exports][] via the `--experimental-conditional-exports` flag, the | ||
| 507 | + ES module wrapper is used for `import` and the CommonJS entry point for | ||
| 508 | + `require`. | ||
| 509 | + | ||
| 510 | + > Note: While `--experimental-conditional-exports` is flagged, a package | ||
| 511 | + > using this pattern will throw when loaded via `require()` in modern | ||
| 512 | + > Node.js, unless package consumers use the `--experimental-conditional-exports` | ||
| 513 | + > flag. | ||
| 523 | 514 | ||
| 524 | 515 | <!-- eslint-skip --> | |
| 525 | 516 | ```js | |
@@ -575,17 +566,37 @@ This approach is appropriate for any of the following use cases: | |||
| 575 | 566 | * The package stores internal state, and the package author would prefer not to | |
| 576 | 567 | refactor the package to isolate its state management. See the next section. | |
| 577 | 568 | ||
| 578 | - A variant of this approach would add an export, e.g. `"./module"`, to point to | ||
| 579 | - an all-ES module-syntax version the package. This could be used via `import | ||
| 569 | + A variant of this approach not requiring `--experimental-conditional-exports` | ||
| 570 | + for consumers could be to add an export, e.g. `"./module"`, to point to an | ||
| 571 | + all-ES module-syntax version of the package. This could be used via `import | ||
| 580 | 572 | 'pkg/module'` by users who are certain that the CommonJS version will not be | |
| 581 | 573 | loaded anywhere in the application, such as by dependencies; or if the CommonJS | |
| 582 | 574 | version can be loaded but doesn’t affect the ES module version (for example, | |
| 583 | - because the package is stateless). | ||
| 575 | + because the package is stateless): | ||
| 576 | + | ||
| 577 | + <!-- eslint-skip --> | ||
| 578 | + ```js | ||
| 579 | + // ./node_modules/pkg/package.json | ||
| 580 | + { | ||
| 581 | + "type": "module", | ||
| 582 | + "main": "./index.cjs", | ||
| 583 | + "exports": { | ||
| 584 | + ".": "./index.cjs", | ||
| 585 | + "./module": "./wrapper.mjs" | ||
| 586 | + } | ||
| 587 | + } | ||
| 588 | + ``` | ||
| 589 | + | ||
| 590 | + If the `--experimental-conditional-exports` flag is dropped and therefore | ||
| 591 | + [Conditional Exports][] become available without a flag, this variant could be | ||
| 592 | + easily updated to use conditional exports by adding conditions to the `"."` | ||
| 593 | + path; while keeping `"./module"` for backward compatibility. | ||
| 584 | 594 | ||
| 585 | 595 | ##### Approach #2: Isolate State | |
| 586 | 596 | ||
| 587 | 597 | The most straightforward `package.json` would be one that defines the separate | |
| 588 | - CommonJS and ES module entry points directly: | ||
| 598 | + CommonJS and ES module entry points directly (requires | ||
| 599 | + `--experimental-conditional-exports`): | ||
| 589 | 600 | ||
| 590 | 601 | <!-- eslint-skip --> | |
| 591 | 602 | ```js | |
@@ -666,6 +677,28 @@ This approach is appropriate for any of the following use cases: | |||
| 666 | 677 | Even with isolated state, there is still the cost of possible extra code | |
| 667 | 678 | execution between the CommonJS and ES module versions of a package. | |
| 668 | 679 | ||
| 680 | + As with the previous approach, a variant of this approach not requiring | ||
| 681 | + `--experimental-conditional-exports` for consumers could be to add an export, | ||
| 682 | + e.g. `"./module"`, to point to an all-ES module-syntax version of the package: | ||
| 683 | + | ||
| 684 | + <!-- eslint-skip --> | ||
| 685 | + ```js | ||
| 686 | + // ./node_modules/pkg/package.json | ||
| 687 | + { | ||
| 688 | + "type": "module", | ||
| 689 | + "main": "./index.cjs", | ||
| 690 | + "exports": { | ||
| 691 | + ".": "./index.cjs", | ||
| 692 | + "./module": "./index.mjs" | ||
| 693 | + } | ||
| 694 | + } | ||
| 695 | + ``` | ||
| 696 | + | ||
| 697 | + If the `--experimental-conditional-exports` flag is dropped and therefore | ||
| 698 | + [Conditional Exports][] become available without a flag, this variant could be | ||
| 699 | + easily updated to use conditional exports by adding conditions to the `"."` | ||
| 700 | + path; while keeping `"./module"` for backward compatibility. | ||
| 701 | + | ||
| 669 | 702 | ## <code>import</code> Specifiers | |
| 670 | 703 | ||
| 671 | 704 | ### Terminology | |
@@ -1359,6 +1392,7 @@ success! | |||
| 1359 | 1392 | [ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md | |
| 1360 | 1393 | [ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration | |
| 1361 | 1394 | [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md | |
| 1395 | + [Package Exports]: #esm_package_exports | ||
| 1362 | 1396 | [Terminology]: #esm_terminology | |
| 1363 | 1397 | [WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script | |
| 1364 | 1398 | [`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | |
| Back | FazBrowse Home | New Git URL |
0 commit comments