Skip to content

Commit b01a2c3

Browse files
committed
chore(tests): unit tests for glob matching
1 parent 7b113cf commit b01a2c3

File tree

4 files changed

+385
-47
lines changed

4 files changed

+385
-47
lines changed

lib/Server.js

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ const fs = require("graceful-fs");
88
const ipaddr = require("ipaddr.js");
99
const { validate } = require("schema-utils");
1010
const schema = require("./options.json");
11+
const {
12+
getGlobbedWatcherPaths,
13+
getIgnoreMatchers,
14+
} = require("./getGlobMatchers");
1115

1216
/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
1317
/** @typedef {import("webpack").Compiler} Compiler */
@@ -3258,55 +3262,14 @@ class Server {
32583262
watchFiles(watchPath, watchOptions = {}) {
32593263
const chokidar = require("chokidar");
32603264

3261-
const watchPathArr = Array.isArray(watchPath) ? watchPath : [watchPath];
3262-
const ignoredArr = Array.isArray(watchOptions.ignored)
3263-
? watchOptions.ignored
3264-
: [];
3265-
3266-
if (watchOptions.disableGlobbing !== true) {
3267-
const picomatch = require("picomatch");
3268-
const isGlob = require("is-glob");
3269-
const watchPathGlobs = watchPathArr.filter((p) => isGlob(p));
3270-
3271-
// No need to do all this work when no globs are used in watcher
3272-
if (watchPathGlobs.length > 0) {
3273-
const globParent = require("glob-parent");
3274-
3275-
watchPathGlobs.forEach((p) => {
3276-
watchPathArr[watchPathArr.indexOf(p)] = globParent(p);
3277-
});
3278-
3279-
const matcher = picomatch(watchPathGlobs, {
3280-
cwd: watchOptions.cwd,
3281-
dot: true,
3282-
});
3283-
3284-
// Ignore all paths that don't match any of the globs
3285-
ignoredArr.push((p) => !watchPathArr.includes(p) && !matcher(p));
3286-
}
3287-
3288-
// Double filter to satisfy typescript. Otherwise nasty casting is required
3289-
const ignoredGlobs = ignoredArr
3290-
.filter((s) => typeof s === "string")
3291-
.filter((s) => isGlob(s));
3292-
3293-
// No need to do all this work when no globs are used in ignored
3294-
if (ignoredGlobs.length > 0) {
3295-
const matcher = picomatch(ignoredGlobs, {
3296-
cwd: watchOptions.cwd,
3297-
dot: true,
3298-
});
3299-
3300-
ignoredArr.push(matcher);
3301-
}
3265+
const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths(
3266+
watchPath,
3267+
watchOptions,
3268+
);
33023269

3303-
// Filter out all the glob strings
3304-
watchOptions.ignored = ignoredArr.filter(
3305-
(s) => typeof s === "string" && isGlob(s),
3306-
);
3307-
}
3270+
watchOptions.ignored = getIgnoreMatchers(watchOptions, ignoreFunction);
33083271

3309-
const watcher = chokidar.watch(watchPathArr, watchOptions);
3272+
const watcher = chokidar.watch(watchPaths, watchOptions);
33103273

33113274
// disabling refreshing on changing the content
33123275
if (this.options.liveReload) {

lib/getGlobMatchers.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use strict";
2+
3+
module.exports = {
4+
/**
5+
* @param {string[] | string} _watchPaths
6+
* @param {import("./Server").WatchOptions} watchOptions
7+
* @returns {[string[], import("chokidar").MatchFunction | null]}*/
8+
getGlobbedWatcherPaths(_watchPaths, { disableGlobbing, cwd }) {
9+
const watchPaths = Array.isArray(_watchPaths) ? _watchPaths : [_watchPaths];
10+
11+
if (disableGlobbing === true) {
12+
return [watchPaths, null];
13+
}
14+
15+
const picomatch = require("picomatch");
16+
const isGlob = require("is-glob");
17+
const watchPathGlobs = watchPaths.filter((p) => isGlob(p));
18+
19+
if (watchPathGlobs.length === 0) {
20+
return [watchPaths, null];
21+
}
22+
23+
const globParent = require("glob-parent");
24+
25+
watchPathGlobs.forEach((p) => {
26+
watchPaths[watchPaths.indexOf(p)] = globParent(p);
27+
});
28+
29+
const matcher = picomatch(watchPathGlobs, { cwd, dot: true });
30+
31+
/** @type {import("chokidar").MatchFunction} */
32+
const ignoreFunction = (p) => !watchPaths.includes(p) && !matcher(p);
33+
34+
// Ignore all paths that don't match any of the globs
35+
return [watchPaths, ignoreFunction];
36+
},
37+
38+
/**
39+
*
40+
* @param {import("./Server").WatchOptions} watchOptions
41+
* @param {import("chokidar").MatchFunction | null } ignoreFunction
42+
* @returns {import("chokidar").Matcher[]}
43+
*/
44+
getIgnoreMatchers({ disableGlobbing, ignored, cwd }, ignoreFunction) {
45+
const _ignored = /** @type {import("chokidar").Matcher[]}**/ (
46+
typeof ignored === "undefined" ? [] : [ignored]
47+
);
48+
const matchers = Array.isArray(ignored) ? ignored : _ignored;
49+
50+
if (disableGlobbing === true) {
51+
return matchers;
52+
}
53+
54+
if (ignoreFunction) {
55+
matchers.push(ignoreFunction);
56+
}
57+
58+
const picomatch = require("picomatch");
59+
const isGlob = require("is-glob");
60+
61+
// Double filter to satisfy typescript. Otherwise nasty casting is required
62+
const ignoredGlobs = matchers
63+
.filter((s) => typeof s === "string")
64+
.filter((s) => isGlob(s));
65+
66+
if (ignoredGlobs.length === 0) {
67+
return matchers;
68+
}
69+
70+
const matcher = picomatch(ignoredGlobs, { cwd, dot: true });
71+
72+
matchers.push(matcher);
73+
74+
return matchers.filter((s) => typeof s !== "string" || !isGlob(s));
75+
},
76+
};

test/unit/globIgnoreMatchers.test.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"use strict";
2+
3+
const { getIgnoreMatchers } = require("../../lib/getGlobMatchers");
4+
5+
describe("getIgnoreMatchers", () => {
6+
it("should return an array of matchers for glob strings", () => {
7+
const watchOptions = {
8+
cwd: process.cwd(),
9+
ignored: ["src/*.js", "tests/*.spec.js"],
10+
};
11+
const matchers = getIgnoreMatchers(watchOptions, null);
12+
13+
expect(matchers).toHaveLength(1);
14+
expect(typeof matchers[0]).toBe("function");
15+
});
16+
17+
it("should return the original value for non-glob strings", () => {
18+
const watchOptions = { cwd: process.cwd(), ignored: "src/file.txt" };
19+
const matchers = getIgnoreMatchers(watchOptions, null);
20+
21+
expect(matchers).toHaveLength(1);
22+
expect(matchers[0]).toBe("src/file.txt");
23+
});
24+
25+
it("should return empty array if ignored is not defined", () => {
26+
const watchOptions = { cwd: process.cwd() };
27+
const matchers = getIgnoreMatchers(watchOptions, null);
28+
29+
expect(matchers).toEqual([]);
30+
});
31+
32+
it("should return an array that includes the passed matcher function", () => {
33+
const watchOptions = { cwd: process.cwd() };
34+
const ignoreFunction = () => true;
35+
const matchers = getIgnoreMatchers(watchOptions, ignoreFunction);
36+
37+
expect(matchers).toHaveLength(1);
38+
expect(matchers[0]).toBe(ignoreFunction);
39+
});
40+
41+
it("should return all original value and only replace all globs with one function", () => {
42+
const ignoreFunction = () => true;
43+
const regex = /src\/.*\.js/;
44+
const watchOptions = {
45+
cwd: process.cwd(),
46+
ignored: [
47+
"src/*.js",
48+
"src/file.txt",
49+
"src/**/*.js",
50+
ignoreFunction,
51+
regex,
52+
],
53+
};
54+
55+
const matchers = getIgnoreMatchers(watchOptions, ignoreFunction);
56+
57+
expect(matchers).toHaveLength(5);
58+
expect(matchers[0]).toBe("src/file.txt");
59+
expect(matchers[1]).toBe(ignoreFunction);
60+
expect(matchers[2]).toBe(regex);
61+
expect(matchers[3]).toBe(ignoreFunction);
62+
expect(typeof matchers[4]).toBe("function");
63+
});
64+
65+
it("should work with complicated glob", () => {
66+
const watchOptions = {
67+
cwd: process.cwd(),
68+
ignored: ["src/**/components/*.js"],
69+
};
70+
const matchers = getIgnoreMatchers(watchOptions, null);
71+
72+
expect(matchers).toHaveLength(1);
73+
expect(typeof matchers[0]).toBe("function");
74+
75+
const filePath = "src/components/file.txt";
76+
expect(matchers[0](filePath)).toBe(false);
77+
78+
const jsFilePath = "src/components/file.js";
79+
expect(matchers[0](jsFilePath)).toBe(true);
80+
81+
const jsFilePath2 = "src/other/components/file.js";
82+
expect(matchers[0](jsFilePath2)).toBe(true);
83+
84+
const nestedJsFilePath = "src/components/nested/file.js";
85+
expect(matchers[0](nestedJsFilePath)).toBe(false);
86+
});
87+
88+
it("should work with negated glob", () => {
89+
const watchOptions = {
90+
cwd: process.cwd(),
91+
ignored: ["src/**/components/!(*.spec).js"],
92+
};
93+
const matchers = getIgnoreMatchers(watchOptions, null);
94+
95+
expect(matchers).toHaveLength(1);
96+
expect(typeof matchers[0]).toBe("function");
97+
98+
const filePath = "src/components/file.txt";
99+
expect(matchers[0](filePath)).toBe(false);
100+
101+
const jsFilePath = "src/components/file.js";
102+
expect(matchers[0](jsFilePath)).toBe(true);
103+
104+
const specJsFilePath = "src/components/file.spec.js";
105+
expect(matchers[0](specJsFilePath)).toBe(false);
106+
});
107+
108+
it("should work with directory glob", () => {
109+
const watchOptions = { cwd: process.cwd(), ignored: ["src/**"] };
110+
const matchers = getIgnoreMatchers(watchOptions, null);
111+
112+
expect(matchers).toHaveLength(1);
113+
expect(typeof matchers[0]).toBe("function");
114+
115+
const filePath = "src/file.txt";
116+
expect(matchers[0](filePath)).toBe(true);
117+
118+
const dirPath = "src/subdir";
119+
expect(matchers[0](dirPath)).toBe(true);
120+
121+
const nestedFilePath = "src/subdir/nested/file.txt";
122+
expect(matchers[0](nestedFilePath)).toBe(true);
123+
124+
const wrongPath = "foo/bar";
125+
expect(matchers[0](wrongPath)).toBe(false);
126+
});
127+
128+
it("should work with directory glob and file extension", () => {
129+
const watchOptions = { cwd: process.cwd(), ignored: ["src/**/*.{js,ts}"] };
130+
const matchers = getIgnoreMatchers(watchOptions, null);
131+
132+
expect(matchers).toHaveLength(1);
133+
expect(typeof matchers[0]).toBe("function");
134+
135+
const jsFilePath = "src/file.js";
136+
expect(matchers[0](jsFilePath)).toBe(true);
137+
138+
const tsFilePath = "src/file.ts";
139+
expect(matchers[0](tsFilePath)).toBe(true);
140+
141+
const txtFilePath = "src/file.txt";
142+
expect(matchers[0](txtFilePath)).toBe(false);
143+
144+
const nestedJsFilePath = "src/subdir/nested/file.js";
145+
expect(matchers[0](nestedJsFilePath)).toBe(true);
146+
});
147+
148+
it("should return the input as array when globbing is disabled", () => {
149+
const watchOptions = {
150+
cwd: process.cwd(),
151+
disableGlobbing: true,
152+
ignored: "src/**/*.{js,ts}",
153+
};
154+
const matchers = getIgnoreMatchers(watchOptions, null);
155+
156+
expect(matchers).toHaveLength(1);
157+
expect(matchers[0]).toBe("src/**/*.{js,ts}");
158+
});
159+
});

0 commit comments

Comments
 (0)