Skip to content

Commit d400388

Browse files
authoredApr 3, 2024··
fix(workspace): set CWD to config directory, allow overriding local .env (#5476)
1 parent 1f54834 commit d400388

File tree

9 files changed

+118
-27
lines changed

9 files changed

+118
-27
lines changed
 

‎packages/vitest/src/node/core.ts

+39-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { existsSync, promises as fs } from 'node:fs'
2+
import { isMainThread } from 'node:worker_threads'
23
import type { ViteDevServer } from 'vite'
34
import { mergeConfig } from 'vite'
45
import { basename, dirname, join, normalize, relative, resolve } from 'pathe'
@@ -312,23 +313,50 @@ export class Vitest {
312313
return acc
313314
}, {} as UserConfig)
314315

315-
const projects = filteredWorkspaces.map(async (workspacePath) => {
316-
// don't start a new server, but reuse existing one
317-
if (
318-
this.server.config.configFile === workspacePath
319-
)
320-
return this.createCoreProject()
321-
return initializeProject(workspacePath, this, { workspaceConfigPath, test: cliOverrides })
322-
})
316+
const cwd = process.cwd()
317+
318+
const projects: (() => Promise<WorkspaceProject>)[] = []
319+
320+
try {
321+
// we have to resolve them one by one because CWD should depend on the project
322+
for (const filepath of filteredWorkspaces) {
323+
if (this.server.config.configFile === filepath) {
324+
const project = await this.createCoreProject()
325+
projects.push(() => Promise.resolve(project))
326+
continue
327+
}
328+
const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath)
329+
if (isMainThread)
330+
process.chdir(dir)
331+
// this just resolves the config, later we also wait when the server is resolved,
332+
// but we can do that in parallel because it doesn't depend on process.cwd()
333+
// this is strictly a performance optimization so we don't need to wait for server to start
334+
projects.push(await initializeProject(filepath, this, { workspaceConfigPath, test: cliOverrides }))
335+
}
336+
}
337+
finally {
338+
if (isMainThread)
339+
process.chdir(cwd)
340+
}
341+
342+
const projectPromises: Promise<() => Promise<WorkspaceProject>>[] = []
323343

324344
projectsOptions.forEach((options, index) => {
325-
projects.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any))
345+
// we can resolve these in parallel because process.cwd() is not changed
346+
projectPromises.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any))
326347
})
327348

328-
if (!projects.length)
349+
if (!projects.length && !projectPromises.length)
329350
return [await this.createCoreProject()]
330351

331-
const resolvedProjects = await Promise.all(projects)
352+
const resolvedProjectsReceivers = [
353+
...projects,
354+
...await Promise.all(projectPromises),
355+
]
356+
// we need to wait when the server is resolved, we can do that in parallel
357+
const resolvedProjects = await Promise.all(
358+
resolvedProjectsReceivers.map(receiver => receiver()),
359+
)
332360
const names = new Set<string>()
333361

334362
for (const project of resolvedProjects) {

‎packages/vitest/src/node/workspace.ts

+37-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { deepMerge } from '../utils'
1212
import type { Typechecker } from '../typecheck/typechecker'
1313
import type { BrowserProvider } from '../types/browser'
1414
import { getBrowserProvider } from '../integrations/browser'
15+
import { createDefer } from '../public/utils'
1516
import { isBrowserEnabled, resolveConfig } from './config'
1617
import { WorkspaceVitestPlugin } from './plugins/workspace'
1718
import { createViteServer } from './vite'
@@ -39,22 +40,40 @@ export async function initializeProject(workspacePath: string | number, ctx: Vit
3940
: workspacePath.endsWith('/') ? workspacePath : dirname(workspacePath)
4041
)
4142

42-
const config: ViteInlineConfig = {
43-
...options,
44-
root,
45-
logLevel: 'error',
46-
configFile,
47-
// this will make "mode": "test" | "benchmark" inside defineConfig
48-
mode: options.test?.mode || options.mode || ctx.config.mode,
49-
plugins: [
50-
...options.plugins || [],
51-
WorkspaceVitestPlugin(project, { ...options, root, workspacePath }),
52-
],
53-
}
43+
return new Promise<() => Promise<WorkspaceProject>>((resolve, reject) => {
44+
const resolution = createDefer<WorkspaceProject>()
45+
let configResolved = false
46+
const config: ViteInlineConfig = {
47+
...options,
48+
root,
49+
logLevel: 'error',
50+
configFile,
51+
// this will make "mode": "test" | "benchmark" inside defineConfig
52+
mode: options.test?.mode || options.mode || ctx.config.mode,
53+
plugins: [
54+
{
55+
name: 'vitest:workspace:resolve',
56+
configResolved() {
57+
configResolved = true
58+
resolve(() => resolution)
59+
},
60+
},
61+
...options.plugins || [],
62+
WorkspaceVitestPlugin(project, { ...options, root, workspacePath }),
63+
],
64+
}
5465

55-
await createViteServer(config)
66+
createViteServer(config)
67+
.then(() => resolution.resolve(project))
68+
.catch((err) => {
69+
if (configResolved)
70+
resolution.reject(err)
71+
else
72+
reject(err)
73+
})
5674

57-
return project
75+
return project
76+
})
5877
}
5978

