From 2447461e1471e78f5e84ce0c550ee476770e6e4d Mon Sep 17 00:00:00 2001 From: sangwook Date: Wed, 18 Feb 2026 06:51:18 +0900 Subject: [PATCH] watch: expose watch flags in child execArgv Previously, watch-related flags were consumed by the watch mode manager and stripped from the arguments passed to the child process. This commit ensures these flags are preserved in 'process.execArgv' of the watched process by passing them via an internal environment variable. Fixes regression where flag order was not preserved. --- lib/internal/main/watch_mode.js | 33 +++++++++---- lib/internal/process/pre_execution.js | 16 +++++++ .../test-watch-mode-watch-flags.mjs | 48 +++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 06c2c8602da444..48f78af26e6ff7 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -7,6 +7,7 @@ const { ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeSlice, + JSONStringify, StringPrototypeStartsWith, } = primordials; @@ -44,17 +45,22 @@ const kCommand = ArrayPrototypeSlice(process.argv, 1); const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' ')); const argsWithoutWatchOptions = []; -for (let i = 0; i < process.execArgv.length; i++) { - const arg = process.execArgv[i]; +const removedWatchFlags = []; +const args = process.execArgv; +for (let i = 0; i < args.length; i++) { + const arg = args[i]; if (StringPrototypeStartsWith(arg, '--watch=')) { + ArrayPrototypePush(removedWatchFlags, arg); continue; } if (arg === '--watch') { - const nextArg = process.execArgv[i + 1]; + ArrayPrototypePush(removedWatchFlags, arg); + const nextArg = args[i + 1]; if (nextArg && nextArg[0] !== '-') { // If `--watch` doesn't include `=` and the next // argument is not a flag then it is interpreted as // the watch argument, so we need to skip that as well + ArrayPrototypePush(removedWatchFlags, nextArg); i++; } continue; @@ -65,7 +71,14 @@ for (let i = 0; i < process.execArgv.length; i++) { // if --watch-path doesn't include `=` it means // that the next arg is the target path, so we // need to skip that as well - i++; + ArrayPrototypePush(removedWatchFlags, arg); + const nextArg = args[i + 1]; + if (nextArg) { + ArrayPrototypePush(removedWatchFlags, nextArg); + i++; + } + } else { + ArrayPrototypePush(removedWatchFlags, arg); } continue; } @@ -94,12 +107,16 @@ let exited; function start() { exited = false; const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : 'inherit'; + const env = { + ...process.env, + WATCH_REPORT_DEPENDENCIES: '1', + }; + if (removedWatchFlags.length > 0) { + env.NODE_WATCH_ARGS = JSONStringify(removedWatchFlags); + } child = spawn(process.execPath, argsWithoutWatchOptions, { stdio, - env: { - ...process.env, - WATCH_REPORT_DEPENDENCIES: '1', - }, + env, }); watcher.watchChildProcessModules(child); if (kEnvFiles.length > 0) { diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 0902536708bf1d..ba8bdbcc0416b0 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -1,7 +1,9 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeForEach, + ArrayPrototypeUnshiftApply, Date, DatePrototypeGetDate, DatePrototypeGetFullYear, @@ -9,6 +11,7 @@ const { DatePrototypeGetMinutes, DatePrototypeGetMonth, DatePrototypeGetSeconds, + JSONParse, NumberParseInt, ObjectDefineProperty, ObjectFreeze, @@ -270,6 +273,19 @@ function patchProcessObject(expandArgv1) { process._exiting = false; process.argv[0] = process.execPath; + const watchArgsFromLauncher = process.env.NODE_WATCH_ARGS; + if (watchArgsFromLauncher !== undefined) { + delete process.env.NODE_WATCH_ARGS; + try { + const parsed = JSONParse(watchArgsFromLauncher); + if (ArrayIsArray(parsed) && parsed.length > 0) { + ArrayPrototypeUnshiftApply(process.execArgv, parsed); + } + } catch { + // Ignore malformed payloads. + } + } + /** @type {string} */ let mainEntry; // If requested, update process.argv[1] to replace whatever the user provided with the resolved absolute file path of diff --git a/test/sequential/test-watch-mode-watch-flags.mjs b/test/sequential/test-watch-mode-watch-flags.mjs index 385f381ad6a0ed..2e8b7a7880ac2d 100644 --- a/test/sequential/test-watch-mode-watch-flags.mjs +++ b/test/sequential/test-watch-mode-watch-flags.mjs @@ -94,4 +94,52 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, ]); }); + + it('exposes watch flags through process.execArgv inside the watched script', async () => { + const projectDir = tmpdir.resolve('project-watch-exec-argv'); + mkdirSync(projectDir); + + const file = createTmpFile(` + console.log(JSON.stringify(process.execArgv)); + `, '.js', projectDir); + const watchPath = path.join(projectDir, 'template.html'); + writeFileSync(watchPath, ''); + + async function assertExecArgv(args, expectedSubsequences) { + const { stdout, stderr } = await runNode({ + args, options: { cwd: projectDir }, + }); + + assert.strictEqual(stderr, ''); + + const execArgvLine = stdout[0]; + const execArgv = JSON.parse(execArgvLine); + assert.ok(Array.isArray(execArgv)); + const matched = expectedSubsequences.some((expectedSeq) => { + for (let i = 0; i <= execArgv.length - expectedSeq.length; i++) { + let ok = true; + for (let j = 0; j < expectedSeq.length; j++) { + if (execArgv[i + j] !== expectedSeq[j]) { + ok = false; + break; + } + } + if (ok) return true; + } + return false; + }); + assert.ok(matched, + `execArgv (${execArgv}) does not contain any expected sequence (${expectedSubsequences.map((seq) => `[${seq}]`).join(', ')})`); + assert.match(stdout.at(-1), /^Completed running/); + } + + await assertExecArgv(['--watch', file], [['--watch']]); + await assertExecArgv(['--watch-path=template.html', file], [['--watch-path=template.html']]); + await assertExecArgv( + ['--watch-path', 'template.html', file], + [ + ['--watch-path', 'template.html'], + ['--watch-path=template.html'], + ]); + }); });