From d8ab4cff4b5f017b0c1300d5f556a1733a079b5f Mon Sep 17 00:00:00 2001 From: productdevbook Date: Sat, 3 Oct 2026 19:09:53 +0200 Subject: [PATCH 1/2] fix(dev): let a restarted fork reload changed `.env` values Editing `.env` restarts Nuxt in a new process, but the fork inherited the parent's `process.env`, including the values the old `.env` had put there. c12 only overwrites variables it set itself and keeps that record per process, so in the fork the stale values looked like shell variables and won: a changed value was never picked up, and a removed one stayed. Drop the variables c12 set from `.env` before forking, so the fork reads the files afresh. Variables from the shell are untouched. Co-Authored-By: Claude Opus 5.5 --- packages/nuxt-cli/src/dev/pool.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/nuxt-cli/src/dev/pool.ts b/packages/nuxt-cli/src/dev/pool.ts index de1df23a8..1e1660472 100644 --- a/packages/nuxt-cli/src/dev/pool.ts +++ b/packages/nuxt-cli/src/dev/pool.ts @@ -130,7 +130,7 @@ export class ForkPool { execArgv: ['--enable-source-maps'], stdio: pipeOutput ? ['ignore', 'pipe', 'pipe', 'ipc'] : undefined, env: { - ...process.env, + ...withoutDotenvVars(process.env), __NUXT__FORK: 'true', ...pipeOutput ? { @@ -250,6 +250,19 @@ function trackServing(child: ChildProcess): Promise { }) } +/** + * The environment without the variables `.env` files set in this process, so a + * fork reads them afresh. c12 only overwrites variables it set itself, and its + * record of those does not survive into a new process. + */ +function withoutDotenvVars(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const dotenvVars = (globalThis as { __c12_dotenv_vars__?: Map> }).__c12_dotenv_vars__?.get(env) + if (!dotenvVars?.size) { + return env + } + return Object.fromEntries(Object.entries(env).filter(([key]) => !dotenvVars.has(key))) +} + /** Colour settings for a piped fork, which cannot detect colour support itself. */ function forcedColorEnv(): Record { if (process.env.NO_COLOR || process.env.FORCE_COLOR) { From afea75d1d00dd3752c47c623d31335b7c3bb648f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 5 Oct 2026 15:20:38 +0100 Subject: [PATCH 2/2] chore: add test + remove comment --- packages/nuxt-cli/src/dev/pool.ts | 5 -- packages/nuxt-cli/test/e2e/dev-dotenv.spec.ts | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 packages/nuxt-cli/test/e2e/dev-dotenv.spec.ts diff --git a/packages/nuxt-cli/src/dev/pool.ts b/packages/nuxt-cli/src/dev/pool.ts index 1e1660472..ff8e0c3fc 100644 --- a/packages/nuxt-cli/src/dev/pool.ts +++ b/packages/nuxt-cli/src/dev/pool.ts @@ -250,11 +250,6 @@ function trackServing(child: ChildProcess): Promise { }) } -/** - * The environment without the variables `.env` files set in this process, so a - * fork reads them afresh. c12 only overwrites variables it set itself, and its - * record of those does not survive into a new process. - */ function withoutDotenvVars(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const dotenvVars = (globalThis as { __c12_dotenv_vars__?: Map> }).__c12_dotenv_vars__?.get(env) if (!dotenvVars?.size) { diff --git a/packages/nuxt-cli/test/e2e/dev-dotenv.spec.ts b/packages/nuxt-cli/test/e2e/dev-dotenv.spec.ts new file mode 100644 index 000000000..ac1ac5ca7 --- /dev/null +++ b/packages/nuxt-cli/test/e2e/dev-dotenv.spec.ts @@ -0,0 +1,73 @@ +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { getPort } from 'get-port-please' +import { expect, it, vi } from 'vitest' +import { createDevFixture } from '../utils' + +const fixtureDir = await createDevFixture('dev-dotenv') +const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url)) + +it('should reload dotenv values across successive fork restarts while preserving shell variables', { timeout: 180_000 }, async () => { + await writeFile(join(fixtureDir, 'nuxt.config.ts'), `export default defineNuxtConfig({ + runtimeConfig: { public: { marker: 'default', shell: 'default' } }, +}) +`) + await writeFile(join(fixtureDir, 'server/api/dotenv.ts'), `export default defineEventHandler(() => ({ + pid: process.pid, + ...useRuntimeConfig().public, +})) +`) + const dotenv = join(fixtureDir, '.env') + await writeFile(dotenv, 'NUXT_PUBLIC_MARKER=before\nNUXT_PUBLIC_SHELL=file\n') + + const host = '127.0.0.1' + const port = await getPort({ host }) + const server = spawn(process.execPath, [nuxi, 'dev', fixtureDir, '--host', host, '--port', String(port), '--no-tui'], { + env: { ...process.env, NODE_ENV: 'development', TEST: '', CI: '1', NUXT_PUBLIC_MARKER: undefined, NUXT_PUBLIC_SHELL: 'shell' }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + const exited = once(server, 'exit') + let output = '' + server.stdout.on('data', (chunk) => { + output += chunk + }) + server.stderr.on('data', (chunk) => { + output += chunk + }) + + async function waitForMarker(marker: string, previousPid?: number): Promise { + let pid = 0 + await vi.waitFor(async () => { + expect(server.exitCode, output).toBeNull() + const response = await fetch(`http://${host}:${port}/api/dotenv`, { signal: AbortSignal.timeout(5000) }) + expect(response.status).toBe(200) + const value = await response.json() as { pid: number, marker: string, shell: string } + expect(value).toMatchObject({ marker, shell: 'shell' }) + expect(value.pid).not.toBe(previousPid) + pid = value.pid + }, { timeout: 45_000, interval: 250 }) + return pid + } + + try { + let pid = await waitForMarker('before') + for (const marker of ['after', 'third']) { + await writeFile(dotenv, `NUXT_PUBLIC_MARKER=${marker}\nNUXT_PUBLIC_SHELL=file\n`) + pid = await waitForMarker(marker, pid) + } + await writeFile(dotenv, '') + await waitForMarker('default', pid) + } + catch (error) { + console.error(output) + throw error + } + finally { + server.kill('SIGTERM') + await exited + } +})