From 106da5896ce66aca3dfe7e8652c8890c1e7f9a08 Mon Sep 17 00:00:00 2001 From: Eli <88557639+lishaduck@users.noreply.github.com> Date: Sun, 17 May 2026 17:40:51 -0500 Subject: [PATCH 1/4] feat: support typescript build mode (#9870) --- docs/guide/cli-generated.md | 7 +++++ packages/vitest/src/node/cli/cli-config.ts | 3 ++ packages/vitest/src/node/types/config.ts | 4 +++ packages/vitest/src/typecheck/typechecker.ts | 29 ++++++++++++++----- test/e2e/project-references/.gitignore | 1 + test/e2e/project-references/package.json | 14 +++++++++ .../packages/project-a/package.json | 6 ++++ .../packages/project-a/src/index.ts | 2 ++ .../packages/project-a/tsconfig.json | 17 +++++++++++ .../packages/project-b/node_modules/project-a | 1 + .../packages/project-b/package.json | 8 +++++ .../packages/project-b/src/b.test-d.ts | 9 ++++++ .../packages/project-b/tsconfig.json | 21 ++++++++++++++ test/e2e/project-references/tsconfig.json | 7 +++++ test/e2e/project-references/vitest.config.ts | 16 ++++++++++ 15 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 test/e2e/project-references/.gitignore create mode 100644 test/e2e/project-references/package.json create mode 100644 test/e2e/project-references/packages/project-a/package.json create mode 100644 test/e2e/project-references/packages/project-a/src/index.ts create mode 100644 test/e2e/project-references/packages/project-a/tsconfig.json create mode 120000 test/e2e/project-references/packages/project-b/node_modules/project-a create mode 100644 test/e2e/project-references/packages/project-b/package.json create mode 100644 test/e2e/project-references/packages/project-b/src/b.test-d.ts create mode 100644 test/e2e/project-references/packages/project-b/tsconfig.json create mode 100644 test/e2e/project-references/tsconfig.json create mode 100644 test/e2e/project-references/vitest.config.ts diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index db20e2726732..93cb69c96faa 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -791,6 +791,13 @@ Allow JavaScript files to be typechecked. By default takes the value from tsconf Ignore type errors from source files +### typecheck.build + +- **CLI:** `--typecheck.build` +- **Config:** [typecheck.build](/config/typecheck#typecheck-build) + +Use TypeScript build mode + ### typecheck.tsconfig - **CLI:** `--typecheck.tsconfig ` diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index ccd6f76cb548..9878b64e33f0 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -726,6 +726,9 @@ export const cliOptionsConfig: VitestCLIOptions = { ignoreSourceErrors: { description: 'Ignore type errors from source files', }, + build: { + description: 'Use TypeScript build mode', + }, tsconfig: { description: 'Path to a custom tsconfig file', argument: '', diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index fe77fb75630b..0203846348a6 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1014,6 +1014,10 @@ export interface TypecheckConfig { * Do not fail, if Vitest found errors outside the test files. */ ignoreSourceErrors?: boolean + /** + * Use TypeScript build mode. + */ + build?: boolean /** * Path to tsconfig, relative to the project root. */ diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index 55340f10e0a4..a7a180f606dd 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -294,16 +294,25 @@ export class Typechecker { const { root, watch, typecheck } = this.project.config const args = [ - '--noEmit', '--pretty', 'false', - '--incremental', - '--tsBuildInfoFile', - join( - process.versions.pnp ? join(os.tmpdir(), this.project.hash) : distDir, - 'tsconfig.tmp.tsbuildinfo', - ), ] + + if (typecheck.build) { + args.unshift('--build') + } + else { + args.push( + '--noEmit', + '--incremental', + '--tsBuildInfoFile', + join( + process.versions.pnp ? join(os.tmpdir(), this.project.hash) : distDir, + 'tsconfig.tmp.tsbuildinfo', + ), + ) + } + // use builtin watcher because it's faster if (watch) { args.push('--watch') @@ -312,7 +321,11 @@ export class Typechecker { args.push('--allowJs', '--checkJs') } if (typecheck.tsconfig) { - args.push('-p', resolve(root, typecheck.tsconfig)) + if (!typecheck.build) { + args.push('-p') + } + + args.push(resolve(root, typecheck.tsconfig)) } this._output = '' this._startTime = performance.now() diff --git a/test/e2e/project-references/.gitignore b/test/e2e/project-references/.gitignore new file mode 100644 index 000000000000..c832a6518c7c --- /dev/null +++ b/test/e2e/project-references/.gitignore @@ -0,0 +1 @@ +!/packages/project-b/node_modules \ No newline at end of file diff --git a/test/e2e/project-references/package.json b/test/e2e/project-references/package.json new file mode 100644 index 000000000000..e97df9fd2c95 --- /dev/null +++ b/test/e2e/project-references/package.json @@ -0,0 +1,14 @@ +{ + "name": "@vitest/test-integration-project-references", + "type": "module", + "private": true, + "scripts": { + "test": "(rm -rf packages/*/dist || true) && vitest run" + }, + "devDependencies": { + "vitest": "workspace:*" + }, + "workspaces": [ + "packages/*" + ] +} diff --git a/test/e2e/project-references/packages/project-a/package.json b/test/e2e/project-references/packages/project-a/package.json new file mode 100644 index 000000000000..0662b858461b --- /dev/null +++ b/test/e2e/project-references/packages/project-a/package.json @@ -0,0 +1,6 @@ +{ + "name": "project-a", + "type": "module", + "private": true, + "exports": "./src/index.ts" +} diff --git a/test/e2e/project-references/packages/project-a/src/index.ts b/test/e2e/project-references/packages/project-a/src/index.ts new file mode 100644 index 000000000000..322d9b3eac82 --- /dev/null +++ b/test/e2e/project-references/packages/project-a/src/index.ts @@ -0,0 +1,2 @@ +export type A = number +export const a: A = 1 diff --git a/test/e2e/project-references/packages/project-a/tsconfig.json b/test/e2e/project-references/packages/project-a/tsconfig.json new file mode 100644 index 000000000000..c275baf434a6 --- /dev/null +++ b/test/e2e/project-references/packages/project-a/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "composite": true, + "target": "esnext", + "lib": ["DOM", "ESNext"], + "module": "nodenext", + "types": [], + "strict": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "sourceMap": true, + "verbatimModuleSyntax": true, + "skipLibCheck": false + }, + "files": ["src/index.ts"] +} diff --git a/test/e2e/project-references/packages/project-b/node_modules/project-a b/test/e2e/project-references/packages/project-b/node_modules/project-a new file mode 120000 index 000000000000..c4bb4d0865a1 --- /dev/null +++ b/test/e2e/project-references/packages/project-b/node_modules/project-a @@ -0,0 +1 @@ +../../project-a \ No newline at end of file diff --git a/test/e2e/project-references/packages/project-b/package.json b/test/e2e/project-references/packages/project-b/package.json new file mode 100644 index 000000000000..31aaefa05c73 --- /dev/null +++ b/test/e2e/project-references/packages/project-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "project-b", + "type": "module", + "private": true, + "dependencies": { + "project-a": "workspace:*" + } +} diff --git a/test/e2e/project-references/packages/project-b/src/b.test-d.ts b/test/e2e/project-references/packages/project-b/src/b.test-d.ts new file mode 100644 index 000000000000..107b57da5fc4 --- /dev/null +++ b/test/e2e/project-references/packages/project-b/src/b.test-d.ts @@ -0,0 +1,9 @@ +import type { A } from 'project-a' +import { a } from 'project-a' +import { describe, expectTypeOf, it } from 'vitest' + +describe('Import types from project references', () => { + it('should import value and type from project references', () => { + expectTypeOf(a).toEqualTypeOf() + }) +}) diff --git a/test/e2e/project-references/packages/project-b/tsconfig.json b/test/e2e/project-references/packages/project-b/tsconfig.json new file mode 100644 index 000000000000..e9848638b08a --- /dev/null +++ b/test/e2e/project-references/packages/project-b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "composite": true, + "target": "esnext", + "lib": ["DOM", "ESNext"], + "module": "nodenext", + "types": [], + "strict": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "sourceMap": true, + "verbatimModuleSyntax": true, + "skipLibCheck": false + }, + "references": [ + { "path": "../project-a" } + ], + "files": ["src/b.test-d.ts"], + "exclude": ["node_modules"] +} diff --git a/test/e2e/project-references/tsconfig.json b/test/e2e/project-references/tsconfig.json new file mode 100644 index 000000000000..1b91001e9074 --- /dev/null +++ b/test/e2e/project-references/tsconfig.json @@ -0,0 +1,7 @@ +{ + "references": [ + { "path": "./packages/project-a" }, + { "path": "./packages/project-b" } + ], + "include": [] +} diff --git a/test/e2e/project-references/vitest.config.ts b/test/e2e/project-references/vitest.config.ts new file mode 100644 index 000000000000..276c63a512f4 --- /dev/null +++ b/test/e2e/project-references/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [{ + test: { + name: 'project-b', + dir: 'packages/project-b', + typecheck: { + enabled: true, + build: true, + }, + }, + }], + }, +}) From 2a27657a457571db73f962403dec0df4b35f4064 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 18 May 2026 07:47:33 +0900 Subject: [PATCH 2/4] refactor(ui): remove unnecesary moduleGraph clone (#10353) --- packages/ui/client/components/FileDetails.vue | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/ui/client/components/FileDetails.vue b/packages/ui/client/components/FileDetails.vue index 7b016cd542bc..c68f6f6a21c8 100644 --- a/packages/ui/client/components/FileDetails.vue +++ b/packages/ui/client/components/FileDetails.vue @@ -3,7 +3,6 @@ import type { RunnerTask, RunnerTestCase } from 'vitest' import type { ModuleGraph } from '~/composables/module-graph' import type { Params } from '~/composables/params' import { debouncedWatch } from '@vueuse/core' -import { toJSON } from 'flatted' import { computed, nextTick, ref } from 'vue' import DetailsHeaderButtons from '~/components/DetailsHeaderButtons.vue' import { @@ -118,19 +117,15 @@ async function loadModuleGraph(force = false) { ) // remove node_modules from the graph when enabled if (hideNodeModules.value) { - // when using static html reporter, we've the meta as global, we need to clone it - if (isReport) { - moduleGraph - = typeof window.structuredClone !== 'undefined' - ? window.structuredClone(moduleGraph) - : toJSON(moduleGraph) + moduleGraph = { + ...moduleGraph, + inlined: moduleGraph.inlined.filter( + n => !nodeModuleRegex.test(n), + ), + externalized: moduleGraph.externalized.filter( + n => !nodeModuleRegex.test(n), + ), } - moduleGraph.inlined = moduleGraph.inlined.filter( - n => !nodeModuleRegex.test(n), - ) - moduleGraph.externalized = moduleGraph.externalized.filter( - n => !nodeModuleRegex.test(n), - ) } graph.value = getModuleGraph( moduleGraph, From 745b30b647dd9ba02dd49a8a8f61d7b4c261b0fb Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 18 May 2026 07:48:20 +0900 Subject: [PATCH 3/4] fix(reporters): fix missing `testModules` in `onTestRunEnd` when merging blobs from different root directory test runs (#10348) Co-authored-by: Codex --- packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/project.ts | 5 +- .../vitest/src/node/test-specification.ts | 9 ++- test/e2e/test/reporters/merge-reports.test.ts | 66 ++++++++++++++++++- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 521649d37e25..efa95c6082e9 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -622,7 +622,7 @@ export class Vitest { const specifications: TestSpecification[] = [] for (const file of files) { const project = this.getProjectByName(file.projectName || '') - const specification = project.createSpecification(file.filepath, undefined, file.pool, file.meta) + const specification = project.createSpecification(file.filepath, undefined, file.pool, file.id) specifications.push(specification) } diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 3ff810a697ec..234bc56a4e00 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -1,4 +1,3 @@ -import type { TaskMeta } from '@vitest/runner/types' import type { GlobOptions } from 'tinyglobby' import type { DevEnvironment, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' import type { ModuleRunner } from 'vite/module-runner' @@ -154,14 +153,14 @@ export class TestProject { /** @internal */ pool?: string, /** @internal */ - metaOverride?: TaskMeta, + taskIdOverride?: string, ): TestSpecification { return new TestSpecification( this, moduleId, pool || getFilePoolName(this), locationsOrOptions, - metaOverride, + taskIdOverride, ) } diff --git a/packages/vitest/src/node/test-specification.ts b/packages/vitest/src/node/test-specification.ts index 133251c347df..ccd1e52ba343 100644 --- a/packages/vitest/src/node/test-specification.ts +++ b/packages/vitest/src/node/test-specification.ts @@ -1,4 +1,3 @@ -import type { TaskMeta } from '@vitest/runner/types' import type { SerializedTestSpecification } from '../runtime/types/utils' import type { TestProject } from './project' import type { TestModule } from './reporters/reported-tasks' @@ -56,14 +55,14 @@ export class TestSpecification { moduleId: string, pool: Pool, testLinesOrOptions?: number[] | TestSpecificationOptions | undefined, - // merge-reports uses the original `file.meta` from the test run - metaOverride?: TaskMeta, + // merge-reports forces the original task id from the test run + taskIdOverride?: string, ) { const projectName = project.config.name - this.taskId = generateFileHash( + this.taskId = taskIdOverride ?? generateFileHash( relative(project.config.root, moduleId), projectName, - metaOverride ?? { typecheck: pool === 'typescript', __vitest_label__: project.config.mergeReportsLabel }, + { typecheck: pool === 'typescript', __vitest_label__: project.config.mergeReportsLabel }, ) this.project = project this.moduleId = moduleId diff --git a/test/e2e/test/reporters/merge-reports.test.ts b/test/e2e/test/reporters/merge-reports.test.ts index 36a29856bba0..961955bc4a77 100644 --- a/test/e2e/test/reporters/merge-reports.test.ts +++ b/test/e2e/test/reporters/merge-reports.test.ts @@ -2,8 +2,9 @@ import type { RunVitestConfig } from '#test-utils' import type { File, Test } from '@vitest/runner/types' import type { TestUserConfig, Vitest } from 'vitest/node' import type { MergeReport } from 'vitest/src/node/reporters/blob.js' -import { existsSync, readdirSync, rmSync } from 'node:fs' +import { cpSync, existsSync, readdirSync, rmSync } from 'node:fs' import { mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' import { buildTestTree, runVitest, useFS } from '#test-utils' import { playwright } from '@vitest/browser-playwright' import { createFileTask } from '@vitest/runner/utils' @@ -1026,6 +1027,69 @@ test("top-test", () => {}) `) }) +test('onTestRunEnd(testModules) are preserved from different test run roots', async () => { + const root1 = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) + const root2 = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) + useFS(root1, { + 'basic.test.ts': `test("ok", () => {})`, + }) + useFS(root2, { + 'basic.test.ts': `test("ok", () => {})`, + }) + + const result1 = await runVitest({ + root: root1, + globals: true, + reporters: [['blob', { label: 'linux' }]], + }) + expect(result1.stderr).toMatchInlineSnapshot(`""`) + expect(result1.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "ok": "passed", + }, + } + `) + + const result2 = await runVitest({ + root: root2, + globals: true, + reporters: [['blob', { label: 'macos' }]], + }) + expect(result2.stderr).toMatchInlineSnapshot(`""`) + expect(result2.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "ok": "passed", + }, + } + `) + + const blobDir1 = path.join(root1, '.vitest/blob') + const blobDir2 = path.join(root2, '.vitest/blob') + for (const filename of readdirSync(blobDir2)) { + cpSync(path.join(blobDir2, filename), path.join(blobDir1, filename)) + } + + const result = await runVitest({ + root: root1, + mergeReports: blobDir1, + }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + // previously this was "1 passed" due to broken onTestRunEnd(testModules) + expect(result.stdout).toContain('Test Files 2 passed') + expect(result.errorTree({ fileLabel: true })).toMatchInlineSnapshot(` + { + "basic.test.ts (linux)": { + "ok": "passed", + }, + "basic.test.ts (macos)": { + "ok": "passed", + }, + } + `) +}) + async function writeBlob(content: MergeReport, filename: string): Promise { const report = stringify(content) From d3c964bfcb5a47840e892e7ff4b1146dd4ca8b6d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 18 May 2026 07:50:15 +0900 Subject: [PATCH 4/4] fix(browser): skip `wrapDynamicImport` transform on ssr environment (#10355) --- packages/browser/src/node/plugin.ts | 5 ++++- .../mocker/src/node/dynamicImportPlugin.ts | 6 +++--- test/browser/test/server/entry.ts | 7 +++++++ test/browser/test/server/ssr-dep.ts | 1 + test/browser/test/ssr.test.ts | 7 +++++++ test/browser/vitest.config.mts | 21 +++++++++++++++++++ 6 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 test/browser/test/server/entry.ts create mode 100644 test/browser/test/server/ssr-dep.ts create mode 100644 test/browser/test/ssr.test.ts diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index 3f5986224132..b6fdd547d55c 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -344,7 +344,10 @@ export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => { BrowserContext(parentServer), dynamicImportPlugin({ globalThisAccessor: '"__vitest_browser_runner__"', - filter(id) { + filter(id, environment) { + if (environment.name !== 'client') { + return false + } if (id.includes(distRoot)) { return false } diff --git a/packages/mocker/src/node/dynamicImportPlugin.ts b/packages/mocker/src/node/dynamicImportPlugin.ts index dd236ec7b6a7..bf087f08d08b 100644 --- a/packages/mocker/src/node/dynamicImportPlugin.ts +++ b/packages/mocker/src/node/dynamicImportPlugin.ts @@ -1,5 +1,5 @@ import type { SourceMap } from 'magic-string' -import type { Plugin, Rollup } from 'vite' +import type { Environment, Plugin, Rollup } from 'vite' import type { Expression, Positioned } from './esmWalker' import MagicString from 'magic-string' import { esmWalker } from './esmWalker' @@ -11,7 +11,7 @@ export interface DynamicImportPluginOptions { * @default `"__vitest_mocker__"` */ globalThisAccessor?: string - filter?: (id: string) => boolean + filter?: (id: string, environment: Environment) => boolean } export function dynamicImportPlugin(options: DynamicImportPluginOptions = {}): Plugin { @@ -23,7 +23,7 @@ export function dynamicImportPlugin(options: DynamicImportPluginOptions = {}): P if (!regexDynamicImport.test(source)) { return } - if (options.filter && !options.filter(id)) { + if (options.filter && !options.filter(id, this.environment)) { return } return injectDynamicImport(source, id, this.parse, options) diff --git a/test/browser/test/server/entry.ts b/test/browser/test/server/entry.ts new file mode 100644 index 000000000000..4385e3314eee --- /dev/null +++ b/test/browser/test/server/entry.ts @@ -0,0 +1,7 @@ +export default async (url: URL) => { + if (url.pathname === '/api/ssr-dep') { + const lib = await import('./ssr-dep') + return lib.default + } + return 'not-found' +} diff --git a/test/browser/test/server/ssr-dep.ts b/test/browser/test/server/ssr-dep.ts new file mode 100644 index 000000000000..dea335a6c734 --- /dev/null +++ b/test/browser/test/server/ssr-dep.ts @@ -0,0 +1 @@ +export default 'ssr-dep' diff --git a/test/browser/test/ssr.test.ts b/test/browser/test/ssr.test.ts new file mode 100644 index 000000000000..f6959394b7dc --- /dev/null +++ b/test/browser/test/ssr.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +test('ssr dynamic import', async () => { + const res = await fetch('/api/ssr-dep') + const text = await res.json() + expect(text).toBe('ssr-dep') +}) diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 7c21962bdc51..161a6143281e 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -106,5 +106,26 @@ export default defineConfig({ await server.ssrLoadModule('/package.json') }, }, + { + name: 'my-ssr', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = new URL(req.url || '', 'http://localhost') + if (url.pathname.startsWith('/api/')) { + try { + const mod = await server.ssrLoadModule('./test/server/entry.ts') + const result = await mod.default(url) + res.end(JSON.stringify(result)) + return + } + catch (e) { + next(e) + return + } + } + next() + }) + }, + }, ], })