mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
module: synchronously load most ES modules
Signed-off-by: Geoffrey Booth <webadmin@geoffreybooth.com> PR-URL: https://github.com/nodejs/node/pull/62530 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
This commit is contained in:
1 parent
a4f6d7587b
commit
c89ec290f0
9 files changed
+373
-84
No files matched your search
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
const tmpdir = require('../../test/common/tmpdir');
|
||||
|
||||
const BRANCHING_FACTOR = 10;
|
||||
const benchmarkDirectory = tmpdir.resolve('esm-graph');
|
||||
|
||||
// Each measured iteration has to load a graph the ESM cache has never seen, so the
|
||||
// fixture holds one independent copy of the graph per iteration.
|
||||
function graphDirectory(modules, copy) {
|
||||
return path.join(benchmarkDirectory, `g${modules}-${copy}`);
|
||||
}
|
||||
|
||||
function entryURL(modules, copy) {
|
||||
return pathToFileURL(path.join(graphDirectory(modules, copy), 'mod0.mjs')).href;
|
||||
}
|
||||
|
||||
// Build a complete BRANCHING_FACTOR-ary tree of `modules` + 1 modules rooted at mod0:
|
||||
// module i imports modules BRANCHING_FACTOR*i+1 through BRANCHING_FACTOR*i+BRANCHING_FACTOR,
|
||||
// capped at the total, so the shape approximates a real dependency tree rather than
|
||||
// one module with hundreds of direct imports.
|
||||
function createGraph(modules, copy) {
|
||||
const dir = graphDirectory(modules, copy);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const total = modules + 1;
|
||||
for (let i = 0; i < total; i++) {
|
||||
let source = '';
|
||||
for (let c = 1; c <= BRANCHING_FACTOR; c++) {
|
||||
const child = BRANCHING_FACTOR * i + c;
|
||||
if (child < total) {
|
||||
source += `import './mod${child}.mjs';\n`;
|
||||
}
|
||||
}
|
||||
source += `export const value${i} = ${i};\n`;
|
||||
fs.writeFileSync(path.join(dir, `mod${i}.mjs`), source);
|
||||
}
|
||||
}
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
modules: [250, 500, 1000, 2000],
|
||||
n: [10],
|
||||
}, {
|
||||
setup(configs) {
|
||||
// Build every fixture once here rather than per configuration: writing tens of
|
||||
// thousands of files is far more expensive than the work being measured.
|
||||
tmpdir.refresh();
|
||||
const maxN = configs.reduce((max, c) => Math.max(max, c.n), 0);
|
||||
for (const modules of new Set(configs.map((c) => c.modules))) {
|
||||
for (let copy = 0; copy < maxN; copy++) {
|
||||
createGraph(modules, copy);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function main({ n, modules }) {
|
||||
bench.start();
|
||||
for (let i = 0; i < n; i++) {
|
||||
await import(entryURL(modules, i));
|
||||
}
|
||||
bench.end(n);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
PromisePrototypeThen,
|
||||
RegExpPrototypeSymbolReplace,
|
||||
encodeURIComponent,
|
||||
globalThis,
|
||||
hardenRegExp,
|
||||
} = primordials;
|
||||
|
||||
@@ -22,6 +23,11 @@ const {
|
||||
kRequiredModuleSymbol,
|
||||
} = require('internal/modules/cjs/loader');
|
||||
const { imported_cjs_symbol } = internalBinding('symbols');
|
||||
const {
|
||||
privateSymbols: {
|
||||
entry_point_module_private_symbol,
|
||||
},
|
||||
} = internalBinding('util');
|
||||
|
||||
const assert = require('internal/assert');
|
||||
const {
|
||||
@@ -350,6 +356,39 @@ class ModuleLoader {
|
||||
return { wrap: job.module, namespace: job.runSync(parent).namespace };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and evaluate the entry point module.
|
||||
* The graph is always loaded and linked synchronously. When it contains no top-level
|
||||
* await it is evaluated synchronously too, so startup creates no promises at all.
|
||||
* When it does contain top-level await, evaluation falls back to `job.run()` here,
|
||||
* reusing the job that was just built rather than making the caller re-enter the
|
||||
* loader and redo the resolution and linking for the same module.
|
||||
* @param {string} url The URL of the entry point module.
|
||||
* @returns {ModuleJobSync|undefined} The job still needing asynchronous evaluation when
|
||||
* the graph has top-level await, otherwise `undefined` - the module is then already
|
||||
* fully evaluated and no promise was ever created.
|
||||
*/
|
||||
importForEntryPoint(url) {
|
||||
return onImport.traceSync(() => {
|
||||
const request = { specifier: url, phase: kEvaluationPhase, attributes: kEmptyObject, __proto__: null };
|
||||
const job = this.getOrCreateModuleJob(undefined, request, kImportInImportedESM);
|
||||
job.instantiateSync();
|
||||
if (job.module.hasAsyncGraph) {
|
||||
// Hand the job back so the caller can evaluate it through the ordinary
|
||||
// asynchronous entry point machinery, without re-resolving and re-linking
|
||||
// the module the way re-entering `import()` would.
|
||||
return job;
|
||||
}
|
||||
globalThis[entry_point_module_private_symbol] = job.module;
|
||||
job.runSync();
|
||||
return undefined;
|
||||
}, {
|
||||
__proto__: null,
|
||||
parentURL: undefined,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check invariants on a cached module job when require()'d from ESM.
|
||||
* @param {string} specifier The first parameter of require().
|
||||
@@ -572,10 +611,14 @@ class ModuleLoader {
|
||||
assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`);
|
||||
}
|
||||
|
||||
// TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
|
||||
const ModuleJobCtor = (requestType === kImportInRequiredESM ? ModuleJobSync : ModuleJob);
|
||||
const isMain = (parentURL === undefined);
|
||||
const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
|
||||
// Use ModuleJobSync whenever we're on the main thread (not the async loader hook worker),
|
||||
// except for kRequireInImportedCJS (TODO: consolidate that case too).
|
||||
// TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
|
||||
const ModuleJobCtor = (this.isForAsyncLoaderHookWorker ||
|
||||
requestType === kRequireInImportedCJS) ?
|
||||
ModuleJob : ModuleJobSync;
|
||||
job = new ModuleJobCtor(
|
||||
this,
|
||||
url,
|
||||
@@ -602,8 +645,9 @@ class ModuleLoader {
|
||||
*/
|
||||
getOrCreateModuleJob(parentURL, request, requestType) {
|
||||
let maybePromise;
|
||||
if (requestType === kRequireInImportedCJS || requestType === kImportInRequiredESM) {
|
||||
// In these two cases, resolution must be synchronous.
|
||||
if (!this.isForAsyncLoaderHookWorker) {
|
||||
// On the main thread, always resolve synchronously;
|
||||
// `resolveSync` coordinates with the async loader hook worker if needed.
|
||||
maybePromise = this.resolveSync(parentURL, request);
|
||||
assert(!isPromise(maybePromise));
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,7 @@ const {
|
||||
getSourceMapsSupport,
|
||||
} = require('internal/source_map/source_map_cache');
|
||||
const assert = require('internal/assert');
|
||||
const resolvedPromise = PromiseResolve();
|
||||
let resolvedPromise;
|
||||
const {
|
||||
setHasStartedUserESMExecution,
|
||||
urlToFilename,
|
||||
@@ -65,7 +65,6 @@ const {
|
||||
ERR_REQUIRE_ASYNC_MODULE,
|
||||
ERR_REQUIRE_ESM_RACE_CONDITION,
|
||||
} = require('internal/errors').codes;
|
||||
let hasPausedEntry = false;
|
||||
|
||||
const CJSGlobalLike = [
|
||||
'require',
|
||||
@@ -241,6 +240,58 @@ function getTopLevelAwaitLocations(root) {
|
||||
return locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `error` is a SyntaxError from V8 for a missing named export on a CJS module, rewrite its message to the friendlier
|
||||
* "Named export '...' not found..." form. Must be called after `decorateErrorStack(error)` so that the arrow (source
|
||||
* context with the import statement text) has been prepended to `error.stack`.
|
||||
* @param {Error} error
|
||||
* @param {ModuleWrap} module The parent module that triggered the instantiation.
|
||||
* @param {boolean[]} commonJsDeps Per-request array indicating whether each dependency is a CJS module, aligned with
|
||||
* `module.getModuleRequests()`.
|
||||
*/
|
||||
const handleCJSNamedExportError = (error, module, commonJsDeps) => {
|
||||
// TODO(@bcoe): Add source map support to exception that occurs as result
|
||||
// of missing named export. This is currently not possible because
|
||||
// stack trace originates in module_job, not the file itself. A hidden
|
||||
// symbol with filename could be set in node_errors.cc to facilitate this.
|
||||
if (!getSourceMapsSupport().enabled &&
|
||||
StringPrototypeIncludes(error.message,
|
||||
' does not provide an export named')) {
|
||||
const splitStack = StringPrototypeSplit(error.stack, '\n', 2);
|
||||
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
|
||||
/module '(.*)' does not provide an export named '(.+)'/,
|
||||
error.message);
|
||||
const moduleRequests = module.getModuleRequests();
|
||||
let isCommonJS = false;
|
||||
for (let i = 0; i < moduleRequests.length; ++i) {
|
||||
if (moduleRequests[i].specifier === childSpecifier) {
|
||||
isCommonJS = commonJsDeps[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isCommonJS) {
|
||||
const importStatement = splitStack[1];
|
||||
// TODO(@ctavan): The original error stack only provides the single
|
||||
// line which causes the error. For multi-line import statements we
|
||||
// cannot generate an equivalent object destructuring assignment by
|
||||
// just parsing the error stack.
|
||||
const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
|
||||
const destructuringAssignment = oneLineNamedImports &&
|
||||
RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
|
||||
error.message = `Named export '${name}' not found. The requested module` +
|
||||
` '${childSpecifier}' is a CommonJS module, which may not support` +
|
||||
' all module.exports as named exports.\nCommonJS modules can ' +
|
||||
'always be imported via the default export, for example using:' +
|
||||
`\n\nimport pkg from '${childSpecifier}';\n${
|
||||
destructuringAssignment ?
|
||||
`const ${destructuringAssignment} = pkg;\n` : ''}`;
|
||||
const newStack = StringPrototypeSplit(error.stack, '\n');
|
||||
newStack[3] = `SyntaxError: ${error.message}`;
|
||||
error.stack = ArrayPrototypeJoin(newStack, '\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ModuleJobBase {
|
||||
constructor(loader, url, importAttributes, phase, isMain, inspectBrk) {
|
||||
assert(typeof phase === 'number');
|
||||
@@ -455,63 +506,17 @@ class ModuleJob extends ModuleJobBase {
|
||||
await addJobsToDependencyGraph(this);
|
||||
|
||||
try {
|
||||
if (!hasPausedEntry && this.inspectBrk) {
|
||||
hasPausedEntry = true;
|
||||
const initWrapper = internalBinding('inspector').callAndPauseOnStart;
|
||||
initWrapper(this.module.instantiate, this.module);
|
||||
} else {
|
||||
this.module.instantiate();
|
||||
}
|
||||
} catch (e) {
|
||||
decorateErrorStack(e);
|
||||
// TODO(@bcoe): Add source map support to exception that occurs as result
|
||||
// of missing named export. This is currently not possible because
|
||||
// stack trace originates in module_job, not the file itself. A hidden
|
||||
// symbol with filename could be set in node_errors.cc to facilitate this.
|
||||
if (!getSourceMapsSupport().enabled &&
|
||||
StringPrototypeIncludes(e.message,
|
||||
' does not provide an export named')) {
|
||||
const splitStack = StringPrototypeSplit(e.stack, '\n', 2);
|
||||
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
|
||||
/module '(.*)' does not provide an export named '(.+)'/,
|
||||
e.message);
|
||||
const moduleRequests = this.module.getModuleRequests();
|
||||
let isCommonJS = false;
|
||||
for (let i = 0; i < moduleRequests.length; ++i) {
|
||||
if (moduleRequests[i].specifier === childSpecifier) {
|
||||
isCommonJS = this.commonJsDeps[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCommonJS) {
|
||||
const importStatement = splitStack[1];
|
||||
// TODO(@ctavan): The original error stack only provides the single
|
||||
// line which causes the error. For multi-line import statements we
|
||||
// cannot generate an equivalent object destructuring assignment by
|
||||
// just parsing the error stack.
|
||||
const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
|
||||
const destructuringAssignment = oneLineNamedImports &&
|
||||
RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
|
||||
e.message = `Named export '${name}' not found. The requested module` +
|
||||
` '${childSpecifier}' is a CommonJS module, which may not support` +
|
||||
' all module.exports as named exports.\nCommonJS modules can ' +
|
||||
'always be imported via the default export, for example using:' +
|
||||
`\n\nimport pkg from '${childSpecifier}';\n${
|
||||
destructuringAssignment ?
|
||||
`const ${destructuringAssignment} = pkg;\n` : ''}`;
|
||||
const newStack = StringPrototypeSplit(e.stack, '\n');
|
||||
newStack[3] = `SyntaxError: ${e.message}`;
|
||||
e.stack = ArrayPrototypeJoin(newStack, '\n');
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
this.module.instantiate();
|
||||
} catch (error) {
|
||||
decorateErrorStack(error);
|
||||
handleCJSNamedExportError(error, this.module, this.commonJsDeps);
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const dependencyJob of jobsInGraph) {
|
||||
// Calling `this.module.instantiate()` instantiates not only the
|
||||
// ModuleWrap in this module, but all modules in the graph.
|
||||
dependencyJob.instantiated = resolvedPromise;
|
||||
dependencyJob.instantiated = resolvedPromise ??= PromiseResolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,12 +582,14 @@ class ModuleJob extends ModuleJobBase {
|
||||
|
||||
/**
|
||||
* This is a fully synchronous job and does not spawn additional threads in any way.
|
||||
* All the steps are ensured to be synchronous and it throws on instantiating
|
||||
* an asynchronous graph. It also disallows CJS <-> ESM cycles.
|
||||
* Loading and linking are always synchronous. Evaluation via runSync() throws on an
|
||||
* asynchronous graph; evaluation via run() falls back to async for top-level await.
|
||||
* It also disallows CJS <-> ESM cycles.
|
||||
*
|
||||
* This is used for ES modules loaded via require(esm). Modules loaded by require() in
|
||||
* imported CJS are handled by ModuleJob with the isForRequireInImportedCJS set to true instead.
|
||||
* The two currently have different caching behaviors.
|
||||
* Used for all ES module imports on the main thread, regardless of how the import was
|
||||
* triggered (entry point, import(), require(esm), --import, etc.).
|
||||
* Modules loaded by require() in imported CJS are handled by ModuleJob with the
|
||||
* isForRequireInImportedCJS set to true instead. The two currently have different caching behaviors.
|
||||
* TODO(joyeecheung): consolidate this with the isForRequireInImportedCJS variant of ModuleJob.
|
||||
*/
|
||||
class ModuleJobSync extends ModuleJobBase {
|
||||
@@ -623,38 +630,76 @@ class ModuleJobSync extends ModuleJobBase {
|
||||
return PromiseResolve(this.module);
|
||||
}
|
||||
|
||||
async run() {
|
||||
instantiateSync() {
|
||||
try {
|
||||
this.module.instantiate();
|
||||
} catch (error) {
|
||||
decorateErrorStack(error);
|
||||
handleCJSNamedExportError(error, this.module, this.commonJsDeps);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async run(isEntryPoint = false) {
|
||||
assert(this.shouldRunModule(this.phase));
|
||||
// This path is hit by a require'd module that is imported again.
|
||||
const status = this.module.getStatus();
|
||||
debug('ModuleJobSync.run()', status, this.module);
|
||||
// If the module was previously required and errored, reject from import() again.
|
||||
if (status === kErrored) {
|
||||
throw this.module.getError();
|
||||
} else if (status > kInstantiated) {
|
||||
}
|
||||
if (status > kInstantiated) {
|
||||
// Already evaluated (e.g. previously require()'d and now import()'d again).
|
||||
if (this.evaluationPromise) {
|
||||
await this.evaluationPromise;
|
||||
}
|
||||
return { __proto__: null, module: this.module };
|
||||
} else if (status === kInstantiated || status === kUninstantiated) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
if (status < kInstantiated) {
|
||||
// Either a fresh module - links were already resolved synchronously in the constructor -
|
||||
// or one that was initially require()'d and is now being imported after its instantiation
|
||||
// failed (e.g. a missing named export). Try finishing the instantiation: if it succeeds,
|
||||
// proceed to evaluation, otherwise re-throw the instantiation error.
|
||||
this.instantiateSync();
|
||||
}
|
||||
// `status === kInstantiated`: either just instantiated above, or previously instantiated
|
||||
// but evaluation was deferred (e.g. TLA detected by a prior `runSync()` call)
|
||||
if (isEntryPoint) {
|
||||
globalThis[entry_point_module_private_symbol] = this.module;
|
||||
}
|
||||
setHasStartedUserESMExecution();
|
||||
if (this.module.hasAsyncGraph) {
|
||||
// Has top-level `await`: fall back to async evaluation
|
||||
const timeout = -1;
|
||||
const breakOnSigint = false;
|
||||
this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint);
|
||||
if (this.inspectBrk) {
|
||||
const { callAndPauseOnStart } = internalBinding('inspector');
|
||||
this.evaluationPromise = callAndPauseOnStart(this.module.evaluate, this.module, timeout, breakOnSigint);
|
||||
} else {
|
||||
this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint);
|
||||
}
|
||||
await this.evaluationPromise;
|
||||
this.evaluationPromise = undefined;
|
||||
return { __proto__: null, module: this.module };
|
||||
}
|
||||
// No top-level `await`: evaluate synchronously
|
||||
try {
|
||||
this.#evaluateSync();
|
||||
} catch (evaluateError) {
|
||||
explainCommonJSGlobalLikeNotDefinedError(evaluateError, this.module.url, this.module.hasTopLevelAwait);
|
||||
throw evaluateError;
|
||||
}
|
||||
return { __proto__: null, module: this.module };
|
||||
}
|
||||
|
||||
assert.fail('Unexpected status of a module that is imported again after being required. ' +
|
||||
`Status = ${status}`);
|
||||
// Evaluate this module synchronously, pausing at the first line of user land JS
|
||||
// that runs if --inspect-brk is active - that may well be inside a dependency.
|
||||
#evaluateSync() {
|
||||
if (this.inspectBrk) {
|
||||
const { callAndPauseOnStart } = internalBinding('inspector');
|
||||
return callAndPauseOnStart(this.module.evaluateSync, this.module);
|
||||
}
|
||||
return this.module.evaluateSync();
|
||||
}
|
||||
|
||||
runSync(parent) {
|
||||
@@ -662,15 +707,14 @@ class ModuleJobSync extends ModuleJobBase {
|
||||
assert(this.shouldRunModule(this.phase));
|
||||
// TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the
|
||||
// async graph error supersedes instantiation (mismatch export) errors in the graph.
|
||||
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
|
||||
this.module.instantiate();
|
||||
this.instantiateSync();
|
||||
// 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.
|
||||
this.throwIfAsyncGraph(parent);
|
||||
setHasStartedUserESMExecution();
|
||||
try {
|
||||
const namespace = this.module.evaluateSync();
|
||||
const namespace = this.#evaluateSync();
|
||||
return { __proto__: null, module: this.module, namespace };
|
||||
} catch (e) {
|
||||
explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait);
|
||||
|
||||
@@ -140,6 +140,8 @@ function runEntryPointWithESMLoader(callback) {
|
||||
* Because of module detection, this function will attempt to run ambiguous (no explicit extension, no
|
||||
* `package.json` type field) entry points as CommonJS first; under certain conditions, it will retry running as ESM.
|
||||
* @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
|
||||
* @returns {Promise|undefined} A promise when the entry point is still being evaluated
|
||||
* asynchronously because its graph contains top-level await, otherwise `undefined`.
|
||||
*/
|
||||
function executeUserEntryPoint(main = process.argv[1]) {
|
||||
if (getOptionValue('[vfs_load_set]')) {
|
||||
@@ -173,6 +175,31 @@ function executeUserEntryPoint(main = process.argv[1]) {
|
||||
const mainPath = resolvedMain || main;
|
||||
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
|
||||
|
||||
// When no async loader hooks or --import are registered, load the entry point
|
||||
// directly. The graph is loaded and linked synchronously, and evaluated
|
||||
// synchronously too unless it contains top-level await.
|
||||
if (getOptionValue('--experimental-loader').length === 0 &&
|
||||
getOptionValue('--import').length === 0) {
|
||||
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
||||
const job = cascadedLoader.importForEntryPoint(mainURL.href);
|
||||
if (job === undefined) {
|
||||
// Evaluated synchronously; no promise was created at any point.
|
||||
return;
|
||||
}
|
||||
// Top-level await: evaluate through the usual asynchronous entry point path,
|
||||
// reusing the job that was already resolved, linked and instantiated above.
|
||||
return runEntryPointWithESMLoader(async () => {
|
||||
// Begin evaluation inside a microtask, as `import()` does by awaiting the
|
||||
// module job before running it, so that a top-level await in the entry point
|
||||
// resumes in the same microtask drain instead of unwinding to the main stack
|
||||
// and letting the `process.nextTick` queue run first.
|
||||
await null;
|
||||
// Await rather than returning the job's promise: resolving a promise *with* a
|
||||
// thenable performs a `then` lookup on it, which user code can observe.
|
||||
await job.run(true);
|
||||
});
|
||||
}
|
||||
|
||||
runEntryPointWithESMLoader((cascadedLoader) => {
|
||||
// Note that if the graph contains unsettled TLA, this may never resolve
|
||||
// even after the event loop stops running.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import '../common/index.mjs';
|
||||
import { spawnSyncAndAssert } from '../common/child_process.js';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import assert from 'assert';
|
||||
|
||||
const entryPoint = fixtures.path('es-modules', 'package-cjs-named-error', 'single-quote.mjs');
|
||||
spawnSyncAndAssert(process.execPath, [entryPoint], {
|
||||
status: 1,
|
||||
stderr(stderr) {
|
||||
assert(stderr.includes("Named export 'comeOn' not found."),
|
||||
'entry point should show the missing named export');
|
||||
assert(stderr.includes('CommonJS modules can always be imported via the default export'),
|
||||
'entry point should show the CommonJS named export hint');
|
||||
assert(stderr.includes("import pkg from './fail.cjs';"),
|
||||
'entry point hint should recommend the default import');
|
||||
assert(stderr.includes('const { comeOn } = pkg;'),
|
||||
'entry point hint should show the named import as destructuring');
|
||||
assert(stderr.includes("import { comeOn } from './fail.cjs';"),
|
||||
'entry point error should include the source import statement');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Flags: --no-warnings
|
||||
import '../common/index.mjs';
|
||||
import { spawnSyncAndAssert } from '../common/child_process.js';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
// Promise counts are implementation details tracked for regressions, not API guarantees.
|
||||
|
||||
describe('synchronous ESM loading', () => {
|
||||
it('should create minimal promises for ESM importing ESM', async () => {
|
||||
const count = getPromiseCount(fixtures.path('es-modules', 'import-esm.mjs'));
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
|
||||
it('should create minimal promises for ESM importing CJS', async () => {
|
||||
const count = getPromiseCount(fixtures.path('es-modules', 'builtin-imports-case.mjs'));
|
||||
assert.strictEqual(count, 4);
|
||||
});
|
||||
|
||||
it('should fall back to async evaluation for top-level await', async () => {
|
||||
const count = getPromiseCount(fixtures.path('es-modules', 'tla', 'resolved.mjs'));
|
||||
assert(count > 1, `Expected TLA fallback to create multiple promises, got ${count}`);
|
||||
});
|
||||
|
||||
it('should create minimal promises when entry point is CJS importing ESM', async () => {
|
||||
const count = getPromiseCount(fixtures.path('es-modules', 'require-esm-entry.cjs'));
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
});
|
||||
|
||||
function getPromiseCount(entry) {
|
||||
const { stderr } = spawnSyncAndAssert(process.execPath, [
|
||||
'--trace-promises',
|
||||
entry,
|
||||
], {
|
||||
stderr: /created promise #/,
|
||||
});
|
||||
return stderr.match(/created promise #/g)?.length ?? 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
require('./imported-esm.mjs');
|
||||
@@ -1,6 +1,6 @@
|
||||
node:internal/modules/run_main:<line>
|
||||
triggerUncaughtException(
|
||||
^
|
||||
node:internal/modules/esm/module_job:<line>
|
||||
return this.module.evaluateSync();
|
||||
^
|
||||
|
||||
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
// Test that --inspect-brk pauses at the first executable line of an ESM entry
|
||||
// point that uses top-level await (which takes the async evaluation path).
|
||||
|
||||
common.skipIfInspectorDisabled();
|
||||
|
||||
const assert = require('assert');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const { NodeInstance } = require('../common/inspector-helper.js');
|
||||
|
||||
async function testBreakpointOnStart(session) {
|
||||
const commands = [
|
||||
{ 'method': 'Runtime.enable' },
|
||||
{ 'method': 'Debugger.enable' },
|
||||
{ 'method': 'Debugger.setPauseOnExceptions',
|
||||
'params': { 'state': 'none' } },
|
||||
{ 'method': 'Debugger.setAsyncCallStackDepth',
|
||||
'params': { 'maxDepth': 0 } },
|
||||
{ 'method': 'Debugger.setBlackboxPatterns',
|
||||
'params': { 'patterns': [] } },
|
||||
{ 'method': 'Runtime.runIfWaitingForDebugger' },
|
||||
];
|
||||
|
||||
await session.send({ method: 'NodeRuntime.enable' });
|
||||
await session.waitForNotification('NodeRuntime.waitingForDebugger');
|
||||
await session.send(commands);
|
||||
await session.send({ method: 'NodeRuntime.disable' });
|
||||
// Static import declarations are not executable statements, so the break
|
||||
// happens at the first executable line (await setTimeout(9), line index 4).
|
||||
await session.waitForBreakOnLine(4, session.scriptURL());
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
const child = new NodeInstance(['--inspect-brk=0'], '',
|
||||
fixtures.path('es-modules', 'esm-top-level-await.mjs'));
|
||||
const session = await child.connectInspectorSession();
|
||||
|
||||
await testBreakpointOnStart(session);
|
||||
await session.runToCompletion();
|
||||
|
||||
assert.strictEqual((await child.expectShutdown()).exitCode, 0);
|
||||
}
|
||||
|
||||
runTests().then(common.mustCall());
|
||||
Reference in new issue
Block a user