diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts index 7d7c490126d04b..a00372733b3575 100644 --- a/ext/node/polyfills/fs.ts +++ b/ext/node/polyfills/fs.ts @@ -3220,6 +3220,94 @@ function watch( return fsWatcher; } +// Mirrors Node's `validateIgnoreOption` / +// `createIgnoreMatcher` from `lib/internal/fs/watchers.js`. +// Accepts a string (minimatch glob), RegExp, function, or array of those. +// Returns a function `(filename) => boolean` (or `null` if `ignore` is nullish). +type IgnoreOption = + | string + | RegExp + | ((filename: string) => boolean) + | (string | RegExp | ((filename: string) => boolean))[] + | undefined + | null; + +let _lazyMinimatch: any = null; +function getMinimatch() { + _lazyMinimatch ??= core.createLazyLoader("ext:deno_node/deps/minimatch.js"); + return _lazyMinimatch(); +} + +function validateIgnoreOptionElement(value: unknown, name: string) { + if (typeof value === "string") { + if (value.length === 0) { + throw new ERR_INVALID_ARG_VALUE( + name, + value, + "must be a non-empty string", + ); + } + return; + } + if (value instanceof RegExp) return; + if (typeof value === "function") return; + throw new ERR_INVALID_ARG_TYPE( + name, + ["string", "RegExp", "Function"], + value, + ); +} + +function validateIgnoreOption(value: unknown, name: string) { + if (value == null) return; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + validateIgnoreOptionElement(value[i], `${name}[${i}]`); + } + return; + } + validateIgnoreOptionElement(value, name); +} + +function createIgnoreMatcher( + ignore: IgnoreOption, +): ((filename: string) => boolean) | null { + if (ignore == null) return null; + const matchers = Array.isArray(ignore) ? ignore : [ignore]; + const compiled: Array<(filename: string) => boolean> = []; + + for (let i = 0; i < matchers.length; i++) { + const matcher = matchers[i]; + if (typeof matcher === "string") { + const { Minimatch } = getMinimatch().default; + const mm = new Minimatch(matcher, { + nocase: isMacOS || isWindows, + windowsPathsNoEscape: true, + nonegate: true, + nocomment: true, + optimizationLevel: 2, + platform: isWindows ? "win32" : "posix", + // Allow patterns without slashes to match the basename + // e.g. '*.log' matches 'subdir/file.log'. + matchBase: true, + }); + compiled.push((filename: string) => mm.match(filename)); + } else if (matcher instanceof RegExp) { + compiled.push((filename: string) => matcher.test(filename)); + } else { + // Function + compiled.push(matcher as (filename: string) => boolean); + } + } + + return (filename: string) => { + for (let i = 0; i < compiled.length; i++) { + if (compiled[i](filename)) return true; + } + return false; + }; +} + function watchPromise( filename: string | Buffer | URL, options?: { @@ -3227,12 +3315,15 @@ function watchPromise( recursive?: boolean; encoding?: string; signal?: AbortSignal; + ignore?: IgnoreOption; }, ): AsyncIterable<{ eventType: string; filename: string | Buffer | null }> { // deno-lint-ignore prefer-primordials const watchPath = getValidatedPath(filename).toString(); const recursive = options?.recursive ?? false; + validateIgnoreOption(options?.ignore, "options.ignore"); + const ignoreMatcher = createIgnoreMatcher(options?.ignore); const watcher = Deno.watchFs(watchPath, { recursive, }); @@ -3255,20 +3346,25 @@ function watchPromise( async next(): Promise< IteratorResult<{ eventType: string; filename: string | Buffer | null }> > { - // deno-lint-ignore prefer-primordials - const iterResult = await fsIterable.next(); - if (iterResult.done) return iterResult; + while (true) { + // deno-lint-ignore prefer-primordials + const iterResult = await fsIterable.next(); + if (iterResult.done) return iterResult; - const eventType = convertDenoFsEventToNodeFsEvent( - iterResult.value.kind, - ); - const fname = recursive - ? relative(resolvedWatchPath, iterResult.value.paths[0]) - : basename(iterResult.value.paths[0]); - return { - value: { eventType, filename: fname }, - done: false, - }; + const eventType = convertDenoFsEventToNodeFsEvent( + iterResult.value.kind, + ); + const fname = recursive + ? relative(resolvedWatchPath, iterResult.value.paths[0]) + : basename(iterResult.value.paths[0]); + if (ignoreMatcher !== null && ignoreMatcher(fname)) { + continue; + } + return { + value: { eventType, filename: fname }, + done: false, + }; + } }, // deno-lint-ignore no-explicit-any return(value?: any): Promise> { diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 93a58f911a2cc9..885858474a4356 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -1089,6 +1089,11 @@ "parallel/test-fs-promises-readfile-empty.js": {}, "parallel/test-fs-promises-readfile-with-fd.js": {}, "parallel/test-fs-promises-statfs-validate-path.js": {}, + "parallel/test-fs-promises-watch-ignore-function.mjs": {}, + "parallel/test-fs-promises-watch-ignore-glob.mjs": {}, + "parallel/test-fs-promises-watch-ignore-invalid.mjs": {}, + "parallel/test-fs-promises-watch-ignore-mixed.mjs": {}, + "parallel/test-fs-promises-watch-ignore-regexp.mjs": {}, "parallel/test-fs-promises-watch-iterator.js": {}, "parallel/test-fs-promises-write-optional-params.js": {}, "parallel/test-fs-promises-writefile-typedarray.js": {},