Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watch: reload changes in contents of --env-file #54109

Merged
merged 2 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
// 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
Loading