mirror of
https://github.com/nodejs/node.git
synced 2025-04-30 15:41:06 +00:00

Fixes: https://github.com/nodejs/node/issues/45035 PR-URL: https://github.com/nodejs/node/pull/45044 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Darshan Sen <raisinten@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1130 lines
36 KiB
JavaScript
1130 lines
36 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayIsArray,
|
|
ArrayPrototypeConcat,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeShift,
|
|
JSONStringify,
|
|
ObjectFreeze,
|
|
ObjectGetOwnPropertyNames,
|
|
ObjectPrototypeHasOwnProperty,
|
|
RegExp,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolReplace,
|
|
SafeMap,
|
|
SafeSet,
|
|
String,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeIndexOf,
|
|
StringPrototypeLastIndexOf,
|
|
StringPrototypeReplace,
|
|
StringPrototypeSlice,
|
|
StringPrototypeSplit,
|
|
StringPrototypeStartsWith,
|
|
} = primordials;
|
|
const internalFS = require('internal/fs/utils');
|
|
const { BuiltinModule } = require('internal/bootstrap/loaders');
|
|
const {
|
|
realpathSync,
|
|
statSync,
|
|
Stats,
|
|
} = require('fs');
|
|
const { getOptionValue } = require('internal/options');
|
|
// Do not eagerly grab .manifest, it may be in TDZ
|
|
const policy = getOptionValue('--experimental-policy') ?
|
|
require('internal/process/policy') :
|
|
null;
|
|
const { sep, relative } = require('path');
|
|
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
|
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
|
const experimentalNetworkImports =
|
|
getOptionValue('--experimental-network-imports');
|
|
const typeFlag = getOptionValue('--input-type');
|
|
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
|
|
const {
|
|
ERR_INPUT_TYPE_NOT_ALLOWED,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_INVALID_MODULE_SPECIFIER,
|
|
ERR_INVALID_PACKAGE_CONFIG,
|
|
ERR_INVALID_PACKAGE_TARGET,
|
|
ERR_MANIFEST_DEPENDENCY_MISSING,
|
|
ERR_MODULE_NOT_FOUND,
|
|
ERR_PACKAGE_IMPORT_NOT_DEFINED,
|
|
ERR_PACKAGE_PATH_NOT_EXPORTED,
|
|
ERR_UNSUPPORTED_DIR_IMPORT,
|
|
ERR_NETWORK_IMPORT_DISALLOWED,
|
|
ERR_UNSUPPORTED_ESM_URL_SCHEME,
|
|
} = require('internal/errors').codes;
|
|
|
|
const { Module: CJSModule } = require('internal/modules/cjs/loader');
|
|
const { getPackageConfig, getPackageScopeConfig } = require('internal/modules/esm/package_config');
|
|
|
|
/**
|
|
* @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig
|
|
*/
|
|
|
|
|
|
const userConditions = getOptionValue('--conditions');
|
|
const noAddons = getOptionValue('--no-addons');
|
|
const addonConditions = noAddons ? [] : ['node-addons'];
|
|
|
|
const DEFAULT_CONDITIONS = ObjectFreeze([
|
|
'node',
|
|
'import',
|
|
...addonConditions,
|
|
...userConditions,
|
|
]);
|
|
|
|
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
|
|
|
|
const emittedPackageWarnings = new SafeSet();
|
|
|
|
function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) {
|
|
const pjsonPath = fileURLToPath(pjsonUrl);
|
|
if (emittedPackageWarnings.has(pjsonPath + '|' + match))
|
|
return;
|
|
emittedPackageWarnings.add(pjsonPath + '|' + match);
|
|
process.emitWarning(
|
|
`Use of deprecated trailing slash pattern mapping "${match}" in the ` +
|
|
`"exports" field module resolution of the package at ${pjsonPath}${
|
|
base ? ` imported from ${fileURLToPath(base)}` :
|
|
''}. Mapping specifiers ending in "/" is no longer supported.`,
|
|
'DeprecationWarning',
|
|
'DEP0155'
|
|
);
|
|
}
|
|
|
|
const doubleSlashRegEx = /[/\\][/\\]/;
|
|
|
|
function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, internal, base, isTarget) {
|
|
const pjsonPath = fileURLToPath(pjsonUrl);
|
|
const double = RegExpPrototypeExec(doubleSlashRegEx, isTarget ? target : request) !== null;
|
|
process.emitWarning(
|
|
`Use of deprecated ${double ? 'double slash' :
|
|
'leading or trailing slash matching'} resolving "${target}" for module ` +
|
|
`request "${request}" ${request !== match ? `matched to "${match}" ` : ''
|
|
}in the "${internal ? 'imports' : 'exports'}" field module resolution of the package at ${
|
|
pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.`,
|
|
'DeprecationWarning',
|
|
'DEP0166'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {URL} url
|
|
* @param {URL} packageJSONUrl
|
|
* @param {string | URL | undefined} base
|
|
* @param {string} main
|
|
* @returns {void}
|
|
*/
|
|
function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
|
|
const format = defaultGetFormatWithoutErrors(url);
|
|
if (format !== 'module')
|
|
return;
|
|
const path = fileURLToPath(url);
|
|
const pkgPath = fileURLToPath(new URL('.', packageJSONUrl));
|
|
const basePath = fileURLToPath(base);
|
|
if (main)
|
|
process.emitWarning(
|
|
`Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` +
|
|
`excluding the full filename and extension to the resolved file at "${
|
|
StringPrototypeSlice(path, pkgPath.length)}", imported from ${
|
|
basePath}.\n Automatic extension resolution of the "main" field is ` +
|
|
'deprecated for ES modules.',
|
|
'DeprecationWarning',
|
|
'DEP0151'
|
|
);
|
|
else
|
|
process.emitWarning(
|
|
`No "main" or "exports" field defined in the package.json for ${pkgPath
|
|
} resolving the main entry point "${
|
|
StringPrototypeSlice(path, pkgPath.length)}", imported from ${basePath
|
|
}.\nDefault "index" lookups for the main are deprecated for ES modules.`,
|
|
'DeprecationWarning',
|
|
'DEP0151'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} [conditions]
|
|
* @returns {Set<string>}
|
|
*/
|
|
function getConditionsSet(conditions) {
|
|
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
|
|
if (!ArrayIsArray(conditions)) {
|
|
throw new ERR_INVALID_ARG_VALUE('conditions', conditions,
|
|
'expected an array');
|
|
}
|
|
return new SafeSet(conditions);
|
|
}
|
|
return DEFAULT_CONDITIONS_SET;
|
|
}
|
|
|
|
const realpathCache = new SafeMap();
|
|
|
|
/**
|
|
* @param {string | URL} path
|
|
* @returns {import('fs').Stats}
|
|
*/
|
|
const tryStatSync =
|
|
(path) => statSync(path, { throwIfNoEntry: false }) ?? new Stats();
|
|
|
|
/**
|
|
* @param {string | URL} url
|
|
* @returns {boolean}
|
|
*/
|
|
function fileExists(url) {
|
|
return statSync(url, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
}
|
|
|
|
/**
|
|
* Legacy CommonJS main resolution:
|
|
* 1. let M = pkg_url + (json main field)
|
|
* 2. TRY(M, M.js, M.json, M.node)
|
|
* 3. TRY(M/index.js, M/index.json, M/index.node)
|
|
* 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node)
|
|
* 5. NOT_FOUND
|
|
* @param {URL} packageJSONUrl
|
|
* @param {PackageConfig} packageConfig
|
|
* @param {string | URL | undefined} base
|
|
* @returns {URL}
|
|
*/
|
|
function legacyMainResolve(packageJSONUrl, packageConfig, base) {
|
|
let guess;
|
|
if (packageConfig.main !== undefined) {
|
|
// Note: fs check redundances will be handled by Descriptor cache here.
|
|
if (fileExists(guess = new URL(`./${packageConfig.main}`,
|
|
packageJSONUrl))) {
|
|
return guess;
|
|
} else if (fileExists(guess = new URL(`./${packageConfig.main}.js`,
|
|
packageJSONUrl)));
|
|
else if (fileExists(guess = new URL(`./${packageConfig.main}.json`,
|
|
packageJSONUrl)));
|
|
else if (fileExists(guess = new URL(`./${packageConfig.main}.node`,
|
|
packageJSONUrl)));
|
|
else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`,
|
|
packageJSONUrl)));
|
|
else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`,
|
|
packageJSONUrl)));
|
|
else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`,
|
|
packageJSONUrl)));
|
|
else guess = undefined;
|
|
if (guess) {
|
|
emitLegacyIndexDeprecation(guess, packageJSONUrl, base,
|
|
packageConfig.main);
|
|
return guess;
|
|
}
|
|
// Fallthrough.
|
|
}
|
|
if (fileExists(guess = new URL('./index.js', packageJSONUrl)));
|
|
// So fs.
|
|
else if (fileExists(guess = new URL('./index.json', packageJSONUrl)));
|
|
else if (fileExists(guess = new URL('./index.node', packageJSONUrl)));
|
|
else guess = undefined;
|
|
if (guess) {
|
|
emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main);
|
|
return guess;
|
|
}
|
|
// Not found.
|
|
throw new ERR_MODULE_NOT_FOUND(
|
|
fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base));
|
|
}
|
|
|
|
const encodedSepRegEx = /%2F|%5C/i;
|
|
/**
|
|
* @param {URL} resolved
|
|
* @param {string | URL | undefined} base
|
|
* @param {boolean} preserveSymlinks
|
|
* @returns {URL | undefined}
|
|
*/
|
|
function finalizeResolution(resolved, base, preserveSymlinks) {
|
|
if (RegExpPrototypeExec(encodedSepRegEx, resolved.pathname) !== null)
|
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
|
resolved.pathname, 'must not include encoded "/" or "\\" characters',
|
|
fileURLToPath(base));
|
|
|
|
const path = fileURLToPath(resolved);
|
|
|
|
const stats = tryStatSync(StringPrototypeEndsWith(path, '/') ?
|
|
StringPrototypeSlice(path, -1) : path);
|
|
if (stats.isDirectory()) {
|
|
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, base && fileURLToPath(base), 'module');
|
|
}
|
|
|
|
if (!preserveSymlinks) {
|
|
const real = realpathSync(path, {
|
|
[internalFS.realpathCacheKey]: realpathCache
|
|
});
|
|
const { search, hash } = resolved;
|
|
resolved =
|
|
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));
|
|
resolved.search = search;
|
|
resolved.hash = hash;
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
/**
|
|
* @param {string} specifier
|
|
* @param {URL} packageJSONUrl
|
|
* @param {string | URL | undefined} base
|
|
*/
|
|
function throwImportNotDefined(specifier, packageJSONUrl, base) {
|
|
throw new ERR_PACKAGE_IMPORT_NOT_DEFINED(
|
|
specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)),
|
|
fileURLToPath(base));
|
|
}
|
|
|
|
/**
|
|
* @param {string} subpath
|
|
* @param {URL} packageJSONUrl
|
|
* @param {string | URL | undefined} base
|
|
*/
|
|
function throwExportsNotFound(subpath, packageJSONUrl, base) {
|
|
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
|
|
fileURLToPath(new URL('.', packageJSONUrl)), subpath,
|
|
base && fileURLToPath(base));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} request
|
|
* @param {string} match
|
|
* @param {URL} packageJSONUrl
|
|
* @param {boolean} internal
|
|
* @param {string | URL | undefined} base
|
|
*/
|
|
function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) {
|
|
const reason = `request is not a valid match in pattern "${match}" for the "${
|
|
internal ? 'imports' : 'exports'}" resolution of ${
|
|
fileURLToPath(packageJSONUrl)}`;
|
|
throw new ERR_INVALID_MODULE_SPECIFIER(request, reason,
|
|
base && fileURLToPath(base));
|
|
}
|
|
|
|
function throwInvalidPackageTarget(
|
|
subpath, target, packageJSONUrl, internal, base) {
|
|
if (typeof target === 'object' && target !== null) {
|
|
target = JSONStringify(target, null, '');
|
|
} else {
|
|
target = `${target}`;
|
|
}
|
|
throw new ERR_INVALID_PACKAGE_TARGET(
|
|
fileURLToPath(new URL('.', packageJSONUrl)), subpath, target,
|
|
internal, base && fileURLToPath(base));
|
|
}
|
|
|
|
const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
|
|
const deprecatedInvalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
|
|
const invalidPackageNameRegEx = /^\.|%|\\/;
|
|
const patternRegEx = /\*/g;
|
|
|
|
function resolvePackageTargetString(
|
|
target,
|
|
subpath,
|
|
match,
|
|
packageJSONUrl,
|
|
base,
|
|
pattern,
|
|
internal,
|
|
isPathMap,
|
|
conditions,
|
|
) {
|
|
|
|
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
|
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
|
|
|
if (!StringPrototypeStartsWith(target, './')) {
|
|
if (internal && !StringPrototypeStartsWith(target, '../') &&
|
|
!StringPrototypeStartsWith(target, '/')) {
|
|
let isURL = false;
|
|
try {
|
|
new URL(target);
|
|
isURL = true;
|
|
} catch {
|
|
// Continue regardless of error.
|
|
}
|
|
if (!isURL) {
|
|
const exportTarget = pattern ?
|
|
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
|
|
target + subpath;
|
|
return packageResolve(
|
|
exportTarget, packageJSONUrl, conditions);
|
|
}
|
|
}
|
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
|
}
|
|
|
|
if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null) {
|
|
if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, 2)) === null) {
|
|
if (!isPathMap) {
|
|
const request = pattern ?
|
|
StringPrototypeReplace(match, '*', () => subpath) :
|
|
match + subpath;
|
|
const resolvedTarget = pattern ?
|
|
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
|
|
target;
|
|
emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, true);
|
|
}
|
|
} else {
|
|
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))
|
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
|
|
|
if (subpath === '') return resolved;
|
|
|
|
if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) {
|
|
const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : match + subpath;
|
|
if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, subpath) === null) {
|
|
if (!isPathMap) {
|
|
const resolvedTarget = pattern ?
|
|
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
|
|
target;
|
|
emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, false);
|
|
}
|
|
} else {
|
|
throwInvalidSubpath(request, match, packageJSONUrl, internal, base);
|
|
}
|
|
}
|
|
|
|
if (pattern) {
|
|
return new URL(
|
|
RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath)
|
|
);
|
|
}
|
|
|
|
return new URL(subpath, resolved);
|
|
}
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @returns {boolean}
|
|
*/
|
|
function isArrayIndex(key) {
|
|
const keyNum = +key;
|
|
if (`${keyNum}` !== key) return false;
|
|
return keyNum >= 0 && keyNum < 0xFFFF_FFFF;
|
|
}
|
|
|
|
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
|
|
base, pattern, internal, isPathMap, conditions) {
|
|
if (typeof target === 'string') {
|
|
return resolvePackageTargetString(
|
|
target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal,
|
|
isPathMap, conditions);
|
|
} else if (ArrayIsArray(target)) {
|
|
if (target.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let lastException;
|
|
for (let i = 0; i < target.length; i++) {
|
|
const targetItem = target[i];
|
|
let resolveResult;
|
|
try {
|
|
resolveResult = resolvePackageTarget(
|
|
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
|
|
internal, isPathMap, conditions);
|
|
} catch (e) {
|
|
lastException = e;
|
|
if (e.code === 'ERR_INVALID_PACKAGE_TARGET') {
|
|
continue;
|
|
}
|
|
throw e;
|
|
}
|
|
if (resolveResult === undefined) {
|
|
continue;
|
|
}
|
|
if (resolveResult === null) {
|
|
lastException = null;
|
|
continue;
|
|
}
|
|
return resolveResult;
|
|
}
|
|
if (lastException === undefined || lastException === null)
|
|
return lastException;
|
|
throw lastException;
|
|
} else if (typeof target === 'object' && target !== null) {
|
|
const keys = ObjectGetOwnPropertyNames(target);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if (isArrayIndex(key)) {
|
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
|
fileURLToPath(packageJSONUrl), base,
|
|
'"exports" cannot contain numeric property keys.');
|
|
}
|
|
}
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if (key === 'default' || conditions.has(key)) {
|
|
const conditionalTarget = target[key];
|
|
const resolveResult = resolvePackageTarget(
|
|
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
|
|
pattern, internal, isPathMap, conditions);
|
|
if (resolveResult === undefined)
|
|
continue;
|
|
return resolveResult;
|
|
}
|
|
}
|
|
return undefined;
|
|
} else if (target === null) {
|
|
return null;
|
|
}
|
|
throwInvalidPackageTarget(packageSubpath, target, packageJSONUrl, internal,
|
|
base);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {import('internal/modules/esm/package_config.js').Exports} exports
|
|
* @param {URL} packageJSONUrl
|
|
* @param {string | URL | undefined} base
|
|
* @returns {boolean}
|
|
*/
|
|
function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
|
|
if (typeof exports === 'string' || ArrayIsArray(exports)) return true;
|
|
if (typeof exports !== 'object' || exports === null) return false;
|
|
|
|
const keys = ObjectGetOwnPropertyNames(exports);
|
|
let isConditionalSugar = false;
|
|
let i = 0;
|
|
for (let j = 0; j < keys.length; j++) {
|
|
const key = keys[j];
|
|
const curIsConditionalSugar = key === '' || key[0] !== '.';
|
|
if (i++ === 0) {
|
|
isConditionalSugar = curIsConditionalSugar;
|
|
} else if (isConditionalSugar !== curIsConditionalSugar) {
|
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
|
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.');
|
|
}
|
|
}
|
|
return isConditionalSugar;
|
|
}
|
|
|
|
/**
|
|
* @param {URL} packageJSONUrl
|
|
* @param {string} packageSubpath
|
|
* @param {PackageConfig} packageConfig
|
|
* @param {string | URL | undefined} base
|
|
* @param {Set<string>} conditions
|
|
* @returns {URL}
|
|
*/
|
|
function packageExportsResolve(
|
|
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
|
|
let exports = packageConfig.exports;
|
|
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
|
|
exports = { '.': exports };
|
|
|
|
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
|
|
!StringPrototypeIncludes(packageSubpath, '*') &&
|
|
!StringPrototypeEndsWith(packageSubpath, '/')) {
|
|
const target = exports[packageSubpath];
|
|
const resolveResult = resolvePackageTarget(
|
|
packageJSONUrl, target, '', packageSubpath, base, false, false, false,
|
|
conditions
|
|
);
|
|
|
|
if (resolveResult == null) {
|
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
|
}
|
|
|
|
return resolveResult;
|
|
}
|
|
|
|
let bestMatch = '';
|
|
let bestMatchSubpath;
|
|
const keys = ObjectGetOwnPropertyNames(exports);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
const patternIndex = StringPrototypeIndexOf(key, '*');
|
|
if (patternIndex !== -1 &&
|
|
StringPrototypeStartsWith(packageSubpath,
|
|
StringPrototypeSlice(key, 0, patternIndex))) {
|
|
// When this reaches EOL, this can throw at the top of the whole function:
|
|
//
|
|
// if (StringPrototypeEndsWith(packageSubpath, '/'))
|
|
// throwInvalidSubpath(packageSubpath)
|
|
//
|
|
// To match "imports" and the spec.
|
|
if (StringPrototypeEndsWith(packageSubpath, '/'))
|
|
emitTrailingSlashPatternDeprecation(packageSubpath, packageJSONUrl,
|
|
base);
|
|
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
|
|
if (packageSubpath.length >= key.length &&
|
|
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
|
|
patternKeyCompare(bestMatch, key) === 1 &&
|
|
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
|
|
bestMatch = key;
|
|
bestMatchSubpath = StringPrototypeSlice(
|
|
packageSubpath, patternIndex,
|
|
packageSubpath.length - patternTrailer.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMatch) {
|
|
const target = exports[bestMatch];
|
|
const resolveResult = resolvePackageTarget(
|
|
packageJSONUrl,
|
|
target,
|
|
bestMatchSubpath,
|
|
bestMatch,
|
|
base,
|
|
true,
|
|
false,
|
|
StringPrototypeEndsWith(packageSubpath, '/'),
|
|
conditions);
|
|
|
|
if (resolveResult == null) {
|
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
|
}
|
|
return resolveResult;
|
|
}
|
|
|
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
|
}
|
|
|
|
function patternKeyCompare(a, b) {
|
|
const aPatternIndex = StringPrototypeIndexOf(a, '*');
|
|
const bPatternIndex = StringPrototypeIndexOf(b, '*');
|
|
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
|
|
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
|
|
if (baseLenA > baseLenB) return -1;
|
|
if (baseLenB > baseLenA) return 1;
|
|
if (aPatternIndex === -1) return 1;
|
|
if (bPatternIndex === -1) return -1;
|
|
if (a.length > b.length) return -1;
|
|
if (b.length > a.length) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {string | URL | undefined} base
|
|
* @param {Set<string>} conditions
|
|
* @returns {URL}
|
|
*/
|
|
function packageImportsResolve(name, base, conditions) {
|
|
if (name === '#' || StringPrototypeStartsWith(name, '#/') ||
|
|
StringPrototypeEndsWith(name, '/')) {
|
|
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);
|
|
if (packageConfig.exists) {
|
|
packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
|
|
const imports = packageConfig.imports;
|
|
if (imports) {
|
|
if (ObjectPrototypeHasOwnProperty(imports, name) &&
|
|
!StringPrototypeIncludes(name, '*')) {
|
|
const resolveResult = resolvePackageTarget(
|
|
packageJSONUrl, imports[name], '', name, base, false, true, false,
|
|
conditions
|
|
);
|
|
if (resolveResult != null) {
|
|
return resolveResult;
|
|
}
|
|
} else {
|
|
let bestMatch = '';
|
|
let bestMatchSubpath;
|
|
const keys = ObjectGetOwnPropertyNames(imports);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
const patternIndex = StringPrototypeIndexOf(key, '*');
|
|
if (patternIndex !== -1 &&
|
|
StringPrototypeStartsWith(name,
|
|
StringPrototypeSlice(key, 0,
|
|
patternIndex))) {
|
|
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
|
|
if (name.length >= key.length &&
|
|
StringPrototypeEndsWith(name, patternTrailer) &&
|
|
patternKeyCompare(bestMatch, key) === 1 &&
|
|
StringPrototypeLastIndexOf(key, '*') === patternIndex) {
|
|
bestMatch = key;
|
|
bestMatchSubpath = StringPrototypeSlice(
|
|
name, patternIndex, name.length - patternTrailer.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMatch) {
|
|
const target = imports[bestMatch];
|
|
const resolveResult = resolvePackageTarget(packageJSONUrl, target,
|
|
bestMatchSubpath,
|
|
bestMatch, base, true,
|
|
true, false, conditions);
|
|
if (resolveResult != null) {
|
|
return resolveResult;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
throwImportNotDefined(name, packageJSONUrl, base);
|
|
}
|
|
|
|
/**
|
|
* @param {URL} url
|
|
* @returns {import('internal/modules/esm/package_config.js').PackageType}
|
|
*/
|
|
function getPackageType(url) {
|
|
const packageConfig = getPackageScopeConfig(url);
|
|
return packageConfig.type;
|
|
}
|
|
|
|
/**
|
|
* @param {string} specifier
|
|
* @param {string | URL | undefined} base
|
|
* @returns {{ packageName: string, packageSubpath: string, isScoped: boolean }}
|
|
*/
|
|
function parsePackageName(specifier, base) {
|
|
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
|
|
let validPackageName = true;
|
|
let isScoped = false;
|
|
if (specifier[0] === '@') {
|
|
isScoped = true;
|
|
if (separatorIndex === -1 || specifier.length === 0) {
|
|
validPackageName = false;
|
|
} else {
|
|
separatorIndex = StringPrototypeIndexOf(
|
|
specifier, '/', separatorIndex + 1);
|
|
}
|
|
}
|
|
|
|
const packageName = separatorIndex === -1 ?
|
|
specifier : StringPrototypeSlice(specifier, 0, separatorIndex);
|
|
|
|
// Package name cannot have leading . and cannot have percent-encoding or
|
|
// \\ separators.
|
|
if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null)
|
|
validPackageName = false;
|
|
|
|
if (!validPackageName) {
|
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
|
specifier, 'is not a valid package name', fileURLToPath(base));
|
|
}
|
|
|
|
const packageSubpath = '.' + (separatorIndex === -1 ? '' :
|
|
StringPrototypeSlice(specifier, separatorIndex));
|
|
|
|
return { packageName, packageSubpath, isScoped };
|
|
}
|
|
|
|
/**
|
|
* @param {string} specifier
|
|
* @param {string | URL | undefined} base
|
|
* @param {Set<string>} conditions
|
|
* @returns {resolved: URL, format? : string}
|
|
*/
|
|
function packageResolve(specifier, base, conditions) {
|
|
if (BuiltinModule.canBeRequiredByUsers(specifier) &&
|
|
BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
|
|
return new URL('node:' + specifier);
|
|
}
|
|
|
|
const { packageName, packageSubpath, isScoped } =
|
|
parsePackageName(specifier, base);
|
|
|
|
// ResolveSelf
|
|
const packageConfig = getPackageScopeConfig(base);
|
|
if (packageConfig.exists) {
|
|
const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
|
|
if (packageConfig.name === packageName &&
|
|
packageConfig.exports !== undefined && packageConfig.exports !== null) {
|
|
return packageExportsResolve(
|
|
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
|
|
}
|
|
}
|
|
|
|
let packageJSONUrl =
|
|
new URL('./node_modules/' + packageName + '/package.json', base);
|
|
let packageJSONPath = fileURLToPath(packageJSONUrl);
|
|
let lastPath;
|
|
do {
|
|
const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0,
|
|
packageJSONPath.length - 13));
|
|
if (!stat.isDirectory()) {
|
|
lastPath = packageJSONPath;
|
|
packageJSONUrl = new URL((isScoped ?
|
|
'../../../../node_modules/' : '../../../node_modules/') +
|
|
packageName + '/package.json', packageJSONUrl);
|
|
packageJSONPath = fileURLToPath(packageJSONUrl);
|
|
continue;
|
|
}
|
|
|
|
// Package match.
|
|
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
|
|
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
|
|
return packageExportsResolve(
|
|
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
|
|
}
|
|
if (packageSubpath === '.') {
|
|
return legacyMainResolve(
|
|
packageJSONUrl,
|
|
packageConfig,
|
|
base
|
|
);
|
|
}
|
|
|
|
return new URL(packageSubpath, packageJSONUrl);
|
|
// Cross-platform root check.
|
|
} while (packageJSONPath.length !== lastPath.length);
|
|
|
|
// eslint can't handle the above code.
|
|
// eslint-disable-next-line no-unreachable
|
|
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
|
|
}
|
|
|
|
/**
|
|
* @param {string} specifier
|
|
* @returns {boolean}
|
|
*/
|
|
function isBareSpecifier(specifier) {
|
|
return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.';
|
|
}
|
|
|
|
function isRelativeSpecifier(specifier) {
|
|
if (specifier[0] === '.') {
|
|
if (specifier.length === 1 || specifier[1] === '/') return true;
|
|
if (specifier[1] === '.') {
|
|
if (specifier.length === 2 || specifier[2] === '/') return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
|
|
if (specifier === '') return false;
|
|
if (specifier[0] === '/') return true;
|
|
return isRelativeSpecifier(specifier);
|
|
}
|
|
|
|
/**
|
|
* @param {string} specifier
|
|
* @param {string | URL | undefined} base
|
|
* @param {Set<string>} conditions
|
|
* @param {boolean} preserveSymlinks
|
|
* @returns {url: URL, format?: string}
|
|
*/
|
|
function moduleResolve(specifier, base, conditions, preserveSymlinks) {
|
|
const isRemote = base.protocol === 'http:' ||
|
|
base.protocol === 'https:';
|
|
// Order swapped from spec for minor perf gain.
|
|
// Ok since relative URLs cannot parse as URLs.
|
|
let resolved;
|
|
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
|
resolved = new URL(specifier, base);
|
|
} else if (!isRemote && specifier[0] === '#') {
|
|
resolved = packageImportsResolve(specifier, base, conditions);
|
|
} else {
|
|
try {
|
|
resolved = new URL(specifier);
|
|
} catch {
|
|
if (!isRemote) {
|
|
resolved = packageResolve(specifier, base, conditions);
|
|
}
|
|
}
|
|
}
|
|
if (resolved.protocol !== 'file:') {
|
|
return resolved;
|
|
}
|
|
return finalizeResolution(resolved, base, preserveSymlinks);
|
|
}
|
|
|
|
/**
|
|
* Try to resolve an import as a CommonJS module
|
|
* @param {string} specifier
|
|
* @param {string} parentURL
|
|
* @returns {boolean|string}
|
|
*/
|
|
function resolveAsCommonJS(specifier, parentURL) {
|
|
try {
|
|
const parent = fileURLToPath(parentURL);
|
|
const tmpModule = new CJSModule(parent, null);
|
|
tmpModule.paths = CJSModule._nodeModulePaths(parent);
|
|
|
|
let found = CJSModule._resolveFilename(specifier, tmpModule, false);
|
|
|
|
// If it is a relative specifier return the relative path
|
|
// to the parent
|
|
if (isRelativeSpecifier(specifier)) {
|
|
found = relative(parent, found);
|
|
// Add '.separator if the path does not start with '..separator'
|
|
// This should be a safe assumption because when loading
|
|
// esm modules there should be always a file specified so
|
|
// there should not be a specifier like '..' or '.'
|
|
if (!StringPrototypeStartsWith(found, `..${sep}`)) {
|
|
found = `.${sep}${found}`;
|
|
}
|
|
} else if (isBareSpecifier(specifier)) {
|
|
// If it is a bare specifier return the relative path within the
|
|
// module
|
|
const pkg = StringPrototypeSplit(specifier, '/')[0];
|
|
const index = StringPrototypeIndexOf(found, pkg);
|
|
if (index !== -1) {
|
|
found = StringPrototypeSlice(found, index);
|
|
}
|
|
}
|
|
// Normalize the path separator to give a valid suggestion
|
|
// on Windows
|
|
if (process.platform === 'win32') {
|
|
found = RegExpPrototypeSymbolReplace(new RegExp(`\\${sep}`, 'g'),
|
|
found, '/');
|
|
}
|
|
return found;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed`
|
|
function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
|
|
if (parsedParentURL) {
|
|
if (
|
|
parsedParentURL.protocol === 'http:' ||
|
|
parsedParentURL.protocol === 'https:'
|
|
) {
|
|
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
|
// data: and blob: disallowed due to allowing file: access via
|
|
// indirection
|
|
if (parsed &&
|
|
parsed.protocol !== 'https:' &&
|
|
parsed.protocol !== 'http:'
|
|
) {
|
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|
specifier,
|
|
parsedParentURL,
|
|
'remote imports cannot import from a local location.'
|
|
);
|
|
}
|
|
|
|
return { url: parsed.href };
|
|
}
|
|
if (BuiltinModule.canBeRequiredByUsers(specifier) &&
|
|
BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
|
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|
specifier,
|
|
parsedParentURL,
|
|
'remote imports cannot import from a local location.'
|
|
);
|
|
}
|
|
|
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|
specifier,
|
|
parsedParentURL,
|
|
'only relative and absolute specifiers are supported.'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function throwIfUnsupportedURLProtocol(url) {
|
|
if (url.protocol !== 'file:' && url.protocol !== 'data:' &&
|
|
url.protocol !== 'node:') {
|
|
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
|
|
}
|
|
}
|
|
|
|
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
|
|
if (
|
|
parsed &&
|
|
parsed.protocol !== 'file:' &&
|
|
parsed.protocol !== 'data:' &&
|
|
(
|
|
!experimentalNetworkImports ||
|
|
(
|
|
parsed.protocol !== 'https:' &&
|
|
parsed.protocol !== 'http:'
|
|
)
|
|
)
|
|
) {
|
|
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, ArrayPrototypeConcat(
|
|
'file',
|
|
'data',
|
|
experimentalNetworkImports ? ['https', 'http'] : [],
|
|
));
|
|
}
|
|
}
|
|
|
|
async function defaultResolve(specifier, context = {}) {
|
|
let { parentURL, conditions } = context;
|
|
if (parentURL && policy?.manifest) {
|
|
const redirects = policy.manifest.getDependencyMapper(parentURL);
|
|
if (redirects) {
|
|
const { resolve, reaction } = redirects;
|
|
const destination = resolve(specifier, new SafeSet(conditions));
|
|
let missing = true;
|
|
if (destination === true) {
|
|
missing = false;
|
|
} else if (destination) {
|
|
const href = destination.href;
|
|
return { url: href };
|
|
}
|
|
if (missing) {
|
|
// Prevent network requests from firing if resolution would be banned.
|
|
// Network requests can extract data by doing things like putting
|
|
// secrets in query params
|
|
reaction(new ERR_MANIFEST_DEPENDENCY_MISSING(
|
|
parentURL,
|
|
specifier,
|
|
ArrayPrototypeJoin([...conditions], ', '))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let parsedParentURL;
|
|
if (parentURL) {
|
|
try {
|
|
parsedParentURL = new URL(parentURL);
|
|
} catch {
|
|
// Ignore exception
|
|
}
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
|
parsed = new URL(specifier, parsedParentURL);
|
|
} else {
|
|
parsed = new URL(specifier);
|
|
}
|
|
|
|
if (parsed.protocol === 'data:' ||
|
|
(experimentalNetworkImports &&
|
|
(
|
|
parsed.protocol === 'https:' ||
|
|
parsed.protocol === 'http:'
|
|
)
|
|
)
|
|
) {
|
|
return { __proto__: null, url: parsed.href };
|
|
}
|
|
} catch {
|
|
// Ignore exception
|
|
}
|
|
|
|
// There are multiple deep branches that can either throw or return; instead
|
|
// of duplicating that deeply nested logic for the possible returns, DRY and
|
|
// check for a return. This seems the least gnarly.
|
|
const maybeReturn = checkIfDisallowedImport(
|
|
specifier,
|
|
parsed,
|
|
parsedParentURL,
|
|
);
|
|
|
|
if (maybeReturn) return maybeReturn;
|
|
|
|
// This must come after checkIfDisallowedImport
|
|
if (parsed && parsed.protocol === 'node:') return { url: specifier };
|
|
|
|
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
|
|
|
|
const isMain = parentURL === undefined;
|
|
if (isMain) {
|
|
parentURL = pathToFileURL(`${process.cwd()}/`).href;
|
|
|
|
// This is the initial entry point to the program, and --input-type has
|
|
// been passed as an option; but --input-type can only be used with
|
|
// --eval, --print or STDIN string input. It is not allowed with file
|
|
// input, to avoid user confusion over how expansive the effect of the
|
|
// flag should be (i.e. entry point only, package scope surrounding the
|
|
// entry point, etc.).
|
|
if (typeFlag) throw new ERR_INPUT_TYPE_NOT_ALLOWED();
|
|
}
|
|
|
|
conditions = getConditionsSet(conditions);
|
|
let url;
|
|
try {
|
|
url = moduleResolve(
|
|
specifier,
|
|
parentURL,
|
|
conditions,
|
|
isMain ? preserveSymlinksMain : preserveSymlinks
|
|
);
|
|
} catch (error) {
|
|
// Try to give the user a hint of what would have been the
|
|
// resolved CommonJS module
|
|
if (error.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
|
|
if (StringPrototypeStartsWith(specifier, 'file://')) {
|
|
specifier = fileURLToPath(specifier);
|
|
}
|
|
const found = resolveAsCommonJS(specifier, parentURL);
|
|
if (found) {
|
|
// Modify the stack and message string to include the hint
|
|
const lines = StringPrototypeSplit(error.stack, '\n');
|
|
const hint = `Did you mean to import ${found}?`;
|
|
error.stack =
|
|
ArrayPrototypeShift(lines) + '\n' +
|
|
hint + '\n' +
|
|
ArrayPrototypeJoin(lines, '\n');
|
|
error.message += `\n${hint}`;
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
throwIfUnsupportedURLProtocol(url);
|
|
|
|
return {
|
|
// Do NOT cast `url` to a string: that will work even when there are real
|
|
// problems, silencing them
|
|
url: url.href,
|
|
format: defaultGetFormatWithoutErrors(url, context),
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_CONDITIONS,
|
|
defaultResolve,
|
|
encodedSepRegEx,
|
|
getPackageScopeConfig,
|
|
getPackageType,
|
|
packageExportsResolve,
|
|
packageImportsResolve,
|
|
};
|
|
|
|
// cycle
|
|
const {
|
|
defaultGetFormatWithoutErrors,
|
|
} = require('internal/modules/esm/get_format');
|
|
|
|
if (policy) {
|
|
const $defaultResolve = defaultResolve;
|
|
module.exports.defaultResolve = async function defaultResolve(
|
|
specifier,
|
|
context
|
|
) {
|
|
const ret = await $defaultResolve(specifier, context);
|
|
// This is a preflight check to avoid data exfiltration by query params etc.
|
|
policy.manifest.mightAllow(ret.url, () =>
|
|
new ERR_MANIFEST_DEPENDENCY_MISSING(
|
|
context.parentURL,
|
|
specifier,
|
|
context.conditions
|
|
)
|
|
);
|
|
return ret;
|
|
};
|
|
}
|