diff --git a/packages/nuxt-cli/src/dev/pool.ts b/packages/nuxt-cli/src/dev/pool.ts index de1df23a8..ff8e0c3fc 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,14 @@ function trackServing(child: ChildProcess): Promise { }) } +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) { 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 + } +})