Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.x] modules backports #35385

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ $ node --completion-bash > node_bash_completion
$ source node_bash_completion
```

### `--conditions=condition`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Enable experimental support for custom conditional exports resolution
conditions.

Any number of custom string condition names are permitted.

The default Node.js conditions of `"node"`, `"default"`, `"import"`, and
`"require"` will always apply as defined.

### `--cpu-prof`
<!-- YAML
added: v12.0.0
Expand Down Expand Up @@ -1166,6 +1181,7 @@ node --require "./a.js" --require "./b.js"

Node.js options that are allowed are:
<!-- node-options-node start -->
* `--conditions`
* `--diagnostic-dir`
* `--disable-proto`
* `--enable-fips`
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,12 @@ A non-context-aware native addon was loaded in a process that disallows them.

A given value is out of the accepted range.

<a id="ERR_PACKAGE_IMPORT_NOT_DEFINED"></a>
### `ERR_PACKAGE_IMPORT_NOT_DEFINED`

The `package.json` ["imports" field][] does not define the given internal
package specifier mapping.

<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>
### `ERR_PACKAGE_PATH_NOT_EXPORTED`

Expand Down Expand Up @@ -2598,3 +2604,4 @@ such as `process.stdout.on('data')`.
[vm]: vm.html
[self-reference a package using its name]: esm.html#esm_self_referencing_a_package_using_its_name
[define a custom subpath]: esm.html#esm_subpath_exports
["imports" field]: esm.html#esm_internal_package_imports
377 changes: 242 additions & 135 deletions doc/api/esm.md

Large diffs are not rendered by default.

71 changes: 40 additions & 31 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,11 @@ require(X) from module at path Y
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. LOAD_SELF_REFERENCE(X, dirname(Y))
5. LOAD_NODE_MODULES(X, dirname(Y))
6. THROW "not found"
4. If X begins with '#'
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
Expand All @@ -189,7 +191,7 @@ LOAD_AS_DIRECTORY(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(DIR, X)
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)

Expand All @@ -204,38 +206,45 @@ NODE_MODULES_PATHS(START)
d. let I = I - 1
5. return DIRS

LOAD_SELF_REFERENCE(X, START)
1. Find the closest package scope to START.
LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the `package.json` has no "exports", return.
4. If the name in `package.json` is a prefix of X, then
a. Load the remainder of X relative to this package as if it was
loaded via `LOAD_NODE_MODULES` with a name in `package.json`.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
["node", "require"]) defined in the ESM resolver.
5. RESOLVE_ESM_MATCH(MATCH).

LOAD_PACKAGE_EXPORTS(DIR, X)
1. Try to interpret X as a combination of name and subpath where the name
LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/name/package.json is not a file,
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
3. Parse DIR/name/package.json, and look for "exports" field.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. If "exports" is an object with some keys starting with "." and some keys
not starting with ".", throw "invalid config".
6. If "exports" is a string, or object with no keys starting with ".", treat
it as having that value as its "." object property.
7. If subpath is "." and "exports" does not have a "." entry, return.
8. Find the longest key in "exports" that the subpath starts with.
9. If no such key can be found, throw "not found".
10. let RESOLVED =
fileURLToPath(PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name),
exports[key], subpath.slice(key.length), ["node", "require"])), as defined
in the ESM resolver.
11. If key ends with "/":
a. LOAD_AS_FILE(RESOLVED)
b. LOAD_AS_DIRECTORY(RESOLVED)
12. Otherwise
a. If RESOLVED is a file, load it as its file extension format. STOP
13. Throw "not found"
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", ["node", "require"]) defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)

LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "exports" is null or undefined, return.
4. If the SCOPE/package.json "name" is not the first segment of X, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
"." + X.slice("name".length), `package.json` "exports", ["node", "require"])
defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)

RESOLVE_ESM_MATCH(MATCH)
1. let { RESOLVED, EXACT } = MATCH
2. let RESOLVED_PATH = fileURLToPath(RESOLVED)
3. If EXACT is true,
a. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
format. STOP
4. Otherwise, if EXACT is false,
a. LOAD_AS_FILE(RESOLVED_PATH)
b. LOAD_AS_DIRECTORY(RESOLVED_PATH)
5. THROW "not found"
```

## Caching
Expand Down
4 changes: 4 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ Aborting instead of exiting causes a core file to be generated for analysis.
.It Fl -completion-bash
Print source-able bash completion script for Node.js.
.
.It Fl -conditions Ar string
Use custom conditional exports conditions
.Ar string
.
.It Fl -cpu-prof
Start the V8 CPU profiler on start up, and write the CPU profile to disk
before exit. If
Expand Down
84 changes: 36 additions & 48 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ const {
NumberIsInteger,
ObjectDefineProperty,
ObjectKeys,
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
WeakMap,
} = primordials;

const sep = process.platform === 'win32' ? '\\' : '/';
const isWindows = process.platform === 'win32';

