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',
+      });
+    }
+  }
+}