Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: transformMode affects only test files, not regular files #3491

Merged
merged 12 commits into from Jul 11, 2023
41 changes: 14 additions & 27 deletions docs/config/index.md
Expand Up @@ -1120,41 +1120,28 @@ Will call [`vi.unstubAllEnvs`](/api/vi#vi-unstuballenvs) before each test.

Will call [`vi.unstubAllGlobals`](/api/vi#vi-unstuballglobals) before each test.

### transformMode
### testTransformMode

- **Type:** `{ web?, ssr? }`
- **Type:** `{ web?, ssr? }`
- **Version:** Since Vitest 0.32.0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.34.0


Determine the transform method of modules
Determine the transform method for all modules inported 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`.

#### transformMode.ssr
#### testTransformMode.ssr

- **Type:** `RegExp[]`
- **Default:** `[/\.([cm]?[jt]sx?|json)$/]`
- **Type:** `string[]`
- **Default:** `[]`

Use SSR transform pipeline for the specified files.<br>
Vite plugins will receive `ssr: true` flag when processing those files.
Use SSR transform pipeline for all modules inside specified tests.<br>
Vite plugins will receive `ssr: true` flag when processing those files.

#### transformMode&#46;web
#### testTransformMode&#46;web

- **Type:** `RegExp[]`
- **Default:** *modules other than those specified in `transformMode.ssr`*
- **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.

When you use JSX as component models other than React (e.g. Vue JSX or SolidJS), you might want to config as following to make `.tsx` / `.jsx` transformed as client-side components:

```ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
transformMode: {
web: [/\.[jt]sx$/],
},
},
})
```
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 />

Expand Down
2 changes: 1 addition & 1 deletion examples/react-storybook/package.json
Expand Up @@ -28,7 +28,7 @@
"@testing-library/react": "^12.1.5",
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.17",
"@vitejs/plugin-react": "^1.3.2",
"@vitejs/plugin-react": "^4.0.1",
"@vitest/ui": "latest",
"babel-loader": "^8.2.5",
"jsdom": "latest",
Expand Down
2 changes: 1 addition & 1 deletion examples/react-testing-lib-msw/package.json
Expand Up @@ -19,7 +19,7 @@
"@testing-library/user-event": "^13.5.0",
"@types/react": "^17.0.45",
"@types/react-dom": "^17.0.17",
"@vitejs/plugin-react": "^1.3.2",
"@vitejs/plugin-react": "^4.0.1",
"@vitest/ui": "latest",
"cross-fetch": "^3.1.5",
"jsdom": "latest",
Expand Down
3 changes: 0 additions & 3 deletions examples/solid/vite.config.mjs
Expand Up @@ -7,9 +7,6 @@ import solid from 'vite-plugin-solid'
export default defineConfig({
test: {
environment: 'jsdom',
transformMode: {
web: [/.[jt]sx?/],
},
threads: false,
isolate: false,
},
Expand Down
3 changes: 0 additions & 3 deletions examples/vue-jsx/vite.config.ts
Expand Up @@ -7,8 +7,5 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
transformMode: {
web: [/.[tj]sx$/],
},
},
})
1 change: 1 addition & 0 deletions packages/vitest/src/integrations/env/edge-runtime.ts
Expand Up @@ -4,6 +4,7 @@ import { populateGlobal } from './utils'

export default <Environment>({
name: 'edge-runtime',
transformMode: 'ssr',
async setup(global) {
const { EdgeVM } = await importModule('@edge-runtime/vm') as typeof import('@edge-runtime/vm')
const vm = new EdgeVM({
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/integrations/env/happy-dom.ts
Expand Up @@ -4,6 +4,7 @@ import { populateGlobal } from './utils'

export default <Environment>({
name: 'happy-dom',
transformMode: 'web',
async setup(global) {
// happy-dom v3 introduced a breaking change to Window, but
// provides GlobalWindow as a way to use previous behaviour
Expand Down
22 changes: 21 additions & 1 deletion packages/vitest/src/integrations/env/index.ts
@@ -1,4 +1,6 @@
import type { VitestEnvironment } from '../../types/config'
import type { BuiltinEnvironment, VitestEnvironment } from '../../types/config'
import type { VitestExecutor } from '../../node'
import type { Environment } from '../../types'
import node from './node'
import jsdom from './jsdom'
import happy from './happy-dom'
Expand All @@ -19,10 +21,28 @@ export const envPackageNames: Record<Exclude<keyof typeof environments, 'node'>,
'edge-runtime': '@edge-runtime/vm',
}

function isBuiltinEnvironment(env: VitestEnvironment): env is BuiltinEnvironment {
return env in environments
}

export function getEnvPackageName(env: VitestEnvironment) {
if (env === 'node')
return null
if (env in envPackageNames)
return (envPackageNames as any)[env]
return `vitest-environment-${env}`
}

export async function loadEnvironment(name: VitestEnvironment, executor: VitestExecutor): Promise<Environment> {
if (isBuiltinEnvironment(name))
return environments[name]
const packageId = (name[0] === '.' || name[0] === '/') ? name : `vitest-environment-${name}`
const pkg = await executor.executeId(packageId)
if (!pkg || !pkg.default || typeof pkg.default !== 'object' || typeof pkg.default.setup !== 'function') {
throw new Error(
`Environment "${name}" is not a valid environment. `
+ `Path "${packageId}" should export default object with a "setup" method.`,
)
}
return pkg.default
}
1 change: 1 addition & 0 deletions packages/vitest/src/integrations/env/jsdom.ts
Expand Up @@ -28,6 +28,7 @@ function catchWindowErrors(window: Window) {

export default <Environment>({
name: 'jsdom',
transformMode: 'web',
async setup(global, { jsdom = {} }) {
const {
CookieJar,
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/integrations/env/node.ts
Expand Up @@ -3,6 +3,7 @@ import type { Environment } from '../../types'

export default <Environment>({
name: 'node',
transformMode: 'ssr',
async setup(global) {
global.console.Console = Console
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -289,6 +289,8 @@ export function resolveConfig(
port: defaultBrowserPort,
}

resolved.testTransformMode ??= {}

return resolved
}

Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/plugins/index.ts
Expand Up @@ -7,7 +7,7 @@ import { ensurePackageInstalled } from '../pkg'
import { resolveApiServerConfig } from '../config'
import { Vitest } from '../core'
import { generateScopedClassName } from '../../integrations/css/css-modules'
import { EnvReplacerPlugin } from './envReplacer'
import { SsrReplacerPlugin } from './ssrReplacer'
import { GlobalSetupPlugin } from './globalSetup'
import { CSSEnablerPlugin } from './cssEnabler'
import { CoverageTransform } from './coverageTransform'
Expand Down Expand Up @@ -169,7 +169,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
await server.watcher.close()
},
},
EnvReplacerPlugin(),
SsrReplacerPlugin(),
GlobalSetupPlugin(ctx, ctx.logger),
...CSSEnablerPlugin(ctx),
CoverageTransform(ctx),
Expand Down
Expand Up @@ -5,16 +5,17 @@ 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 EnvReplacerPlugin(): Plugin {
export function SsrReplacerPlugin(): Plugin {
return {
name: 'vitest:env-replacer',
enforce: 'pre',
transform(code, id) {
if (!/\bimport\.meta\.env\b/g.test(code))
if (!/\bimport\.meta\.env\b/.test(code) && !/\bimport\.meta\.url\b/.test(code))
return null

let s: MagicString | null = null
const envs = stripLiteral(code).matchAll(/\bimport\.meta\.env\b/g)
const cleanCode = stripLiteral(code)
const envs = cleanCode.matchAll(/\bimport\.meta\.env\b/g)

for (const env of envs) {
s ||= new MagicString(code)
Expand All @@ -25,6 +26,17 @@ export function EnvReplacerPlugin(): Plugin {
s.overwrite(startIndex, endIndex, 'process.env')
}

const urls = cleanCode.matchAll(/\bimport\.meta\.url\b/g)

for (const env of urls) {
s ||= new MagicString(code)

const startIndex = env.index!
const endIndex = startIndex + env[0].length

s.overwrite(startIndex, endIndex, '__vite_ssr_import_meta__.url')
}

if (s) {
return {
code: s.toString(),
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/plugins/workspace.ts
Expand Up @@ -7,7 +7,7 @@ import type { WorkspaceProject } from '../workspace'
import type { UserWorkspaceConfig } from '../../types'
import { CoverageTransform } from './coverageTransform'
import { CSSEnablerPlugin } from './cssEnabler'
import { EnvReplacerPlugin } from './envReplacer'
import { SsrReplacerPlugin } from './ssrReplacer'
import { GlobalSetupPlugin } from './globalSetup'
import { MocksPlugin } from './mocks'
import { deleteDefineConfig, resolveOptimizerConfig } from './utils'
Expand Down Expand Up @@ -117,7 +117,7 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp
await server.watcher.close()
},
},
EnvReplacerPlugin(),
SsrReplacerPlugin(),
...CSSEnablerPlugin(project),
CoverageTransform(project.ctx),
GlobalSetupPlugin(project, project.ctx.logger),
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/pools/child.ts
Expand Up @@ -112,7 +112,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env }: PoolProce
if (!files?.length)
continue

const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options))
const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options) + environment.transformMode)

for (const option in filesByOptions) {
const files = filesByOptions[option]
Expand Down
7 changes: 2 additions & 5 deletions packages/vitest/src/node/pools/rpc.ts
@@ -1,6 +1,5 @@
import type { RawSourceMap } from 'vite-node'
import type { RuntimeRPC } from '../../types'
import { getEnvironmentTransformMode } from '../../utils/base'
import type { WorkspaceProject } from '../workspace'

export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC {
Expand All @@ -25,12 +24,10 @@ export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC {
const r = await project.vitenode.transformRequest(id)
return r?.map as RawSourceMap | undefined
},
fetch(id, environment) {
const transformMode = getEnvironmentTransformMode(project.config, environment)
fetch(id, transformMode) {
return project.vitenode.fetchModule(id, transformMode)
},
resolveId(id, importer, environment) {
const transformMode = getEnvironmentTransformMode(project.config, environment)
resolveId(id, importer, transformMode) {
return project.vitenode.resolveId(id, importer, transformMode)
},
onPathsCollected(paths) {
Expand Down
8 changes: 4 additions & 4 deletions packages/vitest/src/runtime/child.ts
Expand Up @@ -11,7 +11,7 @@ import { rpcDone } from './rpc'
import { setupInspect } from './inspector'

function init(ctx: ChildContext) {
const { config } = ctx
const { config, environment } = ctx

process.env.VITEST_WORKER_ID = '1'
process.env.VITEST_POOL_ID = '1'
Expand All @@ -22,7 +22,7 @@ function init(ctx: ChildContext) {
})

// @ts-expect-error untyped global
globalThis.__vitest_environment__ = config.environment
globalThis.__vitest_environment__ = environment.name
// @ts-expect-error I know what I am doing :P
globalThis.__vitest_worker__ = {
ctx,
Expand Down Expand Up @@ -77,8 +77,8 @@ export async function run(ctx: ChildContext) {

try {
init(ctx)
const { run, executor } = await startViteNode(ctx)
await run(ctx.files, ctx.config, ctx.environment, executor)
const { run, executor, environment } = await startViteNode(ctx)
await run(ctx.files, ctx.config, { ...ctx.environment, environment }, executor)
await rpcDone()
}
finally {
Expand Down
8 changes: 4 additions & 4 deletions packages/vitest/src/runtime/entry.ts
Expand Up @@ -2,7 +2,7 @@ import { performance } from 'node:perf_hooks'
import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner'
import { startTests } from '@vitest/runner'
import { resolve } from 'pathe'
import type { ContextTestEnvironment, ResolvedConfig } from '../types'
import type { ResolvedConfig, ResolvedTestEnvironment } from '../types'
import { getWorkerState, resetModules } from '../utils'
import { vi } from '../integrations/vi'
import { distDir } from '../paths'
Expand Down Expand Up @@ -89,7 +89,7 @@ async function getTestRunner(config: ResolvedConfig, executor: VitestExecutor):
}

// browser shouldn't call this!
export async function run(files: string[], config: ResolvedConfig, environment: ContextTestEnvironment, executor: VitestExecutor): Promise<void> {
export async function run(files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor): Promise<void> {
const workerState = getWorkerState()

await setupGlobalEnv(config)
Expand All @@ -104,11 +104,11 @@ export async function run(files: string[], config: ResolvedConfig, environment:
workerState.durations.prepare = performance.now() - workerState.durations.prepare

// @ts-expect-error untyped global
globalThis.__vitest_environment__ = environment
globalThis.__vitest_environment__ = environment.name

workerState.durations.environment = performance.now()

await withEnv(environment.name, environment.options || config.environmentOptions || {}, executor, async () => {
await withEnv(environment, environment.options || config.environmentOptions || {}, async () => {
workerState.durations.environment = performance.now() - workerState.durations.environment

for (const file of files) {
Expand Down
22 changes: 16 additions & 6 deletions packages/vitest/src/runtime/execute.ts
Expand Up @@ -6,11 +6,14 @@ import { normalize, relative, resolve } from 'pathe'
import { processError } from '@vitest/utils/error'
import type { MockMap } from '../types/mocker'
import { getCurrentEnvironment, getWorkerState } from '../utils/global'
import type { ContextRPC, ContextTestEnvironment, ResolvedConfig } from '../types'
import type { ContextRPC, Environment, ResolvedConfig, ResolvedTestEnvironment } from '../types'
import { distDir } from '../paths'
import { loadEnvironment } from '../integrations/env'
import { VitestMocker } from './mocker'
import { rpc } from './rpc'

const entryUrl = pathToFileURL(resolve(distDir, 'entry.js')).href

export interface ExecuteOptions extends ViteNodeRunnerOptions {
mockMap: MockMap
moduleDirectories?: string[]
Expand All @@ -25,8 +28,9 @@ export async function createVitestExecutor(options: ExecuteOptions) {
}

let _viteNode: {
run: (files: string[], config: ResolvedConfig, environment: ContextTestEnvironment, executor: VitestExecutor) => Promise<void>
run: (files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor) => Promise<void>
executor: VitestExecutor
environment: Environment
}

export const moduleCache = new ModuleCacheMap()
Expand Down Expand Up @@ -61,12 +65,14 @@ export async function startViteNode(ctx: ContextRPC) {
process.on('uncaughtException', e => catchError(e, 'Uncaught Exception'))
process.on('unhandledRejection', e => catchError(e, 'Unhandled Rejection'))

let transformMode: 'ssr' | 'web' = ctx.environment.transformMode ?? 'ssr'

const executor = await createVitestExecutor({
fetchModule(id) {
return rpc().fetch(id, ctx.environment.name)
return rpc().fetch(id, transformMode)
},
resolveId(id, importer) {
return rpc().resolveId(id, importer, ctx.environment.name)
return rpc().resolveId(id, importer, transformMode)
},
moduleCache,
mockMap,
Expand All @@ -76,9 +82,13 @@ export async function startViteNode(ctx: ContextRPC) {
base: config.base,
})

const { run } = await import(pathToFileURL(resolve(distDir, 'entry.js')).href)
const environment = await loadEnvironment(ctx.environment.name, executor)
ctx.environment.environment = environment
transformMode = ctx.environment.transformMode ?? environment.transformMode ?? 'ssr'

const { run } = await import(entryUrl)

_viteNode = { run, executor }
_viteNode = { run, executor, environment }

return _viteNode
}
Expand Down