From 9492a00dcc8199015cdf293982319419b1184e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 13 Dec 2024 14:50:21 +0100 Subject: [PATCH 01/10] chore(deps): update chokidar to v4 In order to replace chokidars removed glob functionality, we have to use some trickery. - `glob-parent` is used to get the common ancenstor dir that needs to be watched. - `picomatch` is used as ignore-filter to make sure that only files/dirs are watched that match the given glob - `is-glob` is used to only use glob logic when needed In total this adds 4 direct or transitive dependencies. However, the update to chokidar v4 removes 12. So its a net positive I also added a test to ensure that creation of nested directories is detected properly. --- .eslintignore | 1 + .gitignore | 1 + lib/Server.js | 33 ++- package-lock.json | 208 +++++++++++++----- package.json | 8 +- .../watch-files.test.js.snap.webpack5 | 10 + test/e2e/watch-files.test.js | 83 +++++++ types/lib/Server.d.ts | 50 ++++- 8 files changed, 332 insertions(+), 62 deletions(-) diff --git a/.eslintignore b/.eslintignore index 9bc9ffb002..18329f0a43 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,4 @@ node_modules examples/**/main.js examples/client/trusted-types-overlay/app.js test/fixtures/reload-config/foo.js +test/fixtures/worker-config-dev-server-false/public/worker-bundle.js diff --git a/.gitignore b/.gitignore index de3b694aec..3877986208 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ yarn-error.log test/fixtures/static-config/public/assets/non-exist.txt test/fixtures/watch-files-config/public/assets/non-exist.txt +test/fixtures/watch-files-config/public/non-existant/non-exist.txt test/fixtures/reload-config/main.css test/fixtures/reload-config-2/main.css test/fixtures/worker-config-dev-server-false/public diff --git a/lib/Server.js b/lib/Server.js index 80051e735e..640dc87d06 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -18,7 +18,7 @@ const schema = require("./options.json"); /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").MultiStats} MultiStats */ /** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */ -/** @typedef {import("chokidar").WatchOptions} WatchOptions */ +/** @typedef {import("chokidar").ChokidarOptions} WatchOptions */ /** @typedef {import("chokidar").FSWatcher} FSWatcher */ /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */ /** @typedef {import("bonjour-service").Bonjour} Bonjour */ @@ -3255,9 +3255,36 @@ class Server { * @param {string | string[]} watchPath * @param {WatchOptions} [watchOptions] */ - watchFiles(watchPath, watchOptions) { + watchFiles(watchPath, watchOptions = {}) { const chokidar = require("chokidar"); - const watcher = chokidar.watch(watchPath, watchOptions); + const isGlob = require("is-glob"); + + const watchPathArr = Array.isArray(watchPath) ? watchPath : [watchPath]; + const watchPathGlobs = watchPathArr.filter((p) => isGlob(p)); + + // No need to do all this work when no globs are used + if (watchPathGlobs.length > 0) { + const globParent = require("glob-parent"); + const picomatch = require("picomatch"); + + watchPathGlobs.forEach((p) => { + watchPathArr[watchPathArr.indexOf(p)] = globParent(p); + }); + + const matcher = picomatch(watchPathGlobs); + const ignoreFunc = (/** @type {string} */ p) => + !watchPathArr.includes(p) && !matcher(p); + + if (Array.isArray(watchOptions.ignored)) { + watchOptions.ignored.push(ignoreFunc); + } else { + watchOptions.ignored = watchOptions.ignored + ? [watchOptions.ignored, ignoreFunc] + : ignoreFunc; + } + } + + const watcher = chokidar.watch(watchPathArr, watchOptions); // disabling refreshing on changing the content if (this.options.liveReload) { diff --git a/package-lock.json b/package-lock.json index 48a4337741..76c8cb9149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,17 +18,20 @@ "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", + "chokidar": "^4.0.1", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "express": "^4.21.2", + "glob-parent": "^6.0.2", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", + "is-glob": "^4.0.3", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", + "picomatch": "^4.0.2", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", @@ -52,8 +55,11 @@ "@commitlint/config-conventional": "^19.5.0", "@hono/node-server": "^1.13.3", "@types/compression": "^1.7.2", + "@types/glob-parent": "^5.1.3", + "@types/is-glob": "^4.0.4", "@types/node": "^22.8.4", "@types/node-forge": "^1.3.1", + "@types/picomatch": "^3.0.1", "@types/sockjs-client": "^1.5.1", "@types/trusted-types": "^2.0.2", "acorn": "^8.14.0", @@ -161,6 +167,70 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/cli/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "optional": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@babel/cli/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/cli/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@babel/cli/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", @@ -4146,6 +4216,12 @@ "@types/send": "*" } }, + "node_modules/@types/glob-parent": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/glob-parent/-/glob-parent-5.1.3.tgz", + "integrity": "sha512-p+NciRH8TRvrgISOCQ55CP+lktMmDpOXsp4spULIIz0L4aJ6G9zFX+N0UZ2xulmJRgaQLRxXIp4xHdL6YOQjDg==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4174,6 +4250,12 @@ "@types/node": "*" } }, + "node_modules/@types/is-glob": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/is-glob/-/is-glob-4.0.4.tgz", + "integrity": "sha512-3mFBtIPQ0TQetKRDe94g8YrxJZxdMillMGegyv6zRBXvq4peRRhf2wLZ/Dl53emtTsC29dQQBwYvovS20yXpiQ==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4254,6 +4336,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -4757,6 +4845,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -4765,6 +4854,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5278,6 +5379,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "optional": true, "engines": { "node": ">=8" }, @@ -5593,26 +5696,17 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -9113,18 +9207,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/eslint/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -9957,6 +10039,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -10682,14 +10765,14 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -11525,6 +11608,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -13461,6 +13546,18 @@ "node": ">=8" } }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-util/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14588,6 +14685,17 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -14886,6 +14994,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -15521,11 +15630,11 @@ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -16192,14 +16301,15 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -18236,18 +18346,6 @@ } } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 8a1d0b7725..287d55022b 100644 --- a/package.json +++ b/package.json @@ -55,17 +55,20 @@ "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", + "chokidar": "^4.0.1", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "express": "^4.21.2", + "glob-parent": "^6.0.2", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", + "is-glob": "^4.0.3", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", + "picomatch": "^4.0.2", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", @@ -86,8 +89,11 @@ "@commitlint/config-conventional": "^19.5.0", "@hono/node-server": "^1.13.3", "@types/compression": "^1.7.2", + "@types/glob-parent": "^5.1.3", + "@types/is-glob": "^4.0.4", "@types/node": "^22.8.4", "@types/node-forge": "^1.3.1", + "@types/picomatch": "^3.0.1", "@types/sockjs-client": "^1.5.1", "@types/trusted-types": "^2.0.2", "acorn": "^8.14.0", diff --git a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 index 5c63e7b714..fcff80b9ad 100644 --- a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 @@ -20,6 +20,16 @@ exports[`watchFiles option should work with array config should reload when file exports[`watchFiles option should work with array config should reload when file content is changed: response status 1`] = `200`; +exports[`watchFiles option should work with glob when creating nested directory should reload when file content is changed: console messages 1`] = ` +[ + "Hey.", +] +`; + +exports[`watchFiles option should work with glob when creating nested directory should reload when file content is changed: page errors 1`] = `[]`; + +exports[`watchFiles option should work with glob when creating nested directory should reload when file content is changed: response status 1`] = `200`; + exports[`watchFiles option should work with object with multiple paths should reload when file content is changed: console messages 1`] = ` [ "Hey.", diff --git a/test/e2e/watch-files.test.js b/test/e2e/watch-files.test.js index 6c70b2d8fa..997076b04e 100644 --- a/test/e2e/watch-files.test.js +++ b/test/e2e/watch-files.test.js @@ -309,6 +309,89 @@ describe("watchFiles option", () => { }); }); + describe("should work with glob when creating nested directory", () => { + const nonExistFile = path.join(watchDir, "non-existant/non-exist.txt"); + let compiler; + let server; + let page; + let browser; + let pageErrors; + let consoleMessages; + + beforeEach(async () => { + try { + fs.unlinkSync(nonExistFile); + fs.rmdirSync(path.join(watchDir, "non-existant")); + } catch (error) { + // ignore + } + + compiler = webpack(config); + + server = new Server( + { + watchFiles: `${watchDir}/**/*`, + port, + }, + compiler, + ); + + await server.start(); + + ({ page, browser } = await runBrowser()); + + pageErrors = []; + consoleMessages = []; + }); + + afterEach(async () => { + await browser.close(); + await server.stop(); + }); + + it("should reload when file content is changed", async () => { + page + .on("console", (message) => { + consoleMessages.push(message); + }) + .on("pageerror", (error) => { + pageErrors.push(error); + }); + + const response = await page.goto(`http://127.0.0.1:${port}/`, { + waitUntil: "networkidle0", + }); + + expect(response.status()).toMatchSnapshot("response status"); + + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages", + ); + + expect(pageErrors).toMatchSnapshot("page errors"); + + await new Promise((resolve) => { + server.staticWatchers[0].on("change", async (changedPath) => { + // page reload + await page.waitForNavigation({ waitUntil: "networkidle0" }); + + expect(changedPath).toBe(nonExistFile); + resolve(); + }); + + // create file content + setTimeout(() => { + fs.mkdirSync(path.join(watchDir, "non-existant")); + fs.writeFileSync(nonExistFile, "Kurosaki Ichigo", "utf8"); + // change file content + setTimeout(() => { + fs.writeFileSync(nonExistFile, "Kurosaki Ichigo", "utf8"); + }, 1000); + }, 1000); + }); + }); + }); + describe("should work with object with single path", () => { const file = path.join(watchDir, "assets/example.txt"); let compiler; diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 57ee326f17..92e821a3ab 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1484,7 +1484,7 @@ type StatsCompilation = import("webpack").StatsCompilation; type Stats = import("webpack").Stats; type MultiStats = import("webpack").MultiStats; type NetworkInterfaceInfo = import("os").NetworkInterfaceInfo; -type WatchOptions = import("chokidar").WatchOptions; +type WatchOptions = import("chokidar").ChokidarOptions; type FSWatcher = import("chokidar").FSWatcher; type ConnectHistoryApiFallbackOptions = import("connect-history-api-fallback").Options; @@ -1550,7 +1550,29 @@ type Port = number | string | "auto"; type WatchFiles = { paths: string | string[]; options?: - | (import("chokidar").WatchOptions & { + | (Partial< + { + persistent: boolean; + ignoreInitial: boolean; + followSymlinks: boolean; + cwd?: string; + usePolling: boolean; + interval: number; + binaryInterval: number; + alwaysStat?: boolean; + depth?: number; + ignorePermissionErrors: boolean; + atomic: boolean | number; + } & { + ignored: import("chokidar").Matcher | import("chokidar").Matcher[]; + awaitWriteFinish: + | boolean + | Partial<{ + stabilityThreshold: number; + pollInterval: number; + }>; + } + > & { aggregateTimeout?: number; ignored?: WatchOptions["ignored"]; poll?: number | boolean; @@ -1568,7 +1590,29 @@ type Static = { | undefined; watch?: | boolean - | (import("chokidar").WatchOptions & { + | (Partial< + { + persistent: boolean; + ignoreInitial: boolean; + followSymlinks: boolean; + cwd?: string; + usePolling: boolean; + interval: number; + binaryInterval: number; + alwaysStat?: boolean; + depth?: number; + ignorePermissionErrors: boolean; + atomic: boolean | number; + } & { + ignored: import("chokidar").Matcher | import("chokidar").Matcher[]; + awaitWriteFinish: + | boolean + | Partial<{ + stabilityThreshold: number; + pollInterval: number; + }>; + } + > & { aggregateTimeout?: number; ignored?: WatchOptions["ignored"]; poll?: number | boolean; From 184588dd1b651a3d10023063b4a5b50fd5da2a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 13 Dec 2024 14:50:22 +0100 Subject: [PATCH 02/10] fix: support ignore globs, enable dotfiles --- lib/Server.js | 72 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 640dc87d06..392077d0a1 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -18,7 +18,7 @@ const schema = require("./options.json"); /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").MultiStats} MultiStats */ /** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */ -/** @typedef {import("chokidar").ChokidarOptions} WatchOptions */ +/** @typedef {import("chokidar").ChokidarOptions & { disableGlobbing?: boolean }} WatchOptions */ /** @typedef {import("chokidar").FSWatcher} FSWatcher */ /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */ /** @typedef {import("bonjour-service").Bonjour} Bonjour */ @@ -3257,30 +3257,64 @@ class Server { */ watchFiles(watchPath, watchOptions = {}) { const chokidar = require("chokidar"); - const isGlob = require("is-glob"); const watchPathArr = Array.isArray(watchPath) ? watchPath : [watchPath]; - const watchPathGlobs = watchPathArr.filter((p) => isGlob(p)); - // No need to do all this work when no globs are used - if (watchPathGlobs.length > 0) { - const globParent = require("glob-parent"); - const picomatch = require("picomatch"); + if (watchOptions.disableGlobbing !== true) { + const isGlob = require("is-glob"); + const watchPathGlobs = watchPathArr.filter((p) => isGlob(p)); - watchPathGlobs.forEach((p) => { - watchPathArr[watchPathArr.indexOf(p)] = globParent(p); - }); + // No need to do all this work when no globs are used + if (watchPathGlobs.length > 0) { + const globParent = require("glob-parent"); + const picomatch = require("picomatch"); - const matcher = picomatch(watchPathGlobs); - const ignoreFunc = (/** @type {string} */ p) => - !watchPathArr.includes(p) && !matcher(p); + watchPathGlobs.forEach((p) => { + watchPathArr[watchPathArr.indexOf(p)] = globParent(p); + }); - if (Array.isArray(watchOptions.ignored)) { - watchOptions.ignored.push(ignoreFunc); - } else { - watchOptions.ignored = watchOptions.ignored - ? [watchOptions.ignored, ignoreFunc] - : ignoreFunc; + const matcher = picomatch(watchPathGlobs, { + cwd: watchOptions.cwd, + dot: true, + }); + const ignoreFunc = (/** @type {string} */ p) => + !watchPathArr.includes(p) && !matcher(p); + + if (Array.isArray(watchOptions.ignored)) { + const ignoredGlobs = []; + for (let i = 0; i < watchOptions.ignored.length; i++) { + const ignored = watchOptions.ignored[i]; + if (typeof ignored === "string" && isGlob(ignored)) { + ignoredGlobs.push(ignored); + watchOptions.ignored.splice(i, 1); + } + } + + if (ignoredGlobs.length > 0) { + const ignoreMatcher = picomatch(ignoredGlobs, { + dot: true, + cwd: watchOptions.cwd, + }); + watchOptions.ignored.push(ignoreMatcher); + } + + watchOptions.ignored.push(ignoreFunc); + } else { + if ( + watchOptions.ignored && + typeof watchOptions.ignored === "string" && + isGlob(watchOptions.ignored) + ) { + watchOptions.ignored = picomatch(watchOptions.ignored, { + dot: true, + cwd: watchOptions.cwd, + }); + } + + watchOptions.ignored = watchOptions.ignored + ? [watchOptions.ignored, ignoreFunc] + : ignoreFunc; + } } } From 970c35314b2cd27b9241cd5cd70a570ffe221dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 13 Dec 2024 18:00:53 +0100 Subject: [PATCH 03/10] fix: simply logic of ignored files --- lib/Server.js | 53 ++++++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 392077d0a1..cc007fd383 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -3277,44 +3277,37 @@ class Server { cwd: watchOptions.cwd, dot: true, }); + const ignoreFunc = (/** @type {string} */ p) => !watchPathArr.includes(p) && !matcher(p); - if (Array.isArray(watchOptions.ignored)) { - const ignoredGlobs = []; - for (let i = 0; i < watchOptions.ignored.length; i++) { - const ignored = watchOptions.ignored[i]; - if (typeof ignored === "string" && isGlob(ignored)) { - ignoredGlobs.push(ignored); - watchOptions.ignored.splice(i, 1); - } - } + const ignoredArr = Array.isArray(watchOptions.ignored) + ? watchOptions.ignored + : []; - if (ignoredGlobs.length > 0) { - const ignoreMatcher = picomatch(ignoredGlobs, { - dot: true, - cwd: watchOptions.cwd, - }); - watchOptions.ignored.push(ignoreMatcher); - } + // Nested ternaries are forbidden by eslint so we end up with this + if (watchOptions.ignored && !Array.isArray(watchOptions.ignored)) { + ignoredArr.push(watchOptions.ignored); + } - watchOptions.ignored.push(ignoreFunc); - } else { - if ( - watchOptions.ignored && - typeof watchOptions.ignored === "string" && - isGlob(watchOptions.ignored) - ) { - watchOptions.ignored = picomatch(watchOptions.ignored, { - dot: true, - cwd: watchOptions.cwd, - }); + const ignoredGlobs = []; + for (let i = 0; i < ignoredArr.length; i++) { + const ignored = ignoredArr[i]; + if (typeof ignored === "string" && isGlob(ignored)) { + ignoredGlobs.push(ignored); + ignoredArr.splice(i, 1); } + } - watchOptions.ignored = watchOptions.ignored - ? [watchOptions.ignored, ignoreFunc] - : ignoreFunc; + if (ignoredGlobs.length > 0) { + const ignoreMatcher = picomatch(ignoredGlobs, { + dot: true, + cwd: watchOptions.cwd, + }); + ignoredArr.push(ignoreMatcher); } + + ignoredArr.push(ignoreFunc); } } From d883a1aee02e4c3425c0f63e12390db5f3138ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 13 Dec 2024 18:03:15 +0100 Subject: [PATCH 04/10] fix: missed one line --- lib/Server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Server.js b/lib/Server.js index cc007fd383..02477f868b 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -3308,6 +3308,8 @@ class Server { } ignoredArr.push(ignoreFunc); + + watchOptions.ignored = ignoredArr; } } From 3dd2b4b78546a8f5bcd56951fa2882eba9f8c188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Tue, 17 Dec 2024 11:39:22 +0100 Subject: [PATCH 05/10] fix: add disableGlobbing option back --- types/lib/Server.d.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 92e821a3ab..cb99b24f45 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1484,7 +1484,9 @@ type StatsCompilation = import("webpack").StatsCompilation; type Stats = import("webpack").Stats; type MultiStats = import("webpack").MultiStats; type NetworkInterfaceInfo = import("os").NetworkInterfaceInfo; -type WatchOptions = import("chokidar").ChokidarOptions; +type WatchOptions = import("chokidar").ChokidarOptions & { + disableGlobbing?: boolean; +}; type FSWatcher = import("chokidar").FSWatcher; type ConnectHistoryApiFallbackOptions = import("connect-history-api-fallback").Options; @@ -1573,6 +1575,8 @@ type WatchFiles = { }>; } > & { + disableGlobbing?: boolean; + } & { aggregateTimeout?: number; ignored?: WatchOptions["ignored"]; poll?: number | boolean; @@ -1613,6 +1617,8 @@ type Static = { }>; } > & { + disableGlobbing?: boolean; + } & { aggregateTimeout?: number; ignored?: WatchOptions["ignored"]; poll?: number | boolean; From a34faf65526e094d6cecae28f4b1f4a1ad964764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Thu, 19 Dec 2024 11:27:54 +0100 Subject: [PATCH 06/10] fix: bug fixing and make code more readable --- lib/Server.js | 55 ++++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 02477f868b..3c13932dbb 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -3259,15 +3259,18 @@ class Server { const chokidar = require("chokidar"); const watchPathArr = Array.isArray(watchPath) ? watchPath : [watchPath]; + const ignoredArr = Array.isArray(watchOptions.ignored) + ? watchOptions.ignored + : []; if (watchOptions.disableGlobbing !== true) { + const picomatch = require("picomatch"); const isGlob = require("is-glob"); const watchPathGlobs = watchPathArr.filter((p) => isGlob(p)); - // No need to do all this work when no globs are used + // No need to do all this work when no globs are used in watcher if (watchPathGlobs.length > 0) { const globParent = require("glob-parent"); - const picomatch = require("picomatch"); watchPathGlobs.forEach((p) => { watchPathArr[watchPathArr.indexOf(p)] = globParent(p); @@ -3278,39 +3281,29 @@ class Server { dot: true, }); - const ignoreFunc = (/** @type {string} */ p) => - !watchPathArr.includes(p) && !matcher(p); - - const ignoredArr = Array.isArray(watchOptions.ignored) - ? watchOptions.ignored - : []; - - // Nested ternaries are forbidden by eslint so we end up with this - if (watchOptions.ignored && !Array.isArray(watchOptions.ignored)) { - ignoredArr.push(watchOptions.ignored); - } - - const ignoredGlobs = []; - for (let i = 0; i < ignoredArr.length; i++) { - const ignored = ignoredArr[i]; - if (typeof ignored === "string" && isGlob(ignored)) { - ignoredGlobs.push(ignored); - ignoredArr.splice(i, 1); - } - } + // Ignore all paths that don't match any of the globs + ignoredArr.push((p) => !watchPathArr.includes(p) && !matcher(p)); + } - if (ignoredGlobs.length > 0) { - const ignoreMatcher = picomatch(ignoredGlobs, { - dot: true, - cwd: watchOptions.cwd, - }); - ignoredArr.push(ignoreMatcher); - } + // Double filter to satisfy typescript. Otherwise nasty casting is required + const ignoredGlobs = ignoredArr + .filter((s) => typeof s === "string") + .filter((s) => isGlob(s)); - ignoredArr.push(ignoreFunc); + // No need to do all this work when no globs are used in ignored + if (ignoredGlobs.length > 0) { + const matcher = picomatch(ignoredGlobs, { + cwd: watchOptions.cwd, + dot: true, + }); - watchOptions.ignored = ignoredArr; + ignoredArr.push(matcher); } + + // Filter out all the glob strings + watchOptions.ignored = ignoredArr.filter( + (s) => typeof s === "string" && isGlob(s), + ); } const watcher = chokidar.watch(watchPathArr, watchOptions); From 7b113cf0a83443355e72bcc921d9a6c5d32f1b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Thu, 19 Dec 2024 11:56:53 +0100 Subject: [PATCH 07/10] fix: test snapshots --- .../watch-files.test.js.snap.webpack5 | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 index fcff80b9ad..b76e626f1c 100644 --- a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 @@ -57,7 +57,7 @@ exports[`watchFiles option should work with options {"interval":400,"poll":200} "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 400, "persistent": true, "usePolling": true, @@ -81,7 +81,7 @@ exports[`watchFiles option should work with options {"poll":200} should reload w "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 200, "persistent": true, "usePolling": true, @@ -105,7 +105,7 @@ exports[`watchFiles option should work with options {"poll":true} should reload "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": undefined, "persistent": true, "usePolling": true, @@ -129,7 +129,7 @@ exports[`watchFiles option should work with options {"usePolling":false,"interva "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 200, "persistent": true, "usePolling": false, @@ -153,7 +153,7 @@ exports[`watchFiles option should work with options {"usePolling":false,"poll":2 "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 200, "persistent": true, "usePolling": false, @@ -177,7 +177,7 @@ exports[`watchFiles option should work with options {"usePolling":false,"poll":t "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": undefined, "persistent": true, "usePolling": false, @@ -201,7 +201,7 @@ exports[`watchFiles option should work with options {"usePolling":false} should "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": undefined, "persistent": true, "usePolling": false, @@ -225,7 +225,7 @@ exports[`watchFiles option should work with options {"usePolling":true,"interval "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 200, "persistent": true, "usePolling": true, @@ -249,7 +249,7 @@ exports[`watchFiles option should work with options {"usePolling":true,"poll":20 "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": 200, "persistent": true, "usePolling": true, @@ -273,7 +273,7 @@ exports[`watchFiles option should work with options {"usePolling":true} should r "followSymlinks": false, "ignoreInitial": true, "ignorePermissionErrors": true, - "ignored": undefined, + "ignored": [], "interval": undefined, "persistent": true, "usePolling": true, From b01a2c361866030d4552d50f490019aef90c5573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 20 Dec 2024 20:15:40 +0100 Subject: [PATCH 08/10] chore(tests): unit tests for glob matching --- lib/Server.js | 57 ++-------- lib/getGlobMatchers.js | 76 +++++++++++++ test/unit/globIgnoreMatchers.test.js | 159 +++++++++++++++++++++++++++ test/unit/globWatchers.test.js | 140 +++++++++++++++++++++++ 4 files changed, 385 insertions(+), 47 deletions(-) create mode 100644 lib/getGlobMatchers.js create mode 100644 test/unit/globIgnoreMatchers.test.js create mode 100644 test/unit/globWatchers.test.js diff --git a/lib/Server.js b/lib/Server.js index 3c13932dbb..cfe37127bd 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -8,6 +8,10 @@ const fs = require("graceful-fs"); const ipaddr = require("ipaddr.js"); const { validate } = require("schema-utils"); const schema = require("./options.json"); +const { + getGlobbedWatcherPaths, + getIgnoreMatchers, +} = require("./getGlobMatchers"); /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ /** @typedef {import("webpack").Compiler} Compiler */ @@ -3258,55 +3262,14 @@ class Server { watchFiles(watchPath, watchOptions = {}) { const chokidar = require("chokidar"); - const watchPathArr = Array.isArray(watchPath) ? watchPath : [watchPath]; - const ignoredArr = Array.isArray(watchOptions.ignored) - ? watchOptions.ignored - : []; - - if (watchOptions.disableGlobbing !== true) { - const picomatch = require("picomatch"); - const isGlob = require("is-glob"); - const watchPathGlobs = watchPathArr.filter((p) => isGlob(p)); - - // No need to do all this work when no globs are used in watcher - if (watchPathGlobs.length > 0) { - const globParent = require("glob-parent"); - - watchPathGlobs.forEach((p) => { - watchPathArr[watchPathArr.indexOf(p)] = globParent(p); - }); - - const matcher = picomatch(watchPathGlobs, { - cwd: watchOptions.cwd, - dot: true, - }); - - // Ignore all paths that don't match any of the globs - ignoredArr.push((p) => !watchPathArr.includes(p) && !matcher(p)); - } - - // Double filter to satisfy typescript. Otherwise nasty casting is required - const ignoredGlobs = ignoredArr - .filter((s) => typeof s === "string") - .filter((s) => isGlob(s)); - - // No need to do all this work when no globs are used in ignored - if (ignoredGlobs.length > 0) { - const matcher = picomatch(ignoredGlobs, { - cwd: watchOptions.cwd, - dot: true, - }); - - ignoredArr.push(matcher); - } + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + watchPath, + watchOptions, + ); - // Filter out all the glob strings - watchOptions.ignored = ignoredArr.filter( - (s) => typeof s === "string" && isGlob(s), - ); - } + watchOptions.ignored = getIgnoreMatchers(watchOptions, ignoreFunction); - const watcher = chokidar.watch(watchPathArr, watchOptions); + const watcher = chokidar.watch(watchPaths, watchOptions); // disabling refreshing on changing the content if (this.options.liveReload) { diff --git a/lib/getGlobMatchers.js b/lib/getGlobMatchers.js new file mode 100644 index 0000000000..c75d5e1acf --- /dev/null +++ b/lib/getGlobMatchers.js @@ -0,0 +1,76 @@ +"use strict"; + +module.exports = { + /** + * @param {string[] | string} _watchPaths + * @param {import("./Server").WatchOptions} watchOptions + * @returns {[string[], import("chokidar").MatchFunction | null]}*/ + getGlobbedWatcherPaths(_watchPaths, { disableGlobbing, cwd }) { + const watchPaths = Array.isArray(_watchPaths) ? _watchPaths : [_watchPaths]; + + if (disableGlobbing === true) { + return [watchPaths, null]; + } + + const picomatch = require("picomatch"); + const isGlob = require("is-glob"); + const watchPathGlobs = watchPaths.filter((p) => isGlob(p)); + + if (watchPathGlobs.length === 0) { + return [watchPaths, null]; + } + + const globParent = require("glob-parent"); + + watchPathGlobs.forEach((p) => { + watchPaths[watchPaths.indexOf(p)] = globParent(p); + }); + + const matcher = picomatch(watchPathGlobs, { cwd, dot: true }); + + /** @type {import("chokidar").MatchFunction} */ + const ignoreFunction = (p) => !watchPaths.includes(p) && !matcher(p); + + // Ignore all paths that don't match any of the globs + return [watchPaths, ignoreFunction]; + }, + + /** + * + * @param {import("./Server").WatchOptions} watchOptions + * @param {import("chokidar").MatchFunction | null } ignoreFunction + * @returns {import("chokidar").Matcher[]} + */ + getIgnoreMatchers({ disableGlobbing, ignored, cwd }, ignoreFunction) { + const _ignored = /** @type {import("chokidar").Matcher[]}**/ ( + typeof ignored === "undefined" ? [] : [ignored] + ); + const matchers = Array.isArray(ignored) ? ignored : _ignored; + + if (disableGlobbing === true) { + return matchers; + } + + if (ignoreFunction) { + matchers.push(ignoreFunction); + } + + const picomatch = require("picomatch"); + const isGlob = require("is-glob"); + + // Double filter to satisfy typescript. Otherwise nasty casting is required + const ignoredGlobs = matchers + .filter((s) => typeof s === "string") + .filter((s) => isGlob(s)); + + if (ignoredGlobs.length === 0) { + return matchers; + } + + const matcher = picomatch(ignoredGlobs, { cwd, dot: true }); + + matchers.push(matcher); + + return matchers.filter((s) => typeof s !== "string" || !isGlob(s)); + }, +}; diff --git a/test/unit/globIgnoreMatchers.test.js b/test/unit/globIgnoreMatchers.test.js new file mode 100644 index 0000000000..10dbc171c4 --- /dev/null +++ b/test/unit/globIgnoreMatchers.test.js @@ -0,0 +1,159 @@ +"use strict"; + +const { getIgnoreMatchers } = require("../../lib/getGlobMatchers"); + +describe("getIgnoreMatchers", () => { + it("should return an array of matchers for glob strings", () => { + const watchOptions = { + cwd: process.cwd(), + ignored: ["src/*.js", "tests/*.spec.js"], + }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(typeof matchers[0]).toBe("function"); + }); + + it("should return the original value for non-glob strings", () => { + const watchOptions = { cwd: process.cwd(), ignored: "src/file.txt" }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(matchers[0]).toBe("src/file.txt"); + }); + + it("should return empty array if ignored is not defined", () => { + const watchOptions = { cwd: process.cwd() }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toEqual([]); + }); + + it("should return an array that includes the passed matcher function", () => { + const watchOptions = { cwd: process.cwd() }; + const ignoreFunction = () => true; + const matchers = getIgnoreMatchers(watchOptions, ignoreFunction); + + expect(matchers).toHaveLength(1); + expect(matchers[0]).toBe(ignoreFunction); + }); + + it("should return all original value and only replace all globs with one function", () => { + const ignoreFunction = () => true; + const regex = /src\/.*\.js/; + const watchOptions = { + cwd: process.cwd(), + ignored: [ + "src/*.js", + "src/file.txt", + "src/**/*.js", + ignoreFunction, + regex, + ], + }; + + const matchers = getIgnoreMatchers(watchOptions, ignoreFunction); + + expect(matchers).toHaveLength(5); + expect(matchers[0]).toBe("src/file.txt"); + expect(matchers[1]).toBe(ignoreFunction); + expect(matchers[2]).toBe(regex); + expect(matchers[3]).toBe(ignoreFunction); + expect(typeof matchers[4]).toBe("function"); + }); + + it("should work with complicated glob", () => { + const watchOptions = { + cwd: process.cwd(), + ignored: ["src/**/components/*.js"], + }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(typeof matchers[0]).toBe("function"); + + const filePath = "src/components/file.txt"; + expect(matchers[0](filePath)).toBe(false); + + const jsFilePath = "src/components/file.js"; + expect(matchers[0](jsFilePath)).toBe(true); + + const jsFilePath2 = "src/other/components/file.js"; + expect(matchers[0](jsFilePath2)).toBe(true); + + const nestedJsFilePath = "src/components/nested/file.js"; + expect(matchers[0](nestedJsFilePath)).toBe(false); + }); + + it("should work with negated glob", () => { + const watchOptions = { + cwd: process.cwd(), + ignored: ["src/**/components/!(*.spec).js"], + }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(typeof matchers[0]).toBe("function"); + + const filePath = "src/components/file.txt"; + expect(matchers[0](filePath)).toBe(false); + + const jsFilePath = "src/components/file.js"; + expect(matchers[0](jsFilePath)).toBe(true); + + const specJsFilePath = "src/components/file.spec.js"; + expect(matchers[0](specJsFilePath)).toBe(false); + }); + + it("should work with directory glob", () => { + const watchOptions = { cwd: process.cwd(), ignored: ["src/**"] }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(typeof matchers[0]).toBe("function"); + + const filePath = "src/file.txt"; + expect(matchers[0](filePath)).toBe(true); + + const dirPath = "src/subdir"; + expect(matchers[0](dirPath)).toBe(true); + + const nestedFilePath = "src/subdir/nested/file.txt"; + expect(matchers[0](nestedFilePath)).toBe(true); + + const wrongPath = "foo/bar"; + expect(matchers[0](wrongPath)).toBe(false); + }); + + it("should work with directory glob and file extension", () => { + const watchOptions = { cwd: process.cwd(), ignored: ["src/**/*.{js,ts}"] }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(typeof matchers[0]).toBe("function"); + + const jsFilePath = "src/file.js"; + expect(matchers[0](jsFilePath)).toBe(true); + + const tsFilePath = "src/file.ts"; + expect(matchers[0](tsFilePath)).toBe(true); + + const txtFilePath = "src/file.txt"; + expect(matchers[0](txtFilePath)).toBe(false); + + const nestedJsFilePath = "src/subdir/nested/file.js"; + expect(matchers[0](nestedJsFilePath)).toBe(true); + }); + + it("should return the input as array when globbing is disabled", () => { + const watchOptions = { + cwd: process.cwd(), + disableGlobbing: true, + ignored: "src/**/*.{js,ts}", + }; + const matchers = getIgnoreMatchers(watchOptions, null); + + expect(matchers).toHaveLength(1); + expect(matchers[0]).toBe("src/**/*.{js,ts}"); + }); +}); diff --git a/test/unit/globWatchers.test.js b/test/unit/globWatchers.test.js new file mode 100644 index 0000000000..54fd8d1c48 --- /dev/null +++ b/test/unit/globWatchers.test.js @@ -0,0 +1,140 @@ +"use strict"; + +const { getGlobbedWatcherPaths } = require("../../lib/getGlobMatchers"); + +describe("getGlobbedWatcherPaths", () => { + it("should watch the parent directory of the glob", () => { + const glob = "src/*.js"; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths] = getGlobbedWatcherPaths(glob, watchOptions); + + expect(watchPaths).toEqual(["src"]); + }); + + it("should ignore files that are not part of the glob", () => { + const glob = "src/*.js"; + const watchOptions = { cwd: process.cwd() }; + const [, ignoreFunction] = getGlobbedWatcherPaths(glob, watchOptions); + + const filePath = "src/file.txt"; + expect(ignoreFunction(filePath)).toBe(true); + + const jsFilePath = "src/file.js"; + expect(ignoreFunction(jsFilePath)).toBe(false); + }); + + it("should work with multiple globs", () => { + const globs = ["src/*.js", "tests/*.spec.js"]; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + globs, + watchOptions, + ); + + expect(watchPaths).toEqual(["src", "tests"]); + + const filePath = "src/file.txt"; + expect(ignoreFunction(filePath)).toBe(true); + + const jsFilePath = "src/file.js"; + expect(ignoreFunction(jsFilePath)).toBe(false); + + const specFilePath = "tests/file.spec.js"; + expect(ignoreFunction(specFilePath)).toBe(false); + }); + + it("should work with complicated glob", () => { + const glob = "src/**/components/*.js"; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + glob, + watchOptions, + ); + + expect(watchPaths).toEqual(["src"]); + + const filePath = "src/components/file.txt"; + expect(ignoreFunction(filePath)).toBe(true); + + const jsFilePath = "src/components/file.js"; + expect(ignoreFunction(jsFilePath)).toBe(false); + + const nestedJsFilePath = "src/components/nested/file.js"; + expect(ignoreFunction(nestedJsFilePath)).toBe(true); + }); + + it("should work with negated glob", () => { + const glob = "src/**/components/!(*.spec).js"; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + glob, + watchOptions, + ); + + expect(watchPaths).toEqual(["src"]); + + const filePath = "src/components/file.txt"; + expect(ignoreFunction(filePath)).toBe(true); + + const jsFilePath = "src/components/file.js"; + expect(ignoreFunction(jsFilePath)).toBe(false); + + const specJsFilePath = "src/components/file.spec.js"; + expect(ignoreFunction(specJsFilePath)).toBe(true); + }); + + it("should work with directory glob", () => { + const glob = "src/**"; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + glob, + watchOptions, + ); + + expect(watchPaths).toEqual(["src"]); + + const filePath = "src/file.txt"; + expect(ignoreFunction(filePath)).toBe(false); + + const dirPath = "src/subdir"; + expect(ignoreFunction(dirPath)).toBe(false); + + const nestedFilePath = "src/subdir/nested/file.txt"; + expect(ignoreFunction(nestedFilePath)).toBe(false); + }); + + it("should work with directory glob and file extension", () => { + const glob = "src/**/*.{js,ts}"; + const watchOptions = { cwd: process.cwd() }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + glob, + watchOptions, + ); + + expect(watchPaths).toEqual(["src"]); + + const jsFilePath = "src/file.js"; + expect(ignoreFunction(jsFilePath)).toBe(false); + + const tsFilePath = "src/file.ts"; + expect(ignoreFunction(tsFilePath)).toBe(false); + + const txtFilePath = "src/file.txt"; + expect(ignoreFunction(txtFilePath)).toBe(true); + + const nestedJsFilePath = "src/subdir/nested/file.js"; + expect(ignoreFunction(nestedJsFilePath)).toBe(false); + }); + + it("should return the input as array when globbing is disabled", () => { + const glob = "src/**/*.{js,ts}"; + const watchOptions = { cwd: process.cwd(), disableGlobbing: true }; + const [watchPaths, ignoreFunction] = getGlobbedWatcherPaths( + glob, + watchOptions, + ); + + expect(watchPaths).toEqual(["src/**/*.{js,ts}"]); + expect(ignoreFunction).toBe(null); + }); +}); From 337423440338fbeeed1e62e53f07620210248596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 20 Dec 2024 20:29:14 +0100 Subject: [PATCH 09/10] fix: remove undefined values from options This has to be done because of an issue on chokidars site: https://github.com/paulmillr/chokidar/issues/1394 --- lib/Server.js | 19 ++++++++++++++++++- .../watch-files.test.js.snap.webpack5 | 4 ---- types/lib/Server.d.ts | 12 ++++-------- types/lib/getGlobMatchers.d.ts | 18 ++++++++++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 types/lib/getGlobMatchers.d.ts diff --git a/lib/Server.js b/lib/Server.js index cfe37127bd..b3138160f2 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -316,6 +316,19 @@ function useFn(route, fn) { * @property {typeof useFn} use */ +/** + * @template {Record} T + * @param {T} obj + * @returns {T} + */ +function removeUndefinedValues(obj) { + return /** @type {T} **/ ( + Object.fromEntries( + Object.entries(obj).filter(([, value]) => typeof value !== "undefined"), + ) + ); +} + /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -3269,7 +3282,11 @@ class Server { watchOptions.ignored = getIgnoreMatchers(watchOptions, ignoreFunction); - const watcher = chokidar.watch(watchPaths, watchOptions); + const watcher = chokidar.watch( + watchPaths, + // https://github.com/paulmillr/chokidar/issues/1394 + removeUndefinedValues(watchOptions), + ); // disabling refreshing on changing the content if (this.options.liveReload) { diff --git a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 index b76e626f1c..1eedc2924a 100644 --- a/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/watch-files.test.js.snap.webpack5 @@ -106,7 +106,6 @@ exports[`watchFiles option should work with options {"poll":true} should reload "ignoreInitial": true, "ignorePermissionErrors": true, "ignored": [], - "interval": undefined, "persistent": true, "usePolling": true, } @@ -178,7 +177,6 @@ exports[`watchFiles option should work with options {"usePolling":false,"poll":t "ignoreInitial": true, "ignorePermissionErrors": true, "ignored": [], - "interval": undefined, "persistent": true, "usePolling": false, } @@ -202,7 +200,6 @@ exports[`watchFiles option should work with options {"usePolling":false} should "ignoreInitial": true, "ignorePermissionErrors": true, "ignored": [], - "interval": undefined, "persistent": true, "usePolling": false, } @@ -274,7 +271,6 @@ exports[`watchFiles option should work with options {"usePolling":true} should r "ignoreInitial": true, "ignorePermissionErrors": true, "ignored": [], - "interval": undefined, "persistent": true, "usePolling": true, } diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index cb99b24f45..a66c0be75b 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1,8 +1,4 @@ export = Server; -/** - * @typedef {Object} BasicApplication - * @property {typeof useFn} use - */ /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -1402,6 +1398,7 @@ declare class Server< declare namespace Server { export { DEFAULT_STATS, + BasicApplication, Schema, Compiler, MultiCompiler, @@ -1469,12 +1466,14 @@ declare namespace Server { Middleware, BasicServer, Configuration, - BasicApplication, }; } declare class DEFAULT_STATS { private constructor(); } +type BasicApplication = { + use: typeof useFn; +}; type Schema = import("schema-utils/declarations/validate").Schema; type Compiler = import("webpack").Compiler; type MultiCompiler = import("webpack").MultiCompiler; @@ -1813,9 +1812,6 @@ type Configuration< | ((middlewares: Middleware[], devServer: Server) => Middleware[]) | undefined; }; -type BasicApplication = { - use: typeof useFn; -}; /** * @overload * @param {NextHandleFunction} fn diff --git a/types/lib/getGlobMatchers.d.ts b/types/lib/getGlobMatchers.d.ts new file mode 100644 index 0000000000..6229f0d124 --- /dev/null +++ b/types/lib/getGlobMatchers.d.ts @@ -0,0 +1,18 @@ +/** + * @param {string[] | string} _watchPaths + * @param {import("./Server").WatchOptions} watchOptions + * @returns {[string[], import("chokidar").MatchFunction | null]}*/ +export function getGlobbedWatcherPaths( + _watchPaths: string[] | string, + { disableGlobbing, cwd }: import("./Server").WatchOptions, +): [string[], import("chokidar").MatchFunction | null]; +/** + * + * @param {import("./Server").WatchOptions} watchOptions + * @param {import("chokidar").MatchFunction | null } ignoreFunction + * @returns {import("chokidar").Matcher[]} + */ +export function getIgnoreMatchers( + { disableGlobbing, ignored, cwd }: import("./Server").WatchOptions, + ignoreFunction: import("chokidar").MatchFunction | null, +): import("chokidar").Matcher[]; From 5d42bac4b309364f0b091762492bd519c798a92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 20 Dec 2024 20:36:21 +0100 Subject: [PATCH 10/10] chore(deps): update chokidar to latest --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76c8cb9149..9fd12fab6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", - "chokidar": "^4.0.1", + "chokidar": "^4.0.3", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", @@ -5696,9 +5696,9 @@ } }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dependencies": { "readdirp": "^4.0.1" }, diff --git a/package.json b/package.json index 287d55022b..587dae0ed2 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", - "chokidar": "^4.0.1", + "chokidar": "^4.0.3", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0",