From 50d9ca3171c732ccdc5ad72c1af9a6a685045bd5 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Wed, 25 Feb 2026 21:00:21 +0800 Subject: [PATCH] fix(browser): remove default process shim while keep env stubbing support --- e2e/browser-mode/env.test.ts | 2 +- .../fixtures/env/rstest.config.ts | 4 +- .../fixtures/env/tests/env.test.ts | 26 +++++++-- packages/browser/src/client/entry.ts | 53 ++++++++++--------- packages/browser/src/hostController.ts | 6 +++ packages/core/src/cli/commands.ts | 2 +- packages/core/src/runtime/api/utilities.ts | 38 ++++++++++--- packages/core/src/types/config.ts | 2 +- packages/core/src/types/mock.ts | 5 +- 9 files changed, 95 insertions(+), 43 deletions(-) diff --git a/e2e/browser-mode/env.test.ts b/e2e/browser-mode/env.test.ts index 47b31b13d..7bd6d6d51 100644 --- a/e2e/browser-mode/env.test.ts +++ b/e2e/browser-mode/env.test.ts @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); describe('browser mode - env', () => { - it('should inject env into browser runtime via process shim', async () => { + it('should inject env into browser runtime without process shim', async () => { const { expectExecSuccess } = await runRstestCli({ command: 'rstest', args: ['run'], diff --git a/e2e/browser-mode/fixtures/env/rstest.config.ts b/e2e/browser-mode/fixtures/env/rstest.config.ts index f58715d59..e3f429bd0 100644 --- a/e2e/browser-mode/fixtures/env/rstest.config.ts +++ b/e2e/browser-mode/fixtures/env/rstest.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ env: { RSTEST_E2E_ENV_FOO: 'bar', RSTEST_E2E_ENV_EMPTY: '', - // In browser mode, env is proxied into a process shim. - // Setting a key to undefined should remove it from process.env. + // In browser mode, env is injected into runtime env store. + // Setting a key to undefined should remove it from the store. RSTEST_E2E_ENV_UNSET: undefined, }, }); diff --git a/e2e/browser-mode/fixtures/env/tests/env.test.ts b/e2e/browser-mode/fixtures/env/tests/env.test.ts index c75aa830b..990646386 100644 --- a/e2e/browser-mode/fixtures/env/tests/env.test.ts +++ b/e2e/browser-mode/fixtures/env/tests/env.test.ts @@ -1,14 +1,34 @@ -import { describe, expect, it } from '@rstest/core'; +import { describe, expect, it, rstest } from '@rstest/core'; describe('browser env injection', () => { - it('should expose process.env in browser and apply env changes', () => { + it('should apply env changes without injecting global process', () => { // Browser client ensures global alias exists for libraries expecting Node globals. expect((globalThis as any).global).toBe(globalThis); - expect(typeof (globalThis as any).process).toBe('object'); + expect((globalThis as any).process).toBeUndefined(); + expect((globalThis as any).__RSTEST_ENV__).toBeUndefined(); + expect(Object.hasOwn(globalThis, '__RSTEST_ENV__')).toBe(false); + expect(process.env.RSTEST_E2E_ENV_FOO).toBe('bar'); expect(process.env.RSTEST_E2E_ENV_EMPTY).toBe(''); expect(process.env.RSTEST_E2E_ENV_UNSET).toBeUndefined(); expect(Object.hasOwn(process.env, 'RSTEST_E2E_ENV_UNSET')).toBe(false); + + const originalFoo = process.env.RSTEST_E2E_ENV_FOO; + + rstest.stubEnv('RSTEST_E2E_ENV_FOO', 'changed'); + rstest.stubEnv('RSTEST_E2E_ENV_DYNAMIC', 'dynamic'); + + expect(process.env.RSTEST_E2E_ENV_FOO).toBe('changed'); + expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBe('dynamic'); + + rstest.stubEnv('RSTEST_E2E_ENV_DYNAMIC', undefined); + + expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBeUndefined(); + + rstest.unstubAllEnvs(); + + expect(process.env.RSTEST_E2E_ENV_FOO).toBe(originalFoo); + expect(process.env.RSTEST_E2E_ENV_DYNAMIC).toBeUndefined(); }); }); diff --git a/packages/browser/src/client/entry.ts b/packages/browser/src/client/entry.ts index de49eaf3b..5593cefe2 100644 --- a/packages/browser/src/client/entry.ts +++ b/packages/browser/src/client/entry.ts @@ -62,10 +62,13 @@ const debugLog = (...args: unknown[]): void => { } }; -type GlobalWithProcess = typeof globalThis & { - global?: typeof globalThis; - process?: NodeJS.Process; -}; +type RuntimeEnvStore = Record; +const RSTEST_ENV_SYMBOL = Symbol.for('rstest.env'); + +type GlobalWithRuntimeEnv = typeof globalThis & + Record & { + global?: typeof globalThis; + }; const REGEXP_FLAG_PREFIX = 'RSTEST_REGEXP:'; @@ -94,36 +97,34 @@ const restoreRuntimeConfig = ( }; }; -const ensureProcessEnv = (env: RuntimeConfig['env'] | undefined): void => { - const globalRef = globalThis as GlobalWithProcess; +const ensureRuntimeEnv = (env: RuntimeConfig['env'] | undefined): void => { + const globalRef = globalThis as GlobalWithRuntimeEnv; if (!globalRef.global) { globalRef.global = globalRef; } - if (!globalRef.process) { - const processShim: Partial & { - env: Record; - } = { - env: {}, - argv: [], - version: 'browser', - cwd: () => '/', - platform: 'linux', - nextTick: (cb: (...args: unknown[]) => void, ...args: unknown[]) => - queueMicrotask(() => cb(...args)), - }; - - globalRef.process = processShim as unknown as NodeJS.Process; + const existingEnv = globalRef[RSTEST_ENV_SYMBOL]; + let runtimeEnv: RuntimeEnvStore; + if (existingEnv && typeof existingEnv === 'object') { + runtimeEnv = existingEnv as RuntimeEnvStore; + } else { + runtimeEnv = {}; + globalRef[RSTEST_ENV_SYMBOL] = runtimeEnv; } - globalRef.process.env ??= {}; - if (env) { for (const [key, value] of Object.entries(env)) { - if (value === undefined) { - delete globalRef.process.env[key]; + const normalizedValue = + typeof value === 'string' + ? value + : value == null + ? undefined + : String(value); + + if (normalizedValue === undefined) { + delete runtimeEnv[key]; } else { - globalRef.process.env[key] = value; + runtimeEnv[key] = normalizedValue; } } } @@ -448,7 +449,7 @@ const run = async () => { } const runtimeConfig = restoreRuntimeConfig(projectRuntime.runtimeConfig); - ensureProcessEnv(runtimeConfig.env); + ensureRuntimeEnv(runtimeConfig.env); // Get this project's setup loaders and test context const currentSetupLoaders = diff --git a/packages/browser/src/hostController.ts b/packages/browser/src/hostController.ts index d4137b06f..ea28463ad 100644 --- a/packages/browser/src/hostController.ts +++ b/packages/browser/src/hostController.ts @@ -975,6 +975,12 @@ const createBrowserRuntime = async ({ resolve: { alias: rstestInternalAliases, }, + source: { + define: { + 'process.env': 'globalThis[Symbol.for("rstest.env")]', + 'import.meta.env': 'globalThis[Symbol.for("rstest.env")]', + }, + }, output: { target: 'web', // Enable source map for inline snapshot support diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index 89436ee78..3e0d781cb 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -105,7 +105,7 @@ const applyCommonOptions = (cli: CAC) => { ) .option( '--unstubEnvs', - 'Restores all `process.env` values that were changed with `rstest.stubEnv` before every test', + 'Restores all runtime env values that were changed with `rstest.stubEnv` before every test', ) .option( '--includeTaskLocation', diff --git a/packages/core/src/runtime/api/utilities.ts b/packages/core/src/runtime/api/utilities.ts index edb84c108..6e5c64630 100644 --- a/packages/core/src/runtime/api/utilities.ts +++ b/packages/core/src/runtime/api/utilities.ts @@ -11,6 +11,10 @@ import { initSpy } from './spy'; export const createRstestUtilities: ( workerState: WorkerState, ) => Promise = async (workerState) => { + type RuntimeEnvStore = Record; + const RSTEST_ENV_SYMBOL = Symbol.for('rstest.env'); + type GlobalWithRuntimeEnv = typeof globalThis & Record; + const originalEnvValues = new Map(); const originalGlobalValues = new Map< string | symbol | number, @@ -21,6 +25,22 @@ export const createRstestUtilities: ( let originalConfig: undefined | RuntimeConfig; + const resolveRuntimeEnv = (): RuntimeEnvStore => { + const globalRef = globalThis as GlobalWithRuntimeEnv; + const runtimeEnv = globalRef[RSTEST_ENV_SYMBOL]; + if (runtimeEnv && typeof runtimeEnv === 'object') { + return runtimeEnv as RuntimeEnvStore; + } + + if (typeof process !== 'undefined' && process.env) { + return process.env; + } + + const createdEnv: RuntimeEnvStore = {}; + globalRef[RSTEST_ENV_SYMBOL] = createdEnv; + return createdEnv; + }; + const timers = () => { if (!_timers) { _timers = new FakeTimers({ @@ -155,26 +175,30 @@ export const createRstestUtilities: ( }, stubEnv: (name: string, value: string | undefined): RstestUtilities => { + const runtimeEnv = resolveRuntimeEnv(); + if (!originalEnvValues.has(name)) { - originalEnvValues.set(name, process.env[name]); + originalEnvValues.set(name, runtimeEnv[name]); } - // update process.env + // update runtime env store if (value === undefined) { - delete process.env[name]; + delete runtimeEnv[name]; } else { - process.env[name] = value; + runtimeEnv[name] = value; } return rstest; }, unstubAllEnvs: (): RstestUtilities => { - // restore process.env + const runtimeEnv = resolveRuntimeEnv(); + + // restore runtime env store for (const [name, value] of originalEnvValues) { if (value === undefined) { - delete process.env[name]; + delete runtimeEnv[name]; } else { - process.env[name] = value; + runtimeEnv[name] = value; } } diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 2faed8787..ccc17d29c 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -368,7 +368,7 @@ export interface RstestConfig { */ unstubGlobals?: boolean; /** - * Restores all `process.env` values that were changed with `rstest.stubEnv` before every test. + * Restores all runtime env values that were changed with `rstest.stubEnv` before every test. * @default false */ unstubEnvs?: boolean; diff --git a/packages/core/src/types/mock.ts b/packages/core/src/types/mock.ts index fa9d42abc..77c341db9 100644 --- a/packages/core/src/types/mock.ts +++ b/packages/core/src/types/mock.ts @@ -469,12 +469,13 @@ export interface RstestUtilities { resetModules: () => RstestUtilities; /** - * Changes the value of environmental variable on `process.env`. + * Changes the value of an environment variable in the current runtime env store. + * Uses `process.env` in Node.js and runtime env store in browser mode. */ stubEnv: (name: string, value: string | undefined) => RstestUtilities; /** - * Restores all `process.env` values that were changed with `rstest.stubEnv`. + * Restores all env values that were changed with `rstest.stubEnv`. */ unstubAllEnvs: () => RstestUtilities;