Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2823,12 +2823,30 @@ A QUIC session failed because version negotiation is required.

### `ERR_REQUIRE_ASYNC_MODULE`

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64260
description: Added the `requireStack` and `topLevelAwaitLocations` properties.
-->

When trying to `require()` a [ES Module][], the module turns out to be asynchronous.
That is, it contains top-level await.

To see where the top-level await is, use
`--experimental-print-required-tla` (this would execute the modules
before looking for the top-level awaits).
When uncaught, the flag `--experimental-print-required-tla` prints
the locations of the top-level awaits in the graph to stderr.

This error has the following additional non-enumerable properties:

* `requireStack` {string\[]} The chain of modules that led to the failing
`require()`, starting with the module that required the asynchronous module.
* `topLevelAwaitLocations` {Object\[]} The locations of the top-level awaits in
the graph. Only populated when `--experimental-print-required-tla` is enabled.
Each entry has the following properties:
* `url` {string} The URL of the module containing the top-level await.
* `line` {number} The 1-based line number of the top-level await.
* `column` {number} The 1-based column number of the top-level await.
* `sourceLine` {string} The source line containing the top-level await.

<a id="ERR_REQUIRE_CYCLE_MODULE"></a>

Expand Down
7 changes: 3 additions & 4 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,9 @@ graph it `import`s contains top-level `await`,
[`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should
load the asynchronous module using [`import()`][].

If `--experimental-print-required-tla` is enabled, instead of throwing
`ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the
module, try to locate the top-level awaits, and print their location to
help users fix them.
If `--experimental-print-required-tla` is enabled and the error is uncaught,
Node.js will try to locate the top-level `await`s in the `require()`'d module graph
and print the locations in the stderr.

If support for loading ES modules using `require()` results in unexpected
breakage, it can be disabled using `--no-require-module`.
Expand Down
20 changes: 18 additions & 2 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1719,20 +1719,36 @@ E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parent, locations) {
if (!getOptionValue('--experimental-print-required-tla')) {
message += ' To see where the top-level await comes from, use --experimental-print-required-tla.';
}
if (filename) {
message += `\nRequired module: ${filename}`;
}
if (parent) {
const { getRequireStack } = require('internal/modules/helpers');
const requireStack = getRequireStack(parent);
if (requireStack.length > 0) {
message += '\nRequire stack:\n- ' +
ArrayPrototypeJoin(requireStack, '\n- ');
}
this.requireStack = requireStack;
ObjectDefineProperty(this, 'requireStack', {
__proto__: null,
enumerable: false,
configurable: true,
writable: true,
value: requireStack,
});
}
if (locations && locations.length > 0) {
const { urlToFilename } = require('internal/modules/helpers');
const frames = ArrayPrototypeMap(locations, ({ url, line, column, sourceLine }) =>
`${urlToFilename(url)}:${line}\n\n${sourceLine}\n${StringPrototypeRepeat(' ', column)}^\n`);
`${urlToFilename(url)}:${line}\n\n${sourceLine}\n${StringPrototypeRepeat(' ', column - 1)}^\n`);
setArrowMessage(this, ArrayPrototypeJoin(frames, '\n'));
ObjectDefineProperty(this, 'topLevelAwaitLocations', {
__proto__: null,
enumerable: false,
configurable: true,
writable: true,
value: locations,
});
}
return message;
}, Error);
Expand Down
12 changes: 7 additions & 5 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ class ModuleLoader {
const status = job.module.getStatus();
debug('Module status', job, status);
// hasAsyncGraph is available after module been instantiated.
if (status >= kInstantiated && job.module.hasAsyncGraph) {
job.throwAsyncGraphError(parent);
if (status >= kInstantiated) {
job.throwIfAsyncGraph(parent);
}
if (status === kEvaluated) {
return { wrap: job.module, namespace: job.module.getNamespace() };
Expand Down Expand Up @@ -320,9 +320,11 @@ class ModuleLoader {
}
if (status !== kEvaluating) {
assert(status === kUninstantiated, `Unexpected module status ${status}`);
// A previous require() of the same graph may have bailed out before
// instantiation because it contains top-level await.
job.throwIfAsyncGraph(parent);
// If we get here, either there's a race where the job is still being instantiated
// by an in-flight import(), or the cached module previously encountered an
// instantiation error during a prior load (e.g. due to a mismatched import).
// TODO(joyeecheung): the current check is too broad. We should attempt to
// get the potential instantiation error and throw it.
throw new ERR_REQUIRE_ESM_RACE_CONDITION(filename, parentFilename, false);
}
let message = `Cannot require() ES Module ${filename} in a cycle.`;
Expand Down
132 changes: 60 additions & 72 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
Array,
ArrayIsArray,
ArrayPrototypeFind,
ArrayPrototypeJoin,
ArrayPrototypePop,
Expand Down Expand Up @@ -37,8 +38,10 @@
kUninstantiated,
} = internalBinding('module_wrap');
const {
getPromiseDetails,
privateSymbols: {
entry_point_module_private_symbol,
module_source_private_symbol: kModuleSource,
},
} = internalBinding('util');
/**
Expand Down Expand Up @@ -135,12 +138,12 @@
* @typedef {object} TopLevelAwaitLocation
* @property {string} url URL of the module containing the top-level await.
* @property {number} line 1-based line number of the top-level await.
* @property {number} column 0-based column number of the top-level await.
* @property {number} column 1-based column number of the top-level await.
* @property {string} sourceLine The source line containing the top-level await.
*/

/**
* Locate the top-level awaits in the given module by parsing the source with acron.
* Locate the top-level awaits in the given module by parsing the source with acorn.
* @param {string} source Module source code.
* @returns {object[]} The acorn AST nodes of the top-level awaits, in source order.
*/
Expand Down Expand Up @@ -174,27 +177,62 @@
return found;
}

/**
* Collect the modules that contain top-level await in the linked graph of a job.
* @param {ModuleJobBase} root The root of the module graph to search.
* @returns {ModuleWrap[]} Modules that contain top-level await.
*/
function findModulesWithTopLevelAwait(root) {
const found = [];
const seen = new SafeSet();
const stack = [root];
while (stack.length > 0) {
const job = ArrayPrototypePop(stack);
if (seen.has(job)) { continue; }
seen.add(job);
if (job.module?.hasTopLevelAwait) {
ArrayPrototypePush(found, job.module);
}
let linked = job.linked;
if (isPromise(linked)) {
linked = getPromiseDetails(linked)?.[1];
}
// If `require(esm)` comes from the deprecated async loader hook worker thread,
// linked may be pending at this point. In that case, this branch would be skipped -
// we just allow lossy reporting of TLA locations in an edge case when a deprecated
// feature is used in combination with another experimental flag.
if (ArrayIsArray(linked)) {
for (let i = 0; i < linked.length; i++) {
ArrayPrototypePush(stack, linked[i]);
}
}
}
return found;
}

/**
* Locate the top-level awaits in the given modules.
* @param {ModuleWrap[]} modules Modules that may contain top-level await.
* @param {ModuleJobBase} root The root of the module graph to search.
* @returns {TopLevelAwaitLocation[]} The locations of the top-level awaits.
*/
function getTopLevelAwaitLocations(modules) {
function getTopLevelAwaitLocations(root) {
const modules = findModulesWithTopLevelAwait(root);
const locations = [];
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const source = module.source;
const source = module[kModuleSource];
if (typeof source !== 'string') { continue; } // Not retained during compilation. Skip.
const found = findTopLevelAwait(source);
if (found.length === 0) { continue; }
const lines = StringPrototypeSplit(source, '\n');
const lines = StringPrototypeSplit(source, /\r?\n/);

Check failure on line 227 in lib/internal/modules/esm/module_job.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

%String.prototype.split% looks up the Symbol.split property of the passed regex, use RegExpPrototypeSymbolSplit directly
for (let j = 0; j < found.length; j++) {
const { start } = found[j].loc;
ArrayPrototypePush(locations, {
__proto__: null,
url: module.url,
line: start.line,
column: start.column,
// Acorn reports 0-based columns, convert them to 1-based to match `line`.
column: start.column + 1,
sourceLine: lines[start.line - 1],
});
}
Expand Down Expand Up @@ -261,61 +299,18 @@
}

/**
* Collect the modules that contain top-level await in the linked graph of
* this job. Whether each module contains top-level await is known at
* compilation, so for a synchronously linked graph this finds asynchronous
* graphs before instantiation.
* On the (deprecated) async loader hook worker thread, linking may be asynchronous, in
* which case the subgraphs that are not synchronously linked are skipped
* and callers should still consult hasAsyncGraph after instantiation.
* @returns {ModuleWrap[]}
*/
findModulesWithTopLevelAwait() {
const found = [];
const seen = new SafeSet();
const stack = [this];
while (stack.length > 0) {
const job = ArrayPrototypePop(stack);
if (seen.has(job)) { continue; }
seen.add(job);
if (job.module?.hasTopLevelAwait) {
ArrayPrototypePush(found, job.module);
}
// job.linked is the array of evaluation-phase dependency jobs when the
// linking is synchronous. Skip it if it's still a promise.
if (!isPromise(job.linked)) {
for (let i = 0; i < job.linked.length; i++) {
ArrayPrototypePush(stack, job.linked[i]);
}
}
}
return found;
}

/**
* Throw the ERR_REQUIRE_ASYNC_MODULE with metadata for a require()'d graph that
* contains top-level await.
* @param {Module|undefined} parent CommonJS module that require()'d this, if any.
* @param {ModuleWrap[]} [modules] Modules with top-level await, when already
* collected by the caller, to avoid walking the graph again.
*/
throwAsyncGraphError(parent, modules = this.findModulesWithTopLevelAwait()) {
const locations = getOptionValue('--experimental-print-required-tla') ? getTopLevelAwaitLocations(modules) : [];
const filename = urlToFilename(this.url);
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parent, locations);
}

/**
* If the a require()'d graph contains top-level await, collect the source locations
* If the require()'d graph contains top-level await, collect the source locations
* of the top-level awaits using source code retained during compilation and throw
* ERR_REQUIRE_ASYNC_MODULE. This can be run before instantiation is complete.
* ERR_REQUIRE_ASYNC_MODULE. The module must be at least instantiated.
* @param {Module|undefined} parent CommonJS module that require()'d this, if any.
*/
throwIfAsyncGraph(parent) {
const modules = this.findModulesWithTopLevelAwait();
if (modules.length > 0) {
this.throwAsyncGraphError(parent, modules);
if (!this.module.hasAsyncGraph) {
return;
}
const locations = getOptionValue('--experimental-print-required-tla') ? getTopLevelAwaitLocations(this) : [];
const filename = urlToFilename(this.url);
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parent, locations);
}

/**
Expand Down Expand Up @@ -537,19 +532,15 @@
status = this.module.getStatus();
}
if (status === kInstantiated || status === kErrored) {
if (this.module.hasAsyncGraph) {
this.throwAsyncGraphError(parent);
}
this.throwIfAsyncGraph(parent);
if (status === kInstantiated) {
setHasStartedUserESMExecution();
const namespace = this.module.evaluateSync();
return { __proto__: null, module: this.module, namespace };
}
throw this.module.getError();
} else if (status === kEvaluating || status === kEvaluated) {
if (this.module.hasAsyncGraph) {
this.throwAsyncGraphError(parent);
}
this.throwIfAsyncGraph(parent);
// kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles.
// Allow it for now, since we only need to ban ESM <-> CJS cycles which would be
// detected earlier during the linking phase, though the CJS handling in the ESM
Expand Down Expand Up @@ -645,12 +636,11 @@
}
return { __proto__: null, module: this.module };
} else if (status === kInstantiated || status === kUninstantiated) {
// The require() of this (synchronously linked) module bailed out: either
// it was rejected for containing top-level await after instantiation
// (kInstantiated), or its instantiation failed and left it uninstantiated
// (kUninstantiated, e.g. a missing named export). When it's reached via async
// run() from import, finish the instantiation and evaluate it asynchronously,
// re-throwing any instantiation error.
// If we get here, the module was initially required and is now being imported.
// The require() module failed either because the graph has TLA (kInstantiated),
// or instantiation failed (kUninstantiated, e.g. missing named export).
// Try finishing the instantiation - if it succeeds, proceed to evaluation,
// otherwise the branch below re-throw any instantiation error.
if (status === kUninstantiated) {
this.module.instantiate();
}
Expand All @@ -676,9 +666,7 @@
// On the deprecated async loader hook worker thread, dependencies linked by an
// earlier import may not be walkable synchronously, so double-check with
// V8 now that the graph is instantiated.
if (this.module.hasAsyncGraph) {
this.throwAsyncGraphError(parent);
}
this.throwIfAsyncGraph(parent);
setHasStartedUserESMExecution();
try {
const namespace = this.module.evaluateSync();
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
const {
privateSymbols: {
host_defined_option_symbol,
module_source_private_symbol: kModuleSource,
},
} = internalBinding('util');
const {
Expand Down Expand Up @@ -368,7 +369,7 @@ function compileSourceTextModule(url, source, type, context = kEmptyObject) {
// only serves as a shortcut.
if (wrap.hasTopLevelAwait &&
getOptionValue('--experimental-print-required-tla')) {
wrap.source = source;
wrap[kModuleSource] = source;
}

// Cache the source map for the module if present.
Expand Down
Loading
Loading