Skip to content

Commit

Permalink
watch: reload changes in contents of --env-file
Browse files Browse the repository at this point in the history
Make sure we watch and reload on env file changes.

Ignore env file in parent process, so child process can reload
current vars when we recreate it.

Fixes: nodejs#54001
  • Loading branch information
marekpiechut committed Jul 29, 2024
1 parent 3c50297 commit 9d78bd5
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 1 deletion.
4 changes: 4 additions & 0 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -73,6 +74,9 @@ function start() {
},
});
watcher.watchChildProcessModules(child);
if (kEnvFile) {
watcher.filterFile(resolve(kEnvFile))
}
child.once('exit', (code) => {
exited = true;
if (code === 0) {
Expand Down
5 changes: 4 additions & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
}
#endif

if (env->options()->has_env_file_string) {
// Ignore env file if we're in watch mode or we won't update env
// loaded from file when reloading 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);
}

Expand Down
110 changes: 110 additions & 0 deletions test/sequential/test-watch-mode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit 9d78bd5

Please sign in to comment.