@@ -36,12 +36,22 @@ const {
36
36
getDefaultConditions,
37
37
} = require ( 'internal/modules/esm/utils' ) ;
38
38
const { 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' ) ;
40
46
const {
41
47
urlToFilename,
42
48
} = require ( 'internal/modules/helpers' ) ;
43
49
let defaultResolve , defaultLoad , defaultLoadSync , importMetaInitializer ;
44
50
51
+ let debug = require ( 'internal/util/debuglog' ) . debuglog ( 'esm' , ( fn ) => {
52
+ debug = fn ;
53
+ } ) ;
54
+
45
55
/**
46
56
* @typedef {import('./hooks.js').HooksProxy } HooksProxy
47
57
* @typedef {import('./module_job.js').ModuleJobBase } ModuleJobBase
@@ -75,6 +85,23 @@ function getTranslators() {
75
85
return translators ;
76
86
}
77
87
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
+
78
105
/**
79
106
* @type {HooksProxy }
80
107
* Multiple loader instances exist for various, specific reasons (see code comments at site).
@@ -297,35 +324,53 @@ class ModuleLoader {
297
324
// evaluated at this point.
298
325
// TODO(joyeecheung): add something similar to CJS loader's requireStack to help
299
326
// debugging the the problematic links in the graph for import.
327
+ debug ( 'importSyncForRequire' , parent ?. filename , '->' , filename , job ) ;
300
328
if ( job !== undefined ) {
301
329
mod [ kRequiredModuleSymbol ] = job . module ;
302
330
const parentFilename = urlToFilename ( parent ?. filename ) ;
303
331
// TODO(node:55782): this race may stop to happen when the ESM resolution and loading become synchronous.
304
332
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 ) ) ;
312
334
}
313
335
if ( job . module . async ) {
314
336
throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
315
337
}
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 ( ) } ;
327
353
}
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
+
329
374
}
330
375
// TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
331
376
// cache here, or use a carrier object to carry the compiled module script
0 commit comments