From 2f7cfe5ce9b3dbec58d4c1e382c465c01dec5852 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 9 Aug 2020 16:54:01 -0700 Subject: [PATCH] module: share CJS/ESM resolver fns, refactoring PR-URL: https://github.com/nodejs/node/pull/34744 Reviewed-By: Jan Krems Reviewed-By: James M Snell --- doc/api/esm.md | 259 +++++++--------- doc/api/modules.md | 80 +++-- lib/internal/errors.js | 9 +- lib/internal/modules/cjs/loader.js | 290 ++++-------------- lib/internal/modules/esm/resolve.js | 223 +++++--------- test/es-module/test-esm-exports.mjs | 11 +- test/es-module/test-esm-invalid-pjson.js | 2 +- .../esm_loader_not_found_cjs_hint_bare.out | 1 - 8 files changed, 312 insertions(+), 563 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index adbb55f0d82f0d..8a675182ba7fdd 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1631,7 +1631,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: @@ -1651,40 +1651,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 _"/"_, _"./"_ or _"../"_, then -> 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to +> 1. Set _resolved_ to the URL resolution of _specifier_ relative to > _parentURL_. > 1. Otherwise, if _specifier_ starts with _"#"_, then -> 1. Set _resolvedURL_ to the result of -> **PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_). -> 1. If _resolvedURL_ is **null** or **undefined**, throw a -> _Package Import Not Defined_ error. +> 1. 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. @@ -1692,18 +1693,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. Let _selfUrl_ be the result of -> **SELF_REFERENCE_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_). -> 1. If _selfUrl_ isn't empty, return _selfUrl_. -> 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin +> **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, @@ -1714,126 +1709,127 @@ 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. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( -> _packageURL_, _packageSubpath_, _pjson.exports_). -> 1. If _resolved_ is **null** or **undefined**, throw a -> _Package Path Not Exported_ error. -> 1. Return _resolved_. > 1. Return the URL resolution of _packageSubpath_ in _packageURL_. > 1. Throw a _Module Not Found_ error. -**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. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( -> _packageURL_, _subpath_, _pjson.exports_). -> 1. If _resolved_ is **null** or **undefined**, throw a -> _Package Path Not Exported_ error. -> 1. Return _resolved_. -> 1. Return the URL resolution of _subpath_ in _packageURL_. +> 1. 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_) - -> 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. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _pjson.exports_, _""_, **false**, _defaultEnv_). -> 1. If _resolved_ is **null** or **undefined**, throw a -> _Package Path Not Exported_ error. -> 1. Return _resolved_. -> 1. If _pjson.exports_ is an Object containing a _"."_ property, then -> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_. -> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _mainExport_, _""_, **false**, _defaultEnv_). -> 1. If _resolved_ is **null** or **undefined**, throw a -> _Package Path Not Exported_ error. -> 1. Return _resolved_. -> 1. Throw a _Package Path Not Exported_ error. -> 1. Let _legacyMainURL_ be the result applying the legacy -> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a -> _Module Not Found_ error for no resolution. -> 1. Return _legacyMainURL_. +**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_, _exports_, _conditions_) -**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_TARGET_RESOLVE**(_packageURL_, _target_, -> _""_, **false**, _defaultEnv_). -> 1. Let _directoryKeys_ be the list of keys of _exports_ ending in -> _"/"_, sorted by length descending. -> 1. For each key _directory_ in _directoryKeys_, do -> 1. If _packagePath_ starts with _directory_, then -> 1. Let _target_ be the value of _exports\[directory\]_. -> 1. Let _subpath_ be the substring of _target_ starting at the index -> of the length of _directory_. -> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, -> _subpath_, **false**, _defaultEnv_). -> 1. Return **null**. +> 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**, _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_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_, _""_, _isImports_, _conditions_). +> 1. Return the object _{ resolved, exact: **true** }_. +> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_, +> sorted by length descending. +> 1. For each key _expansionKey_ in _expansionKeys_, do +> 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_, _isImports_, _conditions_). +> 1. Return the object _{ resolved, exact: **false** }_. +> 1. Return the object _{ resolved: **null**, exact: **true** }_. -**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_, _env_) +**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_, +_conditions_) > 1. If _target_ is a String, then -> 1. If _target_ contains any _"node_modules"_ segments including -> _"node_modules"_ percent-encoding, throw an _Invalid Package Target_ -> error. > 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_, > throw an _Invalid Module Specifier_ error. > 1. If _target_ does not start with _"./"_, then -> 1. If _target_ does not start with _"../"_ or _"/"_ and is not a valid -> URL, then -> 1. If _internal_ is **true**, return **PACKAGE_RESOLVE**( -> _target_ + _subpath_, _packageURL_ + _"/"_)_. -> 1. Otherwise throw an _Invalid Package Target_ error. +> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or +> _"/"_ and is not a valid URL, then +> 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. 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. 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. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _internal_, _env_) +> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_). > 1. If _resolved_ is equal to **undefined**, continue the loop. > 1. Return _resolved_. > 1. Return **undefined**. @@ -1841,7 +1837,7 @@ The resolver can throw the following errors: > 1. If _target.length is zero, return **null**. > 1. For each item _targetValue_ in _target_, do > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _internal_, _env_), +> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_), > continuing the loop on any _Invalid Package Target_ error. > 1. If _resolved_ is **undefined**, continue the loop. > 1. Return _resolved_. @@ -1849,31 +1845,6 @@ The resolver can throw the following errors: > 1. Otherwise, if _target_ is _null_, return **null**. > 1. Otherwise throw an _Invalid Package Target_ error. -**PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_) - -> 1. Assert: _specifier_ begins with _"#"_. -> 1. If _specifier_ is exactly equal to _"#"_ or starts with _"#/"_, then -> 1. Throw an _Invalid Module Specifier_ error. -> 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_). -> 1. If _packageURL_ is not **null**, then -> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). -> 1. If _pjson.imports is a non-null Object, then -> 1. Let _imports_ be _pjson.imports_. -> 1. If _specifier_ is a key of _imports_, then -> 1. Let _target_ be the value of _imports\[specifier\]_. -> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, -> _""_, **true**, _defaultEnv_). -> 1. Let _directoryKeys_ be the list of keys of _imports_ ending in -> _"/"_, sorted by length descending. -> 1. For each key _directory_ in _directoryKeys_, do -> 1. If _specifier_ starts with _directory_, then -> 1. Let _target_ be the value of _imports\[directory\]_. -> 1. Let _subpath_ be the substring of _target_ starting at the -> index of the length of _directory_. -> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, -> _subpath_, **true**, _defaultEnv_). -> 1. Return **null**. - **ESM_FORMAT**(_url_) > 1. Assert: _url_ corresponds to an existing file. diff --git a/doc/api/modules.md b/doc/api/modules.md index 7c78c2cda44feb..061c07604b039f 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -161,10 +161,10 @@ require(X) from module at path Y b. LOAD_AS_DIRECTORY(Y + X) c. THROW "not found" 4. If X begins with '#' - a. LOAD_INTERAL_IMPORT(X, Y) -4. LOAD_SELF_REFERENCE(X, Y) -5. LOAD_NODE_MODULES(X, dirname(Y)) -6. THROW "not found" + 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 @@ -191,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) @@ -206,47 +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" - -LOAD_INTERNAL_IMPORT(X, START) -1. Find the closest package scope to START. -2. If no scope was found or the `package.json` has no "imports", return. -3. let RESOLVED = - fileURLToPath(PACKAGE_INTERNAL_RESOLVE(X, pathToFileURL(START)), as defined - in the ESM resolver. -4. If RESOLVED is not a valid file, throw "not found" -5. Load RESOLVED as its file extension format. STOP +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/lib/internal/errors.js b/lib/internal/errors.js index 2fdbdc1593c071..759025496801ba 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -26,8 +26,6 @@ const { WeakMap, } = primordials; -const sep = process.platform === 'win32' ? '\\' : '/'; - const messages = new Map(); const codes = {}; @@ -1114,10 +1112,9 @@ 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 ? ` imported from ${base}` : + ''}${message ? `. ${message}` : ''}`; }, Error); E('ERR_INVALID_PACKAGE_TARGET', (pkgPath, key, target, isImport = false, base = undefined) => { diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 8f9ea40d7079b4..7fe5f4ce8f9b45 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -33,13 +33,11 @@ const { Error, JSONParse, Map, - Number, ObjectCreate, ObjectDefineProperty, ObjectFreeze, ObjectGetOwnPropertyDescriptor, ObjectGetPrototypeOf, - ObjectIs, ObjectKeys, ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, @@ -48,7 +46,6 @@ const { SafeMap, SafeSet, String, - StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeStartsWith, @@ -93,10 +90,7 @@ 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'); @@ -116,7 +110,8 @@ const asyncESM = require('internal/process/esm_loader'); const { kEvaluated } = internalBinding('module_wrap'); const { encodedSepRegEx, - packageInternalResolve + packageExportsResolve, + packageImportsResolve } = require('internal/modules/esm/resolve'); const isWindows = process.platform === 'win32'; @@ -305,18 +300,8 @@ function readPackageScope(checkPath) { 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)?.main; if (!pkg) { return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); @@ -421,108 +406,28 @@ function trySelfParentPath(parent) { function trySelf(parentPath, request) { if (!parentPath) return false; - const { data: pkg, path: basePath } = readPackageScope(parentPath) || {}; + 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]; - const resolved = resolveExportsTarget( - pathToFileURL(basePath + '/'), mapping, '', mappingKey); - if (resolved === null || resolved === undefined) - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - basePath, mappingKey); - return resolved; - } - - 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); - if (resolved === null || resolved === undefined) - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - basePath, mappingKey + subpath); - // 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: @@ -533,107 +438,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(baseUrl.pathname, mappingKey, - target); - const resolved = new URL(subpath, resolvedTarget); - const resolvedPath = resolved.pathname; - if (StringPrototypeStartsWith(resolvedPath, resolvedTargetPath) && - StringPrototypeIndexOf(resolvedPath, '/node_modules/', - pkgPathPath.length - 1) === -1) { - if (StringPrototypeMatch(resolvedPath, encodedSepRegEx)) - throw new ERR_INVALID_MODULE_SPECIFIER( - resolvedPath, 'must not include encoded "/" or "\\" characters', - fileURLToPath(baseUrl)); - return fileURLToPath(resolved); - } - const reason = 'request is not a valid subpath for the "exports" ' + - `resolution of ${baseUrl.pathname}package.json`; - throw new ERR_INVALID_MODULE_SPECIFIER(mappingKey + subpath, reason); - } else if (ArrayIsArray(target)) { - if (target.length === 0) - return null; - let lastException; - for (const targetValue of target) { - let resolved; - try { - resolved = resolveExportsTarget(baseUrl, targetValue, subpath, - mappingKey); - } catch (e) { - lastException = e; - if (e.code !== 'ERR_INVALID_PACKAGE_TARGET') - throw e; - } - if (resolved === undefined) - continue; - if (resolved === null) { - lastException = null; - continue; - } - return resolved; - } - // Throw last fallback error - if (lastException === undefined || lastException === null) - return lastException; - 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) { - if (cjsConditions.has(p) || p === 'default') { - const resolved = resolveExportsTarget(baseUrl, target[p], subpath, - mappingKey); - if (resolved === undefined) - continue; - return resolved; - } + 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; } - return undefined; - } else if (target === null) { - return null; } - throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target); } const trailingSlashRegex = /(?:^|\/)\.?\.$/; @@ -666,12 +485,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); @@ -1036,19 +851,14 @@ Module._resolveFilename = function(request, parent, isMain, options) { if (pkg.data && pkg.data.imports !== null && pkg.data.imports !== undefined) { try { - const resolved = packageInternalResolve( - request, pathToFileURL(parent.filename), cjsConditions); - return fileURLToPath(resolved); - } catch (err) { - if (err.code === 'ERR_MODULE_NOT_FOUND') { - // eslint-disable-next-line no-restricted-syntax - const err = new Error(`Cannot find module '${request}'`); - err.code = 'MODULE_NOT_FOUND'; - err.path = path.resolve(pkg.path, 'package.json'); - // TODO(BridgeAR): Add the requireStack as well. - throw err; - } - throw err; + 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; } } } @@ -1084,6 +894,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) { diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 7cf3552948194d..f6879465451c83 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -15,7 +15,6 @@ const { SafeSet, String, StringPrototypeEndsWith, - StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeReplace, StringPrototypeSlice, @@ -78,14 +77,6 @@ function tryStatSync(path) { } } -/** - * - * '/foo/package.json' -> '/foo' - */ -function removePackageJsonFromPath(path) { - return StringPrototypeSlice(path, 0, path.length - 13); -} - function getPackageConfig(path) { const existing = packageJSONCache.get(path); if (existing !== undefined) { @@ -110,8 +101,7 @@ 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, null, error.message); } let { imports, main, name, type } = packageJSON; @@ -177,7 +167,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. @@ -222,7 +212,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) { @@ -246,35 +237,34 @@ function resolveIndex(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'); } - 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); - 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; @@ -288,14 +278,15 @@ function throwImportNotDefined(specifier, packageJSONUrl, base) { function throwExportsNotFound(subpath, packageJSONUrl, base) { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - fileURLToPath(new URL('.', packageJSONUrl)), subpath, fileURLToPath(base)); + fileURLToPath(new URL('.', packageJSONUrl)), subpath, + base && 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)}${ - base ? ` imported from ${base}` : ''}`; - throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, fileURLToPath(base)); + 'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, + base && fileURLToPath(base)); } function throwInvalidPackageTarget( @@ -307,44 +298,46 @@ function throwInvalidPackageTarget( } throw new ERR_INVALID_PACKAGE_TARGET( fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, - internal, fileURLToPath(base)); + internal, base && fileURLToPath(base)); } +const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; + function resolvePackageTargetString( target, subpath, match, packageJSONUrl, base, internal, conditions) { if (subpath !== '' && target[target.length - 1] !== '/') throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); - if (!target.startsWith('./')) { - if (internal && !target.startsWith('../') && !target.startsWith('/')) { + if (!StringPrototypeStartsWith(target, './')) { + if (internal && !StringPrototypeStartsWith(target, '../') && + !StringPrototypeStartsWith(target, '/')) { let isURL = false; try { new URL(target); isURL = true; } catch {} if (!isURL) - return packageResolve(target + subpath, base, conditions); + return packageResolve(target + subpath, 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)) + 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)) + + if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); - return subpathResolved; + + return new URL(subpath, resolved); } /** @@ -357,12 +350,12 @@ function isArrayIndex(key) { return keyNum >= 0 && keyNum < 0xFFFF_FFFF; } -function resolvePackageTarget( - packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) { +function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, + base, internal, conditions) { if (typeof target === 'string') { - return finalizeResolution(resolvePackageTargetString( + return resolvePackageTargetString( target, subpath, packageSubpath, packageJSONUrl, base, internal, - conditions), base); + conditions); } else if (ArrayIsArray(target)) { if (target.length === 0) return null; @@ -387,7 +380,7 @@ function resolvePackageTarget( lastException = null; continue; } - return finalizeResolution(resolved, base); + return resolved; } if (lastException === undefined || lastException === null) return lastException; @@ -398,8 +391,8 @@ function resolvePackageTarget( 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++) { @@ -436,7 +429,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.'); @@ -445,44 +438,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)) { - const resolved = resolvePackageTarget(packageJSONUrl, exports, '', '', - base, false, conditions); - if (resolved === null || resolved === undefined) - throwExportsNotFound('.', packageJSONUrl, base); - return resolved; - } else if (typeof exports === 'object' && exports !== null) { - const target = exports['.']; - if (target !== undefined) { - const resolved = resolvePackageTarget(packageJSONUrl, target, '', '', - base, false, conditions); - if (resolved === null || resolved === undefined) - throwExportsNotFound('.', packageJSONUrl, base); - return resolved; - } - } - - 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 @@ -493,28 +448,27 @@ 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 = resolvePackageTarget( - packageJSONUrl, target, '', packageSubpath, base, false, conditions); + packageJSONUrl, target, '', packageSubpath, base, false, conditions + ); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); - return finalizeResolution(resolved, 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) && - key.length > bestMatch.length) { + if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(packageSubpath, key) && + key.length > bestMatch.length) { bestMatch = key; } } @@ -522,18 +476,18 @@ function packageExportsResolve( if (bestMatch) { const target = exports[bestMatch]; const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); - const resolved = resolvePackageTarget( - packageJSONUrl, target, subpath, bestMatch, base, false, conditions); + const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, + bestMatch, base, false, conditions); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); - return finalizeResolution(resolved, base); + return { resolved, exact: false }; } throwExportsNotFound(packageSubpath, packageJSONUrl, base); } -function packageInternalResolve(name, base, conditions) { - if (name === '#' || name.startsWith('#/')) { +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)); } @@ -545,17 +499,18 @@ function packageInternalResolve(name, base, conditions) { if (imports) { if (ObjectPrototypeHasOwnProperty(imports, name)) { const resolved = resolvePackageTarget( - packageJSONUrl, imports[name], '', name, base, true, conditions); + packageJSONUrl, imports[name], '', name, base, true, conditions + ); if (resolved !== null) - return finalizeResolution(resolved, base); + 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] !== '/') continue; - if (StringPrototypeStartsWith(name, key) && - key.length > bestMatch.length) { + if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(name, key) && + key.length > bestMatch.length) { bestMatch = key; } } @@ -564,10 +519,9 @@ function packageInternalResolve(name, base, conditions) { const target = imports[bestMatch]; const subpath = StringPrototypeSubstr(name, bestMatch.length); const resolved = resolvePackageTarget( - packageJSONUrl, target, subpath, bestMatch, base, true, - conditions); + packageJSONUrl, target, subpath, bestMatch, base, true, conditions); if (resolved !== null) - return finalizeResolution(resolved, base); + return { resolved, exact: false }; } } } @@ -617,23 +571,18 @@ function packageResolve(specifier, base, conditions) { 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); if (packageConfig.exists) { const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); if (packageConfig.name === packageName && - packageConfig.exports !== undefined) { - if (packageSubpath === './') { - return new URL('./', packageJSONUrl); - } else if (packageSubpath === '') { - return packageMainResolve(packageJSONUrl, packageConfig, base, - conditions); - } + packageConfig.exports !== undefined && packageConfig.exports !== null) { return packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions); + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; } } @@ -642,7 +591,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 ? @@ -654,17 +604,13 @@ 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) { + 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); @@ -706,12 +652,12 @@ function moduleResolve(specifier, base, conditions) { if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { resolved = new URL(specifier, base); } else if (specifier[0] === '#') { - resolved = packageInternalResolve(specifier, base, conditions); + ({ 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); @@ -847,5 +793,6 @@ module.exports = { defaultResolve, encodedSepRegEx, getPackageType, - packageInternalResolve + packageExportsResolve, + packageImportsResolve }; diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index 02caceee64deaa..a4cced41f897b0 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'; @@ -135,9 +136,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) { @@ -153,10 +154,8 @@ 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'); })); } diff --git a/test/es-module/test-esm-invalid-pjson.js b/test/es-module/test-esm-invalid-pjson.js index 83f4ad5baba4a7..53ebd4962f0523 100644 --- a/test/es-module/test-esm-invalid-pjson.js +++ b/test/es-module/test-esm-invalid-pjson.js @@ -21,7 +21,7 @@ child.on('close', mustCall((code, signal) => { stderr.includes( [ '[ERR_INVALID_PACKAGE_CONFIG]: ', - `Invalid package config ${invalidJson}, `, + `Invalid package config ${invalidJson}. `, `Unexpected token } in JSON at position ${isWindows ? 16 : 14}` ].join(''), ), 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 51a99cb29886bd..c3f758577eb4d6 100644 --- a/test/message/esm_loader_not_found_cjs_hint_bare.out +++ b/test/message/esm_loader_not_found_cjs_hint_bare.out @@ -5,7 +5,6 @@ internal/process/esm_loader.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:*:*)