module: refator ESM loader for adding future synchronous hooks

This lays the foundation for supporting synchronous hooks proposed
in https://github.com/nodejs/loaders/pull/198 for ESM.

- Corrects and adds several JSDoc comments for internal functions
  of the ESM loader, as well as explaining how require() for
  import CJS work in the special resolve/load paths. This doesn't
  consolidate it with import in require(esm) yet due to caching
  differences, which is left as a TODO.
- The moduleProvider passed into ModuleJob is replaced as
  moduleOrModulePromise, we call the translators directly in the
  ESM loader and verify it right after loading for clarity.
- Reuse a few refactored out helpers for require(esm) in
  getModuleJobForRequire().

PR-URL: https://github.com/nodejs/node/pull/54769
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Joyee Cheung 2024-09-17 20:38:33 +02:00 committed by GitHub
parent 7014e50ca3
commit 3ac5b49d85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 266 additions and 158 deletions

View file

@ -8,7 +8,6 @@ const {
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseResolve,
ReflectApply,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
SafePromiseAllReturnArrayLike,
@ -56,13 +55,12 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
);
class ModuleJobBase {
constructor(url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) {
constructor(url, importAttributes, isMain, inspectBrk) {
this.importAttributes = importAttributes;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
this.url = url;
this.module = moduleWrapMaybePromise;
}
}
@ -70,21 +68,29 @@ class ModuleJobBase {
* its dependencies, over time. */
class ModuleJob extends ModuleJobBase {
#loader = null;
// `loader` is the Loader instance used for loading dependencies.
constructor(loader, url, importAttributes = { __proto__: null },
moduleProvider, isMain, inspectBrk, sync = false) {
const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
super(url, importAttributes, modulePromise, isMain, inspectBrk);
this.#loader = loader;
// Expose the promise to the ModuleWrap directly for linking below.
// `this.module` is also filled in below.
this.modulePromise = modulePromise;
if (sync) {
this.module = this.modulePromise;
/**
* @param {ModuleLoader} loader The ESM loader.
* @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module.
* @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
* @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.
*/
constructor(loader, url, importAttributes = { __proto__: null },
moduleOrModulePromise, isMain, inspectBrk, isForRequireInImportedCJS = false) {
super(url, importAttributes, isMain, inspectBrk);
this.#loader = loader;
// Expose the promise to the ModuleWrap directly for linking below.
if (isForRequireInImportedCJS) {
this.module = moduleOrModulePromise;
assert(this.module instanceof ModuleWrap);
this.modulePromise = PromiseResolve(this.module);
} else {
this.modulePromise = PromiseResolve(this.modulePromise);
this.modulePromise = moduleOrModulePromise;
}
// Promise for the list of all dependencyJobs.
@ -123,7 +129,7 @@ class ModuleJob extends ModuleJobBase {
for (let idx = 0; idx < moduleRequests.length; idx++) {
const { specifier, attributes } = moduleRequests[idx];
const dependencyJobPromise = this.#loader.getModuleJob(
const dependencyJobPromise = this.#loader.getModuleJobForImport(
specifier, this.url, attributes,
);
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
@ -288,14 +294,33 @@ class ModuleJob extends ModuleJobBase {
}
}
// This is a fully synchronous job and does not spawn additional threads in any way.
// All the steps are ensured to be synchronous and it throws on instantiating
// an asynchronous graph.
/**
* This is a fully synchronous job and does not spawn additional threads in any way.
* All the steps are ensured to be synchronous and it throws on instantiating
* an asynchronous graph. It also disallows CJS <-> ESM cycles.
*
* This is used for ES modules loaded via require(esm). Modules loaded by require() in
* imported CJS are handled by ModuleJob with the isForRequireInImportedCJS set to true instead.
* The two currently have different caching behaviors.
* TODO(joyeecheung): consolidate this with the isForRequireInImportedCJS variant of ModuleJob.
*/
class ModuleJobSync extends ModuleJobBase {
#loader = null;
/**
* @param {ModuleLoader} loader The ESM loader.
* @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module.
* @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
*/
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) {
super(url, importAttributes, moduleWrap, isMain, inspectBrk, true);
super(url, importAttributes, isMain, inspectBrk, true);
this.#loader = loader;
this.module = moduleWrap;
assert(this.module instanceof ModuleWrap);
// Store itself into the cache first before linking in case there are circular