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
1 change: 1 addition & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@

constructor(asyncLoaderHooks) {
this.#setAsyncLoaderHooks(asyncLoaderHooks);
this.importParents = new Map();

Check failure on line 214 in lib/internal/modules/esm/loader.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Use `const { SafeMap } = primordials;` instead of the global
}

/**
Expand Down
74 changes: 71 additions & 3 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,73 @@
debug = fn;
});

const {
overrideStackTrace,
ErrorPrepareStackTrace,

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Out of ASCIIbetical order - overrideStackTrace >= ErrorPrepareStackTrace
codes,

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Use destructuring to define error codes used in the file
} = require('internal/errors');

const { ERR_REQUIRE_ASYNC_MODULE } = codes;

/**

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing JSDoc @returns declaration
* Builds a linear import trace by walking parent modules
* from the module that threw during evaluation.
*/
function buildImportTrace(importParents, startURL) {
const trace = [];
let current = startURL;
const seen = new Set([current]);

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Use `const { SafeSet } = primordials;` instead of the global

while (true) {
const parent = importParents.get(current);
if (!parent || seen.has(parent)) break;

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected { after 'if' condition

trace.push({ child: current, parent });
seen.add(current);
current = parent;
}

return trace.length ? trace : null;
}

/**

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing JSDoc @returns declaration
* Formats an import trace for inclusion in an error stack.
*/
function formatImportTrace(trace) {
return trace
.map(({ child, parent }) => ` ${child} imported by ${parent}`)
.join('\n');
}

/**
* Appends an ESM import trace to an error’s stack output.

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Non-ASCII character '’' detected. Consider replacing with: '
* Uses a per-error stack override; no global side effects.
*/
function decorateErrorWithImportTrace(e, importParents) {
if (!importParents || typeof importParents.get !== 'function') return;

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected { after 'if' condition

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

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected indentation of 2 spaces but found 3
if (!e || typeof e !== 'object') return;

overrideStackTrace.set(e, (error, trace) => {
let thrownURL;
for (const cs of trace) {
const getFileName = cs.getFileName;
if (typeof getFileName === 'function') {
const file = getFileName.call(cs);
if (typeof file === 'string' && file.startsWith('file://')) {
thrownURL = file;
break;
}
}
}

const importTrace = thrownURL ? buildImportTrace(importParents, thrownURL) : null;
const stack = ErrorPrepareStackTrace(error, trace);
if (!importTrace) return stack;

return `${stack}\n\nImport trace:\n${formatImportTrace(importTrace)}`;
});
}

const {
ModuleWrap,
kErrored,
Expand Down Expand Up @@ -53,9 +120,6 @@
} = require('internal/modules/helpers');
const { getOptionValue } = require('internal/options');
const noop = FunctionPrototype;
const {
ERR_REQUIRE_ASYNC_MODULE,
} = require('internal/errors').codes;
let hasPausedEntry = false;

const CJSGlobalLike = [
Expand Down Expand Up @@ -159,6 +223,7 @@
// that hooks can pre-fetch sources off-thread.
const job = this.loader.getOrCreateModuleJob(this.url, request, requestType);
debug(`ModuleJobBase.syncLink() ${this.url} -> ${request.specifier}`, job);
this.loader.importParents.set(job.url, this.url);
assert(!isPromise(job));
assert(job.module instanceof ModuleWrap);
if (request.phase === kEvaluationPhase) {
Expand Down Expand Up @@ -430,6 +495,9 @@
await this.module.evaluate(timeout, breakOnSigint);
} catch (e) {
explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait);

decorateErrorWithImportTrace(e, this.loader.importParents);

throw e;
}
return { __proto__: null, module: this.module };
Expand Down
26 changes: 26 additions & 0 deletions test/es-module/test-esm-import-trace.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spawnSync } from 'node:child_process';
import assert from 'node:assert';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { test } from 'node:test';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const fixture = path.join(
__dirname,
'../fixtures/es-modules/import-trace/entry.mjs'
);

test('includes import trace for evaluation-time errors', () => {
const result = spawnSync(
process.execPath,
[fixture],
{ encoding: 'utf8' }
);

assert.notStrictEqual(result.status, 0);
assert.match(result.stderr, /Import trace:/);
assert.match(result.stderr, /bar\.mjs imported by .*foo\.mjs/);
assert.match(result.stderr, /foo\.mjs imported by .*entry\.mjs/);
});
1 change: 1 addition & 0 deletions test/fixtures/es-modules/import-trace/bar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
throw new Error('bar failed');
1 change: 1 addition & 0 deletions test/fixtures/es-modules/import-trace/entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './foo.mjs';
1 change: 1 addition & 0 deletions test/fixtures/es-modules/import-trace/foo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './bar.mjs';
Loading