Skip to content

Commit 2d48d17

Browse files
joyeecheungaduh95
authored andcommitted
module: refactor and clarify async loader hook customizations
- This updates the comments that assume loader hooks must be async - Differentiate the sync/async loader hook paths in naming `#customizations` is now `#asyncLoaderHooks` to make it clear it's from the async APIs. - Differentiate the paths running on the loader hook thread (affects the loading of async other loader hooks and are async) v.s. paths on the main thread calling out to code on the loader hook thread (do not handle loading of other async loader hooks, and can be sync by blocking). - `Hooks` is now `AsyncLoaderHooksOnLoaderHookWorker` - `CustomizedModuleLoader` is now `AsyncLoaderHooksProxiedToLoaderHookWorker` and moved into `lib/internal/modules/esm/hooks.js` as it implements the same interface as `AsyncLoaderHooksOnLoaderHookWorker` - `HooksProxy` is now `AsyncLoaderHookWorker` - Adjust the JSDoc accordingly - Clarify the "loader worker" as the "async loader hook worker" i.e. when there's no _async_ loader hook registered, there won't be this worker, to avoid the misconception that this worker is spawned unconditionally. - The code run on the loader hook worker to process `--experimental-loader` is moved into `lib/internal/modules/esm/worker.js` for clarity. - The initialization configuration `forceDefaultLoader` is split into `shouldSpawnLoaderHookWorker` and `shouldPreloadModules` as those can be separate. - `--experimental-vm-modules` is now processed during pre-execution and no longer part of the initialization of the built-in ESM loader, as it only exposes the vm APIs of ESM, and is unrelated to built-in ESM loading. PR-URL: #60278 Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 064c8c5 commit 2d48d17

File tree

9 files changed

+355
-309
lines changed

9 files changed

+355
-309
lines changed

lib/internal/main/worker_thread.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ const {
1111
ObjectDefineProperty,
1212
PromisePrototypeThen,
1313
RegExpPrototypeExec,
14-
SafeWeakMap,
1514
globalThis: {
1615
SharedArrayBuffer,
1716
},
1817
} = primordials;
1918

2019
const {
2120
prepareWorkerThreadExecution,
22-
setupUserModules,
21+
initializeModuleLoaders,
2322
markBootstrapComplete,
2423
} = require('internal/process/pre_execution');
2524

