From 75a2cf311240c7f24b52cf23fd0ae6e38fcc58d0 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 27 Jun 2020 22:09:24 -0700 Subject: [PATCH 01/13] module: package "imports" field PR-URL: https://github.com/nodejs/node/pull/34117 Reviewed-By: Jan Krems Reviewed-By: Bradley Farias --- doc/api/errors.md | 7 + doc/api/esm.md | 159 +++++++++--- doc/api/modules.md | 13 +- lib/internal/errors.js | 64 ++--- lib/internal/modules/cjs/loader.js | 73 ++++-- lib/internal/modules/esm/resolve.js | 230 ++++++++++++------ src/node_file.cc | 1 + test/es-module/test-esm-exports.mjs | 5 +- test/es-module/test-esm-imports.mjs | 117 +++++++++ .../es-modules/pkgimports/importbranch.js | 2 + .../es-modules/pkgimports/importer.js | 4 + .../es-modules/pkgimports/package.json | 30 +++ .../es-modules/pkgimports/requirebranch.js | 2 + test/fixtures/es-modules/pkgimports/sub/x.js | 2 + test/fixtures/es-modules/pkgimports/test.js | 1 + test/fixtures/node_modules/#cjs/index.js | 2 + 16 files changed, 537 insertions(+), 175 deletions(-) create mode 100644 test/es-module/test-esm-imports.mjs create mode 100644 test/fixtures/es-modules/pkgimports/importbranch.js create mode 100644 test/fixtures/es-modules/pkgimports/importer.js create mode 100644 test/fixtures/es-modules/pkgimports/package.json create mode 100644 test/fixtures/es-modules/pkgimports/requirebranch.js create mode 100644 test/fixtures/es-modules/pkgimports/sub/x.js create mode 100644 test/fixtures/es-modules/pkgimports/test.js create mode 100644 test/fixtures/node_modules/#cjs/index.js 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..221b1936f0c09a 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -546,6 +546,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 @@ -1577,10 +1614,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.
@@ -1592,11 +1630,14 @@ The resolver can throw the following errors: > 1. If _specifier_ is a valid URL, then > 1. Set _resolvedURL_ to the result of parsing and reserializing > _specifier_ as a URL. -> 1. Otherwise, if _specifier_ starts with _"/"_, then -> 1. Throw an _Invalid Module Specifier_ error. -> 1. Otherwise, if _specifier_ starts with _"./"_ or _"../"_, then +> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_, then > 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to > _parentURL_. +> 1. Otherwise, if _specifier_ starts with _"#"_, then +> 1. Set _resolvedURL_ to the result of +> **PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_). +> 1. If _resolvedURL_ is **null** or **undefined**, throw a +> _Package Import Not Defined_ error. > 1. Otherwise, > 1. Note: _specifier_ is now a bare specifier. > 1. Set _resolvedURL_ the result of @@ -1634,7 +1675,7 @@ The resolver can throw the following errors: > 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent > encoded strings for _"/"_ or _"\\"_, then > 1. Throw an _Invalid Module Specifier_ error. -> 1. Set _selfUrl_ to the result of +> 1. Let _selfUrl_ be the result of > **SELF_REFERENCE_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_). > 1. If _selfUrl_ isn't empty, return _selfUrl_. > 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin @@ -1657,8 +1698,11 @@ The resolver can throw the following errors: > 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then > 1. Let _exports_ be _pjson.exports_. > 1. If _exports_ is not **null** or **undefined**, then -> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, -> _packageSubpath_, _pjson.exports_). +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( +> _packageURL_, _packageSubpath_, _pjson.exports_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Return the URL resolution of _packageSubpath_ in _packageURL_. > 1. Throw a _Module Not Found_ error. @@ -1679,8 +1723,11 @@ The resolver can throw the following errors: > 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then > 1. Let _exports_ be _pjson.exports_. > 1. If _exports_ is not **null** or **undefined**, then -> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_, -> _pjson.exports_). +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( +> _packageURL_, _subpath_, _pjson.exports_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Return the URL resolution of _subpath_ in _packageURL_. > 1. Otherwise, return **undefined**. @@ -1693,12 +1740,18 @@ The resolver can throw the following errors: > not starting with _"."_, throw an _Invalid Package Configuration_ error. > 1. If _pjson.exports_ is a String or Array, or an Object containing no > keys starting with _"."_, then -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _pjson.exports_, _""_). +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _pjson.exports_, _""_, **false**, _defaultEnv_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. If _pjson.exports_ is an Object containing a _"."_ property, then > 1. Let _mainExport_ be the _"."_ property in _pjson.exports_. -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _mainExport_, _""_). +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _mainExport_, _""_, **false**, _defaultEnv_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Throw a _Package Path Not Exported_ error. > 1. Let _legacyMainURL_ be the result applying the legacy > **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a @@ -1712,8 +1765,8 @@ The resolver can throw the following errors: > 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_. > 1. If _packagePath_ is a key of _exports_, then > 1. Let _target_ be the value of _exports\[packagePath\]_. -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, -> _""_, _defaultEnv_). +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _""_, **false**, _defaultEnv_). > 1. Let _directoryKeys_ be the list of keys of _exports_ ending in > _"/"_, sorted by length descending. > 1. For each key _directory_ in _directoryKeys_, do @@ -1721,22 +1774,28 @@ The resolver can throw the following errors: > 1. Let _target_ be the value of _exports\[directory\]_. > 1. Let _subpath_ be the substring of _target_ starting at the index > of the length of _directory_. -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, -> _subpath_, _defaultEnv_). -> 1. Throw a _Package Path Not Exported_ error. +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _subpath_, **false**, _defaultEnv_). +> 1. Return **null**. -**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_) +**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_, _env_) > 1. If _target_ is a String, then -> 1. If _target_ does not start with _"./"_ or contains any _"node_modules"_ -> segments including _"node_modules"_ percent-encoding, throw an -> _Invalid Package Target_ error. +> 1. If _target_ contains any _"node_modules"_ segments including +> _"node_modules"_ percent-encoding, throw an _Invalid Package Target_ +> error. +> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_, +> throw an _Invalid Module Specifier_ error. +> 1. If _target_ does not start with _"./"_, then +> 1. If _target_ does not start with _"../"_ or _"/"_ and is not a valid +> URL, then +> 1. If _internal_ is **true**, return **PACKAGE_RESOLVE**( +> _target_ + _subpath_, _packageURL_ + _"/"_)_. +> 1. Otherwise throw an _Invalid Package Target_ error. > 1. Let _resolvedTarget_ be the URL resolution of the concatenation of > _packageURL_ and _target_. > 1. If _resolvedTarget_ is not contained in _packageURL_, throw an > _Invalid Package Target_ error. -> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_, -> throw an _Invalid Module Specifier_ error. > 1. Let _resolved_ be the URL resolution of the concatenation of > _subpath_ and _resolvedTarget_. > 1. If _resolved_ is not contained in _resolvedTarget_, throw an @@ -1748,22 +1807,48 @@ The resolver can throw the following errors: > 1. For each property _p_ of _target_, in object insertion order as, > 1. If _p_ equals _"default"_ or _env_ contains an entry for _p_, then > 1. Let _targetValue_ be the value of the _p_ property in _target_. -> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _env_), continuing the -> loop on any _Package Path Not Exported_ error. -> 1. Throw a _Package Path Not Exported_ error. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _targetValue_, _subpath_, _internal_, _env_) +> 1. If _resolved_ is equal to **undefined**, continue the loop. +> 1. Return _resolved_. +> 1. Return **undefined**. > 1. Otherwise, if _target_ is an Array, then -> 1. If _target.length is zero, throw a _Package Path Not Exported_ error. +> 1. If _target.length is zero, return **null**. > 1. For each item _targetValue_ in _target_, do -> 1. If _targetValue_ is an Array, continue the loop. -> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _targetValue_, _subpath_, _env_), continuing the loop on any -> _Package Path Not Exported_ or _Invalid Package Target_ error. -> 1. Throw the last fallback resolution error. -> 1. Otherwise, if _target_ is _null_, throw a _Package Path Not Exported_ -> error. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _targetValue_, _subpath_, _internal_, _env_), +> continuing the loop on any _Invalid Package Target_ error. +> 1. If _resolved_ is **undefined**, continue the loop. +> 1. Return _resolved_. +> 1. Return or throw the last fallback resolution **null** return or error. +> 1. Otherwise, if _target_ is _null_, return **null**. > 1. Otherwise throw an _Invalid Package Target_ error. +**PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_) + +> 1. Assert: _specifier_ begins with _"#"_. +> 1. If _specifier_ is exactly equal to _"#"_ or starts with _"#/"_, then +> 1. Throw an _Invalid Module Specifier_ error. +> 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_). +> 1. If _packageURL_ is not **null**, then +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). +> 1. If _pjson.imports is a non-null Object, then +> 1. Let _imports_ be _pjson.imports_. +> 1. If _specifier_ is a key of _imports_, then +> 1. Let _target_ be the value of _imports\[specifier\]_. +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _""_, **true**, _defaultEnv_). +> 1. Let _directoryKeys_ be the list of keys of _imports_ ending in +> _"/"_, sorted by length descending. +> 1. For each key _directory_ in _directoryKeys_, do +> 1. If _specifier_ starts with _directory_, then +> 1. Let _target_ be the value of _imports\[directory\]_. +> 1. Let _subpath_ be the substring of _target_ starting at the +> index of the length of _directory_. +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _subpath_, **true**, _defaultEnv_). +> 1. Return **null**. + **ESM_FORMAT**(_url_) > 1. Assert: _url_ corresponds to an existing file. diff --git a/doc/api/modules.md b/doc/api/modules.md index e8215a2ace67ed..bcac4a6bf88d41 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -160,7 +160,9 @@ require(X) from module at path Y a. LOAD_AS_FILE(Y + X) b. LOAD_AS_DIRECTORY(Y + X) c. THROW "not found" -4. LOAD_SELF_REFERENCE(X, dirname(Y)) +4. If X begins with '#' + a. LOAD_INTERAL_IMPORT(X, Y) +4. LOAD_SELF_REFERENCE(X, Y) 5. LOAD_NODE_MODULES(X, dirname(Y)) 6. THROW "not found" @@ -236,6 +238,15 @@ LOAD_PACKAGE_EXPORTS(DIR, X) 12. Otherwise a. If RESOLVED is a file, load it as its file extension format. STOP 13. Throw "not found" + +LOAD_INTERNAL_IMPORT(X, START) +1. Find the closest package scope to START. +2. If no scope was found or the `package.json` has no "imports", return. +3. let RESOLVED = + fileURLToPath(PACKAGE_INTERNAL_RESOLVE(X, pathToFileURL(START)), as defined + in the ESM resolver. +4. If RESOLVED is not a valid file, throw "not found" +5. Load RESOLVED as its file extension format. STOP ``` ## Caching diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 921bcce2878706..d7b37048bb8163 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -21,7 +21,6 @@ const { NumberIsInteger, ObjectDefineProperty, ObjectKeys, - StringPrototypeSlice, StringPrototypeStartsWith, Symbol, SymbolFor, @@ -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}"`, @@ -1120,31 +1112,20 @@ E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => { return `Invalid JSON in ${path} imported from ${message}`; }, Error); E('ERR_INVALID_PACKAGE_TARGET', - (pkgPath, key, subpath, target, base = undefined) => { - const relError = typeof target === 'string' && + (pkgPath, key, target, isImport = false, base = undefined) => { + const relError = typeof target === 'string' && !isImport && target.length && !StringPrototypeStartsWith(target, './'); - if (key === null) { - if (subpath !== '') { - return `Invalid "exports" target ${JSONStringify(target)} defined ` + - `for '${subpath}' in the package config ${pkgPath} imported from ` + - `${base}.${relError ? '; targets must start with "./"' : ''}`; - } - return `Invalid "exports" main target ${target} defined in the ` + - `package config ${pkgPath} imported from ${base}${relError ? - '; targets must start with "./"' : ''}`; - } else if (key === '.') { + if (key === '.') { + assert(isImport === false); return `Invalid "exports" main target ${JSONStringify(target)} defined ` + - `in the package config ${pkgPath}${sep}package.json${relError ? - '; targets must start with "./"' : ''}`; - } else if (relError) { - return `Invalid "exports" target ${JSONStringify(target)} defined for '${ - StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + - `package config ${pkgPath}${sep}package.json; ` + - 'targets must start with "./"'; + `in the package config ${pkgPath}package.json${base ? + ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; } - return `Invalid "exports" target ${JSONStringify(target)} defined for '${ - StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + - `package config ${pkgPath}${sep}package.json`; + return `Invalid "${isImport ? 'imports' : 'exports'}" target ${ + JSONStringify(target)} defined for '${key}' in the package config ${ + pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; }, Error); E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set', Error); @@ -1293,15 +1274,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,7 +1401,7 @@ E('ERR_UNKNOWN_FILE_EXTENSION', E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " + -'resolving ES modules, imported from %s', Error); +'resolving ES modules imported from %s', Error); E('ERR_UNSUPPORTED_ESM_URL_SCHEME', 'Only file and data URLs are supported ' + 'by the default ESM loader', Error); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index c5d6c4221db076..a08e5f3e941e51 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -21,6 +21,12 @@ 'use strict'; +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + const { ArrayIsArray, Error, @@ -36,6 +42,7 @@ const { ReflectSet, RegExpPrototypeTest, SafeMap, + SafeSet, String, StringPrototypeIndexOf, StringPrototypeMatch, @@ -89,19 +96,20 @@ const { 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 ModuleJob = require('internal/modules/esm/module_job'); +const { ModuleWrap, kInstantiated } = internalBinding('module_wrap'); +const { + encodedSepRegEx, + packageInternalResolve +} = require('internal/modules/esm/resolve'); + const isWindows = process.platform === 'win32'; const relativeResolveCache = ObjectCreate(null); @@ -259,6 +267,7 @@ function readPackage(requestPath) { name: parsed.name, main: parsed.main, exports: parsed.exports, + imports: parsed.imports, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); @@ -532,21 +541,26 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { 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); + 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); } - throw new ERR_INVALID_MODULE_SPECIFIER(StringPrototypeSlice(baseUrl.pathname - , 0, -1), mappingKey); + 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) throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); let lastException; for (const targetValue of target) { try { @@ -588,13 +602,12 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { } } throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); } else if (target === null) { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); } - throw new ERR_INVALID_PACKAGE_TARGET( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target); + throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target); } const trailingSlashRegex = /(?:^|\/)\.?\.$/; @@ -892,6 +905,8 @@ Module._load = function(request, parent, isMain) { return module.exports; }; +// TODO: Use this set when resolving pkg#exports conditions. +const cjsConditions = new SafeSet(['require', 'node']); Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { return request; @@ -934,6 +949,27 @@ Module._resolveFilename = function(request, parent, isMain, options) { } if (parent && parent.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + 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; + } + } + } const filename = trySelf(parent.filename, request); if (filename) { const cacheKey = request + '\x00' + @@ -1282,8 +1318,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/resolve.js b/lib/internal/modules/esm/resolve.js index 987a139c6aae57..16ca78880c7a15 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -10,6 +10,7 @@ const { ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, RegExp, + RegExpPrototypeTest, SafeMap, SafeSet, String, @@ -22,7 +23,6 @@ const { StringPrototypeStartsWith, StringPrototypeSubstr, } = primordials; -const assert = require('internal/assert'); const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { @@ -44,6 +44,7 @@ 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, @@ -91,11 +92,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; @@ -109,19 +112,22 @@ function getPackageConfig(path) { throw new ERR_INVALID_PACKAGE_CONFIG(errorPath, error.message, true); } - 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; @@ -143,14 +149,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; } @@ -233,6 +242,7 @@ function resolveIndex(search) { return resolveExtensions(new URL('index', search)); } +const encodedSepRegEx = /%2F|%2C/i; function finalizeResolution(resolved, base) { if (getOptionValue('--experimental-specifier-resolution') === 'node') { let file = resolveExtensionsWithTryExactName(resolved); @@ -247,6 +257,11 @@ function finalizeResolution(resolved, base) { 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); @@ -263,34 +278,52 @@ function finalizeResolution(resolved, base) { 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, 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)}${ + base ? ` imported from ${base}` : ''}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, 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, fileURLToPath(base)); } -function resolveExportsTargetString( - target, subpath, match, packageJSONUrl, base) { - if (target[0] !== '.' || target[1] !== '/' || - (subpath !== '' && target[target.length - 1] !== '/')) { - throwExportsInvalid(match, target, packageJSONUrl, base); +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('/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch {} + if (!isURL) + return packageResolve(target + subpath, base, conditions); + } + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } const resolved = new URL(target, packageJSONUrl); @@ -299,18 +332,16 @@ function resolveExportsTargetString( if (!StringPrototypeStartsWith(resolvedPath, packagePath) || StringPrototypeIncludes( - resolvedPath, '/node_modules/', packagePath.length - 1)) { - throwExportsInvalid(match, target, packageJSONUrl, base); - } + resolvedPath, '/node_modules/', packagePath.length - 1)) + 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); - } + '/node_modules/', packagePath.length - 1)) + throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); return subpathResolved; } @@ -324,36 +355,43 @@ function isArrayIndex(key) { return keyNum >= 0 && keyNum < 0xFFFF_FFFF; } -function resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions) { +function resolvePackageTarget( + packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) { if (typeof target === 'string') { - const resolved = resolveExportsTargetString( - target, subpath, packageSubpath, packageJSONUrl, base); + const resolved = resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, internal, + conditions); + if (resolved === null) + return null; return finalizeResolution(resolved, base); } 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, + resolved = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, 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; } - + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } return finalizeResolution(resolved, base); } - assert(lastException !== undefined); + if (lastException === undefined || lastException === null) + return lastException; throw lastException; } else if (typeof target === 'object' && target !== null) { const keys = ObjectGetOwnPropertyNames(target); @@ -369,21 +407,20 @@ function resolveExportsTarget( 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, + 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) { @@ -409,19 +446,25 @@ 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); + 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) - return resolveExportsTarget(packageJSONUrl, target, '', '', base, - conditions); + 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, '.'); @@ -457,11 +500,12 @@ function packageExportsResolve( throwExportsNotFound(packageSubpath, packageJSONUrl, base); } - if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { const target = exports[packageSubpath]; - const resolved = resolveExportsTarget( - packageJSONUrl, target, '', packageSubpath, base, conditions); + const resolved = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); return finalizeResolution(resolved, base); } @@ -479,14 +523,59 @@ function packageExportsResolve( if (bestMatch) { const target = exports[bestMatch]; const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); - const resolved = resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); return finalizeResolution(resolved, base); } throwExportsNotFound(packageSubpath, packageJSONUrl, base); } +function packageInternalResolve(name, base, conditions) { + if (name === '#' || name.startsWith('#/')) { + 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, 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, true, conditions); + if (resolved !== null) + return finalizeResolution(resolved, base); + } 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) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const subpath = StringPrototypeSubstr(name, bestMatch.length); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, true, + conditions); + if (resolved !== null) + return finalizeResolution(resolved, base); + } + } + } + } + throwImportNotDefined(name, packageJSONUrl, base); +} + function getPackageType(url) { const packageConfig = getPackageScopeConfig(url, url); return packageConfig.type; @@ -526,7 +615,7 @@ 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 ? @@ -535,17 +624,8 @@ function packageResolve(specifier, base, conditions) { // ResolveSelf const packageConfig = getPackageScopeConfig(base, 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 && + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && packageConfig.exports !== undefined) { if (packageSubpath === './') { return new URL('./', packageJSONUrl); @@ -626,6 +706,8 @@ function moduleResolve(specifier, base, conditions) { let resolved; if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { resolved = new URL(specifier, base); + } else if (specifier[0] === '#') { + resolved = packageInternalResolve(specifier, base, conditions); } else { try { resolved = new URL(specifier); @@ -764,5 +846,7 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) { module.exports = { DEFAULT_CONDITIONS, defaultResolve, - getPackageType + encodedSepRegEx, + getPackageType, + packageInternalResolve }; 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/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index a0348d4a1ab0b2..02caceee64deaa 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -118,7 +118,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); })); } @@ -161,7 +162,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; // 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/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..7cd179631fa618 --- /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'; + From f3a328c1b81d23148e1b58218dbc2bb911e44d30 Mon Sep 17 00:00:00 2001 From: Daniele Belardi Date: Thu, 16 Jul 2020 09:27:53 +0200 Subject: [PATCH 02/13] module: self referential modules in repl or `-r` Load self referential modules from the repl and using the preload flag `-r`. In both cases the base path used for resolution is the current `process.cwd()`. Also fixes an internal cycle bug in the REPL exports resolution. PR-URL: https://github.com/nodejs/node/pull/32261 Fixes: https://github.com/nodejs/node/issues/31595 Reviewed-By: Guy Bedford Reviewed-By: Jan Krems --- lib/internal/modules/cjs/loader.js | 34 +++++++++++++++---- lib/internal/modules/esm/loader.js | 3 ++ lib/internal/modules/esm/resolve.js | 3 +- test/fixtures/self_ref_module/index.js | 4 +++ test/fixtures/self_ref_module/package.json | 13 +++++++ .../parallel/test-preload-self-referential.js | 20 +++++++++++ .../test-repl-require-self-referential.js | 25 ++++++++++++++ 7 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/self_ref_module/index.js create mode 100644 test/fixtures/self_ref_module/package.json create mode 100644 test/parallel/test-preload-self-referential.js create mode 100644 test/parallel/test-repl-require-self-referential.js diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a08e5f3e941e51..422f71c5ec866a 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -29,6 +29,7 @@ module.exports = { const { ArrayIsArray, + ArrayPrototypeJoin, Error, JSONParse, Map, @@ -396,7 +397,23 @@ 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) { + if (!parentPath) return false; + const { data: pkg, path: basePath } = readPackageScope(parentPath) || {}; if (!pkg || pkg.exports === undefined) return false; if (typeof pkg.name !== 'string') return false; @@ -970,13 +987,16 @@ Module._resolveFilename = function(request, parent, isMain, options) { } } } - 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; - } + } + + // 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. diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index be5868553fa8df..e46a0d2cd86642 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -1,5 +1,8 @@ 'use strict'; +// This is needed to avoid cycles in esm/resolve <-> cjs/loader +require('internal/modules/cjs/loader'); + const { FunctionPrototypeBind, ObjectSetPrototypeOf, diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 16ca78880c7a15..7ea59f30c6894e 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -32,7 +32,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'); @@ -49,11 +48,13 @@ const { 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 DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); + function getConditionsSet(conditions) { if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { if (!ArrayIsArray(conditions)) { 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/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(); From 39a42b3fe5e26678d0ad8cd0f64dd88ceb2f3bcc Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 2 Aug 2020 18:03:48 -0700 Subject: [PATCH 03/13] module: use cjsCache over esm injection PR-URL: https://github.com/nodejs/node/pull/34605 Reviewed-By: Bradley Farias Reviewed-By: Rich Trott --- lib/internal/modules/cjs/loader.js | 29 ++++------------- lib/internal/modules/esm/loader.js | 4 +-- lib/internal/modules/esm/translators.js | 42 +++++++++---------------- 3 files changed, 22 insertions(+), 53 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 422f71c5ec866a..d11952592950ef 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -104,8 +104,7 @@ const { } = require('internal/constants'); const asyncESM = require('internal/process/esm_loader'); -const ModuleJob = require('internal/modules/esm/module_job'); -const { ModuleWrap, kInstantiated } = internalBinding('module_wrap'); +const { kEvaluated } = internalBinding('module_wrap'); const { encodedSepRegEx, packageInternalResolve @@ -1037,29 +1036,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 === undefined || + module.module.getStatus() < kEvaluated) && + !ESMLoader.cjsCache.has(this)) + ESMLoader.cjsCache.set(this, exports); }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index e46a0d2cd86642..0d1c09f3d38c7f 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -6,7 +6,7 @@ require('internal/modules/cjs/loader'); const { FunctionPrototypeBind, ObjectSetPrototypeOf, - SafeMap, + SafeWeakMap, } = primordials; const { @@ -52,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/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); }); }); From 9485ad688387f530f67f77f04317d364257db70d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 5 Aug 2020 12:06:28 -0700 Subject: [PATCH 04/13] module: custom --conditions flag option PR-URL: https://github.com/nodejs/node/pull/34637 Reviewed-By: Anna Henningsen Reviewed-By: Geoffrey Booth Reviewed-By: Jan Krems --- doc/api/cli.md | 16 +++++ doc/api/esm.md | 15 +++++ doc/node.1 | 4 ++ lib/internal/modules/cjs/loader.js | 64 ++++++++++--------- lib/internal/modules/esm/resolve.js | 10 ++- src/node_options.cc | 5 ++ src/node_options.h | 1 + test/es-module/test-esm-custom-exports.mjs | 10 +++ .../pkgexports/custom-condition.js | 1 + .../node_modules/pkgexports/package.json | 5 +- 10 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 test/es-module/test-esm-custom-exports.mjs create mode 100644 test/fixtures/node_modules/pkgexports/custom-condition.js diff --git a/doc/api/cli.md b/doc/api/cli.md index f99066477a013b..cdc173dc1730d4 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 ``` +### `-u`, `--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`, `-u` * `--diagnostic-dir` * `--disable-proto` * `--enable-fips` diff --git a/doc/api/esm.md b/doc/api/esm.md index 221b1936f0c09a..5ce648b107bce3 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -501,6 +501,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 diff --git a/doc/node.1 b/doc/node.1 index 3cd85d3cb0dfb2..8c4a6908383adc 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 u , 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/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index d11952592950ef..1758a915e0e5a6 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -80,6 +80,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. @@ -470,8 +471,12 @@ function applyExports(basePath, expansion) { if (typeof pkgExports === 'object') { if (ObjectPrototypeHasOwnProperty(pkgExports, mappingKey)) { const mapping = pkgExports[mappingKey]; - return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', - 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 = ''; @@ -488,6 +493,9 @@ function applyExports(basePath, expansion) { 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; @@ -575,21 +583,29 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { throw new ERR_INVALID_MODULE_SPECIFIER(mappingKey + subpath, reason); } else if (ArrayIsArray(target)) { if (target.length === 0) - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return null; let lastException; for (const targetValue of target) { + let resolved; try { - return resolveExportsTarget(baseUrl, targetValue, subpath, mappingKey); + resolved = resolveExportsTarget(baseUrl, targetValue, subpath, + mappingKey); } 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') throw e; } + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } + return resolved; } // Throw last fallback error - assert(lastException !== undefined); + if (lastException === undefined || lastException === null) + return lastException; throw lastException; } else if (typeof target === 'object' && target !== null) { const keys = ObjectKeys(target); @@ -598,30 +614,17 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { '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 (cjsConditions.has(p) || p === 'default') { + const resolved = resolveExportsTarget(baseUrl, target[p], subpath, + mappingKey); + if (resolved === undefined) + continue; + return resolved; } } - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return undefined; } else if (target === null) { - throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - baseUrl.pathname, mappingKey + subpath); + return null; } throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target); } @@ -921,8 +924,7 @@ Module._load = function(request, parent, isMain) { return module.exports; }; -// TODO: Use this set when resolving pkg#exports conditions. -const cjsConditions = new SafeSet(['require', 'node']); +const cjsConditions = new SafeSet(['require', 'node', ...userConditions]); Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { return request; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 7ea59f30c6894e..7cf3552948194d 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -51,7 +51,8 @@ const { 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); @@ -359,12 +360,9 @@ function isArrayIndex(key) { function resolvePackageTarget( packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) { if (typeof target === 'string') { - const resolved = resolvePackageTargetString( + return finalizeResolution(resolvePackageTargetString( target, subpath, packageSubpath, packageJSONUrl, base, internal, - conditions); - if (resolved === null) - return null; - return finalizeResolution(resolved, base); + conditions), base); } else if (ArrayIsArray(target)) { if (target.length === 0) return null; diff --git a/src/node_options.cc b/src/node_options.cc index 53e4c9bb3a315f..790265106be952 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -283,6 +283,11 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--conditions", + "additional user conditions for conditional exports and imports", + &EnvironmentOptions::conditions, + kAllowedInEnvironment); + AddAlias("-u", "--conditions"); 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-custom-exports.mjs b/test/es-module/test-esm-custom-exports.mjs new file mode 100644 index 00000000000000..ad81abfdafd861 --- /dev/null +++ b/test/es-module/test-esm-custom-exports.mjs @@ -0,0 +1,10 @@ +// Flags: --conditions=custom-condition -u 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/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..71406a407c453d 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": { From 7e20df26113b01421fac564ceb554613af85eb87 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 9 Aug 2020 16:54:01 -0700 Subject: [PATCH 05/13] 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 | 301 ++++-------------- 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, 317 insertions(+), 569 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 5ce648b107bce3..1e66d17ef92252 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1621,7 +1621,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: @@ -1641,40 +1641,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. @@ -1682,18 +1683,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, @@ -1704,126 +1699,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**. @@ -1831,7 +1827,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_. @@ -1839,31 +1835,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 bcac4a6bf88d41..079430bce78e71 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 d7b37048bb8163..8cb6179dff4110 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -27,8 +27,6 @@ const { WeakMap, } = primordials; -const sep = process.platform === 'win32' ? '\\' : '/'; - const messages = new Map(); const codes = {}; @@ -1106,10 +1104,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 1758a915e0e5a6..a6da26ee4a4774 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -33,19 +33,14 @@ const { Error, JSONParse, Map, - Number, ObjectCreate, ObjectDefineProperty, ObjectFreeze, - ObjectIs, ObjectKeys, - ObjectPrototypeHasOwnProperty, ReflectSet, RegExpPrototypeTest, SafeMap, SafeSet, - String, - StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeStartsWith, @@ -89,10 +84,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'); @@ -108,7 +100,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'; @@ -298,24 +291,15 @@ 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); + 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); @@ -335,7 +319,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' @@ -414,108 +398,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: @@ -526,107 +430,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 = /(?:^|\/)\.?\.$/; @@ -659,12 +477,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); @@ -972,19 +786,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; } } } @@ -1020,6 +829,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) { @@ -1041,7 +878,7 @@ Module.prototype.load = function(filename) { // Create module entry at load time to snapshot exports correctly const exports = this.exports; // Preemptively cache - if ((module?.module === undefined || + if ((!module || module.module === undefined || module.module.getStatus() < kEvaluated) && !ESMLoader.cjsCache.has(this)) ESMLoader.cjsCache.set(this, exports); 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 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:*:*) From d6af3ba83ec48f07728bdadac9d63bc0e6ebe046 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 2 Aug 2020 17:27:51 -0400 Subject: [PATCH 06/13] module: fix check for package.json at volume root This patch converts the "read package scope" algorithm's while loop into a do-while loop enabling items at the filesystem root dir to be considered within the scope of a sibling package.json also at the filesystem root dir. Fixes: https://github.com/nodejs/node/issues/33438 Co-authored-by: Guy Bedford PR-URL: https://github.com/nodejs/node/pull/34595 Reviewed-By: Jan Krems Reviewed-By: Mary Marchini --- doc/api/esm.md | 2 +- lib/internal/modules/cjs/loader.js | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 1e66d17ef92252..0efa316ac47dd1 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1854,11 +1854,11 @@ _conditions_) > 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/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a6da26ee4a4774..b0e4a0639b01a9 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -41,6 +41,9 @@ const { RegExpPrototypeTest, SafeMap, SafeSet, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeStartsWith, @@ -58,6 +61,7 @@ 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'); @@ -274,20 +278,19 @@ 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; } From 56393c126a7465b4c16df08f6d0f294af08a0129 Mon Sep 17 00:00:00 2001 From: Richard Lau Date: Wed, 26 Aug 2020 09:56:55 -0400 Subject: [PATCH 07/13] module: drop `-u` alias for `--conditions` Old versions of mocha break after https://github.com/nodejs/node/pull/34637. This was a bug in mocha, but since this is a widely used module we can expect ecosystem breakage until modules are updated to the latest version of mocha. Drop the conflicting `-u` alias -- we can potentially bring it back once modules have been updated. PR-URL: https://github.com/nodejs/node/pull/34935 Refs: https://github.com/mochajs/mocha/issues/4417 Refs: https://github.com/nodejs/node/pull/34637 Reviewed-By: Anna Henningsen Reviewed-By: Jan Krems Reviewed-By: Beth Griggs Reviewed-By: Myles Borins Reviewed-By: Colin Ihrig Reviewed-By: Shelley Vohr --- doc/api/cli.md | 4 ++-- doc/node.1 | 2 +- src/node_options.cc | 1 - test/es-module/test-esm-custom-exports.mjs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index cdc173dc1730d4..574e40232fefb5 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -81,7 +81,7 @@ $ node --completion-bash > node_bash_completion $ source node_bash_completion ``` -### `-u`, `--conditions=condition` +### `--conditions=condition` @@ -1181,7 +1181,7 @@ node --require "./a.js" --require "./b.js" Node.js options that are allowed are: -* `--conditions`, `-u` +* `--conditions` * `--diagnostic-dir` * `--disable-proto` * `--enable-fips` diff --git a/doc/node.1 b/doc/node.1 index 8c4a6908383adc..610b21d0eef114 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -78,7 +78,7 @@ 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 u , Fl -conditions Ar string +.It Fl -conditions Ar string Use custom conditional exports conditions .Ar string . diff --git a/src/node_options.cc b/src/node_options.cc index 790265106be952..824004631f5301 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -287,7 +287,6 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "additional user conditions for conditional exports and imports", &EnvironmentOptions::conditions, kAllowedInEnvironment); - AddAlias("-u", "--conditions"); AddOption("--diagnostic-dir", "set dir for all output files" " (default: current working directory)", diff --git a/test/es-module/test-esm-custom-exports.mjs b/test/es-module/test-esm-custom-exports.mjs index ad81abfdafd861..cf0557fa44215e 100644 --- a/test/es-module/test-esm-custom-exports.mjs +++ b/test/es-module/test-esm-custom-exports.mjs @@ -1,4 +1,4 @@ -// Flags: --conditions=custom-condition -u another +// Flags: --conditions=custom-condition --conditions another import { mustCall } from '../common/index.mjs'; import { strictEqual } from 'assert'; import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs'; From 8030eb5c2447d1d63652d81c6effeacc57a63e50 Mon Sep 17 00:00:00 2001 From: Denys Otrishko Date: Sun, 16 Aug 2020 12:28:54 +0300 Subject: [PATCH 08/13] esm: improve error message of ERR_UNSUPPORTED_ESM_URL_SCHEME Refs: https://github.com/nodejs/node/issues/34765 PR-URL: https://github.com/nodejs/node/pull/34795 Reviewed-By: Matteo Collina Reviewed-By: Ruben Bridgewater Reviewed-By: Jan Krems Reviewed-By: Guy Bedford Reviewed-By: Bradley Farias Reviewed-By: Anna Henningsen Reviewed-By: Rich Trott --- lib/internal/errors.js | 13 +++++++++++-- lib/internal/modules/esm/resolve.js | 2 +- test/es-module/test-esm-dynamic-import.js | 23 ++++++++++++++--------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 8cb6179dff4110..f2c49ec0e545ea 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -27,6 +27,8 @@ const { WeakMap, } = primordials; +const isWindows = process.platform === 'win32'; + const messages = new Map(); const codes = {}; @@ -1399,8 +1401,15 @@ 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); +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 += '. Absolute Windows paths without prefix are not valid URLs, ' + + "consider using 'file://' prefix"; + } + 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/esm/resolve.js b/lib/internal/modules/esm/resolve.js index f6879465451c83..a598d7c0abbc9e 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -722,7 +722,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 diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index e01b86eed143ed..24d13d7fb8662d 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. ' + + 'Absolute Windows paths without prefix are not valid URLs, ' + + "consider using 'file://' prefix. Received protocol 'c:'"; + expectModuleError(import('C:\\example\\foo.mjs'), + 'ERR_UNSUPPORTED_ESM_URL_SCHEME', + msg); + } })(); From 96325b1979b914d980079f0daffd84cd1ffcd7e5 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Tue, 18 Aug 2020 18:06:35 -0700 Subject: [PATCH 09/13] esm: shorten ERR_UNSUPPORTED_ESM_URL_SCHEME message I know it just got modified to include new information, but this shortens the message a bit without (I hope) losing clarity or meaning. PR-URL: https://github.com/nodejs/node/pull/34836 Reviewed-By: Guy Bedford Reviewed-By: Denys Otrishko --- lib/internal/errors.js | 4 ++-- test/es-module/test-esm-dynamic-import.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/errors.js b/lib/internal/errors.js index f2c49ec0e545ea..f6f1d131815b4d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1404,8 +1404,8 @@ E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " + 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 += '. Absolute Windows paths without prefix are not valid URLs, ' + - "consider using 'file://' prefix"; + msg += + '. On Windows, absolute paths must be valid file:// URLs'; } msg += `. Received protocol '${url.protocol}'`; return msg; diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index 24d13d7fb8662d..4e87866b2bad3b 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -62,8 +62,8 @@ function expectFsNamespace(result) { if (common.isWindows) { const msg = 'Only file and data URLs are supported by the default ESM loader. ' + - 'Absolute Windows paths without prefix are not valid URLs, ' + - "consider using 'file://' prefix. Received protocol 'c:'"; + 'On Windows, absolute paths must be valid file:// URLs. ' + + "Received protocol 'c:'"; expectModuleError(import('C:\\example\\foo.mjs'), 'ERR_UNSUPPORTED_ESM_URL_SCHEME', msg); From b29b966d282419d623f87c759a8d083ffbbd6ec3 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 8 Sep 2020 22:27:55 -0700 Subject: [PATCH 10/13] esm: better package.json parser errors PR-URL: https://github.com/nodejs/node/pull/35117 Reviewed-By: Anna Henningsen Reviewed-By: Michael Dawson Reviewed-By: Rich Trott --- lib/internal/errors.js | 2 +- lib/internal/modules/esm/resolve.js | 21 ++++++++++++------- test/es-module/test-esm-invalid-pjson.js | 8 +++---- .../es-modules/pjson-invalid/package.json | 1 + 4 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 test/fixtures/es-modules/pjson-invalid/package.json diff --git a/lib/internal/errors.js b/lib/internal/errors.js index f6f1d131815b4d..0d15a0d069fd9c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1107,7 +1107,7 @@ E('ERR_INVALID_OPT_VALUE', (name, value) => E('ERR_INVALID_OPT_VALUE_ENCODING', 'The value "%s" is invalid for option "encoding"', TypeError); E('ERR_INVALID_PACKAGE_CONFIG', (path, base, message) => { - return `Invalid package config ${path}${base ? ` imported from ${base}` : + return `Invalid package config ${path}${base ? ` while importing ${base}` : ''}${message ? `. ${message}` : ''}`; }, Error); E('ERR_INVALID_PACKAGE_TARGET', diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index a598d7c0abbc9e..dd24019351a72e 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -77,7 +77,7 @@ function tryStatSync(path) { } } -function getPackageConfig(path) { +function getPackageConfig(path, specifier, base) { const existing = packageJSONCache.get(path); if (existing !== undefined) { return existing; @@ -101,7 +101,11 @@ function getPackageConfig(path) { try { packageJSON = JSONParse(source); } catch (error) { - throw new ERR_INVALID_PACKAGE_CONFIG(path, null, error.message); + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); } let { imports, main, name, type } = packageJSON; @@ -125,13 +129,14 @@ function getPackageConfig(path) { 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; @@ -492,7 +497,7 @@ function packageImportsResolve(name, base, conditions) { throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); } let packageJSONUrl; - const packageConfig = getPackageScopeConfig(base, base); + const packageConfig = getPackageScopeConfig(base); if (packageConfig.exists) { packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); const imports = packageConfig.imports; @@ -530,7 +535,7 @@ function packageImportsResolve(name, base, conditions) { } function getPackageType(url) { - const packageConfig = getPackageScopeConfig(url, url); + const packageConfig = getPackageScopeConfig(url); return packageConfig.type; } @@ -575,7 +580,7 @@ function packageResolve(specifier, base, conditions) { StringPrototypeSlice(specifier, separatorIndex)); // ResolveSelf - const packageConfig = getPackageScopeConfig(base, base); + const packageConfig = getPackageScopeConfig(base); if (packageConfig.exists) { const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); if (packageConfig.name === packageName && @@ -603,7 +608,7 @@ function packageResolve(specifier, base, conditions) { } // Package match. - const packageConfig = getPackageConfig(packageJSONPath, base); + const packageConfig = getPackageConfig(packageJSONPath, specifier, base); if (packageConfig.exports !== undefined && packageConfig.exports !== null) return packageExportsResolve( packageJSONUrl, packageSubpath, packageConfig, base, conditions diff --git a/test/es-module/test-esm-invalid-pjson.js b/test/es-module/test-esm-invalid-pjson.js index 53ebd4962f0523..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/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 From be78b835f6f5940616a2f3d93a54bc26a83cbbac Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Thu, 27 Aug 2020 23:36:50 +0200 Subject: [PATCH 11/13] module: use isURLInstance instead of instanceof PR-URL: https://github.com/nodejs/node/pull/34951 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Derek Lewis --- lib/internal/modules/cjs/loader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index b0e4a0639b01a9..ec8a69e494319b 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -54,7 +54,7 @@ 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'); @@ -1087,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); From 0772b5b53737669eaa459524e0eee818c0395a40 Mon Sep 17 00:00:00 2001 From: Christoph Tavan Date: Fri, 18 Sep 2020 16:35:34 +0200 Subject: [PATCH 12/13] module: fix crash on multiline named cjs imports The node process crashes when trying to parse a multiline import statement for named exports of a CommonJS module: TypeError: Cannot read property '0' of null at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77) at async ModuleJob.run (internal/modules/esm/module_job.js:137:5) at async Loader.import (internal/modules/esm/loader.js:165:24) at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3) at async waitForActual (assert.js:721:5) at async rejects (assert.js:830:25), The reason is that the regexp that is currently used to decorate the original error fails for multi line import statements. Unfortunately the undecorated error stack only contains the single line which causes the import to fail: file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2 comeOn, ^^^^^^ SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn' at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21) at async ModuleJob.run (internal/modules/esm/module_job.js:141:5) at async Loader.import (internal/modules/esm/loader.js:165:24) at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3) at async waitForActual (assert.js:721:5) at async rejects (assert.js:830:25) Hence, for multiline import statements we cannot create an equivalent piece of code that uses default import followed by an object destructuring assignment. In any case the node process should definitely not crash. So until we have a more sophisticated way of extracting the entire problematic multiline import statement, show the code example only for single-line imports where the current regexp approach works well. Refs: https://github.com/nodejs/node/issues/35259 PR-URL: https://github.com/nodejs/node/pull/35275 Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen Reviewed-By: Myles Borins Reviewed-By: Rich Trott --- lib/internal/modules/esm/module_job.js | 21 ++++++++++++------- test/es-module/test-esm-cjs-named-error.mjs | 11 ++++++++++ .../package-cjs-named-error/fail.cjs | 3 ++- .../package-cjs-named-error/multi-line.mjs | 4 ++++ 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs 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/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/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'; From ab60e951725011927936f18a17e1b00ceda9c266 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 11 Aug 2020 18:40:05 -0700 Subject: [PATCH 13/13] module: exports pattern support PR-URL: https://github.com/nodejs/node/pull/34718 Reviewed-By: Jan Krems Reviewed-By: Matteo Collina --- doc/api/esm.md | 102 ++++++++++++------ lib/internal/modules/esm/resolve.js | 64 +++++++---- test/es-module/test-esm-exports.mjs | 3 + .../es-modules/pkgimports/package.json | 4 +- .../node_modules/pkgexports/package.json | 4 +- 5 files changed, 122 insertions(+), 55 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 0efa316ac47dd1..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 @@ -1741,7 +1756,8 @@ The resolver can throw the following errors: > 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_). +> _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 @@ -1775,29 +1791,43 @@ _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_). +> _packageURL_, _target_, _""_, **false**, _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. 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_, _isImports_, _conditions_). +> _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_, _internal_, -_conditions_) +**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_, +_internal_, _conditions_) > 1. If _target_ is a String, then -> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_, -> throw an _Invalid Module Specifier_ 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. @@ -1809,8 +1839,12 @@ _conditions_) > 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. 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. @@ -1819,7 +1853,8 @@ _conditions_) > 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_, _conditions_). +> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_, +> _conditions_). > 1. If _resolved_ is equal to **undefined**, continue the loop. > 1. Return _resolved_. > 1. Return **undefined**. @@ -1827,8 +1862,9 @@ _conditions_) > 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_, _conditions_), -> continuing the loop on any _Invalid Package Target_ error. +> _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. diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index dd24019351a72e..92760d201beb4c 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -307,10 +307,11 @@ function throwInvalidPackageTarget( } const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; +const patternRegEx = /\*/g; function resolvePackageTargetString( - target, subpath, match, packageJSONUrl, base, internal, conditions) { - if (subpath !== '' && target[target.length - 1] !== '/') + target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); if (!StringPrototypeStartsWith(target, './')) { @@ -321,8 +322,12 @@ function resolvePackageTargetString( new URL(target); isURL = true; } catch {} - if (!isURL) - return packageResolve(target + subpath, packageJSONUrl, conditions); + if (!isURL) { + const exportTarget = pattern ? + StringPrototypeReplace(target, patternRegEx, subpath) : + target + subpath; + return packageResolve(exportTarget, packageJSONUrl, conditions); + } } throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } @@ -342,6 +347,9 @@ function resolvePackageTargetString( 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); } @@ -356,10 +364,10 @@ function isArrayIndex(key) { } function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, - base, internal, conditions) { + base, pattern, internal, conditions) { if (typeof target === 'string') { return resolvePackageTargetString( - target, subpath, packageSubpath, packageJSONUrl, base, internal, + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, conditions); } else if (ArrayIsArray(target)) { if (target.length === 0) @@ -371,8 +379,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, let resolved; try { resolved = resolvePackageTarget( - packageJSONUrl, targetItem, subpath, packageSubpath, base, internal, - conditions); + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, conditions); } catch (e) { lastException = e; if (e.code === 'ERR_INVALID_PACKAGE_TARGET') @@ -406,7 +414,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, const conditionalTarget = target[key]; const resolved = resolvePackageTarget( packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - internal, conditions); + pattern, internal, conditions); if (resolved === undefined) continue; return resolved; @@ -460,7 +468,7 @@ function packageExportsResolve( if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { const target = exports[packageSubpath]; const resolved = resolvePackageTarget( - packageJSONUrl, target, '', packageSubpath, base, false, conditions + packageJSONUrl, target, '', packageSubpath, base, false, false, conditions ); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); @@ -471,7 +479,13 @@ function packageExportsResolve( const keys = ObjectGetOwnPropertyNames(exports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key[key.length - 1] === '/' && + 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; @@ -480,12 +494,15 @@ function packageExportsResolve( if (bestMatch) { const target = exports[bestMatch]; - const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length - + (pattern ? 1 : 0)); const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, - bestMatch, base, false, conditions); + bestMatch, base, pattern, false, + conditions); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); - return { resolved, exact: false }; + return { resolved, exact: pattern }; } throwExportsNotFound(packageSubpath, packageJSONUrl, base); @@ -504,7 +521,7 @@ function packageImportsResolve(name, base, conditions) { if (imports) { if (ObjectPrototypeHasOwnProperty(imports, name)) { const resolved = resolvePackageTarget( - packageJSONUrl, imports[name], '', name, base, true, conditions + packageJSONUrl, imports[name], '', name, base, false, true, conditions ); if (resolved !== null) return { resolved, exact: true }; @@ -513,7 +530,13 @@ function packageImportsResolve(name, base, conditions) { const keys = ObjectGetOwnPropertyNames(imports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key[key.length - 1] === '/' && + 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; @@ -522,11 +545,14 @@ function packageImportsResolve(name, base, conditions) { if (bestMatch) { const target = imports[bestMatch]; - const subpath = StringPrototypeSubstr(name, bestMatch.length); + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(name, bestMatch.length - + (pattern ? 1 : 0)); const resolved = resolvePackageTarget( - packageJSONUrl, target, subpath, bestMatch, base, true, conditions); + packageJSONUrl, target, subpath, bestMatch, base, pattern, true, + conditions); if (resolved !== null) - return { resolved, exact: false }; + return { resolved, exact: pattern }; } } } diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index a4cced41f897b0..d234099732e3aa 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -33,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) { diff --git a/test/fixtures/es-modules/pkgimports/package.json b/test/fixtures/es-modules/pkgimports/package.json index 7cd179631fa618..a2224b39ddd2ac 100644 --- a/test/fixtures/es-modules/pkgimports/package.json +++ b/test/fixtures/es-modules/pkgimports/package.json @@ -5,9 +5,9 @@ "import": "./importbranch.js", "require": "./requirebranch.js" }, - "#subpath/": "./sub/", + "#subpath/*": "./sub/*", "#external": "pkgexports/valid-cjs", - "#external/subpath/": "pkgexports/sub/", + "#external/subpath/*": "pkgexports/sub/*", "#external/invalidsubpath/": "pkgexports/sub", "#belowbase": "../belowbase", "#url": "some:url", diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index 71406a407c453d..240122d4aaec95 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -47,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" } }