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')
+})