git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
60 files changed+2609−412
since splitsnapshots
unifiedsplit
.c8rc.json+22 .github/workflows/ci.yml+7−2 .gitignore+2 LICENSE+21 README.md+74−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+18−4 spago.lock+129−23 spago.yaml+21−9 src/Yoga/Fetch/Om.js+1 src/Yoga/Fetch/Om.purs+183−42 src/Yoga/Fetch/Om/BuildUrl.js+86−2 src/Yoga/Fetch/Om/BuildUrl.purs+43−13 src/Yoga/Fetch/Om/ClientFunction.purs+37−20 src/Yoga/Fetch/Om/ExtractParams.purs+13 src/Yoga/Fetch/Om/MakeRequest.js+1 src/Yoga/Fetch/Om/MakeRequest.purs+31−27 src/Yoga/Fetch/Om/ParseResponse.purs+33−29 src/Yoga/Fetch/Om/ReadOptional.js+18 src/Yoga/Fetch/Om/ReadOptional.purs+56 src/Yoga/Fetch/Om/Simple.purs+6−21 src/Yoga/Fetch/Om/SplitResponses.purs+24−2 src/Yoga/Fetch/Om/StreamDecode.js+12−1 src/Yoga/Fetch/Om/StreamDecode.purs+43−2 test-compile-fail/cases/ClientBogusSignature.purs+9 test-compile-fail/cases/ClientExtraField.purs+12 test-compile-fail/cases/ClientMissingField.purs+12 test-compile-fail/cases/ClientNoArgsPassedArg.purs+12 test-compile-fail/cases/ClientWrongFieldName.purs+12 test-compile-fail/cases/ClientWrongFieldType.purs+12 test-compile-fail/run.sh+26 test/BodylessResponse.Example.purs+27 test/BodylessResponse.Spec.purs+125 test/BuildUrl.Spec.purs+74−6 test/BuildUrlProperty.Spec.purs+256 test/Complete.Example.purs+17−31 test/Example.purs+15−10 test/MakeRequest.Spec.purs+42−19 test/PlainTextResponse.Spec.purs+122 test/Polymorphic.Spec.purs+21−19 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+74−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
@@ -196,3 +252,4 @@ MIT
196252- [purescript-yoga-fastify-om](https://github.com/rowtype-yoga/purescript-yoga-fastify-om) - Server-side counterpart
197253- [purescript-yoga-json](https://github.com/rowtype-yoga/purescript-yoga-json) - JSON serialization
198254- [purescript-yoga-om](https://github.com/rowtype-yoga/purescript-yoga-om) - Om monad
255+
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+18−4
@@ -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,11 +26,23 @@
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 },
33− "dependencies": {}
45+ "dependencies": {
46+ "pino": "^10.3.1"
47+ }
3448}
spago.lock+129−23
@@ -6,7 +6,7 @@
66 "core": {
77 "dependencies": [
88 {
9− "aff": ">=7.1.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
1010 },
1111 {
1212 "arraybuffer-types": ">=3.0.2 <4.0.0"
@@ -24,7 +24,7 @@
2424 "exceptions": ">=6.1.0 <7.0.0"
2525 },
2626 {
27− "http-methods": ">=6.0.0 <7.0.0"
27+ "http-methods": ">=6.1.0 <7.0.0"
2828 },
2929 {
3030 "js-fetch": ">=0.2.1 <0.3.0"
@@ -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.0 <0.4.0"
66+ "yoga-http-api": ">=0.3.4 <0.4.0"
6467 },
6568 {
6669 "yoga-json": ">=5.2.1 <6.0.0"
@@ -69,7 +72,7 @@
6972 "yoga-om": ">=1.5.2 <2.0.0"
7073 },
7174 {
72− "yoga-om-strom": ">=0.4.2 <0.5.0"
75+ "yoga-om-strom": ">=0.4.3 <0.5.0"
7376 }
7477 ]
7578 },
@@ -78,6 +81,22 @@
7881 {
7982 "console": ">=6.0.0 <7.0.0"
8083 },
84+ "enums",
85+ "foldable-traversable",
86+ "foreign",
87+ "foreign-object",
88+ "integers",
89+ "js-promise",
90+ {
91+ "js-uri": ">=3.1.0 <4.0.0"
92+ },
93+ {
94+ "justifill": ">=0.5.0 <0.6.0"
95+ },
96+ "partial",
97+ {
98+ "quickcheck": ">=8.0.1 <9.0.0"
99+ },
81100 {
82101 "refs": ">=6.0.0 <7.0.0"
83102 },
@@ -88,7 +107,11 @@
88107 "spec-discovery": ">=8.0.0 <9.0.0"
89108 },
90109 {
91− "yoga-fastify-om": ">=0.1.0 <0.4.0"
110+ "spec-quickcheck": ">=5.0.2 <6.0.0"
111+ },
112+ "yoga-fastify",
113+ {
114+ "yoga-fastify-om": ">=0.4.7 <0.5.0"
92115 }
93116 ]
94117 }
@@ -99,8 +122,8 @@
99122 "packages": {
100123 "aff": {
101124 "type": "registry",
102− "version": "7.1.0",
103− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
125+ "version": "8.0.0",
126+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
104127 "dependencies": [
105128 "control",
106129 "datetime",
@@ -542,8 +565,8 @@
542565 },
543566 "http-methods": {
544567 "type": "registry",
545− "version": "6.0.0",
546− "integrity": "sha256-S0UjrJSgwJxEsFHM/pX77dM8V4HfvvDqdHPckQrtxMs=",
568+ "version": "6.1.0",
569+ "integrity": "sha256-WbjEmCv6rysY/aKwlrcspyzShi5bIQy+AFpDzPMnSP4=",
547570 "dependencies": [
548571 "either",
549572 "prelude",
@@ -671,6 +694,17 @@
671694 "prelude"
672695 ]
673696 },
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+ },
674708 "lazy": {
675709 "type": "registry",
676710 "version": "6.0.0",
@@ -682,6 +716,20 @@
682716 "prelude"
683717 ]
684718 },
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+ },
685733 "lists": {
686734 "type": "registry",
687735 "version": "7.0.0",
@@ -1061,6 +1109,39 @@
10611109 "tuples"
10621110 ]
10631111 },
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+ },
10641145 "random": {
10651146 "type": "registry",
10661147 "version": "6.0.0",
@@ -1134,8 +1215,8 @@
11341215 },
11351216 "spec": {
11361217 "type": "registry",
1137− "version": "8.1.1",
1138− "integrity": "sha256-b6qjDb7lUJGLV+WEoywLu6BCXC4zGpfbN43mTL+vBeI=",
1218+ "version": "8.1.2",
1219+ "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
11391220 "dependencies": [
11401221 "aff",
11411222 "ansi",
@@ -1168,8 +1249,8 @@
11681249 },
11691250 "spec-discovery": {
11701251 "type": "registry",
1171− "version": "8.4.0",
1172− "integrity": "sha256-aFp8bllT/wrxQu8hMlDchQQY08dQAz+Xdq2hZi5z3yY=",
1252+ "version": "8.4.1",
1253+ "integrity": "sha256-guvY+NtSOBQmM+HqNJ7woX6taN8hS1wEz7JDmdKF11E=",
11731254 "dependencies": [
11741255 "aff",
11751256 "effect",
@@ -1181,8 +1262,8 @@
11811262 },
11821263 "spec-node": {
11831264 "type": "registry",
1184− "version": "0.0.3",
1185− "integrity": "sha256-8/9+g0obbamV0YtqzmttbWs8HaWF7CEeW+eDtqzuAAo=",
1265+ "version": "0.0.2",
1266+ "integrity": "sha256-HDW02z3Mj+daux0j9487ApDBX32/gX0od6YR4zvDRZI=",
11861267 "dependencies": [
11871268 "aff",
11881269 "argonaut-codecs",
@@ -1211,6 +1292,22 @@
12111292 "tuples"
12121293 ]
12131294 },
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+ },
12141311 "st": {
12151312 "type": "registry",
12161313 "version": "6.2.0",
@@ -1447,8 +1544,8 @@
14471544 },
14481545 "yoga-fastify": {
14491546 "type": "registry",
1450− "version": "0.3.0",
1451− "integrity": "sha256-nQZq9ZSVFPSXFzLtC32iz4RRT0JgigFLKlAe1PGB+Lo=",
1547+ "version": "0.5.4",
1548+ "integrity": "sha256-YenjePRdiApMPpcvkXlzre7c4fIuvk5T19Z2uEAWhR4=",
14521549 "dependencies": [
14531550 "aff",
14541551 "arrays",
@@ -1474,8 +1571,8 @@
14741571 },
14751572 "yoga-fastify-om": {
14761573 "type": "registry",
1477− "version": "0.3.2",
1478− "integrity": "sha256-otv4BiPNnw/YQIgfpAWYgcgjUJYkkL4GUGV34Muni/0=",
1574+ "version": "0.4.7",
1575+ "integrity": "sha256-KGQ7lHI7pcSiEQoEq6j6K+3XilcPy8wQ/M9C1y+uaEk=",
14791576 "dependencies": [
14801577 "aff",
14811578 "console",
@@ -1487,11 +1584,14 @@
14871584 "integers",
14881585 "maybe",
14891586 "node-buffer",
1587+ "numbers",
14901588 "prelude",
14911589 "record",
14921590 "refs",
14931591 "routing-duplex",
1592+ "strings",
14941593 "transformers",
1594+ "typelevel-prelude",
14951595 "unsafe-coerce",
14961596 "variant",
14971597 "yoga-fastify",
@@ -1504,19 +1604,25 @@
15041604 },
15051605 "yoga-http-api": {
15061606 "type": "registry",
1507− "version": "0.3.0",
1508− "integrity": "sha256-cv4dQGgM6SQWXQmZFXMmjkFdddCPBzBP7NnNuov3iaI=",
1607+ "version": "0.3.4",
1608+ "integrity": "sha256-DSIJ8YWr5vB5IMpyaBALM+fx57lz3AAAR0MGtfHeWxo=",
15091609 "dependencies": [
15101610 "aff",
1611+ "arraybuffer-types",
15111612 "arrays",
1613+ "effect",
15121614 "either",
15131615 "foldable-traversable",
15141616 "foreign",
15151617 "foreign-object",
15161618 "identity",
15171619 "integers",
1620+ "js-promise",
1621+ "js-promise-aff",
15181622 "maybe",
15191623 "newtype",
1624+ "nullable",
1625+ "numbers",
15201626 "prelude",
15211627 "record",
15221628 "strings",
@@ -1628,8 +1734,8 @@
16281734 },
16291735 "yoga-om-strom": {
16301736 "type": "registry",
1631− "version": "0.4.2",
1632− "integrity": "sha256-BmKYog5spZOhW1foqbRVJn85UHIGsoDYi2iAbo6XeXw=",
1737+ "version": "0.4.3",
1738+ "integrity": "sha256-dxj1zveEES6lF+iiMHx+qOhUG3zmiJJnlNpwiAfUBRE=",
16331739 "dependencies": [
16341740 "aff",
16351741 "arrays",
spago.yaml+21−9
@@ -2,7 +2,7 @@ workspace: {}
22package:
33 name: yoga-fetch-om
44 publish:
5− version: 0.6.2
5+ version: 0.8.1
66 license: MIT
77 location:
88 githubOwner: rowtype-yoga
@@ -10,37 +10,49 @@ package:
1010 include:
1111 - LICENSE
1212 - README.md
13− - CHANGELOG.md
1413 exclude:
1514 - .github
1615 dependencies:
17− - aff: ">=7.1.0 <8.0.0"
16+ - aff: ">=8.0.0 <9.0.0"
1817 - arraybuffer-types: ">=3.0.2 <4.0.0"
1918 - arrays: ">=7.3.0 <8.0.0"
2019 - effect: ">=4.0.0 <5.0.0"
2120 - either: ">=6.1.0 <7.0.0"
2221 - exceptions: ">=6.1.0 <7.0.0"
23− - http-methods: ">=6.0.0 <7.0.0"
22+ - http-methods: ">=6.1.0 <7.0.0"
2423 - js-fetch: ">=0.2.1 <0.3.0"
2524 - js-promise-aff: ">=1.0.0 <2.0.0"
2625 - maybe: ">=6.0.0 <7.0.0"
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.0 <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"
38− - yoga-om-strom: ">=0.4.2 <0.5.0"
38+ - 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.1.0 <0.4.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+183−42
@@ -1,5 +1,8 @@
11module Yoga.Fetch.Om
22 ( client
3+ , clientWith
4+ , ClientInputs
5+ , ClientWithConfig
36 , deriveClient
47 , class DeriveClient
58 , deriveClientImpl
@@ -7,19 +10,25 @@ 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
1927
2028import Prelude
2129
22−import Data.Symbol (class IsSymbol)
30+import Data.Symbol (class IsSymbol, reflectSymbol)
31+import Data.Tuple.Nested ((/\))
2332import Data.Variant (Variant)
2433import Prim.Row as Row
2534import Prim.RowList (class RowToList, RowList)
@@ -29,20 +38,31 @@ import Type.Proxy (Proxy(..))
2938import Unsafe.Coerce (unsafeCoerce)
3039import JS.Fetch.Headers (Headers)
3140import JS.Fetch.Headers as Headers
32−import Type.Row.Homogeneous (class Homogeneous)
3341import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
3442import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
3543import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
3644import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
37−import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractRequestHeaders)
38−import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class SerializeBody, class ContentType, makeRequest, serializeBody, contentType)
45+import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
46+import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
3947import 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)
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
@@ -57,25 +77,38 @@ class ToHeaders headersRL headers where
5777instance ToHeaders RL.Nil headers where
5878 toHeaders _ _ = Headers.empty
5979
60−else instance Homogeneous headers String => ToHeaders headersRL headers where
61− toHeaders _ = Headers.fromRecord
80+else instance
81+ ( IsSymbol name
82+ , HeaderValue ty
83+ , Row.Cons name ty tailRow headers
84+ , Row.Lacks name tailRow
85+ , ToHeaders tail tailRow
86+ ) =>
87+ ToHeaders (RL.Cons name ty tail) headers where
88+ toHeaders _ headers = Headers.fromFoldable ([ headerName /\ headerValue ] <> Headers.toArray rest)
89+ where
90+ headerName = reflectSymbol (Proxy :: Proxy name)
91+ headerValue = printHeader (Record.get (Proxy :: Proxy name) headers)
92+ rest = toHeaders (Proxy :: Proxy tail) (unsafeCoerce headers :: Record tailRow)
6293
63−class DeriveClientFn :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
94+class DeriveClientFn
95+ :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
6496class
65− DeriveClientFn method segments request response routeErrors successRow result fn
66− | method segments request response -> routeErrors successRow result fn where
97+ DeriveClientFn method segments request response ctx extraErr routeErrors result fn
98+ | method segments request response -> routeErrors result
99+ , method segments request response ctx extraErr -> fn where
67100 deriveClientFn :: String -> Proxy (Route method segments request response) -> fn
68101
69102instance
70103 ( SegmentPathParams segments pathParams
71104 , SegmentQueryParams segments queryParams
72105 , ExtractRequestBody request body
106+ , ExtractBodyEncoding request encoding
73107 , ExtractRequestHeaders request headers
74108 , PathPattern segments
75109 , BuildUrl segments pathParams queryParams
76110 , MakeRequest method
77− , SerializeBody body
78− , ContentType body
111+ , BodyEncoding encoding body
79112 , SplitResponses response successRow routeErrors
80113 , ParseResponse routeErrors successRow
81114 , RowToList successRow successRL
@@ -86,74 +119,182 @@ instance
86119 , RowToList headers headersRL
87120 , ToHeaders headersRL headers
88121 , CheckBodyIsUnit body bodyFlag
89− , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers routeErrors result fn
122+ , Row.Union routeErrors extraErr errRow
123+ , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers (Record pathQuery) { | ctx } errRow result fn
90124 ) =>
91− DeriveClientFn method segments request response routeErrors successRow result fn where
125+ DeriveClientFn method segments request response ctx extraErr routeErrors result fn where
92126 deriveClientFn baseUrl _ =
93− buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) impl
127+ buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) identity impl
94128 where
95− impl :: Record pathQuery -> Record headers -> body -> Om (Record ()) routeErrors result
96− impl pathQueryRec headersRec bodyVal = do
129+ impl :: Record pathQuery -> Record headers -> body -> Om { | ctx } errRow result
130+ impl pathQueryRec headersRec bodyVal = widenOm do
97131 let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
98132 let hdrs = toHeaders (Proxy :: _ headersRL) headersRec
99− fetchResp <- makeRequest (Proxy :: _ method) url hdrs (contentType (Proxy :: _ body)) (serializeBody bodyVal) # toOm
133+ let ct = encodingContentType (Proxy :: _ encoding)
134+ fetchResp <- makeRequest (Proxy :: _ method) url hdrs ct (encodeBody @encoding bodyVal) # toOm
100135 variant <- parseResponse fetchResp :: Om (Record ()) routeErrors (Variant successRow)
101136 variantOrValue (Proxy :: _ successRL) variant # pure
102137 where
103138 pathParamsRec = unsafeCoerce pathQueryRec :: Record pathParams
104139 queryParamsRec = unsafeCoerce pathQueryRec :: Record queryParams
105140
106−class DeriveClient :: Row Type -> Row Type -> Constraint
107−class DeriveClient routesRow clientsRow | routesRow -> clientsRow where
141+ widenOm :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
142+ widenOm = unsafeCoerce
143+
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+
208+class DeriveClient :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
209+class DeriveClient ctx extraErr routesRow clientsRow | routesRow ctx extraErr -> clientsRow where
108210 deriveClientImpl :: String -> Proxy (Record routesRow) -> Record clientsRow
109211
110−-- | 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.
111215-- |
112216-- | ```purescript
113217-- | type UserAPI = { getUser :: Route ... }
114218-- | 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
115222-- | ```
116−client :: forall @routes routesRow clientsRow polyClientsRow. RecordRow routes routesRow => DeriveClient routesRow clientsRow => String -> Record polyClientsRow
117−client baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
223+client :: forall @routes routesRow ctx extraErr clientsRow. RecordRow routes routesRow => DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
224+client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
118225
119226-- | Deprecated: Use `client` with VTA instead
120227-- |
121228-- | ```purescript
122229-- | api = deriveClient @UserAPI "https://api.example.com"
123230-- | ```
124−deriveClient :: forall @routesRow clientsRow polyClientsRow. DeriveClient routesRow clientsRow => String -> Record polyClientsRow
125−deriveClient baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
231+deriveClient :: forall @routesRow ctx extraErr clientsRow. DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
232+deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
233+
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 })
126241
127−plainText :: PlainText -> String
128−plainText = unsafeCoerce
242+instance
243+ ( RowToList routesRow rl
244+ , DeriveClientRL ctx extraErr rl () clientsRow
245+ ) =>
246+ DeriveClient ctx extraErr routesRow clientsRow where
247+ deriveClientImpl baseUrl _ = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ rl) {}
129248
130−polymorphic :: forall a b. a -> b
131−polymorphic = unsafeCoerce
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
132252
133253instance
134254 ( RowToList routesRow rl
135− , DeriveClientRL rl () clientsRow
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
136274 ) =>
137− DeriveClient routesRow clientsRow where
138− deriveClientImpl baseUrl _ = deriveClientRL baseUrl (Proxy :: _ rl) {}
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
139280
140−class DeriveClientRL :: RowList Type -> Row Type -> Row Type -> Constraint
141−class DeriveClientRL rl acc out | rl acc -> out where
281+class DeriveClientRL :: Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
282+class DeriveClientRL ctx extraErr rl acc out | ctx extraErr rl acc -> out where
142283 deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
143284
144−instance DeriveClientRL RL.Nil acc acc where
285+instance DeriveClientRL ctx extraErr RL.Nil acc acc where
145286 deriveClientRL _ _ acc = acc
146287
147288instance
148289 ( IsSymbol label
149− , DeriveClientFn method segments request response routeErrors successRow result fn
150− , DeriveClientRL tail acc1 acc2
290+ , DeriveClientFn method segments request response ctx extraErr routeErrors result fn
291+ , DeriveClientRL ctx extraErr tail acc1 acc2
151292 , Row.Cons label fn acc2 out
152293 , Row.Lacks label acc2
153294 , Row.Cons label (Route method segments request response) routeTail routeRow
154295 ) =>
155− DeriveClientRL (RL.Cons label (Route method segments request response) tail) acc1 out where
296+ DeriveClientRL ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
156297 deriveClientRL baseUrl _ acc = Record.insert (Proxy :: _ label) clientFn rest
157298 where
158− clientFn = deriveClientFn baseUrl (Proxy :: _ (Route method segments request response))
159− rest = deriveClientRL baseUrl (Proxy :: _ tail) acc
299+ clientFn = deriveClientFn @_ @_ @_ @_ @ctx @extraErr baseUrl (Proxy :: _ (Route method segments request response))
300+ rest = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ tail) acc
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+37−20
@@ -22,17 +22,18 @@ instance CheckBodyIsUnit Unit IsUnit
2222else instance CheckBodyIsUnit body IsNotUnit
2323
2424class BuildClientFn
25− :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row 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 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 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− -> (Record pathQuery -> Record headers -> body -> Om (Record ()) errorRow result)
35+ -> (arg -> Record pathQuery)
36+ -> (Record pathQuery -> Record headers -> body -> Om ctx errorRow result)
3637 -> fn
3738
3839-- No path/query, no headers, no body
@@ -43,10 +44,12 @@ instance
4344 Unit
4445 ()
4546 ()
47+ arg
48+ ctx
4649 errorRow
4750 result
48− (Om (Record ()) errorRow result) where
49− buildClientFn _ _ _ f = f {} {} unit
51+ (Om ctx errorRow result) where
52+ buildClientFn _ _ _ _ f = f {} {} unit
5053
5154-- No path/query, no headers, with body
5255instance
@@ -56,10 +59,12 @@ instance
5659 body
5760 ()
5861 ()
62+ arg
63+ ctx
5964 errorRow
6065 result
61− (body -> Om (Record ()) errorRow result) where
62− buildClientFn _ _ _ f b = f {} {} b
66+ (body -> Om ctx errorRow result) where
67+ buildClientFn _ _ _ _ f b = f {} {} b
6368
6469-- No path/query, with headers, no body
6570instance
@@ -69,10 +74,12 @@ instance
6974 Unit
7075 ()
7176 h
77+ arg
78+ ctx
7279 errorRow
7380 result
74− (Record h -> Om (Record ()) errorRow result) where
75− buildClientFn _ _ _ f hdrs = f {} hdrs unit
81+ (Record h -> Om ctx errorRow result) where
82+ buildClientFn _ _ _ _ f hdrs = f {} hdrs unit
7683
7784-- No path/query, with headers, with body
7885instance
@@ -82,10 +89,12 @@ instance
8289 body
8390 ()
8491 h
92+ arg
93+ ctx
8594 errorRow
8695 result
87− (Record h -> body -> Om (Record ()) errorRow result) where
88− buildClientFn _ _ _ f hdrs b = f {} hdrs b
96+ (Record h -> body -> Om ctx errorRow result) where
97+ buildClientFn _ _ _ _ f hdrs b = f {} hdrs b
8998
9099-- With path/query, no headers, no body
91100instance
@@ -95,10 +104,12 @@ instance
95104 Unit
96105 pq
97106 ()
107+ arg
108+ ctx
98109 errorRow
99110 result
100− (Record pq -> Om (Record ()) errorRow result) where
101− buildClientFn _ _ _ f pqr = f pqr {} unit
111+ (arg -> Om ctx errorRow result) where
112+ buildClientFn _ _ _ convert f pqr = f (convert pqr) {} unit
102113
103114-- With path/query, no headers, with body
104115instance
@@ -108,10 +119,12 @@ instance
108119 body
109120 pq
110121 ()
122+ arg
123+ ctx
111124 errorRow
112125 result
113− (Record pq -> body -> Om (Record ()) errorRow result) where
114− 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
115128
116129-- With path/query, with headers, no body
117130instance
@@ -121,10 +134,12 @@ instance
121134 Unit
122135 pq
123136 h
137+ arg
138+ ctx
124139 errorRow
125140 result
126− (Record pq -> Record h -> Om (Record ()) errorRow result) where
127− 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
128143
129144-- With path/query, with headers, with body
130145instance
@@ -134,7 +149,9 @@ instance
134149 body
135150 pq
136151 h
152+ arg
153+ ctx
137154 errorRow
138155 result
139− (Record pq -> Record h -> body -> Om (Record ()) errorRow result) where
140− 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/ExtractParams.purs+13
@@ -1,6 +1,7 @@
11module Yoga.Fetch.Om.ExtractParams
22 ( class ExtractRequestHeaders
33 , class ExtractRequestBody
4+ , class ExtractBodyEncoding
45 , class FindBodyType
56 , class UnwrapEncoding
67 , class FindHeaders
@@ -25,6 +26,18 @@ else instance
2526 ExtractRequestBody (Record row) body =>
2627 ExtractRequestBody (Request (Record row)) body
2728
29+class ExtractBodyEncoding :: Type -> Type -> Constraint
30+class ExtractBodyEncoding request encoding | request -> encoding
31+
32+instance
33+ ( RowToList row rl
34+ , FindBodyType rl encoding
35+ ) =>
36+ ExtractBodyEncoding (Record row) encoding
37+else instance
38+ ExtractBodyEncoding (Record row) encoding =>
39+ ExtractBodyEncoding (Request (Record row)) encoding
40+
2841class ExtractRequestHeaders :: Type -> Row Type -> Constraint
2942class ExtractRequestHeaders request headers | request -> headers
3043
src/Yoga/Fetch/Om/MakeRequest.jsnew file+1
@@ -0,0 +1 @@
1+export const toUrlEncoded = (obj) => new URLSearchParams(obj).toString();
src/Yoga/Fetch/Om/MakeRequest.purs+31−27
@@ -2,10 +2,9 @@ module Yoga.Fetch.Om.MakeRequest
22 ( class MakeRequest
33 , httpMethod
44 , makeRequest
5− , class SerializeBody
6− , serializeBody
7− , class ContentType
8− , contentType
5+ , class BodyEncoding
6+ , encodingContentType
7+ , encodeBody
98 ) where
109
1110import Prelude
@@ -32,6 +31,7 @@ import JS.Fetch.Duplex as Duplex
3231import JS.Fetch.Response (Response) as FetchResponse
3332import Promise.Aff as Promise
3433import Type.Proxy (Proxy)
34+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, PlainText, NoBody)
3535import Yoga.HTTP.API.Route.Method as Method
3636import Yoga.JSON (class WriteForeign, writeJSON)
3737
@@ -54,6 +54,9 @@ instance MakeRequest Method.DELETE where
5454instance MakeRequest Method.PATCH where
5555 httpMethod _ = PATCH
5656
57+instance MakeRequest Method.QUERY where
58+ httpMethod _ = QUERY
59+
5760makeRequest
5861 :: forall method
5962 . MakeRequest method
@@ -63,7 +66,7 @@ makeRequest
6366 -> String
6467 -> Maybe String
6568 -> Aff FetchResponse.Response
66−makeRequest proxy url customHeaders defaultContentType maybeBody = do
69+makeRequest proxy url customHeaders bodyContentType maybeBody = do
6770 request <- Request.new url options # liftEffect
6871 Promise.toAffE $ Fetch.fetch request
6972 where
@@ -73,7 +76,7 @@ makeRequest proxy url customHeaders defaultContentType maybeBody = do
7376 contentTypeArr = case maybeBody of
7477 Nothing -> []
7578 Just _ | hasContentType -> []
76− Just _ -> [ "Content-Type" /\ defaultContentType ]
79+ Just _ -> [ "Content-Type" /\ bodyContentType ]
7780 allHeaders = Headers.fromFoldable (contentTypeArr <> customArr)
7881 body = case maybeBody of
7982 Nothing -> Body.empty
@@ -91,24 +94,25 @@ makeRequest proxy url customHeaders defaultContentType maybeBody = do
9194 , cache: Cache.Default
9295 }
9396
94−class ContentType :: Type -> Constraint
95−class ContentType body where
96− contentType :: Proxy body -> String
97−
98−instance ContentType String where
99− contentType _ = "text/plain"
100−else instance ContentType Unit where
101− contentType _ = "application/json"
102−else instance ContentType body where
103− contentType _ = "application/json"
104−
105−class SerializeBody :: Type -> Constraint
106−class SerializeBody body where
107− serializeBody :: body -> Maybe String
108−
109−instance SerializeBody String where
110− serializeBody s = Just s
111−else instance SerializeBody Unit where
112− serializeBody _ = Nothing
113−else instance WriteForeign body => SerializeBody body where
114− serializeBody b = Just (writeJSON b)
97+class BodyEncoding :: Type -> Type -> Constraint
98+class BodyEncoding encoding body | encoding -> body where
99+ encodingContentType :: Proxy encoding -> String
100+ encodeBody :: body -> Maybe String
101+
102+instance WriteForeign ty => BodyEncoding (JSON ty) ty where
103+ encodingContentType _ = "application/json"
104+ encodeBody b = Just (writeJSON b)
105+
106+else instance BodyEncoding PlainText String where
107+ encodingContentType _ = "text/plain"
108+ encodeBody = Just
109+
110+else instance BodyEncoding (FormData ty) ty where
111+ encodingContentType _ = "application/x-www-form-urlencoded"
112+ encodeBody b = Just (toUrlEncoded b)
113+
114+else instance BodyEncoding NoBody Unit where
115+ encodingContentType _ = ""
116+ encodeBody _ = Nothing
117+
118+foreign import toUrlEncoded :: forall a. a -> String
src/Yoga/Fetch/Om/ParseResponse.purs+33−29
@@ -5,6 +5,8 @@ module Yoga.Fetch.Om.ParseResponse
55 , parseSuccessRL
66 , class ParseErrorRL
77 , parseErrorRL
8+ , class ParseResponseBody
9+ , parseResponseBody
810 ) where
911
1012import Prelude
@@ -27,15 +29,23 @@ import Promise.Aff as Promise
2729import Type.Proxy (Proxy(..))
2830import Unsafe.Coerce (unsafeCoerce)
2931import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
30−import Yoga.HTTP.API.Route.Encoding (PlainText, Streaming)
3132import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
3233import Yoga.JSON (class ReadForeign, readJSON)
3334import Yoga.JSON.Error (withStringErrors)
34−import Yoga.Om (class ToOm, Om, toOm)
35+import Yoga.Om (Om, toOm)
3536import Yoga.Om.Error (Exception)
3637import Yoga.Om.Strom (Strom)
3738import Yoga.Om.Strom.WebStream as WebStream
3839
40+class ParseResponseBody (body :: Type) where
41+ parseResponseBody :: String -> Either String body
42+
43+instance ParseResponseBody String where
44+ parseResponseBody = Right
45+
46+else instance ReadForeign body => ParseResponseBody body where
47+ parseResponseBody = withStringErrors <<< readJSON
48+
3949class ParseResponse (errorRow :: Row Type) (successRow :: Row Type) where
4050 parseResponse :: FetchResponse.Response -> Om {} errorRow (Variant successRow)
4151
@@ -50,11 +60,9 @@ instance
5060 let status = Fetch.status fetchResp
5161 if status >= 200 && status < 300 then
5262 parseSuccessRL (Proxy :: _ successRL) status fetchResp # toOm
53− else if status >= 400 then do
63+ else do
5464 errorText <- Promise.toAffE (Fetch.text fetchResp) # toOm
5565 parseErrorRL (Proxy :: _ errorRL) (Proxy :: _ errorRow) status errorText
56− else
57− throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Unexpected HTTP status code: " <> show status)))
5866
5967class ParseSuccessRL (rl :: RowList Type) (successRow :: Row Type) | rl -> successRow where
6068 parseSuccessRL :: Proxy rl -> Int -> FetchResponse.Response -> Aff (Variant successRow)
@@ -66,41 +74,39 @@ instance ParseSuccessRL RL.Nil successRow where
6674else instance
6775 ( IsSymbol label
6876 , StatusCodeMap label
69− , Row.Cons label PlainText tailRow successRow
77+ , StreamDecode a
78+ , Row.Cons label (Strom streamContext streamError a) tailRow successRow
7079 , Row.Lacks label tailRow
7180 , ParseSuccessRL tailRL successRow
7281 ) =>
73− ParseSuccessRL (RL.Cons label PlainText tailRL) successRow where
82+ ParseSuccessRL (RL.Cons label (Strom streamContext streamError a) tailRL) successRow where
7483 parseSuccessRL _ actualStatus fetchResp = do
7584 let StatusCode expected = statusCodeFor (Proxy :: _ label)
7685 if actualStatus == expected then do
77− text <- liftEffect (Fetch.text fetchResp) >>= Promise.toAff
78− 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))
7989 else
8090 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
8191
8292else instance
8393 ( IsSymbol label
8494 , StatusCodeMap label
85− , StreamDecode a
86− , Row.Cons label (Streaming a) tailRow successRow
95+ , Row.Cons label Unit tailRow successRow
8796 , Row.Lacks label tailRow
8897 , ParseSuccessRL tailRL successRow
8998 ) =>
90− ParseSuccessRL (RL.Cons label (Streaming a) tailRL) successRow where
99+ ParseSuccessRL (RL.Cons label Unit tailRL) successRow where
91100 parseSuccessRL _ actualStatus fetchResp = do
92101 let StatusCode expected = statusCodeFor (Proxy :: _ label)
93− if actualStatus == expected then do
94− webStream <- liftEffect (Fetch.body fetchResp)
95− let strom = decodeStream (WebStream.fromReadableStream (unsafeCoerce webStream)) :: Strom {} () a
96− pure (Variant.inj (Proxy :: _ label) (unsafeCoerce strom))
102+ if actualStatus == expected then
103+ pure (Variant.inj (Proxy :: _ label) unit)
97104 else
98105 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
99−
100106else instance
101107 ( IsSymbol label
102108 , StatusCodeMap label
103− , ReadForeign body
109+ , ParseResponseBody body
104110 , Row.Cons label body tailRow successRow
105111 , Row.Lacks label tailRow
106112 , ParseSuccessRL tailRL successRow
@@ -110,7 +116,7 @@ else instance
110116 let StatusCode expected = statusCodeFor (Proxy :: _ label)
111117 if actualStatus == expected then do
112118 jsonText <- liftEffect (Fetch.text fetchResp) >>= Promise.toAff
113− case withStringErrors (readJSON jsonText) of
119+ case parseResponseBody jsonText of
114120 Left errs -> Aff.throwError (Exception.error ("Failed to parse response (status " <> show actualStatus <> "):\n" <> errs))
115121 Right parsed -> pure (Variant.inj (Proxy :: _ label) parsed)
116122 else
@@ -132,23 +138,21 @@ instance ParseErrorRL RL.Nil errorRow where
132138else instance
133139 ( IsSymbol label
134140 , StatusCodeMap label
135− , Row.Cons label PlainText tailRow errorRow
141+ , Row.Cons label Unit tailRow errorRow
136142 , Row.Lacks label tailRow
137− , Row.Cons label PlainText tailExc (Exception errorRow)
143+ , Row.Cons label Unit tailExc (Exception errorRow)
138144 , ParseErrorRL tailRL errorRow
139145 ) =>
140− ParseErrorRL (RL.Cons label PlainText tailRL) errorRow where
141− parseErrorRL _ errorProxy actualStatus text = do
146+ ParseErrorRL (RL.Cons label Unit tailRL) errorRow where
147+ parseErrorRL _ errorProxy actualStatus jsonText = do
142148 let StatusCode expected = statusCodeFor (Proxy :: _ label)
143− if actualStatus == expected then
144− throwError (Variant.inj (Proxy :: _ label) (unsafeCoerce text))
145− else
146− parseErrorRL (Proxy :: _ tailRL) errorProxy actualStatus text
149+ if actualStatus == expected then throwError (Variant.inj (Proxy :: _ label) unit)
150+ else parseErrorRL (Proxy :: _ tailRL) errorProxy actualStatus jsonText
147151
148152else instance
149153 ( IsSymbol label
150154 , StatusCodeMap label
151− , ReadForeign body
155+ , ParseResponseBody body
152156 , Row.Cons label body tailRow errorRow
153157 , Row.Lacks label tailRow
154158 , Row.Cons label body tailExc (Exception errorRow)
@@ -157,7 +161,7 @@ else instance
157161 ParseErrorRL (RL.Cons label body tailRL) errorRow where
158162 parseErrorRL _ errorProxy actualStatus jsonText = do
159163 let StatusCode expected = statusCodeFor (Proxy :: _ label)
160− if actualStatus == expected then case withStringErrors (readJSON jsonText) of
164+ if actualStatus == expected then case parseResponseBody jsonText of
161165 Left errs -> throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Failed to parse error response (status " <> show actualStatus <> "):\n" <> errs)))
162166 Right parsed -> throwError (Variant.inj (Proxy :: _ label) parsed)
163167 else
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−21
@@ -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
@@ -102,13 +93,7 @@ simpleFetch method headers url maybeBody = do
10293 { method
10394 , headers: allHeaders
10495 , body: reqBody
105− , credentials: Credentials.SameOrigin
106− , mode: Mode.Cors
107− , referrer: Referrer.ReferrerNone
108− , referrerPolicy: ReferrerPolicy.NoReferrer
109− , integrity: Integrity ""
11096 , duplex: Duplex.Half
111− , cache: Cache.Default
11297 }
11398
11499get
@@ -153,7 +138,7 @@ delete_
153138 => String
154139 -> Record headers
155140 -> Om ctx (fetchError :: FetchError | err) Unit
156−delete_ url headers = void $ simpleFetch @PlainTextResponse DELETE headers url Nothing
141+delete_ url headers = void $ simpleFetch @String DELETE headers url Nothing
157142
158143post
159144 :: forall @a result headers body ctx err
@@ -185,7 +170,7 @@ post_
185170 -> Record headers
186171 -> body
187172 -> Om ctx (fetchError :: FetchError | err) Unit
188−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))
189174
190175put
191176 :: forall @a result headers body ctx err
@@ -217,7 +202,7 @@ put_
217202 -> Record headers
218203 -> body
219204 -> Om ctx (fetchError :: FetchError | err) Unit
220−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))
221206
222207patch
223208 :: forall @a result headers body ctx err
@@ -249,4 +234,4 @@ patch_
249234 -> Record headers
250235 -> body
251236 -> Om ctx (fetchError :: FetchError | err) Unit
252−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+24−2
@@ -1,6 +1,8 @@
11module Yoga.Fetch.Om.SplitResponses
22 ( class IsSuccessStatus
3+ , class ExtractBodyRL
34 , class ExtractBody
5+ , class ClientResponseBody
46 , class SplitResponses
57 , class SplitResponsesRL
68 , class DispatchResponse
@@ -10,7 +12,10 @@ import Prim.Boolean (True, False)
1012import Prim.Row as Row
1113import Prim.RowList (RowList)
1214import Prim.RowList as RL
15+import Data.Unit (Unit)
1316import Yoga.HTTP.API.Route (Response)
17+import Yoga.HTTP.API.Route.Encoding (PlainText, Streaming)
18+import Yoga.Om.Strom (Strom)
1419
1520class IsSuccessStatus (label :: Symbol) (isSuccess :: Boolean) | label -> isSuccess
1621
@@ -26,12 +31,29 @@ else instance IsSuccessStatus "alreadyReported" True
2631else instance IsSuccessStatus "imUsed" True
2732else instance IsSuccessStatus label False
2833
34+-- Traverse a RowList to find the "body" field; fall back to Unit if absent.
35+class ExtractBodyRL (rl :: RowList Type) (body :: Type) | rl -> body
36+
37+instance ExtractBodyRL RL.Nil Unit
38+
39+instance ExtractBodyRL (RL.Cons "body" body tail) body
40+
41+else instance ExtractBodyRL tail body => ExtractBodyRL (RL.Cons label ty tail) body
42+
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+
2949class ExtractBody (responseType :: Type) (bodyType :: Type) | responseType -> bodyType
3050
31−instance ExtractBody (Response headers body) body
51+instance ClientResponseBody encoded body => ExtractBody (Response headers encoded) body
3252
3353else instance
34− ( Row.Cons "body" body rest row
54+ ( RL.RowToList row rl
55+ , ExtractBodyRL rl encoded
56+ , ClientResponseBody encoded body
3557 ) =>
3658 ExtractBody (Record row) body
3759
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-compile-fail/cases/ClientBogusSignature.pursnew file+9
@@ -0,0 +1,9 @@
1+module Test.CompileFail.ClientBogusSignature where
2+
3+import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
4+
5+type GetUserRoute = Route GET ("users" / "id" : Int) {} (ok :: { body :: { id :: Int, name :: String } })
6+type TestAPI = { getUser :: GetUserRoute }
7+
8+wrong :: { getUser :: String -> Int }
9+wrong = client @TestAPI "http://localhost:3000"
test-compile-fail/cases/ClientExtraField.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.CompileFail.ClientExtraField where
2+
3+import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
4+import Yoga.Om (Om)
5+
6+type GetUserRoute = Route GET ("users" / "id" : Int) {} (ok :: { body :: { id :: Int, name :: String } })
7+type TestAPI = { getUser :: GetUserRoute }
8+
9+api = client @TestAPI "http://localhost:3000"
10+
11+wrong :: forall ctx err. Om ctx err { id :: Int, name :: String }
12+wrong = api.getUser { id: 42, extra: "oops" }
test-compile-fail/cases/ClientMissingField.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.CompileFail.ClientMissingField where
2+
3+import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
4+import Yoga.Om (Om)
5+
6+type GetUserRoute = Route GET ("users" / "id" : Int) {} (ok :: { body :: { id :: Int, name :: String } })
7+type TestAPI = { getUser :: GetUserRoute }
8+
9+api = client @TestAPI "http://localhost:3000"
10+
11+wrong :: forall ctx err. Om ctx err { id :: Int, name :: String }
12+wrong = api.getUser {}
test-compile-fail/cases/ClientNoArgsPassedArg.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.CompileFail.ClientNoArgsPassedArg where
2+
3+import Yoga.Fetch.Om (GET, Route, client)
4+import Yoga.Om (Om)
5+
6+type HealthRoute = Route GET "health" {} (ok :: { body :: String })
7+type TestAPI = { health :: HealthRoute }
8+
9+api = client @TestAPI "http://localhost:3000"
10+
11+wrong :: forall ctx err. Om ctx err String
12+wrong = api.health "should not accept this"
test-compile-fail/cases/ClientWrongFieldName.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.CompileFail.ClientWrongFieldName where
2+
3+import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
4+import Yoga.Om (Om)
5+
6+type GetUserRoute = Route GET ("users" / "id" : Int) {} (ok :: { body :: { id :: Int, name :: String } })
7+type TestAPI = { getUser :: GetUserRoute }
8+
9+api = client @TestAPI "http://localhost:3000"
10+
11+wrong :: forall ctx err. Om ctx err { id :: Int, name :: String }
12+wrong = api.getUser { userId: 42 }
test-compile-fail/cases/ClientWrongFieldType.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.CompileFail.ClientWrongFieldType where
2+
3+import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
4+import Yoga.Om (Om)
5+
6+type GetUserRoute = Route GET ("users" / "id" : Int) {} (ok :: { body :: { id :: Int, name :: String } })
7+type TestAPI = { getUser :: GetUserRoute }
8+
9+api = client @TestAPI "http://localhost:3000"
10+
11+wrong :: forall ctx err. Om ctx err { id :: Int, name :: String }
12+wrong = api.getUser { id: "not-an-int" }
test-compile-fail/run.shnew file+26
@@ -0,0 +1,26 @@
1+#!/usr/bin/env bash
2+set -euo pipefail
3+
4+# Compile-fail tests for client derivation type safety.
5+# Each .purs file in cases/ must FAIL to compile.
6+
7+SOURCES=$(bunx spago sources 2>/dev/null | grep -v 'test/')
8+OUTPUT_DIR="output-compile-fail"
9+CASES_DIR="test-compile-fail/cases"
10+PASS=0
11+FAIL=0
12+
13+for f in "$CASES_DIR"/*.purs; do
14+ name=$(basename "$f" .purs)
15+ if bunx purs compile --output "$OUTPUT_DIR" $SOURCES "$f" >/dev/null 2>&1; then
16+ echo "FAIL: $name compiled but should not have"
17+ FAIL=$((FAIL + 1))
18+ else
19+ echo " ok: $name correctly rejected"
20+ PASS=$((PASS + 1))
21+ fi
22+done
23+
24+echo ""
25+echo "$PASS passed, $FAIL failed"
26+[ "$FAIL" -eq 0 ]
test/BodylessResponse.Example.pursnew file+27
@@ -0,0 +1,27 @@
1+-- Compile-time test: bodyless responses (e.g. 304, 401) can use {} without
2+-- wrapping in { body :: {} }.
3+module BodylessResponse.Example where
4+
5+import Prelude
6+
7+import Yoga.Fetch.Om (GET, Route, type (/), client)
8+import Yoga.Om (Om)
9+
10+type Resource = { id :: Int, name :: String }
11+
12+type ResourceAPI =
13+ { getResource ::
14+ Route GET
15+ ("resources" / "id")
16+ {}
17+ ( ok :: { body :: Resource }
18+ , notModified :: {}
19+ , unauthorized :: {}
20+ )
21+ }
22+
23+-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
24+api
25+ :: forall ctx err
26+ . { getResource :: Om (Record ctx) (notModified :: Unit, unauthorized :: Unit | err) Resource }
27+api = client @ResourceAPI "https://api.example.com"
test/BodylessResponse.Spec.pursnew file+125
@@ -0,0 +1,125 @@
1+module BodylessResponse.Spec where
2+
3+import Prelude
4+
5+import Effect.Ref (Ref)
6+import Effect.Ref as Ref
7+import Effect.Aff (Aff, bracket)
8+import Effect.Aff.Class (liftAff)
9+import Effect.Class (liftEffect)
10+import Foreign (unsafeToForeign)
11+import Test.Spec (Spec, around, describe, it)
12+import Test.Spec.Assertions (shouldEqual)
13+import Yoga.Fastify.Fastify as F
14+import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15+import Yoga.Fastify.Om (createOmFastify, deleteOm, getOm)
16+import Yoga.Fetch.Om (DELETE, GET, Route, type (/), type (:), client)
17+import Yoga.Om (runOm)
18+
19+type Resource = { id :: Int, name :: String }
20+
21+type ResourceAPI =
22+ { getResource ::
23+ Route GET
24+ ("bodyless-test" / "resource")
25+ {}
26+ ( ok :: { body :: Resource }
27+ , notModified :: {}
28+ , unauthorized :: {}
29+ )
30+ , deleteUser ::
31+ Route DELETE
32+ ("bodyless-test" / "users" / "id" : Int)
33+ {}
34+ ( noContent :: {}
35+ , notFound :: { body :: { error :: String } }
36+ )
37+ }
38+
39+api = client @ResourceAPI "http://localhost:44932"
40+
41+spec :: Spec Unit
42+spec = around withServer $ describe "bodyless responses (notModified / unauthorized)" do
43+ it "304 Not Modified triggers the notModified handler" \serverMode -> do
44+ liftEffect $ Ref.write "notModified" serverMode
45+ triggered <- liftEffect $ Ref.new false
46+ runOm {}
47+ { exception: \_ -> pure unit
48+ , notModified: \_ -> liftEffect $ Ref.write true triggered
49+ , unauthorized: \_ -> pure unit
50+ }
51+ (void api.getResource)
52+ result <- liftEffect $ Ref.read triggered
53+ liftAff $ result `shouldEqual` true
54+
55+ it "401 Unauthorized triggers the unauthorized handler" \serverMode -> do
56+ liftEffect $ Ref.write "unauthorized" serverMode
57+ triggered <- liftEffect $ Ref.new false
58+ runOm {}
59+ { exception: \_ -> pure unit
60+ , notModified: \_ -> pure unit
61+ , unauthorized: \_ -> liftEffect $ Ref.write true triggered
62+ }
63+ (void api.getResource)
64+ result <- liftEffect $ Ref.read triggered
65+ liftAff $ result `shouldEqual` true
66+
67+ it "200 OK returns the resource body" \serverMode -> do
68+ liftEffect $ Ref.write "ok" serverMode
69+ resource <- runOm {}
70+ { exception: \_ -> pure { id: -1, name: "" }
71+ , notModified: \_ -> pure { id: -1, name: "" }
72+ , unauthorized: \_ -> pure { id: -1, name: "" }
73+ }
74+ api.getResource
75+ liftAff $ resource `shouldEqual` { id: 1, name: "test" }
76+
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+
86+withServer :: forall a. (Ref String -> Aff a) -> Aff a
87+withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
88+ where
89+ acquire = do
90+ serverMode <- liftEffect $ Ref.new "ok"
91+ fastify <- liftEffect do
92+ f <- F.fastify {}
93+ omApp <- createOmFastify {} f
94+ getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
95+ deleteOm (RouteURL "/bodyless-test/users/:id") (deleteHandler serverMode) omApp
96+ pure f
97+ void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
98+ pure { fastify, serverMode }
99+
100+ release { fastify } = F.close fastify
101+
102+ handler serverMode reply = do
103+ mode <- liftEffect $ Ref.read serverMode
104+ case mode of
105+ "notModified" -> do
106+ void $ F.status (StatusCode 304) reply # liftEffect
107+ F.send (unsafeToForeign "") reply # liftAff
108+ "unauthorized" -> do
109+ void $ F.status (StatusCode 401) reply # liftEffect
110+ F.send (unsafeToForeign "") reply # liftAff
111+ _ -> do
112+ void $ F.status (StatusCode 200) reply # liftEffect
113+ void $ F.header "content-type" "application/json" reply # liftEffect
114+ 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+17−31
@@ -3,9 +3,10 @@ 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)
9+import Yoga.HTTP.API.Route (BearerToken(..))
910import Yoga.Om (Om, handleErrors)
1011
1112type User =
@@ -30,59 +31,43 @@ type ErrorMessage =
3031
3132type UserAPI =
3233 { getUser ::
33− Route GET
34− (Path ("users" / "id" : Int))
34+ Route GET ("users" / "id" : Int)
3535 {}
3636 ( ok :: { body :: User }
3737 , notFound :: { body :: ErrorMessage }
3838 )
3939 , listUsers ::
40− Route GET
41− (Path "users" :? { limit :: Int, offset :: Int })
40+ Route GET ("users" :? { limit :: Int, offset :: Int })
4241 {}
4342 ( ok :: { body :: Array User }
4443 )
4544 , createUser ::
46− Route POST
47− (Path "users")
45+ Route POST "users"
4846 { body :: JSON CreateUserRequest }
4947 ( created :: { body :: User }
5048 , badRequest :: { body :: ErrorMessage }
5149 )
5250 , updateUser ::
53− Route PUT
54− (Path ("users" / "id" : Int))
51+ Route PUT ("users" / "id" : Int)
5552 { body :: JSON UpdateUserRequest }
5653 ( ok :: { body :: User }
5754 , notFound :: { body :: ErrorMessage }
5855 , badRequest :: { body :: ErrorMessage }
5956 )
6057 , deleteUser ::
61− Route DELETE
62− (Path ("users" / "id" : Int))
58+ Route DELETE ("users" / "id" : Int)
6359 {}
64− ( noContent :: { body :: {} }
60+ ( noContent :: {}
6561 , notFound :: { body :: ErrorMessage }
6662 )
6763 , createUserAuth ::
68− Route POST
69− (Path "users")
70− { headers :: Record (authorization :: String), body :: JSON CreateUserRequest }
64+ Route POST "users"
65+ { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
7166 ( created :: { body :: User }
7267 , badRequest :: { body :: ErrorMessage }
7368 )
7469 }
7570
76−-- Generated client with polymorphic type annotation
77−api
78− :: forall ctx err
79− . { createUser :: CreateUserRequest -> Om ctx (badRequest :: ErrorMessage | err) User
80− , createUserAuth :: { authorization :: String } -> CreateUserRequest -> Om ctx (badRequest :: ErrorMessage | err) User
81− , deleteUser :: { id :: Int } -> Om ctx (notFound :: ErrorMessage | err) {}
82− , getUser :: { id :: Int } -> Om ctx (notFound :: ErrorMessage | err) User
83− , listUsers :: { limit :: Maybe Int, offset :: Maybe Int } -> Om ctx err (Array User)
84− , updateUser :: { id :: Int } -> UpdateUserRequest -> Om ctx (badRequest :: ErrorMessage, notFound :: ErrorMessage | err) User
85− }
8671api = client @UserAPI "https://api.example.com"
8772
8873exampleGetUser :: Om {} (notFound :: ErrorMessage) Unit
@@ -94,15 +79,15 @@ exampleGetUserHandled :: Om {} () Unit
9479exampleGetUserHandled = do
9580 user <- api.getUser { id: 42 }
9681 # handleErrors
97− { notFound: \err -> do
82+ { notFound: \err -> ado
9883 log $ "User not found: " <> err.error
99− pure { id: 0, name: "unknown", email: "" }
84+ in { id: 0, name: "unknown", email: "" }
10085 }
10186 log $ "Found user: " <> user.name
10287
10388exampleListUsers :: Om {} () Unit
10489exampleListUsers = do
105− users <- api.listUsers { limit: Just 10, offset: Just 0 }
90+ users <- api.listUsers (justifill { limit: 10, offset: 0 })
10691 log $ "Found " <> show (length users) <> " users"
10792
10893exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
@@ -115,7 +100,8 @@ exampleCreateUser = do
115100
116101exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
117102exampleUpdateUser = do
118− user <- api.updateUser { id: 42 }
103+ user <- api.updateUser
104+ { id: 42 }
119105 { name: "Alice Updated"
120106 , email: "alice.new@example.com"
121107 }
@@ -128,7 +114,7 @@ exampleDeleteUser = do
128114
129115exampleCreateUserAuth :: Om {} (badRequest :: ErrorMessage) Unit
130116exampleCreateUserAuth = do
131− user <- api.createUserAuth { authorization: "Bearer abc123" }
117+ user <- api.createUserAuth { authorization: BearerToken "abc123" }
132118 { name: "Alice"
133119 , email: "alice@example.com"
134120 }
test/Example.purs+15−10
@@ -1,45 +1,50 @@
11module Example where
22
3−import Yoga.HTTP.API.Route (GET, POST, DELETE, Route)
3+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")
42− { headers :: Record (authorization :: String), body :: JSON CreateUserRequest }
46+ "users"
47+ { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
4348 ( created :: { body :: User }
4449 , badRequest :: { body :: ErrorMessage }
4550 )
test/MakeRequest.Spec.purs+42−19
@@ -2,30 +2,53 @@ module MakeRequest.Spec where
22
33import Prelude
44
5+import Data.HTTP.Method as HTTP
6+import Data.Maybe (Maybe(..))
57import Data.Tuple.Nested ((/\))
68import JS.Fetch.Headers as Headers
79import Prim.RowList as RL
810import Test.Spec (Spec, describe, it)
911import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
1012import Type.Proxy (Proxy(..))
11−import Yoga.Fetch.Om (toHeaders)
13+import Yoga.Fetch.Om (FormData, PlainText, QUERY, toHeaders)
14+import Yoga.Fetch.Om.MakeRequest (encodeBody, encodingContentType, httpMethod)
15+import Yoga.HTTP.API.Route (BearerToken(..))
1216
1317spec :: Spec Unit
14−spec = describe "toHeaders" do
15− it "empty headers produces no entries" do
16− let hdrs = toHeaders (Proxy :: _ RL.Nil) {}
17− Headers.toArray hdrs `shouldEqual` []
18−
19− it "single header is included" do
20− let hdrs = toHeaders (Proxy :: _ (RL.Cons "authorization" String RL.Nil)) { authorization: "Bearer abc123" }
21− Headers.toArray hdrs `shouldEqual` [ "authorization" /\ "Bearer abc123" ]
22−
23− it "multiple headers are included" do
24− let
25− hdrs = toHeaders
26− (Proxy :: _ (RL.Cons "authorization" String (RL.Cons "x-custom" String RL.Nil)))
27− { authorization: "Bearer abc123", "x-custom": "value" }
28− arr = Headers.toArray hdrs
29− arr `shouldSatisfy` \a ->
30− a == [ "authorization" /\ "Bearer abc123", "x-custom" /\ "value" ]
31− || a == [ "x-custom" /\ "value", "authorization" /\ "Bearer abc123" ]
18+spec = do
19+ describe "MakeRequest" do
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"
32+
33+ describe "toHeaders" do
34+ it "empty headers produces no entries" do
35+ let hdrs = toHeaders (Proxy :: _ RL.Nil) {}
36+ Headers.toArray hdrs `shouldEqual` []
37+
38+ it "single header is included" do
39+ let hdrs = toHeaders (Proxy :: _ (RL.Cons "authorization" String RL.Nil)) { authorization: "Bearer abc123" }
40+ Headers.toArray hdrs `shouldEqual` [ "authorization" /\ "Bearer abc123" ]
41+
42+ it "multiple headers are included" do
43+ let
44+ hdrs = toHeaders
45+ (Proxy :: _ (RL.Cons "authorization" String (RL.Cons "x-custom" String RL.Nil)))
46+ { authorization: "Bearer abc123", "x-custom": "value" }
47+ arr = Headers.toArray hdrs
48+ arr `shouldSatisfy` \a ->
49+ a == [ "authorization" /\ "Bearer abc123", "x-custom" /\ "value" ]
50+ || a == [ "x-custom" /\ "value", "authorization" /\ "Bearer abc123" ]
51+
52+ it "typed header values are rendered with HeaderValue" do
53+ let hdrs = toHeaders (Proxy :: _ (RL.Cons "authorization" BearerToken RL.Nil)) { authorization: BearerToken "abc123" }
54+ Headers.toArray hdrs `shouldEqual` [ "authorization" /\ "Bearer abc123" ]
test/PlainTextResponse.Spec.pursnew file+122
@@ -0,0 +1,122 @@
1+module PlainTextResponse.Spec where
2+
3+import Prelude
4+import Data.String as String
5+
6+import Effect.Aff (Aff, bracket)
7+import Effect.Aff.Class (liftAff)
8+import Effect.Class (liftEffect)
9+import Effect.Ref (Ref)
10+import Effect.Exception as Exn
11+import Effect.Ref as Ref
12+import Foreign (unsafeToForeign)
13+import Test.Spec (Spec, around, describe, it)
14+import Test.Spec.Assertions (shouldEqual)
15+import Yoga.Fastify.Fastify as F
16+import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
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
20+import Yoga.Om (Om, runOm)
21+import Yoga.Om.Strom (Strom)
22+import Yoga.Om.Strom as Strom
23+
24+type Payload = { value :: Int }
25+type Result = { doubled :: Int }
26+
27+type TestAPI =
28+ { double ::
29+ Route POST
30+ ("plain-text-test" / "double")
31+ { body :: JSON Payload }
32+ ( ok :: { body :: Result }
33+ , badRequest :: { body :: PlainText }
34+ )
35+ , health :: Route GET ("plain-text-test" / "health") {} (ok :: { body :: PlainText })
36+ , stream :: Route GET ("plain-text-test" / "stream") {} (ok :: { body :: Streaming String })
37+ }
38+
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+ }
45+api = client @TestAPI "http://localhost:44933"
46+
47+spec :: Spec Unit
48+spec = around withServer $ describe "String body on error responses" do
49+ it "200 OK decodes the JSON response body" \_ -> do
50+ result <- runOm {}
51+ { exception: \_ -> pure { doubled: -1 }
52+ , badRequest: \_ -> pure { doubled: -1 }
53+ }
54+ (api.double { value: 21 })
55+ liftAff $ result `shouldEqual` { doubled: 42 }
56+
57+ it "400 badRequest delivers the raw text body as a String" \serverMode -> do
58+ liftEffect $ Ref.write "badRequest" serverMode
59+ captured <- liftEffect $ Ref.new ""
60+ runOm {}
61+ { exception: \_ -> pure unit
62+ , badRequest: \b -> liftEffect $ Ref.write b captured
63+ }
64+ (void $ api.double { value: 0 })
65+ body <- liftEffect $ Ref.read captured
66+ liftAff $ body `shouldEqual` "invalid value"
67+
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+
90+withServer :: forall a. (Ref String -> Aff a) -> Aff a
91+withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
92+ where
93+ acquire = do
94+ serverMode <- liftEffect $ Ref.new "ok"
95+ fastify <- liftEffect do
96+ f <- F.fastify {}
97+ omApp <- createOmFastify {} f
98+ 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
101+ pure f
102+ void $ F.listen { port: Port 44933, host: Host "0.0.0.0" } fastify
103+ pure { fastify, serverMode }
104+
105+ release { fastify } = F.close fastify
106+
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+
112+ handler serverMode reply = do
113+ mode <- liftEffect $ Ref.read serverMode
114+ case mode of
115+ "badRequest" -> do
116+ void $ F.status (StatusCode 400) reply # liftEffect
117+ void $ F.header "content-type" "text/plain" reply # liftEffect
118+ F.send (unsafeToForeign "invalid value") reply # liftAff
119+ _ -> do
120+ void $ F.status (StatusCode 200) reply # liftEffect
121+ void $ F.header "content-type" "application/json" reply # liftEffect
122+ F.sendJson (unsafeToForeign { doubled: 42 }) reply # liftAff
test/Polymorphic.Spec.purs+21−19
@@ -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
@@ -22,32 +23,33 @@ type TestAPI =
2223 { getUser :: GetUserRoute
2324 }
2425
25−api :: forall ctx err. { getUser :: { id :: Int } -> Om ctx err User }
2626api = client @TestAPI "http://localhost:44931"
2727
2828spec :: Spec Unit
2929spec = around withServer $ describe "polymorphic client" do
3030 it "fetches user with { token :: String } context" \_ ->
31− runOm { token: "secret123" } { exception: \_ -> pure unit } do
32− ctx <- ask
33− user <- api.getUser { id: 42 }
34− 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"
3535
3636 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
37− runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
38− ctx <- ask
39− user <- api.getUser { id: ctx.userId }
40− 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"
4141
4242 it "same api.getUser works in two different contexts" \_ -> do
43− runOm { token: "tok1" } { exception: \_ -> pure unit } do
44− ctx <- ask
45− user <- api.getUser { id: 1 }
46− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "tok1:User 1"
47− runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
48− ctx <- ask
49− user <- api.getUser { id: 2 }
50− 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"
5153
5254withServer :: forall a. (Unit -> _ a) -> _ a
5355withServer test = bracket acquire release (\_ -> test unit)
@@ -55,7 +57,7 @@ withServer test = bracket acquire release (\_ -> test unit)
5557 acquire = do
5658 fastify <- liftEffect do
5759 f <- F.fastify {}
58− registerAPI { getUser: getUserHandler } f
60+ registerAPI @TestAPI { getUser: getUserHandler } f
5961 pure f
6062 void $ F.listen { port: Port 44931, host: Host "0.0.0.0" } fastify
6163 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` "€"