From d4003882a05b65754f7000280cef2884e957d333 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 3 Apr 2024 12:14:50 +0200 Subject: [PATCH] fix(workspace): set CWD to config directory, allow overriding local .env (#5476) --- packages/vitest/src/node/core.ts | 50 ++++++++++++++---- packages/vitest/src/node/workspace.ts | 51 ++++++++++++++----- packages/vitest/src/runtime/setup-common.ts | 8 +++ test/workspaces/.env.local | 4 ++ test/workspaces/globalTest.ts | 4 +- test/workspaces/space_1/.env.local | 2 + .../space_1/test/env-injected.spec.ts | 16 ++++++ test/workspaces/space_1/vite.config.ts | 5 ++ test/workspaces/vitest.config.ts | 5 ++ 9 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 test/workspaces/.env.local create mode 100644 test/workspaces/space_1/.env.local diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 18146a0c1e4f..d53f88f48366 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,4 +1,5 @@ import { existsSync, promises as fs } from 'node:fs' +import { isMainThread } from 'node:worker_threads' import type { ViteDevServer } from 'vite' import { mergeConfig } from 'vite' import { basename, dirname, join, normalize, relative, resolve } from 'pathe' @@ -312,23 +313,50 @@ export class Vitest { return acc }, {} as UserConfig) - const projects = filteredWorkspaces.map(async (workspacePath) => { - // don't start a new server, but reuse existing one - if ( - this.server.config.configFile === workspacePath - ) - return this.createCoreProject() - return initializeProject(workspacePath, this, { workspaceConfigPath, test: cliOverrides }) - }) + const cwd = process.cwd() + + const projects: (() => Promise)[] = [] + + 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(() => Promise.resolve(project)) + continue + } + const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) + if (isMainThread) + process.chdir(dir) + // this just resolves the config, later we also wait when the server is resolved, + // but we can do that in parallel because it doesn't depend on process.cwd() + // this is strictly a performance optimization so we don't need to wait for server to start + projects.push(await initializeProject(filepath, this, { workspaceConfigPath, test: cliOverrides })) + } + } + finally { + if (isMainThread) + process.chdir(cwd) + } + + const projectPromises: Promise<() => Promise>[] = [] projectsOptions.forEach((options, index) => { - projects.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any)) + // 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) + if (!projects.length && !projectPromises.length) return [await this.createCoreProject()] - const resolvedProjects = await Promise.all(projects) + const resolvedProjectsReceivers = [ + ...projects, + ...await Promise.all(projectPromises), + ] + // we need to wait when the server is resolved, we can do that in parallel + const resolvedProjects = await Promise.all( + resolvedProjectsReceivers.map(receiver => receiver()), + ) const names = new Set() for (const project of resolvedProjects) { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 7b17e73ed69c..c9df549c8324 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -12,6 +12,7 @@ import { deepMerge } from '../utils' import type { Typechecker } from '../typecheck/typechecker' import type { BrowserProvider } from '../types/browser' import { getBrowserProvider } from '../integrations/browser' +import { createDefer } from '../public/utils' import { isBrowserEnabled, resolveConfig } from './config' import { WorkspaceVitestPlugin } from './plugins/workspace' import { createViteServer } from './vite' @@ -39,22 +40,40 @@ export async function initializeProject(workspacePath: string | number, ctx: Vit : workspacePath.endsWith('/') ? workspacePath : dirname(workspacePath) ) - const config: ViteInlineConfig = { - ...options, - root, - logLevel: 'error', - 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 }), - ], - } + return new Promise<() => Promise>((resolve, reject) => { + const resolution = createDefer() + let configResolved = false + const config: ViteInlineConfig = { + ...options, + root, + logLevel: 'error', + configFile, + // this will make "mode": "test" | "benchmark" inside defineConfig + mode: options.test?.mode || options.mode || ctx.config.mode, + plugins: [ + { + name: 'vitest:workspace:resolve', + configResolved() { + configResolved = true + resolve(() => resolution) + }, + }, + ...options.plugins || [], + WorkspaceVitestPlugin(project, { ...options, root, workspacePath }), + ], + } - await createViteServer(config) + createViteServer(config) + .then(() => resolution.resolve(project)) + .catch((err) => { + if (configResolved) + resolution.reject(err) + else + reject(err) + }) - return project + return project + }) } export class WorkspaceProject { @@ -393,6 +412,10 @@ export class WorkspaceProject { inspectBrk: this.ctx.config.inspectBrk, alias: [], includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation, + env: { + ...this.server?.config.env, + ...this.config.env, + }, }, this.ctx.configOverride || {} as any) as ResolvedConfig } diff --git a/packages/vitest/src/runtime/setup-common.ts b/packages/vitest/src/runtime/setup-common.ts index 1430366f7833..d55cf4ca4aa2 100644 --- a/packages/vitest/src/runtime/setup-common.ts +++ b/packages/vitest/src/runtime/setup-common.ts @@ -10,6 +10,7 @@ let globalSetup = false export async function setupCommonEnv(config: ResolvedConfig) { resetRunOnceCounter() setupDefines(config.defines) + setupEnv(config.env) if (globalSetup) return @@ -26,6 +27,13 @@ function setupDefines(defines: Record) { (globalThis as any)[key] = defines[key] } +function setupEnv(env: Record) { + if (typeof process === 'undefined') + return + for (const key in env) + process.env[key] = env[key] +} + export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExecutor) { if (typeof config.diff !== 'string') return diff --git a/test/workspaces/.env.local b/test/workspaces/.env.local new file mode 100644 index 000000000000..da9f74ae3621 --- /dev/null +++ b/test/workspaces/.env.local @@ -0,0 +1,4 @@ +VITE_MY_TEST_VARIABLE=core +VITE_CORE_VARIABLE=core +CUSTOM_ROOT=custom +ROOT_VARIABLE=root \ No newline at end of file diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index 58d9233401cc..5267232fb7e2 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -33,8 +33,8 @@ export async function teardown() { try { assert.ok(results.success) assert.equal(results.numTotalTestSuites, 28) - assert.equal(results.numTotalTests, 29) - assert.equal(results.numPassedTests, 29) + assert.equal(results.numTotalTests, 30) + assert.equal(results.numPassedTests, 30) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) diff --git a/test/workspaces/space_1/.env.local b/test/workspaces/space_1/.env.local new file mode 100644 index 000000000000..b350e3f42a13 --- /dev/null +++ b/test/workspaces/space_1/.env.local @@ -0,0 +1,2 @@ +VITE_MY_TEST_VARIABLE=local +CUSTOM_MY_TEST_VARIABLE=custom \ No newline at end of file diff --git a/test/workspaces/space_1/test/env-injected.spec.ts b/test/workspaces/space_1/test/env-injected.spec.ts index 2235912a7966..9ac381fa72de 100644 --- a/test/workspaces/space_1/test/env-injected.spec.ts +++ b/test/workspaces/space_1/test/env-injected.spec.ts @@ -7,3 +7,19 @@ declare global { test('dev is injected', () => { expect(__DEV__).toBe(true) }) + +test('env variable is assigned', () => { + // we override it with "local" in .env.local, but dotenv prefers the root .env + // this is consistent with how Vite works + expect(import.meta.env.VITE_MY_TEST_VARIABLE).toBe('core') + expect(process.env.VITE_MY_TEST_VARIABLE).toBe('core') + expect(import.meta.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom') + expect(process.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom') + + expect(process.env.VITE_CORE_VARIABLE).toBe('core') + expect(process.env.CUSTOM_ROOT).toBe('custom') + expect(process.env.ROOT_VARIABLE).toBe('root') + expect(process.env.CONFIG_VAR).toBe('root') + expect(process.env.CONFIG_LOCAL).toBe('local') + expect(process.env.CONFIG_OVERRIDE).toBe('local') +}) diff --git a/test/workspaces/space_1/vite.config.ts b/test/workspaces/space_1/vite.config.ts index cd3b46cb1836..2423c17577bd 100644 --- a/test/workspaces/space_1/vite.config.ts +++ b/test/workspaces/space_1/vite.config.ts @@ -1,11 +1,16 @@ import { defineProject } from 'vitest/config' export default defineProject({ + envPrefix: ['VITE_', 'CUSTOM_'], define: { __DEV__: 'true', }, test: { name: 'space_1', environment: 'happy-dom', + env: { + CONFIG_LOCAL: 'local', + CONFIG_OVERRIDE: 'local', + }, }, }) diff --git a/test/workspaces/vitest.config.ts b/test/workspaces/vitest.config.ts index 7e7b028bf25d..5377d72a8ebf 100644 --- a/test/workspaces/vitest.config.ts +++ b/test/workspaces/vitest.config.ts @@ -7,6 +7,7 @@ if (process.env.TEST_WATCH) { } export default defineConfig({ + envPrefix: ['VITE_', 'CUSTOM_', 'ROOT_'], test: { coverage: { enabled: true, @@ -15,5 +16,9 @@ export default defineConfig({ reporters: ['default', 'json'], outputFile: './results.json', globalSetup: './globalTest.ts', + env: { + CONFIG_VAR: 'root', + CONFIG_OVERRIDE: 'root', + }, }, })