mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
feat!: use module-runner instead of vite-node (#8208)
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
This commit is contained in:
parent
94ab392b36
commit
9be01ba594
2
.npmrc
2
.npmrc
@ -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/
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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.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`
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 = [
|
||||
|
||||
60
packages/utils/src/constants.ts
Normal file
60
packages/utils/src/constants.ts
Normal 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__`
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
99
packages/utils/src/resolver.ts
Normal file
99
packages/utils/src/resolver.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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'
|
||||
|
||||
88
packages/vitest/src/integrations/env/loader.ts
vendored
88
packages/vitest/src/integrations/env/loader.ts
vendored
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
2
packages/vitest/src/integrations/env/node.ts
vendored
2
packages/vitest/src/integrations/env/node.ts
vendored
@ -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')
|
||||
|
||||
@ -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`',
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
|
||||
@ -856,7 +856,6 @@ export const cliOptionsConfig: VitestCLIOptions = {
|
||||
uiBase: null,
|
||||
benchmark: null,
|
||||
include: null,
|
||||
testTransformMode: null,
|
||||
fakeTimers: null,
|
||||
chaiConfig: null,
|
||||
clearMocks: null,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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[]> {
|
||||
|
||||
@ -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
|
||||
|
||||
258
packages/vitest/src/node/environments/fetchModule.ts
Normal file
258
packages/vitest/src/node/environments/fetchModule.ts
Normal 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 {}
|
||||
}
|
||||
}
|
||||
47
packages/vitest/src/node/environments/normalizeUrl.ts
Normal file
47
packages/vitest/src/node/environments/normalizeUrl.ts
Normal 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
|
||||
}
|
||||
56
packages/vitest/src/node/environments/serverRunner.ts
Normal file
56
packages/vitest/src/node/environments/serverRunner.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
)
|
||||
},
|
||||
|
||||
@ -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>>(
|
||||
|
||||
@ -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) {
|
||||
@ -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')
|
||||
) {
|
||||
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
163
packages/vitest/src/node/plugins/runnerTransform.ts
Normal file
163
packages/vitest/src/node/plugins/runnerTransform.ts
Normal 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 []
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
@ -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(),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ''
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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) => {
|
||||
|
||||
222
packages/vitest/src/node/resolver.ts
Normal file
222
packages/vitest/src/node/resolver.ts
Normal 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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -65,7 +65,6 @@ type UnsupportedProperties
|
||||
// non-browser options
|
||||
| 'api'
|
||||
| 'deps'
|
||||
| 'testTransformMode'
|
||||
| 'environment'
|
||||
| 'environmentOptions'
|
||||
| 'server'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { VitestExecutor } from '../runtime/execute'
|
||||
@ -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'
|
||||
|
||||
14
packages/vitest/src/public/module-runner.ts
Normal file
14
packages/vitest/src/public/module-runner.ts
Normal 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'
|
||||
@ -98,7 +98,6 @@ export type {
|
||||
SequenceHooks,
|
||||
SequenceSetupFiles,
|
||||
UserConfig as TestUserConfig,
|
||||
TransformModePatterns,
|
||||
TypecheckConfig,
|
||||
UserWorkspaceConfig,
|
||||
VitestEnvironment,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
|
||||
|
||||
49
packages/vitest/src/runtime/moduleRunner/cachedResolver.ts
Normal file
49
packages/vitest/src/runtime/moduleRunner/cachedResolver.ts
Normal 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
|
||||
}
|
||||
41
packages/vitest/src/runtime/moduleRunner/errorCatcher.ts
Normal file
41
packages/vitest/src/runtime/moduleRunner/errorCatcher.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
61
packages/vitest/src/runtime/moduleRunner/moduleDebug.ts
Normal file
61
packages/vitest/src/runtime/moduleRunner/moduleDebug.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
496
packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
Normal file
496
packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
151
packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
Normal file
151
packages/vitest/src/runtime/moduleRunner/moduleRunner.ts
Normal 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
|
||||
}
|
||||
31
packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
Normal file
31
packages/vitest/src/runtime/moduleRunner/moduleTransport.ts
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
179
packages/vitest/src/runtime/moduleRunner/startModuleRunner.ts
Normal file
179
packages/vitest/src/runtime/moduleRunner/startModuleRunner.ts
Normal 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
|
||||
}
|
||||
@ -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 })
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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']
|
||||
}
|
||||
|
||||
@ -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 =>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user