Skip to content

Commit

Permalink
feat: allow inline workspace configuration (#6923)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Nov 19, 2024
1 parent 78f07c0 commit 562e1b1
Show file tree
Hide file tree
Showing 20 changed files with 256 additions and 86 deletions.
2 changes: 1 addition & 1 deletion docs/advanced/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default function setup({ provide }) {
```
:::
## TestProject <Version>2.2.0</Version>
## TestProject <Version>2.2.0</Version> {#testproject}
- **Alias**: `WorkspaceProject` before 2.2.0
Expand Down
4 changes: 3 additions & 1 deletion docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2437,12 +2437,14 @@ Tells fake timers to clear "native" (i.e. not fake) timers by delegating to thei

### workspace<NonProjectOption /> {#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`
Expand Down
96 changes: 92 additions & 4 deletions docs/guide/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,31 @@ 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]
export default [
'packages/*'
]
```
```ts [vitest.config.ts <Version>2.2.0</Version>]
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.
Expand All @@ -44,6 +55,15 @@ export default [
'packages/*/vitest.config.{e2e,unit}.ts'
]
```
```ts [vitest.config.ts <Version>2.2.0</Version>]
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.
Expand Down Expand Up @@ -77,20 +97,58 @@ export default defineWorkspace([
}
])
```
```ts [vitest.config.ts <Version>2.2.0</Version>]
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]
[
"packages/*"
]
```
```ts [vitest.config.ts <Version>2.2.0</Version>]
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:
Expand Down Expand Up @@ -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]
Expand All @@ -218,6 +276,36 @@ export default defineWorkspace([
},
])
```
```ts [vitest.config.ts <Version>2.2.0</Version>]
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:
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/node/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('", "')}"`,
)
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(', ')}.`,
)
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/cache/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 21 additions & 8 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export class Vitest {
/** @private */
public _browserLastPort = defaultBrowserPort

/** @internal */
public _options: UserConfig = {}

constructor(
public readonly mode: VitestRunMode,
options: VitestOptions = {},
Expand All @@ -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
Expand Down Expand Up @@ -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')))
Expand All @@ -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) {
Expand All @@ -212,7 +216,7 @@ export class Vitest {
/**
* @internal
*/
_createCoreProject() {
_createRootProject() {
this.coreWorkspaceProject = TestProject._createBasicProject(this)
return this.coreWorkspaceProject
}
Expand Down Expand Up @@ -241,8 +245,8 @@ export class Vitest {
|| this.projects[0]
}

private async getWorkspaceConfigPath(): Promise<string | undefined> {
if (this.config.workspace) {
private async resolveWorkspaceConfigPath(): Promise<string | undefined> {
if (typeof this.config.workspace === 'string') {
return this.config.workspace
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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 === '')
}
Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/node/pools/forks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function createForksPool(
invalidates,
environment,
workerId,
projectName: project.getName(),
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
Expand Down Expand Up @@ -199,7 +199,7 @@ export function createForksPool(
const grouped = groupBy(
files,
({ project, environment }) =>
project.getName()
project.name
+ environment.name
+ JSON.stringify(environment.options),
)
Expand Down Expand Up @@ -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)) {
Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/node/pools/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function createThreadsPool(
invalidates,
environment,
workerId,
projectName: project.getName(),
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
Expand Down Expand Up @@ -195,7 +195,7 @@ export function createThreadsPool(
const grouped = groupBy(
files,
({ project, environment }) =>
project.getName()
project.name
+ environment.name
+ JSON.stringify(environment.options),
)
Expand Down Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/pools/vmForks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function createVmForksPool(
invalidates,
environment,
workerId,
projectName: project.getName(),
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/pools/vmThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function createVmThreadsPool(
invalidates,
environment,
workerId,
projectName: project.getName(),
projectName: project.name,
providedContext: project.getProvidedContext(),
}
try {
Expand Down
Loading

0 comments on commit 562e1b1

Please sign in to comment.