From 9b35739d2064a0adec65201f9763fd2fd6e6258c Mon Sep 17 00:00:00 2001 From: louiellan Date: Sun, 4 Jan 2026 20:39:03 +0800 Subject: [PATCH 1/3] lib: support glob on `--watch-path` --- lib/internal/fs/glob.js | 1 + lib/internal/main/watch_mode.js | 18 +++++++- test/sequential/test-watch-mode.mjs | 67 +++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 526efd4c010d7c..ef017a7504192e 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -800,4 +800,5 @@ module.exports = { __proto__: null, Glob, matchGlobPattern, + createMatcher, }; diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 06c2c8602da444..c44e5a5273338c 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -1,5 +1,6 @@ 'use strict'; const { + ArrayPrototypeFlatMap, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeJoin, @@ -28,17 +29,32 @@ const { inspect } = require('util'); const { setTimeout, clearTimeout } = require('timers'); const { resolve } = require('path'); const { once } = require('events'); +const { createMatcher } = require('internal/fs/glob'); +const { globSync } = require('fs'); prepareMainThreadExecution(false, false); markBootstrapComplete(); +function hasGlobPattern(path) { + return createMatcher(path).hasMagic(); +} + +function handleWatchedPath(path) { + if (hasGlobPattern(path)) { + const matchedFilesFromGlob = globSync(path); + const resolvedMatchedFiles = ArrayPrototypeMap(matchedFilesFromGlob, (path) => resolve(path)); + return resolvedMatchedFiles; + } + return resolve(path); +} + const kKillSignal = convertToValidSignal(getOptionValue('--watch-kill-signal')); const kShouldFilterModules = getOptionValue('--watch-path').length === 0; const kEnvFiles = [ ...getOptionValue('--env-file'), ...getOptionValue('--env-file-if-exists'), ]; -const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)); +const kWatchedPaths = ArrayPrototypeFlatMap(getOptionValue('--watch-path'), (path) => handleWatchedPath(path)); const kPreserveOutput = getOptionValue('--watch-preserve-output'); const kCommand = ArrayPrototypeSlice(process.argv, 1); const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' ')); diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index a5cac129ad1c21..25f44d5c42f04a 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -117,10 +117,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 })) { @@ -129,14 +140,16 @@ async function runWriteSucceed({ } if (data.startsWith(completed)) { completes++; - if (completes === restarts) { + if (completes === restarts) break; - } - if (completes === 1) { + if (isWatchingSingleFile && completes === 1) cancelRestarts = restart(watchedFile); + if (isWatchingMultpileFiles && completes < restarts) { + cancelRestarts(); + const currentlyWatchedFile = watchedFiles[currentWatchedFileIndex++]; + cancelRestarts = restart(currentlyWatchedFile); } } - if (!shouldFail && data.startsWith('Failed running')) { break; } @@ -922,4 +935,50 @@ process.on('message', (message) => { await done(); } }); + + it('should watch files from a given glob pattern --watch-path=./**/*.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-path', 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)); + + }); }); From ae95779c672ae28de2c5c1c2973f3cf145e624c0 Mon Sep 17 00:00:00 2001 From: louiellan Date: Thu, 2 Apr 2026 16:15:30 +0800 Subject: [PATCH 2/3] doc: clarify glob support on `--watch-path` --- doc/api/cli.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 18576367d09720..f05579239b21a3 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3417,6 +3417,10 @@ added: - v18.11.0 - v16.19.0 changes: + - version: + - REPLACEME + pr-url: https://github.com/nodejs/node/pull/59478 + description: support glob patterns in paths - version: - v22.0.0 - v20.13.0 @@ -3424,7 +3428,8 @@ changes: description: Watch mode is now stable. --> -Starts Node.js in watch mode and specifies what paths to watch. +Starts Node.js in watch mode and specifies what paths to watch (the paths could +include glob patterns,e.g., `--watch-path='**/*.js'`). 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 @@ -3436,6 +3441,9 @@ This flag cannot be combined with Note: Using `--watch-path` implicitly enables `--watch`, which requires a file path and is incompatible with `--run`, as `--run` takes precedence and ignores watch mode. +When using `--watch-path` with glob patterns, you must include quotations `''` to +ensure it does not get expanded by the shell interpreter + ```bash node --watch-path=./src --watch-path=./tests index.js ``` From 77302eb4797d66fd65be1f58d11bbf6722cde3a3 Mon Sep 17 00:00:00 2001 From: louiellan Date: Thu, 2 Apr 2026 16:16:35 +0800 Subject: [PATCH 3/3] test: using `--watch-path` glob argument with `--permission` --- test/sequential/test-watch-mode.mjs | 121 +++++++++++++++++++++------- 1 file changed, 92 insertions(+), 29 deletions(-) diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 25f44d5c42f04a..bfc6efb59568c7 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -183,6 +183,43 @@ async function failWriteSucceed({ file, watchedFile }) { tmpdir.refresh(); +function createGlobFileStructure(nameOfTheDir) { + const rootDir = tmpdir.resolve(nameOfTheDir); + mkdirSync(rootDir); + + const rootDirGlob = path.resolve(rootDir, '**/*.js'); + const directory1 = path.join(rootDir, 'directory1'); + const directory2 = path.join(rootDir, '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', rootDir); + const watchedFiles = [tmpJsFile1, tmpJsFile2, tmpJsFile3, tmpJsFile4, tmpJsFile5]; + + + return { rootDir, rootDirGlob, mainJsFile, watchedFiles }; +} + +function expectRepeatedCompletes(mainJsFile, 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; +} + + describe('watch mode', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_000 }, () => { it('should watch changes to a file', async () => { const file = createTmpFile(); @@ -937,48 +974,74 @@ process.on('message', (message) => { }); it('should watch files from a given glob pattern --watch-path=./**/*.js', async () => { + const { + rootDirGlob, + mainJsFile, + watchedFiles, + } = createGlobFileStructure('globtestdir-1'); + + const args = ['--watch-path', rootDirGlob, mainJsFile]; - const tmpDirForGlobTest = tmpdir.resolve('glob-test-dir'); - mkdirSync(tmpDirForGlobTest); + const { stderr, stdout } = await runWriteSucceed({ + args, + watchedFile: watchedFiles, + }); - const globPattern = path.resolve(tmpDirForGlobTest, '**/*.js'); + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, expectRepeatedCompletes(mainJsFile, 6)); + }); - const directory1 = path.join(tmpDirForGlobTest, 'directory1'); - const directory2 = path.join(tmpDirForGlobTest, 'directory2'); + it('should not be able to watch glob pattern paths without read access to the directory', async () => { + const { + rootDirGlob, + mainJsFile, + watchedFiles, + } = createGlobFileStructure('globtestdir-2'); - mkdirSync(directory1); - mkdirSync(directory2); + const args = ['--permission', '--watch-path', rootDirGlob, mainJsFile]; + const { stderr, stdout } = await runWriteSucceed({ + args, + watchedFile: watchedFiles, + }); - 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); + assert.match(stderr, /ERR_ACCESS_DENIED/); + assert.deepStrictEqual(stdout, []); + }); + it('should not be able to watch glob pattern paths with partial read access', async () => { + const { + rootDir, + rootDirGlob, + mainJsFile, + watchedFiles, + } = createGlobFileStructure('globtestdir-3'); + + const allowedSubDirectory = path.join(rootDir, 'directory1'); + const args = ['--permission', '--allow-fs-read', allowedSubDirectory, '--watch-path', rootDirGlob, mainJsFile]; + const { stderr, stdout } = await runWriteSucceed({ + args, + watchedFile: watchedFiles, + }); - const mainJsFile = createTmpFile('console.log(\'running\')', '.js', tmpDirForGlobTest); + assert.match(stderr, /ERR_ACCESS_DENIED/); + assert.deepStrictEqual(stdout, []); + }); - const args = ['--watch-path', globPattern, mainJsFile]; - const watchedFiles = [tmpJsFile1, tmpJsFile2, tmpJsFile3, tmpJsFile4, tmpJsFile5]; + it('should be able to watch glob pattern paths with full read access to the directory', async () => { + const { + rootDir, + rootDirGlob, + mainJsFile, + watchedFiles, + } = createGlobFileStructure('globtestdir-4'); + const args = ['--permission', '--allow-fs-read', rootDir, '--watch-path', rootDirGlob, mainJsFile]; 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)); - + assert.deepStrictEqual(stdout, expectRepeatedCompletes(mainJsFile, 6)); }); + });