diff --git a/docs/advanced/api/test-project.md b/docs/advanced/api/test-project.md index d1a2f4f97..9fef2c8c1 100644 --- a/docs/advanced/api/test-project.md +++ b/docs/advanced/api/test-project.md @@ -90,6 +90,12 @@ vitest.config === vitest.projects[0].globalConfig This is the project's resolved test config. +## hash 3.2.0 {#hash} + +The unique hash of this project. This value is consistent between the reruns. + +It is based on the root of the project and its name. Note that the root path is not consistent between different OS, so the hash will also be different. + ## vite This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). All projects have their own Vite servers. diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index 6e39e0f09..836da1d85 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -268,35 +268,6 @@ Repository: git+https://github.com/WebReflection/flatted.git --------------------------------------- -## get-tsconfig -License: MIT -By: Hiroki Osame -Repository: privatenumber/get-tsconfig - -> MIT License -> -> Copyright (c) Hiroki Osame -> -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all -> copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -> SOFTWARE. - ---------------------------------------- - ## js-tokens License: MIT By: Simon Lydell @@ -550,35 +521,6 @@ Repository: git+https://github.com/quansync-dev/quansync.git --------------------------------------- -## resolve-pkg-maps -License: MIT -By: Hiroki Osame -Repository: privatenumber/resolve-pkg-maps - -> MIT License -> -> Copyright (c) Hiroki Osame -> -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all -> copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -> SOFTWARE. - ---------------------------------------- - ## sisteransi License: MIT By: Terkel Gjervig diff --git a/packages/vitest/package.json b/packages/vitest/package.json index e61464c1d..41fdcf2f9 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -198,7 +198,6 @@ "chai-subset": "^1.6.0", "find-up": "^6.3.0", "flatted": "catalog:", - "get-tsconfig": "^4.10.0", "happy-dom": "^17.4.4", "jsdom": "^26.1.0", "local-pkg": "^1.1.1", diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index 8ad0ebc99..4789667f8 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -9,7 +9,7 @@ import { createDefer } from '@vitest/utils' import { Typechecker } from '../../typecheck/typechecker' import { groupBy } from '../../utils/base' -export function createTypecheckPool(ctx: Vitest): ProcessPool { +export function createTypecheckPool(vitest: Vitest): ProcessPool { const promisesMap = new WeakMap>() const rerunTriggered = new WeakSet() @@ -20,11 +20,11 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { const checker = project.typechecker! const { packs, events } = checker.getTestPacksAndEvents() - await ctx._testRun.updated(packs, events) + await vitest._testRun.updated(packs, events) if (!project.config.typecheck.ignoreSourceErrors) { sourceErrors.forEach(error => - ctx.state.catchError(error, 'Unhandled Source Error'), + vitest.state.catchError(error, 'Unhandled Source Error'), ) } @@ -33,7 +33,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { if (processError) { const error = new Error(checker.getOutput()) error.stack = '' - ctx.state.catchError(error, 'Typecheck Error') + vitest.state.catchError(error, 'Typecheck Error') } promisesMap.get(project)?.resolve() @@ -41,11 +41,11 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { rerunTriggered.delete(project) // triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case - if (ctx.config.watch && !ctx.runningPromise) { - await ctx.report('onFinished', files, []) - await ctx.report('onWatcherStart', files, [ + if (vitest.config.watch && !vitest.runningPromise) { + await vitest.report('onFinished', files, []) + await vitest.report('onWatcherStart', files, [ ...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), - ...ctx.state.getUnhandledErrors(), + ...vitest.state.getUnhandledErrors(), ]) } } @@ -65,9 +65,9 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { checker.onParseStart(async () => { const files = checker.getTestFiles() for (const file of files) { - await ctx._testRun.enqueued(project, file) + await vitest._testRun.enqueued(project, file) } - await ctx._testRun.collected(project, files) + await vitest._testRun.collected(project, files) }) checker.onParseEnd(result => onParseEnd(project, result)) @@ -75,9 +75,9 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { checker.onWatcherRerun(async () => { rerunTriggered.add(project) - if (!ctx.runningPromise) { - ctx.state.clearErrors() - await ctx.report( + if (!vitest.runningPromise) { + vitest.state.clearErrors() + await vitest.report( 'onWatcherRerun', files, 'File change detected. Triggering rerun.', @@ -88,15 +88,14 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { const testFiles = checker.getTestFiles() for (const file of testFiles) { - await ctx._testRun.enqueued(project, file) + await vitest._testRun.enqueued(project, file) } - await ctx._testRun.collected(project, testFiles) + await vitest._testRun.collected(project, testFiles) const { packs, events } = checker.getTestPacksAndEvents() - await ctx._testRun.updated(packs, events) + await vitest._testRun.updated(packs, events) }) - await checker.prepare() return checker } @@ -118,7 +117,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { checker.setFiles(files) await checker.collectTests() const testFiles = checker.getTestFiles() - ctx.state.collectFiles(project, testFiles) + vitest.state.collectFiles(project, testFiles) } } @@ -147,9 +146,9 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { if (project.typechecker && !triggered) { const testFiles = project.typechecker.getTestFiles() for (const file of testFiles) { - await ctx._testRun.enqueued(project, file) + await vitest._testRun.enqueued(project, file) } - await ctx._testRun.collected(project, testFiles) + await vitest._testRun.collected(project, testFiles) await onParseEnd(project, project.typechecker.getResult()) continue } @@ -166,7 +165,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { runTests, collectTests, async close() { - const promises = ctx.projects.map(project => + const promises = vitest.projects.map(project => project.typechecker?.stop(), ) await Promise.all(promises) diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 5edf19c58..4d193a8e0 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -70,6 +70,7 @@ export class TestProject { /** @internal */ typechecker?: Typechecker /** @internal */ _config?: ResolvedConfig /** @internal */ _vite?: ViteDevServer + /** @internal */ _hash?: string private runner!: ViteNodeRunner @@ -92,6 +93,18 @@ export class TestProject { this.globalConfig = vitest.config } + /** + * The unique hash of this project. This value is consistent between the reruns. + * + * It is based on the root of the project (not consistent between OS) and its name. + */ + public get hash(): string { + if (!this._hash) { + throw new Error('The server was not set. It means that `project.hash` was called before the Vite server was established.') + } + return this._hash + } + // "provide" is a property, not a method to keep the context when destructed in the global setup, // making it a method would be a breaking change, and can be done in Vitest 3 at minimum /** @@ -601,6 +614,12 @@ export class TestProject { return this._configureServer(options, server) } + private _setHash() { + this._hash = generateHash( + this._config!.root + this._config!.name, + ) + } + /** @internal */ async _configureServer(options: UserConfig, server: ViteDevServer): Promise { this._config = resolveConfig( @@ -611,6 +630,7 @@ export class TestProject { }, server.config, ) + this._setHash() for (const _providedKey in this.config.provide) { const providedKey = _providedKey as keyof ProvidedContext // type is very strict here, so we cast it to any @@ -699,6 +719,7 @@ export class TestProject { project.runner = vitest.runner project._vite = vitest.server project._config = vitest.config + project._setHash() project._provideObject(vitest.config.provide) return project } @@ -713,6 +734,7 @@ export class TestProject { clone.runner = parent.runner clone._vite = parent._vite clone._config = config + clone._setHash() clone._parent = parent clone._provideObject(config.provide) return clone @@ -771,3 +793,16 @@ export async function initializeProject( return project } + +function generateHash(str: string): string { + let hash = 0 + if (str.length === 0) { + return `${hash}` + } + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32bit integer + } + return `${hash}` +} diff --git a/packages/vitest/src/typecheck/parse.ts b/packages/vitest/src/typecheck/parse.ts index 26e72c54a..a61bcb590 100644 --- a/packages/vitest/src/typecheck/parse.ts +++ b/packages/vitest/src/typecheck/parse.ts @@ -1,12 +1,5 @@ -import type { TypecheckConfig } from '../node/types/config' import type { RawErrsMap, TscErrorInfo } from './types' -import { writeFile } from 'node:fs/promises' -import os from 'node:os' -import url from 'node:url' -import { getTsconfig as getTsconfigContent } from 'get-tsconfig' -import { basename, dirname, join, resolve } from 'pathe' -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const newLineRegExp = /\r?\n/ const errCodeRegExp = /error TS(?\d+)/ @@ -63,50 +56,6 @@ export async function makeTscErrorInfo( ] } -export async function getTsconfig(root: string, config: TypecheckConfig): Promise<{ - path: string - config: Record -}> { - const configName = config.tsconfig ? basename(config.tsconfig) : undefined - const configSearchPath = config.tsconfig - ? dirname(resolve(root, config.tsconfig)) - : root - - const tsconfig = getTsconfigContent(configSearchPath, configName) - - if (!tsconfig) { - throw new Error('no tsconfig.json found') - } - - const tsconfigName = basename(tsconfig.path, '.json') - const tempTsConfigName = `${tsconfigName}.vitest-temp.json` - const tempTsbuildinfoName = `${tsconfigName}.tmp.tsbuildinfo` - - const tempConfigPath = join( - dirname(tsconfig.path), - tempTsConfigName, - ) - - try { - const tmpTsConfig: Record = { ...tsconfig.config } - - tmpTsConfig.compilerOptions = tmpTsConfig.compilerOptions || {} - tmpTsConfig.compilerOptions.emitDeclarationOnly = false - tmpTsConfig.compilerOptions.incremental = true - tmpTsConfig.compilerOptions.tsBuildInfoFile = join( - process.versions.pnp ? join(os.tmpdir(), 'vitest') : __dirname, - tempTsbuildinfoName, - ) - - const tsconfigFinalContent = JSON.stringify(tmpTsConfig, null, 2) - await writeFile(tempConfigPath, tsconfigFinalContent) - return { path: tempConfigPath, config: tmpTsConfig } - } - catch (err) { - throw new Error(`failed to write ${tempTsConfigName}`, { cause: err }) - } -} - export async function getRawErrsMapFromTsCompile(tscErrorStdout: string): Promise { const rawErrsMap: RawErrsMap = new Map() diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index e0470a8cb..41eb337b8 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -8,14 +8,15 @@ import type { TestProject } from '../node/project' import type { Awaitable } from '../types/general' import type { FileInformation } from './collect' import type { TscErrorInfo } from './types' -import { rm } from 'node:fs/promises' +import os from 'node:os' import { performance } from 'node:perf_hooks' import { eachMapping, generatedPositionFor, TraceMap } from '@vitest/utils/source-map' -import { basename, extname, resolve } from 'pathe' +import { basename, join, resolve } from 'pathe' import { x } from 'tinyexec' +import { distDir } from '../paths' import { convertTasksToEvents } from '../utils/tasks' import { collectTests } from './collect' -import { getRawErrsMapFromTsCompile, getTsconfig } from './parse' +import { getRawErrsMapFromTsCompile } from './parse' import { createIndexMap } from './utils' export class TypeCheckError extends Error { @@ -49,13 +50,11 @@ export class Typechecker { private _startTime = 0 private _output = '' private _tests: Record | null = {} - private tempConfigPath?: string - private allowJs?: boolean private process?: ChildProcess protected files: string[] = [] - constructor(protected ctx: TestProject) {} + constructor(protected project: TestProject) {} public setFiles(files: string[]): void { this.files = files @@ -76,14 +75,11 @@ export class Typechecker { protected async collectFileTests( filepath: string, ): Promise { - return collectTests(this.ctx, filepath) + return collectTests(this.project, filepath) } protected getFiles(): string[] { - return this.files.filter((filename) => { - const extension = extname(filename) - return extension !== '.js' || this.allowJs - }) + return this.files } public async collectTests(): Promise> { @@ -225,7 +221,7 @@ export class Typechecker { { error: TypeCheckError; originalError: TscErrorInfo }[] >() errorsMap.forEach((errors, path) => { - const filepath = resolve(this.ctx.config.root, path) + const filepath = resolve(this.project.config.root, path) const suiteErrors = errors.map((info) => { const limit = Error.stackTraceLimit Error.stackTraceLimit = 0 @@ -260,14 +256,7 @@ export class Typechecker { return typesErrors } - public async clear(): Promise { - if (this.tempConfigPath) { - await rm(this.tempConfigPath, { force: true }) - } - } - public async stop(): Promise { - await this.clear() this.process?.kill() this.process = undefined } @@ -280,15 +269,6 @@ export class Typechecker { await ctx.packageInstaller.ensureInstalled(packageName, ctx.config.root) } - public async prepare(): Promise { - const { root, typecheck } = this.ctx.config - - const { config, path } = await getTsconfig(root, typecheck) - - this.tempConfigPath = path - this.allowJs = typecheck.allowJs || config.allowJs || false - } - public getExitCode(): number | false { return this.process?.exitCode != null && this.process.exitCode } @@ -302,20 +282,29 @@ export class Typechecker { return } - if (!this.tempConfigPath) { - throw new Error('tsconfig was not initialized') - } - - const { root, watch, typecheck } = this.ctx.config - - const args = ['--noEmit', '--pretty', 'false', '-p', this.tempConfigPath] - // use builtin watcher, because it's faster + 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', + ), + ] + // use builtin watcher because it's faster if (watch) { args.push('--watch') } if (typecheck.allowJs) { args.push('--allowJs', '--checkJs') } + if (typecheck.tsconfig) { + args.push('-p', resolve(root, typecheck.tsconfig)) + } this._output = '' this._startTime = performance.now() const child = x(typecheck.checker, args, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b9666c02..6bb231822 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1073,9 +1073,6 @@ importers: flatted: specifier: 'catalog:' version: 3.3.3 - get-tsconfig: - specifier: ^4.10.0 - version: 4.10.0 happy-dom: specifier: ^17.4.4 version: 17.4.6 diff --git a/test/reporters/tsconfig.json b/test/reporters/tsconfig.json index 57e707262..791c9498a 100644 --- a/test/reporters/tsconfig.json +++ b/test/reporters/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.build.json", "include": ["./tests/**/*.test-d.ts"] } diff --git a/test/typescript/package.json b/test/typescript/package.json index 6f826f281..987881038 100644 --- a/test/typescript/package.json +++ b/test/typescript/package.json @@ -3,7 +3,7 @@ "type": "module", "scripts": { "test": "vitest --test-timeout 60000", - "types": "vitest --typecheck --run", + "types": "vitest --typecheck.only --typecheck.allowJs --run", "tsc": "tsc --watch --pretty false --noEmit" }, "dependencies": { diff --git a/test/typescript/test/__snapshots__/runner.test.ts.snap b/test/typescript/test/__snapshots__/runner.test.ts.snap index 547dd7ad2..266ad1763 100644 --- a/test/typescript/test/__snapshots__/runner.test.ts.snap +++ b/test/typescript/test/__snapshots__/runner.test.ts.snap @@ -96,7 +96,7 @@ Vitest caught 1 unhandled error during the test run. This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected. ⎯⎯⎯⎯⎯⎯ Typecheck Error ⎯⎯⎯⎯⎯⎯⎯ -Error: error TS18003: No inputs were found in config file '/tsconfig.empty.vitest-temp.json'. Specified 'include' paths were '["src"]' and 'exclude' paths were '["**/dist/**"]'. +Error: error TS18003: No inputs were found in config file '/tsconfig.empty.json'. Specified 'include' paths were '["src"]' and 'exclude' paths were '["**/dist/**"]'. ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ diff --git a/test/typescript/tsconfig.custom.json b/test/typescript/tsconfig.custom.json index 6f480114f..74f6daba3 100644 --- a/test/typescript/tsconfig.custom.json +++ b/test/typescript/tsconfig.custom.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.build.json", "include": [ "**/fail.test-d.ts" ], diff --git a/test/typescript/tsconfig.empty.json b/test/typescript/tsconfig.empty.json index 6189af55b..fe6a2c24a 100644 --- a/test/typescript/tsconfig.empty.json +++ b/test/typescript/tsconfig.empty.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.build.json", "include": [ "src" ], diff --git a/test/typescript/tsconfig.fails.json b/test/typescript/tsconfig.fails.json index 976ac5b3b..f912e9d7a 100644 --- a/test/typescript/tsconfig.fails.json +++ b/test/typescript/tsconfig.fails.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.build.json", "include": [ "./failing/*" ], diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json index f7781f12f..7663d8043 100644 --- a/test/typescript/tsconfig.json +++ b/test/typescript/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.build.json", "include": [ "./**/*.ts", "./**/*.js" diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 000000000..4e6d50061 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "paths": { + "vitest": ["./packages/vitest/dist/index.d.ts"] + } + } +}