From b2e6b826f43e85a3f302b42caf8e0560972c62bd Mon Sep 17 00:00:00 2001 From: sjungwon03 Date: Fri, 25 Sep 2026 02:09:30 +0900 Subject: [PATCH] module: centralize builtin exposure policies Keep scheme-only and option-gated builtin exposure rules in the JavaScript loader. Leave option registration and code-cache categorization with their native owners. Assisted-by: Codex Signed-off-by: sjungwon03 --- lib/internal/bootstrap/realm.js | 76 ++++++---- lib/internal/process/pre_execution.js | 72 +-------- lib/internal/test/binding.js | 8 +- test/parallel/test-module-builtin-policies.js | 143 ++++++++++++++++++ 4 files changed, 200 insertions(+), 99 deletions(-) create mode 100644 test/parallel/test-module-builtin-policies.js diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 0be0998e7ee..3e3473b000c 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -120,34 +120,6 @@ const legacyWrapperList = new SafeSet([ 'util', ]); -// The code below assumes that the two lists must not contain any modules -// beginning with "internal/". -// Modules that can only be imported via the node: scheme. -const schemelessBlockList = new SafeSet([ - 'bench', - 'bench/reporters', - 'dtls', - 'ffi', - 'sea', - 'sqlite', - 'quic', - 'test', - 'test/reporters', - 'vfs', -]); -// Modules that will only be enabled at run time. -const experimentalModuleList = new SafeSet([ - 'bench', - 'bench/reporters', - 'dtls', - 'ffi', - 'quic', - 'sqlite', - 'stream/iter', - 'vfs', - 'zlib/iter', -]); - // Set up process.binding() and process._linkedBinding(). { const bindingObj = { __proto__: null }; @@ -220,10 +192,41 @@ const getOwn = (target, property, receiver) => { undefined; }; +// Public builtin exposure policies. Each entry is [id, schemeOnly, option]. +// A null option means that the builtin is not gated by a runtime option. +// Do not include internal modules. Runtime option definitions and code-cache +// categories are owned by their respective native subsystems. +const builtinModulePolicies = [ + ['bench', true, '--experimental-bench'], + ['bench/reporters', true, '--experimental-bench'], + ['dtls', true, '--experimental-dtls'], + ['ffi', true, '--experimental-ffi'], + ['sea', true, null], + ['sqlite', true, '--experimental-sqlite'], + ['quic', true, '--experimental-quic'], + ['stream/iter', false, '--experimental-stream-iter'], + ['test', true, null], + ['test/reporters', true, null], + ['vfs', true, '--experimental-vfs'], + ['zlib/iter', false, '--experimental-stream-iter'], +]; + +const schemelessBlockList = new SafeSet(); +const optionGatedBuiltinOptions = new SafeMap(); +for (let i = 0; i < builtinModulePolicies.length; i++) { + const { 0: id, 1: schemeOnly, 2: option } = builtinModulePolicies[i]; + if (schemeOnly) { + schemelessBlockList.add(id); + } + if (option !== null) { + optionGatedBuiltinOptions.set(id, option); + } +} + const publicBuiltinIds = builtinIds .filter((id) => !StringPrototypeStartsWith(id, 'internal/') && - !experimentalModuleList.has(id), + !optionGatedBuiltinOptions.has(id), ); // Do not expose the loaders to user land even with --expose-internals. const internalBuiltinIds = builtinIds @@ -284,6 +287,21 @@ class BuiltinModule { } } + // Called after runtime options have been initialized, before user modules. + static allowOptionGatedBuiltins(getOptionValue) { + for (const { 0: id, 1: option } of optionGatedBuiltinOptions) { + if (getOptionValue(option)) { + BuiltinModule.allowRequireByUsers(id); + } + } + } + + // Return a copy so internal tests cannot mutate the loader's policy. + static getBuiltinModulePolicies() { + return ArrayPrototypeMap(builtinModulePolicies, + (policy) => ArrayPrototypeSlice(policy)); + } + static setRealmAllowRequireByUsers(ids) { canBeRequiredByUsersList = new SafeSet(ArrayPrototypeFilter(ids, (id) => ArrayPrototypeIncludes(publicBuiltinIds, id))); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 74f4480565e..8b537d71a72 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -116,13 +116,7 @@ function prepareExecution(options) { setupNetworkInspection(); setupNavigator(); setupWarningHandler(); - setupBench(); - setupFFI(); - setupSQLite(); - setupStreamIter(); - setupDTLS(); - setupVfs(); - setupQuic(); + setupOptionGatedBuiltins(); setupWebStorage(); removeWebWorkersIfDisabled(); setupEventsource(); @@ -461,32 +455,9 @@ function setupNavigator() { defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false); } -function setupBench() { - if (!getOptionValue('--experimental-bench')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('bench'); - BuiltinModule.allowRequireByUsers('bench/reporters'); -} - -function setupFFI() { - if (!getOptionValue('--experimental-ffi')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('ffi'); -} - -function setupSQLite() { - if (getOptionValue('--no-experimental-sqlite')) { - return; - } - +function setupOptionGatedBuiltins() { const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('sqlite'); + BuiltinModule.allowOptionGatedBuiltins(getOptionValue); } function initializeConfigFileSupport() { @@ -495,43 +466,6 @@ function initializeConfigFileSupport() { } } -function setupStreamIter() { - if (!getOptionValue('--experimental-stream-iter')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('stream/iter'); - BuiltinModule.allowRequireByUsers('zlib/iter'); -} - -function setupDTLS() { - if (!getOptionValue('--experimental-dtls')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('dtls'); -} - -function setupQuic() { - if (!getOptionValue('--experimental-quic')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('quic'); -} - -function setupVfs() { - if (!getOptionValue('--experimental-vfs')) { - return; - } - - const { BuiltinModule } = require('internal/bootstrap/realm'); - BuiltinModule.allowRequireByUsers('vfs'); -} - function setupWebStorage() { if (getEmbedderOptions().noBrowserGlobals || !getOptionValue('--experimental-webstorage')) { diff --git a/lib/internal/test/binding.js b/lib/internal/test/binding.js index 38f6475257a..4f7457efa89 100644 --- a/lib/internal/test/binding.js +++ b/lib/internal/test/binding.js @@ -30,4 +30,10 @@ if (module.isPreloading) { globalThis.primordials = primordials; } -module.exports = { internalBinding: filteredInternalBinding, primordials }; +module.exports = { + internalBinding: filteredInternalBinding, + primordials, + getBuiltinModulePolicies() { + return require('internal/bootstrap/realm').BuiltinModule.getBuiltinModulePolicies(); + }, +}; diff --git a/test/parallel/test-module-builtin-policies.js b/test/parallel/test-module-builtin-policies.js new file mode 100644 index 00000000000..c092bf6fd16 --- /dev/null +++ b/test/parallel/test-module-builtin-policies.js @@ -0,0 +1,143 @@ +// Flags: --expose-internals +'use strict'; + +// Run before loading common, whose async-hooks checks need --expose-internals. +// Child processes and Workers exercise only the public loaders. +if (process.argv[2] === 'child') { + const policy = JSON.parse(process.argv[3]); + const enabled = process.argv[4] === 'true'; + checkPolicy(policy, enabled).catch((err) => { + console.error(err); + process.exitCode = 1; + }); + return; +} + +const common = require('../common'); +const assert = require('assert'); +const { isBuiltin } = require('module'); +const { Worker } = require('worker_threads'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const { + internalBinding, + getBuiltinModulePolicies, +} = require('internal/test/binding'); +const { getCLIOptionsInfo } = require('internal/options'); + +const policies = getBuiltinModulePolicies(); +const { builtinIds } = internalBinding('builtins'); +const { options } = getCLIOptionsInfo(); +const seenIds = new Set(); +const moduleAvailability = new Map([ + ['dtls', common.hasDtls], + ['ffi', common.hasFFI], + ['quic', common.hasQuic], + ['sqlite', common.hasSQLite], +]); + +for (const policy of policies) { + assert(Array.isArray(policy)); + assert.strictEqual(policy.length, 3); + const [id, schemeOnly, option] = policy; + assert.strictEqual(typeof id, 'string'); + assert(!id.startsWith('internal/'), id); + assert(builtinIds.includes(id), `Unknown builtin: ${id}`); + assert(!seenIds.has(id), `Duplicate policy: ${id}`); + seenIds.add(id); + assert.strictEqual(typeof schemeOnly, 'boolean', id); + + if (option !== null) { + assert.match(option, /^--experimental-[a-z-]+$/); + // FFI has no CLI option in builds without FFI support. + const missingFFIOption = id === 'ffi' && !common.hasFFI && + option === '--experimental-ffi'; + assert(options.has(option) || missingFFIOption, + `Unknown builtin option: ${option}`); + } +} + +// Callers must not be able to change the loader's policy through this API. +const copy = getBuiltinModulePolicies(); +copy[0][0] = 'changed'; +copy.pop(); +assert.deepStrictEqual(getBuiltinModulePolicies(), policies); + +// Cover shared flags, both scheme rules, and an option enabled by default. +const workerPolicyIds = new Set([ + 'bench', 'bench/reporters', 'stream/iter', 'zlib/iter', 'sqlite', +]); + +for (const policy of policies) { + const [id, , option] = policy; + if (moduleAvailability.get(id) === false) continue; + + runPolicyTest(policy, [], option === null || options.get(option).defaultIsTrue); + if (option !== null) { + const disabledOption = option.replace('--', '--no-'); + runPolicyTest(policy, [option], true); + runPolicyTest(policy, [disabledOption], false); + + if (!process.config.variables.node_without_node_options) { + runPolicyTest(policy, [], true, option); + runPolicyTest(policy, [], false, disabledOption); + // Command-line options take precedence over NODE_OPTIONS. + runPolicyTest(policy, [option], true, disabledOption); + runPolicyTest(policy, [disabledOption], false, option); + } + + if (workerPolicyIds.has(id)) { + const parentEnabled = isBuiltin(`node:${id}`); + for (const enabled of [true, false]) { + const worker = new Worker(__filename, { + argv: ['child', JSON.stringify(policy), String(enabled)], + execArgv: [enabled ? option : disabledOption], + env: { ...process.env, NODE_OPTIONS: '' }, + }); + worker.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + assert.strictEqual(isBuiltin(`node:${id}`), parentEnabled, id); + })); + } + } + } +} + +// Run in a fresh process so each case initializes the loader with its flags. +function runPolicyTest(policy, flags, enabled, nodeOptions = '') { + spawnSyncAndAssert(process.execPath, [ + ...flags, + __filename, + 'child', + JSON.stringify(policy), + String(enabled), + ], { env: { ...process.env, NODE_OPTIONS: nodeOptions } }, { status: 0 }); +} + +async function checkPolicy([id, schemeOnly], enabled) { + const assert = require('assert'); + const { builtinModules, isBuiltin } = require('module'); + const prefixed = `node:${id}`; + assert.strictEqual(builtinModules.includes(id), enabled && !schemeOnly, id); + assert.strictEqual(builtinModules.includes(prefixed), + enabled && schemeOnly, prefixed); + + for (const specifier of [id, prefixed]) { + const supported = enabled && (!schemeOnly || specifier === prefixed); + assert.strictEqual(isBuiltin(specifier), supported, specifier); + if (supported) { + const exports = require(specifier); + assert.strictEqual(process.getBuiltinModule(specifier), exports); + assert.strictEqual((await import(specifier)).default, exports); + } else { + assert.strictEqual(process.getBuiltinModule(specifier), undefined); + assert.throws(() => require(specifier), { + code: specifier === prefixed ? + 'ERR_UNKNOWN_BUILTIN_MODULE' : 'MODULE_NOT_FOUND', + }); + await assert.rejects(import(specifier), { + code: specifier === prefixed ? + 'ERR_UNKNOWN_BUILTIN_MODULE' : 'ERR_MODULE_NOT_FOUND', + }); + } + } +}