diff --git a/package-lock.json b/package-lock.json
index da05287a1e7..1e6b070997e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3346,6 +3346,319 @@
"@noble/hashes": "^1.1.5"
}
},
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+ "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.6",
+ "@parcel/watcher-darwin-arm64": "2.5.6",
+ "@parcel/watcher-darwin-x64": "2.5.6",
+ "@parcel/watcher-freebsd-x64": "2.5.6",
+ "@parcel/watcher-linux-arm-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm-musl": "2.5.6",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm64-musl": "2.5.6",
+ "@parcel/watcher-linux-x64-glibc": "2.5.6",
+ "@parcel/watcher-linux-x64-musl": "2.5.6",
+ "@parcel/watcher-win32-arm64": "2.5.6",
+ "@parcel/watcher-win32-ia32": "2.5.6",
+ "@parcel/watcher-win32-x64": "2.5.6"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+ "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+ "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+ "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+ "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+ "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+ "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+ "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+ "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+ "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+ "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+ "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+ "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+ "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -5049,31 +5362,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/append-transform": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
@@ -5526,18 +5814,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@@ -6140,42 +6416,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "license": "MIT",
- "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/chokidar/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==",
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -10297,18 +10537,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-boolean-object": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
@@ -12614,6 +12842,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -12769,15 +13003,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/npm-bundled": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz",
@@ -14807,30 +15032,6 @@
"node": ">= 6"
}
},
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/readdirp/node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/readline-transform": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/readline-transform/-/readline-transform-1.0.0.tgz",
@@ -18440,12 +18641,12 @@
"license": "Apache-2.0",
"dependencies": {
"@npmcli/config": "^10.4.0",
+ "@parcel/watcher": "^2.5.6",
"@ui5/fs": "^5.0.0-alpha.4",
"@ui5/logger": "^5.0.0-alpha.4",
"ajv": "^8.18.0",
"ajv-errors": "^3.0.0",
"chalk": "^5.6.2",
- "chokidar": "^3.6.0",
"escape-string-regexp": "^5.0.0",
"globby": "^14.1.0",
"graceful-fs": "^4.2.11",
diff --git a/packages/project/lib/build/BuildServer.js b/packages/project/lib/build/BuildServer.js
index abe827dd41e..1200a177c19 100644
--- a/packages/project/lib/build/BuildServer.js
+++ b/packages/project/lib/build/BuildServer.js
@@ -115,8 +115,8 @@ class BuildServer extends EventEmitter {
*
* Awaiting watcher readiness avoids a race where source changes made immediately after
* graph.serve() resolves would be missed. The race is most pronounced on
- * Windows, where chokidar's ReadDirectoryChangesW backend has noticeably
- * higher startup latency than inotify/FSEvents.
+ * Windows, where the ReadDirectoryChangesW backend has noticeably
+ * higher startup latency than FSEvents/inotify.
*
* @public
* @param {@ui5/project/graph/ProjectGraph} graph Project graph containing all projects
@@ -146,7 +146,7 @@ class BuildServer extends EventEmitter {
});
watchHandler.on("change", (eventType, resourcePath, project) => {
log.verbose(`Source change detected: ${eventType} ${resourcePath} in project '${project.getName()}'`);
- this._projectResourceChanged(project, resourcePath, ["add", "unlink", "unlinkDir"].includes(eventType));
+ this._projectResourceChanged(project, resourcePath, ["create", "delete"].includes(eventType));
});
await watchHandler.watch(this.#graph.getProjects());
}
diff --git a/packages/project/lib/build/helpers/WatchHandler.js b/packages/project/lib/build/helpers/WatchHandler.js
index 08d5d6d2ffd..7ec3eebb89e 100644
--- a/packages/project/lib/build/helpers/WatchHandler.js
+++ b/packages/project/lib/build/helpers/WatchHandler.js
@@ -1,5 +1,5 @@
import EventEmitter from "node:events";
-import chokidar from "chokidar";
+import parcelWatcher from "@parcel/watcher";
import {getLogger} from "@ui5/logger";
const log = getLogger("build:helpers:WatchHandler");
@@ -10,7 +10,7 @@ const log = getLogger("build:helpers:WatchHandler");
* @memberof @ui5/project/build/helpers
*/
class WatchHandler extends EventEmitter {
- #closeCallbacks = [];
+ #subscriptions = [];
constructor() {
super();
@@ -25,51 +25,47 @@ class WatchHandler extends EventEmitter {
}
async _watchProject(project) {
- let ready = false;
const paths = project.getSourcePaths();
- log.verbose(`Watching source paths: ${paths.join(", ")}`);
+ log.verbose(`Watching source path(s): ${paths.join(", ")}`);
- const watcher = chokidar.watch(paths, {
- ignoreInitial: true,
- });
- this.#closeCallbacks.push(async () => {
- await watcher.close();
- });
- watcher.on("all", (event, filePath) => {
- if (!ready) {
- // Ignore events before ready
- return;
- }
- if (event === "addDir") {
- // Ignore directory creation events
- return;
- }
- this.#handleWatchEvents(event, filePath, project).catch((err) => {
- this.emit("error", err);
+ await Promise.all(paths.map(async (path) => {
+ const subscription = await parcelWatcher.subscribe(path, (err, events) => {
+ if (err) {
+ this.emit("error", err);
+ return;
+ }
+ for (const event of events) {
+ try {
+ this.#handleWatchEvents(event.type, event.path, project);
+ } catch (handlerErr) {
+ this.emit("error", handlerErr);
+ }
+ }
});
- });
- const {promise, resolve} = Promise.withResolvers();
-
- watcher.on("ready", () => {
- ready = true;
- resolve();
- });
- watcher.on("error", (err) => {
- this.emit("error", err);
- });
-
- return promise;
+ this.#subscriptions.push(subscription);
+ }));
}
async destroy() {
- for (const cb of this.#closeCallbacks) {
- await cb();
+ // Drain the subscriptions list so a second destroy() is a no-op and a partial
+ // failure cannot leave stale handles behind to be unsubscribed twice.
+ const subscriptions = this.#subscriptions;
+ this.#subscriptions = [];
+ // Run in parallel and collect failures so a single misbehaving subscription
+ // cannot leak the others.
+ const results = await Promise.allSettled(subscriptions.map((s) => s.unsubscribe()));
+ const failures = results.filter((r) => r.status === "rejected").map((r) => r.reason);
+ if (failures.length) {
+ const err = new AggregateError(failures, "Failed to unsubscribe one or more file watchers");
+ this.emit("error", err);
}
}
- async #handleWatchEvents(eventType, filePath, project) {
+ #handleWatchEvents(eventType, filePath, project) {
const resourcePath = project.getVirtualPath(filePath);
- log.verbose(`File changed: ${eventType} ${filePath} (as ${resourcePath} in project '${project.getName()}')`);
+ if (log.isLevelEnabled("silly")) {
+ log.silly(`FS event: ${eventType} ${filePath} (as ${resourcePath} in project '${project.getName()}')`);
+ }
this.emit("change", eventType, resourcePath, project);
}
}
diff --git a/packages/project/package.json b/packages/project/package.json
index d6fb584b4d6..87c9e155502 100644
--- a/packages/project/package.json
+++ b/packages/project/package.json
@@ -58,12 +58,12 @@
},
"dependencies": {
"@npmcli/config": "^10.4.0",
+ "@parcel/watcher": "^2.5.6",
"@ui5/fs": "^5.0.0-alpha.4",
"@ui5/logger": "^5.0.0-alpha.4",
"ajv": "^8.18.0",
"ajv-errors": "^3.0.0",
"chalk": "^5.6.2",
- "chokidar": "^3.6.0",
"escape-string-regexp": "^5.0.0",
"globby": "^14.1.0",
"graceful-fs": "^4.2.11",
diff --git a/packages/project/test/lib/build/helpers/WatchHandler.js b/packages/project/test/lib/build/helpers/WatchHandler.js
index a7bbba1cbc6..a7c32ccebf1 100644
--- a/packages/project/test/lib/build/helpers/WatchHandler.js
+++ b/packages/project/test/lib/build/helpers/WatchHandler.js
@@ -3,14 +3,14 @@ import sinon from "sinon";
import esmock from "esmock";
let WatchHandler;
-let chokidarWatchStub;
+let subscribeStub;
test.before(async () => {
- chokidarWatchStub = sinon.stub();
+ subscribeStub = sinon.stub();
WatchHandler = await esmock("../../../../lib/build/helpers/WatchHandler.js", {
- "chokidar": {
+ "@parcel/watcher": {
default: {
- watch: chokidarWatchStub
+ subscribe: subscribeStub
}
}
});
@@ -18,24 +18,30 @@ test.before(async () => {
test.afterEach.always(() => {
sinon.restore();
- chokidarWatchStub.reset();
+ subscribeStub.reset();
});
-function createMockWatcher() {
- const callbacks = {};
- const watcher = {
- on: sinon.stub().callsFake((event, cb) => {
- callbacks[event] = cb;
- return watcher;
- }),
- close: sinon.stub().resolves()
+function createMockSubscription() {
+ return {
+ unsubscribe: sinon.stub().resolves()
};
- return {watcher, callbacks};
}
-test.serial("watch: emits change events for file changes", async (t) => {
- const {watcher, callbacks} = createMockWatcher();
- chokidarWatchStub.returns(watcher);
+function captureCallback(subscription) {
+ let resolveCb;
+ const callbackReady = new Promise((resolve) => {
+ resolveCb = resolve;
+ });
+ subscribeStub.callsFake(async (path, cb) => {
+ resolveCb(cb);
+ return subscription;
+ });
+ return callbackReady;
+}
+
+test.serial("watch: emits change events for file updates", async (t) => {
+ const subscription = createMockSubscription();
+ const callbackReady = captureCallback(subscription);
const handler = new WatchHandler();
const project = {
@@ -44,28 +50,27 @@ test.serial("watch: emits change events for file changes", async (t) => {
getName: () => "test-project"
};
- const watchPromise = handler.watch([project]);
- callbacks.ready();
- await watchPromise;
+ await handler.watch([project]);
+ const callback = await callbackReady;
const changePromise = new Promise((resolve) => {
handler.on("change", (eventType, resourcePath, proj) => {
- t.is(eventType, "change");
+ t.is(eventType, "update");
t.is(resourcePath, "/resources/file.js");
t.is(proj, project);
resolve();
});
});
- callbacks.all("change", "/src/file.js");
+ callback(null, [{type: "update", path: "/src/file.js"}]);
await changePromise;
await handler.destroy();
- t.true(watcher.close.calledOnce);
+ t.true(subscription.unsubscribe.calledOnce);
});
-test.serial("watch: ignores events before ready", async (t) => {
- const {watcher, callbacks} = createMockWatcher();
- chokidarWatchStub.returns(watcher);
+test.serial("watch: emits create events for new files", async (t) => {
+ const subscription = createMockSubscription();
+ const callbackReady = captureCallback(subscription);
const handler = new WatchHandler();
const project = {
@@ -74,21 +79,25 @@ test.serial("watch: ignores events before ready", async (t) => {
getName: () => "test-project"
};
- const changeSpy = sinon.spy();
- handler.on("change", changeSpy);
+ await handler.watch([project]);
+ const callback = await callbackReady;
- const watchPromise = handler.watch([project]);
- callbacks.all("change", "/src/file.js");
- callbacks.ready();
- await watchPromise;
+ const changePromise = new Promise((resolve) => {
+ handler.on("change", (eventType, resourcePath) => {
+ t.is(eventType, "create");
+ t.is(resourcePath, "/resources/file.js");
+ resolve();
+ });
+ });
+ callback(null, [{type: "create", path: "/src/file.js"}]);
+ await changePromise;
- t.is(changeSpy.callCount, 0, "No change emitted before ready");
await handler.destroy();
});
-test.serial("watch: ignores addDir events", async (t) => {
- const {watcher, callbacks} = createMockWatcher();
- chokidarWatchStub.returns(watcher);
+test.serial("watch: emits delete events", async (t) => {
+ const subscription = createMockSubscription();
+ const callbackReady = captureCallback(subscription);
const handler = new WatchHandler();
const project = {
@@ -97,22 +106,25 @@ test.serial("watch: ignores addDir events", async (t) => {
getName: () => "test-project"
};
- const changeSpy = sinon.spy();
- handler.on("change", changeSpy);
+ await handler.watch([project]);
+ const callback = await callbackReady;
- const watchPromise = handler.watch([project]);
- callbacks.ready();
- await watchPromise;
+ const changePromise = new Promise((resolve) => {
+ handler.on("change", (eventType, resourcePath) => {
+ t.is(eventType, "delete");
+ t.is(resourcePath, "/resources/gone.js");
+ resolve();
+ });
+ });
+ callback(null, [{type: "delete", path: "/src/gone.js"}]);
+ await changePromise;
- callbacks.all("addDir", "/src/newdir");
- await new Promise((resolve) => setTimeout(resolve, 10));
- t.is(changeSpy.callCount, 0, "No change emitted for addDir");
await handler.destroy();
});
-test.serial("watch: emits error from watcher error event", async (t) => {
- const {watcher, callbacks} = createMockWatcher();
- chokidarWatchStub.returns(watcher);
+test.serial("watch: emits error from watcher callback error", async (t) => {
+ const subscription = createMockSubscription();
+ const callbackReady = captureCallback(subscription);
const handler = new WatchHandler();
const project = {
@@ -120,9 +132,8 @@ test.serial("watch: emits error from watcher error event", async (t) => {
getName: () => "test-project"
};
- const watchPromise = handler.watch([project]);
- callbacks.ready();
- await watchPromise;
+ await handler.watch([project]);
+ const callback = await callbackReady;
const errorPromise = new Promise((resolve) => {
handler.on("error", (err) => {
@@ -130,14 +141,14 @@ test.serial("watch: emits error from watcher error event", async (t) => {
resolve();
});
});
- callbacks.error(new Error("watcher error"));
+ callback(new Error("watcher error"));
await errorPromise;
await handler.destroy();
});
test.serial("watch: emits error when handler throws", async (t) => {
- const {watcher, callbacks} = createMockWatcher();
- chokidarWatchStub.returns(watcher);
+ const subscription = createMockSubscription();
+ const callbackReady = captureCallback(subscription);
const handler = new WatchHandler();
const project = {
@@ -148,9 +159,8 @@ test.serial("watch: emits error when handler throws", async (t) => {
getName: () => "test-project"
};
- const watchPromise = handler.watch([project]);
- callbacks.ready();
- await watchPromise;
+ await handler.watch([project]);
+ const callback = await callbackReady;
const errorPromise = new Promise((resolve) => {
handler.on("error", (err) => {
@@ -158,7 +168,113 @@ test.serial("watch: emits error when handler throws", async (t) => {
resolve();
});
});
- callbacks.all("change", "/src/file.js");
+ callback(null, [{type: "update", path: "/src/file.js"}]);
await errorPromise;
await handler.destroy();
});
+
+test.serial("watch: subscribes to each source path", async (t) => {
+ const subA = createMockSubscription();
+ const subB = createMockSubscription();
+ subscribeStub.onFirstCall().resolves(subA);
+ subscribeStub.onSecondCall().resolves(subB);
+
+ const handler = new WatchHandler();
+ const project = {
+ getSourcePaths: () => ["/src", "/test"],
+ getVirtualPath: (filePath) => filePath,
+ getName: () => "test-project"
+ };
+
+ await handler.watch([project]);
+
+ t.is(subscribeStub.callCount, 2, "subscribed once per source path");
+ t.is(subscribeStub.firstCall.args[0], "/src");
+ t.is(subscribeStub.secondCall.args[0], "/test");
+
+ await handler.destroy();
+ t.true(subA.unsubscribe.calledOnce);
+ t.true(subB.unsubscribe.calledOnce);
+});
+
+test.serial("destroy: unsubscribes subscriptions in parallel", async (t) => {
+ const subA = createMockSubscription();
+ const subB = createMockSubscription();
+ // Make subA's unsubscribe block until subB's has at least started.
+ // If destroy() runs sequentially, subB.unsubscribe is never called while
+ // subA is still pending and the test deadlocks (caught by AVA timeout).
+ let resolveA;
+ subA.unsubscribe = sinon.stub().returns(new Promise((resolve) => {
+ resolveA = resolve;
+ }));
+ subB.unsubscribe = sinon.stub().callsFake(async () => {
+ resolveA();
+ });
+ subscribeStub.onFirstCall().resolves(subA);
+ subscribeStub.onSecondCall().resolves(subB);
+
+ const handler = new WatchHandler();
+ const project = {
+ getSourcePaths: () => ["/src", "/test"],
+ getVirtualPath: (filePath) => filePath,
+ getName: () => "test-project"
+ };
+
+ await handler.watch([project]);
+ await handler.destroy();
+
+ t.true(subA.unsubscribe.calledOnce);
+ t.true(subB.unsubscribe.calledOnce);
+});
+
+test.serial("destroy: continues unsubscribing when one subscription rejects", async (t) => {
+ const subA = createMockSubscription();
+ const subB = createMockSubscription();
+ const subC = createMockSubscription();
+ subA.unsubscribe = sinon.stub().rejects(new Error("unsub A failed"));
+ subB.unsubscribe = sinon.stub().resolves();
+ subC.unsubscribe = sinon.stub().rejects(new Error("unsub C failed"));
+ subscribeStub.onCall(0).resolves(subA);
+ subscribeStub.onCall(1).resolves(subB);
+ subscribeStub.onCall(2).resolves(subC);
+
+ const handler = new WatchHandler();
+ const project = {
+ getSourcePaths: () => ["/a", "/b", "/c"],
+ getVirtualPath: (filePath) => filePath,
+ getName: () => "test-project"
+ };
+
+ const errorSpy = sinon.spy();
+ handler.on("error", errorSpy);
+
+ await handler.watch([project]);
+ await handler.destroy();
+
+ t.true(subA.unsubscribe.calledOnce, "subA was attempted");
+ t.true(subB.unsubscribe.calledOnce, "subB still ran despite subA failing");
+ t.true(subC.unsubscribe.calledOnce, "subC still ran despite subA failing");
+ t.is(errorSpy.callCount, 1, "single aggregated error emitted");
+ const aggErr = errorSpy.firstCall.args[0];
+ t.true(aggErr instanceof AggregateError, "emits AggregateError");
+ t.is(aggErr.errors.length, 2, "both failures collected");
+ t.deepEqual(aggErr.errors.map((e) => e.message).sort(), ["unsub A failed", "unsub C failed"]);
+});
+
+test.serial("destroy: is idempotent", async (t) => {
+ const subscription = createMockSubscription();
+ captureCallback(subscription);
+
+ const handler = new WatchHandler();
+ const project = {
+ getSourcePaths: () => ["/src"],
+ getVirtualPath: (filePath) => filePath,
+ getName: () => "test-project"
+ };
+
+ await handler.watch([project]);
+ await handler.destroy();
+ await handler.destroy();
+
+ t.is(subscription.unsubscribe.callCount, 1, "unsubscribe not invoked on second destroy");
+});