diff --git a/docs/config/index.md b/docs/config/index.md index e0bfccaae12a..bab82c319582 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1117,6 +1117,42 @@ Will call [`vi.unstubAllEnvs`](/api/vi#vi-unstuballenvs) before each test. Will call [`vi.unstubAllGlobals`](/api/vi#vi-unstuballglobals) before each test. +### transformMode + +- **Type:** `{ web?, ssr? }` + +Determine the transform method of modules + +#### transformMode.ssr + +- **Type:** `RegExp[]` +- **Default:** `[/\.([cm]?[jt]sx?|json)$/]` + +Use SSR transform pipeline for the specified files.
+Vite plugins will receive `ssr: true` flag when processing those files. + +#### transformMode.web + +- **Type:** `RegExp[]` +- **Default:** *modules other than those specified in `transformMode.ssr`* + +First do a normal transform pipeline (targeting browser), then do a SSR rewrite to run the code in Node.
+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$/], + }, + }, +}) +``` + ### snapshotFormat - **Type:** `PrettyFormatOptions` diff --git a/examples/solid/vite.config.mjs b/examples/solid/vite.config.mjs index 6e5124f131b3..9e837c7e0dd6 100644 --- a/examples/solid/vite.config.mjs +++ b/examples/solid/vite.config.mjs @@ -7,6 +7,9 @@ import solid from 'vite-plugin-solid' export default defineConfig({ test: { environment: 'jsdom', + transformMode: { + web: [/.[jt]sx?/], + }, threads: false, isolate: false, }, diff --git a/examples/vue-jsx/vite.config.ts b/examples/vue-jsx/vite.config.ts index e957af21263b..c71fb1b68c63 100644 --- a/examples/vue-jsx/vite.config.ts +++ b/examples/vue-jsx/vite.config.ts @@ -7,5 +7,8 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', + transformMode: { + web: [/.[tj]sx$/], + }, }, }) diff --git a/packages/vitest/src/integrations/env/edge-runtime.ts b/packages/vitest/src/integrations/env/edge-runtime.ts index 9d072eb0aaec..e65db7fbf3ca 100644 --- a/packages/vitest/src/integrations/env/edge-runtime.ts +++ b/packages/vitest/src/integrations/env/edge-runtime.ts @@ -4,7 +4,6 @@ import { populateGlobal } from './utils' export default ({ 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({ diff --git a/packages/vitest/src/integrations/env/happy-dom.ts b/packages/vitest/src/integrations/env/happy-dom.ts index 0a0cb1cc89b0..328139fc7ae2 100644 --- a/packages/vitest/src/integrations/env/happy-dom.ts +++ b/packages/vitest/src/integrations/env/happy-dom.ts @@ -4,7 +4,6 @@ import { populateGlobal } from './utils' export default ({ 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 diff --git a/packages/vitest/src/integrations/env/index.ts b/packages/vitest/src/integrations/env/index.ts index 875d45bbf8f9..4084cb71c6ec 100644 --- a/packages/vitest/src/integrations/env/index.ts +++ b/packages/vitest/src/integrations/env/index.ts @@ -1,6 +1,4 @@ -import type { BuiltinEnvironment, VitestEnvironment } from '../../types/config' -import type { VitestExecutor } from '../../node' -import type { Environment } from '../../types' +import type { VitestEnvironment } from '../../types/config' import node from './node' import jsdom from './jsdom' import happy from './happy-dom' @@ -21,10 +19,6 @@ export const envPackageNames: Record, '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 @@ -32,17 +26,3 @@ export function getEnvPackageName(env: VitestEnvironment) { return (envPackageNames as any)[env] return `vitest-environment-${env}` } - -export async function loadEnvironment(name: VitestEnvironment, executor: VitestExecutor): Promise { - 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. ` - + `Package "vitest-environment-${name}" should have default export with "setup" method.`, - ) - } - return pkg.default -} diff --git a/packages/vitest/src/integrations/env/jsdom.ts b/packages/vitest/src/integrations/env/jsdom.ts index f197e9c3444e..a9c9ed784330 100644 --- a/packages/vitest/src/integrations/env/jsdom.ts +++ b/packages/vitest/src/integrations/env/jsdom.ts @@ -28,7 +28,6 @@ function catchWindowErrors(window: Window) { export default ({ name: 'jsdom', - transformMode: 'web', async setup(global, { jsdom = {} }) { const { CookieJar, diff --git a/packages/vitest/src/integrations/env/node.ts b/packages/vitest/src/integrations/env/node.ts index ef1a01cb1523..e984c343dc23 100644 --- a/packages/vitest/src/integrations/env/node.ts +++ b/packages/vitest/src/integrations/env/node.ts @@ -3,7 +3,6 @@ import type { Environment } from '../../types' export default ({ name: 'node', - transformMode: 'ssr', async setup(global) { global.console.Console = Console return { diff --git a/packages/vitest/src/node/pools/rpc.ts b/packages/vitest/src/node/pools/rpc.ts index ad05fbcad48d..0b67d50e4b8a 100644 --- a/packages/vitest/src/node/pools/rpc.ts +++ b/packages/vitest/src/node/pools/rpc.ts @@ -1,5 +1,6 @@ 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 { @@ -24,10 +25,12 @@ export function createMethodsRPC(project: WorkspaceProject): RuntimeRPC { const r = await project.vitenode.transformRequest(id) return r?.map as RawSourceMap | undefined }, - fetch(id, transformMode) { + fetch(id, environment) { + const transformMode = getEnvironmentTransformMode(project.config, environment) return project.vitenode.fetchModule(id, transformMode) }, - resolveId(id, importer, transformMode) { + resolveId(id, importer, environment) { + const transformMode = getEnvironmentTransformMode(project.config, environment) return project.vitenode.resolveId(id, importer, transformMode) }, onPathsCollected(paths) { diff --git a/packages/vitest/src/runtime/child.ts b/packages/vitest/src/runtime/child.ts index 45641f633403..7e606f7f4a45 100644 --- a/packages/vitest/src/runtime/child.ts +++ b/packages/vitest/src/runtime/child.ts @@ -10,7 +10,7 @@ import { rpcDone } from './rpc' import { setupInspect } from './inspector' function init(ctx: ChildContext) { - const { config, environment } = ctx + const { config } = ctx process.env.VITEST_WORKER_ID = '1' process.env.VITEST_POOL_ID = '1' @@ -21,7 +21,7 @@ function init(ctx: ChildContext) { }) // @ts-expect-error untyped global - globalThis.__vitest_environment__ = environment.name + globalThis.__vitest_environment__ = config.environment // @ts-expect-error I know what I am doing :P globalThis.__vitest_worker__ = { ctx, @@ -76,8 +76,8 @@ export async function run(ctx: ChildContext) { try { init(ctx) - const { run, executor, environment } = await startViteNode(ctx) - await run(ctx.files, ctx.config, { ...ctx.environment, environment }, executor) + const { run, executor } = await startViteNode(ctx) + await run(ctx.files, ctx.config, ctx.environment, executor) await rpcDone() } finally { diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index a2c9edaeaa76..45586375c4b1 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -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 { ResolvedConfig, ResolvedTestEnvironment } from '../types' +import type { ContextTestEnvironment, ResolvedConfig } from '../types' import { getWorkerState, resetModules } from '../utils' import { vi } from '../integrations/vi' import { distDir } from '../paths' @@ -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: ResolvedTestEnvironment, executor: VitestExecutor): Promise { +export async function run(files: string[], config: ResolvedConfig, environment: ContextTestEnvironment, executor: VitestExecutor): Promise { const workerState = getWorkerState() await setupGlobalEnv(config) @@ -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.name + globalThis.__vitest_environment__ = environment workerState.durations.environment = performance.now() - await withEnv(environment, environment.options || config.environmentOptions || {}, async () => { + await withEnv(environment.name, environment.options || config.environmentOptions || {}, executor, async () => { workerState.durations.environment = performance.now() - workerState.durations.environment for (const file of files) { diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index ec150ddf5f54..bacd3c3b1747 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -6,9 +6,8 @@ import { normalize, relative, resolve } from 'pathe' import { processError } from '@vitest/runner/utils' import type { MockMap } from '../types/mocker' import { getCurrentEnvironment, getWorkerState } from '../utils/global' -import type { ContextRPC, Environment, ResolvedConfig, ResolvedTestEnvironment } from '../types' +import type { ContextRPC, ContextTestEnvironment, ResolvedConfig } from '../types' import { distDir } from '../paths' -import { loadEnvironment } from '../integrations/env' import { VitestMocker } from './mocker' import { rpc } from './rpc' @@ -26,9 +25,8 @@ export async function createVitestExecutor(options: ExecuteOptions) { } let _viteNode: { - run: (files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, executor: VitestExecutor) => Promise + run: (files: string[], config: ResolvedConfig, environment: ContextTestEnvironment, executor: VitestExecutor) => Promise executor: VitestExecutor - environment: Environment } export const moduleCache = new ModuleCacheMap() @@ -63,14 +61,12 @@ 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' = 'ssr' - const executor = await createVitestExecutor({ fetchModule(id) { - return rpc().fetch(id, transformMode) + return rpc().fetch(id, ctx.environment.name) }, resolveId(id, importer) { - return rpc().resolveId(id, importer, transformMode) + return rpc().resolveId(id, importer, ctx.environment.name) }, moduleCache, mockMap, @@ -80,12 +76,9 @@ export async function startViteNode(ctx: ContextRPC) { base: config.base, }) - const environment = await loadEnvironment(ctx.environment.name, executor) - transformMode = environment.transformMode ?? 'ssr' - const { run } = await import(pathToFileURL(resolve(distDir, 'entry.js')).href) - _viteNode = { run, executor, environment } + _viteNode = { run, executor } return _viteNode } diff --git a/packages/vitest/src/runtime/setup.node.ts b/packages/vitest/src/runtime/setup.node.ts index 15625fcad1f7..789a6e69beb7 100644 --- a/packages/vitest/src/runtime/setup.node.ts +++ b/packages/vitest/src/runtime/setup.node.ts @@ -2,7 +2,8 @@ import { createRequire } from 'node:module' import { isatty } from 'node:tty' import { installSourcemapsSupport } from 'vite-node/source-map' import { createColors, setupColors } from '@vitest/utils' -import type { EnvironmentOptions, ResolvedConfig, ResolvedTestEnvironment } from '../types' +import { environments } from '../integrations/env' +import type { Environment, ResolvedConfig } from '../types' import { VitestSnapshotEnvironment } from '../integrations/snapshot/environments/node' import { getSafeTimers, getWorkerState } from '../utils' import * as VitestIndex from '../index' @@ -10,6 +11,7 @@ import { RealDate } from '../integrations/mock/date' import { expect } from '../integrations/chai' import { rpc } from './rpc' import { setupCommonEnv } from './setup.common' +import type { VitestExecutor } from './execute' // this should only be used in Node let globalSetup = false @@ -155,17 +157,30 @@ export async function setupConsoleLogSpy() { }) } +async function loadEnvironment(name: string, executor: VitestExecutor) { + const pkg = await executor.executeId(`vitest-environment-${name}`) + if (!pkg || !pkg.default || typeof pkg.default !== 'object' || typeof pkg.default.setup !== 'function') { + throw new Error( + `Environment "${name}" is not a valid environment. ` + + `Package "vitest-environment-${name}" should have default export with "setup" method.`, + ) + } + return pkg.default +} + export async function withEnv( - { environment, name }: ResolvedTestEnvironment, - options: EnvironmentOptions, + name: ResolvedConfig['environment'], + options: ResolvedConfig['environmentOptions'], + executor: VitestExecutor, fn: () => Promise, ) { + const config: Environment = (environments as any)[name] || await loadEnvironment(name, executor) // @ts-expect-error untyped global - globalThis.__vitest_environment__ = name + globalThis.__vitest_environment__ = config.name || name expect.setState({ - environment: name, + environment: config.name || name || 'node', }) - const env = await environment.setup(globalThis, options) + const env = await config.setup(globalThis, options) try { await fn() } diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index ce2fa1a8b5f4..0db3280c364a 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -24,7 +24,7 @@ function init(ctx: WorkerContext) { }) // @ts-expect-error untyped global - globalThis.__vitest_environment__ = config.environment.name + globalThis.__vitest_environment__ = config.environment // @ts-expect-error I know what I am doing :P globalThis.__vitest_worker__ = { ctx, @@ -62,8 +62,8 @@ export async function run(ctx: WorkerContext) { try { init(ctx) - const { run, executor, environment } = await startViteNode(ctx) - await run(ctx.files, ctx.config, { ...ctx.environment, environment }, executor) + const { run, executor } = await startViteNode(ctx) + await run(ctx.files, ctx.config, ctx.environment, executor) await rpcDone() } finally { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 6484b0f59693..0a18bca805a6 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -438,6 +438,27 @@ export interface InlineConfig { */ uiBase?: string + /** + * Determine the transform method of modules + */ + transformMode?: { + /** + * Use SSR transform pipeline for the specified files. + * Vite plugins will receive `ssr: true` flag when processing those files. + * + * @default [/\.([cm]?[jt]sx?|json)$/] + */ + ssr?: RegExp[] + /** + * 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 other than `ssr` + */ + web?: RegExp[] + } + /** * Format options for snapshot testing. */ diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 1b059a959be9..e71d3c3ee3a5 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -23,7 +23,6 @@ export interface EnvironmentReturn { export interface Environment { name: string - transformMode?: 'web' | 'ssr' setup(global: any, options: Record): Awaitable } diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index 536eb3fc4274..1e8bbe93d2ac 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -1,16 +1,14 @@ import type { FetchResult, RawSourceMap, ViteNodeResolveId } from 'vite-node' import type { CancelReason } from '@vitest/runner' import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from './config' -import type { Environment, UserConsoleLog } from './general' +import type { UserConsoleLog } from './general' import type { SnapshotResult } from './snapshot' import type { File, TaskResultPack } from './tasks' import type { AfterSuiteRunMeta } from './worker' -type TransformMode = 'web' | 'ssr' - export interface RuntimeRPC { - fetch: (id: string, environment: TransformMode) => Promise - resolveId: (id: string, importer: string | undefined, environment: TransformMode) => Promise + fetch: (id: string, environment: VitestEnvironment) => Promise + resolveId: (id: string, importer: string | undefined, environment: VitestEnvironment) => Promise getSourceMap: (id: string, force?: boolean) => Promise onFinished: (files: File[], errors?: unknown[]) => void @@ -37,12 +35,6 @@ export interface ContextTestEnvironment { options: EnvironmentOptions | null } -export interface ResolvedTestEnvironment extends ContextTestEnvironment { - name: VitestEnvironment - environment: Environment - options: EnvironmentOptions | null -} - export interface ContextRPC { config: ResolvedConfig files: string[] diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index 010a1b0e3107..86e3713143f1 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -1,4 +1,4 @@ -import type { Arrayable, Nullable } from '../types' +import type { Arrayable, Nullable, ResolvedConfig, VitestEnvironment } from '../types' export { notNullish, getCallLastIndex } from '@vitest/utils' @@ -128,6 +128,12 @@ export function stdout(): NodeJS.WriteStream { return console._stdout || process.stdout } +export function getEnvironmentTransformMode(config: ResolvedConfig, environment: VitestEnvironment) { + if (!config.deps?.experimentalOptimizer?.enabled) + return undefined + return (environment === 'happy-dom' || environment === 'jsdom') ? 'web' : 'ssr' +} + // AggregateError is supported in Node.js 15.0.0+ class AggregateErrorPonyfill extends Error { errors: unknown[] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f963be3cdc5f..a65e911d3eaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1530,7 +1530,7 @@ importers: version: 2.3.2(vue@3.3.4) happy-dom: specifier: latest - version: 9.20.3 + version: 9.1.9 istanbul-lib-coverage: specifier: ^3.2.0 version: 3.2.0 @@ -1693,9 +1693,6 @@ importers: test/resolve: devDependencies: - happy-dom: - specifier: ^9.20.3 - version: 9.20.3 vitest: specifier: workspace:* version: link:../../packages/vitest @@ -15655,17 +15652,6 @@ packages: whatwg-mimetype: 3.0.0 dev: true - /happy-dom@9.20.3: - resolution: {integrity: sha512-eBsgauT435fXFvQDNcmm5QbGtYzxEzOaX35Ia+h6yP/wwa4xSWZh1CfP+mGby8Hk6Xu59mTkpyf72rUXHNxY7A==} - dependencies: - css.escape: 1.5.1 - entities: 4.5.0 - iconv-lite: 0.6.3 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - dev: true - /has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} diff --git a/test/core/test/define-web.test.ts b/test/core/test/define-web.test.ts deleted file mode 100644 index 3af47b384850..000000000000 --- a/test/core/test/define-web.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// @vitest-environment jsdom - -import { afterAll, expect, test } from 'vitest' - -declare let __DEFINE__: string -declare let __JSON__: any -declare let __MODE__: string -declare let SOME: { - VARIABLE: string - SOME: { - VARIABLE: string - } -} - -// functions to test that they are not statically replaced -function get__DEFINE__() { - return __DEFINE__ -} -function get__JSON__() { - return __JSON__ -} -function get__MODE__() { - return __MODE__ -} - -const MODE = process.env.MODE - -afterAll(() => { - process.env.MODE = MODE -}) - -test('process.env.HELLO_PROCESS is defined on "defined" but exists on process.env', () => { - expect('HELLO_PROCESS' in process.env).toBe(true) - expect(process.env.HELLO_PROCESS).toBe('hello process') -}) - -test('can redeclare standard define', () => { - expect(get__DEFINE__()).toBe('defined') - __DEFINE__ = 'new defined' - expect(get__DEFINE__()).toBe('new defined') -}) - -test('can redeclare json object', () => { - expect(get__JSON__()).toEqual({ hello: 'world' }) - __JSON__ = { hello: 'test' } - const name = '__JSON__' - expect(get__JSON__()).toEqual({ hello: 'test' }) - expect((globalThis as any)[name]).toEqual({ hello: 'test' }) -}) - -test('reassigning complicated __MODE__', () => { - const env = process.env.MODE - expect(get__MODE__()).toBe(env) - process.env.MODE = 'development' - expect(get__MODE__()).not.toBe('development') -}) - -test('dotted defines can be reassigned', () => { - expect(SOME.VARIABLE).toBe('variable') - expect(SOME.SOME.VARIABLE).toBe('nested variable') - SOME.VARIABLE = 'new variable' - expect(SOME.VARIABLE).toBe('new variable') -}) diff --git a/test/core/test/define-ssr.test.ts b/test/core/test/define.test.ts similarity index 100% rename from test/core/test/define-ssr.test.ts rename to test/core/test/define.test.ts diff --git a/test/resolve/package.json b/test/resolve/package.json index 3d0613a16e6f..9aae7475f41c 100644 --- a/test/resolve/package.json +++ b/test/resolve/package.json @@ -6,7 +6,6 @@ "coverage": "vitest run --coverage" }, "devDependencies": { - "happy-dom": "^9.20.3", "vitest": "workspace:*" } } diff --git a/test/resolve/vitest.config.ts b/test/resolve/vitest.config.ts index f18228bd9474..38ae8d564abf 100644 --- a/test/resolve/vitest.config.ts +++ b/test/resolve/vitest.config.ts @@ -2,10 +2,10 @@ import { defineConfig } from 'vite' export default defineConfig({ test: { - environmentMatchGlobs: [ - ['**/web.test.ts', 'happy-dom'], - ['**/ssr.test.ts', 'node'], - ], + transformMode: { + web: [/web\.test\.ts/], + ssr: [/ssr\.test\.ts/], + }, deps: { external: [/pkg-/], },