Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3233,6 +3233,22 @@ This option is only supported on macOS and Windows.
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
when the option is used on a platform that does not support it.

### `--watch-pattern`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

This is similar to `--watch-path` option, which starts Node.js in
watch mode and is used to specify which paths to watch using globs.
Additionally, this option supports watching files and directories.

```bash
node --watch-pattern=./**/*.js index.js
```

### `--watch-preserve-output`

<!-- YAML
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,9 @@ By default, watch mode will watch the entry point and any required or imported m
.It Fl -watch-path
Starts Node.js in watch mode and specifies what paths to watch. When in watch mode, changes in the watched paths cause the Node.js process to restart.

.It Fl -watch-pattern
Starts Node.js in watch mode and can include glob patterns when specifying what paths to watch. When in watch mode, changes in the watched paths cause the Node.js process to restart.

This will turn off watching of required or imported modules, even when used in combination with --watch.
.
.It Fl -watch-kill-signal
Expand Down
40 changes: 37 additions & 3 deletions lib/internal/main/watch_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
Expand All @@ -27,14 +26,43 @@ const { inspect } = require('util');
const { setTimeout, clearTimeout } = require('timers');
const { resolve } = require('path');
const { once } = require('events');
const { globSync } = require('fs');

prepareMainThreadExecution(false, false);
markBootstrapComplete();

const kWatchPath = getOptionValue('--watch-path');
const kWatchPattern = getOptionValue('--watch-pattern');

function isOptionNotGiven(option) {
return option.length === 0;
}

function isOptionGiven(option) {
return option.length > 0;
}

function handleWatchedPaths() {
const watchedPaths = [];
if (isOptionGiven(kWatchPath)) {
ArrayPrototypeForEach(kWatchPath, (path) => {
ArrayPrototypePush(watchedPaths, resolve(path));
});
}
if (isOptionGiven(kWatchPattern)) {
ArrayPrototypeForEach(kWatchPattern, (path) => {
const matchedPaths = globSync(path);
ArrayPrototypeForEach(matchedPaths, (matchedPathFromGlobPattern) =>
ArrayPrototypePush(watchedPaths, resolve(matchedPathFromGlobPattern)));
});
}
return watchedPaths;
}

const kKillSignal = convertToValidSignal(getOptionValue('--watch-kill-signal'));
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
const kShouldFilterModules = isOptionNotGiven(kWatchPath) && isOptionNotGiven(kWatchPattern);
const kEnvFile = getOptionValue('--env-file') || getOptionValue('--env-file-if-exists');
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
const kWatchedPaths = handleWatchedPaths();
const kPreserveOutput = getOptionValue('--watch-preserve-output');
const kCommand = ArrayPrototypeSlice(process.argv, 1);
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
Expand All @@ -56,6 +84,12 @@ for (let i = 0; i < process.execArgv.length; i++) {
}
continue;
}
if (StringPrototypeStartsWith(arg, '--watch-pattern')) {
if (arg['--watch-pattern'.length] !== '=') {
i++;
}
continue;
}
if (StringPrototypeStartsWith(arg, '--watch-path')) {
const lengthOfWatchPathStr = 12;
if (arg[lengthOfWatchPathStr] !== '=') {
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"path to watch",
&EnvironmentOptions::watch_mode_paths,
kAllowedInEnvvar);
AddOption("--watch-pattern",
"paths to watch (can include a glob pattern)",
&EnvironmentOptions::watch_mode_paths_with_glob_patterns);
AddOption("--watch-kill-signal",
"kill signal to send to the process on watch mode restarts"
"(default: SIGTERM)",
Expand All @@ -1016,6 +1019,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::watch_mode_preserve_output,
kAllowedInEnvvar);
Implies("--watch-path", "--watch");
Implies("--watch-pattern", "--watch");
AddOption("--check",
"syntax check script without executing",
&EnvironmentOptions::syntax_check_only);
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class EnvironmentOptions : public Options {
bool watch_mode_preserve_output = false;
std::string watch_mode_kill_signal = "SIGTERM";
std::vector<std::string> watch_mode_paths;
std::vector<std::string> watch_mode_paths_with_glob_patterns;

bool syntax_check_only = false;
bool has_eval_string = false;
Expand Down
78 changes: 73 additions & 5 deletions test/sequential/test-watch-mode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,21 @@ async function runWriteSucceed({
let stderr = '';
const stdout = [];

let watchedFiles = [];
let currentWatchedFileIndex = 0;
const isWatchingMultpileFiles = Array.isArray(watchedFile) && watchedFile.length > 1;
const isWatchingSingleFile = !isWatchingMultpileFiles;

if (isWatchingMultpileFiles) {
watchedFiles = watchedFile;
restarts = watchedFiles.length + 1;
}

child.stderr.on('data', (data) => {
stderr += data;
});


try {
// Break the chunks into lines
for await (const data of createInterface({ input: child.stdout })) {
Expand All @@ -120,11 +131,22 @@ async function runWriteSucceed({
}
if (data.startsWith(completed)) {
completes++;
if (completes === restarts) {
break;
}
if (completes === 1) {
cancelRestarts = restart(watchedFile);
if (isWatchingSingleFile) {
if (completes === restarts) {
break;
}
if (completes === 1) {
cancelRestarts = restart(watchedFile);
}
} else {
if (completes === restarts) {
break;
}
if (completes < restarts) {
cancelRestarts();
const currentlyWatchedFile = watchedFiles[currentWatchedFileIndex++];
cancelRestarts = restart(currentlyWatchedFile);
}
}
}

Expand Down Expand Up @@ -791,4 +813,50 @@ process.on('message', (message) => {
`Completed running ${inspect(file)}. Waiting for file changes before restarting...`,
]);
});

it('should watch files from a given glob pattern --watch-pattern=./**/*.js', async () => {

const tmpDirForGlobTest = tmpdir.resolve('glob-test-dir');
mkdirSync(tmpDirForGlobTest);

const globPattern = path.resolve(tmpDirForGlobTest, '**/*.js');

const directory1 = path.join(tmpDirForGlobTest, 'directory1');
const directory2 = path.join(tmpDirForGlobTest, 'directory2');

mkdirSync(directory1);
mkdirSync(directory2);

const tmpJsFile1 = createTmpFile('', '.js', directory1);
const tmpJsFile2 = createTmpFile('', '.js', directory1);
const tmpJsFile3 = createTmpFile('', '.js', directory2);
const tmpJsFile4 = createTmpFile('', '.js', directory2);
const tmpJsFile5 = createTmpFile('', '.js', directory2);

const mainJsFile = createTmpFile('console.log(\'running\')', '.js', tmpDirForGlobTest);

const args = ['--watch-pattern', globPattern, mainJsFile];
const watchedFiles = [tmpJsFile1, tmpJsFile2, tmpJsFile3, tmpJsFile4, tmpJsFile5];

const { stderr, stdout } = await runWriteSucceed({
args,
watchedFile: watchedFiles,
});

function expectRepeatedCompletes(n) {
const expectedStdout = [];
for (let i = 0; i < n; i++) {
if (i !== 0) {
expectedStdout.push(`Restarting ${inspect((mainJsFile))}`);
}
expectedStdout.push('running');
expectedStdout.push(`Completed running ${inspect(mainJsFile)}. Waiting for file changes before restarting...`);
}
return expectedStdout;
}

assert.strictEqual(stderr, '');
assert.deepStrictEqual(stdout, expectRepeatedCompletes(6));

});
});
Loading