diff --git a/packages/vitest/src/integrations/env/loader.ts b/packages/vitest/src/integrations/env/loader.ts index 3c761c8e7d29..cd06d913a81e 100644 --- a/packages/vitest/src/integrations/env/loader.ts +++ b/packages/vitest/src/integrations/env/loader.ts @@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs' import { isBuiltin } from 'node:module' import { pathToFileURL } from 'node:url' import { resolve } from 'pathe' -import { ModuleRunner } from 'vite/module-runner' +import { EvaluatedModules, ModuleRunner } from 'vite/module-runner' import { VitestTransport } from '../../runtime/moduleRunner/moduleTransport' import { environments } from './index' @@ -24,6 +24,7 @@ export function createEnvironmentLoader(root: string, rpc: WorkerRPC): ModuleRun if (!cachedLoader || cachedLoader.isClosed()) { _loaders.delete(root) + const evaluatedModules = new EvaluatedModules() const moduleRunner = new ModuleRunner({ hmr: false, sourcemapInterceptor: 'prepareStackTrace', @@ -46,7 +47,7 @@ export function createEnvironmentLoader(root: string, rpc: WorkerRPC): ModuleRun async resolveId(id, importer) { return rpc.resolve(id, importer, '__vitest__') }, - }), + }, evaluatedModules, new WeakMap()), }) _loaders.set(root, moduleRunner) } diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index f15635e152b3..c96cb73dc921 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -301,7 +301,7 @@ export class Logger { this.error(errorMessage) errors.forEach((err) => { this.printError(err, { - fullStack: true, + fullStack: (err as any).name !== 'EnvironmentTeardownError', type: (err as any).type || 'Unhandled Error', }) }) diff --git a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts index 6f7eb54ddbd7..af3093baed71 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleRunner.ts @@ -49,11 +49,13 @@ export class VitestModuleRunner public mocker: VitestMocker public moduleExecutionInfo: ModuleExecutionInfo private _otel: Traces + private _callstacks: WeakMap constructor(private vitestOptions: VitestModuleRunnerOptions) { const options = vitestOptions - const transport = new VitestTransport(options.transport) const evaluatedModules = options.evaluatedModules + const callstacks = new WeakMap() + const transport = new VitestTransport(options.transport, evaluatedModules, callstacks) super( { transport, @@ -64,6 +66,7 @@ export class VitestModuleRunner }, options.evaluator, ) + this._callstacks = callstacks this._otel = vitestOptions.traces || new Traces({ enabled: false }) this.moduleExecutionInfo = options.getWorkerState().moduleExecutionInfo this.mocker = options.mocker || new VitestMocker(this, { @@ -153,6 +156,9 @@ export class VitestModuleRunner metadata?: SSRImportMetadata, ignoreMock = false, ): Promise { + // Track for a better error message if dynamic import is not resolved properly + this._callstacks.set(mod, callstack) + if (ignoreMock) { return this._cachedRequest(url, mod, callstack, metadata) } diff --git a/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts b/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts index 90fe7dc728b8..75398656ffb2 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleTransport.ts @@ -1,5 +1,6 @@ -import type { FetchFunction, ModuleRunnerTransport } from 'vite/module-runner' +import type { EvaluatedModuleNode, EvaluatedModules, FetchFunction, ModuleRunnerTransport } from 'vite/module-runner' import type { ResolveFunctionResult } from '../../types/general' +import { EnvironmentTeardownError } from '../utils' export interface VitestTransportOptions { fetchModule: FetchFunction @@ -7,7 +8,11 @@ export interface VitestTransportOptions { } export class VitestTransport implements ModuleRunnerTransport { - constructor(private options: VitestTransportOptions) {} + constructor( + private options: VitestTransportOptions, + private evaluatedModules: EvaluatedModules, + private callstacks: WeakMap, + ) {} async invoke(event: any): Promise<{ result: any } | { error: any }> { if (event.type !== 'custom') { @@ -29,8 +34,24 @@ export class VitestTransport implements ModuleRunnerTransport { const result = await this.options.fetchModule(...data as Parameters) return { result } } - catch (error) { - return { error } + catch (cause) { + if (cause instanceof EnvironmentTeardownError) { + const [id, importer] = data as Parameters + let message = `Cannot load '${id}'${importer ? ` imported from ${importer}` : ''} after the environment was torn down. ` + + `This is not a bug in Vitest.` + + const moduleNode = importer ? this.evaluatedModules.getModuleById(importer) : undefined + const callstack = moduleNode ? this.callstacks.get(moduleNode) : undefined + if (callstack) { + message += ` The last recorded callstack:\n- ${[...callstack, importer, id].reverse().join('\n- ')}` + } + const error = new EnvironmentTeardownError(message) + if (cause.stack) { + error.stack = cause.stack.replace(cause.message, error.message) + } + return { error } + } + return { error: cause } } } } diff --git a/packages/vitest/src/runtime/utils.ts b/packages/vitest/src/runtime/utils.ts index fcf660bda1af..e19c86bb9fe5 100644 --- a/packages/vitest/src/runtime/utils.ts +++ b/packages/vitest/src/runtime/utils.ts @@ -4,6 +4,10 @@ import { getSafeTimers } from '@vitest/utils/timers' const NAME_WORKER_STATE = '__vitest_worker__' +export class EnvironmentTeardownError extends Error { + name = 'EnvironmentTeardownError' +} + export function getWorkerState(): WorkerGlobalState { // @ts-expect-error untyped global const workerState = globalThis[NAME_WORKER_STATE] diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 62888bf6cf92..b08b8e6dc20a 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -6,6 +6,7 @@ import { setupInspect } from './inspector' import * as listeners from './listeners' import { VitestEvaluatedModules } from './moduleRunner/evaluatedModules' import { onCancel, rpcDone } from './rpc' +import { EnvironmentTeardownError } from './utils' const resolvingModules = new Set() @@ -21,7 +22,7 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: Vites // do not close the RPC channel so that we can get the error messages sent to the main thread cleanups.push(async () => { await Promise.all(rpc.$rejectPendingCalls(({ method, reject }) => { - reject(new Error(`[vitest-worker]: Closing rpc while "${method}" was pending`)) + reject(new EnvironmentTeardownError(`[vitest-worker]: Closing rpc while "${method}" was pending`)) })) })