Skip to content

Commit 9697b38

Browse files
authored
fix: keep the import frame in native loader errors (#11599)
1 parent c010fc2 commit 9697b38

2 files changed

Lines changed: 45 additions & 9 deletions

File tree

‎packages/vitest/src/runtime/moduleRunner/nativeModuleMocker.ts‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ import { BareModuleMocker, normalizeModuleId } from './bareModuleMocker'
1515

1616
export class NativeModuleMocker extends BareModuleMocker {
1717
public wrapDynamicImport<T>(moduleFactory: () => Promise<T>): Promise<T> {
18-
if (typeof moduleFactory === 'function') {
19-
const promise = new Promise<T>((resolve, reject) => {
20-
this.resolveMocks().finally(() => {
21-
moduleFactory().then(resolve, reject)
22-
})
23-
})
24-
return promise
18+
if (typeof moduleFactory !== 'function') {
19+
return moduleFactory
2520
}
26-
return moduleFactory
21+
// Node.js can evaluate the module outside of the importer's async chain,
22+
// so the stack of an import error would not point at the import
23+
const importer = new Error('import location')
24+
return new Promise<T>((resolve, reject) => {
25+
this.resolveMocks().finally(() => {
26+
moduleFactory().then(resolve, (error) => reject(addImporterFrame(error, importer)))
27+
})
28+
})
2729
}
2830

2931
public resolveMockedModule(url: string, parentURL: string): module.ResolveFnOutput | undefined {
@@ -288,3 +290,29 @@ function transformCode(code: string, format: string, filename: string) {
288290
}
289291
return code
290292
}
293+
294+
function addImporterFrame(error: unknown, importer: Error): unknown {
295+
const lines = importer.stack?.split('\n') || []
296+
const index = lines.findIndex((line) => line.includes('wrapDynamicImport'))
297+
const frame = index === -1 ? undefined : lines[index + 1]
298+
const file = frame?.match(/\(?([^()\s]+):\d+:\d+\)?$/)?.[1]
299+
if (!frame || !file || frame.includes('node:')) {
300+
return error
301+
}
302+
const asyncFrame = frame.replace(/^(\s*)at /, '$1at async ')
303+
const seen = new Set<Error>()
304+
let current: unknown = error
305+
while (current instanceof Error && !seen.has(current)) {
306+
seen.add(current)
307+
const stack = current.stack
308+
// older Node.js versions keep the async frame of the import themselves
309+
if (
310+
typeof stack === 'string' &&
311+
!stack.split('\n').some((line) => line.includes('at async ') && line.includes(file))
312+
) {
313+
current.stack = `${stack}\n${asyncFrame}`
314+
}
315+
current = current.cause
316+
}
317+
return error
318+
}

‎test/e2e/test/no-module-runner.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,14 +766,22 @@ test('not reported')
766766
})
767767

768768
// "slice" removes the stack from unhandled error because it references built artifacts
769-
expect(stderr.split('\n').slice(0, 17).join('\n')).toMatchInlineSnapshot(`
769+
expect(stderr.split('\n').slice(0, 25).join('\n')).toMatchInlineSnapshot(`
770770
"
771771
⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
772772
773773
FAIL add.test.js [ add.test.js ]
774774
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
775+
❯ <anonymous> add.test.js:2:1
776+
1|
777+
2| import { add } from './add.js'
778+
| ^
779+
3| vi.mock('./add.js', async () => {
780+
4| await Promise.resolve()
781+
775782
Caused by: Error: error from factory
776783
❯ <anonymous> add.test.js:5:9
784+
❯ <anonymous> add.test.js:2:1
777785
778786
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
779787

0 commit comments

Comments
 (0)