From 55842f0e58cc11bb8ddbd838d06201d77e6dc1f2 Mon Sep 17 00:00:00 2001 From: sangwook Date: Mon, 10 Nov 2025 23:50:45 +0900 Subject: [PATCH 1/4] test: cover execArgv in watch mode --- .../test-watch-mode-watch-flags.mjs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/sequential/test-watch-mode-watch-flags.mjs b/test/sequential/test-watch-mode-watch-flags.mjs index 8cd08ee08a4c0e..3cae7fe931bde0 100644 --- a/test/sequential/test-watch-mode-watch-flags.mjs +++ b/test/sequential/test-watch-mode-watch-flags.mjs @@ -94,4 +94,31 @@ 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, expected) { + const { stdout, stderr } = await runNode({ + args, options: { cwd: projectDir } + }); + + assert.strictEqual(stderr, ''); + + const execArgvLine = stdout[0]; + assert.deepStrictEqual(JSON.parse(execArgvLine), expected); + 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']); + }); }); From 9a6f53867769887357ec3bdc592531c093324bee Mon Sep 17 00:00:00 2001 From: sangwook Date: Tue, 11 Nov 2025 00:07:01 +0900 Subject: [PATCH 2/4] test: relax execArgv watcher assertion --- .../test-watch-mode-watch-flags.mjs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/test/sequential/test-watch-mode-watch-flags.mjs b/test/sequential/test-watch-mode-watch-flags.mjs index 3cae7fe931bde0..947376055645b5 100644 --- a/test/sequential/test-watch-mode-watch-flags.mjs +++ b/test/sequential/test-watch-mode-watch-flags.mjs @@ -105,7 +105,7 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, const watchPath = path.join(projectDir, 'template.html'); writeFileSync(watchPath, ''); - async function assertExecArgv(args, expected) { + async function assertExecArgv(args, expectedPrefixes) { const { stdout, stderr } = await runNode({ args, options: { cwd: projectDir } }); @@ -113,12 +113,27 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, assert.strictEqual(stderr, ''); const execArgvLine = stdout[0]; - assert.deepStrictEqual(JSON.parse(execArgvLine), expected); + const execArgv = JSON.parse(execArgvLine); + assert.ok(Array.isArray(execArgv)); + const matched = expectedPrefixes.some((expectedPrefix) => { + if (execArgv.length < expectedPrefix.length) { + return false; + } + return execArgv.slice(0, expectedPrefix.length) + .every((value, idx) => value === expectedPrefix[idx]); + }); + assert.ok(matched, + `execArgv (${execArgv}) does not start with any expected prefix (${expectedPrefixes.map((p) => `[${p}]`).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']); + 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'], + ]); }); }); From 390c5ca230da55dd581de82a4e6d63f400e6da56 Mon Sep 17 00:00:00 2001 From: sangwook Date: Tue, 11 Nov 2025 00:07:35 +0900 Subject: [PATCH 3/4] watch: re-expose watch flags via execArgv --- lib/internal/main/watch_mode.js | 26 +++++++++++++++++++++----- lib/internal/process/pre_execution.js | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 225436661f5e56..abec0272f4338f 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,18 +45,22 @@ const kCommand = ArrayPrototypeSlice(process.argv, 1); const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' ')); const argsWithoutWatchOptions = []; +const removedWatchFlags = []; const argsFromBinding = getOptionsAsFlagsFromBinding(); for (let i = 0; i < argsFromBinding.length; i++) { const arg = argsFromBinding[i]; if (StringPrototypeStartsWith(arg, '--watch=')) { + ArrayPrototypePush(removedWatchFlags, arg); continue; } if (arg === '--watch') { + ArrayPrototypePush(removedWatchFlags, arg); const nextArg = argsFromBinding[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; @@ -66,7 +71,14 @@ for (let i = 0; i < argsFromBinding.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 = argsFromBinding[i + 1]; + if (nextArg) { + ArrayPrototypePush(removedWatchFlags, nextArg); + i++; + } + } else { + ArrayPrototypePush(removedWatchFlags, arg); } continue; } @@ -95,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 283ec72d388572..6ed36767b97243 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 From 3e2a96fb04ab819f2451d926dd9ab5e1d89cb82b Mon Sep 17 00:00:00 2001 From: sangwook Date: Tue, 11 Nov 2025 05:58:06 +0900 Subject: [PATCH 4/4] test: accept watch flag subsequences in execArgv --- .../test-watch-mode-watch-flags.mjs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/sequential/test-watch-mode-watch-flags.mjs b/test/sequential/test-watch-mode-watch-flags.mjs index 947376055645b5..d8996b5de1c3e6 100644 --- a/test/sequential/test-watch-mode-watch-flags.mjs +++ b/test/sequential/test-watch-mode-watch-flags.mjs @@ -105,7 +105,7 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, const watchPath = path.join(projectDir, 'template.html'); writeFileSync(watchPath, ''); - async function assertExecArgv(args, expectedPrefixes) { + async function assertExecArgv(args, expectedSubsequences) { const { stdout, stderr } = await runNode({ args, options: { cwd: projectDir } }); @@ -115,15 +115,21 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, const execArgvLine = stdout[0]; const execArgv = JSON.parse(execArgvLine); assert.ok(Array.isArray(execArgv)); - const matched = expectedPrefixes.some((expectedPrefix) => { - if (execArgv.length < expectedPrefix.length) { - return false; + 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 execArgv.slice(0, expectedPrefix.length) - .every((value, idx) => value === expectedPrefix[idx]); + return false; }); assert.ok(matched, - `execArgv (${execArgv}) does not start with any expected prefix (${expectedPrefixes.map((p) => `[${p}]`).join(', ')})`); + `execArgv (${execArgv}) does not contain any expected sequence (${expectedSubsequences.map((seq) => `[${seq}]`).join(', ')})`); assert.match(stdout.at(-1), /^Completed running/); }