diff --git a/docs/advanced/api.md b/docs/advanced/api.md index 2c3959fb6ade..f7798354971e 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -140,7 +140,7 @@ export default function setup({ provide }) { ``` ::: -## TestProject 2.2.0 +## TestProject 2.2.0 {#testproject} - **Alias**: `WorkspaceProject` before 2.2.0 diff --git a/docs/config/index.md b/docs/config/index.md index 0bab567ee59f..f8c9624455b8 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -2437,12 +2437,14 @@ Tells fake timers to clear "native" (i.e. not fake) timers by delegating to thei ### workspace {#workspace} -- **Type:** `string` +- **Type:** `string | TestProjectConfiguration` - **CLI:** `--workspace=./file.js` - **Default:** `vitest.{workspace,projects}.{js,ts,json}` close to the config file or root Path to a [workspace](/guide/workspace) config file relative to [root](#root). +Since Vitest 2.2, you can also define the workspace array in the root config. If the `workspace` is defined in the config manually, Vitest will ignore the `vitest.workspace` file in the root. + ### isolate - **Type:** `boolean` diff --git a/docs/guide/workspace.md b/docs/guide/workspace.md index 2c19052dc2c5..916bb4660e97 100644 --- a/docs/guide/workspace.md +++ b/docs/guide/workspace.md @@ -14,13 +14,15 @@ Vitest provides a way to define multiple project configurations within a single ## Defining a Workspace -A workspace must include a `vitest.workspace` or `vitest.projects` file in its root directory (located in the same folder as your root configuration file or working directory if it doesn't exist). Vitest supports `ts`, `js`, and `json` extensions for this file. +A workspace must include a `vitest.workspace` or `vitest.projects` file in its root directory (located in the same folder as your root configuration file or working directory if it doesn't exist). Note that `projects` is just an alias and does not change the behavior or semantics of this feature. Vitest supports `ts`, `js`, and `json` extensions for this file. + +Since Vitest 2.2, you can also define a workspace in the root config. In this case, Vitest will ignore the `vitest.workspace` file in the root, if one exists. ::: tip NAMING Please note that this feature is named `workspace`, not `workspaces` (without an "s" at the end). ::: -Workspace configuration file must have a default export with a list of files or glob patterns referencing your projects. For example, if you have a folder named `packages` that contains your projects, you can define a workspace with this config file: +A workspace is a list of inlined configs, files, or glob patterns referencing your projects. For example, if you have a folder named `packages` that contains your projects, you can either create a workspace file or define an array in the root config: :::code-group ```ts [vitest.workspace.ts] @@ -28,6 +30,15 @@ export default [ 'packages/*' ] ``` +```ts [vitest.config.ts 2.2.0] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + workspace: ['packages/*'], + }, +}) +``` ::: Vitest will treat every folder in `packages` as a separate project even if it doesn't have a config file inside. Since Vitest 2.1, if this glob pattern matches any file it will be considered a Vitest config even if it doesn't have a `vitest` in its name. @@ -44,6 +55,15 @@ export default [ 'packages/*/vitest.config.{e2e,unit}.ts' ] ``` +```ts [vitest.config.ts 2.2.0] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + workspace: ['packages/*/vitest.config.{e2e,unit}.ts'], + }, +}) +``` ::: This pattern will only include projects with a `vitest.config` file that contains `e2e` or `unit` before the extension. @@ -77,13 +97,42 @@ export default defineWorkspace([ } ]) ``` +```ts [vitest.config.ts 2.2.0] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + workspace: [ + // matches every folder and file inside the `packages` folder + 'packages/*', + { + // add "extends: true" to inherit the options from the root config + extends: true, + test: { + include: ['tests/**/*.{browser}.test.{ts,js}'], + // it is recommended to define a name when using inline configs + name: 'happy-dom', + environment: 'happy-dom', + } + }, + { + test: { + include: ['tests/**/*.{node}.test.{ts,js}'], + name: 'node', + environment: 'node', + } + } + ] + } +}) +``` ::: ::: warning All projects must have unique names; otherwise, Vitest will throw an error. If a name is not provided in the inline configuration, Vitest will assign a number. For project configurations defined with glob syntax, Vitest will default to using the "name" property in the nearest `package.json` file or, if none exists, the folder name. ::: -If you do not use inline configurations, you can create a small JSON file in your root directory: +If you do not use inline configurations, you can create a small JSON file in your root directory or just specify it in the root config: :::code-group ```json [vitest.workspace.json] @@ -91,6 +140,15 @@ If you do not use inline configurations, you can create a small JSON file in you "packages/*" ] ``` +```ts [vitest.config.ts 2.2.0] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + workspace: ['packages/*'], + }, +}) +``` ::: Workspace projects do not support all configuration properties. For better type safety, use the `defineProject` method instead of `defineConfig` within project configuration files: @@ -195,7 +253,7 @@ export default mergeConfig( ``` ::: -At the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged. +Additionally, at the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged. ::: code-group ```ts [vitest.workspace.ts] @@ -218,6 +276,36 @@ export default defineWorkspace([ }, ]) ``` +```ts [vitest.config.ts 2.2.0] +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + pool: 'threads', + workspace: [ + { + // will inherit options from this config like plugins and pool + extends: true, + test: { + name: 'unit', + include: ['**/*.unit.test.ts'], + }, + }, + { + // won't inherit any options from this config + // this is the default behaviour + extends: false, + test: { + name: 'integration', + include: ['**/*.integration.test.ts'], + }, + }, + ], + }, +}) +``` ::: Some of the configuration options are not allowed in a project config. Most notably: diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index 42c8a9654710..aef0f4ef9d01 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -33,7 +33,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { if (!origin) { throw new Error( - `Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`, + `Can't find browser origin URL for project "${project.name}" when running tests for files "${files.join('", "')}"`, ) } @@ -67,7 +67,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { debug?.( `[%s] Running %s tests in %s chunks (%s threads)`, - project.getName() || 'core', + project.name || 'core', files.length, chunks.length, threadsCount, diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index f50aaf34892d..56dfbde9f7d6 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -174,13 +174,13 @@ export class BrowserServer implements IBrowserServer { const browser = this.project.config.browser.name if (!browser) { throw new Error( - `[${this.project.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`, + `[${this.project.name}] Browser name is required. Please, set \`test.browser.name\` option manually.`, ) } const supportedBrowsers = this.provider.getSupportedBrowsers() if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { throw new Error( - `[${this.project.getName()}] Browser "${browser}" is not supported by the browser provider "${ + `[${this.project.name}] Browser "${browser}" is not supported by the browser provider "${ this.provider.name }". Supported browsers: ${supportedBrowsers.join(', ')}.`, ) diff --git a/packages/vitest/src/node/cache/files.ts b/packages/vitest/src/node/cache/files.ts index dec874ee6bcd..689af665dd55 100644 --- a/packages/vitest/src/node/cache/files.ts +++ b/packages/vitest/src/node/cache/files.ts @@ -14,7 +14,7 @@ export class FilesStatsCache { public async populateStats(root: string, specs: WorkspaceSpec[]) { const promises = specs.map((spec) => { - const key = `${spec[0].getName()}:${relative(root, spec[1])}` + const key = `${spec[0].name}:${relative(root, spec[1])}` return this.updateStats(spec[1], key) }) await Promise.all(promises) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index b35c5641b048..3ab460a87cdc 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -521,10 +521,10 @@ export function resolveConfig( } } - if (resolved.workspace) { + if (typeof resolved.workspace === 'string') { // if passed down from the CLI and it's relative, resolve relative to CWD resolved.workspace - = options.workspace && options.workspace[0] === '.' + = typeof options.workspace === 'string' && options.workspace[0] === '.' ? resolve(process.cwd(), options.workspace) : resolvePath(resolved.workspace, resolved.root) } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 5cea438bbebb..0e151674264f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -94,6 +94,9 @@ export class Vitest { /** @private */ public _browserLastPort = defaultBrowserPort + /** @internal */ + public _options: UserConfig = {} + constructor( public readonly mode: VitestRunMode, options: VitestOptions = {}, @@ -109,6 +112,7 @@ export class Vitest { private _onUserTestsRerun: OnTestsRerunHandler[] = [] async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) { + this._options = options this.unregisterWatcher?.() clearTimeout(this._rerunTimer) this.restartsCount += 1 @@ -164,7 +168,7 @@ export class Vitest { server.watcher.on('change', async (file) => { file = normalize(file) const isConfig = file === server.config.configFile - || this.resolvedProjects.some(p => p.server.config.configFile === file) + || this.resolvedProjects.some(p => p.vite.config.configFile === file) || file === this._workspaceConfigPath if (isConfig) { await Promise.all(this._onRestartListeners.map(fn => fn('config'))) @@ -191,7 +195,7 @@ export class Vitest { const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s)) if (filters.length > 0) { this.projects = this.projects.filter(p => - filters.some(pattern => pattern.test(p.getName())), + filters.some(pattern => pattern.test(p.name)), ) } if (!this.coreWorkspaceProject) { @@ -212,7 +216,7 @@ export class Vitest { /** * @internal */ - _createCoreProject() { + _createRootProject() { this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } @@ -241,8 +245,8 @@ export class Vitest { || this.projects[0] } - private async getWorkspaceConfigPath(): Promise { - if (this.config.workspace) { + private async resolveWorkspaceConfigPath(): Promise { + if (typeof this.config.workspace === 'string') { return this.config.workspace } @@ -264,12 +268,21 @@ export class Vitest { } private async resolveWorkspace(cliOptions: UserConfig) { - const workspaceConfigPath = await this.getWorkspaceConfigPath() + if (Array.isArray(this.config.workspace)) { + return resolveWorkspace( + this, + cliOptions, + undefined, + this.config.workspace, + ) + } + + const workspaceConfigPath = await this.resolveWorkspaceConfigPath() this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return [this._createCoreProject()] + return [this._createRootProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { @@ -731,7 +744,7 @@ export class Vitest { this.configOverride.project = pattern } - this.projects = this.resolvedProjects.filter(p => p.getName() === pattern) + this.projects = this.resolvedProjects.filter(p => p.name === pattern) const files = (await this.globTestSpecs()).map(spec => spec.moduleId) await this.rerunFiles(files, 'change project filter', pattern === '') } diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 4b4c015c4eb4..65bd2b0e9f33 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -116,7 +116,7 @@ export function createForksPool( invalidates, environment, workerId, - projectName: project.getName(), + projectName: project.name, providedContext: project.getProvidedContext(), } try { @@ -199,7 +199,7 @@ export function createForksPool( const grouped = groupBy( files, ({ project, environment }) => - project.getName() + project.name + environment.name + JSON.stringify(environment.options), ) @@ -256,7 +256,7 @@ export function createForksPool( const filesByOptions = groupBy( files, ({ project, environment }) => - project.getName() + JSON.stringify(environment.options), + project.name + JSON.stringify(environment.options), ) for (const files of Object.values(filesByOptions)) { diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index bfb224a6560a..26cc9d27d1b6 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -111,7 +111,7 @@ export function createThreadsPool( invalidates, environment, workerId, - projectName: project.getName(), + projectName: project.name, providedContext: project.getProvidedContext(), } try { @@ -195,7 +195,7 @@ export function createThreadsPool( const grouped = groupBy( files, ({ project, environment }) => - project.getName() + project.name + environment.name + JSON.stringify(environment.options), ) @@ -252,7 +252,7 @@ export function createThreadsPool( const filesByOptions = groupBy( files, ({ project, environment }) => - project.getName() + JSON.stringify(environment.options), + project.name + JSON.stringify(environment.options), ) for (const files of Object.values(filesByOptions)) { diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 554b16c69989..70443a069853 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -124,7 +124,7 @@ export function createVmForksPool( invalidates, environment, workerId, - projectName: project.getName(), + projectName: project.name, providedContext: project.getProvidedContext(), } try { diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 4b5b72670403..3587dbc4fac0 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -116,7 +116,7 @@ export function createVmThreadsPool( invalidates, environment, workerId, - projectName: project.getName(), + projectName: project.name, providedContext: project.getProvidedContext(), } try { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 1b4ca34363c8..23bf9566b6e5 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -22,13 +22,7 @@ import path from 'node:path' import { deepMerge, nanoid, slash } from '@vitest/utils' import fg from 'fast-glob' import mm from 'micromatch' -import { - dirname, - isAbsolute, - join, - relative, - resolve, -} from 'pathe' +import { isAbsolute, join, relative } from 'pathe' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' import { setup } from '../api/setup' @@ -640,7 +634,7 @@ export interface SerializedTestProject { } interface InitializeProjectOptions extends UserWorkspaceConfig { - workspaceConfigPath: string + configFile: string | false extends?: string } @@ -651,30 +645,16 @@ export async function initializeProject( ) { const project = new TestProject(workspacePath, ctx, options) - const { extends: extendsConfig, workspaceConfigPath, ...restOptions } = options - const root - = options.root - || (typeof workspacePath === 'number' - ? undefined - : workspacePath.endsWith('/') - ? workspacePath - : dirname(workspacePath)) - - const configFile = extendsConfig - ? resolve(dirname(workspaceConfigPath), extendsConfig) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath + const { extends: extendsConfig, configFile, ...restOptions } = options const config: ViteInlineConfig = { ...restOptions, - root, configFile, // this will make "mode": "test" | "benchmark" inside defineConfig mode: options.test?.mode || options.mode || ctx.config.mode, plugins: [ ...(options.plugins || []), - WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), + WorkspaceVitestPlugin(project, { ...options, workspacePath }), ], } diff --git a/packages/vitest/src/node/reporters/blob.ts b/packages/vitest/src/node/reporters/blob.ts index da7678c1701a..8ca267512336 100644 --- a/packages/vitest/src/node/reporters/blob.ts +++ b/packages/vitest/src/node/reporters/blob.ts @@ -45,7 +45,7 @@ export class BlobReporter implements Reporter { const modules = this.ctx.projects.map( (project) => { return [ - project.getName(), + project.name, [...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => { if (!mod[1].file) { return null @@ -126,7 +126,7 @@ export async function readBlobs( // fake module graph - it is used to check if module is imported, but we don't use values inside const projects = Object.fromEntries( - projectsArray.map(p => [p.getName(), p]), + projectsArray.map(p => [p.name, p]), ) blobs.forEach((blob) => { diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 724e1773992b..3dcc8bef9e53 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -383,7 +383,7 @@ export interface InlineConfig { /** * Path to a workspace configuration file */ - workspace?: string + workspace?: string | TestProjectConfiguration[] /** * Update snapshot @@ -1120,9 +1120,10 @@ export type UserProjectConfigExport = export type TestProjectConfiguration = string | (UserProjectConfigExport & { /** * Relative path to the extendable config. All other options will be merged with this config. + * If `true`, the project will inherit all options from the root config. * @example '../vite.config.ts' */ - extends?: string + extends?: string | true }) /** @deprecated use `TestProjectConfiguration` instead */ diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index d826b6915054..c7216dae0610 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -5,7 +5,7 @@ import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' import fg from 'fast-glob' -import { relative, resolve } from 'pathe' +import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' import { initializeProject } from '../project' @@ -14,7 +14,7 @@ import { isDynamicPattern } from './fast-glob-pattern' export async function resolveWorkspace( vitest: Vitest, cliOptions: UserConfig, - workspaceConfigPath: string, + workspaceConfigPath: string | undefined, workspaceDefinition: TestProjectConfiguration[], ): Promise { const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs( @@ -54,33 +54,50 @@ export async function resolveWorkspace( const fileProjects = [...configFiles, ...nonConfigDirectories] const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) - for (const filepath of fileProjects) { + projectConfigs.forEach((options, index) => { + const configRoot = workspaceConfigPath ? dirname(workspaceConfigPath) : vitest.config.root + // if extends a config file, resolve the file path + const configFile = typeof options.extends === 'string' + ? resolve(configRoot, options.extends) + : false + // if extends a root config, use the users root options + const rootOptions = options.extends === true + ? vitest._options + : {} + // if `root` is configured, resolve it relative to the workespace file or vite root (like other options) + // if `root` is not specified, inline configs use the same root as the root project + const root = options.root + ? resolve(configRoot, options.root) + : vitest.config.root + projectPromises.push(concurrent(() => initializeProject( + index, + vitest, + mergeConfig(rootOptions, { ...options, root, configFile }) as any, + ))) + }) + + for (const path of fileProjects) { // if file leads to the root config, then we can just reuse it because we already initialized it - if (vitest.server.config.configFile === filepath) { - projectPromises.push(concurrent(() => vitest._createCoreProject())) + if (vitest.server.config.configFile === path) { + projectPromises.push(Promise.resolve(vitest._createRootProject())) continue } + const configFile = path.endsWith('/') ? false : path + const root = path.endsWith('/') ? path : dirname(path) + projectPromises.push( concurrent(() => initializeProject( - filepath, + path, vitest, - { workspaceConfigPath, test: cliOverrides }, + { root, configFile, test: cliOverrides }, )), ) } - projectConfigs.forEach((options, index) => { - projectPromises.push(concurrent(() => initializeProject( - index, - vitest, - mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any, - ))) - }) - // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - return [await vitest._createCoreProject()] + return [vitest._createRootProject()] } const resolvedProjects = await Promise.all(projectPromises) @@ -88,9 +105,9 @@ export async function resolveWorkspace( // project names are guaranteed to be unique for (const project of resolvedProjects) { - const name = project.getName() + const name = project.name if (names.has(name)) { - const duplicate = resolvedProjects.find(p => p.getName() === name && p !== project)! + const duplicate = resolvedProjects.find(p => p.name === name && p !== project)! const filesError = fileProjects.length ? [ '\n\nYour config matched these files:\n', @@ -115,11 +132,11 @@ export async function resolveWorkspace( async function resolveTestProjectConfigs( vitest: Vitest, - workspaceConfigPath: string, + workspaceConfigPath: string | undefined, workspaceDefinition: TestProjectConfiguration[], ) { // project configurations that were specified directly - const projectsOptions: UserWorkspaceConfig[] = [] + const projectsOptions: (UserWorkspaceConfig & { extends?: true | string })[] = [] // custom config files that were specified directly or resolved from a directory const workspaceConfigFiles: string[] = [] @@ -130,8 +147,6 @@ async function resolveTestProjectConfigs( // directories that don't have a config file inside, but should be treated as projects const nonConfigProjectDirectories: string[] = [] - const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath) - for (const definition of workspaceDefinition) { if (typeof definition === 'string') { const stringOption = definition.replace('', vitest.config.root) @@ -141,7 +156,11 @@ async function resolveTestProjectConfigs( const file = resolve(vitest.config.root, stringOption) if (!existsSync(file)) { - throw new Error(`Workspace config file "${relativeWorkpaceConfigPath}" references a non-existing file or a directory: ${file}`) + const relativeWorkpaceConfigPath = workspaceConfigPath + ? relative(vitest.config.root, workspaceConfigPath) + : undefined + const note = workspaceConfigPath ? `Workspace config file "${relativeWorkpaceConfigPath}"` : 'Inline workspace' + throw new Error(`${note} references a non-existing file or a directory: ${file}`) } const stats = await fs.stat(file) @@ -206,20 +225,20 @@ async function resolveTestProjectConfigs( const workspacesFs = await fg.glob(workspaceGlobMatches, globOptions) - await Promise.all(workspacesFs.map(async (filepath) => { + await Promise.all(workspacesFs.map(async (path) => { // directories are allowed with a glob like `packages/*` // in this case every directory is treated as a project - if (filepath.endsWith('/')) { - const configFile = await resolveDirectoryConfig(filepath) + if (path.endsWith('/')) { + const configFile = await resolveDirectoryConfig(path) if (configFile) { workspaceConfigFiles.push(configFile) } else { - nonConfigProjectDirectories.push(filepath) + nonConfigProjectDirectories.push(path) } } else { - workspaceConfigFiles.push(filepath) + workspaceConfigFiles.push(path) } })) } diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts index 91088b97ec1d..f5babbb5591d 100644 --- a/packages/vitest/src/typecheck/collect.ts +++ b/packages/vitest/src/typecheck/collect.ts @@ -55,7 +55,7 @@ export async function collectTests( request.code = request.code.replace(/__vite_ssr_identity__\((\w+\.\w+)\)/g, '( $1)') const ast = await parseAstAsync(request.code) const testFilepath = relative(ctx.config.root, filepath) - const projectName = ctx.getName() + const projectName = ctx.name const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__' const file: ParsedFile = { filepath, diff --git a/test/config/fixtures/workspace/api/basic.test.ts b/test/config/fixtures/workspace/api/basic.test.ts new file mode 100644 index 000000000000..5e60b1710647 --- /dev/null +++ b/test/config/fixtures/workspace/api/basic.test.ts @@ -0,0 +1,24 @@ +import { expect, it } from 'vitest'; + +it('correctly inherits values', ({ task }) => { + const project = task.file.projectName + switch (project) { + case 'project-1': { + expect(process.env.TEST_ROOT).toBe('1') + return + } + case 'project-2': { + expect(process.env.TEST_ROOT).toBe('2') + return + } + case 'project-3': { + // even if not inherited from the config directly, the `env` is always inherited from root + expect(process.env.TEST_ROOT).toBe('1') + expect(process.env.TEST_PROJECT).toBe('project-3') + return + } + default: { + expect.unreachable() + } + } +}) diff --git a/test/config/fixtures/workspace/api/vite.custom.config.js b/test/config/fixtures/workspace/api/vite.custom.config.js new file mode 100644 index 000000000000..ffa2c730f42b --- /dev/null +++ b/test/config/fixtures/workspace/api/vite.custom.config.js @@ -0,0 +1,7 @@ +export default { + test: { + env: { + TEST_PROJECT: 'project-3', + }, + }, +} \ No newline at end of file diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index 56ff69319ec0..7979d6fb6390 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -90,3 +90,39 @@ it('vite import analysis is applied when loading workspace config', async () => expect(stderr).toBe('') expect(stdout).toContain('test - a') }) + +it('can define inline workspace config programmatically', async () => { + const { stderr, stdout } = await runVitest({ + root: 'fixtures/workspace/api', + env: { + TEST_ROOT: '1', + }, + workspace: [ + { + extends: true, + test: { + name: 'project-1', + }, + }, + { + test: { + name: 'project-2', + env: { + TEST_ROOT: '2', + }, + }, + }, + { + extends: './vite.custom.config.js', + test: { + name: 'project-3', + }, + }, + ], + }) + expect(stderr).toBe('') + expect(stdout).toContain('project-1') + expect(stdout).toContain('project-2') + expect(stdout).toContain('project-3') + expect(stdout).toContain('3 passed') +})