From b335bff4f8f33778b34802f49d2ccda9f001448e Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Thu, 9 Oct 2025 17:12:25 +0200 Subject: [PATCH] src: add watch config namespace --- doc/api/cli.md | 3 ++ doc/node-config-schema.json | 29 ++++++++++++ lib/internal/main/watch_mode.js | 21 ++++++--- src/node_options.cc | 16 ++++--- src/node_options.h | 3 +- test/sequential/test-watch-mode.mjs | 69 +++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 23266e6953832c..460b02b893166b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1022,6 +1022,9 @@ in the `$schema` must be replaced with the version of Node.js you are using. }, "testRunner": { "test-isolation": "process" + }, + "watch": { + "watch-preserve-output": true } } ``` diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 99c031199fbc1e..3f7d6a93de270d 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -734,6 +734,35 @@ "type": "boolean" } } + }, + "watch": { + "type": "object", + "additionalProperties": false, + "properties": { + "watch": { + "type": "boolean" + }, + "watch-kill-signal": { + "type": "string" + }, + "watch-path": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string", + "minItems": 1 + }, + "type": "array" + } + ] + }, + "watch-preserve-output": { + "type": "boolean" + } + } } }, "type": "object" diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 37d126f1257b3e..bf70f7606f5e03 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -1,6 +1,7 @@ 'use strict'; const { ArrayPrototypeForEach, + ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePush, @@ -17,7 +18,7 @@ const { triggerUncaughtException, exitCodes: { kNoFailure }, } = internalBinding('errors'); -const { getOptionValue } = require('internal/options'); +const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options'); const { FilesWatcher } = require('internal/watch_mode/files_watcher'); const { green, blue, red, white, clear } = require('internal/util/colors'); const { convertToValidSignal } = require('internal/util'); @@ -40,14 +41,14 @@ 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 argsFromBinding = getOptionsAsFlagsFromBinding(); +for (let i = 0; i < argsFromBinding.length; i++) { + const arg = argsFromBinding[i]; if (StringPrototypeStartsWith(arg, '--watch=')) { continue; } if (arg === '--watch') { - const nextArg = process.execArgv[i + 1]; + 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 @@ -66,6 +67,16 @@ for (let i = 0; i < process.execArgv.length; i++) { } continue; } + if (StringPrototypeStartsWith(arg, '--experimental-config-file')) { + if (!ArrayPrototypeIncludes(arg, '=')) { + // Skip the flag and the next argument (the config file path) + i++; + } + continue; + } + if (arg === '--experimental-default-config-file') { + continue; + } ArrayPrototypePush(argsWithoutWatchOptions, arg); } diff --git a/src/node_options.cc b/src/node_options.cc index 2d9ccba7fe4feb..c3be290e17bc68 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -242,7 +242,7 @@ void EnvironmentOptions::CheckOptions(std::vector* errors, } else if (test_runner_force_exit) { errors->push_back("either --watch or --test-force-exit " "can be used, not both"); - } else if (!test_runner && (argv->size() < 1 || (*argv)[1].empty())) { + } else if (!test_runner && watch_mode_paths.empty() && argv->size() < 1) { errors->push_back("--watch requires specifying a file"); } @@ -1013,20 +1013,26 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--watch", "run in watch mode", &EnvironmentOptions::watch_mode, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kWatchNamespace); AddOption("--watch-path", "path to watch", &EnvironmentOptions::watch_mode_paths, - kAllowedInEnvvar); + kAllowedInEnvvar, + OptionNamespaces::kWatchNamespace); AddOption("--watch-kill-signal", "kill signal to send to the process on watch mode restarts" "(default: SIGTERM)", &EnvironmentOptions::watch_mode_kill_signal, - kAllowedInEnvvar); + kAllowedInEnvvar, + OptionNamespaces::kWatchNamespace); AddOption("--watch-preserve-output", "preserve outputs on watch mode restart", &EnvironmentOptions::watch_mode_preserve_output, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kWatchNamespace); Implies("--watch-path", "--watch"); AddOption("--check", "syntax check script without executing", diff --git a/src/node_options.h b/src/node_options.h index e1b633a067fb09..c9c41ae81b1897 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -415,7 +415,8 @@ std::vector MapAvailableNamespaces(); // Define all namespace entries #define OPTION_NAMESPACE_LIST(V) \ V(kNoNamespace, "") \ - V(kTestRunnerNamespace, "testRunner") + V(kTestRunnerNamespace, "testRunner") \ + V(kWatchNamespace, "watch") enum class OptionNamespaces { #define V(name, _) name, diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index f0a7d62e560821..b0318f3505c66a 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -791,4 +791,73 @@ process.on('message', (message) => { `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, ]); }); + + it('should watch changes to a file from config file', async () => { + const file = createTmpFile(); + const configFile = createTmpFile(JSON.stringify({ watch: { 'watch': true } }), '.json'); + const { stderr, stdout } = await runWriteSucceed({ + file, watchedFile: file, args: ['--experimental-config-file', configFile, file], options: { + timeout: 10000 + } + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to a file with watch-path from config file', { + skip: !supportsRecursive, + }, async () => { + const dir = tmpdir.resolve('subdir4'); + mkdirSync(dir); + const file = createTmpFile(); + const watchedFile = createTmpFile('', '.js', dir); + const configFile = createTmpFile(JSON.stringify({ watch: { 'watch-path': [dir] } }), '.json', dir); + + const args = ['--experimental-config-file', configFile, file]; + const { stderr, stdout } = await runWriteSucceed({ file, watchedFile, args }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + assert.strictEqual(stderr, ''); + }); + + it('should watch changes to a file from default config file', async () => { + const dir = tmpdir.resolve('subdir5'); + mkdirSync(dir); + + const file = createTmpFile('console.log("running");', '.js', dir); + writeFileSync(path.join(dir, 'node.config.json'), JSON.stringify({ watch: { 'watch': true } })); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: file, + args: ['--experimental-default-config-file', file], + options: { + timeout: 10000, + cwd: dir + } + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); });