Commit 4791219541c for nodejs
commit 4791219541ca3d14bdd0d332f78176aa663030b5
Author: Jungwon Sohn <sjungwon03@gmail.com>
Date: Sat Oct 3 02:29:27 2026 +0900
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 <sjungwon03@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66292
Refs: https://github.com/nodejs/node/pull/65418
Refs: https://github.com/nodejs/node/pull/65964
Refs: https://github.com/nodejs/node/pull/65920
Refs: https://github.com/nodejs/node/pull/65840
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
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 57bfb8eb845..9efccb40f8c 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -118,13 +118,7 @@ function prepareExecution(options) {
setupNetworkInspection();
setupNavigator();
setupWarningHandler();
- setupBench();
- setupFFI();
- setupSQLite();
- setupStreamIter();
- setupDTLS();
- setupVfs();
- setupQuic();
+ setupOptionGatedBuiltins();
setupWebStorage();
removeWebWorkersIfDisabled();
setupEventsource();
@@ -491,32 +485,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() {
@@ -525,43 +496,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..d6db66fcbe8
--- /dev/null
+++ b/test/parallel/test-module-builtin-policies.js
@@ -0,0 +1,171 @@
+// 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 policies = JSON.parse(process.argv[3]);
+ const enabled = process.argv[4] === 'true';
+ Promise.all(policies.map((policy) => 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 moduleAvailability = new Map([
+ ['dtls', common.hasDtls],
+ ['ffi', common.hasFFI],
+ ['quic', common.hasQuic],
+ ['sqlite', common.hasSQLite],
+]);
+
+// Validate every declared policy before testing its behavior.
+{
+ const seenIds = new Set();
+ 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) continue;
+ 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);
+}
+
+// Test defaults, command-line flags, NODE_OPTIONS, and their precedence in
+// fresh processes.
+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);
+ }
+ }
+}
+
+// Test option-gated builtins in Workers without changing the parent state.
+{
+ const policiesByOption = new Map();
+ for (const policy of policies) {
+ const [id, , option] = policy;
+ if (option === null || moduleAvailability.get(id) === false) continue;
+
+ const optionPolicies = policiesByOption.get(option);
+ if (optionPolicies === undefined) {
+ policiesByOption.set(option, [policy]);
+ } else {
+ optionPolicies.push(policy);
+ }
+ }
+
+ // Test each option once, together with all builtins it controls.
+ for (const [option, optionPolicies] of policiesByOption) {
+ const parentState = optionPolicies.map(([id]) => [
+ id,
+ isBuiltin(`node:${id}`),
+ ]);
+ const disabledOption = option.replace('--', '--no-');
+
+ for (const enabled of [true, false]) {
+ const worker = new Worker(__filename, {
+ argv: ['child', JSON.stringify(optionPolicies), String(enabled)],
+ execArgv: [enabled ? option : disabledOption],
+ env: { ...process.env, NODE_OPTIONS: '' },
+ });
+
+ worker.on('exit', common.mustCall((code) => {
+ assert.strictEqual(code, 0);
+ for (const [id, parentEnabled] of parentState) {
+ 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',
+ });
+ }
+ }
+}