Skip to content

Commit

Permalink
fix(workspace): correctly resolve workspace globs and file paths (#6316)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Aug 12, 2024
1 parent abd85e3 commit afdcb8f
Show file tree
Hide file tree
Showing 19 changed files with 390 additions and 164 deletions.
2 changes: 1 addition & 1 deletion packages/vitest/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
170 changes: 17 additions & 153 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -241,160 +242,23 @@ 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 {
default: ReturnType<typeof defineWorkspace>
}

if (!workspaceModule.default || !Array.isArray(workspaceModule.default)) {
throw new Error(`Workspace config file ${workspaceConfigPath} must export a default array of project paths.`)
}

const workspaceGlobMatches: string[] = []
const projectsOptions: UserWorkspaceConfig[] = []

for (const project of workspaceModule.default) {
if (typeof project === 'string') {
workspaceGlobMatches.push(project.replace('<rootDir>', 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<string, string[]>)

const filteredWorkspaces = Object.values(workspacesByFolder).map((configFiles) => {
if (configFiles.length === 1) {
return configFiles[0]
}
const vitestConfig = configFiles.find(configFile => basename(configFile).startsWith('vitest.config'))
return 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<WorkspaceProject>[] = []

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<string>()

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() {
Expand Down
12 changes: 6 additions & 6 deletions packages/vitest/src/node/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down
Loading

0 comments on commit afdcb8f

Please sign in to comment.