Skip to content

Commit

Permalink
module: package "imports" field
Browse files Browse the repository at this point in the history
PR-URL: #34117
Reviewed-By: Jan Krems <jan.krems@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
  • Loading branch information
guybedford authored and MylesBorins committed Jul 16, 2020
1 parent 601ed8e commit 1ae76bd
Show file tree
Hide file tree
Showing 16 changed files with 537 additions and 177 deletions.
7 changes: 7 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1704,6 +1704,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 @@ -2560,3 +2566,4 @@ closed.
[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
159 changes: 122 additions & 37 deletions doc/api/esm.md
Expand Up @@ -538,6 +538,43 @@ and in a CommonJS one. For example, this code will also work:
const { something } = require('a-package/foo'); // Loads from ./foo.js.
```

### Internal package imports

In addition to the `"exports"` field it is possible to define internal package
import maps that only apply to import specifiers from within the package itself.

Entries in the imports field must always start with `#` to ensure they are
clearly disambiguated from package specifiers.

For example, the imports field can be used to gain the benefits of conditional
exports for internal modules:

```json
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}
```

where `import '#dep'` would now get the resolution of the external package
`dep-node-native` (including its exports in turn), and instead get the local
file `./dep-polyfill.js` relative to the package in other environments.

Unlike the exports field, import maps permit mapping to external packages
because this provides an important use case for conditional loading and also can
be done without the risk of cycles, unlike for exports.

Apart from the above, the resolution rules for the imports field are otherwise
analogous to the exports field.

### Dual CommonJS/ES module packages

Prior to the introduction of support for ES modules in Node.js, it was a common
Expand Down Expand Up @@ -1552,10 +1589,11 @@ The resolver can throw the following errors:
or package subpath specifier.
* _Invalid Package Configuration_: package.json configuration is invalid or
contains an invalid configuration.
* _Invalid Package Target_: Package exports define a target module within the
package that is an invalid type or string target.
* _Invalid Package Target_: Package exports or imports define a target module
for the package that is an invalid type or string target.
* _Package Path Not Exported_: Package exports do not define or permit a target
subpath in the package for the given module.
* _Package Import Not Defined_: Package imports do not define the specifier.
* _Module Not Found_: The package or module requested does not exist.

<details>
Expand All @@ -1567,11 +1605,14 @@ The resolver can throw the following errors:
> 1. If _specifier_ is a valid URL, then
> 1. Set _resolvedURL_ to the result of parsing and reserializing
> _specifier_ as a URL.
> 1. Otherwise, if _specifier_ starts with _"/"_, then
> 1. Throw an _Invalid Module Specifier_ error.
> 1. Otherwise, if _specifier_ starts with _"./"_ or _"../"_, then
> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_, then
> 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to
> _parentURL_.
> 1. Otherwise, if _specifier_ starts with _"#"_, then
> 1. Set _resolvedURL_ to the result of
> **PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_).
> 1. If _resolvedURL_ is **null** or **undefined**, throw a
> _Package Import Not Defined_ error.
> 1. Otherwise,
> 1. Note: _specifier_ is now a bare specifier.
> 1. Set _resolvedURL_ the result of
Expand Down Expand Up @@ -1609,7 +1650,7 @@ The resolver can throw the following errors:
> 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent
> encoded strings for _"/"_ or _"\\"_, then
> 1. Throw an _Invalid Module Specifier_ error.
> 1. Set _selfUrl_ to the result of
> 1. Let _selfUrl_ be the result of
> **SELF_REFERENCE_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_).
> 1. If _selfUrl_ isn't empty, return _selfUrl_.
> 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin
Expand All @@ -1632,8 +1673,11 @@ The resolver can throw the following errors:
> 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then
> 1. Let _exports_ be _pjson.exports_.
> 1. If _exports_ is not **null** or **undefined**, then
> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_,
> _packageSubpath_, _pjson.exports_).
> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**(
> _packageURL_, _packageSubpath_, _pjson.exports_).
> 1. If _resolved_ is **null** or **undefined**, throw a
> _Package Path Not Exported_ error.
> 1. Return _resolved_.
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
> 1. Throw a _Module Not Found_ error.
Expand All @@ -1654,8 +1698,11 @@ The resolver can throw the following errors:
> 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then
> 1. Let _exports_ be _pjson.exports_.
> 1. If _exports_ is not **null** or **undefined**, then
> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_,
> _pjson.exports_).
> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**(
> _packageURL_, _subpath_, _pjson.exports_).
> 1. If _resolved_ is **null** or **undefined**, throw a
> _Package Path Not Exported_ error.
> 1. Return _resolved_.
> 1. Return the URL resolution of _subpath_ in _packageURL_.
> 1. Otherwise, return **undefined**.
Expand All @@ -1668,12 +1715,18 @@ The resolver can throw the following errors:
> not starting with _"."_, throw an _Invalid Package Configuration_ error.
> 1. If _pjson.exports_ is a String or Array, or an Object containing no
> keys starting with _"."_, then
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
> _pjson.exports_, _""_).
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _pjson.exports_, _""_, **false**, _defaultEnv_).
> 1. If _resolved_ is **null** or **undefined**, throw a
> _Package Path Not Exported_ error.
> 1. Return _resolved_.
> 1. If _pjson.exports_ is an Object containing a _"."_ property, then
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
> _mainExport_, _""_).
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _mainExport_, _""_, **false**, _defaultEnv_).
> 1. If _resolved_ is **null** or **undefined**, throw a
> _Package Path Not Exported_ error.
> 1. Return _resolved_.
> 1. Throw a _Package Path Not Exported_ error.
> 1. Let _legacyMainURL_ be the result applying the legacy
> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a
Expand All @@ -1687,31 +1740,37 @@ The resolver can throw the following errors:
> 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_.
> 1. If _packagePath_ is a key of _exports_, then
> 1. Let _target_ be the value of _exports\[packagePath\]_.
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
> _""_, _defaultEnv_).
> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_,
> _""_, **false**, _defaultEnv_).
> 1. Let _directoryKeys_ be the list of keys of _exports_ ending in
> _"/"_, sorted by length descending.
> 1. For each key _directory_ in _directoryKeys_, do
> 1. If _packagePath_ starts with _directory_, then
> 1. Let _target_ be the value of _exports\[directory\]_.
> 1. Let _subpath_ be the substring of _target_ starting at the index
> of the length of _directory_.
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
> _subpath_, _defaultEnv_).
> 1. Throw a _Package Path Not Exported_ error.
> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_,
> _subpath_, **false**, _defaultEnv_).
> 1. Return **null**.
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_)
**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_, _env_)
> 1. If _target_ is a String, then
> 1. If _target_ does not start with _"./"_ or contains any _"node_modules"_
> segments including _"node_modules"_ percent-encoding, throw an
> _Invalid Package Target_ error.
> 1. If _target_ contains any _"node_modules"_ segments including
> _"node_modules"_ percent-encoding, throw an _Invalid Package Target_
> error.
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
> throw an _Invalid Module Specifier_ error.
> 1. If _target_ does not start with _"./"_, then
> 1. If _target_ does not start with _"../"_ or _"/"_ and is not a valid
> URL, then
> 1. If _internal_ is **true**, return **PACKAGE_RESOLVE**(
> _target_ + _subpath_, _packageURL_ + _"/"_)_.
> 1. Otherwise throw an _Invalid Package Target_ error.
> 1. Let _resolvedTarget_ be the URL resolution of the concatenation of
> _packageURL_ and _target_.
> 1. If _resolvedTarget_ is not contained in _packageURL_, throw an
> _Invalid Package Target_ error.
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
> throw an _Invalid Module Specifier_ error.
> 1. Let _resolved_ be the URL resolution of the concatenation of
> _subpath_ and _resolvedTarget_.
> 1. If _resolved_ is not contained in _resolvedTarget_, throw an
Expand All @@ -1723,22 +1782,48 @@ The resolver can throw the following errors:
> 1. For each property _p_ of _target_, in object insertion order as,
> 1. If _p_ equals _"default"_ or _env_ contains an entry for _p_, then
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**(
> _packageURL_, _targetValue_, _subpath_, _env_), continuing the
> loop on any _Package Path Not Exported_ error.
> 1. Throw a _Package Path Not Exported_ error.
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _targetValue_, _subpath_, _internal_, _env_)
> 1. If _resolved_ is equal to **undefined**, continue the loop.
> 1. Return _resolved_.
> 1. Return **undefined**.
> 1. Otherwise, if _target_ is an Array, then
> 1. If _target.length is zero, throw a _Package Path Not Exported_ error.
> 1. If _target.length is zero, return **null**.
> 1. For each item _targetValue_ in _target_, do
> 1. If _targetValue_ is an Array, continue the loop.
> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
> _targetValue_, _subpath_, _env_), continuing the loop on any
> _Package Path Not Exported_ or _Invalid Package Target_ error.
> 1. Throw the last fallback resolution error.
> 1. Otherwise, if _target_ is _null_, throw a _Package Path Not Exported_
> error.
> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**(
> _packageURL_, _targetValue_, _subpath_, _internal_, _env_),
> continuing the loop on any _Invalid Package Target_ error.
> 1. If _resolved_ is **undefined**, continue the loop.
> 1. Return _resolved_.
> 1. Return or throw the last fallback resolution **null** return or error.
> 1. Otherwise, if _target_ is _null_, return **null**.
> 1. Otherwise throw an _Invalid Package Target_ error.
**PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_)
> 1. Assert: _specifier_ begins with _"#"_.
> 1. If _specifier_ is exactly equal to _"#"_ or starts with _"#/"_, then
> 1. Throw an _Invalid Module Specifier_ error.
> 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_).
> 1. If _packageURL_ is not **null**, then
> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_).
> 1. If _pjson.imports is a non-null Object, then
> 1. Let _imports_ be _pjson.imports_.
> 1. If _specifier_ is a key of _imports_, then
> 1. Let _target_ be the value of _imports\[specifier\]_.
> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_,
> _""_, **true**, _defaultEnv_).
> 1. Let _directoryKeys_ be the list of keys of _imports_ ending in
> _"/"_, sorted by length descending.
> 1. For each key _directory_ in _directoryKeys_, do
> 1. If _specifier_ starts with _directory_, then
> 1. Let _target_ be the value of _imports\[directory\]_.
> 1. Let _subpath_ be the substring of _target_ starting at the
> index of the length of _directory_.
> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_,
> _subpath_, **true**, _defaultEnv_).
> 1. Return **null**.
**ESM_FORMAT**(_url_)
> 1. Assert: _url_ corresponds to an existing file.
Expand Down
13 changes: 12 additions & 1 deletion doc/api/modules.md
Expand Up @@ -160,7 +160,9 @@ 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))
4. If X begins with '#'
a. LOAD_INTERAL_IMPORT(X, Y)
4. LOAD_SELF_REFERENCE(X, Y)
5. LOAD_NODE_MODULES(X, dirname(Y))
6. THROW "not found"
Expand Down Expand Up @@ -236,6 +238,15 @@ LOAD_PACKAGE_EXPORTS(DIR, X)
12. Otherwise
a. If RESOLVED is a file, load it as its file extension format. STOP
13. Throw "not found"
LOAD_INTERNAL_IMPORT(X, START)
1. Find the closest package scope to START.
2. If no scope was found or the `package.json` has no "imports", return.
3. let RESOLVED =
fileURLToPath(PACKAGE_INTERNAL_RESOLVE(X, pathToFileURL(START)), as defined
in the ESM resolver.
4. If RESOLVED is not a valid file, throw "not found"
5. Load RESOLVED as its file extension format. STOP
```

## Caching
Expand Down
64 changes: 23 additions & 41 deletions lib/internal/errors.js
Expand Up @@ -20,7 +20,6 @@ const {
NumberIsInteger,
ObjectDefineProperty,
ObjectKeys,
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
SymbolFor,
Expand Down Expand Up @@ -1105,16 +1104,9 @@ 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}"`,
Expand All @@ -1128,31 +1120,20 @@ E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => {
return `Invalid JSON in ${path} imported from ${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 @@ -1298,15 +1279,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 @@ -1426,7 +1408,7 @@ 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);
'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);

Expand Down

0 comments on commit 1ae76bd

Please sign in to comment.