diff --git a/doc/api/packages.md b/doc/api/packages.md index fb393eb823b7bf..ef55618c4137ca 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -485,7 +485,7 @@ where `import '#dep'` does not get the resolution of the external package file `./dep-polyfill.js` relative to the package in other environments. Unlike the `"exports"` field, the `"imports"` field permits mapping to external -packages. +packages and locations. The resolution rules for the imports field are otherwise analogous to the exports field. diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index f9673a3ed54571..758b8f582007ad 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -105,6 +105,9 @@ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, interna } const pjsonPath = fileURLToPath(pjsonUrl); const double = RegExpPrototypeExec(doubleSlashRegEx, isTarget ? target : request) !== null; + console.trace({ + target, request + }) process.emitWarning( `Use of deprecated ${double ? 'double slash' : 'leading or trailing slash matching'} resolving "${target}" for module ` + @@ -355,6 +358,7 @@ const patternRegEx = /\*/g; * @param {boolean} internal - Whether the target is internal to the package. * @param {boolean} isPathMap - Whether the target is a path map. * @param {string[]} conditions - The import conditions. + * @param {boolean} mustBeInternalTarget - If target must be in the package boundary. * @returns {URL} - The resolved URL object. * @throws {ERR_INVALID_PACKAGE_TARGET} - If the target is invalid. * @throws {ERR_INVALID_SUBPATH} - If the subpath is invalid. @@ -369,14 +373,15 @@ function resolvePackageTargetString( internal, isPathMap, conditions, + mustBeInternalTarget = true, ) { if (subpath !== '' && !pattern && target[target.length - 1] !== '/') { throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); } - if (!StringPrototypeStartsWith(target, './')) { - if (internal && !StringPrototypeStartsWith(target, '../') && + if (!StringPrototypeStartsWith(target, './') && !StringPrototypeStartsWith(target, '../')) { + if (internal && !StringPrototypeStartsWith(target, '/')) { // No need to convert target to string, since it's already presumed to be if (!URLCanParse(target)) { @@ -390,8 +395,17 @@ function resolvePackageTargetString( throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); } - if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null) { - if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, 2)) === null) { + // skip ../ and ./ prefixes when looking for invalid segments + const skipPrefix = target.length > 1 && target[0] === '.' ? + target[1] === '/' ? + 2 : + target.length > 2 && target[1] === '.' && target[2] === '/' ? + 3 : + 0 + : + 0 + if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, skipPrefix)) !== null) { + if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, skipPrefix)) === null) { if (!isPathMap) { const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : @@ -410,7 +424,7 @@ function resolvePackageTargetString( const resolvedPath = resolved.pathname; const packagePath = new URL('.', packageJSONUrl).pathname; - if (!StringPrototypeStartsWith(resolvedPath, packagePath)) { + if (mustBeInternalTarget && !StringPrototypeStartsWith(resolvedPath, packagePath)) { throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); } @@ -461,14 +475,16 @@ function isArrayIndex(key) { * @param {boolean} internal - Whether the package is internal. * @param {boolean} isPathMap - Whether the package is a path map. * @param {Set} conditions - The conditions to match. + * @param {boolean} mustBeInternalTarget - If the target must be in the package boundary. Used to restrict + * targets to be inside of the package.json directory for "exports". * @returns {URL | null | undefined} - The resolved target, or null if not found, or undefined if not resolvable. */ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, - base, pattern, internal, isPathMap, conditions) { + base, pattern, internal, isPathMap, conditions, mustBeInternalTarget) { if (typeof target === 'string') { return resolvePackageTargetString( target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, - isPathMap, conditions); + isPathMap, conditions, mustBeInternalTarget); } else if (ArrayIsArray(target)) { if (target.length === 0) { return null; @@ -481,7 +497,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, try { resolveResult = resolvePackageTarget( packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, - internal, isPathMap, conditions); + internal, isPathMap, conditions, mustBeInternalTarget); } catch (e) { lastException = e; if (e.code === 'ERR_INVALID_PACKAGE_TARGET') { @@ -518,7 +534,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, const conditionalTarget = target[key]; const resolveResult = resolvePackageTarget( packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - pattern, internal, isPathMap, conditions); + pattern, internal, isPathMap, conditions, mustBeInternalTarget); if (resolveResult === undefined) { continue; } return resolveResult; } @@ -583,6 +599,7 @@ function packageExportsResolve( const resolveResult = resolvePackageTarget( packageJSONUrl, target, '', packageSubpath, base, false, false, false, conditions, + true, ); if (resolveResult == null) { @@ -635,7 +652,7 @@ function packageExportsResolve( true, false, StringPrototypeEndsWith(packageSubpath, '/'), - conditions); + conditions, true); if (resolveResult == null) { throw exportsNotFound(packageSubpath, packageJSONUrl, base); @@ -693,6 +710,7 @@ function packageImportsResolve(name, base, conditions) { const resolveResult = resolvePackageTarget( packageJSONUrl, imports[name], '', name, base, false, true, false, conditions, + false, ); if (resolveResult != null) { return resolveResult; @@ -725,7 +743,8 @@ function packageImportsResolve(name, base, conditions) { const resolveResult = resolvePackageTarget(packageJSONUrl, target, bestMatchSubpath, bestMatch, base, true, - true, false, conditions); + true, false, conditions, + false); if (resolveResult != null) { return resolveResult; } diff --git a/test/es-module/test-esm-imports.mjs b/test/es-module/test-esm-imports.mjs index 4b7e97eb8d2c5c..35b44c9c493de7 100644 --- a/test/es-module/test-esm-imports.mjs +++ b/test/es-module/test-esm-imports.mjs @@ -26,6 +26,10 @@ const { requireImport, importImport } = importer; ['#subpath//asdf.asdf', { default: 'test' }], // Double slash ['#subpath/as//df.asdf', { default: 'test' }], + // Target steps below the package base + ['#belowbase', { default: 'belowbase' }], + // Target steps uses pattern below the package base + ['#belowbase/nested', { default: 'nested' }], ]); for (const [validSpecifier, expected] of internalImports) { @@ -38,8 +42,6 @@ const { requireImport, importImport } = importer; } const invalidImportTargets = new Set([ - // Target steps below the package base - ['#belowbase', '#belowbase'], // Target is a URL ['#url', '#url'], ]); diff --git a/test/fixtures/es-modules/belowbase.js b/test/fixtures/es-modules/belowbase.js new file mode 100644 index 00000000000000..ad892195a79891 --- /dev/null +++ b/test/fixtures/es-modules/belowbase.js @@ -0,0 +1 @@ +module.exports = 'belowbase' diff --git a/test/fixtures/es-modules/belowbase/nested.js b/test/fixtures/es-modules/belowbase/nested.js new file mode 100644 index 00000000000000..895868ff0d1e79 --- /dev/null +++ b/test/fixtures/es-modules/belowbase/nested.js @@ -0,0 +1 @@ +module.exports = 'nested' diff --git a/test/fixtures/es-modules/pkgimports/package.json b/test/fixtures/es-modules/pkgimports/package.json index dbbbcd1ab01ea1..fb056228b9f7bf 100644 --- a/test/fixtures/es-modules/pkgimports/package.json +++ b/test/fixtures/es-modules/pkgimports/package.json @@ -11,7 +11,8 @@ "#external": "pkgexports/valid-cjs", "#external/subpath/*": "pkgexports/sub/*", "#external/invalidsubpath/": "pkgexports/sub", - "#belowbase": "../belowbase", + "#belowbase": "../belowbase.js", + "#belowbase/*": "../belowbase/*.js", "#url": "some:url", "#null": null, "#nullcondition": {