diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26a77e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +www/ +dist/ +dist-electron/ +android/ +ios/ +.vite/ +.vitest/ +coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..16c7df5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog — Gestion table tournante Libre + +## 2026.10.05.01 — 5 octobre 2026 + +**Ce qui change pour vous** +- L'application s'ouvre et affiche sa version dans un bandeau permanent et dans + le titre de sa fenêtre. + +**Version de format des fichiers** : aucune — cette version n'enregistre encore +aucun événement. + +**En remplaçant** : rien à faire, c'est la première version. diff --git a/capacitor.config.json b/capacitor.config.json new file mode 100644 index 0000000..e08428a --- /dev/null +++ b/capacitor.config.json @@ -0,0 +1,5 @@ +{ + "appId": "ca.technolibre.gestiontabletournante", + "appName": "Gestion table tournante Libre", + "webDir": "www" +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..4b15a74 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..49380b7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2835 @@ +{ + "name": "gestion-table-tournante-libre", + "version": "2026.1005.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gestion-table-tournante-libre", + "version": "2026.1005.1", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@capacitor/cli": "^8.5.2", + "@capacitor/core": "^8.5.2", + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@vitest/browser-playwright": "^5.0.3", + "fast-check": "^4.10.2", + "playwright": "^1.63.0", + "svelte": "^5.57.1", + "vite": "^8.3.2", + "vitest": "^5.0.3" + } + }, + "node_modules/@blazediff/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.10.0.tgz", + "integrity": "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@capacitor/cli": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz", + "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xcode": "^3.0.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", + "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/browser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-5.0.3.tgz", + "integrity": "sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.10.0", + "@vitest/mocker": "5.0.3", + "@vitest/ui": "5.0.3", + "@vitest/utils": "5.0.3", + "magic-string": "^1.2.3", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.1", + "ws": "^8.21.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "5.0.3" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-5.0.3.tgz", + "integrity": "sha512-rYBHwIFL/ZtI6dl0rbrAatScXCrvX+TL2BX2AGOigkC1Y6u3rIWw8KMQJJMS4drSa1ksMCmZU2wpgwdE7kqu3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "5.0.3", + "@vitest/mocker": "5.0.3", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "5.0.3" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.3.tgz", + "integrity": "sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.3.tgz", + "integrity": "sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "5.0.3", + "fflate": "^0.8.3", + "flatted": "^3.4.4", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "5.0.3" + } + }, + "node_modules/@vitest/utils": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.3.tgz", + "integrity": "sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "5.0.3", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz", + "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..860891a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "gestion-table-tournante-libre", + "productName": "Gestion table tournante Libre", + "version": "2026.1005.1", + "description": "Placement tournant des participants d’un événement, hors ligne", + "license": "AGPL-3.0-or-later", + "author": "TechnoLibre (http://www.technolibre.ca)", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --mode developpement", + "build": "vite build --mode developpement", + "build:livraison": "node scripts/version.js controler && vite build --mode livraison", + "build:documentation": "vite build --mode documentation", + "test": "vitest run --project node", + "test:surveille": "vitest --project node", + "test:long": "vitest run --project node-long", + "test:navigateur": "vitest run --project navigateur", + "test:tout": "vitest run" + }, + "devDependencies": { + "@capacitor/cli": "^8.5.2", + "@capacitor/core": "^8.5.2", + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@vitest/browser-playwright": "^5.0.3", + "fast-check": "^4.10.2", + "playwright": "^1.63.0", + "svelte": "^5.57.1", + "vite": "^8.3.2", + "vitest": "^5.0.3" + } +} diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000..1a12801 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,724 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// La version datée (§ 18). version.json en est l'unique source ; ce script en +// dérive tout le reste et contrôle que rien n'en diverge. +// +// Une version s'écrit sous quatre formes : +// affichée AAAA.MM.JJ.NN, quatre champs à longueur fixe, rang de 01 à 99 ; +// son ordre alphabétique est l'ordre chronologique ; +// technique AAAA.(MM×100+JJ).NN, trois entiers sans zéro de tête, la forme +// qu'exigent package.json et electron-builder ; +// Windows la forme technique suivie de « .0 » : les quatre champs de la +// ressource de version de l'exécutable, de 16 bits chacun ; +// nom gestion_table_tournante_libre_v___.exe, les +// points devenus soulignés, seul point restant celui de +// l'extension. +// Le jour étant inférieur à 100, MM×100+JJ se redécompose d'une seule façon : +// la dérivation est monotone et bijective. +// +// En ligne de commande, la racine du projet est le parent du répertoire du +// script : +// node scripts/version.js engendrer réécrit src/version.genere.js, le +// champ version de package.json et les +// deux champs de version du paquet +// racine de package-lock.json ; +// node scripts/version.js controler applique les huit points du § 18.5 à +// la date civile locale de la machine ; +// imprime les échecs et sort en code 1, +// ou sort en code 0. +// Ce script n'appartient ni au moteur ni au générateur de démonstrations, et +// l'interdit de lecture d'horloge du § 14.7 ne le vise pas : dateCivileLocale, +// qu'appelle la commande « controler », lit l'horloge de la machine. +import { mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Le nom du livrable (§ 18.2) ne s'écrit qu'ici : le produit, « _v », la +// version dont les points deviennent des soulignés, l'extension. nomDuLivrable +// sert à deriver pour une version et au gabarit pour les champs nommés ; le +// point 7 compare chaque nom cité à ce calcul. +const PRODUIT = 'gestion_table_tournante_libre'; +const PREFIXE_NOM = `${PRODUIT}_v`; +const EXTENSION = '.exe'; +const nomDuLivrable = (version) => `${PREFIXE_NOM}${version.replaceAll('.', '_')}${EXTENSION}`; +const GABARIT_NOM = nomDuLivrable('...'); +const UN_JOUR_MS = 86_400_000; + +const MOIS = [ + 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre', +]; + +// Forme affichée AAAA.MM.JJ.NN. Le rang s'y lit aussi sur trois chiffres ou +// plus sans zéro de tête : la forme est alors reconnue, et le refus nomme le +// plafond du rang. Un rang « 001 » reste une faute de forme. +const FORME_AFFICHEE = /^(\d{4})\.(\d{2})\.(\d{2})\.(\d{2}|[1-9]\d{2,})$/; +// Trois entiers sans zéro de tête ; MM×100+JJ s'écrit sur trois ou quatre +// chiffres, le rang sur un ou deux. +const FORME_TECHNIQUE = /^(\d{4})\.([1-9]\d{2,3})\.([1-9]\d?)$/; + +const deuxChiffres = (n) => String(n).padStart(2, '0'); + +function estBissextile(annee) { + return (annee % 4 === 0 && annee % 100 !== 0) || annee % 400 === 0; +} + +function joursDuMois(annee, mois) { + if (mois === 2) return estBissextile(annee) ? 29 : 28; + return mois === 4 || mois === 6 || mois === 9 || mois === 11 ? 30 : 31; +} + +// Lève quand { annee, mois, jour } n'est pas une date du calendrier +// grégorien dont l'année s'écrit sur quatre chiffres sans zéro de tête — la +// forme technique écrit l'année en entier, et un zéro de tête s'y perdrait. +// « contexte » ouvre le message. +function exigerDate({ annee, mois, jour }, contexte) { + if (!Number.isInteger(annee) || annee < 1000 || annee > 9999) { + throw new RangeError(`${contexte} : année ${annee} hors de 1000 à 9999`); + } + if (!Number.isInteger(mois) || mois < 1 || mois > 12) { + throw new RangeError(`${contexte} : mois ${mois} hors de 1 à 12`); + } + const dernier = joursDuMois(annee, mois); + if (!Number.isInteger(jour) || jour < 1 || jour > dernier) { + throw new RangeError( + `${contexte} : jour ${jour} hors de 1 à ${dernier} en ${MOIS[mois - 1]} ${annee}`, + ); + } +} + +/** + * Formes dérivées d'une version affichée AAAA.MM.JJ.NN : { affichee, + * technique, windows, nomFichier, annee, mois, jour, rang }, les quatre + * derniers en entiers. Lève sur une forme non conforme, une date civile + * invalide, un rang hors de 1 à 99. + */ +export function deriver(versionAffichee) { + if (typeof versionAffichee !== 'string') { + throw new TypeError(`version attendue sous forme de chaîne, reçu ${typeof versionAffichee}`); + } + const contexte = `version « ${versionAffichee} »`; + const champs = FORME_AFFICHEE.exec(versionAffichee); + if (champs === null) { + throw new Error( + `${contexte} : forme attendue AAAA.MM.JJ.NN, quatre champs à longueur fixe (§ 18.1)`, + ); + } + const [annee, mois, jour, rang] = champs.slice(1).map(Number); + exigerDate({ annee, mois, jour }, contexte); + if (rang > 99) throw new RangeError(`${contexte} : rang ${rang}, le rang plafonne à 99 (§ 18.1)`); + if (rang < 1) throw new RangeError(`${contexte} : rang 00 hors de 01 à 99`); + const technique = `${annee}.${mois * 100 + jour}.${rang}`; + return { + affichee: versionAffichee, + technique, + windows: `${technique}.0`, + nomFichier: nomDuLivrable(versionAffichee), + annee, + mois, + jour, + rang, + }; +} + +/** + * Version affichée d'une version technique AAAA.(MM×100+JJ).NN. Lève quand + * la chaîne ne désigne pas exactement une version : forme non conforme, zéro + * de tête, date civile invalide, rang hors de 1 à 99. Sur une chaîne + * acceptée, deriver(redecomposer(t)).technique === t. + */ +export function redecomposer(versionTechnique) { + if (typeof versionTechnique !== 'string') { + throw new TypeError( + `version technique attendue sous forme de chaîne, reçu ${typeof versionTechnique}`, + ); + } + const contexte = `version technique « ${versionTechnique} »`; + const champs = FORME_TECHNIQUE.exec(versionTechnique); + if (champs === null) { + throw new Error( + `${contexte} : forme attendue AAAA.(MM×100+JJ).NN, trois entiers sans zéro de tête, rang de 1 à 99 (§ 18.1)`, + ); + } + const [annee, moisJour, rang] = champs.slice(1).map(Number); + const mois = Math.floor(moisJour / 100); + const jour = moisJour % 100; + exigerDate({ annee, mois, jour }, contexte); + return `${annee}.${deuxChiffres(mois)}.${deuxChiffres(jour)}.${deuxChiffres(rang)}`; +} + +/** + * Date en toutes lettres du titre d'une section du changelog : « 9 mars + * 2031 », « 1er janvier 2027 ». Lève sur une date hors du calendrier. + */ +export function dateEnLettres({ annee, mois, jour }) { + exigerDate({ annee, mois, jour }, 'date'); + return `${jour === 1 ? '1er' : jour} ${MOIS[mois - 1]} ${annee}`; +} + +// --- Le module engendré et les recopies ------------------------------------ + +const MODULE = 'src/version.genere.js'; +const CONSEIL_ENGENDRER = 'lancer « node scripts/version.js engendrer »'; + +const EN_TETE_LICENCE = [ + '// © 2026 TechnoLibre (http://www.technolibre.ca)', + '// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)', +]; + +/** + * Contenu exact de src/version.genere.js pour une version affichée : + * l'en-tête de licence, une ligne vide, puis la constante VERSION sous ses + * formes affichée et technique. Le module ne porte que la version ; la + * provenance de la construction vient du mode de Vite. + */ +export function engendrerModule(versionAffichee) { + const { affichee, technique } = deriver(versionAffichee); + return [ + ...EN_TETE_LICENCE, + '', + '// Engendré par scripts/version.js depuis version.json — ne pas modifier.', + 'export const VERSION = Object.freeze({', + ` affichee: '${affichee}',`, + ` technique: '${technique}',`, + '});', + '', + ].join('\n'); +} + +// JSON écrit comme npm l'écrit : indentation de deux espaces, saut de ligne +// final. +const enJson = (valeur) => `${JSON.stringify(valeur, null, 2)}\n`; + +// Contenu d'un fichier relatif à la racine, ou null s'il n'existe pas. +function lireTexte(racine, chemin) { + try { + return readFileSync(join(racine, chemin), 'utf8'); + } catch (erreur) { + if (erreur.code === 'ENOENT') return null; + throw erreur; + } +} + +// Valeur JSON d'un fichier relatif à la racine. Lève en nommant le fichier +// quand il est illisible, et quand il manque, avec « raisonSiAbsent » pour +// suite du message. +function lireJson(racine, chemin, raisonSiAbsent) { + const texte = lireTexte(racine, chemin); + if (texte === null) throw new Error(`${chemin} absent : ${raisonSiAbsent}`); + try { + return JSON.parse(texte); + } catch (erreur) { + throw new Error(`${chemin} illisible : ${erreur.message}`); + } +} + +// Version affichée que porte version.json, non vérifiée. Lève sur un +// fichier absent, illisible ou sans champ « version » textuel. +function lireVersion(racine) { + const donnees = lireJson(racine, 'version.json', "c'est l'unique source de la version (§ 18.3)"); + if (typeof donnees?.version !== 'string') { + throw new Error('version.json : champ « version » absent ou non textuel'); + } + return donnees.version; +} + +// Formes dérivées de la version de version.json. Chaque refus nomme +// version.json. +function versionDeLaSource(racine) { + const affichee = lireVersion(racine); + try { + return deriver(affichee); + } catch (erreur) { + throw new Error(`version.json : ${erreur.message}`, { cause: erreur }); + } +} + +// Vrai pour un objet JSON, ni tableau ni null. +const estObjet = (valeur) => + typeof valeur === 'object' && valeur !== null && !Array.isArray(valeur); + +const RAISON_PAQUET = 'il porte la forme technique de la version (§ 18.3)'; +const RAISON_VERROU = `« npm install » le crée, puis ${CONSEIL_ENGENDRER}`; + +/** + * Réécrit, depuis version.json, le module engendré, le champ version de + * package.json et les champs version et packages[""].version de + * package-lock.json, en forme technique. Tout est lu et vérifié avant la + * première écriture : une version non conforme, un package.json ou un verrou + * absent font lever sans rien écrire. Un fichier déjà à jour n'est pas + * réécrit. Rend les chemins réécrits, relatifs à la racine. + */ +export function engendrer(racine) { + const { affichee, technique } = versionDeLaSource(racine); + const paquet = lireJson(racine, 'package.json', RAISON_PAQUET); + if (!estObjet(paquet)) throw new Error('package.json : un objet JSON est attendu'); + const verrou = lireJson(racine, 'package-lock.json', RAISON_VERROU); + if (!estObjet(verrou) || !estObjet(verrou.packages) || !estObjet(verrou.packages[''])) { + throw new Error('package-lock.json : paquet racine packages[""] absent'); + } + const cibles = [ + [MODULE, engendrerModule(affichee)], + ['package.json', enJson({ ...paquet, version: technique })], + [ + 'package-lock.json', + enJson({ + ...verrou, + version: technique, + packages: { ...verrou.packages, '': { ...verrou.packages[''], version: technique } }, + }), + ], + ]; + const reecrits = []; + for (const [chemin, contenu] of cibles) { + if (lireTexte(racine, chemin) === contenu) continue; + mkdirSync(dirname(join(racine, chemin)), { recursive: true }); + writeFileSync(join(racine, chemin), contenu); + reecrits.push(chemin); + } + return reecrits; +} + +// --- Le contrôle du § 18.5 ------------------------------------------------ + +// Les deux lignes obligatoires de la section de tête du changelog (§ 18.4) : +// le libellé en gras, les deux-points, puis une réponse. +const LIGNES_OBLIGATOIRES = [ + ['**Version de format des fichiers**', /^\*\*Version de format des fichiers\*\*\s*:\s*\S/], + ['**En remplaçant**', /^\*\*En remplaçant\*\*\s*:\s*\S/], +]; + +// Périmètre du balayage du point 6, posé ici et non après coup : les fichiers +// de la racine, et les arbres des sources, des scripts, de la coquille et des +// épreuves. Seuls ces arbres sont parcourus : les dépendances et les sorties +// de construction (www/, dist*/, android/) n'en font pas partie, et un +// répertoire node_modules n'est jamais descendu. En sont exclus : +// - version.json, package-lock.json, le module engendré et le changelog, où +// la version se recopie par construction et que les points 1 à 3 +// contrôlent ; +// - les documents (*.md), dont le point 7 contrôle les noms cités ; +// - les épreuves de la dérivation, qui citent des versions par nécessité ; +// - les données d'épreuve, sous test/fixtures/. +// package.json reste balayé : la forme technique y est à sa place, la forme +// affichée non. +const ARBRES_BALAYES = ['src', 'scripts', 'electron', 'test']; +const FICHIERS_EXCLUS = new Set([ + 'version.json', + 'package-lock.json', + MODULE, + 'CHANGELOG.md', + 'scripts/version.test.js', +]); +const REPERTOIRES_EXCLUS = new Set(['test/fixtures']); +const DOCUMENT = /\.md$/i; +const ARBRE_DES_SOURCES = 'src/'; + +// Une chaîne de chaque forme, bornée : ni chiffre ni point devant, aucun +// chiffre derrière. La forme Windows contient la forme technique. +const CHAINE_AFFICHEE = /(? deriver(chiffres.join('.')).nomFichier) === nom; +} + +// Résultat de « calcul », ou null s'il lève. +function essayer(calcul) { + try { + return calcul(); + } catch { + return null; + } +} + +// Ordre de deux versions dérivées, comparées champ par champ comme quatre +// entiers : négatif quand a précède b. +function comparerVersions(a, b) { + return a.annee - b.annee || a.mois - b.mois || a.jour - b.jour || a.rang - b.rang; +} + +// Rang d'une date civile dans le calendrier, en jours : la différence de +// deux rangs compte les jours qui séparent les deux dates. +const numeroDeJour = ({ annee, mois, jour }) => Date.UTC(annee, mois - 1, jour) / UN_JOUR_MS; + +// { annee, mois, jour } d'une date civile AAAA-MM-JJ ; lève sur toute autre +// forme et sur une date hors du calendrier. +function lireDateCivile(dateCivile) { + const champs = + typeof dateCivile === 'string' ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateCivile) : null; + if (champs === null) { + throw new TypeError(`dateCivile attendue sous la forme AAAA-MM-JJ, reçu ${String(dateCivile)}`); + } + const [annee, mois, jour] = champs.slice(1).map(Number); + exigerDate({ annee, mois, jour }, `dateCivile « ${dateCivile} »`); + return { annee, mois, jour }; +} + +// Sections du changelog : chaque titre « ## », avec son numéro de ligne, son +// texte et les lignes de son corps. Un bloc de code, de la ligne qui l'ouvre +// par ``` ou ~~~ à celle qui le ferme, n'apporte ni titre ni ligne de corps : +// ce qu'il contient s'affiche comme du code, et une ligne obligatoire citée +// là n'est pas écrite. +function sectionsDuChangelog(texte) { + const sections = []; + let dansUnBloc = false; + texte.split(/\r?\n/).forEach((ligne, i) => { + if (/^\s*(```|~~~)/.test(ligne)) { + dansUnBloc = !dansUnBloc; + return; + } + if (dansUnBloc) return; + const titre = /^## (.*)$/.exec(ligne); + if (titre !== null) { + sections.push({ ligne: i + 1, titre: titre[1].trim(), corps: [] }); + } else if (sections.length > 0) { + sections[sections.length - 1].corps.push(ligne); + } + }); + return sections; +} + +// Chemins relatifs, en « / », des fichiers d'un arbre, sans descendre dans +// node_modules ni dans un répertoire exclu. Un arbre absent n'ajoute rien. +function parcourir(racine, arbre, chemins) { + let entrees; + try { + entrees = readdirSync(join(racine, arbre), { withFileTypes: true }); + } catch (erreur) { + if (erreur.code === 'ENOENT' || erreur.code === 'ENOTDIR') return; + throw erreur; + } + for (const entree of entrees) { + const chemin = `${arbre}/${entree.name}`; + if (entree.isDirectory()) { + if (entree.name !== 'node_modules' && !REPERTOIRES_EXCLUS.has(chemin)) { + parcourir(racine, chemin, chemins); + } + } else if (entree.isFile()) { + chemins.push(chemin); + } + } +} + +// Fichiers de la racine, triés ; un répertoire ou un lien n'en est pas. +function fichiersDeLaRacine(racine) { + return readdirSync(racine, { withFileTypes: true }) + .filter((entree) => entree.isFile()) + .map((entree) => entree.name) + .sort(); +} + +// Fichiers du périmètre du point 6, triés. +function fichiersBalayes(racine) { + const chemins = fichiersDeLaRacine(racine); + for (const arbre of ARBRES_BALAYES) parcourir(racine, arbre, chemins); + return chemins.filter((chemin) => !FICHIERS_EXCLUS.has(chemin) && !DOCUMENT.test(chemin)).sort(); +} + +// Chaque occurrence du motif dans le texte : { ligne, chaine }. +function occurrences(texte, motif) { + const trouvees = []; + texte.split(/\r?\n/).forEach((ligne, i) => { + for (const [chaine] of ligne.matchAll(motif)) trouvees.push({ ligne: i + 1, chaine }); + }); + return trouvees; +} + +// Point 1 : package.json et les deux champs du verrou portent la forme +// technique dérivée de version.json, et elle se redécompose en la version +// affichée. +function controlerRecopies(racine, { affichee, technique }, echec) { + try { + const porte = lireJson(racine, 'package.json', RAISON_PAQUET)?.version; + const retour = essayer(() => redecomposer(porte)); + if (porte !== technique || retour !== affichee) { + const lue = retour === null ? '' : ` (soit ${retour})`; + echec( + 1, + `package.json porte la version ${JSON.stringify(porte)}${lue}, version.json donne ${affichee}, soit ${technique} : ${CONSEIL_ENGENDRER}`, + ); + } + } catch (erreur) { + echec(1, erreur.message); + } + try { + const verrou = lireJson(racine, 'package-lock.json', RAISON_VERROU); + const champs = [ + ['version', verrou?.version], + ['packages[""].version', verrou?.packages?.['']?.version], + ]; + for (const [champ, porte] of champs) { + if (porte !== technique) { + echec( + 1, + `package-lock.json, champ « ${champ} » : ${JSON.stringify(porte)}, version.json donne ${technique} : ${CONSEIL_ENGENDRER}`, + ); + } + } + } catch (erreur) { + echec(1, erreur.message); + } +} + +// Point 2 : le module engendré reproduit, caractère pour caractère, ce que +// le script engendre ; l'échec cite la première ligne qui diffère. +function controlerModule(racine, { affichee }, echec) { + const present = lireTexte(racine, MODULE); + if (present === null) { + echec(2, `${MODULE} absent : ${CONSEIL_ENGENDRER}`); + return; + } + const attendu = engendrerModule(affichee); + if (present === attendu) return; + const lignesPresentes = present.split('\n'); + const lignesAttendues = attendu.split('\n'); + const longueur = Math.max(lignesPresentes.length, lignesAttendues.length); + let i = 0; + while (i < longueur && lignesPresentes[i] === lignesAttendues[i]) i += 1; + const citer = (ligne) => (ligne === undefined ? 'la fin du fichier' : `« ${ligne} »`); + echec( + 2, + `${MODULE}, ligne ${i + 1} : ${citer(lignesPresentes[i])} au lieu de ${citer(lignesAttendues[i])}, que le script engendre : ${CONSEIL_ENGENDRER}`, + ); +} + +// Point 3 : la section de tête porte la version courante, la date en lettres +// que cette version engendre, et les deux lignes obligatoires. Sans version +// valide, seules la forme du titre et les deux lignes se contrôlent. +function controlerTete(sections, version, echec) { + const tete = sections[0]; + if (tete === undefined) { + echec(3, 'CHANGELOG.md : aucune section « ## » ; la section de tête porte la version courante'); + return; + } + const lieu = `CHANGELOG.md, ligne ${tete.ligne}`; + const titre = /^(\S+) — (.+)$/.exec(tete.titre); + if (titre === null) { + echec(3, `${lieu} : le titre « ## ${tete.titre} » n'a pas la forme « ## — »`); + } else if (version !== null) { + const [, porte, date] = titre; + const attendue = dateEnLettres(version); + if (porte !== version.affichee) { + echec( + 3, + `${lieu} : la section de tête porte ${porte}, version.json porte ${version.affichee} ; la section de cette version s'écrit en tête (§ 18.4)`, + ); + } else if (date !== attendue) { + echec(3, `${lieu} : la date « ${date} » n'est pas « ${attendue} », que la version ${porte} engendre`); + } + } + for (const [libelle, motif] of LIGNES_OBLIGATOIRES) { + if (!tete.corps.some((ligne) => motif.test(ligne))) { + echec(3, `${lieu} : la ligne obligatoire « ${libelle} : … » manque à la section de tête (§ 18.4)`); + } + } +} + +// Point 4 : chaque titre de section s'ouvre sur une version, aucune version +// n'est portée deux fois, et chaque section porte une version strictement +// antérieure à celle de la section qui la précède. +function controlerOrdre(sections, echec) { + const vues = new Map(); + let precedente = null; + for (const section of sections) { + const version = essayer(() => deriver(section.titre.split(/\s/)[0])); + if (version === null) { + echec( + 4, + `CHANGELOG.md, ligne ${section.ligne} : le titre « ## ${section.titre} » ne s'ouvre pas sur une version AAAA.MM.JJ.NN`, + ); + continue; + } + const deja = vues.get(version.affichee); + if (deja !== undefined) { + echec(4, `CHANGELOG.md, lignes ${deja} et ${section.ligne} : deux sections portent la version ${version.affichee}`); + } else { + vues.set(version.affichee, section.ligne); + if (precedente !== null && comparerVersions(version, precedente.version) > 0) { + echec( + 4, + `CHANGELOG.md, ligne ${section.ligne} : la section ${version.affichee} suit la section ${precedente.version.affichee} (ligne ${precedente.ligne}) ; les versions vont en ordre strictement décroissant`, + ); + } + } + precedente = { version, ligne: section.ligne }; + } +} + +// Point 5 : la date de la version ne dépasse pas de plus d'un jour la date +// civile de la machine. La tolérance absorbe l'écart de fuseau. +function controlerDate(version, civile, echec) { + const avance = numeroDeJour(version) - numeroDeJour(civile); + if (avance > 1) { + echec( + 5, + `version.json : la version ${version.affichee} est datée du ${dateEnLettres(version)}, ${avance} jours après la date civile de la machine, le ${dateEnLettres(civile)} ; la tolérance est d'un jour`, + ); + } +} + +// Point 6 : aucune chaîne de forme affichée hors de version.json, du module +// engendré et du changelog ; aucune de forme technique hors de package.json +// et du module engendré. Rend le nombre de fichiers de src/ examinés, que +// contrôle le point 8. Un fichier binaire, qui porte un octet nul, n'est pas +// examiné. +function controlerFormes(racine, echec) { + let sourcesExaminees = 0; + for (const chemin of fichiersBalayes(racine)) { + const texte = lireTexte(racine, chemin); + if (texte === null || texte.includes('\u0000')) continue; + if (chemin.startsWith(ARBRE_DES_SOURCES)) sourcesExaminees += 1; + for (const { ligne, chaine } of occurrences(texte, CHAINE_AFFICHEE)) { + echec( + 6, + `${chemin}, ligne ${ligne} : « ${chaine} », forme affichée, hors de version.json, du module engendré et du changelog`, + ); + } + if (chemin === 'package.json') continue; + for (const { ligne, chaine } of occurrences(texte, CHAINE_TECHNIQUE)) { + echec(6, `${chemin}, ligne ${ligne} : « ${chaine} », forme technique, hors de package.json et du module engendré`); + } + } + return sourcesExaminees; +} + +// Point 7 : aucun document ne porte le littéral #_# ni ne cite un nom +// d'exécutable hors du gabarit du § 18.2. Rend le nombre de documents +// examinés, que contrôle le point 8. +function controlerDocuments(racine, echec) { + const documents = fichiersDeLaRacine(racine).filter( + (nom) => DOCUMENTS_FIXES.includes(nom) || GUIDE.test(nom), + ); + for (const document of documents) { + const texte = lireTexte(racine, document); + for (const { ligne, chaine } of occurrences(texte, LITTERAL_NON_SUBSTITUE)) { + echec(7, `${document}, ligne ${ligne} : littéral « ${chaine} », un gabarit non substitué`); + } + for (const { ligne, chaine } of occurrences(texte, NOM_CITE)) { + if (!nomConforme(chaine)) { + echec(7, `${document}, ligne ${ligne} : « ${chaine} » ne se conforme pas au gabarit ${GABARIT_NOM} (§ 18.2)`); + } + } + } + return documents.length; +} + +/** + * Applique à l'arbre « racine » les huit points du § 18.5, à la date civile + * « dateCivile » (AAAA-MM-JJ) de la machine qui construit. Rend la liste des + * échecs, chacun préfixé du numéro de son point (« 1. … ») et nommant + * l'endroit qui diverge ; liste vide = conforme. Lève sur une dateCivile mal + * formée. + */ +export function controler(racine, { dateCivile }) { + const civile = lireDateCivile(dateCivile); + const echecs = []; + const echec = (point, texte) => echecs.push(`${point}. ${texte}`); + + let version = null; + try { + version = versionDeLaSource(racine); + } catch (erreur) { + echec(1, erreur.message); + } + if (version !== null) { + controlerRecopies(racine, version, echec); + controlerModule(racine, version, echec); + } + const changelog = lireTexte(racine, 'CHANGELOG.md'); + if (changelog === null) { + echec(3, "CHANGELOG.md absent : la section de la version courante s'écrit à la livraison (§ 18.4)"); + } else { + const sections = sectionsDuChangelog(changelog); + controlerTete(sections, version, echec); + controlerOrdre(sections, echec); + } + if (version !== null) controlerDate(version, civile, echec); + const sourcesExaminees = controlerFormes(racine, echec); + const documentsExamines = controlerDocuments(racine, echec); + if (sourcesExaminees === 0) { + echec(8, `le balayage du point 6 n'examine aucun fichier sous ${ARBRE_DES_SOURCES} : zéro fichier examiné n'est pas zéro problème (§ 14.2)`); + } + if (documentsExamines === 0) { + echec(8, 'le balayage du point 7 n\'examine aucun document (README.md, CHANGELOG.md, GUIDE-*.md)'); + } + return echecs; +} + +/** + * Date civile locale d'un instant, sous la forme AAAA-MM-JJ ; par défaut, + * celle de la machine au moment de l'appel. + */ +export function dateCivileLocale(instant = new Date()) { + return `${instant.getFullYear()}-${deuxChiffres(instant.getMonth() + 1)}-${deuxChiffres(instant.getDate())}`; +} + +// --- Ligne de commande ------------------------------------------------------ + +const USAGE = 'usage : node scripts/version.js engendrer | controler'; + +// Exécute une commande sur l'arbre « racine » et rend le code de sortie : 0 +// réussi, 1 échec, 2 commande inconnue. +function executer(commande, racine) { + if (commande === 'engendrer') { + try { + const reecrits = engendrer(racine); + const version = lireVersion(racine); + console.log( + reecrits.length === 0 + ? `Version ${version} : les recopies sont à jour.` + : `Version ${version} : réécrit ${reecrits.join(', ')}.`, + ); + return 0; + } catch (erreur) { + console.error(`Version : ${erreur.message}`); + return 1; + } + } + if (commande === 'controler') { + const echecs = controler(racine, { dateCivile: dateCivileLocale() }); + if (echecs.length === 0) { + console.log(`Version ${lireVersion(racine)} : conforme aux huit points du § 18.5.`); + return 0; + } + console.error(`Version : ${echecs.length} échec(s) au contrôle du § 18.5.`); + for (const echec of echecs) console.error(echec); + return 1; + } + console.error(USAGE); + return 2; +} + +// Vrai quand ce fichier est le script que Node a lancé, et non un module +// importé. +function estLanceDirectement() { + if (process.argv[1] === undefined) return false; + try { + return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (estLanceDirectement()) { + process.exitCode = executer(process.argv[2], fileURLToPath(new URL('..', import.meta.url))); +} diff --git a/scripts/version.test.js b/scripts/version.test.js new file mode 100644 index 0000000..1939bf2 --- /dev/null +++ b/scripts/version.test.js @@ -0,0 +1,858 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuves de la version datée (§ 18). Ce fichier cite des versions en clair +// par nécessité : le balayage du point 6 du § 18.5 l'exclut, et lui seul. +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { + copyFileSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, test } from '../test/lanceur.js'; +import { + controler, + dateCivileLocale, + dateEnLettres, + deriver, + engendrer, + engendrerModule, + redecomposer, +} from './version.js'; + +const RACINE = fileURLToPath(new URL('..', import.meta.url)); +const SCRIPT = fileURLToPath(new URL('./version.js', import.meta.url)); + +const UN_JOUR_MS = 86_400_000; + +// Les jours d'une année, énumérés par le calendrier de Date.UTC : l'oracle ne +// partage rien avec la règle des mois et des années bissextiles du module +// éprouvé. +function joursDe(annee) { + const jours = []; + for ( + let instant = Date.UTC(annee, 0, 1); + new Date(instant).getUTCFullYear() === annee; + instant += UN_JOUR_MS + ) { + const date = new Date(instant); + jours.push({ annee, mois: date.getUTCMonth() + 1, jour: date.getUTCDate() }); + } + return jours; +} + +// Dernier jour du mois, lu dans le même calendrier : le jour 0 d'un mois est +// la veille de son premier jour. +function dernierJour(annee, mois) { + return new Date(Date.UTC(annee, mois, 0)).getUTCDate(); +} + +const deux = (n) => String(n).padStart(2, '0'); +const forme = ({ annee, mois, jour }, rang) => + `${annee}.${deux(mois)}.${deux(jour)}.${deux(rang)}`; +const champs = (technique) => technique.split('.').map(Number); + +// Vrai quand a précède strictement b, comparés champ à champ comme des +// entiers. +function precede(a, b) { + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) return a[i] < b[i]; + } + return false; +} + +const MOIS = [ + 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre', +]; + +describe('version : dérivation (§ 18.1, § 18.2)', () => { + test('une version affichée donne ses formes technique et Windows et le nom du livrable', () => { + assert.deepEqual(deriver('2026.10.05.01'), { + affichee: '2026.10.05.01', + technique: '2026.1005.1', + windows: '2026.1005.1.0', + nomFichier: 'gestion_table_tournante_libre_v2026_10_05_01.exe', + annee: 2026, + mois: 10, + jour: 5, + rang: 1, + }); + }); + + test('le mois garde son zéro de tête dans la forme affichée et le perd dans la technique', () => { + assert.equal(deriver('2026.01.05.01').technique, '2026.105.1'); + assert.equal(redecomposer('2026.105.1'), '2026.01.05.01'); + }); + + test('la forme technique croît avec la date, champ à champ', () => { + const suite = ['2026.01.05.01', '2026.10.05.01', '2026.12.31.01', '2027.01.05.01'].map( + (version) => champs(deriver(version).technique), + ); + assert.deepEqual(suite, [ + [2026, 105, 1], + [2026, 1005, 1], + [2026, 1231, 1], + [2027, 105, 1], + ]); + for (let i = 1; i < suite.length; i += 1) { + assert.ok(precede(suite[i - 1], suite[i]), `${suite[i - 1]} < ${suite[i]}`); + } + assert.ok( + precede(champs(deriver('2026.10.05.01').technique), champs(deriver('2026.10.05.02').technique)), + ); + }); + + test('aller-retour sur chaque jour de 2026 et de 2028, sur les 99 rangs', () => { + const jours = [...joursDe(2026), ...joursDe(2028)]; + assert.equal(jours.length, 365 + 366); + const affichees = []; + const noms = []; + let veille = null; + jours.forEach((date, i) => { + const rang = 1 + (i % 99); + const version = forme(date, rang); + const derivee = deriver(version); + assert.equal(derivee.technique, `${date.annee}.${date.mois * 100 + date.jour}.${rang}`); + assert.equal(derivee.windows, `${derivee.technique}.0`); + assert.equal( + derivee.nomFichier, + `gestion_table_tournante_libre_v${version.replaceAll('.', '_')}.exe`, + ); + assert.equal(redecomposer(derivee.technique), version); + // Chaque champ de la ressource de version Windows tient sur 16 bits. + for (const champ of champs(derivee.windows)) assert.ok(champ <= 0xffff, derivee.windows); + const jour = champs(derivee.technique).slice(0, 2); + if (veille !== null) assert.ok(precede(veille, jour), `${veille} < ${jour}`); + veille = jour; + affichees.push(forme(date, 1)); + noms.push(deriver(forme(date, 1)).nomFichier); + }); + // L'ordre alphabétique des formes affichées et des noms de fichiers est + // l'ordre chronologique. + assert.deepEqual([...affichees].sort(), affichees); + assert.deepEqual([...noms].sort(), noms); + }); + + test('le dernier jour de chaque mois passe, le lendemain est refusé', () => { + let mois = 0; + for (const annee of [2026, 2028]) { + for (let m = 1; m <= 12; m += 1) { + const dernier = dernierJour(annee, m); + assert.doesNotThrow(() => deriver(forme({ annee, mois: m, jour: dernier }, 1))); + assert.throws(() => deriver(forme({ annee, mois: m, jour: dernier + 1 }, 1)), /jour/); + assert.throws(() => deriver(forme({ annee, mois: m, jour: 0 }, 1)), /jour/); + mois += 1; + } + } + assert.equal(mois, 24); + assert.equal(dernierJour(2026, 2), 28); + assert.equal(dernierJour(2028, 2), 29); + }); + + test('refuse une forme non conforme, une date invalide, un rang hors de 01 à 99', () => { + const refus = [ + ['2026.1.5.1', /forme/], + ['2026.13.01.01', /mois/], + ['2026.00.10.01', /mois/], + ['2026.02.30.01', /jour/], + ['2026.10.05.00', /rang/], + ['2026.10.05.100', /rang.*99/], + ['2026.10.05.001', /forme/], // zéro de tête : une faute de forme, pas de plafond + ['2026.13.45.100', /mois/], // la date se contrôle avant le plafond du rang + ['2026.10.05.1', /forme/], + ['2026.10.05', /forme/], + ['2026.10.05.01 ', /forme/], + ['v2026.10.05.01', /forme/], + ['2026-10-05-01', /forme/], + ['0999.10.05.01', /année/], + ]; + for (const [version, motif] of refus) { + assert.throws(() => deriver(version), motif, version); + } + assert.throws(() => deriver(20261005), TypeError); + }); + + test('accepte le rang 99', () => { + assert.equal(deriver('2026.10.05.99').technique, '2026.1005.99'); + assert.equal(redecomposer('2026.1005.99'), '2026.10.05.99'); + }); + + test('redecomposer refuse ce qui ne désigne pas exactement une version', () => { + const refus = [ + ['2026.0105.1', /forme/], // zéro de tête + ['2026.1005.01', /forme/], // zéro de tête + ['2026.1005.0', /forme/], // rang 0 + ['2026.1005.100', /forme/], // rang au-delà de 99 + ['2026.99.1', /forme/], // mois et jour sur deux chiffres + ['2026.1005', /forme/], // deux champs + ['2026.10.05.01', /forme/], // forme affichée + ['26.1005.1', /forme/], // année sur deux chiffres + ['2026.1305.1', /mois/], // mois 13 + ['2026.230.1', /jour/], // 30 février + ['2026.1000.1', /jour/], // jour 0 + ]; + for (const [technique, motif] of refus) { + assert.throws(() => redecomposer(technique), motif, technique); + } + assert.throws(() => redecomposer(2026.1005), TypeError); + }); +}); + +describe('version : date en lettres (§ 18.4)', () => { + test('écrit le jour, le mois en lettres et l’année ; le premier du mois est « 1er »', () => { + assert.equal(dateEnLettres({ annee: 2026, mois: 10, jour: 5 }), '5 octobre 2026'); + assert.equal(dateEnLettres({ annee: 2027, mois: 1, jour: 1 }), '1er janvier 2027'); + assert.equal(dateEnLettres({ annee: 2028, mois: 2, jour: 29 }), '29 février 2028'); + }); + + test('nomme les douze mois', () => { + assert.deepEqual( + MOIS.map((_, i) => dateEnLettres({ annee: 2026, mois: i + 1, jour: 2 })), + MOIS.map((nom) => `2 ${nom} 2026`), + ); + }); + + test('refuse une date hors du calendrier', () => { + assert.throws(() => dateEnLettres({ annee: 2026, mois: 2, jour: 29 }), /jour/); + assert.throws(() => dateEnLettres({ annee: 2026, mois: 13, jour: 1 }), /mois/); + }); +}); + +// --- Arbres d'épreuve ------------------------------------------------------- + +const VERSION = '2026.10.05.01'; +const TECHNIQUE = '2026.1005.1'; +const DATE = '2026-10-05'; + +// Le module engendré pour VERSION, octet pour octet. +const MODULE = [ + '// © 2026 TechnoLibre (http://www.technolibre.ca)', + '// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)', + '', + '// Engendré par scripts/version.js depuis version.json — ne pas modifier.', + 'export const VERSION = Object.freeze({', + " affichee: '2026.10.05.01',", + " technique: '2026.1005.1',", + '});', + '', +].join('\n'); + +const TITRE_CHANGELOG = '# Changelog — Gestion table tournante Libre'; + +// Une section de changelog complète : titre, une rubrique, les deux lignes +// obligatoires. +const section = (titre) => + [ + `## ${titre}`, + '', + '**Ce qui change pour vous**', + '- Le bandeau affiche la version.', + '', + '**Version de format des fichiers** : inchangée.', + '', + '**En remplaçant** : rien à faire.', + '', + ].join('\n'); + +const changelog = (...titres) => [TITRE_CHANGELOG, '', ...titres.map(section)].join('\n'); + +const enJson = (valeur) => `${JSON.stringify(valeur, null, 2)}\n`; + +function ecrire(racine, chemin, contenu) { + const complet = join(racine, chemin); + mkdirSync(dirname(complet), { recursive: true }); + writeFileSync(complet, contenu); +} + +const lire = (racine, chemin) => readFileSync(join(racine, chemin), 'utf8'); + +function modifierJson(racine, chemin, modification) { + const valeur = JSON.parse(lire(racine, chemin)); + modification(valeur); + ecrire(racine, chemin, enJson(valeur)); +} + +// Arbre conforme à VERSION et à DATE : la source, ses trois recopies, un +// changelog, un README qui cite le gabarit du nom, une source et une +// configuration à la racine. +function arbreConforme() { + const racine = mkdtempSync(join(tmpdir(), 'version-')); + ecrire(racine, 'version.json', `{ "version": "${VERSION}" }\n`); + ecrire(racine, 'package.json', enJson({ name: 'essai', version: TECHNIQUE, type: 'module' })); + ecrire( + racine, + 'package-lock.json', + enJson({ + name: 'essai', + version: TECHNIQUE, + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'essai', version: TECHNIQUE } }, + }), + ); + ecrire(racine, 'src/version.genere.js', MODULE); + ecrire( + racine, + 'src/main.js', + "import { VERSION } from './version.genere.js';\nexport const titre = VERSION.affichee;\n", + ); + ecrire(racine, 'vite.config.js', 'export default {};\n'); + ecrire(racine, 'CHANGELOG.md', changelog(`${VERSION} — 5 octobre 2026`)); + ecrire( + racine, + 'README.md', + "# Essai\n\nL'exécutable se nomme `gestion_table_tournante_libre_v___.exe`.\n", + ); + return racine; +} + +// Exécute l'épreuve sur un arbre conforme neuf, supprimé ensuite. +function surArbre(epreuve) { + const racine = arbreConforme(); + try { + return epreuve(racine); + } finally { + rmSync(racine, { recursive: true, force: true }); + } +} + +// Numéros de point des échecs, sans doublon, croissants. Un échec sans +// numéro fait échouer l'épreuve. +function points(echecs) { + const numeros = echecs.map((echec) => { + const numero = /^([1-8])\. /.exec(echec); + assert.ok(numero, `échec sans numéro de point : ${echec}`); + return Number(numero[1]); + }); + return [...new Set(numeros)].sort((a, b) => a - b); +} + +// Contrôle l'arbre et exige que les échecs nomment exactement les points +// attendus, et que chaque motif se lise dans l'un d'eux. +function exigerEchecs(racine, attendus, motifs, dateCivile = DATE) { + const echecs = controler(racine, { dateCivile }); + const liste = echecs.join('\n'); + assert.deepEqual(points(echecs), attendus, liste); + for (const motif of motifs) { + assert.ok( + echecs.some((echec) => motif.test(echec)), + `${motif} ne se lit dans aucun échec :\n${liste}`, + ); + } + return echecs; +} + +describe('version : contrôle du § 18.5, un défaut à la fois', () => { + test("l'arbre de référence est conforme", () => + surArbre((racine) => { + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('point 1 : package.json porte une autre version technique', () => + surArbre((racine) => { + modifierJson(racine, 'package.json', (paquet) => { + paquet.version = '2026.1006.1'; + }); + exigerEchecs(racine, [1], [/^1\. package\.json .*2026\.1006\.1.*2026\.1005\.1/]); + })); + + test('point 1 : le champ version du verrou porte une autre version', () => + surArbre((racine) => { + modifierJson(racine, 'package-lock.json', (verrou) => { + verrou.version = '2026.1004.1'; + }); + exigerEchecs(racine, [1], [/^1\. package-lock\.json, champ « version » .*2026\.1004\.1/]); + })); + + test('point 1 : le champ packages[""].version du verrou porte une autre version', () => + surArbre((racine) => { + modifierJson(racine, 'package-lock.json', (verrou) => { + verrou.packages[''].version = '2026.1004.1'; + }); + exigerEchecs( + racine, + [1], + [/^1\. package-lock\.json, champ « packages\[""\]\.version » .*2026\.1004\.1/], + ); + })); + + test('point 1 : le verrou manque', () => + surArbre((racine) => { + unlinkSync(join(racine, 'package-lock.json')); + exigerEchecs(racine, [1], [/^1\. package-lock\.json absent/]); + })); + + test('point 1 : version.json manque', () => + surArbre((racine) => { + unlinkSync(join(racine, 'version.json')); + exigerEchecs(racine, [1], [/^1\. version\.json absent/]); + })); + + test('point 1 : version.json porte une version non conforme', () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.05.100" }\n'); + exigerEchecs(racine, [1], [/^1\. version\.json : .*rang 100.*99/]); + })); + + test('point 2 : le module engendré est modifié à la main', () => + surArbre((racine) => { + ecrire(racine, 'src/version.genere.js', MODULE.replace("'2026.1005.1'", "'2026.1005.2'")); + exigerEchecs(racine, [2], [/^2\. src\/version\.genere\.js, ligne 7 /]); + })); + + test('point 2 : le module engendré manque', () => + surArbre((racine) => { + unlinkSync(join(racine, 'src/version.genere.js')); + exigerEchecs(racine, [2], [/^2\. src\/version\.genere\.js absent/]); + })); + + test('point 3 : le titre de tête ne porte pas la version courante', () => + surArbre((racine) => { + ecrire(racine, 'CHANGELOG.md', changelog('2026.10.04.01 — 4 octobre 2026')); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*2026\.10\.04\.01.*2026\.10\.05\.01/]); + })); + + test('point 3 : la date en lettres du titre de tête est fausse', () => + surArbre((racine) => { + ecrire(racine, 'CHANGELOG.md', changelog(`${VERSION} — 6 octobre 2026`)); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*6 octobre 2026.*5 octobre 2026/]); + })); + + test('point 3 : la ligne de la version de format manque', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire(racine, 'CHANGELOG.md', texte.replace('**Version de format des fichiers** : inchangée.\n', '')); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md.*Version de format des fichiers/]); + })); + + test('point 3 : la ligne « En remplaçant » manque', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire(racine, 'CHANGELOG.md', texte.replace('**En remplaçant** : rien à faire.\n', '')); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md.*En remplaçant/]); + })); + + test('point 3 : le changelog manque', () => + surArbre((racine) => { + unlinkSync(join(racine, 'CHANGELOG.md')); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md absent/]); + })); + + test('point 3 : une ligne obligatoire absente de la section de tête échoue, même portée par une section plus ancienne', () => { + const lignes = [ + ['**Version de format des fichiers** : inchangée.\n', /^3\. CHANGELOG\.md, ligne 3 : .*Version de format des fichiers/], + ['**En remplaçant** : rien à faire.\n', /^3\. CHANGELOG\.md, ligne 3 : .*En remplaçant/], + ]; + for (const [ligne, motif] of lignes) { + surArbre((racine) => { + const texte = changelog(`${VERSION} — 5 octobre 2026`, '2026.10.04.01 — 4 octobre 2026'); + // replace ne retire que la première occurrence : celle de la section de tête. + ecrire(racine, 'CHANGELOG.md', texte.replace(ligne, '')); + exigerEchecs(racine, [3], [motif]); + }); + } + }); + + test('point 3 : une ligne obligatoire citée dans un bloc de code ne compte pas', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire( + racine, + 'CHANGELOG.md', + texte.replace('**En remplaçant** : rien à faire.\n', '```\n**En remplaçant** : rien à faire.\n```\n'), + ); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*En remplaçant/]); + })); + + test('points 3 et 4 : une section « À venir » en tête', () => + surArbre((racine) => { + // La section « À venir » porte ses deux lignes obligatoires : seul son + // titre fait tomber le point 3. + ecrire(racine, 'CHANGELOG.md', changelog('À venir', `${VERSION} — 5 octobre 2026`)); + exigerEchecs(racine, [3, 4], [ + /^3\. CHANGELOG\.md, ligne 3 : le titre « ## À venir » n'a pas la forme/, + /^4\. CHANGELOG\.md, ligne 3 : le titre « ## À venir » ne s'ouvre pas sur une version/, + ]); + })); + + test('point 4 : une section « À venir » entre deux versions', () => + surArbre((racine) => { + ecrire( + racine, + 'CHANGELOG.md', + changelog(`${VERSION} — 5 octobre 2026`, 'À venir', '2026.10.04.01 — 4 octobre 2026'), + ); + exigerEchecs(racine, [4], [ + /^4\. CHANGELOG\.md, ligne 12 : le titre « ## À venir » ne s'ouvre pas sur une version/, + ]); + })); + + test('points 3 et 4 : un titre « ## » cité dans un bloc de code ne découpe pas la section', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire( + racine, + 'CHANGELOG.md', + texte.replace( + '- Le bandeau affiche la version.\n', + '- Le bandeau affiche la version.\n\n```markdown\n## Notes de la soirée\n```\n', + ), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('points 3 et 4 : un sous-titre « ### » ne découpe pas la section', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire( + racine, + 'CHANGELOG.md', + texte.replace('**Ce qui change pour vous**\n', '**Ce qui change pour vous**\n\n### Le bandeau\n'), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('point 4 : deux sections portent la même version', () => + surArbre((racine) => { + ecrire( + racine, + 'CHANGELOG.md', + changelog(`${VERSION} — 5 octobre 2026`, `${VERSION} — 5 octobre 2026`), + ); + exigerEchecs(racine, [4], [/^4\. CHANGELOG\.md, lignes 3 et 12 : .*2026\.10\.05\.01/]); + })); + + test('point 4 : une section plus récente suit une plus ancienne', () => + surArbre((racine) => { + ecrire( + racine, + 'CHANGELOG.md', + changelog(`${VERSION} — 5 octobre 2026`, '2026.10.06.01 — 6 octobre 2026'), + ); + exigerEchecs(racine, [4], [/^4\. CHANGELOG\.md, ligne 12 : .*2026\.10\.06\.01.*2026\.10\.05\.01/]); + })); + + test('point 4 : des sections en ordre strictement décroissant sont admises', () => + surArbre((racine) => { + ecrire( + racine, + 'CHANGELOG.md', + changelog( + `${VERSION} — 5 octobre 2026`, + '2026.10.04.02 — 4 octobre 2026', + '2026.10.04.01 — 4 octobre 2026', + '2025.12.31.01 — 31 décembre 2025', + ), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test("point 5 : la version est datée de plus d'un jour après la date civile", () => + surArbre((racine) => { + exigerEchecs(racine, [5], [/^5\. version\.json : .*5 octobre 2026.*3 octobre 2026/], '2026-10-03'); + })); + + test("point 5 : un jour d'avance sur la date civile est toléré", () => + surArbre((racine) => { + assert.deepEqual(controler(racine, { dateCivile: '2026-10-04' }), []); + assert.deepEqual(controler(racine, { dateCivile: '2027-03-01' }), []); + })); + + test('point 6 : une forme affichée dans src/ hors du module engendré', () => + surArbre((racine) => { + ecrire(racine, 'src/interface/bandeau.js', "// Bandeau.\n\nexport const v = 'v2026.10.05.01';\n"); + exigerEchecs(racine, [6], [/^6\. src\/interface\/bandeau\.js, ligne 3 : « 2026\.10\.05\.01 », forme affichée/]); + })); + + test('point 6 : une forme technique hors de package.json et du module engendré', () => + surArbre((racine) => { + ecrire(racine, 'vite.config.js', "export default { define: { V: '2026.1005.1' } };\n"); + ecrire(racine, 'scripts/construire.sh', '#!/bin/sh\nWINDOWS=2026.1005.1.0\n'); + exigerEchecs(racine, [6], [ + /^6\. vite\.config\.js, ligne 1 : « 2026\.1005\.1 », forme technique/, + /^6\. scripts\/construire\.sh, ligne 2 : « 2026\.1005\.1 », forme technique/, + ]); + })); + + test('point 6 : une forme affichée dans package.json', () => + surArbre((racine) => { + modifierJson(racine, 'package.json', (paquet) => { + paquet.description = 'version 2026.10.05.01'; + }); + exigerEchecs(racine, [6], [/^6\. package\.json, ligne \d+ : « 2026\.10\.05\.01 », forme affichée/]); + })); + + test('point 6 : les épreuves de test/ sont dans le périmètre', () => + surArbre((racine) => { + ecrire(racine, 'test/bandeau.test.js', "export const attendu = '2026.10.05.01';\n"); + exigerEchecs(racine, [6], [/^6\. test\/bandeau\.test\.js, ligne 1 /]); + })); + + test('point 6 : la coquille electron/ est dans le périmètre', () => + surArbre((racine) => { + ecrire(racine, 'electron/main.js', "const titre = 'Gestion table tournante Libre — 2026.10.05.01';\n"); + exigerEchecs(racine, [6], [/^6\. electron\/main\.js, ligne 1 : « 2026\.10\.05\.01 », forme affichée/]); + })); + + test('point 6 : une version ne se lit pas dans un nombre plus long qui la contient', () => + surArbre((racine) => { + // Un chiffre ou un point devant, un chiffre derrière : chaque ligne + // prolonge une version d'un côté, sous l'une des deux formes. + ecrire( + racine, + 'src/donnees.js', + [ + "export const a = '12026.10.05.01';", + "export const b = '1.2026.10.05.01';", + "export const c = '2026.10.05.012';", + "export const d = '12026.1005.1';", + "export const e = '1.2026.1005.1';", + "export const f = '2026.1005.123';", + '', + ].join('\n'), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test("point 6 : un fichier binaire, qui porte un octet nul, n'est pas lu comme du texte", () => + surArbre((racine) => { + ecrire(racine, 'src/interface/icone.png', `PNG\u0000${VERSION} ${TECHNIQUE}\n`); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('point 6 : données, guides, dépendances et sorties restent hors du périmètre', () => + surArbre((racine) => { + const versions = `${VERSION} ${TECHNIQUE} ${TECHNIQUE}.0\n`; + for (const chemin of [ + 'scripts/version.test.js', + 'test/fixtures/demonstration.json', + 'GUIDE-USAGE.md', + 'docs/notes.md', + 'node_modules/paquet/index.js', + 'src/node_modules/paquet/index.js', + 'www/assets/index.js', + 'dist/sortie.js', + 'dist-electron/main.js', + 'android/app/build.gradle', + ]) { + ecrire(racine, chemin, versions); + } + ecrire(racine, 'README.md', `${lire(racine, 'README.md')}\nVersion ${versions}`); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('point 7 : un document porte le littéral #_#', () => + surArbre((racine) => { + ecrire(racine, 'README.md', `${lire(racine, 'README.md')}\nVersion : #_#\n`); + exigerEchecs(racine, [7], [/^7\. README\.md, ligne 5 : .*#_#/]); + })); + + test("point 7 : un document cite un nom d'exécutable hors du gabarit", () => + surArbre((racine) => { + const fautifs = [ + 'gestion_table_tournante_libre_v2026.10.05.01.exe', + 'gestion_table_tournante_libre_v2026_10_05_1.exe', + 'gestion_table_tournante_libre_v2026_13_05_01.exe', + 'gestion_table_tournante_libre_v.exe', + 'gestion_table_tournante_libre.exe', + 'Gestion_Table_Tournante_Libre_v2026_10_05_01.EXE', + ]; + ecrire(racine, 'GUIDE-WINDOWS.md', fautifs.map((nom) => `Lancer \`${nom}\`.\n`).join('')); + const echecs = exigerEchecs( + racine, + [7], + fautifs.map((nom, i) => new RegExp(`^7\\. GUIDE-WINDOWS\\.md, ligne ${i + 1} : « ${nom.replace(/[.<>]/g, '\\$&')} »`)), + ); + assert.equal(echecs.length, fautifs.length, echecs.join('\n')); + })); + + test('point 7 : le gabarit, un nom conforme et le préfixe seul sont admis', () => + surArbre((racine) => { + ecrire( + racine, + 'GUIDE-WINDOWS.md', + [ + 'Le fichier dont le nom commence par `gestion_table_tournante_libre_v`.', + 'Gabarit : `gestion_table_tournante_libre_v___.exe`.', + 'Par exemple gestion_table_tournante_libre_v2026_10_05_01.exe.', + '', + ].join('\n'), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('point 7 : le changelog est balayé', () => + surArbre((racine) => { + const texte = lire(racine, 'CHANGELOG.md'); + ecrire( + racine, + 'CHANGELOG.md', + texte.replace( + '- Le bandeau affiche la version.\n', + '- Le bandeau affiche la version.\n- Lancer `gestion_table_tournante_libre.exe`.\n- Version #_#.\n', + ), + ); + const echecs = exigerEchecs(racine, [7], [ + /^7\. CHANGELOG\.md, ligne 7 : « gestion_table_tournante_libre\.exe »/, + /^7\. CHANGELOG\.md, ligne 8 : littéral « #_# »/, + ]); + assert.equal(echecs.length, 2, echecs.join('\n')); + })); + + test('point 8 : un balayage qui ne trouve aucune source échoue', () => + surArbre((racine) => { + rmSync(join(racine, 'src'), { recursive: true }); + // Sans src/, le module engendré manque aussi : le point 2 tombe avec. + exigerEchecs(racine, [2, 8], [/^8\. .*src\//]); + })); + + test('point 8 : un src/ qui ne porte que le module engendré ne compte aucune source', () => + surArbre((racine) => { + unlinkSync(join(racine, 'src/main.js')); + const echecs = exigerEchecs(racine, [8], [/^8\. le balayage du point 6 n'examine aucun fichier sous src\//]); + assert.equal(echecs.length, 1, echecs.join('\n')); + })); + + test('point 8 : un balayage qui ne trouve aucun document échoue', () => + surArbre((racine) => { + unlinkSync(join(racine, 'README.md')); + unlinkSync(join(racine, 'CHANGELOG.md')); + // Sans changelog, le point 3 tombe avec. + exigerEchecs(racine, [3, 8], [/^8\. le balayage du point 7 n'examine aucun document/]); + })); + + test("modifier version.json seul fait tomber les points 1, 2 et 3, chacun nommant l'endroit resté en arrière", () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.05.02" }\n'); + const echecs = exigerEchecs(racine, [1, 2, 3], [ + /^1\. package\.json /, + /^1\. package-lock\.json, champ « version » /, + /^1\. package-lock\.json, champ « packages\[""\]\.version » /, + /^2\. src\/version\.genere\.js, ligne 6 /, + /^3\. CHANGELOG\.md, ligne 3 : /, + ]); + assert.equal(echecs.length, 5, echecs.join('\n')); + engendrer(racine); + exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : /]); + const texte = lire(racine, 'CHANGELOG.md'); + ecrire( + racine, + 'CHANGELOG.md', + texte.replace(`${TITRE_CHANGELOG}\n\n`, `${TITRE_CHANGELOG}\n\n${section('2026.10.05.02 — 5 octobre 2026')}\n`), + ); + assert.deepEqual(controler(racine, { dateCivile: DATE }), []); + })); + + test('refuse une date civile mal formée', () => { + assert.throws(() => controler(RACINE, { dateCivile: '2026-10-5' }), TypeError); + assert.throws(() => controler(RACINE, { dateCivile: '2026-02-30' }), /jour/); + }); +}); + +describe('version : engendrement', () => { + test("engendrerModule rend le module, l'en-tête de licence en tête", () => { + assert.equal(engendrerModule(VERSION), MODULE); + }); + + test('engendrer réécrit le module, package.json et les deux champs du verrou', () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.06.02" }\n'); + const paquet = JSON.parse(lire(racine, 'package.json')); + const verrou = JSON.parse(lire(racine, 'package-lock.json')); + const reecrits = engendrer(racine); + assert.deepEqual(reecrits, ['src/version.genere.js', 'package.json', 'package-lock.json']); + assert.equal(lire(racine, 'src/version.genere.js'), engendrerModule('2026.10.06.02')); + assert.equal(lire(racine, 'package.json'), enJson({ ...paquet, version: '2026.1006.2' })); + verrou.version = '2026.1006.2'; + verrou.packages[''].version = '2026.1006.2'; + assert.equal(lire(racine, 'package-lock.json'), enJson(verrou)); + })); + + test('sur un arbre à jour, engendrer ne réécrit rien', () => + surArbre((racine) => { + const fichiers = ['src/version.genere.js', 'package.json', 'package-lock.json']; + const avant = fichiers.map((chemin) => lire(racine, chemin)); + assert.deepEqual(engendrer(racine), []); + assert.deepEqual(fichiers.map((chemin) => lire(racine, chemin)), avant); + })); + + test('engendrer lève quand le verrou manque, sans rien écrire', () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.06.02" }\n'); + unlinkSync(join(racine, 'package-lock.json')); + assert.throws(() => engendrer(racine), /package-lock\.json absent.*npm install/); + assert.equal(lire(racine, 'src/version.genere.js'), MODULE); + assert.equal(JSON.parse(lire(racine, 'package.json')).version, TECHNIQUE); + })); + + test('engendrer refuse le centième rang de la journée, sans rien écrire', () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.05.100" }\n'); + assert.throws(() => engendrer(racine), /^Error: version\.json : .*99/); + assert.equal(lire(racine, 'src/version.genere.js'), MODULE); + })); +}); + +describe('version : date civile de la machine', () => { + test('dateCivileLocale écrit la date locale sous la forme AAAA-MM-JJ', () => { + assert.equal(dateCivileLocale(new Date(2026, 9, 5, 23, 59)), '2026-10-05'); + assert.equal(dateCivileLocale(new Date(2027, 0, 1, 0, 0)), '2027-01-01'); + }); +}); + +describe('version : ligne de commande', () => { + // Le script copié dans l'arbre d'épreuve prend pour racine le parent de + // son répertoire, comme dans le projet. + function lancer(racine, ...arguments_) { + const script = join(racine, 'scripts', 'version.js'); + mkdirSync(dirname(script), { recursive: true }); + copyFileSync(SCRIPT, script); + return spawnSync(process.execPath, [script, ...arguments_], { encoding: 'utf8' }); + } + + test('controler sort en 0 sur un arbre conforme, en 1 en imprimant les échecs sinon', () => + surArbre((racine) => { + const conforme = lancer(racine, 'controler'); + assert.equal(conforme.status, 0, conforme.stderr); + modifierJson(racine, 'package.json', (paquet) => { + paquet.version = '2026.1006.1'; + }); + const fautif = lancer(racine, 'controler'); + assert.equal(fautif.status, 1, fautif.stdout); + assert.match(fautif.stderr, /^1\. package\.json /m); + })); + + test('engendrer réécrit les recopies, et sort en 1 quand le verrou manque', () => + surArbre((racine) => { + ecrire(racine, 'version.json', '{ "version": "2026.10.05.02" }\n'); + const reussi = lancer(racine, 'engendrer'); + assert.equal(reussi.status, 0, reussi.stderr); + assert.equal(lire(racine, 'src/version.genere.js'), engendrerModule('2026.10.05.02')); + unlinkSync(join(racine, 'package-lock.json')); + const echoue = lancer(racine, 'engendrer'); + assert.equal(echoue.status, 1, echoue.stdout); + assert.match(echoue.stderr, /package-lock\.json absent/); + })); + + test("une commande inconnue sort en 2 et rappelle l'usage", () => + surArbre((racine) => { + const inconnue = lancer(racine, 'publier'); + assert.equal(inconnue.status, 2); + assert.match(inconnue.stderr, /engendrer.*controler/); + })); +}); + +describe('version : arbre réel', () => { + test("l'arbre réel du projet est conforme, à la date civile de la machine", () => { + assert.deepEqual(controler(RACINE, { dateCivile: dateCivileLocale() }), []); + }); +}); diff --git a/src/interface/App.navigateur.test.js b/src/interface/App.navigateur.test.js new file mode 100644 index 0000000..0512019 --- /dev/null +++ b/src/interface/App.navigateur.test.js @@ -0,0 +1,242 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuve de rendu du bandeau d'application (§ 8.4, § 18.6), dans un vrai +// moteur de rendu. Le test interroge le rendu, jamais la présence d'une +// classe (§ 14.2) : la visibilité de chaque élément, la boîte où la mise en +// page le pose et l'élément qu'atteint le centre de cette boîte, le style +// calculé, le texte, le titre de la fenêtre. Il ne lit aucun pixel : le +// contraste se calcule sur les couleurs calculées, qui ne sont les couleurs +// peintes que sans opacité partielle, filtre ni mode de fusion — ce qu'une +// épreuve vérifie dans l'ascendance de chaque texte (§ 14.5). Les épreuves de +// visibilité, de contraste et d'opacité passent par les deux thèmes : le +// protocole de débogage de Chromium (CDP) émule prefers-color-scheme. +// +// Le projet navigateur n'importe pas node:assert/strict : côté navigateur, +// Vite le remplace par un module qui lève une erreur à tout accès. Il emploie +// expect, et prend test et describe chez Vitest plutôt que dans +// test/lanceur.js, dont le repli vers node:test ne vaut que pour la série node. +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { cdp } from 'vitest/browser'; +import { flushSync, mount, unmount } from 'svelte'; +import App from './App.svelte'; +import { VERSION } from '../version.genere.js'; + +const TRANSPARENT = 'rgba(0, 0, 0, 0)'; +const THEMES = ['light', 'dark']; + +// Composantes [r, g, b, a] d'une couleur calculée. Chromium la rend sous la +// forme « rgb(r, g, b) » ou « rgba(r, g, b, a) » quand la source est une +// couleur sRGB ; toute autre forme fait échouer l'épreuve plutôt que de la +// laisser passer sans mesure. +function composantes(couleur) { + const m = /^rgba?\((\d+), (\d+), (\d+)(?:, ([\d.]+))?\)$/.exec(couleur); + if (m === null) { + throw new Error(`couleur calculée illisible : ${couleur}`); + } + const alpha = m[4] === undefined ? 1 : Number(m[4]); + return [Number(m[1]), Number(m[2]), Number(m[3]), alpha]; +} + +// Luminance relative d'une couleur sRGB, au sens des WCAG. +function luminance([r, g, b]) { + const lineaire = (c) => { + const s = c / 255; + return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * lineaire(r) + 0.7152 * lineaire(g) + 0.0722 * lineaire(b); +} + +function contraste(a, b) { + const la = luminance(a); + const lb = luminance(b); + return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05); +} + +// L'élément, puis chacun de ses ancêtres jusqu'à la racine du document. +function ascendance(element) { + const chaine = []; + for (let n = element; n !== null; n = n.parentElement) { + chaine.push(n); + } + return chaine; +} + +// Fond peint sous un élément : le premier fond non transparent de son +// ascendance, ou null. +function fondPeint(element) { + for (const n of ascendance(element)) { + const fond = composantes(getComputedStyle(n).backgroundColor); + if (fond[3] !== 0) { + return fond; + } + } + return null; +} + +// Éléments du sous-arbre qui portent eux-mêmes un nœud de texte non vide. +function porteursDeTexte(racine) { + return [racine, ...racine.querySelectorAll('*')].filter((el) => + [...el.childNodes].some( + (n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== '', + ), + ); +} + +// Nom lisible d'un élément dans un message d'échec. +function designation(element) { + const classe = element.getAttribute('class'); + return classe ? `${element.localName}.${classe}` : element.localName; +} + +// Exécute mesure() sous le thème demandé et rend sa valeur. L'émulation est +// levée ensuite, même quand mesure() échoue : le test suivant retrouve la +// préférence propre du navigateur. +async function sousTheme(theme, mesure) { + await cdp().send('Emulation.setEmulatedMedia', { + features: [{ name: 'prefers-color-scheme', value: theme }], + }); + try { + expect(matchMedia('(prefers-color-scheme: dark)').matches, theme).toBe( + theme === 'dark', + ); + return mesure(); + } finally { + await cdp().send('Emulation.setEmulatedMedia', { features: [] }); + } +} + +let composant; + +beforeEach(() => { + composant = mount(App, { target: document.body }); + flushSync(); +}); + +afterEach(() => { + unmount(composant); +}); + +function bandeau() { + return document.body.querySelector('header.bandeau'); +} + +// Le bandeau, puis chacun des éléments qui y portent un texte. +function bandeauEtTextes() { + return [...new Set([bandeau(), ...porteursDeTexte(bandeau())])]; +} + +describe("bandeau d'application", () => { + test('existe et porte la version affichée', () => { + expect(bandeau()).not.toBeNull(); + expect(bandeau().textContent).toContain(VERSION.affichee); + }); + + test('le bandeau et chacun de ses textes sont visibles dans la fenêtre, dans les deux thèmes (§ 14.2, § 18.6)', async () => { + // Le texte, le titre et le style calculé se relisent sur un élément qui + // ne peint rien. L'épreuve interroge donc la mise en page : chaque élément + // est visible pour checkVisibility (display, visibility, opacité nulle, + // content-visibility), sa boîte est non vide et tient dans la fenêtre, et + // le point central de cette boîte atteint l'élément ou l'un de ses + // descendants — un élément recouvert, ou rogné par un ancêtre, laisse le + // point à un autre. elementFromPoint suit l'atteinte du pointeur : un + // élément en pointer-events: none n'y répond pas, et l'épreuve le refuse. + for (const theme of THEMES) { + await sousTheme(theme, () => { + const elements = bandeauEtTextes(); + expect(elements.length).toBeGreaterThan(1); + for (const element of elements) { + const lieu = `${theme} : ${designation(element)}`; + expect( + element.checkVisibility({ opacityProperty: true, visibilityProperty: true }), + `${lieu} : checkVisibility`, + ).toBe(true); + const boite = element.getBoundingClientRect(); + expect(boite.width, `${lieu} : largeur`).toBeGreaterThan(0); + expect(boite.height, `${lieu} : hauteur`).toBeGreaterThan(0); + expect(boite.left, `${lieu} : bord gauche`).toBeGreaterThanOrEqual(0); + expect(boite.top, `${lieu} : bord haut`).toBeGreaterThanOrEqual(0); + expect(boite.right, `${lieu} : bord droit`).toBeLessThanOrEqual(innerWidth); + expect(boite.bottom, `${lieu} : bord bas`).toBeLessThanOrEqual(innerHeight); + const atteint = document.elementFromPoint( + boite.left + boite.width / 2, + boite.top + boite.height / 2, + ); + expect( + element.contains(atteint), + `${lieu} : le centre de sa boîte atteint ${atteint === null ? 'aucun élément' : designation(atteint)}`, + ).toBe(true); + } + }); + } + }); + + test('porte la provenance de la construction', () => { + expect(import.meta.env.MODE).toBe('test'); + expect(bandeau().textContent).toContain('épreuve'); + }); + + test('le titre de la fenêtre porte le nom et la version affichée', () => { + expect(document.title).toBe( + `Gestion table tournante Libre — ${VERSION.affichee}`, + ); + }); + + test('le texte et le fond du bandeau ont une couleur calculée', () => { + const style = getComputedStyle(bandeau()); + expect(style.color).not.toBe(TRANSPARENT); + expect(style.backgroundColor).not.toBe(TRANSPARENT); + }); + + test('chaque texte du bandeau tient 4,5:1 sur un fond opaque, dans les deux thèmes (§ 14.5)', async () => { + const couleursParTheme = []; + for (const theme of THEMES) { + const couleurs = await sousTheme(theme, () => { + const textes = porteursDeTexte(bandeau()); + expect(textes.length).toBeGreaterThan(0); + for (const element of textes) { + const lieu = `${theme} : ${designation(element)}`; + const texte = composantes(getComputedStyle(element).color); + const fond = fondPeint(element); + expect(fond, lieu).not.toBeNull(); + expect(texte[3], lieu).toBe(1); + expect(fond[3], lieu).toBe(1); + expect(contraste(texte, fond), lieu).toBeGreaterThanOrEqual(4.5); + } + return [document.body, bandeau()].map((el) => { + const style = getComputedStyle(el); + return `${style.color} sur ${style.backgroundColor}`; + }); + }); + couleursParTheme.push(couleurs); + } + // Une émulation sans effet, ou un thème sombre qui ne redéfinit aucune + // couleur, ferait mesurer deux fois le thème clair. + expect( + couleursParTheme[1], + `mêmes couleurs dans les deux thèmes : ${couleursParTheme[0].join(' ; ')}`, + ).not.toEqual(couleursParTheme[0]); + }); + + test("aucune opacité partielle, aucun filtre ni mode de fusion sous un texte, dans les deux thèmes (§ 14.5)", async () => { + // opacity et filter ne s'héritent pas : posés sur un ancêtre, ils changent + // le pixel peint sans changer le style calculé du texte. Chaque niveau de + // l'ascendance est donc relu, jusqu'à la racine du document. + for (const theme of THEMES) { + await sousTheme(theme, () => { + const textes = porteursDeTexte(bandeau()); + expect(textes.length).toBeGreaterThan(0); + for (const element of textes) { + for (const n of ascendance(element)) { + const style = getComputedStyle(n); + const lieu = `${theme} : ${designation(n)} sous ${designation(element)}`; + expect(style.opacity, lieu).toBe('1'); + expect(style.filter, lieu).toBe('none'); + expect(style.backdropFilter, lieu).toBe('none'); + expect(style.mixBlendMode, lieu).toBe('normal'); + } + } + }); + } + }); +}); diff --git a/src/interface/App.svelte b/src/interface/App.svelte new file mode 100644 index 0000000..2164260 --- /dev/null +++ b/src/interface/App.svelte @@ -0,0 +1,52 @@ + + + + + + {titre} + + +
+ {titre} + {provenance} +
+ + diff --git a/src/interface/jetons.css b/src/interface/jetons.css new file mode 100644 index 0000000..ee04242 --- /dev/null +++ b/src/interface/jetons.css @@ -0,0 +1,42 @@ +/* © 2026 TechnoLibre (http://www.technolibre.ca) + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) */ + +/* Jetons de l'interface : couleurs, typographie, espacements, rayons. Le thème + suit prefers-color-scheme ; chaque thème redéfinit les mêmes jetons de + couleur, et les composants ne lisent que les jetons (§ 13.3). + + Chaque couleur de texte tient au moins 4,5:1 sur le fond où elle est peinte, + et aucune n'emploie de canal alpha : la couleur relue dans le style calculé + est alors la couleur peinte (§ 14.5). */ + +:root { + color-scheme: light; + + --couleur-page-fond: #f6f7f9; + --couleur-page-texte: #1c2128; + --couleur-bandeau-fond: #1f3a5f; + --couleur-bandeau-texte: #ffffff; + --couleur-bandeau-filet: #8fb3d9; + + --police-interface: system-ui, sans-serif; + --taille-texte-bandeau: 0.9375rem; + + --espace-1: 0.25rem; + --espace-2: 0.5rem; + --espace-3: 0.75rem; + --espace-4: 1rem; + + --rayon-1: 0.25rem; +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + + --couleur-page-fond: #121519; + --couleur-page-texte: #e3e6ea; + --couleur-bandeau-fond: #1d2c3f; + --couleur-bandeau-texte: #f2f4f7; + --couleur-bandeau-filet: #5b7fa6; + } +} diff --git a/src/interface/libelles.js b/src/interface/libelles.js new file mode 100644 index 0000000..5d5132c --- /dev/null +++ b/src/interface/libelles.js @@ -0,0 +1,37 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Table des chaînes visibles de l'interface. Un composant n'écrit aucune +// chaîne visible en dur : il les lit ici, seul endroit qu'une traduction +// remplace (§ 14.6). + +const NOM_APPLICATION = 'Gestion table tournante Libre'; + +// Mode de construction de Vite → provenance affichée dans le bandeau +// (§ 18.6). Une Map ne répond qu'aux clés qu'on y a posées : un mode nommé +// comme une propriété héritée d'Object ne rend pas une fonction. +const PROVENANCES = new Map([ + ['developpement', 'développement'], + ['livraison', 'livraison'], + ['documentation', 'documentation'], + ['test', 'épreuve'], +]); + +// Libellé de la provenance pour un mode de Vite. Un mode sans libellé, comme +// celui d'un « vite build » lancé sans --mode, s'affiche sous son propre nom. +export function libelleProvenance(mode) { + return PROVENANCES.get(mode) ?? mode; +} + +// Vrai quand le mode a un libellé déclaré. Comparer libelleProvenance(mode) à +// mode ne le dit pas : « livraison » et « documentation » ont un libellé +// identique à leur nom. +export function aUnLibelle(mode) { + return PROVENANCES.has(mode); +} + +// Nom de l'application suivi de la version affichée : texte du bandeau et +// titre de la fenêtre (§ 18.6). +export function titreAvecVersion(version) { + return `${NOM_APPLICATION} — ${version}`; +} diff --git a/src/interface/libelles.test.js b/src/interface/libelles.test.js new file mode 100644 index 0000000..86e0bd9 --- /dev/null +++ b/src/interface/libelles.test.js @@ -0,0 +1,50 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Chaque mode que pose un script npm (« --mode … » dans package.json) a son +// libellé de provenance, comme le mode test que pose Vitest ; un mode sans +// libellé s'affiche sous son propre nom plutôt que de laisser le bandeau muet +// sur la provenance. +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { describe, test } from '../../test/lanceur.js'; +import { aUnLibelle, libelleProvenance, titreAvecVersion } from './libelles.js'; + +// Modes nommés par les scripts de package.json, triés, sans doublon. +function modesDesScripts() { + const paquet = JSON.parse( + readFileSync(new URL('../../package.json', import.meta.url), 'utf8'), + ); + const modes = new Set(); + for (const commande of Object.values(paquet.scripts)) { + for (const [, mode] of commande.matchAll(/--mode[= ](\S+)/g)) { + modes.add(mode); + } + } + return [...modes].sort(); +} + +describe('libellés', () => { + test('libelleProvenance nomme les quatre modes de construction', () => { + assert.equal(libelleProvenance('developpement'), 'développement'); + assert.equal(libelleProvenance('livraison'), 'livraison'); + assert.equal(libelleProvenance('documentation'), 'documentation'); + assert.equal(libelleProvenance('test'), 'épreuve'); + }); + + test('chaque mode que pose un script npm a un libellé de provenance', () => { + const modes = modesDesScripts(); + assert.ok(modes.length > 0, 'aucun « --mode » dans les scripts de package.json'); + assert.deepEqual(modes.filter((mode) => !aUnLibelle(mode)), []); + }); + + test('libelleProvenance rend un mode sans libellé sous son propre nom', () => { + assert.equal(libelleProvenance('production'), 'production'); + assert.equal(libelleProvenance('constructor'), 'constructor'); + assert.equal(aUnLibelle('constructor'), false); + }); + + test('titreAvecVersion joint le nom et la version par un tiret cadratin', () => { + assert.equal(titreAvecVersion('X'), 'Gestion table tournante Libre — X'); + }); +}); diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..cdbaa06 --- /dev/null +++ b/src/main.js @@ -0,0 +1,8 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Point d'entrée de la page : monte l'interface dans le corps du document. +import { mount } from 'svelte'; +import App from './interface/App.svelte'; + +mount(App, { target: document.body }); diff --git a/src/version.genere.js b/src/version.genere.js new file mode 100644 index 0000000..438f81e --- /dev/null +++ b/src/version.genere.js @@ -0,0 +1,8 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Engendré par scripts/version.js depuis version.json — ne pas modifier. +export const VERSION = Object.freeze({ + affichee: '2026.10.05.01', + technique: '2026.1005.1', +}); diff --git a/test/construction.long.test.js b/test/construction.long.test.js new file mode 100644 index 0000000..d3c3a32 --- /dev/null +++ b/test/construction.long.test.js @@ -0,0 +1,109 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le paquet livré n'emporte rien de l'outillage d'épreuve (§ 14.9, § 14.15) : +// ni fichier de test, ni le lanceur, ni donnée d'épreuve, ni paquet d'un outil +// d'épreuve. Le test construit la page par l'API de Vite, avec la +// configuration et le mode de la livraison, dans un répertoire temporaire ; +// il relit les modules que contient le paquet, les noms d'origine de chaque +// actif, puis le chemin et le contenu de chaque fichier écrit. +import assert from 'node:assert/strict'; +import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'vite'; +import { describe, test } from './lanceur.js'; + +const RACINE = fileURLToPath(new URL('..', import.meta.url)); + +// Paquets qui ne servent qu'aux épreuves. +const OUTILS_EPREUVE = + /[\\/]node_modules[\\/](?:vitest|@vitest|playwright|playwright-core|fast-check)[\\/]/; + +// Vrai quand un module vient de l'outillage d'épreuve : l'arbre test/ +// (lanceur, données d'épreuve), un fichier *.test.js rangé à côté de son +// module, un paquet d'épreuve, ou un module de Node que Vite remplace pour le +// navigateur, comme node:assert. +function moduleDEpreuve(id) { + return ( + id.startsWith(join(RACINE, 'test') + sep) || + /\.test\.[cm]?js(?:\?|$)/.test(id) || + OUTILS_EPREUVE.test(id) || + id.includes('__vite-browser-external') + ); +} + +// Traces de l'outillage dans le chemin ou le contenu d'un fichier écrit. Les +// motifs portent l'extension : le domaine emploie le mot « lanceur » (lanceur +// portable, § 8.6), et un code minifié peut écrire « x.test.call ». +const TRACES_EPREUVE = [/vitest/i, /node:assert/, /lanceur\.js/, /\.test\.[cm]?js/]; + +// Construit la page comme la livraison : vite.config.js, mode livraison, +// NODE_ENV=production. Vitest pose NODE_ENV=test, dont Vite déduirait une +// construction de développement — composants compilés en mode dev, paquets +// résolus sous la condition « development ». +async function construireLivraison(sortie) { + const nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + try { + return await build({ + root: RACINE, + configFile: join(RACINE, 'vite.config.js'), + mode: 'livraison', + logLevel: 'error', + // Sans incrustation, chaque actif sort en fichier et garde ses noms + // d'origine ; incrusté en data:, il ne laisserait aucune trace lisible. + build: { outDir: sortie, emptyOutDir: true, assetsInlineLimit: 0 }, + }); + } finally { + if (nodeEnv === undefined) delete process.env.NODE_ENV; + else process.env.NODE_ENV = nodeEnv; + } +} + +describe('construction', () => { + test("le paquet livré n'emporte aucun fichier ni module d'épreuve (§ 14.9)", async () => { + const sortie = mkdtempSync(join(tmpdir(), 'construction-')); + try { + const resultat = await construireLivraison(sortie); + const sorties = (Array.isArray(resultat) ? resultat : [resultat]).flatMap( + (r) => r.output, + ); + const modules = sorties + .filter((s) => s.type === 'chunk') + .flatMap((s) => s.moduleIds); + assert.ok( + modules.some((id) => id.startsWith(join(RACINE, 'src') + sep)), + `aucun module de src/ parmi ${modules.length} modules examinés`, + ); + assert.deepEqual(modules.filter(moduleDEpreuve), []); + + // Un actif n'est pas un module : moduleIds ne le voit pas. Une donnée + // jointe par new URL(…, import.meta.url) n'apparaît que là. + const actifs = sorties + .filter((s) => s.type === 'asset') + .flatMap((s) => (s.originalFileNames ?? []).map((n) => resolve(RACINE, n))); + assert.ok(actifs.length > 0, `aucun actif d'origine connue parmi ${sorties.length} sorties`); + assert.deepEqual(actifs.filter(moduleDEpreuve), []); + + const fichiers = readdirSync(sortie, { recursive: true, withFileTypes: true }) + .filter((e) => e.isFile()) + .map((e) => join(e.parentPath, e.name)); + assert.ok( + fichiers.some((f) => f.endsWith('.js')), + `aucun script parmi ${fichiers.length} fichiers écrits`, + ); + const traces = fichiers.flatMap((f) => { + const nom = relative(sortie, f); + const contenu = readFileSync(f, 'utf8'); + return TRACES_EPREUVE.filter((m) => m.test(nom) || m.test(contenu)).map( + (m) => `${nom} : ${m}`, + ); + }); + assert.deepEqual(traces, []); + } finally { + rmSync(sortie, { recursive: true, force: true }); + } + }); +}); diff --git a/test/lanceur.js b/test/lanceur.js new file mode 100644 index 0000000..1e92ba5 --- /dev/null +++ b/test/lanceur.js @@ -0,0 +1,7 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Point d'import unique des primitives de test. Remplacer cette ligne par +// « export { test, describe } from 'node:test'; » fait tourner la série node +// sous « node --test » sans toucher aux fichiers de test (§ 14.8). +export { test, describe } from 'vitest'; diff --git a/test/licence.test.js b/test/licence.test.js new file mode 100644 index 0000000..1e305bc --- /dev/null +++ b/test/licence.test.js @@ -0,0 +1,146 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Chaque fichier source du projet s'ouvre sur l'en-tête de licence : la ligne +// du titulaire, puis celle de la licence, après un éventuel shebang ou la +// déclaration . Les deux lignes sont des commentaires du +// langage du fichier ; le texte ne varie pas. package.json déclare la même +// licence et le même titulaire, que reprennent les outils d'emballage. +import assert from 'node:assert/strict'; +import { readdirSync, readFileSync } from 'node:fs'; +import { extname, join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, test } from './lanceur.js'; + +const RACINE = fileURLToPath(new URL('..', import.meta.url)); + +const TITULAIRE = /^© \d{4} TechnoLibre \(http:\/\/www\.technolibre\.ca\)$/; +const LICENCE = 'License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)'; + +// Marqueurs de commentaire, par extension de fichier source. Un commentaire +// de ligne n'a pas de fermant. +const COMMENTAIRE = new Map([ + ['.js', { ouvrant: '//', fermant: null }], + ['.cjs', { ouvrant: '//', fermant: null }], + ['.mjs', { ouvrant: '//', fermant: null }], + ['.sh', { ouvrant: '#', fermant: null }], + ['.svelte', { ouvrant: '' }], + ['.html', { ouvrant: '' }], + ['.css', { ouvrant: '/*', fermant: '*/' }], +]); + +// Périmètre : les fichiers de la racine et les arbres des sources, des +// épreuves, des scripts et de la coquille. Les dépendances et les sorties de +// construction n'y entrent pas, ni les données d'épreuve, qui sont des +// données et non des sources. Le séparateur final borne l'exclusion au +// répertoire test/fixtures/ : un module test/fixtures.js reste une source. +const ARBRES = ['src', 'test', 'scripts', 'electron']; +const DONNEES_EPREUVE = join(RACINE, 'test', 'fixtures') + sep; + +function fichiersSources() { + const chemins = readdirSync(RACINE, { withFileTypes: true }) + .filter((e) => e.isFile()) + .map((e) => join(RACINE, e.name)); + for (const arbre of ARBRES) { + const dossier = join(RACINE, arbre); + let entrees; + try { + entrees = readdirSync(dossier, { withFileTypes: true, recursive: true }); + } catch (erreur) { + if (erreur.code === 'ENOENT') continue; + throw erreur; + } + for (const e of entrees) { + if (e.isFile()) chemins.push(join(e.parentPath, e.name)); + } + } + return chemins + .filter((chemin) => COMMENTAIRE.has(extname(chemin))) + .filter((chemin) => !chemin.startsWith(DONNEES_EPREUVE)) + .sort(); +} + +// Vrai quand les deux lignes sont des commentaires, et que le commentaire ne +// se prolonge pas au-delà de la seconde. Un commentaire de ligne ouvre +// chacune d'elles. Un commentaire de bloc court sur les deux — ouvert sur la +// première sans s'y fermer, fermé en fin de seconde — ou s'ouvre et se ferme +// sur chacune. Une seconde ligne hors commentaire se lit comme du code : un +// texte affiché avant le bandeau dans un composant, une règle de style +// invalidée dans une feuille, une commande dans un script. +function enCommentaire(premiere, seconde, { ouvrant, fermant }) { + const suite = seconde.trim(); + if (!premiere.startsWith(ouvrant)) return false; + if (fermant === null) return suite.startsWith(ouvrant); + const courant = !premiere.includes(fermant) && suite.endsWith(fermant); + const repete = + premiere.trimEnd().endsWith(fermant) && + suite.startsWith(ouvrant) && + suite.endsWith(fermant); + return courant || repete; +} + +// Vrai quand le contenu s'ouvre sur l'en-tête, après un éventuel shebang ou +// la déclaration : deux lignes de commentaire du langage de +// l'extension, dont le texte, débarrassé des marqueurs, est celui de +// l'en-tête. +function porteLEnTete(contenu, extension) { + let lignes = contenu.split('\n'); + if (lignes[0].startsWith('#!')) lignes = lignes.slice(1); + if (/^$/i.test(lignes[0])) lignes = lignes.slice(1); + const [premiere = '', seconde = ''] = lignes; + const texte = (ligne) => + ligne + .replace(/^\s*(?:\/\/|#||\*\/)?\s*$/, ''); + return ( + enCommentaire(premiere, seconde, COMMENTAIRE.get(extension)) && + TITULAIRE.test(texte(premiere)) && + texte(seconde) === LICENCE + ); +} + +describe('licence', () => { + test("chaque fichier source s'ouvre sur l'en-tête de licence", () => { + const fichiers = fichiersSources(); + assert.ok(fichiers.length > 0, 'aucun fichier source examiné'); + const sansEnTete = fichiers + .filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), extname(chemin))) + .map((chemin) => relative(RACINE, chemin)); + assert.deepEqual(sansEnTete, []); + }); + + test("l'en-tête tient en deux lignes de commentaire du langage du fichier", () => { + const titulaire = '© 2026 TechnoLibre (http://www.technolibre.ca)'; + // [extension, contenu, admis] : les formes admises ; puis des secondes + // lignes laissées hors commentaire, que le langage lirait comme du code, + // et un commentaire qui se prolonge au-delà de la seconde ligne. + const cas = [ + ['.js', `// ${titulaire}\n// ${LICENCE}\n`, true], + ['.sh', `#!/bin/sh\n# ${titulaire}\n# ${LICENCE}\n`, true], + ['.svelte', `\n`, true], + ['.svelte', `\n\n`, true], + ['.html', `\n\n`, true], + ['.css', `/* ${titulaire}\n ${LICENCE} */\n`, true], + ['.css', `/* ${titulaire} */\n/* ${LICENCE} */\n`, true], + ['.js', `// ${titulaire}\n${LICENCE}\n`, false], + ['.sh', `#!/bin/sh\n# ${titulaire}\n${LICENCE}\n`, false], + ['.svelte', `\n${LICENCE}\n`, false], + ['.html', `\n\n${LICENCE}\n`, false], + ['.html', `\n`, false], + ['.css', `/* ${titulaire} */\n${LICENCE}\n`, false], + ['.css', `/* ${titulaire} */\n * ${LICENCE} */\n`, false], + ]; + const ecarts = cas + .filter(([extension, contenu, admis]) => porteLEnTete(contenu, extension) !== admis) + .map(([extension, contenu, admis]) => + `${extension} ${admis ? 'refusé' : 'admis'} : ${JSON.stringify(contenu)}`, + ); + assert.deepEqual(ecarts, []); + }); + + test('package.json déclare la licence et le titulaire', () => { + const paquet = JSON.parse(readFileSync(join(RACINE, 'package.json'), 'utf8')); + assert.equal(paquet.license, 'AGPL-3.0-or-later'); + assert.equal(paquet.author, 'TechnoLibre (http://www.technolibre.ca)'); + }); +}); diff --git a/test/projets.test.js b/test/projets.test.js new file mode 100644 index 0000000..c140739 --- /dev/null +++ b/test/projets.test.js @@ -0,0 +1,33 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le projet node n'expose aucun DOM et ne compile aucun composant : un module +// qui toucherait document ou window, ou qui importerait un composant Svelte, +// y échoue au lieu de passer sur un environnement que la série node ne doit +// pas fournir (§ 14.4, § 14.8). +import assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { describe, test } from './lanceur.js'; + +describe('projet node', () => { + test('globalThis.document vaut undefined', () => { + assert.equal(globalThis.document, undefined); + }); + + test('globalThis.window vaut undefined', () => { + assert.equal(globalThis.window, undefined); + }); + + test('un composant Svelte ne se charge pas', async () => { + // Le composant existe, et son import échoue sur la lecture du source : + // sans greffon Svelte, l'analyse des imports de Vite rejette la syntaxe + // d'un .svelte, et sous « node --test » Node en refuse l'extension. Un + // refus venu d'ailleurs, comme un module qui lirait window au chargement, + // fait échouer l'épreuve au lieu de la satisfaire. + assert.ok(existsSync(new URL('../src/interface/App.svelte', import.meta.url))); + await assert.rejects( + import('../src/interface/App.svelte'), + /Failed to parse source for import analysis|Unknown file extension/, + ); + }); +}); diff --git a/version.json b/version.json new file mode 100644 index 0000000..0ef39de --- /dev/null +++ b/version.json @@ -0,0 +1 @@ +{ "version": "2026.10.05.01" } diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..372a00b --- /dev/null +++ b/vite.config.js @@ -0,0 +1,18 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Construit la page que servent la plateforme web de Capacitor et la coquille +// Electron. La base relative fait commencer chaque chemin d'actif par « ./ » : +// la coquille charge la page par file://, où un chemin absolu « /assets/… » +// désigne la racine du disque. +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + base: './', + plugins: [svelte()], + build: { + outDir: 'www', + emptyOutDir: true, + }, +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..03fe9aa --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,63 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Un seul lanceur, trois projets (§ 14.4, § 14.8). +// +// node fonctions pures, sans DOM et sans greffon Svelte : un module qui +// importerait un composant échoue au chargement. +// node-long épreuves lourdes, hors de la série surveillée. Leur durée croît +// avec l'application : chacune dispose de deux minutes, là où le +// délai par défaut de Vitest, 5 s, la ferait échouer par +// dépassement et non sur un défaut. Ce délai arrête un test +// bloqué ; il n'est pas le plafond de la série (§ 14.14). +// navigateur un vrai moteur de rendu. Le projet étend vite.config.js : un +// module s'y résout par les mêmes greffons et réglages que dans la +// construction du livrable, sans seconde configuration à tenir +// d'accord avec la première. +import { defineConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; + +export default defineConfig({ + test: { + // Sous surveillance, une épreuve se relance quand un module de son graphe + // d'imports change. Les fichiers des motifs se lisent hors de tout + // graphe : le contrôle de version (§ 18.5) lit les premiers par le + // système de fichiers ; la coquille s'exécute dans un processus à part, + // la configuration d'electron-builder se charge par un require natif et + // lit capacitor.config.json, et le script de construction se lance + // depuis une copie. Chaque motif rattache ses fichiers aux épreuves qui + // les lisent, et au contrôle de version quand son point 6 les balaie. Un + // fichier qui répond à un motif ne relance que les épreuves nommées, et + // plus celles qui l'importeraient : un motif ne nomme donc aucun fichier + // qu'une épreuve importe. L'option se lit au niveau du lanceur : posée + // dans un projet, elle est ignorée sans avertissement. + watchTriggerPatterns: [ + { pattern: /(^|\/)(version\.json|package-lock\.json|CHANGELOG\.md|README\.md|GUIDE-[^/]*\.md)$/, + testsToRun: () => 'scripts/version.test.js' }, + { pattern: /(^|\/)electron\/[^/]+$/, + testsToRun: () => ['test/coquille.test.js', 'test/livrable.test.js', 'scripts/version.test.js'] }, + { pattern: /(^|\/)test\/electron_factice\.js$/, + testsToRun: () => ['test/coquille.test.js', 'scripts/version.test.js'] }, + { pattern: /(^|\/)(electron-builder\.config\.cjs|capacitor\.config\.json)$/, + testsToRun: () => ['test/livrable.test.js', 'scripts/construire_windows.test.js', 'scripts/version.test.js'] }, + { pattern: /(^|\/)scripts\/construire_windows\.sh$/, + testsToRun: () => ['scripts/construire_windows.test.js', 'scripts/version.test.js'] }, + ], + projects: [ + { test: { + name: 'node', environment: 'node', + include: ['src/**/*.test.js', 'scripts/**/*.test.js', 'test/**/*.test.js'], + exclude: ['**/*.long.test.js', '**/*.navigateur.test.js', 'node_modules/**'] } }, + { test: { + name: 'node-long', environment: 'node', + include: ['src/**/*.long.test.js', 'test/**/*.long.test.js'], + testTimeout: 120_000 } }, + { extends: './vite.config.js', + test: { + name: 'navigateur', + include: ['src/**/*.navigateur.test.js'], + browser: { enabled: true, headless: true, provider: playwright(), + instances: [{ browser: 'chromium' }] } } }, + ], + }, +});