Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/watch-future-glob-matches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-dev-server": patch
---

Keep glob-based file watches active for files created after startup, and match ignored globs against future paths without mutating the supplied watch options.
148 changes: 127 additions & 21 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3417,39 +3417,145 @@ class Server {
const isWin = path.sep === "\\";
const toPosix = (/** @type {string} */ filePath) =>
isWin ? filePath.split(path.sep).join("/") : filePath;
const toNative = (/** @type {string} */ filePath) =>
isWin ? filePath.split("/").join(path.sep) : filePath;
const cwd = watchOptions.cwd || process.cwd();
const absolute = (/** @type {string} */ item) =>
toPosix(path.resolve(cwd, item));
const getWatchRoot = (/** @type {string} */ pattern) => {
let escaped = false;
let magicIndex = -1;

for (let index = 0; index < pattern.length; index++) {
const character = pattern[index];

if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (
"*?[{".includes(character) ||
("!+@".includes(character) && pattern[index + 1] === "(")
) {
magicIndex = index;
break;
}
}

const cwd = watchOptions.cwd ? toPosix(watchOptions.cwd) : undefined;
const prefix = magicIndex === -1 ? pattern : pattern.slice(0, magicIndex);
const base = prefix.endsWith("/")
? prefix.slice(0, -1)
: path.posix.dirname(prefix);

const expand = (/** @type {string} */ item) => {
const posix = toPosix(item);
return isDynamicPattern(posix)
? globSync(posix, { cwd, absolute: true }).map(toNative)
: item;
return path.resolve(cwd, (base || ".").replaceAll(/\\(.)/g, "$1"));
};
const paths = Array.isArray(watchPath) ? watchPath : [watchPath];
/** @type {string[]} */
const roots = [];
/** @type {string[]} */
const patterns = [];
/** @type {string[]} */
const ignoredPatterns = [];
/** @type {string[]} */
const literalPaths = [];
/** @type {import("chokidar").Matcher[]} */
const ignored = [];

for (const item of paths) {
const pattern = toPosix(item);
let patternStart = 0;
while (
pattern[patternStart] === "!" &&
pattern[patternStart + 1] !== "("
) {
patternStart += 1;
}
const positivePattern = pattern.slice(patternStart);

const resolveGlobs = (/** @type {string | string[]} */ input) =>
(Array.isArray(input) ? input : [input]).flatMap((item) =>
typeof item === "string" ? expand(item) : item,
);
if (patternStart % 2 === 1) {
if (isDynamicPattern(positivePattern)) {
ignoredPatterns.push(positivePattern);
} else {
ignored.push(path.resolve(cwd, positivePattern));
}
} else if (isDynamicPattern(positivePattern)) {
roots.push(getWatchRoot(positivePattern));
patterns.push(positivePattern);
} else {
const filePath = absolute(positivePattern);
roots.push(path.resolve(cwd, positivePattern));
literalPaths.push(filePath);
}
}

const resolvedPaths = resolveGlobs(watchPath);
const originalIgnored = watchOptions.ignored;
for (const item of Array.isArray(originalIgnored)
? originalIgnored
: originalIgnored === undefined
? []
: [originalIgnored]) {
if (typeof item === "string" && isDynamicPattern(toPosix(item))) {
ignoredPatterns.push(toPosix(item));
} else {
ignored.push(item);
}
}

if (typeof watchOptions.ignored === "string") {
watchOptions.ignored = resolveGlobs(watchOptions.ignored);
} else if (Array.isArray(watchOptions.ignored)) {
watchOptions.ignored = watchOptions.ignored.flatMap((item) =>
typeof item === "string" ? expand(item) : item,
/** @type {Set<string>} */
let matchedPaths;
/** @type {Set<string>} */
let ignoredPaths;
const refreshGlobMatches = () => {
matchedPaths = new Set(
patterns.flatMap((pattern) =>
globSync(pattern, { cwd, absolute: true, onlyFiles: false }).map(
absolute,
),
),
);
}
ignoredPaths = new Set(
ignoredPatterns.flatMap((pattern) =>
globSync(pattern, { cwd, absolute: true, onlyFiles: false }).map(
absolute,
),
),
);
};

const watcher = chokidar.watch(resolvedPaths, watchOptions);
refreshGlobMatches();

const watcher = chokidar.watch([...new Set(roots)], {
...watchOptions,
ignored,
});
let ready = false;

watcher.on("ready", () => {
ready = true;
});
const refreshAfterAdd = () => {
if (ready && (patterns.length > 0 || ignoredPatterns.length > 0)) {
refreshGlobMatches();
}
};

watcher.on("add", refreshAfterAdd);
watcher.on("addDir", refreshAfterAdd);

// disabling refreshing on changing the content
if (this.options.liveReload) {
watcher.on("change", (item) => {
if (this.webSocketServer) {
const file = absolute(item);
const isMatch =
matchedPaths.has(file) ||
literalPaths.some(
(literalPath) =>
file === literalPath || file.startsWith(`${literalPath}/`),
);

if (this.webSocketServer && isMatch && !ignoredPaths.has(file)) {
this.sendMessage(
this.webSocketServer.clients,
"static-changed",
Expand Down