44 Array,
55 ArrayPrototypeFind,
66 ArrayPrototypeJoin,
7+ ArrayPrototypePop,
78 ArrayPrototypePush,
9+ ArrayPrototypeSort,
810 FunctionPrototype,
11+ ObjectAssign,
912 ObjectSetPrototypeOf,
1013 PromisePrototypeThen,
1114 PromiseResolve,
@@ -127,6 +130,77 @@ const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => {
127130 }
128131} ;
129132
133+ /**
134+ * @typedef {object } TopLevelAwaitLocation
135+ * @property {string } url URL of the module containing the top-level await.
136+ * @property {number } line 1-based line number of the top-level await.
137+ * @property {number } column 0-based column number of the top-level await.
138+ * @property {string } sourceLine The source line containing the top-level await.
139+ */
140+
141+ /**
142+ * Locate the top-level awaits in the given module by parsing the source with acron.
143+ * @param {string } source Module source code.
144+ * @returns {object[] } The acorn AST nodes of the top-level awaits, in source order.
145+ */
146+ function findTopLevelAwait ( source ) {
147+ const { Parser } = require ( 'internal/deps/acorn/acorn/dist/acorn' ) ;
148+ const walk = require ( 'internal/deps/acorn/acorn-walk/dist/walk' ) ;
149+ let ast ;
150+ try {
151+ ast = Parser . parse ( source , {
152+ __proto__ : null , ecmaVersion : 'latest' , sourceType : 'module' , locations : true ,
153+ } ) ;
154+ } catch {
155+ return [ ] ; // The source is not parsable, skip.
156+ }
157+ // We are looking for _top-level_ await, so we don't traverse into function bodies.
158+ const baseVisitor = ObjectAssign ( { __proto__ : null } , walk . base , { Function : noop } ) ;
159+ const found = [ ] ;
160+ walk . simple ( ast , {
161+ __proto__ : null ,
162+ AwaitExpression ( node ) { ArrayPrototypePush ( found , node ) ; } ,
163+ // `for await (...)` is a ForOfStatement with `await: true`, not an AwaitExpression.
164+ ForOfStatement ( node ) {
165+ if ( node . await ) { ArrayPrototypePush ( found , node ) ; }
166+ } ,
167+ // `await using x = ...` is a VariableDeclaration, not an AwaitExpression.
168+ VariableDeclaration ( node ) {
169+ if ( node . kind === 'await using' ) { ArrayPrototypePush ( found , node ) ; }
170+ } ,
171+ } , baseVisitor ) ;
172+ ArrayPrototypeSort ( found , ( a , b ) => a . start - b . start ) ;
173+ return found ;
174+ }
175+
176+ /**
177+ * Locate the top-level awaits in the given modules.
178+ * @param {ModuleWrap[] } modules Modules that may contain top-level await.
179+ * @returns {TopLevelAwaitLocation[] } The locations of the top-level awaits.
180+ */
181+ function getTopLevelAwaitLocations ( modules ) {
182+ const locations = [ ] ;
183+ for ( let i = 0 ; i < modules . length ; i ++ ) {
184+ const module = modules [ i ] ;
185+ const source = module . source ;
186+ if ( typeof source !== 'string' ) { continue ; } // Not retained during compilation. Skip.
187+ const found = findTopLevelAwait ( source ) ;
188+ if ( found . length === 0 ) { continue ; }
189+ const lines = StringPrototypeSplit ( source , '\n' ) ;
190+ for ( let j = 0 ; j < found . length ; j ++ ) {
191+ const { start } = found [ j ] . loc ;
192+ ArrayPrototypePush ( locations , {
193+ __proto__ : null ,
194+ url : module . url ,
195+ line : start . line ,
196+ column : start . column ,
197+ sourceLine : lines [ start . line - 1 ] ,
198+ } ) ;
199+ }
200+ }
201+ return locations ;
202+ }
203+
130204class ModuleJobBase {
131205 constructor ( loader , url , importAttributes , phase , isMain , inspectBrk ) {
132206 assert ( typeof phase === 'number' ) ;
@@ -185,6 +259,64 @@ class ModuleJobBase {
185259 return evaluationDepJobs ;
186260 }
187261
262+ /**
263+ * Collect the modules that contain top-level await in the linked graph of
264+ * this job. Whether each module contains top-level await is known at
265+ * compilation, so for a synchronously linked graph this finds asynchronous
266+ * graphs before instantiation.
267+ * On the (deprecated) async loader hook worker thread, linking may be asynchronous, in
268+ * which case the subgraphs that are not synchronously linked are skipped
269+ * and callers should still consult hasAsyncGraph after instantiation.
270+ * @returns {ModuleWrap[] }
271+ */
272+ findModulesWithTopLevelAwait ( ) {
273+ const found = [ ] ;
274+ const seen = new SafeSet ( ) ;
275+ const stack = [ this ] ;
276+ while ( stack . length > 0 ) {
277+ const job = ArrayPrototypePop ( stack ) ;
278+ if ( seen . has ( job ) ) { continue ; }
279+ seen . add ( job ) ;
280+ if ( job . module ?. hasTopLevelAwait ) {
281+ ArrayPrototypePush ( found , job . module ) ;
282+ }
283+ // job.linked is the array of evaluation-phase dependency jobs when the
284+ // linking is synchronous. Skip it if it's still a promise.
285+ if ( ! isPromise ( job . linked ) ) {
286+ for ( let i = 0 ; i < job . linked . length ; i ++ ) {
287+ ArrayPrototypePush ( stack , job . linked [ i ] ) ;
288+ }
289+ }
290+ }
291+ return found ;
292+ }
293+
294+ /**
295+ * Throw the ERR_REQUIRE_ASYNC_MODULE with metadata for a require()'d graph that
296+ * contains top-level await.
297+ * @param {Module|undefined } parent CommonJS module that require()'d this, if any.
298+ * @param {ModuleWrap[] } [modules] Modules with top-level await, when already
299+ * collected by the caller, to avoid walking the graph again.
300+ */
301+ throwAsyncGraphError ( parent , modules = this . findModulesWithTopLevelAwait ( ) ) {
302+ const locations = getOptionValue ( '--experimental-print-required-tla' ) ? getTopLevelAwaitLocations ( modules ) : [ ] ;
303+ const filename = urlToFilename ( this . url ) ;
304+ throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parent , locations ) ;
305+ }
306+
307+ /**
308+ * If the a require()'d graph contains top-level await, collect the source locations
309+ * of the top-level awaits using source code retained during compilation and throw
310+ * ERR_REQUIRE_ASYNC_MODULE. This can be run before instantiation is complete.
311+ * @param {Module|undefined } parent CommonJS module that require()'d this, if any.
312+ */
313+ throwIfAsyncGraph ( parent ) {
314+ const modules = this . findModulesWithTopLevelAwait ( ) ;
315+ if ( modules . length > 0 ) {
316+ this . throwAsyncGraphError ( parent , modules ) ;
317+ }
318+ }
319+
188320 /**
189321 * Ensure that this ModuleJob is moving towards the required phase
190322 * (does not necessarily mean it is ready at that phase - run does that)
@@ -386,6 +518,8 @@ class ModuleJob extends ModuleJobBase {
386518
387519 debug ( 'ModuleJob.runSync()' , status , this . module ) ;
388520 if ( status === kUninstantiated ) {
521+ // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that
522+ // the async graph error supersedes instantiation (mismatch export) errors in the graph.
389523 // FIXME(joyeecheung): this cannot fully handle < kInstantiated. Make the linking
390524 // fully synchronous instead.
391525 if ( this . module . getModuleRequests ( ) . length === 0 ) {
@@ -395,22 +529,18 @@ class ModuleJob extends ModuleJobBase {
395529 status = this . module . getStatus ( ) ;
396530 }
397531 if ( status === kInstantiated || status === kErrored ) {
398- const filename = urlToFilename ( this . url ) ;
399- const parentFilename = urlToFilename ( parent ?. filename ) ;
400- if ( this . module . hasAsyncGraph && ! getOptionValue ( '--experimental-print-required-tla' ) ) {
401- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
532+ if ( this . module . hasAsyncGraph ) {
533+ this . throwAsyncGraphError ( parent ) ;
402534 }
403535 if ( status === kInstantiated ) {
404536 setHasStartedUserESMExecution ( ) ;
405- const namespace = this . module . evaluateSync ( filename , parentFilename ) ;
537+ const namespace = this . module . evaluateSync ( ) ;
406538 return { __proto__ : null , module : this . module , namespace } ;
407539 }
408540 throw this . module . getError ( ) ;
409541 } else if ( status === kEvaluating || status === kEvaluated ) {
410542 if ( this . module . hasAsyncGraph ) {
411- const filename = urlToFilename ( this . url ) ;
412- const parentFilename = urlToFilename ( parent ?. filename ) ;
413- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
543+ this . throwAsyncGraphError ( parent ) ;
414544 }
415545 // kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles.
416546 // Allow it for now, since we only need to ban ESM <-> CJS cycles which would be
@@ -506,9 +636,16 @@ class ModuleJobSync extends ModuleJobBase {
506636 await this . evaluationPromise ;
507637 }
508638 return { __proto__ : null , module : this . module } ;
509- } else if ( status === kInstantiated ) {
510- // The evaluation may have been canceled because instantiate() detected TLA first.
511- // But when it is imported again, it's fine to re-evaluate it asynchronously.
639+ } else if ( status === kInstantiated || status === kUninstantiated ) {
640+ // The require() of this (synchronously linked) module bailed out: either
641+ // it was rejected for containing top-level await after instantiation
642+ // (kInstantiated), or its instantiation failed and left it uninstantiated
643+ // (kUninstantiated, e.g. a missing named export). When it's reached via async
644+ // run() from import, finish the instantiation and evaluate it asynchronously,
645+ // re-throwing any instantiation error.
646+ if ( status === kUninstantiated ) {
647+ this . module . instantiate ( ) ;
648+ }
512649 const timeout = - 1 ;
513650 const breakOnSigint = false ;
514651 this . evaluationPromise = this . module . evaluate ( timeout , breakOnSigint ) ;
@@ -524,23 +661,19 @@ class ModuleJobSync extends ModuleJobBase {
524661 runSync ( parent ) {
525662 debug ( 'ModuleJobSync.runSync()' , this . module ) ;
526663 assert ( this . phase === kEvaluationPhase ) ;
664+ // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the
665+ // async graph error supersedes instantiation (mismatch export) errors in the graph.
527666 // TODO(joyeecheung): add the error decoration logic from the async instantiate.
528667 this . module . instantiate ( ) ;
529- // If --experimental-print-required-tla is true, proceeds to evaluation even
530- // if it's async because we want to search for the TLA and help users locate
531- // them.
532- // TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait()
533- // and we'll be able to throw right after compilation of the modules, using acron
534- // to find and print the TLA. This requires the linking to be synchronous in case
535- // it runs into cached asynchronous modules that are not yet fetched.
536- const parentFilename = urlToFilename ( parent ?. filename ) ;
537- const filename = urlToFilename ( this . url ) ;
538- if ( this . module . hasAsyncGraph && ! getOptionValue ( '--experimental-print-required-tla' ) ) {
539- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
668+ // On the deprecated async loader hook worker thread, dependencies linked by an
669+ // earlier import may not be walkable synchronously, so double-check with
670+ // V8 now that the graph is instantiated.
671+ if ( this . module . hasAsyncGraph ) {
672+ this . throwAsyncGraphError ( parent ) ;
540673 }
541674 setHasStartedUserESMExecution ( ) ;
542675 try {
543- const namespace = this . module . evaluateSync ( filename , parentFilename ) ;
676+ const namespace = this . module . evaluateSync ( ) ;
544677 return { __proto__ : null , module : this . module , namespace } ;
545678 } catch ( e ) {
546679 explainCommonJSGlobalLikeNotDefinedError ( e , this . module . url , this . module . hasTopLevelAwait ) ;
0 commit comments