From 7410e8d63a7f78db270e3321a58c575ec6d03757 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 7 Mar 2020 06:12:08 +0100 Subject: [PATCH] esm: port loader code to JS There is no reason for this to be in C++. Using JavaScript means that the code is more accessible to more developers, which is important for any Node.js feature. This also simplifies the code significantly in some areas. On the technical side, this potentially also enables making some of the file system operations that are involved asynchronous. PR-URL: https://github.com/nodejs/node/pull/32201 Reviewed-By: Bradley Farias Reviewed-By: Guy Bedford --- lib/internal/errors.js | 59 +- lib/internal/modules/esm/get_format.js | 15 +- lib/internal/modules/esm/resolve.js | 603 ++++++++++++++- src/env.h | 5 - src/module_wrap.cc | 971 +------------------------ src/module_wrap.h | 2 - src/node_errors.h | 5 - test/es-module/test-esm-exports.mjs | 3 +- test/message/esm_loader_not_found.out | 14 +- 9 files changed, 657 insertions(+), 1020 deletions(-) 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' }