-
-
Notifications
You must be signed in to change notification settings - Fork 33.5k
module: refactor and clarify async loader hook customizations #60278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -54,7 +54,6 @@ const { | |||||
} = require('internal/modules/esm/resolve'); | ||||||
const { | ||||||
getDefaultConditions, | ||||||
loaderWorkerId, | ||||||
} = require('internal/modules/esm/utils'); | ||||||
const { deserializeError } = require('internal/error_serdes'); | ||||||
const { | ||||||
|
@@ -105,7 +104,39 @@ function defineImportAssertionAlias(context) { | |||||
|
||||||
// [2] `validate...()`s throw the wrong error | ||||||
|
||||||
class Hooks { | ||||||
/** | ||||||
* @typedef {{ format: ModuleFormat, source: ModuleSource }} LoadResult | ||||||
*/ | ||||||
|
||||||
/** | ||||||
* @typedef {{ format: ModuleFormat, url: string, importAttributes: Record<string, string> }} ResolveResult | ||||||
*/ | ||||||
|
||||||
/** | ||||||
* Interface for classes that implement asynchronous loader hooks that can be attached to the ModuleLoader | ||||||
* via `ModuleLoader.#setAsyncLoaderHooks()`. | ||||||
* @typedef {object} AsyncLoaderHooks | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the reason for the “customization“ and “customization hooks“ names is to avoid confusion where some might think that these hooks only affect loading, when they also impact resolution and other things. It hasn’t been a priority to update the code, but as long as we’re adding new code and defining new names, I think the “customization“ name is more informative and correct.
Suggested change
And elsewhere that the code uses “loader hook“ that this branch touches. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case should I feel that "loader hooks" contain already ample information about it: it's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just a nit, do with it as you will. Another name instead of “customization hooks“ is “module hooks“ which is just as descriptive and the same number of letters as “loader hooks”. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate the brainstorm ;) though to me "foo hooks" means "it's hooking into foo/hook placed in foo", so I'd have some questions when I see "module hooks" - is it hooking into the code of user modules/hook placed in the modules? While it can be used in such a way that inject code into user modules, that is more of the output, instead of the process. "loader hooks" seem more intuitive because, there's a (I thought about just calling it |
||||||
* @property {boolean} allowImportMetaResolve Whether to allow the use of `import.meta.resolve`. | ||||||
* @property {(url: string, context: object, defaultLoad: Function) => Promise<LoadResult>} load | ||||||
* Calling the asynchronous `load` hook asynchronously. | ||||||
* @property {(url: string, context: object, defaultLoad: Function) => LoadResult} loadSync | ||||||
* Calling the asynchronous `load` hook synchronously. | ||||||
* @property {(originalSpecifier: string, parentURL: string, | ||||||
* importAttributes: Record<string, string>) => Promise<ResolveResult>} resolve | ||||||
* Calling the asynchronous `resolve` hook asynchronously. | ||||||
* @property {(originalSpecifier: string, parentURL: string, | ||||||
* importAttributes: Record<string, string>) => ResolveResult} resolveSync | ||||||
* Calling the asynchronous `resolve` hook synchronously. | ||||||
* @property {(specifier: string, parentURL: string) => any} register Register asynchronous loader hooks | ||||||
* @property {() => void} waitForLoaderHookInitialization Force loading of hooks. | ||||||
*/ | ||||||
|
||||||
/** | ||||||
* @implements {AsyncLoaderHooks} | ||||||
* Instances of this class run directly on the loader hook worker thread and customize the module | ||||||
* loading of the hooks worker itself. | ||||||
*/ | ||||||
class AsyncLoaderHooksOnLoaderHookWorker { | ||||||
#chains = { | ||||||
/** | ||||||
* Phase 1 of 2 in ESM loading. | ||||||
|
@@ -452,7 +483,7 @@ class Hooks { | |||||
}; | ||||||
} | ||||||
|
||||||
forceLoadHooks() { | ||||||
waitForLoaderHookInitialization() { | ||||||
// No-op | ||||||
} | ||||||
|
||||||
|
@@ -462,14 +493,20 @@ class Hooks { | |||||
return meta; | ||||||
} | ||||||
} | ||||||
ObjectSetPrototypeOf(Hooks.prototype, null); | ||||||
ObjectSetPrototypeOf(AsyncLoaderHooksOnLoaderHookWorker.prototype, null); | ||||||
|
||||||
/** | ||||||
* There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so | ||||||
* there is only 1 MessageChannel. | ||||||
* There is only one loader hook thread for each non-loader-hook worker thread | ||||||
* (i.e. the non-loader-hook thread and any worker threads that are not loader hook workers themselves), | ||||||
* so there is only 1 MessageChannel. | ||||||
*/ | ||||||
let MessageChannel; | ||||||
class HooksProxy { | ||||||
|
||||||
/** | ||||||
* Abstraction over a worker thread that runs the asynchronous module loader hooks. | ||||||
* Instances of this class run on the non-loader-hook thread and communicate with the loader hooks worker thread. | ||||||
*/ | ||||||
class AsyncLoaderHookWorker { | ||||||
/** | ||||||
* Shared memory. Always use Atomics method to read or write to it. | ||||||
* @type {Int32Array} | ||||||
|
@@ -503,7 +540,7 @@ class HooksProxy { | |||||
const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH); | ||||||
this.#lock = new Int32Array(lock); | ||||||
|
||||||
this.#worker = new InternalWorker(loaderWorkerId, { | ||||||
this.#worker = new InternalWorker('internal/modules/esm/worker', { | ||||||
stderr: false, | ||||||
stdin: false, | ||||||
stdout: false, | ||||||
|
@@ -644,7 +681,7 @@ class HooksProxy { | |||||
this.#importMetaInitializer(meta, context, loader); | ||||||
} | ||||||
} | ||||||
ObjectSetPrototypeOf(HooksProxy.prototype, null); | ||||||
ObjectSetPrototypeOf(AsyncLoaderHookWorker.prototype, null); | ||||||
|
||||||
// TODO(JakobJingleheimer): Remove this when loaders go "stable". | ||||||
let globalPreloadWarningWasEmitted = false; | ||||||
|
@@ -757,6 +794,95 @@ function nextHookFactory(current, meta, { validateArgs, validateOutput }) { | |||||
); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @type {AsyncLoaderHookWorker} | ||||||
* Worker instance used to run async loader hooks in a separate thread. This is a singleton for each | ||||||
* non-loader-hook worker thread (i.e. the main thread and any worker threads that are not | ||||||
* loader hook workers themselves). | ||||||
*/ | ||||||
let asyncLoaderHookWorker; | ||||||
/** | ||||||
* Get the AsyncLoaderHookWorker instance. If it is not defined, then create a new one. | ||||||
* @returns {AsyncLoaderHookWorker} | ||||||
*/ | ||||||
function getAsyncLoaderHookWorker() { | ||||||
asyncLoaderHookWorker ??= new AsyncLoaderHookWorker(); | ||||||
return asyncLoaderHookWorker; | ||||||
} | ||||||
|
||||||
/** | ||||||
* @implements {AsyncLoaderHooks} | ||||||
* Instances of this class are created in the non-loader-hook thread and communicate with the worker thread | ||||||
* spawned to run the async loader hooks. | ||||||
*/ | ||||||
class AsyncLoaderHooksProxiedToLoaderHookWorker { | ||||||
|
||||||
allowImportMetaResolve = true; | ||||||
|
||||||
/** | ||||||
* Instantiate a module loader that uses user-provided custom loader hooks. | ||||||
*/ | ||||||
constructor() { | ||||||
getAsyncLoaderHookWorker(); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Register some loader specifier. | ||||||
* @param {string} originalSpecifier The specified URL path of the loader to | ||||||
* be registered. | ||||||
* @param {string} parentURL The parent URL from where the loader will be | ||||||
* registered if using it package name as specifier | ||||||
* @param {any} [data] Arbitrary data to be passed from the custom loader | ||||||
* (user-land) to the worker. | ||||||
* @param {any[]} [transferList] Objects in `data` that are changing ownership | ||||||
* @param {boolean} [isInternal] For internal loaders that should not be publicly exposed. | ||||||
* @returns {{ format: string, url: URL['href'] }} | ||||||
*/ | ||||||
register(originalSpecifier, parentURL, data, transferList, isInternal) { | ||||||
return asyncLoaderHookWorker.makeSyncRequest('register', transferList, originalSpecifier, parentURL, | ||||||
data, isInternal); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Resolve the location of the module. | ||||||
* @param {string} originalSpecifier The specified URL path of the module to | ||||||
* be resolved. | ||||||
* @param {string} [parentURL] The URL path of the module's parent. | ||||||
* @param {ImportAttributes} importAttributes Attributes from the import | ||||||
* statement or expression. | ||||||
* @returns {{ format: string, url: URL['href'] }} | ||||||
*/ | ||||||
resolve(originalSpecifier, parentURL, importAttributes) { | ||||||
return asyncLoaderHookWorker.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes); | ||||||
} | ||||||
|
||||||
resolveSync(originalSpecifier, parentURL, importAttributes) { | ||||||
// This happens only as a result of `import.meta.resolve` calls, which must be sync per spec. | ||||||
return asyncLoaderHookWorker.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Provide source that is understood by one of Node's translators. | ||||||
* @param {URL['href']} url The URL/path of the module to be loaded | ||||||
* @param {object} [context] Metadata about the module | ||||||
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} | ||||||
*/ | ||||||
load(url, context) { | ||||||
return asyncLoaderHookWorker.makeAsyncRequest('load', undefined, url, context); | ||||||
} | ||||||
loadSync(url, context) { | ||||||
return asyncLoaderHookWorker.makeSyncRequest('load', undefined, url, context); | ||||||
} | ||||||
|
||||||
importMetaInitialize(meta, context, loader) { | ||||||
asyncLoaderHookWorker.importMetaInitialize(meta, context, loader); | ||||||
} | ||||||
|
||||||
waitForLoaderHookInitialization() { | ||||||
asyncLoaderHookWorker.waitForWorker(); | ||||||
} | ||||||
} | ||||||
|
||||||
exports.Hooks = Hooks; | ||||||
exports.HooksProxy = HooksProxy; | ||||||
exports.AsyncLoaderHooksProxiedToLoaderHookWorker = AsyncLoaderHooksProxiedToLoaderHookWorker; | ||||||
exports.AsyncLoaderHooksOnLoaderHookWorker = AsyncLoaderHooksOnLoaderHookWorker; | ||||||
exports.AsyncLoaderHookWorker = AsyncLoaderHookWorker; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is removed as a drive-by, the
callbackMap
is no longer used after #48510