Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow inline workspace configuration #6923

Merged
merged 8 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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