diff --git a/packages/vitest/src/node/plugins/envReplacer.ts b/packages/vitest/src/node/plugins/envReplacer.ts deleted file mode 100644 index 28c7027e182d..000000000000 --- a/packages/vitest/src/node/plugins/envReplacer.ts +++ /dev/null @@ -1,30 +0,0 @@ -import MagicString from 'magic-string' -import type { Plugin } from 'vite' - -export const EnvReplacerPlugin = (): Plugin => { - return { - name: 'vitest:env-replacer', - enforce: 'pre', - transform(code) { - let s: MagicString | null = null - - const envs = code.matchAll(/\bimport\.meta\.env\b/g) - - for (const env of envs) { - s ||= new MagicString(code) - - const startIndex = env.index! - const endIndex = startIndex + env[0].length - - s.overwrite(startIndex, endIndex, 'process.env') - } - - if (s) { - return { - code: s.toString(), - map: s.generateMap({ hires: true }), - } - } - }, - } -} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index b1962c4f09c4..f9be67b3ca70 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -1,12 +1,11 @@ import type { Plugin as VitePlugin } from 'vite' import { configDefaults } from '../../defaults' -import type { UserConfig } from '../../types' +import type { ResolvedConfig, UserConfig } from '../../types' import { deepMerge, ensurePackageInstalled, notNullish } from '../../utils' import { resolveApiConfig } from '../config' import { Vitest } from '../core' import { GlobalSetupPlugin } from './globalSetup' import { MocksPlugin } from './mock' -import { EnvReplacerPlugin } from './envReplacer' export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()): Promise { let haveStarted = false @@ -27,6 +26,39 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) const preOptions = deepMerge({}, options, viteConfig.test ?? {}) preOptions.api = resolveApiConfig(preOptions) + // store defines for globalThis to make them + // reassignable when running in worker in src/runtime/setup.ts + const defines: Record = {} + + for (const key in viteConfig.define) { + const val = viteConfig.define[key] + let replacement: any + try { + replacement = typeof val === 'string' ? JSON.parse(val) : val + } + catch { + // probably means it contains reference to some variable, + // like this: "__VAR__": "process.env.VAR" + continue + } + if (key.startsWith('import.meta.env.')) { + const envKey = key.slice('import.meta.env.'.length) + process.env[envKey] = replacement + delete viteConfig.define[key] + } + else if (key.startsWith('process.env.')) { + const envKey = key.slice('process.env.'.length) + process.env[envKey] = replacement + delete viteConfig.define[key] + } + else if (!key.includes('.')) { + defines[key] = replacement + delete viteConfig.define[key] + } + } + + (options as ResolvedConfig).defines = defines + return { // we are setting NODE_ENV when running CLI to 'test', // but it can be overridden @@ -37,6 +69,14 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) // setting this option can bypass that and fallback to cjs version mainFields: [], }, + define: { + 'process.env.NODE_ENV': 'process.env.NODE_ENV', + 'global.process.env.NODE_ENV': 'global.process.env.NODE_ENV', + 'globalThis.process.env.NODE_ENV': 'globalThis.process.env.NODE_ENV', + // so people can reassign envs at runtime + // import.meta.env.VITE_NAME = 'app' -> process.env.VITE_NAME = 'app' + 'import.meta.env': 'process.env', + }, server: { ...preOptions.api, open: preOptions.ui && preOptions.open @@ -74,15 +114,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) for (const name in envs) process.env[name] ??= envs[name] - - // account for user env defines - for (const key in viteConfig.define) { - if (key.startsWith('import.meta.env.')) { - const val = viteConfig.define[key] - const envKey = key.slice('import.meta.env.'.length) - process.env[envKey] = typeof val === 'string' ? JSON.parse(val) : val - } - } }, async configureServer(server) { if (haveStarted) @@ -97,7 +128,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) await server.watcher.close() }, }, - EnvReplacerPlugin(), MocksPlugin(), GlobalSetupPlugin(ctx), options.ui diff --git a/packages/vitest/src/runtime/setup.ts b/packages/vitest/src/runtime/setup.ts index 7b7f9cefe065..70f2f73d4d9e 100644 --- a/packages/vitest/src/runtime/setup.ts +++ b/packages/vitest/src/runtime/setup.ts @@ -8,6 +8,10 @@ import { rpc } from './rpc' let globalSetup = false export async function setupGlobalEnv(config: ResolvedConfig) { + // should be redeclared for each test + // if run with "threads: false" + setupDefines(config.defines) + if (globalSetup) return @@ -20,6 +24,11 @@ export async function setupGlobalEnv(config: ResolvedConfig) { (await import('../integrations/globals')).registerApiGlobally() } +function setupDefines(defines: Record) { + for (const key in defines) + (globalThis as any)[key] = defines[key] +} + export function setupConsoleLogSpy() { const stdout = new Writable({ write(data, encoding, callback) { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index d245d7d69d45..61e0dcee63c2 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -324,5 +324,7 @@ export interface ResolvedConfig extends Omit, 'config' | 'f reporters: (Reporter | BuiltinReporters)[] + defines: Record + api?: ApiConfig } diff --git a/test/core/test/define.test.ts b/test/core/test/define.test.ts new file mode 100644 index 000000000000..15f87ba52e77 --- /dev/null +++ b/test/core/test/define.test.ts @@ -0,0 +1,55 @@ +import { afterAll, expect, test } from 'vitest' + +declare let __DEFINE__: string +declare let __JSON__: any +declare let __MODE__: string +declare let SOME: { + VARIABLE: string + SOME: { + VARIABLE: string + } +} + +// functions to test that they are not statically replaced +const get__DEFINE__ = () => __DEFINE__ +const get__JSON__ = () => __JSON__ +const get__MODE__ = () => __MODE__ + +const MODE = process.env.MODE + +afterAll(() => { + process.env.MODE = MODE +}) + +test('process.env.HELLO_PROCESS is defined on "defined" but exists on process.env', () => { + expect('HELLO_PROCESS' in process.env).toBe(true) + expect(process.env.HELLO_PROCESS).toBe('hello process') +}) + +test('can redeclare standard define', () => { + expect(get__DEFINE__()).toBe('defined') + __DEFINE__ = 'new defined' + expect(get__DEFINE__()).toBe('new defined') +}) + +test('can redeclare json object', () => { + expect(get__JSON__()).toEqual({ hello: 'world' }) + __JSON__ = { hello: 'test' } + const name = '__JSON__' + expect(get__JSON__()).toEqual({ hello: 'test' }) + expect((globalThis as any)[name]).toEqual({ hello: 'test' }) +}) + +test('reassigning __MODE__', () => { + const env = process.env.MODE + expect(get__MODE__()).toBe(env) + process.env.MODE = 'development' + expect(get__MODE__()).toBe('development') +}) + +test('dotted defines are processed by Vite, but cannot be reassigned', () => { + expect(SOME.VARIABLE).toBe('variable') + expect(SOME.SOME.VARIABLE).toBe('nested variable') + SOME.VARIABLE = 'new variable' + expect(SOME.VARIABLE).not.toBe('new variable') +}) diff --git a/test/core/vitest.config.ts b/test/core/vitest.config.ts index 3a2f0b1749d7..61dd6529fa35 100644 --- a/test/core/vitest.config.ts +++ b/test/core/vitest.config.ts @@ -19,6 +19,16 @@ export default defineConfig({ ], define: { 'import.meta.env.TEST_NAME': '"hello world"', + 'process.env.HELLO_PROCESS': '"hello process"', + // can reassign + '__DEFINE__': '"defined"', + '__JSON__': JSON.stringify({ hello: 'world' }), + // edge cases + // should not be available for reassigning as __MODE__ = 'test2' + // but can reassign with process.env.MODE = 'test2' + '__MODE__': 'process.env.MODE', + 'SOME.VARIABLE': '"variable"', + 'SOME.SOME.VARIABLE': '"nested variable"', }, test: { testTimeout: 2000,