Skip to content

Commit 767bbfb

Browse files
committed
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>
1 parent 96ee81c commit 767bbfb

4 files changed

Lines changed: 200 additions & 99 deletions

File tree

‎lib/internal/bootstrap/realm.js‎

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -120,34 +120,6 @@ const legacyWrapperList = new SafeSet([
120120
'util',
121121
]);
122122

123-
// The code below assumes that the two lists must not contain any modules
124-
// beginning with "internal/".
125-
// Modules that can only be imported via the node: scheme.
126-
const schemelessBlockList = new SafeSet([
127-
'bench',
128-
'bench/reporters',
129-
'dtls',
130-
'ffi',
131-
'sea',
132-
'sqlite',
133-
'quic',
134-
'test',
135-
'test/reporters',
136-
'vfs',
137-
]);
138-
// Modules that will only be enabled at run time.
139-
const experimentalModuleList = new SafeSet([
140-
'bench',
141-
'bench/reporters',
142-
'dtls',
143-
'ffi',
144-
'quic',
145-
'sqlite',
146-
'stream/iter',
147-
'vfs',
148-
'zlib/iter',
149-
]);
150-
151123
// Set up process.binding() and process._linkedBinding().
152124
{
153125
const bindingObj = { __proto__: null };
@@ -220,10 +192,41 @@ const getOwn = (target, property, receiver) => {
220192
undefined;
221193
};
222194

195+
// Public builtin exposure policies. Each entry is [id, schemeOnly, option].
196+
// A null option means that the builtin is not gated by a runtime option.
197+
// Do not include internal modules. Runtime option definitions and code-cache
198+
// categories are owned by their respective native subsystems.
199+
const builtinModulePolicies = [
200+
['bench', true, '--experimental-bench'],
201+
['bench/reporters', true, '--experimental-bench'],
202+
['dtls', true, '--experimental-dtls'],
203+
['ffi', true, '--experimental-ffi'],
204+
['sea', true, null],
205+
['sqlite', true, '--experimental-sqlite'],
206+
['quic', true, '--experimental-quic'],
207+
['stream/iter', false, '--experimental-stream-iter'],
208+
['test', true, null],
209+
['test/reporters', true, null],
210+
['vfs', true, '--experimental-vfs'],
211+
['zlib/iter', false, '--experimental-stream-iter'],
212+
];
213+
214+
const schemelessBlockList = new SafeSet();
215+
const optionGatedBuiltinOptions = new SafeMap();
216+
for (let i = 0; i < builtinModulePolicies.length; i++) {
217+
const { 0: id, 1: schemeOnly, 2: option } = builtinModulePolicies[i];
218+
if (schemeOnly) {
219+
schemelessBlockList.add(id);
220+
}
221+
if (option !== null) {
222+
optionGatedBuiltinOptions.set(id, option);
223+
}
224+
}
225+
223226
const publicBuiltinIds = builtinIds
224227
.filter((id) =>
225228
!StringPrototypeStartsWith(id, 'internal/') &&
226-
!experimentalModuleList.has(id),
229+
!optionGatedBuiltinOptions.has(id),
227230
);
228231
// Do not expose the loaders to user land even with --expose-internals.
229232
const internalBuiltinIds = builtinIds
@@ -284,6 +287,21 @@ class BuiltinModule {
284287
}
285288
}
286289

