From 73cbbe2f6e96a3cb0bf4159abac58525d34614f7 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 26 Sep 2026 12:35:49 +0200 Subject: [PATCH 1/3] worker: preload streams into startup snapshot Worker stdio loads stream after restoring the main-thread context, initializing its constructors and prototype methods on every startup. Preload stream during snapshot construction so workers can restore that initialized state instead. Worker-specific stdio instances and message ports are still created at runtime. Verify snapshot membership, stdio methods, and isolation from parent-thread stream mutations. Assisted-by: pi Signed-off-by: Matteo Collina --- .../bootstrap/switches/is_main_thread.js | 3 ++ test/parallel/test-bootstrap-modules.js | 19 +++++++++ test/parallel/test-worker-snapshot-streams.js | 41 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 test/parallel/test-worker-snapshot-streams.js diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js index e9e37275b17f..3e18fe6e3228 100644 --- a/lib/internal/bootstrap/switches/is_main_thread.js +++ b/lib/internal/bootstrap/switches/is_main_thread.js @@ -312,6 +312,9 @@ if (isBuildingSnapshot()) { require('internal/modules/typescript'); require('internal/blob'); require('internal/dns/utils'); + // Workers restore this context before loading internal/worker/io, which + // needs the initialized stream constructors for its stdio subclasses. + require('stream'); } // Needed to refresh the time origin. diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index e6a40d45e26f..9a99fa33d4a6 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -146,6 +146,25 @@ if (contextFromSnapshot) { 'NativeModule internal/mime', 'NativeModule internal/modules/typescript', 'NativeModule internal/net', + 'NativeModule internal/abort_controller', + 'NativeModule internal/streams/add-abort-signal', + 'NativeModule internal/streams/compose', + 'NativeModule internal/streams/destroy', + 'NativeModule internal/streams/duplex', + 'NativeModule internal/streams/duplexpair', + 'NativeModule internal/streams/end-of-stream', + 'NativeModule internal/streams/from', + 'NativeModule internal/streams/legacy', + 'NativeModule internal/streams/operators', + 'NativeModule internal/streams/passthrough', + 'NativeModule internal/streams/pipeline', + 'NativeModule internal/streams/readable', + 'NativeModule internal/streams/state', + 'NativeModule internal/streams/transform', + 'NativeModule internal/streams/writable', + 'NativeModule stream', + 'NativeModule stream/promises', + 'NativeModule string_decoder', ].forEach(expected.beforePreExec.add.bind(expected.beforePreExec)); } else if (isMainThread) { expected.beforePreExec.delete(getFormatNativeModule); diff --git a/test/parallel/test-worker-snapshot-streams.js b/test/parallel/test-worker-snapshot-streams.js new file mode 100644 index 000000000000..bda73a129c8c --- /dev/null +++ b/test/parallel/test-worker-snapshot-streams.js @@ -0,0 +1,41 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker, isMainThread, workerData } = require('worker_threads'); +const { internalBinding } = require('internal/test/binding'); + +const contextFromSnapshot = + process.config.variables.node_use_node_snapshot && + !process.execArgv.includes('--no-node-snapshot') && + (isMainThread || !process.execArgv.includes('--no-worker-snapshot')); +const { compiledInSnapshot } = internalBinding('builtins').getCacheUsage(); +for (const id of ['stream', 'internal/streams/readable', 'internal/streams/writable']) { + assert.strictEqual(compiledInSnapshot.includes(id), Boolean(contextFromSnapshot), id); +} + +const { Readable, Writable, getDefaultHighWaterMark, setDefaultHighWaterMark } = require('stream'); + +if (isMainThread) { + const highWaterMark = getDefaultHighWaterMark(false); + // Runtime changes in the parent must not become the worker's initial state. + setDefaultHighWaterMark(false, highWaterMark + 1); + Readable.prototype.parentOnly = true; + const worker = new Worker(__filename, { workerData: { highWaterMark } }); + worker.on('error', common.mustNotCall()); + worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); +} else { + assert.strictEqual(Readable.prototype.parentOnly, undefined); + if (workerData?.highWaterMark !== undefined) { + assert.strictEqual(getDefaultHighWaterMark(false), workerData.highWaterMark); + } + assert(process.stdin instanceof Readable); + assert(process.stdout instanceof Writable); + assert(process.stderr instanceof Writable); + assert.strictEqual(typeof process.stdin.map, 'function'); + assert.strictEqual(typeof process.stdin.toArray, 'function'); + Readable.from([1, 2, 3]).map((value) => value * 2).toArray().then((values) => { + assert.deepStrictEqual(values, [2, 4, 6]); + }).then(common.mustCall()); +} From 34d339b9771c6d676f3ebd484c6ebb60a554be69 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 26 Sep 2026 14:47:24 +0200 Subject: [PATCH 2/3] stream: defer unused stream initialization Keep Readable and Writable eagerly initialized for worker stdio, but load operators, pipeline, compose, promises, and the other stream constructors on demand. This keeps their implementation state out of the startup snapshot when it is not needed. Preserve export identity, prototype descriptors, and promisification. Install subclass methods before linking prototype chains so lazy loading also works when a base constructor or prototype has been frozen. Update the bootstrap inventory. Assisted-by: pi Signed-off-by: Matteo Collina --- lib/internal/streams/duplex.js | 12 ++-- lib/internal/streams/lazy.js | 22 +++++++ lib/internal/streams/operators.js | 39 +++++++++++- lib/internal/streams/passthrough.js | 6 +- lib/internal/streams/pipeline.js | 14 ++++- lib/internal/streams/transform.js | 6 +- lib/stream.js | 83 ++++--------------------- lib/stream/promises.js | 3 +- test/parallel/test-bootstrap-modules.js | 18 ------ 9 files changed, 103 insertions(+), 100 deletions(-) create mode 100644 lib/internal/streams/lazy.js diff --git a/lib/internal/streams/duplex.js b/lib/internal/streams/duplex.js index dce8bd6e0fdf..6d31f62d0998 100644 --- a/lib/internal/streams/duplex.js +++ b/lib/internal/streams/duplex.js @@ -46,15 +46,14 @@ const { const destroyImpl = require('internal/streams/destroy'); const { kOnConstructed } = require('internal/streams/utils'); -ObjectSetPrototypeOf(Duplex.prototype, Readable.prototype); -ObjectSetPrototypeOf(Duplex, Readable); - { const keys = ObjectKeys(Writable.prototype); // Allow the keys array to be GC'ed. for (let i = 0; i < keys.length; i++) { const method = keys[i]; - Duplex.prototype[method] ||= Writable.prototype[method]; + if (!Readable.prototype[method]) { + Duplex.prototype[method] = Writable.prototype[method]; + } } } @@ -201,3 +200,8 @@ Duplex.from = function(body) { duplexify ??= require('internal/streams/duplexify'); return duplexify(body, 'body'); }; + +// Define own methods before inheriting from constructors or prototypes that +// user code may have frozen before this module was loaded. +ObjectSetPrototypeOf(Duplex.prototype, Readable.prototype); +ObjectSetPrototypeOf(Duplex, Readable); diff --git a/lib/internal/streams/lazy.js b/lib/internal/streams/lazy.js new file mode 100644 index 000000000000..8d80b5ef6a7b --- /dev/null +++ b/lib/internal/streams/lazy.js @@ -0,0 +1,22 @@ +'use strict'; + +// These modules export a single function rather than named properties. +// Expose them by name for defineLazyProperties() without loading them together. +module.exports = { + __proto__: null, + get Duplex() { + return require('internal/streams/duplex'); + }, + get Transform() { + return require('internal/streams/transform'); + }, + get PassThrough() { + return require('internal/streams/passthrough'); + }, + get duplexPair() { + return require('internal/streams/duplexpair'); + }, + get compose() { + return require('internal/streams/compose'); + }, +}; diff --git a/lib/internal/streams/operators.js b/lib/internal/streams/operators.js index a5cbe1af428f..87b82b65b56b 100644 --- a/lib/internal/streams/operators.js +++ b/lib/internal/streams/operators.js @@ -6,10 +6,13 @@ const { MathFloor, Number, NumberIsNaN, + ObjectDefineProperty, + ObjectKeys, Promise, PromisePrototypeThen, PromiseReject, PromiseResolve, + ReflectApply, Symbol, } = primordials; @@ -18,6 +21,7 @@ const { AbortController, AbortSignal } = require('internal/abort_controller'); const { AbortError, codes: { + ERR_ILLEGAL_CONSTRUCTOR, ERR_MISSING_ARGS, ERR_OUT_OF_RANGE, }, @@ -31,6 +35,7 @@ const { const { kWeakHandler, kResistStopPropagation } = require('internal/event_target'); const destroyImpl = require('internal/streams/destroy'); const { finished } = require('internal/streams/end-of-stream'); +const Stream = require('stream'); const kEmpty = Symbol('kEmpty'); const kEof = Symbol('kEof'); @@ -373,7 +378,7 @@ function take(number, options = undefined) { }.call(this); } -module.exports.streamReturningOperators = { +const streamReturningOperators = { drop, filter, flatMap, @@ -381,7 +386,7 @@ module.exports.streamReturningOperators = { take, }; -module.exports.promiseReturningOperators = { +const promiseReturningOperators = { every, forEach, reduce, @@ -389,3 +394,33 @@ module.exports.promiseReturningOperators = { some, find, }; + +const streamKeys = ObjectKeys(streamReturningOperators); +for (let i = 0; i < streamKeys.length; i++) { + const key = streamKeys[i]; + const op = streamReturningOperators[key]; + function fn(...args) { + if (new.target) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } + return Stream.Readable.from(ReflectApply(op, this, args)); + } + ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name }); + ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length }); + module.exports[key] = fn; +} + +const promiseKeys = ObjectKeys(promiseReturningOperators); +for (let i = 0; i < promiseKeys.length; i++) { + const key = promiseKeys[i]; + const op = promiseReturningOperators[key]; + function fn(...args) { + if (new.target) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } + return ReflectApply(op, this, args); + } + ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name }); + ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length }); + module.exports[key] = fn; +} diff --git a/lib/internal/streams/passthrough.js b/lib/internal/streams/passthrough.js index d37f9caf0116..587361295111 100644 --- a/lib/internal/streams/passthrough.js +++ b/lib/internal/streams/passthrough.js @@ -32,8 +32,6 @@ const { module.exports = PassThrough; const Transform = require('internal/streams/transform'); -ObjectSetPrototypeOf(PassThrough.prototype, Transform.prototype); -ObjectSetPrototypeOf(PassThrough, Transform); function PassThrough(options) { if (!(this instanceof PassThrough)) @@ -45,3 +43,7 @@ function PassThrough(options) { PassThrough.prototype._transform = function(chunk, encoding, cb) { cb(null, chunk); }; + +// Install the override before inheriting a potentially frozen prototype. +ObjectSetPrototypeOf(PassThrough.prototype, Transform.prototype); +ObjectSetPrototypeOf(PassThrough, Transform); diff --git a/lib/internal/streams/pipeline.js b/lib/internal/streams/pipeline.js index 546ff579d8eb..6937d4354340 100644 --- a/lib/internal/streams/pipeline.js +++ b/lib/internal/streams/pipeline.js @@ -5,13 +5,17 @@ const { ArrayIsArray, + ObjectDefineProperty, Promise, SymbolAsyncIterator, SymbolDispose, } = primordials; const { eos } = require('internal/streams/end-of-stream'); -const { once } = require('internal/util'); +const { + once, + promisify: { custom: customPromisify }, +} = require('internal/util'); const destroyImpl = require('internal/streams/destroy'); const Duplex = require('internal/streams/duplex'); const { @@ -469,4 +473,12 @@ function pipe(src, dst, finish, finishOnlyHandleError, { end }) { return eos(dst, { readable: false, writable: true }, finish); } +ObjectDefineProperty(pipeline, customPromisify, { + __proto__: null, + enumerable: true, + get() { + return require('stream/promises').pipeline; + }, +}); + module.exports = { pipelineImpl, pipeline }; diff --git a/lib/internal/streams/transform.js b/lib/internal/streams/transform.js index 2f4d498bc780..2d16f2568edc 100644 --- a/lib/internal/streams/transform.js +++ b/lib/internal/streams/transform.js @@ -74,8 +74,6 @@ const { } = require('internal/errors').codes; const Duplex = require('internal/streams/duplex'); const { getHighWaterMark } = require('internal/streams/state'); -ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype); -ObjectSetPrototypeOf(Transform, Duplex); const kCallback = Symbol('kCallback'); @@ -202,3 +200,7 @@ Transform.prototype._read = function() { callback(); } }; + +// Install overrides before inheriting potentially frozen prototype methods. +ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype); +ObjectSetPrototypeOf(Transform, Duplex); diff --git a/lib/stream.js b/lib/stream.js index 078fa092f483..1eb255dd4865 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -23,32 +23,19 @@ const { ObjectDefineProperty, - ObjectKeys, - ReflectApply, } = primordials; const { + defineLazyProperties, promisify: { custom: customPromisify }, } = require('internal/util'); -const { - streamReturningOperators, - promiseReturningOperators, -} = require('internal/streams/operators'); - -const { - codes: { - ERR_ILLEGAL_CONSTRUCTOR, - }, -} = require('internal/errors'); -const compose = require('internal/streams/compose'); const { setDefaultHighWaterMark, getDefaultHighWaterMark } = require('internal/streams/state'); -const { pipeline } = require('internal/streams/pipeline'); const { destroyer } = require('internal/streams/destroy'); const { eos } = require('internal/streams/end-of-stream'); const internalBuffer = require('internal/buffer'); -const promises = require('stream/promises'); +let promises; const utils = require('internal/streams/utils'); const { isArrayBufferView, isUint8Array } = require('internal/util/types'); @@ -61,57 +48,20 @@ Stream.isReadable = utils.isReadable; Stream.isWritable = utils.isWritable; Stream.Readable = require('internal/streams/readable'); -const streamKeys = ObjectKeys(streamReturningOperators); -for (let i = 0; i < streamKeys.length; i++) { - const key = streamKeys[i]; - const op = streamReturningOperators[key]; - function fn(...args) { - if (new.target) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } - return Stream.Readable.from(ReflectApply(op, this, args)); - } - ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name }); - ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length }); - ObjectDefineProperty(Stream.Readable.prototype, key, { - __proto__: null, - value: fn, - enumerable: false, - configurable: true, - writable: true, - }); -} -const promiseKeys = ObjectKeys(promiseReturningOperators); -for (let i = 0; i < promiseKeys.length; i++) { - const key = promiseKeys[i]; - const op = promiseReturningOperators[key]; - function fn(...args) { - if (new.target) { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } - return ReflectApply(op, this, args); - } - ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name }); - ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length }); - ObjectDefineProperty(Stream.Readable.prototype, key, { - __proto__: null, - value: fn, - enumerable: false, - configurable: true, - writable: true, - }); -} +defineLazyProperties(Stream.Readable.prototype, 'internal/streams/operators', [ + 'drop', 'filter', 'flatMap', 'map', 'take', + 'every', 'forEach', 'reduce', 'toArray', 'some', 'find', +], false); Stream.Writable = require('internal/streams/writable'); -Stream.Duplex = require('internal/streams/duplex'); -Stream.Transform = require('internal/streams/transform'); -Stream.PassThrough = require('internal/streams/passthrough'); -Stream.duplexPair = require('internal/streams/duplexpair'); -Stream.pipeline = pipeline; +defineLazyProperties(Stream, 'internal/streams/lazy', [ + 'Duplex', 'Transform', 'PassThrough', 'duplexPair', +]); +defineLazyProperties(Stream, 'internal/streams/pipeline', ['pipeline']); const { addAbortSignal } = require('internal/streams/add-abort-signal'); Stream.addAbortSignal = addAbortSignal; Stream.finished = eos; Stream.destroy = destroyer; -Stream.compose = compose; +defineLazyProperties(Stream, 'internal/streams/lazy', ['compose']); Stream.setDefaultHighWaterMark = setDefaultHighWaterMark; Stream.getDefaultHighWaterMark = getDefaultHighWaterMark; @@ -120,15 +70,7 @@ ObjectDefineProperty(Stream, 'promises', { configurable: true, enumerable: true, get() { - return promises; - }, -}); - -ObjectDefineProperty(pipeline, customPromisify, { - __proto__: null, - enumerable: true, - get() { - return promises.pipeline; + return promises ??= require('stream/promises'); }, }); @@ -136,6 +78,7 @@ ObjectDefineProperty(eos, customPromisify, { __proto__: null, enumerable: true, get() { + promises ??= require('stream/promises'); return promises.finished; }, }); diff --git a/lib/stream/promises.js b/lib/stream/promises.js index a8b65d62b096..9d36d4da4f15 100644 --- a/lib/stream/promises.js +++ b/lib/stream/promises.js @@ -11,12 +11,13 @@ const { isWebStream, } = require('internal/streams/utils'); -const { pipelineImpl: pl } = require('internal/streams/pipeline'); +let pl; const { finished } = require('internal/streams/end-of-stream'); require('stream'); function pipeline(...streams) { + pl ??= require('internal/streams/pipeline').pipelineImpl; return new Promise((resolve, reject) => { let signal; let end; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 9a99fa33d4a6..1c5db787a8bc 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -146,24 +146,15 @@ if (contextFromSnapshot) { 'NativeModule internal/mime', 'NativeModule internal/modules/typescript', 'NativeModule internal/net', - 'NativeModule internal/abort_controller', 'NativeModule internal/streams/add-abort-signal', - 'NativeModule internal/streams/compose', 'NativeModule internal/streams/destroy', - 'NativeModule internal/streams/duplex', - 'NativeModule internal/streams/duplexpair', 'NativeModule internal/streams/end-of-stream', 'NativeModule internal/streams/from', 'NativeModule internal/streams/legacy', - 'NativeModule internal/streams/operators', - 'NativeModule internal/streams/passthrough', - 'NativeModule internal/streams/pipeline', 'NativeModule internal/streams/readable', 'NativeModule internal/streams/state', - 'NativeModule internal/streams/transform', 'NativeModule internal/streams/writable', 'NativeModule stream', - 'NativeModule stream/promises', 'NativeModule string_decoder', ].forEach(expected.beforePreExec.add.bind(expected.beforePreExec)); } else if (isMainThread) { @@ -173,29 +164,20 @@ if (contextFromSnapshot) { if (!isMainThread) { [ 'NativeModule diagnostics_channel', - 'NativeModule internal/abort_controller', 'NativeModule internal/process/worker_thread_only', 'NativeModule internal/streams/add-abort-signal', - 'NativeModule internal/streams/compose', 'NativeModule internal/streams/destroy', - 'NativeModule internal/streams/duplex', - 'NativeModule internal/streams/duplexpair', 'NativeModule internal/streams/end-of-stream', 'NativeModule internal/streams/from', 'NativeModule internal/streams/legacy', - 'NativeModule internal/streams/operators', - 'NativeModule internal/streams/passthrough', - 'NativeModule internal/streams/pipeline', 'NativeModule internal/streams/readable', 'NativeModule internal/streams/state', - 'NativeModule internal/streams/transform', 'NativeModule internal/streams/utils', 'NativeModule internal/streams/writable', 'NativeModule internal/worker', 'NativeModule internal/worker/io', 'NativeModule internal/worker/messaging', 'NativeModule stream', - 'NativeModule stream/promises', 'NativeModule string_decoder', 'NativeModule worker_threads', ].forEach(expected.atRunTime.add.bind(expected.atRunTime)); From 5e41708c0d38f4ad0e13bb25006ae7968c9c69e4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 30 Sep 2026 09:46:00 +0200 Subject: [PATCH 3/3] stream: inline lazy module mappings Allow defineLazyProperties to map property names directly to module IDs so stream can expose function exports without a separate lazy module. Keep only module IDs in native callback data for snapshot serialization. Set the mapping object's prototype to null after creation to retain fast properties without inheriting from Object.prototype. Assisted-by: pi Signed-off-by: Matteo Collina --- lib/internal/streams/lazy.js | 22 --------------------- lib/stream.js | 14 ++++++++++++-- src/node_util.cc | 37 +++++++++++++++++++++++++----------- 3 files changed, 38 insertions(+), 35 deletions(-) delete mode 100644 lib/internal/streams/lazy.js diff --git a/lib/internal/streams/lazy.js b/lib/internal/streams/lazy.js deleted file mode 100644 index 8d80b5ef6a7b..000000000000 --- a/lib/internal/streams/lazy.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -// These modules export a single function rather than named properties. -// Expose them by name for defineLazyProperties() without loading them together. -module.exports = { - __proto__: null, - get Duplex() { - return require('internal/streams/duplex'); - }, - get Transform() { - return require('internal/streams/transform'); - }, - get PassThrough() { - return require('internal/streams/passthrough'); - }, - get duplexPair() { - return require('internal/streams/duplexpair'); - }, - get compose() { - return require('internal/streams/compose'); - }, -}; diff --git a/lib/stream.js b/lib/stream.js index 1eb255dd4865..5538df766a7b 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -23,6 +23,7 @@ const { ObjectDefineProperty, + ObjectSetPrototypeOf, } = primordials; const { @@ -53,7 +54,16 @@ defineLazyProperties(Stream.Readable.prototype, 'internal/streams/operators', [ 'every', 'forEach', 'reduce', 'toArray', 'some', 'find', ], false); Stream.Writable = require('internal/streams/writable'); -defineLazyProperties(Stream, 'internal/streams/lazy', [ +// These modules export a single function rather than named properties. +// Set the prototype after creation to retain fast properties. +const lazy = ObjectSetPrototypeOf({ + Duplex: 'internal/streams/duplex', + Transform: 'internal/streams/transform', + PassThrough: 'internal/streams/passthrough', + duplexPair: 'internal/streams/duplexpair', + compose: 'internal/streams/compose', +}, null); +defineLazyProperties(Stream, lazy, [ 'Duplex', 'Transform', 'PassThrough', 'duplexPair', ]); defineLazyProperties(Stream, 'internal/streams/pipeline', ['pipeline']); @@ -61,7 +71,7 @@ const { addAbortSignal } = require('internal/streams/add-abort-signal'); Stream.addAbortSignal = addAbortSignal; Stream.finished = eos; Stream.destroy = destroyer; -defineLazyProperties(Stream, 'internal/streams/lazy', ['compose']); +defineLazyProperties(Stream, lazy, ['compose']); Stream.setDefaultHighWaterMark = setDefaultHighWaterMark; Stream.getDefaultHighWaterMark = getDefaultHighWaterMark; diff --git a/src/node_util.cc b/src/node_util.cc index 6d02ecea1daa..d71a7bd2b298 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -352,6 +352,7 @@ static void IsInsideNodeModules(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +template static void DefineLazyPropertiesGetter( Local name, const v8::PropertyCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); @@ -381,6 +382,10 @@ static void DefineLazyPropertiesGetter( // V8 will have scheduled an error to be thrown. return; } + if constexpr (return_module) { + info.GetReturnValue().Set(require_result); + return; + } Local ret; if (!require_result.As()->Get(context, name).ToLocal(&ret)) { // V8 will have scheduled an error to be thrown. @@ -390,13 +395,15 @@ static void DefineLazyPropertiesGetter( } static void DefineLazyProperties(const FunctionCallbackInfo& args) { - // target: object, id: string, keys: string[][, enumerable = true] + // target: object, source: string | object, keys: string[], + // enumerable: boolean = true CHECK_GE(args.Length(), 3); // target: Object where to define the lazy properties. CHECK(args[0]->IsObject()); - // id: Internal module to lazy-load where the API to expose are implemented. - CHECK(args[1]->IsString()); - // keys: Keys to map from `require(id)` and `target`. + // source: Internal module ID for named exports, or a map from property names + // to module IDs for modules whose entire exports object is the lazy value. + CHECK(args[1]->IsString() || args[1]->IsObject()); + // keys: Property names to expose on `target`. CHECK(args[2]->IsArray()); // enumerable: Whether the property should be enumerable. CHECK(args.Length() == 3 || args[3]->IsBoolean()); @@ -404,7 +411,7 @@ static void DefineLazyProperties(const FunctionCallbackInfo& args) { auto context = args.GetIsolate()->GetCurrentContext(); auto target = args[0].As(); - Local id = args[1]; + Local source = args[1]; v8::PropertyAttribute attribute = args.Length() == 3 || args[3]->IsTrue() ? v8::None : v8::DontEnum; @@ -417,12 +424,19 @@ static void DefineLazyProperties(const FunctionCallbackInfo& args) { return; } CHECK(key->IsString()); + Local id = source; + auto getter = DefineLazyPropertiesGetter; + if (source->IsObject()) { + if (!source.As()->Get(context, key).ToLocal(&id)) { + // V8 will have scheduled an error to be thrown. + return; + } + CHECK(id->IsString()); + getter = DefineLazyPropertiesGetter; + } if (target - ->SetLazyDataProperty(context, - key.As(), - DefineLazyPropertiesGetter, - id, - attribute) + ->SetLazyDataProperty( + context, key.As(), getter, id, attribute) .IsNothing()) { // V8 will have scheduled an error to be thrown. return; @@ -499,7 +513,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(ParseEnv); registry->Register(IsInsideNodeModules); registry->Register(DefineLazyProperties); - registry->Register(DefineLazyPropertiesGetter); + registry->Register(DefineLazyPropertiesGetter); + registry->Register(DefineLazyPropertiesGetter); registry->Register(ConstructSharedArrayBuffer); registry->Register(MarkPromiseAsHandled); }