From 596dc8ceda6ce13876b236e5e855e5bc79cac483 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 9 Sep 2026 14:24:11 +0800 Subject: [PATCH] feat: support mixed glob and RegExp arrays in the `ignored` option `ignored` now accepts `(string | RegExp)[]` so glob patterns and regular expressions can be combined in a single array. --- .changeset/mixed-ignored.md | 15 ++++++++++ README.md | 1 + lib/index.js | 31 ++++++++++++++------- test/Watchpack.test.js | 55 +++++++++++++++++++++++++++++++++++++ test/WatchpackUnit.test.js | 24 ++++++++++++++++ types/index.d.ts | 2 +- 6 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .changeset/mixed-ignored.md diff --git a/.changeset/mixed-ignored.md b/.changeset/mixed-ignored.md new file mode 100644 index 0000000..1a9c0fb --- /dev/null +++ b/.changeset/mixed-ignored.md @@ -0,0 +1,15 @@ +--- +"watchpack": minor +--- + +feat: allow the `ignored` option to be a mixed array of glob strings and +`RegExp` instances, e.g. `ignored: ["**/.cache", /generated/]`. + +Previously an array was assumed to contain only glob strings, so passing a +`RegExp` inside it threw `TypeError: Expected a string`. This made it +awkward for frameworks to append their own glob ignores to a user-supplied +`RegExp` ignore, since the two could not be combined in one value. + +Glob strings in the array are still merged into a single `RegExp`; `RegExp` +items are tested as-is. Behaviour for string-only arrays, single strings, +single `RegExp`s and functions is unchanged. diff --git a/README.md b/README.md index da55a3e..c7bf26b 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ const wp = new Watchpack({ ignored: "**/.git", // ignored: "string" - a glob pattern for files or folders that should not be watched // ignored: ["string", "string"] - multiple glob patterns that should be ignored + // ignored: ["string", /regexp/] - a mix of glob patterns and regular expressions that should be ignored // ignored: /regexp/ - a regular expression for files or folders that should not be watched // ignored: (entry) => boolean - an arbitrary function which must return truthy to ignore an entry // For all cases expect the arbitrary function the path will have path separator normalized to '/'. diff --git a/lib/index.js b/lib/index.js index d201c8c..3ba8652 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,7 +24,7 @@ const watchEventSource = require("./watchEventSource"); */ /** @typedef {(item: string) => boolean} IgnoredFunction */ -/** @typedef {string[] | RegExp | string | IgnoredFunction} Ignored */ +/** @typedef {(string | RegExp)[] | RegExp | string | IgnoredFunction} Ignored */ /** * @typedef {object} WatcherOptions @@ -95,17 +95,28 @@ const normalizeSeparators = (item) => */ const ignoredToFunction = (ignored) => { if (Array.isArray(ignored)) { - const stringRegexps = - /** @type {string[]} */ - (ignored.map((i) => stringToRegexp(i)).filter(Boolean)); - if (stringRegexps.length === 0) { + /** @type {string[]} */ + const stringRegexps = []; + /** @type {RegExp[]} */ + const regexps = []; + for (const item of ignored) { + if (item instanceof RegExp) { + regexps.push(item); + } else { + const stringRegexp = stringToRegexp(item); + if (stringRegexp) stringRegexps.push(stringRegexp); + } + } + if (stringRegexps.length === 0 && regexps.length === 0) { return () => false; } - const regexp = - stringRegexps.length === 1 - ? new RegExp(stringRegexps[0]) - : new RegExp(stringRegexps.join("|")); - return (item) => regexp.test(normalizeSeparators(item)); + if (stringRegexps.length > 0) { + regexps.push(new RegExp(stringRegexps.join("|"))); + } + return (item) => { + const normalized = normalizeSeparators(item); + return regexps.some((regexp) => regexp.test(normalized)); + }; } else if (typeof ignored === "string") { const stringRegexp = stringToRegexp(ignored); if (!stringRegexp) { diff --git a/test/Watchpack.test.js b/test/Watchpack.test.js index 37a0ce0..3f71090 100644 --- a/test/Watchpack.test.js +++ b/test/Watchpack.test.js @@ -281,6 +281,61 @@ describe("Watchpack", () => { }); }); + it("should not watch ignored files in a directory (mixed glob and regexp)", (done) => { + const w = new WatchpackTest({ + aggregateTimeout: 300, + ignored: ["**/.cache", /generated$/], + }); + let changeEvents = 0; + let aggregatedEvents = 0; + w.on("change", () => { + changeEvents++; + }); + w.on("aggregated", () => { + aggregatedEvents++; + }); + testHelper.dir("dir"); + testHelper.dir(path.join("dir", ".cache")); + testHelper.tick(200, () => { + w.watch([], [path.join(fixtures, "dir")]); + testHelper.tick(200, () => { + testHelper.file(path.join("dir", ".cache", "a")); + testHelper.file(path.join("dir", "generated")); + testHelper.tick(1000, () => { + expect(changeEvents).toBe(0); + expect(aggregatedEvents).toBe(0); + w.close(); + done(); + }); + }); + }); + }); + + it("should still watch non-ignored files in a directory (mixed glob and regexp)", (done) => { + const w = new WatchpackTest({ + aggregateTimeout: 300, + ignored: ["**/.cache", /generated$/], + }); + let changeEvents = 0; + w.on("change", (file) => { + expect(file).toBe(path.join(fixtures, "dir", "watched")); + changeEvents++; + }); + w.on("aggregated", (changes) => { + expect([...changes]).toEqual([path.join(fixtures, "dir")]); + expect(changeEvents).toBeGreaterThan(0); + w.close(); + done(); + }); + testHelper.dir("dir"); + testHelper.tick(200, () => { + w.watch([], [path.join(fixtures, "dir")]); + testHelper.tick(200, () => { + testHelper.file(path.join("dir", "watched")); + }); + }); + }); + it("should watch a file when ignore is empty array", (done) => { const w = new WatchpackTest({ aggregateTimeout: 1000, diff --git a/test/WatchpackUnit.test.js b/test/WatchpackUnit.test.js index f438b1c..7ad6129 100644 --- a/test/WatchpackUnit.test.js +++ b/test/WatchpackUnit.test.js @@ -98,6 +98,30 @@ describe("Watchpack unit", () => { w.close(); }); + it("should accept a mixed array of globs and RegExps", () => { + const w = new Watchpack({ ignored: ["**/.cache", /generated/] }); + expect(w.watcherOptions.ignored("/project/.cache")).toBe(true); + expect(w.watcherOptions.ignored("/project/.cache/file.js")).toBe(true); + expect(w.watcherOptions.ignored("/project/generated.js")).toBe(true); + expect(w.watcherOptions.ignored("/project/src/index.js")).toBe(false); + w.close(); + }); + + it("should accept an array containing only RegExps", () => { + const w = new Watchpack({ ignored: [/foo/, /bar/] }); + expect(w.watcherOptions.ignored("/x/foo")).toBe(true); + expect(w.watcherOptions.ignored("/x/bar")).toBe(true); + expect(w.watcherOptions.ignored("/x/baz")).toBe(false); + w.close(); + }); + + it("should normalize backslashes before testing RegExps in a mixed array", () => { + const w = new Watchpack({ ignored: ["**/foo", /\/bar$/] }); + expect(w.watcherOptions.ignored("C:\\x\\foo")).toBe(true); + expect(w.watcherOptions.ignored("C:\\x\\bar")).toBe(true); + w.close(); + }); + it("should allow calling pause with no aggregate timer", () => { const w = new Watchpack(); // Just calling pause without any events should be a no-op diff --git a/types/index.d.ts b/types/index.d.ts index 3425674..715a502 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -35,7 +35,7 @@ type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents; type EventMap = Record any>; type Watcher = import("./DirectoryWatcher").Watcher; type IgnoredFunction = (item: string) => boolean; -type Ignored = string[] | RegExp | string | IgnoredFunction; +type Ignored = (string | RegExp)[] | RegExp | string | IgnoredFunction; type WatcherOptions = { /** * true when need to resolve symlinks and watch symlink and real file, otherwise false