diff --git a/doc/api/cli.md b/doc/api/cli.md index 7ee7c4860260c0..fa9c46ab198f1a 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -3355,7 +3355,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 @@ -3367,6 +3368,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 ``` diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 1bfa39150e5196..dd57e79aefe8f6 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -798,4 +798,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 225436661f5e56..f5c5331004e652 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 b9e57cb30bedc9..10159c7f8cb064 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -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 })) { @@ -120,14 +131,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; } @@ -161,6 +174,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(); @@ -884,4 +934,76 @@ process.on('message', (message) => { await done(); } }); + + 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 { stderr, stdout } = await runWriteSucceed({ + args, + watchedFile: watchedFiles, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, expectRepeatedCompletes(mainJsFile, 6)); + }); + + it('should not be able to watch glob pattern paths without read access to the directory', async () => { + const { + rootDirGlob, + mainJsFile, + watchedFiles, + } = createGlobFileStructure('globtestdir-2'); + + const args = ['--permission', '--watch-path', rootDirGlob, mainJsFile]; + const { stderr, stdout } = await runWriteSucceed({ + args, + watchedFile: watchedFiles, + }); + + 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, + }); + + assert.match(stderr, /ERR_ACCESS_DENIED/); + assert.deepStrictEqual(stdout, []); + }); + + 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, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, expectRepeatedCompletes(mainJsFile, 6)); + }); + });