From 2a17f46c2f81536f627721e3604ba8a71a4d0f31 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 9 Aug 2024 17:43:44 +0200 Subject: [PATCH 01/16] fix(vitest): correctly resolve workspace globs and file paths --- packages/vitest/src/node/core.ts | 35 ++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 4a191a9b6ab1..0fdd7c71cddb 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -252,12 +252,39 @@ export class Vitest { throw new Error(`Workspace config file ${workspaceConfigPath} must export a default array of project paths.`) } + const workspaceConfigFiles: string[] = [] const workspaceGlobMatches: string[] = [] const projectsOptions: UserWorkspaceConfig[] = [] for (const project of workspaceModule.default) { if (typeof project === 'string') { - workspaceGlobMatches.push(project.replace('', this.config.root)) + const match = project.replace('', this.config.root) + if (!match.includes('*')) { + const file = resolve(workspaceConfigPath, match) + + if (!existsSync(file)) { + throw new Error(`Workspace config file ${workspaceConfigPath} references a non-existing file or a directory: ${file}`) + } + + const stats = await fs.stat(file) + if (stats.isFile()) { + workspaceConfigFiles.push(file) + } + else if (stats.isDirectory()) { + const filesInside = await fs.readdir(file) + const configFile = configFiles.find(config => filesInside.includes(config)) + if (!configFile) { + throw new Error(`Workspace config file ${workspaceConfigPath} references a directory without a config file: ${file}`) + } + workspaceConfigFiles.push(file) + } + else { + throw new Error(`Unexpected file type: ${file}`) + } + } + else { + workspaceGlobMatches.push(project.replace('', this.config.root)) + } } else if (typeof project === 'function') { projectsOptions.push(await project({ @@ -309,12 +336,12 @@ export class Vitest { return configByFolder }, {} as Record) - const filteredWorkspaces = Object.values(workspacesByFolder).map((configFiles) => { + const filteredWorkspaces = Object.values(workspacesByFolder).flatMap((configFiles) => { if (configFiles.length === 1) { return configFiles[0] } - const vitestConfig = configFiles.find(configFile => basename(configFile).startsWith('vitest.config')) - return vitestConfig || configFiles[0] + const vitestConfig = configFiles.filter(configFile => basename(configFile).startsWith('vitest.config')) + return vitestConfig.length ? vitestConfig : configFiles[0] }) const overridesOptions = [ From 403cd9a2e2dc8a0cc31bf34e8e08c3974a3e3d54 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 14:39:47 +0200 Subject: [PATCH 02/16] refactor: move resolve workspaces to a separate file to make core smaller --- packages/vitest/src/constants.ts | 2 +- packages/vitest/src/node/core.ts | 195 ++-------------- .../src/node/workspace/resolveWorkspace.ts | 211 ++++++++++++++++++ packages/vitest/src/public/config.ts | 2 +- 4 files changed, 229 insertions(+), 181 deletions(-) create mode 100644 packages/vitest/src/node/workspace/resolveWorkspace.ts diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index ed504323c41f..c1bbad185275 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -16,7 +16,7 @@ export const CONFIG_NAMES = ['vitest.config', 'vite.config'] const WORKSPACES_NAMES = ['vitest.workspace', 'vitest.projects'] -const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] +export const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] export const configFiles = CONFIG_NAMES.flatMap(name => CONFIG_EXTENSIONS.map(ext => name + ext), diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 0fdd7c71cddb..27387a9d30e3 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,10 +1,7 @@ import { existsSync, promises as fs } from 'node:fs' import type { Writable } from 'node:stream' -import { isMainThread } from 'node:worker_threads' import type { ViteDevServer } from 'vite' -import { mergeConfig } from 'vite' -import { basename, dirname, join, normalize, relative, resolve } from 'pathe' -import fg from 'fast-glob' +import { dirname, join, normalize, relative, resolve } from 'pathe' import mm from 'micromatch' import { ViteNodeRunner } from 'vite-node/client' import { SnapshotManager } from '@vitest/snapshot/manager' @@ -14,7 +11,7 @@ import type { defineWorkspace } from 'vitest/config' import { version } from '../../package.json' with { type: 'json' } import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils' import { getCoverageProvider } from '../integrations/coverage' -import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants' +import { workspacesFiles as workspaceFiles } from '../constants' import { rootDir } from '../paths' import { WebSocketReporter } from '../api/setup' import type { SerializedCoverageConfig } from '../runtime/config' @@ -27,13 +24,14 @@ import { StateManager } from './state' import { resolveConfig } from './config/resolveConfig' import { Logger } from './logger' import { VitestCache } from './cache' -import { WorkspaceProject, initializeProject } from './workspace' +import { WorkspaceProject } from './workspace' import { VitestPackageInstaller } from './packageInstaller' import { BlobReporter, readBlobs } from './reporters/blob' import { FilesNotFoundError, GitNotFoundError } from './errors' -import type { ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from './types/config' +import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { Reporter } from './types/reporter' import type { CoverageProvider } from './types/coverage' +import { resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -192,7 +190,10 @@ export class Vitest { this.getCoreWorkspaceProject().provide(key, value) } - private async createCoreProject() { + /** + * @internal + */ + async createCoreProject() { this.coreWorkspaceProject = await WorkspaceProject.createCoreProject(this) return this.coreWorkspaceProject } @@ -249,179 +250,15 @@ export class Vitest { } if (!workspaceModule.default || !Array.isArray(workspaceModule.default)) { - throw new Error(`Workspace config file ${workspaceConfigPath} must export a default array of project paths.`) - } - - const workspaceConfigFiles: string[] = [] - const workspaceGlobMatches: string[] = [] - const projectsOptions: UserWorkspaceConfig[] = [] - - for (const project of workspaceModule.default) { - if (typeof project === 'string') { - const match = project.replace('', this.config.root) - if (!match.includes('*')) { - const file = resolve(workspaceConfigPath, match) - - if (!existsSync(file)) { - throw new Error(`Workspace config file ${workspaceConfigPath} references a non-existing file or a directory: ${file}`) - } - - const stats = await fs.stat(file) - if (stats.isFile()) { - workspaceConfigFiles.push(file) - } - else if (stats.isDirectory()) { - const filesInside = await fs.readdir(file) - const configFile = configFiles.find(config => filesInside.includes(config)) - if (!configFile) { - throw new Error(`Workspace config file ${workspaceConfigPath} references a directory without a config file: ${file}`) - } - workspaceConfigFiles.push(file) - } - else { - throw new Error(`Unexpected file type: ${file}`) - } - } - else { - workspaceGlobMatches.push(project.replace('', this.config.root)) - } - } - else if (typeof project === 'function') { - projectsOptions.push(await project({ - command: this.server.config.command, - mode: this.server.config.mode, - isPreview: false, - isSsrBuild: false, - })) - } - else { - projectsOptions.push(await project) - } - } - - const globOptions: fg.Options = { - absolute: true, - dot: true, - onlyFiles: false, - markDirectories: true, - cwd: this.config.root, - ignore: ['**/node_modules/**', '**/*.timestamp-*'], - } - - const workspacesFs = await fg(workspaceGlobMatches, globOptions) - const resolvedWorkspacesPaths = await Promise.all(workspacesFs.filter((file) => { - if (file.endsWith('/')) { - // if it's a directory, check that we don't already have a workspace with a config inside - const hasWorkspaceWithConfig = workspacesFs.some((file2) => { - return file2 !== file && `${dirname(file2)}/` === file - }) - return !hasWorkspaceWithConfig - } - const filename = basename(file) - return CONFIG_NAMES.some(configName => filename.startsWith(configName)) - }).map(async (filepath) => { - if (filepath.endsWith('/')) { - const filesInside = await fs.readdir(filepath) - const configFile = configFiles.find(config => filesInside.includes(config)) - return configFile ? join(filepath, configFile) : filepath - } - return filepath - })) - - const workspacesByFolder = resolvedWorkspacesPaths - .reduce((configByFolder, filepath) => { - const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) - configByFolder[dir] ??= [] - configByFolder[dir].push(filepath) - return configByFolder - }, {} as Record) - - const filteredWorkspaces = Object.values(workspacesByFolder).flatMap((configFiles) => { - if (configFiles.length === 1) { - return configFiles[0] - } - const vitestConfig = configFiles.filter(configFile => basename(configFile).startsWith('vitest.config')) - return vitestConfig.length ? vitestConfig : configFiles[0] - }) - - const overridesOptions = [ - 'logHeapUsage', - 'allowOnly', - 'sequence', - 'testTimeout', - 'pool', - 'update', - 'globals', - 'expandSnapshotDiff', - 'disableConsoleIntercept', - 'retry', - 'testNamePattern', - 'passWithNoTests', - 'bail', - 'isolate', - 'printConsoleTrace', - ] as const - - const cliOverrides = overridesOptions.reduce((acc, name) => { - if (name in cliOptions) { - acc[name] = cliOptions[name] as any - } - return acc - }, {} as UserConfig) - - const cwd = process.cwd() - - const projects: WorkspaceProject[] = [] - - try { - // we have to resolve them one by one because CWD should depend on the project - for (const filepath of filteredWorkspaces) { - if (this.server.config.configFile === filepath) { - const project = await this.createCoreProject() - projects.push(project) - continue - } - const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) - if (isMainThread) { - process.chdir(dir) - } - projects.push( - await initializeProject(filepath, this, { workspaceConfigPath, test: cliOverrides }), - ) - } - } - finally { - if (isMainThread) { - process.chdir(cwd) - } + throw new TypeError(`Workspace config file "${workspaceConfigPath}" must export a default array of project paths.`) } - const projectPromises: Promise[] = [] - - projectsOptions.forEach((options, index) => { - // we can resolve these in parallel because process.cwd() is not changed - projectPromises.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any)) - }) - - if (!projects.length && !projectPromises.length) { - return [await this.createCoreProject()] - } - - const resolvedProjects = await Promise.all([ - ...projects, - ...await Promise.all(projectPromises), - ]) - const names = new Set() - - for (const project of resolvedProjects) { - const name = project.getName() - if (names.has(name)) { - throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`) - } - names.add(name) - } - - return resolvedProjects + return resolveWorkspace( + this, + cliOptions, + workspaceConfigPath, + workspaceModule.default, + ) } private async initCoverageProvider() { diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts new file mode 100644 index 000000000000..3a0114a5c5fb --- /dev/null +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -0,0 +1,211 @@ +import { existsSync, promises as fs } from 'node:fs' +import { isMainThread } from 'node:worker_threads' +import { dirname, resolve } from 'pathe' +import { mergeConfig } from 'vite' +import fg from 'fast-glob' +import c from 'tinyrainbow' +import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config' +import type { Vitest } from '../core' +import type { UserConfig } from '../types/config' +import type { WorkspaceProject } from '../workspace' +import { initializeProject } from '../workspace' +import { configFiles as defaultConfigFiles } from '../../constants' + +export async function resolveWorkspace( + vitest: Vitest, + cliOptions: UserConfig, + workspaceConfigPath: string, + workspaceDefinition: WorkspaceProjectConfiguration[], +): Promise { + const { configFiles, projectConfigs, nonConfigProjects } = await resolveWorkspaceProjectConfigs( + vitest, + workspaceConfigPath, + workspaceDefinition, + ) + + const overridesOptions = [ + 'logHeapUsage', + 'allowOnly', + 'sequence', + 'testTimeout', + 'pool', + 'update', + 'globals', + 'expandSnapshotDiff', + 'disableConsoleIntercept', + 'retry', + 'testNamePattern', + 'passWithNoTests', + 'bail', + 'isolate', + 'printConsoleTrace', + ] as const + + const cliOverrides = overridesOptions.reduce((acc, name) => { + if (name in cliOptions) { + acc[name] = cliOptions[name] as any + } + return acc + }, {} as UserConfig) + + const cwd = process.cwd() + + const projects: WorkspaceProject[] = [] + + try { + // we have to resolve them one by one because CWD should depend on the project + for (const filepath of [...configFiles, ...nonConfigProjects]) { + if (vitest.server.config.configFile === filepath) { + const project = await vitest.createCoreProject() + projects.push(project) + continue + } + const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) + if (isMainThread) { + process.chdir(dir) + } + projects.push( + await initializeProject(filepath, vitest, { workspaceConfigPath, test: cliOverrides }), + ) + } + } + finally { + if (isMainThread) { + process.chdir(cwd) + } + } + + const projectPromises: Promise[] = [] + + projectConfigs.forEach((options, index) => { + // we can resolve these in parallel because process.cwd() is not changed + projectPromises.push(initializeProject(index, vitest, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any)) + }) + + if (!projects.length && !projectPromises.length) { + return [await vitest.createCoreProject()] + } + + const resolvedProjects = await Promise.all([ + ...projects, + ...await Promise.all(projectPromises), + ]) + const names = new Set() + + for (const project of resolvedProjects) { + const name = project.getName() + if (names.has(name)) { + throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`) + } + names.add(name) + } + + return resolvedProjects +} + +async function resolveWorkspaceProjectConfigs( + vitest: Vitest, + workspaceConfigPath: string, + workspaceDefinition: WorkspaceProjectConfiguration[], +) { + const projectsOptions: UserWorkspaceConfig[] = [] + const workspaceConfigFiles: string[] = [] + const workspaceGlobMatches: string[] = [] + let nonConfigProjectDirectories: string[] = [] + + for (const definition of workspaceDefinition) { + if (typeof definition === 'string') { + const stringOption = definition.replace('', vitest.config.root) + if (!stringOption.includes('*')) { + const file = resolve(workspaceConfigPath, stringOption) + + if (!existsSync(file)) { + throw new Error(`Workspace config file "${workspaceConfigPath}" references a non-existing file or a directory: ${file}`) + } + + const stats = await fs.stat(file) + if (stats.isFile()) { + workspaceConfigFiles.push(file) + } + else if (stats.isDirectory()) { + nonConfigProjectDirectories.push(file) + } + else { + throw new TypeError(`Unexpected file type: ${file}`) + } + } + else { + workspaceGlobMatches.push(stringOption) + } + } + else if (typeof definition === 'function') { + projectsOptions.push(await definition({ + command: vitest.server.config.command, + mode: vitest.server.config.mode, + isPreview: false, + isSsrBuild: false, + })) + } + else { + projectsOptions.push(await definition) + } + + if (workspaceGlobMatches.length) { + const globOptions: fg.Options = { + absolute: true, + dot: true, + onlyFiles: false, + markDirectories: true, + cwd: vitest.config.root, + ignore: ['**/node_modules/**', '**/*.timestamp-*'], + } + + const workspacesFs = await fg(workspaceGlobMatches, globOptions) + + await Promise.all(workspacesFs.map(async (filepath) => { + // the directories are allowed with a glob like `packages/*` + if (filepath.endsWith('/')) { + nonConfigProjectDirectories.push(filepath) + } + else { + workspaceConfigFiles.push(filepath) + } + })) + } + } + + const projectConfigFiles = [...new Set(workspaceConfigFiles)] + const duplicateDirectories = new Set() + + for (const config of projectConfigFiles) { + // ignore custom config names because it won't be picked up by the default resolver later + if (!defaultConfigFiles.includes(config)) { + continue + } + + const configDirectory = `${dirname(config)}/` + // if for some reason there is already a config file in a directory that was found by a glob + // we remove it from the list to avoid duplicates when the project is initialized + for (const directory of nonConfigProjectDirectories) { + if (directory === configDirectory) { + vitest.logger.warn( + c.yellow( + `The specified config file "${config}" is located in the directory already found by a glob match. ` + + `The config file will override the directory match to avoid duplicates. You can silence this message by excluding the directory from the glob in "${workspaceConfigPath}".`, + ), + ) + duplicateDirectories.add(directory) + } + } + } + + if (duplicateDirectories.size) { + nonConfigProjectDirectories = nonConfigProjectDirectories.filter(dir => !duplicateDirectories.has(dir)) + } + + return { + projectConfigs: projectsOptions, + nonConfigProjects: nonConfigProjectDirectories, + configFiles: projectConfigFiles, + } +} diff --git a/packages/vitest/src/public/config.ts b/packages/vitest/src/public/config.ts index ee605beb662d..b8a7341e66ae 100644 --- a/packages/vitest/src/public/config.ts +++ b/packages/vitest/src/public/config.ts @@ -58,7 +58,7 @@ export function defineProject(config: UserProjectConfigExport): UserProjectConfi return config } -type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { +export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { /** * Relative path to the extendable config. All other options will be merged with this config. * @example '../vite.config.ts' From 76398b39d85d28b0286063b6f7d4c4ecc34320df Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 15:02:05 +0200 Subject: [PATCH 03/16] fix: use correct root --- .../src/node/workspace/resolveWorkspace.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 3a0114a5c5fb..8a0bf6117f7b 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -1,6 +1,6 @@ import { existsSync, promises as fs } from 'node:fs' import { isMainThread } from 'node:worker_threads' -import { dirname, resolve } from 'pathe' +import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import fg from 'fast-glob' import c from 'tinyrainbow' @@ -17,7 +17,7 @@ export async function resolveWorkspace( workspaceConfigPath: string, workspaceDefinition: WorkspaceProjectConfiguration[], ): Promise { - const { configFiles, projectConfigs, nonConfigProjects } = await resolveWorkspaceProjectConfigs( + const { configFiles, projectConfigs, nonConfigDirectories } = await resolveWorkspaceProjectConfigs( vitest, workspaceConfigPath, workspaceDefinition, @@ -54,7 +54,7 @@ export async function resolveWorkspace( try { // we have to resolve them one by one because CWD should depend on the project - for (const filepath of [...configFiles, ...nonConfigProjects]) { + for (const filepath of [...configFiles, ...nonConfigDirectories]) { if (vitest.server.config.configFile === filepath) { const project = await vitest.createCoreProject() projects.push(project) @@ -113,14 +113,16 @@ async function resolveWorkspaceProjectConfigs( const workspaceGlobMatches: string[] = [] let nonConfigProjectDirectories: string[] = [] + const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath) + for (const definition of workspaceDefinition) { if (typeof definition === 'string') { const stringOption = definition.replace('', vitest.config.root) if (!stringOption.includes('*')) { - const file = resolve(workspaceConfigPath, stringOption) + const file = resolve(vitest.config.root, stringOption) if (!existsSync(file)) { - throw new Error(`Workspace config file "${workspaceConfigPath}" references a non-existing file or a directory: ${file}`) + throw new Error(`Workspace config file ${relativeWorkpaceConfigPath} references a non-existing file or a directory: ${file}`) } const stats = await fs.stat(file) @@ -128,7 +130,8 @@ async function resolveWorkspaceProjectConfigs( workspaceConfigFiles.push(file) } else if (stats.isDirectory()) { - nonConfigProjectDirectories.push(file) + const directory = file[file.length - 1] === '/' ? file : `${file}/` + nonConfigProjectDirectories.push(directory) } else { throw new TypeError(`Unexpected file type: ${file}`) @@ -190,8 +193,8 @@ async function resolveWorkspaceProjectConfigs( if (directory === configDirectory) { vitest.logger.warn( c.yellow( - `The specified config file "${config}" is located in the directory already found by a glob match. ` - + `The config file will override the directory match to avoid duplicates. You can silence this message by excluding the directory from the glob in "${workspaceConfigPath}".`, + `The specified config file "${resolve(vitest.config.root, config)}" is located in the directory already found by a glob match. ` + + `The config file will override the directory match to avoid duplicates. You can silence this message by excluding the directory from the glob in "${relativeWorkpaceConfigPath}".`, ), ) duplicateDirectories.add(directory) @@ -205,7 +208,7 @@ async function resolveWorkspaceProjectConfigs( return { projectConfigs: projectsOptions, - nonConfigProjects: nonConfigProjectDirectories, + nonConfigDirectories: nonConfigProjectDirectories, configFiles: projectConfigFiles, } } From 934d0c5569913ec6bbe9bd3d778d2c5e6d03c0b1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 15:42:08 +0200 Subject: [PATCH 04/16] chore: docs --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 8a0bf6117f7b..5314016c80a8 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -126,9 +126,12 @@ async function resolveWorkspaceProjectConfigs( } const stats = await fs.stat(file) + // user can specify a config file directly if (stats.isFile()) { workspaceConfigFiles.push(file) } + // user can specify a directory that should be used as a project + // the config file inside will be resolved by the default resolver later else if (stats.isDirectory()) { const directory = file[file.length - 1] === '/' ? file : `${file}/` nonConfigProjectDirectories.push(directory) @@ -165,7 +168,7 @@ async function resolveWorkspaceProjectConfigs( const workspacesFs = await fg(workspaceGlobMatches, globOptions) - await Promise.all(workspacesFs.map(async (filepath) => { + workspacesFs.forEach((filepath) => { // the directories are allowed with a glob like `packages/*` if (filepath.endsWith('/')) { nonConfigProjectDirectories.push(filepath) @@ -173,7 +176,7 @@ async function resolveWorkspaceProjectConfigs( else { workspaceConfigFiles.push(filepath) } - })) + }) } } From 29fe8335dd0b23a9a12e5668e17d9df107bffe60 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 15:57:47 +0200 Subject: [PATCH 05/16] test: update workspaces test --- test/test-utils/cli.ts | 2 +- test/workspaces/vitest.workspace.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test-utils/cli.ts b/test/test-utils/cli.ts index 44709d7c4a8c..087aa3c01716 100644 --- a/test/test-utils/cli.ts +++ b/test/test-utils/cli.ts @@ -76,7 +76,7 @@ export class Cli { } const timeout = setTimeout(() => { - error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this[source]}` + error.message = `Timeout when waiting for error "${expected}".\nReceived:\nstdout: ${this.stdout}\nstderr: ${this.stderr}` reject(error) }, process.env.CI ? 20_000 : 4_000) diff --git a/test/workspaces/vitest.workspace.ts b/test/workspaces/vitest.workspace.ts index 038fed32aa47..f09cdee0f22f 100644 --- a/test/workspaces/vitest.workspace.ts +++ b/test/workspaces/vitest.workspace.ts @@ -5,7 +5,8 @@ import type { Plugin } from 'vite' export default defineWorkspace([ 'space_2', - './space_*/*.config.ts', + './space_*/vitest.config.ts', + './space_1/*.config.ts', async () => ({ test: { name: 'happy-dom', From ad4e952a6ad560844be447681b572c8a18afec1a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:03:01 +0200 Subject: [PATCH 06/16] chore: improve error message --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 5314016c80a8..2e2113eeefcf 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -95,7 +95,14 @@ export async function resolveWorkspace( for (const project of resolvedProjects) { const name = project.getName() if (names.has(name)) { - throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`) + const duplicate = resolvedProjects.find(p => p.getName() === name && p !== project)! + throw new Error([ + `Project name "${name}"`, + project.server.config.configFile ? ` from "${relative(vitest.config.root, project.server.config.configFile)}"` : '', + ' is not unique. ', + duplicate?.server.config.configFile ? `The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '', + ' All projects in a workspace should have unique names. Make sure your configuration is correct.', + ].join('')) } names.add(name) } From 372a32f6d5e43cd6755f90d5010a388dda01afac Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:31:37 +0200 Subject: [PATCH 07/16] fix: resolve config file correctly --- packages/vitest/src/node/workspace.ts | 12 +++--- .../src/node/workspace/resolveWorkspace.ts | 40 ++++++++++++++++--- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index e85b5d1275e2..408f1da3f870 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -52,12 +52,6 @@ export async function initializeProject( ) { const project = new WorkspaceProject(workspacePath, ctx, options) - const configFile = options.extends - ? resolve(dirname(options.workspaceConfigPath), options.extends) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath - const root = options.root || (typeof workspacePath === 'number' @@ -66,6 +60,12 @@ export async function initializeProject( ? workspacePath : dirname(workspacePath)) + const configFile = options.extends + ? resolve(dirname(options.workspaceConfigPath), options.extends) + : typeof workspacePath === 'number' || workspacePath.endsWith('/') + ? false + : workspacePath + const config: ViteInlineConfig = { ...options, root, diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 2e2113eeefcf..e397506b27ef 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -115,9 +115,16 @@ async function resolveWorkspaceProjectConfigs( workspaceConfigPath: string, workspaceDefinition: WorkspaceProjectConfiguration[], ) { + // project configurations that were specified directly const projectsOptions: UserWorkspaceConfig[] = [] + + // custom config files that were specified directly const workspaceConfigFiles: string[] = [] + + // custom glob matches that should be resolved as directories or config files const workspaceGlobMatches: string[] = [] + + // directories that don't have a config file inside, but should be treated as projects let nonConfigProjectDirectories: string[] = [] const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath) @@ -140,8 +147,14 @@ async function resolveWorkspaceProjectConfigs( // user can specify a directory that should be used as a project // the config file inside will be resolved by the default resolver later else if (stats.isDirectory()) { - const directory = file[file.length - 1] === '/' ? file : `${file}/` - nonConfigProjectDirectories.push(directory) + const configFile = await resolveDirectoryConfig(file) + if (configFile) { + workspaceConfigFiles.push(configFile) + } + else { + const directory = file[file.length - 1] === '/' ? file : `${file}/` + nonConfigProjectDirectories.push(directory) + } } else { throw new TypeError(`Unexpected file type: ${file}`) @@ -175,19 +188,25 @@ async function resolveWorkspaceProjectConfigs( const workspacesFs = await fg(workspaceGlobMatches, globOptions) - workspacesFs.forEach((filepath) => { + await Promise.all(workspacesFs.map(async (filepath) => { // the directories are allowed with a glob like `packages/*` if (filepath.endsWith('/')) { - nonConfigProjectDirectories.push(filepath) + const configFile = await resolveDirectoryConfig(filepath) + if (configFile) { + workspaceConfigFiles.push(configFile) + } + else { + nonConfigProjectDirectories.push(filepath) + } } else { workspaceConfigFiles.push(filepath) } - }) + })) } } - const projectConfigFiles = [...new Set(workspaceConfigFiles)] + const projectConfigFiles = Array.from(new Set(workspaceConfigFiles)) const duplicateDirectories = new Set() for (const config of projectConfigFiles) { @@ -222,3 +241,12 @@ async function resolveWorkspaceProjectConfigs( configFiles: projectConfigFiles, } } + +async function resolveDirectoryConfig(directory: string) { + const files = new Set(await fs.readdir(directory)) + const configFile = defaultConfigFiles.find(file => files.has(file)) + if (configFile) { + return resolve(directory, configFile) + } + return null +} From 5d41d7d2729b11cfaf5fd70782e5ae69efe4d5b2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:45:10 +0200 Subject: [PATCH 08/16] chore: cleanup --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index e397506b27ef..c8abaa827b4b 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -145,7 +145,6 @@ async function resolveWorkspaceProjectConfigs( workspaceConfigFiles.push(file) } // user can specify a directory that should be used as a project - // the config file inside will be resolved by the default resolver later else if (stats.isDirectory()) { const configFile = await resolveDirectoryConfig(file) if (configFile) { @@ -189,7 +188,7 @@ async function resolveWorkspaceProjectConfigs( const workspacesFs = await fg(workspaceGlobMatches, globOptions) await Promise.all(workspacesFs.map(async (filepath) => { - // the directories are allowed with a glob like `packages/*` + // directories are allowed with a glob like `packages/*` if (filepath.endsWith('/')) { const configFile = await resolveDirectoryConfig(filepath) if (configFile) { From 7d4ac238eb859963acaff78aac0c538b81c396b9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:45:31 +0200 Subject: [PATCH 09/16] chore: cleanup --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index c8abaa827b4b..c5d4a13f5440 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -118,7 +118,7 @@ async function resolveWorkspaceProjectConfigs( // project configurations that were specified directly const projectsOptions: UserWorkspaceConfig[] = [] - // custom config files that were specified directly + // custom config files that were specified directly or resolved from a directory const workspaceConfigFiles: string[] = [] // custom glob matches that should be resolved as directories or config files From 043d30b60045c53f40011172f21c6b1a6ed933fd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:49:15 +0200 Subject: [PATCH 10/16] chore: cleanup --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index c5d4a13f5440..775e188f2c8a 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -99,8 +99,8 @@ export async function resolveWorkspace( throw new Error([ `Project name "${name}"`, project.server.config.configFile ? ` from "${relative(vitest.config.root, project.server.config.configFile)}"` : '', - ' is not unique. ', - duplicate?.server.config.configFile ? `The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '', + ' is not unique.', + duplicate?.server.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '', ' All projects in a workspace should have unique names. Make sure your configuration is correct.', ].join('')) } From 7dd1ab9546d1dd7d404a1717bed35c577c1e6e95 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 16:49:44 +0200 Subject: [PATCH 11/16] chore: cleanup --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 775e188f2c8a..5867ae080dc7 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -88,7 +88,7 @@ export async function resolveWorkspace( const resolvedProjects = await Promise.all([ ...projects, - ...await Promise.all(projectPromises), + ...projectPromises, ]) const names = new Set() From b9a7e1d4bf5b49220aadeae674ba16495e1edf07 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 17:36:02 +0200 Subject: [PATCH 12/16] chore: cleanup --- .../vitest/src/node/workspace/resolveWorkspace.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 5867ae080dc7..8044f2c25bbf 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -65,7 +65,11 @@ export async function resolveWorkspace( process.chdir(dir) } projects.push( - await initializeProject(filepath, vitest, { workspaceConfigPath, test: cliOverrides }), + await initializeProject( + filepath, + vitest, + { workspaceConfigPath, test: cliOverrides }, + ), ) } } @@ -79,7 +83,11 @@ export async function resolveWorkspace( projectConfigs.forEach((options, index) => { // we can resolve these in parallel because process.cwd() is not changed - projectPromises.push(initializeProject(index, vitest, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any)) + projectPromises.push(initializeProject( + index, + vitest, + mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any, + )) }) if (!projects.length && !projectPromises.length) { From b1d60cc2db31dd79c75091d6a52dab993dc1c268 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 18:00:49 +0200 Subject: [PATCH 13/16] test: workspace runs correctly with several configs --- .../workspace/several-configs/test/1_test.test.ts | 5 +++++ .../workspace/several-configs/test/2_test.test.ts | 5 +++++ .../several-configs/test/vitest.config.one.ts | 8 ++++++++ .../several-configs/test/vitest.config.two.ts | 8 ++++++++ .../workspace/several-configs/vitest.workspace.ts | 3 +++ test/config/test/workspace.test.ts | 13 +++++++++++++ 6 files changed, 42 insertions(+) create mode 100644 test/config/fixtures/workspace/several-configs/test/1_test.test.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/2_test.test.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts create mode 100644 test/config/fixtures/workspace/several-configs/vitest.workspace.ts diff --git a/test/config/fixtures/workspace/several-configs/test/1_test.test.ts b/test/config/fixtures/workspace/several-configs/test/1_test.test.ts new file mode 100644 index 000000000000..93bab1515828 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/1_test.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('1 + 1 = 2', () => { + expect(1 + 1).toBe(2); +}) diff --git a/test/config/fixtures/workspace/several-configs/test/2_test.test.ts b/test/config/fixtures/workspace/several-configs/test/2_test.test.ts new file mode 100644 index 000000000000..46bfb80b3a71 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/2_test.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('2 + 2 = 4', () => { + expect(2 + 2).toBe(4); +}) diff --git a/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts b/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts new file mode 100644 index 000000000000..1ed79a0766c2 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + name: '1_test', + include: ['./1_test.test.ts'], + } +}) diff --git a/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts b/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts new file mode 100644 index 000000000000..8633fc14f624 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + name: '2_test', + include: ['./2_test.test.ts'], + } +}) diff --git a/test/config/fixtures/workspace/several-configs/vitest.workspace.ts b/test/config/fixtures/workspace/several-configs/vitest.workspace.ts new file mode 100644 index 000000000000..1efaf00d4482 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/vitest.workspace.ts @@ -0,0 +1,3 @@ +export default [ + './test/*.config.*.ts' +] \ No newline at end of file diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index f7edc226d12c..f0147b0ff7d8 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -10,3 +10,16 @@ it('correctly runs workspace tests when workspace config path is specified', asy expect(stdout).toContain('1 + 1 = 2') expect(stdout).not.toContain('2 + 2 = 4') }) + +it('runs the workspace if there are several vitest config files', async () => { + const { stderr, stdout } = await runVitest({ + root: 'fixtures/workspace/several-configs', + workspace: './fixtures/workspace/several-configs/vitest.workspace.ts', + }) + expect(stderr).toBe('') + expect(stdout).toContain('workspace/several-configs') + expect(stdout).toContain('| 1_test') + expect(stdout).toContain('| 2_test') + expect(stdout).toContain('1 + 1 = 2') + expect(stdout).toContain('2 + 2 = 4') +}) From 4df1024939c624d813f43c6ad580ce35f7611917 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 18:14:06 +0200 Subject: [PATCH 14/16] test: more workspace tests --- .../src/node/workspace/resolveWorkspace.ts | 32 ++----------------- .../vitest.workspace.ts | 6 ++++ .../vitest1.config.js | 5 +++ .../vitest2.config.js | 5 +++ .../vitest.workspace.ts | 14 ++++++++ .../vitest.workspace.ts | 5 +++ test/config/test/workspace.test.ts | 31 ++++++++++++++++++ test/test-utils/index.ts | 5 ++- 8 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js create mode 100644 test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts create mode 100644 test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 8044f2c25bbf..3889a87fec58 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -3,7 +3,6 @@ import { isMainThread } from 'node:worker_threads' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import fg from 'fast-glob' -import c from 'tinyrainbow' import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config' import type { Vitest } from '../core' import type { UserConfig } from '../types/config' @@ -133,7 +132,7 @@ async function resolveWorkspaceProjectConfigs( const workspaceGlobMatches: string[] = [] // directories that don't have a config file inside, but should be treated as projects - let nonConfigProjectDirectories: string[] = [] + const nonConfigProjectDirectories: string[] = [] const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath) @@ -144,7 +143,7 @@ async function resolveWorkspaceProjectConfigs( 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}`) + throw new Error(`Workspace config file "${relativeWorkpaceConfigPath}" references a non-existing file or a directory: ${file}`) } const stats = await fs.stat(file) @@ -214,33 +213,6 @@ async function resolveWorkspaceProjectConfigs( } const projectConfigFiles = Array.from(new Set(workspaceConfigFiles)) - const duplicateDirectories = new Set() - - for (const config of projectConfigFiles) { - // ignore custom config names because it won't be picked up by the default resolver later - if (!defaultConfigFiles.includes(config)) { - continue - } - - const configDirectory = `${dirname(config)}/` - // if for some reason there is already a config file in a directory that was found by a glob - // we remove it from the list to avoid duplicates when the project is initialized - for (const directory of nonConfigProjectDirectories) { - if (directory === configDirectory) { - vitest.logger.warn( - c.yellow( - `The specified config file "${resolve(vitest.config.root, config)}" is located in the directory already found by a glob match. ` - + `The config file will override the directory match to avoid duplicates. You can silence this message by excluding the directory from the glob in "${relativeWorkpaceConfigPath}".`, - ), - ) - duplicateDirectories.add(directory) - } - } - } - - if (duplicateDirectories.size) { - nonConfigProjectDirectories = nonConfigProjectDirectories.filter(dir => !duplicateDirectories.has(dir)) - } return { projectConfigs: projectsOptions, diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts new file mode 100644 index 000000000000..1aae3649d451 --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts @@ -0,0 +1,6 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + './vitest1.config.js', + './vitest2.config.js', +]) \ No newline at end of file diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js new file mode 100644 index 000000000000..e7efee0ec43d --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js @@ -0,0 +1,5 @@ +export default { + test: { + name: 'test', + } +} diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js new file mode 100644 index 000000000000..e7efee0ec43d --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js @@ -0,0 +1,5 @@ +export default { + test: { + name: 'test', + } +} diff --git a/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts new file mode 100644 index 000000000000..5ac17ebfad18 --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts @@ -0,0 +1,14 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + test: { + name: 'test', + }, + }, + { + test: { + name: 'test', + }, + }, +]) \ No newline at end of file diff --git a/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts new file mode 100644 index 000000000000..abfecd280a0c --- /dev/null +++ b/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts @@ -0,0 +1,5 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + './vitest.config.js' +]) \ No newline at end of file diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index f0147b0ff7d8..7640843f1ed6 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -1,4 +1,5 @@ import { expect, it } from 'vitest' +import { resolve } from 'pathe' import { runVitest } from '../../test-utils' it('correctly runs workspace tests when workspace config path is specified', async () => { @@ -23,3 +24,33 @@ it('runs the workspace if there are several vitest config files', async () => { expect(stdout).toContain('1 + 1 = 2') expect(stdout).toContain('2 + 2 = 4') }) + +it('fails if project names are identical with a nice error message', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-duplicate-configs', + workspace: './fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + 'Project name "test" from "vitest2.config.js" is not unique. The project is already defined by "vitest1.config.js". All projects in a workspace should have unique names. Make sure your configuration is correct.', + ) +}) + +it('fails if project names are identical inside the inline config', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-duplicate-inline', + workspace: './fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + 'Project name "test" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.', + ) +}) + +it('fails if referenced file doesnt exist', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-non-existing-config', + workspace: './fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + `Workspace config file "vitest.workspace.ts" references a non-existing file or a directory: ${resolve('fixtures/workspace/invalid-non-existing-config/vitest.config.js')}`, + ) +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index c0c0b34a352b..b8ccd158299b 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -12,6 +12,7 @@ import { Cli } from './cli' interface VitestRunnerCLIOptions { std?: 'inherit' + fails?: boolean } export async function runVitest( @@ -83,7 +84,9 @@ export async function runVitest( }) } catch (e: any) { - console.error(e) + if (runnerOptions.fails !== true) { + console.error(e) + } cli.stderr += e.stack } finally { From 374c9d5598a755890c0f74a67b003a0df482f520 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 18:33:04 +0200 Subject: [PATCH 15/16] chore: add docs to workspace resolution --- packages/vitest/src/node/core.ts | 4 +-- .../src/node/workspace/resolveWorkspace.ts | 30 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 27387a9d30e3..c940e63a73c7 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -193,7 +193,7 @@ export class Vitest { /** * @internal */ - async createCoreProject() { + async _createCoreProject() { this.coreWorkspaceProject = await WorkspaceProject.createCoreProject(this) return this.coreWorkspaceProject } @@ -242,7 +242,7 @@ export class Vitest { const workspaceConfigPath = await this.getWorkspaceConfigPath() if (!workspaceConfigPath) { - return [await this.createCoreProject()] + return [await this._createCoreProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 3889a87fec58..20d533d6f4aa 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -22,6 +22,8 @@ export async function resolveWorkspace( workspaceDefinition, ) + // cli options that affect the project config, + // not all options are allowed to be overridden const overridesOptions = [ 'logHeapUsage', 'allowOnly', @@ -54,14 +56,19 @@ export async function resolveWorkspace( try { // we have to resolve them one by one because CWD should depend on the project for (const filepath of [...configFiles, ...nonConfigDirectories]) { + // if file leads to the root config, then we can just reuse it because we already initialized it if (vitest.server.config.configFile === filepath) { - const project = await vitest.createCoreProject() + const project = await vitest._createCoreProject() projects.push(project) continue } - const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) + + const directory = filepath.endsWith('/') + ? filepath.slice(0, -1) + : dirname(filepath) + if (isMainThread) { - process.chdir(dir) + process.chdir(directory) } projects.push( await initializeProject( @@ -89,8 +96,9 @@ export async function resolveWorkspace( )) }) + // pretty rare case - the glob didn't match anything and there are no inline configs if (!projects.length && !projectPromises.length) { - return [await vitest.createCoreProject()] + return [await vitest._createCoreProject()] } const resolvedProjects = await Promise.all([ @@ -99,6 +107,7 @@ export async function resolveWorkspace( ]) const names = new Set() + // project names are guaranteed to be unique for (const project of resolvedProjects) { const name = project.getName() if (names.has(name)) { @@ -139,6 +148,8 @@ async function resolveWorkspaceProjectConfigs( for (const definition of workspaceDefinition) { if (typeof definition === 'string') { const stringOption = definition.replace('', vitest.config.root) + // if the string doesn't contain a glob, we can resolve it directly + // ['./vitest.config.js'] if (!stringOption.includes('*')) { const file = resolve(vitest.config.root, stringOption) @@ -163,13 +174,17 @@ async function resolveWorkspaceProjectConfigs( } } else { + // should never happen throw new TypeError(`Unexpected file type: ${file}`) } } + // if the string is a glob pattern, resolve it later + // ['./packages/*'] else { workspaceGlobMatches.push(stringOption) } } + // if the config is inlined, we can resolve it immediately else if (typeof definition === 'function') { projectsOptions.push(await definition({ command: vitest.server.config.command, @@ -178,6 +193,7 @@ async function resolveWorkspaceProjectConfigs( isSsrBuild: false, })) } + // the config is an object or a Promise that returns an object else { projectsOptions.push(await definition) } @@ -194,8 +210,10 @@ async function resolveWorkspaceProjectConfigs( const workspacesFs = await fg(workspaceGlobMatches, globOptions) - await Promise.all(workspacesFs.map(async (filepath) => { + await Promise.all(workspacesFs.map(async (filepath_) => { + const filepath = resolve(filepath_) // 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 (configFile) { @@ -223,6 +241,8 @@ async function resolveWorkspaceProjectConfigs( async function resolveDirectoryConfig(directory: string) { const files = new Set(await fs.readdir(directory)) + // default resolution looks for vitest.config.* or vite.config.* files + // this simulates how `findUp` works in packages/vitest/src/node/create.ts:29 const configFile = defaultConfigFiles.find(file => files.has(file)) if (configFile) { return resolve(directory, configFile) From 2c7a4933beec37b66f9807eaf7c358bd5d3048ae Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 12 Aug 2024 18:44:45 +0200 Subject: [PATCH 16/16] chore: remove resolve --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 20d533d6f4aa..a84857be76d6 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -210,8 +210,7 @@ async function resolveWorkspaceProjectConfigs( const workspacesFs = await fg(workspaceGlobMatches, globOptions) - await Promise.all(workspacesFs.map(async (filepath_) => { - const filepath = resolve(filepath_) + await Promise.all(workspacesFs.map(async (filepath) => { // directories are allowed with a glob like `packages/*` // in this case every directory is treated as a project if (filepath.endsWith('/')) {