Skip to content

Commit 562e1b1

Browse files
authoredNov 19, 2024··
feat: allow inline workspace configuration (#6923)
1 parent 78f07c0 commit 562e1b1

File tree

20 files changed

+256
-86
lines changed

20 files changed

+256
-86
lines changed
 

‎docs/advanced/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default function setup({ provide }) {
140140
```
141141
:::
142142
143-
## TestProject <Version>2.2.0</Version>
143+
## TestProject <Version>2.2.0</Version> {#testproject}
144144
145145
- **Alias**: `WorkspaceProject` before 2.2.0
146146

‎docs/config/index.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2437,12 +2437,14 @@ Tells fake timers to clear "native" (i.e. not fake) timers by delegating to thei
24372437

24382438
### workspace<NonProjectOption /> {#workspace}
24392439

2440-
- **Type:** `string`
2440+
- **Type:** `string | TestProjectConfiguration`
24412441
- **CLI:** `--workspace=./file.js`
24422442
- **Default:** `vitest.{workspace,projects}.{js,ts,json}` close to the config file or root
24432443

24442444
Path to a [workspace](/guide/workspace) config file relative to [root](#root).
24452445

2446+
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.
2447+
24462448
### isolate
24472449

24482450
- **Type:** `boolean`

‎docs/guide/workspace.md

+92-4
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,31 @@ Vitest provides a way to define multiple project configurations within a single
1414

1515
## Defining a Workspace
1616

17-
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.
17+
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.
18+
19+
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.
1820

1921
::: tip NAMING
2022
Please note that this feature is named `workspace`, not `workspaces` (without an "s" at the end).
2123
:::
2224

23-
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:
25+
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:
2426

2527
:::code-group
2628
```ts [vitest.workspace.ts]
2729
export default [
2830
'packages/*'
2931
]
3032
```
33+
```ts [vitest.config.ts <Version>2.2.0</Version>]
34+
import { defineConfig } from 'vitest/config'
35+
36+
export default defineConfig({
37+
test: {
38+
workspace: ['packages/*'],
39+
},
40+
})
41+
```
3142
:::
3243

3344
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 [
4455
'packages/*/vitest.config.{e2e,unit}.ts'
4556
]
4657
```
58+
```ts [vitest.config.ts <Version>2.2.0</Version>]
59+
import { defineConfig } from 'vitest/config'
60+
61+
export default defineConfig({
62+
test: {
63+
workspace: ['packages/*/vitest.config.{e2e,unit}.ts'],
64+
},
65+
})
66+
```
4767
:::
4868

4969
This pattern will only include projects with a `vitest.config` file that contains `e2e` or `unit` before the extension.
@@ -77,20 +97,58 @@ export default defineWorkspace([
7797
}
7898
])
7999
```
100+
```ts [vitest.config.ts <Version>2.2.0</Version>]
101+
import { defineConfig } from 'vitest/config'
102+
103+
export default defineConfig({
104+
test: {
105+
workspace: [
106+
// matches every folder and file inside the `packages` folder
107+
'packages/*',
108+
{
109+
// add "extends: true" to inherit the options from the root config
110+
extends: true,
111+
test: {
112+
include: ['tests/**/*.{browser}.test.{ts,js}'],
113+
// it is recommended to define a name when using inline configs
114+
name: 'happy-dom',
115+
environment: 'happy-dom',
116+
}
117+
},
118+
{
119+
test: {
120+
include: ['tests/**/*.{node}.test.{ts,js}'],
121+
name: 'node',
122+
environment: 'node',
123+
}
124+
}
125+
]
126+
}
127+
})
128+
```
80129
:::
81130

82131
::: warning
83132
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.
84133
:::
85134

86-
If you do not use inline configurations, you can create a small JSON file in your root directory:
135+
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:
87136

88137
:::code-group
89138
```json [vitest.workspace.json]
90139
[
91140
"packages/*"
92141
]
93142
```
143+
```ts [vitest.config.ts <Version>2.2.0</Version>]
144+
import { defineConfig } from 'vitest/config'
145+
146+
export default defineConfig({
147+
test: {
148+
workspace: ['packages/*'],
149+
},
150+
})
151+
```
94152
:::
95153

96154
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(
195253
```
196254
:::
197255

198-
At the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged.
256+
Additionally, at the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged.
199257

200258
::: code-group
201259
```ts [vitest.workspace.ts]
@@ -218,6 +276,36 @@ export default defineWorkspace([
218276
},
219277
])
220278
```
279+
```ts [vitest.config.ts <Version>2.2.0</Version>]
280+
import { defineConfig } from 'vitest/config'
281+
import react from '@vitejs/plugin-react'
282+
283+
export default defineConfig({
284+
plugins: [react()],
285+
test: {
286+
pool: 'threads',
287+
workspace: [
288+
{
289+
// will inherit options from this config like plugins and pool
290+
extends: true,
291+
test: {
292+
name: 'unit',
293+
include: ['**/*.unit.test.ts'],
294+
},
295+
},
296+
{
297+
// won't inherit any options from this config
298+
// this is the default behaviour
299+
extends: false,
300+
test: {
301+
name: 'integration',
302+
include: ['**/*.integration.test.ts'],
303+
},
304+
},
305+
],
306+
},
307+
})
308+
```
221309
:::
222310

223311
Some of the configuration options are not allowed in a project config. Most notably:

‎packages/browser/src/node/pool.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
3333

3434
if (!origin) {
3535
throw new Error(
36-
`Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`,
36+
`Can't find browser origin URL for project "${project.name}" when running tests for files "${files.join('", "')}"`,
3737
)
3838
}
3939

@@ -67,7 +67,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
6767

6868
debug?.(
6969
`[%s] Running %s tests in %s chunks (%s threads)`,
70-
project.getName() || 'core',
70+
project.name || 'core',
7171
files.length,
7272
chunks.length,
7373
threadsCount,

‎packages/browser/src/node/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@ export class BrowserServer implements IBrowserServer {
174174
const browser = this.project.config.browser.name
175175
if (!browser) {
176176
throw new Error(
177-
`[${this.project.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`,
177+
`[${this.project.name}] Browser name is required. Please, set \`test.browser.name\` option manually.`,
178178
)
179179
}
180180
const supportedBrowsers = this.provider.getSupportedBrowsers()
181181
if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
182182
throw new Error(
183-
`[${this.project.getName()}] Browser "${browser}" is not supported by the browser provider "${
183+
`[${this.project.name}] Browser "${browser}" is not supported by the browser provider "${
184184
this.provider.name
185185
}". Supported browsers: ${supportedBrowsers.join(', ')}.`,
186186
)

‎packages/vitest/src/node/cache/files.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class FilesStatsCache {
1414

1515
public async populateStats(root: string, specs: WorkspaceSpec[]) {
1616
const promises = specs.map((spec) => {
17-
const key = `${spec[0].getName()}:${relative(root, spec[1])}`
17+
const key = `${spec[0].name}:${relative(root, spec[1])}`
1818
return this.updateStats(spec[1], key)
1919
})
2020
await Promise.all(promises)

‎packages/vitest/src/node/config/resolveConfig.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -521,10 +521,10 @@ export function resolveConfig(
521521
}
522522
}
523523

524-
if (resolved.workspace) {
524+
if (typeof resolved.workspace === 'string') {
525525
// if passed down from the CLI and it's relative, resolve relative to CWD
526526
resolved.workspace
527-
= options.workspace && options.workspace[0] === '.'
527+
= typeof options.workspace === 'string' && options.workspace[0] === '.'
528528
? resolve(process.cwd(), options.workspace)
529529
: resolvePath(resolved.workspace, resolved.root)
530530
}

‎packages/vitest/src/node/core.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export class Vitest {
9494
/** @private */
9595
public _browserLastPort = defaultBrowserPort
9696

97+
/** @internal */
98+
public _options: UserConfig = {}
99+
97100
constructor(
98101
public readonly mode: VitestRunMode,
99102
options: VitestOptions = {},
@@ -109,6 +112,7 @@ export class Vitest {
109112
private _onUserTestsRerun: OnTestsRerunHandler[] = []
110113

111114
async setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) {
115+
this._options = options
112116
this.unregisterWatcher?.()
113117
clearTimeout(this._rerunTimer)
114118
this.restartsCount += 1
@@ -164,7 +168,7 @@ export class Vitest {
164168
server.watcher.on('change', async (file) => {
165169
file = normalize(file)
166170
const isConfig = file === server.config.configFile
167-
|| this.resolvedProjects.some(p => p.server.config.configFile === file)
171+
|| this.resolvedProjects.some(p => p.vite.config.configFile === file)
168172
|| file === this._workspaceConfigPath
169173
if (isConfig) {
170174
await Promise.all(this._onRestartListeners.map(fn => fn('config')))
@@ -191,7 +195,7 @@ export class Vitest {
191195
const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s))
192196
if (filters.length > 0) {
193197
this.projects = this.projects.filter(p =>
194-
filters.some(pattern => pattern.test(p.getName())),
198+
filters.some(pattern => pattern.test(p.name)),
195199
)
196200
}
197201
if (!this.coreWorkspaceProject) {
@@ -212,7 +216,7 @@ export class Vitest {
212216
/**
213217
* @internal
214218
*/
215-
_createCoreProject() {
219+
_createRootProject() {
216220
this.coreWorkspaceProject = TestProject._createBasicProject(this)
217221
return this.coreWorkspaceProject
218222
}
@@ -241,8 +245,8 @@ export class Vitest {
241245
|| this.projects[0]
242246
}
243247

244-
private async getWorkspaceConfigPath(): Promise<string | undefined> {
245-
if (this.config.workspace) {
248+
private async resolveWorkspaceConfigPath(): Promise<string | undefined> {
249+
if (typeof this.config.workspace === 'string') {
246250
return this.config.workspace
247251
}
248252

@@ -264,12 +268,21 @@ export class Vitest {
264268
}
265269

266270
private async resolveWorkspace(cliOptions: UserConfig) {
267-
const workspaceConfigPath = await this.getWorkspaceConfigPath()
271+
if (Array.isArray(this.config.workspace)) {
272+
return resolveWorkspace(
273+
this,
274+
cliOptions,
275+
undefined,
276+
this.config.workspace,
277+
)
278+
}
279+
280+
const workspaceConfigPath = await this.resolveWorkspaceConfigPath()
268281

269282
this._workspaceConfigPath = workspaceConfigPath
270283

271284
if (!workspaceConfigPath) {
272-
return [this._createCoreProject()]
285+
return [this._createRootProject()]
273286
}
274287

275288
const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as {
@@ -731,7 +744,7 @@ export class Vitest {
731744
this.configOverride.project = pattern
732745
}
733746

734-
this.projects = this.resolvedProjects.filter(p => p.getName() === pattern)
747+
this.projects = this.resolvedProjects.filter(p => p.name === pattern)
735748
const files = (await this.globTestSpecs()).map(spec => spec.moduleId)
736749
await this.rerunFiles(files, 'change project filter', pattern === '')
737750
}

‎packages/vitest/src/node/pools/forks.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function createForksPool(
116116
invalidates,
117117
environment,
118118
workerId,
119-
projectName: project.getName(),
119+
projectName: project.name,
120120
providedContext: project.getProvidedContext(),
121121
}
122122
try {
@@ -199,7 +199,7 @@ export function createForksPool(
199199
const grouped = groupBy(
200200
files,
201201
({ project, environment }) =>
202-
project.getName()
202+
project.name
203203
+ environment.name
204204
+ JSON.stringify(environment.options),
205205
)
@@ -256,7 +256,7 @@ export function createForksPool(
256256
const filesByOptions = groupBy(
257257
files,
258258
({ project, environment }) =>
259-
project.getName() + JSON.stringify(environment.options),
259+
project.name + JSON.stringify(environment.options),
260260
)
261261

262262
for (const files of Object.values(filesByOptions)) {

‎packages/vitest/src/node/pools/threads.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function createThreadsPool(
111111
invalidates,
112112
environment,
113113
workerId,
114-
projectName: project.getName(),
114+
projectName: project.name,
115115
providedContext: project.getProvidedContext(),
116116
}
117117
try {
@@ -195,7 +195,7 @@ export function createThreadsPool(
195195
const grouped = groupBy(
196196
files,
197197
({ project, environment }) =>
198-
project.getName()
198+
project.name
199199
+ environment.name
200200
+ JSON.stringify(environment.options),
201201
)
@@ -252,7 +252,7 @@ export function createThreadsPool(
252252
const filesByOptions = groupBy(
253253
files,
254254
({ project, environment }) =>
255-
project.getName() + JSON.stringify(environment.options),
255+
project.name + JSON.stringify(environment.options),
256256
)
257257

258258
for (const files of Object.values(filesByOptions)) {

‎packages/vitest/src/node/pools/vmForks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function createVmForksPool(
124124
invalidates,
125125
environment,
126126
workerId,
127-
projectName: project.getName(),
127+
projectName: project.name,
128128
providedContext: project.getProvidedContext(),
129129
}
130130
try {

‎packages/vitest/src/node/pools/vmThreads.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function createVmThreadsPool(
116116
invalidates,
117117
environment,
118118
workerId,
119-
projectName: project.getName(),
119+
projectName: project.name,
120120
providedContext: project.getProvidedContext(),
121121
}
122122
try {

‎packages/vitest/src/node/project.ts

+4-24
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@ import path from 'node:path'
2222
import { deepMerge, nanoid, slash } from '@vitest/utils'
2323
import fg from 'fast-glob'
2424
import mm from 'micromatch'
25-
import {
26-
dirname,
27-
isAbsolute,
28-
join,
29-
relative,
30-
resolve,
31-
} from 'pathe'
25+
import { isAbsolute, join, relative } from 'pathe'
3226
import { ViteNodeRunner } from 'vite-node/client'
3327
import { ViteNodeServer } from 'vite-node/server'
3428
import { setup } from '../api/setup'
@@ -640,7 +634,7 @@ export interface SerializedTestProject {
640634
}
641635

642636
interface InitializeProjectOptions extends UserWorkspaceConfig {
643-
workspaceConfigPath: string
637+
configFile: string | false
644638
extends?: string
645639
}
646640

@@ -651,30 +645,16 @@ export async function initializeProject(
651645
) {
652646
const project = new TestProject(workspacePath, ctx, options)
653647

654-
const { extends: extendsConfig, workspaceConfigPath, ...restOptions } = options
655-
const root
656-
= options.root
657-
|| (typeof workspacePath === 'number'
658-
? undefined
659-
: workspacePath.endsWith('/')
660-
? workspacePath
661-
: dirname(workspacePath))
662-
663-
const configFile = extendsConfig
664-
? resolve(dirname(workspaceConfigPath), extendsConfig)
665-
: typeof workspacePath === 'number' || workspacePath.endsWith('/')
666-
? false
667-
: workspacePath
648+
const { extends: extendsConfig, configFile, ...restOptions } = options
668649

669650
const config: ViteInlineConfig = {
670651
...restOptions,
671-
root,
672652
configFile,
673653
// this will make "mode": "test" | "benchmark" inside defineConfig
674654
mode: options.test?.mode || options.mode || ctx.config.mode,
675655
plugins: [
676656
...(options.plugins || []),
677-
WorkspaceVitestPlugin(project, { ...options, root, workspacePath }),
657+
WorkspaceVitestPlugin(project, { ...options, workspacePath }),
678658
],
679659
}
680660

‎packages/vitest/src/node/reporters/blob.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class BlobReporter implements Reporter {
4545
const modules = this.ctx.projects.map<MergeReportModuleKeys>(
4646
(project) => {
4747
return [
48-
project.getName(),
48+
project.name,
4949
[...project.vite.moduleGraph.idToModuleMap.entries()].map<SerializedModuleNode | null>((mod) => {
5050
if (!mod[1].file) {
5151
return null
@@ -126,7 +126,7 @@ export async function readBlobs(
126126

127127
// fake module graph - it is used to check if module is imported, but we don't use values inside
128128
const projects = Object.fromEntries(
129-
projectsArray.map(p => [p.getName(), p]),
129+
projectsArray.map(p => [p.name, p]),
130130
)
131131

132132
blobs.forEach((blob) => {

‎packages/vitest/src/node/types/config.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export interface InlineConfig {
383383
/**
384384
* Path to a workspace configuration file
385385
*/
386-
workspace?: string
386+
workspace?: string | TestProjectConfiguration[]
387387

388388
/**
389389
* Update snapshot
@@ -1120,9 +1120,10 @@ export type UserProjectConfigExport =
11201120
export type TestProjectConfiguration = string | (UserProjectConfigExport & {
11211121
/**
11221122
* Relative path to the extendable config. All other options will be merged with this config.
1123+
* If `true`, the project will inherit all options from the root config.
11231124
* @example '../vite.config.ts'
11241125
*/
1125-
extends?: string
1126+
extends?: string | true
11261127
})
11271128

11281129
/** @deprecated use `TestProjectConfiguration` instead */

‎packages/vitest/src/node/workspace/resolveWorkspace.ts

+47-28
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { existsSync, promises as fs } from 'node:fs'
55
import os from 'node:os'
66
import { limitConcurrency } from '@vitest/runner/utils'
77
import fg from 'fast-glob'
8-
import { relative, resolve } from 'pathe'
8+
import { dirname, relative, resolve } from 'pathe'
99
import { mergeConfig } from 'vite'
1010
import { configFiles as defaultConfigFiles } from '../../constants'
1111
import { initializeProject } from '../project'
@@ -14,7 +14,7 @@ import { isDynamicPattern } from './fast-glob-pattern'
1414
export async function resolveWorkspace(
1515
vitest: Vitest,
1616
cliOptions: UserConfig,
17-
workspaceConfigPath: string,
17+
workspaceConfigPath: string | undefined,
1818
workspaceDefinition: TestProjectConfiguration[],
1919
): Promise<TestProject[]> {
2020
const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs(
@@ -54,43 +54,60 @@ export async function resolveWorkspace(
5454
const fileProjects = [...configFiles, ...nonConfigDirectories]
5555
const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5)
5656

57-
for (const filepath of fileProjects) {
57+
projectConfigs.forEach((options, index) => {
58+
const configRoot = workspaceConfigPath ? dirname(workspaceConfigPath) : vitest.config.root
59+
// if extends a config file, resolve the file path
60+
const configFile = typeof options.extends === 'string'
61+
? resolve(configRoot, options.extends)
62+
: false
63+
// if extends a root config, use the users root options
64+
const rootOptions = options.extends === true
65+
? vitest._options
66+
: {}
67+
// if `root` is configured, resolve it relative to the workespace file or vite root (like other options)
68+
// if `root` is not specified, inline configs use the same root as the root project
69+
const root = options.root
70+
? resolve(configRoot, options.root)
71+
: vitest.config.root
72+
projectPromises.push(concurrent(() => initializeProject(
73+
index,
74+
vitest,
75+
mergeConfig(rootOptions, { ...options, root, configFile }) as any,
76+
)))
77+
})
78+
79+
for (const path of fileProjects) {
5880
// if file leads to the root config, then we can just reuse it because we already initialized it
59-
if (vitest.server.config.configFile === filepath) {
60-
projectPromises.push(concurrent(() => vitest._createCoreProject()))
81+
if (vitest.server.config.configFile === path) {
82+
projectPromises.push(Promise.resolve(vitest._createRootProject()))
6183
continue
6284
}
6385

86+
const configFile = path.endsWith('/') ? false : path
87+
const root = path.endsWith('/') ? path : dirname(path)
88+
6489
projectPromises.push(
6590
concurrent(() => initializeProject(
66-
filepath,
91+
path,
6792
vitest,
68-
{ workspaceConfigPath, test: cliOverrides },
93+
{ root, configFile, test: cliOverrides },
6994
)),
7095
)
7196
}
7297

73-
projectConfigs.forEach((options, index) => {
74-
projectPromises.push(concurrent(() => initializeProject(
75-
index,
76-
vitest,
77-
mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any,
78-
)))
79-
})
80-
8198
// pretty rare case - the glob didn't match anything and there are no inline configs
8299
if (!projectPromises.length) {
83-
return [await vitest._createCoreProject()]
100+
return [vitest._createRootProject()]
84101
}
85102

86103
const resolvedProjects = await Promise.all(projectPromises)
87104
const names = new Set<string>()
88105

89106
// project names are guaranteed to be unique
90107
for (const project of resolvedProjects) {
91-
const name = project.getName()
108+
const name = project.name
92109
if (names.has(name)) {
93-
const duplicate = resolvedProjects.find(p => p.getName() === name && p !== project)!
110+
const duplicate = resolvedProjects.find(p => p.name === name && p !== project)!
94111
const filesError = fileProjects.length
95112
? [
96113
'\n\nYour config matched these files:\n',
@@ -115,11 +132,11 @@ export async function resolveWorkspace(
115132

116133
async function resolveTestProjectConfigs(
117134
vitest: Vitest,
118-
workspaceConfigPath: string,
135+
workspaceConfigPath: string | undefined,
119136
workspaceDefinition: TestProjectConfiguration[],
120137
) {
121138
// project configurations that were specified directly
122-
const projectsOptions: UserWorkspaceConfig[] = []
139+
const projectsOptions: (UserWorkspaceConfig & { extends?: true | string })[] = []
123140

124141
// custom config files that were specified directly or resolved from a directory
125142
const workspaceConfigFiles: string[] = []
@@ -130,8 +147,6 @@ async function resolveTestProjectConfigs(
130147
// directories that don't have a config file inside, but should be treated as projects
131148
const nonConfigProjectDirectories: string[] = []
132149

133-
const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath)
134-
135150
for (const definition of workspaceDefinition) {
136151
if (typeof definition === 'string') {
137152
const stringOption = definition.replace('<rootDir>', vitest.config.root)
@@ -141,7 +156,11 @@ async function resolveTestProjectConfigs(
141156
const file = resolve(vitest.config.root, stringOption)
142157

143158
if (!existsSync(file)) {
144-
throw new Error(`Workspace config file "${relativeWorkpaceConfigPath}" references a non-existing file or a directory: ${file}`)
159+
const relativeWorkpaceConfigPath = workspaceConfigPath
160+
? relative(vitest.config.root, workspaceConfigPath)
161+
: undefined
162+
const note = workspaceConfigPath ? `Workspace config file "${relativeWorkpaceConfigPath}"` : 'Inline workspace'
163+
throw new Error(`${note} references a non-existing file or a directory: ${file}`)
145164
}
146165

147166
const stats = await fs.stat(file)
@@ -206,20 +225,20 @@ async function resolveTestProjectConfigs(
206225

207226
const workspacesFs = await fg.glob(workspaceGlobMatches, globOptions)
208227

209-
await Promise.all(workspacesFs.map(async (filepath) => {
228+
await Promise.all(workspacesFs.map(async (path) => {
210229
// directories are allowed with a glob like `packages/*`
211230
// in this case every directory is treated as a project
212-
if (filepath.endsWith('/')) {
213-
const configFile = await resolveDirectoryConfig(filepath)
231+
if (path.endsWith('/')) {
232+
const configFile = await resolveDirectoryConfig(path)
214233
if (configFile) {
215234
workspaceConfigFiles.push(configFile)
216235
}
217236
else {
218-
nonConfigProjectDirectories.push(filepath)
237+
nonConfigProjectDirectories.push(path)
219238
}
220239
}
221240
else {
222-
workspaceConfigFiles.push(filepath)
241+
workspaceConfigFiles.push(path)
223242
}
224243
}))
225244
}

‎packages/vitest/src/typecheck/collect.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function collectTests(
5555
request.code = request.code.replace(/__vite_ssr_identity__\((\w+\.\w+)\)/g, '( $1)')
5656
const ast = await parseAstAsync(request.code)
5757
const testFilepath = relative(ctx.config.root, filepath)
58-
const projectName = ctx.getName()
58+
const projectName = ctx.name
5959
const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__'
6060
const file: ParsedFile = {
6161
filepath,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect, it } from 'vitest';
2+
3+
it('correctly inherits values', ({ task }) => {
4+
const project = task.file.projectName
5+
switch (project) {
6+
case 'project-1': {
7+
expect(process.env.TEST_ROOT).toBe('1')
8+
return
9+
}
10+
case 'project-2': {
11+
expect(process.env.TEST_ROOT).toBe('2')
12+
return
13+
}
14+
case 'project-3': {
15+
// even if not inherited from the config directly, the `env` is always inherited from root
16+
expect(process.env.TEST_ROOT).toBe('1')
17+
expect(process.env.TEST_PROJECT).toBe('project-3')
18+
return
19+
}
20+
default: {
21+
expect.unreachable()
22+
}
23+
}
24+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
test: {
3+
env: {
4+
TEST_PROJECT: 'project-3',
5+
},
6+
},
7+
}

‎test/config/test/workspace.test.ts

+36
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,39 @@ it('vite import analysis is applied when loading workspace config', async () =>
9090
expect(stderr).toBe('')
9191
expect(stdout).toContain('test - a')
9292
})
93+
94+
it('can define inline workspace config programmatically', async () => {
95+
const { stderr, stdout } = await runVitest({
96+
root: 'fixtures/workspace/api',
97+
env: {
98+
TEST_ROOT: '1',
99+
},
100+
workspace: [
101+
{
102+
extends: true,
103+
test: {
104+
name: 'project-1',
105+
},
106+
},
107+
{
108+
test: {
109+
name: 'project-2',
110+
env: {
111+
TEST_ROOT: '2',
112+
},
113+
},
114+
},
115+
{
116+
extends: './vite.custom.config.js',
117+
test: {
118+
name: 'project-3',
119+
},
120+
},
121+
],
122+
})
123+
expect(stderr).toBe('')
124+
expect(stdout).toContain('project-1')
125+
expect(stdout).toContain('project-2')
126+
expect(stdout).toContain('project-3')
127+
expect(stdout).toContain('3 passed')
128+
})

0 commit comments

Comments
 (0)
Please sign in to comment.