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 diff --git a/test/sequential/test-watch-mode-watch-flags.mjs b/test/sequential/test-watch-mode-watch-flags.mjs index 8cd08ee08a4c0e..d8996b5de1c3e6 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'], + ]); + }); });