diff --git a/www/package-lock.json b/www/package-lock.json index 3cab52375..2f4cbda1a 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -35,7 +35,8 @@ "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.10" }, "engines": { "node": ">=22.0.0 <23.0.0", @@ -59,6 +60,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -70,6 +72,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -80,6 +83,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -213,6 +217,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -251,6 +256,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -267,6 +273,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -283,6 +290,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -299,6 +307,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -315,6 +324,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -331,6 +341,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -347,6 +358,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -363,6 +375,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -379,6 +392,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -395,6 +409,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -411,6 +426,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -427,6 +443,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -443,6 +460,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -461,6 +479,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -477,6 +496,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -517,9 +537,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha1-p5tV26+GBIEvUtFAssmrQbwVC7g=", "devOptional": true, "license": "MIT" }, @@ -1602,9 +1622,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1622,9 +1639,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1642,9 +1656,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1662,9 +1673,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1815,12 +1823,24 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1828,6 +1848,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1911,6 +1938,129 @@ "node": ">=20" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha1-eZwG/ES7DPfieEE3tifFzBcyhdQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha1-JBOYerTNf6HCthS0BMQHv2rR6tE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "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/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha1-dVQucnOgjMEP1Nja1OPrHxbNlYw=", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha1-/r8KIakWhCFCLRlVNw5gb+q2A1U=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha1-fj6f7H1NRyMuSTz9y9IXDeQ3HAQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha1-XAv6l7Vrup43QDyXbbd2/2q1b2U=", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha1-/8cQVfGL/Msf0FhjZevCgkiS5AM=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -1964,6 +2114,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-sema": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", @@ -2144,6 +2304,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-6.2.2.tgz", + "integrity": "sha1-rkG1LJrKh3NFBTYnF/MlX6zaNg4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2239,6 +2409,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha1-S1YPZJ/E6RjdCrdc9JYei8iC2Co=", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2338,6 +2515,13 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha1-W/LfBpmdu+XwBqX0ahH7n1t7ORs=", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2380,6 +2564,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2433,6 +2627,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2503,7 +2698,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -2556,6 +2751,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2576,6 +2772,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2596,6 +2793,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2616,6 +2814,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2636,6 +2835,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2656,6 +2856,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2676,6 +2877,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2696,6 +2898,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2716,6 +2919,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2736,6 +2940,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2756,6 +2961,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -3055,6 +3261,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3366,6 +3579,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha1-MudscLeXJOO7Vny51UPrhYzPrzA=", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3391,6 +3611,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha1-jr4OxgSFZoq0ciezEvQlTN+AydM=", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-object": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", @@ -3590,6 +3824,23 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha1-EDyfi6bXI3pHq23R3P93JRhjQms=", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha1-rkW7Lt69qUxw9OqJfg8SQ+Rw23E=", + "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", @@ -3607,6 +3858,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha1-HYpiOJP5XPCi3bnl0RFQ4ZFAlCE=", + "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", @@ -3628,14 +3889,14 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3781,6 +4042,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha1-fpKF7+JksRZwULejp/80eI4bevw=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.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/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3799,6 +4150,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha1-o/aalxB/SUs83Dvd3Yg6fWXOvwQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/www/package.json b/www/package.json index 953e8eeab..3f6886a16 100644 --- a/www/package.json +++ b/www/package.json @@ -22,7 +22,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", - "lint": "prettier --check ." + "lint": "prettier --check .", + "test": "vitest run" }, "devDependencies": { "@sveltejs/adapter-vercel": "^6.3.3", @@ -48,7 +49,8 @@ "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.10" }, "dependencies": { "@vercel/analytics": "^2.0.1" diff --git a/www/src/app.d.ts b/www/src/app.d.ts index 9d100de4a..74e1e6f17 100644 --- a/www/src/app.d.ts +++ b/www/src/app.d.ts @@ -4,7 +4,9 @@ declare global { namespace App { // interface Error {} // interface Locals {} - // interface PageData {} + interface PageData { + cliLinks: import('$lib/cli-release').CliLinks; + } // interface PageState {} interface Platform { env: { diff --git a/www/src/lib/cli-release.test.ts b/www/src/lib/cli-release.test.ts new file mode 100644 index 000000000..1ce9569ef --- /dev/null +++ b/www/src/lib/cli-release.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { resolveCliLinks } from './cli-release'; + +const RELEASES_PAGE = 'https://github.com/microsoft/Foundry-Local/releases'; + +function makeRelease(tag: string, publishedAt: string | null, opts: { draft?: boolean } = {}) { + return { + tag_name: tag, + draft: opts.draft ?? false, + prerelease: true, + published_at: publishedAt, + html_url: `https://github.com/microsoft/Foundry-Local/releases/tag/${tag}` + }; +} + +// Minimal fetch stubs. Types are loosened because resolveCliLinks only reads `ok` and `json()`. +const okFetch = (body: unknown) => + (async () => ({ ok: true, json: async () => body })) as unknown as typeof fetch; +const statusFetch = (ok: boolean) => + (async () => ({ ok, json: async () => [] })) as unknown as typeof fetch; +const throwingFetch = () => + (async () => { + throw new Error('network down'); + }) as unknown as typeof fetch; + +describe('resolveCliLinks', () => { + it('resolves the newest cli-preview release by published date, not array order', async () => { + const releases = [ + makeRelease('cli-preview-0.10.0', '2026-06-04T20:35:47Z'), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z'), + makeRelease('cli-preview-0.10.1', '2026-06-22T23:45:41Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + expect(links.version).toBe('0.10.2'); + expect(links.releasePage).toBe( + 'https://github.com/microsoft/Foundry-Local/releases/tag/cli-preview-0.10.2' + ); + }); + + it('ignores non cli-preview tags and draft releases', async () => { + const releases = [ + makeRelease('v-unrelated', '2027-01-01T00:00:00Z'), + makeRelease('cli-preview-0.11.0', '2026-08-01T00:00:00Z', { draft: true }), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + }); + + it('treats a null published_at as oldest, deterministically, regardless of array order', async () => { + const releases = [ + makeRelease('cli-preview-0.10.3', null), + makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z') + ]; + const links = await resolveCliLinks(okFetch(releases)); + expect(links.tag).toBe('cli-preview-0.10.2'); + }); + + it('sends a bearer Authorization header only when a token is provided', async () => { + const seen: Array | undefined> = []; + const spyFetch = ((_url: string, init?: { headers?: Record }) => { + seen.push(init?.headers); + return Promise.resolve({ + ok: true, + json: async () => [makeRelease('cli-preview-0.10.2', '2026-07-14T21:39:03Z')] + }); + }) as unknown as typeof fetch; + + await resolveCliLinks(spyFetch, 'secret-token'); + await resolveCliLinks(spyFetch); + + expect(seen[0]?.Authorization).toBe('Bearer secret-token'); + expect(seen[1]?.Authorization).toBeUndefined(); + }); + + it('falls back to the releases page when the API responds non-OK', async () => { + const links = await resolveCliLinks(statusFetch(false)); + expect(links.tag).toBeNull(); + expect(links.version).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back to the releases page when no cli-preview release exists', async () => { + const links = await resolveCliLinks(okFetch([makeRelease('v1.0.0', '2027-01-01T00:00:00Z')])); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back to the releases page when fetch throws', async () => { + const links = await resolveCliLinks(throwingFetch()); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); + + it('falls back when the API returns a non-array error object (e.g. rate limited)', async () => { + const links = await resolveCliLinks(okFetch({ message: 'API rate limit exceeded' })); + expect(links.tag).toBeNull(); + expect(links.releasePage).toBe(RELEASES_PAGE); + }); +}); diff --git a/www/src/lib/cli-release.ts b/www/src/lib/cli-release.ts new file mode 100644 index 000000000..0afd938e0 --- /dev/null +++ b/www/src/lib/cli-release.ts @@ -0,0 +1,73 @@ +// Resolves the latest `cli-preview-*` GitHub release at build time so the site never hardcodes a +// specific preview tag (issue #924). Returns the release's page URL; the platform download buttons +// link there and let the user pick their architecture/variant (mirroring the arch-safe `winget`/ +// `brew` install path in platform.ts). If release discovery fails, it falls back to the general +// releases page. + +const OWNER_REPO = 'microsoft/Foundry-Local'; +const RELEASES_PAGE = `https://github.com/${OWNER_REPO}/releases`; +const RELEASES_API = `https://api.github.com/repos/${OWNER_REPO}/releases?per_page=30`; +const CLI_TAG_PREFIX = 'cli-preview-'; + +export interface CliLinks { + /** Release tag (e.g. "cli-preview-0.10.2"), or null when discovery failed. */ + tag: string | null; + /** Version portion of the tag (e.g. "0.10.2"), or null when discovery failed. */ + version: string | null; + /** The resolved release's page, or the general releases page on fallback. */ + releasePage: string; +} + +interface GitHubRelease { + tag_name: string; + draft: boolean; + published_at: string | null; + html_url: string; +} + +function fallbackLinks(): CliLinks { + return { tag: null, version: null, releasePage: RELEASES_PAGE }; +} + +/** + * Fetch the repo's releases and resolve the newest non-draft `cli-preview-*` release. + * Pass the SvelteKit-provided `fetch` from a server `load` so this runs at build/prerender time. + * An optional `token` (from the build environment) is sent as a bearer credential to raise the + * GitHub API rate limit; it is never required and never exposed to the client. + */ +export async function resolveCliLinks( + fetchFn: typeof fetch = fetch, + token?: string +): Promise { + try { + const headers: Record = { Accept: 'application/vnd.github+json' }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const response = await fetchFn(RELEASES_API, { headers }); + if (!response.ok) { + return fallbackLinks(); + } + + const releases = (await response.json()) as GitHubRelease[]; + const latest = releases + .filter((release) => release.tag_name?.startsWith(CLI_TAG_PREFIX) && !release.draft) + // Coerce invalid/missing published_at to 0 so a null date sorts oldest deterministically. + .sort( + (a, b) => (Date.parse(b.published_at ?? '') || 0) - (Date.parse(a.published_at ?? '') || 0) + )[0]; + + if (!latest) { + return fallbackLinks(); + } + + return { + tag: latest.tag_name, + version: latest.tag_name.slice(CLI_TAG_PREFIX.length) || null, + releasePage: latest.html_url ?? RELEASES_PAGE + }; + } catch { + return fallbackLinks(); + } +} diff --git a/www/src/lib/components/download-dropdown.svelte b/www/src/lib/components/download-dropdown.svelte index ce7a37ef9..591549643 100644 --- a/www/src/lib/components/download-dropdown.svelte +++ b/www/src/lib/components/download-dropdown.svelte @@ -4,9 +4,7 @@ import { Download, Copy, Check, ExternalLink } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { IsMobile } from '$lib/hooks/is-mobile.svelte'; - - const CLI_RELEASE_URL = - 'https://github.com/microsoft/Foundry-Local/releases/tag/cli-preview-0.10.0'; + import { page } from '$app/stores'; interface Props { variant?: 'default' | 'ghost' | 'outline'; @@ -24,6 +22,10 @@ const isMobile = new IsMobile(); + // CLI download links resolve to the latest cli-preview-* release at build time (issue #924), + // falling back to the releases page when discovery fails. Data comes from the root layout load. + const cliLinks = $derived($page.data.cliLinks); + // Platform SVG icons const AppleIcon = ``; @@ -54,26 +56,26 @@ crossPlatformCommand: string; }; - const cliInstallOptions: CliInstallOption[] = [ + const cliInstallOptions: CliInstallOption[] = $derived([ { id: 'windows', label: 'Windows CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: WindowsIcon }, { id: 'macos', label: 'macOS CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: AppleIcon }, { id: 'linux', label: 'Linux CLI', - href: CLI_RELEASE_URL, + href: cliLinks.releasePage, icon: LinuxIcon } - ]; + ]); const sdkInstallOptions: SdkInstallOption[] = [ { @@ -195,7 +197,9 @@
{item.label} - cli-preview-0.10.0 on GitHub + {cliLinks.tag ? `${cliLinks.tag} on GitHub` : 'Download from GitHub'}