6079
export class WorkspaceProject {
@@ -393,6 +412,10 @@ export class WorkspaceProject {
393412
inspectBrk: this.ctx.config.inspectBrk,
394413
alias: [],
395414
includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation,
415+
env: {
416+
...this.server?.config.env,
417+
...this.config.env,
418+
},
396419
}, this.ctx.configOverride || {} as any) as ResolvedConfig
397420
}
398421

‎packages/vitest/src/runtime/setup-common.ts

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ let globalSetup = false
1010
export async function setupCommonEnv(config: ResolvedConfig) {
1111
resetRunOnceCounter()
1212
setupDefines(config.defines)
13+
setupEnv(config.env)
1314

1415
if (globalSetup)
1516
return
@@ -26,6 +27,13 @@ function setupDefines(defines: Record<string, any>) {
2627
(globalThis as any)[key] = defines[key]
2728
}
2829

30+
function setupEnv(env: Record<string, any>) {
31+
if (typeof process === 'undefined')
32+
return
33+
for (const key in env)
34+
process.env[key] = env[key]
35+
}
36+
2937
export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExecutor) {
3038
if (typeof config.diff !== 'string')
3139
return

‎test/workspaces/.env.local

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
VITE_MY_TEST_VARIABLE=core
2+
VITE_CORE_VARIABLE=core
3+
CUSTOM_ROOT=custom
4+
ROOT_VARIABLE=root

‎test/workspaces/globalTest.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export async function teardown() {
3333
try {
3434
assert.ok(results.success)
3535
assert.equal(results.numTotalTestSuites, 28)
36-
assert.equal(results.numTotalTests, 29)
37-
assert.equal(results.numPassedTests, 29)
36+
assert.equal(results.numTotalTests, 30)
37+
assert.equal(results.numPassedTests, 30)
3838

3939
const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts'))
4040

‎test/workspaces/space_1/.env.local

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
VITE_MY_TEST_VARIABLE=local
2+
CUSTOM_MY_TEST_VARIABLE=custom

‎test/workspaces/space_1/test/env-injected.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,19 @@ declare global {
77
test('dev is injected', () => {
88
expect(__DEV__).toBe(true)
99
})
10+
11+
test('env variable is assigned', () => {
12+
// we override it with "local" in .env.local, but dotenv prefers the root .env
13+
// this is consistent with how Vite works
14+
expect(import.meta.env.VITE_MY_TEST_VARIABLE).toBe('core')
15+
expect(process.env.VITE_MY_TEST_VARIABLE).toBe('core')
16+
expect(import.meta.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom')
17+
expect(process.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom')
18+
19+
expect(process.env.VITE_CORE_VARIABLE).toBe('core')
20+
expect(process.env.CUSTOM_ROOT).toBe('custom')
21+
expect(process.env.ROOT_VARIABLE).toBe('root')
22+
expect(process.env.CONFIG_VAR).toBe('root')
23+
expect(process.env.CONFIG_LOCAL).toBe('local')
24+
expect(process.env.CONFIG_OVERRIDE).toBe('local')
25+
})
+5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { defineProject } from 'vitest/config'
22

33
export default defineProject({
4+
envPrefix: ['VITE_', 'CUSTOM_'],
45
define: {
56
__DEV__: 'true',
67
},
78
test: {
89
name: 'space_1',
910
environment: 'happy-dom',
11+
env: {
12+
CONFIG_LOCAL: 'local',
13+
CONFIG_OVERRIDE: 'local',
14+
},
1015
},
1116
})

‎test/workspaces/vitest.config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ if (process.env.TEST_WATCH) {
77
}
88

99
export default defineConfig({
10+
envPrefix: ['VITE_', 'CUSTOM_', 'ROOT_'],
1011
test: {
1112
coverage: {
1213
enabled: true,
@@ -15,5 +16,9 @@ export default defineConfig({
1516
reporters: ['default', 'json'],
1617
outputFile: './results.json',
1718
globalSetup: './globalTest.ts',
19+
env: {
20+
CONFIG_VAR: 'root',
21+
CONFIG_OVERRIDE: 'root',
22+
},
1823
},
1924
})

0 commit comments

Comments
 (0)
Please sign in to comment.