From cd2fdffa775c7fbc093b61a24d01e18ffd0795fd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 11:07:36 +0100 Subject: [PATCH 1/8] feat: allow inline workspace configuration --- .../vitest/src/node/config/resolveConfig.ts | 4 +- packages/vitest/src/node/core.ts | 25 +++++++---- packages/vitest/src/node/project.ts | 15 +++---- packages/vitest/src/node/types/config.ts | 5 ++- .../src/node/workspace/resolveWorkspace.ts | 44 ++++++++++++------- 5 files changed, 57 insertions(+), 36 deletions(-) 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..f984936d7182 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 @@ -212,7 +216,7 @@ export class Vitest { /** * @internal */ - _createCoreProject() { + _createRootProject() { this.coreWorkspaceProject = TestProject._createBasicProject(this) return this.coreWorkspaceProject } @@ -241,11 +245,7 @@ export class Vitest { || this.projects[0] } - private async getWorkspaceConfigPath(): Promise { - if (this.config.workspace) { - return this.config.workspace - } - + private async resolveWorkspaceConfigPath(): Promise { const configDir = this.server.config.configFile ? dirname(this.server.config.configFile) : this.config.root @@ -264,12 +264,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 = this.config.workspace || await this.resolveWorkspaceConfigPath() this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return [this._createCoreProject()] + return [this._createRootProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 1b4ca34363c8..8ac704f01fa5 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -27,7 +27,6 @@ import { isAbsolute, join, relative, - resolve, } from 'pathe' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' @@ -640,7 +639,7 @@ export interface SerializedTestProject { } interface InitializeProjectOptions extends UserWorkspaceConfig { - workspaceConfigPath: string + configFile: string | false extends?: string } @@ -651,7 +650,7 @@ export async function initializeProject( ) { const project = new TestProject(workspacePath, ctx, options) - const { extends: extendsConfig, workspaceConfigPath, ...restOptions } = options + const { extends: extendsConfig, configFile, ...restOptions } = options const root = options.root || (typeof workspacePath === 'number' @@ -660,11 +659,11 @@ export async function initializeProject( ? workspacePath : dirname(workspacePath)) - const configFile = extendsConfig - ? resolve(dirname(workspaceConfigPath), extendsConfig) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath + // const configFile = extendsConfig + // ? resolve(dirname(workspaceConfigPath), extendsConfig) + // : typeof workspacePath === 'number' || workspacePath.endsWith('/') + // ? false + // : workspacePath const config: ViteInlineConfig = { ...restOptions, diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 724e1773992b..b17b8d33407e 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`, will inherit all options from the root config. If `root` config doesn't exist, will throw an error. * @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..79b045c12091 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -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,10 +54,27 @@ export async function resolveWorkspace( const fileProjects = [...configFiles, ...nonConfigDirectories] const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) + projectConfigs.forEach((options, index) => { + const parentConfigPath = workspaceConfigPath || vitest.server.config.configFile + // if extends a config file, resolve the file path + const configFile = typeof options.extends === 'string' && typeof parentConfigPath === 'string' + ? resolve(parentConfigPath, options.extends) + : false + // if extends a root config, use the users root options + const rootOptions = options.extends === true + ? vitest._options + : {} + projectPromises.push(concurrent(() => initializeProject( + index, + vitest, + mergeConfig(rootOptions, { configFile, ...options }) as any, + ))) + }) + for (const filepath 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())) + projectPromises.push(concurrent(() => vitest._createRootProject())) continue } @@ -65,22 +82,14 @@ export async function resolveWorkspace( concurrent(() => initializeProject( filepath, vitest, - { workspaceConfigPath, test: cliOverrides }, + { configFile: filepath, 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) @@ -115,11 +124,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,7 +139,9 @@ 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) + const relativeWorkpaceConfigPath = workspaceConfigPath + ? relative(vitest.config.root, workspaceConfigPath) + : undefined for (const definition of workspaceDefinition) { if (typeof definition === 'string') { @@ -141,7 +152,8 @@ 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 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) From 61200934592520bbc3b16928cf06ff7655bece16 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 11:24:47 +0100 Subject: [PATCH 2/8] chore: cleanup --- docs/advanced/api.md | 2 +- packages/browser/src/node/pool.ts | 4 ++-- packages/browser/src/node/server.ts | 4 ++-- packages/vitest/src/node/cache/files.ts | 2 +- packages/vitest/src/node/core.ts | 4 ++-- packages/vitest/src/node/pools/forks.ts | 6 +++--- packages/vitest/src/node/pools/threads.ts | 6 +++--- packages/vitest/src/node/pools/vmForks.ts | 2 +- packages/vitest/src/node/pools/vmThreads.ts | 2 +- packages/vitest/src/node/reporters/blob.ts | 4 ++-- packages/vitest/src/node/types/config.ts | 2 +- packages/vitest/src/node/workspace/resolveWorkspace.ts | 6 +++--- packages/vitest/src/typecheck/collect.ts | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) 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/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/core.ts b/packages/vitest/src/node/core.ts index f984936d7182..6936bf6c2357 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -195,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) { @@ -740,7 +740,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/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 b17b8d33407e..90ffeddf0fab 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1120,7 +1120,7 @@ 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`, will inherit all options from the root config. If `root` config doesn't exist, will throw an error. + * If `true`, will inherit all options from the root config. * @example '../vite.config.ts' */ extends?: string | true diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 79b045c12091..26f1e20389fa 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -74,7 +74,7 @@ export async function resolveWorkspace( for (const filepath 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._createRootProject())) + projectPromises.push(Promise.resolve(vitest._createRootProject())) continue } @@ -97,9 +97,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', 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, From 23d22c98f580218a93dbdd72caf91d62d5406cdb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 14:31:49 +0100 Subject: [PATCH 3/8] chore: resolve root and configFile for workspace projects --- packages/vitest/src/node/core.ts | 8 +++++-- packages/vitest/src/node/project.ts | 23 ++----------------- .../src/node/workspace/resolveWorkspace.ts | 20 +++++++++++----- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 6936bf6c2357..0e151674264f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -168,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'))) @@ -246,6 +246,10 @@ export class Vitest { } private async resolveWorkspaceConfigPath(): Promise { + if (typeof this.config.workspace === 'string') { + return this.config.workspace + } + const configDir = this.server.config.configFile ? dirname(this.server.config.configFile) : this.config.root @@ -273,7 +277,7 @@ export class Vitest { ) } - const workspaceConfigPath = this.config.workspace || await this.resolveWorkspaceConfigPath() + const workspaceConfigPath = await this.resolveWorkspaceConfigPath() this._workspaceConfigPath = workspaceConfigPath diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 8ac704f01fa5..23bf9566b6e5 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -22,12 +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, -} 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' @@ -651,29 +646,15 @@ export async function initializeProject( const project = new TestProject(workspacePath, ctx, options) const { extends: extendsConfig, configFile, ...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 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/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 26f1e20389fa..6de6fa1ea7d8 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' @@ -64,25 +64,33 @@ export async function resolveWorkspace( 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(workspaceConfigPath || vitest.config.root) + : vitest.config.root projectPromises.push(concurrent(() => initializeProject( index, vitest, - mergeConfig(rootOptions, { configFile, ...options }) as any, + mergeConfig(rootOptions, { ...options, root, configFile }) as any, ))) }) - for (const filepath of fileProjects) { + 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) { + 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, - { configFile: filepath, test: cliOverrides }, + { root, configFile, test: cliOverrides }, )), ) } From 503b54745dc94fbc99597bfaeb0cc8ec9d0d5e3a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 14:50:23 +0100 Subject: [PATCH 4/8] docs: add docs about inlined workspace --- docs/config/index.md | 4 +- docs/guide/workspace.md | 96 ++++++++++++++++++- .../src/node/workspace/resolveWorkspace.ts | 17 ++-- 3 files changed, 103 insertions(+), 14 deletions(-) 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..4ff44bd92440 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] +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] +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] +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] +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] +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/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 6de6fa1ea7d8..610342642f03 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -147,10 +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 = workspaceConfigPath - ? relative(vitest.config.root, workspaceConfigPath) - : undefined - for (const definition of workspaceDefinition) { if (typeof definition === 'string') { const stringOption = definition.replace('', vitest.config.root) @@ -160,6 +156,9 @@ async function resolveTestProjectConfigs( const file = resolve(vitest.config.root, stringOption) if (!existsSync(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}`) } @@ -226,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) } })) } From 127483b0a1f10f8486f08da5634ba9e28153d823 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 15:03:26 +0100 Subject: [PATCH 5/8] chore: add version to vitest.config.ts --- docs/guide/workspace.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/workspace.md b/docs/guide/workspace.md index 4ff44bd92440..916bb4660e97 100644 --- a/docs/guide/workspace.md +++ b/docs/guide/workspace.md @@ -30,7 +30,7 @@ export default [ 'packages/*' ] ``` -```ts [vitest.config.ts] +```ts [vitest.config.ts 2.2.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -55,7 +55,7 @@ export default [ 'packages/*/vitest.config.{e2e,unit}.ts' ] ``` -```ts [vitest.config.ts] +```ts [vitest.config.ts 2.2.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -97,7 +97,7 @@ export default defineWorkspace([ } ]) ``` -```ts [vitest.config.ts] +```ts [vitest.config.ts 2.2.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -140,7 +140,7 @@ If you do not use inline configurations, you can create a small JSON file in you "packages/*" ] ``` -```ts [vitest.config.ts] +```ts [vitest.config.ts 2.2.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -276,7 +276,7 @@ export default defineWorkspace([ }, ]) ``` -```ts [vitest.config.ts] +```ts [vitest.config.ts 2.2.0] import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' From 6795add29a74f14b777f8b02f385d54334dac918 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 15:18:28 +0100 Subject: [PATCH 6/8] fix: resolve .extends correctly --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 610342642f03..8c2880a8b0d7 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -56,9 +56,10 @@ export async function resolveWorkspace( projectConfigs.forEach((options, index) => { const parentConfigPath = workspaceConfigPath || vitest.server.config.configFile + const configDir = parentConfigPath ? dirname(parentConfigPath) : vitest.config.root // if extends a config file, resolve the file path const configFile = typeof options.extends === 'string' && typeof parentConfigPath === 'string' - ? resolve(parentConfigPath, options.extends) + ? resolve(configDir, options.extends) : false // if extends a root config, use the users root options const rootOptions = options.extends === true @@ -66,9 +67,7 @@ export async function resolveWorkspace( : {} // 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(workspaceConfigPath || vitest.config.root) - : vitest.config.root + const root = options.root ? resolve(configDir) : vitest.config.root projectPromises.push(concurrent(() => initializeProject( index, vitest, From 851b02f9b1e1094af7aa5d16936cd2d4aabe2d94 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 18 Nov 2024 17:12:39 +0100 Subject: [PATCH 7/8] test: add inline workspace test --- .../src/node/workspace/resolveWorkspace.ts | 11 +++--- .../fixtures/workspace/api/basic.test.ts | 24 +++++++++++++ .../workspace/api/vite.custom.config.js | 7 ++++ test/config/test/workspace.test.ts | 36 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 test/config/fixtures/workspace/api/basic.test.ts create mode 100644 test/config/fixtures/workspace/api/vite.custom.config.js diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 8c2880a8b0d7..c7216dae0610 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -55,11 +55,10 @@ export async function resolveWorkspace( const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) projectConfigs.forEach((options, index) => { - const parentConfigPath = workspaceConfigPath || vitest.server.config.configFile - const configDir = parentConfigPath ? dirname(parentConfigPath) : vitest.config.root + const configRoot = workspaceConfigPath ? dirname(workspaceConfigPath) : vitest.config.root // if extends a config file, resolve the file path - const configFile = typeof options.extends === 'string' && typeof parentConfigPath === 'string' - ? resolve(configDir, options.extends) + 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 @@ -67,7 +66,9 @@ export async function resolveWorkspace( : {} // 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(configDir) : vitest.config.root + const root = options.root + ? resolve(configRoot, options.root) + : vitest.config.root projectPromises.push(concurrent(() => initializeProject( index, vitest, 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') +}) From d8aa6e5c077f9871a78994d42246d8ae25064708 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 19 Nov 2024 13:27:46 +0100 Subject: [PATCH 8/8] chore: docs --- packages/vitest/src/node/types/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 90ffeddf0fab..3dcc8bef9e53 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1120,7 +1120,7 @@ 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`, will inherit all options from the root config. + * If `true`, the project will inherit all options from the root config. * @example '../vite.config.ts' */ extends?: string | true