diff --git a/README.md b/README.md index 37c98d30..1e58235b 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,7 @@ These options are resolved relative to the [workspace file](https://code.visuals - `vitest.debuggerPort`: Port that the debugger will be attached to. By default uses 9229 or tries to find a free port if it's not available. - `vitest.debuggerAddress`: TCP/IP address of process to be debugged. Default: localhost -> [!NOTE] -> The `vitest.nodeExecutable` and `vitest.nodeExecArgs` settings are used as `execPath` and `execArgv` when spawning a new `child_process`, and as `runtimeExecutable` and `runtimeArgs` when [debugging a test](https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md). +> 💡 The `vitest.nodeExecutable` and `vitest.nodeExecArgs` settings are used as `execPath` and `execArgv` when spawning a new `child_process`, and as `runtimeExecutable` and `runtimeArgs` when [debugging a test](https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md). > The `vitest.terminalShellPath` and `vitest.terminalShellArgs` settings are used as `shellPath` and `shellArgs` when creating a new [terminal](https://code.visualstudio.com/api/references/vscode-api#Terminal) ### Other Options @@ -96,6 +95,10 @@ These options are resolved relative to the [workspace file](https://code.visuals - `vitest.maximumConfigs`: The maximum amount of configs that Vitest extension can load. If exceeded, the extension will show a warning suggesting to use a workspace config file. Default: `3` - `vitest.logLevel`: How verbose should the logger be in the "Output" channel. Default: `info` +### Experimental + +If the extension hangs, consider enabling `vitest.experimentalStaticAstCollect` option to use static analysis instead of actually running the test file every time you make a change which can cause visible hangs if it takes a long time to setup the test. + ## FAQs (Frequently Asked Questions) ### How can I use it in monorepo? diff --git a/package.json b/package.json index 46bde730..7ed8e7bf 100644 --- a/package.json +++ b/package.json @@ -207,6 +207,12 @@ "markdownDescription": "The arguments to pass to the shell executable. This is applied only when `vitest.shellType` is `terminal`.", "type": "array", "scope": "resource" + }, + "vitest.experimentalStaticAstCollect": { + "markdownDescription": "Enable static AST analysis for faster test discovery. This feature is experimental and may not work with all projects.", + "type": "boolean", + "default": false, + "scope": "resource" } } } diff --git a/samples/basic/.vscode/settings.json b/samples/basic/.vscode/settings.json index 53d6bf68..0842d6a8 100644 --- a/samples/basic/.vscode/settings.json +++ b/samples/basic/.vscode/settings.json @@ -2,6 +2,7 @@ "vitest.nodeEnv": { "TEST_CUSTOM_ENV": "hello" }, + "vitest.experimentalStaticAstCollect": true, "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" } diff --git a/samples/basic/vitest.config.ts b/samples/basic/vitest.config.ts index 3214bef8..eeae7f53 100644 --- a/samples/basic/vitest.config.ts +++ b/samples/basic/vitest.config.ts @@ -6,7 +6,7 @@ import { defineConfig } from 'vite' export default defineConfig({ esbuild: { - target: 'es2020', + target: 'es2022', }, test: { include: ['src/should_included_test.ts', 'test/**/*.test.ts'], diff --git a/src/api/child_process.ts b/src/api/child_process.ts index 6a616232..dc9a035a 100644 --- a/src/api/child_process.ts +++ b/src/api/child_process.ts @@ -117,6 +117,7 @@ async function createChildVitestProcess(pkg: VitestPackage) { : undefined, }, debug: false, + astCollect: getConfig(pkg.folder).experimentalStaticAstCollect, } vitest.send(runnerOptions) diff --git a/src/api/ws.ts b/src/api/ws.ts index e6a4a994..6dc1d3e1 100644 --- a/src/api/ws.ts +++ b/src/api/ws.ts @@ -86,6 +86,7 @@ export function waitForWsResolvedMeta( : undefined, }, debug, + astCollect: getConfig(pkg.folder).experimentalStaticAstCollect, } ws.send(JSON.stringify(runnerOptions)) diff --git a/src/config.ts b/src/config.ts index a35dbf72..08f126ee 100644 --- a/src/config.ts +++ b/src/config.ts @@ -59,6 +59,8 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { const shellType = get<'child_process' | 'terminal'>('shellType', 'child_process') const nodeExecArgs = get('nodeExecArgs') + const experimentalStaticAstCollect = get('experimentalStaticAstCollect', false)! + return { env: get>('nodeEnv', null), debugExclude: get('debugExclude', []), @@ -67,6 +69,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { terminalShellPath, shellType, nodeExecArgs, + experimentalStaticAstCollect, vitestPackagePath: resolvedVitestPackagePath, workspaceConfig: resolveConfigPath(workspaceConfig), rootConfig: resolveConfigPath(rootConfigFile), diff --git a/src/extension.ts b/src/extension.ts index cebbe9a2..f01a3ad0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -283,6 +283,7 @@ class VitestExtension { 'vitest.terminalShellArgs', 'vitest.terminalShellPath', 'vitest.filesWatcherInclude', + 'vitest.experimentalStaticAstCollect', ] this.disposables = [ diff --git a/src/testTree.ts b/src/testTree.ts index 70055b31..d2f14be3 100644 --- a/src/testTree.ts +++ b/src/testTree.ts @@ -280,6 +280,7 @@ export class TestTree extends vscode.Disposable { if (file.result?.errors) { const error = file.result.errors.map(error => error.stack || error.message).join('\n') fileTestItem.error = error + log.error(`Error in ${file.filepath}`, error) } else if (!file.tasks.length) { fileTestItem.error = `No tests found in ${file.filepath}` diff --git a/src/worker/collect.ts b/src/worker/collect.ts index 2bd0c56a..e86a0f21 100644 --- a/src/worker/collect.ts +++ b/src/worker/collect.ts @@ -182,8 +182,9 @@ export function astParseFile(filepath: string, code: string) { export async function astCollectTests( ctx: WorkspaceProject, filepath: string, + transformMode: 'web' | 'ssr', ): Promise { - const request = await ctx.vitenode.transformRequest(filepath, filepath, 'web') + const request = await ctx.vitenode.transformRequest(filepath, filepath, transformMode) // TODO: error cannot parse const testFilepath = relative(ctx.config.root, filepath) if (!request) { diff --git a/src/worker/types.ts b/src/worker/types.ts index f98ac585..8311f610 100644 --- a/src/worker/types.ts +++ b/src/worker/types.ts @@ -14,6 +14,7 @@ export interface WorkerRunnerOptions { type: 'init' meta: WorkerMeta debug: boolean + astCollect: boolean } export interface EventReady { diff --git a/src/worker/vitest.ts b/src/worker/vitest.ts index 05cc3875..77bf0aaf 100644 --- a/src/worker/vitest.ts +++ b/src/worker/vitest.ts @@ -24,6 +24,7 @@ export class Vitest implements VitestMethods { constructor( public readonly ctx: VitestCore, private readonly debug = false, + public readonly alwaysAstCollect = false, ) { this.watcher = new VitestWatcher(this) this.coverage = new VitestCoverage(ctx, this) @@ -34,37 +35,42 @@ export class Vitest implements VitestMethods { } public async collectTests(files: [projectName: string, filepath: string][]) { - const browserTests: [project: WorkspaceProject, filepath: string][] = [] + const astCollect: [project: WorkspaceProject, filepath: string][] = [] const otherTests: [project: WorkspaceProject, filepath: string][] = [] for (const [projectName, filepath] of files) { const project = this.ctx.projects.find(project => project.getName() === projectName) assert(project, `Project ${projectName} not found for file ${filepath}`) - if (project.config.browser.enabled) { - browserTests.push([project, filepath]) + if (this.alwaysAstCollect || project.config.browser.enabled) { + astCollect.push([project, filepath]) } else { otherTests.push([project, filepath]) } } - if (browserTests.length) { - await this.astCollect(browserTests) - } - - if (otherTests.length) { - const files = otherTests.map(([_, filepath]) => filepath) + await Promise.all([ + (async () => { + if (astCollect.length) { + await this.astCollect(astCollect, 'web') + } + })(), + (async () => { + if (otherTests.length) { + const files = otherTests.map(([_, filepath]) => filepath) - try { - await this.runTestFiles(files, Vitest.COLLECT_NAME_PATTERN) - } - finally { - this.setTestNamePattern(undefined) - } - } + try { + await this.runTestFiles(files, Vitest.COLLECT_NAME_PATTERN) + } + finally { + this.setTestNamePattern(undefined) + } + } + })(), + ]) } - public async astCollect(specs: [project: WorkspaceProject, file: string][]) { + public async astCollect(specs: [project: WorkspaceProject, file: string][], transformMode: 'web' | 'ssr') { if (!specs.length) { return } @@ -72,7 +78,7 @@ export class Vitest implements VitestMethods { const runConcurrently = limitConcurrency(5) const promises = specs.map(([project, filename]) => runConcurrently( - () => astCollectTests(project, filename), + () => astCollectTests(project, filename, transformMode), )) const result = await Promise.all(promises) const files = result.filter(r => r != null).map((r => r!.file)) diff --git a/src/worker/watcher.ts b/src/worker/watcher.ts index a9e5c28a..9cece996 100644 --- a/src/worker/watcher.ts +++ b/src/worker/watcher.ts @@ -46,20 +46,20 @@ export class VitestWatcher { const tests = Array.from(this.changedTests) const specs = tests.flatMap(file => this.getProjectsByTestFile(file)) - const browserSpecs: [project: WorkspaceProject, file: string][] = [] + const astSpecs: [project: WorkspaceProject, file: string][] = [] for (const [project, file] of specs) { - if (project.config.browser.enabled) { - browserSpecs.push([project, file]) + if (vitest.alwaysAstCollect || project.config.browser.enabled) { + astSpecs.push([project, file]) } } ctx.configOverride.testNamePattern = new RegExp(Vitest.COLLECT_NAME_PATTERN) ctx.logger.log('Collecting tests due to file changes:', ...files.map(f => relative(ctx.config.root, f))) - if (browserSpecs.length) { + if (astSpecs.length) { ctx.logger.log('Collecting using AST explorer...') - await vitest.astCollect(browserSpecs) + await vitest.astCollect(astSpecs, 'web') this.changedTests.clear() return await originalScheduleRerun.call(this, []) } diff --git a/src/worker/worker.ts b/src/worker/worker.ts index 8652cec4..fe263e8a 100644 --- a/src/worker/worker.ts +++ b/src/worker/worker.ts @@ -31,7 +31,7 @@ emitter.on('message', async function onMessage(message: any) { : {}, ) - const rpc = createWorkerRPC(new Vitest(vitest, data.debug), { + const rpc = createWorkerRPC(new Vitest(vitest, data.debug, data.astCollect), { on(listener) { emitter.on('message', listener) },