@@ -36,12 +36,22 @@ const {
3636 getDefaultConditions,
3737} = require ( 'internal/modules/esm/utils' ) ;
3838const { kImplicitAssertType } = require ( 'internal/modules/esm/assert' ) ;
39- const { ModuleWrap, kEvaluating, kEvaluated } = internalBinding ( 'module_wrap' ) ;
39+ const {
40+ ModuleWrap,
41+ kEvaluated,
42+ kEvaluating,
43+ kInstantiated,
44+ throwIfPromiseRejected,
45+ } = internalBinding ( 'module_wrap' ) ;
4046const {
4147 urlToFilename,
4248} = require ( 'internal/modules/helpers' ) ;
4349let defaultResolve , defaultLoad , defaultLoadSync , importMetaInitializer ;
4450
51+ let debug = require ( 'internal/util/debuglog' ) . debuglog ( 'esm' , ( fn ) => {
52+ debug = fn ;
53+ } ) ;
54+
4555/**
4656 * @typedef {import('./hooks.js').HooksProxy } HooksProxy
4757 * @typedef {import('./module_job.js').ModuleJobBase } ModuleJobBase
@@ -75,6 +85,23 @@ function getTranslators() {
7585 return translators ;
7686}
7787
88+ /**
89+ * Generate message about potential race condition caused by requiring a cached module that has started
90+ * async linking.
91+ * @param {string } filename Filename of the module being required.
92+ * @param {string|undefined } parentFilename Filename of the module calling require().
93+ * @returns {string } Error message.
94+ */
95+ function getRaceMessage ( filename , parentFilename ) {
96+ let raceMessage = `Cannot require() ES Module ${ filename } because it is not yet fully loaded. ` ;
97+ raceMessage += 'This may be caused by a race condition if the module is simultaneously dynamically ' ;
98+ raceMessage += 'import()-ed via Promise.all(). Try await-ing the import() sequentially in a loop instead.' ;
99+ if ( parentFilename ) {
100+ raceMessage += ` (from ${ parentFilename } )` ;
101+ }
102+ return raceMessage ;
103+ }
104+
78105/**
79106 * @type {HooksProxy }
80107 * Multiple loader instances exist for various, specific reasons (see code comments at site).
@@ -297,35 +324,53 @@ class ModuleLoader {
297324 // evaluated at this point.
298325 // TODO(joyeecheung): add something similar to CJS loader's requireStack to help
299326 // debugging the the problematic links in the graph for import.
327+ debug ( 'importSyncForRequire' , parent ?. filename , '->' , filename , job ) ;
300328 if ( job !== undefined ) {
301329 mod [ kRequiredModuleSymbol ] = job . module ;
302330 const parentFilename = urlToFilename ( parent ?. filename ) ;
303331 // TODO(node:55782): this race may stop to happen when the ESM resolution and loading become synchronous.
304332 if ( ! job . module ) {
305- let message = `Cannot require() ES Module ${ filename } because it is not yet fully loaded. ` ;
306- message += 'This may be caused by a race condition if the module is simultaneously dynamically ' ;
307- message += 'import()-ed via Promise.all(). Try await-ing the import() sequentially in a loop instead.' ;
308- if ( parentFilename ) {
309- message += ` (from ${ parentFilename } )` ;
310- }
311- assert ( job . module , message ) ;
333+ assert . fail ( getRaceMessage ( filename , parentFilename ) ) ;
312334 }
313335 if ( job . module . async ) {
314336 throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
315337 }
316- // job.module may be undefined if it's asynchronously loaded. Which means
317- // there is likely a cycle.
318- if ( job . module . getStatus ( ) !== kEvaluated ) {
319- let message = `Cannot require() ES Module ${ filename } in a cycle.` ;
320- if ( parentFilename ) {
321- message += ` (from ${ parentFilename } )` ;
322- }
323- message += 'A cycle involving require(esm) is disallowed to maintain ' ;
324- message += 'invariants madated by the ECMAScript specification' ;
325- message += 'Try making at least part of the dependency in the graph lazily loaded.' ;
326- throw new ERR_REQUIRE_CYCLE_MODULE ( message ) ;
338+ const status = job . module . getStatus ( ) ;
339+ debug ( 'Module status' , filename , status ) ;
340+ if ( status === kEvaluated ) {
341+ return { wrap : job . module , namespace : job . module . getNamespaceSync ( filename , parentFilename ) } ;
342+ } else if ( status === kInstantiated ) {
343+ // When it's an async job cached by another import request,
344+ // which has finished linking but has not started its
345+ // evaluation because the async run() task would be later
346+ // in line. Then start the evaluation now with runSync(), which
347+ // is guaranteed to finish by the time the other run() get to it,
348+ // and the other task would just get the cached evaluation results,
349+ // similar to what would happen when both are async.
350+ mod [ kRequiredModuleSymbol ] = job . module ;
351+ const { namespace } = job . runSync ( parent ) ;
352+ return { wrap : job . module , namespace : namespace || job . module . getNamespace ( ) } ;
327353 }
328- return { wrap : job . module , namespace : job . module . getNamespaceSync ( filename , parentFilename ) } ;
354+ // When the cached async job have already encountered a linking
355+ // error that gets wrapped into a rejection, but is still later
356+ // in line to throw on it, just unwrap and throw the linking error
357+ // from require().
358+ if ( job . instantiated ) {
359+ throwIfPromiseRejected ( job . instantiated ) ;
360+ }
361+ if ( status !== kEvaluating ) {
362+ assert . fail ( `Unexpected module status ${ status } . ` +
363+ getRaceMessage ( filename , parentFilename ) ) ;
364+ }
365+ let message = `Cannot require() ES Module ${ filename } in a cycle.` ;
366+ if ( parentFilename ) {
367+ message += ` (from ${ parentFilename } )` ;
368+ }
369+ message += 'A cycle involving require(esm) is disallowed to maintain ' ;
370+ message += 'invariants madated by the ECMAScript specification' ;
371+ message += 'Try making at least part of the dependency in the graph lazily loaded.' ;
372+ throw new ERR_REQUIRE_CYCLE_MODULE ( message ) ;
373+
329374 }
330375 // TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
331376 // cache here, or use a carrier object to carry the compiled module script
0 commit comments