Skip to content

Commit c89c934

Browse files
joyeecheungtargos
authored andcommittedOct 4, 2024
module: refator ESM loader for adding future synchronous hooks
This lays the foundation for supporting synchronous hooks proposed in nodejs/loaders#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: #54769 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 397ae41 commit c89c934

File tree

3 files changed

+264
-156
lines changed

3 files changed

+264
-156
lines changed
 

‎lib/internal/modules/esm/loader.js

+211-111
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,11 @@ class ModuleLoader {
206206
}
207207

208208
async eval(source, url, isEntryPoint = false) {
209-
const evalInstance = (url) => {
210-
return compileSourceTextModule(url, source, this);
211-
};
212209
const { ModuleJob } = require('internal/modules/esm/module_job');
210+
const wrap = compileSourceTextModule(url, source, this);
213211
const module = await onImport.tracePromise(async () => {
214212
const job = new ModuleJob(
215-
this, url, undefined, evalInstance, false, false);
213+
this, url, undefined, wrap, false, false);
216214
this.loadCache.set(url, undefined, job);
217215
const { module } = await job.run(isEntryPoint);
218216
return module;
@@ -230,40 +228,49 @@ class ModuleLoader {
230228
}
231229

232230
/**
233-
* Get a (possibly still pending) module job from the cache,
234-
* or create one and return its Promise.
235-
* @param {string} specifier The string after `from` in an `import` statement,
236-
* or the first parameter of an `import()`
237-
* expression
238-
* @param {string | undefined} parentURL The URL of the module importing this
239-
* one, unless this is the Node.js entry
240-
* point.
241-
* @param {Record<string, string>} importAttributes Validations for the
242-
* module import.
243-
* @returns {Promise<ModuleJob>} The (possibly pending) module job
231+
* Get a (possibly not yet fully linked) module job from the cache, or create one and return its Promise.
232+
* @param {string} specifier The module request of the module to be resolved. Typically, what's
233+
* requested by `import '<specifier>'` or `import('<specifier>')`.
234+
* @param {string} [parentURL] The URL of the module where the module request is initiated.
235+
* It's undefined if it's from the root module.
236+
* @param {ImportAttributes} importAttributes Attributes from the import statement or expression.
237+
* @returns {Promise<ModuleJobBase}
244238
*/
245-
async getModuleJob(specifier, parentURL, importAttributes) {
239+
async getModuleJobForImport(specifier, parentURL, importAttributes) {
246240
const resolveResult = await this.resolve(specifier, parentURL, importAttributes);
247-
return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes);
241+
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, false);
248242
}
249243

250-
getModuleJobSync(specifier, parentURL, importAttributes) {
244+
/**
245+
* Similar to {@link getModuleJobForImport} but it's used for `require()` resolved by the ESM loader
246+
* in imported CJS modules. This runs synchronously and when it returns, the module job's module
247+
* requests are all linked.
248+
* @param {string} specifier See {@link getModuleJobForImport}
249+
* @param {string} [parentURL] See {@link getModuleJobForImport}
250+
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
251+
* @returns {Promise<ModuleJobBase}
252+
*/
253+
getModuleJobForRequireInImportedCJS(specifier, parentURL, importAttributes) {
251254
const resolveResult = this.resolveSync(specifier, parentURL, importAttributes);
252-
return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes, true);
255+
return this.#getJobFromResolveResult(resolveResult, parentURL, importAttributes, true);
253256
}
254257

255-
getJobFromResolveResult(resolveResult, parentURL, importAttributes, sync) {
258+
/**
259+
* Given a resolved module request, obtain a ModuleJobBase from it - if it's already cached,
260+
* return the cached ModuleJobBase. Otherwise, load its source and translate it into a ModuleWrap first.
261+
* @param {{ format: string, url: string }} resolveResult Resolved module request.
262+
* @param {string} [parentURL] See {@link getModuleJobForImport}
263+
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
264+
* @param {boolean} isForRequireInImportedCJS Whether this is done for require() in imported CJS.
265+
* @returns {ModuleJobBase}
266+
*/
267+
#getJobFromResolveResult(resolveResult, parentURL, importAttributes, isForRequireInImportedCJS = false) {
256268
const { url, format } = resolveResult;
257269
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
258270
let job = this.loadCache.get(url, resolvedImportAttributes.type);
259271

260-
// CommonJS will set functions for lazy job evaluation.
261-
if (typeof job === 'function') {
262-
this.loadCache.set(url, undefined, job = job());
263-
}
264-
265272
if (job === undefined) {
266-
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, sync);
273+
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, isForRequireInImportedCJS);
267274
}
268275