@@ -138,11 +137,13 @@ port.on('message', (message) => {
138137
workerIo.sharedCwdCounter = cwdCounter;
139138
}
140139

141-
const isLoaderWorker =
142-
doEval === 'internal' &&
143-
filename === require('internal/modules/esm/utils').loaderWorkerId;
144-
// Disable custom loaders in loader worker.
145-
setupUserModules(isLoaderWorker);
140+
const isLoaderHookWorker = (filename === 'internal/modules/esm/worker' && doEval === 'internal');
141+
if (!isLoaderHookWorker) {
142+
// If we are in the loader hook worker, delay the module loader initializations until
143+
// initializeAsyncLoaderHooksOnLoaderHookWorker() which needs to run preloads
144+
// after the asynchronous loader hooks are registered.
145+
initializeModuleLoaders({ shouldSpawnLoaderHookWorker: true, shouldPreloadModules: true });
146+
}
146147

147148
if (!hasStdin)
148149
process.stdin.push(null);
@@ -152,9 +153,10 @@ port.on('message', (message) => {
152153
port.postMessage({ type: UP_AND_RUNNING });
153154
switch (doEval) {
154155
case 'internal': {
155-
// Create this WeakMap in js-land because V8 has no C++ API for WeakMap.
156-
internalBinding('module_wrap').callbackMap = new SafeWeakMap();
157-
require(filename)(workerData, publicPort);
156+
// Currently the only user of internal eval is the async loader hook thread.
157+
assert(isLoaderHookWorker, `Unexpected internal eval ${filename}`);
158+
const setupModuleWorker = require('internal/modules/esm/worker');
159+
setupModuleWorker(workerData, publicPort);
158160
break;
159161
}
160162

lib/internal/modules/esm/hooks.js

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ const {
5454
} = require('internal/modules/esm/resolve');
5555
const {
5656
getDefaultConditions,
57-
loaderWorkerId,
5857
} = require('internal/modules/esm/utils');
5958
const { deserializeError } = require('internal/error_serdes');
6059
const {
@@ -105,7 +104,39 @@ function defineImportAssertionAlias(context) {
105104

106105
// [2] `validate...()`s throw the wrong error
107106

108-
class Hooks {
107+
/**
108+
* @typedef {{ format: ModuleFormat, source: ModuleSource }} LoadResult
109+
*/
110+
111+
/**
112+
* @typedef {{ format: ModuleFormat, url: string, importAttributes: Record<string, string> }} ResolveResult
113+
*/
114+
115+
/**
116+
* Interface for classes that implement asynchronous loader hooks that can be attached to the ModuleLoader
117+
* via `ModuleLoader.#setAsyncLoaderHooks()`.
118+
* @typedef {object} AsyncLoaderHooks
119+
* @property {boolean} allowImportMetaResolve Whether to allow the use of `import.meta.resolve`.
120+
* @property {(url: string, context: object, defaultLoad: Function) => Promise<LoadResult>} load
121+
* Calling the asynchronous `load` hook asynchronously.
122+
* @property {(url: string, context: object, defaultLoad: Function) => LoadResult} loadSync
123+
* Calling the asynchronous `load` hook synchronously.
124+
* @property {(originalSpecifier: string, parentURL: string,
125+
* importAttributes: Record<string, string>) => Promise<ResolveResult>} resolve
126+
* Calling the asynchronous `resolve` hook asynchronously.
127+
* @property {(originalSpecifier: string, parentURL: string,
128+
* importAttributes: Record<string, string>) => ResolveResult} resolveSync
129+
* Calling the asynchronous `resolve` hook synchronously.
130+
* @property {(specifier: string, parentURL: string) => any} register Register asynchronous loader hooks
131+
* @property {() => void} waitForLoaderHookInitialization Force loading of hooks.
132+
*/
133+
134+
/**
135+
* @implements {AsyncLoaderHooks}
136+
* Instances of this class run directly on the loader hook worker thread and customize the module
137+
* loading of the hooks worker itself.
138+
*/
139+
class AsyncLoaderHooksOnLoaderHookWorker {
109140
#chains = {
110141
/**
111142
* Phase 1 of 2 in ESM loading.
@@ -452,7 +483,7 @@ class Hooks {
452483
};
453484
}
454485

455-
forceLoadHooks() {
486+
waitForLoaderHookInitialization() {
456487
// No-op
457488
}
458489

@@ -462,14 +493,20 @@ class Hooks {
462493
return meta;
463494
}
464495
}
465-
ObjectSetPrototypeOf(Hooks.prototype, null);
496+
ObjectSetPrototypeOf(AsyncLoaderHooksOnLoaderHookWorker.prototype, null);
466497

467498
/**
468-
* There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so
469-
* there is only 1 MessageChannel.
499+
* There is only one loader hook thread for each non-loader-hook worker thread
500+
* (i.e. the non-loader-hook thread and any worker threads that are not loader hook workers themselves),
501+
* so there is only 1 MessageChannel.
470502
*/
471503
let MessageChannel;
472-
class HooksProxy {
504+
505+
/**
506+
* Abstraction over a worker thread that runs the asynchronous module loader hooks.
507+
* Instances of this class run on the non-loader-hook thread and communicate with the loader hooks worker thread.
508+
*/
509+
class AsyncLoaderHookWorker {
473510
/**
474511
* Shared memory. Always use Atomics method to read or write to it.
475512
* @type {Int32Array}
@@ -503,7 +540,7 @@ class HooksProxy {
503540
const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH);
504541
this.#lock = new Int32Array(lock);
505542

506-
this.#worker = new InternalWorker(loaderWorkerId, {
543+
this.#worker = new InternalWorker('internal/modules/esm/worker', {
507544
stderr: false,
508545
stdin: false,
509546
stdout: false,
@@ -644,7 +681,7 @@ class HooksProxy {
644681
this.#importMetaInitializer(meta, context, loader);
645682
}
646683
}
647-
ObjectSetPrototypeOf(HooksProxy.prototype, null);
684+
ObjectSetPrototypeOf(AsyncLoaderHookWorker.prototype, null);
648685

649686
// TODO(JakobJingleheimer): Remove this when loaders go "stable".
650687
let globalPreloadWarningWasEmitted = false;
@@ -757,6 +794,95 @@ function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
757794
);
758795
}
759796

797+
/**
798+
* @type {AsyncLoaderHookWorker}
799+
* Worker instance used to run async loader hooks in a separate thread. This is a singleton for each
800+
* non-loader-hook worker thread (i.e. the main thread and any worker threads that are not
801+
* loader hook workers themselves).
802+
*/
803+
let asyncLoaderHookWorker;
804+
/**
805+
* Get the AsyncLoaderHookWorker instance. If it is not defined, then create a new one.
806+
* @returns {AsyncLoaderHookWorker}
807+
*/
808+
function getAsyncLoaderHookWorker() {
809+
asyncLoaderHookWorker ??= new AsyncLoaderHookWorker();
810+
return asyncLoaderHookWorker;
811+
}
812+
813+
/**
814+
* @implements {AsyncLoaderHooks}
815+
* Instances of this class are created in the non-loader-hook thread and communicate with the worker thread
816+
* spawned to run the async loader hooks.
817+
*/
818+
class AsyncLoaderHooksProxiedToLoaderHookWorker {
819+
820+
allowImportMetaResolve = true;
821+
822+
/**
823+
* Instantiate a module loader that uses user-provided custom loader hooks.
824+
*/
825+
constructor() {
826+
getAsyncLoaderHookWorker();
827+
}
828+
829+
/**
830+
* Register some loader specifier.
831+
* @param {string} originalSpecifier The specified URL path of the loader to
832+
* be registered.
833+
* @param {string} parentURL The parent URL from where the loader will be
834+
* registered if using it package name as specifier
835+
* @param {any} [data] Arbitrary data to be passed from the custom loader
836+
* (user-land) to the worker.
837+
* @param {any[]} [transferList] Objects in `data` that are changing ownership
838+
* @param {boolean} [isInternal] For internal loaders that should not be publicly exposed.
839+
* @returns {{ format: string, url: URL['href'] }}
840+
*/
841+
register(originalSpecifier, parentURL, data, transferList, isInternal) {
842+
return asyncLoaderHookWorker.makeSyncRequest('register', transferList, originalSpecifier, parentURL,
843+
data, isInternal);
844+
}
845+
846+
/**
847+
* Resolve the location of the module.
848+
* @param {string} originalSpecifier The specified URL path of the module to
849+
* be resolved.
850+
* @param {string} [parentURL] The URL path of the module's parent.
851+
* @param {ImportAttributes} importAttributes Attributes from the import
852+
* statement or expression.
853+
* @returns {{ format: string, url: URL['href'] }}
854+
*/
855+
resolve(originalSpecifier, parentURL, importAttributes) {
856+
return asyncLoaderHookWorker.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
857+
}
858+
859+
resolveSync(originalSpecifier, parentURL, importAttributes) {
860+
// This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
861+
return asyncLoaderHookWorker.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
862+
}
863+
864+
/**
865+
* Provide source that is understood by one of Node's translators.
866+
* @param {URL['href']} url The URL/path of the module to be loaded
867+
* @param {object} [context] Metadata about the module
868+
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
869+
*/
870+
load(url, context) {
871+
return asyncLoaderHookWorker.makeAsyncRequest('load', undefined, url, context);
872+
}
873+
loadSync(url, context) {
874+
return asyncLoaderHookWorker.makeSyncRequest('load', undefined, url, context);
875+
}
876+
877+
importMetaInitialize(meta, context, loader) {
878+
asyncLoaderHookWorker.importMetaInitialize(meta, context, loader);
879+
}
880+
881+
waitForLoaderHookInitialization() {
882+
asyncLoaderHookWorker.waitForWorker();
883+
}
884+
}
760885

761-
exports.Hooks = Hooks;
762-
exports.HooksProxy = HooksProxy;
886+
exports.AsyncLoaderHooksProxiedToLoaderHookWorker = AsyncLoaderHooksProxiedToLoaderHookWorker;
887+
exports.AsyncLoaderHooksOnLoaderHookWorker = AsyncLoaderHooksOnLoaderHookWorker;
888+
exports.AsyncLoaderHookWorker = AsyncLoaderHookWorker;

0 commit comments

Comments
 (0)