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: #54001
PR-URL: #54109
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Moshe Atlow <[email protected]>
  • Loading branch information
marekpiechut authored and targos committed Aug 14, 2024
1 parent e218b7c commit 4d8b53e
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 @@ -379,7 +379,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.
// 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);
}

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 4d8b53e

Please sign in to comment.