From 03f6bb4fbeb8359fbb259e22a78920c846527cfa Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Mon, 19 Dec 2022 21:16:22 +0100 Subject: [PATCH 001/126] esm: move hook execution to separate thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antoine du Hamel Co-authored-by: Geoffrey Booth Co-authored-by: Michaƫl Zasso --- lib/internal/modules/esm/loader.js | 105 ++++++++++++++++++++++++----- lib/internal/modules/esm/worker.js | 72 ++++++++++++++++++++ lib/internal/process/esm_loader.js | 10 +-- lib/internal/worker.js | 24 ++++++- src/env-inl.h | 3 +- 5 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 lib/internal/modules/esm/worker.js diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 1fc25512b2c69c..96e816adc0433f 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -19,6 +19,10 @@ const { getOptionValue } = require('internal/options'); const { pathToFileURL } = require('internal/url'); const { emitExperimentalWarning } = require('internal/util'); +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); + const { getDefaultConditions, } = require('internal/modules/esm/utils'); @@ -33,6 +37,76 @@ function getTranslators() { return translators; } +class HooksProxy { + /** + * The communication vehicle constituting all memory shared between the main and the worker. + */ + #commsChannel = new SharedArrayBuffer(2048); // maybe use buffer.constants.MAX_LENGTH ? + /** + * The lock/unlock segment of the shared memory. Atomics require this to be a Int32Array. This + * segment is used to tell the main to sleep when the worker is processing, and vice versa + * (for the worker to sleep whilst the main thread is processing). + * 0 -> main sleeps + * 1 -> worker sleeps + */ + #lock = new Int32Array(this.#commsChannel, 0, 4); + /** + * The request & response segment of the shared memory. TextEncoder/Decoder (needed to convert + * requests & responses into a format supported by the comms channel) reads and writes with + * Uint8Array. + */ + #requestResponseData = new Uint8Array(this.#commsChannel, 4, 2044); + + #isReady = false; + + constructor() { + const { InternalWorker } = require('internal/worker'); + const worker = this.worker = new InternalWorker('internal/modules/esm/worker', { + stderr: false, + stdin: false, + stdout: false, + trackUnmanagedFds: false, + workerData: { commsChannel: this.#commsChannel }, + }); + worker.unref(); // ! Allows the process to eventually exit when worker is in its final sleep. + } + + makeRequest(type, ...args) { + if (!this.#isReady) { + const { kIsOnline } = require('internal/worker'); + if (!this.worker[kIsOnline]) { + debug('blocking main thread until the loader thread is ready'); + Atomics.wait(this.#lock, 0, 0); // ! Block this module until the worker is ready. + } + + this.#isReady = true; + } + + const { deserialize, serialize } = require('v8'); + + TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data + + const request = serialize({ type, args }); + debug(`sending ${type} request to worker (and then sleeping) with args`, ...args); + TypedArrayPrototypeSet(this.#requestResponseData, request); + + Atomics.store(this.#lock, 0, 0); // Send request to worker + Atomics.notify(this.#lock, 0); // Notify worker of new request + Atomics.wait(this.#lock, 0, 0); // Sleep until worker responds + + debug(`received response from worker for ${type} request`, this.#requestResponseData); + return deserialize(this.#requestResponseData); + } +} +ObjectSetPrototypeOf(HooksProxy.prototype, null); + +/** + * Multiple instances of ESMLoader exist for various, specific reasons (see code comments at site). + * In order to maintain consitency, we use a single worker (sandbox), which must sit apart of an + * individual ESMLoader instance. + */ +let hookProxy; + /** * @typedef {object} ExportedHooks * @property {Function} globalPreload Global preload hook. @@ -66,7 +140,6 @@ function getTranslators() { */ class ESMLoader { - #hooks; #defaultResolve; #defaultLoad; #importMetaInitializer; @@ -105,13 +178,12 @@ class ESMLoader { } } - addCustomLoaders(userLoaders) { - const { Hooks } = require('internal/modules/esm/hooks'); - this.#hooks = new Hooks(userLoaders); + addCustomLoaders() { + hookProxy ??= new HooksProxy(); // The worker adds custom hooks as part of its startup. } preload() { - this.#hooks?.preload(); + hookProxy?.makeRequest('preload'); } async eval( @@ -160,7 +232,7 @@ class ESMLoader { // We can skip cloning if there are no user-provided loaders because // the Node.js default resolve hook does not use import assertions. - if (this.#hooks?.hasCustomResolveOrLoadHooks) { + if (hookProxy) { // This exists only when there are custom hooks. // This method of cloning only works so long as import assertions cannot contain objects as values, // which they currently cannot per spec. importAssertionsForResolve = { @@ -307,6 +379,7 @@ class ESMLoader { if (this.#hooks) { return this.#hooks.resolve(originalSpecifier, parentURL, importAssertions); } + if (!this.#defaultResolve) { this.#defaultResolve = require('internal/modules/esm/resolve').defaultResolve; } @@ -316,8 +389,8 @@ class ESMLoader { importAssertions, parentURL, }; - return this.#defaultResolve(originalSpecifier, context); + return this.#defaultResolve(originalSpecifier, context); } /** @@ -329,8 +402,8 @@ class ESMLoader { */ async load(url, context) { let loadResult; - if (this.#hooks) { - loadResult = await this.#hooks.load(url, context); + if (hookProxy) { + loadResult = await hookProxy.makeRequest('load', url, context); } else { if (!this.#defaultLoad) { this.#defaultLoad = require('internal/modules/esm/load').defaultLoad; @@ -347,14 +420,14 @@ class ESMLoader { } importMetaInitialize(meta, context) { - if (this.#hooks) { - this.#hooks.importMetaInitialize(meta, context); - } else { - if (!this.#importMetaInitializer) { - this.#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; - } - this.#importMetaInitializer(meta, context); + if (hookProxy) { + return hookProxy.makeRequest('importMetaInitialize', meta, context); + } + + if (!this.#importMetaInitializer) { + this.#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; } + this.#importMetaInitializer(meta, context); } } diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js new file mode 100644 index 00000000000000..70381af0926ab4 --- /dev/null +++ b/lib/internal/modules/esm/worker.js @@ -0,0 +1,72 @@ +'use strict'; + +const { appendFileSync, writeFileSync } = require('fs'); +const { inspect } = require('util'); + +function debug(...args) { + appendFileSync('./__debug.log', args.map((arg) => inspect(arg)).join(' ') + '\n'); +} +writeFileSync('./__debug.log', 'worker for public ESM running\n'); + +// let debug = require('internal/util/debuglog').debuglog('esm_worker', (fn) => { +// debug = fn; +// }); + +const { + ReflectApply, + SafeWeakMap, +} = primordials; + +// Create this WeakMap in js-land because V8 has no C++ API for WeakMap. +internalBinding('module_wrap').callbackMap = new SafeWeakMap(); + +const { triggerUncaughtException } = internalBinding('errors'); +const { Hooks } = require('internal/modules/esm/hooks'); +const { workerData } = require('worker_threads'); +const { deserialize, serialize } = require('v8'); + +const { commsChannel } = workerData; +// lock = 0 -> main sleeps +// lock = 1 -> worker sleeps +const lock = new Int32Array(commsChannel, 0, 4); // Required by Atomics +const requestResponseData = new Uint8Array(commsChannel, 4, 2044); // For v8.serialize/deserialize + +const hooks = new Hooks(); + +function releaseLock() { + Atomics.store(lock, 0, 1); // Send response to main + Atomics.notify(lock, 0); // Notify main of new response +} + +releaseLock(); // Send 'ready' signal to main + +(async function setupESMWorker() { + const customLoaders = getOptionValue('--experimental-loader'); + hooks.addCustomLoaders(customLoaders); + + while (true) { + debug('blocking worker thread until main thread is ready'); + + Atomics.wait(lock, 0, 1); // This pauses the while loop + + debug('worker awakened'); + + let type, args; + try { + ({ type, args } = deserialize(requestResponseData)); + } catch(err) { + debug('deserialising request failed'); + throw err; + } +debug('worker request', { type, args }) + const response = await ReflectApply(hooks[type], publicESMLoader, args); + requestResponseData.fill(0); +debug('worker response', response) + requestResponseData.set(serialize(response)); + releaseLock(); + } +})().catch((err) => { + releaseLock(); + debug('worker failed to handle request', err); + triggerUncaughtException(err); +}); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 9a04e094e001c4..de2b8c20529a1f 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -12,8 +12,8 @@ const { const { pathToFileURL } = require('internal/url'); const { kEmptyObject } = require('internal/util'); -const esmLoader = new ESMLoader(); -exports.esmLoader = esmLoader; +const publicESMLoader = new ESMLoader(); +exports.esmLoader = publicESMLoader; // Module.runMain() causes loadESM() to re-run (which it should do); however, this should NOT cause // ESM to be re-initialized; doing so causes duplicate custom loaders to be added to the public @@ -59,8 +59,8 @@ async function initializeLoader() { // Hooks must then be added to external/public loader // (so they're triggered in userland) - esmLoader.addCustomLoaders(allLoaders); - esmLoader.preload(); + publicESMLoader.addCustomLoaders(allLoaders); + publicESMLoader.preload(); // Preload after loaders are added so they can be used if (preloadModules?.length) { @@ -91,7 +91,7 @@ function loadModulesInIsolation(parentURL, specifiers, loaders = []) { exports.loadESM = async function loadESM(callback) { try { await initializeLoader(); - await callback(esmLoader); + await callback(publicESMLoader); } catch (err) { if (hasUncaughtExceptionCaptureCallback()) { process._fatalException(err); diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 3b4b2f6ffaf0dc..47921179913bed 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -128,7 +128,13 @@ function assignEnvironmentData(data) { class Worker extends EventEmitter { constructor(filename, options = kEmptyObject) { super(); - debug(`[${threadId}] create new worker`, filename, options); + const isInternal = this instanceof InternalWorker; + debug( + `[${threadId}] create new worker`, + filename, + options, + `isInternal: ${isInternal}`, + ); if (options.execArgv) validateArray(options.execArgv, 'options.execArgv'); @@ -139,7 +145,10 @@ class Worker extends EventEmitter { } let url, doEval; - if (options.eval) { + if (isInternal) { + doEval = 'internal'; + url = `node:${filename}`; + } else if (options.eval) { if (typeof filename !== 'string') { throw new ERR_INVALID_ARG_VALUE( 'options.eval', @@ -195,6 +204,7 @@ class Worker extends EventEmitter { name = StringPrototypeTrim(options.name); } + debug('instantiating Worker.', `url: ${url}`, `doEval: ${doEval}`); // Set up the C++ handle for the worker, as well as some internal wiring. this[kHandle] = new WorkerImpl(url, env === process.env ? null : env, @@ -252,6 +262,7 @@ class Worker extends EventEmitter { type: messageTypes.LOAD_SCRIPT, filename, doEval, + isInternal, cwdCounter: cwdCounter || workerIo.sharedCwdCounter, workerData: options.workerData, environmentData, @@ -441,6 +452,13 @@ class Worker extends EventEmitter { } } +class InternalWorker extends Worker { + // eslint-disable-next-line no-useless-constructor + constructor(filename, options) { + super(filename, options); + } +} + function pipeWithoutWarning(source, dest) { const sourceMaxListeners = source._maxListeners; const destMaxListeners = dest._maxListeners; @@ -505,6 +523,7 @@ function eventLoopUtilization(util1, util2) { module.exports = { ownsProcessState, + kIsOnline, isMainThread, SHARE_ENV, resourceLimits: @@ -513,5 +532,6 @@ module.exports = { getEnvironmentData, assignEnvironmentData, threadId, + InternalWorker, Worker, }; diff --git a/src/env-inl.h b/src/env-inl.h index 7b135c7b83293a..f3192f19f551f6 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -636,8 +636,7 @@ inline bool Environment::owns_inspector() const { } inline bool Environment::should_create_inspector() const { - return (flags_ & EnvironmentFlags::kNoCreateInspector) == 0 && - !options_->test_runner && !options_->watch_mode; + return (flags_ & EnvironmentFlags::kNoCreateInspector) == 0; // ! FIXME: do not merge } inline bool Environment::tracks_unmanaged_fds() const { From a0bb205304bb96aa781a655c616e4d0628ebb2c7 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Mon, 9 Jan 2023 17:09:40 -0800 Subject: [PATCH 002/126] esm: resolve optionally returns import assertions --- doc/api/esm.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 6293779131e4e0..481f8cbc43bcb2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -752,7 +752,7 @@ changes: > signature may change. Do not rely on the API described below. * `specifier` {string} -* `context` {Object} +* `context` {object} * `conditions` {string\[]} Export conditions of the relevant `package.json` * `importAssertions` {Object} An object whose key-value pairs represent the assertions for the module to import @@ -846,16 +846,16 @@ changes: > deprecated, hooks (`getFormat`, `getSource`, and `transformSource`). * `url` {string} The URL returned by the `resolve` chain -* `context` {Object} +* `context` {object} * `conditions` {string\[]} Export conditions of the relevant `package.json` * `format` {string|null|undefined} The format optionally supplied by the `resolve` hook chain - * `importAssertions` {Object} + * `importAssertions` {object} * `nextLoad` {Function} The subsequent `load` hook in the chain, or the Node.js default `load` hook after the last user-supplied `load` hook * `specifier` {string} - * `context` {Object} -* Returns: {Object} + * `context` {object} +* Returns: {object} * `format` {string} * `shortCircuit` {undefined|boolean} A signal that this hook intends to terminate the chain of `resolve` hooks. **Default:** `false` @@ -943,7 +943,7 @@ changes: > In a previous version of this API, this hook was named > `getGlobalPreloadCode`. -* `context` {Object} Information to assist the preload code +* `context` {object} Information to assist the preload code * `port` {MessagePort} * Returns: {string} Code to run before application startup From cd16ccdf852c61869078e381f08dbaa22aca58f6 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 20 Dec 2022 23:28:55 +0100 Subject: [PATCH 003/126] port updates to `worker_thread` --- lib/internal/main/worker_thread.js | 78 ++++++++++++++++++------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 2ebfb849663eb6..e5489ce9908daf 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -89,17 +89,19 @@ port.on('message', (message) => { const { argv, cwdCounter, - filename, doEval, - workerData, environmentData, - publicPort, + filename, + hasStdin, manifestSrc, manifestURL, - hasStdin, + publicPort, + workerData, } = message; - if (argv !== undefined) { + debug(`LOAD_SCRIPT (${doEval}): ${filename}`); + + if (doEval !== 'internal' && argv !== undefined) { ArrayPrototypePushApply(process.argv, argv); } @@ -130,7 +132,7 @@ port.on('message', (message) => { if (manifestSrc) { require('internal/process/policy').setup(manifestSrc, manifestURL); } - setupUserModules(); + if (doEval !== 'internal') setupUserModules(); if (!hasStdin) process.stdin.push(null); @@ -138,31 +140,45 @@ port.on('message', (message) => { debug(`[${threadId}] starts worker script ${filename} ` + `(eval = ${doEval}) at cwd = ${process.cwd()}`); port.postMessage({ type: UP_AND_RUNNING }); - if (doEval === 'classic') { - const { evalScript } = require('internal/process/execution'); - const name = '[worker eval]'; - // This is necessary for CJS module compilation. - // TODO: pass this with something really internal. - ObjectDefineProperty(process, '_eval', { - __proto__: null, - configurable: true, - enumerable: true, - value: filename, - }); - ArrayPrototypeSplice(process.argv, 1, 0, name); - evalScript(name, filename); - } else if (doEval === 'module') { - const { evalModule } = require('internal/process/execution'); - PromisePrototypeThen(evalModule(filename), undefined, (e) => { - workerOnGlobalUncaughtException(e, true); - }); - } else { - // script filename - // runMain here might be monkey-patched by users in --require. - // XXX: the monkey-patchability here should probably be deprecated. - ArrayPrototypeSplice(process.argv, 1, 0, filename); - const CJSLoader = require('internal/modules/cjs/loader'); - CJSLoader.Module.runMain(filename); + switch (doEval) { + case 'internal': { + require(filename); + break; + } + + case 'classic': { + const { evalScript } = require('internal/process/execution'); + const name = '[worker eval]'; + // This is necessary for CJS module compilation. + // TODO: pass this with something really internal. + ObjectDefineProperty(process, '_eval', { + __proto__: null, + configurable: true, + enumerable: true, + value: filename, + }); + ArrayPrototypeSplice(process.argv, 1, 0, name); + evalScript(name, filename); + break; + } + + case 'module': { + const { evalModule } = require('internal/process/execution'); + PromisePrototypeThen(evalModule(filename), undefined, (e) => { + workerOnGlobalUncaughtException(e, true); + }); + break; + } + + default: { + // script filename + // runMain here might be monkey-patched by users in --require. + // XXX: the monkey-patchability here should probably be deprecated. + ArrayPrototypeSplice(process.argv, 1, 0, filename); + const CJSLoader = require('internal/modules/cjs/loader'); + CJSLoader.Module.runMain(filename); + break; + } } } else if (message.type === STDIO_PAYLOAD) { const { stream, chunks } = message; From 01f1191ef8d8271f3b3a232b180bed43d2c7dc02 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 20 Dec 2022 23:33:12 +0100 Subject: [PATCH 004/126] move `HooksProxy` to `hooks.js` --- lib/internal/modules/esm/hooks.js | 89 ++++++++++++++++++++++++++++-- lib/internal/modules/esm/loader.js | 86 +++++------------------------ 2 files changed, 97 insertions(+), 78 deletions(-) diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 513204bb9d7e48..268ba4cd1811e6 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -1,5 +1,16 @@ 'use strict'; +// let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { +// debug = fn; +// }); +const { appendFileSync } = require('fs'); +const { inspect } = require('util'); +function debug(...args) { + appendFileSync('/dev/fd/1', + 'Hooks: ' + args.map((arg) => inspect(arg)).join(' ') + '\n' + ); +} + const { ArrayPrototypeJoin, ArrayPrototypePush, @@ -10,6 +21,8 @@ const { SafeSet, StringPrototypeSlice, StringPrototypeToUpperCase, + TypedArrayPrototypeFill, + TypedArrayPrototypeSet, globalThis, } = primordials; @@ -83,15 +96,13 @@ class Hooks { ], }; - // Enable an optimization in ESMLoader.getModuleJob - hasCustomResolveOrLoadHooks = false; - // Cache URLs we've already validated to avoid repeated validation #validatedUrls = new SafeSet(); #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; constructor(userLoaders) { + debug('initialising'); this.#addCustomLoaders(userLoaders); } @@ -104,6 +115,7 @@ class Hooks { #addCustomLoaders( customLoaders = [], ) { + debug('adding custom loaders') for (let i = 0; i < customLoaders.length; i++) { const { exports, @@ -135,7 +147,6 @@ class Hooks { ); } if (load) { - this.hasCustomResolveOrLoadHooks = true; ArrayPrototypePush( this.#hooks.load, { @@ -515,9 +526,76 @@ class Hooks { }; } } - ObjectSetPrototypeOf(Hooks.prototype, null); +class HooksProxy { + /** + * The communication vehicle constituting all memory shared between the main and the worker. + */ + #commsChannel = new SharedArrayBuffer(2048); // maybe use buffer.constants.MAX_LENGTH ? + /** + * The lock/unlock segment of the shared memory. Atomics require this to be a Int32Array. This + * segment is used to tell the main to sleep when the worker is processing, and vice versa + * (for the worker to sleep whilst the main thread is processing). + * 0 -> main sleeps + * 1 -> worker sleeps + */ + #lock = new Int32Array(this.#commsChannel, 0, 4); + /** + * The request & response segment of the shared memory. TextEncoder/Decoder (needed to convert + * requests & responses into a format supported by the comms channel) reads and writes with + * Uint8Array. + */ + #requestResponseData = new Uint8Array(this.#commsChannel, 4, 2044); + + #isReady = false; + + constructor() { + const { InternalWorker } = require('internal/worker'); + const worker = this.worker = new InternalWorker('internal/modules/esm/worker', { + stderr: false, + stdin: false, + stdout: false, + trackUnmanagedFds: false, + workerData: { commsChannel: this.#commsChannel }, + }); + worker.unref(); // ! Allows the process to eventually exit when worker is in its final sleep. + } + + makeRequest(type, ...args) { + if (!this.#isReady) { + const { kIsOnline } = require('internal/worker'); + if (!this.worker[kIsOnline]) { + debug('blocking main thread until the loader thread is ready'); + Atomics.wait(this.#lock, 0, 0); // ! Block this module until the worker is ready. + debug('main thread awakened'); + } + + this.#isReady = true; + } + + const { deserialize, serialize } = require('v8'); + + TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data + + const request = serialize({ type, args }); + debug( + `sending ${type} request to worker (and then sleeping) with`, + ...(args.length ? args : ['no args']), + ); + TypedArrayPrototypeSet(this.#requestResponseData, request); + + Atomics.store(this.#lock, 0, 0); // Send request to worker + Atomics.notify(this.#lock, 0); // Notify worker of new request + debug('notified worker of request; sleeping main.') + Atomics.wait(this.#lock, 0, 0); // Sleep until worker responds + + debug(`received response from worker for ${type} request`, this.#requestResponseData); + return deserialize(this.#requestResponseData); + } +} +ObjectSetPrototypeOf(HooksProxy.prototype, null); + /** * A utility function to pluck the hooks from a user-defined loader. @@ -665,3 +743,4 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { exports.Hooks = Hooks; +exports.HooksProxy = HooksProxy; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 96e816adc0433f..e24bafd23baec7 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -37,75 +37,12 @@ function getTranslators() { return translators; } -class HooksProxy { - /** - * The communication vehicle constituting all memory shared between the main and the worker. - */ - #commsChannel = new SharedArrayBuffer(2048); // maybe use buffer.constants.MAX_LENGTH ? - /** - * The lock/unlock segment of the shared memory. Atomics require this to be a Int32Array. This - * segment is used to tell the main to sleep when the worker is processing, and vice versa - * (for the worker to sleep whilst the main thread is processing). - * 0 -> main sleeps - * 1 -> worker sleeps - */ - #lock = new Int32Array(this.#commsChannel, 0, 4); - /** - * The request & response segment of the shared memory. TextEncoder/Decoder (needed to convert - * requests & responses into a format supported by the comms channel) reads and writes with - * Uint8Array. - */ - #requestResponseData = new Uint8Array(this.#commsChannel, 4, 2044); - - #isReady = false; - - constructor() { - const { InternalWorker } = require('internal/worker'); - const worker = this.worker = new InternalWorker('internal/modules/esm/worker', { - stderr: false, - stdin: false, - stdout: false, - trackUnmanagedFds: false, - workerData: { commsChannel: this.#commsChannel }, - }); - worker.unref(); // ! Allows the process to eventually exit when worker is in its final sleep. - } - - makeRequest(type, ...args) { - if (!this.#isReady) { - const { kIsOnline } = require('internal/worker'); - if (!this.worker[kIsOnline]) { - debug('blocking main thread until the loader thread is ready'); - Atomics.wait(this.#lock, 0, 0); // ! Block this module until the worker is ready. - } - - this.#isReady = true; - } - - const { deserialize, serialize } = require('v8'); - - TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data - - const request = serialize({ type, args }); - debug(`sending ${type} request to worker (and then sleeping) with args`, ...args); - TypedArrayPrototypeSet(this.#requestResponseData, request); - - Atomics.store(this.#lock, 0, 0); // Send request to worker - Atomics.notify(this.#lock, 0); // Notify worker of new request - Atomics.wait(this.#lock, 0, 0); // Sleep until worker responds - - debug(`received response from worker for ${type} request`, this.#requestResponseData); - return deserialize(this.#requestResponseData); - } -} -ObjectSetPrototypeOf(HooksProxy.prototype, null); - /** * Multiple instances of ESMLoader exist for various, specific reasons (see code comments at site). * In order to maintain consitency, we use a single worker (sandbox), which must sit apart of an * individual ESMLoader instance. */ -let hookProxy; +let hooksProxy; /** * @typedef {object} ExportedHooks @@ -179,11 +116,14 @@ class ESMLoader { } addCustomLoaders() { - hookProxy ??= new HooksProxy(); // The worker adds custom hooks as part of its startup. + if (hooksProxy) { return; } + + const { HooksProxy } = require('internal/modules/esm/hooks'); + hooksProxy = new HooksProxy(); // The worker adds custom hooks as part of its startup. } preload() { - hookProxy?.makeRequest('preload'); + hooksProxy?.makeRequest('preload'); } async eval( @@ -232,7 +172,7 @@ class ESMLoader { // We can skip cloning if there are no user-provided loaders because // the Node.js default resolve hook does not use import assertions. - if (hookProxy) { // This exists only when there are custom hooks. + if (hooksProxy) { // This exists only when there are custom hooks. // This method of cloning only works so long as import assertions cannot contain objects as values, // which they currently cannot per spec. importAssertionsForResolve = { @@ -376,8 +316,8 @@ class ESMLoader { * @returns {Promise<{ format: string, url: URL['href'] }>} */ async resolve(originalSpecifier, parentURL, importAssertions) { - if (this.#hooks) { - return this.#hooks.resolve(originalSpecifier, parentURL, importAssertions); + if (hooksProxy) { + return hooksProxy.makeRequest('resolve', originalSpecifier, parentURL, importAssertions); } if (!this.#defaultResolve) { @@ -402,8 +342,8 @@ class ESMLoader { */ async load(url, context) { let loadResult; - if (hookProxy) { - loadResult = await hookProxy.makeRequest('load', url, context); + if (hooksProxy) { + loadResult = await hooksProxy.makeRequest('load', url, context); } else { if (!this.#defaultLoad) { this.#defaultLoad = require('internal/modules/esm/load').defaultLoad; @@ -420,8 +360,8 @@ class ESMLoader { } importMetaInitialize(meta, context) { - if (hookProxy) { - return hookProxy.makeRequest('importMetaInitialize', meta, context); + if (hooksProxy) { + return hooksProxy.makeRequest('importMetaInitialize', meta, context); } if (!this.#importMetaInitializer) { From 5f0229735a83da47da043a085a030ad4dfd134e9 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 20 Dec 2022 23:34:32 +0100 Subject: [PATCH 005/126] =?UTF-8?q?`ESMLoader::addCustomLoaders`=20?= =?UTF-8?q?=E2=86=92=20`ESMLoader::registerCustomLoaders`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/internal/modules/esm/loader.js | 2 +- lib/internal/process/esm_loader.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index e24bafd23baec7..8f35dc1be1e676 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -115,7 +115,7 @@ class ESMLoader { } } - addCustomLoaders() { + registerCustomLoaders() { if (hooksProxy) { return; } const { HooksProxy } = require('internal/modules/esm/hooks'); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index de2b8c20529a1f..6b4f4f41d6b7b5 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -43,6 +43,7 @@ async function initializeLoader() { const parentURL = pathToFileURL(cwd).href; + // TODO: move this to worker for (let i = 0; i < customLoaders.length; i++) { const customLoader = customLoaders[i]; @@ -53,13 +54,13 @@ async function initializeLoader() { kEmptyObject, ); - internalEsmLoader.addCustomLoaders(keyedExportsSublist); + internalEsmLoader.registerCustomLoaders(keyedExportsSublist); ArrayPrototypePushApply(allLoaders, keyedExportsSublist); } // Hooks must then be added to external/public loader // (so they're triggered in userland) - publicESMLoader.addCustomLoaders(allLoaders); + publicESMLoader.registerCustomLoaders(allLoaders); publicESMLoader.preload(); // Preload after loaders are added so they can be used @@ -77,7 +78,7 @@ function loadModulesInIsolation(parentURL, specifiers, loaders = []) { // between internal Node.js and userland. For example, a module with internal // state (such as a counter) should be independent. const internalEsmLoader = new ESMLoader(); - internalEsmLoader.addCustomLoaders(loaders); + internalEsmLoader.registerCustomLoaders(loaders); internalEsmLoader.preload(); // Importation must be handled by internal loader to avoid polluting userland From 820aa5560446062d0b695ae5ae90442ed597d400 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 20 Dec 2022 23:37:51 +0100 Subject: [PATCH 006/126] fix startup issues in hooks worker Hooks initialisation has a cart-before-the-horse issue (see fixme in worker) --- lib/internal/modules/esm/worker.js | 46 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index 70381af0926ab4..d3b16545be7fd2 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -1,16 +1,17 @@ 'use strict'; -const { appendFileSync, writeFileSync } = require('fs'); +const { appendFileSync } = require('fs'); const { inspect } = require('util'); -function debug(...args) { - appendFileSync('./__debug.log', args.map((arg) => inspect(arg)).join(' ') + '\n'); -} -writeFileSync('./__debug.log', 'worker for public ESM running\n'); - // let debug = require('internal/util/debuglog').debuglog('esm_worker', (fn) => { // debug = fn; // }); +function debug(...args) { + appendFileSync('/dev/fd/1', + 'esm_worker: ' + args.map((arg) => inspect(arg)).join(' ') + '\n' + ); +} +debug('worker running'); const { ReflectApply, @@ -20,29 +21,39 @@ const { // Create this WeakMap in js-land because V8 has no C++ API for WeakMap. internalBinding('module_wrap').callbackMap = new SafeWeakMap(); -const { triggerUncaughtException } = internalBinding('errors'); -const { Hooks } = require('internal/modules/esm/hooks'); -const { workerData } = require('worker_threads'); -const { deserialize, serialize } = require('v8'); +const { workerData: { commsChannel } } = require('worker_threads'); -const { commsChannel } = workerData; // lock = 0 -> main sleeps // lock = 1 -> worker sleeps const lock = new Int32Array(commsChannel, 0, 4); // Required by Atomics -const requestResponseData = new Uint8Array(commsChannel, 4, 2044); // For v8.serialize/deserialize - -const hooks = new Hooks(); +const requestResponseData = new Uint8Array(commsChannel, 4, 2044); // For v8.deserialize/serialize function releaseLock() { Atomics.store(lock, 0, 1); // Send response to main Atomics.notify(lock, 0); // Notify main of new response } -releaseLock(); // Send 'ready' signal to main - +/** + * ! Put everything possible within this function so errors get reported. + */ (async function setupESMWorker() { + debug('starting sandbox setup'); + const { initializeESM } = require('internal/modules/esm/utils'); + debug('initialising ESM'); + initializeESM(); + + const { getOptionValue } = require('internal/options'); const customLoaders = getOptionValue('--experimental-loader'); - hooks.addCustomLoaders(customLoaders); + const { Hooks } = require('internal/modules/esm/hooks'); + // FIXME: custom loaders need to already be import()'d. handle main-side and pass as a request? + const hooks = new Hooks(customLoaders); + debug('hooks inititalised'); + + // ! Put as little above this line as possible + releaseLock(); // Send 'ready' signal to main + debug('lock released; main notified worker is up.'); + + const { deserialize, serialize } = require('v8'); while (true) { debug('blocking worker thread until main thread is ready'); @@ -66,6 +77,7 @@ debug('worker response', response) releaseLock(); } })().catch((err) => { + const { triggerUncaughtException } = internalBinding('errors'); releaseLock(); debug('worker failed to handle request', err); triggerUncaughtException(err); From 231b9644b6a10684653d7aa43b4b6081e0ca410b Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 21 Dec 2022 00:01:35 +0100 Subject: [PATCH 007/126] WIP: move importing custom loaders back to main thread --- lib/internal/modules/esm/hooks.js | 38 ++++++++++++++++++++---------- lib/internal/modules/esm/loader.js | 4 ++-- lib/internal/process/esm_loader.js | 1 - 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 268ba4cd1811e6..cb2f8e87683ba7 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -550,7 +550,11 @@ class HooksProxy { #isReady = false; - constructor() { + /** + * @param {import('./loader.js).KeyedExports} customLoaders Exports from user-defined loaders + * (as returned by `ESMLoader.import()`). + */ + constructor(customLoaders) { const { InternalWorker } = require('internal/worker'); const worker = this.worker = new InternalWorker('internal/modules/esm/worker', { stderr: false, @@ -560,6 +564,22 @@ class HooksProxy { workerData: { commsChannel: this.#commsChannel }, }); worker.unref(); // ! Allows the process to eventually exit when worker is in its final sleep. + + debug('setting initial data for worker', customLoaders); + this.#setRequest('init', customLoaders); + } + + #setRequest(type, ...args) { + const { serialize } = require('v8'); + + TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data + + const request = serialize({ type, args }); + debug( + `sending ${type} request to worker (and then sleeping) with`, + ...(args.length ? args : ['no args']), + ); + TypedArrayPrototypeSet(this.#requestResponseData, request); } makeRequest(type, ...args) { @@ -574,22 +594,16 @@ class HooksProxy { this.#isReady = true; } - const { deserialize, serialize } = require('v8'); - - TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data - - const request = serialize({ type, args }); - debug( - `sending ${type} request to worker (and then sleeping) with`, - ...(args.length ? args : ['no args']), - ); - TypedArrayPrototypeSet(this.#requestResponseData, request); + this.#setRequest(type, ...args); Atomics.store(this.#lock, 0, 0); // Send request to worker Atomics.notify(this.#lock, 0); // Notify worker of new request - debug('notified worker of request; sleeping main.') + debug('notified worker of request; sleeping main.'); + Atomics.wait(this.#lock, 0, 0); // Sleep until worker responds + const { deserialize } = require('v8'); + debug(`received response from worker for ${type} request`, this.#requestResponseData); return deserialize(this.#requestResponseData); } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 8f35dc1be1e676..a97f7fa334b369 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -115,11 +115,11 @@ class ESMLoader { } } - registerCustomLoaders() { + registerCustomLoaders(customLoaders) { if (hooksProxy) { return; } const { HooksProxy } = require('internal/modules/esm/hooks'); - hooksProxy = new HooksProxy(); // The worker adds custom hooks as part of its startup. + hooksProxy = new HooksProxy(customLoaders); // The worker adds custom hooks as part of its startup. } preload() { diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 6b4f4f41d6b7b5..335e98d70c7416 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -43,7 +43,6 @@ async function initializeLoader() { const parentURL = pathToFileURL(cwd).href; - // TODO: move this to worker for (let i = 0; i < customLoaders.length; i++) { const customLoader = customLoaders[i]; From ed412d5502c90fff770a46fd0f26db3e546646c5 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Thu, 22 Dec 2022 21:07:21 +0100 Subject: [PATCH 008/126] =?UTF-8?q?=F0=9F=8E=89=20`/test-esm-example-loade?= =?UTF-8?q?r.mjs`=20is=20passing!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/internal/main/worker_thread.js | 2 - lib/internal/modules/esm/hooks.js | 45 ++++--------------- lib/internal/modules/esm/loader.js | 17 +++---- lib/internal/modules/esm/utils.js | 55 ++++++++++++++++++++++- lib/internal/modules/esm/worker.js | 37 ++-------------- lib/internal/process/esm_loader.js | 71 ++---------------------------- 6 files changed, 75 insertions(+), 152 deletions(-) diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index e5489ce9908daf..428a326c60b9a4 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -99,8 +99,6 @@ port.on('message', (message) => { workerData, } = message; - debug(`LOAD_SCRIPT (${doEval}): ${filename}`); - if (doEval !== 'internal' && argv !== undefined) { ArrayPrototypePushApply(process.argv, argv); } diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index cb2f8e87683ba7..3c6fb45dfdf101 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -1,16 +1,5 @@ 'use strict'; -// let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { -// debug = fn; -// }); -const { appendFileSync } = require('fs'); -const { inspect } = require('util'); -function debug(...args) { - appendFileSync('/dev/fd/1', - 'Hooks: ' + args.map((arg) => inspect(arg)).join(' ') + '\n' - ); -} - const { ArrayPrototypeJoin, ArrayPrototypePush, @@ -102,7 +91,6 @@ class Hooks { #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; constructor(userLoaders) { - debug('initialising'); this.#addCustomLoaders(userLoaders); } @@ -115,7 +103,6 @@ class Hooks { #addCustomLoaders( customLoaders = [], ) { - debug('adding custom loaders') for (let i = 0; i < customLoaders.length; i++) { const { exports, @@ -554,7 +541,7 @@ class HooksProxy { * @param {import('./loader.js).KeyedExports} customLoaders Exports from user-defined loaders * (as returned by `ESMLoader.import()`). */ - constructor(customLoaders) { + constructor() { const { InternalWorker } = require('internal/worker'); const worker = this.worker = new InternalWorker('internal/modules/esm/worker', { stderr: false, @@ -564,47 +551,31 @@ class HooksProxy { workerData: { commsChannel: this.#commsChannel }, }); worker.unref(); // ! Allows the process to eventually exit when worker is in its final sleep. - - debug('setting initial data for worker', customLoaders); - this.#setRequest('init', customLoaders); - } - - #setRequest(type, ...args) { - const { serialize } = require('v8'); - - TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data - - const request = serialize({ type, args }); - debug( - `sending ${type} request to worker (and then sleeping) with`, - ...(args.length ? args : ['no args']), - ); - TypedArrayPrototypeSet(this.#requestResponseData, request); } makeRequest(type, ...args) { if (!this.#isReady) { const { kIsOnline } = require('internal/worker'); if (!this.worker[kIsOnline]) { - debug('blocking main thread until the loader thread is ready'); - Atomics.wait(this.#lock, 0, 0); // ! Block this module until the worker is ready. - debug('main thread awakened'); + Atomics.wait(this.#lock, 0, 0); // ! Block the main thread until the worker is ready. } this.#isReady = true; } - this.#setRequest(type, ...args); + const { serialize } = require('v8'); + + TypedArrayPrototypeFill(this.#requestResponseData, 0); // Erase handled request/response data + + const request = serialize({ type, args }); + TypedArrayPrototypeSet(this.#requestResponseData, request); Atomics.store(this.#lock, 0, 0); // Send request to worker Atomics.notify(this.#lock, 0); // Notify worker of new request - debug('notified worker of request; sleeping main.'); - Atomics.wait(this.#lock, 0, 0); // Sleep until worker responds const { deserialize } = require('v8'); - debug(`received response from worker for ${type} request`, this.#requestResponseData); return deserialize(this.#requestResponseData); } } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index a97f7fa334b369..4edb5d68e969a5 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -18,11 +18,6 @@ const { const { getOptionValue } = require('internal/options'); const { pathToFileURL } = require('internal/url'); const { emitExperimentalWarning } = require('internal/util'); - -let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { - debug = fn; -}); - const { getDefaultConditions, } = require('internal/modules/esm/utils'); @@ -115,15 +110,15 @@ class ESMLoader { } } - registerCustomLoaders(customLoaders) { + registerCustomLoaders() { if (hooksProxy) { return; } - const { HooksProxy } = require('internal/modules/esm/hooks'); - hooksProxy = new HooksProxy(customLoaders); // The worker adds custom hooks as part of its startup. - } + const customLoaders = getOptionValue('--experimental-loader'); + + if (!customLoaders.length) { return; } // Nothing to do. - preload() { - hooksProxy?.makeRequest('preload'); + const { HooksProxy } = require('internal/modules/esm/hooks'); + hooksProxy = new HooksProxy(); // The worker adds custom hooks as part of its startup. } async eval( diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index bf3edc86518b4c..f0d9d6a7d38e11 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -1,6 +1,8 @@ 'use strict'; + const { ArrayIsArray, + ArrayPrototypePushApply, SafeSet, SafeWeakMap, ObjectFreeze, @@ -10,9 +12,9 @@ const { ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; - const { getOptionValue } = require('internal/options'); - +const { pathToFileURL } = require('internal/url'); +const { kEmptyObject } = require('internal/util'); const { setImportModuleDynamicallyCallback, setInitializeImportMetaObjectCallback, @@ -97,9 +99,58 @@ function initializeESM() { setImportModuleDynamicallyCallback(importModuleDynamicallyCallback); } +async function initializeHooks() { + const customLoaderPaths = getOptionValue('--experimental-loader'); + + let cwd; + try { + cwd = process.cwd() + '/'; + } catch { + cwd = '/'; + } + + const { ESMLoader } = require('internal/modules/esm/loader'); + const internalEsmLoader = new ESMLoader(); + const importedCustomLoaders = []; + + const parentURL = pathToFileURL(cwd).href; + + for (let i = 0; i < customLoaderPaths.length; i++) { + const customLoaderPath = customLoaderPaths[i]; + + // Importation must be handled by internal loader to avoid polluting user-land + const keyedExportsSublist = await internalEsmLoader.import( + [customLoaderPath], // Import can handle multiple paths, but custom loaders must be sequential + parentURL, + kEmptyObject, + ); + + ArrayPrototypePushApply(importedCustomLoaders, keyedExportsSublist); + } + + const { Hooks } = require('internal/modules/esm/hooks'); + const hooks = new Hooks(importedCustomLoaders); + + hooks.preload(); + + // Preload after loaders are added so they can be used + const preloadModules = getOptionValue('--import'); + + if (preloadModules) { + await internalEsmLoader.import( + preloadModules, + parentURL, + kEmptyObject, + ); + } + + return hooks; +} + module.exports = { setCallbackForWrap, initializeESM, + initializeHooks, getDefaultConditions, getConditionsSet, }; diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index d3b16545be7fd2..c49134b5d7e5c1 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -1,18 +1,5 @@ 'use strict'; -const { appendFileSync } = require('fs'); -const { inspect } = require('util'); - -// let debug = require('internal/util/debuglog').debuglog('esm_worker', (fn) => { -// debug = fn; -// }); -function debug(...args) { - appendFileSync('/dev/fd/1', - 'esm_worker: ' + args.map((arg) => inspect(arg)).join(' ') + '\n' - ); -} -debug('worker running'); - const { ReflectApply, SafeWeakMap, @@ -34,51 +21,35 @@ function releaseLock() { } /** - * ! Put everything possible within this function so errors get reported. + * ! Run everything possible within this function so errors get reported. */ (async function setupESMWorker() { - debug('starting sandbox setup'); - const { initializeESM } = require('internal/modules/esm/utils'); - debug('initialising ESM'); + const { initializeESM, initializeHooks } = require('internal/modules/esm/utils'); initializeESM(); - const { getOptionValue } = require('internal/options'); - const customLoaders = getOptionValue('--experimental-loader'); - const { Hooks } = require('internal/modules/esm/hooks'); - // FIXME: custom loaders need to already be import()'d. handle main-side and pass as a request? - const hooks = new Hooks(customLoaders); - debug('hooks inititalised'); + const hooks = await initializeHooks(); // ! Put as little above this line as possible releaseLock(); // Send 'ready' signal to main - debug('lock released; main notified worker is up.'); const { deserialize, serialize } = require('v8'); while (true) { - debug('blocking worker thread until main thread is ready'); - Atomics.wait(lock, 0, 1); // This pauses the while loop - debug('worker awakened'); - let type, args; try { ({ type, args } = deserialize(requestResponseData)); } catch(err) { - debug('deserialising request failed'); throw err; } -debug('worker request', { type, args }) - const response = await ReflectApply(hooks[type], publicESMLoader, args); + const response = await ReflectApply(hooks[type], hooks, args); requestResponseData.fill(0); -debug('worker response', response) requestResponseData.set(serialize(response)); releaseLock(); } })().catch((err) => { const { triggerUncaughtException } = internalBinding('errors'); releaseLock(); - debug('worker failed to handle request', err); triggerUncaughtException(err); }); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 335e98d70c7416..21c83c463626a0 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -1,19 +1,12 @@ 'use strict'; -const { - ArrayIsArray, - ArrayPrototypePushApply, -} = primordials; - const { ESMLoader } = require('internal/modules/esm/loader'); const { hasUncaughtExceptionCaptureCallback, } = require('internal/process/execution'); -const { pathToFileURL } = require('internal/url'); -const { kEmptyObject } = require('internal/util'); -const publicESMLoader = new ESMLoader(); -exports.esmLoader = publicESMLoader; +const esmLoader = new ESMLoader(); +exports.esmLoader = esmLoader; // Module.runMain() causes loadESM() to re-run (which it should do); however, this should NOT cause // ESM to be re-initialized; doing so causes duplicate custom loaders to be added to the public @@ -27,71 +20,15 @@ let isESMInitialized = false; async function initializeLoader() { if (isESMInitialized) { return; } - const { getOptionValue } = require('internal/options'); - const customLoaders = getOptionValue('--experimental-loader'); - const preloadModules = getOptionValue('--import'); - - let cwd; - try { - cwd = process.cwd() + '/'; - } catch { - cwd = '/'; - } - - const internalEsmLoader = new ESMLoader(); - const allLoaders = []; - - const parentURL = pathToFileURL(cwd).href; - - for (let i = 0; i < customLoaders.length; i++) { - const customLoader = customLoaders[i]; - - // Importation must be handled by internal loader to avoid polluting user-land - const keyedExportsSublist = await internalEsmLoader.import( - [customLoader], - parentURL, - kEmptyObject, - ); - - internalEsmLoader.registerCustomLoaders(keyedExportsSublist); - ArrayPrototypePushApply(allLoaders, keyedExportsSublist); - } - - // Hooks must then be added to external/public loader - // (so they're triggered in userland) - publicESMLoader.registerCustomLoaders(allLoaders); - publicESMLoader.preload(); - - // Preload after loaders are added so they can be used - if (preloadModules?.length) { - await loadModulesInIsolation(parentURL, preloadModules, allLoaders); - } + esmLoader.registerCustomLoaders(); isESMInitialized = true; } -function loadModulesInIsolation(parentURL, specifiers, loaders = []) { - if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; } - - // A separate loader instance is necessary to avoid cross-contamination - // between internal Node.js and userland. For example, a module with internal - // state (such as a counter) should be independent. - const internalEsmLoader = new ESMLoader(); - internalEsmLoader.registerCustomLoaders(loaders); - internalEsmLoader.preload(); - - // Importation must be handled by internal loader to avoid polluting userland - return internalEsmLoader.import( - specifiers, - parentURL, - kEmptyObject, - ); -} - exports.loadESM = async function loadESM(callback) { try { await initializeLoader(); - await callback(publicESMLoader); + await callback(esmLoader); } catch (err) { if (hasUncaughtExceptionCaptureCallback()) { process._fatalException(err); From 4776a6d664ec4146f9d33c7dadad1f3b098ee381 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 22 Dec 2022 22:02:02 -0800 Subject: [PATCH 009/126] Fix tests that are loading modules/esm/worker.js on the main thread --- lib/internal/modules/esm/worker.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index c49134b5d7e5c1..d12a0c99001f73 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -8,7 +8,9 @@ const { // Create this WeakMap in js-land because V8 has no C++ API for WeakMap. internalBinding('module_wrap').callbackMap = new SafeWeakMap(); -const { workerData: { commsChannel } } = require('worker_threads'); +const { isMainThread, workerData } = require('worker_threads'); +if (isMainThread) { return; } // Needed to pass some tests that happen to load this file on the main thread +const { commsChannel } = workerData; // lock = 0 -> main sleeps // lock = 1 -> worker sleeps From 2aed49a1e4fba890ba451a7d0a53d025315aab69 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sat, 24 Dec 2022 00:09:27 +0100 Subject: [PATCH 010/126] WIP: convert resolve to sync --- doc/api/esm.md | 29 +++++--- lib/internal/modules/esm/hooks.js | 17 ++--- .../modules/esm/initialize_import_meta.js | 29 ++++---- lib/internal/modules/esm/loader.js | 18 ++--- lib/internal/modules/esm/module_job.js | 7 +- lib/internal/modules/esm/resolve.js | 6 +- lib/internal/modules/esm/translators.js | 7 +- lib/internal/modules/esm/worker.js | 19 +++--- lib/internal/process/esm_loader.js | 6 +- .../test-esm-import-meta-resolve.mjs | 68 +++++++++++-------- test/es-module/test-esm-loader-search.js | 2 +- test/es-module/test-esm-resolve-type.mjs | 20 +++--- 12 files changed, 131 insertions(+), 97 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 481f8cbc43bcb2..28c05f173ed22b 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -7,6 +7,9 @@ ```js -const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); +const dependencyAsset = import.meta.resolve('component-lib/asset.css'); ``` `import.meta.resolve` also accepts a second argument which is the parent module @@ -357,11 +364,11 @@ from which to resolve from: ```js -await import.meta.resolve('./dep', import.meta.url); +import.meta.resolve('./dep', import.meta.url); ``` -This function is asynchronous because the ES module resolver in Node.js is -allowed to be asynchronous. +This function is synchronous because the ES module resolver in Node.js is +synchronous. ## Interoperability with CommonJS @@ -734,6 +741,9 @@ prevent unintentional breaks in the chain. - ```js const dependencyAsset = import.meta.resolve('component-lib/asset.css'); ``` @@ -361,15 +362,10 @@ const dependencyAsset = import.meta.resolve('component-lib/asset.css'); `import.meta.resolve` also accepts a second argument which is the parent module from which to resolve from: - - ```js import.meta.resolve('./dep', import.meta.url); ``` -This function is synchronous because the ES module resolver in Node.js is -synchronous. - ## Interoperability with CommonJS ### `import` statements @@ -741,9 +737,6 @@ prevent unintentional breaks in the chain.