src: add watch config namespace

PR-URL: https://github.com/nodejs/node/pull/60178
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
Marco Ippolito 2025-10-09 17:12:25 +02:00
parent 6a0c9c8e33
commit cec1bd5498
No known key found for this signature in database
GPG Key ID: 27F5E38D5B0A215F
6 changed files with 130 additions and 11 deletions

View File

@ -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
}
}
```

View File

@ -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"

View File

@ -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);
}

View File

@ -250,7 +250,7 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* 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");
}
@ -1021,20 +1021,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",

View File

@ -415,7 +415,8 @@ std::vector<std::string> 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,

View File

@ -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...`,
]);
});
});