From e324efa5fbef602e566ad442a1133131651fece8 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:17:04 -0700 Subject: [PATCH 1/2] watch: strip underscore aliases of watch flags The C++ option parser treats `_` and `-` as the same character in option names, so `--watch_path` is the same as `--watch-path`. Watch mode compared spellings literally when it removed its own flags from `process.execArgv` and `NODE_OPTIONS` before spawning the child. The underscore spellings got through, so each child went back into watch mode and spawned another child, and the application never ran. Normalize option names before comparing them. Forward the original arguments to the child unchanged. Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Assisted-by: claude:opus-5.5 --- lib/internal/main/watch_mode.js | 36 ++++++++++++++----- test/sequential/test-watch-mode.mjs | 54 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index f0f1e26bb26c..367eee142883 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -7,6 +7,9 @@ const { ArrayPrototypePushApply, ArrayPrototypeSlice, StringPrototypeIncludes, + StringPrototypeIndexOf, + StringPrototypeReplaceAll, + StringPrototypeSlice, StringPrototypeStartsWith, } = primordials; @@ -41,9 +44,23 @@ const kPreserveOutput = getOptionValue('--watch-preserve-output'); const kCommand = ArrayPrototypeSlice(process.argv, 1); const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' ')); +// The C++ option parser treats `_` and `-` as equivalent in option names +// (e.g. --watch_path is --watch-path), so normalize before matching. +function normalizeOptionName(arg) { + if (!StringPrototypeStartsWith(arg, '--')) { + return arg; + } + const equalsIndex = StringPrototypeIndexOf(arg, '='); + if (equalsIndex === -1) { + return StringPrototypeReplaceAll(arg, '_', '-'); + } + return StringPrototypeReplaceAll(StringPrototypeSlice(arg, 0, equalsIndex), '_', '-') + + StringPrototypeSlice(arg, equalsIndex); +} + const argsWithoutWatchOptions = []; for (let i = 0; i < process.execArgv.length; i++) { - const arg = process.execArgv[i]; + const arg = normalizeOptionName(process.execArgv[i]); if (StringPrototypeStartsWith(arg, '--watch=')) { continue; } @@ -74,7 +91,7 @@ for (let i = 0; i < process.execArgv.length; i++) { if (arg === '--experimental-default-config-file') { continue; } - ArrayPrototypePush(argsWithoutWatchOptions, arg); + ArrayPrototypePush(argsWithoutWatchOptions, process.execArgv[i]); } ArrayPrototypePushApply(argsWithoutWatchOptions, kCommand); @@ -88,15 +105,16 @@ if (kNodeOptions != null) { const parts = parseNodeOptionsEnvVar(kNodeOptions); for (let i = 0; i < parts.length; i++) { const part = parts[i]; - if (part === '--watch' || - part === '--watch-preserve-output' || - StringPrototypeStartsWith(part, '--watch=') || - StringPrototypeStartsWith(part, '--watch-preserve-output=') || - StringPrototypeStartsWith(part, '--watch-path=') || - StringPrototypeStartsWith(part, '--watch-kill-signal=')) { + const name = normalizeOptionName(part); + if (name === '--watch' || + name === '--watch-preserve-output' || + StringPrototypeStartsWith(name, '--watch=') || + StringPrototypeStartsWith(name, '--watch-preserve-output=') || + StringPrototypeStartsWith(name, '--watch-path=') || + StringPrototypeStartsWith(name, '--watch-kill-signal=')) { continue; } - if (part === '--watch-path' || part === '--watch-kill-signal') { + if (name === '--watch-path' || name === '--watch-kill-signal') { // Skip the flag and its separate value argument i++; continue; diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 0c28adec0171..5ca9f9e0492a 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -1033,6 +1033,60 @@ process.on('message', (message) => { } }); + it('should strip underscore aliases of watch flags from NODE_OPTIONS', async () => { + // Avoid recursively watching the repository's source and test trees. + const cwd = tmpdir.resolve('node-options-underscore'); + mkdirSync(path.join(cwd, 'src'), { recursive: true }); + mkdirSync(path.join(cwd, 'test')); + const file = createTmpFile('console.log(process.env.NODE_OPTIONS);'); + const nodeOptions = [ + '--watch_path=./src', + '--watch_path', './test', + '--watch_preserve_output', + '--watch_preserve_output=true', + '--watch_kill_signal=SIGKILL', + '--watch_kill_signal', 'SIGINT', + '--max_old_space_size=4096', + '--no-warnings', + ].join(' '); + const { done, restart } = runInBackground({ + args: [file], + options: { + cwd, + env: { ...process.env, NODE_OPTIONS: nodeOptions }, + }, + }); + + try { + const { stdout, stderr } = await restart(); + + assert.strictEqual(stderr, ''); + const nodeOptionsLine = stdout.find((line) => line.includes('--max_old_space_size')); + assert.ok(nodeOptionsLine); + assert.strictEqual(nodeOptionsLine, '--max_old_space_size=4096 --no-warnings'); + } finally { + await done(); + } + }); + + it('should strip --watch_path from execArgv in child process', async () => { + const dir = tmpdir.resolve('exec-argv-underscore'); + mkdirSync(dir, { recursive: true }); + const file = createTmpFile('console.log(JSON.stringify(process.execArgv));'); + const { done, restart } = runInBackground({ + args: [`--watch_path=${dir}`, file], + }); + + try { + const { stdout, stderr } = await restart(); + + assert.strictEqual(stderr, ''); + assert.ok(stdout.includes('["--no-warnings"]')); + } finally { + await done(); + } + }); + it('should not strip --watch when it appears inside a quoted NODE_OPTIONS value', { // Honoring --require from NODE_OPTIONS is required for this test. skip: !!process.config.variables.node_without_node_options, From 3ef07c59840a8e57e1fd1c72b0a9baed7f80186d Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:24:22 -0700 Subject: [PATCH 2/2] fixup! watch: strip underscore aliases of watch flags --- test/sequential/test-watch-mode.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 5ca9f9e0492a..7a6bb0cc0a9e 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -1050,7 +1050,7 @@ process.on('message', (message) => { '--no-warnings', ].join(' '); const { done, restart } = runInBackground({ - args: [file], + args: ['--watch', file], options: { cwd, env: { ...process.env, NODE_OPTIONS: nodeOptions },