diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 32d750d6178a89..dbc10e6e0739c1 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1070,10 +1070,17 @@ 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) => { - assert(subpath !== '.'); - return `Package subpath '${subpath}' is not a valid module request for the ` + - `"exports" resolution of ${pkgPath}${sep}package.json`; +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`; + } else { + return `Package subpath '${subpath}' is not a valid module request for ` + + `the "exports" resolution of ${pkgPath} imported from ${base}`; + } }, TypeError); E('ERR_INVALID_OPT_VALUE', (name, value) => `The value "${String(value)}" is invalid for option "${name}"`, @@ -1081,18 +1088,32 @@ E('ERR_INVALID_OPT_VALUE', (name, value) => RangeError); E('ERR_INVALID_OPT_VALUE_ENCODING', 'The value "%s" is invalid for option "encoding"', TypeError); -E('ERR_INVALID_PACKAGE_CONFIG', - `Invalid package config %s${sep}package.json, %s`, Error); -E('ERR_INVALID_PACKAGE_TARGET', (pkgPath, key, subpath, target) => { - if (key === '.') { - return `Invalid "exports" main target ${JSONStringify(target)} defined ` + +E('ERR_INVALID_PACKAGE_CONFIG', (path, message, hasMessage = true) => { + if (hasMessage) + return `Invalid package config ${path}${sep}package.json, ${message}`; + else + return `Invalid JSON in ${path} imported from ${message}`; +}, Error); +E('ERR_INVALID_PACKAGE_TARGET', + (pkgPath, key, subpath, target, base = undefined) => { + if (key === null) { + if (subpath !== '') { + return `Invalid "exports" target ${JSONStringify(target)} defined ` + + `for '${subpath}' in the package config ${pkgPath} imported from ` + + base; + } else { + return `Invalid "exports" main target ${target} defined in the ` + + `package config ${pkgPath} imported from ${base}.`; + } + } else if (key === '.') { + return `Invalid "exports" main target ${JSONStringify(target)} defined ` + `in the package config ${pkgPath}${sep}package.json`; - } else { - return `Invalid "exports" target ${JSONStringify(target)} defined for '${ - StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + + } else { + return `Invalid "exports" target ${JSONStringify(target)} defined for '${ + StringPrototypeSlice(key, 0, -subpath.length || key.length)}' in the ` + `package config ${pkgPath}${sep}package.json`; - } -}, Error); + } + }, Error); E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set', Error); E('ERR_INVALID_PROTOCOL', @@ -1203,6 +1224,9 @@ E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', 'The ES Module loader may not return a format of \'dynamic\' when no ' + 'dynamicInstantiate function was provided', Error); E('ERR_MISSING_OPTION', '%s is required', TypeError); +E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => { + return `Cannot find ${type} '${path}' imported from ${base}`; +}, Error); E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); E('ERR_NAPI_INVALID_DATAVIEW_ARGS', @@ -1237,12 +1261,15 @@ E('ERR_OUT_OF_RANGE', msg += ` It must be ${range}. Received ${received}`; return msg; }, RangeError); -E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath) => { +E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => { if (subpath === '.') { return `No "exports" main resolved in ${pkgPath}${sep}package.json`; - } else { + } else if (base === undefined) { return `Package subpath '${subpath}' is not defined by "exports" in ${ pkgPath}${sep}package.json`; + } else { + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath} imported from ${base}`; } }, Error); E('ERR_REQUIRE_ESM', diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 9815077c3a6dcb..616b2cf52309ea 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -1,5 +1,5 @@ 'use strict'; - +const { StringPrototypeStartsWith } = primordials; const { extname } = require('path'); const { getOptionValue } = require('internal/options'); @@ -7,14 +7,10 @@ const experimentalJsonModules = getOptionValue('--experimental-json-modules'); const experimentalSpeciferResolution = getOptionValue('--experimental-specifier-resolution'); const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); -const { getPackageType } = internalBinding('module_wrap'); +const { getPackageType } = require('internal/modules/esm/resolve'); const { URL, fileURLToPath } = require('internal/url'); const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; -// const TYPE_NONE = 0; -// const TYPE_COMMONJS = 1; -const TYPE_MODULE = 2; - const extensionFormatMap = { '__proto__': null, '.cjs': 'commonjs', @@ -37,8 +33,8 @@ if (experimentalWasmModules) if (experimentalJsonModules) extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; -function defaultGetFormat(url, context, defaultGetFormat) { - if (url.startsWith('nodejs:')) { +function defaultGetFormat(url, context, defaultGetFormatUnused) { + if (StringPrototypeStartsWith(url, 'nodejs:')) { return { format: 'builtin' }; } const parsed = new URL(url); @@ -55,8 +51,7 @@ function defaultGetFormat(url, context, defaultGetFormat) { const ext = extname(parsed.pathname); let format; if (ext === '.js') { - format = getPackageType(parsed.href) === TYPE_MODULE ? - 'module' : 'commonjs'; + format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs'; } else { format = extensionFormatMap[ext]; } diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index ec2e681e621d0d..730c815b8435f0 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1,26 +1,609 @@ 'use strict'; const { + ArrayIsArray, + JSONParse, + JSONStringify, + ObjectGetOwnPropertyNames, + ObjectPrototypeHasOwnProperty, SafeMap, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeIndexOf, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeSubstr, } = primordials; const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); -const { realpathSync } = require('fs'); +const { + closeSync, + fstatSync, + openSync, + readFileSync, + realpathSync, + statSync, + Stats, +} = require('fs'); const { getOptionValue } = require('internal/options'); const { sep } = require('path'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const typeFlag = getOptionValue('--input-type'); -const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); -const { ERR_INPUT_TYPE_NOT_ALLOWED, - ERR_UNSUPPORTED_ESM_URL_SCHEME } = require('internal/errors').codes; +const { + ERR_INPUT_TYPE_NOT_ALLOWED, + ERR_INVALID_MODULE_SPECIFIER, + ERR_INVALID_PACKAGE_CONFIG, + ERR_INVALID_PACKAGE_TARGET, + ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_PATH_NOT_EXPORTED, + ERR_UNSUPPORTED_ESM_URL_SCHEME, +} = require('internal/errors').codes; const realpathCache = new SafeMap(); +const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ + +function tryStatSync(path) { + try { + return statSync(path); + } catch { + return new Stats(); + } +} + +function readIfFile(path) { + let fd; + try { + fd = openSync(path, 'r'); + } catch { + return undefined; + } + try { + if (!fstatSync(fd).isFile()) return undefined; + return readFileSync(fd, 'utf8'); + } finally { + closeSync(fd); + } +} + +function getPackageConfig(path, base) { + const existing = packageJSONCache.get(path); + if (existing !== undefined) { + if (!existing.isValid) { + throw new ERR_INVALID_PACKAGE_CONFIG(path, fileURLToPath(base), false); + } + return existing; + } + + const source = readIfFile(path); + if (source === undefined) { + const packageConfig = { + exists: false, + main: undefined, + name: undefined, + isValid: true, + type: 'none', + exports: undefined + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let packageJSON; + try { + packageJSON = JSONParse(source); + } catch { + const packageConfig = { + exists: true, + main: undefined, + name: undefined, + isValid: false, + type: 'none', + exports: undefined + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let { main, name, type } = packageJSON; + const { exports } = packageJSON; + 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 = { + exists: true, + main, + name, + isValid: true, + type, + exports + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; +} + +function getPackageScopeConfig(resolved, base) { + let packageJSONUrl = new URL('./package.json', resolved); + while (true) { + const packageJSONPath = packageJSONUrl.pathname; + if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) + break; + const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), base); + if (packageConfig.exists) return packageConfig; + + const lastPackageJSONUrl = packageJSONUrl; + packageJSONUrl = new URL('../package.json', packageJSONUrl); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; + } + const packageConfig = { + exists: false, + main: undefined, + name: undefined, + isValid: true, + type: 'none', + exports: undefined + }; + packageJSONCache.set(fileURLToPath(packageJSONUrl), packageConfig); + return packageConfig; +} + +/* + * 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 + */ +function fileExists(url) { + return tryStatSync(fileURLToPath(url)).isFile(); +} + +function legacyMainResolve(packageJSONUrl, packageConfig) { + 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; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.node`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, + packageJSONUrl))) { + return guess; + } + // Fallthrough. + } + if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { + return guess; + } + // So fs. + if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { + return guess; + } + // Not found. + return undefined; +} + +function resolveExtensionsWithTryExactName(search) { + if (fileExists(search)) return search; + return resolveExtensions(search); +} + +const extensions = ['.js', '.json', '.node', '.mjs']; +function resolveExtensions(search) { + for (let i = 0; i < extensions.length; i++) { + const extension = extensions[i]; + const guess = new URL(`${search.pathname}${extension}`, search); + if (fileExists(guess)) return guess; + } + return undefined; +} + +function resolveIndex(search) { + return resolveExtensions(new URL('index', search)); +} + +function finalizeResolution(resolved, base) { + if (getOptionValue('--experimental-specifier-resolution') === 'node') { + let file = resolveExtensionsWithTryExactName(resolved); + if (file !== undefined) return file; + if (!StringPrototypeEndsWith(resolved.pathname, '/')) { + file = resolveIndex(new URL(`${resolved.pathname}/`, base)); + } else { + file = resolveIndex(resolved); + } + if (file !== undefined) return file; + throw new ERR_MODULE_NOT_FOUND( + resolved.pathname, fileURLToPath(base), 'module'); + } + + if (StringPrototypeEndsWith(resolved.pathname, '/')) return resolved; + const path = fileURLToPath(resolved); + + if (!tryStatSync(path).isFile()) { + throw new ERR_MODULE_NOT_FOUND( + path || resolved.pathname, fileURLToPath(base), 'module'); + } + + return resolved; +} + +function throwExportsNotFound(subpath, packageJSONUrl, base) { + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); +} + +function throwSubpathInvalid(subpath, packageJSONUrl, base) { + throw new ERR_INVALID_MODULE_SPECIFIER( + fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); +} + +function throwExportsInvalid( + subpath, target, packageJSONUrl, 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)); +} + +function resolveExportsTargetString( + target, subpath, match, packageJSONUrl, base) { + if (target[0] !== '.' || target[1] !== '/' || + (subpath !== '' && target[target.length - 1] !== '/')) { + throwExportsInvalid(match, target, packageJSONUrl, base); + } + + const resolved = new URL(target, packageJSONUrl); + const resolvedPath = resolved.pathname; + const packagePath = new URL('.', packageJSONUrl).pathname; + + if (!StringPrototypeStartsWith(resolvedPath, packagePath) || + StringPrototypeIncludes( + resolvedPath, '/node_modules/', packagePath.length - 1)) { + throwExportsInvalid(match, target, packageJSONUrl, base); + } -function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { + 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); + } + return subpathResolved; +} + +function isArrayIndex(key /* string */) { /* -> boolean */ + const keyNum = +key; + if (`${keyNum}` !== key) return false; + return keyNum >= 0 && keyNum < 0xFFFF_FFFF; +} + +function resolveExportsTarget( + packageJSONUrl, target, subpath, packageSubpath, base) { + if (typeof target === 'string') { + const resolved = resolveExportsTargetString( + target, subpath, packageSubpath, packageJSONUrl, base); + return finalizeResolution(resolved, base); + } else if (ArrayIsArray(target)) { + if (target.length === 0) + throwExportsInvalid(packageSubpath, target, packageJSONUrl, base); + + let lastException; + for (let i = 0; i < target.length; i++) { + const targetItem = target[i]; + let resolved; + try { + resolved = resolveExportsTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base); + } catch (e) { + lastException = e; + if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || + e.code === 'ERR_INVALID_PACKAGE_TARGET') { + continue; + } + throw e; + } + + return finalizeResolution(resolved, base); + } + 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), + '"exports" cannot contain numeric property keys'); + } + } + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key === 'node' || key === 'import' || key === 'default') { + const conditionalTarget = target[key]; + try { + return resolveExportsTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base); + } catch (e) { + if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') continue; + throw e; + } + } + } + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + } + throwExportsInvalid(packageSubpath, target, packageJSONUrl, base); +} + +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), + '"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; +} + + +function packageMainResolve(packageJSONUrl, packageConfig, base) { + if (packageConfig.exists) { + const exports = packageConfig.exports; + if (exports !== undefined) { + if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { + return resolveExportsTarget(packageJSONUrl, exports, '', '', base); + } else if (typeof exports === 'object' && exports !== null) { + const target = exports['.']; + if (target !== undefined) + return resolveExportsTarget(packageJSONUrl, target, '', '', base); + } + + throw new ERR_PACKAGE_PATH_NOT_EXPORTED(packageJSONUrl, '.'); + } + if (packageConfig.main !== undefined) { + const resolved = new URL(packageConfig.main, packageJSONUrl); + const path = fileURLToPath(resolved); + if (tryStatSync(path).isFile()) return resolved; + } + if (getOptionValue('--experimental-specifier-resolution') === 'node') { + if (packageConfig.main !== undefined) { + return finalizeResolution( + new URL(packageConfig.main, packageJSONUrl), base); + } else { + return finalizeResolution( + new URL('index', packageJSONUrl), base); + } + } + if (packageConfig.type !== 'module') { + return legacyMainResolve(packageJSONUrl, packageConfig); + } + } + + throw new ERR_MODULE_NOT_FOUND( + fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); +} + + +function packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base) /* -> URL */ { + const exports = packageConfig.exports; + if (exports === undefined || + isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + } + + + if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { + const target = exports[packageSubpath]; + const resolved = resolveExportsTarget( + packageJSONUrl, target, '', packageSubpath, base); + return finalizeResolution(resolved, base); + } + + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] !== '/') continue; + if (StringPrototypeStartsWith(packageSubpath, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = exports[bestMatch]; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); + const resolved = resolveExportsTarget( + packageJSONUrl, target, subpath, packageSubpath, base); + return finalizeResolution(resolved, base); + } + + throwExportsNotFound(packageSubpath, packageJSONUrl, base); +} + +function getPackageType(url) { + const packageConfig = getPackageScopeConfig(url, url); + return packageConfig.type; +} + +function packageResolve(specifier /* string */, base /* URL */) { /* -> URL */ + 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. + for (let i = 0; i < packageName.length; i++) { + if (packageName[i] === '%' || packageName[i] === '\\') { + validPackageName = false; + break; + } + } + + if (!validPackageName) { + throw new ERR_INVALID_MODULE_SPECIFIER( + specifier, undefined, fileURLToPath(base)); + } + + const packageSubpath = separatorIndex === -1 ? + '' : '.' + StringPrototypeSlice(specifier, separatorIndex); + + // 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 && + packageConfig.exports !== undefined) { + if (packageSubpath === './') { + return new URL('./', packageJSONUrl); + } else if (packageSubpath === '') { + return packageMainResolve(packageJSONUrl, packageConfig, base); + } else { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base); + } + } + } + + 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, base); + if (packageSubpath === './') { + return new URL('./', packageJSONUrl); + } else if (packageSubpath === '') { + return packageMainResolve(packageJSONUrl, packageConfig, base); + } else if (packageConfig.exports !== undefined) { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base); + } else { + return finalizeResolution( + new URL(packageSubpath, packageJSONUrl), base); + } + // 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)); +} + +function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { + if (specifier === '') return false; + if (specifier[0] === '/') return true; + 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 moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */ + // 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 { + try { + resolved = new URL(specifier); + } catch { + return packageResolve(specifier, base); + } + } + return finalizeResolution(resolved, base); +} + +function defaultResolve(specifier, { parentURL } = {}, defaultResolveUnused) { let parsed; try { parsed = new URL(specifier); @@ -39,7 +622,7 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { url: 'nodejs:' + specifier }; } - if (parentURL && parentURL.startsWith('data:')) { + if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) { // This is gonna blow up, we want the error new URL(specifier, parentURL); } @@ -58,7 +641,7 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } - let url = moduleWrapResolve(specifier, parentURL); + let url = moduleResolve(specifier, new URL(parentURL)); if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { const urlPath = fileURLToPath(url); @@ -73,4 +656,8 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { return { url: `${url}` }; } -exports.defaultResolve = defaultResolve; + +module.exports = { + defaultResolve, + getPackageType +}; diff --git a/src/env.h b/src/env.h index db1237a92f5a6a..9886fd32633c98 100644 --- a/src/env.h +++ b/src/env.h @@ -220,7 +220,6 @@ constexpr size_t kFsStatsBufferLength = V(dns_srv_string, "SRV") \ V(dns_txt_string, "TXT") \ V(done_string, "done") \ - V(dot_string, ".") \ V(duration_string, "duration") \ V(ecdh_string, "ECDH") \ V(emit_warning_string, "emitWarning") \ @@ -275,7 +274,6 @@ constexpr size_t kFsStatsBufferLength = V(kind_string, "kind") \ V(library_string, "library") \ V(mac_string, "mac") \ - V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ V(message_port_constructor_string, "MessagePort") \ V(message_port_string, "messagePort") \ @@ -994,9 +992,6 @@ class Environment : public MemoryRetainer { inline uint32_t get_next_script_id(); inline uint32_t get_next_function_id(); - std::unordered_map - package_json_cache; - inline double* heap_statistics_buffer() const; inline void set_heap_statistics_buffer(double* pointer); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 7750108aaf34ad..f578f752b02d8b 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -30,7 +30,6 @@ using v8::EscapableHandleScope; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; -using v8::Global; using v8::HandleScope; using v8::Integer; using v8::IntegrityLevel; @@ -40,7 +39,6 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Module; -using v8::Nothing; using v8::Number; using v8::Object; using v8::PrimitiveArray; @@ -53,21 +51,14 @@ using v8::UnboundModuleScript; using v8::Undefined; using v8::Value; -static const char* const EXTENSIONS[] = { - ".js", - ".json", - ".node", - ".mjs" -}; - ModuleWrap::ModuleWrap(Environment* env, Local object, Local module, - Local url) : - BaseObject(env, object), - id_(env->get_next_module_id()) { - module_.Reset(env->isolate(), module); - url_.Reset(env->isolate(), url); + Local url) + : BaseObject(env, object), + module_(env->isolate(), module), + url_(env->isolate(), url), + id_(env->get_next_module_id()) { env->id_to_module_map.emplace(id_, this); } @@ -282,7 +273,7 @@ void ModuleWrap::Link(const FunctionCallbackInfo& args) { }; MaybeLocal maybe_resolve_return_value = - resolver_arg->Call(mod_context, that, 1, argv); + resolver_arg->Call(mod_context, that, arraysize(argv), argv); if (maybe_resolve_return_value.IsEmpty()) { return; } @@ -496,954 +487,6 @@ MaybeLocal ModuleWrap::ResolveCallback(Local context, return module->module_.Get(isolate); } -namespace { - -// Tests whether a path starts with /, ./ or ../ -// In WhatWG terminology, the alternative case is called a "bare" specifier -// (e.g. in `import "jquery"`). -inline bool ShouldBeTreatedAsRelativeOrAbsolutePath( - const std::string& specifier) { - size_t len = specifier.length(); - if (len == 0) - return false; - if (specifier[0] == '/') { - return true; - } else if (specifier[0] == '.') { - if (len == 1 || specifier[1] == '/') - return true; - if (specifier[1] == '.') { - if (len == 2 || specifier[2] == '/') - return true; - } - } - return false; -} - -std::string ReadFile(uv_file file) { - std::string contents; - uv_fs_t req; - char buffer_memory[4096]; - uv_buf_t buf = uv_buf_init(buffer_memory, sizeof(buffer_memory)); - - do { - const int r = uv_fs_read(nullptr, - &req, - file, - &buf, - 1, - contents.length(), // offset - nullptr); - uv_fs_req_cleanup(&req); - - if (r <= 0) - break; - contents.append(buf.base, r); - } while (true); - return contents; -} - -enum DescriptorType { - FILE, - DIRECTORY, - NONE -}; - -// When DescriptorType cache is added, this can also return -// Nothing for the "null" cache entries. -inline Maybe OpenDescriptor(const std::string& path) { - uv_fs_t fs_req; -#ifdef _WIN32 - std::string pth = "\\\\.\\" + path; - uv_file fd = uv_fs_open(nullptr, &fs_req, pth.c_str(), O_RDONLY, 0, nullptr); -#else - uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr); -#endif - uv_fs_req_cleanup(&fs_req); - if (fd < 0) return Nothing(); - return Just(fd); -} - -inline void CloseDescriptor(uv_file fd) { - uv_fs_t fs_req; - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); - uv_fs_req_cleanup(&fs_req); -} - -inline DescriptorType CheckDescriptorAtFile(uv_file fd) { - uv_fs_t fs_req; - int rc = uv_fs_fstat(nullptr, &fs_req, fd, nullptr); - if (rc == 0) { - uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; - uv_fs_req_cleanup(&fs_req); - return is_directory ? DIRECTORY : FILE; - } - uv_fs_req_cleanup(&fs_req); - return NONE; -} - -// TODO(@guybedford): Add a DescriptorType cache layer here. -// Should be directory based -> if path/to/dir doesn't exist -// then the cache should early-fail any path/to/dir/file check. -DescriptorType CheckDescriptorAtPath(const std::string& path) { - Maybe fd = OpenDescriptor(path); - if (fd.IsNothing()) return NONE; - DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); - CloseDescriptor(fd.FromJust()); - return type; -} - -Maybe ReadIfFile(const std::string& path) { - Maybe fd = OpenDescriptor(path); - if (fd.IsNothing()) return Nothing(); - DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); - if (type != FILE) return Nothing(); - std::string source = ReadFile(fd.FromJust()); - CloseDescriptor(fd.FromJust()); - return Just(source); -} - -using Exists = PackageConfig::Exists; -using IsValid = PackageConfig::IsValid; -using HasMain = PackageConfig::HasMain; -using HasName = PackageConfig::HasName; -using PackageType = PackageConfig::PackageType; - -Maybe GetPackageConfig(Environment* env, - const std::string& path, - const URL& base) { - auto existing = env->package_json_cache.find(path); - if (existing != env->package_json_cache.end()) { - const PackageConfig* pcfg = &existing->second; - if (pcfg->is_valid == IsValid::No) { - std::string msg = "Invalid JSON in " + path + - " imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); - return Nothing(); - } - return Just(pcfg); - } - - Maybe source = ReadIfFile(path); - - if (source.IsNothing()) { - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", - HasName::No, "", - PackageType::None, Global() }); - return Just(&entry.first->second); - } - - std::string pkg_src = source.FromJust(); - - Isolate* isolate = env->isolate(); - HandleScope handle_scope(isolate); - - Local pkg_json; - { - Local src; - Local pkg_json_v; - Local context = env->context(); - - if (!ToV8Value(context, pkg_src).ToLocal(&src) || - !v8::JSON::Parse(context, src.As()).ToLocal(&pkg_json_v) || - !pkg_json_v->ToObject(context).ToLocal(&pkg_json)) { - env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "", - HasName::No, "", - PackageType::None, Global() }); - std::string msg = "Invalid JSON in " + path + - " imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); - return Nothing(); - } - } - - Local pkg_main; - HasMain has_main = HasMain::No; - std::string main_std; - if (pkg_json->Get(env->context(), env->main_string()).ToLocal(&pkg_main)) { - if (pkg_main->IsString()) { - has_main = HasMain::Yes; - } - Utf8Value main_utf8(isolate, pkg_main); - main_std.assign(std::string(*main_utf8, main_utf8.length())); - } - - Local pkg_name; - HasName has_name = HasName::No; - std::string name_std; - if (pkg_json->Get(env->context(), env->name_string()).ToLocal(&pkg_name)) { - if (pkg_name->IsString()) { - has_name = HasName::Yes; - - Utf8Value name_utf8(isolate, pkg_name); - name_std.assign(std::string(*name_utf8, name_utf8.length())); - } - } - - PackageType pkg_type = PackageType::None; - Local type_v; - if (pkg_json->Get(env->context(), env->type_string()).ToLocal(&type_v)) { - if (type_v->StrictEquals(env->module_string())) { - pkg_type = PackageType::Module; - } else if (type_v->StrictEquals(env->commonjs_string())) { - pkg_type = PackageType::CommonJS; - } - // ignore unknown types for forwards compatibility - } - - Local exports_v; - if (pkg_json->Get(env->context(), - env->exports_string()).ToLocal(&exports_v) && - !exports_v->IsNullOrUndefined()) { - Global exports; - exports.Reset(env->isolate(), exports_v); - - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, - has_name, name_std, - pkg_type, std::move(exports) }); - return Just(&entry.first->second); - } - - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, - has_name, name_std, - pkg_type, Global() }); - return Just(&entry.first->second); -} - -Maybe GetPackageScopeConfig(Environment* env, - const URL& resolved, - const URL& base) { - URL pjson_url("./package.json", &resolved); - while (true) { - std::string pjson_url_path = pjson_url.path(); - if (pjson_url_path.length() > 25 && - pjson_url_path.substr(pjson_url_path.length() - 25, 25) == - "node_modules/package.json") { - break; - } - Maybe pkg_cfg = - GetPackageConfig(env, pjson_url.ToFilePath(), base); - if (pkg_cfg.IsNothing()) return pkg_cfg; - if (pkg_cfg.FromJust()->exists == Exists::Yes) return pkg_cfg; - - URL last_pjson_url = pjson_url; - pjson_url = URL("../package.json", pjson_url); - - // Terminates at root where ../package.json equals ../../package.json - // (can't just check "/package.json" for Windows support). - if (pjson_url.path() == last_pjson_url.path()) break; - } - auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(), - PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", - HasName::No, "", - PackageType::None, Global() }); - const PackageConfig* pcfg = &entry.first->second; - return Just(pcfg); -} - -/* - * 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 - */ -inline bool FileExists(const URL& url) { - return CheckDescriptorAtPath(url.ToFilePath()) == FILE; -} -Maybe LegacyMainResolve(const URL& pjson_url, - const PackageConfig& pcfg) { - URL guess; - if (pcfg.has_main == HasMain::Yes) { - // Note: fs check redundances will be handled by Descriptor cache here. - if (FileExists(guess = URL("./" + pcfg.main, pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./" + pcfg.main + ".js", pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./" + pcfg.main + ".json", pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./" + pcfg.main + ".node", pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./" + pcfg.main + "/index.js", pjson_url))) { - return Just(guess); - } - // Such stat. - if (FileExists(guess = URL("./" + pcfg.main + "/index.json", pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./" + pcfg.main + "/index.node", pjson_url))) { - return Just(guess); - } - // Fallthrough. - } - if (FileExists(guess = URL("./index.js", pjson_url))) { - return Just(guess); - } - // So fs. - if (FileExists(guess = URL("./index.json", pjson_url))) { - return Just(guess); - } - if (FileExists(guess = URL("./index.node", pjson_url))) { - return Just(guess); - } - // Not found. - return Nothing(); -} - -enum ResolveExtensionsOptions { - TRY_EXACT_NAME, - ONLY_VIA_EXTENSIONS -}; - -template -Maybe ResolveExtensions(const URL& search) { - if (options == TRY_EXACT_NAME) { - if (FileExists(search)) { - return Just(search); - } - } - - for (const char* extension : EXTENSIONS) { - URL guess(search.path() + extension, &search); - if (FileExists(guess)) { - return Just(guess); - } - } - - return Nothing(); -} - -inline Maybe ResolveIndex(const URL& search) { - return ResolveExtensions(URL("index", search)); -} - -Maybe FinalizeResolution(Environment* env, - const URL& resolved, - const URL& base) { - if (env->options()->experimental_specifier_resolution == "node") { - Maybe file = ResolveExtensions(resolved); - if (!file.IsNothing()) { - return file; - } - if (resolved.path().back() != '/') { - file = ResolveIndex(URL(resolved.path() + "/", &base)); - } else { - file = ResolveIndex(resolved); - } - if (!file.IsNothing()) { - return file; - } - std::string msg = "Cannot find module " + resolved.path() + - " imported from " + base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); - } - - if (resolved.path().back() == '/') { - return Just(resolved); - } - - const std::string& path = resolved.ToFilePath(); - if (CheckDescriptorAtPath(path) != FILE) { - std::string msg = "Cannot find module " + - (path.length() != 0 ? path : resolved.path()) + - " imported from " + base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); - } - - return Just(resolved); -} - -void ThrowExportsNotFound(Environment* env, - const std::string& subpath, - const URL& pjson_url, - const URL& base) { - const std::string msg = "Package subpath '" + subpath + "' is not defined" + - " by \"exports\" in " + pjson_url.ToFilePath() + " imported from " + - base.ToFilePath(); - node::THROW_ERR_PACKAGE_PATH_NOT_EXPORTED(env, msg.c_str()); -} - -void ThrowSubpathInvalid(Environment* env, - const std::string& subpath, - const URL& pjson_url, - const URL& base) { - const std::string msg = "Package subpath '" + subpath + "' is not a valid " + - "module request for the \"exports\" resolution of " + - pjson_url.ToFilePath() + " imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_MODULE_SPECIFIER(env, msg.c_str()); -} - -void ThrowExportsInvalid(Environment* env, - const std::string& subpath, - const std::string& target, - const URL& pjson_url, - const URL& base) { - if (subpath.length()) { - const std::string msg = "Invalid \"exports\" target \"" + target + - "\" defined for '" + subpath + "' in the package config " + - pjson_url.ToFilePath() + " imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_PACKAGE_TARGET(env, msg.c_str()); - } else { - const std::string msg = "Invalid \"exports\" main target " + target + - " defined in the package config " + pjson_url.ToFilePath() + - " imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_PACKAGE_TARGET(env, msg.c_str()); - } -} - -void ThrowExportsInvalid(Environment* env, - const std::string& subpath, - Local target, - const URL& pjson_url, - const URL& base) { - Local target_string; - if (target->IsObject()) { - if (!v8::JSON::Stringify(env->context(), target.As(), - v8::String::Empty(env->isolate())).ToLocal(&target_string)) - return; - } else { - if (!target->ToString(env->context()).ToLocal(&target_string)) - return; - } - Utf8Value target_utf8(env->isolate(), target_string); - std::string target_str(*target_utf8, target_utf8.length()); - if (target->IsArray()) { - target_str = '[' + target_str + ']'; - } - ThrowExportsInvalid(env, subpath, target_str, pjson_url, base); -} - -Maybe ResolveExportsTargetString(Environment* env, - const std::string& target, - const std::string& subpath, - const std::string& match, - const URL& pjson_url, - const URL& base) { - if (target.substr(0, 2) != "./") { - ThrowExportsInvalid(env, match, target, pjson_url, base); - return Nothing(); - } - if (subpath.length() > 0 && target.back() != '/') { - ThrowExportsInvalid(env, match, target, pjson_url, base); - return Nothing(); - } - URL resolved(target, pjson_url); - std::string resolved_path = resolved.path(); - std::string pkg_path = URL(".", pjson_url).path(); - if (resolved_path.find(pkg_path) != 0 || - resolved_path.find("/node_modules/", pkg_path.length() - 1) != - std::string::npos) { - ThrowExportsInvalid(env, match, target, pjson_url, base); - return Nothing(); - } - if (subpath.length() == 0) return Just(resolved); - URL subpath_resolved(subpath, resolved); - std::string subpath_resolved_path = subpath_resolved.path(); - if (subpath_resolved_path.find(resolved_path) != 0 || - subpath_resolved_path.find("/node_modules/", pkg_path.length() - 1) - != std::string::npos) { - ThrowSubpathInvalid(env, match + subpath, pjson_url, base); - return Nothing(); - } - return Just(subpath_resolved); -} - -bool IsArrayIndex(Environment* env, Local p) { - Local context = env->context(); - Local p_str = p->ToString(context).ToLocalChecked(); - double n_dbl = static_cast(p_str->NumberValue(context).FromJust()); - Local n = Number::New(env->isolate(), n_dbl); - Local cmp_str = n->ToString(context).ToLocalChecked(); - if (!p_str->Equals(context, cmp_str).FromJust()) { - return false; - } - if (n_dbl == 0 && std::signbit(n_dbl) == false) { - return true; - } - Local cmp_integer; - if (!n->ToInteger(context).ToLocal(&cmp_integer)) { - return false; - } - return n_dbl > 0 && n_dbl < (1LL << 32) - 1; -} - -Maybe ResolveExportsTarget(Environment* env, - const URL& pjson_url, - Local target, - const std::string& subpath, - const std::string& pkg_subpath, - const URL& base) { - Isolate* isolate = env->isolate(); - Local context = env->context(); - if (target->IsString()) { - Utf8Value target_utf8(isolate, target.As()); - std::string target_str(*target_utf8, target_utf8.length()); - Maybe resolved = ResolveExportsTargetString(env, target_str, subpath, - pkg_subpath, pjson_url, base); - if (resolved.IsNothing()) { - return Nothing(); - } - return FinalizeResolution(env, resolved.FromJust(), base); - } else if (target->IsArray()) { - Local target_arr = target.As(); - const uint32_t length = target_arr->Length(); - if (length == 0) { - ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base); - return Nothing(); - } - for (uint32_t i = 0; i < length; i++) { - auto target_item = target_arr->Get(context, i).ToLocalChecked(); - { - TryCatchScope try_catch(env); - Maybe resolved = ResolveExportsTarget(env, pjson_url, - target_item, subpath, pkg_subpath, base); - if (resolved.IsNothing()) { - CHECK(try_catch.HasCaught()); - if (try_catch.Exception().IsEmpty()) return Nothing(); - Local e; - if (!try_catch.Exception()->ToObject(context).ToLocal(&e)) - return Nothing(); - Local code; - if (!e->Get(context, env->code_string()).ToLocal(&code)) - return Nothing(); - Local code_string; - if (!code->ToString(context).ToLocal(&code_string)) - return Nothing(); - Utf8Value code_utf8(env->isolate(), code_string); - if (strcmp(*code_utf8, "ERR_PACKAGE_PATH_NOT_EXPORTED") == 0 || - strcmp(*code_utf8, "ERR_INVALID_PACKAGE_TARGET") == 0) { - continue; - } - try_catch.ReThrow(); - return Nothing(); - } - CHECK(!try_catch.HasCaught()); - return FinalizeResolution(env, resolved.FromJust(), base); - } - } - auto invalid = target_arr->Get(context, length - 1).ToLocalChecked(); - Maybe resolved = ResolveExportsTarget(env, pjson_url, invalid, - subpath, pkg_subpath, base); - CHECK(resolved.IsNothing()); - return Nothing(); - } else if (target->IsObject()) { - Local target_obj = target.As(); - Local target_obj_keys = - target_obj->GetOwnPropertyNames(context).ToLocalChecked(); - Local conditionalTarget; - for (uint32_t i = 0; i < target_obj_keys->Length(); ++i) { - Local key = - target_obj_keys->Get(context, i).ToLocalChecked(); - if (IsArrayIndex(env, key)) { - const std::string msg = "Invalid package config " + - pjson_url.ToFilePath() + " imported from " + base.ToFilePath() + - ". \"exports\" cannot contain numeric property keys."; - node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); - return Nothing(); - } - } - for (uint32_t i = 0; i < target_obj_keys->Length(); ++i) { - Local key = target_obj_keys->Get(context, i).ToLocalChecked(); - Utf8Value key_utf8(env->isolate(), - key->ToString(context).ToLocalChecked()); - std::string key_str(*key_utf8, key_utf8.length()); - if (key_str == "node" || key_str == "import") { - conditionalTarget = target_obj->Get(context, key).ToLocalChecked(); - { - TryCatchScope try_catch(env); - Maybe resolved = ResolveExportsTarget(env, pjson_url, - conditionalTarget, subpath, pkg_subpath, base); - if (resolved.IsNothing()) { - CHECK(try_catch.HasCaught()); - if (try_catch.Exception().IsEmpty()) return Nothing(); - Local e; - if (!try_catch.Exception()->ToObject(context).ToLocal(&e)) - return Nothing(); - Local code; - if (!e->Get(context, env->code_string()).ToLocal(&code)) - return Nothing(); - Local code_string; - if (!code->ToString(context).ToLocal(&code_string)) - return Nothing(); - Utf8Value code_utf8(env->isolate(), code_string); - if (strcmp(*code_utf8, "ERR_PACKAGE_PATH_NOT_EXPORTED") == 0) - continue; - try_catch.ReThrow(); - return Nothing(); - } - CHECK(!try_catch.HasCaught()); - return resolved; - } - } else if (key_str == "default") { - conditionalTarget = target_obj->Get(context, key).ToLocalChecked(); - { - TryCatchScope try_catch(env); - Maybe resolved = ResolveExportsTarget(env, pjson_url, - conditionalTarget, subpath, pkg_subpath, base); - if (resolved.IsNothing()) { - CHECK(try_catch.HasCaught() && !try_catch.Exception().IsEmpty()); - auto e = try_catch.Exception()->ToObject(context).ToLocalChecked(); - auto code = e->Get(context, env->code_string()).ToLocalChecked(); - Utf8Value code_utf8(env->isolate(), - code->ToString(context).ToLocalChecked()); - std::string code_str(*code_utf8, code_utf8.length()); - if (code_str == "ERR_PACKAGE_PATH_NOT_EXPORTED") continue; - try_catch.ReThrow(); - return Nothing(); - } - CHECK(!try_catch.HasCaught()); - return resolved; - } - } - } - ThrowExportsNotFound(env, pkg_subpath, pjson_url, base); - return Nothing(); - } - ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base); - return Nothing(); -} - -Maybe IsConditionalExportsMainSugar(Environment* env, - Local exports, - const URL& pjson_url, - const URL& base) { - if (exports->IsString() || exports->IsArray()) return Just(true); - if (!exports->IsObject()) return Just(false); - Local context = env->context(); - Local exports_obj = exports.As(); - Local keys = - exports_obj->GetOwnPropertyNames(context).ToLocalChecked(); - bool isConditionalSugar = false; - for (uint32_t i = 0; i < keys->Length(); ++i) { - Local key = keys->Get(context, i).ToLocalChecked(); - Utf8Value key_utf8(env->isolate(), key->ToString(context).ToLocalChecked()); - bool curIsConditionalSugar = key_utf8.length() == 0 || key_utf8[0] != '.'; - if (i == 0) { - isConditionalSugar = curIsConditionalSugar; - } else if (isConditionalSugar != curIsConditionalSugar) { - const std::string msg = "Invalid package config " + pjson_url.ToFilePath() - + " imported from " + base.ToFilePath() + ". " + - "\"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."; - node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); - return Nothing(); - } - } - return Just(isConditionalSugar); -} - -Maybe PackageMainResolve(Environment* env, - const URL& pjson_url, - const PackageConfig& pcfg, - const URL& base) { - if (pcfg.exists == Exists::Yes) { - Isolate* isolate = env->isolate(); - - if (!pcfg.exports.IsEmpty()) { - Local exports = pcfg.exports.Get(isolate); - Maybe isConditionalExportsMainSugar = - IsConditionalExportsMainSugar(env, exports, pjson_url, base); - if (isConditionalExportsMainSugar.IsNothing()) - return Nothing(); - if (isConditionalExportsMainSugar.FromJust()) { - return ResolveExportsTarget(env, pjson_url, exports, "", "", base); - } else if (exports->IsObject()) { - Local exports_obj = exports.As(); - if (exports_obj->HasOwnProperty(env->context(), env->dot_string()) - .FromJust()) { - Local target = - exports_obj->Get(env->context(), env->dot_string()) - .ToLocalChecked(); - return ResolveExportsTarget(env, pjson_url, target, "", "", base); - } - } - std::string msg = "No \"exports\" main resolved in " + - pjson_url.ToFilePath(); - node::THROW_ERR_PACKAGE_PATH_NOT_EXPORTED(env, msg.c_str()); - } - if (pcfg.has_main == HasMain::Yes) { - URL resolved(pcfg.main, pjson_url); - const std::string& path = resolved.ToFilePath(); - if (CheckDescriptorAtPath(path) == FILE) { - return Just(resolved); - } - } - if (env->options()->experimental_specifier_resolution == "node") { - if (pcfg.has_main == HasMain::Yes) { - return FinalizeResolution(env, URL(pcfg.main, pjson_url), base); - } else { - return FinalizeResolution(env, URL("index", pjson_url), base); - } - } - if (pcfg.type != PackageType::Module) { - Maybe resolved = LegacyMainResolve(pjson_url, pcfg); - if (!resolved.IsNothing()) { - return resolved; - } - } - } - std::string msg = "Cannot find main entry point for " + - URL(".", pjson_url).ToFilePath() + " imported from " + - base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); -} - -Maybe PackageExportsResolve(Environment* env, - const URL& pjson_url, - const std::string& pkg_subpath, - const PackageConfig& pcfg, - const URL& base) { - Isolate* isolate = env->isolate(); - Local context = env->context(); - Local exports = pcfg.exports.Get(isolate); - Maybe isConditionalExportsMainSugar = - IsConditionalExportsMainSugar(env, exports, pjson_url, base); - if (isConditionalExportsMainSugar.IsNothing()) - return Nothing(); - if (!exports->IsObject() || isConditionalExportsMainSugar.FromJust()) { - ThrowExportsNotFound(env, pkg_subpath, pjson_url, base); - return Nothing(); - } - Local exports_obj = exports.As(); - Local subpath = String::NewFromUtf8(isolate, - pkg_subpath.c_str(), v8::NewStringType::kNormal).ToLocalChecked(); - - if (exports_obj->HasOwnProperty(context, subpath).FromJust()) { - Local target = exports_obj->Get(context, subpath).ToLocalChecked(); - Maybe resolved = ResolveExportsTarget(env, pjson_url, target, "", - pkg_subpath, base); - if (resolved.IsNothing()) { - return Nothing(); - } - return FinalizeResolution(env, resolved.FromJust(), base); - } - - Local best_match; - std::string best_match_str = ""; - Local keys = - exports_obj->GetOwnPropertyNames(context).ToLocalChecked(); - for (uint32_t i = 0; i < keys->Length(); ++i) { - Local key = keys->Get(context, i).ToLocalChecked(); - Utf8Value key_utf8(isolate, key->ToString(context).ToLocalChecked()); - std::string key_str(*key_utf8, key_utf8.length()); - if (key_str.back() != '/') continue; - if (pkg_subpath.substr(0, key_str.length()) == key_str && - key_str.length() > best_match_str.length()) { - best_match = key->ToString(context).ToLocalChecked(); - best_match_str = key_str; - } - } - - if (best_match_str.length() > 0) { - auto target = exports_obj->Get(context, best_match).ToLocalChecked(); - std::string subpath = pkg_subpath.substr(best_match_str.length()); - - Maybe resolved = ResolveExportsTarget(env, pjson_url, target, subpath, - pkg_subpath, base); - if (resolved.IsNothing()) { - return Nothing(); - } - return FinalizeResolution(env, resolved.FromJust(), base); - } - - ThrowExportsNotFound(env, pkg_subpath, pjson_url, base); - return Nothing(); -} - -Maybe PackageResolve(Environment* env, - const std::string& specifier, - const URL& base) { - size_t sep_index = specifier.find('/'); - bool valid_package_name = true; - bool scope = false; - if (specifier[0] == '@') { - scope = true; - if (sep_index == std::string::npos || specifier.length() == 0) { - valid_package_name = false; - } else { - sep_index = specifier.find('/', sep_index + 1); - } - } else if (specifier[0] == '.') { - valid_package_name = false; - } - std::string pkg_name = specifier.substr(0, - sep_index == std::string::npos ? std::string::npos : sep_index); - // Package name cannot have leading . and cannot have percent-encoding or - // separators. - for (size_t i = 0; i < pkg_name.length(); i++) { - char c = pkg_name[i]; - if (c == '%' || c == '\\') { - valid_package_name = false; - break; - } - } - if (!valid_package_name) { - std::string msg = "Invalid package name '" + specifier + - "' imported from " + base.ToFilePath(); - node::THROW_ERR_INVALID_MODULE_SPECIFIER(env, msg.c_str()); - return Nothing(); - } - std::string pkg_subpath; - if (sep_index == std::string::npos) { - pkg_subpath = ""; - } else { - pkg_subpath = "." + specifier.substr(sep_index); - } - - // ResolveSelf - const PackageConfig* pcfg; - if (GetPackageScopeConfig(env, base, base).To(&pcfg) && - pcfg->exists == Exists::Yes) { - // TODO(jkrems): Find a way to forward the pair/iterator already generated - // while executing GetPackageScopeConfig - URL pjson_url(""); - bool found_pjson = false; - for (const auto& it : env->package_json_cache) { - if (&it.second == pcfg) { - pjson_url = URL::FromFilePath(it.first); - found_pjson = true; - } - } - if (found_pjson && pcfg->name == pkg_name && !pcfg->exports.IsEmpty()) { - if (pkg_subpath == "./") { - return Just(URL("./", pjson_url)); - } else if (!pkg_subpath.length()) { - return PackageMainResolve(env, pjson_url, *pcfg, base); - } else { - return PackageExportsResolve(env, pjson_url, pkg_subpath, *pcfg, base); - } - } - } - - URL pjson_url("./node_modules/" + pkg_name + "/package.json", &base); - std::string pjson_path = pjson_url.ToFilePath(); - std::string last_path; - do { - DescriptorType check = - CheckDescriptorAtPath(pjson_path.substr(0, pjson_path.length() - 13)); - if (check != DIRECTORY) { - last_path = pjson_path; - pjson_url = URL((scope ? - "../../../../node_modules/" : "../../../node_modules/") + - pkg_name + "/package.json", &pjson_url); - pjson_path = pjson_url.ToFilePath(); - continue; - } - - // Package match. - Maybe pcfg = GetPackageConfig(env, pjson_path, base); - // Invalid package configuration error. - if (pcfg.IsNothing()) return Nothing(); - if (pkg_subpath == "./") { - return Just(URL("./", pjson_url)); - } else if (!pkg_subpath.length()) { - return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base); - } else { - if (!pcfg.FromJust()->exports.IsEmpty()) { - return PackageExportsResolve(env, pjson_url, pkg_subpath, - *pcfg.FromJust(), base); - } else { - return FinalizeResolution(env, URL(pkg_subpath, pjson_url), base); - } - } - CHECK(false); - // Cross-platform root check. - } while (pjson_path.length() != last_path.length()); - - std::string msg = "Cannot find package '" + pkg_name + - "' imported from " + base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); -} - -} // anonymous namespace - -Maybe Resolve(Environment* env, - const std::string& specifier, - const URL& base) { - // Order swapped from spec for minor perf gain. - // Ok since relative URLs cannot parse as URLs. - URL resolved; - if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { - resolved = URL(specifier, base); - } else { - URL pure_url(specifier); - if (!(pure_url.flags() & URL_FLAGS_FAILED)) { - resolved = pure_url; - } else { - return PackageResolve(env, specifier, base); - } - } - return FinalizeResolution(env, resolved, base); -} - -void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - // module.resolve(specifier, url) - CHECK_EQ(args.Length(), 2); - - CHECK(args[0]->IsString()); - Utf8Value specifier_utf8(env->isolate(), args[0]); - std::string specifier_std(*specifier_utf8, specifier_utf8.length()); - - CHECK(args[1]->IsString()); - Utf8Value url_utf8(env->isolate(), args[1]); - URL url(*url_utf8, url_utf8.length()); - - if (url.flags() & URL_FLAGS_FAILED) { - return node::THROW_ERR_INVALID_ARG_TYPE( - env, "second argument is not a URL string"); - } - - Maybe result = - node::loader::Resolve(env, - specifier_std, - url); - if (result.IsNothing()) { - return; - } - - URL resolution = result.FromJust(); - CHECK(!(resolution.flags() & URL_FLAGS_FAILED)); - - Local resolution_obj; - if (resolution.ToObject(env).ToLocal(&resolution_obj)) - args.GetReturnValue().Set(resolution_obj); -} - -void ModuleWrap::GetPackageType(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - // module.getPackageType(url) - CHECK_EQ(args.Length(), 1); - - CHECK(args[0]->IsString()); - Utf8Value url_utf8(env->isolate(), args[0]); - URL url(*url_utf8, url_utf8.length()); - - PackageType pkg_type = PackageType::None; - Maybe pcfg = - GetPackageScopeConfig(env, url, url); - if (!pcfg.IsNothing()) { - pkg_type = pcfg.FromJust()->type; - } - - args.GetReturnValue().Set(Integer::New(env->isolate(), pkg_type)); -} - static MaybeLocal ImportModuleDynamically( Local context, Local referrer, @@ -1664,8 +707,6 @@ void ModuleWrap::Initialize(Local target, target->Set(env->context(), FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction(context).ToLocalChecked()).Check(); - env->SetMethod(target, "resolve", Resolve); - env->SetMethod(target, "getPackageType", GetPackageType); env->SetMethod(target, "setImportModuleDynamicallyCallback", SetImportModuleDynamicallyCallback); diff --git a/src/module_wrap.h b/src/module_wrap.h index 8937431022c0e1..cd51a497acd87e 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -65,8 +65,6 @@ class ModuleWrap : public BaseObject { static void GetStaticDependencySpecifiers( const v8::FunctionCallbackInfo& args); - static void Resolve(const v8::FunctionCallbackInfo& args); - static void GetPackageType(const v8::FunctionCallbackInfo& args); static void SetImportModuleDynamicallyCallback( const v8::FunctionCallbackInfo& args); static void SetInitializeImportMetaObjectCallback( diff --git a/src/node_errors.h b/src/node_errors.h index 960cb725323e92..0c4dcf63e7ef45 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -43,9 +43,6 @@ void OnFatalError(const char* location, const char* message); V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ - V(ERR_INVALID_MODULE_SPECIFIER, TypeError) \ - V(ERR_INVALID_PACKAGE_CONFIG, Error) \ - V(ERR_INVALID_PACKAGE_TARGET, Error) \ V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ @@ -53,9 +50,7 @@ void OnFatalError(const char* location, const char* message); V(ERR_MISSING_PASSPHRASE, TypeError) \ V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \ - V(ERR_MODULE_NOT_FOUND, Error) \ V(ERR_OUT_OF_RANGE, RangeError) \ - V(ERR_PACKAGE_PATH_NOT_EXPORTED, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index 8aaa66aae47d19..0b47df3f6a2354 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -152,8 +152,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, isRequire ? 'ERR_INVALID_FILE_URL_PATH' : - 'ERR_MODULE_NOT_FOUND'); + strictEqual(err.code, 'ERR_INVALID_FILE_URL_PATH'); })); // Package export with numeric index properties must throw a validation error diff --git a/test/message/esm_loader_not_found.out b/test/message/esm_loader_not_found.out index 770ffdc1cb3559..a054dacf2f930a 100644 --- a/test/message/esm_loader_not_found.out +++ b/test/message/esm_loader_not_found.out @@ -1,10 +1,11 @@ (node:*) ExperimentalWarning: The ESM module loader is experimental. (node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time -internal/modules/esm/resolve.js:* - let url = moduleWrapResolve(specifier, parentURL); - ^ - -Error: Cannot find package 'i-dont-exist' imported from * +internal/modules/run_main.js:* + internalBinding('errors').triggerUncaughtException( + ^ +Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'i-dont-exist' imported from * + at packageResolve (internal/modules/esm/resolve.js:*:*) + at moduleResolve (internal/modules/esm/resolve.js:*:*) at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*) at Loader.resolve (internal/modules/esm/loader.js:*:*) at Loader.getModuleJob (internal/modules/esm/loader.js:*:*) @@ -12,7 +13,6 @@ Error: Cannot find package 'i-dont-exist' imported from * at internal/process/esm_loader.js:*:* at Object.initializeLoader (internal/process/esm_loader.js:*:*) at runMainESM (internal/modules/run_main.js:*:*) - at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:*:*) - at internal/main/run_main_module.js:*:* { + at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:*:*) { code: 'ERR_MODULE_NOT_FOUND' }