From 1ae76bd0750e2d05ab2078eae4980335cd2c7922 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 27 Jun 2020 22:09:24 -0700 Subject: [PATCH] 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 | 74 ++++-- lib/internal/modules/esm/resolve.js | 230 ++++++++++++------ src/node_file.cc | 1 + test/es-module/test-esm-exports.mjs | 6 +- 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(+), 177 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 660d0d539672fa..f9d75a8cf0726f 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1704,6 +1704,12 @@ A non-context-aware native addon was loaded in a process that disallows them. A given value is out of the accepted range. + +### `ERR_PACKAGE_IMPORT_NOT_DEFINED` + +The `package.json` ["imports" field][] does not define the given internal +package specifier mapping. + ### `ERR_PACKAGE_PATH_NOT_EXPORTED` @@ -2560,3 +2566,4 @@ closed. [vm]: vm.html [self-reference a package using its name]: esm.html#esm_self_referencing_a_package_using_its_name [define a custom subpath]: esm.html#esm_subpath_exports +["imports" field]: esm.html#esm_internal_package_imports diff --git a/doc/api/esm.md b/doc/api/esm.md index 75a7c07ddbcbe3..d705e2d8efdfda 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -538,6 +538,43 @@ and in a CommonJS one. For example, this code will also work: const { something } = require('a-package/foo'); // Loads from ./foo.js. ``` +### Internal package imports + +In addition to the `"exports"` field it is possible to define internal package +import maps that only apply to import specifiers from within the package itself. + +Entries in the imports field must always start with `#` to ensure they are +clearly disambiguated from package specifiers. + +For example, the imports field can be used to gain the benefits of conditional +exports for internal modules: + +```json +// package.json +{ + "imports": { + "#dep": { + "node": "dep-node-native", + "default": "./dep-polyfill.js" + } + }, + "dependencies": { + "dep-node-native": "^1.0.0" + } +} +``` + +where `import '#dep'` would now get the resolution of the external package +`dep-node-native` (including its exports in turn), and instead get the local +file `./dep-polyfill.js` relative to the package in other environments. + +Unlike the exports field, import maps permit mapping to external packages +because this provides an important use case for conditional loading and also can +be done without the risk of cycles, unlike for exports. + +Apart from the above, the resolution rules for the imports field are otherwise +analogous to the exports field. + ### Dual CommonJS/ES module packages Prior to the introduction of support for ES modules in Node.js, it was a common @@ -1552,10 +1589,11 @@ The resolver can throw the following errors: or package subpath specifier. * _Invalid Package Configuration_: package.json configuration is invalid or contains an invalid configuration. -* _Invalid Package Target_: Package exports define a target module within the - package that is an invalid type or string target. +* _Invalid Package Target_: Package exports or imports define a target module + for the package that is an invalid type or string target. * _Package Path Not Exported_: Package exports do not define or permit a target subpath in the package for the given module. +* _Package Import Not Defined_: Package imports do not define the specifier. * _Module Not Found_: The package or module requested does not exist.
@@ -1567,11 +1605,14 @@ The resolver can throw the following errors: > 1. If _specifier_ is a valid URL, then > 1. Set _resolvedURL_ to the result of parsing and reserializing > _specifier_ as a URL. -> 1. Otherwise, if _specifier_ starts with _"/"_, then -> 1. Throw an _Invalid Module Specifier_ error. -> 1. Otherwise, if _specifier_ starts with _"./"_ or _"../"_, then +> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_, then > 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to > _parentURL_. +> 1. Otherwise, if _specifier_ starts with _"#"_, then +> 1. Set _resolvedURL_ to the result of +> **PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_). +> 1. If _resolvedURL_ is **null** or **undefined**, throw a +> _Package Import Not Defined_ error. > 1. Otherwise, > 1. Note: _specifier_ is now a bare specifier. > 1. Set _resolvedURL_ the result of @@ -1609,7 +1650,7 @@ The resolver can throw the following errors: > 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent > encoded strings for _"/"_ or _"\\"_, then > 1. Throw an _Invalid Module Specifier_ error. -> 1. Set _selfUrl_ to the result of +> 1. Let _selfUrl_ be the result of > **SELF_REFERENCE_RESOLVE**(_packageName_, _packageSubpath_, _parentURL_). > 1. If _selfUrl_ isn't empty, return _selfUrl_. > 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin @@ -1632,8 +1673,11 @@ The resolver can throw the following errors: > 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then > 1. Let _exports_ be _pjson.exports_. > 1. If _exports_ is not **null** or **undefined**, then -> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, -> _packageSubpath_, _pjson.exports_). +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( +> _packageURL_, _packageSubpath_, _pjson.exports_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Return the URL resolution of _packageSubpath_ in _packageURL_. > 1. Throw a _Module Not Found_ error. @@ -1654,8 +1698,11 @@ The resolver can throw the following errors: > 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then > 1. Let _exports_ be _pjson.exports_. > 1. If _exports_ is not **null** or **undefined**, then -> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_, -> _pjson.exports_). +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_RESOLVE**( +> _packageURL_, _subpath_, _pjson.exports_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Return the URL resolution of _subpath_ in _packageURL_. > 1. Otherwise, return **undefined**. @@ -1668,12 +1715,18 @@ The resolver can throw the following errors: > not starting with _"."_, throw an _Invalid Package Configuration_ error. > 1. If _pjson.exports_ is a String or Array, or an Object containing no > keys starting with _"."_, then -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _pjson.exports_, _""_). +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _pjson.exports_, _""_, **false**, _defaultEnv_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. If _pjson.exports_ is an Object containing a _"."_ property, then > 1. Let _mainExport_ be the _"."_ property in _pjson.exports_. -> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _mainExport_, _""_). +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _mainExport_, _""_, **false**, _defaultEnv_). +> 1. If _resolved_ is **null** or **undefined**, throw a +> _Package Path Not Exported_ error. +> 1. Return _resolved_. > 1. Throw a _Package Path Not Exported_ error. > 1. Let _legacyMainURL_ be the result applying the legacy > **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a @@ -1687,8 +1740,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 @@ -1696,22 +1749,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 @@ -1723,22 +1782,48 @@ The resolver can throw the following errors: > 1. For each property _p_ of _target_, in object insertion order as, > 1. If _p_ equals _"default"_ or _env_ contains an entry for _p_, then > 1. Let _targetValue_ be the value of the _p_ property in _target_. -> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _env_), continuing the -> loop on any _Package Path Not Exported_ error. -> 1. Throw a _Package Path Not Exported_ error. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _targetValue_, _subpath_, _internal_, _env_) +> 1. If _resolved_ is equal to **undefined**, continue the loop. +> 1. Return _resolved_. +> 1. Return **undefined**. > 1. Otherwise, if _target_ is an Array, then -> 1. If _target.length is zero, throw a _Package Path Not Exported_ error. +> 1. If _target.length is zero, return **null**. > 1. For each item _targetValue_ in _target_, do -> 1. If _targetValue_ is an Array, continue the loop. -> 1. Return the result of **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, -> _targetValue_, _subpath_, _env_), continuing the loop on any -> _Package Path Not Exported_ or _Invalid Package Target_ error. -> 1. Throw the last fallback resolution error. -> 1. Otherwise, if _target_ is _null_, throw a _Package Path Not Exported_ -> error. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _targetValue_, _subpath_, _internal_, _env_), +> continuing the loop on any _Invalid Package Target_ error. +> 1. If _resolved_ is **undefined**, continue the loop. +> 1. Return _resolved_. +> 1. Return or throw the last fallback resolution **null** return or error. +> 1. Otherwise, if _target_ is _null_, return **null**. > 1. Otherwise throw an _Invalid Package Target_ error. +**PACKAGE_INTERNAL_RESOLVE**(_specifier_, _parentURL_) + +> 1. Assert: _specifier_ begins with _"#"_. +> 1. If _specifier_ is exactly equal to _"#"_ or starts with _"#/"_, then +> 1. Throw an _Invalid Module Specifier_ error. +> 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_). +> 1. If _packageURL_ is not **null**, then +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). +> 1. If _pjson.imports is a non-null Object, then +> 1. Let _imports_ be _pjson.imports_. +> 1. If _specifier_ is a key of _imports_, then +> 1. Let _target_ be the value of _imports\[specifier\]_. +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _""_, **true**, _defaultEnv_). +> 1. Let _directoryKeys_ be the list of keys of _imports_ ending in +> _"/"_, sorted by length descending. +> 1. For each key _directory_ in _directoryKeys_, do +> 1. If _specifier_ starts with _directory_, then +> 1. Let _target_ be the value of _imports\[directory\]_. +> 1. Let _subpath_ be the substring of _target_ starting at the +> index of the length of _directory_. +> 1. Return **PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, +> _subpath_, **true**, _defaultEnv_). +> 1. Return **null**. + **ESM_FORMAT**(_url_) > 1. Assert: _url_ corresponds to an existing file. diff --git a/doc/api/modules.md b/doc/api/modules.md index eccf4ed68d3e58..1d4c429529ec05 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 e43c0006363d13..2fdbdc1593c071 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -20,7 +20,6 @@ const { NumberIsInteger, ObjectDefineProperty, ObjectKeys, - StringPrototypeSlice, StringPrototypeStartsWith, Symbol, SymbolFor, @@ -1105,16 +1104,9 @@ E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError); E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError); E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); -E('ERR_INVALID_MODULE_SPECIFIER', (pkgPath, subpath, base = undefined) => { - if (subpath === undefined) { - return `Invalid package name '${pkgPath}' imported from ${base}`; - } else if (base === undefined) { - assert(subpath !== '.'); - return `Package subpath '${subpath}' is not a valid module request for ` + - `the "exports" resolution of ${pkgPath}${sep}package.json`; - } - return `Package subpath '${subpath}' is not a valid module request for ` + - `the "exports" resolution of ${pkgPath} imported from ${base}`; +E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => { + return `Invalid module "${request}" ${reason}${base ? + ` imported from ${base}` : ''}`; }, TypeError); E('ERR_INVALID_OPT_VALUE', (name, value) => `The value "${String(value)}" is invalid for option "${name}"`, @@ -1128,31 +1120,20 @@ E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => { return `Invalid JSON in ${path} imported from ${message}`; }, Error); E('ERR_INVALID_PACKAGE_TARGET', - (pkgPath, key, subpath, target, base = undefined) => { - const relError = typeof target === 'string' && + (pkgPath, key, target, isImport = false, base = undefined) => { + const relError = typeof target === 'string' && !isImport && target.length && !StringPrototypeStartsWith(target, './'); - if (key === null) { - if (subpath !== '') { - return `Invalid "exports" target ${JSONStringify(target)} defined ` + - `for '${subpath}' in the package config ${pkgPath} imported from ` + - `${base}.${relError ? '; targets must start with "./"' : ''}`; - } - return `Invalid "exports" main target ${target} defined in the ` + - `package config ${pkgPath} imported from ${base}${relError ? - '; targets must start with "./"' : ''}`; - } else if (key === '.') { + if (key === '.') { + assert(isImport === false); return `Invalid "exports" main target ${JSONStringify(target)} defined ` + - `in the package config ${pkgPath}${sep}package.json${relError ? - '; targets must start with "./"' : ''}`; - } else if (relError) { - return `Invalid "exports" target ${JSONStringify(target)} defined for '${ - StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + - `package config ${pkgPath}${sep}package.json; ` + - 'targets must start with "./"'; + `in the package config ${pkgPath}package.json${base ? + ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; } - return `Invalid "exports" target ${JSONStringify(target)} defined for '${ - StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + - `package config ${pkgPath}${sep}package.json`; + return `Invalid "${isImport ? 'imports' : 'exports'}" target ${ + JSONStringify(target)} defined for '${key}' in the package config ${ + pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; }, Error); E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set', Error); @@ -1298,15 +1279,16 @@ E('ERR_OUT_OF_RANGE', msg += ` It must be ${range}. Received ${received}`; return msg; }, RangeError); +E('ERR_PACKAGE_IMPORT_NOT_DEFINED', (specifier, packagePath, base) => { + return `Package import specifier "${specifier}" is not defined${packagePath ? + ` in package ${packagePath}package.json` : ''} imported from ${base}`; +}, TypeError); E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => { - if (subpath === '.') { - return `No "exports" main resolved in ${pkgPath}${sep}package.json`; - } else if (base === undefined) { - return `Package subpath '${subpath}' is not defined by "exports" in ${ - pkgPath}${sep}package.json`; - } + if (subpath === '.') + return `No "exports" main defined in ${pkgPath}package.json${base ? + ` imported from ${base}` : ''}`; return `Package subpath '${subpath}' is not defined by "exports" in ${ - pkgPath} imported from ${base}`; + pkgPath}package.json${base ? ` imported from ${base}` : ''}`; }, Error); E('ERR_REQUIRE_ESM', (filename, parentPath = null, packageJsonPath = null) => { @@ -1426,7 +1408,7 @@ E('ERR_UNKNOWN_FILE_EXTENSION', E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " + -'resolving ES modules, imported from %s', Error); +'resolving ES modules imported from %s', Error); E('ERR_UNSUPPORTED_ESM_URL_SCHEME', 'Only file and data URLs are supported ' + 'by the default ESM loader', Error); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 198afb3a7689f2..96cfffe90a0d50 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, @@ -39,6 +45,7 @@ const { ReflectSet, RegExpPrototypeTest, SafeMap, + SafeSet, String, StringPrototypeIndexOf, StringPrototypeMatch, @@ -93,24 +100,24 @@ 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 { isProxy } = require('internal/util/types'); +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); @@ -272,6 +279,7 @@ function readPackage(requestPath) { name: parsed.name, main: parsed.main, exports: parsed.exports, + imports: parsed.imports, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); @@ -545,21 +553,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 { @@ -601,13 +614,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 = /(?:^|\/)\.?\.$/; @@ -962,6 +974,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; @@ -1004,6 +1018,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' + @@ -1356,8 +1391,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 ccbf624c2179bc..f1d9824dfe871f 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -957,6 +957,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 f9dc6ec472f424..02caceee64deaa 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -1,4 +1,3 @@ -// Flags: --experimental-modules import { mustCall } from '../common/index.mjs'; import { ok, deepStrictEqual, strictEqual } from 'assert'; @@ -119,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); })); } @@ -162,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'; +