269276
return job;
@@ -336,13 +343,9 @@ class ModuleLoader {
336343
assert(protocol === 'file:' || protocol === 'node:' || protocol === 'data:');
337344
}
338345

339-
const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes);
340-
let resolveResult = this.#resolveCache.get(requestKey, parentURL);
341-
if (resolveResult == null) {
342-
resolveResult = this.defaultResolve(specifier, parentURL, importAttributes);
343-
this.#resolveCache.set(requestKey, parentURL, resolveResult);
344-
}
345-
346+
// TODO(joyeecheung): consolidate cache behavior and use resolveSync() and
347+
// loadSync() here.
348+
const resolveResult = this.#cachedDefaultResolve(specifier, parentURL, importAttributes);
346349
const { url, format } = resolveResult;
347350
if (!getOptionValue('--experimental-require-module')) {
348351
throw new ERR_REQUIRE_ESM(url, true);
@@ -371,23 +374,16 @@ class ModuleLoader {
371374
const loadResult = defaultLoadSync(url, { format, importAttributes });
372375
const {
373376
format: finalFormat,
374-
responseURL,
375377
source,
376378
} = loadResult;
377379

378-
this.validateLoadResult(url, finalFormat);
379380
if (finalFormat === 'wasm') {
380381
assert.fail('WASM is currently unsupported by require(esm)');
381382
}
382383

383-
const translator = getTranslators().get(finalFormat);
384-
if (!translator) {
385-
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
386-
}
387-
388384
const isMain = (parentURL === undefined);
389-
const wrap = FunctionPrototypeCall(translator, this, responseURL, source, isMain);
390-
assert(wrap instanceof ModuleWrap); // No asynchronous translators should be called.
385+
const wrap = this.#translate(url, finalFormat, source, isMain);
386+
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
391387

392388
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
393389
process.send({ 'watch:import': [url] });
@@ -416,33 +412,96 @@ class ModuleLoader {
416412
}
417413

418414
/**
419-
* Create and cache an object representing a loaded module.
420-
* @param {string} url The absolute URL that was resolved for this module
421-
* @param {Record<string, string>} importAttributes Validations for the
422-
* module import.
423-
* @param {string} [parentURL] The absolute URL of the module importing this
424-
* one, unless this is the Node.js entry point
425-
* @param {string} [format] The format hint possibly returned by the
426-
* `resolve` hook
427-
* @returns {Promise<ModuleJob>} The (possibly pending) module job
415+
* Translate a loaded module source into a ModuleWrap. This is run synchronously,
416+
* but the translator may return the ModuleWrap in a Promise.
417+
* @param {stirng} url URL of the module to be translated.
418+
* @param {string} format Format of the module to be translated. This is used to find
419+
* matching translators.
420+
* @param {ModuleSource} source Source of the module to be translated.
421+
* @param {boolean} isMain Whether the module to be translated is the entry point.
422+
* @returns {ModuleWrap | Promise<ModuleWrap>}
423+
*/
424+
#translate(url, format, source, isMain) {
425+
this.validateLoadResult(url, format);
426+
const translator = getTranslators().get(format);
427+
428+
if (!translator) {
429+
throw new ERR_UNKNOWN_MODULE_FORMAT(format, url);
430+
}
431+
432+
return FunctionPrototypeCall(translator, this, url, source, isMain);
433+
}
434+
435+
/**
436+
* Load a module and translate it into a ModuleWrap for require() in imported CJS.
437+
* This is run synchronously, and the translator always return a ModuleWrap synchronously.
438+
* @param {string} url URL of the module to be translated.
439+
* @param {object} loadContext See {@link load}
440+
* @param {boolean} isMain Whether the module to be translated is the entry point.
441+
* @returns {ModuleWrap}
428442
*/
429-
#createModuleJob(url, importAttributes, parentURL, format, sync) {
430-
const callTranslator = ({ format: finalFormat, responseURL, source }, isMain) => {
431-
const translator = getTranslators().get(finalFormat);
443+
loadAndTranslateForRequireInImportedCJS(url, loadContext, isMain) {
444+
const { format: formatFromLoad, source } = this.#loadSync(url, loadContext);
445+
446+
if (formatFromLoad === 'wasm') { // require(wasm) is not supported.
447+
throw new ERR_UNKNOWN_MODULE_FORMAT(formatFromLoad, url);
448+
}
432449

433-
if (!translator) {
434-
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
450+
if (formatFromLoad === 'module' || formatFromLoad === 'module-typescript') {
451+
if (!getOptionValue('--experimental-require-module')) {
452+
throw new ERR_REQUIRE_ESM(url, true);
435453
}
454+
}
436455

437-
return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
438-
};
439-
const context = { format, importAttributes };
456+
let finalFormat = formatFromLoad;
457+
if (formatFromLoad === 'commonjs') {
458+
finalFormat = 'require-commonjs';
459+
}
460+
if (formatFromLoad === 'commonjs-typescript') {
461+
finalFormat = 'require-commonjs-typescript';
462+
}
440463

441-
const moduleProvider = sync ?
442-
(url, isMain) => callTranslator(this.loadSync(url, context), isMain) :
443-
async (url, isMain) => callTranslator(await this.load(url, context), isMain);
464+
const wrap = this.#translate(url, finalFormat, source, isMain);
465+
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
466+
return wrap;
467+
}
468+
469+
/**
470+
* Load a module and translate it into a ModuleWrap for ordinary imported ESM.
471+
* This is run asynchronously.
472+
* @param {string} url URL of the module to be translated.
473+
* @param {object} loadContext See {@link load}
474+
* @param {boolean} isMain Whether the module to be translated is the entry point.
475+
* @returns {Promise<ModuleWrap>}
476+
*/
477+
async loadAndTranslate(url, loadContext, isMain) {
478+
const { format, source } = await this.load(url, loadContext);
479+
return this.#translate(url, format, source, isMain);
480+
}
481+
482+
/**
483+
* Load a module and translate it into a ModuleWrap, and create a ModuleJob from it.
484+
* This runs synchronously. If isForRequireInImportedCJS is true, the module should be linked
485+
* by the time this returns. Otherwise it may still have pending module requests.
486+
* @param {string} url The URL that was resolved for this module.
487+
* @param {ImportAttributes} importAttributes See {@link getModuleJobForImport}
488+
* @param {string} [parentURL] See {@link getModuleJobForImport}
489+
* @param {string} [format] The format hint possibly returned by the `resolve` hook
490+
* @param {boolean} isForRequireInImportedCJS Whether this module job is created for require()
491+
* in imported CJS.
492+
* @returns {ModuleJobBase} The (possibly pending) module job
493+
*/
494+
#createModuleJob(url, importAttributes, parentURL, format, isForRequireInImportedCJS) {
495+
const context = { format, importAttributes };
444496

445497
const isMain = parentURL === undefined;
498+
let moduleOrModulePromise;
499+
if (isForRequireInImportedCJS) {
500+
moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, isMain);
501+
} else {
502+
moduleOrModulePromise = this.loadAndTranslate(url, context, isMain);
503+
}
504+
446505
const inspectBrk = (
447506
isMain &&
448507
getOptionValue('--inspect-brk')
@@ -457,10 +516,10 @@ class ModuleLoader {
457516
this,
458517
url,
459518
importAttributes,
460-
moduleProvider,
519+
moduleOrModulePromise,
461520
isMain,
462521
inspectBrk,
463-
sync,
522+
isForRequireInImportedCJS,
464523
);
465524

466525
this.loadCache.set(url, importAttributes.type, job);
@@ -479,7 +538,7 @@ class ModuleLoader {
479538
*/
480539
async import(specifier, parentURL, importAttributes, isEntryPoint = false) {
481540
return onImport.tracePromise(async () => {
482-
const moduleJob = await this.getModuleJob(specifier, parentURL, importAttributes);
541+
const moduleJob = await this.getModuleJobForImport(specifier, parentURL, importAttributes);
483542
const { module } = await moduleJob.run(isEntryPoint);
484543
return module.getNamespace();
485544
}, {
@@ -504,39 +563,72 @@ class ModuleLoader {
504563
}
505564

506565
/**
507-
* Resolve the location of the module.
508-
* @param {string} originalSpecifier The specified URL path of the module to
509-
* be resolved.
510-
* @param {string} [parentURL] The URL path of the module's parent.
511-
* @param {ImportAttributes} importAttributes Attributes from the import
512-
* statement or expression.
513-
* @returns {{ format: string, url: URL['href'] }}
566+
* Resolve a module request to a URL identifying the location of the module. Handles customization hooks,
567+
* if any.
568+
* @param {string|URL} specifier The module request of the module to be resolved. Typically, what's
569+
* requested by `import specifier`, `import(specifier)` or
570+
* `import.meta.resolve(specifier)`.
571+
* @param {string} [parentURL] The URL of the module where the module request is initiated.
572+
* It's undefined if it's from the root module.
573+
* @param {ImportAttributes} importAttributes Attributes from the import statement or expression.
574+
* @returns {Promise<{format: string, url: string}>}
514575
*/
515-
resolve(originalSpecifier, parentURL, importAttributes) {
516-
originalSpecifier = `${originalSpecifier}`;
517-
if (this.#customizations) {
518-
return this.#customizations.resolve(originalSpecifier, parentURL, importAttributes);
576+
resolve(specifier, parentURL, importAttributes) {
577+
specifier = `${specifier}`;
578+
if (this.#customizations) { // Only has module.register hooks.
579+
return this.#customizations.resolve(specifier, parentURL, importAttributes);
519580
}
520-
const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAttributes);
581+
return this.#cachedDefaultResolve(specifier, parentURL, importAttributes);
582+
}
583+
584+
/**
585+
* Either return a cached resolution, or perform the default resolution which is synchronous, and
586+
* cache the result.
587+
* @param {string} specifier See {@link resolve}.
588+
* @param {string} [parentURL] See {@link resolve}.
589+
* @param {ImportAttributes} importAttributes See {@link resolve}.
590+
* @returns {{ format: string, url: string }}
591+
*/
592+
#cachedDefaultResolve(specifier, parentURL, importAttributes) {
593+
const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes);
521594
const cachedResult = this.#resolveCache.get(requestKey, parentURL);
522595
if (cachedResult != null) {
523596
return cachedResult;
524597
}
525-
const result = this.defaultResolve(originalSpecifier, parentURL, importAttributes);
598+
const result = this.defaultResolve(specifier, parentURL, importAttributes);
526599
this.#resolveCache.set(requestKey, parentURL, result);
527600
return result;
528601
}
529602

530603
/**
531-
* Just like `resolve` except synchronous. This is here specifically to support
532-
* `import.meta.resolve` which must happen synchronously.
604+
* This is the default resolve step for future synchronous hooks, which incorporates asynchronous hooks
605+
* from module.register() which are run in a blocking fashion for it to be synchronous.
606+
* @param {string|URL} specifier See {@link resolveSync}.
607+
* @param {{ parentURL?: string, importAttributes: ImportAttributes}} context See {@link resolveSync}.
608+
* @returns {{ format: string, url: string }}
533609
*/
534-
resolveSync(originalSpecifier, parentURL, importAttributes) {
535-
originalSpecifier = `${originalSpecifier}`;
610+
#resolveAndMaybeBlockOnLoaderThread(specifier, context) {
536611
if (this.#customizations) {
537-
return this.#customizations.resolveSync(originalSpecifier, parentURL, importAttributes);
612+
return this.#customizations.resolveSync(specifier, context.parentURL, context.importAttributes);
538613
}
539-
return this.defaultResolve(originalSpecifier, parentURL, importAttributes);
614+
return this.#cachedDefaultResolve(specifier, context.parentURL, context.importAttributes);
615+
}
616+
617+
/**
618+
* Similar to {@link resolve}, but the results are always synchronously returned. If there are any
619+
* asynchronous resolve hooks from module.register(), it will block until the results are returned
620+
* from the loader thread for this to be synchornous.
621+
* This is here to support `import.meta.resolve()`, `require()` in imported CJS, and
622+
* future synchronous hooks.
623+
*
624+
* TODO(joyeecheung): consolidate the cache behavior and use this in require(esm).
625+
* @param {string|URL} specifier See {@link resolve}.
626+
* @param {string} [parentURL] See {@link resolve}.
627+
* @param {ImportAttributes} [importAttributes] See {@link resolve}.
628+
* @returns {{ format: string, url: string }}
629+
*/
630+
resolveSync(specifier, parentURL, importAttributes = { __proto__: null }) {
631+
return this.#resolveAndMaybeBlockOnLoaderThread(`${specifier}`, { parentURL, importAttributes });
540632
}
541633

542634
/**
@@ -558,41 +650,49 @@ class ModuleLoader {
558650
}
559651

560652
/**
561-
* Provide source that is understood by one of Node's translators.
562-
* @param {URL['href']} url The URL/path of the module to be loaded
563-
* @param {object} [context] Metadata about the module
653+
* Provide source that is understood by one of Node's translators. Handles customization hooks,
654+
* if any.
655+
* @param {string} url The URL of the module to be loaded.
656+
* @param {object} context Metadata about the module
564657
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
565658
*/
566659
async load(url, context) {
660+
if (this.#customizations) {
661+
return this.#customizations.load(url, context);
662+
}
663+
567664
defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
568-
const result = this.#customizations ?
569-
await this.#customizations.load(url, context) :
570-
await defaultLoad(url, context);
571-
this.validateLoadResult(url, result?.format);
572-
return result;
665+
return defaultLoad(url, context);
573666
}
574667

575-
loadSync(url, context) {
576-
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
577-
578-
let result = this.#customizations ?
579-
this.#customizations.loadSync(url, context) :
580-
defaultLoadSync(url, context);
581-
let format = result?.format;
582-
if (format === 'module' || format === 'module-typescript') {
583-
throw new ERR_REQUIRE_ESM(url, true);
584-
}
585-
if (format === 'commonjs') {
586-
format = 'require-commonjs';
587-
result = { __proto__: result, format };
588-
}
589-
if (format === 'commonjs-typescript') {
590-
format = 'require-commonjs-typescript';
591-
result = { __proto__: result, format };
668+
/**
669+
* This is the default load step for future synchronous hooks, which incorporates asynchronous hooks
670+
* from module.register() which are run in a blocking fashion for it to be synchronous.
671+
* @param {string} url See {@link load}
672+
* @param {object} context See {@link load}
673+
* @returns {{ format: ModuleFormat, source: ModuleSource }}
674+
*/
675+
#loadAndMaybeBlockOnLoaderThread(url, context) {
676+
if (this.#customizations) {
677+
return this.#customizations.loadSync(url, context);
592678
}
679+
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
680+
return defaultLoadSync(url, context);
681+
}
593682

594-
this.validateLoadResult(url, format);
595-
return result;
683+
/**
684+
* Similar to {@link load} but this is always run synchronously. If there are asynchronous hooks
685+
* from module.register(), this blocks on the loader thread for it to return synchronously.
686+
*
687+
* This is here to support `require()` in imported CJS and future synchronous hooks.
688+
*
689+
* TODO(joyeecheung): consolidate the cache behavior and use this in require(esm).
690+
* @param {string} url See {@link load}
691+
* @param {object} [context] See {@link load}
692+
* @returns {{ format: ModuleFormat, source: ModuleSource }}
693+
*/
694+
#loadSync(url, context) {
695+
return this.#loadAndMaybeBlockOnLoaderThread(url, context);
596696
}
597697

598698
validateLoadResult(url, format) {

‎lib/internal/modules/esm/module_job.js

+43-18
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const {
88
ObjectSetPrototypeOf,
99
PromisePrototypeThen,
1010
PromiseResolve,
11-
ReflectApply,
1211
RegExpPrototypeExec,
1312
RegExpPrototypeSymbolReplace,
1413
SafePromiseAllReturnArrayLike,
@@ -56,35 +55,42 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
5655
);
5756

5857
class ModuleJobBase {
59-
constructor(url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) {
58+
constructor(url, importAttributes, isMain, inspectBrk) {
6059
this.importAttributes = importAttributes;
6160
this.isMain = isMain;
6261
this.inspectBrk = inspectBrk;
6362

6463
this.url = url;
65-
this.module = moduleWrapMaybePromise;
6664
}
6765
}
6866

6967
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
7068
* its dependencies, over time. */
7169
class ModuleJob extends ModuleJobBase {
7270
#loader = null;
73-
// `loader` is the Loader instance used for loading dependencies.
71+
72+
/**
73+
* @param {ModuleLoader} loader The ESM loader.
74+
* @param {string} url URL of the module to be wrapped in ModuleJob.
75+
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
76+
* @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module.
77+
* @param {boolean} isMain Whether the module is the entry point.
78+
* @param {boolean} inspectBrk Whether this module should be evaluated with the
79+
* first line paused in the debugger (because --inspect-brk is passed).
80+
* @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.
81+
*/
7482
constructor(loader, url, importAttributes = { __proto__: null },
75-
moduleProvider, isMain, inspectBrk, sync = false) {
76-
const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
77-
super(url, importAttributes, modulePromise, isMain, inspectBrk);
83+
moduleOrModulePromise, isMain, inspectBrk, isForRequireInImportedCJS = false) {
84+
super(url, importAttributes, isMain, inspectBrk);
7885
this.#loader = loader;
79-
// Expose the promise to the ModuleWrap directly for linking below.
80-
// `this.module` is also filled in below.
81-
this.modulePromise = modulePromise;
8286

83-
if (sync) {
84-
this.module = this.modulePromise;
87+
// Expose the promise to the ModuleWrap directly for linking below.
88+
if (isForRequireInImportedCJS) {
89+
this.module = moduleOrModulePromise;
90+
assert(this.module instanceof ModuleWrap);
8591
this.modulePromise = PromiseResolve(this.module);
8692
} else {
87-
this.modulePromise = PromiseResolve(this.modulePromise);
93+
this.modulePromise = moduleOrModulePromise;
8894
}
8995

9096
// Promise for the list of all dependencyJobs.
@@ -123,7 +129,7 @@ class ModuleJob extends ModuleJobBase {
123129
for (let idx = 0; idx < moduleRequests.length; idx++) {
124130
const { specifier, attributes } = moduleRequests[idx];
125131

126-
const dependencyJobPromise = this.#loader.getModuleJob(
132+
const dependencyJobPromise = this.#loader.getModuleJobForImport(
127133
specifier, this.url, attributes,
128134
);
129135
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
@@ -288,14 +294,33 @@ class ModuleJob extends ModuleJobBase {
288294
}
289295
}
290296

291-
// This is a fully synchronous job and does not spawn additional threads in any way.
292-
// All the steps are ensured to be synchronous and it throws on instantiating
293-
// an asynchronous graph.
297+
/**
298+
* This is a fully synchronous job and does not spawn additional threads in any way.
299+
* All the steps are ensured to be synchronous and it throws on instantiating
300+
* an asynchronous graph. It also disallows CJS <-> ESM cycles.
301+
*
302+
* This is used for ES modules loaded via require(esm). Modules loaded by require() in
303+
* imported CJS are handled by ModuleJob with the isForRequireInImportedCJS set to true instead.
304+
* The two currently have different caching behaviors.
305+
* TODO(joyeecheung): consolidate this with the isForRequireInImportedCJS variant of ModuleJob.
306+
*/
294307
class ModuleJobSync extends ModuleJobBase {
295308
#loader = null;
309+
310+
/**
311+
* @param {ModuleLoader} loader The ESM loader.
312+
* @param {string} url URL of the module to be wrapped in ModuleJob.
313+
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
314+
* @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module.
315+
* @param {boolean} isMain Whether the module is the entry point.
316+
* @param {boolean} inspectBrk Whether this module should be evaluated with the
317+
* first line paused in the debugger (because --inspect-brk is passed).
318+
*/
296319
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) {
297-
super(url, importAttributes, moduleWrap, isMain, inspectBrk, true);
320+
super(url, importAttributes, isMain, inspectBrk, true);
321+
298322
this.#loader = loader;
323+
this.module = moduleWrap;
299324

300325
assert(this.module instanceof ModuleWrap);
301326
// Store itself into the cache first before linking in case there are circular

‎lib/internal/modules/esm/translators.js

+10-27
Original file line numberDiff line numberDiff line change
@@ -68,28 +68,11 @@ function getSource(url) {
6868
/** @type {import('deps/cjs-module-lexer/lexer.js').parse} */
6969
let cjsParse;
7070
/**
71-
* Initializes the CommonJS module lexer parser.
72-
* If WebAssembly is available, it uses the optimized version from the dist folder.
73-
* Otherwise, it falls back to the JavaScript version from the lexer folder.
71+
* Initializes the CommonJS module lexer parser using the JavaScript version.
72+
* TODO(joyeecheung): Use `require('internal/deps/cjs-module-lexer/dist/lexer').initSync()`
73+
* when cjs-module-lexer 1.4.0 is rolled in.
7474
*/
75-
async function initCJSParse() {
76-
if (typeof WebAssembly === 'undefined') {
77-
initCJSParseSync();
78-
} else {
79-
const { parse, init } =
80-
require('internal/deps/cjs-module-lexer/dist/lexer');
81-
try {
82-
await init();
83-
cjsParse = parse;
84-
} catch {
85-
initCJSParseSync();
86-
}
87-
}
88-
}
89-
9075
function initCJSParseSync() {
91-
// TODO(joyeecheung): implement a binding that directly compiles using
92-
// v8::WasmModuleObject::Compile() synchronously.
9376
if (cjsParse === undefined) {
9477
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
9578
}
@@ -159,7 +142,7 @@ function loadCJSModule(module, source, url, filename, isMain) {
159142
}
160143
specifier = `${pathToFileURL(path)}`;
161144
}
162-
const job = cascadedLoader.getModuleJobSync(specifier, url, importAttributes);
145+
const job = cascadedLoader.getModuleJobForRequireInImportedCJS(specifier, url, importAttributes);
163146
job.runSync();
164147
return cjsCache.get(job.url).exports;
165148
};
@@ -250,6 +233,7 @@ translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
250233
// Handle CommonJS modules referenced by `require` calls.
251234
// This translator function must be sync, as `require` is sync.
252235
translators.set('require-commonjs', (url, source, isMain) => {
236+
initCJSParseSync();
253237
assert(cjsParse);
254238

255239
return createCJSModuleWrap(url, source);
@@ -266,10 +250,9 @@ translators.set('require-commonjs-typescript', (url, source, isMain) => {
266250

267251
// Handle CommonJS modules referenced by `import` statements or expressions,
268252
// or as the initial entry point when the ESM loader handles a CommonJS entry.
269-
translators.set('commonjs', async function commonjsStrategy(url, source,
270-
isMain) {
253+
translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
271254
if (!cjsParse) {
272-
await initCJSParse();
255+
initCJSParseSync();
273256
}
274257

275258
// For backward-compatibility, it's possible to return a nullish value for
@@ -287,7 +270,6 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
287270
// Continue regardless of error.
288271
}
289272
return createCJSModuleWrap(url, source, isMain, cjsLoader);
290-
291273
});
292274

293275
/**
@@ -448,8 +430,9 @@ translators.set('wasm', async function(url, source) {
448430

449431
let compiled;
450432
try {
451-
// TODO(joyeecheung): implement a binding that directly compiles using
452-
// v8::WasmModuleObject::Compile() synchronously.
433+
// TODO(joyeecheung): implement a translator that just uses
434+
// compiled = new WebAssembly.Module(source) to compile it
435+
// synchronously.
453436
compiled = await WebAssembly.compile(source);
454437
} catch (err) {
455438
err.message = errPath(url) + ': ' + err.message;

0 commit comments

Comments
 (0)
Please sign in to comment.