diff --git a/doc/api/cli.md b/doc/api/cli.md index f99066477a013b..574e40232fefb5 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -81,6 +81,21 @@ $ node --completion-bash > node_bash_completion $ source node_bash_completion ``` +### `--conditions=condition` + + +> 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` +* `--conditions` * `--diagnostic-dir` * `--disable-proto` * `--enable-fips` diff --git a/doc/api/errors.md b/doc/api/errors.md index 64bd22f337f2c6..e081361a633cc7 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -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. + +### `ERR_PACKAGE_IMPORT_NOT_DEFINED` + +The `package.json` ["imports" field][] does not define the given internal +package specifier mapping. + ### `ERR_PACKAGE_PATH_NOT_EXPORTED` @@ -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 diff --git a/doc/api/esm.md b/doc/api/esm.md index dc543091d75010..4b384dece42eb9 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -298,14 +298,14 @@ package. It is not a strong encapsulation since a direct require of any absolute subpath of the package such as `require('/path/to/node_modules/pkg/subpath.js')` will still load `subpath.js`. -#### Subpath exports +### Subpath exports -When using the `"exports"` field, custom subpaths can be defined along -with the main entry point by treating the main entry point as the -`"."` subpath: +> Stability: 1 - Experimental - -```js +When using the `"exports"` field, custom subpaths can be defined along with the +main entry point by treating the main entry point as the `"."` subpath: + +```json { "main": "./main.js", "exports": { @@ -315,8 +315,7 @@ with the main entry point by treating the main entry point as the } ``` -Now only the defined subpath in `"exports"` can be imported by a -consumer: +Now only the defined subpath in `"exports"` can be imported by a consumer: ```js import submodule from 'es-module-package/submodule'; @@ -330,30 +329,46 @@ import submodule from 'es-module-package/private-module.js'; // Throws ERR_PACKAGE_PATH_NOT_EXPORTED ``` -Entire folders can also be mapped with package exports: +### Subpath export patterns - -```js +> Stability: 1 - Experimental + +Explicitly listing each exports subpath entry is recommended for packages with +a small number of exports. But for packages that have very large numbers of +subpaths this can start to cause package.json bloat and maintenance issues. + +For these use cases, subpath export patterns can be used instead: + +```json // ./node_modules/es-module-package/package.json { "exports": { - "./features/": "./src/features/" + "./features/*": "./src/features/*.js" } } ``` -With the above, all modules within the `./src/features/` folder -are exposed deeply to `import` and `require`: +The left hand matching pattern must always end in `*`. All instances of `*` on +the right hand side will then be replaced with this value, including if it +contains any `/` separators. ```js -import feature from 'es-module-package/features/x.js'; +import featureX from 'es-module-package/features/x'; // Loads ./node_modules/es-module-package/src/features/x.js + +import featureY from 'es-module-package/features/y/y'; +// Loads ./node_modules/es-module-package/src/features/y/y.js ``` -When using folder mappings, ensure that you do want to expose every -module inside the subfolder. Any modules which are not public -should be moved to another folder to retain the encapsulation -benefits of exports. +This is a direct static replacement without any special handling for file +extensions. In the previous example, `pkg/features/x.json` would be resolved to +`./src/features/x.json.js` in the mapping. + +The property of exports being statically enumerable is maintained with exports +patterns since the individual exports for a package can be determined by +treating the right hand side target pattern as a `**` glob against the list of +files within the package. Because `node_modules` paths are forbidden in exports +targets, this expansion is dependent on only the files of the package itself. #### Package exports fallbacks @@ -501,6 +516,21 @@ a nested conditional does not have any mapping it will continue checking the remaining conditions of the parent condition. In this way nested conditions behave analogously to nested JavaScript `if` statements. +#### Resolving user conditions + +When running Node.js, custom user conditions can be added with the +`--conditions` or `-u` flag: + +```bash +node --conditions=development main.js +``` + +which would then resolve the `"development"` condition in package imports and +exports, while resolving the existing `"node"`, `"default"`, `"import"`, and +`"require"` conditions as appropriate. + +Any number of custom conditions can be set with repeat flags. + #### Self-referencing a package using its name Within a package, the values defined in the package’s @@ -546,6 +576,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 @@ -1569,7 +1636,7 @@ future updates. In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise. -_defaultEnv_ is the conditional environment name priority array, +_defaultConditions_ is the conditional environment name array, `["node", "import"]`. The resolver can throw the following errors: @@ -1577,10 +1644,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.
@@ -1588,37 +1656,41 @@ The resolver can throw the following errors: **ESM_RESOLVE**(_specifier_, _parentURL_) -> 1. Let _resolvedURL_ be **undefined**. +> 1. Let _resolved_ be **undefined**. > 1. If _specifier_ is a valid URL, then -> 1. Set _resolvedURL_ to the result of parsing and reserializing +> 1. Set _resolved_ 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. Set _resolvedURL_ to the URL resolution of _specifier_ relative to +> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_, then +> 1. Set _resolved_ to the URL resolution of _specifier_ relative to > _parentURL_. +> 1. Otherwise, if _specifier_ starts with _"#"_, then +> 1. Set _resolved_ to the destructured value of the result of +> **PACKAGE_IMPORTS_RESOLVE**(_specifier_, _parentURL_, +> _defaultConditions_). > 1. Otherwise, > 1. Note: _specifier_ is now a bare specifier. -> 1. Set _resolvedURL_ the result of +> 1. Set _resolved_ the result of > **PACKAGE_RESOLVE**(_specifier_, _parentURL_). -> 1. If _resolvedURL_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_ +> 1. If _resolved_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_ > and _"%5C"_ respectively), then > 1. Throw an _Invalid Module Specifier_ error. -> 1. If the file at _resolvedURL_ is a directory, then +> 1. If the file at _resolved_ is a directory, then > 1. Throw an _Unsupported Directory Import_ error. -> 1. If the file at _resolvedURL_ does not exist, then +> 1. If the file at _resolved_ does not exist, then > 1. Throw a _Module Not Found_ error. -> 1. Set _resolvedURL_ to the real path of _resolvedURL_. -> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_). -> 1. Load _resolvedURL_ as module format, _format_. -> 1. Return _resolvedURL_. +> 1. Set _resolved_ to the real path of _resolved_. +> 1. Let _format_ be the result of **ESM_FORMAT**(_resolved_). +> 1. Load _resolved_ as module format, _format_. +> 1. Return _resolved_. **PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_) -> 1. Let _packageName_ be *undefined*. -> 1. Let _packageSubpath_ be *undefined*. +> 1. Let _packageName_ be **undefined**. > 1. If _packageSpecifier_ is an empty string, then > 1. Throw an _Invalid Module Specifier_ error. +> 1. If _packageSpecifier_ does not start with _"@"_, then +> 1. Set _packageName_ to the substring of _packageSpecifier_ until the first +> _"/"_ separator or the end of the string. > 1. Otherwise, > 1. If _packageSpecifier_ does not contain a _"/"_ separator, then > 1. Throw an _Invalid Module Specifier_ error. @@ -1626,18 +1698,12 @@ The resolver can throw the following errors: > until the second _"/"_ separator or the end of the string. > 1. If _packageName_ starts with _"."_ or contains _"\\"_ or _"%"_, then > 1. Throw an _Invalid Module Specifier_ error. -> 1. Let _packageSubpath_ be _undefined_. -> 1. If the length of _packageSpecifier_ is greater than the length of -> _packageName_, then -> 1. Set _packageSubpath_ to _"."_ concatenated with the substring of +> 1. Let _packageSubpath_ be _"."_ concatenated with the substring of > _packageSpecifier_ from the position at the length of _packageName_. -> 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 -> **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 +> 1. Let _selfUrl_ be the result of +> **PACKAGE_SELF_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_). +> 1. If _selfUrl_ is not **undefined**, return _selfUrl_. +> 1. If _packageSubpath_ is _"."_ and _packageName_ is a Node.js builtin > module, then > 1. Return the string _"nodejs:"_ concatenated with _packageSpecifier_. > 1. While _parentURL_ is not the file system root, @@ -1648,120 +1714,161 @@ The resolver can throw the following errors: > 1. Set _parentURL_ to the parent URL path of _parentURL_. > 1. Continue the next loop iteration. > 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). -> 1. If _packageSubpath_ is equal to _"./"_, then -> 1. Return _packageURL_ + _"/"_. -> 1. If _packageSubpath_ is _undefined__, then -> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, -> _pjson_). +> 1. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or +> **undefined**, then +> 1. Let _exports_ be _pjson.exports_. +> 1. Return the _resolved_ destructured value of the result of +> **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packageSubpath_, +> _pjson.exports_, _defaultConditions_). +> 1. Otherwise, if _packageSubpath_ is equal to _"."_, then +> 1. Return the result applying the legacy **LOAD_AS_DIRECTORY** +> CommonJS resolver to _packageURL_, throwing a _Module Not Found_ +> error for no resolution. > 1. Otherwise, -> 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. Return the URL resolution of _packageSubpath_ in _packageURL_. > 1. Throw a _Module Not Found_ error. -**SELF_REFERENCE_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_) +**PACKAGE_SELF_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_) > 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_). > 1. If _packageURL_ is **null**, then > 1. Return **undefined**. > 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). -> 1. If _pjson_ does not include an _"exports"_ property, then +> 1. If _pjson_ is **null** or if _pjson_._exports_ is **null** or +> **undefined**, then > 1. Return **undefined**. > 1. If _pjson.name_ is equal to _packageName_, then -> 1. If _packageSubpath_ is equal to _"./"_, then -> 1. Return _packageURL_ + _"/"_. -> 1. If _packageSubpath_ is _undefined_, then -> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_). -> 1. Otherwise, -> 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. Return the URL resolution of _subpath_ in _packageURL_. +> 1. Return the _resolved_ destructured value of the result of +> **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_, _pjson.exports_, +> _defaultConditions_). > 1. Otherwise, return **undefined**. -**PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_) +**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_, _exports_, _conditions_) -> 1. If _pjson_ is **null**, then -> 1. Throw a _Module Not Found_ error. -> 1. If _pjson.exports_ is not **null** or **undefined**, then -> 1. If _exports_ is an Object with both a key starting with _"."_ and a key -> 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. 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. 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 -> _Module Not Found_ error for no resolution. -> 1. Return _legacyMainURL_. - -**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_) > 1. If _exports_ is an Object with both a key starting with _"."_ and a key not > starting with _"."_, throw an _Invalid Package Configuration_ error. -> 1. If _exports_ is an Object and all keys of _exports_ start with _"."_, then -> 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. 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. If _subpath_ is equal to _"."_, then +> 1. Let _mainExport_ be **undefined**. +> 1. If _exports_ is a String or Array, or an Object containing no keys +> starting with _"."_, then +> 1. Set _mainExport_ to _exports_. +> 1. Otherwise if _exports_ is an Object containing a _"."_ property, then +> 1. Set _mainExport_ to _exports_\[_"."_\]. +> 1. If _mainExport_ is not **undefined**, then +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _mainExport_, _""_, **false**, **false**, +> _conditions_). +> 1. If _resolved_ is not **null** or **undefined**, then +> 1. Return _resolved_. +> 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with +> _"."_, then +> 1. Let _matchKey_ be the string _"./"_ concatenated with _subpath_. +> 1. Let _resolvedMatch_ be result of **PACKAGE_IMPORTS_EXPORTS_RESOLVE**( +> _matchKey_, _exports_, _packageURL_, **false**, _conditions_). +> 1. If _resolvedMatch_._resolve_ is not **null** or **undefined**, then +> 1. Return _resolvedMatch_. > 1. Throw a _Package Path Not Exported_ error. -**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_) +**PACKAGE_IMPORTS_RESOLVE**(_specifier_, _parentURL_, _conditions_) + +> 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 _resolvedMatch_ be the result of +> **PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_specifier_, _pjson.imports_, +> _packageURL_, **true**, _conditions_). +> 1. If _resolvedMatch_._resolve_ is not **null** or **undefined**, then +> 1. Return _resolvedMatch_. +> 1. Throw a _Package Import Not Defined_ error. + +**PACKAGE_IMPORTS_EXPORTS_RESOLVE**(_matchKey_, _matchObj_, _packageURL_, +_isImports_, _conditions_) + +> 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then +> 1. Let _target_ be the value of _matchObj_\[_matchKey_\]. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_). +> 1. Return the object _{ resolved, exact: **true** }_. +> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_ +> or _"*"_, sorted by length descending. +> 1. For each key _expansionKey_ in _expansionKeys_, do +> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is +> not equal to the substring of _expansionKey_ excluding the last _"*"_ +> character, then +> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\]. +> 1. Let _subpath_ be the substring of _matchKey_ starting at the +> index of the length of _expansionKey_ minus one. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _target_, _subpath_, **true**, _isImports_, +> _conditions_). +> 1. Return the object _{ resolved, exact: **true** }_. +> 1. If _matchKey_ starts with _expansionKey_, then +> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\]. +> 1. Let _subpath_ be the substring of _matchKey_ starting at the +> index of the length of _expansionKey_. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _target_, _subpath_, **false**, _isImports_, +> _conditions_). +> 1. Return the object _{ resolved, exact: **false** }_. +> 1. Return the object _{ resolved: **null**, exact: **true** }_. + +**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_, +_internal_, _conditions_) > 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 _pattern_ is **false**, _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 _internal_ is **true** and _target_ does not start with _"../"_ or +> _"/"_ and is not a valid URL, then +> 1. If _pattern_ is **true**, then +> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of +> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_. +> 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_, +> _packageURL_ + _"/"_)_. +> 1. Otherwise, throw an _Invalid Package Target_ error. +> 1. If _target_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or +> _"node_modules"_ segments after the first segment, throw an +> _Invalid Module Specifier_ 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 -> _Invalid Module Specifier_ error. -> 1. Return _resolved_. +> 1. Assert: _resolvedTarget_ is contained in _packageURL_. +> 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or +> _"node_modules"_ segments, throw an _Invalid Module Specifier_ error. +> 1. If _pattern_ is **true**, then +> 1. Return the URL resolution of _resolvedTarget_ with every instance of +> _"*"_ replaced with _subpath_. +> 1. Otherwise, +> 1. Return the URL resolution of the concatenation of _subpath_ and +> _resolvedTarget_. > 1. Otherwise, if _target_ is a non-null Object, then > 1. If _exports_ contains any index property keys, as defined in ECMA-262 > [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error. > 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. If _p_ equals _"default"_ or _conditions_ 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_, _pattern_, _internal_, +> _conditions_). +> 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_, _pattern_, _internal_, +> _conditions_), 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. **ESM_FORMAT**(_url_) @@ -1783,11 +1890,11 @@ The resolver can throw the following errors: > 1. Let _scopeURL_ be _url_. > 1. While _scopeURL_ is not the file system root, +> 1. Set _scopeURL_ to the parent URL of _scopeURL_. > 1. If _scopeURL_ ends in a _"node_modules"_ path segment, return **null**. > 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_scopeURL_). > 1. If _pjson_ is not **null**, then > 1. Return _pjson_. -> 1. Set _scopeURL_ to the parent URL of _scopeURL_. > 1. Return **null**. **READ_PACKAGE_JSON**(_packageURL_) diff --git a/doc/api/modules.md b/doc/api/modules.md index e8215a2ace67ed..079430bce78e71 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -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 @@ -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) @@ -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 diff --git a/doc/node.1 b/doc/node.1 index 3cd85d3cb0dfb2..610b21d0eef114 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -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 diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 921bcce2878706..0d15a0d069fd9c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -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 = {}; @@ -1097,16 +1096,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}"`, @@ -1114,37 +1106,25 @@ E('ERR_INVALID_OPT_VALUE', (name, value) => 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); @@ -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) => { @@ -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', diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index c5d6c4221db076..ec8a69e494319b 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -21,23 +21,29 @@ 'use strict'; +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + const { ArrayIsArray, + ArrayPrototypeJoin, Error, JSONParse, Map, - Number, ObjectCreate, ObjectDefineProperty, ObjectFreeze, - ObjectIs, ObjectKeys, - ObjectPrototypeHasOwnProperty, ReflectSet, RegExpPrototypeTest, SafeMap, - String, + SafeSet, + StringPrototypeEndsWith, StringPrototypeIndexOf, + StringPrototypeLastIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeStartsWith, @@ -48,13 +54,14 @@ const { maybeCacheSourceMap, rekeySourceMap } = require('internal/source_map/source_map_cache'); -const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); +const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); const { deprecate } = require('internal/util'); const vm = require('vm'); const assert = require('internal/assert'); const fs = require('fs'); const internalFS = require('internal/fs/utils'); const path = require('path'); +const { sep } = path; const { internalModuleStat } = internalBinding('fs'); const packageJsonReader = require('internal/modules/package_json_reader'); const { safeGetenv } = internalBinding('credentials'); @@ -72,6 +79,7 @@ const manifest = getOptionValue('--experimental-policy') ? require('internal/process/policy').manifest : null; const { compileFunction } = internalBinding('contextify'); +const userConditions = getOptionValue('--conditions'); // Whether any user-provided CJS modules had been loaded (executed). // Used for internal assertions. @@ -80,28 +88,26 @@ let hasLoadedAnyUserCJSModule = false; const { ERR_INVALID_ARG_VALUE, ERR_INVALID_OPT_VALUE, - ERR_INVALID_PACKAGE_CONFIG, - ERR_INVALID_PACKAGE_TARGET, ERR_INVALID_MODULE_SPECIFIER, - ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_REQUIRE_ESM } = require('internal/errors').codes; const { validateString } = require('internal/validators'); const pendingDeprecation = getOptionValue('--pending-deprecation'); -module.exports = { - wrapSafe, Module, toRealPath, readPackageScope, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } -}; - -let asyncESM, ModuleJob, ModuleWrap, kInstantiated; - const { CHAR_FORWARD_SLASH, CHAR_BACKWARD_SLASH, CHAR_COLON } = require('internal/constants'); +const asyncESM = require('internal/process/esm_loader'); +const { kEvaluated } = internalBinding('module_wrap'); +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = require('internal/modules/esm/resolve'); + const isWindows = process.platform === 'win32'; const relativeResolveCache = ObjectCreate(null); @@ -259,6 +265,7 @@ function readPackage(requestPath) { name: parsed.name, main: parsed.main, exports: parsed.exports, + imports: parsed.imports, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); @@ -271,41 +278,31 @@ function readPackage(requestPath) { } function readPackageScope(checkPath) { - const rootSeparatorIndex = checkPath.indexOf(path.sep); + const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); let separatorIndex; - while ( - (separatorIndex = checkPath.lastIndexOf(path.sep)) > rootSeparatorIndex - ) { - checkPath = checkPath.slice(0, separatorIndex); - if (checkPath.endsWith(path.sep + 'node_modules')) + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); + checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); + if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) return false; - const pjson = readPackage(checkPath); + const pjson = readPackage(checkPath + sep); if (pjson) return { + data: pjson, path: checkPath, - data: pjson }; - } + } while (separatorIndex > rootSeparatorIndex); return false; } -function readPackageMain(requestPath) { - const pkg = readPackage(requestPath); - return pkg ? pkg.main : undefined; -} - -function readPackageExports(requestPath) { - const pkg = readPackage(requestPath); - return pkg ? pkg.exports : undefined; -} - function tryPackage(requestPath, exts, isMain, originalPath) { - const pkg = readPackageMain(requestPath); + const pkg = readPackage(requestPath); + const pkgMain = pkg && pkg.main; - if (!pkg) { + if (!pkgMain) { return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); } - const filename = path.resolve(requestPath, pkg); + const filename = path.resolve(requestPath, pkgMain); let actual = tryFile(filename, isMain) || tryExtensions(filename, exts, isMain) || tryExtensions(path.resolve(filename, 'index'), exts, isMain); @@ -325,7 +322,7 @@ function tryPackage(requestPath, exts, isMain, originalPath) { } else if (pendingDeprecation) { const jsonPath = path.resolve(requestPath, 'package.json'); process.emitWarning( - `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + `Invalid 'main' field in '${jsonPath}' of '${pkgMain}'. ` + 'Please either fix that or report it to the module author', 'DeprecationWarning', 'DEP0128' @@ -387,102 +384,45 @@ function findLongestRegisteredExtension(filename) { return '.js'; } +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + function trySelf(parentPath, request) { - const { data: pkg, path: basePath } = readPackageScope(parentPath) || {}; + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; if (!pkg || pkg.exports === undefined) return false; if (typeof pkg.name !== 'string') return false; let expansion; if (request === pkg.name) { - expansion = ''; + expansion = '.'; } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { - expansion = StringPrototypeSlice(request, pkg.name.length); + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); } else { return false; } - const fromExports = applyExports(basePath, expansion); - if (fromExports) { - return tryFile(fromExports, false); - } - assert(fromExports !== false); -} - -function isConditionalDotExportSugar(exports, basePath) { - if (typeof exports === 'string') - return true; - if (ArrayIsArray(exports)) - return true; - if (typeof exports !== 'object') - return false; - let isConditional = false; - let firstCheck = true; - for (const key of ObjectKeys(exports)) { - const curIsConditional = key[0] !== '.'; - if (firstCheck) { - firstCheck = false; - isConditional = curIsConditional; - } else if (isConditional !== curIsConditional) { - throw new ERR_INVALID_PACKAGE_CONFIG(basePath, '"exports" cannot ' + - 'contain some keys starting with \'.\' and some not. The exports ' + - 'object must either be an object of package subpath keys or an ' + - 'object of main entry condition name keys only.'); - } - } - return isConditional; -} - -function applyExports(basePath, expansion) { - const mappingKey = `.${expansion}`; - - let pkgExports = readPackageExports(basePath); - if (pkgExports === undefined || pkgExports === null) - return false; - - if (isConditionalDotExportSugar(pkgExports, basePath)) - pkgExports = { '.': pkgExports }; - - if (typeof pkgExports === 'object') { - if (ObjectPrototypeHasOwnProperty(pkgExports, mappingKey)) { - const mapping = pkgExports[mappingKey]; - return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', - mappingKey); - } - - let dirMatch = ''; - for (const candidateKey of ObjectKeys(pkgExports)) { - if (candidateKey[candidateKey.length - 1] !== '/') continue; - if (candidateKey.length > dirMatch.length && - StringPrototypeStartsWith(mappingKey, candidateKey)) { - dirMatch = candidateKey; - } - } - - if (dirMatch !== '') { - const mapping = pkgExports[dirMatch]; - const subpath = StringPrototypeSlice(mappingKey, dirMatch.length); - const resolved = resolveExportsTarget(pathToFileURL(basePath + '/'), - mapping, subpath, mappingKey); - // Extension searching for folder exports only - const rc = stat(resolved); - if (rc === 0) return resolved; - if (!(RegExpPrototypeTest(trailingSlashRegex, resolved))) { - const exts = ObjectKeys(Module._extensions); - const filename = tryExtensions(resolved, exts, false); - if (filename) return filename; - } - if (rc === 1) { - const exts = ObjectKeys(Module._extensions); - const filename = tryPackage(resolved, exts, false, - basePath + expansion); - if (filename) return filename; - } - // Undefined means not found - return; - } + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions), request, parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; } - - throw new ERR_PACKAGE_PATH_NOT_EXPORTED(basePath, mappingKey); } // This only applies to requests of a specific form: @@ -493,108 +433,21 @@ function resolveExports(nmPath, request) { // The implementation's behavior is meant to mirror resolution in ESM. const [, name, expansion = ''] = StringPrototypeMatch(request, EXPORTS_PATTERN) || []; - if (!name) { - return false; - } - - const basePath = path.resolve(nmPath, name); - const fromExports = applyExports(basePath, expansion); - if (fromExports) { - return tryFile(fromExports, false); - } - return fromExports; -} - -function isArrayIndex(p) { - assert(typeof p === 'string'); - const n = Number(p); - if (String(n) !== p) - return false; - if (ObjectIs(n, +0)) - return true; - if (!Number.isInteger(n)) - return false; - return n >= 0 && n < (2 ** 32) - 1; -} - -function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { - if (typeof target === 'string') { - let resolvedTarget, resolvedTargetPath; - const pkgPathPath = baseUrl.pathname; - if (StringPrototypeStartsWith(target, './')) { - resolvedTarget = new URL(target, baseUrl); - resolvedTargetPath = resolvedTarget.pathname; - if (!StringPrototypeStartsWith(resolvedTargetPath, pkgPathPath) || - StringPrototypeIndexOf(resolvedTargetPath, '/node_modules/', - pkgPathPath.length - 1) !== -1) - resolvedTarget = undefined; - } - if (subpath.length > 0 && target[target.length - 1] !== '/') - resolvedTarget = undefined; - if (resolvedTarget === undefined) - throw new ERR_INVALID_PACKAGE_TARGET(StringPrototypeSlice(baseUrl.pathname - , 0, -1), mappingKey, subpath, target); - const resolved = new URL(subpath, resolvedTarget); - const resolvedPath = resolved.pathname; - if (StringPrototypeStartsWith(resolvedPath, resolvedTargetPath) && - StringPrototypeIndexOf(resolvedPath, '/node_modules/', - pkgPathPath.length - 1) === -1) { - return fileURLToPath(resolved); - } - throw new ERR_INVALID_MODULE_SPECIFIER(StringPrototypeSlice(baseUrl.pathname - , 0, -1), mappingKey); - } else if (ArrayIsArray(target)) { - if (target.length === 0) - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); - let lastException; - for (const targetValue of target) { - try { - return resolveExportsTarget(baseUrl, targetValue, subpath, mappingKey); - } catch (e) { - lastException = e; - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && - e.code !== 'ERR_INVALID_PACKAGE_TARGET') - throw e; - } - } - // Throw last fallback error - assert(lastException !== undefined); - throw lastException; - } else if (typeof target === 'object' && target !== null) { - const keys = ObjectKeys(target); - if (keys.some(isArrayIndex)) { - throw new ERR_INVALID_PACKAGE_CONFIG(baseUrl, '"exports" cannot ' + - 'contain numeric property keys.'); - } - for (const p of keys) { - switch (p) { - case 'node': - case 'require': - try { - return resolveExportsTarget(baseUrl, target[p], subpath, - mappingKey); - } catch (e) { - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; - } - break; - case 'default': - try { - return resolveExportsTarget(baseUrl, target.default, subpath, - mappingKey); - } catch (e) { - if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw e; - } - } + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + if (pkg && pkg.exports !== null && pkg.exports !== undefined) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions), request, null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; } - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); - } else if (target === null) { - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); } - throw new ERR_INVALID_PACKAGE_TARGET( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target); } const trailingSlashRegex = /(?:^|\/)\.?\.$/; @@ -627,12 +480,8 @@ Module._findPath = function(request, paths, isMain) { if (!absoluteRequest) { const exportsResolved = resolveExports(curPath, request); - // Undefined means not found, false means no exports - if (exportsResolved === undefined) - break; - if (exportsResolved) { + if (exportsResolved) return exportsResolved; - } } const basePath = path.resolve(curPath, request); @@ -892,6 +741,7 @@ Module._load = function(request, parent, isMain) { return module.exports; }; +const cjsConditions = new SafeSet(['require', 'node', ...userConditions]); Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { return request; @@ -934,15 +784,34 @@ Module._resolveFilename = function(request, parent, isMain, options) { } if (parent && parent.filename) { - const filename = trySelf(parent.filename, request); - if (filename) { - const cacheKey = request + '\x00' + - (paths.length === 1 ? paths[0] : paths.join('\x00')); - Module._pathCache[cacheKey] = filename; - return filename; + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + if (pkg.data && pkg.data.imports !== null && + pkg.data.imports !== undefined) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), request, parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } } } + // Try module self resoultion first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + // Look up the filename first, since that's the cache key. const filename = Module._findPath(request, paths, isMain, false); if (filename) return filename; @@ -963,6 +832,34 @@ Module._resolveFilename = function(request, parent, isMain, options) { throw err; }; +function finalizeEsmResolution(match, request, parentPath, pkgPath) { + const { resolved, exact } = match; + if (StringPrototypeMatch(resolved, encodedSepRegEx)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + let actual = tryFile(filename); + if (!exact && !actual) { + const exts = ObjectKeys(Module._extensions); + actual = tryExtensions(filename, exts, false) || + tryPackage(filename, exts, false, request); + } + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} // Given a file name, pass it to the proper extension handler. Module.prototype.load = function(filename) { @@ -981,29 +878,13 @@ Module.prototype.load = function(filename) { this.loaded = true; const ESMLoader = asyncESM.ESMLoader; - const url = `${pathToFileURL(filename)}`; - const module = ESMLoader.moduleMap.get(url); // Create module entry at load time to snapshot exports correctly const exports = this.exports; - // Called from cjs translator - if (module !== undefined && module.module !== undefined) { - if (module.module.getStatus() >= kInstantiated) - module.module.setExport('default', exports); - } else { - // Preemptively cache - // We use a function to defer promise creation for async hooks. - ESMLoader.moduleMap.set( - url, - // Module job creation will start promises. - // We make it a function to lazily trigger those promises - // for async hooks compatibility. - () => new ModuleJob(ESMLoader, url, () => - new ModuleWrap(url, undefined, ['default'], function() { - this.setExport('default', exports); - }) - , false /* isMain */, false /* inspectBrk */) - ); - } + // Preemptively cache + if ((!module || module.module === undefined || + module.module.getStatus() < kEvaluated) && + !ESMLoader.cjsCache.has(this)) + ESMLoader.cjsCache.set(this, exports); }; @@ -1206,7 +1087,7 @@ const createRequireError = 'must be a file URL object, file URL string, or ' + function createRequire(filename) { let filepath; - if (filename instanceof URL || + if (isURLInstance(filename) || (typeof filename === 'string' && !path.isAbsolute(filename))) { try { filepath = fileURLToPath(filename); @@ -1282,8 +1163,3 @@ Module.syncBuiltinESMExports = function syncBuiltinESMExports() { // Backwards compatibility Module.Module = Module; - -// We have to load the esm things after module.exports! -asyncESM = require('internal/process/esm_loader'); -ModuleJob = require('internal/modules/esm/module_job'); -({ ModuleWrap, kInstantiated } = internalBinding('module_wrap')); diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index be5868553fa8df..0d1c09f3d38c7f 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -1,9 +1,12 @@ 'use strict'; +// This is needed to avoid cycles in esm/resolve <-> cjs/loader +require('internal/modules/cjs/loader'); + const { FunctionPrototypeBind, ObjectSetPrototypeOf, - SafeMap, + SafeWeakMap, } = primordials; const { @@ -49,7 +52,7 @@ class Loader { this.moduleMap = new ModuleMap(); // Map of already-loaded CJS modules to use - this.cjsCache = new SafeMap(); + this.cjsCache = new SafeWeakMap(); // This hook is called before the first root module is imported. It's a // function that returns a piece of code that runs as a sloppy-mode script. diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 3f11ffc768eedb..ed681541d12723 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -107,16 +107,23 @@ class ModuleJob { await this.loader.resolve(childSpecifier, parentFileUrl); const format = await this.loader.getFormat(childFileURL); if (format === 'commonjs') { - const importStatement = splitStack[1]; - const namedImports = StringPrototypeMatch(importStatement, /{.*}/)[0]; - const destructuringAssignment = StringPrototypeReplace(namedImports, /\s+as\s+/g, ': '); e.message = `The requested module '${childSpecifier}' is expected ` + 'to be of type CommonJS, which does not support named exports. ' + 'CommonJS modules can be imported by importing the default ' + - 'export.\n' + - 'For example:\n' + - `import pkg from '${childSpecifier}';\n` + - `const ${destructuringAssignment} = pkg;`; + 'export.'; + // TODO(@ctavan): The original error stack only provides the single + // line which causes the error. For multi-line import statements we + // cannot generate an equivalent object descructuring assignment by + // just parsing the error stack. + const importStatement = splitStack[1]; + const oneLineNamedImports = StringPrototypeMatch(importStatement, /{.*}/); + if (oneLineNamedImports) { + const destructuringAssignment = + StringPrototypeReplace(oneLineNamedImports[0], /\s+as\s+/g, ': '); + e.message += '\nFor example:\n' + + `import pkg from '${childSpecifier}';\n` + + `const ${destructuringAssignment} = pkg;`; + } const newStack = StringPrototypeSplit(e.stack, '\n'); newStack[3] = `SyntaxError: ${e.message}`; e.stack = ArrayPrototypeJoin(newStack, '\n'); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 987a139c6aae57..92760d201beb4c 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -10,11 +10,11 @@ const { ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, RegExp, + RegExpPrototypeTest, SafeMap, SafeSet, String, StringPrototypeEndsWith, - StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeReplace, StringPrototypeSlice, @@ -22,7 +22,6 @@ const { StringPrototypeStartsWith, StringPrototypeSubstr, } = primordials; -const assert = require('internal/assert'); const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { @@ -32,7 +31,6 @@ const { } = require('fs'); const { getOptionValue } = require('internal/options'); const { sep, relative } = require('path'); -const { Module: CJSModule } = require('internal/modules/cjs/loader'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const typeFlag = getOptionValue('--input-type'); @@ -44,15 +42,19 @@ const { ERR_INVALID_PACKAGE_CONFIG, ERR_INVALID_PACKAGE_TARGET, ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_IMPORT_NOT_DEFINED, ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_UNSUPPORTED_DIR_IMPORT, ERR_UNSUPPORTED_ESM_URL_SCHEME, } = require('internal/errors').codes; +const { Module: CJSModule } = require('internal/modules/cjs/loader'); const packageJsonReader = require('internal/modules/package_json_reader'); -const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']); +const userConditions = getOptionValue('--conditions'); +const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); + function getConditionsSet(conditions) { if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { if (!ArrayIsArray(conditions)) { @@ -75,15 +77,7 @@ function tryStatSync(path) { } } -/** - * - * '/foo/package.json' -> '/foo' - */ -function removePackageJsonFromPath(path) { - return StringPrototypeSlice(path, 0, path.length - 13); -} - -function getPackageConfig(path) { +function getPackageConfig(path, specifier, base) { const existing = packageJSONCache.get(path); if (existing !== undefined) { return existing; @@ -91,11 +85,13 @@ function getPackageConfig(path) { const source = packageJsonReader.read(path).string; if (source === undefined) { const packageConfig = { + pjsonPath: path, exists: false, main: undefined, name: undefined, type: 'none', - exports: undefined + exports: undefined, + imports: undefined, }; packageJSONCache.set(path, packageConfig); return packageConfig; @@ -105,35 +101,42 @@ function getPackageConfig(path) { try { packageJSON = JSONParse(source); } catch (error) { - const errorPath = removePackageJsonFromPath(path); - throw new ERR_INVALID_PACKAGE_CONFIG(errorPath, error.message, true); + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); } - let { main, name, type } = packageJSON; + let { imports, main, name, type } = packageJSON; const { exports } = packageJSON; + if (typeof imports !== 'object' || imports === null) imports = undefined; if (typeof main !== 'string') main = undefined; if (typeof name !== 'string') name = undefined; // Ignore unknown types for forwards compatibility if (type !== 'module' && type !== 'commonjs') type = 'none'; const packageConfig = { + pjsonPath: path, exists: true, main, name, type, - exports + exports, + imports, }; packageJSONCache.set(path, packageConfig); return packageConfig; } -function getPackageScopeConfig(resolved, base) { +function getPackageScopeConfig(resolved) { let packageJSONUrl = new URL('./package.json', resolved); while (true) { const packageJSONPath = packageJSONUrl.pathname; if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) break; - const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), base); + const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), + resolved); if (packageConfig.exists) return packageConfig; const lastPackageJSONUrl = packageJSONUrl; @@ -143,14 +146,17 @@ function getPackageScopeConfig(resolved, base) { // (can't just check "/package.json" for Windows support). if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; } + const packageJSONPath = fileURLToPath(packageJSONUrl); const packageConfig = { + pjsonPath: packageJSONPath, exists: false, main: undefined, name: undefined, type: 'none', - exports: undefined + exports: undefined, + imports: undefined, }; - packageJSONCache.set(fileURLToPath(packageJSONUrl), packageConfig); + packageJSONCache.set(packageJSONPath, packageConfig); return packageConfig; } @@ -166,7 +172,7 @@ function fileExists(url) { return tryStatSync(fileURLToPath(url)).isFile(); } -function legacyMainResolve(packageJSONUrl, packageConfig) { +function legacyMainResolve(packageJSONUrl, packageConfig, base) { let guess; if (packageConfig.main !== undefined) { // Note: fs check redundances will be handled by Descriptor cache here. @@ -211,7 +217,8 @@ function legacyMainResolve(packageJSONUrl, packageConfig) { return guess; } // Not found. - return undefined; + throw new ERR_MODULE_NOT_FOUND( + fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); } function resolveExtensionsWithTryExactName(search) { @@ -233,85 +240,117 @@ function resolveIndex(search) { return resolveExtensions(new URL('index', search)); } +const encodedSepRegEx = /%2F|%2C/i; function finalizeResolution(resolved, base) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved.pathname, 'must not include encoded "/" or "\\" characters', + fileURLToPath(base)); + + const path = fileURLToPath(resolved); if (getOptionValue('--experimental-specifier-resolution') === 'node') { let file = resolveExtensionsWithTryExactName(resolved); if (file !== undefined) return file; - if (!StringPrototypeEndsWith(resolved.pathname, '/')) { - file = resolveIndex(new URL(`${resolved.pathname}/`, base)); + if (!StringPrototypeEndsWith(path, '/')) { + file = resolveIndex(new URL(`${resolved}/`)); + if (file !== undefined) return file; } else { - file = resolveIndex(resolved); + return resolveIndex(resolved) || resolved; } - if (file !== undefined) return file; throw new ERR_MODULE_NOT_FOUND( resolved.pathname, fileURLToPath(base), 'module'); } - const path = fileURLToPath(resolved); - const stats = tryStatSync(path); - + const stats = tryStatSync(StringPrototypeEndsWith(path, '/') ? + StringPrototypeSlice(path, -1) : path); if (stats.isDirectory()) { - const err = new ERR_UNSUPPORTED_DIR_IMPORT( - path || resolved.pathname, fileURLToPath(base)); + const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base)); err.url = String(resolved); throw err; } else if (!stats.isFile()) { throw new ERR_MODULE_NOT_FOUND( - path || resolved.pathname, fileURLToPath(base), 'module'); + path || resolved.pathname, base && fileURLToPath(base), 'module'); } return resolved; } +function throwImportNotDefined(specifier, packageJSONUrl, base) { + throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( + specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), + fileURLToPath(base)); +} + function throwExportsNotFound(subpath, packageJSONUrl, base) { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); + fileURLToPath(new URL('.', packageJSONUrl)), subpath, + base && fileURLToPath(base)); } -function throwSubpathInvalid(subpath, packageJSONUrl, base) { - throw new ERR_INVALID_MODULE_SPECIFIER( - fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); +function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) { + const reason = `request is not a valid subpath for the "${internal ? + 'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, + base && fileURLToPath(base)); } -function throwExportsInvalid( - subpath, target, packageJSONUrl, base) { +function throwInvalidPackageTarget( + subpath, target, packageJSONUrl, internal, base) { if (typeof target === 'object' && target !== null) { target = JSONStringify(target, null, ''); - } else if (ArrayIsArray(target)) { - target = `[${target}]`; } else { target = `${target}`; } throw new ERR_INVALID_PACKAGE_TARGET( - fileURLToPath(packageJSONUrl), null, subpath, target, fileURLToPath(base)); + fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, + internal, base && fileURLToPath(base)); } -function resolveExportsTargetString( - target, subpath, match, packageJSONUrl, base) { - if (target[0] !== '.' || target[1] !== '/' || - (subpath !== '' && target[target.length - 1] !== '/')) { - throwExportsInvalid(match, target, packageJSONUrl, base); +const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; +const patternRegEx = /\*/g; + +function resolvePackageTargetString( + target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (!StringPrototypeStartsWith(target, './')) { + if (internal && !StringPrototypeStartsWith(target, '../') && + !StringPrototypeStartsWith(target, '/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch {} + if (!isURL) { + const exportTarget = pattern ? + StringPrototypeReplace(target, patternRegEx, subpath) : + target + subpath; + return packageResolve(exportTarget, packageJSONUrl, conditions); + } + } + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } + if (RegExpPrototypeTest(invalidSegmentRegEx, StringPrototypeSlice(target, 2))) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + const resolved = new URL(target, packageJSONUrl); const resolvedPath = resolved.pathname; const packagePath = new URL('.', packageJSONUrl).pathname; - if (!StringPrototypeStartsWith(resolvedPath, packagePath) || - StringPrototypeIncludes( - resolvedPath, '/node_modules/', packagePath.length - 1)) { - throwExportsInvalid(match, target, packageJSONUrl, base); - } + if (!StringPrototypeStartsWith(resolvedPath, packagePath)) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); if (subpath === '') return resolved; - const subpathResolved = new URL(subpath, resolved); - const subpathResolvedPath = subpathResolved.pathname; - if (!StringPrototypeStartsWith(subpathResolvedPath, resolvedPath) || - StringPrototypeIncludes(subpathResolvedPath, - '/node_modules/', packagePath.length - 1)) { - throwSubpathInvalid(match + subpath, packageJSONUrl, base); - } - return subpathResolved; + + if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) + throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); + + if (pattern) + return new URL(StringPrototypeReplace(resolved.href, patternRegEx, + subpath)); + return new URL(subpath, resolved); } /** @@ -324,36 +363,40 @@ function isArrayIndex(key) { return keyNum >= 0 && keyNum < 0xFFFF_FFFF; } -function resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions) { +function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, + base, pattern, internal, conditions) { if (typeof target === 'string') { - const resolved = resolveExportsTargetString( - target, subpath, packageSubpath, packageJSONUrl, base); - return finalizeResolution(resolved, base); + return resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, + conditions); } else if (ArrayIsArray(target)) { if (target.length === 0) - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return null; let lastException; for (let i = 0; i < target.length; i++) { const targetItem = target[i]; let resolved; try { - resolved = resolveExportsTarget( - packageJSONUrl, targetItem, subpath, packageSubpath, base, - conditions); + resolved = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, conditions); } catch (e) { lastException = e; - if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || - e.code === 'ERR_INVALID_PACKAGE_TARGET') { + if (e.code === 'ERR_INVALID_PACKAGE_TARGET') continue; - } throw e; } - - return finalizeResolution(resolved, base); + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } + return resolved; } - assert(lastException !== undefined); + if (lastException === undefined || lastException === null) + return lastException; throw lastException; } else if (typeof target === 'object' && target !== null) { const keys = ObjectGetOwnPropertyNames(target); @@ -361,29 +404,28 @@ function resolveExportsTarget( const key = keys[i]; if (isArrayIndex(key)) { throw new ERR_INVALID_PACKAGE_CONFIG( - fileURLToPath(packageJSONUrl), - '"exports" cannot contain numeric property keys'); + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain numeric property keys.'); } } for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key === 'default' || conditions.has(key)) { const conditionalTarget = target[key]; - try { - return resolveExportsTarget( - packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - conditions); - } catch (e) { - if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') continue; - throw e; - } + const resolved = resolvePackageTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, + pattern, internal, conditions); + if (resolved === undefined) + continue; + return resolved; } } - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return undefined; } else if (target === null) { - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return null; } - throwExportsInvalid(packageSubpath, target, packageJSONUrl, base); + throwInvalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, + base); } function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { @@ -400,7 +442,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { isConditionalSugar = curIsConditionalSugar; } else if (isConditionalSugar !== curIsConditionalSugar) { throw new ERR_INVALID_PACKAGE_CONFIG( - fileURLToPath(packageJSONUrl), + fileURLToPath(packageJSONUrl), base, '"exports" cannot contain some keys starting with \'.\' and some not.' + ' The exports object must either be an object of package subpath keys' + ' or an object of main entry condition name keys only.'); @@ -409,38 +451,6 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { return isConditionalSugar; } - -function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) { - if (packageConfig.exists) { - const exports = packageConfig.exports; - if (exports !== undefined) { - if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { - return resolveExportsTarget(packageJSONUrl, exports, '', '', base, - conditions); - } else if (typeof exports === 'object' && exports !== null) { - const target = exports['.']; - if (target !== undefined) - return resolveExportsTarget(packageJSONUrl, target, '', '', base, - conditions); - } - - throw new ERR_PACKAGE_PATH_NOT_EXPORTED(packageJSONUrl, '.'); - } - if (getOptionValue('--experimental-specifier-resolution') === 'node') { - if (packageConfig.main !== undefined) { - return finalizeResolution( - new URL(packageConfig.main, packageJSONUrl), base); - } - return finalizeResolution( - new URL('index', packageJSONUrl), base); - } - return legacyMainResolve(packageJSONUrl, packageConfig); - } - - throw new ERR_MODULE_NOT_FOUND( - fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); -} - /** * @param {URL} packageJSONUrl * @param {string} packageSubpath @@ -451,44 +461,107 @@ function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) { */ function packageExportsResolve( packageJSONUrl, packageSubpath, packageConfig, base, conditions) { - const exports = packageConfig.exports; - if (exports === undefined || - isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { - throwExportsNotFound(packageSubpath, packageJSONUrl, base); - } - + let exports = packageConfig.exports; + if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) + exports = { '.': exports }; if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { const target = exports[packageSubpath]; - const resolved = resolveExportsTarget( - packageJSONUrl, target, '', packageSubpath, base, conditions); - return finalizeResolution(resolved, base); + const resolved = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, false, conditions + ); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return { resolved, exact: true }; } let bestMatch = ''; const keys = ObjectGetOwnPropertyNames(exports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key[key.length - 1] !== '/') continue; - if (StringPrototypeStartsWith(packageSubpath, key) && + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(packageSubpath, + StringPrototypeSlice(key, 0, -1)) && + packageSubpath.length >= key.length && key.length > bestMatch.length) { bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(packageSubpath, key) && + key.length > bestMatch.length) { + bestMatch = key; } } if (bestMatch) { const target = exports[bestMatch]; - const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); - const resolved = resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions); - return finalizeResolution(resolved, base); + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, + bestMatch, base, pattern, false, + conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return { resolved, exact: pattern }; } throwExportsNotFound(packageSubpath, packageJSONUrl, base); } +function packageImportsResolve(name, base, conditions) { + if (name === '#' || StringPrototypeStartsWith(name, '#/')) { + const reason = 'is not a valid internal imports specifier name'; + throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); + } + let packageJSONUrl; + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + const imports = packageConfig.imports; + if (imports) { + if (ObjectPrototypeHasOwnProperty(imports, name)) { + const resolved = resolvePackageTarget( + packageJSONUrl, imports[name], '', name, base, false, true, conditions + ); + if (resolved !== null) + return { resolved, exact: true }; + } else { + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(imports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(name, + StringPrototypeSlice(key, 0, -1)) && + name.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(name, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(name, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, pattern, true, + conditions); + if (resolved !== null) + return { resolved, exact: pattern }; + } + } + } + } + throwImportNotDefined(name, packageJSONUrl, base); +} + function getPackageType(url) { - const packageConfig = getPackageScopeConfig(url, url); + const packageConfig = getPackageScopeConfig(url); return packageConfig.type; } @@ -526,35 +599,21 @@ function packageResolve(specifier, base, conditions) { if (!validPackageName) { throw new ERR_INVALID_MODULE_SPECIFIER( - specifier, undefined, fileURLToPath(base)); + specifier, 'is not a valid package name', fileURLToPath(base)); } - const packageSubpath = separatorIndex === -1 ? - '' : '.' + StringPrototypeSlice(specifier, separatorIndex); + const packageSubpath = '.' + (separatorIndex === -1 ? '' : + StringPrototypeSlice(specifier, separatorIndex)); // ResolveSelf - const packageConfig = getPackageScopeConfig(base, base); + const packageConfig = getPackageScopeConfig(base); if (packageConfig.exists) { - // TODO(jkrems): Find a way to forward the pair/iterator already generated - // while executing GetPackageScopeConfig - let packageJSONUrl; - for (const [ filename, packageConfigCandidate ] of packageJSONCache) { - if (packageConfig === packageConfigCandidate) { - packageJSONUrl = pathToFileURL(filename); - break; - } - } - if (packageJSONUrl !== undefined && - packageConfig.name === packageName && - packageConfig.exports !== undefined) { - if (packageSubpath === './') { - return new URL('./', packageJSONUrl); - } else if (packageSubpath === '') { - return packageMainResolve(packageJSONUrl, packageConfig, base, - conditions); - } + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && + packageConfig.exports !== undefined && packageConfig.exports !== null) { return packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions); + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; } } @@ -563,7 +622,8 @@ function packageResolve(specifier, base, conditions) { let packageJSONPath = fileURLToPath(packageJSONUrl); let lastPath; do { - const stat = tryStatSync(removePackageJsonFromPath(packageJSONPath)); + const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0, + packageJSONPath.length - 13)); if (!stat.isDirectory()) { lastPath = packageJSONPath; packageJSONUrl = new URL((isScoped ? @@ -574,18 +634,14 @@ function packageResolve(specifier, base, conditions) { } // Package match. - const packageConfig = getPackageConfig(packageJSONPath, base); - if (packageSubpath === './') { - return new URL('./', packageJSONUrl); - } else if (packageSubpath === '') { - return packageMainResolve(packageJSONUrl, packageConfig, base, - conditions); - } else if (packageConfig.exports !== undefined) { + const packageConfig = getPackageConfig(packageJSONPath, specifier, base); + if (packageConfig.exports !== undefined && packageConfig.exports !== null) return packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions); - } - return finalizeResolution( - new URL(packageSubpath, packageJSONUrl), base); + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; + if (packageSubpath === '.') + return legacyMainResolve(packageJSONUrl, packageConfig, base); + return new URL(packageSubpath, packageJSONUrl); // Cross-platform root check. } while (packageJSONPath.length !== lastPath.length); @@ -626,11 +682,13 @@ function moduleResolve(specifier, base, conditions) { let resolved; if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { resolved = new URL(specifier, base); + } else if (specifier[0] === '#') { + ({ resolved } = packageImportsResolve(specifier, base, conditions)); } else { try { resolved = new URL(specifier); } catch { - return packageResolve(specifier, base, conditions); + resolved = packageResolve(specifier, base, conditions); } } return finalizeResolution(resolved, base); @@ -695,7 +753,7 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) { if (parsed && parsed.protocol === 'nodejs:') return { url: specifier }; if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:') - throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(); + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed); if (NativeModule.canBeRequiredByUsers(specifier)) { return { url: 'nodejs:' + specifier @@ -764,5 +822,8 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) { module.exports = { DEFAULT_CONDITIONS, defaultResolve, - getPackageType + encodedSepRegEx, + getPackageType, + packageExportsResolve, + packageImportsResolve }; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index f314ba96b3476c..99bf560657ab20 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -48,6 +48,8 @@ const debug = debuglog('esm'); const translators = new SafeMap(); exports.translators = translators; +const asyncESM = require('internal/process/esm_loader'); + let DECODER = null; function assertBufferSource(body, allowString, hookName) { if (allowString && typeof body === 'string') { @@ -80,21 +82,14 @@ function errPath(url) { return url; } -let esmLoader; async function importModuleDynamically(specifier, { url }) { - if (!esmLoader) { - esmLoader = require('internal/process/esm_loader').ESMLoader; - } - return esmLoader.import(specifier, url); + return asyncESM.ESMLoader.import(specifier, url); } function createImportMetaResolve(defaultParentUrl) { return async function resolve(specifier, parentUrl = defaultParentUrl) { - if (!esmLoader) { - esmLoader = require('internal/process/esm_loader').ESMLoader; - } return PromisePrototypeCatch( - esmLoader.resolve(specifier, parentUrl), + asyncESM.ESMLoader.resolve(specifier, parentUrl), (error) => ( error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? error.url : PromiseReject(error)) @@ -132,27 +127,18 @@ const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; translators.set('commonjs', function commonjsStrategy(url, isMain) { debug(`Translating CJSModule ${url}`); - const pathname = internalURLModule.fileURLToPath(new URL(url)); - const cached = this.cjsCache.get(url); - if (cached) { - this.cjsCache.delete(url); - return cached; - } - const module = CJSModule._cache[ - isWindows ? StringPrototypeReplace(pathname, winSepRegEx, '\\') : pathname - ]; - if (module && module.loaded) { - const exports = module.exports; - return new ModuleWrap(url, undefined, ['default'], function() { - this.setExport('default', exports); - }); - } return new ModuleWrap(url, undefined, ['default'], function() { debug(`Loading CJSModule ${url}`); - // We don't care about the return val of _load here because Module#load - // will handle it for us by checking the loader registry and filling the - // exports like above - CJSModule._load(pathname, undefined, isMain); + const pathname = internalURLModule.fileURLToPath(new URL(url)); + let exports; + const cachedModule = CJSModule._cache[pathname]; + if (cachedModule && asyncESM.ESMLoader.cjsCache.has(cachedModule)) { + exports = asyncESM.ESMLoader.cjsCache.get(cachedModule); + asyncESM.ESMLoader.cjsCache.delete(cachedModule); + } else { + exports = CJSModule._load(pathname, undefined, isMain); + } + this.setExport('default', exports); }); }); diff --git a/src/node_file.cc b/src/node_file.cc index ecf49dead7cdaa..e5b5a298b24ee5 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -908,6 +908,7 @@ static void InternalModuleReadJSON(const FunctionCallbackInfo& args) { if (0 == memcmp(s, "type", 4)) break; } else if (n == 7) { if (0 == memcmp(s, "exports", 7)) break; + if (0 == memcmp(s, "imports", 7)) break; } } diff --git a/src/node_options.cc b/src/node_options.cc index 53e4c9bb3a315f..824004631f5301 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -283,6 +283,10 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--conditions", + "additional user conditions for conditional exports and imports", + &EnvironmentOptions::conditions, + kAllowedInEnvironment); AddOption("--diagnostic-dir", "set dir for all output files" " (default: current working directory)", diff --git a/src/node_options.h b/src/node_options.h index 603edb0c405270..1e7ffadf0dafbc 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -100,6 +100,7 @@ class DebugOptions : public Options { class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; + std::vector conditions; bool enable_source_maps = false; bool experimental_json_modules = false; bool experimental_modules = false; diff --git a/test/es-module/test-esm-cjs-named-error.mjs b/test/es-module/test-esm-cjs-named-error.mjs index d71dc959e21fb7..e9ddc67c0fbcea 100644 --- a/test/es-module/test-esm-cjs-named-error.mjs +++ b/test/es-module/test-esm-cjs-named-error.mjs @@ -10,6 +10,10 @@ const expectedRelative = 'The requested module \'./fail.cjs\' is expected to ' + 'import pkg from \'./fail.cjs\';\n' + 'const { comeOn } = pkg;'; +const expectedWithoutExample = 'The requested module \'./fail.cjs\' is ' + + 'expected to be of type CommonJS, which does not support named exports. ' + + 'CommonJS modules can be imported by importing the default export.'; + const expectedRenamed = 'The requested module \'./fail.cjs\' is expected to ' + 'be of type CommonJS, which does not support named exports. CommonJS ' + 'modules can be imported by importing the default export.\n' + @@ -52,6 +56,13 @@ rejects(async () => { message: expectedRenamed }, 'should correctly format named imports with renames'); +rejects(async () => { + await import(`${fixtureBase}/multi-line.mjs`); +}, { + name: 'SyntaxError', + message: expectedWithoutExample, +}, 'should correctly format named imports across multiple lines'); + rejects(async () => { await import(`${fixtureBase}/json-hack.mjs`); }, { diff --git a/test/es-module/test-esm-custom-exports.mjs b/test/es-module/test-esm-custom-exports.mjs new file mode 100644 index 00000000000000..cf0557fa44215e --- /dev/null +++ b/test/es-module/test-esm-custom-exports.mjs @@ -0,0 +1,10 @@ +// Flags: --conditions=custom-condition --conditions another +import { mustCall } from '../common/index.mjs'; +import { strictEqual } from 'assert'; +import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs'; +[requireFixture, importFixture].forEach((loadFixture) => { + loadFixture('pkgexports/condition') + .then(mustCall((actual) => { + strictEqual(actual.default, 'from custom condition'); + })); +}); diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index e01b86eed143ed..4e87866b2bad3b 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -8,15 +8,11 @@ const absolutePath = require.resolve('../fixtures/es-modules/test-esm-ok.mjs'); const targetURL = new URL('file:///'); targetURL.pathname = absolutePath; -function expectErrorProperty(result, propertyKey, value) { - Promise.resolve(result) - .catch(common.mustCall((error) => { - assert.strictEqual(error[propertyKey], value); - })); -} - -function expectModuleError(result, err) { - expectErrorProperty(result, 'code', err); +function expectModuleError(result, code, message) { + Promise.resolve(result).catch(common.mustCall((error) => { + assert.strictEqual(error.code, code); + if (message) assert.strictEqual(error.message, message); + })); } function expectOkNamespace(result) { @@ -63,4 +59,13 @@ function expectFsNamespace(result) { 'ERR_MODULE_NOT_FOUND'); expectModuleError(import('http://example.com/foo.js'), 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + if (common.isWindows) { + const msg = + 'Only file and data URLs are supported by the default ESM loader. ' + + 'On Windows, absolute paths must be valid file:// URLs. ' + + "Received protocol 'c:'"; + expectModuleError(import('C:\\example\\foo.mjs'), + 'ERR_UNSUPPORTED_ESM_URL_SCHEME', + msg); + } })(); diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index a0348d4a1ab0b2..d234099732e3aa 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -1,5 +1,6 @@ import { mustCall } from '../common/index.mjs'; import { ok, deepStrictEqual, strictEqual } from 'assert'; +import { sep } from 'path'; import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs'; import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; @@ -32,6 +33,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; { default: 'self-cjs' } : { default: 'self-mjs' }], // Resolve self sugar ['pkgexports-sugar', { default: 'main' }], + // Path patterns + ['pkgexports/subpath/sub-dir1', { default: 'main' }], + ['pkgexports/features/dir1', { default: 'main' }] ]); if (isRequire) { @@ -118,7 +122,8 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; for (const [specifier, subpath] of invalidSpecifiers) { loadFixture(specifier).catch(mustCall((err) => { strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); - assertStartsWith(err.message, 'Package subpath '); + assertStartsWith(err.message, 'Invalid module '); + assertIncludes(err.message, 'is not a valid subpath'); assertIncludes(err.message, subpath); })); } @@ -134,9 +139,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; const notFoundExports = new Map([ // Non-existing file - ['pkgexports/sub/not-a-file.js', 'pkgexports/sub/not-a-file.js'], + ['pkgexports/sub/not-a-file.js', `pkgexports${sep}not-a-file.js`], // No extension lookups - ['pkgexports/no-ext', 'pkgexports/no-ext'], + ['pkgexports/no-ext', `pkgexports${sep}asdf`], ]); if (!isRequire) { @@ -152,16 +157,14 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; for (const [specifier, request] of notFoundExports) { loadFixture(specifier).catch(mustCall((err) => { strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND'); - // ESM returns a full file path - assertStartsWith(err.message, isRequire ? - `Cannot find module '${request}'` : - 'Cannot find module'); + assertIncludes(err.message, request); + assertStartsWith(err.message, 'Cannot find module'); })); } // The use of %2F escapes in paths fails loading loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => { - strictEqual(err.code, 'ERR_INVALID_FILE_URL_PATH'); + strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); })); // Package export with numeric index properties must throw a validation error diff --git a/test/es-module/test-esm-imports.mjs b/test/es-module/test-esm-imports.mjs new file mode 100644 index 00000000000000..694496a2ff2c93 --- /dev/null +++ b/test/es-module/test-esm-imports.mjs @@ -0,0 +1,117 @@ +import { mustCall } from '../common/index.mjs'; +import { ok, deepStrictEqual, strictEqual } from 'assert'; + +import importer from '../fixtures/es-modules/pkgimports/importer.js'; +import { requireFixture } from '../fixtures/pkgexports.mjs'; + +const { requireImport, importImport } = importer; + +[requireImport, importImport].forEach((loadFixture) => { + const isRequire = loadFixture === requireImport; + + const internalImports = new Map([ + // Base case + ['#test', { default: 'test' }], + // import / require conditions + ['#branch', { default: isRequire ? 'requirebranch' : 'importbranch' }], + // Subpath imports + ['#subpath/x.js', { default: 'xsubpath' }], + // External imports + ['#external', { default: 'asdf' }], + // External subpath imports + ['#external/subpath/asdf.js', { default: 'asdf' }], + ]); + + for (const [validSpecifier, expected] of internalImports) { + if (validSpecifier === null) continue; + + loadFixture(validSpecifier) + .then(mustCall((actual) => { + deepStrictEqual({ ...actual }, expected); + })); + } + + const invalidImportTargets = new Set([ + // External subpath import without trailing slash + ['#external/invalidsubpath/x', '#external/invalidsubpath/'], + // Target steps below the package base + ['#belowbase', '#belowbase'], + // Target is a URL + ['#url', '#url'], + ]); + + for (const [specifier, subpath] of invalidImportTargets) { + loadFixture(specifier).catch(mustCall((err) => { + strictEqual(err.code, 'ERR_INVALID_PACKAGE_TARGET'); + assertStartsWith(err.message, 'Invalid "imports"'); + assertIncludes(err.message, subpath); + assertNotIncludes(err.message, 'targets must start with'); + })); + } + + const invalidImportSpecifiers = new Map([ + // Backtracking below the package base + ['#subpath/sub/../../../belowbase', 'request is not a valid subpath'], + // Percent-encoded slash errors + ['#external/subpath/x%2Fy', 'must not include encoded "/"'], + // Target must have a name + ['#', '#'], + // Initial slash target must have a leading name + ['#/initialslash', '#/initialslash'], + // Percent-encoded target paths + ['#percent', 'must not include encoded "/"'], + ]); + + for (const [specifier, expected] of invalidImportSpecifiers) { + loadFixture(specifier).catch(mustCall((err) => { + strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); + assertStartsWith(err.message, 'Invalid module'); + assertIncludes(err.message, expected); + })); + } + + const undefinedImports = new Set([ + // Missing import + '#missing', + // Explicit null import + '#null', + // No condition match import + '#nullcondition', + // Null subpath shadowing + '#subpath/nullshadow/x', + ]); + + for (const specifier of undefinedImports) { + loadFixture(specifier).catch(mustCall((err) => { + strictEqual(err.code, 'ERR_PACKAGE_IMPORT_NOT_DEFINED'); + assertStartsWith(err.message, 'Package import '); + assertIncludes(err.message, specifier); + })); + } + + // Handle not found for the defined imports target not existing + loadFixture('#notfound').catch(mustCall((err) => { + strictEqual(err.code, + isRequire ? 'MODULE_NOT_FOUND' : 'ERR_MODULE_NOT_FOUND'); + })); +}); + +// CJS resolver must still support #package packages in node_modules +requireFixture('#cjs').then(mustCall((actual) => { + strictEqual(actual.default, 'cjs backcompat'); +})); + +function assertStartsWith(actual, expected) { + const start = actual.toString().substr(0, expected.length); + strictEqual(start, expected); +} + +function assertIncludes(actual, expected) { + ok(actual.toString().indexOf(expected) !== -1, + `${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`); +} + +function assertNotIncludes(actual, expected) { + ok(actual.toString().indexOf(expected) === -1, + `${JSON.stringify(actual)} doesn't include ${JSON.stringify(expected)}`); +} diff --git a/test/es-module/test-esm-invalid-pjson.js b/test/es-module/test-esm-invalid-pjson.js index 83f4ad5baba4a7..9f4711321230bf 100644 --- a/test/es-module/test-esm-invalid-pjson.js +++ b/test/es-module/test-esm-invalid-pjson.js @@ -19,11 +19,9 @@ child.on('close', mustCall((code, signal) => { strictEqual(signal, null); ok( stderr.includes( - [ - '[ERR_INVALID_PACKAGE_CONFIG]: ', - `Invalid package config ${invalidJson}, `, - `Unexpected token } in JSON at position ${isWindows ? 16 : 14}` - ].join(''), + `[ERR_INVALID_PACKAGE_CONFIG]: Invalid package config ${invalidJson} ` + + `while importing "invalid-pjson" from ${entry}. ` + + `Unexpected token } in JSON at position ${isWindows ? 16 : 14}` ), stderr); })); diff --git a/test/fixtures/es-modules/package-cjs-named-error/fail.cjs b/test/fixtures/es-modules/package-cjs-named-error/fail.cjs index 40c512ab0e5ad2..cab82d3eb60d60 100644 --- a/test/fixtures/es-modules/package-cjs-named-error/fail.cjs +++ b/test/fixtures/es-modules/package-cjs-named-error/fail.cjs @@ -1,3 +1,4 @@ module.exports = { - comeOn: 'fhqwhgads' + comeOn: 'fhqwhgads', + everybody: 'to the limit', }; diff --git a/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs b/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs new file mode 100644 index 00000000000000..a4f80eba042576 --- /dev/null +++ b/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs @@ -0,0 +1,4 @@ +import { + comeOn, + everybody, +} from './fail.cjs'; diff --git a/test/fixtures/es-modules/pjson-invalid/package.json b/test/fixtures/es-modules/pjson-invalid/package.json new file mode 100644 index 00000000000000..c91736ab5c7dbc --- /dev/null +++ b/test/fixtures/es-modules/pjson-invalid/package.json @@ -0,0 +1 @@ +syntax error diff --git a/test/fixtures/es-modules/pkgimports/importbranch.js b/test/fixtures/es-modules/pkgimports/importbranch.js new file mode 100644 index 00000000000000..ebae53309112a8 --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/importbranch.js @@ -0,0 +1,2 @@ +module.exports = 'importbranch'; + diff --git a/test/fixtures/es-modules/pkgimports/importer.js b/test/fixtures/es-modules/pkgimports/importer.js new file mode 100644 index 00000000000000..30fe06bd613492 --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/importer.js @@ -0,0 +1,4 @@ +module.exports = { + importImport: x => import(x), + requireImport: x => Promise.resolve(x).then(x => ({ default: require(x) })) +}; diff --git a/test/fixtures/es-modules/pkgimports/package.json b/test/fixtures/es-modules/pkgimports/package.json new file mode 100644 index 00000000000000..a2224b39ddd2ac --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/package.json @@ -0,0 +1,30 @@ +{ + "imports": { + "#test": "./test.js", + "#branch": { + "import": "./importbranch.js", + "require": "./requirebranch.js" + }, + "#subpath/*": "./sub/*", + "#external": "pkgexports/valid-cjs", + "#external/subpath/*": "pkgexports/sub/*", + "#external/invalidsubpath/": "pkgexports/sub", + "#belowbase": "../belowbase", + "#url": "some:url", + "#null": null, + "#nullcondition": { + "import": { + "default": null + }, + "require": { + "default": null + }, + "default": "./test.js" + }, + "#subpath/nullshadow/": [null], + "#": "./test.js", + "#/initialslash": "./test.js", + "#notfound": "./notfound.js", + "#percent": "./..%2F/x.js" + } +} diff --git a/test/fixtures/es-modules/pkgimports/requirebranch.js b/test/fixtures/es-modules/pkgimports/requirebranch.js new file mode 100644 index 00000000000000..fd58e34be95332 --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/requirebranch.js @@ -0,0 +1,2 @@ +module.exports = 'requirebranch'; + diff --git a/test/fixtures/es-modules/pkgimports/sub/x.js b/test/fixtures/es-modules/pkgimports/sub/x.js new file mode 100644 index 00000000000000..48cca8c5646659 --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/sub/x.js @@ -0,0 +1,2 @@ +module.exports = 'xsubpath'; + diff --git a/test/fixtures/es-modules/pkgimports/test.js b/test/fixtures/es-modules/pkgimports/test.js new file mode 100644 index 00000000000000..37a4648424da6a --- /dev/null +++ b/test/fixtures/es-modules/pkgimports/test.js @@ -0,0 +1 @@ +module.exports = 'test'; diff --git a/test/fixtures/node_modules/#cjs/index.js b/test/fixtures/node_modules/#cjs/index.js new file mode 100644 index 00000000000000..c60af759886ce6 --- /dev/null +++ b/test/fixtures/node_modules/#cjs/index.js @@ -0,0 +1,2 @@ +module.exports = 'cjs backcompat'; + diff --git a/test/fixtures/node_modules/pkgexports/custom-condition.js b/test/fixtures/node_modules/pkgexports/custom-condition.js new file mode 100644 index 00000000000000..63d77460d8d6b7 --- /dev/null +++ b/test/fixtures/node_modules/pkgexports/custom-condition.js @@ -0,0 +1 @@ +module.exports = 'from custom condition'; diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index b99e5c7b79f6a8..240122d4aaec95 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -21,7 +21,10 @@ "./nofallback2": [null, {}, "builtin:x"], "./nodemodules": "./node_modules/internalpkg/x.js", "./condition": [{ - "custom-condition": "./custom-condition.mjs", + "custom-condition": { + "import": "./custom-condition.mjs", + "require": "./custom-condition.js" + }, "import": "///overridden", "require": { "require": { @@ -44,6 +47,8 @@ "require": "./resolve-self-invalid.js", "import": "./resolve-self-invalid.mjs" }, - "./subpath/": "./subpath/" + "./subpath/": "./subpath/", + "./subpath/sub-*": "./subpath/dir1/*.js", + "./features/*": "./subpath/*/*.js" } } diff --git a/test/fixtures/self_ref_module/index.js b/test/fixtures/self_ref_module/index.js new file mode 100644 index 00000000000000..7faa73693b54aa --- /dev/null +++ b/test/fixtures/self_ref_module/index.js @@ -0,0 +1,4 @@ +'use strict' + +module.exports = 'Self resolution working'; + diff --git a/test/fixtures/self_ref_module/package.json b/test/fixtures/self_ref_module/package.json new file mode 100644 index 00000000000000..7280b184c71357 --- /dev/null +++ b/test/fixtures/self_ref_module/package.json @@ -0,0 +1,13 @@ +{ + "name": "self_ref", + "version": "1.0.0", + "description": "", + "main": "index.js", + "exports": "./index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/test/message/esm_loader_not_found_cjs_hint_bare.out b/test/message/esm_loader_not_found_cjs_hint_bare.out index 77c5248bb59423..a4691dfb928a0e 100644 --- a/test/message/esm_loader_not_found_cjs_hint_bare.out +++ b/test/message/esm_loader_not_found_cjs_hint_bare.out @@ -6,7 +6,6 @@ internal/modules/run_main.js:* Error [ERR_MODULE_NOT_FOUND]: Cannot find module '*test*fixtures*node_modules*some_module*obj' imported from *test*fixtures*esm_loader_not_found_cjs_hint_bare.mjs Did you mean to import some_module/obj.js? at finalizeResolution (internal/modules/esm/resolve.js:*:*) - at packageResolve (internal/modules/esm/resolve.js:*:*) at moduleResolve (internal/modules/esm/resolve.js:*:*) at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*) at Loader.resolve (internal/modules/esm/loader.js:*:*) diff --git a/test/parallel/test-preload-self-referential.js b/test/parallel/test-preload-self-referential.js new file mode 100644 index 00000000000000..2624527deb3984 --- /dev/null +++ b/test/parallel/test-preload-self-referential.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { exec } = require('child_process'); + +const nodeBinary = process.argv[0]; + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const selfRefModule = fixtures.path('self_ref_module'); +const fixtureA = fixtures.path('printA.js'); + +exec(`"${nodeBinary}" -r self_ref "${fixtureA}"`, { cwd: selfRefModule }, + (err, stdout, stderr) => { + assert.ifError(err); + assert.strictEqual(stdout, 'A\n'); + }); diff --git a/test/parallel/test-repl-require-self-referential.js b/test/parallel/test-repl-require-self-referential.js new file mode 100644 index 00000000000000..7ced6dbf11721e --- /dev/null +++ b/test/parallel/test-repl-require-self-referential.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const selfRefModule = fixtures.path('self_ref_module'); +const child = spawn(process.execPath, + ['--interactive'], + { cwd: selfRefModule } +); +let output = ''; +child.stdout.on('data', (chunk) => output += chunk); +child.on('exit', common.mustCall(() => { + const results = output.replace(/^> /mg, '').split('\n').slice(2); + assert.deepStrictEqual(results, [ "'Self resolution working'", '' ]); +})); + +child.stdin.write('require("self_ref");\n'); +child.stdin.write('.exit'); +child.stdin.end();