Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .changeset/mixed-ignored.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '/'.
Expand Down
31 changes: 21 additions & 10 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
55 changes: 55 additions & 0 deletions test/Watchpack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions test/WatchpackUnit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type FileWatcherEvents = import("./DirectoryWatcher").FileWatcherEvents;
type EventMap = Record<string, (...args: any[]) => any>;
type Watcher<T extends EventMap> = import("./DirectoryWatcher").Watcher<T>;
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
Expand Down