From 9e6da5b842631b776c1890fb4d267a47733d6ab7 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 6 Feb 2026 18:21:01 +0100 Subject: [PATCH 1/3] lib: remove top-level getOptionValue() calls in lib/internal/modules --- lib/internal/modules/esm/formats.js | 70 ------------------------- lib/internal/modules/esm/get_format.js | 64 +++++++++++++++++++--- lib/internal/modules/esm/resolve.js | 7 +-- lib/internal/process/pre_execution.js | 3 ++ lib/internal/repl/completion.js | 2 +- test/parallel/test-bootstrap-modules.js | 1 + 6 files changed, 65 insertions(+), 82 deletions(-) delete mode 100644 lib/internal/modules/esm/formats.js diff --git a/lib/internal/modules/esm/formats.js b/lib/internal/modules/esm/formats.js deleted file mode 100644 index 6a18bd992fa1d9..00000000000000 --- a/lib/internal/modules/esm/formats.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -const { - RegExpPrototypeExec, -} = primordials; - -const { getOptionValue } = require('internal/options'); -const { getValidatedPath } = require('internal/fs/utils'); -const fsBindings = internalBinding('fs'); -const { internal: internalConstants } = internalBinding('constants'); - -const experimentalAddonModules = getOptionValue('--experimental-addon-modules'); - -const extensionFormatMap = { - '__proto__': null, - '.cjs': 'commonjs', - '.js': 'module', - '.json': 'json', - '.mjs': 'module', - '.wasm': 'wasm', -}; - -if (experimentalAddonModules) { - extensionFormatMap['.node'] = 'addon'; -} - -if (getOptionValue('--strip-types')) { - extensionFormatMap['.ts'] = 'module-typescript'; - extensionFormatMap['.mts'] = 'module-typescript'; - extensionFormatMap['.cts'] = 'commonjs-typescript'; -} - -/** - * @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 (mime === 'application/wasm') { return 'wasm'; } - return null; -} - -/** - * For extensionless files in a `module` package scope, we check the file contents to disambiguate between ES module - * JavaScript and Wasm. - * We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`). - * @param {URL} url - * @returns {'wasm'|'module'} - */ -function getFormatOfExtensionlessFile(url) { - const path = getValidatedPath(url); - switch (fsBindings.getFormatOfExtensionlessFile(path)) { - case internalConstants.EXTENSIONLESS_FORMAT_WASM: - return 'wasm'; - default: - return 'module'; - } -} - -module.exports = { - extensionFormatMap, - getFormatOfExtensionlessFile, - mimeToFormat, -}; diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 48ccb97a6244ea..4f334c7d88c336 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -9,13 +9,63 @@ const { StringPrototypeSlice, } = primordials; const { getOptionValue } = require('internal/options'); -const { - extensionFormatMap, - getFormatOfExtensionlessFile, - mimeToFormat, -} = require('internal/modules/esm/formats'); +const { getValidatedPath } = require('internal/fs/utils'); +const fsBindings = internalBinding('fs'); +const { internal: internalConstants } = internalBinding('constants'); + +const extensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'module', + '.json': 'json', + '.mjs': 'module', + '.wasm': 'wasm', +}; + +function initializeExtensionFormatMap() { + if (getOptionValue('--experimental-addon-modules')) { + extensionFormatMap['.node'] = 'addon'; + } + + if (getOptionValue('--strip-types')) { + extensionFormatMap['.ts'] = 'module-typescript'; + extensionFormatMap['.mts'] = 'module-typescript'; + extensionFormatMap['.cts'] = 'commonjs-typescript'; + } +} -const detectModule = getOptionValue('--experimental-detect-module'); +/** + * @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 (mime === 'application/wasm') { return 'wasm'; } + return null; +} + +/** + * For extensionless files in a `module` package scope, we check the file contents to disambiguate between ES module + * JavaScript and Wasm. + * We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`). + * @param {URL} url + * @returns {'wasm'|'module'} + */ +function getFormatOfExtensionlessFile(url) { + const path = getValidatedPath(url); + switch (fsBindings.getFormatOfExtensionlessFile(path)) { + case internalConstants.EXTENSIONLESS_FORMAT_WASM: + return 'wasm'; + default: + return 'module'; + } +} const { containsModuleSyntax } = internalBinding('contextify'); const { getPackageScopeConfig, getPackageType } = require('internal/modules/package_json_reader'); const { fileURLToPath } = require('internal/url'); @@ -35,6 +85,7 @@ const protocolHandlers = { * @returns {'module'|'commonjs'} */ function detectModuleFormat(source, url) { + const detectModule = getOptionValue('--experimental-detect-module'); if (!source) { return detectModule ? null : 'commonjs'; } if (!detectModule) { return 'commonjs'; } return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs'; @@ -216,4 +267,5 @@ module.exports = { defaultGetFormatWithoutErrors, extensionFormatMap, extname, + initializeExtensionFormatMap, }; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index cc1230648881d8..473398bf29bcdf 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -29,9 +29,6 @@ const { realpathSync } = require('fs'); const { getOptionValue } = require('internal/options'); // Do not eagerly grab .manifest, it may be in TDZ const { sep, posix: { relative: relativePosixPath }, resolve } = require('path'); -const preserveSymlinks = getOptionValue('--preserve-symlinks'); -const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); -const inputTypeFlag = getOptionValue('--input-type'); const { URL, pathToFileURL, fileURLToPath, isURL, URLParse } = require('internal/url'); const { getCWDURL, setOwnProperty } = require('internal/util'); const { canParse: URLCanParse } = internalBinding('url'); @@ -982,7 +979,7 @@ 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 (inputTypeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } + if (getOptionValue('--input-type')) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } } conditions = getConditionsSet(conditions); @@ -992,7 +989,7 @@ 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 diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 0902536708bf1d..ebb44b935e1b58 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -161,6 +161,9 @@ function prepareExecution(options) { assert(!initializeModules); } + const { initializeExtensionFormatMap } = require('internal/modules/esm/get_format'); + initializeExtensionFormatMap(); + setupVmModules(); if (initializeModules) { initializeModuleLoaders({ shouldSpawnLoaderHookWorker, shouldPreloadModules }); diff --git a/lib/internal/repl/completion.js b/lib/internal/repl/completion.js index a16b7af0d1732c..8587e5e5b2333b 100644 --- a/lib/internal/repl/completion.js +++ b/lib/internal/repl/completion.js @@ -48,7 +48,7 @@ const CJSModule = require('internal/modules/cjs/loader').Module; const { extensionFormatMap, -} = require('internal/modules/esm/formats'); +} = require('internal/modules/esm/get_format'); const path = require('path'); const fs = require('fs'); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index ae96258e95d89d..4c8be110e065b0 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -118,6 +118,7 @@ expected.beforePreExec = new Set([ ]); expected.atRunTime = new Set([ + 'NativeModule internal/modules/esm/get_format', 'NativeModule internal/process/pre_execution', ]); From 0659ac3b1b6981236b5e456643f3a3f9f16302c1 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 6 Feb 2026 18:41:49 +0100 Subject: [PATCH 2/3] lib: reduce cycles in esm loader and load it in snapshot --- .../bootstrap/switches/is_main_thread.js | 1 + lib/internal/modules/esm/loader.js | 30 ++++--------------- lib/internal/modules/esm/resolve.js | 9 ++---- lib/internal/modules/esm/translators.js | 17 ++++------- test/parallel/test-bootstrap-modules.js | 8 ++++- 5 files changed, 21 insertions(+), 44 deletions(-) diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index 74486ba5310b78..06ac27c8486963 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -296,6 +296,7 @@ require('util'); require('url'); // eslint-disable-line no-restricted-modules internalBinding('module_wrap'); require('internal/modules/cjs/loader'); +require('internal/modules/esm/loader'); require('internal/modules/esm/utils'); // Needed to refresh the time origin. diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 41513d1b9f3658..37eb267e154cc7 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -65,8 +65,6 @@ const { validateLoadSloppy, } = require('internal/modules/customization_hooks'); -let defaultResolve, defaultLoadSync; - const { tracingChannel } = require('diagnostics_channel'); const onImport = tracingChannel('module.import'); @@ -100,19 +98,9 @@ function newLoadCache() { return new LoadCache(); } -let _translators; -function lazyLoadTranslators() { - _translators ??= require('internal/modules/esm/translators'); - return _translators; -} - -/** - * Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used). - * @returns {import('./translators.js').Translators} - */ -function getTranslators() { - return lazyLoadTranslators().translators; -} +const { translators } = require('internal/modules/esm/translators'); +const { defaultResolve } = require('internal/modules/esm/resolve'); +const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/esm/load'); /** * Generate message about potential race condition caused by requiring a cached module that has started @@ -181,11 +169,6 @@ class ModuleLoader { */ loadCache = newLoadCache(); - /** - * Methods which translate input code or other information into ES modules - */ - translators = getTranslators(); - /** * @see {AsyncLoaderHooks.isForAsyncLoaderHookWorker} * Shortcut to this.#asyncLoaderHooks.isForAsyncLoaderHookWorker. @@ -459,7 +442,7 @@ class ModuleLoader { #translate(url, translateContext, parentURL) { const { translatorKey, format } = translateContext; this.validateLoadResult(url, format); - const translator = getTranslators().get(translatorKey); + const translator = translators.get(translatorKey); if (!translator) { throw new ERR_UNKNOWN_MODULE_FORMAT(translatorKey, url); @@ -710,7 +693,7 @@ class ModuleLoader { if (cachedResult != null) { return cachedResult; } - defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; + const result = defaultResolve(specifier, context); this.#resolveCache.set(requestKey, parentURL, result); return result; @@ -787,7 +770,6 @@ class ModuleLoader { if (this.#asyncLoaderHooks?.loadSync) { return this.#asyncLoaderHooks.loadSync(url, context); } - defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync; return defaultLoadSync(url, context); } @@ -813,7 +795,7 @@ class ModuleLoader { validateLoadResult(url, format) { if (format == null) { - require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); + throwUnknownModuleFormat(url, format); } } diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 473398bf29bcdf..d253a3ff67280c 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -46,8 +46,7 @@ const { ERR_UNSUPPORTED_DIR_IMPORT, ERR_UNSUPPORTED_RESOLVE_REQUEST, } = require('internal/errors').codes; - -const { Module: CJSModule } = require('internal/modules/cjs/loader'); +const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format'); const { getConditionsSet } = require('internal/modules/esm/utils'); const packageJsonReader = require('internal/modules/package_json_reader'); const internalFsBinding = internalBinding('fs'); @@ -870,6 +869,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); @@ -1043,8 +1043,3 @@ module.exports = { packageResolve, throwIfInvalidParentURL, }; - -// cycle -const { - defaultGetFormatWithoutErrors, -} = require('internal/modules/esm/get_format'); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index f45defe9dad88a..8cf008d52ab380 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -13,7 +13,7 @@ const { StringPrototypeReplaceAll, StringPrototypeSlice, StringPrototypeStartsWith, - globalThis: { WebAssembly }, + globalThis, } = primordials; const { @@ -57,16 +57,7 @@ const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); const moduleWrap = internalBinding('module_wrap'); const { ModuleWrap, kEvaluationPhase } = moduleWrap; -// Lazy-loading to avoid circular dependencies. -let getSourceSync; -/** - * @param {Parameters[0]} url - * @returns {ReturnType} - */ -function getSource(url) { - getSourceSync ??= require('internal/modules/esm/load').getSourceSync; - return getSourceSync(url); -} +const { getSourceSync } = require('internal/modules/esm/load'); const { parse: cjsParse } = internalBinding('cjs_lexer'); @@ -210,7 +201,7 @@ function createCJSModuleWrap(url, translateContext, parentURL, loadCJS = loadCJS const isMain = (parentURL === undefined); const filename = urlToFilename(url); // In case the source was not provided by the `load` step, we need fetch it now. - source = stringify(source ?? getSource(new URL(url)).source); + source = stringify(source ?? getSourceSync(new URL(url)).source); const { exportNames, module } = cjsPreparseModuleExports(filename, source, sourceFormat); cjsCache.set(url, module); @@ -516,6 +507,8 @@ translators.set('json', function jsonStrategy(url, translateContext) { const wasmInstances = new SafeWeakMap(); translators.set('wasm', function(url, translateContext) { const { source } = translateContext; + // WebAssembly global is not available during snapshot building, so we need to get it lazily. + const { WebAssembly } = globalThis; assertBufferSource(source, false, 'load'); debug(`Translating WASMModule ${url}`, translateContext); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 4c8be110e065b0..a16573124d733f 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -115,10 +115,16 @@ expected.beforePreExec = new Set([ 'NativeModule internal/modules/run_main', 'NativeModule internal/net', 'NativeModule internal/dns/utils', + 'NativeModule internal/modules/esm/assert', + 'NativeModule internal/modules/esm/loader', + 'Internal Binding cjs_lexer', + 'NativeModule internal/modules/esm/get_format', + 'NativeModule internal/modules/esm/load', + 'NativeModule internal/modules/esm/resolve', + 'NativeModule internal/modules/esm/translators', ]); expected.atRunTime = new Set([ - 'NativeModule internal/modules/esm/get_format', 'NativeModule internal/process/pre_execution', ]); From 71c535af26fdbd8691d839cebd94723a1396b4c8 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 6 Feb 2026 19:28:52 +0100 Subject: [PATCH 3/3] benchmark: add startup benchmark for ESM entrypoint --- benchmark/fixtures/empty.mjs | 0 benchmark/fixtures/import-builtins.mjs | 34 ++++++++++++++++++++++++++ benchmark/misc/startup-core.js | 10 +++++--- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 benchmark/fixtures/empty.mjs create mode 100644 benchmark/fixtures/import-builtins.mjs diff --git a/benchmark/fixtures/empty.mjs b/benchmark/fixtures/empty.mjs new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/benchmark/fixtures/import-builtins.mjs b/benchmark/fixtures/import-builtins.mjs new file mode 100644 index 00000000000000..03feaa4887c362 --- /dev/null +++ b/benchmark/fixtures/import-builtins.mjs @@ -0,0 +1,34 @@ +import 'node:async_hooks'; +import 'node:assert'; +import 'node:buffer'; +import 'node:child_process'; +import 'node:console'; +import 'node:constants'; +import 'node:crypto'; +import 'node:cluster'; +import 'node:dgram'; +import 'node:dns'; +import 'node:domain'; +import 'node:events'; +import 'node:fs'; +import 'node:http'; +import 'node:http2'; +import 'node:https'; +import 'node:module'; +import 'node:net'; +import 'node:os'; +import 'node:path'; +import 'node:perf_hooks'; +import 'node:process'; +import 'node:querystring'; +import 'node:readline'; +import 'node:repl'; +import 'node:stream'; +import 'node:string_decoder'; +import 'node:timers'; +import 'node:tls'; +import 'node:tty'; +import 'node:url'; +import 'node:util'; +import 'node:vm'; +import 'node:zlib'; diff --git a/benchmark/misc/startup-core.js b/benchmark/misc/startup-core.js index 053a1ec0cbff8f..414b00176ad2c2 100644 --- a/benchmark/misc/startup-core.js +++ b/benchmark/misc/startup-core.js @@ -7,9 +7,11 @@ let Worker; // Lazy loaded in main const bench = common.createBenchmark(main, { script: [ - 'benchmark/fixtures/require-builtins', - 'test/fixtures/semicolon', - 'test/fixtures/snapshot/typescript', + 'benchmark/fixtures/empty.mjs', + 'benchmark/fixtures/import-builtins.mjs', + 'benchmark/fixtures/require-builtins.js', + 'test/fixtures/semicolon.js', + 'test/fixtures/snapshot/typescript.js', ], mode: ['process', 'worker'], n: [30], @@ -58,7 +60,7 @@ function spawnWorker(script, bench, state) { } function main({ n, script, mode }) { - script = path.resolve(__dirname, '../../', `${script}.js`); + script = path.resolve(__dirname, '../../', `${script}`); const warmup = 3; const state = { n, finished: -warmup }; if (mode === 'worker') {