diff --git a/package-lock.json b/package-lock.json index 9a48c6e2..8750f015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7299,6 +7299,15 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@swc/html": { "version": "1.15.30", "resolved": "https://registry.npmjs.org/@swc/html/-/html-1.15.30.tgz", @@ -9761,9 +9770,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -9920,9 +9929,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "funding": [ { "type": "opencollective", @@ -9939,11 +9948,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -10121,9 +10130,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "funding": [ { "type": "opencollective", @@ -12389,9 +12398,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "version": "1.5.468", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.468.tgz", + "integrity": "sha512-8XdEp3sIqO+PzmVPxhzqHq/3+yCxp44m27cm2WY1sRn3ZhwKyKmqSi2hx6gEhmnOXmh8NUuYE00z6Onzn5m6DA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -18130,10 +18139,13 @@ } }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "license": "MIT" + "version": "2.0.62", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.62.tgz", + "integrity": "sha512-A0Eq+qVDQI97IbAD6g/HriIuv256el9N8EwF4+xqJ49pwrdRYpdCSmLk0sO4I3fnS6hpYE5v22B73Bk+WArEoQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -23283,9 +23295,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "funding": [ { "type": "opencollective", @@ -24524,8 +24536,7 @@ "dependencies": { "@evjs/ev": "*", "@logtape/logtape": "^2.0.4", - "@utoo/pack": "^1.4.27", - "chokidar": "^3.6.0", + "@utoo/pack": "^1.5.36", "fast-glob": "^3.3.3", "less": "4.1.3", "less-loader": "12.3.2" @@ -24547,26 +24558,19 @@ "node": ">=6.9.0" } }, - "packages/bundler-utoopack/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "packages/bundler-utoopack/node_modules/@utoo/pack": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack/-/pack-1.4.27.tgz", - "integrity": "sha512-zSBdpSDZlI6IFxSuH06TsMhn/wg1H0x6V18LsCHzH+JcH2SiAkVlHT+7CX7btc5SO6fPML84Wcl49i3M2cy9FQ==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack/-/pack-1.5.36.tgz", + "integrity": "sha512-WuN0uqpnp/M5IyAzQFkBbSSCSzddlXdAfAo2UyNK906C0TjBFC5DL9VR9zFuiyMZTV3QI4PAMzjJ3u5bokyshQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "7.22.5", "@hono/node-server": "^1.19.11", "@hono/node-ws": "^1.3.0", - "@swc/helpers": "0.5.15", - "@utoo/pack-shared": "1.4.27", + "@jridgewell/trace-mapping": "^0.3.31", + "@swc/helpers": "0.5.23", + "@utoo/pack-shared": "1.5.36", + "browserslist": "^4.28.5", "domparser-rs": "^0.0.7", "find-up": "4.1.0", "get-port": "5.1.1", @@ -24574,20 +24578,19 @@ "nanoid": "^3.3.11", "picocolors": "^1.1.1", "semver": "^7.7.1", - "send": "0.17.1", "ws": "^8.18.1" }, "engines": { "node": ">= 20" }, "optionalDependencies": { - "@utoo/pack-darwin-arm64": "1.4.27", - "@utoo/pack-darwin-x64": "1.4.27", - "@utoo/pack-linux-arm64-gnu": "1.4.27", - "@utoo/pack-linux-arm64-musl": "1.4.27", - "@utoo/pack-linux-x64-gnu": "1.4.27", - "@utoo/pack-linux-x64-musl": "1.4.27", - "@utoo/pack-win32-x64-msvc": "1.4.27" + "@utoo/pack-darwin-arm64": "1.5.36", + "@utoo/pack-darwin-x64": "1.5.36", + "@utoo/pack-linux-arm64-gnu": "1.5.36", + "@utoo/pack-linux-arm64-musl": "1.5.36", + "@utoo/pack-linux-x64-gnu": "1.5.36", + "@utoo/pack-linux-x64-musl": "1.5.36", + "@utoo/pack-win32-x64-msvc": "1.5.36" }, "peerDependencies": { "less": "^4.0.0", @@ -24600,9 +24603,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-darwin-arm64": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-darwin-arm64/-/pack-darwin-arm64-1.4.27.tgz", - "integrity": "sha512-HpsjVFc9wBJcO48YZqxi0/DSz5jez/NsrzmTSwLpRniNLVvw+pUgPRb5dyHeMbfIQ4dJgNDMw/KwLfRzYhgRzA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-darwin-arm64/-/pack-darwin-arm64-1.5.36.tgz", + "integrity": "sha512-Be3OZ87rYzEFxF7H8WnaFleHQV/sXTtutnUXdV4lHXwlKNKcvikoy5tTqAGEwUvt+lf22YqKf/gYZX6G+/4X4Q==", "cpu": [ "arm64" ], @@ -24616,9 +24619,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-darwin-x64": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-darwin-x64/-/pack-darwin-x64-1.4.27.tgz", - "integrity": "sha512-E5AIxJzxXmNebmxLU+k8T88Zaa/xRY4c1K/i791bo/72kW+fBSgoE3o5Pt2wLA97pjWFTES9UhSIh2wcJQthkA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-darwin-x64/-/pack-darwin-x64-1.5.36.tgz", + "integrity": "sha512-WSCVqQajW0wasinAFHQCvXzC3XxtJE6UkJ5dxwQ/clh9OriEZz3yG2Qs1VbnPH/t/p9YCuHMb8MgT3y44zoCyA==", "cpu": [ "x64" ], @@ -24632,9 +24635,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-linux-arm64-gnu": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-linux-arm64-gnu/-/pack-linux-arm64-gnu-1.4.27.tgz", - "integrity": "sha512-Et6/G3jX1KX7HvnRd9QxCviHa9eZTQ9zLtRAAMd2MMt5GHx/2iE6SM99L8qF8RCISXv8rVw+FJpaKSt4y4WCeg==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-linux-arm64-gnu/-/pack-linux-arm64-gnu-1.5.36.tgz", + "integrity": "sha512-vMrS6zd8hh7L+UnKgbBGclMcYahlzf1bKcSOgZBqLrlnYQ6KLwRNq0X0th6ClGsPMSTwX05MwBINdOYLKDqj5Q==", "cpu": [ "arm64" ], @@ -24648,9 +24651,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-linux-arm64-musl": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-linux-arm64-musl/-/pack-linux-arm64-musl-1.4.27.tgz", - "integrity": "sha512-jMQtdFc7Wco6uBn9IW9Y54uiO5YR4Njw/jc5Ndd81eEE7guYv+GubHlJQSoie6Ogi9RP8G1Em0lqmwC4zl13vQ==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-linux-arm64-musl/-/pack-linux-arm64-musl-1.5.36.tgz", + "integrity": "sha512-s7J1CkmzdWD3D1DfojWTZKG8G79nh0OZxUlEs6bIIk5DoG2OkIa2eQ19HCYi/dXfcZR7yBPIYDT+DuYBmyzgtA==", "cpu": [ "arm64" ], @@ -24664,9 +24667,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-linux-x64-gnu": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-linux-x64-gnu/-/pack-linux-x64-gnu-1.4.27.tgz", - "integrity": "sha512-TgQQC7LMXRbB0qpFpa/ByKTCkaZZSl6y1qtgNE4WjuYL97Ug1/JrlHTqvvddnPsJCUwdBEbxdb5vAPK7JuQsag==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-linux-x64-gnu/-/pack-linux-x64-gnu-1.5.36.tgz", + "integrity": "sha512-LKc2BTDnn/IGqE7tTllfx7QGhLiSr+FMMmnLdM6qa1HBdfgmuE/v8qr6+Y9A1+dcfdkWf3EeLlN/NJykb/k05w==", "cpu": [ "x64" ], @@ -24680,9 +24683,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-linux-x64-musl": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-linux-x64-musl/-/pack-linux-x64-musl-1.4.27.tgz", - "integrity": "sha512-MC9zs3QMNJnquh3ByxS9pkijxbQCbwHfkPJUJR14Bisf8FNpC7HbwTAF5WU4KT6Y3yBMySfq7KagyfjlHwfP9w==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-linux-x64-musl/-/pack-linux-x64-musl-1.5.36.tgz", + "integrity": "sha512-9WntMbc9F1vokvuSfUcAoSlUnJXtYYulieWo1bxqaHjEyJxAwzd26DeRKKRoAV2jufbMPEmiwTMdQfoeaJnjZw==", "cpu": [ "x64" ], @@ -24696,9 +24699,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-shared": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-shared/-/pack-shared-1.4.27.tgz", - "integrity": "sha512-itGbKOJvWNXKEwOG3OfXQL1XHxLsk2QXVOy84VRFWaLcI8bmXSZDQe77sirzn5aGFRDNSHiGTtDOOH4adbP/kg==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-shared/-/pack-shared-1.5.36.tgz", + "integrity": "sha512-7ISpVi9JzTdwlejbx168j0CJ3ybgkObGWF9YZgRuschsM0C9LFR2AhZJQ5JMXkJD3H+Qjac2T/aEocJ0859g4g==", "license": "MIT", "dependencies": { "@babel/code-frame": "7.22.5", @@ -24709,9 +24712,9 @@ } }, "packages/bundler-utoopack/node_modules/@utoo/pack-win32-x64-msvc": { - "version": "1.4.27", - "resolved": "https://registry.npmjs.org/@utoo/pack-win32-x64-msvc/-/pack-win32-x64-msvc-1.4.27.tgz", - "integrity": "sha512-Z6/hE9xd/cXx2ttIIaZ16BLoKlEVQxvcnHEqUeobBtcZ6Ww846PJ8+fuDmd5xczqvj3iIqWl7YdLZ3G7AWyVpw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/@utoo/pack-win32-x64-msvc/-/pack-win32-x64-msvc-1.5.36.tgz", + "integrity": "sha512-z40ExNM1OvC5NR9JBnsIgNEBOBIrTWXcjg2rYE+Gb5kvBungyKmYgGl6TpMIeo8EQdiINgHewhW2VmTj8nJIIQ==", "cpu": [ "x64" ], @@ -24736,36 +24739,6 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "packages/bundler-utoopack/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "packages/bundler-utoopack/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "packages/bundler-utoopack/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "packages/bundler-utoopack/node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "license": "MIT" - }, "packages/bundler-utoopack/node_modules/domparser-darwin-arm64": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/domparser-darwin-arm64/-/domparser-darwin-arm64-0.0.7.tgz", @@ -24916,15 +24889,6 @@ "node": ">= 10" } }, - "packages/bundler-utoopack/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "packages/bundler-utoopack/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -24938,22 +24902,6 @@ "node": ">=8" } }, - "packages/bundler-utoopack/node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "packages/bundler-utoopack/node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -25018,24 +24966,6 @@ "node": ">=8" } }, - "packages/bundler-utoopack/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "license": "MIT" - }, - "packages/bundler-utoopack/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "packages/bundler-utoopack/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -25101,15 +25031,6 @@ "node": "^10 || ^12 || >=14" } }, - "packages/bundler-utoopack/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "packages/bundler-utoopack/node_modules/sass": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", @@ -25128,36 +25049,6 @@ "node": ">=12.0.0" } }, - "packages/bundler-utoopack/node_modules/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "packages/bundler-utoopack/node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "license": "ISC" - }, "packages/bundler-utoopack/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -25168,24 +25059,6 @@ "node": ">=0.10.0" } }, - "packages/bundler-utoopack/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "packages/bundler-utoopack/node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "packages/bundler-webpack": { "name": "@evjs/bundler-webpack", "version": "0.0.0", diff --git a/packages/bundler-utoopack/package.json b/packages/bundler-utoopack/package.json index 72de8fe8..d130b512 100644 --- a/packages/bundler-utoopack/package.json +++ b/packages/bundler-utoopack/package.json @@ -34,8 +34,7 @@ "dependencies": { "@evjs/ev": "*", "@logtape/logtape": "^2.0.4", - "@utoo/pack": "^1.4.27", - "chokidar": "^3.6.0", + "@utoo/pack": "^1.5.36", "fast-glob": "^3.3.3", "less": "4.1.3", "less-loader": "12.3.2" diff --git a/packages/bundler-utoopack/src/adapter/client-stats-watcher.ts b/packages/bundler-utoopack/src/adapter/client-stats-watcher.ts deleted file mode 100644 index edc1cf06..00000000 --- a/packages/bundler-utoopack/src/adapter/client-stats-watcher.ts +++ /dev/null @@ -1,85 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { watch } from "chokidar"; - -export async function readClientStatsVersion( - statsPath: string, -): Promise { - try { - const stats = await fs.promises.stat(statsPath, { bigint: true }); - return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}`; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; - throw error; - } -} - -export interface ClientStatsWatcher { - advance(version: string | undefined): void; - close(): Promise; -} - -/** Watch the directory so atomic replacement does not detach the listener. */ -export function watchClientStats(options: { - statsPath: string; - initialVersion: string | undefined; - onChange(): Promise; - onError(error: unknown): void; -}): ClientStatsWatcher { - const statsPath = path.resolve(options.statsPath); - let version = options.initialVersion; - let closed = false; - let scheduled = false; - let queue = Promise.resolve(); - let closePromise: Promise | undefined; - const watcher = watch(path.dirname(statsPath), { - atomic: true, - awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }, - depth: 0, - disableGlobbing: true, - ignoreInitial: true, - }); - - function schedule(): void { - if (closed || scheduled) return; - scheduled = true; - queue = queue.then(async () => { - scheduled = false; - if (closed) return; - try { - const nextVersion = await readClientStatsVersion(statsPath); - if (!nextVersion || nextVersion === version || closed) return; - // Only a successful publication advances the baseline. Incomplete - // stats can then recover on the next real write without losing it. - await options.onChange(); - } catch (error) { - if (!closed) options.onError(error); - } - }); - } - - const onFileChange = (file: string) => { - if (path.resolve(file) === statsPath) schedule(); - }; - watcher.on("add", onFileChange); - watcher.on("change", onFileChange); - watcher.on("error", (error) => { - if (!closed) options.onError(error); - }); - // Catch writes between the initial publication and watcher attachment. - watcher.on("ready", schedule); - - return { - advance(nextVersion) { - version = nextVersion; - }, - close() { - closePromise ??= (async () => { - closed = true; - await watcher.close(); - await queue; - })(); - return closePromise; - }, - }; -} diff --git a/packages/bundler-utoopack/src/adapter/index.ts b/packages/bundler-utoopack/src/adapter/index.ts index ac0ef4a7..8f28a1c8 100644 --- a/packages/bundler-utoopack/src/adapter/index.ts +++ b/packages/bundler-utoopack/src/adapter/index.ts @@ -9,7 +9,6 @@ import fs from "node:fs"; import { createRequire } from "node:module"; -import path from "node:path"; import type { BundlerAdapter, BundlerBuildContext, @@ -25,16 +24,19 @@ import { IncompleteClientStatsError, UtoopackManifestGenerator, } from "../manifest-generator.js"; -import { - type ClientStatsWatcher, - readClientStatsVersion, - watchClientStats, -} from "./client-stats-watcher.js"; import { getOutputPaths } from "./output-paths.js"; const logger = getLogger(["evjs", "bundler-utoopack"]); const require = createRequire(import.meta.url); type UtoopackRuntime = Pick; +type UtoopackServerOptions = NonNullable< + Parameters[3] +> & { + onClientBuild(context: { + clientPaths: string[]; + isRebuild: boolean; + }): Promise; +}; async function cleanServerOutput( cwd: string, @@ -48,29 +50,6 @@ async function cleanServerOutput( }); } -async function generateDevArtifacts( - config: ResolvedConfig, - cwd: string, - plan: BuildPlan, - onBuildFacts: ( - facts: BundlerBuildFacts, - options?: { isRebuild?: boolean }, - ) => void | Promise, - options: { isRebuild?: boolean } = {}, - facts?: BundlerBuildFacts, -): Promise { - const outputPaths = getOutputPaths(cwd, config.output, plan.distDir); - const clientStatsPath = path.join(outputPaths.clientDir, "stats.json"); - if (!fs.existsSync(clientStatsPath)) return false; - - logger.info`Generating development manifest and HTML...`; - const buildFacts = - facts ?? - (await new UtoopackManifestGenerator(cwd, plan).collectBuildFacts()); - await onBuildFacts(buildFacts, options); - return true; -} - function requireUtoopack(): UtoopackRuntime { // @utoo/pack's import condition targets ESM .js files; Node 18 parses them as CJS. return require("@utoo/pack") as UtoopackRuntime; @@ -112,12 +91,33 @@ export const utoopackAdapter: BundlerAdapter = { const { serve } = requireUtoopack(); let devServerOrigin = formatDevServerOrigin(config, config.dev.port); - await serve({ config: utoopackConfig }, undefined, undefined, { + const controller = new UtoopackDevController({ + cwd, + plan, + onBuildFacts: callbacks.onBuildFacts, + closeWatcher() { + serverReadyWatcher?.close(); + }, + }); + let clientBuildReceived = false; + const serverOptions: UtoopackServerOptions = { port: config.dev.port, https: config.dev.https !== false, hostname: "0.0.0.0", logServerInfo: false, + async onClientBuild({ isRebuild }) { + clientBuildReceived = true; + await controller.onClientBuild(isRebuild); + }, onReady({ port, hostname }) { + if ( + !clientBuildReceived && + plan.entries.some((entry) => entry.environment === "client") + ) { + throw new Error( + "[evjs] Utoopack dev requires @utoo/pack with onClientBuild support. Upgrade @utoo/pack before starting ev dev.", + ); + } devServerOrigin = formatDevServerOrigin(config, port, hostname); const fallbackUpdated = updateSpaHistoryFallbackTarget( utoopackConfig, @@ -130,20 +130,15 @@ export const utoopackAdapter: BundlerAdapter = { logger.warn`Reserved client port ${config.dev.port} became unavailable during startup; Utoopack is listening on ${port}.${fallbackStatus}`; } }, - }); - - const controller = new UtoopackDevController({ - config, - cwd, - plan, - onBuildFacts: callbacks.onBuildFacts, - closeWatcher() { - serverReadyWatcher?.close(); - }, - }); + }; try { - await controller.initialize(); + await serve( + { config: utoopackConfig }, + undefined, + undefined, + serverOptions, + ); await callbacks.onDevServerReady?.({ origin: devServerOrigin }); if (!hasRuntimeServerEntry(plan)) return controller; @@ -207,16 +202,12 @@ function formatDevServerOrigin( class UtoopackDevController implements BundlerDevController { private plan: BuildPlan; - private clientStatsWatcher: ClientStatsWatcher | undefined; - private publishedClientStatsVersion: string | undefined; - private hasPublishedArtifacts = false; private publicationQueue: Promise = Promise.resolve(); private closed = false; private closePromise: Promise | undefined; constructor( private options: { - config: ResolvedConfig; cwd: string; plan: BuildPlan; onBuildFacts: BundlerDevContext["callbacks"]["onBuildFacts"]; @@ -226,32 +217,14 @@ class UtoopackDevController implements BundlerDevController { this.plan = options.plan; } - private get clientStatsPath(): string { - return path.join( - getOutputPaths( - this.options.cwd, - this.options.config.output, - this.plan.distDir, - ).clientDir, - "stats.json", - ); - } - - async initialize(): Promise { - await this.enqueue(() => this.publishArtifacts(this.plan, false)); - if (this.closed) return; - this.clientStatsWatcher = watchClientStats({ - statsPath: this.clientStatsPath, - initialVersion: this.publishedClientStatsVersion, - onChange: () => - this.enqueue(async () => { - const version = await readClientStatsVersion(this.clientStatsPath); - if (!version || version === this.publishedClientStatsVersion) return; - await this.publishArtifacts(this.plan, this.hasPublishedArtifacts); - }), - onError(error) { - logger.warn`Failed to publish Utoopack client rebuild: ${error}`; - }, + async onClientBuild(isRebuild: boolean): Promise { + await this.enqueue(async () => { + const emitted = await this.publishArtifacts(this.plan, isRebuild); + if (!emitted && !this.closed) { + throw new Error( + "[evjs] Utoopack completed a client build without complete client stats.", + ); + } }); } @@ -259,7 +232,6 @@ class UtoopackDevController implements BundlerDevController { this.closePromise ??= (async () => { this.closed = true; this.options.closeWatcher?.(); - await this.clientStatsWatcher?.close(); await this.publicationQueue; })(); return this.closePromise; @@ -279,8 +251,6 @@ class UtoopackDevController implements BundlerDevController { isRebuild: boolean, ): Promise { if (this.closed) return false; - const versionBefore = await readClientStatsVersion(this.clientStatsPath); - if (!versionBefore) return false; let facts: BundlerBuildFacts; try { facts = await new UtoopackManifestGenerator( @@ -291,23 +261,10 @@ class UtoopackDevController implements BundlerDevController { if (error instanceof IncompleteClientStatsError) return false; throw error; } - const versionAfter = await readClientStatsVersion(this.clientStatsPath); - if (this.closed || versionBefore !== versionAfter) return false; - - const emitted = await generateDevArtifacts( - this.options.config, - this.options.cwd, - plan, - this.options.onBuildFacts, - { isRebuild }, - facts, - ); - if (emitted) { - this.hasPublishedArtifacts = true; - this.publishedClientStatsVersion = versionAfter; - this.clientStatsWatcher?.advance(versionAfter); - } - return emitted; + if (this.closed) return false; + logger.info`Generating development manifest and HTML...`; + await this.options.onBuildFacts(facts, { isRebuild }); + return true; } async updatePlan(update: BuildPlanUpdate): Promise { diff --git a/packages/bundler-utoopack/tests/adapter.test.ts b/packages/bundler-utoopack/tests/adapter.test.ts index 403540f8..b7f79866 100644 --- a/packages/bundler-utoopack/tests/adapter.test.ts +++ b/packages/bundler-utoopack/tests/adapter.test.ts @@ -29,7 +29,13 @@ import { utoopackAdapter } from "../src/adapter/index.js"; const utoopackMock = vi.hoisted(() => ({ initialClientAssets: undefined as string[] | undefined, - initialClientStats: undefined as string | undefined, + supportsClientBuild: true, + onClientBuild: undefined as + | ((context: { + clientPaths: string[]; + isRebuild: boolean; + }) => Promise) + | undefined, requireUtoopack: vi.fn(() => ({ serve: vi.fn(async ({ config }, _projectPath, _rootPath, serverOptions) => { const fs = await import("node:fs"); @@ -46,15 +52,14 @@ const utoopackMock = vi.hoisted(() => ({ } await fs.promises.writeFile( path.join(clientOutDir, "stats.json"), - utoopackMock.initialClientStats ?? - JSON.stringify({ - entrypoints: Object.fromEntries( - config.entry.map((entry: { name: string }) => [ - entry.name, - { assets: clientAssets.map((name) => ({ name })) }, - ]), - ), - }), + JSON.stringify({ + entrypoints: Object.fromEntries( + config.entry.map((entry: { name: string }) => [ + entry.name, + { assets: clientAssets.map((name) => ({ name })) }, + ]), + ), + }), ); if (config.server) { @@ -72,6 +77,13 @@ const utoopackMock = vi.hoisted(() => ({ }), ); } + if (utoopackMock.supportsClientBuild) { + utoopackMock.onClientBuild = serverOptions.onClientBuild; + await utoopackMock.onClientBuild?.({ + clientPaths: clientAssets, + isRebuild: false, + }); + } await serverOptions?.onReady?.({ port: 3210, hostname: "0.0.0.0", @@ -120,7 +132,8 @@ async function makeProject() { afterEach(async () => { utoopackMock.initialClientAssets = undefined; - utoopackMock.initialClientStats = undefined; + utoopackMock.supportsClientBuild = true; + utoopackMock.onClientBuild = undefined; await Promise.all( tempDirs.splice(0).map((dir) => fs.promises.rm(dir, { @@ -149,6 +162,7 @@ async function writeClientStats( }, }), ); + await utoopackMock.onClientBuild?.({ clientPaths: assets, isRebuild: true }); } function createFrameworkCallbacks(options: { @@ -307,55 +321,25 @@ async function expectRejectedMessage(action: () => void | Promise) { } describe("utoopackAdapter dev", () => { - it.each([ - ["truncated JSON", '{"entrypoints":'], - ["empty entrypoints", JSON.stringify({ entrypoints: {} })], - ])("waits for complete initial client stats after %s", async (_name, stats) => { + it("rejects Utoopack versions without client build callbacks", async () => { const cwd = await makeProject(); - utoopackMock.initialClientStats = stats; + utoopackMock.supportsClientBuild = false; const config = resolveConfig({ html: "./index.html" }); const buildContext = await createBuildContext(config, cwd); - const onBuildOutput = vi.fn(); - const rebuildFlags: boolean[] = []; - const framework = createFrameworkCallbacks({ - config, - cwd, - ...buildContext, - onBuildOutput, - }); - const controller = await utoopackAdapter.dev({ - config, - cwd, - ...buildContext, - callbacks: { - ...framework, - async onBuildFacts(facts, options) { - rebuildFlags.push(options?.isRebuild ?? false); - await framework.onBuildFacts(facts); + const onBuildFacts = vi.fn(); + await expect( + utoopackAdapter.dev({ + config, + cwd, + ...buildContext, + callbacks: { + onBuildFacts, + onServerBundleReady: vi.fn(), }, - }, - hooks: [], - }); - - try { - const htmlPath = path.join(cwd, "dist/client/index.html"); - expect(fs.existsSync(htmlPath)).toBe(false); - expect(onBuildOutput).not.toHaveBeenCalled(); - - await writeClientStats(cwd, ["root-initial.js", "main.js"]); - await expect - .poll(() => fs.promises.readFile(htmlPath, "utf-8")) - .toContain('src="/root-initial.js"'); - expect(rebuildFlags).toEqual([false]); - - await writeClientStats(cwd, ["root-rebuilt.js", "main.js"]); - await expect - .poll(() => fs.promises.readFile(htmlPath, "utf-8")) - .toContain('src="/root-rebuilt.js"'); - expect(rebuildFlags).toEqual([false, true]); - } finally { - await controller?.close?.(); - } + hooks: [], + }), + ).rejects.toThrow("requires @utoo/pack with onClientBuild support"); + expect(onBuildFacts).not.toHaveBeenCalled(); }); it("rejects initialization when publishing initial client artifacts fails", async () => { @@ -415,10 +399,6 @@ describe("utoopackAdapter dev", () => { ); await writeClientStats(cwd, ["root-new.js", "main.js", "new.css"]); - await expect - .poll(() => fs.promises.readFile(htmlPath, "utf-8")) - .toContain('src="/root-new.js"'); - const html = await fs.promises.readFile(htmlPath, "utf-8"); const manifest = JSON.parse( await fs.promises.readFile( @@ -450,7 +430,7 @@ describe("utoopackAdapter dev", () => { } }); - it("keeps published HTML while client stats are incomplete and recovers", async () => { + it("publishes only completed client builds and recovers after a publication error", async () => { const cwd = await makeProject(); const config = resolveConfig({ html: "./index.html" }); const buildContext = await createBuildContext(config, cwd); @@ -479,16 +459,21 @@ describe("utoopackAdapter dev", () => { path.join(cwd, "dist/client/stats.json"), incompleteStats, ); - await new Promise((resolve) => setTimeout(resolve, 200)); expect(await fs.promises.readFile(htmlPath, "utf-8")).toBe(initialHtml); expect(onBuildOutput).toHaveBeenCalledTimes(1); } + onBuildOutput.mockRejectedValueOnce(new Error("publish failed")); + await expect( + writeClientStats(cwd, ["root-failed.js", "main.js"]), + ).rejects.toThrow("publish failed"); + expect(await fs.promises.readFile(htmlPath, "utf-8")).toBe(initialHtml); + await writeClientStats(cwd, ["root-recovered.js", "main.js"]); - await expect - .poll(() => fs.promises.readFile(htmlPath, "utf-8")) - .toContain('src="/root-recovered.js"'); - expect(onBuildOutput).toHaveBeenCalledTimes(2); + expect(await fs.promises.readFile(htmlPath, "utf-8")).toContain( + 'src="/root-recovered.js"', + ); + expect(onBuildOutput).toHaveBeenCalledTimes(3); } finally { await controller?.close?.(); } @@ -517,7 +502,6 @@ describe("utoopackAdapter dev", () => { const initialHtml = await fs.promises.readFile(htmlPath, "utf-8"); await controller?.close?.(); await writeClientStats(cwd, ["root-after-close.js", "main.js"]); - await new Promise((resolve) => setTimeout(resolve, 200)); expect(await fs.promises.readFile(htmlPath, "utf-8")).toBe(initialHtml); expect(onBuildOutput).toHaveBeenCalledTimes(1); @@ -536,7 +520,12 @@ describe("utoopackAdapter dev", () => { const onBuildOutput = vi.fn((output: BuildOutput) => { output.assets.devHook = { js: ["dev-hook.js"], css: [] }; }); - const onDevServerReady = vi.fn(); + const onDevServerReady = vi.fn(async () => { + expect( + await fs.promises.readFile(path.join(cwd, "dist/index.html"), "utf-8"), + ).toContain('src="/main.js"'); + expect(onBuildOutput).toHaveBeenCalledTimes(1); + }); const buildContext = await createBuildContext(config, cwd); const hooks: PluginHooks[] = [ { @@ -736,11 +725,9 @@ describe("utoopackAdapter dev", () => { "dist", "home", ); - await expect - .poll(() => - fs.promises.readFile(path.join(cwd, "dist/home.html"), "utf-8"), - ) - .toContain('src="/root-next-plan.js"'); + expect( + await fs.promises.readFile(path.join(cwd, "dist/home.html"), "utf-8"), + ).toContain('src="/root-next-plan.js"'); expect( await fs.promises.readFile(path.join(cwd, "dist/home.html"), "utf-8"), ).toContain("next-shell"); diff --git a/packages/bundler-utoopack/tests/client-stats-watcher.test.ts b/packages/bundler-utoopack/tests/client-stats-watcher.test.ts deleted file mode 100644 index dce4228c..00000000 --- a/packages/bundler-utoopack/tests/client-stats-watcher.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { setTimeout as delay } from "node:timers/promises"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - type ClientStatsWatcher, - readClientStatsVersion, - watchClientStats, -} from "../src/adapter/client-stats-watcher.js"; - -const directories: string[] = []; -const watchers: ClientStatsWatcher[] = []; -const releasePending: Array<() => void> = []; - -function deferred() { - let resolve!: () => void; - const promise = new Promise((done) => { - resolve = done; - }); - releasePending.push(resolve); - return { promise, resolve }; -} - -function statsFor(asset: string): string { - return JSON.stringify({ - entrypoints: { main: { assets: [`${asset}.js`] } }, - }); -} - -async function createStats(asset = "initial") { - const directory = await fs.mkdtemp(path.join(os.tmpdir(), "evjs-stats-")); - directories.push(directory); - const statsPath = path.join(directory, "stats.json"); - await fs.writeFile(statsPath, statsFor(asset)); - return statsPath; -} - -function observe(options: Parameters[0]) { - const watcher = watchClientStats(options); - watchers.push(watcher); - return watcher; -} - -async function waitForPublication(assertion: () => void) { - await vi.waitFor(assertion, { timeout: 5_000, interval: 10 }); -} - -afterEach(async () => { - for (const release of releasePending.splice(0)) release(); - await Promise.all(watchers.splice(0).map((watcher) => watcher.close())); - await Promise.all( - directories - .splice(0) - .map((directory) => fs.rm(directory, { recursive: true, force: true })), - ); -}); - -describe("client stats watcher", () => { - it("publishes a change made before the watcher attaches", async () => { - const statsPath = await createStats(); - const initialVersion = await readClientStatsVersion(statsPath); - await fs.writeFile(statsPath, statsFor("changed-before-attachment")); - const published: string[] = []; - const onError = vi.fn(); - const watcher = observe({ - statsPath, - initialVersion, - async onChange() { - const contents = await fs.readFile(statsPath, "utf8"); - watcher.advance(await readClientStatsVersion(statsPath)); - published.push(contents); - }, - onError, - }); - - await waitForPublication(() => { - expect(published).toEqual([statsFor("changed-before-attachment")]); - }); - expect(onError).not.toHaveBeenCalled(); - }); - - it("keeps observing stats after an atomic file replacement", async () => { - const statsPath = await createStats(); - const published: string[] = []; - const onError = vi.fn(); - const watcher = observe({ - statsPath, - initialVersion: undefined, - async onChange() { - const contents = await fs.readFile(statsPath, "utf8"); - watcher.advance(await readClientStatsVersion(statsPath)); - published.push(contents); - }, - onError, - }); - await waitForPublication(() => { - expect(published).toEqual([statsFor("initial")]); - }); - - const replacement = path.join(path.dirname(statsPath), "stats.next"); - await fs.writeFile(replacement, statsFor("atomic-replacement")); - await fs.rename(replacement, statsPath); - await waitForPublication(() => { - expect(published).toEqual([ - statsFor("initial"), - statsFor("atomic-replacement"), - ]); - }); - - await fs.writeFile(statsPath, statsFor("written-after-replacement")); - await waitForPublication(() => { - expect(published).toEqual([ - statsFor("initial"), - statsFor("atomic-replacement"), - statsFor("written-after-replacement"), - ]); - }); - expect(onError).not.toHaveBeenCalled(); - }); - - it("preserves a newer acknowledged version while publication is pending", async () => { - const statsPath = await createStats(); - const firstPublication = deferred(); - const releasePublication = deferred(); - const published: string[] = []; - const onError = vi.fn(); - const watcher = observe({ - statsPath, - initialVersion: undefined, - async onChange() { - published.push(await fs.readFile(statsPath, "utf8")); - if (published.length === 1) { - firstPublication.resolve(); - await releasePublication.promise; - } else { - watcher.advance(await readClientStatsVersion(statsPath)); - } - }, - onError, - }); - await firstPublication.promise; - - await fs.writeFile(statsPath, statsFor("already-published-newer-version")); - watcher.advance(await readClientStatsVersion(statsPath)); - // Allow the real filesystem event to arrive while the first publication - // remains blocked, then let the queued observation inspect its baseline. - await delay(200); - releasePublication.resolve(); - await delay(200); - expect(published).toEqual([statsFor("initial")]); - - await fs.writeFile(statsPath, statsFor("next-unpublished-version")); - await waitForPublication(() => { - expect(published).toEqual([ - statsFor("initial"), - statsFor("next-unpublished-version"), - ]); - }); - expect(onError).not.toHaveBeenCalled(); - }); - - it("closes idempotently after an in-flight publication finishes", async () => { - const statsPath = await createStats(); - const publicationStarted = deferred(); - const releasePublication = deferred(); - const onChange = vi.fn(async () => { - publicationStarted.resolve(); - await releasePublication.promise; - }); - const onError = vi.fn(); - const watcher = observe({ - statsPath, - initialVersion: undefined, - onChange, - onError, - }); - await publicationStarted.promise; - - const closing = watcher.close(); - expect(watcher.close()).toBe(closing); - let closed = false; - void closing.then(() => { - closed = true; - }); - await fs.writeFile(statsPath, statsFor("changed-during-close")); - expect(closed).toBe(false); - - releasePublication.resolve(); - await closing; - expect(closed).toBe(true); - expect(watcher.close()).toBe(closing); - await fs.writeFile(statsPath, statsFor("changed-after-close")); - await delay(100); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onError).not.toHaveBeenCalled(); - }); -});