Skip to content
Merged
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
5 changes: 3 additions & 2 deletions packages/vitest/src/integrations/env/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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',
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
})
Expand Down
8 changes: 7 additions & 1 deletion packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ export class VitestModuleRunner
public mocker: VitestMocker
public moduleExecutionInfo: ModuleExecutionInfo
private _otel: Traces
private _callstacks: WeakMap<EvaluatedModuleNode, string[]>

constructor(private vitestOptions: VitestModuleRunnerOptions) {
const options = vitestOptions
const transport = new VitestTransport(options.transport)
const evaluatedModules = options.evaluatedModules
const callstacks = new WeakMap<EvaluatedModuleNode, string[]>()
const transport = new VitestTransport(options.transport, evaluatedModules, callstacks)
super(
{
transport,
Expand All @@ -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, {
Expand Down Expand Up @@ -153,6 +156,9 @@ export class VitestModuleRunner
metadata?: SSRImportMetadata,
ignoreMock = false,
): Promise<any> {
// 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)
}
Expand Down
29 changes: 25 additions & 4 deletions packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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
resolveId: (id: string, importer?: string) => Promise<ResolveFunctionResult | null>
}

export class VitestTransport implements ModuleRunnerTransport {
constructor(private options: VitestTransportOptions) {}
constructor(
private options: VitestTransportOptions,
private evaluatedModules: EvaluatedModules,
private callstacks: WeakMap<EvaluatedModuleNode, string[]>,
) {}

async invoke(event: any): Promise<{ result: any } | { error: any }> {
if (event.type !== 'custom') {
Expand All @@ -29,8 +34,24 @@ export class VitestTransport implements ModuleRunnerTransport {
const result = await this.options.fetchModule(...data as Parameters<FetchFunction>)
return { result }
}
catch (error) {
return { error }
catch (cause) {
if (cause instanceof EnvironmentTeardownError) {
const [id, importer] = data as Parameters<FetchFunction>
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 }
}
}
}
4 changes: 4 additions & 0 deletions packages/vitest/src/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/runtime/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>()

Expand All @@ -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`))
}))
})

Expand Down
Loading