const messages = new Map();
const codes = {};
Expand Down Expand Up @@ -1097,54 +1096,35 @@ E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError);
E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);
E('ERR_INVALID_MODULE_SPECIFIER', (pkgPath, subpath, base = undefined) => {
if (subpath === undefined) {
return `Invalid package name '${pkgPath}' imported from ${base}`;
} else if (base === undefined) {
assert(subpath !== '.');
return `Package subpath '${subpath}' is not a valid module request for ` +
`the "exports" resolution of ${pkgPath}${sep}package.json`;
}
return `Package subpath '${subpath}' is not a valid module request for ` +
`the "exports" resolution of ${pkgPath} imported from ${base}`;
E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => {
return `Invalid module "${request}" ${reason}${base ?
` imported from ${base}` : ''}`;
}, TypeError);
E('ERR_INVALID_OPT_VALUE', (name, value) =>
`The value "${String(value)}" is invalid for option "${name}"`,
TypeError,
RangeError);
E('ERR_INVALID_OPT_VALUE_ENCODING',
'The value "%s" is invalid for option "encoding"', TypeError);
E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => {
if (hasMessage)
return `Invalid package config ${path}${sep}package.json, ${message}`;
return `Invalid JSON in ${path} imported from ${message}`;
E('ERR_INVALID_PACKAGE_CONFIG', (path, base, message) => {
return `Invalid package config ${path}${base ? ` while importing ${base}` :
''}${message ? `. ${message}` : ''}`;
}, Error);
E('ERR_INVALID_PACKAGE_TARGET',
(pkgPath, key, subpath, target, base = undefined) => {
const relError = typeof target === 'string' &&
(pkgPath, key, target, isImport = false, base = undefined) => {
const relError = typeof target === 'string' && !isImport &&
target.length && !StringPrototypeStartsWith(target, './');
if (key === null) {
if (subpath !== '') {
return `Invalid "exports" target ${JSONStringify(target)} defined ` +
`for '${subpath}' in the package config ${pkgPath} imported from ` +
`${base}.${relError ? '; targets must start with "./"' : ''}`;
}
return `Invalid "exports" main target ${target} defined in the ` +
`package config ${pkgPath} imported from ${base}${relError ?
'; targets must start with "./"' : ''}`;
} else if (key === '.') {
if (key === '.') {
assert(isImport === false);
return `Invalid "exports" main target ${JSONStringify(target)} defined ` +
`in the package config ${pkgPath}${sep}package.json${relError ?
'; targets must start with "./"' : ''}`;
} else if (relError) {
return `Invalid "exports" target ${JSONStringify(target)} defined for '${
StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` +
`package config ${pkgPath}${sep}package.json; ` +
'targets must start with "./"';
`in the package config ${pkgPath}package.json${base ?
` imported from ${base}` : ''}${relError ?
'; targets must start with "./"' : ''}`;
}
return `Invalid "exports" target ${JSONStringify(target)} defined for '${
StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` +
`package config ${pkgPath}${sep}package.json`;
return `Invalid "${isImport ? 'imports' : 'exports'}" target ${
JSONStringify(target)} defined for '${key}' in the package config ${
pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ?
'; targets must start with "./"' : ''}`;
}, Error);
E('ERR_INVALID_PERFORMANCE_MARK',
'The "%s" performance mark has not been set', Error);
Expand Down Expand Up @@ -1293,15 +1273,16 @@ E('ERR_OUT_OF_RANGE',
msg += ` It must be ${range}. Received ${received}`;
return msg;
}, RangeError);
E('ERR_PACKAGE_IMPORT_NOT_DEFINED', (specifier, packagePath, base) => {
return `Package import specifier "${specifier}" is not defined${packagePath ?
` in package ${packagePath}package.json` : ''} imported from ${base}`;
}, TypeError);
E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
if (subpath === '.') {
return `No "exports" main resolved in ${pkgPath}${sep}package.json`;
} else if (base === undefined) {
return `Package subpath '${subpath}' is not defined by "exports" in ${
pkgPath}${sep}package.json`;
}
if (subpath === '.')
return `No "exports" main defined in ${pkgPath}package.json${base ?
` imported from ${base}` : ''}`;
return `Package subpath '${subpath}' is not defined by "exports" in ${
pkgPath} imported from ${base}`;
pkgPath}package.json${base ? ` imported from ${base}` : ''}`;
}, Error);
E('ERR_REQUIRE_ESM',
(filename, parentPath = null, packageJsonPath = null) => {
Expand Down Expand Up @@ -1419,9 +1400,16 @@ E('ERR_UNKNOWN_FILE_EXTENSION',
E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError);
E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError);
E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " +
'resolving ES modules, imported from %s', Error);
E('ERR_UNSUPPORTED_ESM_URL_SCHEME', 'Only file and data URLs are supported ' +
'by the default ESM loader', Error);
'resolving ES modules imported from %s', Error);
E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url) => {
let msg = 'Only file and data URLs are supported by the default ESM loader';
if (isWindows && url.protocol.length === 2) {
msg +=
'. On Windows, absolute paths must be valid file:// URLs';
}
msg += `. Received protocol '${url.protocol}'`;
return msg;
}, Error);

// This should probably be a `TypeError`.
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
Expand Down
Loading