diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index d70908e6e3cd9b..9be47ed114ae2b 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -33,6 +33,7 @@ markBootstrapComplete(); // TODO(MoLow): Make kill signal configurable const kKillSignal = 'SIGTERM'; const kShouldFilterModules = getOptionValue('--watch-path').length === 0; +const kEnvFile = getOptionValue('--env-file'); const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)); const kPreserveOutput = getOptionValue('--watch-preserve-output'); const kCommand = ArrayPrototypeSlice(process.argv, 1); @@ -73,6 +74,9 @@ function start() { }, }); watcher.watchChildProcessModules(child); + if (kEnvFile) { + watcher.filterFile(resolve(kEnvFile)); + } child.once('exit', (code) => { exited = true; if (code === 0) { diff --git a/src/node.cc b/src/node.cc index c314b22c5c7289..50d1d17942194a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -330,7 +330,10 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } #endif - if (env->options()->has_env_file_string) { + // Ignore env file if we're in watch mode. + // Without it env is not updated when restarting child process. + // Child process has --watch flag removed, so it will load the file. + if (env->options()->has_env_file_string && !env->options()->watch_mode) { per_process::dotenv_file.SetEnvironment(env); } diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 300e4f4e8dc472..bdb0750e398e16 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -30,6 +30,66 @@ function createTmpFile(content = 'console.log("running");', ext = '.js', basenam return file; } +function runInBackground({ args = [], options = {}, completed = 'Completed running', shouldFail = false }) { + let future = Promise.withResolvers(); + let child; + let stderr = ''; + let stdout = []; + + const run = () => { + args.unshift('--no-warnings'); + child = spawn(execPath, args, { encoding: 'utf8', stdio: 'pipe', ...options }); + + child.stderr.on('data', (data) => { + stderr += data; + }); + + const rl = createInterface({ input: child.stdout }); + rl.on('line', (data) => { + if (!data.startsWith('Waiting for graceful termination') && !data.startsWith('Gracefully restarted')) { + stdout.push(data); + if (data.startsWith(completed)) { + future.resolve({ stderr, stdout }); + future = Promise.withResolvers(); + stdout = []; + stderr = ''; + } else if (data.startsWith('Failed running')) { + if (shouldFail) { + future.resolve({ stderr, stdout }); + } else { + future.reject({ stderr, stdout }); + } + future = Promise.withResolvers(); + stdout = []; + stderr = ''; + } + } + }); + }; + + return { + async done() { + child?.kill(); + future.resolve(); + return { stdout, stderr }; + }, + restart(timeout = 1000) { + if (!child) { + run(); + } + const timer = setTimeout(() => { + if (!future.resolved) { + child.kill(); + future.reject(new Error('Timed out waiting for restart')); + } + }, timeout); + return future.promise.finally(() => { + clearTimeout(timer); + }); + } + }; +} + async function runWriteSucceed({ file, watchedFile, @@ -132,6 +192,56 @@ describe('watch mode', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_00 ]); }); + it('should reload env variables when --env-file changes', async () => { + const envKey = `TEST_ENV_${Date.now()}`; + const jsFile = createTmpFile(`console.log('ENV: ' + process.env.${envKey});`); + const envFile = createTmpFile(`${envKey}=value1`, '.env'); + const { done, restart } = runInBackground({ args: ['--watch', `--env-file=${envFile}`, jsFile] }); + + try { + await restart(); + writeFileSync(envFile, `${envKey}=value2`); + + // Second restart, after env change + const { stdout, stderr } = await restart(); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + `Restarting ${inspect(jsFile)}`, + 'ENV: value2', + `Completed running ${inspect(jsFile)}`, + ]); + } finally { + await done(); + } + }); + + it('should load new env variables when --env-file changes', async () => { + const envKey = `TEST_ENV_${Date.now()}`; + const envKey2 = `TEST_ENV_2_${Date.now()}`; + const jsFile = createTmpFile(`console.log('ENV: ' + process.env.${envKey} + '\\n' + 'ENV2: ' + process.env.${envKey2});`); + const envFile = createTmpFile(`${envKey}=value1`, '.env'); + const { done, restart } = runInBackground({ args: ['--watch', `--env-file=${envFile}`, jsFile] }); + + try { + await restart(); + await writeFileSync(envFile, `${envKey}=value1\n${envKey2}=newValue`); + + // Second restart, after env change + const { stderr, stdout } = await restart(); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + `Restarting ${inspect(jsFile)}`, + 'ENV: value1', + 'ENV2: newValue', + `Completed running ${inspect(jsFile)}`, + ]); + } finally { + await done(); + } + }); + it('should watch changes to a failing file', async () => { const file = createTmpFile('throw new Error("fails");'); const { stderr, stdout } = await runWriteSucceed({