asphodelos 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +199 -0
- package/dist/core-DS3sAabb.mjs +4018 -0
- package/dist/format-B0W7HIAg.mjs +48 -0
- package/dist/index.d.mts +993 -0
- package/dist/index.mjs +202 -0
- package/dist/openapi-B4aAnx4P.mjs +40 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/shared-CrbXsFUJ.mjs +221 -0
- package/dist/vite-plugin/index.d.mts +11 -0
- package/dist/vite-plugin/index.mjs +336 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 - present, Asphodelos Contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# Asphodelos
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**[Asphodelos](https://www.npmjs.com/package/asphodelos)** generates type-safe [Elysia](https://elysiajs.com/) code from [OpenAPI](https://www.openapis.org/) / [TypeSpec](https://typespec.io/) specifications.
|
|
6
|
+
|
|
7
|
+
- OpenAPI schemas to [TypeBox](https://github.com/sinclairzx81/typebox) schemas (via Elysia's `t`)
|
|
8
|
+
- Elysia routes with per-route validation
|
|
9
|
+
- App entry point + per-resource modules (controller / service / model)
|
|
10
|
+
- TypeBox component bundles (schemas, responses, parameters, …)
|
|
11
|
+
- Eden Treaty wrappers and a self-contained `App` type
|
|
12
|
+
- Client library hooks (SWR, TanStack Query, Preact Query, Solid Query, Vue Query, Svelte Query, Angular Query)
|
|
13
|
+
- `bun:test` tests and a mock server
|
|
14
|
+
|
|
15
|
+
Asphodelos targets the [Bun](https://bun.sh/) runtime.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add -D asphodelos
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bunx asphodelos path/to/input.{yaml,json,tsp} -o path/to/output.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Configuration File
|
|
32
|
+
|
|
33
|
+
Create `asphodelos.config.ts`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { defineConfig } from 'asphodelos'
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
input: 'openapi.yaml',
|
|
40
|
+
output: 'src/index.ts', // default
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bunx asphodelos
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### CLI Reference
|
|
49
|
+
|
|
50
|
+
`asphodelos --help`:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
DESCRIPTION
|
|
54
|
+
Generate Elysia code from OpenAPI or TypeSpec
|
|
55
|
+
|
|
56
|
+
USAGE
|
|
57
|
+
asphodelos [flags] [<input>]
|
|
58
|
+
|
|
59
|
+
ARGUMENTS
|
|
60
|
+
input input.{yaml,json,tsp} OpenAPI (.yaml, .json) or TypeSpec (.tsp) document to generate from (optional)
|
|
61
|
+
|
|
62
|
+
FLAGS
|
|
63
|
+
--output, -o output.ts TypeScript file the generated app is written to
|
|
64
|
+
--config, -c file Config file to run (default: ./asphodelos.config.ts)
|
|
65
|
+
--watch, -w Rerun the config on every change to its documents or itself
|
|
66
|
+
|
|
67
|
+
GLOBAL FLAGS
|
|
68
|
+
--help, -h Show help information
|
|
69
|
+
--version, -v Show version information
|
|
70
|
+
--wizard Start wizard mode for a command
|
|
71
|
+
--completions <bash|zsh|fish|sh> Print shell completion script (choices: bash, zsh, fish, sh)
|
|
72
|
+
--log-level <all|trace|debug|info|warn|warning|error|fatal|none> Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
|
|
73
|
+
|
|
74
|
+
EXAMPLES
|
|
75
|
+
# Generate a single app from one document
|
|
76
|
+
asphodelos openapi.yaml -o src/index.ts
|
|
77
|
+
|
|
78
|
+
# Run every generator declared in ./asphodelos.config.ts
|
|
79
|
+
asphodelos
|
|
80
|
+
|
|
81
|
+
# Run a config file from another location
|
|
82
|
+
asphodelos --config config/api.config.ts
|
|
83
|
+
|
|
84
|
+
# Rerun on every change to the input documents or the config
|
|
85
|
+
asphodelos --watch
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
With an `<input>` the CLI generates one app and ignores any config file. With no `<input>` it
|
|
89
|
+
runs the config file.
|
|
90
|
+
|
|
91
|
+
### Watch Mode
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bunx asphodelos --watch
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Reruns the config on every change to the input documents or to the config itself, and keeps
|
|
98
|
+
watching when a run fails. It cannot be combined with `<input>` / `--output`.
|
|
99
|
+
|
|
100
|
+
### Example
|
|
101
|
+
|
|
102
|
+
input:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
openapi: 3.1.0
|
|
106
|
+
info:
|
|
107
|
+
title: Asphodelos API
|
|
108
|
+
version: '1.0.0'
|
|
109
|
+
paths:
|
|
110
|
+
/elysia:
|
|
111
|
+
get:
|
|
112
|
+
summary: Welcome
|
|
113
|
+
operationId: welcome
|
|
114
|
+
responses:
|
|
115
|
+
'200':
|
|
116
|
+
description: OK
|
|
117
|
+
content:
|
|
118
|
+
application/json:
|
|
119
|
+
schema:
|
|
120
|
+
type: object
|
|
121
|
+
required: [message]
|
|
122
|
+
properties:
|
|
123
|
+
message:
|
|
124
|
+
type: string
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
output:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
src/
|
|
131
|
+
├── index.ts
|
|
132
|
+
└── modules/
|
|
133
|
+
└── elysia/
|
|
134
|
+
├── index.ts // controller
|
|
135
|
+
├── service.ts // abstract class for business logic
|
|
136
|
+
└── model.ts // TypeBox models
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// src/index.ts
|
|
141
|
+
import { Elysia } from 'elysia'
|
|
142
|
+
import { elysia } from './modules/elysia'
|
|
143
|
+
|
|
144
|
+
export const app = new Elysia().use(elysia)
|
|
145
|
+
|
|
146
|
+
if (import.meta.main) {
|
|
147
|
+
app.listen(3000)
|
|
148
|
+
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// src/modules/elysia/index.ts
|
|
154
|
+
import { Elysia } from 'elysia'
|
|
155
|
+
import { ElysiaModel } from './model'
|
|
156
|
+
|
|
157
|
+
export const elysia = new Elysia().get('/elysia', () => {}, {
|
|
158
|
+
response: { 200: ElysiaModel.welcomeResponse200 },
|
|
159
|
+
detail: { tags: [], summary: 'Welcome', operationId: 'welcome' },
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// src/modules/elysia/model.ts
|
|
165
|
+
import { t, type UnwrapSchema } from 'elysia'
|
|
166
|
+
|
|
167
|
+
export const ElysiaModel = { welcomeResponse200: t.Object({ message: t.String() }) } as const
|
|
168
|
+
|
|
169
|
+
export type ElysiaModel = { [k in keyof typeof ElysiaModel]: UnwrapSchema<(typeof ElysiaModel)[k]> }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Handlers are empty stubs: TypeScript flags each one whose response is non-void until you
|
|
173
|
+
implement it. Importing `app` never starts a server.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bun add elysia
|
|
177
|
+
bun run src/index.ts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Vite Plugin
|
|
181
|
+
|
|
182
|
+
Watches your OpenAPI spec and `asphodelos.config.ts` for changes, then auto-regenerates code on save.
|
|
183
|
+
|
|
184
|
+
Requires `asphodelos.config.ts` in your project root.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// vite.config.ts
|
|
188
|
+
import { asphodelosVite } from 'asphodelos/vite-plugin'
|
|
189
|
+
import { defineConfig } from 'vite'
|
|
190
|
+
|
|
191
|
+
export default defineConfig({
|
|
192
|
+
plugins: [asphodelosVite()],
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **What it watches**: `asphodelos.config.ts`, and every `.yaml` / `.json` / `.tsp` in the
|
|
197
|
+
directory of `input` — a `$ref` or a TypeSpec import can reach a sibling file.
|
|
198
|
+
- **When it regenerates**: a config save always regenerates. A document save regenerates only when
|
|
199
|
+
the documents' contents changed, or a generated file has gone missing; a save with nothing new in
|
|
200
|
+
it is skipped.
|
|
201
|
+
- **When the browser reloads**: only when a generated file actually changed.
|
|
202
|
+
- **Cleanup**: an output that the config or the document no longer produces is removed. The app
|
|
203
|
+
entry, `modules/` and the generated tests are never removed, because they hold your code.
|
|
204
|
+
- A config that fails to load is reported and the previous one stays in effect; the next save
|
|
205
|
+
retries, so a typo never needs a restart. Every run is queued, so two never overlap.
|
|
206
|
+
|
|
207
|
+
## Eden Treaty Integration
|
|
208
|
+
|
|
209
|
+
### Type-Only Distribution
|
|
210
|
+
|
|
211
|
+
Emit a self-contained `export type App` so [Eden](https://elysiajs.com/eden/treaty/overview.html)
|
|
212
|
+
clients can call `treaty<App>(...)` without importing the runtime app.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
export default defineConfig({
|
|
216
|
+
input: 'openapi.yaml',
|
|
217
|
+
types: { output: 'src/types.ts' },
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Wrapper Functions
|
|
222
|
+
|
|
223
|
+
Generate one wrapper per operation over a Treaty client you supply.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
export default defineConfig({
|
|
227
|
+
input: 'openapi.yaml',
|
|
228
|
+
eden: {
|
|
229
|
+
output: 'src/eden.ts',
|
|
230
|
+
import: './lib', // module exporting `client` = treaty<App>(...)
|
|
231
|
+
client: 'client',
|
|
232
|
+
docs: true, // JSDoc above each wrapper
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Client Library Integrations
|
|
238
|
+
|
|
239
|
+
Supported: SWR, TanStack Query, Preact Query, Solid Query, Vue Query, Svelte Query, Angular Query.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
export default defineConfig({
|
|
243
|
+
input: 'openapi.yaml',
|
|
244
|
+
'tanstack-query': {
|
|
245
|
+
output: './src/tanstack-query',
|
|
246
|
+
import: '../lib',
|
|
247
|
+
split: true,
|
|
248
|
+
client: 'client',
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
TanStack-family mutations also get a `<operation>MutationOptions()` factory, and SWR queries a
|
|
254
|
+
`useImmutable<Operation>` hook. The generated hooks are compiled and run against the real
|
|
255
|
+
libraries in [`test/`](test/README.md).
|
|
256
|
+
|
|
257
|
+
### Infinite Query (`x-pagination`)
|
|
258
|
+
|
|
259
|
+
Set `x-pagination: true` on a GET operation to generate infinite query hooks.
|
|
260
|
+
|
|
261
|
+
```yaml
|
|
262
|
+
paths:
|
|
263
|
+
/items:
|
|
264
|
+
get:
|
|
265
|
+
x-pagination: true
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The paging rules go in a `pagination` argument:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const items = useListItemsInfinite(undefined, {
|
|
272
|
+
initialPageParam: 0,
|
|
273
|
+
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
274
|
+
buildInit: (pageParam) => ({ query: { page: String(pageParam) } }),
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Vue Query takes only `buildInit` there, with `initialPageParam` / `getNextPageParam` in the third
|
|
279
|
+
argument. SWR takes `buildInit(pageIndex, previousPage)` and stops when it returns `null`.
|
|
280
|
+
|
|
281
|
+
## Test & Mock Generation
|
|
282
|
+
|
|
283
|
+
### Test Generation
|
|
284
|
+
|
|
285
|
+
Generates `bun:test` tests that call `app.handle(...)` on the real app: a success-status test per
|
|
286
|
+
operation, plus `401` / `404` tests when the spec declares them. They start red against the empty
|
|
287
|
+
handlers; re-running keeps your hand-written tests.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
export default defineConfig({
|
|
291
|
+
input: 'openapi.yaml',
|
|
292
|
+
test: {
|
|
293
|
+
output: 'src/app.test.ts', // or `split: true` for modules/<resource>/index.test.ts
|
|
294
|
+
pathAlias: '@/', // optional: import the app through a tsconfig alias
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Mock Server Generation
|
|
300
|
+
|
|
301
|
+
Generates a standalone Elysia server that answers every operation with a
|
|
302
|
+
[`@faker-js/faker`](https://fakerjs.dev/) mock of its success response. Secured operations that
|
|
303
|
+
declare a `401` answer it when the credential is missing, and path parameters answer a declared
|
|
304
|
+
`404` for the same sentinel values the generated tests send.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
export default defineConfig({
|
|
308
|
+
input: 'openapi.yaml',
|
|
309
|
+
mock: {
|
|
310
|
+
output: 'src/mock.ts',
|
|
311
|
+
delay: { min: 50, max: 200 },
|
|
312
|
+
locale: 'ja',
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Like [Prism](https://stoplight.io/open-source/prism), a request can pick any response or named
|
|
318
|
+
example the document declares with the `Prefer` header (or the `__code` / `__example` query):
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
curl -H 'Prefer: code=404' http://localhost:3000/orders/1 # the 404 response
|
|
322
|
+
curl -H 'Prefer: example=pending' http://localhost:3000/orders/1 # a named example
|
|
323
|
+
curl -H 'Prefer: code=404, example=gone' http://localhost:3000/orders/1
|
|
324
|
+
curl 'http://localhost:3000/orders/1?__code=503'
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A code falls back to its `4XX` range, then `default`. A code or example the operation does not
|
|
328
|
+
declare answers `500` with an `application/problem+json` body saying what is missing.
|
|
329
|
+
|
|
330
|
+
## Full Config Reference
|
|
331
|
+
|
|
332
|
+
With `split: true`, `output` is a directory (one file per entry + `index.ts` barrel); otherwise it
|
|
333
|
+
is a single `.ts` file. `components.output` and the per-type components are mutually exclusive.
|
|
334
|
+
|
|
335
|
+
A split directory belongs to the generator: every run empties its `.ts` files before refilling it,
|
|
336
|
+
so an entry that leaves the document does not leave an orphaned file behind. Subdirectories, other
|
|
337
|
+
files and the single-file outputs of other generators are left alone.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { defineConfig } from 'asphodelos'
|
|
341
|
+
|
|
342
|
+
export default defineConfig({
|
|
343
|
+
input: 'openapi.yaml',
|
|
344
|
+
|
|
345
|
+
output: 'src/index.ts', // app entry; its directory holds modules/ and components/
|
|
346
|
+
prefix: '/api/v3', // new Elysia({ prefix })
|
|
347
|
+
port: '3000',
|
|
348
|
+
integration: false, // true: no .listen(), a host framework owns the server
|
|
349
|
+
pathAlias: false, // true: `@/` imports between generated files
|
|
350
|
+
readonly: false, // wrap top-level schemas in t.Readonly(...)
|
|
351
|
+
// format: {}, // oxfmt FormatConfig
|
|
352
|
+
|
|
353
|
+
// `exportTypes` adds `Static<typeof XSchema>` aliases.
|
|
354
|
+
components: {
|
|
355
|
+
// output: 'src/components.ts', // single-file mode
|
|
356
|
+
|
|
357
|
+
schemas: {
|
|
358
|
+
output: 'src/components/schemas',
|
|
359
|
+
split: true,
|
|
360
|
+
import: '../schemas',
|
|
361
|
+
exportTypes: true,
|
|
362
|
+
},
|
|
363
|
+
responses: {
|
|
364
|
+
output: 'src/components/responses',
|
|
365
|
+
split: true,
|
|
366
|
+
import: '../responses',
|
|
367
|
+
exportTypes: true,
|
|
368
|
+
},
|
|
369
|
+
parameters: {
|
|
370
|
+
output: 'src/components/parameters',
|
|
371
|
+
split: true,
|
|
372
|
+
import: '../parameters',
|
|
373
|
+
exportTypes: true,
|
|
374
|
+
},
|
|
375
|
+
requestBodies: {
|
|
376
|
+
output: 'src/components/requestBodies',
|
|
377
|
+
split: true,
|
|
378
|
+
import: '../requestBodies',
|
|
379
|
+
exportTypes: true,
|
|
380
|
+
},
|
|
381
|
+
headers: {
|
|
382
|
+
output: 'src/components/headers',
|
|
383
|
+
split: true,
|
|
384
|
+
import: '../headers',
|
|
385
|
+
exportTypes: true,
|
|
386
|
+
},
|
|
387
|
+
mediaTypes: {
|
|
388
|
+
output: 'src/components/mediaTypes',
|
|
389
|
+
split: true,
|
|
390
|
+
import: '../mediaTypes',
|
|
391
|
+
exportTypes: true,
|
|
392
|
+
},
|
|
393
|
+
examples: {
|
|
394
|
+
output: 'src/components/examples',
|
|
395
|
+
split: true,
|
|
396
|
+
import: '../examples',
|
|
397
|
+
},
|
|
398
|
+
securitySchemes: {
|
|
399
|
+
output: 'src/components/securitySchemes',
|
|
400
|
+
split: true,
|
|
401
|
+
import: '../securitySchemes',
|
|
402
|
+
},
|
|
403
|
+
links: {
|
|
404
|
+
output: 'src/components/links',
|
|
405
|
+
split: true,
|
|
406
|
+
import: '../links',
|
|
407
|
+
},
|
|
408
|
+
callbacks: {
|
|
409
|
+
output: 'src/components/callbacks',
|
|
410
|
+
split: true,
|
|
411
|
+
import: '../callbacks',
|
|
412
|
+
},
|
|
413
|
+
pathItems: {
|
|
414
|
+
output: 'src/components/pathItems',
|
|
415
|
+
split: true,
|
|
416
|
+
import: '../pathItems',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
types: {
|
|
421
|
+
output: 'src/types.ts',
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
eden: {
|
|
425
|
+
output: 'src/eden.ts',
|
|
426
|
+
import: './lib',
|
|
427
|
+
client: 'client',
|
|
428
|
+
docs: false,
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
test: {
|
|
432
|
+
split: true, // false: a single file at `output`
|
|
433
|
+
pathAlias: '@/',
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
mock: {
|
|
437
|
+
output: 'src/mock.ts',
|
|
438
|
+
useExamples: true, // true: response examples | 'all': also schema/property examples | false
|
|
439
|
+
locale: 'en', // @faker-js/faker/locale/<locale>
|
|
440
|
+
// seed: 42, // optional: same body per route on every request (snapshot-friendly)
|
|
441
|
+
delay: false, // ms, { min, max }, or false
|
|
442
|
+
arrayMin: 1, // array length when the schema sets no minItems / maxItems
|
|
443
|
+
arrayMax: 5,
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
swr: {
|
|
447
|
+
output: 'src/swr',
|
|
448
|
+
import: '../lib',
|
|
449
|
+
split: true,
|
|
450
|
+
client: 'client',
|
|
451
|
+
},
|
|
452
|
+
'tanstack-query': {
|
|
453
|
+
output: 'src/tanstack-query',
|
|
454
|
+
import: '../lib',
|
|
455
|
+
split: true,
|
|
456
|
+
client: 'client',
|
|
457
|
+
},
|
|
458
|
+
'preact-query': {
|
|
459
|
+
output: 'src/preact-query',
|
|
460
|
+
import: '../lib',
|
|
461
|
+
split: true,
|
|
462
|
+
client: 'client',
|
|
463
|
+
},
|
|
464
|
+
'solid-query': {
|
|
465
|
+
output: 'src/solid-query',
|
|
466
|
+
import: '../lib',
|
|
467
|
+
split: true,
|
|
468
|
+
client: 'client',
|
|
469
|
+
},
|
|
470
|
+
'vue-query': {
|
|
471
|
+
output: 'src/vue-query',
|
|
472
|
+
import: '../lib',
|
|
473
|
+
split: true,
|
|
474
|
+
client: 'client',
|
|
475
|
+
},
|
|
476
|
+
'svelte-query': {
|
|
477
|
+
output: 'src/svelte-query',
|
|
478
|
+
import: '../lib',
|
|
479
|
+
split: true,
|
|
480
|
+
client: 'client',
|
|
481
|
+
},
|
|
482
|
+
'angular-query': {
|
|
483
|
+
output: 'src/angular-query',
|
|
484
|
+
import: '../lib',
|
|
485
|
+
split: true,
|
|
486
|
+
client: 'client',
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Vendor Extensions (x-\*)
|
|
492
|
+
|
|
493
|
+
### Custom Validation Error Messages
|
|
494
|
+
|
|
495
|
+
Attach a custom error message with `x-<jsonSchemaKeyword>-message`, one extension per keyword.
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
name:
|
|
499
|
+
type: string
|
|
500
|
+
minLength: 3
|
|
501
|
+
maxLength: 20
|
|
502
|
+
x-error-message: 'name must be a string'
|
|
503
|
+
x-minLength-message: 'name must be at least 3 characters'
|
|
504
|
+
x-maxLength-message: 'name must be at most 20 characters'
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
| Group | Extensions |
|
|
508
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
509
|
+
| Common | `x-error-message`, `x-required-message`†, `x-const-message`, `x-enum-message` |
|
|
510
|
+
| Numeric | `x-minimum-message`, `x-maximum-message`, `x-exclusiveMinimum-message`, `x-exclusiveMaximum-message`, `x-multipleOf-message` |
|
|
511
|
+
| String | `x-minLength-message`, `x-maxLength-message`, `x-pattern-message`, `x-length-message` |
|
|
512
|
+
| Array | `x-minItems-message`, `x-maxItems-message`, `x-uniqueItems-message`, `x-contains-message`, `x-minContains-message`, `x-maxContains-message`, `x-prefixItems-message`, `x-items-message` |
|
|
513
|
+
| Object | `x-minProperties-message`, `x-maxProperties-message`, `x-additionalProperties-message`†, `x-propertyNames-message`, `x-patternProperties-message`†, `x-dependentRequired-message`, `x-dependentSchemas-message`, `x-properties-message`†, `x-unevaluatedProperties-message`†, `x-unevaluatedItems-message`† |
|
|
514
|
+
| Combinators | `x-allOf-message`‡, `x-anyOf-message`, `x-oneOf-message`, `x-not-message`, `x-implication-message` |
|
|
515
|
+
| Conditional | `x-if-message`†, `x-then-message`†, `x-else-message`† |
|
|
516
|
+
|
|
517
|
+
- † Kept on the schema, but Elysia 1.4 cannot route a 422 to it.
|
|
518
|
+
- ‡ Best-effort: a sibling error is usually reported first.
|
|
519
|
+
|
|
520
|
+
### Behavior Extensions
|
|
521
|
+
|
|
522
|
+
| Extension | Effect |
|
|
523
|
+
| -------------------------------------------- | ------------------------------------------------------------------ |
|
|
524
|
+
| `x-trim` | `t.Transform` that trims the string |
|
|
525
|
+
| `x-toLowerCase` / `x-lowercase` | `t.Transform` that lower-cases the string |
|
|
526
|
+
| `x-toUpperCase` / `x-uppercase` | `t.Transform` that upper-cases the string |
|
|
527
|
+
| `x-normalize` | `t.Transform` with `normalize('NFC' \| 'NFD' \| 'NFKC' \| 'NFKD')` |
|
|
528
|
+
| `x-startsWith` / `x-endsWith` / `x-includes` | Folded into `pattern` |
|
|
529
|
+
| `x-emailRegex` | Replaces `pattern` on `format: email` |
|
|
530
|
+
| `x-readonly` | `t.Readonly(...)` |
|
|
531
|
+
| `x-brand` | `t.Unsafe<T & { readonly __brand: 'Name' }>(...)` on primitives |
|
|
532
|
+
| `x-transform` | Replaces the schema with a raw `t.Transform(...)` expression |
|
|
533
|
+
|
|
534
|
+
String transforms run **after** validation, so write `minLength` / `pattern` against the value as
|
|
535
|
+
it arrives, not as it looks after trimming or case-folding.
|
|
536
|
+
|
|
537
|
+
```yaml
|
|
538
|
+
UpdatedAt:
|
|
539
|
+
type: string
|
|
540
|
+
format: date-time
|
|
541
|
+
x-transform: >-
|
|
542
|
+
t.Transform(t.String()).Decode((v) => new Date(v)).Encode((v) => v.toISOString())
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
> **⚠️ Security:** `x-transform` is emitted verbatim and runs when the generated module is
|
|
546
|
+
> imported. Only generate from specs you author or fully trust.
|
|
547
|
+
|
|
548
|
+
`Decode` runs on requests and `Encode` on responses (only when the response declares a schema).
|
|
549
|
+
Keep query / path / header transforms on a `string` base. Eden types the decoded value, but the
|
|
550
|
+
wire carries the encoded one.
|
|
551
|
+
|
|
552
|
+
`x-uuidVersion`, `x-urlHostname`, `x-urlProtocol`, `x-urlNormalize`, `x-isoPrecision`,
|
|
553
|
+
`x-isoOffset`, `x-isoLocal`, `x-macDelimiter`, `x-jwtAlg`, `x-hashAlg` and `x-hashEnc` are
|
|
554
|
+
round-tripped through OpenAPI but emit no code.
|
|
555
|
+
|
|
556
|
+
### Coercion Formats
|
|
557
|
+
|
|
558
|
+
| `format` | Generates |
|
|
559
|
+
| ---------------- | ------------------- |
|
|
560
|
+
| `numeric` | `t.Numeric()` |
|
|
561
|
+
| `boolean-string` | `t.BooleanString()` |
|
|
562
|
+
|
|
563
|
+
Elysia already coerces query / path / header / cookie values and JSON bodies, so these only make
|
|
564
|
+
the intent explicit in a body. Do not add them to parameters.
|
|
565
|
+
|
|
566
|
+
### Unsupported Extensions
|
|
567
|
+
|
|
568
|
+
`x-refine`, `x-superRefine`, `x-prefault` (use `default`), `x-decode` / `x-encode` (use
|
|
569
|
+
`x-transform`), `x-cookie-secrets`, `x-unionEnum`, `x-composite` and `x-form` are not supported.
|
|
570
|
+
|
|
571
|
+
## Contributing
|
|
572
|
+
|
|
573
|
+
We welcome feedback and contributions!
|
|
574
|
+
|
|
575
|
+
- Open an issue at [GitHub Issues](https://github.com/nakita628/asphodelos/issues)
|
|
576
|
+
- Submit a pull request with your improvements
|
|
577
|
+
|
|
578
|
+
Lint and tests run from the repository root:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
bun run check # format check, lint, type check, tests, then the client suite
|
|
582
|
+
bun run fix # autofix formatting, oxlint, markdownlint and textlint
|
|
583
|
+
bun run lint # oxlint, markdownlint, textlint, cspell, secretlint, actionlint
|
|
584
|
+
bun run test # unit tests
|
|
585
|
+
bun run test:clients # build, then compile and run the generated hooks under test/
|
|
586
|
+
bun run test:pack # build, pack, and install the tarball with npm into an empty project
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Coding agents: start with [AGENTS.md](AGENTS.md).
|
|
590
|
+
|
|
591
|
+
## License
|
|
592
|
+
|
|
593
|
+
Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/asphodelos?tab=MIT-1-ov-file) for more information.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|