290+
// Called after runtime options have been initialized, before user modules.
291+
static allowOptionGatedBuiltins(getOptionValue) {
292+
for (const { 0: id, 1: option } of optionGatedBuiltinOptions) {
293+
if (getOptionValue(option)) {
294+
BuiltinModule.allowRequireByUsers(id);
295+
}
296+
}
297+
}
298+
299+
// Return a copy so internal tests cannot mutate the loader's policy.
300+
static getBuiltinModulePolicies() {
301+
return ArrayPrototypeMap(builtinModulePolicies,
302+
(policy) => ArrayPrototypeSlice(policy));
303+
}
304+
287305
static setRealmAllowRequireByUsers(ids) {
288306
canBeRequiredByUsersList =
289307
new SafeSet(ArrayPrototypeFilter(ids, (id) => ArrayPrototypeIncludes(publicBuiltinIds, id)));

‎lib/internal/process/pre_execution.js‎

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,7 @@ function prepareExecution(options) {
116116
setupNetworkInspection();
117117
setupNavigator();
118118
setupWarningHandler();
119-
setupBench();
120-
setupFFI();
121-
setupSQLite();
122-
setupStreamIter();
123-
setupDTLS();
124-
setupVfs();
125-
setupQuic();
119+
setupOptionGatedBuiltins();
126120
setupWebStorage();
127121
removeWebWorkersIfDisabled();
128122
setupEventsource();
@@ -461,32 +455,9 @@ function setupNavigator() {
461455
defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false);
462456
}
463457

464-
function setupBench() {
465-
if (!getOptionValue('--experimental-bench')) {
466-
return;
467-
}
468-
469-
const { BuiltinModule } = require('internal/bootstrap/realm');
470-
BuiltinModule.allowRequireByUsers('bench');
471-
BuiltinModule.allowRequireByUsers('bench/reporters');
472-
}
473-
474-
function setupFFI() {
475-
if (!getOptionValue('--experimental-ffi')) {
476-
return;
477-
}
478-
479-
const { BuiltinModule } = require('internal/bootstrap/realm');
480-
BuiltinModule.allowRequireByUsers('ffi');
481-
}
482-
483-
function setupSQLite() {
484-
if (getOptionValue('--no-experimental-sqlite')) {
485-
return;
486-
}
487-
458+
function setupOptionGatedBuiltins() {
488459
const { BuiltinModule } = require('internal/bootstrap/realm');
489-
BuiltinModule.allowRequireByUsers('sqlite');
460+
BuiltinModule.allowOptionGatedBuiltins(getOptionValue);
490461
}
491462

492463
function initializeConfigFileSupport() {
@@ -495,43 +466,6 @@ function initializeConfigFileSupport() {
495466
}
496467
}
497468

498-
function setupStreamIter() {
499-
if (!getOptionValue('--experimental-stream-iter')) {
500-
return;
501-
}
502-
503-
const { BuiltinModule } = require('internal/bootstrap/realm');
504-
BuiltinModule.allowRequireByUsers('stream/iter');
505-
BuiltinModule.allowRequireByUsers('zlib/iter');
506-
}
507-
508-
function setupDTLS() {
509-
if (!getOptionValue('--experimental-dtls')) {
510-
return;
511-
}
512-
513-
const { BuiltinModule } = require('internal/bootstrap/realm');
514-
BuiltinModule.allowRequireByUsers('dtls');
515-
}
516-
517-
function setupQuic() {
518-
if (!getOptionValue('--experimental-quic')) {
519-
return;
520-
}
521-
522-
const { BuiltinModule } = require('internal/bootstrap/realm');
523-
BuiltinModule.allowRequireByUsers('quic');
524-
}
525-
526-
function setupVfs() {
527-
if (!getOptionValue('--experimental-vfs')) {
528-
return;
529-
}
530-
531-
const { BuiltinModule } = require('internal/bootstrap/realm');
532-
BuiltinModule.allowRequireByUsers('vfs');
533-
}
534-
535469
function setupWebStorage() {
536470
if (getEmbedderOptions().noBrowserGlobals ||
537471
!getOptionValue('--experimental-webstorage')) {

‎lib/internal/test/binding.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@ if (module.isPreloading) {
3030
globalThis.primordials = primordials;
3131
}
3232

33-
module.exports = { internalBinding: filteredInternalBinding, primordials };
33+
module.exports = {
34+
internalBinding: filteredInternalBinding,
35+
primordials,
36+
getBuiltinModulePolicies() {
37+
return require('internal/bootstrap/realm').BuiltinModule.getBuiltinModulePolicies();
38+
},
39+
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
// Run before loading common, whose async-hooks checks need --expose-internals.
5+
// Child processes and Workers exercise only the public loaders.
6+
if (process.argv[2] === 'child') {
7+
const policy = JSON.parse(process.argv[3]);
8+
const enabled = process.argv[4] === 'true';
9+
checkPolicy(policy, enabled).catch((err) => {
10+
console.error(err);
11+
process.exitCode = 1;
12+
});
13+
return;
14+
}
15+
16+
const common = require('../common');
17+
const assert = require('assert');
18+
const { isBuiltin } = require('module');
19+
const { Worker } = require('worker_threads');
20+
const { spawnSyncAndAssert } = require('../common/child_process');
21+
const {
22+
internalBinding,
23+
getBuiltinModulePolicies,
24+
} = require('internal/test/binding');
25+
const { getCLIOptionsInfo } = require('internal/options');
26+
27+
const policies = getBuiltinModulePolicies();
28+
const { builtinIds } = internalBinding('builtins');
29+
const { options } = getCLIOptionsInfo();
30+
const seenIds = new Set();
31+
const moduleAvailability = new Map([
32+
['dtls', common.hasDtls],
33+
['ffi', common.hasFFI],
34+
['quic', common.hasQuic],
35+
['sqlite', common.hasSQLite],
36+
]);
37+
38+
for (const policy of policies) {
39+
assert(Array.isArray(policy));
40+
assert.strictEqual(policy.length, 3);
41+
const [id, schemeOnly, option] = policy;
42+
assert.strictEqual(typeof id, 'string');
43+
assert(!id.startsWith('internal/'), id);
44+
assert(builtinIds.includes(id), `Unknown builtin: ${id}`);
45+
assert(!seenIds.has(id), `Duplicate policy: ${id}`);
46+
seenIds.add(id);
47+
assert.strictEqual(typeof schemeOnly, 'boolean', id);
48+
49+
if (option !== null) {
50+
assert.match(option, /^--experimental-[a-z-]+$/);
51+
// FFI has no CLI option in builds without FFI support.
52+
const missingFFIOption = id === 'ffi' && !common.hasFFI &&
53+
option === '--experimental-ffi';
54+
assert(options.has(option) || missingFFIOption,
55+
`Unknown builtin option: ${option}`);
56+
}
57+
}
58+
59+
// Callers must not be able to change the loader's policy through this API.
60+
const copy = getBuiltinModulePolicies();
61+
copy[0][0] = 'changed';
62+
copy.pop();
63+
assert.deepStrictEqual(getBuiltinModulePolicies(), policies);
64+
65+
// Cover shared flags, both scheme rules, and an option enabled by default.
66+
const workerPolicyIds = new Set([
67+
'bench', 'bench/reporters', 'stream/iter', 'zlib/iter', 'sqlite',
68+
]);
69+
70+
for (const policy of policies) {
71+
const [id, , option] = policy;
72+
if (moduleAvailability.get(id) === false) continue;
73+
74+
runPolicyTest(policy, [], option === null || options.get(option).defaultIsTrue);
75+
if (option !== null) {
76+
const disabledOption = option.replace('--', '--no-');
77+
runPolicyTest(policy, [option], true);
78+
runPolicyTest(policy, [disabledOption], false);
79+
80+
if (!process.config.variables.node_without_node_options) {
81+
runPolicyTest(policy, [], true, option);
82+
runPolicyTest(policy, [], false, disabledOption);
83+
// Command-line options take precedence over NODE_OPTIONS.
84+
runPolicyTest(policy, [option], true, disabledOption);
85+
runPolicyTest(policy, [disabledOption], false, option);
86+
}
87+
88+
if (workerPolicyIds.has(id)) {
89+
const parentEnabled = isBuiltin(`node:${id}`);
90+
for (const enabled of [true, false]) {
91+
const worker = new Worker(__filename, {
92+
argv: ['child', JSON.stringify(policy), String(enabled)],
93+
execArgv: [enabled ? option : disabledOption],
94+
env: { ...process.env, NODE_OPTIONS: '' },
95+
});
96+
worker.on('exit', common.mustCall((code) => {
97+
assert.strictEqual(code, 0);
98+
assert.strictEqual(isBuiltin(`node:${id}`), parentEnabled, id);
99+
}));
100+
}
101+
}
102+
}
103+
}
104+
105+
// Run in a fresh process so each case initializes the loader with its flags.
106+
function runPolicyTest(policy, flags, enabled, nodeOptions = '') {
107+
spawnSyncAndAssert(process.execPath, [
108+
...flags,
109+
__filename,
110+
'child',
111+
JSON.stringify(policy),
112+
String(enabled),
113+
], { env: { ...process.env, NODE_OPTIONS: nodeOptions } }, { status: 0 });
114+
}
115+
116+
async function checkPolicy([id, schemeOnly], enabled) {
117+
const assert = require('assert');
118+
const { builtinModules, isBuiltin } = require('module');
119+
const prefixed = `node:${id}`;
120+
assert.strictEqual(builtinModules.includes(id), enabled && !schemeOnly, id);
121+
assert.strictEqual(builtinModules.includes(prefixed),
122+
enabled && schemeOnly, prefixed);
123+
124+
for (const specifier of [id, prefixed]) {
125+
const supported = enabled && (!schemeOnly || specifier === prefixed);
126+
assert.strictEqual(isBuiltin(specifier), supported, specifier);
127+
if (supported) {
128+
const exports = require(specifier);
129+
assert.strictEqual(process.getBuiltinModule(specifier), exports);
130+
assert.strictEqual((await import(specifier)).default, exports);
131+
} else {
132+
assert.strictEqual(process.getBuiltinModule(specifier), undefined);
133+
assert.throws(() => require(specifier), {
134+
code: specifier === prefixed ?
135+
'ERR_UNKNOWN_BUILTIN_MODULE' : 'MODULE_NOT_FOUND',
136+
});
137+
await assert.rejects(import(specifier), {
138+
code: specifier === prefixed ?
139+
'ERR_UNKNOWN_BUILTIN_MODULE' : 'ERR_MODULE_NOT_FOUND',
140+
});
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)