feat!: use module-runner instead of vite-node (#8208)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
This commit is contained in:
Vladimir 2025-07-28 13:43:53 +02:00 committed by GitHub
parent 94ab392b36
commit 9be01ba594
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 3182 additions and 1673 deletions

2
.npmrc
View File

@ -3,4 +3,4 @@ strict-peer-dependencies=false
provenance=true
shell-emulator=true
registry=https://registry.npmjs.org/
VITE_NODE_DEPS_MODULE_DIRECTORIES=/node_modules/,/packages/
VITEST_MODULE_DIRECTORIES=/node_modules/,/packages/

View File

@ -121,14 +121,14 @@ export default CustomRunner
```
::: warning
Vitest also injects an instance of `ViteNodeRunner` as `__vitest_executor` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`).
Vitest also injects an instance of `ModuleRunner` from `vite/module-runner` as `moduleRunner` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`).
`ViteNodeRunner` exposes `executeId` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it:
`ModuleRunner` exposes `import` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it:
```ts
export default class Runner {
async importFile(filepath: string) {
await this.__vitest_executor.executeId(filepath)
await this.moduleRunner.import(filepath)
}
}
```

View File

@ -233,7 +233,7 @@ Handling for dependencies resolution.
#### deps.optimizer {#deps-optimizer}
- **Type:** `{ ssr?, web? }`
- **Type:** `{ ssr?, client? }`
- **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html)
Enable dependency optimization. If you have a lot of tests, this might improve their performance.
@ -245,7 +245,7 @@ When Vitest encounters the external library listed in `include`, it will be bund
- Your `alias` configuration is now respected inside bundled packages
- Code in your tests is running closer to how it's running in the browser
Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.web` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments, but it is configurable by [`transformMode`](#testtransformmode).
Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.client` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments.
This options also inherits your `optimizeDeps` configuration (for web Vitest will extend `optimizeDeps`, for ssr - `ssr.optimizeDeps`). If you redefine `include`/`exclude` option in `deps.optimizer` it will extend your `optimizeDeps` when running tests. Vitest automatically removes the same options from `include`, if they are listed in `exclude`.
@ -260,15 +260,15 @@ You will not be able to edit your `node_modules` code for debugging, since the c
Enable dependency optimization.
#### deps.web {#deps-web}
#### deps.client {#deps-client}
- **Type:** `{ transformAssets?, ... }`
Options that are applied to external files when transform mode is set to `web`. By default, `jsdom` and `happy-dom` use `web` mode, while `node` and `edge` environments use `ssr` transform mode, so these options will have no affect on files inside those environments.
Options that are applied to external files when the environment is set to `client`. By default, `jsdom` and `happy-dom` use `client` environment, while `node` and `edge` environments use `ssr`, so these options will have no affect on files inside those environments.
Usually, files inside `node_modules` are externalized, but these options also affect files in [`server.deps.external`](#server-deps-external).
#### deps.web.transformAssets
#### deps.client.transformAssets
- **Type:** `boolean`
- **Default:** `true`
@ -281,7 +281,7 @@ This module will have a default export equal to the path to the asset, if no que
At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools.
:::
#### deps.web.transformCss
#### deps.client.transformCss
- **Type:** `boolean`
- **Default:** `true`
@ -294,7 +294,7 @@ If CSS files are disabled with [`css`](#css) options, this option will just sile
At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools.
:::
#### deps.web.transformGlobPattern
#### deps.client.transformGlobPattern
- **Type:** `RegExp | RegExp[]`
- **Default:** `[]`
@ -560,7 +560,7 @@ import type { Environment } from 'vitest'
export default <Environment>{
name: 'custom',
transformMode: 'ssr',
viteEnvironment: 'ssr',
setup() {
// custom setup
return {
@ -1676,28 +1676,6 @@ Will call [`vi.unstubAllEnvs`](/api/vi#vi-unstuballenvs) before each test.
Will call [`vi.unstubAllGlobals`](/api/vi#vi-unstuballglobals) before each test.
### testTransformMode {#testtransformmode}
- **Type:** `{ web?, ssr? }`
Determine the transform method for all modules imported inside a test that matches the glob pattern. By default, relies on the environment. For example, tests with JSDOM environment will process all files with `ssr: false` flag and tests with Node environment process all modules with `ssr: true`.
#### testTransformMode.ssr
- **Type:** `string[]`
- **Default:** `[]`
Use SSR transform pipeline for all modules inside specified tests.<br>
Vite plugins will receive `ssr: true` flag when processing those files.
#### testTransformMode&#46;web
- **Type:** `string[]`
- **Default:** `[]`
First do a normal transform pipeline (targeting browser), then do a SSR rewrite to run the code in Node.<br>
Vite plugins will receive `ssr: false` flag when processing those files.
### snapshotFormat<NonProjectOption />
- **Type:** `PrettyFormatOptions`

View File

@ -48,7 +48,7 @@ import type { Environment } from 'vitest/environments'
export default <Environment>{
name: 'custom',
transformMode: 'ssr',
viteEnvironment: 'ssr',
// optional - only if you support "experimental-vm" pool
async setupVM() {
const vm = await import('node:vm')
@ -74,7 +74,7 @@ export default <Environment>{
```
::: warning
Vitest requires `transformMode` option on environment object. It should be equal to `ssr` or `web`. This value determines how plugins will transform source code. If it's set to `ssr`, plugin hooks will receive `ssr: true` when transforming or resolving files. Otherwise, `ssr` is set to `false`.
Vitest requires `viteEnvironment` option on environment object (fallbacks to the Vitest environment name by default). It should be equal to `ssr`, `client` or any custom [Vite environment](https://vite.dev/guide/api-environment) name. This value determines which environment is used to process file.
:::
You also have access to default Vitest environments through `vitest/environments` entry:

View File

@ -145,6 +145,21 @@ $ pnpm run test:dev math.test.ts
```
:::
### Replacing `vite-node` with [Module Runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner)
Module Runner is a successor to `vite-node` implemented directly in Vite. Vitest now uses it directly instead of having a wrapper around Vite SSR handler. This means that certain features are no longer available:
- `VITE_NODE_DEPS_MODULE_DIRECTORIES` environment variable was replaced with `VITEST_MODULE_DIRECTORIES`
- Vitest no longer injects `__vitest_executor` into every [test runner](/advanced/runner). Instead, it injects `moduleRunner` which is an instance of [`ModuleRunner`](https://vite.dev/guide/api-environment-runtimes.html#modulerunner)
- `vitest/execute` entry point was removed. It was always meant to be internal
- [Custom environments](/guide/environment) no longer need to provide a `transformMode` property. Instead, provide `viteEnvironment`. If it is not provided, Vitest will use the environment name to transform files on the server (see [`server.environments`](https://vite.dev/guide/api-environment-instances.html))
- `vite-node` is no longer a dependency of Vitest
- `deps.optimizer.web` was renamed to [`deps.optimizer.client`](/config/#deps-optimizer-client). You can also use any custom names to apply optimizer configs when using other server environments
Vite has its own externalization mechanism, but we decided to keep using the old one to reduce the amount of breaking changes. You can keep using [`server.deps`](/config/#server-deps) to inline or externalize packages.
This update should not be noticeable unless you rely on advanced features mentioned above.
### Deprecated APIs are Removed
Vitest 4.0 removes some deprecated APIs, including:
@ -152,8 +167,9 @@ Vitest 4.0 removes some deprecated APIs, including:
- `poolMatchGlobs` config option. Use [`projects`](/guide/projects) instead.
- `environmentMatchGlobs` config option. Use [`projects`](/guide/projects) instead.
- `workspace` config option. Use [`projects`](/guide/projects) instead.
- `deps.external`, `deps.inline`, `deps.fallbackCJS` config options. Use `server.deps.external`, `server.deps.inline`, or `server.deps.fallbackCJS` instead.
This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in `node` types (see [#5481](https://github.com/vitest-dev/vitest/issues/5481) and [#6141](https://github.com/vitest-dev/vitest/issues/6141)).
This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in `@types/node` (see [#5481](https://github.com/vitest-dev/vitest/issues/5481) and [#6141](https://github.com/vitest-dev/vitest/issues/6141)).
## Migrating from Jest {#jest}

View File

@ -12,7 +12,7 @@ import {
} from 'vitest/internal/browser'
import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
import { createStackString, parseStacktrace } from '../../../../utils/src/source-map'
import { executor, getWorkerState } from '../utils'
import { getWorkerState, moduleRunner } from '../utils'
import { rpc } from './rpc'
import { VitestBrowserSnapshotEnvironment } from './snapshot'
@ -117,7 +117,7 @@ export function createBrowserRunner(
await rpc().onAfterSuiteRun({
coverage,
testFiles: files.map(file => file.name),
transformMode: 'browser',
environment: '__browser__',
projectName: this.config.name,
})
}
@ -223,7 +223,7 @@ export async function initiateRunner(
const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, {
takeCoverage: () =>
takeCoverageInsideWorker(config.coverage, executor),
takeCoverageInsideWorker(config.coverage, moduleRunner),
})
if (!config.snapshotOptions.snapshotEnvironment) {
config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
@ -238,8 +238,8 @@ export async function initiateRunner(
})
const [diffOptions] = await Promise.all([
loadDiffConfig(config, executor as any),
loadSnapshotSerializers(config, executor as any),
loadDiffConfig(config, moduleRunner as any),
loadSnapshotSerializers(config, moduleRunner as any),
])
runner.config.diffOptions = diffOptions
getWorkerState().onFilterStackTrace = (stack: string) => {

View File

@ -25,14 +25,16 @@ const state: WorkerGlobalState = {
config,
environment: {
name: 'browser',
transformMode: 'web',
viteEnvironment: 'client',
setup() {
throw new Error('Not called in the browser')
},
},
onCleanup: fn => getBrowserState().cleanups.push(fn),
moduleCache: getBrowserState().moduleCache,
evaluatedModules: getBrowserState().evaluatedModules,
resolvingModules: getBrowserState().resolvingModules,
moduleExecutionInfo: new Map(),
metaEnv: null as any,
rpc: null as any,
durations: {
environment: 0,

View File

@ -10,7 +10,7 @@ import {
startTests,
stopCoverageInsideWorker,
} from 'vitest/internal/browser'
import { executor, getBrowserState, getConfig, getWorkerState } from '../utils'
import { getBrowserState, getConfig, getWorkerState, moduleRunner } from '../utils'
import { setupDialogsSpy } from './dialog'
import { setupConsoleLogSpy } from './logger'
import { VitestBrowserClientMocker } from './mocker'
@ -101,6 +101,7 @@ async function prepareTestEnvironment(options: PrepareOptions) {
const state = getWorkerState()
state.metaEnv = import.meta.env
state.onCancel = onCancel
state.rpc = rpc as any
@ -207,7 +208,7 @@ async function prepare(options: PrepareOptions) {
await Promise.all([
setupCommonEnv(config),
startCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }),
startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.browser.isolate }),
(async () => {
const VitestIndex = await import('vitest')
Object.defineProperty(window, '__vitest_index__', {
@ -249,7 +250,7 @@ async function cleanup() {
.catch(error => unhandledError(error, 'Cleanup Error'))
}
state.environmentTeardownRun = true
await stopCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }).catch((error) => {
await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.browser.isolate }).catch((error) => {
return unhandledError(error, 'Coverage Error')
})
}

View File

@ -1,5 +1,5 @@
import type { VitestRunner } from '@vitest/runner'
import type { SerializedConfig, WorkerGlobalState } from 'vitest'
import type { EvaluatedModules, SerializedConfig, WorkerGlobalState } from 'vitest'
import type { IframeOrchestrator } from './orchestrator'
import type { CommandsManager } from './tester/utils'
@ -13,10 +13,10 @@ export async function importFs(id: string): Promise<any> {
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
}
export const executor = {
export const moduleRunner = {
isBrowser: true,
executeId: (id: string): Promise<any> => {
import: (id: string): Promise<any> => {
if (id[0] === '/' || id[1] === ':') {
return importFs(id)
}
@ -65,7 +65,8 @@ export function ensureAwaited<T>(promise: (error?: Error) => Promise<T>): Promis
export interface BrowserRunnerState {
files: string[]
runningFiles: string[]
moduleCache: Map<string, any>
resolvingModules: Set<string>
evaluatedModules: EvaluatedModules
config: SerializedConfig
provider: string
runner: VitestRunner

View File

@ -122,7 +122,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
function setupClient(project: TestProject, rpcId: string, ws: WebSocket) {
const mockResolver = new ServerMockResolver(globalServer.vite, {
moduleDirectories: project.config.server?.deps?.moduleDirectories,
moduleDirectories: project.config?.deps?.moduleDirectories,
})
const mocker = project.browser?.provider.mocker

View File

@ -89,19 +89,19 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
const start = debug.enabled ? performance.now() : 0
const coverageMap = this.createCoverageMap()
let coverageMapByTransformMode = this.createCoverageMap()
let coverageMapByEnvironment = this.createCoverageMap()
await this.readCoverageFiles<CoverageMap>({
onFileRead(coverage) {
coverageMapByTransformMode.merge(coverage)
coverageMapByEnvironment.merge(coverage)
},
onFinished: async () => {
// Source maps can change based on projectName and transform mode.
// Coverage transform re-uses source maps so we need to separate transforms from each other.
const transformedCoverage = await transformCoverage(coverageMapByTransformMode)
const transformedCoverage = await transformCoverage(coverageMapByEnvironment)
coverageMap.merge(transformedCoverage)
coverageMapByTransformMode = this.createCoverageMap()
coverageMapByEnvironment = this.createCoverageMap()
},
onDebug: debug,
})

View File

@ -55,6 +55,7 @@
},
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "workspace:*",
"ast-v8-to-istanbul": "^0.3.3",
"debug": "catalog:",
"istanbul-lib-coverage": "catalog:",
@ -73,7 +74,6 @@
"@types/istanbul-reports": "catalog:",
"@vitest/browser": "workspace:*",
"pathe": "catalog:",
"vite-node": "workspace:*",
"vitest": "workspace:*"
}
}

View File

@ -37,10 +37,12 @@ const mod: CoverageProviderModule = {
try {
const result = coverage.result
.filter(filterResult)
.map(res => ({
...res,
startOffset: options?.moduleExecutionInfo?.get(fileURLToPath(res.url))?.startOffset || 0,
}))
.map((res) => {
return {
...res,
startOffset: options?.moduleExecutionInfo?.get(fileURLToPath(res.url))?.startOffset || 0,
}
})
resolve({ result })
}

View File

@ -1,13 +1,12 @@
import type { CoverageMap } from 'istanbul-lib-coverage'
import type { ProxifiedModule } from 'magicast'
import type { Profiler } from 'node:inspector'
import type { EncodedSourceMap, FetchResult } from 'vite-node'
import type { AfterSuiteRunMeta } from 'vitest'
import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vitest } from 'vitest/node'
import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vite, Vitest } from 'vitest/node'
import { promises as fs } from 'node:fs'
import { fileURLToPath } from 'node:url'
// @ts-expect-error -- untyped
import { mergeProcessCovs } from '@bcoe/v8-coverage'
import { cleanUrl } from '@vitest/utils'
import astV8ToIstanbul from 'ast-v8-to-istanbul'
import createDebug from 'debug'
import libCoverage from 'istanbul-lib-coverage'
@ -17,18 +16,17 @@ import reports from 'istanbul-reports'
import { parseModule } from 'magicast'
import { normalize } from 'pathe'
import { provider } from 'std-env'
import c from 'tinyrainbow'
import { cleanUrl } from 'vite-node/utils'
import c from 'tinyrainbow'
import { BaseCoverageProvider } from 'vitest/coverage'
import { parseAstAsync } from 'vitest/node'
import { isCSSRequest, parseAstAsync } from 'vitest/node'
import { version } from '../package.json' with { type: 'json' }
export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage {
startOffset: number
}
type TransformResults = Map<string, FetchResult>
type TransformResults = Map<string, Vite.TransformResult>
interface RawCoverage { result: ScriptCoverageWithOffset[] }
const FILE_PROTOCOL = 'file://'
@ -65,11 +63,11 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
}
})
},
onFinished: async (project, transformMode) => {
onFinished: async (project, environment) => {
const converted = await this.convertCoverage(
merged,
project,
transformMode,
environment,
)
// Source maps can change based on projectName and transform mode.
@ -148,7 +146,7 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
private async getCoverageMapForUncoveredFiles(testedFiles: string[]): Promise<CoverageMap> {
const transformResults = normalizeTransformResults(
this.ctx.vitenode.fetchCache,
this.ctx.vite.environments,
)
const transform = this.createUncoveredFileTransformer(this.ctx)
@ -293,6 +291,17 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
return true
}
// SSR mode's "import.meta.env ="
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& node.expression.left.type === 'MemberExpression'
&& node.expression.left.object.type === 'Identifier'
&& node.expression.left.object.name === '__vite_ssr_import_meta__') {
return true
}
// SWC's decorators
if (
type === 'statement'
@ -307,27 +316,27 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
)
}
private async getSources<TransformResult extends (FetchResult | Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>)>(
private async getSources(
url: string,
transformResults: TransformResults,
onTransform: (filepath: string) => Promise<TransformResult>,
onTransform: (filepath: string) => Promise<Vite.TransformResult | undefined | null>,
functions: Profiler.FunctionCoverage[] = [],
): Promise<{
code: string
map?: EncodedSourceMap
map?: Vite.Rollup.SourceMap
}> {
const filePath = normalize(fileURLToPath(url))
let transformResult: FetchResult | TransformResult | undefined = transformResults.get(filePath)
let transformResult: Vite.TransformResult | null | undefined = transformResults.get(filePath)
if (!transformResult) {
transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined)
}
const map = transformResult?.map as EncodedSourceMap | undefined
const map = transformResult?.map as Vite.Rollup.SourceMap | undefined
const code = transformResult?.code
if (!code) {
if (code == null) {
const original = await fs.readFile(filePath, 'utf-8').catch(() => {
// If file does not exist construct a dummy source for it.
// These can be files that were generated dynamically during the test run and were removed after it.
@ -357,31 +366,37 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
private async convertCoverage(
coverage: RawCoverage,
project: TestProject = this.ctx.getRootProject(),
transformMode?: AfterSuiteRunMeta['transformMode'],
environment: string,
): Promise<CoverageMap> {
let fetchCache = project.vitenode.fetchCache
if (transformMode) {
fetchCache = transformMode === 'browser' ? new Map() : project.vitenode.fetchCaches[transformMode]
if (environment === '__browser__' && !project.browser) {
throw new Error(`Cannot access browser module graph because it was torn down.`)
}
const transformResults = normalizeTransformResults(fetchCache)
const moduleGraph = environment === '__browser__'
? project.browser!.vite.environments.client.moduleGraph
: project.vite.environments[environment]?.moduleGraph
if (!moduleGraph) {
throw new Error(`Module graph for environment ${environment} was not defined.`)
}
const transformResults = normalizeTransformResults({ [environment]: { moduleGraph } })
async function onTransform(filepath: string) {
if (transformMode === 'browser' && project.browser) {
if (environment === '__browser__' && project.browser) {
const result = await project.browser.vite.transformRequest(removeStartsWith(filepath, project.config.root))
if (result) {
return { ...result, code: `${result.code}// <inline-source-map>` }
}
}
return project.vitenode.transformRequest(filepath)
return project.vite.environments[environment].transformRequest(filepath)
}
const scriptCoverages = []
for (const result of coverage.result) {
if (transformMode === 'browser') {
if (environment === '__browser__') {
if (result.url.startsWith('/@fs')) {
result.url = `${FILE_PROTOCOL}${removeStartsWith(result.url, '/@fs')}`
}
@ -393,7 +408,10 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
}
}
if (this.isIncluded(fileURLToPath(result.url))) {
// Ignore all CSS requests, so we don't override the actual code coverage
// In cases where CSS and JS are in the same file (.vue, .svelte)
// The file has a `.vue` extension, but the URL has `lang.css` query
if (!isCSSRequest(result.url) && this.isIncluded(fileURLToPath(result.url))) {
scriptCoverages.push(result)
}
}
@ -466,15 +484,17 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
}
function normalizeTransformResults(
fetchCache: Map<string, { result: FetchResult }>,
environments: Record<string, { moduleGraph: Vite.EnvironmentModuleGraph }>,
) {
const normalized: TransformResults = new Map()
for (const [key, value] of fetchCache.entries()) {
const cleanEntry = cleanUrl(key)
if (!normalized.has(cleanEntry)) {
normalized.set(cleanEntry, value.result)
for (const environmentName in environments) {
const moduleGraph = environments[environmentName].moduleGraph
for (const [key, value] of moduleGraph.idToModuleMap) {
const cleanEntry = cleanUrl(key)
if (value.transformResult && !normalized.has(cleanEntry)) {
normalized.set(cleanEntry, value.transformResult)
}
}
}

View File

@ -135,6 +135,10 @@ export class MockerRegistry {
this.registryByUrl.delete(id)
}
public deleteById(id: string): void {
this.registryById.delete(id)
}
public get(id: string): MockedModule | undefined {
return this.registryByUrl.get(id)
}

View File

@ -28,6 +28,10 @@
"types": "./dist/ast.d.ts",
"default": "./dist/ast.js"
},
"./resolver": {
"types": "./dist/resolver.d.ts",
"default": "./dist/resolver.js"
},
"./error": {
"types": "./dist/error.d.ts",
"default": "./dist/error.js"

View File

@ -16,6 +16,7 @@ const entries = {
'error': 'src/error.ts',
'source-map': 'src/source-map.ts',
'types': 'src/types.ts',
'resolver': 'src/resolver.ts',
}
const external = [

View File

@ -0,0 +1,60 @@
// TODO: this is all copy pasted from Vite - can they expose a module that exports only constants?
export const KNOWN_ASSET_TYPES: string[] = [
// images
'apng',
'bmp',
'png',
'jpe?g',
'jfif',
'pjpeg',
'pjp',
'gif',
'svg',
'ico',
'webp',
'avif',
// media
'mp4',
'webm',
'ogg',
'mp3',
'wav',
'flac',
'aac',
// fonts
'woff2?',
'eot',
'ttf',
'otf',
// other
'webmanifest',
'pdf',
'txt',
]
export const KNOWN_ASSET_RE: RegExp = new RegExp(
`\\.(${KNOWN_ASSET_TYPES.join('|')})$`,
)
export const CSS_LANGS_RE: RegExp
= /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
/**
* Prefix for resolved Ids that are not valid browser import specifiers
*/
export const VALID_ID_PREFIX = `/@id/`
/**
* Plugins that use 'virtual modules' (e.g. for helper functions), prefix the
* module ID with `\0`, a convention from the rollup ecosystem.
* This prevents other plugins from trying to process the id (like node resolution),
* and core features like sourcemaps can use this info to differentiate between
* virtual modules and regular files.
* `\0` is not a permitted char in import URLs so we have to replace them during
* import analysis. The id will be decoded back before entering the plugins pipeline.
* These encoded virtual ids are also prefixed by the VALID_ID_PREFIX, so virtual
* modules in the browser end up encoded as `/@id/__x00__{id}`
*/
export const NULL_BYTE_PLACEHOLDER = `__x00__`

View File

@ -1,4 +1,5 @@
import type { Arrayable, Nullable } from './types'
import { NULL_BYTE_PLACEHOLDER, VALID_ID_PREFIX } from './constants'
interface CloneOptions {
forceWritable?: boolean
@ -56,6 +57,47 @@ export function slash(path: string): string {
return path.replace(/\\/g, '/')
}
const postfixRE = /[?#].*$/
export function cleanUrl(url: string): string {
return url.replace(postfixRE, '')
}
const externalRE = /^(?:[a-z]+:)?\/\//
export const isExternalUrl = (url: string): boolean => externalRE.test(url)
/**
* Prepend `/@id/` and replace null byte so the id is URL-safe.
* This is prepended to resolved ids that are not valid browser
* import specifiers by the importAnalysis plugin.
*/
export function wrapId(id: string): string {
return id.startsWith(VALID_ID_PREFIX)
? id
: VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
}
/**
* Undo {@link wrapId}'s `/@id/` and null byte replacements.
*/
export function unwrapId(id: string): string {
return id.startsWith(VALID_ID_PREFIX)
? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
: id
}
export function withTrailingSlash(path: string): string {
if (path[path.length - 1] !== '/') {
return `${path}/`
}
return path
}
const bareImportRE = /^(?![a-z]:)[\w@](?!.*:\/\/)/i
export function isBareImport(id: string): boolean {
return bareImportRE.test(id)
}
// convert RegExp.toString to RegExp
export function parseRegexp(input: string): RegExp {
// Parse input

View File

@ -1,3 +1,10 @@
export {
CSS_LANGS_RE,
KNOWN_ASSET_RE,
KNOWN_ASSET_TYPES,
NULL_BYTE_PLACEHOLDER,
VALID_ID_PREFIX,
} from './constants'
export {
format,
inspect,
@ -8,6 +15,7 @@ export type { LoupeOptions, StringifyOptions } from './display'
export {
assertTypes,
cleanUrl,
clone,
createDefer,
createSimpleStackTrace,
@ -16,6 +24,8 @@ export {
getCallLastIndex,
getOwnProperties,
getType,
isBareImport,
isExternalUrl,
isNegativeNaN,
isObject,
isPrimitive,
@ -25,6 +35,9 @@ export {
parseRegexp,
slash,
toArray,
unwrapId,
withTrailingSlash,
wrapId,
} from './helpers'
export type { DeferPromise } from './helpers'

View File

@ -0,0 +1,99 @@
import fs from 'node:fs'
import { dirname, join } from 'pathe'
const packageCache = new Map<string, { type?: 'module' | 'commonjs' }>()
export function findNearestPackageData(
basedir: string,
): { type?: 'module' | 'commonjs' } {
const originalBasedir = basedir
while (basedir) {
const cached = getCachedData(packageCache, basedir, originalBasedir)
if (cached) {
return cached
}
const pkgPath = join(basedir, 'package.json')
if (tryStatSync(pkgPath)?.isFile()) {
const pkgData = JSON.parse(stripBomTag(fs.readFileSync(pkgPath, 'utf8')))
if (packageCache) {
setCacheData(packageCache, pkgData, basedir, originalBasedir)
}
return pkgData
}
const nextBasedir = dirname(basedir)
if (nextBasedir === basedir) {
break
}
basedir = nextBasedir
}
return {}
}
function stripBomTag(content: string): string {
if (content.charCodeAt(0) === 0xFEFF) {
return content.slice(1)
}
return content
}
function tryStatSync(file: string): fs.Stats | undefined {
try {
// The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
return fs.statSync(file, { throwIfNoEntry: false })
}
catch {
// Ignore errors
}
}
export function getCachedData<T>(
cache: Map<string, T>,
basedir: string,
originalBasedir: string,
): NonNullable<T> | undefined {
const pkgData = cache.get(getFnpdCacheKey(basedir))
if (pkgData) {
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
cache.set(getFnpdCacheKey(dir), pkgData)
})
return pkgData
}
}
export function setCacheData<T>(
cache: Map<string, T>,
data: T,
basedir: string,
originalBasedir: string,
): void {
cache.set(getFnpdCacheKey(basedir), data)
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
cache.set(getFnpdCacheKey(dir), data)
})
}
function getFnpdCacheKey(basedir: string) {
return `fnpd_${basedir}`
}
/**
* Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir.
* @param longerDir Longer dir path, e.g. `/User/foo/bar/baz`
* @param shorterDir Shorter dir path, e.g. `/User/foo`
*/
function traverseBetweenDirs(
longerDir: string,
shorterDir: string,
cb: (dir: string) => void,
) {
while (longerDir !== shorterDir) {
cb(longerDir)
longerDir = dirname(longerDir)
}
}

View File

@ -34,6 +34,8 @@ const stackIgnorePatterns = [
'/node_modules/chai/',
'/node_modules/tinypool/',
'/node_modules/tinyspy/',
'/vite/dist/node/module-runner',
'/rolldown-vite/dist/node/module-runner',
// browser related deps
'/deps/chunk-',
'/deps/@vitest',

View File

@ -22,6 +22,12 @@
"jest"
],
"sideEffects": false,
"imports": {
"#module-evaluator": {
"types": "./dist/module-evaluator.d.ts",
"default": "./dist/module-evaluator.js"
}
},
"exports": {
".": {
"import": {
@ -50,10 +56,6 @@
"types": "./dist/node.d.ts",
"default": "./dist/node.js"
},
"./execute": {
"types": "./dist/execute.d.ts",
"default": "./dist/execute.js"
},
"./workers": {
"types": "./dist/workers.d.ts",
"import": "./dist/workers.js"
@ -62,6 +64,10 @@
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
},
"./internal/module-runner": {
"types": "./dist/module-runner.d.ts",
"default": "./dist/module-runner.js"
},
"./runners": {
"types": "./dist/runners.d.ts",
"default": "./dist/runners.js"
@ -160,6 +166,7 @@
"@vitest/utils": "workspace:*",
"chai": "catalog:",
"debug": "catalog:",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "catalog:",
"pathe": "catalog:",
@ -171,7 +178,6 @@
"tinypool": "^1.1.1",
"tinyrainbow": "catalog:",
"vite": "^6.0.0 || ^7.0.0-0",
"vite-node": "workspace:*",
"why-is-node-running": "^2.3.0"
},
"devDependencies": {

View File

@ -28,10 +28,11 @@ const entries = {
'mocker': 'src/public/mocker.ts',
'spy': 'src/integrations/spy.ts',
'coverage': 'src/public/coverage.ts',
'execute': 'src/public/execute.ts',
'reporters': 'src/public/reporters.ts',
// TODO: advanced docs
'workers': 'src/public/workers.ts',
'module-runner': 'src/public/module-runner.ts',
'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts',
// for performance reasons we bundle them separately so we don't import everything at once
'worker': 'src/runtime/worker.ts',
@ -46,19 +47,19 @@ const entries = {
}
const dtsEntries = {
index: 'src/public/index.ts',
node: 'src/public/node.ts',
environments: 'src/public/environments.ts',
browser: 'src/public/browser.ts',
runners: 'src/public/runners.ts',
suite: 'src/public/suite.ts',
config: 'src/public/config.ts',
coverage: 'src/public/coverage.ts',
execute: 'src/public/execute.ts',
reporters: 'src/public/reporters.ts',
mocker: 'src/public/mocker.ts',
workers: 'src/public/workers.ts',
snapshot: 'src/public/snapshot.ts',
'index': 'src/public/index.ts',
'node': 'src/public/node.ts',
'environments': 'src/public/environments.ts',
'browser': 'src/public/browser.ts',
'runners': 'src/public/runners.ts',
'suite': 'src/public/suite.ts',
'config': 'src/public/config.ts',
'coverage': 'src/public/coverage.ts',
'reporters': 'src/public/reporters.ts',
'mocker': 'src/public/mocker.ts',
'workers': 'src/public/workers.ts',
'snapshot': 'src/public/snapshot.ts',
'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts',
}
const external = [
@ -75,11 +76,7 @@ const external = [
'node:console',
'inspector',
'vitest/optional-types.js',
'vite-node/source-map',
'vite-node/client',
'vite-node/server',
'vite-node/constants',
'vite-node/utils',
'vite/module-runner',
'@vitest/mocker',
'@vitest/mocker/node',
'@vitest/utils/diff',
@ -90,6 +87,8 @@ const external = [
'@vitest/runner/types',
'@vitest/snapshot/environment',
'@vitest/snapshot/manager',
'#module-evaluator',
]
const dir = dirname(fileURLToPath(import.meta.url))

View File

@ -94,7 +94,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
const project = ctx.getProjectByName(projectName)
const result: TransformResultWithSource | null | undefined = browser
? await project.browser!.vite.transformRequest(id)
: await project.vitenode.transformRequest(id)
: await project.vite.transformRequest(id)
if (result) {
try {
result.source = result.source || (await fs.readFile(id, 'utf-8'))

View File

@ -5,13 +5,6 @@ export const defaultInspectPort = 9229
export const API_PATH = '/__vitest_api__'
export const extraInlineDeps: RegExp[] = [
/^(?!.*node_modules).*\.mjs$/,
/^(?!.*node_modules).*\.cjs\.js$/,
// Vite client
/vite\w*\/dist\/client\/env.mjs/,
]
export const CONFIG_NAMES: string[] = ['vitest.config', 'vite.config']
export const CONFIG_EXTENSIONS: string[] = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']

View File

@ -4,7 +4,7 @@ import { populateGlobal } from './utils'
export default <Environment>{
name: 'edge-runtime',
transformMode: 'ssr',
viteEnvironment: 'ssr',
async setupVM() {
const { EdgeVM } = await import('@edge-runtime/vm')
const vm = new EdgeVM({

View File

@ -16,7 +16,7 @@ async function teardownWindow(win: {
export default <Environment>{
name: 'happy-dom',
transformMode: 'web',
viteEnvironment: 'client',
async setupVM({ happyDOM = {} }) {
const { Window } = await import('happy-dom')
let win = new Window({

View File

@ -35,7 +35,7 @@ function catchWindowErrors(window: Window) {
export default <Environment>{
name: 'jsdom',
transformMode: 'web',
viteEnvironment: 'client',
async setupVM({ jsdom = {} }) {
const { CookieJar, JSDOM, ResourceLoader, VirtualConsole } = await import(
'jsdom'

View File

@ -1,10 +1,12 @@
import type { ViteNodeRunnerOptions } from 'vite-node'
import type { BuiltinEnvironment, VitestEnvironment } from '../../node/types/config'
import type { Environment } from '../../types/environment'
import type { ContextRPC, WorkerRPC } from '../../types/worker'
import { readFileSync } from 'node:fs'
import { normalize, resolve } from 'pathe'
import { ViteNodeRunner } from 'vite-node/client'
import { isBuiltin } from 'node:module'
import { pathToFileURL } from 'node:url'
import { resolve } from 'pathe'
import { ModuleRunner } from 'vite/module-runner'
import { VitestTransport } from '../../runtime/moduleRunner/moduleTransport'
import { environments } from './index'
function isBuiltinEnvironment(
@ -13,43 +15,60 @@ function isBuiltinEnvironment(
return env in environments
}
const _loaders = new Map<string, ViteNodeRunner>()
const isWindows = process.platform === 'win32'
const _loaders = new Map<string, ModuleRunner>()
export async function createEnvironmentLoader(options: ViteNodeRunnerOptions): Promise<ViteNodeRunner> {
if (!_loaders.has(options.root)) {
const loader = new ViteNodeRunner(options)
await loader.executeId('/@vite/env')
_loaders.set(options.root, loader)
export async function createEnvironmentLoader(root: string, rpc: WorkerRPC): Promise<ModuleRunner> {
const cachedLoader = _loaders.get(root)
if (!cachedLoader || cachedLoader.isClosed()) {
_loaders.delete(root)
const moduleRunner = new ModuleRunner({
hmr: false,
sourcemapInterceptor: 'prepareStackTrace',
transport: new VitestTransport({
async fetchModule(id, importer, options) {
const result = await rpc.fetch(id, importer, '__vitest__', options)
if ('cached' in result) {
const code = readFileSync(result.tmp, 'utf-8')
return { code, ...result }
}
if (isWindows && 'externalize' in result) {
// TODO: vitest returns paths for external modules, but Vite returns file://
// https://github.com/vitejs/vite/pull/20449
result.externalize = isBuiltin(id) || /^(?:node:|data:|http:|https:|file:)/.test(id)
? result.externalize
: pathToFileURL(result.externalize).toString()
}
return result
},
async resolveId(id, importer) {
return rpc.resolve(id, importer, '__vitest__')
},
}),
})
_loaders.set(root, moduleRunner)
await moduleRunner.import('/@vite/env')
}
return _loaders.get(options.root)!
return _loaders.get(root)!
}
export async function loadEnvironment(
ctx: ContextRPC,
rpc: WorkerRPC,
): Promise<Environment> {
): Promise<{ environment: Environment; loader?: ModuleRunner }> {
const name = ctx.environment.name
if (isBuiltinEnvironment(name)) {
return environments[name]
return { environment: environments[name] }
}
const loader = await createEnvironmentLoader({
root: ctx.config.root,
fetchModule: async (id) => {
const result = await rpc.fetch(id, 'ssr')
if (result.id) {
return { code: readFileSync(result.id, 'utf-8') }
}
return result
},
resolveId: (id, importer) => rpc.resolveId(id, importer, 'ssr'),
})
const root = loader.root
const root = ctx.config.root
const loader = await createEnvironmentLoader(root, rpc)
const packageId
= name[0] === '.' || name[0] === '/'
? resolve(root, name)
: (await rpc.resolveId(`vitest-environment-${name}`, undefined, 'ssr'))
: (await rpc.resolve(`vitest-environment-${name}`, undefined, '__vitest__'))
?.id ?? resolve(root, name)
const pkg = await loader.executeId(normalize(packageId))
const pkg = await loader.import(packageId) as { default: Environment }
if (!pkg || !pkg.default || typeof pkg.default !== 'object') {
throw new TypeError(
`Environment "${name}" is not a valid environment. `
@ -58,13 +77,24 @@ export async function loadEnvironment(
}
const environment = pkg.default
if (
environment.transformMode !== 'web'
environment.transformMode != null
&& environment.transformMode !== 'web'
&& environment.transformMode !== 'ssr'
) {
throw new TypeError(
`Environment "${name}" is not a valid environment. `
+ `Path "${packageId}" should export default object with a "transformMode" method equal to "ssr" or "web".`,
+ `Path "${packageId}" should export default object with a "transformMode" method equal to "ssr" or "web", received "${environment.transformMode}".`,
)
}
return environment
if (environment.transformMode) {
console.warn(`The Vitest environment ${environment.name} defines the "transformMode". This options was deprecated in Vitest 4 and will be removed in the next major version. Please, use "viteEnvironment" instead.`)
// keep for backwards compat
environment.viteEnvironment ??= environment.transformMode === 'ssr'
? 'ssr'
: 'client'
}
return {
environment,
loader,
}
}

View File

@ -32,7 +32,7 @@ const nodeGlobals = new Map(
export default <Environment>{
name: 'node',
transformMode: 'ssr',
viteEnvironment: 'ssr',
// this is largely copied from jest's node environment
async setupVM() {
const vm = await import('node:vm')

View File

@ -1,17 +1,17 @@
import type { SnapshotEnvironment } from '@vitest/snapshot/environment'
import type { SerializedConfig } from '../../../runtime/config'
import type { VitestExecutor } from '../../../runtime/execute'
import type { VitestModuleRunner } from '../../../runtime/moduleRunner/moduleRunner'
export async function resolveSnapshotEnvironment(
config: SerializedConfig,
executor: VitestExecutor,
executor: VitestModuleRunner,
): Promise<SnapshotEnvironment> {
if (!config.snapshotEnvironment) {
const { VitestNodeSnapshotEnvironment } = await import('./node')
return new VitestNodeSnapshotEnvironment()
}
const mod = await executor.executeId(config.snapshotEnvironment)
const mod = await executor.import(config.snapshotEnvironment)
if (typeof mod.default !== 'object' || !mod.default) {
throw new Error(
'Snapshot environment module must have a default export object with a shape of `SnapshotEnvironment`',

View File

@ -7,7 +7,7 @@ import type {
MockInstance,
} from '@vitest/spy'
import type { RuntimeOptions, SerializedConfig } from '../runtime/config'
import type { VitestMocker } from '../runtime/mocker'
import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker'
import type { MockFactoryWithHelper, MockOptions } from '../types/mocker'
import { fn, isMockFunction, mocks, spyOn } from '@vitest/spy'
import { assertTypes, createSimpleStackTrace } from '@vitest/utils'
@ -687,17 +687,19 @@ function createVitest(): VitestUtils {
},
stubEnv(name: string, value: string | boolean | undefined) {
const state = getWorkerState()
const env = state.metaEnv
if (!_stubsEnv.has(name)) {
_stubsEnv.set(name, process.env[name])
_stubsEnv.set(name, env[name])
}
if (_envBooleans.includes(name)) {
process.env[name] = value ? '1' : ''
env[name] = value ? '1' : ''
}
else if (value === undefined) {
delete process.env[name]
delete env[name]
}
else {
process.env[name] = String(value)
env[name] = String(value)
}
return utils
},
@ -716,12 +718,14 @@ function createVitest(): VitestUtils {
},
unstubAllEnvs() {
const state = getWorkerState()
const env = state.metaEnv
_stubsEnv.forEach((original, name) => {
if (original === undefined) {
delete process.env[name]
delete env[name]
}
else {
process.env[name] = original
env[name] = original
}
})
_stubsEnv.clear()
@ -729,7 +733,7 @@ function createVitest(): VitestUtils {
},
resetModules() {
resetModules(workerState.moduleCache as any)
resetModules(workerState.evaluatedModules)
return utils
},

View File

@ -856,7 +856,6 @@ export const cliOptionsConfig: VitestCLIOptions = {
uiBase: null,
benchmark: null,
include: null,
testTransformMode: null,
fakeTimers: null,
chaiConfig: null,
clearMocks: null,

View File

@ -20,7 +20,6 @@ import {
defaultBrowserPort,
defaultInspectPort,
defaultPort,
extraInlineDeps,
} from '../../constants'
import { benchmarkConfigDefaults, configDefaults } from '../../defaults'
import { isCI, stdProvider } from '../../utils/env'
@ -337,6 +336,15 @@ export function resolveConfig(
resolved.deps ??= {}
resolved.deps.moduleDirectories ??= []
const envModuleDirectories
= process.env.VITEST_MODULE_DIRECTORIES
|| process.env.npm_config_VITEST_MODULE_DIRECTORIES
if (envModuleDirectories) {
resolved.deps.moduleDirectories.push(...envModuleDirectories.split(','))
}
resolved.deps.moduleDirectories = resolved.deps.moduleDirectories.map(
(dir) => {
if (!dir.startsWith('/')) {
@ -354,9 +362,9 @@ export function resolveConfig(
resolved.deps.optimizer ??= {}
resolved.deps.optimizer.ssr ??= {}
resolved.deps.optimizer.ssr.enabled ??= true
resolved.deps.optimizer.web ??= {}
resolved.deps.optimizer.web.enabled ??= true
resolved.deps.optimizer.ssr.enabled ??= false
resolved.deps.optimizer.client ??= {}
resolved.deps.optimizer.client.enabled ??= false
resolved.deps.web ??= {}
resolved.deps.web.transformAssets ??= true
@ -403,72 +411,10 @@ export function resolveConfig(
...resolved.setupFiles,
]
resolved.server ??= {}
resolved.server.deps ??= {}
const deprecatedDepsOptions = ['inline', 'external', 'fallbackCJS'] as const
deprecatedDepsOptions.forEach((option) => {
if (resolved.deps[option] === undefined) {
return
}
if (option === 'fallbackCJS') {
logger.console.warn(
c.yellow(
`${c.inverse(
c.yellow(' Vitest '),
)} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`,
),
)
}
else {
const transformMode
= resolved.environment === 'happy-dom' || resolved.environment === 'jsdom'
? 'web'
: 'ssr'
logger.console.warn(
c.yellow(
`${c.inverse(
c.yellow(' Vitest '),
)} "deps.${option}" is deprecated. If you rely on vite-node directly, use "server.deps.${option}" instead. Otherwise, consider using "deps.optimizer.${transformMode}.${
option === 'external' ? 'exclude' : 'include'
}"`,
),
)
}
if (resolved.server.deps![option] === undefined) {
resolved.server.deps![option] = resolved.deps[option] as any
}
})
if (resolved.cliExclude) {
resolved.exclude.push(...resolved.cliExclude)
}
// vitenode will try to import such file with native node,
// but then our mocker will not work properly
if (resolved.server.deps.inline !== true) {
const ssrOptions = viteConfig.ssr
if (
ssrOptions?.noExternal === true
&& resolved.server.deps.inline == null
) {
resolved.server.deps.inline = true
}
else {
resolved.server.deps.inline ??= []
resolved.server.deps.inline.push(...extraInlineDeps)
}
}
resolved.server.deps.inlineFiles ??= []
resolved.server.deps.inlineFiles.push(...resolved.setupFiles)
resolved.server.deps.moduleDirectories ??= []
resolved.server.deps.moduleDirectories.push(
...resolved.deps.moduleDirectories,
)
if (resolved.runner) {
resolved.runner = resolvePath(resolved.runner, resolved.root)
}
@ -858,7 +804,8 @@ export function resolveConfig(
resolved.includeTaskLocation ??= true
}
resolved.testTransformMode ??= {}
resolved.server ??= {}
resolved.server.deps ??= {}
resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000
resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000

View File

@ -6,7 +6,7 @@ export function serializeConfig(
coreConfig: ResolvedConfig,
viteConfig: ViteConfig | undefined,
): SerializedConfig {
const optimizer = config.deps?.optimizer
const optimizer = config.deps?.optimizer || {}
const poolOptions = config.poolOptions
// Resolve from server.config to avoid comparing against default value
@ -103,14 +103,10 @@ export function serializeConfig(
},
deps: {
web: config.deps.web || {},
optimizer: {
web: {
enabled: optimizer?.web?.enabled ?? true,
},
ssr: {
enabled: optimizer?.ssr?.enabled ?? true,
},
},
optimizer: Object.entries(optimizer).reduce((acc, [name, option]) => {
acc[name] = { enabled: option?.enabled ?? false }
return acc
}, {} as Record<string, { enabled: boolean }>),
interopDefault: config.deps.interopDefault,
moduleDirectories: config.deps.moduleDirectories,
},

View File

@ -2,6 +2,7 @@ import type { CancelReason, File } from '@vitest/runner'
import type { Awaitable } from '@vitest/utils'
import type { Writable } from 'node:stream'
import type { ViteDevServer } from 'vite'
import type { ModuleRunner } from 'vite/module-runner'
import type { SerializedCoverageConfig } from '../runtime/config'
import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general'
import type { CliOptions } from './cli/cli-api'
@ -15,8 +16,6 @@ import { getTasks, hasFailed } from '@vitest/runner/utils'
import { SnapshotManager } from '@vitest/snapshot/manager'
import { noop, toArray } from '@vitest/utils'
import { normalize, relative } from 'pathe'
import { ViteNodeRunner } from 'vite-node/client'
import { ViteNodeServer } from 'vite-node/server'
import { version } from '../../package.json' with { type: 'json' }
import { WebSocketReporter } from '../api/setup'
import { distDir } from '../paths'
@ -26,6 +25,7 @@ import { BrowserSessions } from './browser/sessions'
import { VitestCache } from './cache'
import { resolveConfig } from './config/resolveConfig'
import { getCoverageProvider } from './coverage'
import { ServerModuleRunner } from './environments/serverRunner'
import { FilesNotFoundError } from './errors'
import { Logger } from './logger'
import { VitestPackageInstaller } from './packageInstaller'
@ -35,6 +35,7 @@ import { getDefaultTestProject, resolveBrowserProjects, resolveProjects } from '
import { BlobReporter, readBlobs } from './reporters/blob'
import { HangingProcessReporter } from './reporters/hanging-process'
import { createBenchmarkReporters, createReporters } from './reporters/utils'
import { VitestResolver } from './resolver'
import { VitestSpecifications } from './specifications'
import { StateManager } from './state'
import { TestRun } from './test-run'
@ -91,9 +92,9 @@ export class Vitest {
/** @internal */ _browserSessions = new BrowserSessions()
/** @internal */ _cliOptions: CliOptions = {}
/** @internal */ reporters: Reporter[] = []
/** @internal */ vitenode: ViteNodeServer = undefined!
/** @internal */ runner: ViteNodeRunner = undefined!
/** @internal */ runner!: ModuleRunner
/** @internal */ _testRun: TestRun = undefined!
/** @internal */ _resolver!: VitestResolver
private isFirstRun = true
private restartsCount = 0
@ -220,19 +221,13 @@ export class Vitest {
this.watcher.registerWatcher()
}
this.vitenode = new ViteNodeServer(server, this.config.server)
const node = this.vitenode
this.runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id: string) {
return node.fetchModule(id)
},
resolveId(id: string, importer?: string) {
return node.resolveId(id, importer)
},
})
this._resolver = new VitestResolver(server.config.cacheDir, resolved)
const environment = server.environments.__vitest__
this.runner = new ServerModuleRunner(
environment,
this._resolver,
resolved,
)
if (this.config.watch) {
// hijack server restart
@ -387,7 +382,7 @@ export class Vitest {
* @param moduleId The ID of the module in Vite module graph
*/
public import<T>(moduleId: string): Promise<T> {
return this.runner.executeId(moduleId)
return this.runner.import(moduleId)
}
private async resolveProjects(cliOptions: UserConfig): Promise<TestProject[]> {

View File

@ -6,11 +6,11 @@ import type { SerializedCoverageConfig } from '../runtime/config'
import type { AfterSuiteRunMeta } from '../types/general'
import { existsSync, promises as fs, readdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { cleanUrl, slash } from '@vitest/utils'
import { relative, resolve } from 'pathe'
import pm from 'picomatch'
import { glob } from 'tinyglobby'
import c from 'tinyrainbow'
import { cleanUrl, slash } from 'vite-node/utils'
import { coverageConfigDefaults } from '../defaults'
import { resolveCoverageReporters } from '../node/config/resolveConfig'
import { resolveCoverageProviderModule } from '../utils/coverage'
@ -42,7 +42,7 @@ interface ResolvedThreshold {
type CoverageFiles = Map<
NonNullable<AfterSuiteRunMeta['projectName']> | symbol,
Record<
AfterSuiteRunMeta['transformMode'],
AfterSuiteRunMeta['environment'],
{ [TestFilenames: string]: string }
>
>
@ -253,19 +253,15 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
this.pendingPromises = []
}
onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }: AfterSuiteRunMeta): void {
onAfterSuiteRun({ coverage, environment, projectName, testFiles }: AfterSuiteRunMeta): void {
if (!coverage) {
return
}
if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') {
throw new Error(`Invalid transform mode: ${transformMode}`)
}
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)
if (!entry) {
entry = { web: {}, ssr: {}, browser: {} }
entry = {}
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
}
@ -275,8 +271,9 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
`coverage-${uniqueId++}.json`,
)
entry[environment] ??= {}
// If there's a result from previous run, overwrite it
entry[transformMode][testFilenames] = filename
entry[environment][testFilenames] = filename
const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8')
this.pendingPromises.push(promise)
@ -286,7 +283,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
/** Callback invoked with a single coverage result */
onFileRead: (data: CoverageType) => void
/** Callback invoked once all results of a project for specific transform mode are read */
onFinished: (project: Vitest['projects'][number], transformMode: AfterSuiteRunMeta['transformMode']) => Promise<void>
onFinished: (project: Vitest['projects'][number], environment: string) => Promise<void>
onDebug: ((...logs: any[]) => void) & { enabled: boolean }
}): Promise<void> {
let index = 0
@ -296,7 +293,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
this.pendingPromises = []
for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) {
for (const [transformMode, coverageByTestfiles] of Object.entries(coveragePerProject) as Entries<typeof coveragePerProject>) {
for (const [environment, coverageByTestfiles] of Object.entries(coveragePerProject) as Entries<typeof coveragePerProject>) {
const filenames = Object.values(coverageByTestfiles)
const project = this.ctx.getProjectByName(projectName as string)
@ -315,7 +312,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
)
}
await onFinished(project, transformMode)
await onFinished(project, environment)
}
}
}
@ -640,23 +637,23 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
...ctx.projects.map(project => ({
root: project.config.root,
isBrowserEnabled: project.isBrowserEnabled(),
vitenode: project.vitenode,
vite: project.vite,
})),
// Check core last as it will match all files anyway
{ root: ctx.config.root, vitenode: ctx.vitenode, isBrowserEnabled: ctx.getRootProject().isBrowserEnabled() },
{ root: ctx.config.root, vite: ctx.vite, isBrowserEnabled: ctx.getRootProject().isBrowserEnabled() },
]
return async function transformFile(filename: string): Promise<TransformResult | null | undefined> {
let lastError
for (const { root, vitenode, isBrowserEnabled } of servers) {
for (const { root, vite, isBrowserEnabled } of servers) {
// On Windows root doesn't start with "/" while filenames do
if (!filename.startsWith(root) && !filename.startsWith(`/${root}`)) {
continue
}
if (isBrowserEnabled) {
const result = await vitenode.transformRequest(filename, undefined, 'web').catch(() => null)
const result = await vite.environments.client.transformRequest(filename).catch(() => null)
if (result) {
return result
@ -664,7 +661,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
}
try {
return await vitenode.transformRequest(filename)
return await vite.environments.ssr.transformRequest(filename)
}
catch (error) {
lastError = error

View File

@ -0,0 +1,258 @@
import type { DevEnvironment, FetchResult, Rollup, TransformResult } from 'vite'
import type { FetchFunctionOptions } from 'vite/module-runner'
import type { FetchCachedFileSystemResult } from '../../types/general'
import type { VitestResolver } from '../resolver'
import { mkdirSync } from 'node:fs'
import { rename, stat, unlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { isExternalUrl, nanoid, unwrapId } from '@vitest/utils'
import { dirname, join } from 'pathe'
import { fetchModule } from 'vite'
import { hash } from '../hash'
const created = new Set()
const promises = new Map<string, Promise<void>>()
export function createFetchModuleFunction(
resolver: VitestResolver,
cacheFs: boolean = false,
tmpDir: string = join(tmpdir(), nanoid()),
): (
url: string,
importer: string | undefined,
environment: DevEnvironment,
options?: FetchFunctionOptions
) => Promise<FetchResult | FetchCachedFileSystemResult> {
const cachedFsResults = new Map<string, string>()
return async (
url,
importer,
environment,
options,
) => {
// We are copy pasting Vite's externalization logic from `fetchModule` because
// we instead rely on our own `shouldExternalize` method because Vite
// doesn't support `resolve.external` in non SSR environments (jsdom/happy-dom)
if (url.startsWith('data:')) {
return { externalize: url, type: 'builtin' }
}
if (url === '/@vite/client' || url === '@vite/client') {
// this will be stubbed
return { externalize: '/@vite/client', type: 'module' }
}
const isFileUrl = url.startsWith('file://')
if (isExternalUrl(url) && !isFileUrl) {
return { externalize: url, type: 'network' }
}
// Vite does the same in `fetchModule`, but we want to externalize modules ourselves,
// so we do this first to resolve the module and check its `id`. The next call of
// `ensureEntryFromUrl` inside `fetchModule` is cached and should take no time
// This also makes it so externalized modules are inside the module graph.
const moduleGraphModule = await environment.moduleGraph.ensureEntryFromUrl(unwrapId(url))
const cached = !!moduleGraphModule.transformResult
// if url is already cached, we can just confirm it's also cached on the server
if (options?.cached && cached) {
return { cache: true }
}
if (moduleGraphModule.id) {
const externalize = await resolver.shouldExternalize(moduleGraphModule.id)
if (externalize) {
return { externalize, type: 'module' }
}
}
const moduleRunnerModule = await fetchModule(
environment,
url,
importer,
{
...options,
inlineSourceMap: false,
},
).catch(handleRollupError)
const result = processResultSource(environment, moduleRunnerModule)
if (!cacheFs || !('code' in result)) {
return result
}
const code = result.code
// to avoid serialising large chunks of code,
// we store them in a tmp file and read in the test thread
if (cachedFsResults.has(result.id)) {
return getCachedResult(result, cachedFsResults)
}
const dir = join(tmpDir, environment.name)
const name = hash('sha1', result.id, 'hex')
const tmp = join(dir, name)
if (!created.has(dir)) {
mkdirSync(dir, { recursive: true })
created.add(dir)
}
if (promises.has(tmp)) {
await promises.get(tmp)
cachedFsResults.set(result.id, tmp)
return getCachedResult(result, cachedFsResults)
}
promises.set(
tmp,
atomicWriteFile(tmp, code)
// Fallback to non-atomic write for windows case where file already exists:
.catch(() => writeFile(tmp, code, 'utf-8'))
.finally(() => promises.delete(tmp)),
)
await promises.get(tmp)
cachedFsResults.set(result.id, tmp)
return getCachedResult(result, cachedFsResults)
}
}
let SOURCEMAPPING_URL = 'sourceMa'
SOURCEMAPPING_URL += 'ppingURL'
const MODULE_RUNNER_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-generated'
function processResultSource(environment: DevEnvironment, result: FetchResult): FetchResult {
if (!('code' in result)) {
return result
}
const node = environment.moduleGraph.getModuleById(result.id)
if (node?.transformResult) {
// this also overrides node.transformResult.code which is also what the module
// runner does under the hood by default (we disable source maps inlining)
inlineSourceMap(node.transformResult)
}
return {
...result,
code: node?.transformResult?.code || result.code,
}
}
const OTHER_SOURCE_MAP_REGEXP = new RegExp(
`//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,([A-Za-z0-9+/=]+)$`,
'gm',
)
// we have to inline the source map ourselves, because
// - we don't need //# sourceURL since we are running code in VM
// - important in stack traces and the V8 coverage
// - we need to inject an empty line for --inspect-brk
function inlineSourceMap(result: TransformResult) {
const map = result.map
let code = result.code
if (
!map
|| !('version' in map)
|| code.includes(MODULE_RUNNER_SOURCEMAPPING_SOURCE)
) {
return result
}
// to reduce the payload size, we only inline vite node source map, because it's also the only one we use
OTHER_SOURCE_MAP_REGEXP.lastIndex = 0
if (OTHER_SOURCE_MAP_REGEXP.test(code)) {
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')
}
const sourceMap = { ...map }
// If the first line is not present on source maps, add simple 1:1 mapping ([0,0,0,0], [1,0,0,0])
// so that debuggers can be set to break on first line
if (sourceMap.mappings.startsWith(';')) {
sourceMap.mappings = `AAAA,CAAA${sourceMap.mappings}`
}
result.code = `${code.trimEnd()}\n${
MODULE_RUNNER_SOURCEMAPPING_SOURCE
}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n`
return result
}
function genSourceMapUrl(map: Rollup.SourceMap | string): string {
if (typeof map !== 'string') {
map = JSON.stringify(map)
}
return `data:application/json;base64,${Buffer.from(map).toString('base64')}`
}
function getCachedResult(result: Extract<FetchResult, { code: string }>, cachedFsResults: Map<string, string>): FetchCachedFileSystemResult {
const tmp = cachedFsResults.get(result.id)
if (!tmp) {
throw new Error(`The cached result was returned too early for ${result.id}.`)
}
return {
cached: true as const,
file: result.file,
id: result.id,
tmp,
url: result.url,
invalidate: result.invalidate,
}
}
// serialize rollup error on server to preserve details as a test error
export function handleRollupError(e: unknown): never {
if (
e instanceof Error
&& ('plugin' in e || 'frame' in e || 'id' in e)
) {
// eslint-disable-next-line no-throw-literal
throw {
name: e.name,
message: e.message,
stack: e.stack,
cause: e.cause,
__vitest_rollup_error__: {
plugin: (e as any).plugin,
id: (e as any).id,
loc: (e as any).loc,
frame: (e as any).frame,
},
}
}
throw e
}
/**
* Performs an atomic write operation using the write-then-rename pattern.
*
* Why we need this:
* - Ensures file integrity by never leaving partially written files on disk
* - Prevents other processes from reading incomplete data during writes
* - Particularly important for test files where incomplete writes could cause test failures
*
* The implementation writes to a temporary file first, then renames it to the target path.
* This rename operation is atomic on most filesystems (including POSIX-compliant ones),
* guaranteeing that other processes will only ever see the complete file.
*
* Added in https://github.com/vitest-dev/vitest/pull/7531
*/
async function atomicWriteFile(realFilePath: string, data: string): Promise<void> {
const dir = dirname(realFilePath)
const tmpFilePath = join(dir, `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`)
try {
await writeFile(tmpFilePath, data, 'utf-8')
await rename(tmpFilePath, realFilePath)
}
finally {
try {
if (await stat(tmpFilePath)) {
await unlink(tmpFilePath)
}
}
catch {}
}
}

View File

@ -0,0 +1,47 @@
import type { DevEnvironment } from 'vite'
import { existsSync } from 'node:fs'
import path from 'node:path'
import { cleanUrl, withTrailingSlash, wrapId } from '@vitest/utils'
// this is copy pasted from vite
export function normalizeResolvedIdToUrl(
environment: DevEnvironment,
resolvedId: string,
): string {
const root = environment.config.root
const depsOptimizer = environment.depsOptimizer
let url: string
// normalize all imports into resolved URLs
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
if (resolvedId.startsWith(withTrailingSlash(root))) {
// in root: infer short absolute path from root
url = resolvedId.slice(root.length)
}
else if (
depsOptimizer?.isOptimizedDepFile(resolvedId)
// vite-plugin-react isn't following the leading \0 virtual module convention.
// This is a temporary hack to avoid expensive fs checks for React apps.
// We'll remove this as soon we're able to fix the react plugins.
|| (resolvedId !== '/@react-refresh'
&& path.isAbsolute(resolvedId)
&& existsSync(cleanUrl(resolvedId)))
) {
// an optimized deps may not yet exists in the filesystem, or
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
url = path.posix.join('/@fs/', resolvedId)
}
else {
url = resolvedId
}
// if the resolved id is not a valid browser import specifier,
// prefix it to make it valid. We will strip this before feeding it
// back into the transform pipeline
if (url[0] !== '.' && url[0] !== '/') {
url = wrapId(resolvedId)
}
return url
}

View File

@ -0,0 +1,56 @@
import type { DevEnvironment } from 'vite'
import type { VitestResolver } from '../resolver'
import type { ResolvedConfig } from '../types/config'
import { VitestModuleEvaluator } from '#module-evaluator'
import { ModuleRunner } from 'vite/module-runner'
import { createFetchModuleFunction } from './fetchModule'
import { normalizeResolvedIdToUrl } from './normalizeUrl'
export class ServerModuleRunner extends ModuleRunner {
constructor(
private environment: DevEnvironment,
resolver: VitestResolver,
private config: ResolvedConfig,
) {
const fetchModule = createFetchModuleFunction(
resolver,
false,
)
super(
{
hmr: false,
sourcemapInterceptor: 'node',
transport: {
async invoke(event) {
if (event.type !== 'custom') {
throw new Error(`Vitest Module Runner doesn't support Vite HMR events.`)
}
const { data } = event.data
try {
const result = await fetchModule(data[0], data[1], environment, data[2])
return { result }
}
catch (error) {
return { error }
}
},
},
},
new VitestModuleEvaluator(),
)
}
async import(rawId: string): Promise<any> {
const resolved = await this.environment.pluginContainer.resolveId(
rawId,
this.config.root,
)
if (!resolved) {
return super.import(rawId)
}
// Vite will make "@vitest/coverage-v8" into "@vitest/coverage-v8.js" url
// instead of using an actual file path-like URL, so we resolve it here first
const url = normalizeResolvedIdToUrl(this.environment, resolved.id)
return super.import(url)
}
}

View File

@ -1,4 +1,4 @@
import type { ViteNodeRunner } from 'vite-node/client'
import type { ModuleRunner } from 'vite/module-runner'
import type { TestProject } from './project'
import { toArray } from '@vitest/utils'
@ -9,7 +9,7 @@ export interface GlobalSetupFile {
}
export async function loadGlobalSetupFiles(
runner: ViteNodeRunner,
runner: ModuleRunner,
globalSetup: string | string[],
): Promise<GlobalSetupFile[]> {
const globalSetupFiles = toArray(globalSetup)
@ -20,9 +20,9 @@ export async function loadGlobalSetupFiles(
async function loadGlobalSetupFile(
file: string,
runner: ViteNodeRunner,
runner: ModuleRunner,
): Promise<GlobalSetupFile> {
const m = await runner.executeFile(file)
const m = await runner.import(file)
for (const exp of ['default', 'setup', 'teardown']) {
if (m[exp] != null && typeof m[exp] !== 'function') {
throw new Error(

View File

@ -1,15 +1,13 @@
import type { Plugin as VitePlugin } from 'vite'
import type { Vitest } from '../core'
import { normalizeRequestId } from 'vite-node/utils'
export function CoverageTransform(ctx: Vitest): VitePlugin {
return {
name: 'vitest:coverage-transform',
transform(srcCode, id) {
return ctx.coverageProvider?.onFileTransform?.(
srcCode,
normalizeRequestId(id),
id,
this,
)
},

View File

@ -4,7 +4,6 @@ import {
deepClone,
deepMerge,
notNullish,
toArray,
} from '@vitest/utils'
import { relative } from 'pathe'
import { defaultPort } from '../../constants'
@ -15,10 +14,11 @@ import { Vitest } from '../core'
import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger'
import { CoverageTransform } from './coverageTransform'
import { CSSEnablerPlugin } from './cssEnabler'
import { MetaEnvReplacerPlugin } from './metaEnvReplacer'
import { MocksPlugins } from './mocks'
import { NormalizeURLPlugin } from './normalizeURL'
import { VitestOptimizer } from './optimizer'
import { SsrReplacerPlugin } from './ssrReplacer'
import { ModuleRunnerTransform } from './runnerTransform'
import {
deleteDefineConfig,
getDefaultResolveOptions,
@ -121,6 +121,9 @@ export async function VitestPlugin(
ssr: {
resolve: resolveOptions,
},
__vitest__: {
dev: {},
},
},
test: {
poolOptions: {
@ -164,29 +167,6 @@ export async function VitestPlugin(
)
config.customLogger = silenceImportViteIgnoreWarning(config.customLogger)
// we want inline dependencies to be resolved by analyser plugin so module graph is populated correctly
if (viteConfig.ssr?.noExternal !== true) {
const inline = testConfig.server?.deps?.inline
if (inline === true) {
config.ssr = { noExternal: true }
}
else {
const noExternal = viteConfig.ssr?.noExternal
const noExternalArray
= typeof noExternal !== 'undefined'
? toArray(noExternal)
: undefined
// filter the same packages
const uniqueInline
= inline && noExternalArray
? inline.filter(dep => !noExternalArray.includes(dep))
: inline
config.ssr = {
noExternal: uniqueInline,
}
}
}
// chokidar fsevents is unstable on macos when emitting "ready" event
if (
process.platform === 'darwin'
@ -298,13 +278,14 @@ export async function VitestPlugin(
},
},
},
SsrReplacerPlugin(),
MetaEnvReplacerPlugin(),
...CSSEnablerPlugin(vitest),
CoverageTransform(vitest),
VitestCoreResolver(vitest),
...MocksPlugins(),
VitestOptimizer(),
NormalizeURLPlugin(),
ModuleRunnerTransform(),
].filter(notNullish)
}
function removeUndefinedValues<T extends Record<string, any>>(

View File

@ -1,13 +1,13 @@
import type { Plugin } from 'vite'
import { cleanUrl } from '@vitest/utils'
import MagicString from 'magic-string'
import { stripLiteral } from 'strip-literal'
import { cleanUrl } from 'vite-node/utils'
// so people can reassign envs at runtime
// import.meta.env.VITE_NAME = 'app' -> process.env.VITE_NAME = 'app'
export function SsrReplacerPlugin(): Plugin {
export function MetaEnvReplacerPlugin(): Plugin {
return {
name: 'vitest:ssr-replacer',
name: 'vitest:meta-env-replacer',
enforce: 'pre',
transform(code, id) {
if (!/\bimport\.meta\.env\b/.test(code)) {
@ -24,7 +24,11 @@ export function SsrReplacerPlugin(): Plugin {
const startIndex = env.index!
const endIndex = startIndex + env[0].length
s.overwrite(startIndex, endIndex, '__vite_ssr_import_meta__.env')
s.overwrite(
startIndex,
endIndex,
`Object.assign(/* istanbul ignore next */ globalThis.__vitest_worker__?.metaEnv ?? import.meta.env)`,
)
}
if (s) {

View File

@ -12,10 +12,9 @@ export function NormalizeURLPlugin(): Plugin {
return {
name: 'vitest:normalize-url',
enforce: 'post',
transform(code, id, options) {
const ssr = options?.ssr === true
transform(code) {
if (
ssr
this.environment.name !== 'client'
|| !code.includes('new URL')
|| !code.includes('import.meta.url')
) {

View File

@ -1,7 +1,6 @@
import type { Plugin } from 'vite'
import { resolve } from 'pathe'
import { VitestCache } from '../cache'
import { resolveOptimizerConfig } from './utils'
export function VitestOptimizer(): Plugin {
return {
@ -10,14 +9,6 @@ export function VitestOptimizer(): Plugin {
order: 'post',
handler(viteConfig) {
const testConfig = viteConfig.test || {}
const webOptimizer = resolveOptimizerConfig(
testConfig.deps?.optimizer?.web,
viteConfig.optimizeDeps,
)
const ssrOptimizer = resolveOptimizerConfig(
testConfig.deps?.optimizer?.ssr,
viteConfig.ssr?.optimizeDeps,
)
const root = resolve(viteConfig.root || process.cwd())
const name = viteConfig.test?.name
@ -30,9 +21,6 @@ export function VitestOptimizer(): Plugin {
: viteConfig.cacheDir,
label,
)
viteConfig.optimizeDeps = webOptimizer.optimizeDeps
viteConfig.ssr ??= {}
viteConfig.ssr.optimizeDeps = ssrOptimizer.optimizeDeps
},
},
}

View File

@ -0,0 +1,163 @@
import type { ResolvedConfig, UserConfig, Plugin as VitePlugin } from 'vite'
import { builtinModules } from 'node:module'
import { mergeConfig } from 'vite'
import { resolveOptimizerConfig } from './utils'
export function ModuleRunnerTransform(): VitePlugin {
// make sure Vite always applies the module runner transform
return {
name: 'vitest:environments-module-runner',
config: {
order: 'post',
handler(config) {
const testConfig = config.test || {}
config.environments ??= {}
const names = new Set(Object.keys(config.environments))
names.add('client')
names.add('ssr')
const pool = config.test?.pool
if (pool === 'vmForks' || pool === 'vmThreads') {
names.add('__vitest_vm__')
}
const external: (string | RegExp)[] = []
const noExternal: (string | RegExp)[] = []
let noExternalAll: true | undefined
for (const name of names) {
config.environments[name] ??= {}
const environment = config.environments[name]
environment.dev ??= {}
// vm tests run using the native import mechanism
if (name === '__vitest_vm__') {
environment.dev.moduleRunnerTransform = false
environment.consumer = 'client'
}
else {
environment.dev.moduleRunnerTransform = true
}
environment.dev.preTransformRequests = false
environment.keepProcessEnv = true
const resolveExternal = name === 'client'
? config.resolve?.external
: []
const resolveNoExternal = name === 'client'
? config.resolve?.noExternal
: []
const topLevelResolveOptions: UserConfig['resolve'] = {}
if (resolveExternal != null) {
topLevelResolveOptions.external = resolveExternal
}
if (resolveNoExternal != null) {
topLevelResolveOptions.noExternal = resolveNoExternal
}
const currentResolveOptions = mergeConfig(
topLevelResolveOptions,
environment.resolve || {},
) as ResolvedConfig['resolve']
const envNoExternal = resolveViteResolveOptions('noExternal', currentResolveOptions)
if (envNoExternal === true) {
noExternalAll = true
}
else {
noExternal.push(...envNoExternal)
}
const envExternal = resolveViteResolveOptions('external', currentResolveOptions)
if (envExternal !== true) {
external.push(...envExternal)
}
// remove Vite's externalization logic because we have our own (unfortunetly)
environment.resolve ??= {}
environment.resolve.external = [
...builtinModules,
...builtinModules.map(m => `node:${m}`),
]
// by setting `noExternal` to `true`, we make sure that
// Vite will never use its own externalization mechanism
// to externalize modules and always resolve static imports
// in both SSR and Client environments
environment.resolve.noExternal = true
if (name === '__vitest_vm__' || name === '__vitest__') {
continue
}
const currentOptimizeDeps = environment.optimizeDeps || (
name === 'client'
? config.optimizeDeps
: name === 'ssr'
? config.ssr?.optimizeDeps
: undefined
)
const optimizeDeps = resolveOptimizerConfig(
testConfig.deps?.optimizer?.[name],
currentOptimizeDeps,
)
// Vite respects the root level optimize deps, so we override it instead
if (name === 'client') {
config.optimizeDeps = optimizeDeps
environment.optimizeDeps = undefined
}
else if (name === 'ssr') {
config.ssr ??= {}
config.ssr.optimizeDeps = optimizeDeps
environment.optimizeDeps = undefined
}
else {
environment.optimizeDeps = optimizeDeps
}
}
testConfig.server ??= {}
testConfig.server.deps ??= {}
if (testConfig.server.deps.inline !== true) {
if (noExternalAll) {
testConfig.server.deps.inline = true
}
else if (noExternal.length) {
testConfig.server.deps.inline ??= []
testConfig.server.deps.inline.push(...noExternal)
}
}
if (external.length) {
testConfig.server.deps.external ??= []
testConfig.server.deps.external.push(...external)
}
},
},
}
}
function resolveViteResolveOptions(
key: 'noExternal' | 'external',
options: ResolvedConfig['resolve'],
): true | (string | RegExp)[] {
if (Array.isArray(options[key])) {
return options[key]
}
else if (
typeof options[key] === 'string'
|| options[key] instanceof RegExp
) {
return [options[key]]
}
else if (typeof options[key] === 'boolean') {
return true
}
return []
}

View File

@ -11,27 +11,13 @@ import { rootDir } from '../../paths'
export function resolveOptimizerConfig(
_testOptions: DepsOptimizationOptions | undefined,
viteOptions: DepOptimizationOptions | undefined,
): { cacheDir?: string; optimizeDeps: DepOptimizationOptions } {
): DepOptimizationOptions {
const testOptions = _testOptions || {}
const newConfig: { cacheDir?: string; optimizeDeps: DepOptimizationOptions }
= {} as any
const [major, minor, fix] = viteVersion.split('.').map(Number)
const allowed
= major >= 5
|| (major === 4 && minor >= 4)
|| (major === 4 && minor === 3 && fix >= 2)
if (!allowed && testOptions?.enabled === true) {
console.warn(
`Vitest: "deps.optimizer" is only available in Vite >= 4.3.2, current Vite version: ${viteVersion}`,
)
}
// disabled by default
else {
let optimizeDeps: DepOptimizationOptions
if (testOptions.enabled !== true) {
testOptions.enabled ??= false
}
if (!allowed || testOptions?.enabled !== true) {
newConfig.cacheDir = undefined
newConfig.optimizeDeps = {
optimizeDeps = {
// experimental in Vite >2.9.2, entries remains to help with older versions
disabled: true,
entries: [],
@ -55,7 +41,7 @@ export function resolveOptimizerConfig(
(n: string) => !exclude.includes(n),
)
newConfig.optimizeDeps = {
optimizeDeps = {
...viteOptions,
...testOptions,
noDiscovery: true,
@ -68,15 +54,13 @@ export function resolveOptimizerConfig(
// `optimizeDeps.disabled` is deprecated since v5.1.0-beta.1
// https://github.com/vitejs/vite/pull/15184
if ((major >= 5 && minor >= 1) || major >= 6) {
if (newConfig.optimizeDeps.disabled) {
newConfig.optimizeDeps.noDiscovery = true
newConfig.optimizeDeps.include = []
}
delete newConfig.optimizeDeps.disabled
if (optimizeDeps.disabled) {
optimizeDeps.noDiscovery = true
optimizeDeps.include = []
}
delete optimizeDeps.disabled
return newConfig
return optimizeDeps
}
export function deleteDefineConfig(viteConfig: ViteConfig): Record<string, any> {

View File

@ -10,10 +10,11 @@ import { VitestFilteredOutProjectError } from '../errors'
import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger'
import { CoverageTransform } from './coverageTransform'
import { CSSEnablerPlugin } from './cssEnabler'
import { MetaEnvReplacerPlugin } from './metaEnvReplacer'
import { MocksPlugins } from './mocks'
import { NormalizeURLPlugin } from './normalizeURL'
import { VitestOptimizer } from './optimizer'
import { SsrReplacerPlugin } from './ssrReplacer'
import { ModuleRunnerTransform } from './runnerTransform'
import {
deleteDefineConfig,
getDefaultResolveOptions,
@ -92,6 +93,11 @@ export function WorkspaceVitestPlugin(
}
return {
environments: {
__vitest__: {
dev: {},
},
},
test: {
name: { label: name, color },
},
@ -202,12 +208,13 @@ export function WorkspaceVitestPlugin(
await server.watcher.close()
},
},
SsrReplacerPlugin(),
MetaEnvReplacerPlugin(),
...CSSEnablerPlugin(project),
CoverageTransform(project.vitest),
...MocksPlugins(),
VitestProjectResolver(project.vitest),
VitestOptimizer(),
NormalizeURLPlugin(),
ModuleRunnerTransform(),
]
}

View File

@ -144,7 +144,7 @@ export function createPool(ctx: Vitest): ProcessPool {
return customPools.get(filepath)!
}
const pool = await ctx.runner.executeId(filepath)
const pool = await ctx.runner.import(filepath)
if (typeof pool.default !== 'function') {
throw new TypeError(
`Custom pool "${filepath}" must export a function as default export`,

View File

@ -1,13 +1,10 @@
import type { RuntimeRPC } from '../../types/rpc'
import type { TestProject } from '../project'
import type { ResolveSnapshotPathHandlerContext } from '../types/config'
import { mkdirSync } from 'node:fs'
import { rename, stat, unlink, writeFile } from 'node:fs/promises'
import { dirname, join } from 'pathe'
import { hash } from '../hash'
const created = new Set()
const promises = new Map<string, Promise<void>>()
import { fileURLToPath } from 'node:url'
import { cleanUrl } from '@vitest/utils'
import { createFetchModuleFunction, handleRollupError } from '../environments/fetchModule'
import { normalizeResolvedIdToUrl } from '../environments/normalizeUrl'
interface MethodsOptions {
cacheFs?: boolean
@ -18,7 +15,44 @@ interface MethodsOptions {
export function createMethodsRPC(project: TestProject, options: MethodsOptions = {}): RuntimeRPC {
const ctx = project.vitest
const cacheFs = options.cacheFs ?? false
const fetch = createFetchModuleFunction(project._resolver, cacheFs, project.tmpDir)
return {
async fetch(
url,
importer,
environmentName,
options,
) {
const environment = project.vite.environments[environmentName]
if (!environment) {
throw new Error(`The environment ${environmentName} was not defined in the Vite config.`)
}
const start = performance.now()
try {
return await fetch(url, importer, environment, options)
}
finally {
project.vitest.state.transformTime += (performance.now() - start)
}
},
async resolve(id, importer, environmentName) {
const environment = project.vite.environments[environmentName]
if (!environment) {
throw new Error(`The environment ${environmentName} was not defined in the Vite config.`)
}
const resolved = await environment.pluginContainer.resolveId(id, importer)
if (!resolved) {
return null
}
return {
file: cleanUrl(resolved.id),
url: normalizeResolvedIdToUrl(environment, resolved.id),
id: resolved.id,
}
},
snapshotSaved(snapshot) {
ctx.snapshot.add(snapshot)
},
@ -27,48 +61,15 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
config: project.serializedConfig,
})
},
async fetch(id, transformMode) {
const result = await project.vitenode.fetchResult(id, transformMode).catch(handleRollupError)
const code = result.code
if (!cacheFs || result.externalize) {
return result
}
if ('id' in result && typeof result.id === 'string') {
return { id: result.id }
async transform(id) {
const environment = project.vite.environments.__vitest_vm__
if (!environment) {
throw new Error(`The VM environment was not defined in the Vite config. This is a bug in Vitest. Please, open a new issue with reproduction.`)
}
if (code == null) {
throw new Error(`Failed to fetch module ${id}`)
}
const dir = join(project.tmpDir, transformMode)
const name = hash('sha1', id, 'hex')
const tmp = join(dir, name)
if (!created.has(dir)) {
mkdirSync(dir, { recursive: true })
created.add(dir)
}
if (promises.has(tmp)) {
await promises.get(tmp)
return { id: tmp }
}
promises.set(
tmp,
atomicWriteFile(tmp, code)
// Fallback to non-atomic write for windows case where file already exists:
.catch(() => writeFile(tmp, code, 'utf-8'))
.finally(() => promises.delete(tmp)),
)
await promises.get(tmp)
Object.assign(result, { id: tmp })
return { id: tmp }
},
resolveId(id, importer, transformMode) {
return project.vitenode.resolveId(id, importer, transformMode).catch(handleRollupError)
},
transform(id, environment) {
return project.vitenode.transformModule(id, environment).catch(handleRollupError)
const url = normalizeResolvedIdToUrl(environment, fileURLToPath(id))
const result = await environment.transformRequest(url).catch(handleRollupError)
return { code: result?.code }
},
async onQueued(file) {
if (options.collect) {
@ -119,58 +120,3 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
},
}
}
// serialize rollup error on server to preserve details as a test error
function handleRollupError(e: unknown): never {
if (
e instanceof Error
&& ('plugin' in e || 'frame' in e || 'id' in e)
) {
// eslint-disable-next-line no-throw-literal
throw {
name: e.name,
message: e.message,
stack: e.stack,
cause: e.cause,
__vitest_rollup_error__: {
plugin: (e as any).plugin,
id: (e as any).id,
loc: (e as any).loc,
frame: (e as any).frame,
},
}
}
throw e
}
/**
* Performs an atomic write operation using the write-then-rename pattern.
*
* Why we need this:
* - Ensures file integrity by never leaving partially written files on disk
* - Prevents other processes from reading incomplete data during writes
* - Particularly important for test files where incomplete writes could cause test failures
*
* The implementation writes to a temporary file first, then renames it to the target path.
* This rename operation is atomic on most filesystems (including POSIX-compliant ones),
* guaranteeing that other processes will only ever see the complete file.
*
* Added in https://github.com/vitest-dev/vitest/pull/7531
*/
async function atomicWriteFile(realFilePath: string, data: string): Promise<void> {
const dir = dirname(realFilePath)
const tmpFilePath = join(dir, `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`)
try {
await writeFile(tmpFilePath, data, 'utf-8')
await rename(tmpFilePath, realFilePath)
}
finally {
try {
if (await stat(tmpFilePath)) {
await unlink(tmpFilePath)
}
}
catch {}
}
}

View File

@ -129,9 +129,9 @@ function printErrorInner(
? error.stacks[0]
: stacks.find((stack) => {
try {
const module = project._vite && project.getModuleById(stack.file)
return (
project._vite
&& project.getModuleById(stack.file)
(module?.transformResult || module?.ssrTransformResult)
&& existsSync(stack.file)
)
}
@ -261,6 +261,7 @@ const skipErrorProperties = new Set([
'actual',
'expected',
'diffOptions',
'runnerError',
// webkit props
'sourceURL',
'column',
@ -448,9 +449,14 @@ export function generateCodeFrame(
}
const lineLength = lines[j].length
const strippedContent = stripVTControlCharacters(lines[j])
if (strippedContent.startsWith('//# sourceMappingURL')) {
continue
}
// too long, maybe it's a minified file, skip for codeframe
if (stripVTControlCharacters(lines[j]).length > 200) {
if (strippedContent.length > 200) {
return ''
}

View File

@ -1,10 +1,6 @@
import type { GlobOptions } from 'tinyglobby'
import type {
ModuleNode,
TransformResult,
ViteDevServer,
InlineConfig as ViteInlineConfig,
} from 'vite'
import type { ModuleNode, TransformResult, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite'
import type { ModuleRunner } from 'vite/module-runner'
import type { Typechecker } from '../typecheck/typechecker'
import type { ProvidedContext } from '../types/general'
import type { OnTestsRerunHandler, Vitest } from './core'
@ -28,16 +24,17 @@ import { deepMerge, nanoid, slash } from '@vitest/utils'
import { isAbsolute, join, relative } from 'pathe'
import pm from 'picomatch'
import { glob } from 'tinyglobby'
import { ViteNodeRunner } from 'vite-node/client'
import { ViteNodeServer } from 'vite-node/server'
import { setup } from '../api/setup'
import { isBrowserEnabled, resolveConfig } from './config/resolveConfig'
import { serializeConfig } from './config/serializeConfig'
import { ServerModuleRunner } from './environments/serverRunner'
import { loadGlobalSetupFiles } from './globalSetup'
import { CoverageTransform } from './plugins/coverageTransform'
import { MetaEnvReplacerPlugin } from './plugins/metaEnvReplacer'
import { MocksPlugins } from './plugins/mocks'
import { WorkspaceVitestPlugin } from './plugins/workspace'
import { getFilePoolName } from './pool'
import { VitestResolver } from './resolver'
import { TestSpecification } from './spec'
import { createViteServer } from './vite'
@ -66,13 +63,13 @@ export class TestProject {
*/
public readonly tmpDir: string = join(tmpdir(), nanoid())
/** @internal */ vitenode!: ViteNodeServer
/** @internal */ typechecker?: Typechecker
/** @internal */ _config?: ResolvedConfig
/** @internal */ _vite?: ViteDevServer
/** @internal */ _hash?: string
/** @internal */ _resolver!: VitestResolver
private runner!: ViteNodeRunner
private runner!: ModuleRunner
private closingPromise: Promise<void> | undefined
@ -556,6 +553,7 @@ export class TestProject {
return true
},
}),
MetaEnvReplacerPlugin(),
],
[CoverageTransform(this.vitest)],
)
@ -601,7 +599,7 @@ export class TestProject {
* @param moduleId The ID of the module in Vite module graph
*/
public import<T>(moduleId: string): Promise<T> {
return this.runner.executeId(moduleId)
return this.runner.import(moduleId)
}
/** @deprecated use `name` instead */
@ -642,20 +640,15 @@ export class TestProject {
this.closingPromise = undefined
this._resolver = new VitestResolver(server.config.cacheDir, this._config)
this._vite = server
this.vitenode = new ViteNodeServer(server, this.config.server)
const node = this.vitenode
this.runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id: string) {
return node.fetchModule(id)
},
resolveId(id: string, importer?: string) {
return node.resolveId(id, importer)
},
})
const environment = server.environments.__vitest__
this.runner = new ServerModuleRunner(
environment,
this._resolver,
this._config,
)
}
private _serializeOverriddenConfig(): SerializedConfig {
@ -715,10 +708,10 @@ export class TestProject {
vitest.config.name || vitest.config.root,
vitest,
)
project.vitenode = vitest.vitenode
project.runner = vitest.runner
project._vite = vitest.server
project._config = vitest.config
project._resolver = vitest._resolver
project._setHash()
project._provideObject(vitest.config.provide)
return project
@ -730,9 +723,9 @@ export class TestProject {
parent.path,
parent.vitest,
)
clone.vitenode = parent.vitenode
clone.runner = parent.runner
clone._vite = parent._vite
clone._resolver = parent._resolver
clone._config = config
clone._setHash()
clone._parent = parent

View File

@ -524,7 +524,7 @@ export abstract class BaseReporter implements Reporter {
const environmentTime = sum(files, file => file.environmentLoad)
const prepareTime = sum(files, file => file.prepareDuration)
const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration())
const transformTime = this.ctx.state.transformTime
const typecheck = sum(this.ctx.projects, project => project.typechecker?.getResult().time)
const timers = [

View File

@ -150,6 +150,11 @@ export async function readBlobs(
const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file)
moduleNode.url = url
moduleNode.id = moduleId
moduleNode.transformResult = {
// print error checks that transformResult is set
code: ' ',
map: null,
}
project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode)
})
})

View File

@ -1,4 +1,4 @@
import type { ViteNodeRunner } from 'vite-node/client'
import type { ModuleRunner } from 'vite/module-runner'
import type { Vitest } from '../core'
import type { ResolvedConfig } from '../types/config'
import type { Reporter } from '../types/reporter'
@ -8,11 +8,11 @@ import { BenchmarkReportsMap, ReportersMap } from './index'
async function loadCustomReporterModule<C extends Reporter>(
path: string,
runner: ViteNodeRunner,
runner: ModuleRunner,
): Promise<new (options?: unknown) => C> {
let customReporterModule: { default: new () => C }
try {
customReporterModule = await runner.executeId(path)
customReporterModule = await runner.import(path)
}
catch (customReporterModuleError) {
throw new Error(`Failed to load custom Reporter from ${path}`, {
@ -43,7 +43,7 @@ function createReporters(
const [reporterName, reporterOptions] = referenceOrInstance
if (reporterName === 'html') {
await ctx.packageInstaller.ensureInstalled('@vitest/ui', runner.root, ctx.version)
await ctx.packageInstaller.ensureInstalled('@vitest/ui', ctx.config.root, ctx.version)
const CustomReporter = await loadCustomReporterModule(
'@vitest/ui/reporter',
runner,
@ -72,7 +72,7 @@ function createReporters(
function createBenchmarkReporters(
reporterReferences: Array<string | Reporter | BenchmarkBuiltinReporters>,
runner: ViteNodeRunner,
runner: ModuleRunner,
): Promise<(Reporter | BenchmarkReporter)[]> {
const promisedReporters = reporterReferences.map(
async (referenceOrInstance) => {

View File

@ -0,0 +1,222 @@
import type { ResolvedConfig, ServerDepsOptions } from './types/config'
import { existsSync, promises as fsp } from 'node:fs'
import { isBuiltin } from 'node:module'
import { pathToFileURL } from 'node:url'
import { KNOWN_ASSET_RE } from '@vitest/utils'
import { findNearestPackageData } from '@vitest/utils/resolver'
import * as esModuleLexer from 'es-module-lexer'
import { dirname, extname, join, resolve } from 'pathe'
import { isWindows } from '../utils/env'
export class VitestResolver {
private options: ExternalizeOptions
private externalizeCache = new Map<string, Promise<string | false>>()
constructor(cacheDir: string, config: ResolvedConfig) {
this.options = {
moduleDirectories: config.deps.moduleDirectories,
inlineFiles: config.setupFiles.flatMap((file) => {
if (file.startsWith('file://')) {
return file
}
const resolvedId = resolve(file)
return [resolvedId, pathToFileURL(resolvedId).href]
}),
cacheDir,
inline: config.server.deps?.inline,
external: config.server.deps?.external,
}
}
public shouldExternalize(file: string): Promise<string | false> {
return shouldExternalize(normalizeId(file), this.options, this.externalizeCache)
}
}
function normalizeId(id: string) {
if (id.startsWith('/@fs/')) {
id = id.slice(isWindows ? 5 : 4)
}
return id
}
interface ExternalizeOptions extends ServerDepsOptions {
moduleDirectories?: string[]
inlineFiles?: string[]
cacheDir?: string
}
const BUILTIN_EXTENSIONS = new Set(['.mjs', '.cjs', '.node', '.wasm'])
const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/
const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/
const defaultInline = [
/virtual:/,
/\.[mc]?ts$/,
// special Vite query strings
/[?&](init|raw|url|inline)\b/,
// Vite returns a string for assets imports, even if it's inside "node_modules"
KNOWN_ASSET_RE,
/^(?!.*node_modules).*\.mjs$/,
/^(?!.*node_modules).*\.cjs\.js$/,
// Vite client
/vite\w*\/dist\/client\/env.mjs/,
]
const depsExternal = [
/\/node_modules\/.*\.cjs\.js$/,
/\/node_modules\/.*\.mjs$/,
]
export function guessCJSversion(id: string): string | undefined {
if (id.match(ESM_EXT_RE)) {
for (const i of [
id.replace(ESM_EXT_RE, '.mjs'),
id.replace(ESM_EXT_RE, '.umd.js'),
id.replace(ESM_EXT_RE, '.cjs.js'),
id.replace(ESM_EXT_RE, '.js'),
]) {
if (existsSync(i)) {
return i
}
}
}
if (id.match(ESM_FOLDER_RE)) {
for (const i of [
id.replace(ESM_FOLDER_RE, '/umd/$1'),
id.replace(ESM_FOLDER_RE, '/cjs/$1'),
id.replace(ESM_FOLDER_RE, '/lib/$1'),
id.replace(ESM_FOLDER_RE, '/$1'),
]) {
if (existsSync(i)) {
return i
}
}
}
}
// The code from https://github.com/unjs/mlly/blob/c5bcca0cda175921344fd6de1bc0c499e73e5dac/src/syntax.ts#L51-L98
async function isValidNodeImport(id: string) {
const extension = extname(id)
if (BUILTIN_EXTENSIONS.has(extension)) {
return true
}
if (extension !== '.js') {
return false
}
id = id.replace('file:///', '')
const package_ = findNearestPackageData(dirname(id))
if (package_.type === 'module') {
return true
}
if (/\.(?:\w+-)?esm?(?:-\w+)?\.js$|\/esm?\//.test(id)) {
return false
}
try {
await esModuleLexer.init
const code = await fsp.readFile(id, 'utf8')
const [, , , hasModuleSyntax] = esModuleLexer.parse(code)
return !hasModuleSyntax
}
catch {
return false
}
}
export async function shouldExternalize(
id: string,
options: ExternalizeOptions,
cache: Map<string, Promise<string | false>>,
): Promise<string | false> {
if (!cache.has(id)) {
cache.set(id, _shouldExternalize(id, options))
}
return cache.get(id)!
}
async function _shouldExternalize(
id: string,
options?: ExternalizeOptions,
): Promise<string | false> {
if (isBuiltin(id)) {
return id
}
// data: should be processed by native import,
// since it is a feature of ESM.
// also externalize network imports since nodejs allows it when --experimental-network-imports
if (id.startsWith('data:') || /^(?:https?:)?\/\//.test(id)) {
return id
}
const moduleDirectories = options?.moduleDirectories || ['/node_modules/']
if (matchExternalizePattern(id, moduleDirectories, options?.inline)) {
return false
}
if (options?.inlineFiles && options?.inlineFiles.includes(id)) {
return false
}
if (matchExternalizePattern(id, moduleDirectories, options?.external)) {
return id
}
// Unless the user explicitly opted to inline them, externalize Vite deps.
// They are too big to inline by default.
if (options?.cacheDir && id.includes(options.cacheDir)) {
return id
}
const isLibraryModule = moduleDirectories.some(dir => id.includes(dir))
const guessCJS = isLibraryModule && options?.fallbackCJS
id = guessCJS ? guessCJSversion(id) || id : id
if (matchExternalizePattern(id, moduleDirectories, defaultInline)) {
return false
}
if (matchExternalizePattern(id, moduleDirectories, depsExternal)) {
return id
}
if (isLibraryModule && (await isValidNodeImport(id))) {
return id
}
return false
}
function matchExternalizePattern(
id: string,
moduleDirectories: string[],
patterns?: (string | RegExp)[] | true,
) {
if (patterns == null) {
return false
}
if (patterns === true) {
return true
}
for (const ex of patterns) {
if (typeof ex === 'string') {
if (moduleDirectories.some(dir => id.includes(join(dir, ex)))) {
return true
}
}
else {
if (ex.test(id)) {
return true
}
}
}
return false
}

View File

@ -1,8 +1,8 @@
import type { Vitest } from '../core'
import type { TestSpecification } from '../spec'
import type { TestSequencer } from './types'
import { slash } from '@vitest/utils'
import { relative, resolve } from 'pathe'
import { slash } from 'vite-node/utils'
import { hash } from '../hash'
export class BaseSequencer implements TestSequencer {

View File

@ -176,8 +176,8 @@ export class VitestSpecifications {
}
deps.add(filepath)
const mod = project.vite.moduleGraph.getModuleById(filepath)
const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath)
const mod = project.vite.environments.ssr.moduleGraph.getModuleById(filepath)
const transformed = mod?.transformResult || await project.vite.environments.ssr.transformRequest(filepath)
if (!transformed) {
return
}

View File

@ -24,6 +24,7 @@ export class StateManager {
processTimeoutCauses: Set<string> = new Set()
reportedTasksMap: WeakMap<Task, TestModule | TestCase | TestSuite> = new WeakMap()
blobs?: MergedBlobs
transformTime = 0
onUnhandledError?: OnUnhandledErrorCallback

View File

@ -65,7 +65,6 @@ type UnsupportedProperties
// non-browser options
| 'api'
| 'deps'
| 'testTransformMode'
| 'environment'
| 'environmentOptions'
| 'server'

View File

@ -4,7 +4,6 @@ import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
import type { SnapshotStateOptions } from '@vitest/snapshot'
import type { SerializedDiffOptions } from '@vitest/utils/diff'
import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite'
import type { ViteNodeServerOptions } from 'vite-node'
import type { ChaiConfig } from '../../integrations/chai/config'
import type { SerializedConfig } from '../../runtime/config'
import type { Arrayable, LabelColor, ParsedStack, ProvidedContext, TestError } from '../../types/general'
@ -135,32 +134,11 @@ export type DepsOptimizationOptions = Omit<
enabled?: boolean
}
export interface TransformModePatterns {
/**
* Use SSR transform pipeline for all modules inside specified tests.
* Vite plugins will receive `ssr: true` flag when processing those files.
*
* @default tests with node or edge environment
*/
ssr?: string[]
/**
* First do a normal transform pipeline (targeting browser),
* then then do a SSR rewrite to run the code in Node.
* Vite plugins will receive `ssr: false` flag when processing those files.
*
* @default tests with jsdom or happy-dom environment
*/
web?: string[]
}
interface DepsOptions {
/**
* Enable dependency optimization. This can improve the performance of your tests.
*/
optimizer?: {
web?: DepsOptimizationOptions
ssr?: DepsOptimizationOptions
}
optimizer?: Partial<Record<'client' | 'ssr' | ({} & string), DepsOptimizationOptions>>
web?: {
/**
* Should Vitest process assets (.png, .svg, .jpg, etc) files and resolve them like Vite does in the browser.
@ -193,27 +171,6 @@ interface DepsOptions {
*/
transformGlobPattern?: RegExp | RegExp[]
}
/**
* Externalize means that Vite will bypass the package to native Node.
*
* Externalized dependencies will not be applied Vite's transformers and resolvers.
* And does not support HMR on reload.
*
* Typically, packages under `node_modules` are externalized.
*
* @deprecated If you rely on vite-node directly, use `server.deps.external` instead. Otherwise, consider using `deps.optimizer.{web,ssr}.exclude`.
*/
external?: (string | RegExp)[]
/**
* Vite will process inlined modules.
*
* This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle).
*
* If `true`, every dependency will be inlined
*
* @deprecated If you rely on vite-node directly, use `server.deps.inline` instead. Otherwise, consider using `deps.optimizer.{web,ssr}.include`.
*/
inline?: (string | RegExp)[] | true
/**
* Interpret CJS module's default as named exports
@ -222,17 +179,6 @@ interface DepsOptions {
*/
interopDefault?: boolean
/**
* When a dependency is a valid ESM package, try to guess the cjs version based on the path.
* This will significantly improve the performance in huge repo, but might potentially
* cause some misalignment if a package have different logic in ESM and CJS mode.
*
* @default false
*
* @deprecated Use `server.deps.fallbackCJS` instead.
*/
fallbackCJS?: boolean
/**
* A list of directories relative to the config file that should be treated as module directories.
*
@ -297,10 +243,9 @@ export interface InlineConfig {
*/
deps?: DepsOptions
/**
* Vite-node server options
*/
server?: Omit<ViteNodeServerOptions, 'transformMode'>
server?: {
deps?: ServerDepsOptions
}
/**
* Base directory to scan for the test files
@ -560,11 +505,6 @@ export interface InlineConfig {
*/
uiBase?: string
/**
* Determine the transform method for all modules imported inside a test that matches the glob pattern.
*/
testTransformMode?: TransformModePatterns
/**
* Format options for snapshot testing.
*/
@ -1108,6 +1048,31 @@ type NonProjectOptions
| 'fileParallelism'
| 'watchTriggerPatterns'
export interface ServerDepsOptions {
/**
* Externalize means that Vite will bpass the package to native Node.
*
* Externalized dependencies will not be applied Vite's transformers and resolvers.
* And does not support HMR on reload.
*
* Typically, packages under `node_modules` are externalized.
*/
external?: (string | RegExp)[]
/**
* Vite will process inlined modules.
*
* This could be helpful to handle packages that ship `.js` in ESM format (that Node can't handle).
*
* If `true`, every dependency will be inlined
*/
inline?: (string | RegExp)[] | true
/**
* Try to guess the CJS version of a package when it's invalid ESM
* @default false
*/
fallbackCJS?: boolean
}
export type ProjectConfig = Omit<
InlineConfig,
NonProjectOptions

View File

@ -59,7 +59,7 @@ export interface ReportContext {
}
export interface CoverageModuleLoader extends RuntimeCoverageModuleLoader {
executeId: (id: string) => Promise<{ default: CoverageProviderModule }>
import: (id: string) => Promise<{ default: CoverageProviderModule }>
}
export interface CoverageProviderModule extends RuntimeCoverageProviderModule {

View File

@ -10,7 +10,6 @@ import type {
} from '../node/types/config'
import '../node/types/vite'
export { extraInlineDeps } from '../constants'
// will import vitest declare test in module 'vite'
export {
configDefaults,

View File

@ -1 +0,0 @@
export { VitestExecutor } from '../runtime/execute'

View File

@ -136,3 +136,5 @@ export type {
export type { SerializedError } from '@vitest/utils'
export type { SerializedTestSpecification }
export type { DiffOptions } from '@vitest/utils/diff'
export { EvaluatedModules } from 'vite/module-runner'

View File

@ -0,0 +1,14 @@
export {
VitestModuleEvaluator,
type VitestModuleEvaluatorOptions,
} from '../runtime/moduleRunner/moduleEvaluator'
export {
VitestModuleRunner,
type VitestModuleRunnerOptions,
} from '../runtime/moduleRunner/moduleRunner'
export {
type ContextModuleRunnerOptions,
startVitestModuleRunner,
VITEST_VM_CONTEXT_SYMBOL,
} from '../runtime/moduleRunner/startModuleRunner'
export { getWorkerState } from '../runtime/utils'

View File

@ -98,7 +98,6 @@ export type {
SequenceHooks,
SequenceSetupFiles,
UserConfig as TestUserConfig,
TransformModePatterns,
TypecheckConfig,
UserWorkspaceConfig,
VitestEnvironment,

View File

@ -71,14 +71,7 @@ export interface SerializedConfig {
transformCss?: boolean
transformGlobPattern?: RegExp | RegExp[]
}
optimizer: {
web: {
enabled: boolean
}
ssr: {
enabled: boolean
}
}
optimizer: Record<string, { enabled: boolean }>
interopDefault: boolean | undefined
moduleDirectories: string[] | undefined
}

View File

@ -1,404 +0,0 @@
import type { ViteNodeRunnerOptions } from 'vite-node'
import type { ModuleCacheMap, ModuleExecutionInfo } from 'vite-node/client'
import type { WorkerGlobalState } from '../types/worker'
import type { ExternalModulesExecutor } from './external-executor'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import vm from 'node:vm'
import { processError } from '@vitest/utils/error'
import { normalize } from 'pathe'
import { DEFAULT_REQUEST_STUBS, ViteNodeRunner } from 'vite-node/client'
import {
isInternalRequest,
isNodeBuiltin,
isPrimitive,
toFilePath,
} from 'vite-node/utils'
import { distDir } from '../paths'
import { VitestMocker } from './mocker'
const normalizedDistDir = normalize(distDir)
const { readFileSync } = fs
export interface ExecuteOptions extends ViteNodeRunnerOptions {
moduleDirectories?: string[]
state: WorkerGlobalState
context?: vm.Context
externalModulesExecutor?: ExternalModulesExecutor
}
export async function createVitestExecutor(options: ExecuteOptions): Promise<VitestExecutor> {
const runner = new VitestExecutor(options)
await runner.executeId('/@vite/env')
await runner.mocker.initializeSpyModule()
return runner
}
const externalizeMap = new Map<string, string>()
export interface ContextExecutorOptions {
moduleCache?: ModuleCacheMap
context?: vm.Context
externalModulesExecutor?: ExternalModulesExecutor
state: WorkerGlobalState
requestStubs: Record<string, any>
}
const bareVitestRegexp = /^@?vitest(?:\/|$)/
const dispose: (() => void)[] = []
function listenForErrors(state: () => WorkerGlobalState) {
dispose.forEach(fn => fn())
dispose.length = 0
function catchError(err: unknown, type: string, event: 'uncaughtException' | 'unhandledRejection') {
const worker = state()
const listeners = process.listeners(event as 'uncaughtException')
// if there is another listener, assume that it's handled by user code
// one is Vitest's own listener
if (listeners.length > 1) {
return
}
const error = processError(err)
if (!isPrimitive(error)) {
error.VITEST_TEST_NAME = worker.current?.type === 'test' ? worker.current.name : undefined
if (worker.filepath) {
error.VITEST_TEST_PATH = worker.filepath
}
error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun
}
state().rpc.onUnhandledError(error, type)
}
const uncaughtException = (e: Error) => catchError(e, 'Uncaught Exception', 'uncaughtException')
const unhandledRejection = (e: Error) => catchError(e, 'Unhandled Rejection', 'unhandledRejection')
process.on('uncaughtException', uncaughtException)
process.on('unhandledRejection', unhandledRejection)
dispose.push(() => {
process.off('uncaughtException', uncaughtException)
process.off('unhandledRejection', unhandledRejection)
})
}
const relativeIds: Record<string, string> = {}
function getVitestImport(id: string, state: () => WorkerGlobalState) {
if (externalizeMap.has(id)) {
return { externalize: externalizeMap.get(id)! }
}
// always externalize Vitest because we import from there before running tests
// so we already have it cached by Node.js
const root = state().config.root
const relativeRoot = relativeIds[root] ?? (relativeIds[root] = normalizedDistDir.slice(root.length))
if (
// full dist path
id.includes(distDir)
|| id.includes(normalizedDistDir)
// "relative" to root path:
// /node_modules/.pnpm/vitest/dist
|| (relativeRoot && relativeRoot !== '/' && id.startsWith(relativeRoot))
) {
const { path } = toFilePath(id, root)
const externalize = pathToFileURL(path).toString()
externalizeMap.set(id, externalize)
return { externalize }
}
if (bareVitestRegexp.test(id)) {
externalizeMap.set(id, id)
return { externalize: id }
}
return null
}
export async function startVitestExecutor(options: ContextExecutorOptions): Promise<VitestExecutor> {
const state = (): WorkerGlobalState =>
// @ts-expect-error injected untyped global
globalThis.__vitest_worker__ || options.state
const rpc = () => state().rpc
process.exit = (code = process.exitCode || 0): never => {
throw new Error(`process.exit unexpectedly called with "${code}"`)
}
listenForErrors(state)
const getTransformMode = () => {
return state().environment.transformMode ?? 'ssr'
}
return await createVitestExecutor({
async fetchModule(id) {
const vitest = getVitestImport(id, state)
if (vitest) {
return vitest
}
const result = await rpc().fetch(id, getTransformMode())
if (result.id && !result.externalize) {
const code = readFileSync(result.id, 'utf-8')
return { code }
}
return result
},
resolveId(id, importer) {
return rpc().resolveId(id, importer, getTransformMode())
},
get moduleCache() {
return state().moduleCache as ModuleCacheMap
},
get moduleExecutionInfo() {
return state().moduleExecutionInfo
},
get interopDefault() {
return state().config.deps.interopDefault
},
get moduleDirectories() {
return state().config.deps.moduleDirectories
},
get root() {
return state().config.root
},
get base() {
return state().config.base
},
...options,
})
}
function updateStyle(id: string, css: string) {
if (typeof document === 'undefined') {
return
}
const element = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (element) {
element.textContent = css
return
}
const head = document.querySelector('head')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.setAttribute('data-vite-dev-id', id)
style.textContent = css
head?.appendChild(style)
}
function removeStyle(id: string) {
if (typeof document === 'undefined') {
return
}
const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (sheet) {
document.head.removeChild(sheet)
}
}
export function getDefaultRequestStubs(context?: vm.Context): {
'/@vite/client': any
'@vite/client': any
} {
if (!context) {
const clientStub = {
...DEFAULT_REQUEST_STUBS['@vite/client'],
updateStyle,
removeStyle,
}
return {
'/@vite/client': clientStub,
'@vite/client': clientStub,
}
}
const clientStub = vm.runInContext(
`(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`,
context,
)(DEFAULT_REQUEST_STUBS['@vite/client'])
return {
'/@vite/client': clientStub,
'@vite/client': clientStub,
}
}
export class VitestExecutor extends ViteNodeRunner {
public mocker: VitestMocker
public externalModules?: ExternalModulesExecutor
private primitives: {
Object: typeof Object
Reflect: typeof Reflect
Symbol: typeof Symbol
}
constructor(public options: ExecuteOptions) {
super({
...options,
// interop is done inside the external executor instead
interopDefault: options.context ? false : options.interopDefault,
})
this.mocker = new VitestMocker(this)
if (!options.context) {
Object.defineProperty(globalThis, '__vitest_mocker__', {
value: this.mocker,
writable: true,
configurable: true,
})
this.primitives = { Object, Reflect, Symbol }
}
else if (options.externalModulesExecutor) {
this.primitives = vm.runInContext(
'({ Object, Reflect, Symbol })',
options.context,
)
this.externalModules = options.externalModulesExecutor
}
else {
throw new Error(
'When context is provided, externalModulesExecutor must be provided as well.',
)
}
}
protected getContextPrimitives(): {
Object: typeof Object
Reflect: typeof Reflect
Symbol: typeof Symbol
} {
return this.primitives
}
get state(): WorkerGlobalState {
// @ts-expect-error injected untyped global
return globalThis.__vitest_worker__ || this.options.state
}
get moduleExecutionInfo(): ModuleExecutionInfo | undefined {
return this.options.moduleExecutionInfo
}
shouldResolveId(id: string, _importee?: string | undefined): boolean {
if (isInternalRequest(id) || id.startsWith('data:')) {
return false
}
const transformMode = this.state.environment?.transformMode ?? 'ssr'
// do not try and resolve node builtins in Node
// import('url') returns Node internal even if 'url' package is installed
return transformMode === 'ssr'
? !isNodeBuiltin(id)
: !id.startsWith('node:')
}
async originalResolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> {
return super.resolveUrl(id, importer)
}
async resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> {
if (VitestMocker.pendingIds.length) {
await this.mocker.resolveMocks()
}
if (importer && importer.startsWith('mock:')) {
importer = importer.slice(5)
}
try {
return await super.resolveUrl(id, importer)
}
catch (error: any) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
const { id } = error[Symbol.for('vitest.error.not_found.data')]
const path = this.mocker.normalizePath(id)
const mock = this.mocker.getDependencyMock(path)
if (mock !== undefined) {
return [id, id] as [string, string]
}
}
throw error
}
}
protected async runModule(context: Record<string, any>, transformed: string): Promise<void> {
const vmContext = this.options.context
if (!vmContext || !this.externalModules) {
return super.runModule(context, transformed)
}
// add 'use strict' since ESM enables it by default
const codeDefinition = `'use strict';async (${Object.keys(context).join(
',',
)})=>{{`
const code = `${codeDefinition}${transformed}\n}}`
const options = {
filename: context.__filename,
lineOffset: 0,
columnOffset: -codeDefinition.length,
}
const finishModuleExecutionInfo = this.startCalculateModuleExecutionInfo(options.filename, codeDefinition.length)
try {
const fn = vm.runInContext(code, vmContext, {
...options,
// if we encountered an import, it's not inlined
importModuleDynamically: this.externalModules
.importModuleDynamically as any,
} as any)
await fn(...Object.values(context))
}
finally {
this.options.moduleExecutionInfo?.set(options.filename, finishModuleExecutionInfo())
}
}
public async importExternalModule(path: string): Promise<any> {
if (this.externalModules) {
return this.externalModules.import(path)
}
return super.importExternalModule(path)
}
async dependencyRequest(
id: string,
fsPath: string,
callstack: string[],
): Promise<any> {
const mocked = await this.mocker.requestWithMock(fsPath, callstack)
if (typeof mocked === 'string') {
return super.dependencyRequest(mocked, mocked, callstack)
}
if (mocked && typeof mocked === 'object') {
return mocked
}
return super.dependencyRequest(id, fsPath, callstack)
}
prepareContext(context: Record<string, any>): Record<string, any> {
// support `import.meta.vitest` for test entry
if (
this.state.filepath
&& normalize(this.state.filepath) === normalize(context.__filename)
) {
const globalNamespace = this.options.context || globalThis
Object.defineProperty(context.__vite_ssr_import_meta__, 'vitest', {
// @ts-expect-error injected untyped global
get: () => globalNamespace.__vitest_index__,
})
}
if (this.options.context && this.externalModules) {
context.require = this.externalModules.createRequire(context.__filename)
}
return context
}
}

View File

@ -3,15 +3,16 @@ import type { RuntimeRPC } from '../types/rpc'
import type { FileMap } from './vm/file-map'
import type { VMModule } from './vm/types'
import fs from 'node:fs'
import { dirname } from 'node:path'
import { isBuiltin } from 'node:module'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { extname, join, normalize } from 'pathe'
import { getCachedData, isBareImport, isNodeBuiltin, setCacheData } from 'vite-node/utils'
import { isBareImport } from '@vitest/utils'
import { findNearestPackageData } from '@vitest/utils/resolver'
import { extname, normalize } from 'pathe'
import { CommonjsExecutor } from './vm/commonjs-executor'
import { EsmExecutor } from './vm/esm-executor'
import { ViteExecutor } from './vm/vite-executor'
const { existsSync, statSync } = fs
const { existsSync } = fs
// always defined when we use vm pool
const nativeResolve = import.meta.resolve!
@ -119,48 +120,13 @@ export class ExternalModulesExecutor {
return nativeResolve(specifier, parent)
}
private findNearestPackageData(basedir: string): {
type?: 'module' | 'commonjs'
} {
const originalBasedir = basedir
const packageCache = this.options.packageCache
while (basedir) {
const cached = getCachedData(packageCache, basedir, originalBasedir)
if (cached) {
return cached
}
const pkgPath = join(basedir, 'package.json')
try {
if (statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) {
const pkgData = JSON.parse(this.fs.readFile(pkgPath))
if (packageCache) {
setCacheData(packageCache, pkgData, basedir, originalBasedir)
}
return pkgData
}
}
catch {}
const nextBasedir = dirname(basedir)
if (nextBasedir === basedir) {
break
}
basedir = nextBasedir
}
return {}
}
private getModuleInformation(identifier: string): ModuleInformation {
if (identifier.startsWith('data:')) {
return { type: 'data', url: identifier, path: identifier }
}
const extension = extname(identifier)
if (extension === '.node' || isNodeBuiltin(identifier)) {
if (extension === '.node' || isBuiltin(identifier)) {
return { type: 'builtin', url: identifier, path: identifier }
}
@ -193,7 +159,7 @@ export class ExternalModulesExecutor {
type = 'wasm'
}
else {
const pkgData = this.findNearestPackageData(normalize(pathUrl))
const pkgData = findNearestPackageData(normalize(pathUrl))
type = pkgData.type === 'module' ? 'module' : 'commonjs'
}

View File

@ -0,0 +1,49 @@
import type { WorkerGlobalState } from '../../types/worker'
import { pathToFileURL } from 'node:url'
import { join, normalize } from 'pathe'
import { distDir } from '../../paths'
const bareVitestRegexp = /^@?vitest(?:\/|$)/
const normalizedDistDir = normalize(distDir)
const relativeIds: Record<string, string> = {}
const externalizeMap = new Map<string, string>()
// all Vitest imports always need to be externalized
export function getCachedVitestImport(
id: string,
state: () => WorkerGlobalState,
): null | { externalize: string; type: 'module' } {
if (id.startsWith('/@fs/') || id.startsWith('\\@fs\\')) {
id = id.slice(process.platform === 'win32' ? 5 : 4)
}
if (externalizeMap.has(id)) {
return { externalize: externalizeMap.get(id)!, type: 'module' }
}
// always externalize Vitest because we import from there before running tests
// so we already have it cached by Node.js
const root = state().config.root
const relativeRoot = relativeIds[root] ?? (relativeIds[root] = normalizedDistDir.slice(root.length))
if (id.includes(distDir) || id.includes(normalizedDistDir)) {
const externalize = id.startsWith('file://')
? id
: pathToFileURL(id).toString()
externalizeMap.set(id, externalize)
return { externalize, type: 'module' }
}
if (
// "relative" to root path:
// /node_modules/.pnpm/vitest/dist
(relativeRoot && relativeRoot !== '/' && id.startsWith(relativeRoot))
) {
const path = join(root, id)
const externalize = pathToFileURL(path).toString()
externalizeMap.set(id, externalize)
return { externalize, type: 'module' }
}
if (bareVitestRegexp.test(id)) {
externalizeMap.set(id, id)
return { externalize: id, type: 'module' }
}
return null
}

View File

@ -0,0 +1,41 @@
import type { WorkerGlobalState } from '../../types/worker'
import { processError } from '@vitest/utils/error'
const dispose: (() => void)[] = []
export function listenForErrors(state: () => WorkerGlobalState): void {
dispose.forEach(fn => fn())
dispose.length = 0
function catchError(err: unknown, type: string, event: 'uncaughtException' | 'unhandledRejection') {
const worker = state()
const listeners = process.listeners(event as 'uncaughtException')
// if there is another listener, assume that it's handled by user code
// one is Vitest's own listener
if (listeners.length > 1) {
return
}
const error = processError(err)
if (typeof error === 'object' && error != null) {
error.VITEST_TEST_NAME = worker.current?.type === 'test' ? worker.current.name : undefined
if (worker.filepath) {
error.VITEST_TEST_PATH = worker.filepath
}
error.VITEST_AFTER_ENV_TEARDOWN = worker.environmentTeardownRun
}
state().rpc.onUnhandledError(error, type)
}
const uncaughtException = (e: Error) => catchError(e, 'Uncaught Exception', 'uncaughtException')
const unhandledRejection = (e: Error) => catchError(e, 'Unhandled Rejection', 'unhandledRejection')
process.on('uncaughtException', uncaughtException)
process.on('unhandledRejection', unhandledRejection)
dispose.push(() => {
process.off('uncaughtException', uncaughtException)
process.off('unhandledRejection', unhandledRejection)
})
}

View File

@ -0,0 +1,61 @@
export type ModuleExecutionInfo = Map<string, ModuleExecutionInfoEntry>
export interface ModuleExecutionInfoEntry {
startOffset: number
/** The duration that was spent executing the module. */
duration: number
/** The time that was spent executing the module itself and externalized imports. */
selfTime: number
}
/** Stack to track nested module execution for self-time calculation. */
export type ExecutionStack = Array<{
/** The file that is being executed. */
filename: string
/** The start time of this module's execution. */
startTime: number
/** Accumulated time spent importing all sub-imports. */
subImportTime: number
}>
const performanceNow = performance.now.bind(performance)
export class ModuleDebug {
private executionStack: ExecutionStack = []
startCalculateModuleExecutionInfo(filename: string, startOffset: number): () => ModuleExecutionInfoEntry {
const startTime = performanceNow()
this.executionStack.push({
filename,
startTime,
subImportTime: 0,
})
return () => {
const duration = performanceNow() - startTime
const currentExecution = this.executionStack.pop()
if (currentExecution == null) {
throw new Error('Execution stack is empty, this should never happen')
}
const selfTime = duration - currentExecution.subImportTime
if (this.executionStack.length > 0) {
this.executionStack.at(-1)!.subImportTime += duration
}
return {
startOffset,
duration,
selfTime,
}
}
}
}

View File

@ -0,0 +1,496 @@
import type {
EvaluatedModuleNode,
ModuleEvaluator,
ModuleRunnerContext,
ModuleRunnerImportMeta,
} from 'vite/module-runner'
import type { ModuleExecutionInfo } from './moduleDebug'
import type { VitestVmOptions } from './moduleRunner'
import { createRequire, isBuiltin } from 'node:module'
import { pathToFileURL } from 'node:url'
import vm from 'node:vm'
import { isAbsolute } from 'pathe'
import {
ssrDynamicImportKey,
ssrExportAllKey,
ssrImportKey,
ssrImportMetaKey,
ssrModuleExportsKey,
} from 'vite/module-runner'
import { ModuleDebug } from './moduleDebug'
const isWindows = process.platform === 'win32'
export interface VitestModuleEvaluatorOptions {
interopDefault?: boolean | undefined
moduleExecutionInfo?: ModuleExecutionInfo
getCurrentTestFilepath?: () => string | undefined
compiledFunctionArgumentsNames?: string[]
compiledFunctionArgumentsValues?: unknown[]
}
export class VitestModuleEvaluator implements ModuleEvaluator {
public stubs: Record<string, any> = {}
public env: ModuleRunnerImportMeta['env'] = createImportMetaEnvProxy()
private vm: VitestVmOptions | undefined
private compiledFunctionArgumentsNames?: string[]
private compiledFunctionArgumentsValues: unknown[] = []
private primitives: {
Object: typeof Object
Proxy: typeof Proxy
Reflect: typeof Reflect
}
private debug = new ModuleDebug()
constructor(
vmOptions?: VitestVmOptions | undefined,
private options: VitestModuleEvaluatorOptions = {},
) {
this.vm = vmOptions
this.stubs = getDefaultRequestStubs(vmOptions?.context)
if (options.compiledFunctionArgumentsNames) {
this.compiledFunctionArgumentsNames = options.compiledFunctionArgumentsNames
}
if (options.compiledFunctionArgumentsValues) {
this.compiledFunctionArgumentsValues = options.compiledFunctionArgumentsValues
}
if (vmOptions) {
this.primitives = vm.runInContext(
'({ Object, Proxy, Reflect })',
vmOptions.context,
)
}
else {
this.primitives = {
Object,
Proxy,
Reflect,
}
}
}
private convertIdToImportUrl(id: string) {
// TODO: vitest returns paths for external modules, but Vite returns file://
// unfortunetly, there is a bug in Vite where ID is resolved incorrectly, so we can't return files until the fix is merged
// https://github.com/vitejs/vite/pull/20449
if (!isWindows || isBuiltin(id) || /^(?:node:|data:|http:|https:|file:)/.test(id)) {
return id
}
const [filepath, query] = id.split('?')
if (query) {
return `${pathToFileURL(filepath).toString()}?${query}`
}
return pathToFileURL(filepath).toString()
}
async runExternalModule(id: string): Promise<any> {
if (id in this.stubs) {
return this.stubs[id]
}
const file = this.convertIdToImportUrl(id)
const namespace = this.vm
? await this.vm.externalModulesExecutor.import(file)
: await import(file)
if (!this.shouldInterop(file, namespace)) {
return namespace
}
const { mod, defaultExport } = interopModule(namespace)
const { Proxy, Reflect } = this.primitives
const proxy = new Proxy(mod, {
get(mod, prop) {
if (prop === 'default') {
return defaultExport
}
return mod[prop] ?? defaultExport?.[prop]
},
has(mod, prop) {
if (prop === 'default') {
return defaultExport !== undefined
}
return prop in mod || (defaultExport && prop in defaultExport)
},
getOwnPropertyDescriptor(mod, prop) {
const descriptor = Reflect.getOwnPropertyDescriptor(mod, prop)
if (descriptor) {
return descriptor
}
if (prop === 'default' && defaultExport !== undefined) {
return {
value: defaultExport,
enumerable: true,
configurable: true,
}
}
},
})
return proxy
}
async runInlinedModule(
context: ModuleRunnerContext,
code: string,
module: Readonly<EvaluatedModuleNode>,
): Promise<any> {
context.__vite_ssr_import_meta__.env = this.env
const { Reflect, Proxy, Object } = this.primitives
const exportsObject = context[ssrModuleExportsKey]
const SYMBOL_NOT_DEFINED = Symbol('not defined')
let moduleExports: unknown = SYMBOL_NOT_DEFINED
// this proxy is triggered only on exports.{name} and module.exports access
// inside the module itself. imported module is always "exports"
const cjsExports = new Proxy(exportsObject, {
get: (target, p, receiver) => {
if (Reflect.has(target, p)) {
return Reflect.get(target, p, receiver)
}
return Reflect.get(Object.prototype, p, receiver)
},
getPrototypeOf: () => Object.prototype,
set: (_, p, value) => {
// treat "module.exports =" the same as "exports.default =" to not have nested "default.default",
// so "exports.default" becomes the actual module
if (
p === 'default'
&& this.shouldInterop(module.file, { default: value })
&& cjsExports !== value
) {
exportAll(cjsExports, value)
exportsObject.default = value
return true
}
if (!Reflect.has(exportsObject, 'default')) {
exportsObject.default = {}
}
// returns undefined, when accessing named exports, if default is not an object
// but is still present inside hasOwnKeys, this is Node behaviour for CJS
if (
moduleExports !== SYMBOL_NOT_DEFINED
&& isPrimitive(moduleExports)
) {
defineExport(exportsObject, p, () => undefined)
return true
}
if (!isPrimitive(exportsObject.default)) {
exportsObject.default[p] = value
}
if (p !== 'default') {
defineExport(exportsObject, p, () => value)
}
return true
},
})
const moduleProxy = {
set exports(value) {
exportAll(cjsExports, value)
exportsObject.default = value
moduleExports = value
},
get exports() {
return cjsExports
},
}
const meta = context[ssrImportMetaKey]
const testFilepath = this.options.getCurrentTestFilepath?.()
if (testFilepath === module.file) {
const globalNamespace = this.vm?.context || globalThis
Object.defineProperty(meta, 'vitest', {
// @ts-expect-error injected untyped global
get: () => globalNamespace.__vitest_index__,
})
}
const filename = meta.filename
const dirname = meta.dirname
const require = this.createRequire(filename)
const argumentsList = [
ssrModuleExportsKey,
ssrImportMetaKey,
ssrImportKey,
ssrDynamicImportKey,
ssrExportAllKey,
// vite 7 support
'__vite_ssr_exportName__',
// TODO@discuss deprecate in Vitest 5, remove in Vitest 6(?)
// backwards compat for vite-node
'__filename',
'__dirname',
'module',
'exports',
'require',
]
if (this.compiledFunctionArgumentsNames) {
argumentsList.push(...this.compiledFunctionArgumentsNames)
}
// add 'use strict' since ESM enables it by default
const codeDefinition = `'use strict';async (${argumentsList.join(
',',
)})=>{{`
const wrappedCode = `${codeDefinition}${code}\n}}`
const options = {
// we are using a normalized file name by default because this is what
// Vite expects in the source maps handler
filename: module.file || filename,
lineOffset: 0,
columnOffset: -codeDefinition.length,
}
const finishModuleExecutionInfo = this.debug.startCalculateModuleExecutionInfo(filename, codeDefinition.length)
try {
const initModule = this.vm
? vm.runInContext(wrappedCode, this.vm.context, options)
: vm.runInThisContext(wrappedCode, options)
const dynamicRequest = async (dep: string, options: ImportCallOptions) => {
dep = String(dep)
// TODO: support more edge cases?
// vite doesn't support dynamic modules by design, but we have to
if (dep[0] === '#') {
return context[ssrDynamicImportKey](wrapId(dep), options)
}
return context[ssrDynamicImportKey](dep, options)
}
await initModule(
context[ssrModuleExportsKey],
context[ssrImportMetaKey],
context[ssrImportKey],
dynamicRequest,
context[ssrExportAllKey],
// vite 7 support, remove when vite 7+ is supported
(context as any).__vite_ssr_exportName__
|| ((name: string, getter: () => unknown) => Object.defineProperty(exportsObject, name, {
enumerable: true,
configurable: true,
get: getter,
})),
filename,
dirname,
moduleProxy,
cjsExports,
require,
...this.compiledFunctionArgumentsValues,
)
}
finally {
// moduleExecutionInfo needs to use Node filename instead of the normalized one
// because we rely on this behaviour in coverage-v8, for example
this.options.moduleExecutionInfo?.set(filename, finishModuleExecutionInfo())
}
}
private createRequire(filename: string) {
// \x00 is a rollup convention for virtual files,
// it is not allowed in actual file names
if (filename.startsWith('\x00') || !isAbsolute(filename)) {
return () => ({})
}
return this.vm
? this.vm.externalModulesExecutor.createRequire(filename)
: createRequire(filename)
}
private shouldInterop(path: string, mod: any): boolean {
if (this.options.interopDefault === false) {
return false
}
// never interop ESM modules
// TODO: should also skip for `.js` with `type="module"`
return !path.endsWith('.mjs') && 'default' in mod
}
}
export function createImportMetaEnvProxy(): ModuleRunnerImportMeta['env'] {
// packages/vitest/src/node/plugins/index.ts:146
const booleanKeys = ['DEV', 'PROD', 'SSR']
return new Proxy(process.env, {
get(_, key) {
if (typeof key !== 'string') {
return undefined
}
if (booleanKeys.includes(key)) {
return !!process.env[key]
}
return process.env[key]
},
set(_, key, value) {
if (typeof key !== 'string') {
return true
}
if (booleanKeys.includes(key)) {
process.env[key] = value ? '1' : ''
}
else {
process.env[key] = value
}
return true
},
}) as ModuleRunnerImportMeta['env']
}
function updateStyle(id: string, css: string) {
if (typeof document === 'undefined') {
return
}
const element = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (element) {
element.textContent = css
return
}
const head = document.querySelector('head')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.setAttribute('data-vite-dev-id', id)
style.textContent = css
head?.appendChild(style)
}
function removeStyle(id: string) {
if (typeof document === 'undefined') {
return
}
const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`)
if (sheet) {
document.head.removeChild(sheet)
}
}
const defaultClientStub = {
injectQuery: (id: string) => id,
createHotContext: () => {
return {
accept: () => {},
prune: () => {},
dispose: () => {},
decline: () => {},
invalidate: () => {},
on: () => {},
send: () => {},
}
},
updateStyle: () => {},
removeStyle: () => {},
}
export function getDefaultRequestStubs(context?: vm.Context): Record<string, any> {
if (!context) {
const clientStub = {
...defaultClientStub,
updateStyle,
removeStyle,
}
return {
'/@vite/client': clientStub,
}
}
const clientStub = vm.runInContext(
`(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`,
context,
)(defaultClientStub)
return {
'/@vite/client': clientStub,
}
}
function exportAll(exports: any, sourceModule: any) {
// #1120 when a module exports itself it causes
// call stack error
if (exports === sourceModule) {
return
}
if (
isPrimitive(sourceModule)
|| Array.isArray(sourceModule)
|| sourceModule instanceof Promise
) {
return
}
for (const key in sourceModule) {
if (key !== 'default' && !(key in exports)) {
try {
defineExport(exports, key, () => sourceModule[key])
}
catch {}
}
}
}
// keep consistency with Vite on how exports are defined
function defineExport(exports: any, key: string | symbol, value: () => any) {
Object.defineProperty(exports, key, {
enumerable: true,
configurable: true,
get: value,
})
}
export function isPrimitive(v: any): boolean {
const isObject = typeof v === 'object' || typeof v === 'function'
return !isObject || v == null
}
function interopModule(mod: any) {
if (isPrimitive(mod)) {
return {
mod: { default: mod },
defaultExport: mod,
}
}
let defaultExport = 'default' in mod ? mod.default : mod
if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) {
mod = defaultExport
if ('default' in defaultExport) {
defaultExport = defaultExport.default
}
}
return { mod, defaultExport }
}
const VALID_ID_PREFIX = `/@id/`
const NULL_BYTE_PLACEHOLDER = `__x00__`
export function wrapId(id: string): string {
return id.startsWith(VALID_ID_PREFIX)
? id
: VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
}
export function unwrapId(id: string): string {
return id.startsWith(VALID_ID_PREFIX)
? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
: id
}

View File

@ -1,12 +1,13 @@
import type { ManualMockedModule, MockedModule, MockedModuleType } from '@vitest/mocker'
import type { MockFactory, MockOptions, PendingSuiteMock } from '../types/mocker'
import type { VitestExecutor } from './execute'
import type { EvaluatedModuleNode } from 'vite/module-runner'
import type { MockFactory, MockOptions, PendingSuiteMock } from '../../types/mocker'
import type { VitestModuleRunner } from './moduleRunner'
import { isAbsolute, resolve } from 'node:path'
import vm from 'node:vm'
import { AutomockedModule, MockerRegistry, mockObject, RedirectedModule } from '@vitest/mocker'
import { findMockRedirect } from '@vitest/mocker/redirect'
import { highlight } from '@vitest/utils'
import { distDir } from '../paths'
import { distDir } from '../../paths'
const spyModulePath = resolve(distDir, 'spy.js')
@ -17,6 +18,19 @@ interface MockContext {
callstack: null | string[]
}
export interface VitestMockerOptions {
context?: vm.Context
root: string
moduleDirectories: string[]
resolveId: (id: string, importer?: string) => Promise<{
id: string
file: string
url: string
} | null>
getCurrentTestFilepath: () => string | undefined
}
export class VitestMocker {
static pendingIds: PendingSuiteMock[] = []
private spyModule?: typeof import('@vitest/spy')
@ -38,8 +52,8 @@ export class VitestMocker {
callstack: null,
}
constructor(public executor: VitestExecutor) {
const context = this.executor.options.context
constructor(public moduleRunner: VitestModuleRunner, private options: VitestMockerOptions) {
const context = this.options.context
if (context) {
this.primitives = vm.runInContext(
'({ Object, Error, Function, RegExp, Symbol, Array, Map })',
@ -79,19 +93,23 @@ export class VitestMocker {
}
private get root() {
return this.executor.options.root
return this.options.root
}
private get moduleCache() {
return this.executor.moduleCache
private get evaluatedModules() {
return this.moduleRunner.evaluatedModules
}
private get moduleDirectories() {
return this.executor.options.moduleDirectories || []
return this.options.moduleDirectories || []
}
public async initializeSpyModule(): Promise<void> {
this.spyModule = await this.executor.executeId(spyModulePath)
if (this.spyModule) {
return
}
this.spyModule = await this.moduleRunner.import(spyModulePath)
}
private getMockerRegistry() {
@ -106,10 +124,12 @@ export class VitestMocker {
this.registries.clear()
}
private deleteCachedItem(id: string) {
private invalidateModuleById(id: string) {
const mockId = this.getMockPath(id)
if (this.moduleCache.has(mockId)) {
this.moduleCache.delete(mockId)
const node = this.evaluatedModules.getModuleById(mockId)
if (node) {
this.evaluatedModules.invalidateModule(node)
node.mockedExports = undefined
}
}
@ -118,7 +138,7 @@ export class VitestMocker {
}
public getSuiteFilepath(): string {
return this.executor.state.filepath || 'global'
return this.options.getCurrentTestFilepath() || 'global'
}
private createError(message: string, codeFrame?: string) {
@ -128,33 +148,28 @@ export class VitestMocker {
return error
}
private async resolvePath(rawId: string, importer: string) {
let id: string
let fsPath: string
try {
[id, fsPath] = await this.executor.originalResolveUrl(rawId, importer)
}
catch (error: any) {
// it's allowed to mock unresolved modules
if (error.code === 'ERR_MODULE_NOT_FOUND') {
const { id: unresolvedId }
= error[Symbol.for('vitest.error.not_found.data')]
id = unresolvedId
fsPath = unresolvedId
}
else {
throw error
public async resolveId(rawId: string, importer?: string): Promise<{
id: string
url: string
external: string | null
}> {
const result = await this.options.resolveId(rawId, importer)
if (!result) {
const id = normalizeModuleId(rawId)
return {
id,
url: rawId,
external: id,
}
}
// external is node_module or unresolved module
// for example, some people mock "vscode" and don't have it installed
const external
= !isAbsolute(fsPath) || this.isModuleDirectory(fsPath) ? rawId : null
= !isAbsolute(result.file) || this.isModuleDirectory(result.file) ? normalizeModuleId(rawId) : null
return {
id,
fsPath,
external: external ? this.normalizePath(external) : external,
...result,
id: normalizeModuleId(result.id),
external,
}
}
@ -165,17 +180,18 @@ export class VitestMocker {
await Promise.all(
VitestMocker.pendingIds.map(async (mock) => {
const { fsPath, external } = await this.resolvePath(
const { id, url, external } = await this.resolveId(
mock.id,
mock.importer,
)
if (mock.action === 'unmock') {
this.unmockPath(fsPath)
this.unmockPath(id)
}
if (mock.action === 'mock') {
this.mockPath(
mock.id,
fsPath,
id,
url,
external,
mock.type,
mock.factory,
@ -187,10 +203,17 @@ export class VitestMocker {
VitestMocker.pendingIds = []
}
private async callFunctionMock(dep: string, mock: ManualMockedModule) {
const cached = this.moduleCache.get(dep)?.exports
if (cached) {
return cached
private ensureModule(id: string, url: string) {
const node = this.evaluatedModules.ensureModule(id, url)
// TODO
node.meta = { id, url, code: '', file: null, invalidate: false }
return node
}
private async callFunctionMock(id: string, url: string, mock: ManualMockedModule) {
const node = this.ensureModule(id, url)
if (node.exports) {
return node.exports
}
const exports = await mock.resolve()
@ -226,7 +249,7 @@ export class VitestMocker {
},
})
this.moduleCache.set(dep, { exports: moduleExports })
node.exports = moduleExports
return moduleExports
}
@ -243,14 +266,10 @@ export class VitestMocker {
public getDependencyMock(id: string): MockedModule | undefined {
const registry = this.getMockerRegistry()
return registry.get(id)
return registry.getById(fixLeadingSlashes(id))
}
public normalizePath(path: string): string {
return this.moduleCache.normalizePath(path)
}
public resolveMockPath(mockPath: string, external: string | null): string | null {
public findMockRedirect(mockPath: string, external: string | null): string | null {
return findMockRedirect(this.root, mockPath, external)
}
@ -265,49 +284,52 @@ export class VitestMocker {
'[vitest] `spyModule` is not defined. This is a Vitest error. Please open a new issue with reproduction.',
)
}
return mockObject({
globalConstructors: this.primitives,
spyOn,
type: behavior,
}, object, mockExports)
return mockObject(
{
globalConstructors: this.primitives,
spyOn,
type: behavior,
},
object,
mockExports,
)
}
public unmockPath(path: string): void {
public unmockPath(id: string): void {
const registry = this.getMockerRegistry()
const id = this.normalizePath(path)
registry.delete(id)
this.deleteCachedItem(id)
registry.deleteById(id)
this.invalidateModuleById(id)
}
public mockPath(
originalId: string,
path: string,
id: string,
url: string,
external: string | null,
mockType: MockedModuleType | undefined,
factory: MockFactory | undefined,
): void {
const registry = this.getMockerRegistry()
const id = this.normalizePath(path)
if (mockType === 'manual') {
registry.register('manual', originalId, id, id, factory!)
registry.register('manual', originalId, id, url, factory!)
}
else if (mockType === 'autospy') {
registry.register('autospy', originalId, id, id)
registry.register('autospy', originalId, id, url)
}
else {
const redirect = this.resolveMockPath(id, external)
const redirect = this.findMockRedirect(id, external)
if (redirect) {
registry.register('redirect', originalId, id, id, redirect)
registry.register('redirect', originalId, id, url, redirect)
}
else {
registry.register('automock', originalId, id, id)
registry.register('automock', originalId, id, url)
}
}
// every time the mock is registered, we remove the previous one from the cache
this.deleteCachedItem(id)
this.invalidateModuleById(id)
}
public async importActual<T>(
@ -315,80 +337,106 @@ export class VitestMocker {
importer: string,
callstack?: string[] | null,
): Promise<T> {
const { id, fsPath } = await this.resolvePath(rawId, importer)
const result = await this.executor.cachedRequest(
id,
fsPath,
const { url } = await this.resolveId(rawId, importer)
const node = await this.moduleRunner.fetchModule(url, importer)
const result = await this.moduleRunner.cachedRequest(
node.url,
node,
callstack || [importer],
undefined,
true,
)
return result as T
}
public async importMock(rawId: string, importee: string): Promise<any> {
const { id, fsPath, external } = await this.resolvePath(rawId, importee)
public async importMock(rawId: string, importer: string): Promise<any> {
const { id, url, external } = await this.resolveId(rawId, importer)
const normalizedId = this.normalizePath(fsPath)
let mock = this.getDependencyMock(normalizedId)
let mock = this.getDependencyMock(id)
if (!mock) {
const redirect = this.resolveMockPath(normalizedId, external)
const redirect = this.findMockRedirect(id, external)
if (redirect) {
mock = new RedirectedModule(rawId, normalizedId, normalizedId, redirect)
mock = new RedirectedModule(rawId, id, rawId, redirect)
}
else {
mock = new AutomockedModule(rawId, normalizedId, normalizedId)
mock = new AutomockedModule(rawId, id, rawId)
}
}
if (mock.type === 'automock' || mock.type === 'autospy') {
const mod = await this.executor.cachedRequest(id, fsPath, [importee])
return this.mockObject(mod, {}, mock.type)
const node = await this.moduleRunner.fetchModule(url, importer)
const mod = await this.moduleRunner.cachedRequest(url, node, [importer], undefined, true)
const Object = this.primitives.Object
return this.mockObject(mod, Object.create(Object.prototype), mock.type)
}
if (mock.type === 'manual') {
return this.callFunctionMock(fsPath, mock)
return this.callFunctionMock(id, url, mock)
}
return this.executor.dependencyRequest(mock.redirect, mock.redirect, [importee])
const node = await this.moduleRunner.fetchModule(mock.redirect)
return this.moduleRunner.cachedRequest(
mock.redirect,
node,
[importer],
undefined,
true,
)
}
public async requestWithMock(url: string, callstack: string[]): Promise<any> {
const id = this.normalizePath(url)
const mock = this.getDependencyMock(id)
if (!mock) {
return
}
const mockPath = this.getMockPath(id)
public async requestWithMockedModule(
url: string,
evaluatedNode: EvaluatedModuleNode,
callstack: string[],
mock: MockedModule,
): Promise<any> {
const mockId = this.getMockPath(evaluatedNode.id)
if (mock.type === 'automock' || mock.type === 'autospy') {
const cache = this.moduleCache.get(mockPath)
if (cache.exports) {
return cache.exports
const cache = this.evaluatedModules.getModuleById(mockId)
if (cache && cache.mockedExports) {
return cache.mockedExports
}
const exports = {}
// Assign the empty exports object early to allow for cycles to work. The object will be filled by mockObject()
this.moduleCache.set(mockPath, { exports })
const mod = await this.executor.directRequest(url, url, callstack)
const Object = this.primitives.Object
// we have to define a separate object that will copy all properties into itself
// and can't just use the same `exports` define automatically by Vite before the evaluator
const exports = Object.create(null)
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module',
configurable: true,
writable: true,
})
const node = this.ensureModule(mockId, this.getMockPath(evaluatedNode.url))
node.meta = evaluatedNode.meta
node.file = evaluatedNode.file
node.mockedExports = exports
const mod = await this.moduleRunner.cachedRequest(
url,
node,
callstack,
undefined,
true,
)
this.mockObject(mod, exports, mock.type)
return exports
}
if (
mock.type === 'manual'
&& !callstack.includes(mockPath)
&& !callstack.includes(mockId)
&& !callstack.includes(url)
) {
try {
callstack.push(mockPath)
callstack.push(mockId)
// this will not work if user does Promise.all(import(), import())
// we can also use AsyncLocalStorage to store callstack, but this won't work in the browser
// maybe we should improve mock API in the future?
this.mockContext.callstack = callstack
return await this.callFunctionMock(mockPath, mock)
return await this.callFunctionMock(mockId, this.getMockPath(url), mock)
}
finally {
this.mockContext.callstack = null
const indexMock = callstack.indexOf(mockPath)
const indexMock = callstack.indexOf(mockId)
callstack.splice(indexMock, 1)
}
}
@ -397,6 +445,16 @@ export class VitestMocker {
}
}
public async mockedRequest(url: string, evaluatedNode: EvaluatedModuleNode, callstack: string[]): Promise<any> {
const mock = this.getDependencyMock(evaluatedNode.id)
if (!mock) {
return
}
return this.requestWithMockedModule(url, evaluatedNode, callstack, mock)
}
public queueMock(
id: string,
importer: string,
@ -421,6 +479,12 @@ export class VitestMocker {
}
}
declare module 'vite/module-runner' {
interface EvaluatedModuleNode {
mockedExports?: Record<string, any>
}
}
function getMockType(factoryOrOptions?: MockFactory | MockOptions): MockedModuleType {
if (!factoryOrOptions) {
return 'automock'
@ -430,3 +494,51 @@ function getMockType(factoryOrOptions?: MockFactory | MockOptions): MockedModule
}
return factoryOrOptions.spy ? 'autospy' : 'automock'
}
// unique id that is not available as "$bare_import" like "test"
// https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix
const prefixedBuiltins = new Set([
'node:sea',
'node:sqlite',
'node:test',
'node:test/reporters',
])
const isWindows = process.platform === 'win32'
// transform file url to id
// virtual:custom -> virtual:custom
// \0custom -> \0custom
// /root/id -> /id
// /root/id.js -> /id.js
// C:/root/id.js -> /id.js
// C:\root\id.js -> /id.js
// TODO: expose this in vite/module-runner
function normalizeModuleId(file: string): string {
if (prefixedBuiltins.has(file)) {
return file
}
// unix style, but Windows path still starts with the drive letter to check the root
const unixFile = slash(file)
.replace(/^\/@fs\//, isWindows ? '' : '/')
.replace(/^node:/, '')
.replace(/^\/+/, '/')
// if it's not in the root, keep it as a path, not a URL
return unixFile.replace(/^file:\//, '/')
}
const windowsSlashRE = /\\/g
function slash(p: string): string {
return p.replace(windowsSlashRE, '/')
}
const multipleSlashRe = /^\/+/
// module-runner incorrectly replaces file:///path with `///path`
function fixLeadingSlashes(id: string): string {
if (id.startsWith('//')) {
return id.replace(multipleSlashRe, '/')
}
return id
}

View File

@ -0,0 +1,151 @@
import type { MockedModule } from '@vitest/mocker'
import type vm from 'node:vm'
import type { EvaluatedModuleNode, EvaluatedModules, SSRImportMetadata } from 'vite/module-runner'
import type { WorkerGlobalState } from '../../types/worker'
import type { ExternalModulesExecutor } from '../external-executor'
import type { ModuleExecutionInfo } from './moduleDebug'
import type { VitestModuleEvaluator } from './moduleEvaluator'
import type { VitestTransportOptions } from './moduleTransport'
import { ModuleRunner } from 'vite/module-runner'
import { VitestMocker } from './moduleMocker'
import { VitestTransport } from './moduleTransport'
// @ts-expect-error overriding private method
export class VitestModuleRunner extends ModuleRunner {
public mocker: VitestMocker
public moduleExecutionInfo: ModuleExecutionInfo
constructor(private options: VitestModuleRunnerOptions) {
const transport = new VitestTransport(options.transport)
const evaluatedModules = options.evaluatedModules
super(
{
transport,
hmr: false,
evaluatedModules,
sourcemapInterceptor: 'prepareStackTrace',
},
options.evaluator,
)
this.moduleExecutionInfo = options.getWorkerState().moduleExecutionInfo
this.mocker = options.mocker || new VitestMocker(this, {
context: options.vm?.context,
resolveId: options.transport.resolveId,
get root() {
return options.getWorkerState().config.root
},
get moduleDirectories() {
return options.getWorkerState().config.deps.moduleDirectories || []
},
getCurrentTestFilepath() {
return options.getWorkerState().filepath
},
})
if (options.vm) {
options.vm.context.__vitest_mocker__ = this.mocker
}
else {
Object.defineProperty(globalThis, '__vitest_mocker__', {
configurable: true,
writable: true,
value: this.mocker,
})
}
}
public async import(rawId: string): Promise<any> {
const resolved = await this.options.transport.resolveId(rawId)
if (!resolved) {
return super.import(rawId)
}
return super.import(resolved.url)
}
public async fetchModule(url: string, importer?: string): Promise<EvaluatedModuleNode> {
const module = await (this as any).cachedModule(url, importer)
return module
}
private _cachedRequest(
url: string,
module: EvaluatedModuleNode,
callstack: string[] = [],
metadata?: SSRImportMetadata,
) {
// @ts-expect-error "cachedRequest" is private
return super.cachedRequest(url, module, callstack, metadata)
}
/**
* @internal
*/
public async cachedRequest(
url: string,
mod: EvaluatedModuleNode,
callstack: string[] = [],
metadata?: SSRImportMetadata,
ignoreMock = false,
): Promise<any> {
if (ignoreMock) {
return this._cachedRequest(url, mod, callstack, metadata)
}
let mocked: any
if (mod.meta && 'mockedModule' in mod.meta) {
mocked = await this.mocker.requestWithMockedModule(
url,
mod,
callstack,
mod.meta.mockedModule as MockedModule,
)
}
else {
mocked = await this.mocker.mockedRequest(url, mod, callstack)
}
if (typeof mocked === 'string') {
const node = await this.fetchModule(mocked)
return this._cachedRequest(mocked, node, callstack, metadata)
}
if (mocked != null && typeof mocked === 'object') {
return mocked
}
return this._cachedRequest(url, mod, callstack, metadata)
}
/** @internal */
public _invalidateSubTreeById(ids: string[], invalidated = new Set<string>()): void {
for (const id of ids) {
if (invalidated.has(id)) {
continue
}
const node = this.evaluatedModules.getModuleById(id)
if (!node) {
continue
}
invalidated.add(id)
const subIds = Array.from(this.evaluatedModules.idToModuleMap)
.filter(([, mod]) => mod.importers.has(id))
.map(([key]) => key)
if (subIds.length) {
this._invalidateSubTreeById(subIds, invalidated)
}
this.evaluatedModules.invalidateModule(node)
}
}
}
export interface VitestModuleRunnerOptions {
transport: VitestTransportOptions
evaluator: VitestModuleEvaluator
evaluatedModules: EvaluatedModules
getWorkerState: () => WorkerGlobalState
mocker?: VitestMocker
vm?: VitestVmOptions
}
export interface VitestVmOptions {
context: vm.Context
externalModulesExecutor: ExternalModulesExecutor
}

View File

@ -0,0 +1,31 @@
import type { FetchFunction, ModuleRunnerTransport } from 'vite/module-runner'
import type { ResolveFunctionResult } from '../../types/general'
export interface VitestTransportOptions {
fetchModule: FetchFunction
resolveId: (id: string, importer?: string) => Promise<ResolveFunctionResult | null>
}
export class VitestTransport implements ModuleRunnerTransport {
constructor(private options: VitestTransportOptions) {}
async invoke(event: any): Promise<{ result: any } | { error: any }> {
if (event.type !== 'custom') {
return { error: new Error(`Vitest Module Runner doesn't support Vite HMR events.`) }
}
if (event.event !== 'vite:invoke') {
return { error: new Error(`Vitest Module Runner doesn't support ${event.event} event.`) }
}
const { name, data } = event.data
if (name !== 'fetchModule') {
return { error: new Error(`Unknown method: ${name}. Expected "fetchModule".`) }
}
try {
const result = await this.options.fetchModule(...data as Parameters<FetchFunction>)
return { result }
}
catch (error) {
return { error }
}
}
}

View File

@ -0,0 +1,179 @@
import type vm from 'node:vm'
import type { EvaluatedModules } from 'vite/module-runner'
import type { WorkerGlobalState } from '../../types/worker'
import type { ExternalModulesExecutor } from '../external-executor'
import fs from 'node:fs'
import { isBuiltin } from 'node:module'
import { isBareImport } from '@vitest/utils'
import { getCachedVitestImport } from './cachedResolver'
import { listenForErrors } from './errorCatcher'
import { unwrapId, VitestModuleEvaluator } from './moduleEvaluator'
import { VitestMocker } from './moduleMocker'
import { VitestModuleRunner } from './moduleRunner'
const { readFileSync } = fs
const browserExternalId = '__vite-browser-external'
const browserExternalLength = browserExternalId.length + 1 // 1 is ":"
export const VITEST_VM_CONTEXT_SYMBOL: string = '__vitest_vm_context__'
export interface ContextModuleRunnerOptions {
evaluatedModules: EvaluatedModules
mocker?: VitestMocker
evaluator?: VitestModuleEvaluator
context?: vm.Context
externalModulesExecutor?: ExternalModulesExecutor
state: WorkerGlobalState
}
const cwd = process.cwd()
const isWindows = process.platform === 'win32'
export async function startVitestModuleRunner(options: ContextModuleRunnerOptions): Promise<VitestModuleRunner> {
const state = (): WorkerGlobalState =>
// @ts-expect-error injected untyped global
globalThis.__vitest_worker__ || options.state
const rpc = () => state().rpc
process.exit = (code = process.exitCode || 0): never => {
throw new Error(`process.exit unexpectedly called with "${code}"`)
}
listenForErrors(state)
const environment = () => {
const environment = state().environment
return environment.viteEnvironment || environment.name
}
const vm = options.context && options.externalModulesExecutor
? {
context: options.context,
externalModulesExecutor: options.externalModulesExecutor,
}
: undefined
const evaluator = options.evaluator || new VitestModuleEvaluator(
vm,
{
get moduleExecutionInfo() {
return state().moduleExecutionInfo
},
get interopDefault() {
return state().config.deps.interopDefault
},
getCurrentTestFilepath: () => state().filepath,
},
)
const moduleRunner: VitestModuleRunner = new VitestModuleRunner({
evaluatedModules: options.evaluatedModules,
evaluator,
mocker: options.mocker,
transport: {
async fetchModule(id, importer, options) {
const resolvingModules = state().resolvingModules
if (isWindows) {
if (id[1] === ':') {
// The drive letter is different for whatever reason, we need to normalize it to CWD
if (id[0] !== cwd[0] && id[0].toUpperCase() === cwd[0].toUpperCase()) {
const isUpperCase = cwd[0].toUpperCase() === cwd[0]
id = (isUpperCase ? id[0].toUpperCase() : id[0].toLowerCase()) + id.slice(1)
}
// always mark absolute windows paths, otherwise Vite will externalize it
id = `/@id/${id}`
}
}
const vitest = getCachedVitestImport(id, state)
if (vitest) {
return vitest
}
const rawId = unwrapId(id)
resolvingModules.add(rawId)
try {
if (VitestMocker.pendingIds.length) {
await moduleRunner.mocker.resolveMocks()
}
const resolvedMock = moduleRunner.mocker.getDependencyMock(rawId)
if (resolvedMock?.type === 'manual' || resolvedMock?.type === 'redirect') {
return {
code: '',
file: null,
id,
url: id,
invalidate: false,
mockedModule: resolvedMock,
}
}
if (isBuiltin(rawId) || rawId.startsWith(browserExternalId)) {
return { externalize: toBuiltin(rawId), type: 'builtin' }
}
const result = await rpc().fetch(
id,
importer,
environment(),
options,
)
if ('cached' in result) {
const code = readFileSync(result.tmp, 'utf-8')
return { code, ...result }
}
return result
}
catch (cause: any) {
// rethrow vite error if it cannot load the module because it's not resolved
if (
(typeof cause === 'object' && cause != null && cause.code === 'ERR_LOAD_URL')
|| (typeof cause?.message === 'string' && cause.message.includes('Failed to load url'))
|| (typeof cause?.message === 'string' && cause.message.startsWith('Cannot find module \''))
) {
const error = new Error(
`Cannot find ${isBareImport(id) ? 'package' : 'module'} '${id}'${importer ? ` imported from '${importer}'` : ''}`,
{ cause },
) as Error & { code: string }
error.code = 'ERR_MODULE_NOT_FOUND'
throw error
}
throw cause
}
finally {
resolvingModules.delete(rawId)
}
},
resolveId(id, importer) {
return rpc().resolve(
id,
importer,
environment(),
)
},
},
getWorkerState: state,
vm,
})
await moduleRunner.import('/@vite/env')
await moduleRunner.mocker.initializeSpyModule()
return moduleRunner
}
export function toBuiltin(id: string): string {
if (id.startsWith(browserExternalId)) {
id = id.slice(browserExternalLength)
}
if (!id.startsWith('node:')) {
id = `node:${id}`
}
return id
}

View File

@ -1,8 +1,7 @@
import type { FileSpecification } from '@vitest/runner'
import type { ModuleCacheMap } from 'vite-node'
import type { ResolvedTestEnvironment } from '../types/environment'
import type { SerializedConfig } from './config'
import type { VitestExecutor } from './execute'
import type { VitestModuleRunner } from './moduleRunner/moduleRunner'
import { performance } from 'node:perf_hooks'
import { collectTests, startTests } from '@vitest/runner'
import { setupChaiConfig } from '../integrations/chai/config'
@ -22,7 +21,7 @@ export async function run(
files: FileSpecification[],
config: SerializedConfig,
environment: ResolvedTestEnvironment,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<void> {
const workerState = getWorkerState()
@ -30,14 +29,14 @@ export async function run(
const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true)
const isolate = isIsolatedThreads || isIsolatedForks
await setupGlobalEnv(config, environment, executor)
await startCoverageInsideWorker(config.coverage, executor, { isolate })
await setupGlobalEnv(config, environment, moduleRunner)
await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate })
if (config.chaiConfig) {
setupChaiConfig(config.chaiConfig)
}
const runner = await resolveTestRunner(config, executor)
const runner = await resolveTestRunner(config, moduleRunner)
workerState.onCancel.then((reason) => {
closeInspector(config)
@ -56,8 +55,8 @@ export async function run(
for (const file of files) {
if (isolate) {
executor.mocker.reset()
resetModules(workerState.moduleCache as ModuleCacheMap, true)
moduleRunner.mocker.reset()
resetModules(workerState.evaluatedModules, true)
}
workerState.filepath = file.filepath
@ -75,7 +74,7 @@ export async function run(
vi.restoreAllMocks()
}
await stopCoverageInsideWorker(config.coverage, executor, { isolate })
await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate })
},
)

View File

@ -1,15 +1,13 @@
import type { FileSpecification } from '@vitest/runner'
import type { ModuleCacheMap } from 'vite-node'
import type { SerializedConfig } from './config'
import type { VitestExecutor } from './execute'
import type { VitestModuleRunner } from './moduleRunner/moduleRunner'
import { createRequire } from 'node:module'
import { performance } from 'node:perf_hooks'
import timers from 'node:timers'
import timersPromises from 'node:timers/promises'
import util from 'node:util'
import { collectTests, startTests } from '@vitest/runner'
import { KNOWN_ASSET_TYPES } from 'vite-node/constants'
import { installSourcemapsSupport } from 'vite-node/source-map'
import { KNOWN_ASSET_TYPES } from '@vitest/utils'
import { setupChaiConfig } from '../integrations/chai/config'
import {
startCoverageInsideWorker,
@ -26,7 +24,7 @@ export async function run(
method: 'run' | 'collect',
files: FileSpecification[],
config: SerializedConfig,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<void> {
const workerState = getWorkerState()
@ -37,7 +35,8 @@ export async function run(
enumerable: false,
})
if (workerState.environment.transformMode === 'web') {
const viteEnvironment = workerState.environment.viteEnvironment || workerState.environment.name
if (viteEnvironment === 'client') {
const _require = createRequire(import.meta.url)
// always mock "required" `css` files, because we cannot process them
_require.extensions['.css'] = resolveCss
@ -61,19 +60,15 @@ export async function run(
timersPromises,
}
installSourcemapsSupport({
getSourceMap: source => (workerState.moduleCache as ModuleCacheMap).getSourceMap(source),
})
await startCoverageInsideWorker(config.coverage, executor, { isolate: false })
await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: false })
if (config.chaiConfig) {
setupChaiConfig(config.chaiConfig)
}
const [runner, snapshotEnvironment] = await Promise.all([
resolveTestRunner(config, executor),
resolveSnapshotEnvironment(config, executor),
resolveTestRunner(config, moduleRunner),
resolveSnapshotEnvironment(config, moduleRunner),
])
config.snapshotOptions.snapshotEnvironment = snapshotEnvironment
@ -106,7 +101,7 @@ export async function run(
vi.restoreAllMocks()
}
await stopCoverageInsideWorker(config.coverage, executor, { isolate: false })
await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: false })
}
function resolveCss(mod: NodeJS.Module) {

View File

@ -5,6 +5,7 @@ import type {
VitestRunner,
VitestRunnerImportSource,
} from '@vitest/runner'
import type { ModuleRunner } from 'vite/module-runner'
import type { SerializedConfig } from '../config'
// import type { VitestExecutor } from '../execute'
import type {
@ -150,7 +151,7 @@ async function runBenchmarkSuite(suite: Suite, runner: NodeBenchmarkRunner) {
}
export class NodeBenchmarkRunner implements VitestRunner {
private __vitest_executor!: any
private moduleRunner!: ModuleRunner
constructor(public config: SerializedConfig) {}
@ -160,9 +161,12 @@ export class NodeBenchmarkRunner implements VitestRunner {
importFile(filepath: string, source: VitestRunnerImportSource): unknown {
if (source === 'setup') {
getWorkerState().moduleCache.delete(filepath)
const moduleNode = getWorkerState().evaluatedModules.getModuleById(filepath)
if (moduleNode) {
getWorkerState().evaluatedModules.invalidateModule(moduleNode)
}
}
return this.__vitest_executor.executeId(filepath)
return this.moduleRunner.import(filepath)
}
async runSuite(suite: Suite): Promise<void> {

View File

@ -1,7 +1,7 @@
import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner'
import type { SerializedConfig } from '../config'
import type { VitestExecutor } from '../execute'
import { resolve } from 'node:path'
import type { VitestModuleRunner } from '../moduleRunner/moduleRunner'
import { join, resolve } from 'node:path'
import { takeCoverageInsideWorker } from '../../integrations/coverage'
import { distDir } from '../../paths'
import { rpc } from '../rpc'
@ -12,16 +12,17 @@ const runnersFile = resolve(distDir, 'runners.js')
async function getTestRunnerConstructor(
config: SerializedConfig,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<VitestRunnerConstructor> {
if (!config.runner) {
const { VitestTestRunner, NodeBenchmarkRunner }
= await executor.executeFile(runnersFile)
const { VitestTestRunner, NodeBenchmarkRunner } = await moduleRunner.import(
join('/@fs/', runnersFile),
)
return (
config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner
) as VitestRunnerConstructor
}
const mod = await executor.executeId(config.runner)
const mod = await moduleRunner.import(config.runner)
if (!mod.default && typeof mod.default !== 'function') {
throw new Error(
`Runner must export a default function, but got ${typeof mod.default} imported from ${
@ -34,14 +35,14 @@ async function getTestRunnerConstructor(
export async function resolveTestRunner(
config: SerializedConfig,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<VitestRunner> {
const TestRunner = await getTestRunnerConstructor(config, executor)
const TestRunner = await getTestRunnerConstructor(config, moduleRunner)
const testRunner = new TestRunner(config)
// inject private executor to every runner
Object.defineProperty(testRunner, '__vitest_executor', {
value: executor,
Object.defineProperty(testRunner, 'moduleRunner', {
value: moduleRunner,
enumerable: false,
configurable: false,
})
@ -55,8 +56,8 @@ export async function resolveTestRunner(
}
const [diffOptions] = await Promise.all([
loadDiffConfig(config, executor),
loadSnapshotSerializers(config, executor),
loadDiffConfig(config, moduleRunner),
loadSnapshotSerializers(config, moduleRunner),
])
testRunner.config.diffOptions = diffOptions
@ -100,13 +101,13 @@ export async function resolveTestRunner(
const originalOnAfterRun = testRunner.onAfterRunFiles
testRunner.onAfterRunFiles = async (files) => {
const state = getWorkerState()
const coverage = await takeCoverageInsideWorker(config.coverage, executor)
const coverage = await takeCoverageInsideWorker(config.coverage, moduleRunner)
if (coverage) {
rpc().onAfterSuiteRun({
coverage,
testFiles: files.map(file => file.name).sort(),
transformMode: state.environment.transformMode,
environment: state.environment.viteEnvironment || state.environment.name,
projectName: state.ctx.projectName,
})
}

View File

@ -10,6 +10,7 @@ import type {
VitestRunner,
VitestRunnerImportSource,
} from '@vitest/runner'
import type { ModuleRunner } from 'vite/module-runner'
import type { SerializedConfig } from '../config'
// import type { VitestExecutor } from '../execute'
import { getState, GLOBAL_EXPECT, setState } from '@vitest/expect'
@ -29,7 +30,7 @@ const workerContext = Object.create(null)
export class VitestTestRunner implements VitestRunner {
private snapshotClient = getSnapshotClient()
private workerState = getWorkerState()
private __vitest_executor!: any
private moduleRunner!: ModuleRunner
private cancelRun = false
private assertionsErrors = new WeakMap<Readonly<Task>, Error>()
@ -40,9 +41,12 @@ export class VitestTestRunner implements VitestRunner {
importFile(filepath: string, source: VitestRunnerImportSource): unknown {
if (source === 'setup') {
this.workerState.moduleCache.delete(filepath)
const moduleNode = this.workerState.evaluatedModules.getModuleById(filepath)
if (moduleNode) {
this.workerState.evaluatedModules.invalidateModule(moduleNode)
}
}
return this.__vitest_executor.executeId(filepath)
return this.moduleRunner.import(filepath)
}
onCollectStart(file: File): void {

View File

@ -2,9 +2,10 @@ import type { DiffOptions } from '@vitest/expect'
import type { SnapshotSerializer } from '@vitest/snapshot'
import type { SerializedDiffOptions } from '@vitest/utils/diff'
import type { SerializedConfig } from './config'
import type { VitestExecutor } from './execute'
import type { VitestModuleRunner } from './moduleRunner/moduleRunner'
import { addSerializer } from '@vitest/snapshot'
import { setSafeTimers } from '@vitest/utils'
import { getWorkerState } from './utils'
let globalSetup = false
export async function setupCommonEnv(config: SerializedConfig): Promise<void> {
@ -30,21 +31,19 @@ function setupDefines(defines: Record<string, any>) {
}
function setupEnv(env: Record<string, any>) {
if (typeof process === 'undefined') {
return
}
const state = getWorkerState()
// same boolean-to-string assignment as VitestPlugin.configResolved
const { PROD, DEV, ...restEnvs } = env
process.env.PROD = PROD ? '1' : ''
process.env.DEV = DEV ? '1' : ''
state.metaEnv.PROD = PROD
state.metaEnv.DEV = DEV
for (const key in restEnvs) {
process.env[key] = env[key]
state.metaEnv[key] = env[key]
}
}
export async function loadDiffConfig(
config: SerializedConfig,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<SerializedDiffOptions | undefined> {
if (typeof config.diff === 'object') {
return config.diff
@ -53,7 +52,7 @@ export async function loadDiffConfig(
return
}
const diffModule = await executor.executeId(config.diff)
const diffModule = await moduleRunner.import(config.diff)
if (
diffModule
@ -71,13 +70,13 @@ export async function loadDiffConfig(
export async function loadSnapshotSerializers(
config: SerializedConfig,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<void> {
const files = config.snapshotSerializers
const snapshotSerializers = await Promise.all(
files.map(async (file) => {
const mo = await executor.executeId(file)
const mo = await moduleRunner.import(file)
if (!mo || typeof mo.default !== 'object' || mo.default === null) {
throw new Error(
`invalid snapshot serializer file ${file}. Must export a default object`,

View File

@ -1,14 +1,11 @@
import type { ModuleCacheMap } from 'vite-node'
import type { ResolvedTestEnvironment } from '../types/environment'
import type { SerializedConfig } from './config'
import type { VitestExecutor } from './execute'
import type { VitestModuleRunner } from './moduleRunner/moduleRunner'
import { createRequire } from 'node:module'
import timers from 'node:timers'
import timersPromises from 'node:timers/promises'
import util from 'node:util'
import { getSafeTimers } from '@vitest/utils'
import { KNOWN_ASSET_TYPES } from 'vite-node/constants'
import { installSourcemapsSupport } from 'vite-node/source-map'
import { getSafeTimers, KNOWN_ASSET_TYPES } from '@vitest/utils'
import { expect } from '../integrations/chai'
import { resolveSnapshotEnvironment } from '../integrations/snapshot/environments/resolveSnapshotEnvironment'
import * as VitestIndex from '../public/index'
@ -20,7 +17,7 @@ let globalSetup = false
export async function setupGlobalEnv(
config: SerializedConfig,
{ environment }: ResolvedTestEnvironment,
executor: VitestExecutor,
moduleRunner: VitestModuleRunner,
): Promise<void> {
await setupCommonEnv(config)
@ -33,7 +30,7 @@ export async function setupGlobalEnv(
if (!state.config.snapshotOptions.snapshotEnvironment) {
state.config.snapshotOptions.snapshotEnvironment
= await resolveSnapshotEnvironment(config, executor)
= await resolveSnapshotEnvironment(config, moduleRunner)
}
if (globalSetup) {
@ -42,7 +39,8 @@ export async function setupGlobalEnv(
globalSetup = true
if (environment.transformMode === 'web') {
const viteEnvironment = environment.viteEnvironment || environment.name
if (viteEnvironment === 'client') {
const _require = createRequire(import.meta.url)
// always mock "required" `css` files, because we cannot process them
_require.extensions['.css'] = resolveCss
@ -66,10 +64,6 @@ export async function setupGlobalEnv(
timersPromises,
}
installSourcemapsSupport({
getSourceMap: source => (state.moduleCache as ModuleCacheMap).getSourceMap(source),
})
if (!config.disableConsoleIntercept) {
await setupConsoleLogSpy()
}

View File

@ -1,5 +1,4 @@
import type { ModuleCacheMap } from 'vite-node/client'
import type { EvaluatedModules } from 'vite/module-runner'
import type { WorkerGlobalState } from '../types/worker'
import { getSafeTimers } from '@vitest/utils'
@ -48,11 +47,10 @@ export function setProcessTitle(title: string): void {
catch {}
}
export function resetModules(modules: ModuleCacheMap, resetMocks = false): void {
export function resetModules(modules: EvaluatedModules, resetMocks = false): void {
const skipPaths = [
// Vitest
/\/vitest\/dist\//,
/\/vite-node\/dist\//,
// yarn's .store folder
/vitest-virtual-\w+\/dist/,
// cnpm
@ -60,11 +58,15 @@ export function resetModules(modules: ModuleCacheMap, resetMocks = false): void
// don't clear mocks
...(!resetMocks ? [/^mock:/] : []),
]
modules.forEach((mod, path) => {
modules.idToModuleMap.forEach((node, path) => {
if (skipPaths.some(re => re.test(path))) {
return
}
modules.invalidateModule(mod)
node.promise = undefined
node.exports = undefined
node.evaluated = false
node.importers.clear()
})
}
@ -77,14 +79,11 @@ export async function waitForImportsToResolve(): Promise<void> {
await waitNextTick()
const state = getWorkerState()
const promises: Promise<unknown>[] = []
let resolvingCount = 0
for (const mod of state.moduleCache.values()) {
const resolvingCount = state.resolvingModules.size
for (const [_, mod] of state.evaluatedModules.idToModuleMap) {
if (mod.promise && !mod.evaluated) {
promises.push(mod.promise)
}
if (mod.resolving) {
resolvingCount++
}
}
if (!promises.length && !resolvingCount) {
return

View File

@ -1,9 +1,8 @@
import type { FileMap } from './file-map'
import type { ImportModuleDynamically, VMSyntheticModule } from './types'
import { Module as _Module, createRequire } from 'node:module'
import { Module as _Module, createRequire, isBuiltin } from 'node:module'
import vm from 'node:vm'
import { basename, dirname, extname } from 'pathe'
import { isNodeBuiltin } from 'vite-node/utils'
import { interopCommonJsModule, SyntheticModule } from './utils'
interface CommonjsExecutorOptions {
@ -206,7 +205,7 @@ export class CommonjsExecutor {
const require = ((id: string) => {
const resolved = _require.resolve(id)
const ext = extname(resolved)
if (ext === '.node' || isNodeBuiltin(resolved)) {
if (ext === '.node' || isBuiltin(resolved)) {
return this.requireCoreModule(resolved)
}
const module = new this.Module(resolved)
@ -358,7 +357,7 @@ export class CommonjsExecutor {
public require(identifier: string): any {
const ext = extname(identifier)
if (ext === '.node' || isNodeBuiltin(identifier)) {
if (ext === '.node' || isBuiltin(identifier)) {
return this.requireCoreModule(identifier)
}
const module = new this.Module(identifier)

View File

@ -1,6 +1,5 @@
import type { VMSourceTextModule, VMSyntheticModule } from './types'
import vm from 'node:vm'
import { isPrimitive } from 'vite-node/utils'
export function interopCommonJsModule(
interopDefault: boolean | undefined,
@ -45,6 +44,11 @@ export function interopCommonJsModule(
}
}
function isPrimitive(obj: unknown): boolean {
const isObject = obj != null && (typeof obj === 'object' || typeof obj === 'function')
return !isObject
}
export const SyntheticModule: typeof VMSyntheticModule = (vm as any)
.SyntheticModule
export const SourceTextModule: typeof VMSourceTextModule = (vm as any)

View File

@ -4,9 +4,7 @@ import type { WorkerGlobalState } from '../../types/worker'
import type { EsmExecutor } from './esm-executor'
import type { VMModule } from './types'
import { pathToFileURL } from 'node:url'
import { normalize } from 'pathe'
import { CSS_LANGS_RE, KNOWN_ASSET_RE } from 'vite-node/constants'
import { toArray } from 'vite-node/utils'
import { CSS_LANGS_RE, KNOWN_ASSET_RE, toArray } from '@vitest/utils'
import { SyntheticModule } from './utils'
interface ViteExecutorOptions {
@ -26,15 +24,9 @@ export class ViteExecutor {
this.esm = options.esmExecutor
}
public resolve = (identifier: string, parent: string): string | undefined => {
public resolve = (identifier: string): string | undefined => {
if (identifier === CLIENT_ID) {
if (this.workerState.environment.transformMode === 'web') {
return identifier
}
const packageName = this.getPackageName(parent)
throw new Error(
`[vitest] Vitest cannot handle ${CLIENT_ID} imported in ${parent} when running in SSR environment. Add "${packageName}" to "ssr.noExternal" if you are using Vite SSR, or to "server.deps.inline" if you are using Vite Node.`,
)
return identifier
}
}
@ -42,20 +34,8 @@ export class ViteExecutor {
return this.options.context.__vitest_worker__
}
private getPackageName(modulePath: string) {
const path = normalize(modulePath)
let name = path.split('/node_modules/').pop() || ''
if (name?.startsWith('@')) {
name = name.split('/').slice(0, 2).join('/')
}
else {
name = name.split('/')[0]
}
return name
}
public async createViteModule(fileUrl: string): Promise<VMModule> {
if (fileUrl === CLIENT_FILE) {
if (fileUrl === CLIENT_FILE || fileUrl === CLIENT_ID) {
return this.createViteClientModule()
}
const cached = this.esm.resolveCachedModule(fileUrl)
@ -64,7 +44,7 @@ export class ViteExecutor {
}
return this.esm.createEsModule(fileUrl, async () => {
try {
const result = await this.options.transform(fileUrl, 'web')
const result = await this.options.transform(fileUrl)
if (result.code) {
return result.code
}
@ -112,10 +92,6 @@ export class ViteExecutor {
}
public canResolve = (fileUrl: string): boolean => {
const transformMode = this.workerState.environment.transformMode
if (transformMode !== 'web') {
return false
}
if (fileUrl === CLIENT_FILE) {
return true
}

View File

@ -1,9 +1,10 @@
import type { ModuleRunner } from 'vite/module-runner'
import type { ContextRPC, WorkerGlobalState } from '../types/worker'
import type { VitestWorker } from './workers/types'
import { pathToFileURL } from 'node:url'
import { createStackString, parseStacktrace } from '@vitest/utils/source-map'
import { workerId as poolId } from 'tinypool'
import { ModuleCacheMap } from 'vite-node/client'
import { EvaluatedModules } from 'vite/module-runner'
import { loadEnvironment } from '../integrations/env/loader'
import { addCleanupListener, cleanup as cleanupWorker } from './cleanup'
import { setupInspect } from './inspector'
@ -30,6 +31,8 @@ if (isChildProcess()) {
}
}
const resolvingModules = new Set<string>()
// this is what every pool executes when running tests
async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
disposeInternalListeners()
@ -41,6 +44,8 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
process.env.VITEST_WORKER_ID = String(ctx.workerId)
process.env.VITEST_POOL_ID = String(poolId)
let environmentLoader: ModuleRunner | undefined
try {
// worker is a filepath or URL to a file that exposes a default export with "getRpcOptions" and "runTests" methods
if (ctx.worker[0] === '.') {
@ -81,15 +86,14 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
})
const beforeEnvironmentTime = performance.now()
const environment = await loadEnvironment(ctx, rpc)
if (ctx.environment.transformMode) {
environment.transformMode = ctx.environment.transformMode
}
const { environment, loader } = await loadEnvironment(ctx, rpc)
environmentLoader = loader
const state = {
ctx,
// here we create a new one, workers can reassign this if they need to keep it non-isolated
moduleCache: new ModuleCacheMap(),
evaluatedModules: new EvaluatedModules(),
resolvingModules,
moduleExecutionInfo: new Map(),
config: ctx.config,
onCancel,
@ -104,6 +108,7 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
onFilterStackTrace(stack) {
return createStackString(parseStacktrace(stack))
},
metaEnv: createImportMetaEnvProxy(),
} satisfies WorkerGlobalState
const methodName = method === 'collect' ? 'collectTests' : 'runTests'
@ -120,6 +125,7 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
await Promise.all(cleanups.map(fn => fn()))
await rpcDone().catch(() => {})
environmentLoader?.close()
}
}
@ -134,3 +140,33 @@ export function collect(ctx: ContextRPC): Promise<void> {
export async function teardown(): Promise<void> {
return cleanupWorker()
}
function createImportMetaEnvProxy(): WorkerGlobalState['metaEnv'] {
// packages/vitest/src/node/plugins/index.ts:146
const booleanKeys = ['DEV', 'PROD', 'SSR']
return new Proxy(process.env, {
get(_, key) {
if (typeof key !== 'string') {
return undefined
}
if (booleanKeys.includes(key)) {
return !!process.env[key]
}
return process.env[key]
},
set(_, key, value) {
if (typeof key !== 'string') {
return true
}
if (booleanKeys.includes(key)) {
process.env[key] = value ? '1' : ''
}
else {
process.env[key] = value
}
return true
},
}) as WorkerGlobalState['metaEnv']
}

View File

@ -1,43 +1,52 @@
import type { WorkerGlobalState } from '../../types/worker'
import type { ContextExecutorOptions, VitestExecutor } from '../execute'
import { ModuleCacheMap } from 'vite-node/client'
import { getDefaultRequestStubs, startVitestExecutor } from '../execute'
import type { VitestModuleRunner } from '../moduleRunner/moduleRunner'
import type { ContextModuleRunnerOptions } from '../moduleRunner/startModuleRunner'
import { EvaluatedModules } from 'vite/module-runner'
import { startVitestModuleRunner } from '../moduleRunner/startModuleRunner'
import { provideWorkerState } from '../utils'
let _viteNode: VitestExecutor
let _moduleRunner: VitestModuleRunner
const moduleCache = new ModuleCacheMap()
const evaluatedModules = new EvaluatedModules()
const moduleExecutionInfo = new Map()
async function startViteNode(options: ContextExecutorOptions) {
if (_viteNode) {
return _viteNode
async function startModuleRunner(options: ContextModuleRunnerOptions) {
if (_moduleRunner) {
return _moduleRunner
}
_viteNode = await startVitestExecutor(options)
return _viteNode
_moduleRunner = await startVitestModuleRunner(options)
return _moduleRunner
}
export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> {
const { ctx } = state
// state has new context, but we want to reuse existing ones
state.moduleCache = moduleCache
state.evaluatedModules = evaluatedModules
state.moduleExecutionInfo = moduleExecutionInfo
provideWorkerState(globalThis, state)
if (ctx.invalidates) {
ctx.invalidates.forEach((fsPath) => {
moduleCache.delete(fsPath)
moduleCache.delete(`mock:${fsPath}`)
ctx.invalidates.forEach((filepath) => {
const modules = state.evaluatedModules.fileToModulesMap.get(filepath) || []
modules.forEach((module) => {
state.evaluatedModules.invalidateModule(module)
})
// evaluatedModules.delete(fsPath)
// evaluatedModules.delete(`mock:${fsPath}`)
})
}
ctx.files.forEach(i => state.moduleCache.delete(
typeof i === 'string' ? i : i.filepath,
))
ctx.files.forEach((i) => {
const filepath = typeof i === 'string' ? i : i.filepath
const modules = state.evaluatedModules.fileToModulesMap.get(filepath) || []
modules.forEach((module) => {
state.evaluatedModules.invalidateModule(module)
})
})
const [executor, { run }] = await Promise.all([
startViteNode({ state, requestStubs: getDefaultRequestStubs() }),
startModuleRunner({ state, evaluatedModules: state.evaluatedModules }),
import('../runBaseTests'),
])
const fileSpecs = ctx.files.map(f =>

View File

@ -1,13 +1,13 @@
import type { Context } from 'node:vm'
import type { ModuleCacheMap } from 'vite-node'
import type { WorkerGlobalState } from '../../types/worker'
import { pathToFileURL } from 'node:url'
import { isContext } from 'node:vm'
import { resolve } from 'pathe'
import { distDir } from '../../paths'
import { createCustomConsole } from '../console'
import { getDefaultRequestStubs, startVitestExecutor } from '../execute'
import { ExternalModulesExecutor } from '../external-executor'
import { getDefaultRequestStubs } from '../moduleRunner/moduleEvaluator'
import { startVitestModuleRunner, VITEST_VM_CONTEXT_SYMBOL } from '../moduleRunner/startModuleRunner'
import { provideWorkerState } from '../utils'
import { FileMap } from '../vm/file-map'
@ -75,17 +75,25 @@ export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalS
viteClientModule: stubs['/@vite/client'],
})
const executor = await startVitestExecutor({
const moduleRunner = await startVitestModuleRunner({
context,
moduleCache: state.moduleCache as ModuleCacheMap,
evaluatedModules: state.evaluatedModules,
state,
externalModulesExecutor,
requestStubs: stubs,
})
context.__vitest_mocker__ = executor.mocker
Object.defineProperty(context, VITEST_VM_CONTEXT_SYMBOL, {
value: {
context,
externalModulesExecutor,
},
configurable: true,
enumerable: false,
writable: false,
})
context.__vitest_mocker__ = moduleRunner.mocker
const { run } = (await executor.importExternalModule(
const { run } = (await moduleRunner.import(
entryFile,
)) as typeof import('../runVmTests')
const fileSpecs = ctx.files.map(f =>
@ -99,7 +107,7 @@ export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalS
method,
fileSpecs,
ctx.config,
executor,
moduleRunner,
)
}
finally {

View File

@ -1,5 +1,5 @@
import type { File, RunMode, Suite, Test } from '@vitest/runner'
import type { RawSourceMap } from 'vite-node'
import type { Rollup } from 'vite'
import type { TestProject } from '../node/project'
import {
calculateSuiteHash,
@ -39,7 +39,7 @@ export interface FileInformation {
file: File
filepath: string
parsed: string
map: RawSourceMap | null
map: Rollup.SourceMap | null
definitions: LocalCallDefinition[]
}
@ -47,7 +47,7 @@ export async function collectTests(
ctx: TestProject,
filepath: string,
): Promise<null | FileInformation> {
const request = await ctx.vitenode.transformRequest(filepath, filepath)
const request = await ctx.vite.environments.ssr.transformRequest(filepath)
if (!request) {
return null
}
@ -229,7 +229,7 @@ export async function collectTests(
file,
parsed: request.code,
filepath,
map: request.map as RawSourceMap | null,
map: request.map as Rollup.SourceMap | null,
definitions,
}
}

View File

@ -11,7 +11,17 @@ export interface VmEnvironmentReturn {
export interface Environment {
name: string
transformMode: 'web' | 'ssr'
/**
* @deprecated use `viteEnvironment` instead. Uses `name` by default
*/
transformMode?: 'web' | 'ssr'
/**
* Environment initiated by the Vite server. It is usually available
* as `vite.server.environments.${name}`.
*
* By default, fallbacks to `name`.
*/
viteEnvironment?: 'client' | 'ssr' | ({} & string)
setupVM?: (options: Record<string, any>) => Awaitable<VmEnvironmentReturn>
setup: (
global: any,

View File

@ -4,12 +4,10 @@ export type Awaitable<T> = T | PromiseLike<T>
export type Arrayable<T> = T | Array<T>
export type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never
export type TransformMode = 'web' | 'ssr'
export interface AfterSuiteRunMeta {
coverage?: unknown
testFiles: string[]
transformMode: TransformMode | 'browser'
environment: string
projectName?: string
}
@ -31,5 +29,20 @@ export interface ModuleGraphData {
export interface ProvidedContext {}
export interface ResolveFunctionResult {
id: string
file: string
url: string
}
export interface FetchCachedFileSystemResult {
cached: true
tmp: string
id: string
file: string | null
url: string
invalidate: boolean
}
// These need to be compatible with Tinyrainbow's bg-colors, and CSS's background-color
export type LabelColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white'

View File

@ -1,26 +1,12 @@
import type { CancelReason, File, TaskEventPack, TaskResultPack, TestAnnotation } from '@vitest/runner'
import type { SnapshotResult } from '@vitest/snapshot'
import type { AfterSuiteRunMeta, TransformMode, UserConsoleLog } from './general'
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
import type { AfterSuiteRunMeta, FetchCachedFileSystemResult, ResolveFunctionResult, UserConsoleLog } from './general'
export interface RuntimeRPC {
fetch: (
id: string,
transformMode: TransformMode
) => Promise<{
externalize?: string
id?: string
}>
transform: (id: string, transformMode: TransformMode) => Promise<{
code?: string
}>
resolveId: (
id: string,
importer: string | undefined,
transformMode: TransformMode
) => Promise<{
external?: boolean | 'absolute' | 'relative'
id: string
} | null>
fetch: (id: string, importer: string | undefined, environment: string, options?: FetchFunctionOptions) => Promise<FetchResult | FetchCachedFileSystemResult>
resolve: (id: string, importer: string | undefined, environment: string) => Promise<ResolveFunctionResult | null>
transform: (id: string) => Promise<{ code?: string }>
onUserConsoleLog: (log: UserConsoleLog) => void
onUnhandledError: (err: unknown, type: string) => void

View File

@ -1,15 +1,14 @@
import type { CancelReason, FileSpecification, Task } from '@vitest/runner'
import type { BirpcReturn } from 'birpc'
import type { EvaluatedModules } from 'vite/module-runner'
import type { SerializedConfig } from '../runtime/config'
import type { Environment } from './environment'
import type { TransformMode } from './general'
import type { RunnerRPC, RuntimeRPC } from './rpc'
export type WorkerRPC = BirpcReturn<RuntimeRPC, RunnerRPC>
export interface ContextTestEnvironment {
name: string
transformMode?: TransformMode
options: Record<string, any> | null
}
@ -33,10 +32,19 @@ export interface WorkerGlobalState {
rpc: WorkerRPC
current?: Task
filepath?: string
metaEnv: {
[key: string]: any
BASE_URL: string
MODE: string
DEV: boolean
PROD: boolean
SSR: boolean
}
environment: Environment
environmentTeardownRun?: boolean
onCancel: Promise<CancelReason>
moduleCache: Map<string, any>
evaluatedModules: EvaluatedModules
resolvingModules: Set<string>
moduleExecutionInfo: Map<string, any>
onCleanup: (listener: () => unknown) => void
providedContext: Record<string, any>

View File

@ -1,10 +1,9 @@
import type { ModuleExecutionInfo } from 'vite-node/client'
import type { SerializedCoverageConfig } from '../runtime/config'
export interface RuntimeCoverageModuleLoader {
executeId: (id: string) => Promise<{ default: RuntimeCoverageProviderModule }>
import: (id: string) => Promise<{ default: RuntimeCoverageProviderModule }>
isBrowser?: boolean
moduleExecutionInfo?: ModuleExecutionInfo
moduleExecutionInfo?: Map<string, { startOffset: number }>
}
export interface RuntimeCoverageProviderModule {
@ -21,7 +20,7 @@ export interface RuntimeCoverageProviderModule {
/**
* Executed on after each run in the worker thread. Possible to return a payload passed to the provider
*/
takeCoverage?: (runtimeOptions?: { moduleExecutionInfo?: ModuleExecutionInfo }) => unknown | Promise<unknown>
takeCoverage?: (runtimeOptions?: { moduleExecutionInfo?: Map<string, { startOffset: number }> }) => unknown | Promise<unknown>
/**
* Executed after all tests have been run in the worker thread.
@ -51,7 +50,7 @@ export async function resolveCoverageProviderModule(
builtInModule += '/browser'
}
const { default: coverageModule } = await loader.executeId(builtInModule)
const { default: coverageModule } = await loader.import(builtInModule)
if (!coverageModule) {
throw new Error(
@ -65,7 +64,7 @@ export async function resolveCoverageProviderModule(
let customProviderModule
try {
customProviderModule = await loader.executeId(options.customProviderModule!)
customProviderModule = await loader.import(options.customProviderModule!)
}
catch (error) {
throw new Error(

Some files were not shown because too many files have changed in this diff Show More