From c330a6296096ab6477578dbf1a17bbe79852c616 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 11 Dec 2022 23:06:08 +0100 Subject: [PATCH 1/5] module: reduce unnecessary circular dependencies --- lib/internal/bootstrap/browser.js | 11 +- .../bootstrap/switches/is_main_thread.js | 25 + lib/internal/main/check_syntax.js | 6 +- lib/internal/main/eval_string.js | 2 +- lib/internal/main/repl.js | 3 +- lib/internal/modules/cjs/loader.js | 263 ++-- lib/internal/modules/esm/assert.js | 110 -- .../esm/cascaded_loader.js} | 84 +- .../modules/esm/create_dynamic_module.js | 72 - lib/internal/modules/esm/formats.js | 41 - lib/internal/modules/esm/get_format.js | 122 -- .../modules/esm/handle_process_exit.js | 14 - .../modules/esm/initialize_import_meta.js | 41 - lib/internal/modules/esm/load.js | 105 +- lib/internal/modules/esm/loader.js | 456 +++++- lib/internal/modules/esm/module_job.js | 223 --- lib/internal/modules/esm/module_map.js | 44 - lib/internal/modules/esm/package_config.js | 144 -- lib/internal/modules/esm/resolve.js | 819 +--------- lib/internal/modules/esm/translators.js | 349 ----- lib/internal/modules/esm/utils.js | 1348 +++++++++++++++++ lib/internal/modules/{cjs => }/helpers.js | 156 +- lib/internal/modules/package_json_reader.js | 41 - lib/internal/modules/run_main.js | 4 +- lib/internal/process/execution.js | 22 +- lib/internal/process/per_thread.js | 2 +- lib/internal/process/pre_execution.js | 42 +- lib/internal/source_map/source_map_cache.js | 20 +- lib/internal/util.js | 18 + lib/internal/util/inspector.js | 2 +- lib/internal/vm.js | 7 +- lib/internal/vm/module.js | 12 +- lib/repl.js | 25 +- lib/vm.js | 7 +- src/module_wrap.cc | 20 + src/module_wrap.h | 2 + src/node_external_reference.h | 1 + .../test-esm-import-assertion-validation.js | 2 +- test/es-module/test-esm-loader-modulemap.js | 11 +- ...ction.js => inspector-global-function.mjs} | 0 test/parallel/test-bootstrap-modules.js | 26 +- test/parallel/test-util-inspect.js | 2 +- .../test-inspector-break-when-eval.js | 4 +- 43 files changed, 2364 insertions(+), 2344 deletions(-) delete mode 100644 lib/internal/modules/esm/assert.js rename lib/internal/{process/esm_loader.js => modules/esm/cascaded_loader.js} (58%) delete mode 100644 lib/internal/modules/esm/create_dynamic_module.js delete mode 100644 lib/internal/modules/esm/formats.js delete mode 100644 lib/internal/modules/esm/get_format.js delete mode 100644 lib/internal/modules/esm/handle_process_exit.js delete mode 100644 lib/internal/modules/esm/initialize_import_meta.js delete mode 100644 lib/internal/modules/esm/module_job.js delete mode 100644 lib/internal/modules/esm/module_map.js delete mode 100644 lib/internal/modules/esm/package_config.js delete mode 100644 lib/internal/modules/esm/translators.js create mode 100644 lib/internal/modules/esm/utils.js rename lib/internal/modules/{cjs => }/helpers.js (64%) delete mode 100644 lib/internal/modules/package_json_reader.js rename test/fixtures/{inspector-global-function.js => inspector-global-function.mjs} (100%) diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js index 705ddc49dd8ade..c34b9c1ed4641d 100644 --- a/lib/internal/bootstrap/browser.js +++ b/lib/internal/bootstrap/browser.js @@ -42,9 +42,14 @@ defineOperation(globalThis, 'setTimeout', timers.setTimeout); exposeLazyInterfaces(globalThis, 'internal/abort_controller', [ 'AbortController', 'AbortSignal', ]); -exposeLazyInterfaces(globalThis, 'internal/event_target', [ - 'EventTarget', 'Event', -]); +const { + EventTarget, Event, +} = require('internal/event_target'); +exposeInterface(globalThis, 'Event', Event); +exposeInterface(globalThis, 'EventTarget', EventTarget); +// exposeLazyInterfaces(globalThis, 'internal/event_target', [ +// 'EventTarget', 'Event', +// ]); exposeLazyInterfaces(globalThis, 'internal/worker/io', [ 'MessageChannel', 'MessagePort', 'MessageEvent', ]); diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index 241729de35cf41..fc2078e3e46312 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -286,3 +286,28 @@ rawMethods.resetStdioForTesting = function() { stdout = undefined; stderr = undefined; }; + +// Needed by the module loader and generally needed everywhere. +require('url'); +require('fs'); +require('util'); + +require('internal/modules/cjs/loader'); +require('internal/modules/esm/utils'); +require('internal/vm/module'); +// Needed to refresh the time origin. +require('internal/perf/utils'); +// Needed to register the async hooks. +if (internalBinding('config').hasInspector) { + require('internal/inspector_async_hook'); +} +// Needed to set the wasm web API callbacks. +internalBinding('wasm_web_api'); +// Needed to detect whether it's on main thread. +internalBinding('worker'); +// Needed to setup source maps. +require('internal/source_map/source_map_cache'); +// Needed by most execution modes. +require('internal/modules/run_main'); +// Needed to refresh DNS configurations. +require('internal/dns/utils'); diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js index 65f578be50fe1e..af493d8a6dde19 100644 --- a/lib/internal/main/check_syntax.js +++ b/lib/internal/main/check_syntax.js @@ -52,7 +52,7 @@ function loadESMIfNeeded(cb) { const hasModulePreImport = getOptionValue('--import').length > 0; if (hasModulePreImport) { - const { loadESM } = require('internal/process/esm_loader'); + const { loadESM } = require('internal/modules/esm/cascaded_loader'); loadESM(cb); return; } @@ -65,7 +65,7 @@ async function checkSyntax(source, filename) { isModule = getOptionValue('--input-type') === 'module'; } else { const { defaultResolve } = require('internal/modules/esm/resolve'); - const { defaultGetFormat } = require('internal/modules/esm/get_format'); + const { defaultGetFormat } = require('internal/modules/esm/utils'); const { url } = await defaultResolve(pathToFileURL(filename).toString()); const format = await defaultGetFormat(url); isModule = format === 'module'; @@ -77,7 +77,7 @@ async function checkSyntax(source, filename) { return; } - const { loadESM } = require('internal/process/esm_loader'); + const { loadESM } = require('internal/modules/esm/cascaded_loader'); const { handleMainPromise } = require('internal/modules/run_main'); handleMainPromise(loadESM((loader) => wrapSafe(filename, source))); } diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index e92e088b8e0ceb..5ce5b809c7aa4b 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -14,7 +14,7 @@ const { markBootstrapComplete } = require('internal/process/pre_execution'); const { evalModule, evalScript } = require('internal/process/execution'); -const { addBuiltinLibsToObject } = require('internal/modules/cjs/helpers'); +const { addBuiltinLibsToObject } = require('internal/modules/cjs/loader'); const { getOptionValue } = require('internal/options'); diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js index c371706b352c82..0358d194f7505f 100644 --- a/lib/internal/main/repl.js +++ b/lib/internal/main/repl.js @@ -36,8 +36,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) { process.exit(kGenericUserError); } - const esmLoader = require('internal/process/esm_loader'); - esmLoader.loadESM(() => { + require('internal/modules/esm/cascaded_loader').loadESM(() => { console.log(`Welcome to Node.js ${process.version}.\n` + 'Type ".help" for more information.'); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 36efb48fc574cb..4cd28f322246f5 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -25,6 +25,7 @@ const { ArrayIsArray, ArrayPrototypeConcat, ArrayPrototypeFilter, + ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeJoin, @@ -64,6 +65,7 @@ const { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, + StringPrototypeIncludes, } = primordials; // Map used to store CJS parsing data. @@ -72,13 +74,11 @@ const cjsParseCache = new SafeWeakMap(); // Set first due to cycle with ESM loader functions. module.exports = { wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; }, + initializeCJS, addBuiltinLibsToObject, }; const { BuiltinModule } = require('internal/bootstrap/loaders'); -const { - maybeCacheSourceMap, -} = require('internal/source_map/source_map_cache'); const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); const { deprecate, @@ -86,8 +86,8 @@ const { kEmptyObject, filterOwnProperties, setOwnProperty, + getLazy, } = require('internal/util'); -const { Script } = require('vm'); const { internalCompileFunction } = require('internal/vm'); const assert = require('internal/assert'); const fs = require('fs'); @@ -97,21 +97,32 @@ const { sep } = path; const { internalModuleStat } = internalBinding('fs'); const { safeGetenv } = internalBinding('credentials'); const { - cjsConditions, + getCjsConditions, + initializeCjsConditions, hasEsmSyntax, loadBuiltinModule, makeRequireFunction, normalizeReferrerURL, stripBOM, -} = require('internal/modules/cjs/helpers'); -const { getOptionValue } = require('internal/options'); -const preserveSymlinks = getOptionValue('--preserve-symlinks'); -const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); -const shouldReportRequiredModules = process.env.WATCH_REPORT_DEPENDENCIES; -// Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; + enrichCJSError, + readPackageJSON, +} = require('internal/modules/helpers'); +const { getOptionValue, getEmbedderOptions } = require('internal/options'); +const policy = getLazy( + () => (getOptionValue('--experimental-policy') ? require('internal/process/policy') : null) +); +const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES); +let kEvaluated; +const isModuleEvaluated = (status) => { + if (kEvaluated === undefined) { + kEvaluated = internalBinding('module_wrap').kEvaluated; + } + return status === kEvaluated; +}; + +const getCascadedLoader = getLazy( + () => require('internal/modules/esm/cascaded_loader').getCascadedLoader() +); // Whether any user-provided CJS modules had been loaded (executed). // Used for internal assertions. @@ -127,7 +138,6 @@ const { setArrowMessage, } = require('internal/errors'); const { validateString } = require('internal/validators'); -const pendingDeprecation = getOptionValue('--pending-deprecation'); const { CHAR_BACKWARD_SLASH, @@ -140,15 +150,6 @@ const { isProxy } = require('internal/util/types'); -const asyncESM = require('internal/process/esm_loader'); -const { enrichCJSError } = require('internal/modules/esm/translators'); -const { kEvaluated } = internalBinding('module_wrap'); -const { - encodedSepRegEx, - packageExportsResolve, - packageImportsResolve -} = require('internal/modules/esm/resolve'); - const isWindows = process.platform === 'win32'; const relativeResolveCache = ObjectCreate(null); @@ -190,13 +191,13 @@ function updateChildren(parent, child, scan) { } function reportModuleToWatchMode(filename) { - if (shouldReportRequiredModules && process.send) { + if (shouldReportRequiredModules() && process.send) { process.send({ 'watch:require': [filename] }); } } function reportModuleNotFoundToWatchMode(basePath, extensions) { - if (shouldReportRequiredModules && process.send) { + if (shouldReportRequiredModules() && process.send) { process.send({ 'watch:require': ArrayPrototypeMap(extensions, (ext) => path.resolve(`${basePath}${ext}`)) }); } } @@ -213,22 +214,6 @@ function Module(id = '', parent) { this.children = []; } -const builtinModules = []; -for (const { 0: id, 1: mod } of BuiltinModule.map) { - if (mod.canBeRequiredByUsers && - BuiltinModule.canBeRequiredWithoutScheme(id)) { - ArrayPrototypePush(builtinModules, id); - } -} - -const allBuiltins = new SafeSet( - ArrayPrototypeFlatMap(builtinModules, (bm) => [bm, `node:${bm}`]) -); -BuiltinModule.getSchemeOnlyModuleNames().forEach((builtin) => allBuiltins.add(`node:${builtin}`)); - -ObjectFreeze(builtinModules); -Module.builtinModules = builtinModules; - Module._cache = ObjectCreate(null); Module._pathCache = ObjectCreate(null); Module._extensions = ObjectCreate(null); @@ -297,26 +282,113 @@ function setModuleParent(value) { moduleParentCache.set(this, value); } -ObjectDefineProperty(Module.prototype, 'parent', { - __proto__: null, - get: pendingDeprecation ? deprecate( - getModuleParent, - 'module.parent is deprecated due to accuracy issues. Please use ' + - 'require.main to find program entry point instead.', - 'DEP0144' - ) : getModuleParent, - set: pendingDeprecation ? deprecate( - setModuleParent, - 'module.parent is deprecated due to accuracy issues. Please use ' + - 'require.main to find program entry point instead.', - 'DEP0144' - ) : setModuleParent, -}); - let debug = require('internal/util/debuglog').debuglog('module', (fn) => { debug = fn; }); -Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); + +const builtinModules = []; +function initializeCJS() { + const pendingDeprecation = getOptionValue('--pending-deprecation'); + ObjectDefineProperty(Module.prototype, 'parent', { + __proto__: null, + get: pendingDeprecation ? deprecate( + getModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : getModuleParent, + set: pendingDeprecation ? deprecate( + setModuleParent, + 'module.parent is deprecated due to accuracy issues. Please use ' + + 'require.main to find program entry point instead.', + 'DEP0144' + ) : setModuleParent, + }); + Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077'); + + for (const { 0: id, 1: mod } of BuiltinModule.map) { + if (mod.canBeRequiredByUsers && + BuiltinModule.canBeRequiredWithoutScheme(id)) { + ArrayPrototypePush(builtinModules, id); + } + } + + const allBuiltins = new SafeSet( + ArrayPrototypeFlatMap(builtinModules, (bm) => [bm, `node:${bm}`]) + ); + BuiltinModule.getSchemeOnlyModuleNames().forEach((builtin) => allBuiltins.add(`node:${builtin}`)); + ObjectFreeze(builtinModules); + Module.builtinModules = builtinModules; + + Module.isBuiltin = function isBuiltin(moduleName) { + return allBuiltins.has(moduleName); + }; + + initializeCjsConditions(); + + if (!getEmbedderOptions().noGlobalSearchPaths) { + Module._initPaths(); + } + + // TODO(joyeecheung): deprecate this in favor of a proper hook? + Module.runMain = + require('internal/modules/run_main').executeUserEntryPoint; +} + +function addBuiltinLibsToObject(object, dummyModuleName) { + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + + ArrayPrototypeForEach(builtinModules, (name) => { + // Neither add underscored modules, nor ones that contain slashes (e.g., + // 'fs/promises') or ones that are already defined. + if (StringPrototypeStartsWith(name, '_') || + StringPrototypeIncludes(name, '/') || + ObjectPrototypeHasOwnProperty(object, name)) { + return; + } + // Goals of this mechanism are: + // - Lazy loading of built-in modules + // - Having all built-in modules available as non-enumerable properties + // - Allowing the user to re-assign these variables as if there were no + // pre-existing globals with the same name. + + const setReal = (val) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + ObjectDefineProperty(object, name, { + __proto__: null, + get: () => { + const lib = dummyModule.require(name); + + try { + // Override the current getter/setter and set up a new + // non-enumerable property. + ObjectDefineProperty(object, name, { + __proto__: null, + get: () => lib, + set: setReal, + configurable: true, + enumerable: false, + }); + } catch { + // If the property is no longer configurable, ignore the error. + } + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + }); +} // Given a module name, and a list of paths to test, returns the first // matching file in the following precedence. @@ -337,8 +409,7 @@ function readPackage(requestPath) { const existing = packageJsonCache.get(jsonPath); if (existing !== undefined) return existing; - const packageJsonReader = require('internal/modules/package_json_reader'); - const result = packageJsonReader.read(jsonPath); + const result = readPackageJSON(jsonPath); const json = result.containsKeys === false ? '{}' : result.string; if (json === undefined) { packageJsonCache.set(jsonPath, false); @@ -440,7 +511,7 @@ const realpathCache = new SafeMap(); function tryFile(requestPath, isMain) { const rc = _stat(requestPath); if (rc !== 0) return; - if (preserveSymlinks && !isMain) { + if (getOptionValue('--preserve-symlinks') && !isMain) { return path.resolve(requestPath); } return toRealPath(requestPath); @@ -511,9 +582,10 @@ function trySelf(parentPath, request) { } try { + const { packageExportsResolve } = require('internal/modules/esm/utils'); return finalizeEsmResolution(packageExportsResolve( pathToFileURL(pkgPath + '/package.json'), expansion, pkg, - pathToFileURL(parentPath), cjsConditions), parentPath, pkgPath); + pathToFileURL(parentPath), getCjsConditions()), parentPath, pkgPath); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') throw createEsmNotFoundErr(request, pkgPath + '/package.json'); @@ -535,9 +607,10 @@ function resolveExports(nmPath, request) { const pkg = _readPackage(pkgPath); if (pkg?.exports != null) { try { + const { packageExportsResolve } = require('internal/modules/esm/utils'); return finalizeEsmResolution(packageExportsResolve( pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, - cjsConditions), null, pkgPath); + getCjsConditions()), null, pkgPath); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') throw createEsmNotFoundErr(request, pkgPath + '/package.json'); @@ -616,19 +689,19 @@ Module._findPath = function(request, paths, isMain) { if (!trailingSlash) { if (rc === 0) { // File. if (!isMain) { - if (preserveSymlinks) { + if (getOptionValue('--preserve-symlinks')) { filename = path.resolve(basePath); } else { filename = toRealPath(basePath); } - } else if (preserveSymlinksMain) { - // For the main module, we use the preserveSymlinksMain flag instead + } else if (getOptionValue('--preserve-symlinks-main')) { + // For the main module, we use the --preserve-symlinks-main flag instead // mainly for backward compatibility, as the preserveSymlinks flag // historically has not applied to the main module. Most likely this // was intended to keep .bin/ binaries working, as following those // symlinks is usually required for the imports in the corresponding // files to resolve; that said, in some use cases following symlinks - // causes bigger problems which is why the preserveSymlinksMain option + // causes bigger problems which is why the --preserve-symlinks-main option // is needed. filename = path.resolve(basePath); } else { @@ -999,9 +1072,10 @@ Module._resolveFilename = function(request, parent, isMain, options) { const pkg = readPackageScope(parentPath) || {}; if (pkg.data?.imports != null) { try { + const { packageImportsResolve } = require('internal/modules/esm/utils'); return finalizeEsmResolution( packageImportsResolve(request, pathToFileURL(parentPath), - cjsConditions), parentPath, + getCjsConditions()), parentPath, pkg.path); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') @@ -1043,6 +1117,7 @@ Module._resolveFilename = function(request, parent, isMain, options) { }; function finalizeEsmResolution(resolved, parentPath, pkgPath) { + const { encodedSepRegEx } = require('internal/modules/esm/utils'); if (RegExpPrototypeExec(encodedSepRegEx, resolved) !== null) throw new ERR_INVALID_MODULE_SPECIFIER( resolved, 'must not include encoded "/" or "\\" characters', parentPath); @@ -1081,14 +1156,14 @@ Module.prototype.load = function(filename) { Module._extensions[extension](this, filename); this.loaded = true; - const esmLoader = asyncESM.esmLoader; + const cascadedLoader = getCascadedLoader(); // Create module entry at load time to snapshot exports correctly const exports = this.exports; // Preemptively cache if ((module?.module === undefined || - module.module.getStatus() < kEvaluated) && - !esmLoader.cjsCache.has(this)) - esmLoader.cjsCache.set(this, exports); + isModuleEvaluated(module.module.getStatus())) && + !cascadedLoader.cjsCache.has(this)) + cascadedLoader.cjsCache.set(this, exports); }; @@ -1113,22 +1188,26 @@ Module.prototype.require = function(id) { // (needed for setting breakpoint when called with --inspect-brk) let resolvedArgv; let hasPausedEntry = false; - +let Script; function wrapSafe(filename, content, cjsModuleInstance) { if (patched) { const wrapper = Module.wrap(content); + if (Script === undefined) { + ({ Script } = require('vm')); + } const script = new Script(wrapper, { filename, lineOffset: 0, importModuleDynamically: async (specifier, _, importAssertions) => { - const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename), - importAssertions); + const cascadedLoader = getCascadedLoader(); + return cascadedLoader.import(specifier, normalizeReferrerURL(filename), + importAssertions); }, }); // Cache the source map for the module if present. if (script.sourceMapURL) { + const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL); } @@ -1147,21 +1226,23 @@ function wrapSafe(filename, content, cjsModuleInstance) { ], { filename, importModuleDynamically(specifier, _, importAssertions) { - const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename), - importAssertions); + const cascadedLoader = getCascadedLoader(); + return cascadedLoader.import(specifier, normalizeReferrerURL(filename), + importAssertions); }, }); // Cache the source map for the module if present. if (result.sourceMapURL) { + const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); } return result.function; } catch (err) { - if (process.mainModule === cjsModuleInstance) + if (process.mainModule === cjsModuleInstance) { enrichCJSError(err, content); + } throw err; } } @@ -1173,10 +1254,10 @@ function wrapSafe(filename, content, cjsModuleInstance) { Module.prototype._compile = function(content, filename) { let moduleURL; let redirects; - if (policy?.manifest) { + if (policy()?.manifest) { moduleURL = pathToFileURL(filename); - redirects = policy.manifest.getDependencyMapper(moduleURL); - policy.manifest.assertIntegrity(moduleURL, content); + redirects = policy().manifest.getDependencyMapper(moduleURL); + policy().manifest.assertIntegrity(moduleURL, content); } const compiledWrapper = wrapSafe(filename, content, this); @@ -1277,9 +1358,9 @@ Module._extensions['.js'] = function(module, filename) { Module._extensions['.json'] = function(module, filename) { const content = fs.readFileSync(filename, 'utf8'); - if (policy?.manifest) { + if (policy()?.manifest) { const moduleURL = pathToFileURL(filename); - policy.manifest.assertIntegrity(moduleURL, content); + policy().manifest.assertIntegrity(moduleURL, content); } try { @@ -1293,10 +1374,10 @@ Module._extensions['.json'] = function(module, filename) { // Native extension for .node Module._extensions['.node'] = function(module, filename) { - if (policy?.manifest) { + if (policy()?.manifest) { const content = fs.readFileSync(filename); const moduleURL = pathToFileURL(filename); - policy.manifest.assertIntegrity(moduleURL, content); + policy().manifest.assertIntegrity(moduleURL, content); } // Be aware this doesn't use `content` return process.dlopen(module, path.toNamespacedPath(filename)); @@ -1405,9 +1486,5 @@ Module.syncBuiltinESMExports = function syncBuiltinESMExports() { } }; -Module.isBuiltin = function isBuiltin(moduleName) { - return allBuiltins.has(moduleName); -}; - // Backwards compatibility Module.Module = Module; diff --git a/lib/internal/modules/esm/assert.js b/lib/internal/modules/esm/assert.js deleted file mode 100644 index 4402306b934af7..00000000000000 --- a/lib/internal/modules/esm/assert.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -const { - ArrayPrototypeFilter, - ArrayPrototypeIncludes, - ObjectCreate, - ObjectValues, - ObjectPrototypeHasOwnProperty, -} = primordials; -const { validateString } = require('internal/validators'); - -const { - ERR_IMPORT_ASSERTION_TYPE_FAILED, - ERR_IMPORT_ASSERTION_TYPE_MISSING, - ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED, -} = require('internal/errors').codes; - -// The HTML spec has an implied default type of `'javascript'`. -const kImplicitAssertType = 'javascript'; - -/** - * Define a map of module formats to import assertion types (the value of - * `type` in `assert { type: 'json' }`). - * @type {Map} - */ -const formatTypeMap = { - '__proto__': null, - 'builtin': kImplicitAssertType, - 'commonjs': kImplicitAssertType, - 'json': 'json', - 'module': kImplicitAssertType, - 'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an assertion type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42 -}; - -/** - * The HTML spec disallows the default type to be explicitly specified - * (for now); so `import './file.js'` is okay but - * `import './file.js' assert { type: 'javascript' }` throws. - * @type {Array} - */ -const supportedAssertionTypes = ArrayPrototypeFilter( - ObjectValues(formatTypeMap), - (type) => type !== kImplicitAssertType); - - -/** - * Test a module's import assertions. - * @param {string} url The URL of the imported module, for error reporting. - * @param {string} format One of Node's supported translators - * @param {Record} importAssertions Validations for the - * module import. - * @returns {true} - * @throws {TypeError} If the format and assertion type are incompatible. - */ -function validateAssertions(url, format, - importAssertions = ObjectCreate(null)) { - const validType = formatTypeMap[format]; - - switch (validType) { - case undefined: - // Ignore assertions for module formats we don't recognize, to allow new - // formats in the future. - return true; - - case kImplicitAssertType: - // This format doesn't allow an import assertion type, so the property - // must not be set on the import assertions object. - if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) { - return true; - } - return handleInvalidType(url, importAssertions.type); - - case importAssertions.type: - // The asserted type is the valid type for this format. - return true; - - default: - // There is an expected type for this format, but the value of - // `importAssertions.type` might not have been it. - if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) { - // `type` wasn't specified at all. - throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType); - } - return handleInvalidType(url, importAssertions.type); - } -} - -/** - * Throw the correct error depending on what's wrong with the type assertion. - * @param {string} url The resolved URL for the module to be imported - * @param {string} type The value of the import assertion `type` property - */ -function handleInvalidType(url, type) { - // `type` might have not been a string. - validateString(type, 'type'); - - // `type` might not have been one of the types we understand. - if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) { - throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type); - } - - // `type` was the wrong value for this format. - throw new ERR_IMPORT_ASSERTION_TYPE_FAILED(url, type); -} - - -module.exports = { - kImplicitAssertType, - validateAssertions, -}; diff --git a/lib/internal/process/esm_loader.js b/lib/internal/modules/esm/cascaded_loader.js similarity index 58% rename from lib/internal/process/esm_loader.js rename to lib/internal/modules/esm/cascaded_loader.js index 473cea48cafe64..ceeb6da1a8aaba 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/modules/esm/cascaded_loader.js @@ -1,68 +1,41 @@ 'use strict'; +// This file includes a cascaded ESM loader that includes customized +// loader hooks and pre-loaded modules. + const { ArrayIsArray, ObjectCreate, } = primordials; -const { - ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, -} = require('internal/errors').codes; -const { ESMLoader } = require('internal/modules/esm/loader'); const { hasUncaughtExceptionCaptureCallback, } = require('internal/process/execution'); +const { getOptionValue } = require('internal/options'); const { pathToFileURL } = require('internal/url'); -const { - getModuleFromWrap, -} = require('internal/vm/module'); - -exports.initializeImportMetaObject = function(wrap, meta) { - const { callbackMap } = internalBinding('module_wrap'); - if (callbackMap.has(wrap)) { - const { initializeImportMeta } = callbackMap.get(wrap); - if (initializeImportMeta !== undefined) { - initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap); - } - } -}; - -exports.importModuleDynamicallyCallback = -async function importModuleDynamicallyCallback(wrap, specifier, assertions) { - const { callbackMap } = internalBinding('module_wrap'); - if (callbackMap.has(wrap)) { - const { importModuleDynamically } = callbackMap.get(wrap); - if (importModuleDynamically !== undefined) { - return importModuleDynamically( - specifier, getModuleFromWrap(wrap) || wrap, assertions); - } - } - throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); -}; - -const esmLoader = new ESMLoader(); -exports.esmLoader = esmLoader; // Module.runMain() causes loadESM() to re-run (which it should do); however, this should NOT cause // ESM to be re-initialised; doing so causes duplicate custom loaders to be added to the public // esmLoader. let isESMInitialized = false; +/** + * @typedef {import('./loader').ESMLoader} ESMLoader + * @type {ESMLoader} + */ +let cascadedLoader; /** * Causes side-effects: user-defined loader hooks are added to esmLoader. * @returns {void} */ -async function initializeLoader() { +async function initializeCascadedLoader(cascadedLoader, customLoaders, preloadModules) { if (isESMInitialized) { return; } - const { getOptionValue } = require('internal/options'); - const customLoaders = getOptionValue('--experimental-loader'); - const preloadModules = getOptionValue('--import'); const loaders = await loadModulesInIsolation(customLoaders); // Hooks must then be added to external/public loader // (so they're triggered in userland) - esmLoader.addCustomLoaders(loaders); + cascadedLoader.addCustomLoaders(loaders); // Preload after loaders are added so they can be used if (preloadModules?.length) { @@ -85,6 +58,10 @@ function loadModulesInIsolation(specifiers, loaders = []) { // A separate loader instance is necessary to avoid cross-contamination // between internal Node.js and userland. For example, a module with internal // state (such as a counter) should be independent. + + // It's necessary to lazy-load the constructor because of circular + // dependency. + const { ESMLoader } = require('internal/modules/esm/loader'); const internalEsmLoader = new ESMLoader(); internalEsmLoader.addCustomLoaders(loaders); @@ -96,10 +73,30 @@ function loadModulesInIsolation(specifiers, loaders = []) { ); } -exports.loadESM = async function loadESM(callback) { +function getCascadedLoader() { + if (cascadedLoader === undefined) { + const { ESMLoader } = require('internal/modules/esm/loader'); + cascadedLoader = new ESMLoader(); + } + return cascadedLoader; +} + +// XXX(joyeecheung): we have been getting references to the main loader +// synchronously in places where it's unclear whether the loader hooks +// matter. We need to figure that out and make the call sites that are +// affected by the hooks wait for the hooks to load. +async function getInitializedCascadedLoader() { + const customLoaders = getOptionValue('--experimental-loader'); + const preloadModules = getOptionValue('--import'); + const cascadedLoader = getCascadedLoader(); + await initializeCascadedLoader(cascadedLoader, customLoaders, preloadModules); + return cascadedLoader; +} + +async function loadESM(callback) { try { - await initializeLoader(); - await callback(esmLoader); + const cascadedLoader = await getInitializedCascadedLoader(); + await callback(cascadedLoader); } catch (err) { if (hasUncaughtExceptionCaptureCallback()) { process._fatalException(err); @@ -110,4 +107,9 @@ exports.loadESM = async function loadESM(callback) { true /* fromPromise */ ); } +} + +module.exports = { + loadESM, + getCascadedLoader, }; diff --git a/lib/internal/modules/esm/create_dynamic_module.js b/lib/internal/modules/esm/create_dynamic_module.js deleted file mode 100644 index f7c20083b6c918..00000000000000 --- a/lib/internal/modules/esm/create_dynamic_module.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const { - ArrayPrototypeJoin, - ArrayPrototypeMap, - JSONStringify, - ObjectCreate, - SafeSet, -} = primordials; - -let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { - debug = fn; -}); - -function createImport(impt, index) { - const imptPath = JSONStringify(impt); - return `import * as $import_${index} from ${imptPath}; -import.meta.imports[${imptPath}] = $import_${index};`; -} - -function createExport(expt) { - const name = `${expt}`; - return `let $${name}; -export { $${name} as ${name} }; -import.meta.exports.${name} = { - get: () => $${name}, - set: (v) => $${name} = v, -};`; -} - -const createDynamicModule = (imports, exports, url = '', evaluate) => { - debug('creating ESM facade for %s with exports: %j', url, exports); - const source = ` -${ArrayPrototypeJoin(ArrayPrototypeMap(imports, createImport), '\n')} -${ArrayPrototypeJoin(ArrayPrototypeMap(exports, createExport), '\n')} -import.meta.done(); -`; - const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); - const m = new ModuleWrap(`${url}`, undefined, source, 0, 0); - - const readyfns = new SafeSet(); - const reflect = { - exports: ObjectCreate(null), - onReady: (cb) => { readyfns.add(cb); }, - }; - - if (imports.length) - reflect.imports = ObjectCreate(null); - - callbackMap.set(m, { - initializeImportMeta: (meta, wrap) => { - meta.exports = reflect.exports; - if (reflect.imports) - meta.imports = reflect.imports; - meta.done = () => { - evaluate(reflect); - reflect.onReady = (cb) => cb(reflect); - for (const fn of readyfns) { - readyfns.delete(fn); - fn(reflect); - } - }; - }, - }); - - return { - module: m, - reflect, - }; -}; - -module.exports = createDynamicModule; diff --git a/lib/internal/modules/esm/formats.js b/lib/internal/modules/esm/formats.js deleted file mode 100644 index 9853dc160d3566..00000000000000 --- a/lib/internal/modules/esm/formats.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const { - RegExpPrototypeExec, -} = primordials; -const { getOptionValue } = require('internal/options'); - -const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); - -const extensionFormatMap = { - '__proto__': null, - '.cjs': 'commonjs', - '.js': 'module', - '.json': 'json', - '.mjs': 'module', -}; - -if (experimentalWasmModules) { - extensionFormatMap['.wasm'] = 'wasm'; -} - -/** - * @param {string} mime - * @returns {string | null} - */ -function mimeToFormat(mime) { - if ( - RegExpPrototypeExec( - /\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?/i, - mime - ) !== null - ) return 'module'; - if (mime === 'application/json') return 'json'; - if (experimentalWasmModules && mime === 'application/wasm') return 'wasm'; - return null; -} - -module.exports = { - extensionFormatMap, - mimeToFormat, -}; diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js deleted file mode 100644 index 640d240acd4ca1..00000000000000 --- a/lib/internal/modules/esm/get_format.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; -const { - RegExpPrototypeExec, - ObjectPrototypeHasOwnProperty, - PromisePrototypeThen, - PromiseResolve, - StringPrototypeSlice, -} = primordials; -const { basename, extname, relative } = require('path'); -const { getOptionValue } = require('internal/options'); -const { - extensionFormatMap, - mimeToFormat, -} = require('internal/modules/esm/formats'); - -const experimentalNetworkImports = - getOptionValue('--experimental-network-imports'); -const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve'); -const { URL, fileURLToPath } = require('internal/url'); -const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; - -const protocolHandlers = { - '__proto__': null, - 'data:': getDataProtocolModuleFormat, - 'file:': getFileProtocolModuleFormat, - 'http:': getHttpProtocolModuleFormat, - 'https:': getHttpProtocolModuleFormat, - 'node:'() { return 'builtin'; }, -}; - -/** - * @param {URL} parsed - * @returns {string | null} - */ -function getDataProtocolModuleFormat(parsed) { - const { 1: mime } = RegExpPrototypeExec( - /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, - parsed.pathname, - ) || [ null, null, null ]; - - return mimeToFormat(mime); -} - -/** - * @param {URL} url - * @param {{parentURL: string}} context - * @param {boolean} ignoreErrors - * @returns {string} - */ -function getFileProtocolModuleFormat(url, context, ignoreErrors) { - const filepath = fileURLToPath(url); - const ext = extname(filepath); - if (ext === '.js') { - return getPackageType(url) === 'module' ? 'module' : 'commonjs'; - } - - const format = extensionFormatMap[ext]; - if (format) return format; - - // Explicit undefined return indicates load hook should rerun format check - if (ignoreErrors) { return undefined; } - let suggestion = ''; - if (getPackageType(url) === 'module' && ext === '') { - const config = getPackageScopeConfig(url); - const fileBasename = basename(filepath); - const relativePath = StringPrototypeSlice(relative(config.pjsonPath, filepath), 1); - suggestion = 'Loading extensionless files is not supported inside of ' + - '"type":"module" package.json contexts. The package.json file ' + - `${config.pjsonPath} caused this "type":"module" context. Try ` + - `changing ${filepath} to have a file extension. Note the "bin" ` + - 'field of package.json can point to a file with an extension, for example ' + - `{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`; - } - throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath, suggestion); -} - -/** - * @param {URL} url - * @param {{parentURL: string}} context - * @returns {Promise | undefined} only works when enabled - */ -function getHttpProtocolModuleFormat(url, context) { - if (experimentalNetworkImports) { - const { fetchModule } = require('internal/modules/esm/fetch_module'); - return PromisePrototypeThen( - PromiseResolve(fetchModule(url, context)), - (entry) => { - return mimeToFormat(entry.headers['content-type']); - } - ); - } -} - -/** - * @param {URL | URL['href']} url - * @param {{parentURL: string}} context - * @returns {Promise | string | undefined} only works when enabled - */ -function defaultGetFormatWithoutErrors(url, context) { - const parsed = new URL(url); - if (!ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol)) - return null; - return protocolHandlers[parsed.protocol](parsed, context, true); -} - -/** - * @param {URL | URL['href']} url - * @param {{parentURL: string}} context - * @returns {Promise | string | undefined} only works when enabled - */ -function defaultGetFormat(url, context) { - const parsed = new URL(url); - return ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol) ? - protocolHandlers[parsed.protocol](parsed, context, false) : - null; -} - -module.exports = { - defaultGetFormat, - defaultGetFormatWithoutErrors, - extensionFormatMap, -}; diff --git a/lib/internal/modules/esm/handle_process_exit.js b/lib/internal/modules/esm/handle_process_exit.js deleted file mode 100644 index 9d6b609ef1cfc3..00000000000000 --- a/lib/internal/modules/esm/handle_process_exit.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); - -// Handle a Promise from running code that potentially does Top-Level Await. -// In that case, it makes sense to set the exit code to a specific non-zero -// value if the main code never finishes running. -function handleProcessExit() { - process.exitCode ??= kUnfinishedTopLevelAwait; -} - -module.exports = { - handleProcessExit, -}; diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js deleted file mode 100644 index d6be06f23e1493..00000000000000 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const { getOptionValue } = require('internal/options'); -const experimentalImportMetaResolve = - getOptionValue('--experimental-import-meta-resolve'); -const { - PromisePrototypeThen, - PromiseReject, -} = primordials; -const asyncESM = require('internal/process/esm_loader'); - -function createImportMetaResolve(defaultParentUrl) { - return async function resolve(specifier, parentUrl = defaultParentUrl) { - return PromisePrototypeThen( - asyncESM.esmLoader.resolve(specifier, parentUrl), - ({ url }) => url, - (error) => ( - error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? - error.url : PromiseReject(error)) - ); - }; -} - -/** - * @param {object} meta - * @param {{url: string}} context - */ -function initializeImportMeta(meta, context) { - const { url } = context; - - // Alphabetical - if (experimentalImportMetaResolve) { - meta.resolve = createImportMetaResolve(url); - } - - meta.url = url; -} - -module.exports = { - initializeImportMeta -}; diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index f39576afdcc13c..3e4f7c41fe496f 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -1,21 +1,28 @@ 'use strict'; const { - ArrayPrototypePush, RegExpPrototypeExec, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypePush, decodeURIComponent, + ObjectCreate, + ObjectPrototypeHasOwnProperty, + ObjectValues, } = primordials; -const { defaultGetFormat } = require('internal/modules/esm/get_format'); -const { validateAssertions } = require('internal/modules/esm/assert'); -const { getOptionValue } = require('internal/options'); +const { + defaultGetFormat, + formatTypeMap, + kImplicitAssertType, +} = require('internal/modules/esm/utils'); +const { validateString } = require('internal/validators'); -// Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; -const experimentalNetworkImports = - getOptionValue('--experimental-network-imports'); +const { getOptionValue } = require('internal/options'); +const { getLazy } = require('internal/util'); +const policy = getLazy( + () => (getOptionValue('--experimental-policy') ? require('internal/process/policy') : null) +); const { Buffer: { from: BufferFrom } } = require('buffer'); @@ -23,6 +30,9 @@ const { URL } = require('internal/url'); const { ERR_INVALID_URL, ERR_UNSUPPORTED_ESM_URL_SCHEME, + ERR_IMPORT_ASSERTION_TYPE_MISSING, + ERR_IMPORT_ASSERTION_TYPE_FAILED, + ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED, } = require('internal/errors').codes; const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/; @@ -31,6 +41,7 @@ async function getSource(url, context) { const parsed = new URL(url); let responseURL = url; let source; + const experimentalNetworkImports = getOptionValue('--experimental-network-imports'); if (parsed.protocol === 'file:') { const { readFile: readFileAsync } = require('internal/fs/promises').exports; source = await readFileAsync(parsed); @@ -56,12 +67,81 @@ async function getSource(url, context) { } throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, supportedSchemes); } - if (policy?.manifest) { - policy.manifest.assertIntegrity(parsed, source); + if (policy()?.manifest) { + policy().manifest.assertIntegrity(parsed, source); } return { __proto__: null, responseURL, source }; } +/** + * The HTML spec disallows the default type to be explicitly specified + * (for now); so `import './file.js'` is okay but + * `import './file.js' assert { type: 'javascript' }` throws. + * @type {Array} + */ +const supportedAssertionTypes = ArrayPrototypeFilter( + ObjectValues(formatTypeMap), + (type) => type !== kImplicitAssertType); + +/** + * Throw the correct error depending on what's wrong with the type assertion. + * @param {string} url The resolved URL for the module to be imported + * @param {string} type The value of the import assertion `type` property + */ +function handleInvalidType(url, type) { + // `type` might have not been a string. + validateString(type, 'type'); + + // `type` might not have been one of the types we understand. + if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) { + throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type); + } + + // `type` was the wrong value for this format. + throw new ERR_IMPORT_ASSERTION_TYPE_FAILED(url, type); +} + +/** + * Test a module's import assertions. + * @param {string} url The URL of the imported module, for error reporting. + * @param {string} format One of Node's supported translators + * @param {Record} importAssertions Validations for the + * module import. + * @returns {true} + * @throws {TypeError} If the format and assertion type are incompatible. + */ +function validateAssertions(url, format, + importAssertions = ObjectCreate(null)) { + const validType = formatTypeMap[format]; + + switch (validType) { + case undefined: + // Ignore assertions for module formats we don't recognize, to allow new + // formats in the future. + return true; + + case kImplicitAssertType: + // This format doesn't allow an import assertion type, so the property + // must not be set on the import assertions object. + if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) { + return true; + } + return handleInvalidType(url, importAssertions.type); + + case importAssertions.type: + // The asserted type is the valid type for this format. + return true; + + default: + // There is an expected type for this format, but the value of + // `importAssertions.type` might not have been it. + if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) { + // `type` wasn't specified at all. + throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType); + } + return handleInvalidType(url, importAssertions.type); + } +} /** * Node.js default load hook. @@ -102,4 +182,5 @@ async function defaultLoad(url, context) { module.exports = { defaultLoad, + validateAssertions, // for tests. }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index c30cc13756cdf4..87e519b43212b1 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -1,26 +1,39 @@ 'use strict'; -// This is needed to avoid cycles in esm/resolve <-> cjs/loader -require('internal/modules/cjs/loader'); - const { Array, ArrayIsArray, ArrayPrototypeJoin, ArrayPrototypePush, + ArrayPrototypeForEach, + ArrayPrototypeMap, + Boolean, FunctionPrototypeCall, + JSONParse, ObjectAssign, ObjectCreate, ObjectDefineProperty, + ObjectKeys, + ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, RegExpPrototypeExec, SafePromiseAllReturnArrayLike, + SafeArrayIterator, SafeWeakMap, + SafeMap, + SafeSet, + StringPrototypeReplaceAll, StringPrototypeSlice, + StringPrototypeStartsWith, StringPrototypeToUpperCase, globalThis, + JSONStringify, + PromisePrototypeThen, + PromiseReject, } = primordials; +const { WebAssembly } = globalThis; + const { ERR_LOADER_CHAIN_INCOMPLETE, ERR_INTERNAL_ASSERTION, @@ -29,32 +42,383 @@ const { ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_VALUE, ERR_UNKNOWN_MODULE_FORMAT, + ERR_UNKNOWN_BUILTIN_MODULE, } = require('internal/errors').codes; -const { pathToFileURL, isURLInstance, URL } = require('internal/url'); -const { emitExperimentalWarning } = require('internal/util'); -const { - isAnyArrayBuffer, - isArrayBufferView, -} = require('internal/util/types'); -const { - validateObject, - validateString, -} = require('internal/validators'); -function newModuleMap() { - const ModuleMap = require('internal/modules/esm/module_map'); - return new ModuleMap(); -} +const { + pathToFileURL, isURLInstance, URL, fileURLToPath, +} = require('internal/url'); +const { emitExperimentalWarning, getLazy } = require('internal/util'); +const { isAnyArrayBuffer, isArrayBufferView } = require('internal/util/types'); +const { readFileSync } = require('fs'); +const { extname, isAbsolute } = require('path'); +const { validateObject, validateString } = require('internal/validators'); +const { getOptionValue } = require('internal/options'); +const getCascadedLoader = getLazy( + () => require('internal/modules/esm/cascaded_loader').getCascadedLoader() +); const { defaultResolve, - DEFAULT_CONDITIONS, } = require('internal/modules/esm/resolve'); -function getTranslators() { - const { translators } = require('internal/modules/esm/translators'); - return translators; +const { + loadBuiltinModule, + stripBOM, + enrichCJSError, +} = require('internal/modules/helpers'); +const { + Module: CJSModule, + cjsParseCache +} = require('internal/modules/cjs/loader'); + +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); + +const { ModuleWrap } = internalBinding('module_wrap'); +const { + setCallbackForWrap, + ModuleMap, + ModuleJob, + getDefaultConditions, +} = require('internal/modules/esm/utils'); + +const isWindows = process.platform === 'win32'; +let DECODER = null; + +let cjsParse; +async function initCJSParse() { + if (typeof WebAssembly === 'undefined') { + cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; + } else { + const { parse, init } = + require('internal/deps/cjs-module-lexer/dist/lexer'); + try { + await init(); + cjsParse = parse; + } catch { + cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; + } + } +} + +function createImport(impt, index) { + const imptPath = JSONStringify(impt); + return `import * as $import_${index} from ${imptPath}; +import.meta.imports[${imptPath}] = $import_${index};`; } -const { getOptionValue } = require('internal/options'); + +function createExport(expt) { + const name = `${expt}`; + return `let $${name}; +export { $${name} as ${name} }; +import.meta.exports.${name} = { + get: () => $${name}, + set: (v) => $${name} = v, +};`; +} + +function createDynamicModule(imports, exports, url = '', evaluate) { + debug('creating ESM facade for %s with exports: %j', url, exports); + const source = ` +${ArrayPrototypeJoin(ArrayPrototypeMap(imports, createImport), '\n')} +${ArrayPrototypeJoin(ArrayPrototypeMap(exports, createExport), '\n')} +import.meta.done(); +`; + const m = new ModuleWrap(`${url}`, undefined, source, 0, 0); + + const readyfns = new SafeSet(); + const reflect = { + exports: ObjectCreate(null), + onReady: (cb) => { readyfns.add(cb); }, + }; + + if (imports.length) + reflect.imports = ObjectCreate(null); + + setCallbackForWrap(m, { + initializeImportMeta: (meta, wrap) => { + meta.exports = reflect.exports; + if (reflect.imports) + meta.imports = reflect.imports; + meta.done = () => { + evaluate(reflect); + reflect.onReady = (cb) => cb(reflect); + for (const fn of readyfns) { + readyfns.delete(fn); + fn(reflect); + } + }; + }, + }); + + return { + module: m, + reflect, + }; +} + +function assertBufferSource(body, allowString, hookName) { + if (allowString && typeof body === 'string') { + return; + } + if (isArrayBufferView(body) || isAnyArrayBuffer(body)) { + return; + } + throw new ERR_INVALID_RETURN_PROPERTY_VALUE( + `${allowString ? 'string, ' : ''}array buffer, or typed array`, + hookName, + 'source', + body + ); +} + +function stringify(body) { + if (typeof body === 'string') return body; + assertBufferSource(body, false, 'transformSource'); + const { TextDecoder } = require('internal/encoding'); + DECODER = DECODER === null ? new TextDecoder() : DECODER; + return DECODER.decode(body); +} + +function errPath(url) { + const parsed = new URL(url); + if (parsed.protocol === 'file:') { + return fileURLToPath(parsed); + } + return url; +} + +async function moduleStrategy(url, source, isMain) { + assertBufferSource(source, true, 'load'); + source = stringify(source); + const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); + maybeCacheSourceMap(url, source); + debug(`Translating StandardModule ${url}`); + const module = new ModuleWrap(url, undefined, source, 0, 0); + setCallbackForWrap(module, { + initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), + importModuleDynamically: async function(specifier, { url }, assertions) { + const cascadedLoader = getCascadedLoader(); + return cascadedLoader.import(specifier, url, assertions); + }, + }); + return module; +} + +async function commonjsStrategy(url, source, isMain) { + debug(`Translating CJSModule ${url}`); + + let filename = fileURLToPath(new URL(url)); + if (isWindows) + filename = StringPrototypeReplaceAll(filename, '/', '\\'); + + if (!cjsParse) await initCJSParse(); + const { module, exportNames } = cjsPreparseModuleExports(filename); + const namesWithDefault = exportNames.has('default') ? + [...exportNames] : ['default', ...exportNames]; + + return new ModuleWrap(url, undefined, namesWithDefault, function() { + debug(`Loading CJSModule ${url}`); + + let exports; + const cascadedLoader = getCascadedLoader(); + if (cascadedLoader.cjsCache.has(module)) { + exports = cascadedLoader.cjsCache.get(module); + cascadedLoader.cjsCache.delete(module); + } else { + try { + exports = CJSModule._load(filename, undefined, isMain); + } catch (err) { + enrichCJSError(err, undefined, filename); + throw err; + } + } + + for (const exportName of exportNames) { + if (!ObjectPrototypeHasOwnProperty(exports, exportName) || + exportName === 'default') + continue; + // We might trigger a getter -> dont fail. + let value; + try { + value = exports[exportName]; + } catch { + // Continue regardless of error. + } + this.setExport(exportName, value); + } + this.setExport('default', exports); + }); +} + +function cjsPreparseModuleExports(filename) { + let module = CJSModule._cache[filename]; + if (module) { + const cached = cjsParseCache.get(module); + if (cached) + return { module, exportNames: cached.exportNames }; + } + const loaded = Boolean(module); + if (!loaded) { + module = new CJSModule(filename); + module.filename = filename; + module.paths = CJSModule._nodeModulePaths(module.path); + CJSModule._cache[filename] = module; + } + + let source; + try { + source = readFileSync(filename, 'utf8'); + } catch { + // Continue regardless of error. + } + + let exports, reexports; + try { + ({ exports, reexports } = cjsParse(source || '')); + } catch { + exports = []; + reexports = []; + } + + const exportNames = new SafeSet(new SafeArrayIterator(exports)); + + // Set first for cycles. + cjsParseCache.set(module, { source, exportNames, loaded }); + + if (reexports.length) { + module.filename = filename; + module.paths = CJSModule._nodeModulePaths(module.path); + } + ArrayPrototypeForEach(reexports, (reexport) => { + let resolved; + try { + resolved = CJSModule._resolveFilename(reexport, module); + } catch { + return; + } + const ext = extname(resolved); + if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && + isAbsolute(resolved)) { + const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); + for (const name of reexportNames) + exportNames.add(name); + } + }); + + return { module, exportNames }; +} + +async function builtinStrategy(url) { + debug(`Translating BuiltinModule ${url}`); + // Slice 'node:' scheme + const id = StringPrototypeSlice(url, 5); + const module = loadBuiltinModule(id, url); + if (!StringPrototypeStartsWith(url, 'node:') || !module) { + throw new ERR_UNKNOWN_BUILTIN_MODULE(url); + } + debug(`Loading BuiltinModule ${url}`); + return module.getESMFacade(); +} + +async function jsonStrategy(url, source) { + emitExperimentalWarning('Importing JSON modules'); + assertBufferSource(source, true, 'load'); + debug(`Loading JSONModule ${url}`); + const pathname = StringPrototypeStartsWith(url, 'file:') ? + fileURLToPath(url) : null; + let modulePath; + let module; + if (pathname) { + modulePath = isWindows ? + StringPrototypeReplaceAll(pathname, '/', '\\') : pathname; + module = CJSModule._cache[modulePath]; + if (module && module.loaded) { + const exports = module.exports; + return new ModuleWrap(url, undefined, ['default'], function() { + this.setExport('default', exports); + }); + } + } + source = stringify(source); + if (pathname) { + // A require call could have been called on the same file during loading and + // that resolves synchronously. To make sure we always return the identical + // export, we have to check again if the module already exists or not. + module = CJSModule._cache[modulePath]; + if (module && module.loaded) { + const exports = module.exports; + return new ModuleWrap(url, undefined, ['default'], function() { + this.setExport('default', exports); + }); + } + } + try { + const exports = JSONParse(stripBOM(source)); + module = { + exports, + loaded: true + }; + } catch (err) { + // TODO (BridgeAR): We could add a NodeCore error that wraps the JSON + // parse error instead of just manipulating the original error message. + // That would allow to add further properties and maybe additional + // debugging information. + err.message = errPath(url) + ': ' + err.message; + throw err; + } + if (pathname) { + CJSModule._cache[modulePath] = module; + } + return new ModuleWrap(url, undefined, ['default'], function() { + debug(`Parsing JSONModule ${url}`); + this.setExport('default', module.exports); + }); +} + +async function wasmStrategy(url, source) { + emitExperimentalWarning('Importing WebAssembly modules'); + + assertBufferSource(source, false, 'load'); + + debug(`Translating WASMModule ${url}`); + + let compiled; + try { + compiled = await WebAssembly.compile(source); + } catch (err) { + err.message = errPath(url) + ': ' + err.message; + throw err; + } + + const imports = + ArrayPrototypeMap(WebAssembly.Module.imports(compiled), + ({ module }) => module); + const exports = + ArrayPrototypeMap(WebAssembly.Module.exports(compiled), + ({ name }) => name); + + return createDynamicModule(imports, exports, url, (reflect) => { + const { exports } = new WebAssembly.Instance(compiled, reflect.imports); + for (const expt of ObjectKeys(exports)) + reflect.exports[expt].set(exports[expt]); + }).module; +} + +const translators = new SafeMap([ + // Strategy for loading a standard JavaScript module. + ['module', moduleStrategy], + // Strategy for loading a node-style CommonJS module + ['commonjs', commonjsStrategy], + // Strategy for loading a node builtin CommonJS module that isn't + // through normal resolution + ['builtin', builtinStrategy], + // Strategy for loading a JSON file + ['json', jsonStrategy], + // Strategy for loading a wasm module + ['wasm', wasmStrategy], +]); /** * @typedef {object} ExportedHooks @@ -171,6 +535,34 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { ); } +function createImportMetaResolve(defaultParentUrl) { + return async function resolve(specifier, parentUrl = defaultParentUrl) { + const loader = getCascadedLoader(); + return PromisePrototypeThen( + loader.resolve(specifier, parentUrl), + ({ url }) => url, + (error) => ( + error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? + error.url : PromiseReject(error)) + ); + }; +} + +/** + * @param {object} meta + * @param {{url: string}} context + */ +function defaultInitializeImportMeta(meta, context) { + const { url } = context; + + // Alphabetical + if (getOptionValue('--experimental-import-meta-resolve')) { + meta.resolve = createImportMetaResolve(url); + } + + meta.url = url; +} + /** * An ESMLoader instance is used as the main entry point for loading ES modules. * Currently, this is a singleton -- there is only one used for loading @@ -211,7 +603,7 @@ class ESMLoader { ], }; - #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + #importMetaInitializer = defaultInitializeImportMeta; /** * Map of already-loaded CJS modules to use @@ -226,12 +618,12 @@ class ESMLoader { /** * Registry of loaded modules, akin to `require.cache` */ - moduleMap = newModuleMap(); + moduleMap = new ModuleMap(); /** * Methods which translate input code or other information into ES modules */ - translators = getTranslators(); + translators = translators; constructor() { if (getOptionValue('--experimental-loader').length > 0) { @@ -363,9 +755,8 @@ class ESMLoader { url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href ) { const evalInstance = (url) => { - const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const module = new ModuleWrap(url, undefined, source, 0, 0); - callbackMap.set(module, { + setCallbackForWrap(module, { importModuleDynamically: (specifier, { url }, importAssertions) => { return this.import(specifier, url, importAssertions); } @@ -373,7 +764,6 @@ class ESMLoader { return module; }; - const ModuleJob = require('internal/modules/esm/module_job'); const job = new ModuleJob( this, url, undefined, evalInstance, false, false); this.moduleMap.set(url, undefined, job); @@ -450,7 +840,7 @@ class ESMLoader { importAssertions, }); - const translator = getTranslators().get(finalFormat); + const translator = translators.get(finalFormat); if (!translator) { throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); @@ -467,7 +857,6 @@ class ESMLoader { if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { process.send({ 'watch:import': [url] }); } - const ModuleJob = require('internal/modules/esm/module_job'); const job = new ModuleJob( this, url, @@ -798,7 +1187,7 @@ class ESMLoader { } const chain = this.#hooks.resolve; const context = { - conditions: DEFAULT_CONDITIONS, + conditions: getDefaultConditions(), importAssertions, parentURL, }; @@ -894,4 +1283,7 @@ class ESMLoader { ObjectSetPrototypeOf(ESMLoader.prototype, null); -exports.ESMLoader = ESMLoader; +module.exports = { + ESMLoader, + createDynamicModule, // for tests +}; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js deleted file mode 100644 index ddf574b07a8e8a..00000000000000 --- a/lib/internal/modules/esm/module_job.js +++ /dev/null @@ -1,223 +0,0 @@ -'use strict'; - -const { - ArrayPrototypeJoin, - ArrayPrototypePush, - ArrayPrototypeSome, - FunctionPrototype, - ObjectCreate, - ObjectSetPrototypeOf, - PromiseResolve, - PromisePrototypeThen, - ReflectApply, - RegExpPrototypeExec, - RegExpPrototypeSymbolReplace, - SafePromiseAllReturnArrayLike, - SafePromiseAllReturnVoid, - SafeSet, - StringPrototypeIncludes, - StringPrototypeSplit, - StringPrototypeStartsWith, -} = primordials; - -const { ModuleWrap } = internalBinding('module_wrap'); - -const { decorateErrorStack } = require('internal/util'); -const { - getSourceMapsEnabled, -} = require('internal/source_map/source_map_cache'); -const assert = require('internal/assert'); -const resolvedPromise = PromiseResolve(); - -const noop = FunctionPrototype; - -let hasPausedEntry = false; - -const CJSGlobalLike = [ - 'require', - 'module', - 'exports', - '__filename', - '__dirname', -]; -const isCommonJSGlobalLikeNotDefinedError = (errorMessage) => - ArrayPrototypeSome( - CJSGlobalLike, - (globalLike) => errorMessage === `${globalLike} is not defined` - ); - -/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of - * its dependencies, over time. */ -class ModuleJob { - // `loader` is the Loader instance used for loading dependencies. - // `moduleProvider` is a function - constructor(loader, url, importAssertions = ObjectCreate(null), - moduleProvider, isMain, inspectBrk) { - this.loader = loader; - this.importAssertions = importAssertions; - this.isMain = isMain; - this.inspectBrk = inspectBrk; - - this.module = undefined; - // Expose the promise to the ModuleWrap directly for linking below. - // `this.module` is also filled in below. - this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); - - // Wait for the ModuleWrap instance being linked with all dependencies. - const link = async () => { - this.module = await this.modulePromise; - assert(this.module instanceof ModuleWrap); - - // Explicitly keeping track of dependency jobs is needed in order - // to flatten out the dependency graph below in `_instantiate()`, - // so that circular dependencies can't cause a deadlock by two of - // these `link` callbacks depending on each other. - const dependencyJobs = []; - const promises = this.module.link(async (specifier, assertions) => { - const jobPromise = this.loader.getModuleJob(specifier, url, assertions); - ArrayPrototypePush(dependencyJobs, jobPromise); - const job = await jobPromise; - return job.modulePromise; - }); - - if (promises !== undefined) - await SafePromiseAllReturnVoid(promises); - - return SafePromiseAllReturnArrayLike(dependencyJobs); - }; - // Promise for the list of all dependencyJobs. - this.linked = link(); - // This promise is awaited later anyway, so silence - // 'unhandled rejection' warnings. - PromisePrototypeThen(this.linked, undefined, noop); - - // instantiated == deep dependency jobs wrappers are instantiated, - // and module wrapper is instantiated. - this.instantiated = undefined; - } - - instantiate() { - if (this.instantiated === undefined) { - this.instantiated = this._instantiate(); - } - return this.instantiated; - } - - async _instantiate() { - const jobsInGraph = new SafeSet(); - const addJobsToDependencyGraph = async (moduleJob) => { - if (jobsInGraph.has(moduleJob)) { - return; - } - jobsInGraph.add(moduleJob); - const dependencyJobs = await moduleJob.linked; - return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph); - }; - await addJobsToDependencyGraph(this); - - try { - if (!hasPausedEntry && this.inspectBrk) { - hasPausedEntry = true; - const initWrapper = internalBinding('inspector').callAndPauseOnStart; - initWrapper(this.module.instantiate, this.module); - } else { - this.module.instantiate(); - } - } catch (e) { - decorateErrorStack(e); - // TODO(@bcoe): Add source map support to exception that occurs as result - // of missing named export. This is currently not possible because - // stack trace originates in module_job, not the file itself. A hidden - // symbol with filename could be set in node_errors.cc to facilitate this. - if (!getSourceMapsEnabled() && - StringPrototypeIncludes(e.message, - ' does not provide an export named')) { - const splitStack = StringPrototypeSplit(e.stack, '\n'); - const parentFileUrl = RegExpPrototypeSymbolReplace( - /:\d+$/, - splitStack[0], - '' - ); - const { 1: childSpecifier, 2: name } = RegExpPrototypeExec( - /module '(.*)' does not provide an export named '(.+)'/, - e.message); - const { url: childFileURL } = await this.loader.resolve( - childSpecifier, parentFileUrl, - ); - let format; - try { - // This might throw for non-CommonJS modules because we aren't passing - // in the import assertions and some formats require them; but we only - // care about CommonJS for the purposes of this error message. - ({ format } = - await this.loader.load(childFileURL)); - } catch { - // Continue regardless of error. - } - - if (format === 'commonjs') { - const importStatement = splitStack[1]; - // TODO(@ctavan): The original error stack only provides the single - // line which causes the error. For multi-line import statements we - // cannot generate an equivalent object destructuring assignment by - // just parsing the error stack. - const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement); - const destructuringAssignment = oneLineNamedImports && - RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': '); - e.message = `Named export '${name}' not found. The requested module` + - ` '${childSpecifier}' is a CommonJS module, which may not support` + - ' all module.exports as named exports.\nCommonJS modules can ' + - 'always be imported via the default export, for example using:' + - `\n\nimport pkg from '${childSpecifier}';\n${ - destructuringAssignment ? - `const ${destructuringAssignment} = pkg;\n` : ''}`; - const newStack = StringPrototypeSplit(e.stack, '\n'); - newStack[3] = `SyntaxError: ${e.message}`; - e.stack = ArrayPrototypeJoin(newStack, '\n'); - } - } - throw e; - } - - for (const dependencyJob of jobsInGraph) { - // Calling `this.module.instantiate()` instantiates not only the - // ModuleWrap in this module, but all modules in the graph. - dependencyJob.instantiated = resolvedPromise; - } - } - - async run() { - await this.instantiate(); - const timeout = -1; - const breakOnSigint = false; - try { - await this.module.evaluate(timeout, breakOnSigint); - } catch (e) { - if (e?.name === 'ReferenceError' && - isCommonJSGlobalLikeNotDefinedError(e.message)) { - e.message += ' in ES module scope'; - - if (StringPrototypeStartsWith(e.message, 'require ')) { - e.message += ', you can use import instead'; - } - - const packageConfig = - StringPrototypeStartsWith(this.module.url, 'file://') && - RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, this.module.url) !== null && - require('internal/modules/esm/resolve') - .getPackageScopeConfig(this.module.url); - if (packageConfig.type === 'module') { - e.message += - '\nThis file is being treated as an ES module because it has a ' + - `'.js' file extension and '${packageConfig.pjsonPath}' contains ` + - '"type": "module". To treat it as a CommonJS script, rename it ' + - 'to use the \'.cjs\' file extension.'; - } - } - throw e; - } - return { __proto__: null, module: this.module }; - } -} -ObjectSetPrototypeOf(ModuleJob.prototype, null); -module.exports = ModuleJob; diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js deleted file mode 100644 index 7280f052fef59a..00000000000000 --- a/lib/internal/modules/esm/module_map.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const { kImplicitAssertType } = require('internal/modules/esm/assert'); -const { - ObjectCreate, - SafeMap, -} = primordials; -let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { - debug = fn; -}); -const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes; -const { validateString } = require('internal/validators'); - -// Tracks the state of the loader-level module cache -class ModuleMap extends SafeMap { - constructor(i) { super(i); } // eslint-disable-line no-useless-constructor - get(url, type = kImplicitAssertType) { - validateString(url, 'url'); - validateString(type, 'type'); - return super.get(url)?.[type]; - } - set(url, type = kImplicitAssertType, job) { - validateString(url, 'url'); - validateString(type, 'type'); - - const ModuleJob = require('internal/modules/esm/module_job'); - if (job instanceof ModuleJob !== true && - typeof job !== 'function') { - throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job); - } - debug(`Storing ${url} (${ - type === kImplicitAssertType ? 'implicit type' : type - }) in ModuleMap`); - const cachedJobsForUrl = super.get(url) ?? ObjectCreate(null); - cachedJobsForUrl[type] = job; - return super.set(url, cachedJobsForUrl); - } - has(url, type = kImplicitAssertType) { - validateString(url, 'url'); - validateString(type, 'type'); - return super.get(url)?.[type] !== undefined; - } -} -module.exports = ModuleMap; diff --git a/lib/internal/modules/esm/package_config.js b/lib/internal/modules/esm/package_config.js deleted file mode 100644 index a3fe2228e03c29..00000000000000 --- a/lib/internal/modules/esm/package_config.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -const { - JSONParse, - ObjectPrototypeHasOwnProperty, - SafeMap, - StringPrototypeEndsWith, -} = primordials; -const { URL, fileURLToPath } = require('internal/url'); -const { - ERR_INVALID_PACKAGE_CONFIG, -} = require('internal/errors').codes; - -const { filterOwnProperties } = require('internal/util'); - - -/** - * @typedef {string | string[] | Record} Exports - * @typedef {'module' | 'commonjs'} PackageType - * @typedef {{ - * pjsonPath: string, - * exports?: ExportConfig, - * name?: string, - * main?: string, - * type?: PackageType, - * }} PackageConfig - */ - -/** @type {Map} */ -const packageJSONCache = new SafeMap(); - - -/** - * @param {string} path - * @param {string} specifier - * @param {string | URL | undefined} base - * @returns {PackageConfig} - */ -function getPackageConfig(path, specifier, base) { - const existing = packageJSONCache.get(path); - if (existing !== undefined) { - return existing; - } - const packageJsonReader = require('internal/modules/package_json_reader'); - const source = packageJsonReader.read(path).string; - if (source === undefined) { - const packageConfig = { - pjsonPath: path, - exists: false, - main: undefined, - name: undefined, - type: 'none', - exports: undefined, - imports: undefined, - }; - packageJSONCache.set(path, packageConfig); - return packageConfig; - } - - let packageJSON; - try { - packageJSON = JSONParse(source); - } catch (error) { - throw new ERR_INVALID_PACKAGE_CONFIG( - path, - (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), - error.message - ); - } - - let { imports, main, name, type } = filterOwnProperties(packageJSON, ['imports', 'main', 'name', 'type']); - const exports = ObjectPrototypeHasOwnProperty(packageJSON, 'exports') ? packageJSON.exports : undefined; - if (typeof imports !== 'object' || imports === null) { - imports = undefined; - } - 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 = { - pjsonPath: path, - exists: true, - main, - name, - type, - exports, - imports, - }; - packageJSONCache.set(path, packageConfig); - return packageConfig; -} - - -/** - * @param {URL | string} resolved - * @returns {PackageConfig} - */ -function getPackageScopeConfig(resolved) { - 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), resolved); - 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 packageJSONPath = fileURLToPath(packageJSONUrl); - const packageConfig = { - pjsonPath: packageJSONPath, - exists: false, - main: undefined, - name: undefined, - type: 'none', - exports: undefined, - imports: undefined, - }; - packageJSONCache.set(packageJSONPath, packageConfig); - return packageConfig; -} - - -module.exports = { - getPackageConfig, - getPackageScopeConfig, -}; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index e4ad685a6938e7..f9d9a9abc87d00 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1,14 +1,9 @@ 'use strict'; const { - ArrayIsArray, ArrayPrototypeConcat, ArrayPrototypeJoin, ArrayPrototypeShift, - JSONStringify, - ObjectFreeze, - ObjectGetOwnPropertyNames, - ObjectPrototypeHasOwnProperty, RegExp, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, @@ -16,10 +11,7 @@ const { SafeSet, String, StringPrototypeEndsWith, - StringPrototypeIncludes, StringPrototypeIndexOf, - StringPrototypeLastIndexOf, - StringPrototypeReplace, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, @@ -28,211 +20,41 @@ const internalFS = require('internal/fs/utils'); const { BuiltinModule } = require('internal/bootstrap/loaders'); const { realpathSync, - statSync, - Stats, } = require('fs'); const { getOptionValue } = require('internal/options'); -// Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; +const { getLazy } = require('internal/util'); +const policy = getLazy( + () => (getOptionValue('--experimental-policy') ? require('internal/process/policy') : null) +); + const { sep, relative } = require('path'); -const preserveSymlinks = getOptionValue('--preserve-symlinks'); -const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); -const experimentalNetworkImports = - getOptionValue('--experimental-network-imports'); -const typeFlag = getOptionValue('--input-type'); const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); const { ERR_INPUT_TYPE_NOT_ALLOWED, - ERR_INVALID_ARG_VALUE, ERR_INVALID_MODULE_SPECIFIER, - ERR_INVALID_PACKAGE_CONFIG, - ERR_INVALID_PACKAGE_TARGET, ERR_MANIFEST_DEPENDENCY_MISSING, ERR_MODULE_NOT_FOUND, - ERR_PACKAGE_IMPORT_NOT_DEFINED, - ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_UNSUPPORTED_DIR_IMPORT, ERR_NETWORK_IMPORT_DISALLOWED, ERR_UNSUPPORTED_ESM_URL_SCHEME, } = require('internal/errors').codes; -const { Module: CJSModule } = require('internal/modules/cjs/loader'); -const { getPackageConfig, getPackageScopeConfig } = require('internal/modules/esm/package_config'); - -/** - * @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig - */ - - -const userConditions = getOptionValue('--conditions'); -const noAddons = getOptionValue('--no-addons'); -const addonConditions = noAddons ? [] : ['node-addons']; - -const DEFAULT_CONDITIONS = ObjectFreeze([ - 'node', - 'import', - ...addonConditions, - ...userConditions, -]); - -const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); - -const emittedPackageWarnings = new SafeSet(); - -function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) { - const pjsonPath = fileURLToPath(pjsonUrl); - if (emittedPackageWarnings.has(pjsonPath + '|' + match)) - return; - emittedPackageWarnings.add(pjsonPath + '|' + match); - process.emitWarning( - `Use of deprecated trailing slash pattern mapping "${match}" in the ` + - `"exports" field module resolution of the package at ${pjsonPath}${ - base ? ` imported from ${fileURLToPath(base)}` : - ''}. Mapping specifiers ending in "/" is no longer supported.`, - 'DeprecationWarning', - 'DEP0155' - ); -} - -const doubleSlashRegEx = /[/\\][/\\]/; - -function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, internal, base, isTarget) { - const pjsonPath = fileURLToPath(pjsonUrl); - const double = RegExpPrototypeExec(doubleSlashRegEx, isTarget ? target : request) !== null; - process.emitWarning( - `Use of deprecated ${double ? 'double slash' : - 'leading or trailing slash matching'} resolving "${target}" for module ` + - `request "${request}" ${request !== match ? `matched to "${match}" ` : '' - }in the "${internal ? 'imports' : 'exports'}" field module resolution of the package at ${ - pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.`, - 'DeprecationWarning', - 'DEP0166' - ); -} +const { + getConditionsSet, + packageResolve, + packageImportsResolve, + encodedSepRegEx, + tryStatSync, + defaultGetFormatWithoutErrors, +} = require('internal/modules/esm/utils'); -/** - * @param {URL} url - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - * @param {string} main - * @returns {void} - */ -function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) { - const format = defaultGetFormatWithoutErrors(url); - if (format !== 'module') - return; - const path = fileURLToPath(url); - const pkgPath = fileURLToPath(new URL('.', packageJSONUrl)); - const basePath = fileURLToPath(base); - if (main) - process.emitWarning( - `Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` + - `excluding the full filename and extension to the resolved file at "${ - StringPrototypeSlice(path, pkgPath.length)}", imported from ${ - basePath}.\n Automatic extension resolution of the "main" field is ` + - 'deprecated for ES modules.', - 'DeprecationWarning', - 'DEP0151' - ); - else - process.emitWarning( - `No "main" or "exports" field defined in the package.json for ${pkgPath - } resolving the main entry point "${ - StringPrototypeSlice(path, pkgPath.length)}", imported from ${basePath - }.\nDefault "index" lookups for the main are deprecated for ES modules.`, - 'DeprecationWarning', - 'DEP0151' - ); -} /** - * @param {string[]} [conditions] - * @returns {Set} + * @typedef {import('internal/modules/esm/utils.js').PackageConfig} PackageConfig */ -function getConditionsSet(conditions) { - if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { - if (!ArrayIsArray(conditions)) { - throw new ERR_INVALID_ARG_VALUE('conditions', conditions, - 'expected an array'); - } - return new SafeSet(conditions); - } - return DEFAULT_CONDITIONS_SET; -} const realpathCache = new SafeMap(); -/** - * @param {string | URL} path - * @returns {import('fs').Stats} - */ -const tryStatSync = - (path) => statSync(path, { throwIfNoEntry: false }) ?? new Stats(); - -/** - * @param {string | URL} url - * @returns {boolean} - */ -function fileExists(url) { - return statSync(url, { throwIfNoEntry: false })?.isFile() ?? false; -} - -/** - * Legacy CommonJS main resolution: - * 1. let M = pkg_url + (json main field) - * 2. TRY(M, M.js, M.json, M.node) - * 3. TRY(M/index.js, M/index.json, M/index.node) - * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) - * 5. NOT_FOUND - * @param {URL} packageJSONUrl - * @param {PackageConfig} packageConfig - * @param {string | URL | undefined} base - * @returns {URL} - */ -function legacyMainResolve(packageJSONUrl, packageConfig, base) { - let guess; - if (packageConfig.main !== undefined) { - // Note: fs check redundances will be handled by Descriptor cache here. - if (fileExists(guess = new URL(`./${packageConfig.main}`, - packageJSONUrl))) { - return guess; - } else if (fileExists(guess = new URL(`./${packageConfig.main}.js`, - packageJSONUrl))); - else if (fileExists(guess = new URL(`./${packageConfig.main}.json`, - packageJSONUrl))); - else if (fileExists(guess = new URL(`./${packageConfig.main}.node`, - packageJSONUrl))); - else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, - packageJSONUrl))); - else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, - packageJSONUrl))); - else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, - packageJSONUrl))); - else guess = undefined; - if (guess) { - emitLegacyIndexDeprecation(guess, packageJSONUrl, base, - packageConfig.main); - return guess; - } - // Fallthrough. - } - if (fileExists(guess = new URL('./index.js', packageJSONUrl))); - // So fs. - else if (fileExists(guess = new URL('./index.json', packageJSONUrl))); - else if (fileExists(guess = new URL('./index.node', packageJSONUrl))); - else guess = undefined; - if (guess) { - emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main); - return guess; - } - // Not found. - throw new ERR_MODULE_NOT_FOUND( - fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); -} - -const encodedSepRegEx = /%2F|%5C/i; /** * @param {URL} resolved * @param {string | URL | undefined} base @@ -275,554 +97,6 @@ function finalizeResolution(resolved, base, preserveSymlinks) { return resolved; } -/** - * @param {string} specifier - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - */ -function importNotDefined(specifier, packageJSONUrl, base) { - return new ERR_PACKAGE_IMPORT_NOT_DEFINED( - specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), - fileURLToPath(base)); -} - -/** - * @param {string} subpath - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - */ -function exportsNotFound(subpath, packageJSONUrl, base) { - return new ERR_PACKAGE_PATH_NOT_EXPORTED( - fileURLToPath(new URL('.', packageJSONUrl)), subpath, - base && fileURLToPath(base)); -} - -/** - * - * @param {string} request - * @param {string} match - * @param {URL} packageJSONUrl - * @param {boolean} internal - * @param {string | URL | undefined} base - */ -function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) { - const reason = `request is not a valid match in pattern "${match}" for the "${ - internal ? 'imports' : 'exports'}" resolution of ${ - fileURLToPath(packageJSONUrl)}`; - throw new ERR_INVALID_MODULE_SPECIFIER(request, reason, - base && fileURLToPath(base)); -} - -function invalidPackageTarget( - subpath, target, packageJSONUrl, internal, base) { - if (typeof target === 'object' && target !== null) { - target = JSONStringify(target, null, ''); - } else { - target = `${target}`; - } - return new ERR_INVALID_PACKAGE_TARGET( - fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, - internal, base && fileURLToPath(base)); -} - -const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i; -const deprecatedInvalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i; -const invalidPackageNameRegEx = /^\.|%|\\/; -const patternRegEx = /\*/g; - -/** - * - * @param {string} target - * @param {*} subpath - * @param {*} match - * @param {*} packageJSONUrl - * @param {*} base - * @param {*} pattern - * @param {*} internal - * @param {*} isPathMap - * @param {*} conditions - * @returns {URL} - */ -function resolvePackageTargetString( - target, - subpath, - match, - packageJSONUrl, - base, - pattern, - internal, - isPathMap, - conditions, -) { - - if (subpath !== '' && !pattern && target[target.length - 1] !== '/') - throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); - - if (!StringPrototypeStartsWith(target, './')) { - if (internal && !StringPrototypeStartsWith(target, '../') && - !StringPrototypeStartsWith(target, '/')) { - let isURL = false; - try { - new URL(target); - isURL = true; - } catch { - // Continue regardless of error. - } - if (!isURL) { - const exportTarget = pattern ? - RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : - target + subpath; - return packageResolve( - exportTarget, packageJSONUrl, conditions); - } - } - throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); - } - - if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null) { - if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, 2)) === null) { - if (!isPathMap) { - const request = pattern ? - StringPrototypeReplace(match, '*', () => subpath) : - match + subpath; - const resolvedTarget = pattern ? - RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : - target; - emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, true); - } - } else { - throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); - } - } - - const resolved = new URL(target, packageJSONUrl); - const resolvedPath = resolved.pathname; - const packagePath = new URL('.', packageJSONUrl).pathname; - - if (!StringPrototypeStartsWith(resolvedPath, packagePath)) - throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); - - if (subpath === '') return resolved; - - if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) { - const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : match + subpath; - if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, subpath) === null) { - if (!isPathMap) { - const resolvedTarget = pattern ? - RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : - target; - emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, false); - } - } else { - throwInvalidSubpath(request, match, packageJSONUrl, internal, base); - } - } - - if (pattern) { - return new URL( - RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath) - ); - } - - return new URL(subpath, resolved); -} - -/** - * @param {string} key - * @returns {boolean} - */ -function isArrayIndex(key) { - const keyNum = +key; - if (`${keyNum}` !== key) return false; - return keyNum >= 0 && keyNum < 0xFFFF_FFFF; -} - -/** - * - * @param {*} packageJSONUrl - * @param {string|[string]} target - * @param {*} subpath - * @param {*} packageSubpath - * @param {*} base - * @param {*} pattern - * @param {*} internal - * @param {*} isPathMap - * @param {*} conditions - * @returns {URL|null} - */ -function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, - base, pattern, internal, isPathMap, conditions) { - if (typeof target === 'string') { - return resolvePackageTargetString( - target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, - isPathMap, conditions); - } else if (ArrayIsArray(target)) { - if (target.length === 0) { - return null; - } - - let lastException; - for (let i = 0; i < target.length; i++) { - const targetItem = target[i]; - let resolveResult; - try { - resolveResult = resolvePackageTarget( - packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, - internal, isPathMap, conditions); - } catch (e) { - lastException = e; - if (e.code === 'ERR_INVALID_PACKAGE_TARGET') { - continue; - } - throw e; - } - if (resolveResult === undefined) { - continue; - } - if (resolveResult === null) { - lastException = null; - continue; - } - return resolveResult; - } - if (lastException === undefined || lastException === null) - return lastException; - throw lastException; - } else if (typeof target === 'object' && target !== null) { - const keys = ObjectGetOwnPropertyNames(target); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (isArrayIndex(key)) { - throw new ERR_INVALID_PACKAGE_CONFIG( - fileURLToPath(packageJSONUrl), base, - '"exports" cannot contain numeric property keys.'); - } - } - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (key === 'default' || conditions.has(key)) { - const conditionalTarget = target[key]; - const resolveResult = resolvePackageTarget( - packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - pattern, internal, isPathMap, conditions); - if (resolveResult === undefined) - continue; - return resolveResult; - } - } - return undefined; - } else if (target === null) { - return null; - } - throw invalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, - base); -} - -/** - * - * @param {import('internal/modules/esm/package_config.js').Exports} exports - * @param {URL} packageJSONUrl - * @param {string | URL | undefined} base - * @returns {boolean} - */ -function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { - if (typeof exports === 'string' || ArrayIsArray(exports)) return true; - if (typeof exports !== 'object' || exports === null) return false; - - const keys = ObjectGetOwnPropertyNames(exports); - let isConditionalSugar = false; - let i = 0; - for (let j = 0; j < keys.length; j++) { - const key = keys[j]; - const curIsConditionalSugar = key === '' || key[0] !== '.'; - if (i++ === 0) { - isConditionalSugar = curIsConditionalSugar; - } else if (isConditionalSugar !== curIsConditionalSugar) { - throw new ERR_INVALID_PACKAGE_CONFIG( - fileURLToPath(packageJSONUrl), base, - '"exports" cannot contain some keys starting with \'.\' and some not.' + - ' The exports object must either be an object of package subpath keys' + - ' or an object of main entry condition name keys only.'); - } - } - return isConditionalSugar; -} - -/** - * @param {URL} packageJSONUrl - * @param {string} packageSubpath - * @param {PackageConfig} packageConfig - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {URL} - */ -function packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions) { - let exports = packageConfig.exports; - if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) - exports = { '.': exports }; - - if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) && - !StringPrototypeIncludes(packageSubpath, '*') && - !StringPrototypeEndsWith(packageSubpath, '/')) { - const target = exports[packageSubpath]; - const resolveResult = resolvePackageTarget( - packageJSONUrl, target, '', packageSubpath, base, false, false, false, - conditions - ); - - if (resolveResult == null) { - throw exportsNotFound(packageSubpath, packageJSONUrl, base); - } - - return resolveResult; - } - - let bestMatch = ''; - let bestMatchSubpath; - const keys = ObjectGetOwnPropertyNames(exports); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const patternIndex = StringPrototypeIndexOf(key, '*'); - if (patternIndex !== -1 && - StringPrototypeStartsWith(packageSubpath, - StringPrototypeSlice(key, 0, patternIndex))) { - // When this reaches EOL, this can throw at the top of the whole function: - // - // if (StringPrototypeEndsWith(packageSubpath, '/')) - // throwInvalidSubpath(packageSubpath) - // - // To match "imports" and the spec. - if (StringPrototypeEndsWith(packageSubpath, '/')) - emitTrailingSlashPatternDeprecation(packageSubpath, packageJSONUrl, - base); - const patternTrailer = StringPrototypeSlice(key, patternIndex + 1); - if (packageSubpath.length >= key.length && - StringPrototypeEndsWith(packageSubpath, patternTrailer) && - patternKeyCompare(bestMatch, key) === 1 && - StringPrototypeLastIndexOf(key, '*') === patternIndex) { - bestMatch = key; - bestMatchSubpath = StringPrototypeSlice( - packageSubpath, patternIndex, - packageSubpath.length - patternTrailer.length); - } - } - } - - if (bestMatch) { - const target = exports[bestMatch]; - const resolveResult = resolvePackageTarget( - packageJSONUrl, - target, - bestMatchSubpath, - bestMatch, - base, - true, - false, - StringPrototypeEndsWith(packageSubpath, '/'), - conditions); - - if (resolveResult == null) { - throw exportsNotFound(packageSubpath, packageJSONUrl, base); - } - return resolveResult; - } - - throw exportsNotFound(packageSubpath, packageJSONUrl, base); -} - -function patternKeyCompare(a, b) { - const aPatternIndex = StringPrototypeIndexOf(a, '*'); - const bPatternIndex = StringPrototypeIndexOf(b, '*'); - const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; - const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; - if (baseLenA > baseLenB) return -1; - if (baseLenB > baseLenA) return 1; - if (aPatternIndex === -1) return 1; - if (bPatternIndex === -1) return -1; - if (a.length > b.length) return -1; - if (b.length > a.length) return 1; - return 0; -} - -/** - * @param {string} name - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {URL} - */ -function packageImportsResolve(name, base, conditions) { - if (name === '#' || StringPrototypeStartsWith(name, '#/') || - StringPrototypeEndsWith(name, '/')) { - const reason = 'is not a valid internal imports specifier name'; - throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); - } - let packageJSONUrl; - const packageConfig = getPackageScopeConfig(base); - if (packageConfig.exists) { - packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); - const imports = packageConfig.imports; - if (imports) { - if (ObjectPrototypeHasOwnProperty(imports, name) && - !StringPrototypeIncludes(name, '*')) { - const resolveResult = resolvePackageTarget( - packageJSONUrl, imports[name], '', name, base, false, true, false, - conditions - ); - if (resolveResult != null) { - return resolveResult; - } - } else { - let bestMatch = ''; - let bestMatchSubpath; - const keys = ObjectGetOwnPropertyNames(imports); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const patternIndex = StringPrototypeIndexOf(key, '*'); - if (patternIndex !== -1 && - StringPrototypeStartsWith(name, - StringPrototypeSlice(key, 0, - patternIndex))) { - const patternTrailer = StringPrototypeSlice(key, patternIndex + 1); - if (name.length >= key.length && - StringPrototypeEndsWith(name, patternTrailer) && - patternKeyCompare(bestMatch, key) === 1 && - StringPrototypeLastIndexOf(key, '*') === patternIndex) { - bestMatch = key; - bestMatchSubpath = StringPrototypeSlice( - name, patternIndex, name.length - patternTrailer.length); - } - } - } - - if (bestMatch) { - const target = imports[bestMatch]; - const resolveResult = resolvePackageTarget(packageJSONUrl, target, - bestMatchSubpath, - bestMatch, base, true, - true, false, conditions); - if (resolveResult != null) { - return resolveResult; - } - } - } - } - } - throw importNotDefined(name, packageJSONUrl, base); -} - -/** - * @param {URL} url - * @returns {import('internal/modules/esm/package_config.js').PackageType} - */ -function getPackageType(url) { - const packageConfig = getPackageScopeConfig(url); - return packageConfig.type; -} - -/** - * @param {string} specifier - * @param {string | URL | undefined} base - * @returns {{ packageName: string, packageSubpath: string, isScoped: boolean }} - */ -function parsePackageName(specifier, base) { - let separatorIndex = StringPrototypeIndexOf(specifier, '/'); - let validPackageName = true; - let isScoped = false; - if (specifier[0] === '@') { - isScoped = true; - if (separatorIndex === -1 || specifier.length === 0) { - validPackageName = false; - } else { - separatorIndex = StringPrototypeIndexOf( - specifier, '/', separatorIndex + 1); - } - } - - const packageName = separatorIndex === -1 ? - specifier : StringPrototypeSlice(specifier, 0, separatorIndex); - - // Package name cannot have leading . and cannot have percent-encoding or - // \\ separators. - if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) - validPackageName = false; - - if (!validPackageName) { - throw new ERR_INVALID_MODULE_SPECIFIER( - specifier, 'is not a valid package name', fileURLToPath(base)); - } - - const packageSubpath = '.' + (separatorIndex === -1 ? '' : - StringPrototypeSlice(specifier, separatorIndex)); - - return { packageName, packageSubpath, isScoped }; -} - -/** - * @param {string} specifier - * @param {string | URL | undefined} base - * @param {Set} conditions - * @returns {resolved: URL, format? : string} - */ -function packageResolve(specifier, base, conditions) { - if (BuiltinModule.canBeRequiredByUsers(specifier) && - BuiltinModule.canBeRequiredWithoutScheme(specifier)) { - return new URL('node:' + specifier); - } - - const { packageName, packageSubpath, isScoped } = - parsePackageName(specifier, base); - - // ResolveSelf - const packageConfig = getPackageScopeConfig(base); - if (packageConfig.exists) { - const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); - if (packageConfig.name === packageName && - packageConfig.exports !== undefined && packageConfig.exports !== null) { - return packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions); - } - } - - let packageJSONUrl = - new URL('./node_modules/' + packageName + '/package.json', base); - let packageJSONPath = fileURLToPath(packageJSONUrl); - let lastPath; - do { - const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0, - packageJSONPath.length - 13)); - if (!stat.isDirectory()) { - lastPath = packageJSONPath; - packageJSONUrl = new URL((isScoped ? - '../../../../node_modules/' : '../../../node_modules/') + - packageName + '/package.json', packageJSONUrl); - packageJSONPath = fileURLToPath(packageJSONUrl); - continue; - } - - // Package match. - const packageConfig = getPackageConfig(packageJSONPath, specifier, base); - if (packageConfig.exports !== undefined && packageConfig.exports !== null) { - return packageExportsResolve( - packageJSONUrl, packageSubpath, packageConfig, base, conditions); - } - if (packageSubpath === '.') { - return legacyMainResolve( - packageJSONUrl, - packageConfig, - base - ); - } - - return new URL(packageSubpath, packageJSONUrl); - // Cross-platform root check. - } while (packageJSONPath.length !== lastPath.length); - - // eslint can't handle the above code. - // eslint-disable-next-line no-unreachable - throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); -} - /** * @param {string} specifier * @returns {boolean} @@ -887,6 +161,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) { */ function resolveAsCommonJS(specifier, parentURL) { try { + const { Module: CJSModule } = require('internal/modules/cjs/loader'); const parent = fileURLToPath(parentURL); const tmpModule = new CJSModule(parent, null); tmpModule.paths = CJSModule._nodeModulePaths(parent); @@ -994,10 +269,10 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) { } } -async function defaultResolve(specifier, context = {}) { +async function defaultResolveWithoutPolicy(specifier, context = {}) { let { parentURL, conditions } = context; - if (parentURL && policy?.manifest) { - const redirects = policy.manifest.getDependencyMapper(parentURL); + if (parentURL && policy()?.manifest) { + const redirects = policy().manifest.getDependencyMapper(parentURL); if (redirects) { const { resolve, reaction } = redirects; const destination = resolve(specifier, new SafeSet(conditions)); @@ -1039,7 +314,7 @@ async function defaultResolve(specifier, context = {}) { } if (parsed.protocol === 'data:' || - (experimentalNetworkImports && + (getOptionValue('--experimental-network-imports') && ( parsed.protocol === 'https:' || parsed.protocol === 'http:' @@ -1066,7 +341,7 @@ async function defaultResolve(specifier, context = {}) { // This must come after checkIfDisallowedImport if (parsed && parsed.protocol === 'node:') return { __proto__: null, url: specifier }; - throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports); + throwIfUnsupportedURLScheme(parsed, getOptionValue('--experimental-network-imports')); const isMain = parentURL === undefined; if (isMain) { @@ -1078,7 +353,7 @@ async function defaultResolve(specifier, context = {}) { // input, to avoid user confusion over how expansive the effect of the // flag should be (i.e. entry point only, package scope surrounding the // entry point, etc.). - if (typeFlag) throw new ERR_INPUT_TYPE_NOT_ALLOWED(); + if (getOptionValue('--input-type')) throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } conditions = getConditionsSet(conditions); @@ -1088,7 +363,7 @@ async function defaultResolve(specifier, context = {}) { specifier, parentURL, conditions, - isMain ? preserveSymlinksMain : preserveSymlinks + isMain ? getOptionValue('--preserve-symlinks-main') : getOptionValue('--preserve-symlinks') ); } catch (error) { // Try to give the user a hint of what would have been the @@ -1124,36 +399,26 @@ async function defaultResolve(specifier, context = {}) { }; } +async function defaultResolveWithPolicy(specifier, context) { + const ret = await defaultResolveWithoutPolicy(specifier, context); + // This is a preflight check to avoid data exfiltration by query params etc. + policy().manifest.mightAllow(ret.url, () => + new ERR_MANIFEST_DEPENDENCY_MISSING( + context.parentURL, + specifier, + context.conditions + ) + ); + return ret; +} + +async function defaultResolve(specifier, context) { + if (policy()) { + return defaultResolveWithPolicy(specifier, context); + } + return defaultResolveWithoutPolicy(specifier, context); +} + module.exports = { - DEFAULT_CONDITIONS, defaultResolve, - encodedSepRegEx, - getPackageScopeConfig, - getPackageType, - packageExportsResolve, - packageImportsResolve, }; - -// cycle -const { - defaultGetFormatWithoutErrors, -} = require('internal/modules/esm/get_format'); - -if (policy) { - const $defaultResolve = defaultResolve; - module.exports.defaultResolve = async function defaultResolve( - specifier, - context - ) { - const ret = await $defaultResolve(specifier, context); - // This is a preflight check to avoid data exfiltration by query params etc. - policy.manifest.mightAllow(ret.url, () => - new ERR_MANIFEST_DEPENDENCY_MISSING( - context.parentURL, - specifier, - context.conditions - ) - ); - return ret; - }; -} diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js deleted file mode 100644 index 38fa41e47603dd..00000000000000 --- a/lib/internal/modules/esm/translators.js +++ /dev/null @@ -1,349 +0,0 @@ -'use strict'; - -const { - ArrayPrototypeForEach, - ArrayPrototypeMap, - Boolean, - JSONParse, - ObjectGetPrototypeOf, - ObjectPrototypeHasOwnProperty, - ObjectKeys, - SafeArrayIterator, - SafeMap, - SafeSet, - StringPrototypeReplaceAll, - StringPrototypeSlice, - StringPrototypeStartsWith, - SyntaxErrorPrototype, - globalThis: { WebAssembly }, -} = primordials; - -let _TYPES = null; -function lazyTypes() { - if (_TYPES !== null) return _TYPES; - return _TYPES = require('internal/util/types'); -} - -const { readFileSync } = require('fs'); -const { extname, isAbsolute } = require('path'); -const { - hasEsmSyntax, - loadBuiltinModule, - stripBOM, -} = require('internal/modules/cjs/helpers'); -const { - Module: CJSModule, - cjsParseCache -} = require('internal/modules/cjs/loader'); -const internalURLModule = require('internal/url'); -const { fileURLToPath, URL } = require('url'); -let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { - debug = fn; -}); -const { emitExperimentalWarning } = require('internal/util'); -const { - ERR_UNKNOWN_BUILTIN_MODULE, - ERR_INVALID_RETURN_PROPERTY_VALUE -} = require('internal/errors').codes; -const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); -const moduleWrap = internalBinding('module_wrap'); -const { ModuleWrap } = moduleWrap; -const asyncESM = require('internal/process/esm_loader'); -const { emitWarningSync } = require('internal/process/warning'); - -let cjsParse; -async function initCJSParse() { - if (typeof WebAssembly === 'undefined') { - cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; - } else { - const { parse, init } = - require('internal/deps/cjs-module-lexer/dist/lexer'); - try { - await init(); - cjsParse = parse; - } catch { - cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; - } - } -} - -const translators = new SafeMap(); -exports.translators = translators; -exports.enrichCJSError = enrichCJSError; - -let DECODER = null; -function assertBufferSource(body, allowString, hookName) { - if (allowString && typeof body === 'string') { - return; - } - const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes(); - if (isArrayBufferView(body) || isAnyArrayBuffer(body)) { - return; - } - throw new ERR_INVALID_RETURN_PROPERTY_VALUE( - `${allowString ? 'string, ' : ''}array buffer, or typed array`, - hookName, - 'source', - body - ); -} - -function stringify(body) { - if (typeof body === 'string') return body; - assertBufferSource(body, false, 'transformSource'); - const { TextDecoder } = require('internal/encoding'); - DECODER = DECODER === null ? new TextDecoder() : DECODER; - return DECODER.decode(body); -} - -function errPath(url) { - const parsed = new URL(url); - if (parsed.protocol === 'file:') { - return fileURLToPath(parsed); - } - return url; -} - -async function importModuleDynamically(specifier, { url }, assertions) { - return asyncESM.esmLoader.import(specifier, url, assertions); -} - -// Strategy for loading a standard JavaScript module. -translators.set('module', async function moduleStrategy(url, source, isMain) { - assertBufferSource(source, true, 'load'); - source = stringify(source); - maybeCacheSourceMap(url, source); - debug(`Translating StandardModule ${url}`); - const module = new ModuleWrap(url, undefined, source, 0, 0); - moduleWrap.callbackMap.set(module, { - initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), - importModuleDynamically, - }); - return module; -}); - -/** - * @param {Error | any} err - * @param {string} [content] Content of the file, if known. - * @param {string} [filename] Useful only if `content` is unknown. - */ -function enrichCJSError(err, content, filename) { - if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype && - hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) { - // Emit the warning synchronously because we are in the middle of handling - // a SyntaxError that will throw and likely terminate the process before an - // asynchronous warning would be emitted. - emitWarningSync( - 'To load an ES module, set "type": "module" in the package.json or use ' + - 'the .mjs extension.' - ); - } -} - -// Strategy for loading a node-style CommonJS module -const isWindows = process.platform === 'win32'; -translators.set('commonjs', async function commonjsStrategy(url, source, - isMain) { - debug(`Translating CJSModule ${url}`); - - let filename = internalURLModule.fileURLToPath(new URL(url)); - if (isWindows) - filename = StringPrototypeReplaceAll(filename, '/', '\\'); - - if (!cjsParse) await initCJSParse(); - const { module, exportNames } = cjsPreparseModuleExports(filename); - const namesWithDefault = exportNames.has('default') ? - [...exportNames] : ['default', ...exportNames]; - - return new ModuleWrap(url, undefined, namesWithDefault, function() { - debug(`Loading CJSModule ${url}`); - - let exports; - if (asyncESM.esmLoader.cjsCache.has(module)) { - exports = asyncESM.esmLoader.cjsCache.get(module); - asyncESM.esmLoader.cjsCache.delete(module); - } else { - try { - exports = CJSModule._load(filename, undefined, isMain); - } catch (err) { - enrichCJSError(err, undefined, filename); - throw err; - } - } - - for (const exportName of exportNames) { - if (!ObjectPrototypeHasOwnProperty(exports, exportName) || - exportName === 'default') - continue; - // We might trigger a getter -> dont fail. - let value; - try { - value = exports[exportName]; - } catch { - // Continue regardless of error. - } - this.setExport(exportName, value); - } - this.setExport('default', exports); - }); -}); - -function cjsPreparseModuleExports(filename) { - let module = CJSModule._cache[filename]; - if (module) { - const cached = cjsParseCache.get(module); - if (cached) - return { module, exportNames: cached.exportNames }; - } - const loaded = Boolean(module); - if (!loaded) { - module = new CJSModule(filename); - module.filename = filename; - module.paths = CJSModule._nodeModulePaths(module.path); - CJSModule._cache[filename] = module; - } - - let source; - try { - source = readFileSync(filename, 'utf8'); - } catch { - // Continue regardless of error. - } - - let exports, reexports; - try { - ({ exports, reexports } = cjsParse(source || '')); - } catch { - exports = []; - reexports = []; - } - - const exportNames = new SafeSet(new SafeArrayIterator(exports)); - - // Set first for cycles. - cjsParseCache.set(module, { source, exportNames, loaded }); - - if (reexports.length) { - module.filename = filename; - module.paths = CJSModule._nodeModulePaths(module.path); - } - ArrayPrototypeForEach(reexports, (reexport) => { - let resolved; - try { - resolved = CJSModule._resolveFilename(reexport, module); - } catch { - return; - } - const ext = extname(resolved); - if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && - isAbsolute(resolved)) { - const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); - for (const name of reexportNames) - exportNames.add(name); - } - }); - - return { module, exportNames }; -} - -// Strategy for loading a node builtin CommonJS module that isn't -// through normal resolution -translators.set('builtin', async function builtinStrategy(url) { - debug(`Translating BuiltinModule ${url}`); - // Slice 'node:' scheme - const id = StringPrototypeSlice(url, 5); - const module = loadBuiltinModule(id, url); - if (!StringPrototypeStartsWith(url, 'node:') || !module) { - throw new ERR_UNKNOWN_BUILTIN_MODULE(url); - } - debug(`Loading BuiltinModule ${url}`); - return module.getESMFacade(); -}); - -// Strategy for loading a JSON file -translators.set('json', async function jsonStrategy(url, source) { - emitExperimentalWarning('Importing JSON modules'); - assertBufferSource(source, true, 'load'); - debug(`Loading JSONModule ${url}`); - const pathname = StringPrototypeStartsWith(url, 'file:') ? - fileURLToPath(url) : null; - let modulePath; - let module; - if (pathname) { - modulePath = isWindows ? - StringPrototypeReplaceAll(pathname, '/', '\\') : pathname; - module = CJSModule._cache[modulePath]; - if (module && module.loaded) { - const exports = module.exports; - return new ModuleWrap(url, undefined, ['default'], function() { - this.setExport('default', exports); - }); - } - } - source = stringify(source); - if (pathname) { - // A require call could have been called on the same file during loading and - // that resolves synchronously. To make sure we always return the identical - // export, we have to check again if the module already exists or not. - module = CJSModule._cache[modulePath]; - if (module && module.loaded) { - const exports = module.exports; - return new ModuleWrap(url, undefined, ['default'], function() { - this.setExport('default', exports); - }); - } - } - try { - const exports = JSONParse(stripBOM(source)); - module = { - exports, - loaded: true - }; - } catch (err) { - // TODO (BridgeAR): We could add a NodeCore error that wraps the JSON - // parse error instead of just manipulating the original error message. - // That would allow to add further properties and maybe additional - // debugging information. - err.message = errPath(url) + ': ' + err.message; - throw err; - } - if (pathname) { - CJSModule._cache[modulePath] = module; - } - return new ModuleWrap(url, undefined, ['default'], function() { - debug(`Parsing JSONModule ${url}`); - this.setExport('default', module.exports); - }); -}); - -// Strategy for loading a wasm module -translators.set('wasm', async function(url, source) { - emitExperimentalWarning('Importing WebAssembly modules'); - - assertBufferSource(source, false, 'load'); - - debug(`Translating WASMModule ${url}`); - - let compiled; - try { - compiled = await WebAssembly.compile(source); - } catch (err) { - err.message = errPath(url) + ': ' + err.message; - throw err; - } - - const imports = - ArrayPrototypeMap(WebAssembly.Module.imports(compiled), - ({ module }) => module); - const exports = - ArrayPrototypeMap(WebAssembly.Module.exports(compiled), - ({ name }) => name); - - const createDynamicModule = require( - 'internal/modules/esm/create_dynamic_module'); - return createDynamicModule(imports, exports, url, (reflect) => { - const { exports } = new WebAssembly.Instance(compiled, reflect.imports); - for (const expt of ObjectKeys(exports)) - reflect.exports[expt].set(exports[expt]); - }).module; -}); diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js new file mode 100644 index 00000000000000..cb53e4b1dc91b1 --- /dev/null +++ b/lib/internal/modules/esm/utils.js @@ -0,0 +1,1348 @@ +'use strict'; +const { + ArrayIsArray, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSome, + FunctionPrototype, + JSONParse, + JSONStringify, + PromiseResolve, + PromisePrototypeThen, + ReflectApply, + RegExpPrototypeSymbolReplace, + RegExpPrototypeExec, + SafePromiseAllReturnArrayLike, + SafePromiseAllReturnVoid, + SafeSet, + SafeWeakMap, + SafeMap, + StringPrototypeIncludes, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeReplace, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeEndsWith, + ObjectCreate, + ObjectFreeze, + ObjectSetPrototypeOf, + ObjectGetOwnPropertyNames, + ObjectPrototypeHasOwnProperty, +} = primordials; + +const { + ERR_INVALID_ARG_TYPE, + ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, + ERR_INVALID_PACKAGE_CONFIG, + ERR_UNKNOWN_FILE_EXTENSION, + ERR_INVALID_MODULE_SPECIFIER, + ERR_INVALID_PACKAGE_TARGET, + ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_IMPORT_NOT_DEFINED, + ERR_PACKAGE_PATH_NOT_EXPORTED, + ERR_INVALID_ARG_VALUE, +} = require('internal/errors').codes; + +const { validateString } = require('internal/validators'); +const assert = require('internal/assert'); +const { getOptionValue, getEmbedderOptions } = require('internal/options'); +const { BuiltinModule } = require('internal/bootstrap/loaders'); + +const { + ModuleWrap, + setImportModuleDynamicallyCallback, + setInitializeImportMetaObjectCallback +} = internalBinding('module_wrap'); + +const { basename, extname, relative } = require('path'); +const { decorateErrorStack, filterOwnProperties } = require('internal/util'); +const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); +const { statSync, Stats } = require('fs'); +const { readPackageJSON } = require('internal/modules/helpers'); + +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); + +/** + * @typedef {string | string[] | Record} Exports + * @typedef {'module' | 'commonjs'} PackageType + * @typedef {{ + * pjsonPath: string, + * exports?: ExportConfig, + * name?: string, + * main?: string, + * type?: PackageType, + * }} PackageConfig + */ + +/** @type {Map} */ +const packageJSONCache = new SafeMap(); + +const callbackMap = new SafeWeakMap(); +function setCallbackForWrap(wrap, data) { + callbackMap.set(wrap, data); +} + +const wrapToModuleMap = new SafeWeakMap(); + +const resolvedPromise = PromiseResolve(); + +const noop = FunctionPrototype; + +let hasPausedEntry = false; + +const CJSGlobalLike = [ + 'require', + 'module', + 'exports', + '__filename', + '__dirname', +]; + +// The HTML spec has an implied default type of `'javascript'`. +const kImplicitAssertType = 'javascript'; + +/** + * Define a map of module formats to import assertion types (the value of + * `type` in `assert { type: 'json' }`). + * @type {Map} + */ +const formatTypeMap = { + '__proto__': null, + 'builtin': kImplicitAssertType, + 'commonjs': kImplicitAssertType, + 'json': 'json', + 'module': kImplicitAssertType, + 'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an assertion type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42 +}; + +const extensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'module', + '.json': 'json', + '.mjs': 'module', +}; +const extensionFormatMapInitialized = false; + +function getExtensionFormatMap() { + if (!extensionFormatMapInitialized && getOptionValue('--experimental-wasm-modules')) { + extensionFormatMap['.wasm'] = 'wasm'; + } + return extensionFormatMap; +} + +let defaultConditions; +function getDefaultConditions() { + if (defaultConditions === undefined) { + initializeDefaultConditions(); + } + return defaultConditions; +} + +let defaultConditionsSet; +function getDefaultConditionsSet() { + if (defaultConditionsSet === undefined) { + initializeDefaultConditions(); + } + return defaultConditionsSet; +} + +function initializeDefaultConditions() { + const userConditions = getOptionValue('--conditions'); + const noAddons = getOptionValue('--no-addons'); + const addonConditions = noAddons ? [] : ['node-addons']; + + defaultConditions = ObjectFreeze([ + 'node', + 'import', + ...addonConditions, + ...userConditions, + ]); + defaultConditionsSet = new SafeSet(defaultConditions); +} + +/** + * @param {string[]} [conditions] + * @returns {Set} + */ +function getConditionsSet(conditions) { + if (conditions !== undefined && conditions !== getDefaultConditions()) { + if (!ArrayIsArray(conditions)) { + throw new ERR_INVALID_ARG_VALUE('conditions', conditions, + 'expected an array'); + } + return new SafeSet(conditions); + } + return getDefaultConditionsSet(); +} + +/** + * @param {string} path + * @param {string} specifier + * @param {string | URL | undefined} base + * @returns {PackageConfig} + */ +function getPackageConfig(path, specifier, base) { + const existing = packageJSONCache.get(path); + if (existing !== undefined) { + return existing; + } + const source = readPackageJSON(path).string; + if (source === undefined) { + const packageConfig = { + pjsonPath: path, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let packageJSON; + try { + packageJSON = JSONParse(source); + } catch (error) { + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); + } + + let { imports, main, name, type } = filterOwnProperties(packageJSON, ['imports', 'main', 'name', 'type']); + const exports = ObjectPrototypeHasOwnProperty(packageJSON, 'exports') ? packageJSON.exports : undefined; + if (typeof imports !== 'object' || imports === null) { + imports = undefined; + } + 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 = { + pjsonPath: path, + exists: true, + main, + name, + type, + exports, + imports, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; +} + + +/** + * @param {URL | string} resolved + * @returns {PackageConfig} + */ +function getPackageScopeConfig(resolved) { + 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), resolved); + 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 packageJSONPath = fileURLToPath(packageJSONUrl); + const packageConfig = { + pjsonPath: packageJSONPath, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(packageJSONPath, packageConfig); + return packageConfig; +} + +const emittedPackageWarnings = new SafeSet(); + +function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) { + const pjsonPath = fileURLToPath(pjsonUrl); + if (emittedPackageWarnings.has(pjsonPath + '|' + match)) + return; + emittedPackageWarnings.add(pjsonPath + '|' + match); + process.emitWarning( + `Use of deprecated trailing slash pattern mapping "${match}" in the ` + + `"exports" field module resolution of the package at ${pjsonPath}${ + base ? ` imported from ${fileURLToPath(base)}` : + ''}. Mapping specifiers ending in "/" is no longer supported.`, + 'DeprecationWarning', + 'DEP0155' + ); +} + +const doubleSlashRegEx = /[/\\][/\\]/; + +function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, internal, base, isTarget) { + const pjsonPath = fileURLToPath(pjsonUrl); + const double = RegExpPrototypeExec(doubleSlashRegEx, isTarget ? target : request) !== null; + process.emitWarning( + `Use of deprecated ${double ? 'double slash' : + 'leading or trailing slash matching'} resolving "${target}" for module ` + + `request "${request}" ${request !== match ? `matched to "${match}" ` : '' + }in the "${internal ? 'imports' : 'exports'}" field module resolution of the package at ${ + pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.`, + 'DeprecationWarning', + 'DEP0166' + ); +} + +/** + * @param {URL} url + * @param {URL} packageJSONUrl + * @param {string | URL | undefined} base + * @param {string} main + * @returns {void} + */ +function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) { + const format = defaultGetFormatWithoutErrors(url); + if (format !== 'module') + return; + const path = fileURLToPath(url); + const pkgPath = fileURLToPath(new URL('.', packageJSONUrl)); + const basePath = fileURLToPath(base); + if (main) + process.emitWarning( + `Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` + + `excluding the full filename and extension to the resolved file at "${ + StringPrototypeSlice(path, pkgPath.length)}", imported from ${ + basePath}.\n Automatic extension resolution of the "main" field is ` + + 'deprecated for ES modules.', + 'DeprecationWarning', + 'DEP0151' + ); + else + process.emitWarning( + `No "main" or "exports" field defined in the package.json for ${pkgPath + } resolving the main entry point "${ + StringPrototypeSlice(path, pkgPath.length)}", imported from ${basePath + }.\nDefault "index" lookups for the main are deprecated for ES modules.`, + 'DeprecationWarning', + 'DEP0151' + ); +} + +/** + * @param {string | URL} path + * @returns {import('fs').Stats} + */ +const tryStatSync = + (path) => statSync(path, { throwIfNoEntry: false }) ?? new Stats(); + +/** + * @param {string | URL} url + * @returns {boolean} + */ +function fileExists(url) { + return statSync(url, { throwIfNoEntry: false })?.isFile() ?? false; +} + +/** + * Legacy CommonJS main resolution: + * 1. let M = pkg_url + (json main field) + * 2. TRY(M, M.js, M.json, M.node) + * 3. TRY(M/index.js, M/index.json, M/index.node) + * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) + * 5. NOT_FOUND + * @param {URL} packageJSONUrl + * @param {PackageConfig} packageConfig + * @param {string | URL | undefined} base + * @returns {URL} + */ +function legacyMainResolve(packageJSONUrl, packageConfig, base) { + let guess; + if (packageConfig.main !== undefined) { + // Note: fs check redundances will be handled by Descriptor cache here. + if (fileExists(guess = new URL(`./${packageConfig.main}`, + packageJSONUrl))) { + return guess; + } else if (fileExists(guess = new URL(`./${packageConfig.main}.js`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}.json`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}.node`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, + packageJSONUrl))); + else guess = undefined; + if (guess) { + emitLegacyIndexDeprecation(guess, packageJSONUrl, base, + packageConfig.main); + return guess; + } + // Fallthrough. + } + if (fileExists(guess = new URL('./index.js', packageJSONUrl))); + // So fs. + else if (fileExists(guess = new URL('./index.json', packageJSONUrl))); + else if (fileExists(guess = new URL('./index.node', packageJSONUrl))); + else guess = undefined; + if (guess) { + emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main); + return guess; + } + // Not found. + throw new ERR_MODULE_NOT_FOUND( + fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); +} + +const encodedSepRegEx = /%2F|%5C/i; + +/** + * @param {string} specifier + * @param {URL} packageJSONUrl + * @param {string | URL | undefined} base + */ +function importNotDefined(specifier, packageJSONUrl, base) { + return new ERR_PACKAGE_IMPORT_NOT_DEFINED( + specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), + fileURLToPath(base)); +} + +/** + * @param {string} subpath + * @param {URL} packageJSONUrl + * @param {string | URL | undefined} base + */ +function exportsNotFound(subpath, packageJSONUrl, base) { + return new ERR_PACKAGE_PATH_NOT_EXPORTED( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, + base && fileURLToPath(base)); +} + +/** + * + * @param {string} request + * @param {string} match + * @param {URL} packageJSONUrl + * @param {boolean} internal + * @param {string | URL | undefined} base + */ +function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) { + const reason = `request is not a valid match in pattern "${match}" for the "${ + internal ? 'imports' : 'exports'}" resolution of ${ + fileURLToPath(packageJSONUrl)}`; + throw new ERR_INVALID_MODULE_SPECIFIER(request, reason, + base && fileURLToPath(base)); +} + +function invalidPackageTarget( + subpath, target, packageJSONUrl, internal, base) { + if (typeof target === 'object' && target !== null) { + target = JSONStringify(target, null, ''); + } else { + target = `${target}`; + } + return new ERR_INVALID_PACKAGE_TARGET( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, + internal, base && fileURLToPath(base)); +} + +const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i; +const deprecatedInvalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i; +const invalidPackageNameRegEx = /^\.|%|\\/; +const patternRegEx = /\*/g; + +/** + * + * @param {string} target + * @param {*} subpath + * @param {*} match + * @param {*} packageJSONUrl + * @param {*} base + * @param {*} pattern + * @param {*} internal + * @param {*} isPathMap + * @param {*} conditions + * @returns {URL} + */ +function resolvePackageTargetString( + target, + subpath, + match, + packageJSONUrl, + base, + pattern, + internal, + isPathMap, + conditions, +) { + + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') + throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (!StringPrototypeStartsWith(target, './')) { + if (internal && !StringPrototypeStartsWith(target, '../') && + !StringPrototypeStartsWith(target, '/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch { + // Continue regardless of error. + } + if (!isURL) { + const exportTarget = pattern ? + RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : + target + subpath; + return packageResolve( + exportTarget, packageJSONUrl, conditions); + } + } + throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); + } + + if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null) { + if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, 2)) === null) { + if (!isPathMap) { + const request = pattern ? + StringPrototypeReplace(match, '*', () => subpath) : + match + subpath; + const resolvedTarget = pattern ? + RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : + target; + emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, true); + } + } else { + throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); + } + } + + const resolved = new URL(target, packageJSONUrl); + const resolvedPath = resolved.pathname; + const packagePath = new URL('.', packageJSONUrl).pathname; + + if (!StringPrototypeStartsWith(resolvedPath, packagePath)) + throw invalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (subpath === '') return resolved; + + if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) { + const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : match + subpath; + if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, subpath) === null) { + if (!isPathMap) { + const resolvedTarget = pattern ? + RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : + target; + emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, internal, base, false); + } + } else { + throwInvalidSubpath(request, match, packageJSONUrl, internal, base); + } + } + + if (pattern) { + return new URL( + RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath) + ); + } + + return new URL(subpath, resolved); +} + +/** + * @param {string} key + * @returns {boolean} + */ +function isArrayIndex(key) { + const keyNum = +key; + if (`${keyNum}` !== key) return false; + return keyNum >= 0 && keyNum < 0xFFFF_FFFF; +} + +/** + * + * @param {*} packageJSONUrl + * @param {string|[string]} target + * @param {*} subpath + * @param {*} packageSubpath + * @param {*} base + * @param {*} pattern + * @param {*} internal + * @param {*} isPathMap + * @param {*} conditions + * @returns {URL|null} + */ +function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, + base, pattern, internal, isPathMap, conditions) { + if (typeof target === 'string') { + return resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, + isPathMap, conditions); + } else if (ArrayIsArray(target)) { + if (target.length === 0) { + return null; + } + + let lastException; + for (let i = 0; i < target.length; i++) { + const targetItem = target[i]; + let resolveResult; + try { + resolveResult = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, isPathMap, conditions); + } catch (e) { + lastException = e; + if (e.code === 'ERR_INVALID_PACKAGE_TARGET') { + continue; + } + throw e; + } + if (resolveResult === undefined) { + continue; + } + if (resolveResult === null) { + lastException = null; + continue; + } + return resolveResult; + } + if (lastException === undefined || lastException === null) + return lastException; + throw lastException; + } else if (typeof target === 'object' && target !== null) { + const keys = ObjectGetOwnPropertyNames(target); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (isArrayIndex(key)) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain numeric property keys.'); + } + } + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key === 'default' || conditions.has(key)) { + const conditionalTarget = target[key]; + const resolveResult = resolvePackageTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, + pattern, internal, isPathMap, conditions); + if (resolveResult === undefined) + continue; + return resolveResult; + } + } + return undefined; + } else if (target === null) { + return null; + } + throw invalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, + base); +} + +/** + * + * @param {Exports} exports + * @param {URL} packageJSONUrl + * @param {string | URL | undefined} base + * @returns {boolean} + */ +function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { + if (typeof exports === 'string' || ArrayIsArray(exports)) return true; + if (typeof exports !== 'object' || exports === null) return false; + + const keys = ObjectGetOwnPropertyNames(exports); + let isConditionalSugar = false; + let i = 0; + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const curIsConditionalSugar = key === '' || key[0] !== '.'; + if (i++ === 0) { + isConditionalSugar = curIsConditionalSugar; + } else if (isConditionalSugar !== curIsConditionalSugar) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain some keys starting with \'.\' and some not.' + + ' The exports object must either be an object of package subpath keys' + + ' or an object of main entry condition name keys only.'); + } + } + return isConditionalSugar; +} + +/** + * @param {URL} packageJSONUrl + * @param {string} packageSubpath + * @param {PackageConfig} packageConfig + * @param {string | URL | undefined} base + * @param {Set} conditions + * @returns {URL} + */ +function packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions) { + let exports = packageConfig.exports; + if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) + exports = { '.': exports }; + + if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) && + !StringPrototypeIncludes(packageSubpath, '*') && + !StringPrototypeEndsWith(packageSubpath, '/')) { + const target = exports[packageSubpath]; + const resolveResult = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, false, false, + conditions + ); + + if (resolveResult == null) { + throw exportsNotFound(packageSubpath, packageJSONUrl, base); + } + + return resolveResult; + } + + let bestMatch = ''; + let bestMatchSubpath; + const keys = ObjectGetOwnPropertyNames(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const patternIndex = StringPrototypeIndexOf(key, '*'); + if (patternIndex !== -1 && + StringPrototypeStartsWith(packageSubpath, + StringPrototypeSlice(key, 0, patternIndex))) { + // When this reaches EOL, this can throw at the top of the whole function: + // + // if (StringPrototypeEndsWith(packageSubpath, '/')) + // throwInvalidSubpath(packageSubpath) + // + // To match "imports" and the spec. + if (StringPrototypeEndsWith(packageSubpath, '/')) + emitTrailingSlashPatternDeprecation(packageSubpath, packageJSONUrl, + base); + const patternTrailer = StringPrototypeSlice(key, patternIndex + 1); + if (packageSubpath.length >= key.length && + StringPrototypeEndsWith(packageSubpath, patternTrailer) && + patternKeyCompare(bestMatch, key) === 1 && + StringPrototypeLastIndexOf(key, '*') === patternIndex) { + bestMatch = key; + bestMatchSubpath = StringPrototypeSlice( + packageSubpath, patternIndex, + packageSubpath.length - patternTrailer.length); + } + } + } + + if (bestMatch) { + const target = exports[bestMatch]; + const resolveResult = resolvePackageTarget( + packageJSONUrl, + target, + bestMatchSubpath, + bestMatch, + base, + true, + false, + StringPrototypeEndsWith(packageSubpath, '/'), + conditions); + + if (resolveResult == null) { + throw exportsNotFound(packageSubpath, packageJSONUrl, base); + } + return resolveResult; + } + + throw exportsNotFound(packageSubpath, packageJSONUrl, base); +} + +function patternKeyCompare(a, b) { + const aPatternIndex = StringPrototypeIndexOf(a, '*'); + const bPatternIndex = StringPrototypeIndexOf(b, '*'); + const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; + const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; + if (baseLenA > baseLenB) return -1; + if (baseLenB > baseLenA) return 1; + if (aPatternIndex === -1) return 1; + if (bPatternIndex === -1) return -1; + if (a.length > b.length) return -1; + if (b.length > a.length) return 1; + return 0; +} + +/** + * @param {string} name + * @param {string | URL | undefined} base + * @param {Set} conditions + * @returns {URL} + */ +function packageImportsResolve(name, base, conditions) { + if (name === '#' || StringPrototypeStartsWith(name, '#/') || + StringPrototypeEndsWith(name, '/')) { + const reason = 'is not a valid internal imports specifier name'; + throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); + } + let packageJSONUrl; + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + const imports = packageConfig.imports; + if (imports) { + if (ObjectPrototypeHasOwnProperty(imports, name) && + !StringPrototypeIncludes(name, '*')) { + const resolveResult = resolvePackageTarget( + packageJSONUrl, imports[name], '', name, base, false, true, false, + conditions + ); + if (resolveResult != null) { + return resolveResult; + } + } else { + let bestMatch = ''; + let bestMatchSubpath; + const keys = ObjectGetOwnPropertyNames(imports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const patternIndex = StringPrototypeIndexOf(key, '*'); + if (patternIndex !== -1 && + StringPrototypeStartsWith(name, + StringPrototypeSlice(key, 0, + patternIndex))) { + const patternTrailer = StringPrototypeSlice(key, patternIndex + 1); + if (name.length >= key.length && + StringPrototypeEndsWith(name, patternTrailer) && + patternKeyCompare(bestMatch, key) === 1 && + StringPrototypeLastIndexOf(key, '*') === patternIndex) { + bestMatch = key; + bestMatchSubpath = StringPrototypeSlice( + name, patternIndex, name.length - patternTrailer.length); + } + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const resolveResult = resolvePackageTarget(packageJSONUrl, target, + bestMatchSubpath, + bestMatch, base, true, + true, false, conditions); + if (resolveResult != null) { + return resolveResult; + } + } + } + } + } + throw importNotDefined(name, packageJSONUrl, base); +} + +/** + * @param {string} specifier + * @param {string | URL | undefined} base + * @returns {{ packageName: string, packageSubpath: string, isScoped: boolean }} + */ +function parsePackageName(specifier, base) { + let separatorIndex = StringPrototypeIndexOf(specifier, '/'); + let validPackageName = true; + let isScoped = false; + if (specifier[0] === '@') { + isScoped = true; + if (separatorIndex === -1 || specifier.length === 0) { + validPackageName = false; + } else { + separatorIndex = StringPrototypeIndexOf( + specifier, '/', separatorIndex + 1); + } + } + + const packageName = separatorIndex === -1 ? + specifier : StringPrototypeSlice(specifier, 0, separatorIndex); + + // Package name cannot have leading . and cannot have percent-encoding or + // \\ separators. + if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) + validPackageName = false; + + if (!validPackageName) { + throw new ERR_INVALID_MODULE_SPECIFIER( + specifier, 'is not a valid package name', fileURLToPath(base)); + } + + const packageSubpath = '.' + (separatorIndex === -1 ? '' : + StringPrototypeSlice(specifier, separatorIndex)); + + return { packageName, packageSubpath, isScoped }; +} + +/** + * @param {string} specifier + * @param {string | URL | undefined} base + * @param {Set} conditions + * @returns {resolved: URL, format? : string} + */ +function packageResolve(specifier, base, conditions) { + if (BuiltinModule.canBeRequiredByUsers(specifier) && + BuiltinModule.canBeRequiredWithoutScheme(specifier)) { + return new URL('node:' + specifier); + } + + const { packageName, packageSubpath, isScoped } = + parsePackageName(specifier, base); + + // ResolveSelf + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && + packageConfig.exports !== undefined && packageConfig.exports !== null) { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions); + } + } + + let packageJSONUrl = + new URL('./node_modules/' + packageName + '/package.json', base); + let packageJSONPath = fileURLToPath(packageJSONUrl); + let lastPath; + do { + const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0, + packageJSONPath.length - 13)); + if (!stat.isDirectory()) { + lastPath = packageJSONPath; + packageJSONUrl = new URL((isScoped ? + '../../../../node_modules/' : '../../../node_modules/') + + packageName + '/package.json', packageJSONUrl); + packageJSONPath = fileURLToPath(packageJSONUrl); + continue; + } + + // Package match. + const packageConfig = getPackageConfig(packageJSONPath, specifier, base); + if (packageConfig.exports !== undefined && packageConfig.exports !== null) { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions); + } + if (packageSubpath === '.') { + return legacyMainResolve( + packageJSONUrl, + packageConfig, + base + ); + } + + return new URL(packageSubpath, packageJSONUrl); + // Cross-platform root check. + } while (packageJSONPath.length !== lastPath.length); + + // eslint can't handle the above code. + // eslint-disable-next-line no-unreachable + throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); +} + +/** + * @param {URL} url + * @returns {PackageType} + */ +function getPackageType(url) { + const packageConfig = getPackageScopeConfig(url); + return packageConfig.type; +} + +/** + * @param {string} mime + * @returns {string | null} + */ +function mimeToFormat(mime) { + if ( + RegExpPrototypeExec( + /\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?/i, + mime + ) !== null + ) return 'module'; + if (mime === 'application/json') return 'json'; + if (getOptionValue('--experimental-wasm-modules') && mime === 'application/wasm') { + return 'wasm'; + } + return null; +} + +const protocolHandlers = { + '__proto__': null, + 'data:': getDataProtocolModuleFormat, + 'file:': getFileProtocolModuleFormat, + 'http:': getHttpProtocolModuleFormat, + 'https:': getHttpProtocolModuleFormat, + 'node:'() { return 'builtin'; }, +}; + +/** + * @param {URL} parsed + * @returns {string | null} + */ +function getDataProtocolModuleFormat(parsed) { + const { 1: mime } = RegExpPrototypeExec( + /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, + parsed.pathname, + ) || [ null, null, null ]; + + return mimeToFormat(mime); +} + +/** + * @param {URL} url + * @param {{parentURL: string}} context + * @param {boolean} ignoreErrors + * @returns {string} + */ +function getFileProtocolModuleFormat(url, context, ignoreErrors) { + const filepath = fileURLToPath(url); + const ext = extname(filepath); + if (ext === '.js') { + return getPackageType(url) === 'module' ? 'module' : 'commonjs'; + } + + const format = getExtensionFormatMap()[ext]; + if (format) return format; + + // Explicit undefined return indicates load hook should rerun format check + if (ignoreErrors) { return undefined; } + let suggestion = ''; + if (getPackageType(url) === 'module' && ext === '') { + const config = getPackageScopeConfig(url); + const fileBasename = basename(filepath); + const relativePath = StringPrototypeSlice(relative(config.pjsonPath, filepath), 1); + suggestion = 'Loading extensionless files is not supported inside of ' + + '"type":"module" package.json contexts. The package.json file ' + + `${config.pjsonPath} caused this "type":"module" context. Try ` + + `changing ${filepath} to have a file extension. Note the "bin" ` + + 'field of package.json can point to a file with an extension, for example ' + + `{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`; + } + throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath, suggestion); +} + +/** + * @param {URL} url + * @param {{parentURL: string}} context + * @returns {Promise | undefined} only works when enabled + */ +function getHttpProtocolModuleFormat(url, context) { + if (getOptionValue('--experimental-network-imports')) { + const { fetchModule } = require('internal/modules/esm/fetch_module'); + return PromisePrototypeThen( + PromiseResolve(fetchModule(url, context)), + (entry) => { + return mimeToFormat(entry.headers['content-type']); + } + ); + } +} + +/** + * @param {URL | URL['href']} url + * @param {{parentURL: string}} context + * @returns {Promise | string | undefined} only works when enabled + */ +function defaultGetFormatWithoutErrors(url, context) { + const parsed = new URL(url); + if (!ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol)) + return null; + return protocolHandlers[parsed.protocol](parsed, context, true); +} + +/** + * @param {URL | URL['href']} url + * @param {{parentURL: string}} context + * @returns {Promise | string | undefined} only works when enabled + */ +function defaultGetFormat(url, context) { + const parsed = new URL(url); + return ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol) ? + protocolHandlers[parsed.protocol](parsed, context, false) : + null; +} + +function isCommonJSGlobalLikeNotDefinedError(errorMessage) { + return ArrayPrototypeSome( + CJSGlobalLike, + (globalLike) => errorMessage === `${globalLike} is not defined` + ); +} + +/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of + * its dependencies, over time. */ +class ModuleJob { + // `loader` is the Loader instance used for loading dependencies. + // `moduleProvider` is a function + constructor(loader, url, importAssertions = ObjectCreate(null), + moduleProvider, isMain, inspectBrk) { + this.loader = loader; + this.importAssertions = importAssertions; + this.isMain = isMain; + this.inspectBrk = inspectBrk; + + this.module = undefined; + // Expose the promise to the ModuleWrap directly for linking below. + // `this.module` is also filled in below. + this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); + + // Wait for the ModuleWrap instance being linked with all dependencies. + const link = async () => { + this.module = await this.modulePromise; + assert(this.module instanceof ModuleWrap); + + // Explicitly keeping track of dependency jobs is needed in order + // to flatten out the dependency graph below in `_instantiate()`, + // so that circular dependencies can't cause a deadlock by two of + // these `link` callbacks depending on each other. + const dependencyJobs = []; + const promises = this.module.link(async (specifier, assertions) => { + const jobPromise = this.loader.getModuleJob(specifier, url, assertions); + ArrayPrototypePush(dependencyJobs, jobPromise); + const job = await jobPromise; + return job.modulePromise; + }); + + if (promises !== undefined) + await SafePromiseAllReturnVoid(promises); + + return SafePromiseAllReturnArrayLike(dependencyJobs); + }; + // Promise for the list of all dependencyJobs. + this.linked = link(); + // This promise is awaited later anyway, so silence + // 'unhandled rejection' warnings. + PromisePrototypeThen(this.linked, undefined, noop); + + // instantiated == deep dependency jobs wrappers are instantiated, + // and module wrapper is instantiated. + this.instantiated = undefined; + } + + instantiate() { + if (this.instantiated === undefined) { + this.instantiated = this._instantiate(); + } + return this.instantiated; + } + + async _instantiate() { + const jobsInGraph = new SafeSet(); + const addJobsToDependencyGraph = async (moduleJob) => { + if (jobsInGraph.has(moduleJob)) { + return; + } + jobsInGraph.add(moduleJob); + const dependencyJobs = await moduleJob.linked; + return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph); + }; + await addJobsToDependencyGraph(this); + + try { + if (!hasPausedEntry && this.inspectBrk) { + hasPausedEntry = true; + const initWrapper = internalBinding('inspector').callAndPauseOnStart; + initWrapper(this.module.instantiate, this.module); + } else { + this.module.instantiate(); + } + } catch (e) { + decorateErrorStack(e); + const { + getSourceMapsEnabled, + } = require('internal/source_map/source_map_cache'); + // TODO(@bcoe): Add source map support to exception that occurs as result + // of missing named export. This is currently not possible because + // stack trace originates in module_job, not the file itself. A hidden + // symbol with filename could be set in node_errors.cc to facilitate this. + if (!getSourceMapsEnabled() && + StringPrototypeIncludes(e.message, + ' does not provide an export named')) { + const splitStack = StringPrototypeSplit(e.stack, '\n'); + const parentFileUrl = RegExpPrototypeSymbolReplace( + /:\d+$/, + splitStack[0], + '' + ); + const { 1: childSpecifier, 2: name } = RegExpPrototypeExec( + /module '(.*)' does not provide an export named '(.+)'/, + e.message); + const { url: childFileURL } = await this.loader.resolve( + childSpecifier, parentFileUrl, + ); + let format; + try { + // This might throw for non-CommonJS modules because we aren't passing + // in the import assertions and some formats require them; but we only + // care about CommonJS for the purposes of this error message. + ({ format } = + await this.loader.load(childFileURL)); + } catch { + // Continue regardless of error. + } + + if (format === 'commonjs') { + const importStatement = splitStack[1]; + // TODO(@ctavan): The original error stack only provides the single + // line which causes the error. For multi-line import statements we + // cannot generate an equivalent object destructuring assignment by + // just parsing the error stack. + const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement); + const destructuringAssignment = oneLineNamedImports && + RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': '); + e.message = `Named export '${name}' not found. The requested module` + + ` '${childSpecifier}' is a CommonJS module, which may not support` + + ' all module.exports as named exports.\nCommonJS modules can ' + + 'always be imported via the default export, for example using:' + + `\n\nimport pkg from '${childSpecifier}';\n${ + destructuringAssignment ? + `const ${destructuringAssignment} = pkg;\n` : ''}`; + const newStack = StringPrototypeSplit(e.stack, '\n'); + newStack[3] = `SyntaxError: ${e.message}`; + e.stack = ArrayPrototypeJoin(newStack, '\n'); + } + } + throw e; + } + + for (const dependencyJob of jobsInGraph) { + // Calling `this.module.instantiate()` instantiates not only the + // ModuleWrap in this module, but all modules in the graph. + dependencyJob.instantiated = resolvedPromise; + } + } + + async run() { + await this.instantiate(); + const timeout = -1; + const breakOnSigint = false; + try { + await this.module.evaluate(timeout, breakOnSigint); + } catch (e) { + if (e?.name === 'ReferenceError' && + isCommonJSGlobalLikeNotDefinedError(e.message)) { + e.message += ' in ES module scope'; + + if (StringPrototypeStartsWith(e.message, 'require ')) { + e.message += ', you can use import instead'; + } + + const packageConfig = + StringPrototypeStartsWith(this.module.url, 'file://') && + RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, this.module.url) !== null && + getPackageScopeConfig(this.module.url); + if (packageConfig.type === 'module') { + e.message += + '\nThis file is being treated as an ES module because it has a ' + + `'.js' file extension and '${packageConfig.pjsonPath}' contains ` + + '"type": "module". To treat it as a CommonJS script, rename it ' + + 'to use the \'.cjs\' file extension.'; + } + } + throw e; + } + return { __proto__: null, module: this.module }; + } +} +ObjectSetPrototypeOf(ModuleJob.prototype, null); + +// Tracks the state of the loader-level module cache +class ModuleMap extends SafeMap { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + get(url, type = kImplicitAssertType) { + validateString(url, 'url'); + validateString(type, 'type'); + return super.get(url)?.[type]; + } + set(url, type = kImplicitAssertType, job) { + validateString(url, 'url'); + validateString(type, 'type'); + + if (job instanceof ModuleJob !== true && + typeof job !== 'function') { + throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job); + } + debug(`Storing ${url} (${ + type === kImplicitAssertType ? 'implicit type' : type + }) in ModuleMap`); + const cachedJobsForUrl = super.get(url) ?? ObjectCreate(null); + cachedJobsForUrl[type] = job; + return super.set(url, cachedJobsForUrl); + } + has(url, type = kImplicitAssertType) { + validateString(url, 'url'); + validateString(type, 'type'); + return super.get(url)?.[type] !== undefined; + } +} + +function initializeImportMetaObject(wrap, meta) { + if (callbackMap.has(wrap)) { + const { initializeImportMeta } = callbackMap.get(wrap); + if (initializeImportMeta !== undefined) { + initializeImportMeta(meta, wrapToModuleMap.get(wrap) || wrap); + } + } +} + +async function importModuleDynamicallyCallback(wrap, specifier, assertions) { + if (callbackMap.has(wrap)) { + const { importModuleDynamically } = callbackMap.get(wrap); + if (importModuleDynamically !== undefined) { + return importModuleDynamically( + specifier, wrapToModuleMap.get(wrap) || wrap, assertions); + } + } + throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); +} + +function initializeESM() { + if (getEmbedderOptions().shouldNotRegisterESMLoader) return; + initializeDefaultConditions(); + setInitializeImportMetaObjectCallback(initializeImportMetaObject); + setImportModuleDynamicallyCallback(importModuleDynamicallyCallback); +} + +module.exports = { + wrapToModuleMap, + setCallbackForWrap, + initializeESM, + ModuleMap, + ModuleJob, + getExtensionFormatMap, + defaultGetFormat, + defaultGetFormatWithoutErrors, + packageExportsResolve, + packageImportsResolve, + encodedSepRegEx, + tryStatSync, + formatTypeMap, + kImplicitAssertType, + packageResolve, + getDefaultConditions, + getConditionsSet, +}; diff --git a/lib/internal/modules/cjs/helpers.js b/lib/internal/modules/helpers.js similarity index 64% rename from lib/internal/modules/cjs/helpers.js rename to lib/internal/modules/helpers.js index fea3fbcf48dc00..bb74892f45fafd 100644 --- a/lib/internal/modules/cjs/helpers.js +++ b/lib/internal/modules/helpers.js @@ -1,17 +1,14 @@ 'use strict'; const { - ArrayPrototypeForEach, ArrayPrototypeJoin, ArrayPrototypeSome, - ObjectDefineProperty, - ObjectPrototypeHasOwnProperty, + ObjectGetPrototypeOf, SafeMap, SafeSet, StringPrototypeCharCodeAt, - StringPrototypeIncludes, StringPrototypeSlice, - StringPrototypeStartsWith, + SyntaxErrorPrototype, } = primordials; const { ERR_MANIFEST_DEPENDENCY_MISSING, @@ -21,26 +18,39 @@ const { BuiltinModule } = require('internal/bootstrap/loaders'); const { validateString } = require('internal/validators'); const path = require('path'); +const { toNamespacedPath } = path; +const { emitWarningSync } = require('internal/process/warning'); +const { readFileSync } = require('fs'); +const { internalModuleReadJSON } = internalBinding('fs'); const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); const { getOptionValue } = require('internal/options'); const { setOwnProperty } = require('internal/util'); -const userConditions = getOptionValue('--conditions'); let debug = require('internal/util/debuglog').debuglog('module', (fn) => { debug = fn; }); -const noAddons = getOptionValue('--no-addons'); -const addonConditions = noAddons ? [] : ['node-addons']; +let cjsConditions; +function initializeCjsConditions() { + const userConditions = getOptionValue('--conditions'); + const noAddons = getOptionValue('--no-addons'); + const addonConditions = noAddons ? [] : ['node-addons']; + // TODO: Use this set when resolving pkg#exports conditions in loader.js. + cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, + ]); +} -// TODO: Use this set when resolving pkg#exports conditions in loader.js. -const cjsConditions = new SafeSet([ - 'require', - 'node', - ...addonConditions, - ...userConditions, -]); +function getCjsConditions() { + if (cjsConditions === undefined) { + initializeCjsConditions(); + } + return cjsConditions; +} function loadBuiltinModule(filename, request) { const mod = BuiltinModule.map.get(filename); @@ -62,7 +72,7 @@ function makeRequireFunction(mod, redirects) { let require; if (redirects) { const id = mod.filename || mod.id; - const conditions = cjsConditions; + const conditions = getCjsConditions(); const { resolve, reaction } = redirects; require = function require(specifier) { let missing = true; @@ -140,65 +150,6 @@ function stripBOM(content) { return content; } -function addBuiltinLibsToObject(object, dummyModuleName) { - // Make built-in modules available directly (loaded lazily). - const Module = require('internal/modules/cjs/loader').Module; - const { builtinModules } = Module; - - // To require built-in modules in user-land and ignore modules whose - // `canBeRequiredByUsers` is false. So we create a dummy module object and not - // use `require()` directly. - const dummyModule = new Module(dummyModuleName); - - ArrayPrototypeForEach(builtinModules, (name) => { - // Neither add underscored modules, nor ones that contain slashes (e.g., - // 'fs/promises') or ones that are already defined. - if (StringPrototypeStartsWith(name, '_') || - StringPrototypeIncludes(name, '/') || - ObjectPrototypeHasOwnProperty(object, name)) { - return; - } - // Goals of this mechanism are: - // - Lazy loading of built-in modules - // - Having all built-in modules available as non-enumerable properties - // - Allowing the user to re-assign these variables as if there were no - // pre-existing globals with the same name. - - const setReal = (val) => { - // Deleting the property before re-assigning it disables the - // getter/setter mechanism. - delete object[name]; - object[name] = val; - }; - - ObjectDefineProperty(object, name, { - __proto__: null, - get: () => { - const lib = dummyModule.require(name); - - try { - // Override the current getter/setter and set up a new - // non-enumerable property. - ObjectDefineProperty(object, name, { - __proto__: null, - get: () => lib, - set: setReal, - configurable: true, - enumerable: false, - }); - } catch { - // If the property is no longer configurable, ignore the error. - } - - return lib; - }, - set: setReal, - configurable: true, - enumerable: false - }); - }); -} - /** * * @param {string | URL} referrer @@ -229,12 +180,63 @@ function hasEsmSyntax(code) { stmt.type === 'ExportAllDeclaration'); } +/** + * @param {Error | any} err + * @param {string} [content] Content of the file, if known. + * @param {string} [filename] Useful only if `content` is unknown. + */ +function enrichCJSError(err, content, filename) { + if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype && + hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) { + // Emit the warning synchronously because we are in the middle of handling + // a SyntaxError that will throw and likely terminate the process before an + // asynchronous warning would be emitted. + emitWarningSync( + 'To load an ES module, set "type": "module" in the package.json or use ' + + 'the .mjs extension.' + ); + } +} + +const packageJSONCache = new SafeMap(); + +let manifest; +/** + * + * @param {string} jsonPath + */ +function readPackageJSON(jsonPath) { + if (packageJSONCache.has(jsonPath)) { + return packageJSONCache.get(jsonPath); + } + + const { 0: string, 1: containsKeys } = internalModuleReadJSON( + toNamespacedPath(jsonPath) + ); + const result = { string, containsKeys }; + if (string !== undefined) { + if (manifest === undefined) { + manifest = getOptionValue('--experimental-policy') ? + require('internal/process/policy').manifest : + null; + } + if (manifest !== null) { + const jsonURL = pathToFileURL(jsonPath); + manifest.assertIntegrity(jsonURL, string); + } + } + packageJSONCache.set(jsonPath, result); + return result; +} + module.exports = { - addBuiltinLibsToObject, - cjsConditions, + getCjsConditions, + initializeCjsConditions, hasEsmSyntax, loadBuiltinModule, makeRequireFunction, normalizeReferrerURL, stripBOM, + enrichCJSError, + readPackageJSON, }; diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js deleted file mode 100644 index 09eb12bd1533bf..00000000000000 --- a/lib/internal/modules/package_json_reader.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const { SafeMap } = primordials; -const { internalModuleReadJSON } = internalBinding('fs'); -const { pathToFileURL } = require('url'); -const { toNamespacedPath } = require('path'); - -const cache = new SafeMap(); - -let manifest; - -/** - * - * @param {string} jsonPath - */ -function read(jsonPath) { - if (cache.has(jsonPath)) { - return cache.get(jsonPath); - } - - const { 0: string, 1: containsKeys } = internalModuleReadJSON( - toNamespacedPath(jsonPath) - ); - const result = { string, containsKeys }; - const { getOptionValue } = require('internal/options'); - if (string !== undefined) { - if (manifest === undefined) { - manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : - null; - } - if (manifest !== null) { - const jsonURL = pathToFileURL(jsonPath); - manifest.assertIntegrity(jsonURL, string); - } - } - cache.set(jsonPath, result); - return result; -} - -module.exports = { read }; diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 738c945bc21c21..163043c8afbb41 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -47,7 +47,7 @@ function shouldUseESMLoader(mainPath) { } function runMainESM(mainPath) { - const { loadESM } = require('internal/process/esm_loader'); + const { loadESM } = require('internal/modules/esm/cascaded_loader'); const { pathToFileURL } = require('internal/url'); handleMainPromise(loadESM((esmLoader) => { @@ -60,7 +60,7 @@ function runMainESM(mainPath) { async function handleMainPromise(promise) { const { handleProcessExit, - } = require('internal/modules/esm/handle_process_exit'); + } = require('internal/process/execution'); process.on('exit', handleProcessExit); try { return await promise; diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 7c17e6f729cc44..77e199303eb456 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -14,7 +14,12 @@ const { ERR_EVAL_ESM_CANNOT_PRINT, }, } = require('internal/errors'); -const { exitCodes: { kGenericUserError } } = internalBinding('errors'); +const { + exitCodes: { + kGenericUserError, + kUnfinishedTopLevelAwait, + }, +} = internalBinding('errors'); const { executionAsyncId, @@ -45,7 +50,7 @@ function evalModule(source, print) { if (print) { throw new ERR_EVAL_ESM_CANNOT_PRINT(); } - const { loadESM } = require('internal/process/esm_loader'); + const { loadESM } = require('internal/modules/esm/cascaded_loader'); const { handleMainPromise } = require('internal/modules/run_main'); RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. return handleMainPromise(loadESM((loader) => loader.eval(source))); @@ -64,9 +69,8 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { module.paths = CJSModule._nodeModulePaths(cwd); const { handleMainPromise } = require('internal/modules/run_main'); - const asyncESM = require('internal/process/esm_loader'); + const { loadESM, getCascadedLoader } = require('internal/modules/esm/cascaded_loader'); const baseUrl = pathToFileURL(module.filename).href; - const { loadESM } = asyncESM; const runScript = () => { // Create wrapper for cache entry @@ -85,7 +89,7 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { displayErrors: true, [kVmBreakFirstLineSymbol]: !!breakFirstLine, importModuleDynamically(specifier, _, importAssertions) { - const loader = asyncESM.esmLoader; + const loader = getCascadedLoader(); return loader.import(specifier, baseUrl, importAssertions); } })); @@ -206,7 +210,15 @@ function readStdin(callback) { }); } +// Handle a Promise from running code that potentially does Top-Level Await. +// In that case, it makes sense to set the exit code to a specific non-zero +// value if the main code never finishes running. +function handleProcessExit() { + process.exitCode ??= kUnfinishedTopLevelAwait; +} + module.exports = { + handleProcessExit, readStdin, tryGetCwd, evalModule, diff --git a/lib/internal/process/per_thread.js b/lib/internal/process/per_thread.js index cf3017d246e65a..0e61ff946f8373 100644 --- a/lib/internal/process/per_thread.js +++ b/lib/internal/process/per_thread.js @@ -179,7 +179,7 @@ function wrapProcessMethods(binding) { function exit(code) { const { handleProcessExit, - } = require('internal/modules/esm/handle_process_exit'); + } = require('internal/process/execution'); process.off('exit', handleProcessExit); if (arguments.length !== 0) { diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index d808f65424c2de..793d6f918a4b5e 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -6,14 +6,12 @@ const { ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, SafeMap, - SafeWeakMap, StringPrototypeStartsWith, globalThis, } = primordials; const { getOptionValue, - getEmbedderOptions, refreshOptions, } = require('internal/options'); const { reconnectZeroFillToggle } = require('internal/buffer'); @@ -22,6 +20,7 @@ const { exposeInterface, exposeLazyInterfaces, defineReplaceableLazyAttribute, + getLazy, } = require('internal/util'); const { @@ -82,6 +81,7 @@ function prepareExecution(options) { initializeSourceMapsHandlers(); initializeDeprecations(); initializeWASI(); + require('internal/dns/utils').initializeDns(); if (isMainThread) { @@ -264,8 +264,10 @@ function setupFetch() { }); // The WebAssembly Web API: https://webassembly.github.io/spec/web-api - const { wasmStreamingCallback } = require('internal/wasm_web_api'); - internalBinding('wasm_web_api').setImplementation(wasmStreamingCallback); + const lazyWasmStreamingCallback = getLazy(() => require('internal/wasm_web_api').wasmStreamingCallback); + internalBinding('wasm_web_api').setImplementation((streamState, source) => { + lazyWasmStreamingCallback()(streamState, source); + }); } // TODO(aduh95): move this to internal/bootstrap/browser when the CLI flag is @@ -338,12 +340,12 @@ function setupStacktracePrinterOnSigint() { } function initializeReport() { - const { report } = require('internal/process/report'); ObjectDefineProperty(process, 'report', { __proto__: null, enumerable: true, configurable: true, get() { + const { report } = require('internal/process/report'); return report; } }); @@ -358,9 +360,10 @@ function setupDebugEnv() { // This has to be called after initializeReport() is called function initializeReportSignalHandlers() { - const { addSignalHandler } = require('internal/process/report'); - - addSignalHandler(); + if (getOptionValue('--report-on-signal')) { + const { addSignalHandler } = require('internal/process/report'); + addSignalHandler(); + } } function initializeHeapSnapshotSignalHandlers() { @@ -561,30 +564,15 @@ function initializeWASI() { } function initializeCJSLoader() { - const CJSLoader = require('internal/modules/cjs/loader'); - if (!getEmbedderOptions().noGlobalSearchPaths) { - CJSLoader.Module._initPaths(); - } - // TODO(joyeecheung): deprecate this in favor of a proper hook? - CJSLoader.Module.runMain = - require('internal/modules/run_main').executeUserEntryPoint; + const { initializeCJS } = require('internal/modules/cjs/loader'); + initializeCJS(); } function initializeESMLoader() { - // Create this WeakMap in js-land because V8 has no C++ API for WeakMap. - internalBinding('module_wrap').callbackMap = new SafeWeakMap(); - - if (getEmbedderOptions().shouldNotRegisterESMLoader) return; - - const { - setImportModuleDynamicallyCallback, - setInitializeImportMetaObjectCallback - } = internalBinding('module_wrap'); - const esm = require('internal/process/esm_loader'); + const { initializeESM } = require('internal/modules/esm/utils'); // Setup per-isolate callbacks that locate data or callbacks that we keep // track of for different ESM modules. - setInitializeImportMetaObjectCallback(esm.initializeImportMetaObject); - setImportModuleDynamicallyCallback(esm.importModuleDynamicallyCallback); + initializeESM(); // Patch the vm module when --experimental-vm-modules is on. // Please update the comments in vm.js when this block changes. diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js index 1eb5c69bd3ff93..2faf4489059b1e 100644 --- a/lib/internal/source_map/source_map_cache.js +++ b/lib/internal/source_map/source_map_cache.js @@ -24,17 +24,19 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => { debug = fn; }); const { getOptionValue } = require('internal/options'); -const { IterableWeakMap } = require('internal/util/iterable_weak_map'); -const { - normalizeReferrerURL, -} = require('internal/modules/cjs/helpers'); + const { validateBoolean } = require('internal/validators'); const { setMaybeCacheGeneratedSourceMap } = internalBinding('errors'); +const { getLazy } = require('internal/util'); // Since the CJS module cache is mutable, which leads to memory leaks when // modules are deleted, we use a WeakMap so that the source map cache will // be purged automatically: -const cjsSourceMapCache = new IterableWeakMap(); +const getCjsSourceMapCache = getLazy(() => { + const { IterableWeakMap } = require('internal/util/iterable_weak_map'); + return new IterableWeakMap(); +}); + // The esm cache is not mutable, so we can use a Map without memory concerns: const esmSourceMapCache = new SafeMap(); // The generated sources is not mutable, so we can use a Map without memory concerns: @@ -44,6 +46,7 @@ const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?[^\s]+)/g; const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); + let SourceMap; let sourceMapsEnabled; @@ -114,6 +117,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo const sourceMapsEnabled = getSourceMapsEnabled(); if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return; try { + const { normalizeReferrerURL } = require('internal/modules/helpers'); filename = normalizeReferrerURL(filename); } catch (err) { // This is most likely an invalid filename in sourceURL of [eval]-wrapper. @@ -137,7 +141,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSo const data = dataFromUrl(filename, sourceMapURL); const url = data ? null : sourceMapURL; if (cjsModuleInstance) { - cjsSourceMapCache.set(cjsModuleInstance, { + getCjsSourceMapCache().set(cjsModuleInstance, { filename, lineLengths: lineLengths(content), data, @@ -291,7 +295,7 @@ function sourceMapCacheToObject() { } function appendCJSCache(obj) { - for (const value of cjsSourceMapCache) { + for (const value of getCjsSourceMapCache()) { obj[ObjectGetValueSafe(value, 'filename')] = { lineLengths: ObjectGetValueSafe(value, 'lineLengths'), data: ObjectGetValueSafe(value, 'data'), @@ -309,7 +313,7 @@ function findSourceMap(sourceURL) { } let sourceMap = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL); if (sourceMap === undefined) { - for (const value of cjsSourceMapCache) { + for (const value of getCjsSourceMapCache()) { const filename = ObjectGetValueSafe(value, 'filename'); const cachedSourceURL = ObjectGetValueSafe(value, 'sourceURL'); if (sourceURL === filename || sourceURL === cachedSourceURL) { diff --git a/lib/internal/util.js b/lib/internal/util.js index fd4a6e3eca3421..ecb88334ef4a82 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -654,7 +654,25 @@ function isArrayBufferDetached(value) { return false; } +/** + * @template T return value of loader + * @param {()=>T} loader + * @returns {()=>T} + */ +function getLazy(loader) { + let value; + let initialized = false; + return function() { + if (initialized === false) { + value = loader(); + initialized = true; + } + return value; + }; +} + module.exports = { + getLazy, assertCrypto, cachedResult, convertToValidSignal, diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js index c7f18ffdb61a33..3a589423b2d280 100644 --- a/lib/internal/util/inspector.js +++ b/lib/internal/util/inspector.js @@ -66,7 +66,7 @@ function installConsoleExtensions(commandLineApi) { if (commandLineApi.require) { return; } const { tryGetCwd } = require('internal/process/execution'); const CJSModule = require('internal/modules/cjs/loader').Module; - const { makeRequireFunction } = require('internal/modules/cjs/helpers'); + const { makeRequireFunction } = require('internal/modules/helpers'); const consoleAPIModule = new CJSModule(''); const cwd = tryGetCwd(); consoleAPIModule.paths = ArrayPrototypeConcat( diff --git a/lib/internal/vm.js b/lib/internal/vm.js index 32bcdf06234b67..a813c55d9e3aad 100644 --- a/lib/internal/vm.js +++ b/lib/internal/vm.js @@ -94,12 +94,11 @@ function internalCompileFunction(code, params, options) { if (importModuleDynamically !== undefined) { validateFunction(importModuleDynamically, 'options.importModuleDynamically'); - const { importModuleDynamicallyWrap } = - require('internal/vm/module'); - const { callbackMap } = internalBinding('module_wrap'); + const { importModuleDynamicallyWrap } = require('internal/vm/module'); const wrapped = importModuleDynamicallyWrap(importModuleDynamically); const func = result.function; - callbackMap.set(result.cacheKey, { + const { setCallbackForWrap } = require('internal/modules/esm/utils'); + setCallbackForWrap(result.cacheKey, { importModuleDynamically: (s, _k, i) => wrapped(s, func, i), }); } diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js index 7e0131c7db2872..91e0b735184f93 100644 --- a/lib/internal/vm/module.js +++ b/lib/internal/vm/module.js @@ -12,7 +12,6 @@ const { ObjectSetPrototypeOf, ReflectApply, SafePromiseAllReturnVoid, - SafeWeakMap, Symbol, SymbolToStringTag, TypeError, @@ -48,7 +47,6 @@ const { validateString, } = require('internal/validators'); -const binding = internalBinding('module_wrap'); const { ModuleWrap, kUninstantiated, @@ -57,7 +55,7 @@ const { kEvaluating, kEvaluated, kErrored, -} = binding; +} = internalBinding('module_wrap'); const STATUS_MAP = { [kUninstantiated]: 'unlinked', @@ -70,7 +68,6 @@ const STATUS_MAP = { let globalModuleId = 0; const defaultModuleName = 'vm:module'; -const wrapToModuleMap = new SafeWeakMap(); const kWrap = Symbol('kWrap'); const kContext = Symbol('kContext'); @@ -125,8 +122,8 @@ class Module { this[kWrap] = new ModuleWrap(identifier, context, sourceText, options.lineOffset, options.columnOffset, options.cachedData); - - binding.callbackMap.set(this[kWrap], { + const { setCallbackForWrap } = require('internal/modules/esm/utils'); + setCallbackForWrap(this[kWrap], { initializeImportMeta: options.initializeImportMeta, importModuleDynamically: options.importModuleDynamically ? importModuleDynamicallyWrap(options.importModuleDynamically) : @@ -138,7 +135,7 @@ class Module { syntheticExportNames, syntheticEvaluationSteps); } - + const { wrapToModuleMap } = require('internal/modules/esm/utils'); wrapToModuleMap.set(this[kWrap], this); this[kContext] = context; @@ -455,5 +452,4 @@ module.exports = { SourceTextModule, SyntheticModule, importModuleDynamicallyWrap, - getModuleFromWrap: (wrap) => wrapToModuleMap.get(wrap), }; diff --git a/lib/repl.js b/lib/repl.js index 0780b5a54743c6..4a112d54f1593f 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -102,8 +102,12 @@ const { const { BuiltinModule } = require('internal/bootstrap/loaders'); const { makeRequireFunction, - addBuiltinLibsToObject -} = require('internal/modules/cjs/helpers'); +} = require('internal/modules/helpers'); +const { + Module: CJSModule, + addBuiltinLibsToObject, +} = require('internal/modules/cjs/loader'); + const { isIdentifierStart, isIdentifierChar @@ -122,7 +126,6 @@ const { commonPrefix } = require('internal/readline/utils'); const { Console } = require('console'); -const CJSModule = require('internal/modules/cjs/loader').Module; let _builtinLibs = ArrayPrototypeFilter( CJSModule.builtinModules, (e) => !StringPrototypeStartsWith(e, '_'), @@ -179,8 +182,8 @@ const { const history = require('internal/repl/history'); const { - extensionFormatMap, -} = require('internal/modules/esm/formats'); + getExtensionFormatMap, +} = require('internal/modules/esm/utils'); let nextREPLResourceNumber = 1; // This prevents v8 code cache from getting confused and using a different @@ -407,7 +410,7 @@ function REPLServer(prompt, } function defaultEval(code, context, file, cb) { - const asyncESM = require('internal/process/esm_loader'); + const { getCascadedLoader } = require('internal/modules/esm/cascaded_loader'); let result, script, wrappedErr; let err = null; @@ -461,8 +464,8 @@ function REPLServer(prompt, filename: file, displayErrors: true, importModuleDynamically: (specifier, _, importAssertions) => { - return asyncESM.esmLoader.import(specifier, parentURL, - importAssertions); + return getCascadedLoader().import(specifier, parentURL, + importAssertions); } }); } catch (fallbackError) { @@ -505,8 +508,8 @@ function REPLServer(prompt, filename: file, displayErrors: true, importModuleDynamically: (specifier, _, importAssertions) => { - return asyncESM.esmLoader.import(specifier, parentURL, - importAssertions); + return getCascadedLoader().import(specifier, parentURL, + importAssertions); } }); } catch (e) { @@ -1376,7 +1379,7 @@ function complete(line, callback) { if (this.allowBlockingCompletions) { const subdir = match[2] || ''; // File extensions that can be imported: - const extensions = ObjectKeys(extensionFormatMap); + const extensions = ObjectKeys(getExtensionFormatMap()); // Only used when loading bare module specifiers from `node_modules`: const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`); diff --git a/lib/vm.js b/lib/vm.js index ec91c8a2651775..dea012156628b6 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -111,10 +111,9 @@ class Script extends ContextifyScript { if (importModuleDynamically !== undefined) { validateFunction(importModuleDynamically, 'options.importModuleDynamically'); - const { importModuleDynamicallyWrap } = - require('internal/vm/module'); - const { callbackMap } = internalBinding('module_wrap'); - callbackMap.set(this, { + const { importModuleDynamicallyWrap } = require('internal/vm/module'); + const { setCallbackForWrap } = require('internal/modules/esm/utils'); + setCallbackForWrap(this, { importModuleDynamically: importModuleDynamicallyWrap(importModuleDynamically), }); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 73ce4aa42035a1..ca393109dc535e 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -4,6 +4,7 @@ #include "memory_tracker-inl.h" #include "node_contextify.h" #include "node_errors.h" +#include "node_external_reference.h" #include "node_internals.h" #include "node_process-inl.h" #include "node_url.h" @@ -811,8 +812,27 @@ void ModuleWrap::Initialize(Local target, #undef V } +void ModuleWrap::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + + registry->Register(Link); + registry->Register(Instantiate); + registry->Register(Evaluate); + registry->Register(SetSyntheticExport); + registry->Register(CreateCachedData); + registry->Register(GetNamespace); + registry->Register(GetStatus); + registry->Register(GetError); + registry->Register(GetStaticDependencySpecifiers); + + registry->Register(SetImportModuleDynamicallyCallback); + registry->Register(SetInitializeImportMetaObjectCallback); +} } // namespace loader } // namespace node NODE_BINDING_CONTEXT_AWARE_INTERNAL(module_wrap, node::loader::ModuleWrap::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(module_wrap, + node::loader::ModuleWrap::RegisterExternalReferences) diff --git a/src/module_wrap.h b/src/module_wrap.h index 58b233d036515c..c609ba5509dcd0 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -11,6 +11,7 @@ namespace node { class Environment; +class ExternalReferenceRegistry; namespace contextify { class ContextifyContext; @@ -44,6 +45,7 @@ class ModuleWrap : public BaseObject { v8::Local unused, v8::Local context, void* priv); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static void HostInitializeImportMetaObjectCallback( v8::Local context, v8::Local module, diff --git a/src/node_external_reference.h b/src/node_external_reference.h index bf4b49670de310..c3ab57c0bb0f98 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -74,6 +74,7 @@ class ExternalReferenceRegistry { V(heap_utils) \ V(messaging) \ V(mksnapshot) \ + V(module_wrap) \ V(options) \ V(os) \ V(performance) \ diff --git a/test/es-module/test-esm-import-assertion-validation.js b/test/es-module/test-esm-import-assertion-validation.js index 3792ad7ff1617c..ef4b9d95171bc3 100644 --- a/test/es-module/test-esm-import-assertion-validation.js +++ b/test/es-module/test-esm-import-assertion-validation.js @@ -4,7 +4,7 @@ require('../common'); const assert = require('assert'); -const { validateAssertions } = require('internal/modules/esm/assert'); +const { validateAssertions } = require('internal/modules/esm/load'); const url = 'test://'; diff --git a/test/es-module/test-esm-loader-modulemap.js b/test/es-module/test-esm-loader-modulemap.js index 190676ec725cd2..a108f437adba34 100644 --- a/test/es-module/test-esm-loader-modulemap.js +++ b/test/es-module/test-esm-loader-modulemap.js @@ -4,11 +4,12 @@ require('../common'); const { strictEqual, throws } = require('assert'); -const { ESMLoader } = require('internal/modules/esm/loader'); -const ModuleMap = require('internal/modules/esm/module_map'); -const ModuleJob = require('internal/modules/esm/module_job'); -const createDynamicModule = require( - 'internal/modules/esm/create_dynamic_module'); +const { + ESMLoader, + createDynamicModule, +} = require('internal/modules/esm/loader'); +const { ModuleMap } = require('internal/modules/esm/utils'); +const { ModuleJob } = require('internal/modules/esm/utils'); const jsModuleDataUrl = 'data:text/javascript,export{}'; const jsonModuleDataUrl = 'data:application/json,""'; diff --git a/test/fixtures/inspector-global-function.js b/test/fixtures/inspector-global-function.mjs similarity index 100% rename from test/fixtures/inspector-global-function.js rename to test/fixtures/inspector-global-function.mjs diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index d3111513424182..5ba8ac0097fb9f 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -25,7 +25,6 @@ const expectedModules = new Set([ 'Internal Binding options', 'Internal Binding performance', 'Internal Binding process_methods', - 'Internal Binding report', 'Internal Binding string_decoder', 'Internal Binding symbols', 'Internal Binding task_queue', @@ -53,30 +52,18 @@ const expectedModules = new Set([ 'NativeModule internal/fs/utils', 'NativeModule internal/idna', 'NativeModule internal/linkedlist', - 'NativeModule internal/modules/cjs/helpers', 'NativeModule internal/modules/cjs/loader', - 'NativeModule internal/modules/esm/assert', - 'NativeModule internal/modules/esm/formats', - 'NativeModule internal/modules/esm/get_format', - 'NativeModule internal/modules/esm/initialize_import_meta', - 'NativeModule internal/modules/esm/load', - 'NativeModule internal/modules/esm/loader', - 'NativeModule internal/modules/esm/module_map', - 'NativeModule internal/modules/esm/package_config', - 'NativeModule internal/modules/esm/resolve', - 'NativeModule internal/modules/esm/translators', - 'NativeModule internal/modules/package_json_reader', + 'NativeModule internal/modules/esm/utils', + 'NativeModule internal/modules/helpers', 'NativeModule internal/modules/run_main', 'NativeModule internal/net', 'NativeModule internal/options', 'NativeModule internal/perf/utils', 'NativeModule internal/priority_queue', - 'NativeModule internal/process/esm_loader', 'NativeModule internal/process/execution', 'NativeModule internal/process/per_thread', 'NativeModule internal/process/pre_execution', 'NativeModule internal/process/promises', - 'NativeModule internal/process/report', 'NativeModule internal/process/signal', 'NativeModule internal/process/task_queues', 'NativeModule internal/process/warning', @@ -87,28 +74,21 @@ const expectedModules = new Set([ 'NativeModule internal/util', 'NativeModule internal/util/debuglog', 'NativeModule internal/util/inspect', - 'NativeModule internal/util/iterable_weak_map', 'NativeModule internal/util/types', 'NativeModule internal/v8/startup_snapshot', 'NativeModule internal/validators', 'NativeModule internal/vm', 'NativeModule internal/vm/module', - 'NativeModule internal/wasm_web_api', 'NativeModule internal/worker/js_transferable', 'NativeModule path', 'NativeModule querystring', 'NativeModule timers', - 'NativeModule url', 'NativeModule util', - 'NativeModule vm', + 'NativeModule url', ]); if (!common.isMainThread) { [ - 'Internal Binding messaging', - 'Internal Binding performance', - 'Internal Binding symbols', - 'Internal Binding worker', 'NativeModule diagnostics_channel', 'NativeModule internal/abort_controller', 'NativeModule internal/error_serdes', diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 57f8dd064005c6..2e0d726d3dcecb 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2832,7 +2832,7 @@ assert.strictEqual( ' at Function.Module._load (node:internal/modules/cjs/loader:621:3)', // This file is not an actual Node.js core file. ' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)', - ' at require (node:internal/modules/cjs/helpers:14:16)', + ' at require (node:internal/modules/helpers:14:16)', ' at Array.forEach ()', ` at ${process.cwd()}/test/parallel/test-util-inspect.js:2760:12`, ` at Object. (${process.cwd()}/node_modules/hyper_module/folder/file.js:2753:10)`, diff --git a/test/sequential/test-inspector-break-when-eval.js b/test/sequential/test-inspector-break-when-eval.js index 1e7ab513dadbbb..0841aba057abbb 100644 --- a/test/sequential/test-inspector-break-when-eval.js +++ b/test/sequential/test-inspector-break-when-eval.js @@ -6,7 +6,9 @@ const { NodeInstance } = require('../common/inspector-helper.js'); const fixtures = require('../common/fixtures'); const { pathToFileURL } = require('url'); -const script = fixtures.path('inspector-global-function.js'); +// This needs to be a ESM to work around +// https://bugs.chromium.org/p/chromium/issues/detail?id=1246905 +const script = fixtures.path('inspector-global-function.mjs'); async function setupDebugger(session) { console.log('[test]', 'Setting up a debugger'); From 4959b49bb3136f3736e1d011252bc5c62b47bb1c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 12 Dec 2022 16:04:33 +0100 Subject: [PATCH 2/5] snapshot pre-execution --- lib/internal/bootstrap/browser.js | 3 --- lib/internal/bootstrap/switches/is_main_thread.js | 4 ++++ ...ector-global-function.mjs => inspector-global-function.js} | 0 test/sequential/test-inspector-break-when-eval.js | 4 +--- 4 files changed, 5 insertions(+), 6 deletions(-) rename test/fixtures/{inspector-global-function.mjs => inspector-global-function.js} (100%) diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js index c34b9c1ed4641d..b94ac9891a399c 100644 --- a/lib/internal/bootstrap/browser.js +++ b/lib/internal/bootstrap/browser.js @@ -47,9 +47,6 @@ const { } = require('internal/event_target'); exposeInterface(globalThis, 'Event', Event); exposeInterface(globalThis, 'EventTarget', EventTarget); -// exposeLazyInterfaces(globalThis, 'internal/event_target', [ -// 'EventTarget', 'Event', -// ]); exposeLazyInterfaces(globalThis, 'internal/worker/io', [ 'MessageChannel', 'MessagePort', 'MessageEvent', ]); diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index fc2078e3e46312..0c81043dffa8cd 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -311,3 +311,7 @@ require('internal/source_map/source_map_cache'); require('internal/modules/run_main'); // Needed to refresh DNS configurations. require('internal/dns/utils'); +// Needed by almost all execution modes. It's fine to +// load them into the snapshot as long as we don't run +// any of the initialization. +require('internal/process/pre_execution'); diff --git a/test/fixtures/inspector-global-function.mjs b/test/fixtures/inspector-global-function.js similarity index 100% rename from test/fixtures/inspector-global-function.mjs rename to test/fixtures/inspector-global-function.js diff --git a/test/sequential/test-inspector-break-when-eval.js b/test/sequential/test-inspector-break-when-eval.js index 0841aba057abbb..1e7ab513dadbbb 100644 --- a/test/sequential/test-inspector-break-when-eval.js +++ b/test/sequential/test-inspector-break-when-eval.js @@ -6,9 +6,7 @@ const { NodeInstance } = require('../common/inspector-helper.js'); const fixtures = require('../common/fixtures'); const { pathToFileURL } = require('url'); -// This needs to be a ESM to work around -// https://bugs.chromium.org/p/chromium/issues/detail?id=1246905 -const script = fixtures.path('inspector-global-function.mjs'); +const script = fixtures.path('inspector-global-function.js'); async function setupDebugger(session) { console.log('[test]', 'Setting up a debugger'); From 20391fb7994003e3df41d7ae0b45a62545aa776c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 12 Dec 2022 19:31:09 +0100 Subject: [PATCH 3/5] work around race from v8 bug --- ...ector-global-function.js => inspector-global-function.mjs} | 0 test/sequential/test-inspector-break-when-eval.js | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) rename test/fixtures/{inspector-global-function.js => inspector-global-function.mjs} (100%) diff --git a/test/fixtures/inspector-global-function.js b/test/fixtures/inspector-global-function.mjs similarity index 100% rename from test/fixtures/inspector-global-function.js rename to test/fixtures/inspector-global-function.mjs diff --git a/test/sequential/test-inspector-break-when-eval.js b/test/sequential/test-inspector-break-when-eval.js index 1e7ab513dadbbb..0841aba057abbb 100644 --- a/test/sequential/test-inspector-break-when-eval.js +++ b/test/sequential/test-inspector-break-when-eval.js @@ -6,7 +6,9 @@ const { NodeInstance } = require('../common/inspector-helper.js'); const fixtures = require('../common/fixtures'); const { pathToFileURL } = require('url'); -const script = fixtures.path('inspector-global-function.js'); +// This needs to be a ESM to work around +// https://bugs.chromium.org/p/chromium/issues/detail?id=1246905 +const script = fixtures.path('inspector-global-function.mjs'); async function setupDebugger(session) { console.log('[test]', 'Setting up a debugger'); From 8f999c84923d69613fbb06ed632f60e35d15dba3 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 12 Dec 2022 19:33:10 +0100 Subject: [PATCH 4/5] fix lint --- src/module_wrap.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module_wrap.cc b/src/module_wrap.cc index ca393109dc535e..d6c766244fd416 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -834,5 +834,5 @@ void ModuleWrap::RegisterExternalReferences( NODE_BINDING_CONTEXT_AWARE_INTERNAL(module_wrap, node::loader::ModuleWrap::Initialize) -NODE_BINDING_EXTERNAL_REFERENCE(module_wrap, - node::loader::ModuleWrap::RegisterExternalReferences) +NODE_BINDING_EXTERNAL_REFERENCE( + module_wrap, node::loader::ModuleWrap::RegisterExternalReferences) From e46cb9db9220595ce42212536ad600e4cbc11b92 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 13 Dec 2022 15:10:45 +0100 Subject: [PATCH 5/5] fix bootstrap modules test --- lib/internal/bootstrap/switches/is_main_thread.js | 1 - lib/internal/modules/helpers.js | 2 +- test/parallel/test-bootstrap-modules.js | 5 +++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index 0c81043dffa8cd..58e2f0b39db73c 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -288,7 +288,6 @@ rawMethods.resetStdioForTesting = function() { }; // Needed by the module loader and generally needed everywhere. -require('url'); require('fs'); require('util'); diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index bb74892f45fafd..59ccd449c1247a 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -22,7 +22,7 @@ const { toNamespacedPath } = path; const { emitWarningSync } = require('internal/process/warning'); const { readFileSync } = require('fs'); const { internalModuleReadJSON } = internalBinding('fs'); -const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); +const { pathToFileURL, fileURLToPath, URL } = require('url'); const { getOptionValue } = require('internal/options'); const { setOwnProperty } = require('internal/util'); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 5ba8ac0097fb9f..5c9cf13455fe7f 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -119,6 +119,11 @@ if (!common.isMainThread) { ].forEach(expectedModules.add.bind(expectedModules)); } +if (common.isWindows) { + // On Windows fs needs SideEffectFreeRegExpPrototypeExec which uses vm. + expectedModules.add('NativeModule vm'); +} + if (common.hasIntl) { expectedModules.add('Internal Binding icu'); } else {