| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ea3b870 commit 851b460
24 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2803,12 +2803,30 @@ A QUIC session failed because version negotiation is required. | |||
| 2803 | 2803 | ||
| 2804 | 2804 | ### `ERR_REQUIRE_ASYNC_MODULE` | |
| 2805 | 2805 | ||
| 2806 | + <!-- YAML | ||
| 2807 | + changes: | ||
| 2808 | + - version: REPLACEME | ||
| 2809 | + pr-url: https://github.com/nodejs/node/pull/64260 | ||
| 2810 | + description: Added the `requireStack` and `topLevelAwaitLocations` properties. | ||
| 2811 | + --> | ||
| 2812 | + | ||
| 2806 | 2813 | When trying to `require()` a [ES Module][], the module turns out to be asynchronous. | |
| 2807 | 2814 | That is, it contains top-level await. | |
| 2808 | 2815 | ||
| 2809 | - To see where the top-level await is, use | ||
| 2810 | - `--experimental-print-required-tla` (this would execute the modules | ||
| 2811 | - before looking for the top-level awaits). | ||
| 2816 | + When uncaught, the flag `--experimental-print-required-tla` prints | ||
| 2817 | + the locations of the top-level awaits in the graph to stderr. | ||
| 2818 | + | ||
| 2819 | + This error has the following additional non-enumerable properties: | ||
| 2820 | + | ||
| 2821 | + * `requireStack` {string\[]} The chain of modules that led to the failing | ||
| 2822 | + `require()`, starting with the module that required the asynchronous module. | ||
| 2823 | + * `topLevelAwaitLocations` {Object\[]} The locations of the top-level awaits in | ||
| 2824 | + the graph. Only populated when `--experimental-print-required-tla` is enabled. | ||
| 2825 | + Each entry has the following properties: | ||
| 2826 | + * `url` {string} The URL of the module containing the top-level await. | ||
| 2827 | + * `line` {number} The 1-based line number of the top-level await. | ||
| 2828 | + * `column` {number} The 1-based column number of the top-level await. | ||
| 2829 | + * `sourceLine` {string} The source line containing the top-level await. | ||
| 2812 | 2830 | ||
| 2813 | 2831 | <a id="ERR_REQUIRE_CYCLE_MODULE"></a> | |
| 2814 | 2832 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -316,10 +316,9 @@ graph it `import`s contains top-level `await`, | |||
| 316 | 316 | [`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should | |
| 317 | 317 | load the asynchronous module using [`import()`][]. | |
| 318 | 318 | ||
| 319 | - If `--experimental-print-required-tla` is enabled, instead of throwing | ||
| 320 | - `ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the | ||
| 321 | - module, try to locate the top-level awaits, and print their location to | ||
| 322 | - help users fix them. | ||
| 319 | + If `--experimental-print-required-tla` is enabled and the error is uncaught, | ||
| 320 | + Node.js will try to locate the top-level `await`s in the `require()`'d module graph | ||
| 321 | + and print the locations in the stderr. | ||
| 323 | 322 | ||
| 324 | 323 | If support for loading ES modules using `require()` results in unexpected | |
| 325 | 324 | breakage, it can be disabled using `--no-require-module`. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1714,20 +1714,36 @@ E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parent, locations) { | |||
| 1714 | 1714 | if (!getOptionValue('--experimental-print-required-tla')) { | |
| 1715 | 1715 | message += ' To see where the top-level await comes from, use --experimental-print-required-tla.'; | |
| 1716 | 1716 | } | |
| 1717 | + if (filename) { | ||
| 1718 | + message += `\nRequired module: ${filename}`; | ||
| 1719 | + } | ||
| 1717 | 1720 | if (parent) { | |
| 1718 | 1721 | const { getRequireStack } = require('internal/modules/helpers'); | |
| 1719 | 1722 | const requireStack = getRequireStack(parent); | |
| 1720 | 1723 | if (requireStack.length > 0) { | |
| 1721 | 1724 | message += '\nRequire stack:\n- ' + | |
| 1722 | 1725 | ArrayPrototypeJoin(requireStack, '\n- '); | |
| 1723 | 1726 | } | |
| 1724 | - this.requireStack = requireStack; | ||
| 1727 | + ObjectDefineProperty(this, 'requireStack', { | ||
| 1728 | + __proto__: null, | ||
| 1729 | + enumerable: false, | ||
| 1730 | + configurable: true, | ||
| 1731 | + writable: true, | ||
| 1732 | + value: requireStack, | ||
| 1733 | + }); | ||
| 1725 | 1734 | } | |
| 1726 | 1735 | if (locations && locations.length > 0) { | |
| 1727 | 1736 | const { urlToFilename } = require('internal/modules/helpers'); | |
| 1728 | 1737 | const frames = ArrayPrototypeMap(locations, ({ url, line, column, sourceLine }) => | |
| 1729 | - `${urlToFilename(url)}:${line}\n\n${sourceLine}\n${StringPrototypeRepeat(' ', column)}^\n`); | ||
| 1738 | + `${urlToFilename(url)}:${line}\n\n${sourceLine}\n${StringPrototypeRepeat(' ', column - 1)}^\n`); | ||
| 1730 | 1739 | setArrowMessage(this, ArrayPrototypeJoin(frames, '\n')); | |
| 1740 | + ObjectDefineProperty(this, 'topLevelAwaitLocations', { | ||
| 1741 | + __proto__: null, | ||
| 1742 | + enumerable: false, | ||
| 1743 | + configurable: true, | ||
| 1744 | + writable: true, | ||
| 1745 | + value: locations, | ||
| 1746 | + }); | ||
| 1731 | 1747 | } | |
| 1732 | 1748 | return message; | |
| 1733 | 1749 | }, Error); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -291,8 +291,8 @@ class ModuleLoader { | |||
| 291 | 291 | const status = job.module.getStatus(); | |
| 292 | 292 | debug('Module status', job, status); | |
| 293 | 293 | // hasAsyncGraph is available after module been instantiated. | |
| 294 | - if (status >= kInstantiated && job.module.hasAsyncGraph) { | ||
| 295 | - job.throwAsyncGraphError(parent); | ||
| 294 | + if (status >= kInstantiated) { | ||
| 295 | + job.throwIfAsyncGraph(parent); | ||
| 296 | 296 | } | |
| 297 | 297 | if (status === kEvaluated) { | |
| 298 | 298 | return { wrap: job.module, namespace: job.module.getNamespace() }; | |
@@ -320,9 +320,11 @@ class ModuleLoader { | |||
| 320 | 320 | } | |
| 321 | 321 | if (status !== kEvaluating) { | |
| 322 | 322 | assert(status === kUninstantiated, `Unexpected module status ${status}`); | |
| 323 | - // A previous require() of the same graph may have bailed out before | ||
| 324 | - // instantiation because it contains top-level await. | ||
| 325 | - job.throwIfAsyncGraph(parent); | ||
| 323 | + // If we get here, either there's a race where the job is still being instantiated | ||
| 324 | + // by an in-flight import(), or the cached module previously encountered an | ||
| 325 | + // instantiation error during a prior load (e.g. due to a mismatched import). | ||
| 326 | + // TODO(joyeecheung): the current check is too broad. We should attempt to | ||
| 327 | + // get the potential instantiation error and throw it. | ||
| 326 | 328 | throw new ERR_REQUIRE_ESM_RACE_CONDITION(filename, parentFilename, false); | |
| 327 | 329 | } | |
| 328 | 330 | let message = `Cannot require() ES Module ${filename} in a cycle.`; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | const { | |
| 4 | 4 | Array, | |
| 5 | + ArrayIsArray, | ||
| 5 | 6 | ArrayPrototypeFind, | |
| 6 | 7 | ArrayPrototypeJoin, | |
| 7 | 8 | ArrayPrototypePop, | |
@@ -14,6 +15,7 @@ const { | |||
| 14 | 15 | PromiseResolve, | |
| 15 | 16 | RegExpPrototypeExec, | |
| 16 | 17 | RegExpPrototypeSymbolReplace, | |
| 18 | + RegExpPrototypeSymbolSplit, | ||
| 17 | 19 | SafePromiseAllReturnArrayLike, | |
| 18 | 20 | SafePromiseAllReturnVoid, | |
| 19 | 21 | SafeSet, | |
@@ -37,8 +39,10 @@ const { | |||
| 37 | 39 | kUninstantiated, | |
| 38 | 40 | } = internalBinding('module_wrap'); | |
| 39 | 41 | const { | |
| 42 | + getPromiseDetails, | ||
| 40 | 43 | privateSymbols: { | |
| 41 | 44 | entry_point_module_private_symbol, | |
| 45 | + module_source_private_symbol: kModuleSource, | ||
| 42 | 46 | }, | |
| 43 | 47 | } = internalBinding('util'); | |
| 44 | 48 | /** | |
@@ -135,12 +139,12 @@ const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => { | |||
| 135 | 139 | * @typedef {object} TopLevelAwaitLocation | |
| 136 | 140 | * @property {string} url URL of the module containing the top-level await. | |
| 137 | 141 | * @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. | ||
| 142 | + * @property {number} column 1-based column number of the top-level await. | ||
| 139 | 143 | * @property {string} sourceLine The source line containing the top-level await. | |
| 140 | 144 | */ | |
| 141 | 145 | ||
| 142 | 146 | /** | |
| 143 | - * Locate the top-level awaits in the given module by parsing the source with acron. | ||
| 147 | + * Locate the top-level awaits in the given module by parsing the source with acorn. | ||
| 144 | 148 | * @param {string} source Module source code. | |
| 145 | 149 | * @returns {object[]} The acorn AST nodes of the top-level awaits, in source order. | |
| 146 | 150 | */ | |
@@ -174,27 +178,62 @@ function findTopLevelAwait(source) { | |||
| 174 | 178 | return found; | |
| 175 | 179 | } | |
| 176 | 180 | ||
| 181 | + /** | ||
| 182 | + * Collect the modules that contain top-level await in the linked graph of a job. | ||
| 183 | + * @param {ModuleJobBase} root The root of the module graph to search. | ||
| 184 | + * @returns {ModuleWrap[]} Modules that contain top-level await. | ||
| 185 | + */ | ||
| 186 | + function findModulesWithTopLevelAwait(root) { | ||
| 187 | + const found = []; | ||
| 188 | + const seen = new SafeSet(); | ||
| 189 | + const stack = [root]; | ||
| 190 | + while (stack.length > 0) { | ||
| 191 | + const job = ArrayPrototypePop(stack); | ||
| 192 | + if (seen.has(job)) { continue; } | ||
| 193 | + seen.add(job); | ||
| 194 | + if (job.module?.hasTopLevelAwait) { | ||
| 195 | + ArrayPrototypePush(found, job.module); | ||
| 196 | + } | ||
| 197 | + let linked = job.linked; | ||
| 198 | + if (isPromise(linked)) { | ||
| 199 | + linked = getPromiseDetails(linked)?.[1]; | ||
| 200 | + } | ||
| 201 | + // If `require(esm)` comes from the deprecated async loader hook worker thread, | ||
| 202 | + // linked may be pending at this point. In that case, this branch would be skipped - | ||
| 203 | + // we just allow lossy reporting of TLA locations in an edge case when a deprecated | ||
| 204 | + // feature is used in combination with another experimental flag. | ||
| 205 | + if (ArrayIsArray(linked)) { | ||
| 206 | + for (let i = 0; i < linked.length; i++) { | ||
| 207 | + ArrayPrototypePush(stack, linked[i]); | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + return found; | ||
| 212 | + } | ||
| 213 | + | ||
| 177 | 214 | /** | |
| 178 | 215 | * Locate the top-level awaits in the given modules. | |
| 179 | - * @param {ModuleWrap[]} modules Modules that may contain top-level await. | ||
| 216 | + * @param {ModuleJobBase} root The root of the module graph to search. | ||
| 180 | 217 | * @returns {TopLevelAwaitLocation[]} The locations of the top-level awaits. | |
| 181 | 218 | */ | |
| 182 | - function getTopLevelAwaitLocations(modules) { | ||
| 219 | + function getTopLevelAwaitLocations(root) { | ||
| 220 | + const modules = findModulesWithTopLevelAwait(root); | ||
| 183 | 221 | const locations = []; | |
| 184 | 222 | for (let i = 0; i < modules.length; i++) { | |
| 185 | 223 | const module = modules[i]; | |
| 186 | - const source = module.source; | ||
| 224 | + const source = module[kModuleSource]; | ||
| 187 | 225 | if (typeof source !== 'string') { continue; } // Not retained during compilation. Skip. | |
| 188 | 226 | const found = findTopLevelAwait(source); | |
| 189 | 227 | if (found.length === 0) { continue; } | |
| 190 | - const lines = StringPrototypeSplit(source, '\n'); | ||
| 228 | + const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source); | ||
| 191 | 229 | for (let j = 0; j < found.length; j++) { | |
| 192 | 230 | const { start } = found[j].loc; | |
| 193 | 231 | ArrayPrototypePush(locations, { | |
| 194 | 232 | __proto__: null, | |
| 195 | 233 | url: module.url, | |
| 196 | 234 | line: start.line, | |
| 197 | - column: start.column, | ||
| 235 | + // Acorn reports 0-based columns, convert them to 1-based to match `line`. | ||
| 236 | + column: start.column + 1, | ||
| 198 | 237 | sourceLine: lines[start.line - 1], | |
| 199 | 238 | }); | |
| 200 | 239 | } | |
@@ -261,61 +300,18 @@ class ModuleJobBase { | |||
| 261 | 300 | } | |
| 262 | 301 | ||
| 263 | 302 | /** | |
| 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 | ||
| 303 | + * If the require()'d graph contains top-level await, collect the source locations | ||
| 310 | 304 | * 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. | ||
| 305 | + * ERR_REQUIRE_ASYNC_MODULE. The module must be at least instantiated. | ||
| 312 | 306 | * @param {Module|undefined} parent CommonJS module that require()'d this, if any. | |
| 313 | 307 | */ | |
| 314 | 308 | throwIfAsyncGraph(parent) { | |
| 315 | - const modules = this.findModulesWithTopLevelAwait(); | ||
| 316 | - if (modules.length > 0) { | ||
| 317 | - this.throwAsyncGraphError(parent, modules); | ||
| 309 | + if (!this.module.hasAsyncGraph) { | ||
| 310 | + return; | ||
| 318 | 311 | } | |
| 312 | + const locations = getOptionValue('--experimental-print-required-tla') ? getTopLevelAwaitLocations(this) : []; | ||
| 313 | + const filename = urlToFilename(this.url); | ||
| 314 | + throw new ERR_REQUIRE_ASYNC_MODULE(filename, parent, locations); | ||
| 319 | 315 | } | |
| 320 | 316 | ||
| 321 | 317 | /** | |
@@ -537,19 +533,15 @@ class ModuleJob extends ModuleJobBase { | |||
| 537 | 533 | status = this.module.getStatus(); | |
| 538 | 534 | } | |
| 539 | 535 | if (status === kInstantiated || status === kErrored) { | |
| 540 | - if (this.module.hasAsyncGraph) { | ||
| 541 | - this.throwAsyncGraphError(parent); | ||
| 542 | - } | ||
| 536 | + this.throwIfAsyncGraph(parent); | ||
| 543 | 537 | if (status === kInstantiated) { | |
| 544 | 538 | setHasStartedUserESMExecution(); | |
| 545 | 539 | const namespace = this.module.evaluateSync(); | |
| 546 | 540 | return { __proto__: null, module: this.module, namespace }; | |
| 547 | 541 | } | |
| 548 | 542 | throw this.module.getError(); | |
| 549 | 543 | } else if (status === kEvaluating || status === kEvaluated) { | |
| 550 | - if (this.module.hasAsyncGraph) { | ||
| 551 | - this.throwAsyncGraphError(parent); | ||
| 552 | - } | ||
| 544 | + this.throwIfAsyncGraph(parent); | ||
| 553 | 545 | // kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles. | |
| 554 | 546 | // Allow it for now, since we only need to ban ESM <-> CJS cycles which would be | |
| 555 | 547 | // detected earlier during the linking phase, though the CJS handling in the ESM | |
@@ -645,12 +637,11 @@ class ModuleJobSync extends ModuleJobBase { | |||
| 645 | 637 | } | |
| 646 | 638 | return { __proto__: null, module: this.module }; | |
| 647 | 639 | } 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. | ||
| 640 | + // If we get here, the module was initially required and is now being imported. | ||
| 641 | + // The require() module failed either because the graph has TLA (kInstantiated), | ||
| 642 | + // or instantiation failed (kUninstantiated, e.g. missing named export). | ||
| 643 | + // Try finishing the instantiation - if it succeeds, proceed to evaluation, | ||
| 644 | + // otherwise the branch below re-throw any instantiation error. | ||
| 654 | 645 | if (status === kUninstantiated) { | |
| 655 | 646 | this.module.instantiate(); | |
| 656 | 647 | } | |
@@ -676,9 +667,7 @@ class ModuleJobSync extends ModuleJobBase { | |||
| 676 | 667 | // On the deprecated async loader hook worker thread, dependencies linked by an | |
| 677 | 668 | // earlier import may not be walkable synchronously, so double-check with | |
| 678 | 669 | // V8 now that the graph is instantiated. | |
| 679 | - if (this.module.hasAsyncGraph) { | ||
| 680 | - this.throwAsyncGraphError(parent); | ||
| 681 | - } | ||
| 670 | + this.throwIfAsyncGraph(parent); | ||
| 682 | 671 | setHasStartedUserESMExecution(); | |
| 683 | 672 | try { | |
| 684 | 673 | const namespace = this.module.evaluateSync(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,7 @@ const { | |||
| 11 | 11 | const { | |
| 12 | 12 | privateSymbols: { | |
| 13 | 13 | host_defined_option_symbol, | |
| 14 | + module_source_private_symbol: kModuleSource, | ||
| 14 | 15 | }, | |
| 15 | 16 | } = internalBinding('util'); | |
| 16 | 17 | const { | |
@@ -368,7 +369,7 @@ function compileSourceTextModule(url, source, type, context = kEmptyObject) { | |||
| 368 | 369 | // only serves as a shortcut. | |
| 369 | 370 | if (wrap.hasTopLevelAwait && | |
| 370 | 371 | getOptionValue('--experimental-print-required-tla')) { | |
| 371 | - wrap.source = source; | ||
| 372 | + wrap[kModuleSource] = source; | ||
| 372 | 373 | } | |
| 373 | 374 | ||
| 374 | 375 | // Cache the source map for the module if present. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments