Commit 57bb81779be for nodejs

commit 57bb81779be411b906feb7ce6481d42679f0d01b
Author: Trivikram Kamat <trivikr.dev@gmail.com>
Date:   Tue Oct 6 08:59:00 2026 -0700

    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
    PR-URL: https://github.com/nodejs/node/pull/66365
    Fixes: https://github.com/nodejs/node/issues/66364
    Reviewed-By: Moshe Atlow <moshe@atlow.co.il>

diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js
index 8d6b2b23dd1..1cb73be912e 100644
--- a/lib/internal/main/watch_mode.js
+++ b/lib/internal/main/watch_mode.js
@@ -7,7 +7,9 @@ const {
   ArrayPrototypePushApply,
   ArrayPrototypeSlice,
   StringPrototypeIncludes,
+  StringPrototypeIndexOf,
   StringPrototypeReplaceAll,
+  StringPrototypeSlice,
   StringPrototypeStartsWith,
 } = primordials;

@@ -42,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;
   }
@@ -77,7 +93,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);
@@ -91,15 +107,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 1fe4f0597cb..6b7d9f02194 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: ['--watch', 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,