Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

loader: fix package resolution for edge case #41218

Merged
4 changes: 2 additions & 2 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ function trySelf(parentPath, request) {
try {
return finalizeEsmResolution(packageExportsResolve(
pathToFileURL(pkgPath + '/package.json'), expansion, pkg,
pathToFileURL(parentPath), cjsConditions).resolved, parentPath, pkgPath);
pathToFileURL(parentPath), cjsConditions), parentPath, pkgPath);
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND')
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
Expand All @@ -481,7 +481,7 @@ function resolveExports(nmPath, request) {
try {
return finalizeEsmResolution(packageExportsResolve(
pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null,
cjsConditions).resolved, null, pkgPath);
cjsConditions), null, pkgPath);
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND')
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
Expand Down
61 changes: 41 additions & 20 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const legacyExtensionFormatMap = {
'.node': 'commonjs'
};

let experimentalSpecifierResolutionWarned = false;

if (experimentalWasmModules)
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';

Expand All @@ -54,30 +56,48 @@ const protocolHandlers = ObjectAssign(ObjectCreate(null), {
return format;
},
'file:'(parsed, url) {
const ext = extname(parsed.pathname);
let format;

if (ext === '.js') {
format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs';
} else {
format = extensionFormatMap[ext];
}
if (!format) {
if (experimentalSpecifierResolution === 'node') {
process.emitWarning(
'The Node.js specifier resolution in ESM is experimental.',
'ExperimentalWarning');
format = legacyExtensionFormatMap[ext];
} else {
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
}
}

return format || null;
return getModuleFileFormat(parsed, true, url) || null;
},
'node:'() { return 'builtin'; },
});

function throwIfNotExperimentalSpecifierResolution(ext, url) {
if (experimentalSpecifierResolution !== 'node') {
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
}
return getLegacyExtensionFormat(ext);
}

function getLegacyExtensionFormat(ext) {
if (
experimentalSpecifierResolution === 'node' &&
!experimentalSpecifierResolutionWarned
) {
process.emitWarning(
'The Node.js specifier resolution in ESM is experimental.',
'ExperimentalWarning');
experimentalSpecifierResolutionWarned = true;
}
return legacyExtensionFormatMap[ext];
}

function getModuleFileFormat(url, throwIfNotResolved, urlForThrow) {
let format;

const ext = extname(url.pathname);
if (ext === '.js') {
format = getPackageType(url) === 'module' ? 'module' : 'commonjs';
aduh95 marked this conversation as resolved.
Show resolved Hide resolved
} else {
format = extensionFormatMap[ext] ??
(throwIfNotResolved ?
throwIfNotExperimentalSpecifierResolution(ext, urlForThrow) :
getLegacyExtensionFormat(ext)
);
}

return format;
}

function defaultGetFormat(url, context) {
const parsed = new URL(url);

Expand All @@ -89,5 +109,6 @@ function defaultGetFormat(url, context) {
module.exports = {
defaultGetFormat,
extensionFormatMap,
getModuleFileFormat,
legacyExtensionFormatMap,
};
73 changes: 36 additions & 37 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,22 +464,6 @@ const patternRegEx = /\*/g;
function resolvePackageTargetString(
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {

const composeResult = (resolved) => {
let format;
try {
format = getPackageType(resolved);
} catch (err) {
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
const invalidModuleErr = new ERR_INVALID_MODULE_SPECIFIER(
resolved, 'must not include encoded "/" or "\\" characters', base);
invalidModuleErr.cause = err;
throw invalidModuleErr;
}
throw err;
}
return { resolved, ...(format !== 'none') && { format } };
};

if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);

Expand Down Expand Up @@ -512,7 +496,7 @@ function resolvePackageTargetString(
if (!StringPrototypeStartsWith(resolvedPath, packagePath))
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);

if (subpath === '') return composeResult(resolved);
if (subpath === '') return resolved;

if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) {
const request = pattern ?
Expand All @@ -521,12 +505,16 @@ function resolvePackageTargetString(
}

if (pattern) {
return composeResult(new URL(RegExpPrototypeSymbolReplace(patternRegEx,
resolved.href,
() => subpath)));
return new URL(
RegExpPrototypeSymbolReplace(
patternRegEx,
resolved.href,
() => subpath
)
);
}

return composeResult(new URL(subpath, resolved));
return new URL(subpath, resolved);
}

/**
Expand Down Expand Up @@ -753,7 +741,7 @@ function packageImportsResolve(name, base, conditions) {
packageJSONUrl, imports[name], '', name, base, false, true, conditions
);
if (resolveResult != null) {
return resolveResult.resolved;
return resolveResult;
}
} else {
let bestMatch = '';
Expand Down Expand Up @@ -785,7 +773,7 @@ function packageImportsResolve(name, base, conditions) {
bestMatch, base, true,
true, conditions);
if (resolveResult != null) {
return resolveResult.resolved;
return resolveResult;
}
}
}
Expand Down Expand Up @@ -849,7 +837,7 @@ function parsePackageName(specifier, base) {
*/
function packageResolve(specifier, base, conditions) {
if (NativeModule.canBeRequiredByUsers(specifier))
return { resolved: new URL('node:' + specifier) };
return new URL('node:' + specifier);

const { packageName, packageSubpath, isScoped } =
parsePackageName(specifier, base);
Expand Down Expand Up @@ -888,19 +876,14 @@ function packageResolve(specifier, base, conditions) {
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
if (packageSubpath === '.') {
return {
resolved: legacyMainResolve(
packageJSONUrl,
packageConfig,
base),
...(packageConfig.type !== 'none') && { format: packageConfig.type }
};
return legacyMainResolve(
packageJSONUrl,
packageConfig,
base
);
}

return {
resolved: new URL(packageSubpath, packageJSONUrl),
...(packageConfig.type !== 'none') && { format: packageConfig.type }
};
return new URL(packageSubpath, packageJSONUrl);
// Cross-platform root check.
} while (packageJSONPath.length !== lastPath.length);

Expand Down Expand Up @@ -933,6 +916,12 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
return isRelativeSpecifier(specifier);
}

function amendFormatToUrl(resolved) {
const format = getModuleFileFormat(resolved, false);

return { resolved, ...(format !== null) && { format } };
Copy link
Contributor

@aduh95 aduh95 Dec 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this code snippet at the top of test/es-module/test-esm-resolve-type.js please?

Reflect.defineProperty(Object.prototype, 'format', {
  get: common.mustNotCall('get Object.prototype.format'),
})

And wrap all assert.strictEqual(resolveResult.format, expectedResolvedFormat); in a if (Object.hasOwn(resolveResult, 'format') || expectedResolvedFormat !== undefined).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot commit this because the tests would fail. For example here.

}

/**
* @param {string} specifier
* @param {string | URL | undefined} base
Expand All @@ -945,6 +934,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
// Ok since relative URLs cannot parse as URLs.
let resolved;
let format;

if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
resolved = new URL(specifier, base);
} else if (specifier[0] === '#') {
Expand All @@ -953,7 +943,13 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
try {
resolved = new URL(specifier);
} catch {
({ resolved, format } = packageResolve(specifier, base, conditions));
({ resolved, format } =
amendFormatToUrl(
guybedford marked this conversation as resolved.
Show resolved Hide resolved
packageResolve(
specifier,
base,
conditions
)));
}
}
if (resolved.protocol !== 'file:') {
Expand Down Expand Up @@ -1107,4 +1103,7 @@ module.exports = {
};

// cycle
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const {
defaultGetFormat,
getModuleFileFormat,
} = require('internal/modules/esm/get_format');
Loading