44 Array,
55 ArrayPrototypeFind,
66 ArrayPrototypeJoin,
7+ ArrayPrototypePop,
78 ArrayPrototypePush,
9+ ArrayPrototypeSort,
810 FunctionPrototype,
11+ ObjectAssign,
912 ObjectSetPrototypeOf,
1013 PromisePrototypeThen,
1114 PromiseResolve,
@@ -128,6 +131,77 @@ const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => {
128131 }
129132} ;
130133
134+ /**
135+ * @typedef {object } TopLevelAwaitLocation
136+ * @property {string } url URL of the module containing the top-level await.
137+ * @property {number } line 1-based line number of the top-level await.
138+ * @property {number } column 0-based column number of the top-level await.
139+ * @property {string } sourceLine The source line containing the top-level await.
140+ */
141+
142+ /**
143+ * Locate the top-level awaits in the given module by parsing the source with acron.
144+ * @param {string } source Module source code.
145+ * @returns {object[] } The acorn AST nodes of the top-level awaits, in source order.
146+ */
147+ function findTopLevelAwait ( source ) {
148+ const { Parser } = require ( 'internal/deps/acorn/acorn/dist/acorn' ) ;
149+ const walk = require ( 'internal/deps/acorn/acorn-walk/dist/walk' ) ;
150+ let ast ;
151+ try {
152+ ast = Parser . parse ( source , {
153+ __proto__ : null , ecmaVersion : 'latest' , sourceType : 'module' , locations : true ,
154+ } ) ;
155+ } catch {
156+ return [ ] ; // The source is not parsable, skip.
157+ }
158+ // We are looking for _top-level_ await, so we don't traverse into function bodies.
159+ const baseVisitor = ObjectAssign ( { __proto__ : null } , walk . base , { Function : noop } ) ;
160+ const found = [ ] ;
161+ walk . simple ( ast , {
162+ __proto__ : null ,
163+ AwaitExpression ( node ) { ArrayPrototypePush ( found , node ) ; } ,
164+ // `for await (...)` is a ForOfStatement with `await: true`, not an AwaitExpression.
165+ ForOfStatement ( node ) {
166+ if ( node . await ) { ArrayPrototypePush ( found , node ) ; }
167+ } ,
168+ // `await using x = ...` is a VariableDeclaration, not an AwaitExpression.
169+ VariableDeclaration ( node ) {
170+ if ( node . kind === 'await using' ) { ArrayPrototypePush ( found , node ) ; }
171+ } ,
172+ } , baseVisitor ) ;
173+ ArrayPrototypeSort ( found , ( a , b ) => a . start - b . start ) ;
174+ return found ;
175+ }
176+
177+ /**
178+ * Locate the top-level awaits in the given modules.
179+ * @param {ModuleWrap[] } modules Modules that may contain top-level await.
180+ * @returns {TopLevelAwaitLocation[] } The locations of the top-level awaits.
181+ */
182+ function getTopLevelAwaitLocations ( modules ) {
183+ const locations = [ ] ;
184+ for ( let i = 0 ; i < modules . length ; i ++ ) {
185+ const module = modules [ i ] ;
186+ const source = module . source ;
187+ if ( typeof source !== 'string' ) { continue ; } // Not retained during compilation. Skip.
188+ const found = findTopLevelAwait ( source ) ;
189+ if ( found . length === 0 ) { continue ; }
190+ const lines = StringPrototypeSplit ( source , '\n' ) ;
191+ for ( let j = 0 ; j < found . length ; j ++ ) {
192+ const { start } = found [ j ] . loc ;
193+ ArrayPrototypePush ( locations , {
194+ __proto__ : null ,
195+ url : module . url ,
196+ line : start . line ,
197+ column : start . column ,
198+ sourceLine : lines [ start . line - 1 ] ,
199+ } ) ;
200+ }
201+ }
202+ return locations ;
203+ }
204+
131205class ModuleJobBase {
132206 constructor ( loader , url , importAttributes , phase , isMain , inspectBrk ) {
133207 assert ( typeof phase === 'number' ) ;
@@ -186,6 +260,64 @@ class ModuleJobBase {
186260 return evaluationDepJobs ;
187261 }
188262
263+ /**
264+ * Collect the modules that contain top-level await in the linked graph of
265+ * this job. Whether each module contains top-level await is known at
266+ * compilation, so for a synchronously linked graph this finds asynchronous
267+ * graphs before instantiation.
268+ * On the (deprecated) async loader hook worker thread, linking may be asynchronous, in
269+ * which case the subgraphs that are not synchronously linked are skipped
270+ * and callers should still consult hasAsyncGraph after instantiation.
271+ * @returns {ModuleWrap[] }
272+ */
273+ findModulesWithTopLevelAwait ( ) {
274+ const found = [ ] ;
275+ const seen = new SafeSet ( ) ;
276+ const stack = [ this ] ;
277+ while ( stack . length > 0 ) {
278+ const job = ArrayPrototypePop ( stack ) ;
279+ if ( seen . has ( job ) ) { continue ; }
280+ seen . add ( job ) ;
281+ if ( job . module ?. hasTopLevelAwait ) {
282+ ArrayPrototypePush ( found , job . module ) ;
283+ }
284+ // job.linked is the array of evaluation-phase dependency jobs when the
285+ // linking is synchronous. Skip it if it's still a promise.
286+ if ( ! isPromise ( job . linked ) ) {
287+ for ( let i = 0 ; i < job . linked . length ; i ++ ) {
288+ ArrayPrototypePush ( stack , job . linked [ i ] ) ;
289+ }
290+ }
291+ }
292+ return found ;
293+ }
294+
295+ /**
296+ * Throw the ERR_REQUIRE_ASYNC_MODULE with metadata for a require()'d graph that
297+ * contains top-level await.
298+ * @param {Module|undefined } parent CommonJS module that require()'d this, if any.
299+ * @param {ModuleWrap[] } [modules] Modules with top-level await, when already
300+ * collected by the caller, to avoid walking the graph again.
301+ */
302+ throwAsyncGraphError ( parent , modules = this . findModulesWithTopLevelAwait ( ) ) {
303+ const locations = getOptionValue ( '--experimental-print-required-tla' ) ? getTopLevelAwaitLocations ( modules ) : [ ] ;
304+ const filename = urlToFilename ( this . url ) ;
305+ throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parent , locations ) ;
306+ }
307+
308+ /**
309+ * If the a require()'d graph contains top-level await, collect the source locations
310+ * of the top-level awaits using source code retained during compilation and throw
311+ * ERR_REQUIRE_ASYNC_MODULE. This can be run before instantiation is complete.
312+ * @param {Module|undefined } parent CommonJS module that require()'d this, if any.
313+ */
314+ throwIfAsyncGraph ( parent ) {
315+ const modules = this . findModulesWithTopLevelAwait ( ) ;
316+ if ( modules . length > 0 ) {
317+ this . throwAsyncGraphError ( parent , modules ) ;
318+ }
319+ }
320+
189321 /**
190322 * Ensure that this ModuleJob is moving towards the required phase
191323 * (does not necessarily mean it is ready at that phase - run does that)
@@ -394,6 +526,8 @@ class ModuleJob extends ModuleJobBase {
394526
395527 debug ( 'ModuleJob.runSync()' , status , this . module ) ;
396528 if ( status === kUninstantiated ) {
529+ // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that
530+ // the async graph error supersedes instantiation (mismatch export) errors in the graph.
397531 // FIXME(joyeecheung): this cannot fully handle < kInstantiated. Make the linking
398532 // fully synchronous instead.
399533 if ( this . module . getModuleRequests ( ) . length === 0 ) {
@@ -403,22 +537,18 @@ class ModuleJob extends ModuleJobBase {
403537 status = this . module . getStatus ( ) ;
404538 }
405539 if ( status === kInstantiated || status === kErrored ) {
406- const filename = urlToFilename ( this . url ) ;
407- const parentFilename = urlToFilename ( parent ?. filename ) ;
408- if ( this . module . hasAsyncGraph && ! getOptionValue ( '--experimental-print-required-tla' ) ) {
409- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
540+ if ( this . module . hasAsyncGraph ) {
541+ this . throwAsyncGraphError ( parent ) ;
410542 }
411543 if ( status === kInstantiated ) {
412544 setHasStartedUserESMExecution ( ) ;
413- const namespace = this . module . evaluateSync ( filename , parentFilename ) ;
545+ const namespace = this . module . evaluateSync ( ) ;
414546 return { __proto__ : null , module : this . module , namespace } ;
415547 }
416548 throw this . module . getError ( ) ;
417549 } else if ( status === kEvaluating || status === kEvaluated ) {
418550 if ( this . module . hasAsyncGraph ) {
419- const filename = urlToFilename ( this . url ) ;
420- const parentFilename = urlToFilename ( parent ?. filename ) ;
421- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
551+ this . throwAsyncGraphError ( parent ) ;
422552 }
423553 // kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles.
424554 // Allow it for now, since we only need to ban ESM <-> CJS cycles which would be
@@ -514,9 +644,16 @@ class ModuleJobSync extends ModuleJobBase {
514644 await this . evaluationPromise ;
515645 }
516646 return { __proto__ : null , module : this . module } ;
517- } else if ( status === kInstantiated ) {
518- // The evaluation may have been canceled because instantiate() detected TLA first.
519- // But when it is imported again, it's fine to re-evaluate it asynchronously.
647+ } else if ( status === kInstantiated || status === kUninstantiated ) {
648+ // The require() of this (synchronously linked) module bailed out: either
649+ // it was rejected for containing top-level await after instantiation
650+ // (kInstantiated), or its instantiation failed and left it uninstantiated
651+ // (kUninstantiated, e.g. a missing named export). When it's reached via async
652+ // run() from import, finish the instantiation and evaluate it asynchronously,
653+ // re-throwing any instantiation error.
654+ if ( status === kUninstantiated ) {
655+ this . module . instantiate ( ) ;
656+ }
520657 const timeout = - 1 ;
521658 const breakOnSigint = false ;
522659 this . evaluationPromise = this . module . evaluate ( timeout , breakOnSigint ) ;
@@ -532,23 +669,19 @@ class ModuleJobSync extends ModuleJobBase {
532669 runSync ( parent ) {
533670 debug ( 'ModuleJobSync.runSync()' , this . module ) ;
534671 assert ( this . shouldRunModule ( this . phase ) ) ;
672+ // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the
673+ // async graph error supersedes instantiation (mismatch export) errors in the graph.
535674 // TODO(joyeecheung): add the error decoration logic from the async instantiate.
536675 this . module . instantiate ( ) ;
537- // If --experimental-print-required-tla is true, proceeds to evaluation even
538- // if it's async because we want to search for the TLA and help users locate
539- // them.
540- // TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait()
541- // and we'll be able to throw right after compilation of the modules, using acron
542- // to find and print the TLA. This requires the linking to be synchronous in case
543- // it runs into cached asynchronous modules that are not yet fetched.
544- const parentFilename = urlToFilename ( parent ?. filename ) ;
545- const filename = urlToFilename ( this . url ) ;
546- if ( this . module . hasAsyncGraph && ! getOptionValue ( '--experimental-print-required-tla' ) ) {
547- throw new ERR_REQUIRE_ASYNC_MODULE ( filename , parentFilename ) ;
676+ // On the deprecated async loader hook worker thread, dependencies linked by an
677+ // earlier import may not be walkable synchronously, so double-check with
678+ // V8 now that the graph is instantiated.
679+ if ( this . module . hasAsyncGraph ) {
680+ this . throwAsyncGraphError ( parent ) ;
548681 }
549682 setHasStartedUserESMExecution ( ) ;
550683 try {
551- const namespace = this . module . evaluateSync ( filename , parentFilename ) ;
684+ const namespace = this . module . evaluateSync ( ) ;
552685 return { __proto__ : null , module : this . module , namespace } ;
553686 } catch ( e ) {
554687 explainCommonJSGlobalLikeNotDefinedError ( e , this . module . url , this . module . hasTopLevelAwait ) ;
0 commit comments