[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix: filter projects eagerly during config resolution (#7313)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(Jan 23, 2025, 2:00 PM +0100) dff4406d ed9aeba2

+359 -84
+5 -3
test/test-utils/index.ts
··· 6 6 import fs from 'node:fs' 7 7 import { Readable, Writable } from 'node:stream' 8 8 import { fileURLToPath } from 'node:url' 9 + import { inspect } from 'node:util' 9 10 import { dirname, resolve } from 'pathe' 10 11 import { x } from 'tinyexec' 11 12 import * as tinyrainbow from 'tinyrainbow' ··· 17 18 // override default colors to disable them in tests 18 19 Object.assign(tinyrainbow.default, tinyrainbow.getDefaultColors()) 19 20 20 - interface VitestRunnerCLIOptions { 21 + export interface VitestRunnerCLIOptions { 21 22 std?: 'inherit' 22 23 fails?: boolean 23 24 preserveAnsi?: boolean ··· 101 102 console.error(e) 102 103 } 103 104 thrown = true 104 - cli.stderr += e.stack 105 + cli.stderr += inspect(e) 105 106 } 106 107 finally { 107 108 exitCode = process.exitCode ··· 293 294 export async function runInlineTests( 294 295 structure: Record<string, string | ViteUserConfig | WorkspaceProjectConfiguration[]>, 295 296 config?: UserConfig, 297 + options?: VitestRunnerCLIOptions, 296 298 ) { 297 299 const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 298 300 const fs = useFS(root, structure) 299 301 const vitest = await runVitest({ 300 302 root, 301 303 ...config, 302 - }) 304 + }, [], 'test', {}, options) 303 305 return { 304 306 fs, 305 307 root,
+3 -1
packages/utils/src/source-map.ts
··· 167 167 } 168 168 169 169 // normalize Windows path (\ -> /) 170 - file = resolve(file) 170 + file = file.startsWith('node:') || file.startsWith('internal:') 171 + ? file 172 + : resolve(file) 171 173 172 174 if (method) { 173 175 method = method.replace(/__vite_ssr_import_\d+__\./g, '')
+1 -1
test/browser/specs/browser-crash.test.ts
··· 13 13 }, 14 14 }) 15 15 16 - expect(stderr).contains('Page crashed when executing tests') 16 + expect(stderr).toContain('Page crashed when executing tests') 17 17 })
+62
test/config/test/browser-configs.test.ts
··· 198 198 ]) 199 199 }) 200 200 201 + test('coverage provider v8 works correctly in workspaced browser mode if instances are filtered', async () => { 202 + const { projects } = await vitest({ 203 + project: 'browser (chromium)', 204 + workspace: [ 205 + { 206 + test: { 207 + name: 'browser', 208 + browser: { 209 + enabled: true, 210 + provider: 'playwright', 211 + instances: [ 212 + { browser: 'chromium' }, 213 + { browser: 'firefox' }, 214 + { browser: 'webkit' }, 215 + ], 216 + }, 217 + }, 218 + }, 219 + ], 220 + coverage: { 221 + enabled: true, 222 + provider: 'v8', 223 + }, 224 + }) 225 + expect(projects.map(p => p.name)).toEqual([ 226 + 'browser (chromium)', 227 + ]) 228 + }) 229 + 230 + test('filter for the global browser project includes all browser instances', async () => { 231 + const { projects } = await vitest({ 232 + project: 'myproject', 233 + workspace: [ 234 + { 235 + test: { 236 + name: 'myproject', 237 + browser: { 238 + enabled: true, 239 + provider: 'playwright', 240 + headless: true, 241 + instances: [ 242 + { browser: 'chromium' }, 243 + { browser: 'firefox' }, 244 + { browser: 'webkit' }, 245 + ], 246 + }, 247 + }, 248 + }, 249 + { 250 + test: { 251 + name: 'skip', 252 + }, 253 + }, 254 + ], 255 + }) 256 + expect(projects.map(p => p.name)).toEqual([ 257 + 'myproject (chromium)', 258 + 'myproject (firefox)', 259 + 'myproject (webkit)', 260 + ]) 261 + }) 262 + 201 263 test('can enable browser-cli options for multi-project workspace', async () => { 202 264 const { projects } = await vitest( 203 265 {
+17 -13
test/config/test/failures.test.ts
··· 1 1 import type { UserConfig } from 'vitest/node' 2 + import type { VitestRunnerCLIOptions } from '../../test-utils' 2 3 import { normalize, resolve } from 'pathe' 3 - import { beforeEach, expect, test } from 'vitest' 4 4 5 + import { beforeEach, expect, test } from 'vitest' 5 6 import { version } from 'vitest/package.json' 6 7 import * as testUtils from '../../test-utils' 7 8 ··· 9 10 const names = ['edge', 'chromium', 'webkit', 'chrome', 'firefox', 'safari'] as const 10 11 const browsers = providers.map(provider => names.map(name => ({ name, provider }))).flat() 11 12 12 - function runVitest(config: NonNullable<UserConfig> & { shard?: any }) { 13 - return testUtils.runVitest({ root: './fixtures/test', ...config }, []) 13 + function runVitest(config: NonNullable<UserConfig> & { shard?: any }, runnerOptions?: VitestRunnerCLIOptions) { 14 + return testUtils.runVitest({ root: './fixtures/test', ...config }, [], undefined, {}, runnerOptions) 14 15 } 15 16 16 17 function runVitestCli(...cliArgs: string[]) { ··· 286 287 }) 287 288 288 289 test('v8 coverage provider cannot be used in workspace without playwright + chromium', async () => { 289 - const { stderr } = await runVitest({ coverage: { enabled: true }, workspace: './fixtures/workspace/browser/workspace-with-browser.ts' }) 290 + const { stderr } = await runVitest({ 291 + coverage: { enabled: true }, 292 + workspace: './fixtures/workspace/browser/workspace-with-browser.ts', 293 + }, { fails: true }) 290 294 expect(stderr).toMatch( 291 295 `Error: @vitest/coverage-v8 does not work with 292 - { 293 - "browser": { 294 - "provider": "webdriverio", 295 - "instances": [ 296 - { 297 - "browser": "chrome" 296 + { 297 + "browser": { 298 + "provider": "webdriverio", 299 + "instances": [ 300 + { 301 + "browser": "chrome" 302 + } 303 + ] 298 304 } 299 - ] 300 - } 301 - }`, 305 + }`, 302 306 ) 303 307 }) 304 308
+21
test/config/test/workspace.test.ts
··· 134 134 expect(stderr).toBe('') 135 135 expect(stdout).toContain('repro.test.js > importing a virtual module') 136 136 }) 137 + 138 + it('fails if workspace is empty', async () => { 139 + const { stderr } = await runVitest({ 140 + workspace: [], 141 + }) 142 + expect(stderr).toContain('No projects were found. Make sure your configuration is correct. The workspace: [].') 143 + }) 144 + 145 + it('fails if workspace is filtered by the project', async () => { 146 + const { stderr } = await runVitest({ 147 + project: 'non-existing', 148 + root: 'fixtures/workspace/config-empty', 149 + config: './vitest.config.js', 150 + workspace: [ 151 + './vitest.config.js', 152 + ], 153 + }) 154 + expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: non-existing. The workspace: [ 155 + "./vitest.config.js" 156 + ].`) 157 + })
+1 -1
test/core/test/cli-test.test.ts
··· 292 292 clearScreen: viteClearScreen, 293 293 } 294 294 const vitestConfig = getCLIOptions(vitestClearScreen) 295 - const config = resolveConfig('test', vitestConfig, viteConfig, undefined as any) 295 + const config = resolveConfig({ logger: undefined, mode: 'test' } as any, vitestConfig, viteConfig) 296 296 return config.clearScreen 297 297 }) 298 298 expect(results).toMatchInlineSnapshot(`
+44
test/watch/test/change-project.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { runInlineTests } from '../../test-utils' 3 + 4 + test('reruns tests when config changes', async () => { 5 + const { vitest, ctx } = await runInlineTests({ 6 + 'vitest.config.ts': ` 7 + 8 + process.stdin.isTTY = true 9 + process.stdin.setRawMode = () => process.stdin 10 + 11 + export default { 12 + test: { 13 + workspace: [ 14 + './project-1', 15 + './project-2', 16 + ], 17 + }, 18 + }`, 19 + 'project-1/vitest.config.ts': { test: { name: 'project-1' } }, 20 + 'project-1/basic-1.test.ts': /* ts */` 21 + import { test } from 'vitest' 22 + test('basic test 1', () => {}) 23 + `, 24 + 'project-2/vitest.config.ts': { test: { name: 'project-2' } }, 25 + 'project-2/basic-2.test.ts': /* ts */` 26 + import { test } from 'vitest' 27 + test('basic test 2', () => {}) 28 + `, 29 + }, { watch: true }) 30 + 31 + await vitest.waitForStdout('Waiting for file changes') 32 + 33 + expect(vitest.stdout).toContain('2 passed') 34 + expect(vitest.stdout).toContain('basic-1.test.ts') 35 + expect(vitest.stdout).toContain('basic-2.test.ts') 36 + vitest.resetOutput() 37 + 38 + await ctx!.changeProjectName('project-2') 39 + 40 + await vitest.waitForStdout('Waiting for file changes') 41 + 42 + expect(vitest.stdout).toContain('1 passed') 43 + expect(vitest.stdout).toContain('basic-2.test.ts') 44 + })
+3
packages/browser/src/node/pool.ts
··· 133 133 } 134 134 await project._initBrowserProvider() 135 135 136 + if (!project.browser) { 137 + throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ''}. This is a bug in Vitest. Please, open a new issue with reproduction.`) 138 + } 136 139 await executeTests(method, project, files) 137 140 } 138 141 }
+38 -18
packages/vitest/src/node/core.ts
··· 40 40 import { StateManager } from './state' 41 41 import { TestRun } from './test-run' 42 42 import { VitestWatcher } from './watcher' 43 - import { resolveBrowserWorkspace, resolveWorkspace } from './workspace/resolveWorkspace' 43 + import { getDefaultTestProject, resolveBrowserWorkspace, resolveWorkspace } from './workspace/resolveWorkspace' 44 44 45 45 const WATCHER_DEBOUNCE = 100 46 46 ··· 90 90 /** @internal */ closingPromise?: Promise<void> 91 91 /** @internal */ isCancelling = false 92 92 /** @internal */ coreWorkspaceProject: TestProject | undefined 93 - /** @internal */ resolvedProjects: TestProject[] = [] 93 + /** 94 + * @internal 95 + * @deprecated 96 + */ 97 + resolvedProjects: TestProject[] = [] 94 98 /** @internal */ _browserLastPort = defaultBrowserPort 95 99 /** @internal */ _browserSessions = new BrowserSessions() 96 100 /** @internal */ _options: UserConfig = {} ··· 98 102 /** @internal */ vitenode: ViteNodeServer = undefined! 99 103 /** @internal */ runner: ViteNodeRunner = undefined! 100 104 /** @internal */ _testRun: TestRun = undefined! 105 + /** @internal */ _projectFilters: RegExp[] = [] 101 106 102 107 private isFirstRun = true 103 108 private restartsCount = 0 ··· 211 216 this.specifications.clearCache() 212 217 this._onUserTestsRerun = [] 213 218 214 - const resolved = resolveConfig(this.mode, options, server.config, this.logger) 215 - 219 + this._projectFilters = toArray(options.project || []).map(project => wildcardPatternToRegExp(project)) 216 220 this._vite = server 221 + 222 + const resolved = resolveConfig(this, options, server.config) 223 + 217 224 this._config = resolved 218 225 this._state = new StateManager() 219 226 this._cache = new VitestCache(this.version) ··· 272 279 const projects = await this.resolveWorkspace(cliOptions) 273 280 this.resolvedProjects = projects 274 281 this.projects = projects 275 - const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s)) 276 - if (filters.length > 0) { 277 - this.projects = this.projects.filter(p => 278 - filters.some(pattern => pattern.test(p.name)), 279 - ) 280 - if (!this.projects.length) { 281 - throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) 282 - } 282 + if (!this.projects.length) { 283 + throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) 283 284 } 284 285 if (!this.coreWorkspaceProject) { 285 286 this.coreWorkspaceProject = TestProject._createBasicProject(this) ··· 397 398 398 399 this._workspaceConfigPath = workspaceConfigPath 399 400 401 + // user doesn't have a workspace config, return default project 400 402 if (!workspaceConfigPath) { 401 - return resolveBrowserWorkspace(this, new Set(), [this._ensureRootProject()]) 403 + // user can filter projects with --project flag, `getDefaultTestProject` 404 + // returns the project only if it matches the filter 405 + const project = getDefaultTestProject(this) 406 + if (!project) { 407 + return [] 408 + } 409 + return resolveBrowserWorkspace(this, new Set(), [project]) 402 410 } 403 411 404 412 const workspaceModule = await this.import<{ ··· 858 866 /** @internal */ 859 867 async changeProjectName(pattern: string): Promise<void> { 860 868 if (pattern === '') { 861 - delete this.configOverride.project 869 + this.configOverride.project = undefined 870 + this._projectFilters = [] 862 871 } 863 872 else { 864 - this.configOverride.project = pattern 873 + this.configOverride.project = [pattern] 874 + this._projectFilters = [wildcardPatternToRegExp(pattern)] 865 875 } 866 876 867 - this.projects = this.resolvedProjects.filter(p => p.name === pattern) 868 - const files = (await this.globTestSpecifications()).map(spec => spec.moduleId) 869 - await this.rerunFiles(files, 'change project filter', pattern === '') 877 + await this.vite.restart() 870 878 } 871 879 872 880 /** @internal */ ··· 1246 1254 /** @internal */ 1247 1255 onAfterSetServer(fn: OnServerRestartHandler): void { 1248 1256 this._onSetServer.push(fn) 1257 + } 1258 + 1259 + /** 1260 + * Check if the project with a given name should be included. 1261 + * @internal 1262 + */ 1263 + _matchesProjectFilter(name: string): boolean { 1264 + // no filters applied, any project can be included 1265 + if (!this._projectFilters.length) { 1266 + return true 1267 + } 1268 + return this._projectFilters.some(filter => filter.test(name)) 1249 1269 } 1250 1270 } 1251 1271
+8
packages/vitest/src/node/errors.ts
··· 39 39 + `are not supported. Consider specifying the exact line numbers of your tests.`) 40 40 } 41 41 } 42 + 43 + export class VitestFilteredOutProjectError extends Error { 44 + code = 'VITEST_FILTERED_OUT_PROJECT' 45 + 46 + constructor() { 47 + super('VITEST_FILTERED_OUT_PROJECT') 48 + } 49 + }
+1 -2
packages/vitest/src/node/project.ts
··· 577 577 /** @internal */ 578 578 async _configureServer(options: UserConfig, server: ViteDevServer): Promise<void> { 579 579 this._config = resolveConfig( 580 - this.vitest.mode, 580 + this.vitest, 581 581 { 582 582 ...options, 583 583 coverage: this.vitest.config.coverage, 584 584 }, 585 585 server.config, 586 - this.vitest.logger, 587 586 ) 588 587 for (const _providedKey in this.config.provide) { 589 588 const providedKey = _providedKey as keyof ProvidedContext
+1 -2
packages/vitest/src/node/stdin.ts
··· 2 2 import type { Vitest } from './core' 3 3 import readline from 'node:readline' 4 4 import { getTests } from '@vitest/runner/utils' 5 - import { toArray } from '@vitest/utils' 6 5 import { relative, resolve } from 'pathe' 7 6 import prompt from 'prompts' 8 7 import c from 'tinyrainbow' ··· 182 181 name: 'filter', 183 182 type: 'text', 184 183 message: 'Input a single project name', 185 - initial: toArray(ctx.configOverride.project)[0] || '', 184 + initial: ctx.config.project[0] || '', 186 185 }, 187 186 ]) 188 187 on()
+10 -11
packages/vitest/src/node/config/resolveConfig.ts
··· 1 1 import type { ResolvedConfig as ResolvedViteConfig } from 'vite' 2 - import type { Logger } from '../logger' 2 + import type { Vitest } from '../core' 3 3 import type { BenchmarkBuiltinReporters } from '../reporters' 4 4 import type { 5 5 ApiConfig, 6 6 ResolvedConfig, 7 7 UserConfig, 8 - VitestRunMode, 9 8 } from '../types/config' 10 9 import type { BaseCoverageOptions, CoverageReporterWithOptions } from '../types/coverage' 11 10 import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options' ··· 20 19 extraInlineDeps, 21 20 } from '../../constants' 22 21 import { benchmarkConfigDefaults, configDefaults } from '../../defaults' 23 - import { wildcardPatternToRegExp } from '../../utils/base' 24 22 import { isCI, stdProvider } from '../../utils/env' 25 23 import { getWorkersCountByPercentage } from '../../utils/workers' 26 24 import { VitestCache } from '../cache' ··· 111 109 } 112 110 113 111 export function resolveConfig( 114 - mode: VitestRunMode, 112 + vitest: Vitest, 115 113 options: UserConfig, 116 114 viteConfig: ResolvedViteConfig, 117 - logger: Logger, 118 115 ): ResolvedConfig { 116 + const mode = vitest.mode 117 + const logger = vitest.logger 119 118 if (options.dom) { 120 119 if ( 121 120 viteConfig.test?.environment != null ··· 142 141 mode, 143 142 } as any as ResolvedConfig 144 143 144 + resolved.project = toArray(resolved.project) 145 145 resolved.provide ??= {} 146 146 147 147 const inspector = resolved.inspect || resolved.inspectBrk ··· 256 256 } 257 257 } 258 258 259 - const playwrightChromiumOnly = isPlaywrightChromiumOnly(resolved) 259 + const playwrightChromiumOnly = isPlaywrightChromiumOnly(vitest, resolved) 260 260 261 261 // Browser-mode "Playwright + Chromium" only features: 262 262 if (browser.enabled && !playwrightChromiumOnly) { ··· 264 264 browser: { 265 265 provider: browser.provider, 266 266 name: browser.name, 267 - instances: browser.instances, 267 + instances: browser.instances?.map(i => ({ browser: i.browser })), 268 268 }, 269 269 } 270 270 ··· 469 469 resolved.forceRerunTriggers.push(...resolved.snapshotSerializers) 470 470 471 471 if (options.resolveSnapshotPath) { 472 - delete (resolved as UserConfig).resolveSnapshotPath 472 + delete (resolved as any).resolveSnapshotPath 473 473 } 474 474 475 475 resolved.pool ??= 'threads' ··· 897 897 return resolvedReporters 898 898 } 899 899 900 - function isPlaywrightChromiumOnly(config: ResolvedConfig) { 900 + function isPlaywrightChromiumOnly(vitest: Vitest, config: ResolvedConfig) { 901 901 const browser = config.browser 902 902 if (!browser || browser.provider !== 'playwright' || !browser.enabled) { 903 903 return false ··· 908 908 if (!browser.instances) { 909 909 return false 910 910 } 911 - const filteredProjects = toArray(config.project).map(p => wildcardPatternToRegExp(p)) 912 911 for (const instance of browser.instances) { 913 912 const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser) 914 913 // browser config is filtered out 915 - if (filteredProjects.length && !filteredProjects.every(p => p.test(name))) { 914 + if (!vitest._matchesProjectFilter(name)) { 916 915 continue 917 916 } 918 917 if (instance.browser !== 'chromium') {
+16 -4
packages/vitest/src/node/plugins/index.ts
··· 63 63 64 64 // store defines for globalThis to make them 65 65 // reassignable when running in worker in src/runtime/setup.ts 66 - const defines: Record<string, any> = deleteDefineConfig(viteConfig); 66 + const defines: Record<string, any> = deleteDefineConfig(viteConfig) 67 67 68 - (options as ResolvedConfig).defines = defines 68 + ;(options as unknown as ResolvedConfig).defines = defines 69 69 70 70 let open: string | boolean | undefined = false 71 71 ··· 145 145 }, 146 146 } 147 147 148 + if (ctx.configOverride.project) { 149 + // project filter was set by the user, so we need to filter the project 150 + options.project = ctx.configOverride.project 151 + } 152 + 148 153 config.customLogger = createViteLogger( 149 154 ctx.logger, 150 155 viteConfig.logLevel || 'warn', ··· 217 222 return config 218 223 }, 219 224 async configResolved(viteConfig) { 220 - const viteConfigTest = (viteConfig.test as any) || {} 225 + const viteConfigTest = (viteConfig.test as UserConfig) || {} 221 226 if (viteConfigTest.watch === false) { 222 - viteConfigTest.run = true 227 + ;(viteConfigTest as any).run = true 223 228 } 224 229 225 230 if ('alias' in viteConfigTest) { ··· 255 260 enumerable: false, 256 261 configurable: true, 257 262 }) 263 + 264 + const originalName = options.name 265 + if (options.browser?.enabled && options.browser?.instances) { 266 + options.browser.instances.forEach((instance) => { 267 + instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser 268 + }) 269 + } 258 270 }, 259 271 configureServer: { 260 272 // runs after vite:import-analysis as it relies on `server` instance on Vite 5
+1 -2
packages/vitest/src/node/plugins/publicConfig.ts
··· 45 45 // Reflect just to avoid type error 46 46 const updatedOptions = Reflect.get(config, '_vitest') as UserConfig 47 47 const vitestConfig = resolveVitestConfig( 48 - 'test', 48 + vitest, 49 49 updatedOptions, 50 50 config, 51 - vitest.logger, 52 51 ) 53 52 return { 54 53 viteConfig: config,
+32 -2
packages/vitest/src/node/plugins/workspace.ts
··· 6 6 import { basename, dirname, relative, resolve } from 'pathe' 7 7 import { configDefaults } from '../../defaults' 8 8 import { generateScopedClassName } from '../../integrations/css/css-modules' 9 + import { VitestFilteredOutProjectError } from '../errors' 9 10 import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' 10 11 import { CoverageTransform } from './coverageTransform' 11 12 import { CSSEnablerPlugin } from './cssEnabler' ··· 62 63 } 63 64 } 64 65 66 + // keep project names to potentially filter it out 67 + const workspaceNames = [name] 68 + if (viteConfig.test?.browser?.enabled) { 69 + if (viteConfig.test.browser.name) { 70 + const browser = viteConfig.test.browser.name 71 + // vitest injects `instances` in this case later on 72 + workspaceNames.push(name ? `${name} (${browser})` : browser) 73 + } 74 + 75 + viteConfig.test.browser.instances?.forEach((instance) => { 76 + // every instance is a potential project 77 + instance.name ??= name ? `${name} (${instance.browser})` : instance.browser 78 + workspaceNames.push(instance.name) 79 + }) 80 + } 81 + 82 + const filters = project.vitest.config.project 83 + // if there is `--project=...` filter, check if any of the potential projects match 84 + // if projects don't match, we ignore the test project altogether 85 + // if some of them match, they will later be filtered again by `resolveWorkspace` 86 + if (filters.length) { 87 + const hasProject = workspaceNames.some((name) => { 88 + return project.vitest._matchesProjectFilter(name) 89 + }) 90 + if (!hasProject) { 91 + throw new VitestFilteredOutProjectError() 92 + } 93 + } 94 + 65 95 const config: ViteConfig = { 66 96 root, 67 97 resolve: { ··· 92 122 fs: { 93 123 allow: resolveFsAllow( 94 124 project.vitest.config.root, 95 - project.vitest.server.config.configFile, 125 + project.vitest.vite.config.configFile, 96 126 ), 97 127 }, 98 128 }, ··· 138 168 } 139 169 } 140 170 config.customLogger = createViteLogger( 141 - project.logger, 171 + project.vitest.logger, 142 172 viteConfig.logLevel || 'warn', 143 173 { 144 174 allowClearScreen: false,
+2
packages/vitest/src/node/types/config.ts
··· 965 965 export interface ResolvedConfig 966 966 extends Omit< 967 967 Required<UserConfig>, 968 + | 'project' 968 969 | 'config' 969 970 | 'filters' 970 971 | 'browser' ··· 1016 1017 api?: ApiConfig 1017 1018 cliExclude?: string[] 1018 1019 1020 + project: string[] 1019 1021 benchmark?: Required< 1020 1022 Omit<BenchmarkUserOptions, 'outputFile' | 'compare' | 'outputJson'> 1021 1023 > &
+89 -23
packages/vitest/src/node/workspace/resolveWorkspace.ts
··· 3 3 import { existsSync, promises as fs } from 'node:fs' 4 4 import os from 'node:os' 5 5 import { limitConcurrency } from '@vitest/runner/utils' 6 - import { deepClone, toArray } from '@vitest/utils' 6 + import { deepClone } from '@vitest/utils' 7 7 import fg from 'fast-glob' 8 8 import { dirname, relative, resolve } from 'pathe' 9 9 import { mergeConfig } from 'vite' 10 10 import { configFiles as defaultConfigFiles } from '../../constants' 11 - import { wildcardPatternToRegExp } from '../../utils/base' 12 11 import { isTTY } from '../../utils/env' 12 + import { VitestFilteredOutProjectError } from '../errors' 13 13 import { initializeProject, TestProject } from '../project' 14 14 import { withLabel } from '../reporters/renderers/utils' 15 15 import { isDynamicPattern } from './fast-glob-pattern' ··· 80 80 for (const path of fileProjects) { 81 81 // if file leads to the root config, then we can just reuse it because we already initialized it 82 82 if (vitest.vite.config.configFile === path) { 83 - projectPromises.push(Promise.resolve(vitest._ensureRootProject())) 83 + const project = getDefaultTestProject(vitest) 84 + if (project) { 85 + projectPromises.push(Promise.resolve(project)) 86 + } 84 87 continue 85 88 } 86 89 ··· 98 101 99 102 // pretty rare case - the glob didn't match anything and there are no inline configs 100 103 if (!projectPromises.length) { 101 - return resolveBrowserWorkspace(vitest, new Set(), [vitest._ensureRootProject()]) 104 + throw new Error( 105 + [ 106 + 'No projects were found. Make sure your configuration is correct. ', 107 + vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.join(', ')}. ` : '', 108 + `The workspace: ${JSON.stringify(workspaceDefinition, null, 4)}.`, 109 + ].join(''), 110 + ) 102 111 } 103 112 104 - const resolvedProjects = await Promise.all(projectPromises) 113 + const resolvedProjectsPromises = await Promise.allSettled(projectPromises) 105 114 const names = new Set<string>() 115 + 116 + const errors: Error[] = [] 117 + const resolvedProjects: TestProject[] = [] 118 + 119 + for (const result of resolvedProjectsPromises) { 120 + if (result.status === 'rejected') { 121 + if (result.reason instanceof VitestFilteredOutProjectError) { 122 + // filter out filtered out projects 123 + continue 124 + } 125 + errors.push(result.reason) 126 + } 127 + else { 128 + resolvedProjects.push(result.value) 129 + } 130 + } 131 + 132 + if (errors.length) { 133 + throw new AggregateError( 134 + errors, 135 + 'Failed to initialize projects. There were errors during workspace setup. See below for more details.', 136 + ) 137 + } 106 138 107 139 // project names are guaranteed to be unique 108 140 for (const project of resolvedProjects) { ··· 136 168 names: Set<string>, 137 169 resolvedProjects: TestProject[], 138 170 ) { 139 - const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) 140 171 const removeProjects = new Set<TestProject>() 141 172 142 173 resolvedProjects.forEach((project) => { 143 174 if (!project.config.browser.enabled) { 144 175 return 145 176 } 146 - const configs = project.config.browser.instances || [] 147 - if (configs.length === 0) { 177 + const instances = project.config.browser.instances || [] 178 + if (instances.length === 0) { 179 + const browser = project.config.browser.name 148 180 // browser.name should be defined, otherwise the config fails in "resolveConfig" 149 - configs.push({ browser: project.config.browser.name }) 181 + instances.push({ 182 + browser, 183 + name: project.name ? `${project.name} (${browser})` : browser, 184 + }) 150 185 console.warn( 151 186 withLabel( 152 187 'yellow', ··· 162 197 ) 163 198 } 164 199 const originalName = project.config.name 165 - const filteredConfigs = !filters.length 166 - ? configs 167 - : configs.filter((config) => { 168 - const browser = config.browser 169 - const newName = config.name || (originalName ? `${originalName} (${browser})` : browser) 170 - return filters.some(pattern => pattern.test(newName)) 200 + // if original name is in the --project=name filter, keep all instances 201 + const filteredInstances = !vitest._projectFilters.length || vitest._matchesProjectFilter(originalName) 202 + ? instances 203 + : instances.filter((instance) => { 204 + const newName = instance.name! // name is set in "workspace" plugin 205 + return vitest._matchesProjectFilter(newName) 171 206 }) 172 207 173 208 // every project was filtered out 174 - if (!filteredConfigs.length) { 209 + if (!filteredInstances.length) { 210 + removeProjects.add(project) 175 211 return 176 212 } 177 213 ··· 181 217 ) 182 218 } 183 219 184 - filteredConfigs.forEach((config, index) => { 220 + filteredInstances.forEach((config, index) => { 185 221 const browser = config.browser 186 222 if (!browser) { 187 223 const nth = index + 1 188 224 const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' 189 225 throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) 190 226 } 191 - const name = config.name 192 - const newName = name || (originalName ? `${originalName} (${browser})` : browser) 227 + const name = config.name! 193 228 194 - if (names.has(newName)) { 229 + if (name == null) { 230 + throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) 231 + } 232 + 233 + if (names.has(name)) { 195 234 throw new Error( 196 235 [ 197 - `Cannot define a nested project for a ${browser} browser. The project name "${newName}" was already defined. `, 236 + `Cannot define a nested project for a ${browser} browser. The project name "${name}" was already defined. `, 198 237 'If you have multiple instances for the same browser, make sure to define a custom "name". ', 199 238 'All projects in a workspace should have unique names. Make sure your configuration is correct.', 200 239 ].join(''), 201 240 ) 202 241 } 203 - names.add(newName) 242 + names.add(name) 204 243 const clonedConfig = cloneConfig(project, config) 205 - clonedConfig.name = newName 244 + clonedConfig.name = name 206 245 const clone = TestProject._cloneBrowserProject(project, clonedConfig) 207 246 resolvedProjects.push(clone) 208 247 }) ··· 410 449 return resolve(directory, configFile) 411 450 } 412 451 return null 452 + } 453 + 454 + export function getDefaultTestProject(vitest: Vitest): TestProject | null { 455 + const filter = vitest.config.project 456 + const project = vitest._ensureRootProject() 457 + if (!filter.length) { 458 + return project 459 + } 460 + // check for the project name and browser names 461 + const hasProjects = getPotentialProjectNames(project).some(p => 462 + vitest._matchesProjectFilter(p), 463 + ) 464 + if (hasProjects) { 465 + return project 466 + } 467 + return null 468 + } 469 + 470 + function getPotentialProjectNames(project: TestProject) { 471 + const names = [project.name] 472 + if (project.config.browser.instances) { 473 + names.push(...project.config.browser.instances.map(i => i.name!)) 474 + } 475 + else if (project.config.browser.name) { 476 + names.push(project.config.browser.name) 477 + } 478 + return names 413 479 }
+1 -1
test/cli/fixtures/git-changed/workspace/vitest.workspace.js
··· 1 1 export default [ 2 - "packages/*/vitest.config.js", 2 + "packages/*/vitest.config.mjs", 3 3 ];
+3
test/config/fixtures/workspace/config-empty/vitest.config.js
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({})