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..7a6bb0cc0a9e 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,