◐ Shell
clean mode source ↗

doc: move dual package shipping docs to separate repo · nodejs/node@8e76cc6

@@ -902,273 +902,7 @@ $ node other.js

902902903903

## Dual CommonJS/ES module packages

904904905-

<!-- This section should not be in the API documentation:

906-907-

1. It teaches opinionated practices that some consider dangerous, see

908-

https://github.com/nodejs/node/issues/52174

909-

2. It will soon be obsolete when we unflag --experimental-require-module.

910-

3. It's difficult to understand a multi-file structure via long texts and snippets in

911-

a markdown document.

912-913-

TODO(?): Move this section to its own repository with example folders.

914-

-->

915-916-

Prior to the introduction of support for ES modules in Node.js, it was a common

917-

pattern for package authors to include both CommonJS and ES module JavaScript

918-

sources in their package, with `package.json` [`"main"`][] specifying the

919-

CommonJS entry point and `package.json` `"module"` specifying the ES module

920-

entry point.

921-

This enabled Node.js to run the CommonJS entry point while build tools such as

922-

bundlers used the ES module entry point, since Node.js ignored (and still

923-

ignores) the top-level `"module"` field.

924-925-

Node.js can now run ES module entry points, and a package can contain both

926-

CommonJS and ES module entry points (either via separate specifiers such as

927-

`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional

928-

exports][]). Unlike in the scenario where top-level `"module"` field is only used by bundlers,

929-

or ES module files are transpiled into CommonJS on the fly before evaluation by

930-

Node.js, the files referenced by the ES module entry point are evaluated as ES

931-

modules.

932-933-

### Dual package hazard

934-935-

When an application is using a package that provides both CommonJS and ES module

936-

sources, there is a risk of certain bugs if both versions of the package get

937-

loaded. This potential comes from the fact that the `pkgInstance` created by

938-

`const pkgInstance = require('pkg')` is not the same as the `pkgInstance`

939-

created by `import pkgInstance from 'pkg'` (or an alternative main path like

940-

`'pkg/module'`). This is the “dual package hazard,” where two versions of the

941-

same package can be loaded within the same runtime environment. While it is

942-

unlikely that an application or package would intentionally load both versions

943-

directly, it is common for an application to load one version while a dependency

944-

of the application loads the other version. This hazard can happen because

945-

Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected

946-

behavior.

947-948-

If the package main export is a constructor, an `instanceof` comparison of

949-

instances created by the two versions returns `false`, and if the export is an

950-

object, properties added to one (like `pkgInstance.foo = 3`) are not present on

951-

the other. This differs from how `import` and `require` statements work in

952-

all-CommonJS or all-ES module environments, respectively, and therefore is

953-

surprising to users. It also differs from the behavior users are familiar with

954-

when using transpilation via tools like [Babel][] or [`esm`][].

955-956-

### Writing dual packages while avoiding or minimizing hazards

957-958-

First, the hazard described in the previous section occurs when a package

959-

contains both CommonJS and ES module sources and both sources are provided for

960-

use in Node.js, either via separate main entry points or exported paths. A

961-

package might instead be written where any version of Node.js receives only

962-

CommonJS sources, and any separate ES module sources the package might contain

963-

are intended only for other environments such as browsers. Such a package

964-

would be usable by any version of Node.js, since `import` can refer to CommonJS

965-

files; but it would not provide any of the advantages of using ES module syntax.

966-967-

A package might also switch from CommonJS to ES module syntax in a [breaking

968-

change](https://semver.org/) version bump. This has the disadvantage that the

969-

newest version of the package would only be usable in ES module-supporting

970-

versions of Node.js.

971-972-

Every pattern has tradeoffs, but there are two broad approaches that satisfy the

973-

following conditions:

974-975-

1. The package is usable via both `require` and `import`.

976-

2. The package is usable in both current Node.js and older versions of Node.js

977-

that lack support for ES modules.

978-

3. The package main entry point, e.g. `'pkg'` can be used by both `require` to

979-

resolve to a CommonJS file and by `import` to resolve to an ES module file.

980-

(And likewise for exported paths, e.g. `'pkg/feature'`.)

981-

4. The package provides named exports, e.g. `import { name } from 'pkg'` rather

982-

than `import pkg from 'pkg'; pkg.name`.

983-

5. The package is potentially usable in other ES module environments such as

984-

browsers.

985-

6. The hazards described in the previous section are avoided or minimized.

986-987-

#### Approach #1: Use an ES module wrapper

988-989-

Write the package in CommonJS or transpile ES module sources into CommonJS, and

990-

create an ES module wrapper file that defines the named exports. Using

991-

[Conditional exports][], the ES module wrapper is used for `import` and the

992-

CommonJS entry point for `require`.

993-994-

```json

995-

// ./node_modules/pkg/package.json

996-

{

997-

"type": "module",

998-

"exports": {

999-

"import": "./wrapper.mjs",

1000-

"require": "./index.cjs"

1001-

}

1002-

}

1003-

```

1004-1005-

The preceding example uses explicit extensions `.mjs` and `.cjs`.

1006-

If your files use the `.js` extension, `"type": "module"` will cause such files

1007-

to be treated as ES modules, just as `"type": "commonjs"` would cause them

1008-

to be treated as CommonJS.

1009-

See [Enabling](esm.md#enabling).

1010-1011-

```cjs

1012-

// ./node_modules/pkg/index.cjs

1013-

exports.name = 'value';

1014-

```

1015-1016-

```js

1017-

// ./node_modules/pkg/wrapper.mjs

1018-

import cjsModule from './index.cjs';

1019-

export const name = cjsModule.name;

1020-

```

1021-1022-

In this example, the `name` from `import { name } from 'pkg'` is the same

1023-

singleton as the `name` from `const { name } = require('pkg')`. Therefore `===`

1024-

returns `true` when comparing the two `name`s and the divergent specifier hazard

1025-

is avoided.

1026-1027-

If the module is not simply a list of named exports, but rather contains a

1028-

unique function or object export like `module.exports = function () { ... }`,

1029-

or if support in the wrapper for the `import pkg from 'pkg'` pattern is desired,

1030-

then the wrapper would instead be written to export the default optionally

1031-

along with any named exports as well:

1032-1033-

```js

1034-

import cjsModule from './index.cjs';

1035-

export const name = cjsModule.name;

1036-

export default cjsModule;

1037-

```

1038-1039-

This approach is appropriate for any of the following use cases:

1040-1041-

* The package is currently written in CommonJS and the author would prefer not

1042-

to refactor it into ES module syntax, but wishes to provide named exports for

1043-

ES module consumers.

1044-

* The package has other packages that depend on it, and the end user might

1045-

install both this package and those other packages. For example a `utilities`

1046-

package is used directly in an application, and a `utilities-plus` package

1047-

adds a few more functions to `utilities`. Because the wrapper exports

1048-

underlying CommonJS files, it doesn't matter if `utilities-plus` is written in

1049-

CommonJS or ES module syntax; it will work either way.

1050-

* The package stores internal state, and the package author would prefer not to

1051-

refactor the package to isolate its state management. See the next section.

1052-1053-

A variant of this approach not requiring conditional exports for consumers could

1054-

be to add an export, e.g. `"./module"`, to point to an all-ES module-syntax

1055-

version of the package. This could be used via `import 'pkg/module'` by users

1056-

who are certain that the CommonJS version will not be loaded anywhere in the

1057-

application, such as by dependencies; or if the CommonJS version can be loaded

1058-

but doesn't affect the ES module version (for example, because the package is

1059-

stateless):

1060-1061-

```json

1062-

// ./node_modules/pkg/package.json

1063-

{

1064-

"type": "module",

1065-

"exports": {

1066-

".": "./index.cjs",

1067-

"./module": "./wrapper.mjs"

1068-

}

1069-

}

1070-

```

1071-1072-

#### Approach #2: Isolate state

1073-1074-

A [`package.json`][] file can define the separate CommonJS and ES module entry

1075-

points directly:

1076-1077-

```json

1078-

// ./node_modules/pkg/package.json

1079-

{

1080-

"type": "module",

1081-

"exports": {

1082-

"import": "./index.mjs",

1083-

"require": "./index.cjs"

1084-

}

1085-

}

1086-

```

1087-1088-

This can be done if both the CommonJS and ES module versions of the package are

1089-

equivalent, for example because one is the transpiled output of the other; and

1090-

the package's management of state is carefully isolated (or the package is

1091-

stateless).

1092-1093-

The reason that state is an issue is because both the CommonJS and ES module

1094-

versions of the package might get used within an application; for example, the

1095-

user's application code could `import` the ES module version while a dependency

1096-

`require`s the CommonJS version. If that were to occur, two copies of the

1097-

package would be loaded in memory and therefore two separate states would be

1098-

present. This would likely cause hard-to-troubleshoot bugs.

1099-1100-

Aside from writing a stateless package (if JavaScript's `Math` were a package,

1101-

for example, it would be stateless as all of its methods are static), there are

1102-

some ways to isolate state so that it's shared between the potentially loaded

1103-

CommonJS and ES module instances of the package:

1104-1105-

1. If possible, contain all state within an instantiated object. JavaScript's

1106-

`Date`, for example, needs to be instantiated to contain state; if it were a

1107-

package, it would be used like this:

1108-1109-

```js

1110-

import Date from 'date';

1111-

const someDate = new Date();

1112-

// someDate contains state; Date does not

1113-

```

1114-1115-

The `new` keyword isn't required; a package's function can return a new

1116-

object, or modify a passed-in object, to keep the state external to the

1117-

package.

1118-1119-

2. Isolate the state in one or more CommonJS files that are shared between the

1120-

CommonJS and ES module versions of the package. For example, if the CommonJS

1121-

and ES module entry points are `index.cjs` and `index.mjs`, respectively:

1122-1123-

```cjs

1124-

// ./node_modules/pkg/index.cjs

1125-

const state = require('./state.cjs');

1126-

module.exports.state = state;

1127-

```

1128-1129-

```js

1130-

// ./node_modules/pkg/index.mjs

1131-

import state from './state.cjs';

1132-

export {

1133-

state,

1134-

};

1135-

```

1136-1137-

Even if `pkg` is used via both `require` and `import` in an application (for

1138-

example, via `import` in application code and via `require` by a dependency)

1139-

each reference of `pkg` will contain the same state; and modifying that

1140-

state from either module system will apply to both.

1141-1142-

Any plugins that attach to the package's singleton would need to separately

1143-

attach to both the CommonJS and ES module singletons.

1144-1145-

This approach is appropriate for any of the following use cases:

1146-1147-

* The package is currently written in ES module syntax and the package author

1148-

wants that version to be used wherever such syntax is supported.

1149-

* The package is stateless or its state can be isolated without too much

1150-

difficulty.

1151-

* The package is unlikely to have other public packages that depend on it, or if

1152-

it does, the package is stateless or has state that need not be shared between

1153-

dependencies or with the overall application.

1154-1155-

Even with isolated state, there is still the cost of possible extra code

1156-

execution between the CommonJS and ES module versions of a package.

1157-1158-

As with the previous approach, a variant of this approach not requiring

1159-

conditional exports for consumers could be to add an export, e.g.

1160-

`"./module"`, to point to an all-ES module-syntax version of the package:

1161-1162-

```json

1163-

// ./node_modules/pkg/package.json

1164-

{

1165-

"type": "module",

1166-

"exports": {

1167-

".": "./index.cjs",

1168-

"./module": "./index.mjs"

1169-

}

1170-

}

1171-

```

905+

See [the package examples repository][] for details.

11729061173907

## Node.js `package.json` field definitions

1174908

@@ -1412,7 +1146,6 @@ Package imports permit mapping to external packages.

1412114614131147

This field defines [subpath imports][] for the current package.

141411481415-

[Babel]: https://babeljs.io/

14161149

[CommonJS]: modules.md

14171150

[Conditional exports]: #conditional-exports

14181151

[Corepack]: corepack.md

@@ -1432,7 +1165,6 @@ This field defines [subpath imports][] for the current package.

14321165

[`--experimental-default-type`]: cli.md#--experimental-default-typetype

14331166

[`--no-addons` flag]: cli.md#--no-addons

14341167

[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported

1435-

[`esm`]: https://github.com/standard-things/esm#readme

14361168

[`package.json`]: #nodejs-packagejson-field-definitions

14371169

[entry points]: #package-entry-points

14381170

[folders as modules]: modules.md#folders-as-modules

@@ -1446,3 +1178,4 @@ This field defines [subpath imports][] for the current package.

14461178

[supported package managers]: corepack.md#supported-package-managers

14471179

[the dual CommonJS/ES module packages section]: #dual-commonjses-module-packages

14481180

[the full specifier path]: esm.md#mandatory-file-extensions

1181+

[the package examples repository]: https://github.com/nodejs/package-examples