git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
51 files changed+2130−500
since splitsnapshots
unifiedsplit
.c8rc.json+22 .github/workflows/ci.yml+7−2 .gitignore+2 LICENSE+21 README.md+73−17 bun.lock+220−113 compile-fail-tests/ClientBadBodyType.purs+12 compile-fail-tests/ClientBadCurriedBodyType.purs+12 compile-fail-tests/ClientBadHeaderType.purs+13 compile-fail-tests/ClientBadOptionalQueryType.purs+13 compile-fail-tests/ClientBadPathType.purs+12 compile-fail-tests/ClientExtraBodyField.purs+12 compile-fail-tests/ClientExtraHeaderField.purs+13 compile-fail-tests/ClientExtraOptionalQueryField.purs+13 compile-fail-tests/ClientExtraPathField.purs+12 compile-fail-tests/ClientMissingBodyArgument.purs+12 compile-fail-tests/ClientMissingBodyField.purs+12 compile-fail-tests/ClientMissingHeaderArgument.purs+13 compile-fail-tests/ClientMissingHeaderField.purs+13 compile-fail-tests/ClientMissingPathField.purs+12 compile-fail-tests/ClientMissingRequiredQuery.purs+13 compile-fail-tests/ClientMissingThirdArgument.purs+13 compile-fail-tests/run.js+101 package.json+15−3 spago.lock+111−189 spago.yaml+18−6 src/Yoga/Fetch/Om.js+1 src/Yoga/Fetch/Om.purs+135−8 src/Yoga/Fetch/Om/BuildUrl.js+86−2 src/Yoga/Fetch/Om/BuildUrl.purs+43−13 src/Yoga/Fetch/Om/ClientFunction.purs+24−15 src/Yoga/Fetch/Om/MakeRequest.purs+6−13 src/Yoga/Fetch/Om/ParseResponse.purs+11−30 src/Yoga/Fetch/Om/ReadOptional.js+18 src/Yoga/Fetch/Om/ReadOptional.purs+56 src/Yoga/Fetch/Om/Simple.purs+6−15 src/Yoga/Fetch/Om/SplitResponses.purs+12−2 src/Yoga/Fetch/Om/StreamDecode.js+12−1 src/Yoga/Fetch/Om/StreamDecode.purs+43−2 test/BodylessResponse.Example.purs+1−1 test/BodylessResponse.Spec.purs+31−4 test/BuildUrl.Spec.purs+74−6 test/BuildUrlProperty.Spec.purs+256 test/Complete.Example.purs+14−19 test/Example.purs+13−8 test/MakeRequest.Spec.purs+15−5 test/PlainTextResponse.Spec.purs+49−8 test/Polymorphic.Spec.purs+21−18 test/Roundtrip.Spec.purs+380 test/StreamDecode.Spec.js+1 test/StreamDecode.Spec.purs+32
.c8rc.jsonnew file+22
@@ -0,0 +1,22 @@
1+{
2+ "all": true,
3+ "include": [
4+ "output/Yoga.Fetch.Om*/index.js",
5+ "output/Yoga.Fetch.Om*/foreign.js"
6+ ],
7+ "exclude": [
8+ "output/Yoga.Fetch.Om.ExtractParams/index.js",
9+ "output/Yoga.Fetch.Om.SplitResponses/index.js"
10+ ],
11+ "reporter": [
12+ "text",
13+ "html",
14+ "lcov"
15+ ],
16+ "reports-dir": "coverage",
17+ "check-coverage": true,
18+ "statements": 95,
19+ "branches": 90,
20+ "functions": 85,
21+ "lines": 95
22+}
.github/workflows/ci.yml+7−2
@@ -12,5 +12,10 @@ jobs:
1212 steps:
1313 - uses: actions/checkout@v4
1414 - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
15− - run: spago build
16− - run: spago test
15+ - run: bun run coverage
16+ - uses: actions/upload-artifact@v4
17+ if: always()
18+ with:
19+ name: coverage
20+ path: coverage/
21+ - run: bun compile-fail-tests/run.js
.gitignore+2
@@ -1,5 +1,7 @@
11output/
22output-compile-fail/
3+coverage/
4+generated-docs/
35node_modules/
46.spago/
57.psc-ide-port
LICENSEnew file+21
@@ -0,0 +1,21 @@
1+The MIT License (MIT)
2+
3+Copyright (c) 2026 Mark Eibes
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
README.md+73−17
@@ -8,6 +8,34 @@ Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions
88spago install yoga-fetch-om
99```
1010
11+Optional query examples below use [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill):
12+
13+```bash
14+spago install justifill
15+```
16+
17+## Migrating from 0.7
18+
19+Version 0.8 exposes optional query parameters as explicit `Maybe` fields. Calls that passed plain values no longer compile:
20+
21+```purescript
22+-- 0.7: offset omitted
23+users <- api.listUsers { limit: 10 }
24+
25+-- 0.8: every optional field is explicit
26+users <- api.listUsers { limit: Just 10, offset: Nothing }
27+allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
28+```
29+
30+Import `Maybe(..)` from `Data.Maybe` when constructing these records directly. Alternatively, [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill) wraps supplied values in `Just` and fills omitted fields with `Nothing`:
31+
32+```purescript
33+-- offset remains omitted at the call site
34+users <- api.listUsers (justifill { limit: 10 })
35+```
36+
37+Path parameters and request bodies keep their existing argument shapes.
38+
1139## Quick Start
1240
1341### Example 1: Simple GET and POST Requests
@@ -30,8 +58,13 @@ main = do
3058### Example 2: Type-Safe API Client with Full CRUD
3159
3260```purescript
61+import Justifill (justifill)
62+
3363type UserAPI =
34− { getUser ::
64+ { health ::
65+ Route GET "health" {}
66+ ( ok :: { body :: { status :: String } } )
67+ , getUser ::
3568 Route GET ("users" / "id" : Int) {}
3669 ( ok :: { body :: User }
3770 , notFound :: { body :: ErrorMessage }
@@ -59,7 +92,8 @@ type UserAPI =
5992
6093api = client @UserAPI "https://api.example.com"
6194
62−-- Create a user
95+-- Generated methods use ordinary function application.
96+health <- api.health
6397user <- api.createUser { name: "Alice", email: "alice@example.com" }
6498 # handleErrors
6599 { badRequest: \err -> do
@@ -67,15 +101,21 @@ user <- api.createUser { name: "Alice", email: "alice@example.com" }
67101 throw err
68102 }
69103
70−-- Update with path param + body
71−updated <- api.updateUser { id: user.id } { name: "Alice Updated", email: user.email }
104+-- Path parameters and request bodies remain separate arguments.
105+updated <- api.updateUser
106+ { id: user.id }
107+ { name: "Alice Updated", email: user.email }
72108 # handleErrors
73109 { notFound: \_ -> throw userNotFound
74110 , badRequest: \err -> throw validationError
75111 }
76112
77−-- Query with pagination
78−users <- api.listUsers { limit: 10, offset: 0 }
113+-- Optional query parameters are explicit `Maybe` fields. Justifill can wrap
114+-- supplied values in `Just` and fill omitted fields with `Nothing`.
115+users <- api.listUsers (justifill { limit: 10 })
116+
117+-- The equivalent call without Justifill:
118+allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
79119```
80120
81121## More Examples
@@ -92,10 +132,10 @@ See the test files for complete, runnable examples with all imports:
92132
93133### ✅ Type-Safe Everything
94134
95−- **Path parameters**: `/users/:id` requires `{ id: Int }`
96−- **Query parameters**: Type-safe query strings with `?`
97−- **Request bodies**: Automatic JSON serialization
98−- **Response bodies**: Automatic JSON parsing
135+- **Path parameters**: `/users/:id` requires `{ id :: Int }`; names and values are percent-encoded
136+- **Query parameters**: Type-safe, percent-encoded query strings with explicit `Maybe` fields for optional values
137+- **Request bodies**: `JSON`, `PlainText`, and URL-encoded `FormData`
138+- **Response bodies**: Parsed JSON, `PlainText` as `String`, and `Streaming a` as `Strom {} () a`
99139- **Error handling**: Exhaustive pattern matching on variants
100140
101141### ✅ Single Source of Truth
@@ -115,12 +155,12 @@ Changes to routes automatically update both client and server!
115155### ✅ Automatic Derivation
116156
117157No manual client code:
118−- ✅ URL building with path parameter substitution
119−- ✅ Query string construction
120−- ✅ JSON request serialization
121−- ✅ JSON response parsing
158+- ✅ URL building with segment-safe path substitution
159+- ✅ Query construction that preserves existing queries and fragments
160+- ✅ JSON, plain-text, and URL-encoded request serialization
161+- ✅ JSON and plain-text response decoding
162+- ✅ Incremental UTF-8 and binary response streams
122163- ✅ Status code → variant mapping
123−- ✅ CORS and credentials handling
124164
125165### ✅ Integration with yoga-om
126166
@@ -140,8 +180,9 @@ The library uses PureScript's type system to:
140180
1411811. **Extract parameters** from route definitions at compile time
142182 - Path params: `"users" / "id" : Int` → `{ id :: Int }`
143− - Query params: `:? { limit :: Int }` → `{ limit :: Int }`
144− - Body params: `{ body :: JSON User }` → request body
183+ - Optional query params: `:? { limit :: Int }` → `{ limit :: Maybe Int }`
184+ - Required query params: `:? { limit :: Required Int }` → `{ limit :: Int }`
185+ - Body params: `{ body :: JSON User }` → `User`
145186
1461872. **Build URLs** automatically
147188 - Pattern: `/users/:id` + params: `{ id: 42 }` → `/users/42`
@@ -186,6 +227,21 @@ api = client @UserAPI "https://api.example.com"
186227- ✅ Variant response handling
187228- ✅ Om monad integration
188229
230+## Testing
231+
232+```sh
233+bun test
234+bun run test:compile-fail
235+bun run coverage
236+```
237+The suite combines example-based specifications with QuickCheck properties for
238+URL encoding, suffix preservation, repeated parameters, optional fields, and
239+query-separator invariants.
240+
241+The coverage command uses compiler source maps to report PureScript source
242+coverage, includes JavaScript FFI modules, enforces regression thresholds, and
243+writes HTML and LCOV reports to `coverage/`.
244+
189245## License
190246
191247MIT
bun.lock+220−113
@@ -3,11 +3,24 @@
33 "workspaces": {
44 "": {
55 "name": "purescript-yoga-fetch-om",
6+ "dependencies": {
7+ "pino": "^10.3.1",
8+ },
69 "devDependencies": {
10+ "@fastify/cookie": "^11.1.2",
11+ "@fastify/cors": "^11.3.0",
12+ "@fastify/formbody": "^9.0.0",
13+ "@fastify/helmet": "^13.1.0",
14+ "@fastify/multipart": "^10.1.0",
15+ "@fastify/rate-limit": "^11.2.0",
16+ "@fastify/websocket": "^11.3.0",
17+ "argon2": "^0.45.1",
18+ "c8": "^12.0.0",
719 "fastify": "^5.7.4",
20+ "jose": "^6.2.8",
821 "jsdom": "^28.0.0",
922 "purescript": "^0.15.15",
10− "spago": "0.93.42",
23+ "spago": "^1.0.3",
1124 "vitest": "^2.1.8",
1225 },
1326 },
@@ -21,6 +34,8 @@
2134
2235 "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
2336
37+ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
38+
2439 "@csstools/color-helpers": ["@csstools/color-helpers@6.0.1", "", {}, "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ=="],
2540
2641 "@csstools/css-calc": ["@csstools/css-calc@3.0.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA=="],
@@ -33,6 +48,8 @@
3348
3449 "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
3550
51+ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
52+
3653 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
3754
3855 "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -83,37 +100,63 @@
83100
84101 "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
85102
103+ "@fastify/busboy": ["@fastify/busboy@3.2.1", "", {}, "sha512-tgK4O+57iz5ycYNGXE5ZWj1ES03lD2XnnBYWSbU/3wYZRMQzCUq7Ycds/RdyBZQeL5MU4fxBt6lzbIWf/Bickw=="],
104+
105+ "@fastify/cookie": ["@fastify/cookie@11.1.2", "", { "dependencies": { "cookie": "^2.0.0", "fastify-plugin": "^6.0.0" } }, "sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g=="],
106+
107+ "@fastify/cors": ["@fastify/cors@11.3.0", "", { "dependencies": { "fastify-plugin": "^6.0.0", "toad-cache": "^3.7.0" } }, "sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw=="],
108+
109+ "@fastify/deepmerge": ["@fastify/deepmerge@3.2.1", "", {}, "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA=="],
110+
86111 "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
87112
88113 "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
89114
115+ "@fastify/formbody": ["@fastify/formbody@9.0.0", "", { "dependencies": { "fast-querystring": "^1.1.2", "fastify-plugin": "^6.0.0" } }, "sha512-T/af26CSrUARBCvsEmv+DJLPfZlrRKESzqironxP1j7qzuLyKcoZtj6MuTGShuKx1THXugoie2oFbUJxXfGFzA=="],
116+
90117 "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
91118
119+ "@fastify/helmet": ["@fastify/helmet@13.1.0", "", { "dependencies": { "fastify-plugin": "^6.0.0", "helmet": "^8.0.0" } }, "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA=="],
120+
92121 "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
93122
123+ "@fastify/multipart": ["@fastify/multipart@10.1.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@fastify/deepmerge": "^3.0.0", "@fastify/error": "^4.0.0", "fastify-plugin": "^6.0.0", "secure-json-parse": "^4.0.0" } }, "sha512-b2r6CovmLQvLFJ5HJDtxVigZjcO9TwkRGslhiNQxsGJwilm5B3eqvXPOVLudC44zXMXJITpQ/iEATIE7zoXqcQ=="],
124+
94125 "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
95126
127+ "@fastify/rate-limit": ["@fastify/rate-limit@11.2.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^6.0.0", "ip-address": "^10.2.0", "toad-cache": "^3.7.0" } }, "sha512-X7osJd4XSvMoejYrnJkSZYYjY1eNYoBqhjlzf1RakC2204qExFqZFTKj5+T7VuzA/iUI9Z3UoSqQRkB2HpG0oQ=="],
128+
129+ "@fastify/websocket": ["@fastify/websocket@11.3.0", "", { "dependencies": { "duplexify": "^4.1.3", "fastify-plugin": "^6.0.0", "ws": "^8.16.0" } }, "sha512-g89ag4BCcD9YP5wBZXixzoLnuf5j89p/sXFcfpCiv2pdEkYYukBEoK3heVzqsp0EAtszVDc2BBZG0KZqeAShIA=="],
130+
96131 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
97132
98− "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
133+ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
99134
100135 "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
101136
137+ "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="],
138+
139+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
140+
102141 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
103142
104− "@nodelib/fs.scandir": ["@nodelib/fs.scandir@3.0.0", "", { "dependencies": { "@nodelib/fs.stat": "3.0.0", "run-parallel": "^1.2.0" } }, "sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg=="],
143+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
144+
145+ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
105146
106− "@nodelib/fs.stat": ["@nodelib/fs.stat@3.0.0", "", {}, "sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ=="],
147+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@4.0.1", "", { "dependencies": { "@nodelib/fs.stat": "4.0.0", "run-parallel": "^1.2.0" } }, "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw=="],
107148
108− "@nodelib/fs.walk": ["@nodelib/fs.walk@2.0.0", "", { "dependencies": { "@nodelib/fs.scandir": "3.0.0", "fastq": "^1.15.0" } }, "sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A=="],
149+ "@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="],
150+
151+ "@nodelib/fs.walk": ["@nodelib/fs.walk@3.0.1", "", { "dependencies": { "@nodelib/fs.scandir": "4.0.1", "fastq": "^1.15.0" } }, "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw=="],
109152
110153 "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
111154
112155 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
113156
114− "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
157+ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
115158
116− "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
159+ "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
117160
118161 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
119162
@@ -169,6 +212,8 @@
169212
170213 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
171214
215+ "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
216+
172217 "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="],
173218
174219 "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="],
@@ -197,12 +242,14 @@
197242
198243 "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
199244
200− "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
245+ "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
201246
202247 "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
203248
204249 "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="],
205250
251+ "argon2": ["argon2@0.45.1", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.1.0", "node-addon-api": "^8.9.0", "node-gyp-build": "^4.8.4" } }, "sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ=="],
252+
206253 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
207254
208255 "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
@@ -215,28 +262,18 @@
215262
216263 "avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
217264
218− "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
219−
220− "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
265+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
221266
222267 "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
223268
224− "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="],
225−
226269 "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
227270
228− "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
229−
230− "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
231−
232271 "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
233272
234− "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
273+ "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
235274
236275 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
237276
238− "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
239−
240277 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
241278
242279 "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
@@ -245,6 +282,8 @@
245282
246283 "byline": ["byline@5.0.0", "", {}, "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q=="],
247284
285+ "c8": ["c8@12.0.0", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", "yargs": "^18.0.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "monocart-coverage-reports": "^2" }, "optionalPeers": ["monocart-coverage-reports"], "bin": { "c8": "bin/c8.js" } }, "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg=="],
286+
248287 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
249288
250289 "cacache": ["cacache@11.3.3", "", { "dependencies": { "bluebird": "^3.5.5", "chownr": "^1.1.1", "figgy-pudding": "^3.5.1", "glob": "^7.1.4", "graceful-fs": "^4.1.15", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "mkdirp": "^0.5.1", "move-concurrently": "^1.0.1", "promise-inflight": "^1.0.1", "rimraf": "^2.6.3", "ssri": "^6.0.1", "unique-filename": "^1.1.1", "y18n": "^4.0.0" } }, "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA=="],
@@ -261,6 +300,8 @@
261300
262301 "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
263302
303+ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
304+
264305 "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
265306
266307 "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
@@ -269,7 +310,9 @@
269310
270311 "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
271312
272− "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
313+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
314+
315+ "cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
273316
274317 "copy-concurrently": ["copy-concurrently@1.0.5", "", { "dependencies": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", "iferr": "^0.1.5", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" } }, "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A=="],
275318
@@ -277,6 +320,8 @@
277320
278321 "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
279322
323+ "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
324+
280325 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
281326
282327 "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
@@ -291,12 +336,8 @@
291336
292337 "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
293338
294− "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
295−
296339 "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
297340
298− "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
299−
300341 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
301342
302343 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
@@ -305,13 +346,9 @@
305346
306347 "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
307348
308− "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
309−
310− "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
349+ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
311350
312− "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
313−
314− "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
351+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
315352
316353 "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
317354
@@ -327,14 +364,14 @@
327364
328365 "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
329366
367+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
368+
330369 "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
331370
332371 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
333372
334373 "execa": ["execa@2.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^3.0.0", "onetime": "^5.1.0", "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw=="],
335374
336− "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
337−
338375 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
339376
340377 "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
@@ -349,26 +386,26 @@
349386
350387 "fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
351388
389+ "fastify-plugin": ["fastify-plugin@6.0.0", "", {}, "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg=="],
390+
352391 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
353392
354393 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
355394
356− "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
357−
358395 "filesize": ["filesize@4.2.1", "", {}, "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA=="],
359396
360397 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
361398
362399 "find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="],
363400
401+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
402+
364403 "flush-write-stream": ["flush-write-stream@1.1.1", "", { "dependencies": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" } }, "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w=="],
365404
366405 "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
367406
368407 "from2": ["from2@2.3.0", "", { "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g=="],
369408
370− "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
371−
372409 "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
373410
374411 "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
@@ -381,18 +418,24 @@
381418
382419 "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
383420
384− "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
421+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
385422
386− "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
423+ "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
424+
425+ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
387426
388− "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
427+ "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
389428
390429 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
391430
392− "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
431+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
432+
433+ "helmet": ["helmet@8.3.0", "", {}, "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w=="],
393434
394435 "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
395436
437+ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
438+
396439 "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
397440
398441 "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -403,8 +446,6 @@
403446
404447 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
405448
406− "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
407−
408449 "iferr": ["iferr@0.1.5", "", {}, "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA=="],
409450
410451 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -417,9 +458,7 @@
417458
418459 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
419460
420− "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
421−
422− "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
461+ "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
423462
424463 "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
425464
@@ -445,7 +484,15 @@
445484
446485 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
447486
448− "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
487+ "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
488+
489+ "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
490+
491+ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
492+
493+ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
494+
495+ "jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="],
449496
450497 "jsdom": ["jsdom@28.0.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.11.0", "cssstyle": "^5.3.7", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.20.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA=="],
451498
@@ -459,6 +506,8 @@
459506
460507 "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
461508
509+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
510+
462511 "log-symbols": ["log-symbols@3.0.0", "", { "dependencies": { "chalk": "^2.4.2" } }, "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ=="],
463512
464513 "log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="],
@@ -469,6 +518,8 @@
469518
470519 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
471520
521+ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
522+
472523 "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="],
473524
474525 "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
@@ -483,9 +534,7 @@
483534
484535 "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
485536
486− "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
487−
488− "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
537+ "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
489538
490539 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
491540
@@ -507,8 +556,6 @@
507556
508557 "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
509558
510− "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
511−
512559 "move-concurrently": ["move-concurrently@1.0.1", "", { "dependencies": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", "fs-write-stream-atomic": "^1.0.8", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" } }, "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ=="],
513560
514561 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -517,11 +564,11 @@
517564
518565 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
519566
520− "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
521−
522567 "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
523568
524− "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
569+ "node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="],
570+
571+ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
525572
526573 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
527574
@@ -535,6 +582,10 @@
535582
536583 "p-finally": ["p-finally@2.0.1", "", {}, "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw=="],
537584
585+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
586+
587+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
588+
538589 "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
539590
540591 "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
@@ -543,11 +594,13 @@
543594
544595 "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
545596
597+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
598+
546599 "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
547600
548601 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
549602
550− "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
603+ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
551604
552605 "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
553606
@@ -565,8 +618,6 @@
565618
566619 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
567620
568− "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
569−
570621 "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
571622
572623 "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
@@ -591,9 +642,7 @@
591642
592643 "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
593644
594− "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
595−
596− "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
645+ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
597646
598647 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
599648
@@ -643,11 +692,7 @@
643692
644693 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
645694
646− "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
647−
648− "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
649−
650− "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
695+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
651696
652697 "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
653698
@@ -661,7 +706,7 @@
661706
662707 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
663708
664− "spago": ["spago@0.93.42", "", { "dependencies": { "@nodelib/fs.walk": "^2.0.0", "better-sqlite3": "^11.0.0", "env-paths": "^3.0.0", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", "glob": "^10.4.1", "markdown-it": "^14.1.0", "micromatch": "^4.0.7", "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", "readline-sync": "^1.4.10", "semver": "^7.6.2", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.15.0", "supports-color": "^9.4.0", "tar": "^7.2.0", "tmp": "^0.2.3", "xhr2": "^0.2.1", "yaml": "^2.4.5" }, "bin": { "spago": "bin/bundle.js" } }, "sha512-R8/CCfR3jtbN3NFZOso+sBTKsaGDu96j8X55JA8CDx12GyB3f64wpwbIXKJ8Y+E4UMjYWhTB2vkP+kqf4R5Y5w=="],
709+ "spago": ["spago@1.0.4", "", { "dependencies": { "@nodelib/fs.walk": "^3.0.1", "env-paths": "^3.0.0", "fs-extra": "^11.3.0", "fuse.js": "^7.1.0", "glob": "^11.0.1", "markdown-it": "^14.1.0", "micromatch": "^4.0.8", "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", "readline-sync": "^1.4.10", "semver": "^7.7.1", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.16.0", "supports-color": "^10.0.0", "tar": "^7.4.3", "tmp": "^0.2.3", "xhr2": "^0.2.1", "yaml": "^2.7.0" }, "bin": { "spago": "bin/bundle.js" } }, "sha512-iv1HBJppzGJRWWd/DZ32oselcKrhq/PboBdXy5VMX4GiwRqLCUUp3loZ6Yhm8zymNRKHkN/w52cxX6k521nwng=="],
665710
666711 "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
667712
@@ -683,29 +728,21 @@
683728
684729 "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
685730
686− "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
731+ "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
687732
688− "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
733+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
689734
690− "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
691−
692− "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
693−
694− "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
735+ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
695736
696737 "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
697738
698− "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
699−
700− "supports-color": ["supports-color@9.4.0", "", {}, "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw=="],
739+ "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
701740
702741 "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
703742
704743 "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
705744
706− "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
707−
708− "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
745+ "test-exclude": ["test-exclude@8.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^13.0.6", "minimatch": "^10.2.2" } }, "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ=="],
709746
710747 "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
711748
@@ -735,8 +772,6 @@
735772
736773 "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
737774
738− "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
739−
740775 "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
741776
742777 "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
@@ -755,6 +790,8 @@
755790
756791 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
757792
793+ "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="],
794+
758795 "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
759796
760797 "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="],
@@ -773,12 +810,12 @@
773810
774811 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
775812
776− "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
777−
778− "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
813+ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
779814
780815 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
781816
817+ "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
818+
782819 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
783820
784821 "xhr2": ["xhr2@0.2.1", "", {}, "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw=="],
@@ -789,42 +826,58 @@
789826
790827 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
791828
792− "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
829+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
793830
794831 "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
795832
796833 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
797834
798− "zen-observable": ["zen-observable@0.8.15", "", {}, "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="],
835+ "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
799836
800− "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
837+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
801838
802− "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
839+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
803840
804− "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
841+ "zen-observable": ["zen-observable@0.8.15", "", {}, "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="],
805842
806843 "@npmcli/move-file/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
807844
808845 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
809846
810− "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
811−
812847 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
813848
814849 "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
815850
816851 "cacache/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
817852
853+ "cacache/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
854+
818855 "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
819856
857+ "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
858+
859+ "concat-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
860+
820861 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
821862
822− "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
863+ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
864+
865+ "flush-write-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
866+
867+ "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
823868
824869 "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
825870
871+ "fs-write-stream-atomic/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
872+
873+ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
874+
875+ "light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
876+
826877 "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
827878
879+ "log-update/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
880+
828881 "make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
829882
830883 "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
@@ -853,7 +906,11 @@
853906
854907 "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
855908
856− "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
909+ "mississippi/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
910+
911+ "parallel-transform/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
912+
913+ "pumpify/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
857914
858915 "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="],
859916
@@ -861,40 +918,58 @@
861918
862919 "purescript-installer/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
863920
864− "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
921+ "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
865922
866923 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
867924
868925 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
869926
927+ "socks/ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
928+
870929 "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
871930
872− "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
931+ "test-exclude/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
873932
874− "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
933+ "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
875934
876− "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
935+ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
877936
878− "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
937+ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
879938
880− "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
939+ "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
881940
882− "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
941+ "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
883942
884− "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
943+ "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
885944
886− "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
945+ "cacache/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
887946
888− "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
947+ "chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
889948
890− "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
949+ "concat-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
891950
892− "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
951+ "concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
893952
894− "cacache/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
953+ "flush-write-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
954+
955+ "flush-write-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
956+
957+ "from2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
958+
959+ "from2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
895960
896961 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
897962
963+ "fs-write-stream-atomic/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
964+
965+ "fs-write-stream-atomic/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
966+
967+ "log-update/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
968+
969+ "log-update/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
970+
971+ "log-update/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
972+
898973 "make-fetch-happen/cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
899974
900975 "make-fetch-happen/cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
@@ -925,6 +1000,14 @@
9251000
9261001 "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
9271002
1003+ "mississippi/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
1004+
1005+ "parallel-transform/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1006+
1007+ "parallel-transform/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1008+
1009+ "pumpify/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
1010+
9281011 "purescript-installer/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
9291012
9301013 "purescript-installer/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
@@ -939,16 +1022,22 @@
9391022
9401023 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
9411024
942− "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
1025+ "test-exclude/glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
9431026
944− "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1027+ "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
9451028
946− "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1029+ "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
9471030
9481031 "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
9491032
9501033 "cacache/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
9511034
1035+ "log-update/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1036+
1037+ "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1038+
1039+ "log-update/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1040+
9521041 "make-fetch-happen/cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
9531042
9541043 "make-fetch-happen/cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -961,22 +1050,40 @@
9611050
9621051 "make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
9631052
1053+ "mississippi/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1054+
1055+ "mississippi/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1056+
1057+ "pumpify/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1058+
1059+ "pumpify/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1060+
9641061 "purescript-installer/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
9651062
9661063 "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
9671064
9681065 "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
9691066
970− "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
1067+ "@npmcli/move-file/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
9711068
972− "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
1069+ "cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
9731070
974− "@npmcli/move-file/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
1071+ "log-update/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
1072+
1073+ "make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
9751074
9761075 "make-fetch-happen/cacache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
9771076
9781077 "make-fetch-happen/cacache/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
9791078
1079+ "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
1080+
1081+ "@npmcli/move-file/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
1082+
1083+ "make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
1084+
9801085 "make-fetch-happen/cacache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
1086+
1087+ "make-fetch-happen/cacache/rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
9811088 }
9821089}
compile-fail-tests/ClientBadBodyType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: String
2+module CompileFail.ClientBadBodyType where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { name: 42 }
compile-fail-tests/ClientBadCurriedBodyType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: String
2+module CompileFail.ClientBadCurriedBodyType where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { updateUser :: Route POST ("users" / "id" : Int) { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.updateUser { id: 1 } { name: 42 }
compile-fail-tests/ClientBadHeaderType.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: BearerToken
2+module CompileFail.ClientBadHeaderType where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken)
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me { authorization: "secret" }
compile-fail-tests/ClientBadOptionalQueryType.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: limit
2+module CompileFail.ClientBadOptionalQueryType where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Route, client, type (:?))
6+
7+type UserApi =
8+ { listUsers :: Route GET ("users" :? { limit :: Int, offset :: Int }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.listUsers (justifill { limit: "10" })
compile-fail-tests/ClientBadPathType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: Int
2+module CompileFail.ClientBadPathType where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser { id: "1" }
compile-fail-tests/ClientExtraBodyField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraBodyField where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { name: "Alice", extra: true }
compile-fail-tests/ClientExtraHeaderField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraHeaderField where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken(..))
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me { authorization: BearerToken "secret", extra: true }
compile-fail-tests/ClientExtraOptionalQueryField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraOptionalQueryField where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Route, client, type (:?))
6+
7+type UserApi =
8+ { listUsers :: Route GET ("users" :? { limit :: Int, offset :: Int }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.listUsers (justifill { limit: 1, extra: "nope" })
compile-fail-tests/ClientExtraPathField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraPathField where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser { id: 1, extra: true }
compile-fail-tests/ClientMissingBodyArgument.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingBodyArgument where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+import Yoga.Om (Om)
6+
7+type UserApi =
8+ { updateUser :: Route POST ("users" / "id" : Int) { body :: JSON { token :: String } } (ok :: { body :: {} })
9+ }
10+
11+api :: forall ctx err. { updateUser :: { id :: Int } -> Om { | ctx } err {} }
12+api = client @UserApi ""
compile-fail-tests/ClientMissingBodyField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: name :: String
2+module CompileFail.ClientMissingBodyField where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String, age :: Int } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { age: 42 }
compile-fail-tests/ClientMissingHeaderArgument.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingHeaderArgument where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+import Yoga.HTTP.API.Route (BearerToken)
6+import Yoga.Om (Om)
7+
8+type UserApi =
9+ { getUser :: Route GET ("users" / "id" : Int) { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
10+ }
11+
12+api :: forall ctx err. { getUser :: { id :: Int } -> Om { | ctx } err {} }
13+api = client @UserApi ""
compile-fail-tests/ClientMissingHeaderField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: authorization :: BearerToken
2+module CompileFail.ClientMissingHeaderField where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken)
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me {}
compile-fail-tests/ClientMissingPathField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: id :: Int
2+module CompileFail.ClientMissingPathField where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser {}
compile-fail-tests/ClientMissingRequiredQuery.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: id :: Int
2+module CompileFail.ClientMissingRequiredQuery where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Required, Route, client, type (:?))
6+
7+type UserApi =
8+ { getUser :: Route GET ("users" :? { id :: Required Int, token :: Required String }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.getUser (justifill { token: "secret" })
compile-fail-tests/ClientMissingThirdArgument.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingThirdArgument where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+import Yoga.HTTP.API.Route (BearerToken)
6+import Yoga.Om (Om)
7+
8+type UserApi =
9+ { updateUser :: Route POST ("users" / "id" : Int) { headers :: { authorization :: BearerToken }, body :: JSON { token :: String } } (ok :: { body :: {} })
10+ }
11+
12+api :: forall ctx err. { updateUser :: { id :: Int } -> { authorization :: BearerToken } -> Om { | ctx } err {} }
13+api = client @UserApi ""
compile-fail-tests/run.jsnew file+101
@@ -0,0 +1,101 @@
1+import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { basename, join } from "node:path";
4+
5+const cwd = new URL("..", import.meta.url).pathname;
6+const casesDir = join(cwd, "compile-fail-tests");
7+const decoder = new TextDecoder();
8+
9+function run(args, options = {}) {
10+ const proc = Bun.spawnSync(args, {
11+ cwd,
12+ stdout: options.stdout ?? "pipe",
13+ stderr: options.stderr ?? "pipe",
14+ });
15+ return {
16+ code: proc.exitCode,
17+ stdout: proc.stdout ? decoder.decode(proc.stdout) : "",
18+ stderr: proc.stderr ? decoder.decode(proc.stderr) : "",
19+ };
20+}
21+
22+function printResult(label, result) {
23+ console.error(`\n${label} failed with exit code ${result.code}`);
24+ if (result.stdout) console.error(result.stdout);
25+ if (result.stderr) console.error(result.stderr);
26+}
27+
28+function expectedSubstring(file) {
29+ const firstLine = readFileSync(file, "utf8").split("\n", 1)[0];
30+ return firstLine.startsWith("-- EXPECT: ") ? firstLine.slice("-- EXPECT: ".length) : null;
31+}
32+
33+const sourcesResult = run(["bunx", "spago", "sources", "--quiet", "--json"]);
34+if (sourcesResult.code !== 0) {
35+ printResult("spago sources", sourcesResult);
36+ process.exit(1);
37+}
38+
39+const excludedSourceGlobs = new Set([
40+ "test/**/*.purs",
41+ "../purescript-yoga-fastify-om/src/**/*.purs",
42+ "../purescript-yoga-fastify-om/local-packages/yoga-fastify/src/**/*.purs",
43+]);
44+
45+const librarySources = JSON.parse(sourcesResult.stdout)
46+ .filter((source) => !excludedSourceGlobs.has(source));
47+
48+const cases = readdirSync(casesDir)
49+ .filter((file) => file.endsWith(".purs"))
50+ .sort()
51+ .map((file) => join("compile-fail-tests", file));
52+
53+const libOutput = mkdtempSync(join(tmpdir(), "yoga-fetch-om-lib-"));
54+
55+try {
56+ const libResult = run(["bunx", "purs", "compile", ...librarySources, "--output", libOutput], { stdout: "ignore" });
57+ if (libResult.code !== 0) {
58+ printResult("library compile", libResult);
59+ process.exit(1);
60+ }
61+
62+ let failed = 0;
63+
64+ for (const testCase of cases) {
65+ const failOutput = mkdtempSync(join(tmpdir(), "yoga-fetch-om-compile-fail-"));
66+ try {
67+ const result = run([
68+ "bunx",
69+ "purs",
70+ "compile",
71+ ...librarySources,
72+ testCase,
73+ "--output",
74+ failOutput,
75+ ]);
76+
77+ const output = `${result.stdout}\n${result.stderr}`;
78+ const expected = expectedSubstring(join(cwd, testCase));
79+ const name = basename(testCase, ".purs");
80+
81+ if (result.code === 0) {
82+ console.error(`FAIL ${name} - compiled successfully (should have failed)`);
83+ failed++;
84+ } else if (expected && !output.includes(expected)) {
85+ printResult(`FAIL ${name}`, result);
86+ console.error(`Expected failure to contain: ${expected}`);
87+ failed++;
88+ } else {
89+ console.log(`PASS ${name}`);
90+ }
91+ } finally {
92+ rmSync(failOutput, { recursive: true, force: true });
93+ }
94+ }
95+
96+ if (failed > 0) {
97+ process.exit(1);
98+ }
99+} finally {
100+ rmSync(libOutput, { recursive: true, force: true });
101+}
package.json+15−3
@@ -1,12 +1,14 @@
11{
22 "name": "purescript-yoga-fetch-om",
3− "version": "0.4.0",
3+ "version": "0.8.1",
44 "description": "Derive type-safe fetch clients from yoga HTTP API definitions",
55 "type": "module",
66 "scripts": {
77 "build": "spago build",
88 "test": "spago test",
9− "clean": "rm -rf output .spago",
9+ "coverage": "spago build && c8 spago test",
10+ "test:compile-fail": "bun compile-fail-tests/run.js",
11+ "clean": "rm -rf output .spago coverage",
1012 "watch": "spago build --watch",
1113 "repl": "spago repl",
1214 "docs": "spago docs",
@@ -24,10 +26,20 @@
2426 "author": "",
2527 "license": "MIT",
2628 "devDependencies": {
29+ "@fastify/cookie": "^11.1.2",
30+ "@fastify/cors": "^11.3.0",
31+ "@fastify/formbody": "^9.0.0",
32+ "@fastify/helmet": "^13.1.0",
33+ "@fastify/multipart": "^10.1.0",
34+ "@fastify/rate-limit": "^11.2.0",
35+ "@fastify/websocket": "^11.3.0",
36+ "argon2": "^0.45.1",
37+ "c8": "^12.0.0",
2738 "fastify": "^5.7.4",
39+ "jose": "^6.2.8",
2840 "jsdom": "^28.0.0",
2941 "purescript": "^0.15.15",
30− "spago": "0.93.42",
42+ "spago": "^1.0.3",
3143 "vitest": "^2.1.8"
3244 },
3345 "dependencies": {
spago.lock+111−189
@@ -44,6 +44,9 @@
4444 {
4545 "strings": ">=6.0.1 <7.0.0"
4646 },
47+ {
48+ "tailrec": ">=6.1.0 <7.0.0"
49+ },
4750 {
4851 "transformers": ">=6.1.0 <7.0.0"
4952 },
@@ -60,7 +63,7 @@
6063 "variant": ">=8.0.0 <9.0.0"
6164 },
6265 {
63− "yoga-http-api": ">=0.3.2 <0.4.0"
66+ "yoga-http-api": ">=0.3.4 <0.4.0"
6467 },
6568 {
6669 "yoga-json": ">=5.2.1 <6.0.0"
@@ -71,97 +74,28 @@
7174 {
7275 "yoga-om-strom": ">=0.4.3 <0.5.0"
7376 }
74− ],
75− "build_plan": [
76− "aff",
77− "aff-retry",
78− "ansi",
79− "arraybuffer-types",
80− "arrays",
81− "avar",
82− "bifunctors",
83− "catenable-lists",
84− "console",
85− "const",
86− "contravariant",
87− "control",
88− "datetime",
89− "distributive",
90− "effect",
91− "either",
77+ ]
78+ },
79+ "test": {
80+ "dependencies": [
81+ {
82+ "console": ">=6.0.0 <7.0.0"
83+ },
9284 "enums",
93− "exceptions",
94− "exists",
9585 "foldable-traversable",
9686 "foreign",
9787 "foreign-object",
98− "fork",
99− "free",
100− "functions",
101− "functors",
102− "gen",
103− "heterogeneous",
104− "http-methods",
105− "identity",
10688 "integers",
107− "invariant",
108− "js-bigints",
109− "js-date",
110− "js-fetch",
11189 "js-promise",
112− "js-promise-aff",
113− "lazy",
114− "lists",
115− "literals",
116− "maybe",
117− "media-types",
118− "mmorph",
119− "newtype",
120− "nonempty",
121− "now",
122− "nullable",
123− "numbers",
124− "ordered-collections",
125− "orders",
126− "parallel",
90+ {
91+ "js-uri": ">=3.1.0 <4.0.0"
92+ },
93+ {
94+ "justifill": ">=0.5.0 <0.6.0"
95+ },
12796 "partial",
128− "pipes",
129− "prelude",
130− "profunctor",
131− "random",
132− "record",
133− "record-studio",
134− "refs",
135− "safe-coerce",
136− "spec",
137− "st",
138− "strings",
139− "tailrec",
140− "transformers",
141− "tuples",
142− "type-equality",
143− "typelevel-prelude",
144− "uncurried-transformers",
145− "undefined-is-not-a-problem",
146− "unfoldable",
147− "unsafe-coerce",
148− "untagged-union",
149− "variant",
150− "web-events",
151− "web-file",
152− "web-streams",
153− "yoga-http-api",
154− "yoga-json",
155− "yoga-om",
156− "yoga-om-strom",
157− "yoga-options",
158− "yoga-tree"
159− ]
160− },
161− "test": {
162− "dependencies": [
16397 {
164− "console": ">=6.0.0 <7.0.0"
98+ "quickcheck": ">=8.0.1 <9.0.0"
16599 },
166100 {
167101 "refs": ">=6.0.0 <7.0.0"
@@ -173,106 +107,12 @@
173107 "spec-discovery": ">=8.0.0 <9.0.0"
174108 },
175109 {
176− "yoga-fastify-om": ">=0.4.5 <0.5.0"
177− }
178− ],
179− "build_plan": [
180− "aff",
181− "aff-retry",
182− "ansi",
183− "argonaut-codecs",
184− "argonaut-core",
185− "arraybuffer-types",
186− "arrays",
187− "avar",
188− "bifunctors",
189− "catenable-lists",
190− "console",
191− "const",
192− "contravariant",
193− "control",
194− "datetime",
195− "distributive",
196− "effect",
197− "either",
198− "enums",
199− "exceptions",
200− "exists",
201− "exitcodes",
202− "foldable-traversable",
203− "foreign",
204− "foreign-object",
205− "fork",
206− "free",
207− "functions",
208− "functors",
209− "gen",
210− "heterogeneous",
211− "identity",
212− "integers",
213− "invariant",
214− "js-bigints",
215− "js-date",
216− "js-promise",
217− "js-promise-aff",
218− "js-uri",
219− "lazy",
220− "lists",
221− "literals",
222− "maybe",
223− "mmorph",
224− "newtype",
225− "node-buffer",
226− "node-event-emitter",
227− "node-fs",
228− "node-path",
229− "node-process",
230− "node-streams",
231− "nonempty",
232− "now",
233− "nullable",
234− "numbers",
235− "open-memoize",
236− "optparse",
237− "ordered-collections",
238− "orders",
239− "parallel",
240− "partial",
241− "pipes",
242− "posix-types",
243− "prelude",
244− "profunctor",
245− "random",
246− "record",
247− "record-studio",
248− "refs",
249− "routing-duplex",
250− "safe-coerce",
251− "spec",
252− "spec-discovery",
253− "spec-node",
254− "st",
255− "strings",
256− "tailrec",
257− "transformers",
258− "tuples",
259− "type-equality",
260− "typelevel-prelude",
261− "uncurried-transformers",
262− "undefined-is-not-a-problem",
263− "unfoldable",
264− "unsafe-coerce",
265− "untagged-union",
266− "variant",
110+ "spec-quickcheck": ">=5.0.2 <6.0.0"
111+ },
267112 "yoga-fastify",
268− "yoga-fastify-om",
269− "yoga-http-api",
270− "yoga-json",
271− "yoga-om",
272− "yoga-om-layer",
273− "yoga-om-strom",
274− "yoga-options",
275− "yoga-tree"
113+ {
114+ "yoga-fastify-om": ">=0.4.7 <0.5.0"
115+ }
276116 ]
277117 }
278118 }
@@ -854,6 +694,17 @@
854694 "prelude"
855695 ]
856696 },
697+ "justifill": {
698+ "type": "registry",
699+ "version": "0.5.0",
700+ "integrity": "sha256-IwoDO72+IROcUvFIwnronb/ny8SepssgTUG3Qp2TEKg=",
701+ "dependencies": [
702+ "maybe",
703+ "prelude",
704+ "record",
705+ "typelevel-prelude"
706+ ]
707+ },
857708 "lazy": {
858709 "type": "registry",
859710 "version": "6.0.0",
@@ -865,6 +716,20 @@
865716 "prelude"
866717 ]
867718 },
719+ "lcg": {
720+ "type": "registry",
721+ "version": "4.0.0",
722+ "integrity": "sha256-bUYyfXYmjTn7zSpj2hTiePywzsuD80fCjkonsrpdGOw=",
723+ "dependencies": [
724+ "effect",
725+ "integers",
726+ "maybe",
727+ "numbers",
728+ "partial",
729+ "prelude",
730+ "random"
731+ ]
732+ },
868733 "lists": {
869734 "type": "registry",
870735 "version": "7.0.0",
@@ -1244,6 +1109,39 @@
12441109 "tuples"
12451110 ]
12461111 },
1112+ "quickcheck": {
1113+ "type": "registry",
1114+ "version": "8.0.1",
1115+ "integrity": "sha256-SfW+maesCt0jnnFhfrgJZGPcWLj8oFcGbcDIVklmdzc=",
1116+ "dependencies": [
1117+ "arrays",
1118+ "console",
1119+ "control",
1120+ "effect",
1121+ "either",
1122+ "enums",
1123+ "exceptions",
1124+ "foldable-traversable",
1125+ "gen",
1126+ "identity",
1127+ "integers",
1128+ "lazy",
1129+ "lcg",
1130+ "lists",
1131+ "maybe",
1132+ "newtype",
1133+ "nonempty",
1134+ "numbers",
1135+ "partial",
1136+ "prelude",
1137+ "record",
1138+ "st",
1139+ "strings",
1140+ "tailrec",
1141+ "transformers",
1142+ "tuples"
1143+ ]
1144+ },
12471145 "random": {
12481146 "type": "registry",
12491147 "version": "6.0.0",
@@ -1394,6 +1292,22 @@
13941292 "tuples"
13951293 ]
13961294 },
1295+ "spec-quickcheck": {
1296+ "type": "registry",
1297+ "version": "5.0.2",
1298+ "integrity": "sha256-pmnLu9e9Yk+Yx4hi71XwbyuPS27ya4hjTOKk2KFK/Ns=",
1299+ "dependencies": [
1300+ "aff",
1301+ "arrays",
1302+ "effect",
1303+ "foldable-traversable",
1304+ "lists",
1305+ "maybe",
1306+ "prelude",
1307+ "quickcheck",
1308+ "tuples"
1309+ ]
1310+ },
13971311 "st": {
13981312 "type": "registry",
13991313 "version": "6.2.0",
@@ -1630,8 +1544,8 @@
16301544 },
16311545 "yoga-fastify": {
16321546 "type": "registry",
1633− "version": "0.5.3",
1634− "integrity": "sha256-KeAoEYXXNH2gHEl5Y7j9uMo5ipByLZp/p7u6pgQn6KU=",
1547+ "version": "0.5.4",
1548+ "integrity": "sha256-YenjePRdiApMPpcvkXlzre7c4fIuvk5T19Z2uEAWhR4=",
16351549 "dependencies": [
16361550 "aff",
16371551 "arrays",
@@ -1657,8 +1571,8 @@
16571571 },
16581572 "yoga-fastify-om": {
16591573 "type": "registry",
1660− "version": "0.4.5",
1661− "integrity": "sha256-fPDRni+HjddpI7GPad5U2kVHxMPOZIsvTPFZtOPpiWI=",
1574+ "version": "0.4.7",
1575+ "integrity": "sha256-KGQ7lHI7pcSiEQoEq6j6K+3XilcPy8wQ/M9C1y+uaEk=",
16621576 "dependencies": [
16631577 "aff",
16641578 "console",
@@ -1670,10 +1584,12 @@
16701584 "integers",
16711585 "maybe",
16721586 "node-buffer",
1587+ "numbers",
16731588 "prelude",
16741589 "record",
16751590 "refs",
16761591 "routing-duplex",
1592+ "strings",
16771593 "transformers",
16781594 "typelevel-prelude",
16791595 "unsafe-coerce",
@@ -1688,19 +1604,25 @@
16881604 },
16891605 "yoga-http-api": {
16901606 "type": "registry",
1691− "version": "0.3.2",
1692− "integrity": "sha256-SiL07TYVjHPiQjMhnkYdoI3KrL6gNn1W1LigCN+80jA=",
1607+ "version": "0.3.4",
1608+ "integrity": "sha256-DSIJ8YWr5vB5IMpyaBALM+fx57lz3AAAR0MGtfHeWxo=",
16931609 "dependencies": [
16941610 "aff",
1611+ "arraybuffer-types",
16951612 "arrays",
1613+ "effect",
16961614 "either",
16971615 "foldable-traversable",
16981616 "foreign",
16991617 "foreign-object",
17001618 "identity",
17011619 "integers",
1620+ "js-promise",
1621+ "js-promise-aff",
17021622 "maybe",
17031623 "newtype",
1624+ "nullable",
1625+ "numbers",
17041626 "prelude",
17051627 "record",
17061628 "strings",
spago.yaml+18−6
@@ -2,7 +2,7 @@ workspace: {}
22package:
33 name: yoga-fetch-om
44 publish:
5− version: 0.7.0
5+ version: 0.8.1
66 license: MIT
77 location:
88 githubOwner: rowtype-yoga
@@ -10,7 +10,6 @@ package:
1010 include:
1111 - LICENSE
1212 - README.md
13− - CHANGELOG.md
1413 exclude:
1514 - .github
1615 dependencies:
@@ -27,20 +26,33 @@ package:
2726 - prelude: ">=6.0.2 <7.0.0"
2827 - record: ">=4.0.0 <5.0.0"
2928 - strings: ">=6.0.1 <7.0.0"
29+ - tailrec: ">=6.1.0 <7.0.0"
3030 - transformers: ">=6.1.0 <7.0.0"
3131 - tuples: ">=7.0.0 <8.0.0"
3232 - typelevel-prelude: ">=7.0.0 <8.0.0"
3333 - unsafe-coerce: ">=6.0.0 <7.0.0"
3434 - variant: ">=8.0.0 <9.0.0"
35− - yoga-http-api: ">=0.3.2 <0.4.0"
35+ - yoga-http-api: ">=0.3.4 <0.4.0"
3636 - yoga-json: ">=5.2.1 <6.0.0"
3737 - yoga-om: ">=1.5.2 <2.0.0"
3838 - yoga-om-strom: ">=0.4.3 <0.5.0"
3939 test:
4040 main: Test.Main
4141 dependencies:
42− - spec: ">=8.0.0 <9.0.0"
43− - spec-discovery: ">=8.0.0 <9.0.0"
42+ - enums
43+ - foldable-traversable
44+ - foreign
45+ - foreign-object
46+ - integers
47+ - js-promise
48+ - partial
49+ - yoga-fastify
4450 - console: ">=6.0.0 <7.0.0"
45− - yoga-fastify-om: ">=0.4.5 <0.5.0"
51+ - js-uri: ">=3.1.0 <4.0.0"
52+ - justifill: ">=0.5.0 <0.6.0"
53+ - quickcheck: ">=8.0.1 <9.0.0"
4654 - refs: ">=6.0.0 <7.0.0"
55+ - spec: ">=8.0.0 <9.0.0"
56+ - spec-discovery: ">=8.0.0 <9.0.0"
57+ - spec-quickcheck: ">=5.0.2 <6.0.0"
58+ - yoga-fastify-om: ">=0.4.7 <0.5.0"
src/Yoga/Fetch/Om.jsnew file+1
@@ -0,0 +1 @@
1+export const mergeRecords = (left) => (right) => ({ ...right, ...left });
src/Yoga/Fetch/Om.purs+135−8
@@ -1,5 +1,8 @@
11module Yoga.Fetch.Om
22 ( client
3+ , clientWith
4+ , ClientInputs
5+ , ClientWithConfig
36 , deriveClient
47 , class DeriveClient
58 , deriveClientImpl
@@ -7,12 +10,17 @@ module Yoga.Fetch.Om
710 , deriveClientRL
811 , class DeriveClientFn
912 , deriveClientFn
13+ , class DeriveClientFnWith
14+ , deriveClientFnWith
15+ , class DeriveClientWith
16+ , deriveClientWithImpl
17+ , class DeriveClientWithRL
18+ , deriveClientWithRL
1019 , class RecordRow
1120 , class ToHeaders
1221 , toHeaders
1322 , module Yoga.HTTP.API.Route
1423 , module Yoga.HTTP.API.Path
15− , plainText
1624 , module Yoga.Fetch.Om.StreamDecode
1725 , module Yoga.Fetch.Om.Simple
1826 ) where
@@ -40,9 +48,21 @@ import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
4048import Yoga.Fetch.Om.SplitResponses (class SplitResponses)
4149import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
4250import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
43−import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
51+import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, QUERY, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
4452import Yoga.HTTP.API.Route.Handler (class SegmentPathParams, class SegmentQueryParams)
45−import Yoga.Om (class ToOm, Om, toOm)
53+import Yoga.Om (Om, toOm)
54+
55+type ClientInputs providedHeaders providedQuery =
56+ { headers :: Record providedHeaders
57+ , query :: Record providedQuery
58+ }
59+
60+type ClientWithConfig ctx err providedHeaders providedQuery =
61+ { baseUrl :: Om { | ctx } err String
62+ , provide :: Om { | ctx } err (ClientInputs providedHeaders providedQuery)
63+ }
64+
65+foreign import mergeRecords :: forall left right out. Record left -> Record right -> Record out
4666
4767-- | Extract row type from Record type
4868class RecordRow :: Type -> Row Type -> Constraint
@@ -100,11 +120,11 @@ instance
100120 , ToHeaders headersRL headers
101121 , CheckBodyIsUnit body bodyFlag
102122 , Row.Union routeErrors extraErr errRow
103− , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers { | ctx } errRow result fn
123+ , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers (Record pathQuery) { | ctx } errRow result fn
104124 ) =>
105125 DeriveClientFn method segments request response ctx extraErr routeErrors result fn where
106126 deriveClientFn baseUrl _ =
107− buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) impl
127+ buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) identity impl
108128 where
109129 impl :: Record pathQuery -> Record headers -> body -> Om { | ctx } errRow result
110130 impl pathQueryRec headersRec bodyVal = widenOm do
@@ -117,18 +137,88 @@ instance
117137 where
118138 pathParamsRec = unsafeCoerce pathQueryRec :: Record pathParams
119139 queryParamsRec = unsafeCoerce pathQueryRec :: Record queryParams
140+
120141 widenOm :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
121142 widenOm = unsafeCoerce
122143
144+class DeriveClientFnWith
145+ :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
146+class
147+ DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn
148+ | method segments request response -> routeErrors result
149+ , method segments request response providedHeaders providedQuery ctx extraErr -> fn where
150+ deriveClientFnWith
151+ :: ClientWithConfig ctx extraErr providedHeaders providedQuery
152+ -> Proxy (Route method segments request response)
153+ -> fn
154+
155+instance
156+ ( SegmentPathParams segments pathParams
157+ , SegmentQueryParams segments queryParams
158+ , ExtractRequestBody request body
159+ , ExtractBodyEncoding request encoding
160+ , ExtractRequestHeaders request headers
161+ , PathPattern segments
162+ , BuildUrl segments pathParams queryParams
163+ , MakeRequest method
164+ , BodyEncoding encoding body
165+ , SplitResponses response successRow routeErrors
166+ , Row.Union pathParams queryParams pathQuery
167+ , Row.Nub pathQuery pathQuery
168+ , ParseResponse routeErrors successRow
169+ , RowToList successRow successRL
170+ , VariantOrValue successRL successRow result
171+ , Row.Union routeErrors extraErr errRow
172+ , Row.Union providedQuery remainingQuery queryParams
173+ , Row.Union pathParams remainingQuery callerPathQuery
174+ , Row.Nub callerPathQuery callerPathQuery
175+ , RowToList callerPathQuery callerPathQueryRL
176+ , RowToList headers headersRL
177+ , ToHeaders headersRL headers
178+ , Row.Union providedHeaders remainingHeaders headers
179+ , Row.Nub headers headers
180+ , RowToList remainingHeaders remainingHeadersRL
181+ , CheckBodyIsUnit body bodyFlag
182+ , BuildClientFn callerPathQueryRL remainingHeadersRL bodyFlag body callerPathQuery remainingHeaders (Record callerPathQuery) { | ctx } errRow result fn
183+ ) =>
184+ DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn where
185+ deriveClientFnWith config _ =
186+ buildClientFn (Proxy :: _ callerPathQueryRL) (Proxy :: _ remainingHeadersRL) (Proxy :: _ bodyFlag) identity impl
187+ where
188+ impl :: Record callerPathQuery -> Record remainingHeaders -> body -> Om { | ctx } errRow result
189+ impl pathQueryRec headersRec bodyVal = do
190+ baseUrl <- widenExtra config.baseUrl
191+ provided <- widenExtra config.provide
192+ let pathParamsRec = unsafeCoerce pathQueryRec :: Record pathParams
193+ let queryParamsRec = mergeRecords provided.query (unsafeCoerce pathQueryRec :: Record remainingQuery) :: Record queryParams
194+ let fullHeaders = mergeRecords provided.headers headersRec :: Record headers
195+ let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
196+ let hdrs = toHeaders (Proxy :: _ headersRL) fullHeaders
197+ let ct = encodingContentType (Proxy :: _ encoding)
198+ fetchResp <- makeRequest (Proxy :: _ method) url hdrs ct (encodeBody @encoding bodyVal) # toOm # widenRoute
199+ variant <- parseResponse fetchResp # widenRoute :: Om { | ctx } errRow (Variant successRow)
200+ variantOrValue (Proxy :: _ successRL) variant # pure
201+ where
202+ widenExtra :: Om { | ctx } extraErr ~> Om { | ctx } errRow
203+ widenExtra = unsafeCoerce
204+
205+ widenRoute :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
206+ widenRoute = unsafeCoerce
207+
123208class DeriveClient :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
124209class DeriveClient ctx extraErr routesRow clientsRow | routesRow ctx extraErr -> clientsRow where
125210 deriveClientImpl :: String -> Proxy (Record routesRow) -> Record clientsRow
126211
127−-- | Derive API client functions from route definitions using VTA
212+-- | Derive API client functions from route definitions using VTA.
213+-- | Generated fields use ordinary function application. Path parameters are
214+-- | plain records; optional query parameters are explicit `Maybe` fields.
128215-- |
129216-- | ```purescript
130217-- | type UserAPI = { getUser :: Route ... }
131218-- | api = client @UserAPI "https://api.example.com"
219+-- | user <- api.getUser { id: 42 }
220+-- | users <- api.listUsers { limit: Just 10, offset: Nothing }
221+-- | health <- api.health
132222-- | ```
133223client :: forall @routes routesRow ctx extraErr clientsRow. RecordRow routes routesRow => DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
134224client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
@@ -141,8 +231,13 @@ client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesR
141231deriveClient :: forall @routesRow ctx extraErr clientsRow. DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
142232deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
143233
144−plainText :: PlainText -> String
145−plainText = unsafeCoerce
234+clientWith
235+ :: forall @routes @providedHeaders @providedQuery routesRow ctx extraErr clientsRow
236+ . RecordRow routes routesRow
237+ => DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow
238+ => ClientWithConfig ctx extraErr providedHeaders providedQuery
239+ -> Record clientsRow
240+clientWith config = deriveClientWithImpl @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ { | routesRow })
146241
147242instance
148243 ( RowToList routesRow rl
@@ -151,6 +246,38 @@ instance
151246 DeriveClient ctx extraErr routesRow clientsRow where
152247 deriveClientImpl baseUrl _ = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ rl) {}
153248
249+class DeriveClientWith :: Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Constraint
250+class DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow | providedHeaders providedQuery routesRow ctx extraErr -> clientsRow where
251+ deriveClientWithImpl :: ClientWithConfig ctx extraErr providedHeaders providedQuery -> Proxy (Record routesRow) -> Record clientsRow
252+
253+instance
254+ ( RowToList routesRow rl
255+ , DeriveClientWithRL providedHeaders providedQuery ctx extraErr rl () clientsRow
256+ ) =>
257+ DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow where
258+ deriveClientWithImpl config _ = deriveClientWithRL @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ rl) {}
259+
260+class DeriveClientWithRL :: Row Type -> Row Type -> Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
261+class DeriveClientWithRL providedHeaders providedQuery ctx extraErr rl acc out | providedHeaders providedQuery ctx extraErr rl acc -> out where
262+ deriveClientWithRL :: ClientWithConfig ctx extraErr providedHeaders providedQuery -> Proxy rl -> Record acc -> Record out
263+
264+instance DeriveClientWithRL providedHeaders providedQuery ctx extraErr RL.Nil acc acc where
265+ deriveClientWithRL _ _ acc = acc
266+
267+instance
268+ ( IsSymbol label
269+ , DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn
270+ , DeriveClientWithRL providedHeaders providedQuery ctx extraErr tail acc1 acc2
271+ , Row.Cons label fn acc2 out
272+ , Row.Lacks label acc2
273+ , Row.Cons label (Route method segments request response) routeTail routeRow
274+ ) =>
275+ DeriveClientWithRL providedHeaders providedQuery ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
276+ deriveClientWithRL config _ acc = Record.insert (Proxy :: _ label) clientFn rest
277+ where
278+ clientFn = deriveClientFnWith @_ @_ @_ @_ @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ (Route method segments request response))
279+ rest = deriveClientWithRL @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ tail) acc
280+
154281class DeriveClientRL :: Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
155282class DeriveClientRL ctx extraErr rl acc out | ctx extraErr rl acc -> out where
156283 deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
src/Yoga/Fetch/Om/BuildUrl.js+86−2
@@ -1,3 +1,87 @@
1−export function encodeURIComponent_(s) {
2− return encodeURIComponent(s);
1+export function encodeURIComponent_(value) {
2+ try {
3+ return encodeURIComponent(value);
4+ } catch (error) {
5+ if (!(error instanceof URIError)) throw error;
6+ return encodeURIComponent(toWellFormed(value));
7+ }
8+}
9+
10+function toWellFormed(value) {
11+ if (typeof value.toWellFormed === "function") return value.toWellFormed();
12+
13+ let result = "";
14+ let sliceStart = 0;
15+ for (let index = 0; index < value.length; index += 1) {
16+ const codeUnit = value.charCodeAt(index);
17+ const previous = index === 0 ? 0 : value.charCodeAt(index - 1);
18+ const next =
19+ index + 1 === value.length ? 0 : value.charCodeAt(index + 1);
20+ const loneHigh =
21+ codeUnit >= 0xd800 &&
22+ codeUnit <= 0xdbff &&
23+ (next < 0xdc00 || next > 0xdfff);
24+ const loneLow =
25+ codeUnit >= 0xdc00 &&
26+ codeUnit <= 0xdfff &&
27+ (previous < 0xd800 || previous > 0xdbff);
28+
29+ if (!loneHigh && !loneLow) continue;
30+ result += value.slice(sliceStart, index) + "\ufffd";
31+ sliceStart = index + 1;
32+ }
33+
34+ return sliceStart === 0 ? value : result + value.slice(sliceStart);
35+}
36+
37+function urlSuffixIndex(url) {
38+ const queryIndex = url.indexOf("?");
39+ const fragmentIndex = url.indexOf("#");
40+ return Math.min(
41+ queryIndex === -1 ? url.length : queryIndex,
42+ fragmentIndex === -1 ? url.length : fragmentIndex,
43+ );
44+}
45+
46+export function appendPath_(base) {
47+ return function (path) {
48+ const suffixIndex = urlSuffixIndex(base);
49+ const prefix = base.slice(0, suffixIndex).replace(/\/+$/, "");
50+ return prefix + path + base.slice(suffixIndex);
51+ };
52+}
53+
54+
55+export function substitutePathParam_(name) {
56+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57+ const segment = new RegExp(`(^|/):${escapedName}(?=/|[?#]|$)`, "g");
58+ return function (value) {
59+ const encodedValue = encodeURIComponent_(value);
60+ return function (url) {
61+ const suffixIndex = urlSuffixIndex(url);
62+ const path = url.slice(0, suffixIndex);
63+ return (
64+ path.replace(segment, (_, prefix) => prefix + encodedValue) +
65+ url.slice(suffixIndex)
66+ );
67+ };
68+ };
69+}
70+
71+export function appendQueryString_(url) {
72+ return function (queryString) {
73+ if (queryString === "") return url;
74+
75+ const fragmentIndex = url.indexOf("#");
76+ const beforeFragment =
77+ fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
78+ const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
79+ const separator = beforeFragment.includes("?")
80+ ? beforeFragment.endsWith("?") || beforeFragment.endsWith("&")
81+ ? ""
82+ : "&"
83+ : "?";
84+
85+ return beforeFragment + separator + queryString + fragment;
86+ };
387}
src/Yoga/Fetch/Om/BuildUrl.purs+43−13
@@ -19,7 +19,6 @@ import Prelude
1919
2020import Data.Array as Array
2121import Data.Maybe (Maybe(..))
22−import Data.String (Pattern(..), Replacement(..))
2322import Data.String as String
2423import Data.Symbol (class IsSymbol, reflectSymbol)
2524import Prim.Row as Row
@@ -28,8 +27,13 @@ import Prim.RowList as RL
2827import Record as Record
2928import Type.Proxy (Proxy(..))
3029import Yoga.HTTP.API.Path (class PathPattern, pathPattern)
30+import Yoga.Fetch.Om.ReadOptional (readOptional)
3131
3232foreign import encodeURIComponent_ :: String -> String
33+foreign import appendPath_ :: String -> String -> String
34+foreign import substitutePathParam_ :: String -> String -> String -> String
35+
36+foreign import appendQueryString_ :: String -> String -> String
3337
3438class BuildUrl :: forall k. k -> Row Type -> Row Type -> Constraint
3539class BuildUrl segments pathParams queryParams | segments -> pathParams queryParams where
@@ -46,10 +50,7 @@ instance
4650 pattern = pathPattern segmentsProxy
4751 withBase = case baseUrl of
4852 "" -> pattern
49− base -> dropTrailingSlash base <> pattern
50− dropTrailingSlash s = case String.stripSuffix (Pattern "/") s of
51− Just s' -> dropTrailingSlash s'
52− Nothing -> s
53+ base -> appendPath_ base pattern
5354 withPathParams = substitutePathParamsImpl (Proxy :: _ pathParams) pathParamsRec withBase
5455 withQueryParams = appendQueryParamsImpl (Proxy :: _ queryParams) queryParamsRec withPathParams
5556
@@ -84,8 +85,8 @@ instance
8485 substitutePathParamsRL _ rec url = substitutePathParamsRL (Proxy :: _ tail) rec replaced
8586 where
8687 value = Record.get (Proxy :: _ name) rec
87− pattern = Pattern (":" <> reflectSymbol (Proxy :: _ name))
88− replaced = String.replaceAll pattern (Replacement (serializeParam value)) url
88+ name = reflectSymbol (Proxy :: _ name)
89+ replaced = substitutePathParam_ name (serializeParam value) url
8990
9091class AppendQueryParams :: Row Type -> Constraint
9192class AppendQueryParams params where
@@ -99,12 +100,11 @@ instance
99100 , AppendQueryParamsRL rl params
100101 ) =>
101102 AppendQueryParams params where
102− appendQueryParamsImpl _ rec url = url <> queryString
103+ appendQueryParamsImpl _ rec url = case pairs of
104+ [] -> url
105+ _ -> appendQueryString_ url (String.joinWith "&" pairs)
103106 where
104107 pairs = appendQueryParamsRL (Proxy :: _ rl) rec []
105− queryString = case pairs of
106− [] -> ""
107− _ -> "?" <> String.joinWith "&" pairs
108108
109109class AppendQueryParamsRL :: RowList Type -> Row Type -> Constraint
110110class AppendQueryParamsRL rl r where
@@ -113,19 +113,49 @@ class AppendQueryParamsRL rl r where
113113instance AppendQueryParamsRL RL.Nil r where
114114 appendQueryParamsRL _ _ acc = acc
115115
116+-- Maybe (Array ty): optional repeated query param — missing/empty field → no pairs
116117instance
117118 ( IsSymbol name
118119 , SerializeParam ty
119120 , AppendQueryParamsRL tail r
120− , Row.Cons name (Maybe ty) tailRow r
121+ ) =>
122+ AppendQueryParamsRL (RL.Cons name (Maybe (Array ty)) tail) r where
123+ appendQueryParamsRL _ rec acc = appendQueryParamsRL (Proxy :: _ tail) rec acc'
124+ where
125+ encName = encodeURIComponent_ (reflectSymbol (Proxy :: _ name))
126+ acc' = case (readOptional @name rec :: Maybe (Array ty)) of
127+ Nothing -> acc
128+ Just values -> acc <> map (\v -> encName <> "=" <> encodeURIComponent_ (serializeParam v)) values
129+
130+-- Maybe ty: read with undefined-awareness — missing field → skip
131+else instance
132+ ( IsSymbol name
133+ , SerializeParam ty
134+ , AppendQueryParamsRL tail r
121135 ) =>
122136 AppendQueryParamsRL (RL.Cons name (Maybe ty) tail) r where
123137 appendQueryParamsRL _ rec acc = appendQueryParamsRL (Proxy :: _ tail) rec acc'
124138 where
125− acc' = case Record.get (Proxy :: _ name) rec of
139+ acc' = case (readOptional @name rec :: Maybe ty) of
126140 Nothing -> acc
127141 Just value -> Array.snoc acc (encodeURIComponent_ (reflectSymbol (Proxy :: _ name)) <> "=" <> encodeURIComponent_ (serializeParam value))
128142
143+-- Array ty: emit one `name=value` pair per element (repeated keys)
144+else instance
145+ ( IsSymbol name
146+ , SerializeParam ty
147+ , AppendQueryParamsRL tail r
148+ , Row.Cons name (Array ty) tailRow r
149+ ) =>
150+ AppendQueryParamsRL (RL.Cons name (Array ty) tail) r where
151+ appendQueryParamsRL _ rec acc =
152+ appendQueryParamsRL (Proxy :: _ tail) rec (acc <> pairs)
153+ where
154+ encName = encodeURIComponent_ (reflectSymbol (Proxy :: _ name))
155+ values = Record.get (Proxy :: _ name) rec
156+ pairs = map (\v -> encName <> "=" <> encodeURIComponent_ (serializeParam v)) values
157+
158+-- bare ty (Required query param): always present
129159else instance
130160 ( IsSymbol name
131161 , SerializeParam ty
src/Yoga/Fetch/Om/ClientFunction.purs+24−15
@@ -22,16 +22,17 @@ instance CheckBodyIsUnit Unit IsUnit
2222else instance CheckBodyIsUnit body IsNotUnit
2323
2424class BuildClientFn
25− :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Type -> Row Type -> Type -> Type -> Constraint
25+ :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Type -> Type -> Row Type -> Type -> Type -> Constraint
2626class
27− BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers ctx errorRow result fn
27+ BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers arg ctx errorRow result fn
2828 | pathQueryRL -> pathQuery
2929 , headersRL -> headers
30− , pathQueryRL headersRL bodyFlag body ctx errorRow result -> fn where
30+ , pathQueryRL headersRL bodyFlag body arg ctx errorRow result -> fn where
3131 buildClientFn
3232 :: Proxy pathQueryRL
3333 -> Proxy headersRL
3434 -> Proxy bodyFlag
35+ -> (arg -> Record pathQuery)
3536 -> (Record pathQuery -> Record headers -> body -> Om ctx errorRow result)
3637 -> fn
3738
@@ -43,11 +44,12 @@ instance
4344 Unit
4445 ()
4546 ()
47+ arg
4648 ctx
4749 errorRow
4850 result
4951 (Om ctx errorRow result) where
50− buildClientFn _ _ _ f = f {} {} unit
52+ buildClientFn _ _ _ _ f = f {} {} unit
5153
5254-- No path/query, no headers, with body
5355instance
@@ -57,11 +59,12 @@ instance
5759 body
5860 ()
5961 ()
62+ arg
6063 ctx
6164 errorRow
6265 result
6366 (body -> Om ctx errorRow result) where
64− buildClientFn _ _ _ f b = f {} {} b
67+ buildClientFn _ _ _ _ f b = f {} {} b
6568
6669-- No path/query, with headers, no body
6770instance
@@ -71,11 +74,12 @@ instance
7174 Unit
7275 ()
7376 h
77+ arg
7478 ctx
7579 errorRow
7680 result
7781 (Record h -> Om ctx errorRow result) where
78− buildClientFn _ _ _ f hdrs = f {} hdrs unit
82+ buildClientFn _ _ _ _ f hdrs = f {} hdrs unit
7983
8084-- No path/query, with headers, with body
8185instance
@@ -85,11 +89,12 @@ instance
8589 body
8690 ()
8791 h
92+ arg
8893 ctx
8994 errorRow
9095 result
9196 (Record h -> body -> Om ctx errorRow result) where
92− buildClientFn _ _ _ f hdrs b = f {} hdrs b
97+ buildClientFn _ _ _ _ f hdrs b = f {} hdrs b
9398
9499-- With path/query, no headers, no body
95100instance
@@ -99,11 +104,12 @@ instance
99104 Unit
100105 pq
101106 ()
107+ arg
102108 ctx
103109 errorRow
104110 result
105− (Record pq -> Om ctx errorRow result) where
106− buildClientFn _ _ _ f pqr = f pqr {} unit
111+ (arg -> Om ctx errorRow result) where
112+ buildClientFn _ _ _ convert f pqr = f (convert pqr) {} unit
107113
108114-- With path/query, no headers, with body
109115instance
@@ -113,11 +119,12 @@ instance
113119 body
114120 pq
115121 ()
122+ arg
116123 ctx
117124 errorRow
118125 result
119− (Record pq -> body -> Om ctx errorRow result) where
120− buildClientFn _ _ _ f pqr b = f pqr {} b
126+ (arg -> body -> Om ctx errorRow result) where
127+ buildClientFn _ _ _ convert f pqr b = f (convert pqr) {} b
121128
122129-- With path/query, with headers, no body
123130instance
@@ -127,11 +134,12 @@ instance
127134 Unit
128135 pq
129136 h
137+ arg
130138 ctx
131139 errorRow
132140 result
133− (Record pq -> Record h -> Om ctx errorRow result) where
134− buildClientFn _ _ _ f pqr hdrs = f pqr hdrs unit
141+ (arg -> Record h -> Om ctx errorRow result) where
142+ buildClientFn _ _ _ convert f pqr hdrs = f (convert pqr) hdrs unit
135143
136144-- With path/query, with headers, with body
137145instance
@@ -141,8 +149,9 @@ instance
141149 body
142150 pq
143151 h
152+ arg
144153 ctx
145154 errorRow
146155 result
147− (Record pq -> Record h -> body -> Om ctx errorRow result) where
148− buildClientFn _ _ _ f = f
156+ (arg -> Record h -> body -> Om ctx errorRow result) where
157+ buildClientFn _ _ _ convert f pqr hdrs b = f (convert pqr) hdrs b
src/Yoga/Fetch/Om/MakeRequest.purs+6−13
@@ -2,8 +2,6 @@ module Yoga.Fetch.Om.MakeRequest
22 ( class MakeRequest
33 , httpMethod
44 , makeRequest
5− , class SerializeBody
6− , serializeBody
75 , class BodyEncoding
86 , encodingContentType
97 , encodeBody
@@ -32,8 +30,8 @@ import JS.Fetch.Integrity (Integrity(..))
3230import JS.Fetch.Duplex as Duplex
3331import JS.Fetch.Response (Response) as FetchResponse
3432import Promise.Aff as Promise
35−import Type.Proxy (Proxy(..))
36−import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
33+import Type.Proxy (Proxy)
34+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, PlainText, NoBody)
3735import Yoga.HTTP.API.Route.Method as Method
3836import Yoga.JSON (class WriteForeign, writeJSON)
3937
@@ -96,15 +94,6 @@ makeRequest proxy url customHeaders bodyContentType maybeBody = do
9694 , cache: Cache.Default
9795 }
9896
99−class SerializeBody :: Type -> Constraint
100−class SerializeBody body where
101− serializeBody :: body -> Maybe String
102−
103−instance SerializeBody Unit where
104− serializeBody _ = Nothing
105−else instance WriteForeign body => SerializeBody body where
106− serializeBody b = Just (writeJSON b)
107−
10897class BodyEncoding :: Type -> Type -> Constraint
10998class BodyEncoding encoding body | encoding -> body where
11099 encodingContentType :: Proxy encoding -> String
@@ -114,6 +103,10 @@ instance WriteForeign ty => BodyEncoding (JSON ty) ty where
114103 encodingContentType _ = "application/json"
115104 encodeBody b = Just (writeJSON b)
116105
106+else instance BodyEncoding PlainText String where
107+ encodingContentType _ = "text/plain"
108+ encodeBody = Just
109+
117110else instance BodyEncoding (FormData ty) ty where
118111 encodingContentType _ = "application/x-www-form-urlencoded"
119112 encodeBody b = Just (toUrlEncoded b)
src/Yoga/Fetch/Om/ParseResponse.purs+11−30
@@ -29,11 +29,10 @@ import Promise.Aff as Promise
2929import Type.Proxy (Proxy(..))
3030import Unsafe.Coerce (unsafeCoerce)
3131import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
32−import Yoga.HTTP.API.Route.Encoding (PlainText, Streaming)
3332import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
3433import Yoga.JSON (class ReadForeign, readJSON)
3534import Yoga.JSON.Error (withStringErrors)
36−import Yoga.Om (class ToOm, Om, toOm)
35+import Yoga.Om (Om, toOm)
3736import Yoga.Om.Error (Exception)
3837import Yoga.Om.Strom (Strom)
3938import Yoga.Om.Strom.WebStream as WebStream
@@ -75,37 +74,35 @@ instance ParseSuccessRL RL.Nil successRow where
7574else instance
7675 ( IsSymbol label
7776 , StatusCodeMap label
78− , Row.Cons label PlainText tailRow successRow
77+ , StreamDecode a
78+ , Row.Cons label (Strom streamContext streamError a) tailRow successRow
7979 , Row.Lacks label tailRow
8080 , ParseSuccessRL tailRL successRow
8181 ) =>
82− ParseSuccessRL (RL.Cons label PlainText tailRL) successRow where
82+ ParseSuccessRL (RL.Cons label (Strom streamContext streamError a) tailRL) successRow where
8383 parseSuccessRL _ actualStatus fetchResp = do
8484 let StatusCode expected = statusCodeFor (Proxy :: _ label)
8585 if actualStatus == expected then do
86− text <- liftEffect (Fetch.text fetchResp) >>= Promise.toAff
87− pure (Variant.inj (Proxy :: _ label) (unsafeCoerce text))
86+ webStream <- liftEffect (Fetch.body fetchResp)
87+ let strom = decodeStream (WebStream.fromReadableStream (unsafeCoerce webStream)) :: Strom {} () a
88+ pure (Variant.inj (Proxy :: _ label) (unsafeCoerce strom))
8889 else
8990 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
9091
9192else instance
9293 ( IsSymbol label
9394 , StatusCodeMap label
94− , StreamDecode a
95− , Row.Cons label (Streaming a) tailRow successRow
95+ , Row.Cons label Unit tailRow successRow
9696 , Row.Lacks label tailRow
9797 , ParseSuccessRL tailRL successRow
9898 ) =>
99− ParseSuccessRL (RL.Cons label (Streaming a) tailRL) successRow where
99+ ParseSuccessRL (RL.Cons label Unit tailRL) successRow where
100100 parseSuccessRL _ actualStatus fetchResp = do
101101 let StatusCode expected = statusCodeFor (Proxy :: _ label)
102− if actualStatus == expected then do
103− webStream <- liftEffect (Fetch.body fetchResp)
104− let strom = decodeStream (WebStream.fromReadableStream (unsafeCoerce webStream)) :: Strom {} () a
105− pure (Variant.inj (Proxy :: _ label) (unsafeCoerce strom))
102+ if actualStatus == expected then
103+ pure (Variant.inj (Proxy :: _ label) unit)
106104 else
107105 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
108−
109106else instance
110107 ( IsSymbol label
111108 , StatusCodeMap label
@@ -138,22 +135,6 @@ instance ParseErrorRL RL.Nil errorRow where
138135 parseErrorRL _ _ status _ =
139136 throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Unexpected error status code: " <> show status)))
140137
141−else instance
142− ( IsSymbol label
143− , StatusCodeMap label
144− , Row.Cons label PlainText tailRow errorRow
145− , Row.Lacks label tailRow
146− , Row.Cons label PlainText tailExc (Exception errorRow)
147− , ParseErrorRL tailRL errorRow
148− ) =>
149− ParseErrorRL (RL.Cons label PlainText tailRL) errorRow where
150− parseErrorRL _ errorProxy actualStatus text = do
151− let StatusCode expected = statusCodeFor (Proxy :: _ label)
152− if actualStatus == expected then
153− throwError (Variant.inj (Proxy :: _ label) (unsafeCoerce text))
154− else
155− parseErrorRL (Proxy :: _ tailRL) errorProxy actualStatus text
156−
157138else instance
158139 ( IsSymbol label
159140 , StatusCodeMap label
src/Yoga/Fetch/Om/ReadOptional.jsnew file+18
@@ -0,0 +1,18 @@
1+import * as Maybe from "../Data.Maybe/index.js";
2+
3+export function readOptionalImpl(key) {
4+ return function (obj) {
5+ const val = obj[key];
6+ if (val === undefined || val instanceof Maybe.Nothing) {
7+ return Maybe.Nothing.value;
8+ }
9+ if (val instanceof Maybe.Just) return val;
10+ return Maybe.Just.create(val);
11+ };
12+}
13+
14+export function readRequiredField(key) {
15+ return function (obj) {
16+ return obj[key];
17+ };
18+}
src/Yoga/Fetch/Om/ReadOptional.pursnew file+56
@@ -0,0 +1,56 @@
1+module Yoga.Fetch.Om.ReadOptional
2+ ( readOptional
3+ , class ReadPartialQuery
4+ , readPartialQuery
5+ ) where
6+
7+import Data.Maybe (Maybe)
8+import Data.Symbol (class IsSymbol, reflectSymbol)
9+import Prim.Row as Row
10+import Prim.RowList (RowList)
11+import Prim.RowList as RL
12+import Record as Record
13+import Type.Proxy (Proxy(..))
14+
15+foreign import readOptionalImpl :: forall a r. String -> Record r -> Maybe a
16+
17+-- | Accept explicit `Maybe` fields as well as undefined-aware partial records.
18+readOptional :: forall @name ty r. IsSymbol name => Record r -> Maybe ty
19+readOptional rec = readOptionalImpl (reflectSymbol (Proxy :: _ name)) rec
20+
21+-- | Read query params from a potentially-partial record.
22+-- | Walks the query params RowList:
23+-- | Maybe ty → read with undefined-awareness (present → Just, absent → Nothing)
24+-- | bare ty → read directly (required, always present)
25+class ReadPartialQuery (rl :: RowList Type) (queryParams :: Row Type) | rl -> queryParams where
26+ readPartialQuery :: forall r. Record r -> Record queryParams
27+
28+instance ReadPartialQuery RL.Nil () where
29+ readPartialQuery _ = {}
30+
31+-- Maybe ty: optional field, may be undefined at runtime
32+instance
33+ ( IsSymbol name
34+ , ReadPartialQuery tail tailQuery
35+ , Row.Cons name (Maybe ty) tailQuery queryParams
36+ , Row.Lacks name tailQuery
37+ ) =>
38+ ReadPartialQuery (RL.Cons name (Maybe ty) tail) queryParams where
39+ readPartialQuery rec = Record.insert (Proxy :: _ name) (readOptional @name rec) rest
40+ where
41+ rest = readPartialQuery @tail rec
42+
43+-- bare ty: required field (from Required query params), always present
44+else instance
45+ ( IsSymbol name
46+ , ReadPartialQuery tail tailQuery
47+ , Row.Cons name ty tailQuery queryParams
48+ , Row.Lacks name tailQuery
49+ ) =>
50+ ReadPartialQuery (RL.Cons name ty tail) queryParams where
51+ readPartialQuery rec = Record.insert (Proxy :: _ name) val rest
52+ where
53+ val = readRequiredField (reflectSymbol (Proxy :: _ name)) rec
54+ rest = readPartialQuery @tail rec
55+
56+foreign import readRequiredField :: forall a r. String -> Record r -> a
src/Yoga/Fetch/Om/Simple.purs+6−15
@@ -32,21 +32,14 @@ import JS.Fetch as Fetch
3232import JS.Fetch.Duplex as Duplex
3333import JS.Fetch.Headers (Headers)
3434import JS.Fetch.Headers as Headers
35−import JS.Fetch.Integrity (Integrity(..))
36−import JS.Fetch.Referrer as Referrer
37−import JS.Fetch.ReferrerPolicy as ReferrerPolicy
3835import JS.Fetch.Request as Request
3936import JS.Fetch.RequestBody as Body
40−import JS.Fetch.RequestCache as Cache
41−import JS.Fetch.RequestCredentials as Credentials
42−import JS.Fetch.RequestMode as Mode
4337import JS.Fetch.Response as Resp
4438import Promise.Aff as Promise
4539import Type.Row.Homogeneous (class Homogeneous)
46−-- PlainText removed, using local phantom type
4740import Yoga.JSON (class ReadForeign, class WriteForeign, readJSON, writeJSON)
4841import Yoga.JSON.Error (withStringErrors)
49−import Yoga.Om (class ToOm, Om, toOm, throw)
42+import Yoga.Om (Om, toOm, throw)
5043
5144type FetchError = { status :: Int, body :: String }
5245
@@ -56,9 +49,7 @@ class DecodeResponse :: forall k. k -> Type -> Constraint
5649class DecodeResponse a result | a -> result where
5750 decodeResponse :: String -> Either String result
5851
59−data PlainTextResponse
60−
61−instance DecodeResponse PlainTextResponse String where
52+instance DecodeResponse String String where
6253 decodeResponse = Right
6354
6455else instance ReadForeign a => DecodeResponse a a where
@@ -147,7 +138,7 @@ delete_
147138 => String
148139 -> Record headers
149140 -> Om ctx (fetchError :: FetchError | err) Unit
150−delete_ url headers = void $ simpleFetch @PlainTextResponse DELETE headers url Nothing
141+delete_ url headers = void $ simpleFetch @String DELETE headers url Nothing
151142
152143post
153144 :: forall @a result headers body ctx err
@@ -179,7 +170,7 @@ post_
179170 -> Record headers
180171 -> body
181172 -> Om ctx (fetchError :: FetchError | err) Unit
182−post_ url headers body = void $ simpleFetch @PlainTextResponse POST headers url (Just (writeJSON body))
173+post_ url headers body = void $ simpleFetch @String POST headers url (Just (writeJSON body))
183174
184175put
185176 :: forall @a result headers body ctx err
@@ -211,7 +202,7 @@ put_
211202 -> Record headers
212203 -> body
213204 -> Om ctx (fetchError :: FetchError | err) Unit
214−put_ url headers body = void $ simpleFetch @PlainTextResponse PUT headers url (Just (writeJSON body))
205+put_ url headers body = void $ simpleFetch @String PUT headers url (Just (writeJSON body))
215206
216207patch
217208 :: forall @a result headers body ctx err
@@ -243,4 +234,4 @@ patch_
243234 -> Record headers
244235 -> body
245236 -> Om ctx (fetchError :: FetchError | err) Unit
246−patch_ url headers body = void $ simpleFetch @PlainTextResponse PATCH headers url (Just (writeJSON body))
237+patch_ url headers body = void $ simpleFetch @String PATCH headers url (Just (writeJSON body))
src/Yoga/Fetch/Om/SplitResponses.purs+12−2
@@ -2,6 +2,7 @@ module Yoga.Fetch.Om.SplitResponses
22 ( class IsSuccessStatus
33 , class ExtractBodyRL
44 , class ExtractBody
5+ , class ClientResponseBody
56 , class SplitResponses
67 , class SplitResponsesRL
78 , class DispatchResponse
@@ -13,6 +14,8 @@ import Prim.RowList (RowList)
1314import Prim.RowList as RL
1415import Data.Unit (Unit)
1516import Yoga.HTTP.API.Route (Response)
17+import Yoga.HTTP.API.Route.Encoding (PlainText, Streaming)
18+import Yoga.Om.Strom (Strom)
1619
1720class IsSuccessStatus (label :: Symbol) (isSuccess :: Boolean) | label -> isSuccess
1821
@@ -37,13 +40,20 @@ instance ExtractBodyRL (RL.Cons "body" body tail) body
3740
3841else instance ExtractBodyRL tail body => ExtractBodyRL (RL.Cons label ty tail) body
3942
43+class ClientResponseBody (encoded :: Type) (decoded :: Type) | encoded -> decoded
44+
45+instance ClientResponseBody PlainText String
46+else instance ClientResponseBody (Streaming a) (Strom {} () a)
47+else instance ClientResponseBody body body
48+
4049class ExtractBody (responseType :: Type) (bodyType :: Type) | responseType -> bodyType
4150
42−instance ExtractBody (Response headers body) body
51+instance ClientResponseBody encoded body => ExtractBody (Response headers encoded) body
4352
4453else instance
4554 ( RL.RowToList row rl
46− , ExtractBodyRL rl body
55+ , ExtractBodyRL rl encoded
56+ , ClientResponseBody encoded body
4757 ) =>
4858 ExtractBody (Record row) body
4959
src/Yoga/Fetch/Om/StreamDecode.js+12−1
@@ -1 +1,12 @@
1−export const decodeUtf8 = (uint8array) => new TextDecoder().decode(uint8array);
1+export const newUtf8Decoder = () => new TextDecoder();
2+
3+export const decodeUtf8Chunks = (decoder) => (chunks) => () => {
4+ const decoded = [];
5+ for (const bytes of chunks) {
6+ const text = decoder.decode(bytes, { stream: true });
7+ if (text !== "") decoded.push(text);
8+ }
9+ return decoded;
10+};
11+
12+export const flushUtf8Decoder = (decoder) => () => decoder.decode();
src/Yoga/Fetch/Om/StreamDecode.purs+43−2
@@ -4,7 +4,15 @@ module Yoga.Fetch.Om.StreamDecode
44 ) where
55
66import Prelude
7+
8+import Control.Monad.Rec.Class (Step(..))
9+import Data.Array as Array
710import Data.ArrayBuffer.Types (Uint8Array)
11+import Data.Maybe (Maybe(..))
12+import Data.Tuple.Nested ((/\))
13+import Effect (Effect)
14+import Effect.Class (liftEffect)
15+import Yoga.Om (Om)
816import Yoga.Om.Strom (Strom)
917import Yoga.Om.Strom as Strom
1018
@@ -15,6 +23,39 @@ instance StreamDecode Uint8Array where
1523 decodeStream = identity
1624
1725else instance StreamDecode String where
18− decodeStream = Strom.mapStrom decodeUtf8
26+ decodeStream stream =
27+ Strom.fromOm (liftEffect newUtf8Decoder) >>= \decoder ->
28+ decodeUtf8Stream decoder stream
29+
30+data Utf8Decoder
31+
32+foreign import newUtf8Decoder :: Effect Utf8Decoder
33+
34+foreign import decodeUtf8Chunks :: Utf8Decoder -> Array Uint8Array -> Effect (Array String)
35+
36+foreign import flushUtf8Decoder :: Utf8Decoder -> Effect String
37+
38+decodeUtf8Stream :: Utf8Decoder -> Strom {} () Uint8Array -> Strom {} () String
39+decodeUtf8Stream decoder stream = Strom.mkStrom do
40+ step <- Strom.runStrom stream
41+ case step of
42+ Done maybeChunk -> do
43+ decoded <- decodeChunk decoder maybeChunk
44+ trailing <- liftEffect $ flushUtf8Decoder decoder
45+ let
46+ finalChunk =
47+ if trailing == "" then decoded
48+ else Array.snoc decoded trailing
49+ pure $ Done (nonEmpty finalChunk)
50+ Loop (maybeChunk /\ next) -> do
51+ decoded <- decodeChunk decoder maybeChunk
52+ pure $ Loop (nonEmpty decoded /\ decodeUtf8Stream decoder next)
53+
54+decodeChunk :: Utf8Decoder -> Maybe (Array Uint8Array) -> Om {} () (Array String)
55+decodeChunk _ Nothing = pure []
56+decodeChunk decoder (Just chunk) = liftEffect $ decodeUtf8Chunks decoder chunk
1957
20−foreign import decodeUtf8 :: Uint8Array -> String
58+nonEmpty :: forall a. Array a -> Maybe (Array a)
59+nonEmpty values
60+ | Array.null values = Nothing
61+ | otherwise = Just values
test/BodylessResponse.Example.purs+1−1
@@ -23,5 +23,5 @@ type ResourceAPI =
2323-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
2424api
2525 :: forall ctx err
26− . { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
26+ . { getResource :: Om (Record ctx) (notModified :: Unit, unauthorized :: Unit | err) Resource }
2727api = client @ResourceAPI "https://api.example.com"
test/BodylessResponse.Spec.purs+31−4
@@ -12,9 +12,9 @@ import Test.Spec (Spec, around, describe, it)
1212import Test.Spec.Assertions (shouldEqual)
1313import Yoga.Fastify.Fastify as F
1414import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15−import Yoga.Fastify.Om (createOmFastify, getOm)
16−import Yoga.Fetch.Om (GET, Route, type (/), client)
17−import Yoga.Om (Om, runOm)
15+import Yoga.Fastify.Om (createOmFastify, deleteOm, getOm)
16+import Yoga.Fetch.Om (DELETE, GET, Route, type (/), type (:), client)
17+import Yoga.Om (runOm)
1818
1919type Resource = { id :: Int, name :: String }
2020
@@ -27,9 +27,15 @@ type ResourceAPI =
2727 , notModified :: {}
2828 , unauthorized :: {}
2929 )
30+ , deleteUser ::
31+ Route DELETE
32+ ("bodyless-test" / "users" / "id" : Int)
33+ {}
34+ ( noContent :: {}
35+ , notFound :: { body :: { error :: String } }
36+ )
3037 }
3138
32−api :: forall ctx err. { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
3339api = client @ResourceAPI "http://localhost:44932"
3440
3541spec :: Spec Unit
@@ -68,6 +74,15 @@ spec = around withServer $ describe "bodyless responses (notModified / unauthori
6874 api.getResource
6975 liftAff $ resource `shouldEqual` { id: 1, name: "test" }
7076
77+ it "204 No Content returns Unit for noContent shorthand" \serverMode -> do
78+ liftEffect $ Ref.write "deleted" serverMode
79+ result <- runOm {}
80+ { exception: \_ -> pure false
81+ , notFound: \_ -> pure false
82+ }
83+ (api.deleteUser { id: 1 } $> true)
84+ liftAff $ result `shouldEqual` true
85+
7186withServer :: forall a. (Ref String -> Aff a) -> Aff a
7287withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
7388 where
@@ -77,6 +92,7 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
7792 f <- F.fastify {}
7893 omApp <- createOmFastify {} f
7994 getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
95+ deleteOm (RouteURL "/bodyless-test/users/:id") (deleteHandler serverMode) omApp
8096 pure f
8197 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
8298 pure { fastify, serverMode }
@@ -96,3 +112,14 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
96112 void $ F.status (StatusCode 200) reply # liftEffect
97113 void $ F.header "content-type" "application/json" reply # liftEffect
98114 F.sendJson (unsafeToForeign { id: 1, name: "test" }) reply # liftAff
115+
116+ deleteHandler serverMode reply = do
117+ mode <- liftEffect $ Ref.read serverMode
118+ case mode of
119+ "deleted" -> do
120+ void $ F.status (StatusCode 204) reply # liftEffect
121+ F.send (unsafeToForeign "") reply # liftAff
122+ _ -> do
123+ void $ F.status (StatusCode 404) reply # liftEffect
124+ void $ F.header "content-type" "application/json" reply # liftEffect
125+ F.sendJson (unsafeToForeign { error: "Missing user" }) reply # liftAff
test/BuildUrl.Spec.purs+74−6
@@ -6,6 +6,7 @@ import Data.Maybe (Maybe(..))
66import Test.Spec (Spec, describe, it)
77import Test.Spec.Assertions (shouldEqual)
88import Type.Proxy (Proxy(..))
9+import Unsafe.Coerce (unsafeCoerce)
910import Yoga.Fetch.Om.BuildUrl (buildUrl, substitutePathParams, appendQueryParams)
1011import Yoga.HTTP.API.Path (Path, type (/), type (:))
1112
@@ -33,6 +34,19 @@ spec = do
3334 { slug: "hello-world" }
3435 "/posts/:slug"
3536 result `shouldEqual` "/posts/hello-world"
37+ it "URL-encodes reserved characters in path params" do
38+ let
39+ result = substitutePathParams @(slug :: String)
40+ { slug: "a/b c?d#e" }
41+ "/users/:slug"
42+ result `shouldEqual` "/users/a%2Fb%20c%3Fd%23e"
43+
44+ it "distinguishes path param names with shared prefixes" do
45+ let
46+ result = substitutePathParams @(id :: Int, id2 :: Int)
47+ { id: 1, id2: 2 }
48+ "/pairs/:id/:id2"
49+ result `shouldEqual` "/pairs/1/2"
3650
3751 it "handles empty params" do
3852 let result = substitutePathParams @() {} "/users"
@@ -50,24 +64,32 @@ spec = do
5064 "/users"
5165 result `shouldEqual` "/users?limit=10&offset=20"
5266
53− it "handles Maybe params - Just" do
67+ it "handles optional params - provided via partial record" do
5468 let
5569 result = appendQueryParams @(limit :: Maybe Int)
56− { limit: Just 10 }
70+ (unsafeCoerce { limit: 10 } :: Record (limit :: Maybe Int))
5771 "/users"
5872 result `shouldEqual` "/users?limit=10"
5973
60− it "handles Maybe params - Nothing" do
74+ it "serializes a present explicit Maybe query param" do
75+ let result = appendQueryParams @(id :: Maybe Int) { id: Just 42 } "/users"
76+ result `shouldEqual` "/users?id=42"
77+
78+ it "omits an explicit Nothing query param" do
79+ let result = appendQueryParams @(id :: Maybe Int) { id: Nothing } "/users"
80+ result `shouldEqual` "/users"
81+
82+ it "handles optional params - omitted via partial record" do
6183 let
6284 result = appendQueryParams @(limit :: Maybe Int)
63− { limit: Nothing }
85+ (unsafeCoerce {} :: Record (limit :: Maybe Int))
6486 "/users"
6587 result `shouldEqual` "/users"
6688
67− it "handles mixed Maybe and required params" do
89+ it "handles mixed optional and required params" do
6890 let
6991 result = appendQueryParams @(limit :: Maybe Int, offset :: Int)
70− { limit: Nothing, offset: 20 }
92+ (unsafeCoerce { offset: 20 } :: Record (limit :: Maybe Int, offset :: Int))
7193 "/users"
7294 result `shouldEqual` "/users?offset=20"
7395
@@ -78,6 +100,34 @@ spec = do
78100 it "URL-encodes special characters in values" do
79101 let result = appendQueryParams @(search :: String) { search: "hello world&more=yes" } "/users"
80102 result `shouldEqual` "/users?search=hello%20world%26more%3Dyes"
103+ it "appends query params to an existing query string" do
104+ let result = appendQueryParams @(search :: String) { search: "new value" } "/users?token=old"
105+ result `shouldEqual` "/users?token=old&search=new%20value"
106+
107+ it "inserts query params before a URL fragment" do
108+ let result = appendQueryParams @(search :: String) { search: "new value" } "/users#details"
109+ result `shouldEqual` "/users?search=new%20value#details"
110+
111+ it "repeats Array query params one pair per element" do
112+ let
113+ result = appendQueryParams @(categories :: Array Int)
114+ { categories: [ 3030, 103130 ] }
115+ "/users"
116+ result `shouldEqual` "/users?categories=3030&categories=103130"
117+
118+ it "omits empty Array query params" do
119+ let
120+ result = appendQueryParams @(categories :: Array Int)
121+ { categories: [] }
122+ "/users"
123+ result `shouldEqual` "/users"
124+
125+ it "mixes Array and bare query params" do
126+ let
127+ result = appendQueryParams @(query :: String, categories :: Array Int)
128+ { query: "foo", categories: [ 1, 2 ] }
129+ "/users"
130+ result `shouldEqual` "/users?categories=1&categories=2&query=foo"
81131
82132 describe "buildUrl" do
83133 it "builds URL with path params only" do
@@ -97,3 +147,21 @@ spec = do
97147 {}
98148 { limit: 10, offset: 20 }
99149 result `shouldEqual` "https://api.example.com/users?limit=10&offset=20"
150+
151+ it "appends route paths before base URL query strings and fragments" do
152+ let
153+ result = buildUrl
154+ "https://api.example.com/base/?token=old#details"
155+ (Proxy :: _ (Path "users"))
156+ {}
157+ { search: "new value" }
158+ result `shouldEqual` "https://api.example.com/base/users?token=old&search=new%20value#details"
159+
160+ it "does not substitute path captures inside a base URL query string" do
161+ let
162+ result = buildUrl
163+ "https://api.example.com?redirect=/:id#details"
164+ (Proxy :: _ (Path ("users" / "id" : Int)))
165+ { id: 42 }
166+ {}
167+ result `shouldEqual` "https://api.example.com/users/42?redirect=/:id#details"
test/BuildUrlProperty.Spec.pursnew file+256
@@ -0,0 +1,256 @@
1+module BuildUrlProperty.Spec where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Enum (toEnum)
8+import Data.Foldable (all)
9+import Data.Maybe (Maybe(..), fromJust, isJust)
10+import Data.String as String
11+import Data.String.CodeUnits as CodeUnits
12+import Data.String.Gen (genAsciiString)
13+import Data.String.CodePoints (CodePoint, fromCodePointArray)
14+import Data.String.Pattern (Pattern(..))
15+import JSURI (decodeURIComponent)
16+import Test.QuickCheck ((<?>), (===))
17+import Test.QuickCheck.Arbitrary (arbitrary)
18+import Test.QuickCheck.Gen (Gen, arrayOf1, chooseInt)
19+import Test.Spec (Spec, describe, it)
20+import Test.Spec.Assertions (shouldEqual)
21+import Test.Spec.QuickCheck (quickCheck)
22+import Partial.Unsafe (unsafePartial)
23+import Unsafe.Coerce (unsafeCoerce)
24+import Yoga.Fetch.Om.BuildUrl (appendQueryParams, substitutePathParams)
25+
26+spec :: Spec Unit
27+spec = describe "URL properties" do
28+ it "never substitutes path captures in the query or fragment" $ quickCheck do
29+ value <- (genAsciiString :: Gen String)
30+ let
31+ suffix = "?next=/:slug#/:slug"
32+ result = substitutePathParams @(slug :: String)
33+ { slug: value }
34+ ("/items/:slug" <> suffix)
35+ property = case String.stripSuffix (Pattern suffix) result of
36+ Nothing -> false
37+ Just path -> not $ String.contains (Pattern ":slug") path
38+ pure $ property <?> "unexpected path substitution result: " <> show result
39+
40+ it "round-trips percent-encoded path and query values" $ quickCheck do
41+ value <- (genAsciiString :: Gen String)
42+ let
43+ pathResult = substitutePathParams @(value :: String)
44+ { value }
45+ "/items/:value"
46+ queryResult = appendQueryParams @(value :: String)
47+ { value }
48+ "/items"
49+ property = case
50+ String.stripPrefix (Pattern "/items/") pathResult,
51+ String.stripPrefix (Pattern "/items?value=") queryResult
52+ of
53+ Just encodedPath, Just encodedQuery ->
54+ decodeURIComponent encodedPath == Just value
55+ && decodeURIComponent encodedQuery == Just value
56+ _, _ -> false
57+ pure $ property <?> "failed to round-trip value " <> show value
58+
59+ it "preserves existing query values and fragments" $ quickCheck do
60+ existing <- (genAsciiString :: Gen String)
61+ added <- (genAsciiString :: Gen String)
62+ let
63+ base = appendQueryParams @(token :: String)
64+ { token: existing }
65+ "/items#details"
66+ result = appendQueryParams @(search :: String)
67+ { search: added }
68+ base
69+ property = do
70+ withoutFragment <- String.stripSuffix (Pattern "#details") result
71+ query <- String.stripPrefix (Pattern "/items?token=") withoutFragment
72+ case String.split (Pattern "&search=") query of
73+ [ encodedExisting, encodedAdded ] ->
74+ pure
75+ $ decodeURIComponent encodedExisting == Just existing
76+ && decodeURIComponent encodedAdded == Just added
77+ _ -> Nothing
78+ pure
79+ $ (property == Just true)
80+ <?> "query or fragment changed unexpectedly: " <> show result
81+
82+ it "preserves repeated query parameter order and multiplicity" $ quickCheck do
83+ values <- arbitrary
84+ let
85+ result = appendQueryParams @(tag :: Array Int) { tag: values } "/items"
86+ expected = case values of
87+ [] -> "/items"
88+ _ -> "/items?" <> String.joinWith "&" (map (\value -> "tag=" <> show value) values)
89+ pure $ result === expected
90+
91+ it "normalizes absent, Nothing, and Just optional fields" $ quickCheck do
92+ missing <- arbitrary
93+ value <- arbitrary
94+ let
95+ params :: Record (id :: Maybe Int)
96+ params = if missing then unsafeCoerce {} else { id: value }
97+ result = appendQueryParams @(id :: Maybe Int) params "/items"
98+ expected = case missing, value of
99+ true, _ -> "/items"
100+ false, Nothing -> "/items"
101+ false, Just id -> "/items?id=" <> show id
102+ pure $ result === expected
103+
104+ it "never introduces malformed query separators" $ quickCheck do
105+ value <- arbitrary
106+ let
107+ bases =
108+ [ "/items"
109+ , "/items?existing=1"
110+ , "/items?"
111+ , "/items?existing=1&"
112+ , "/items#details"
113+ , "/items?existing=1#details"
114+ , "/items?#details"
115+ , "/items?existing=1&#details"
116+ ]
117+ results = map (appendQueryParams @(id :: Int) { id: value }) bases
118+ pure
119+ $ all isWellFormed results
120+ <?> "malformed URL generated from " <> show results
121+
122+ it "preserves repeated existing query keys" $ quickCheck do
123+ first <- (arbitrary :: Gen Int)
124+ second <- (arbitrary :: Gen Int)
125+ page <- (arbitrary :: Gen Int)
126+ let
127+ base = "/items?id=" <> show first <> "&id=" <> show second <> "#details"
128+ result = appendQueryParams @(page :: Int) { page } base
129+ expected =
130+ "/items?id=" <> show first <> "&id=" <> show second
131+ <> "&page="
132+ <> show page
133+ <> "#details"
134+ pure $ result === expected
135+
136+ it "appends duplicate query values without coalescing existing ones" $ quickCheck do
137+ first <- (arbitrary :: Gen Int)
138+ second <- (arbitrary :: Gen Int)
139+ added <- (arbitrary :: Gen (Array Int))
140+ let
141+ base = "/items?id=" <> show first <> "&id=" <> show second
142+ result = appendQueryParams @(id :: Array Int) { id: added } base
143+ expected = case added of
144+ [] -> base
145+ _ -> base <> "&" <> String.joinWith "&" (map (\value -> "id=" <> show value) added)
146+ pure $ result === expected
147+
148+ it "ignores query-looking text inside fragments" $ quickCheck do
149+ value <- (arbitrary :: Gen Int)
150+ let
151+ rendered = show (value :: Int)
152+ cases =
153+ [ { base: "/items#section?fake=1", expected: "/items?id=" <> rendered <> "#section?fake=1" }
154+ , { base: "/items?real=1#section?fake=1", expected: "/items?real=1&id=" <> rendered <> "#section?fake=1" }
155+ , { base: "/items#?fake=1&other=2", expected: "/items?id=" <> rendered <> "#?fake=1&other=2" }
156+ , { base: "/items#first#second?fake=1", expected: "/items?id=" <> rendered <> "#first#second?fake=1" }
157+ ]
158+ property = all
159+ (\entry -> appendQueryParams @(id :: Int) { id: value } entry.base == entry.expected)
160+ cases
161+ pure $ property <?> "fragment handling failed for id " <> rendered
162+
163+ it "encodes every query delimiter inside values" $ quickCheck do
164+ prefix <- (genAsciiString :: Gen String)
165+ suffix <- (genAsciiString :: Gen String)
166+ let
167+ value = prefix <> "?#&=%+ /" <> suffix
168+ result = appendQueryParams @(value :: String) { value } "/items"
169+ property = case String.stripPrefix (Pattern "/items?value=") result of
170+ Nothing -> false
171+ Just encoded ->
172+ decodeURIComponent encoded == Just value
173+ && all
174+ (\delimiter -> not $ String.contains (Pattern delimiter) encoded)
175+ [ "?", "#", "&", "=", "+", "/" ]
176+ && String.contains (Pattern "%25") encoded
177+ pure $ property <?> "delimiter encoding failed: " <> show result
178+
179+ it "substitutes repeated captures without matching overlapping names" $ quickCheck do
180+ id <- (arbitrary :: Gen Int)
181+ id2 <- (arbitrary :: Gen Int)
182+ let
183+ result = substitutePathParams @(id :: Int, id2 :: Int)
184+ { id, id2 }
185+ "/:id/:id/:id2/:id20?next=/:id#/:id2"
186+ expected =
187+ "/" <> show id <> "/" <> show id <> "/" <> show id2
188+ <> "/:id20?next=/:id#/:id2"
189+ pure $ result === expected
190+
191+ it "normalizes lone UTF-16 surrogates before percent-encoding" do
192+ let
193+ value = codeUnit 0xD800 <> "x" <> codeUnit 0xDFFF
194+ result = appendQueryParams @(value :: String) { value } "/items"
195+ result `shouldEqual` "/items?value=%EF%BF%BDx%EF%BF%BD"
196+
197+ it "percent-encodes arbitrary UTF-16 strings without throwing" $ quickCheck do
198+ value <- (arbitrary :: Gen String)
199+ let
200+ pathResult = substitutePathParams @(value :: String) { value } "/items/:value"
201+ queryResult = appendQueryParams @(value :: String) { value } "/items"
202+ property =
203+ isJust (String.stripPrefix (Pattern "/items/") pathResult)
204+ && isJust (String.stripPrefix (Pattern "/items?value=") queryResult)
205+ pure $ property <?> "encoding failed for UTF-16 input " <> show value
206+
207+ it "round-trips valid non-ASCII scalar values" $ quickCheck do
208+ value <- unicodeScalarString
209+ let
210+ pathResult = substitutePathParams @(value :: String) { value } "/items/:value"
211+ queryResult = appendQueryParams @(value :: String) { value } "/items"
212+ property = case
213+ String.stripPrefix (Pattern "/items/") pathResult,
214+ String.stripPrefix (Pattern "/items?value=") queryResult
215+ of
216+ Just encodedPath, Just encodedQuery ->
217+ decodeURIComponent encodedPath == Just value
218+ && decodeURIComponent encodedQuery == Just value
219+ _, _ -> false
220+ pure $ property <?> "Unicode round-trip failed for " <> show value
221+
222+isWellFormed :: String -> Boolean
223+isWellFormed url =
224+ occurrences "?" url == 1
225+ && occurrences "#" url <= 1
226+ && not (String.contains (Pattern "??") url)
227+ && not (String.contains (Pattern "&&") url)
228+ && not (String.contains (Pattern "?#") url)
229+ && not (String.contains (Pattern "&#") url)
230+ && queryPrecedesFragment url
231+
232+queryPrecedesFragment :: String -> Boolean
233+queryPrecedesFragment url = case String.indexOf (Pattern "#") url of
234+ Nothing -> true
235+ Just fragmentIndex -> case String.indexOf (Pattern "?") url of
236+ Nothing -> false
237+ Just queryIndex -> queryIndex < fragmentIndex
238+
239+occurrences :: String -> String -> Int
240+occurrences needle = (_ - 1) <<< Array.length <<< String.split (Pattern needle)
241+
242+unicodeScalarString :: Gen String
243+unicodeScalarString =
244+ fromCodePointArray <<< NEA.toArray <$> arrayOf1 unicodeScalar
245+
246+unicodeScalar :: Gen CodePoint
247+unicodeScalar = do
248+ useBasicMultilingualPlane <- arbitrary
249+ value <- if useBasicMultilingualPlane then
250+ chooseInt 0x80 0xD7FF
251+ else
252+ chooseInt 0xE000 0x10FFFF
253+ pure $ unsafePartial $ fromJust $ toEnum value
254+
255+codeUnit :: Int -> String
256+codeUnit value = CodeUnits.singleton $ unsafePartial $ fromJust $ toEnum value
test/Complete.Example.purs+14−19
@@ -3,9 +3,9 @@ module Complete.Example where
33import Prelude
44
55import Data.Array (length)
6−import Data.Maybe (Maybe(..))
6+import Justifill (justifill)
77import Effect.Class.Console (log)
8−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), client)
8+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client)
99import Yoga.HTTP.API.Route (BearerToken(..))
1010import Yoga.Om (Om, handleErrors)
1111
@@ -31,43 +31,37 @@ type ErrorMessage =
3131
3232type UserAPI =
3333 { getUser ::
34− Route GET
35− (Path ("users" / "id" : Int))
34+ Route GET ("users" / "id" : Int)
3635 {}
3736 ( ok :: { body :: User }
3837 , notFound :: { body :: ErrorMessage }
3938 )
4039 , listUsers ::
41− Route GET
42− (Path "users" :? { limit :: Int, offset :: Int })
40+ Route GET ("users" :? { limit :: Int, offset :: Int })
4341 {}
4442 ( ok :: { body :: Array User }
4543 )
4644 , createUser ::
47− Route POST
48− (Path "users")
45+ Route POST "users"
4946 { body :: JSON CreateUserRequest }
5047 ( created :: { body :: User }
5148 , badRequest :: { body :: ErrorMessage }
5249 )
5350 , updateUser ::
54− Route PUT
55− (Path ("users" / "id" : Int))
51+ Route PUT ("users" / "id" : Int)
5652 { body :: JSON UpdateUserRequest }
5753 ( ok :: { body :: User }
5854 , notFound :: { body :: ErrorMessage }
5955 , badRequest :: { body :: ErrorMessage }
6056 )
6157 , deleteUser ::
62− Route DELETE
63− (Path ("users" / "id" : Int))
58+ Route DELETE ("users" / "id" : Int)
6459 {}
65− ( noContent :: { body :: {} }
60+ ( noContent :: {}
6661 , notFound :: { body :: ErrorMessage }
6762 )
6863 , createUserAuth ::
69− Route POST
70− (Path "users")
64+ Route POST "users"
7165 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
7266 ( created :: { body :: User }
7367 , badRequest :: { body :: ErrorMessage }
@@ -85,15 +79,15 @@ exampleGetUserHandled :: Om {} () Unit
8579exampleGetUserHandled = do
8680 user <- api.getUser { id: 42 }
8781 # handleErrors
88− { notFound: \err -> do
82+ { notFound: \err -> ado
8983 log $ "User not found: " <> err.error
90− pure { id: 0, name: "unknown", email: "" }
84+ in { id: 0, name: "unknown", email: "" }
9185 }
9286 log $ "Found user: " <> user.name
9387
9488exampleListUsers :: Om {} () Unit
9589exampleListUsers = do
96− users <- api.listUsers { limit: Just 10, offset: Just 0 }
90+ users <- api.listUsers (justifill { limit: 10, offset: 0 })
9791 log $ "Found " <> show (length users) <> " users"
9892
9993exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
@@ -106,7 +100,8 @@ exampleCreateUser = do
106100
107101exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
108102exampleUpdateUser = do
109− user <- api.updateUser { id: 42 }
103+ user <- api.updateUser
104+ { id: 42 }
110105 { name: "Alice Updated"
111106 , email: "alice.new@example.com"
112107 }
test/Example.purs+13−8
@@ -4,41 +4,46 @@ import Yoga.HTTP.API.Route (GET, POST, DELETE, Route, BearerToken)
44import Yoga.HTTP.API.Route.Encoding (JSON)
55import Yoga.HTTP.API.Path (Path, type (/), type (:), type (:?))
66
7−type User = { id :: Int, name :: String, email :: String }
8−type CreateUserRequest = { name :: String, email :: String }
7+type User = { id :: UserId, name :: UserName, email :: UserMail }
8+type CreateUserRequest = { name :: String, email :: UserMail }
99type ErrorMessage = { error :: String }
10+newtype UserId = UserId Int
11+newtype UserName = UserName String
12+newtype UserMail = UserMail String
13+newtype Limit = Limit Int
14+newtype Offset = Offset Int
1015
1116type UserAPI =
1217 { getUser ::
1318 Route GET
14− (Path ("users" / "id" : Int))
19+ ("users" / "id" : UserId)
1520 {}
1621 ( ok :: { body :: User }
1722 , notFound :: { body :: ErrorMessage }
1823 )
1924 , listUsers ::
2025 Route GET
21− (Path "users" :? { limit :: Int, offset :: Int })
26+ ("users" :? { limit :: Limit, offset :: Offset })
2227 {}
2328 ( ok :: { body :: Array User }
2429 )
2530 , createUser ::
2631 Route POST
27− (Path "users")
32+ "users"
2833 { body :: JSON CreateUserRequest }
2934 ( created :: { body :: User }
3035 , badRequest :: { body :: ErrorMessage }
3136 )
3237 , deleteUser ::
3338 Route DELETE
34− (Path ("users" / "id" : Int))
39+ ("users" / "id" : UserId)
3540 {}
36− ( noContent :: { body :: {} }
41+ ( noContent :: {}
3742 , notFound :: { body :: ErrorMessage }
3843 )
3944 , createUserAuth ::
4045 Route POST
41− (Path "users")
46+ "users"
4247 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
4348 ( created :: { body :: User }
4449 , badRequest :: { body :: ErrorMessage }
test/MakeRequest.Spec.purs+15−5
@@ -3,22 +3,32 @@ module MakeRequest.Spec where
33import Prelude
44
55import Data.HTTP.Method as HTTP
6+import Data.Maybe (Maybe(..))
67import Data.Tuple.Nested ((/\))
78import JS.Fetch.Headers as Headers
89import Prim.RowList as RL
910import Test.Spec (Spec, describe, it)
1011import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
1112import Type.Proxy (Proxy(..))
12−import Yoga.Fetch.Om (toHeaders)
13−import Yoga.Fetch.Om.MakeRequest (httpMethod)
13+import Yoga.Fetch.Om (FormData, PlainText, QUERY, toHeaders)
14+import Yoga.Fetch.Om.MakeRequest (encodeBody, encodingContentType, httpMethod)
1415import Yoga.HTTP.API.Route (BearerToken(..))
15−import Yoga.HTTP.API.Route.Method as Route
1616
1717spec :: Spec Unit
1818spec = do
1919 describe "MakeRequest" do
20− it "maps a QUERY route method to the QUERY runtime method" do
21− httpMethod (Proxy :: Proxy Route.QUERY) `shouldEqual` HTTP.QUERY
20+ it "maps a QUERY route method imported from the public module" do
21+ httpMethod (Proxy :: Proxy QUERY) `shouldEqual` HTTP.QUERY
22+
23+ describe "BodyEncoding" do
24+ it "sends PlainText request bodies without JSON quoting" do
25+ encodingContentType (Proxy :: Proxy PlainText) `shouldEqual` "text/plain"
26+ encodeBody @PlainText "hello" `shouldEqual` Just "hello"
27+
28+ it "URL-encodes FormData request bodies" do
29+ encodingContentType (Proxy :: Proxy (FormData { username :: String })) `shouldEqual` "application/x-www-form-urlencoded"
30+ encodeBody @(FormData { username :: String }) { username: "Ada Lovelace" }
31+ `shouldEqual` Just "username=Ada+Lovelace"
2232
2333 describe "toHeaders" do
2434 it "empty headers produces no entries" do
test/PlainTextResponse.Spec.purs+49−8
@@ -1,42 +1,54 @@
11module PlainTextResponse.Spec where
22
33import Prelude
4+import Data.String as String
45
56import Effect.Aff (Aff, bracket)
67import Effect.Aff.Class (liftAff)
78import Effect.Class (liftEffect)
89import Effect.Ref (Ref)
10+import Effect.Exception as Exn
911import Effect.Ref as Ref
1012import Foreign (unsafeToForeign)
1113import Test.Spec (Spec, around, describe, it)
1214import Test.Spec.Assertions (shouldEqual)
1315import Yoga.Fastify.Fastify as F
1416import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15−import Yoga.Fastify.Om (createOmFastify, postOm)
16−import Yoga.Fetch.Om (POST, Route, type (/), JSON, client)
17+import Yoga.Fastify.Om (createOmFastify, getOm, postOm)
18+import Yoga.Fetch.Om (GET, POST, Route, type (/), JSON, PlainText, Streaming, client)
19+import Yoga.Fetch.Om.Simple as Simple
1720import Yoga.Om (Om, runOm)
21+import Yoga.Om.Strom (Strom)
22+import Yoga.Om.Strom as Strom
1823
1924type Payload = { value :: Int }
20−type Result = { doubled :: Int }
25+type Result = { doubled :: Int }
2126
2227type TestAPI =
2328 { double ::
2429 Route POST
2530 ("plain-text-test" / "double")
2631 { body :: JSON Payload }
27− ( ok :: { body :: Result }
28− , badRequest :: { body :: String }
32+ ( ok :: { body :: Result }
33+ , badRequest :: { body :: PlainText }
2934 )
35+ , health :: Route GET ("plain-text-test" / "health") {} (ok :: { body :: PlainText })
36+ , stream :: Route GET ("plain-text-test" / "stream") {} (ok :: { body :: Streaming String })
3037 }
3138
32−api :: { double :: forall ctx err. Payload -> Om ctx (badRequest :: String | err) Result }
39+api
40+ :: forall ctx err
41+ . { double :: Payload -> Om (Record ctx) (badRequest :: String | err) Result
42+ , health :: Om (Record ctx) err String
43+ , stream :: Om (Record ctx) err (Strom {} () String)
44+ }
3345api = client @TestAPI "http://localhost:44933"
3446
3547spec :: Spec Unit
3648spec = around withServer $ describe "String body on error responses" do
3749 it "200 OK decodes the JSON response body" \_ -> do
3850 result <- runOm {}
39− { exception: \_ -> pure { doubled: -1 }
51+ { exception: \_ -> pure { doubled: -1 }
4052 , badRequest: \_ -> pure { doubled: -1 }
4153 }
4254 (api.double { value: 21 })
@@ -46,13 +58,35 @@ spec = around withServer $ describe "String body on error responses" do
4658 liftEffect $ Ref.write "badRequest" serverMode
4759 captured <- liftEffect $ Ref.new ""
4860 runOm {}
49− { exception: \_ -> pure unit
61+ { exception: \_ -> pure unit
5062 , badRequest: \b -> liftEffect $ Ref.write b captured
5163 }
5264 (void $ api.double { value: 0 })
5365 body <- liftEffect $ Ref.read captured
5466 liftAff $ body `shouldEqual` "invalid value"
5567
68+ it "Simple.get returns plain text for String responses" \_ -> do
69+ result <- runOm {}
70+ { exception: \e -> pure $ "exception: " <> Exn.message e
71+ , fetchError: \e -> pure $ "fetch error: " <> e.body
72+ }
73+ (Simple.get @String "http://localhost:44933/plain-text-test/health" {})
74+ liftAff $ result `shouldEqual` "healthy"
75+
76+ it "derived PlainText responses decode to String" \_ -> do
77+ result <- runOm {}
78+ { exception: \e -> pure $ "exception: " <> Exn.message e }
79+ api.health
80+ liftAff $ result `shouldEqual` "healthy"
81+
82+ it "derived Streaming responses decode to a usable Strom" \_ -> do
83+ chunks <- runOm {}
84+ { exception: \e -> pure [ "exception: " <> Exn.message e ] }
85+ do
86+ stream <- api.stream
87+ Strom.runCollect stream
88+ liftAff $ String.joinWith "" chunks `shouldEqual` "stream €"
89+
5690withServer :: forall a. (Ref String -> Aff a) -> Aff a
5791withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
5892 where
@@ -62,12 +96,19 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
6296 f <- F.fastify {}
6397 omApp <- createOmFastify {} f
6498 postOm (RouteURL "/plain-text-test/double") (handler serverMode) omApp
99+ getOm (RouteURL "/plain-text-test/health") (plainTextHandler "healthy") omApp
100+ getOm (RouteURL "/plain-text-test/stream") (plainTextHandler "stream €") omApp
65101 pure f
66102 void $ F.listen { port: Port 44933, host: Host "0.0.0.0" } fastify
67103 pure { fastify, serverMode }
68104
69105 release { fastify } = F.close fastify
70106
107+ plainTextHandler text reply = do
108+ void $ F.status (StatusCode 200) reply # liftEffect
109+ void $ F.header "content-type" "text/plain" reply # liftEffect
110+ F.send (unsafeToForeign text) reply # liftAff
111+
71112 handler serverMode reply = do
72113 mode <- liftEffect $ Ref.read serverMode
73114 case mode of
test/Polymorphic.Spec.purs+21−18
@@ -12,7 +12,8 @@ import Yoga.Fastify.Fastify (Port(..), Host(..))
1212import Yoga.Fastify.Om.API (registerAPI)
1313import Yoga.Fastify.Om.Route (Handler, handle, respond)
1414import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
15−import Yoga.Om (Om, ask, runOm)
15+import Yoga.Om (ask, runOm)
16+import Control.Monad.Error.Class (throwError)
1617
1718type User = { id :: Int, name :: String }
1819
@@ -27,26 +28,28 @@ api = client @TestAPI "http://localhost:44931"
2728spec :: Spec Unit
2829spec = around withServer $ describe "polymorphic client" do
2930 it "fetches user with { token :: String } context" \_ ->
30− runOm { token: "secret123" } { exception: \_ -> pure unit } do
31− ctx <- ask
32− user <- api.getUser { id: 42 }
33− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "secret123:User 42"
31+ runOm { token: "secret123" } { exception: throwError } do
32+ { token } <- ask
33+ { name } <- api.getUser { id: 42 }
34+ liftAff $ (token <> ":" <> name) `shouldEqual` "secret123:User 42"
3435
3536 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
36− runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
37− ctx <- ask
38− user <- api.getUser { id: ctx.userId }
39− liftAff $ (ctx.apiKey <> ":" <> user.name) `shouldEqual` "mykey:User 7"
37+ runOm { apiKey: "mykey", userId: 7 } { exception: throwError } do
38+ { apiKey, userId } <- ask
39+ { name } <- api.getUser { id: userId }
40+ liftAff $ (apiKey <> ":" <> name) `shouldEqual` "mykey:User 7"
4041
4142 it "same api.getUser works in two different contexts" \_ -> do
42− runOm { token: "tok1" } { exception: \_ -> pure unit } do
43− ctx <- ask
44− user <- api.getUser { id: 1 }
45− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "tok1:User 1"
46− runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
47− ctx <- ask
48− user <- api.getUser { id: 2 }
49− liftAff $ (ctx.endpoint <> "-" <> ctx.region <> ":" <> user.name) `shouldEqual` "prod-eu:User 2"
43+
44+ runOm { token: "tok1" } { exception: throwError } do
45+ { token } <- ask
46+ { name } <- api.getUser { id: 1 }
47+ liftAff $ (token <> ":" <> name) `shouldEqual` "tok1:User 1"
48+
49+ runOm { endpoint: "prod", region: "eu" } { exception: throwError } do
50+ { endpoint, region } <- ask
51+ { name } <- api.getUser { id: 2 }
52+ liftAff $ (endpoint <> "-" <> region <> ":" <> name) `shouldEqual` "prod-eu:User 2"
5053
5154withServer :: forall a. (Unit -> _ a) -> _ a
5255withServer test = bracket acquire release (\_ -> test unit)
@@ -54,7 +57,7 @@ withServer test = bracket acquire release (\_ -> test unit)
5457 acquire = do
5558 fastify <- liftEffect do
5659 f <- F.fastify {}
57− registerAPI { getUser: getUserHandler } f
60+ registerAPI @TestAPI { getUser: getUserHandler } f
5861 pure f
5962 void $ F.listen { port: Port 44931, host: Host "0.0.0.0" } fastify
6063 pure fastify
test/Roundtrip.Spec.pursnew file+380
@@ -0,0 +1,380 @@
1+module Roundtrip.Spec where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Effect.Aff (bracket)
7+import Effect.Aff.Class (liftAff)
8+import Effect.Class (liftEffect)
9+import Effect.Ref as Ref
10+import Effect.Exception as Exn
11+import Justifill (justifill)
12+import Test.Spec (Spec, around, describe, it)
13+import Test.Spec.Assertions (shouldEqual)
14+import Yoga.Fastify.Fastify as F
15+import Yoga.Fastify.Fastify (Port(..), Host(..))
16+import Yoga.Fastify.Om.API (registerAPI)
17+import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
18+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Required, Route, JSON, type (/), type (:), type (:?), client, clientWith)
19+import Yoga.Om (ask, handleErrors, runOm)
20+
21+-- Types
22+
23+type User = { id :: Int, name :: String, email :: String }
24+type CreateUserReq = { name :: String, email :: String }
25+type ErrorMsg = { error :: String }
26+
27+-- Route definitions shared by server and client
28+
29+type GetUserRoute = Route GET
30+ ("users" / "id" : Int)
31+ {}
32+ ( ok :: { body :: User }
33+ , notFound :: { body :: ErrorMsg }
34+ )
35+
36+type ListUsersRoute = Route GET
37+ ("users" :? { limit :: Int, offset :: Int })
38+ {}
39+ (ok :: { body :: Array User })
40+
41+type CreateUserRoute = Route POST
42+ "users"
43+ { body :: JSON CreateUserReq }
44+ ( created :: { body :: User }
45+ , badRequest :: { body :: ErrorMsg }
46+ )
47+
48+type UpdateUserRoute = Route PUT
49+ ("users" / "id" : Int)
50+ { body :: JSON CreateUserReq }
51+ ( ok :: { body :: User }
52+ , notFound :: { body :: ErrorMsg }
53+ )
54+
55+type DeleteUserRoute = Route DELETE
56+ ("users" / "id" : Int)
57+ {}
58+ ( ok :: { body :: {} }
59+ , notFound :: { body :: ErrorMsg }
60+ )
61+
62+type AuthRoute = Route GET
63+ "me"
64+ { headers :: { authorization :: BearerToken } }
65+ ( ok :: { body :: User }
66+ , unauthorized :: { body :: ErrorMsg }
67+ )
68+
69+type SearchRoute = Route GET
70+ ("search" :? { active :: Boolean, minScore :: Number })
71+ {}
72+ (ok :: { body :: { active :: Boolean, minScore :: Number } })
73+
74+type FindByQueryRoute = Route GET
75+ ("find-by-query" :? { id :: Int })
76+ {}
77+ (ok :: { body :: { id :: Int } })
78+
79+type InjectedTenantRoute = Route GET
80+ ("tenant-users" / "id" : Int :? { tenant :: Required String })
81+ { headers :: { authorization :: BearerToken, requestid :: String } }
82+ ( ok :: { body :: User }
83+ , unauthorized :: { body :: ErrorMsg }
84+ )
85+
86+type InjectedAPI =
87+ { getTenantUser :: InjectedTenantRoute
88+ }
89+
90+injectedApi tokenRef =
91+ clientWith @InjectedAPI
92+ { baseUrl: pure "http://localhost:44932"
93+ , provide: do
94+ ctx <- ask
95+ token <- liftEffect $ Ref.read tokenRef
96+ pure
97+ { headers: { authorization: BearerToken token, requestid: ctx.requestid }
98+ , query: { tenant: ctx.tenant }
99+ }
100+ }
101+
102+type API =
103+ { getUser :: GetUserRoute
104+ , listUsers :: ListUsersRoute
105+ , createUser :: CreateUserRoute
106+ , updateUser :: UpdateUserRoute
107+ , deleteUser :: DeleteUserRoute
108+ , me :: AuthRoute
109+ , search :: SearchRoute
110+ , findByQuery :: FindByQueryRoute
111+ }
112+
113+api = client @API "http://localhost:44932"
114+
115+-- Handlers
116+
117+getUserHandler :: Handler GetUserRoute ()
118+getUserHandler = handle do
119+ { path } <- ask
120+ if path.id == 42 then respond @"ok" { id: 42, name: "Alice", email: "alice@test.com" }
121+ else reject @"notFound" { error: "User " <> show path.id <> " not found" }
122+
123+listUsersHandler :: Handler ListUsersRoute ()
124+listUsersHandler = handle do
125+ { query } <- ask
126+ let
127+ users =
128+ [ { id: 1, name: "Alice", email: "a@t.com" }
129+ , { id: 2, name: "Bob", email: "b@t.com" }
130+ , { id: 3, name: "Charlie", email: "c@t.com" }
131+ ]
132+ let
133+ limited = case query.limit of
134+ Just l -> take l users
135+ Nothing -> users
136+ respond @"ok" limited
137+ where
138+ take n xs =
139+ if n <= 0 then []
140+ else case xs of
141+ [] -> []
142+ [ a ] -> [ a ]
143+ [ a, _ ] | n == 1 -> [ a ]
144+ [ a, b ] -> [ a, b ]
145+ _ -> xs
146+
147+createUserHandler :: Handler CreateUserRoute ()
148+createUserHandler = handle do
149+ { body } <- ask
150+ if body.name == "" then reject @"badRequest" { error: "Name required" }
151+ else respond @"created" { id: 99, name: body.name, email: body.email }
152+
153+updateUserHandler :: Handler UpdateUserRoute ()
154+updateUserHandler = handle do
155+ { path, body } <- ask
156+ if path.id /= 42 then reject @"notFound" { error: "Not found" }
157+ else respond @"ok" { id: path.id, name: body.name, email: body.email }
158+
159+deleteUserHandler :: Handler DeleteUserRoute ()
160+deleteUserHandler = handle do
161+ { path } <- ask
162+ if path.id /= 42 then reject @"notFound" { error: "Not found" }
163+ else respond @"ok" {}
164+
165+meHandler :: Handler AuthRoute ()
166+meHandler = handle do
167+ { headers } <- ask
168+ case headers.authorization of
169+ BearerToken "secret" -> respond @"ok" { id: 1, name: "Admin", email: "admin@test.com" }
170+ _ -> reject @"unauthorized" { error: "Invalid token" }
171+
172+searchHandler :: Handler SearchRoute ()
173+searchHandler = handle do
174+ { query } <- ask
175+ let
176+ active = case query.active of
177+ Just a -> a
178+ Nothing -> false
179+ let
180+ minScore = case query.minScore of
181+ Just s -> s
182+ Nothing -> 0.0
183+ respond @"ok" { active, minScore }
184+
185+findByQueryHandler :: Handler FindByQueryRoute ()
186+findByQueryHandler = handle do
187+ { query } <- ask
188+ let
189+ id = case query.id of
190+ Just value -> value
191+ Nothing -> -1
192+ respond @"ok" { id }
193+
194+tenantUserHandler :: Handler InjectedTenantRoute ()
195+tenantUserHandler = handle do
196+ { path, query, headers } <- ask
197+ case headers.authorization of
198+ BearerToken "secret" -> respond @"ok" { id: path.id, name: query.tenant <> ":" <> headers.requestid, email: "tenant@test.com" }
199+ _ -> reject @"unauthorized" { error: "Invalid token" }
200+
201+-- Test suite
202+
203+spec :: Spec Unit
204+spec = around withServer $ describe "server ↔ client roundtrip" do
205+
206+ describe "GET with path params" do
207+ it "returns user for valid id" \_ ->
208+ runOm {} { exception: \_ -> pure unit } do
209+ user <- api.getUser { id: 42 }
210+ # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
211+ liftAff $ user.name `shouldEqual` "Alice"
212+
213+ it "returns notFound for invalid id" \_ ->
214+ runOm {} { exception: \_ -> pure unit } do
215+ user <- api.getUser { id: 999 }
216+ # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
217+ liftAff $ user.name `shouldEqual` "User 999 not found"
218+
219+ describe "GET with query params" do
220+ it "passes query params correctly" \_ ->
221+ runOm {} { exception: \_ -> pure unit } do
222+ users <- api.listUsers (justifill { limit: 1 })
223+ liftAff $ (map _.name users) `shouldEqual` [ "Alice" ]
224+
225+ it "serializes a single explicit Maybe query through the derived client" \_ ->
226+ runOm {} { exception: \_ -> pure unit } do
227+ result <- api.findByQuery { id: Just 42 }
228+ liftAff $ result.id `shouldEqual` 42
229+
230+ it "handles no query params" \_ ->
231+ runOm {} { exception: \_ -> pure unit } do
232+ users <- api.listUsers (justifill {})
233+ liftAff $ (map _.name users) `shouldEqual` [ "Alice", "Bob", "Charlie" ]
234+
235+ describe "POST with JSON body" do
236+ it "sends and receives JSON body" \_ ->
237+ runOm {} { exception: \_ -> pure unit } do
238+ user <- api.createUser { name: "Dave", email: "d@t.com" }
239+ # handleErrors { badRequest: \e -> pure { id: 0, name: e.error, email: "" } }
240+ liftAff do
241+ user.name `shouldEqual` "Dave"
242+ user.email `shouldEqual` "d@t.com"
243+ user.id `shouldEqual` 99
244+
245+ it "rejects with badRequest for empty name" \_ ->
246+ runOm {} { exception: \_ -> pure unit } do
247+ user <- api.createUser { name: "", email: "e@t.com" }
248+ # handleErrors { badRequest: \e -> pure { id: 0, name: e.error, email: "" } }
249+ liftAff $ user.name `shouldEqual` "Name required"
250+
251+ describe "PUT with path params and JSON body" do
252+ it "sends path params and body together" \_ ->
253+ runOm {} { exception: \_ -> pure unit } do
254+ user <-
255+ api.updateUser
256+ { id: 42 }
257+ { name: "Updated", email: "u@t.com" }
258+ # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
259+ liftAff do
260+ user.id `shouldEqual` 42
261+ user.name `shouldEqual` "Updated"
262+
263+ it "supports partial application before the body argument" \_ ->
264+ runOm {} { exception: \_ -> pure unit } do
265+ let update42 = api.updateUser { id: 42 }
266+ user <- update42 { name: "Curried", email: "curried@t.com" }
267+ # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
268+ liftAff do
269+ user.id `shouldEqual` 42
270+ user.name `shouldEqual` "Curried"
271+
272+ describe "DELETE with path params" do
273+ it "deletes successfully" \_ ->
274+ runOm {} { exception: \_ -> pure unit } do
275+ result <- api.deleteUser { id: 42 }
276+ # handleErrors { notFound: \_ -> pure {} }
277+ liftAff $ result `shouldEqual` {}
278+
279+ it "returns notFound for wrong id" \_ ->
280+ runOm {} { exception: \_ -> pure unit } do
281+ result <- api.deleteUser { id: 1 }
282+ # handleErrors { notFound: \_ -> pure {} }
283+ liftAff $ result `shouldEqual` {}
284+
285+ describe "GET with authorization header" do
286+ it "passes bearer token in header" \_ ->
287+ runOm {} { exception: \_ -> pure unit } do
288+ user <- api.me { authorization: BearerToken "secret" }
289+ # handleErrors { unauthorized: \e -> pure { id: 0, name: e.error, email: "" } }
290+ liftAff $ user.name `shouldEqual` "Admin"
291+
292+ it "rejects unauthorized" \_ ->
293+ runOm {} { exception: \_ -> pure unit } do
294+ user <- api.me { authorization: BearerToken "wrong" }
295+ # handleErrors { unauthorized: \e -> pure { id: 0, name: e.error, email: "" } }
296+ liftAff $ user.name `shouldEqual` "Invalid token"
297+
298+ describe "GET with Boolean and Number query params" do
299+ it "passes Boolean and Number query params" \_ ->
300+ runOm {} { exception: \_ -> pure unit } do
301+ result <- api.search (justifill { active: true, minScore: 3.14 })
302+ liftAff do
303+ result.active `shouldEqual` true
304+ result.minScore `shouldEqual` 3.14
305+
306+ it "handles missing optional query params" \_ ->
307+ runOm {} { exception: \_ -> pure unit } do
308+ result <- api.search (justifill {})
309+ liftAff do
310+ result.active `shouldEqual` false
311+ result.minScore `shouldEqual` 0.0
312+
313+ describe "optional query params" do
314+ it "accepts a selected optional query parameter" \_ ->
315+ runOm {} { exception: \_ -> pure unit } do
316+ users <- api.listUsers (justifill { limit: 1 })
317+ liftAff $ (map _.name users) `shouldEqual` [ "Alice" ]
318+
319+ it "accepts all optional query parameters" \_ ->
320+ runOm {} { exception: \_ -> pure unit } do
321+ users <- api.listUsers (justifill { limit: 2, offset: 0 })
322+ liftAff $ (map _.name users) `shouldEqual` [ "Alice", "Bob" ]
323+
324+ it "fills omitted optional query parameters with Nothing" \_ ->
325+ runOm {} { exception: \_ -> pure unit } do
326+ users <- api.listUsers (justifill {})
327+ liftAff $ (map _.name users) `shouldEqual` [ "Alice", "Bob", "Charlie" ]
328+
329+ it "passes selected Boolean query param" \_ ->
330+ runOm {} { exception: \_ -> pure unit } do
331+ result <- api.search (justifill { active: true })
332+ liftAff do
333+ result.active `shouldEqual` true
334+ result.minScore `shouldEqual` 0.0
335+
336+ describe "clientWith injected inputs" do
337+ it "reads provided headers and query params from Om context for each call" \_ -> do
338+ tokenRef <- liftEffect $ Ref.new "secret"
339+ let injected = injectedApi tokenRef
340+ user <- runOm { tenant: "acme", requestid: "req-1" }
341+ { exception: \e -> pure { id: -1, name: Exn.message e, email: "" }
342+ , unauthorized: \e -> pure { id: -2, name: e.error, email: "" }
343+ }
344+ (injected.getTenantUser { id: 7 })
345+ liftAff do
346+ user.id `shouldEqual` 7
347+ user.name `shouldEqual` "acme:req-1"
348+ liftEffect $ Ref.write "wrong" tokenRef
349+ rejected <- runOm { tenant: "acme", requestid: "req-2" }
350+ { exception: \e -> pure { id: -1, name: Exn.message e, email: "" }
351+ , unauthorized: \e -> pure { id: -2, name: e.error, email: "" }
352+ }
353+ (injected.getTenantUser { id: 8 })
354+ liftAff $ rejected.name `shouldEqual` "Invalid token"
355+
356+-- Server setup
357+
358+withServer :: forall a. (Unit -> _ a) -> _ a
359+withServer test = bracket acquire release (\_ -> test unit)
360+ where
361+ acquire = do
362+ fastify <- liftEffect do
363+ f <- F.fastify {}
364+ registerAPI @API
365+ { getUser: getUserHandler
366+ , listUsers: listUsersHandler
367+ , createUser: createUserHandler
368+ , updateUser: updateUserHandler
369+ , deleteUser: deleteUserHandler
370+ , me: meHandler
371+ , search: searchHandler
372+ , findByQuery: findByQueryHandler
373+ }
374+ f
375+ registerAPI @InjectedAPI { getTenantUser: tenantUserHandler } f
376+ pure f
377+ void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
378+ pure fastify
379+
380+ release = F.close
test/StreamDecode.Spec.jsnew file+1
@@ -0,0 +1 @@
1+export const uint8Array = (values) => new Uint8Array(values);
test/StreamDecode.Spec.pursnew file+32
@@ -0,0 +1,32 @@
1+module StreamDecode.Spec where
2+
3+import Prelude
4+
5+import Data.ArrayBuffer.Types (Uint8Array)
6+import Data.String as String
7+import Effect.Aff (Aff)
8+import Effect.Class (liftEffect)
9+import Effect.Exception (throwException)
10+import Test.Spec (Spec, describe, it)
11+import Test.Spec.Assertions (shouldEqual)
12+import Yoga.Fetch.Om.StreamDecode (decodeStream)
13+import Yoga.Om (Om)
14+import Yoga.Om as Om
15+import Yoga.Om.Strom as Strom
16+
17+foreign import uint8Array :: Array Int -> Uint8Array
18+
19+runOm :: forall a. Om {} () a -> Aff a
20+runOm om = Om.runOm {} { exception: \err -> liftEffect (throwException err) } om
21+
22+spec :: Spec Unit
23+spec = describe "StreamDecode" do
24+ it "preserves UTF-8 code points split across chunks" do
25+ chunks <- runOm $
26+ Strom.fromArray
27+ [ uint8Array [ 0xe2 ]
28+ , uint8Array [ 0x82, 0xac ]
29+ ]
30+ # decodeStream @String
31+ # Strom.runCollect
32+ String.joinWith "" chunks `shouldEqual` "€"