nitro-openapi-schemas 2.0.0
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 +184 -0
- package/dist/h3.d.mts +36 -0
- package/dist/h3.mjs +27 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.mjs +30 -0
- package/dist/runtime/generator.mjs +190 -0
- package/dist/runtime/route.mjs +13 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oumar Barry
|
|
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,184 @@
|
|
|
1
|
+
# nitro-openapi-schemas
|
|
2
|
+
|
|
3
|
+
[](https://github.com/oumarbarry/nitro-openapi-schemas/actions/workflows/ci.yml)
|
|
4
|
+
[](https://npmjs.com/package/nitro-openapi-schemas/v/nitro-v2)
|
|
5
|
+
[](https://github.com/oumarbarry/nitro-openapi-schemas/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Schema-driven OpenAPI for [Nitro](https://nitro.build). The Zod, Valibot or
|
|
8
|
+
ArkType schema that validates a route at runtime also documents it: every
|
|
9
|
+
handler with `validate` schemas or `meta.openAPI` ends up in `/_openapi.json`,
|
|
10
|
+
with a Scalar UI at `/_scalar`. Nothing is declared twice and nothing is
|
|
11
|
+
extracted from source at build time.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Zod / Valibot / ArkType schema
|
|
15
|
+
→ defineValidatedHandler validates the request
|
|
16
|
+
→ /_openapi.json OpenAPI 3.1, generated from the same schemas
|
|
17
|
+
→ openapi.json written after `nitro build`, ready for SDK codegen
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- This is the Nitro v2 and Nuxt 4 line: `nitropack` 2.x and h3 1.x, published
|
|
21
|
+
as 2.x under the `nitro-v2` dist-tag from the branch of the same name. Nitro v3 users
|
|
22
|
+
want the [`main` branch](https://github.com/oumarbarry/nitro-openapi-schemas/tree/main) and the `latest` tag.
|
|
23
|
+
- Zod 4.2+, ArkType 2.1.28+, and Valibot through `@valibot/to-json-schema`
|
|
24
|
+
1.5+, via [Standard JSON Schema](https://github.com/standard-schema/standard-schema/pull/134).
|
|
25
|
+
Bring the one you use; none is installed for you.
|
|
26
|
+
- Named schemas become `components/schemas` entries, emitted once and
|
|
27
|
+
referenced by `$ref` wherever they appear.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bun add nitro-openapi-schemas@nitro-v2 # or npm, pnpm
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Register the module and, optionally, the `info` block of the spec:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// nitro.config.ts
|
|
41
|
+
import { defineNitroConfig } from "nitropack/config";
|
|
42
|
+
import openAPISchemas from "nitro-openapi-schemas";
|
|
43
|
+
|
|
44
|
+
export default defineNitroConfig({
|
|
45
|
+
modules: [openAPISchemas],
|
|
46
|
+
openAPISchemas: {
|
|
47
|
+
info: { title: "Payments API", version: "1.0.0" },
|
|
48
|
+
// route: "/_openapi.json", // default
|
|
49
|
+
// scalar: true, // default, Scalar UI at /_scalar
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// nuxt.config.ts (Nuxt 4): same options, nested under `nitro`
|
|
56
|
+
import openAPISchemas from "nitro-openapi-schemas";
|
|
57
|
+
|
|
58
|
+
export default defineNuxtConfig({
|
|
59
|
+
nitro: {
|
|
60
|
+
modules: [openAPISchemas],
|
|
61
|
+
openAPISchemas: { info: { title: "Payments API", version: "1.0.0" } },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then write routes with `defineValidatedHandler` from `nitro-openapi-schemas/h3`.
|
|
67
|
+
It validates `query`, `headers` and `body` through each schema's own
|
|
68
|
+
`~standard.validate` (400 with the issues in `data` on failure, body skipped
|
|
69
|
+
for GET and HEAD) and documents them. `meta.openAPI` is merged into the
|
|
70
|
+
operation, and a `schema` on a response is converted too:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// routes/api/payments/index.post.ts (Nuxt: server/api/payments/index.post.ts)
|
|
74
|
+
import { readBody } from "h3";
|
|
75
|
+
import { defineValidatedHandler } from "nitro-openapi-schemas/h3";
|
|
76
|
+
import { createPaymentSchema, paymentSchema } from "../../shared/schema.ts";
|
|
77
|
+
|
|
78
|
+
export default defineValidatedHandler({
|
|
79
|
+
validate: { body: createPaymentSchema },
|
|
80
|
+
meta: {
|
|
81
|
+
openAPI: {
|
|
82
|
+
tags: ["payments"],
|
|
83
|
+
summary: "Create a payment",
|
|
84
|
+
responses: { 200: { description: "Payment created", schema: paymentSchema } },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
handler: async (event) => {
|
|
88
|
+
const body = await readBody(event); // already validated
|
|
89
|
+
// ...
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`readBody` returns the parsed body as it was sent, not the schema's output, so
|
|
95
|
+
defaults and coercions declared in the schema are not applied to it. Start the
|
|
96
|
+
server and open `/_scalar`, or fetch `/_openapi.json`. Path parameters are
|
|
97
|
+
documented from the route pattern, so `defineValidatedHandler` with only `meta`
|
|
98
|
+
is enough for a route without validation.
|
|
99
|
+
|
|
100
|
+
### Naming components
|
|
101
|
+
|
|
102
|
+
A schema with an id is hoisted into `components/schemas` under that id and
|
|
103
|
+
referenced by `$ref` everywhere it appears, nested schemas included. Each
|
|
104
|
+
library has its own way to set it:
|
|
105
|
+
|
|
106
|
+
| Library | Named schema |
|
|
107
|
+
| ------- | ---------------------------------------------------------- |
|
|
108
|
+
| Zod | `z.object({ ... }).meta({ id: "Payment" })` |
|
|
109
|
+
| ArkType | `type({ ... }).configure({ id: "Payment" })` |
|
|
110
|
+
| Valibot | `v.pipe(v.object({ ... }), v.metadata({ id: "Payment" }))` |
|
|
111
|
+
|
|
112
|
+
Anonymous schemas are inlined. A query or header schema is flattened into one
|
|
113
|
+
parameter per property.
|
|
114
|
+
|
|
115
|
+
### The spec as a build artifact
|
|
116
|
+
|
|
117
|
+
The spec is generated at runtime from the handlers in the bundle, so getting it
|
|
118
|
+
as a file means booting the built server once and saving the response.
|
|
119
|
+
[examples/nitro/scripts/emit-openapi.mjs](examples/nitro/scripts/emit-openapi.mjs)
|
|
120
|
+
does exactly that, and `openapi-typescript` turns the result into a typed
|
|
121
|
+
client:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
bun run openapi # nitro build, boot the output, write openapi.json
|
|
125
|
+
bun run sdk # openapi-typescript openapi.json -o sdk.d.ts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Nitro v3
|
|
129
|
+
|
|
130
|
+
The `latest` dist-tag targets Nitro v3 and h3 v2, where `defineValidatedHandler`
|
|
131
|
+
comes from `nitro/h3` itself. Install with `bun add nitro-openapi-schemas` and
|
|
132
|
+
read the [`main` branch](https://github.com/oumarbarry/nitro-openapi-schemas) README.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
The module emits a virtual module that imports every scanned route handler
|
|
137
|
+
directly, bypassing Nitro's lazy wrappers. `defineValidatedHandler` from
|
|
138
|
+
`nitro-openapi-schemas/h3` assigns the `validate` schemas and `meta` onto the
|
|
139
|
+
handler function, the same contract h3 v2 provides natively, so the spec route
|
|
140
|
+
can read those live objects, convert each schema through `~standard.jsonSchema`
|
|
141
|
+
(or wrap it with `@valibot/to-json-schema` when the library does not expose
|
|
142
|
+
that yet), hoist named schemas and Zod's nested `$defs` into
|
|
143
|
+
`components/schemas`, and cache the document after the first request.
|
|
144
|
+
|
|
145
|
+
The trade-off: routes imported by the spec route are no longer lazy-loaded.
|
|
146
|
+
Nuxt's catch-all renderer (`/**`) is left out of the import for that reason.
|
|
147
|
+
|
|
148
|
+
## Limitations
|
|
149
|
+
|
|
150
|
+
- JSON request bodies only. Cookie parameters and other content types are not
|
|
151
|
+
described.
|
|
152
|
+
- One HTTP method per route file, as in Nitro's file routing.
|
|
153
|
+
- Header parameter names are emitted as written in the schema, without case
|
|
154
|
+
normalization.
|
|
155
|
+
- The document is cached after the first request. In dev, restart the server
|
|
156
|
+
after changing a schema in a route that was already loaded.
|
|
157
|
+
- Response schemas go in `meta.openAPI.responses[status].schema`, since h3's
|
|
158
|
+
`validate` has no `response` field.
|
|
159
|
+
|
|
160
|
+
## Background
|
|
161
|
+
|
|
162
|
+
This module is the implementation behind
|
|
163
|
+
[nitrojs/nitro#4402](https://github.com/nitrojs/nitro/discussions/4402), a
|
|
164
|
+
proposal to generate OpenAPI from Standard Schema validators inside Nitro
|
|
165
|
+
itself (see also [#2974](https://github.com/nitrojs/nitro/issues/2974) and
|
|
166
|
+
[#3542](https://github.com/nitrojs/nitro/issues/3542)). Until that lands, it
|
|
167
|
+
works as a standalone module.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
bun install && bun install --force # the second install links the built dist/ into the examples
|
|
173
|
+
bun run dev # example app at http://localhost:3000/_scalar
|
|
174
|
+
bun run check # oxlint + oxfmt
|
|
175
|
+
bun run build # obuild, writes dist/
|
|
176
|
+
bun run openapi # end-to-end: build examples/nitro and write its openapi.json
|
|
177
|
+
bun run openapi:nuxt4 # same for examples/nuxt4
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
package/dist/h3.d.mts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventHandler, EventHandlerObject, EventHandlerRequest, EventHandlerResponse } from "h3";
|
|
2
|
+
interface StandardSchema {
|
|
3
|
+
"~standard": {
|
|
4
|
+
vendor: string;
|
|
5
|
+
version: number;
|
|
6
|
+
validate: (value: unknown) => {
|
|
7
|
+
value: unknown;
|
|
8
|
+
issues?: undefined;
|
|
9
|
+
} | {
|
|
10
|
+
issues: readonly unknown[];
|
|
11
|
+
} | Promise<{
|
|
12
|
+
value: unknown;
|
|
13
|
+
issues?: undefined;
|
|
14
|
+
} | {
|
|
15
|
+
issues: readonly unknown[];
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface ValidateSchemas {
|
|
20
|
+
body?: StandardSchema;
|
|
21
|
+
query?: StandardSchema;
|
|
22
|
+
headers?: StandardSchema;
|
|
23
|
+
}
|
|
24
|
+
interface HandlerMeta {
|
|
25
|
+
openAPI?: Record<string, any>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
type ValidatedHandler<Request extends EventHandlerRequest = EventHandlerRequest, Response = EventHandlerResponse> = EventHandler<Request, Response> & {
|
|
29
|
+
validate?: ValidateSchemas;
|
|
30
|
+
meta?: HandlerMeta;
|
|
31
|
+
};
|
|
32
|
+
declare function defineValidatedHandler<Request extends EventHandlerRequest = EventHandlerRequest, Response = EventHandlerResponse>(def: EventHandlerObject<Request, Response> & {
|
|
33
|
+
validate?: ValidateSchemas;
|
|
34
|
+
meta?: HandlerMeta;
|
|
35
|
+
}): ValidatedHandler<Request, Response>;
|
|
36
|
+
export { HandlerMeta, ValidateSchemas, ValidatedHandler, defineValidatedHandler };
|
package/dist/h3.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getQuery, getRequestHeaders, readBody } from "h3";
|
|
2
|
+
function defineValidatedHandler(def) {
|
|
3
|
+
const { validate, meta } = def;
|
|
4
|
+
const handler = defineEventHandler({
|
|
5
|
+
...def,
|
|
6
|
+
handler: async (event) => {
|
|
7
|
+
if (validate?.query) await validatePart(validate.query, getQuery(event), "query");
|
|
8
|
+
if (validate?.headers) await validatePart(validate.headers, getRequestHeaders(event), "headers");
|
|
9
|
+
if (validate?.body && event.method !== "GET" && event.method !== "HEAD") await validatePart(validate.body, await readBody(event), "body");
|
|
10
|
+
return def.handler(event);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return Object.assign(handler, {
|
|
14
|
+
validate,
|
|
15
|
+
meta
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function validatePart(schema, value, part) {
|
|
19
|
+
const result = await schema["~standard"].validate(value);
|
|
20
|
+
if (result.issues) throw createError({
|
|
21
|
+
status: 400,
|
|
22
|
+
statusMessage: "Validation Error",
|
|
23
|
+
message: `Invalid ${part}`,
|
|
24
|
+
data: result.issues
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export { defineValidatedHandler };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NitroModule } from "nitropack/types";
|
|
2
|
+
interface NitroOpenAPISchemasOptions {
|
|
3
|
+
/** Route serving the generated spec. Default: `/_openapi.json` */
|
|
4
|
+
route?: string;
|
|
5
|
+
/** Mount Nitro's Scalar UI at `/_scalar` on top of the spec. Default: true */
|
|
6
|
+
scalar?: boolean;
|
|
7
|
+
/** OpenAPI info object (title, version, description). */
|
|
8
|
+
info?: {
|
|
9
|
+
title?: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
declare module "nitropack/types" {
|
|
15
|
+
interface NitroConfig {
|
|
16
|
+
openAPISchemas?: NitroOpenAPISchemasOptions;
|
|
17
|
+
}
|
|
18
|
+
interface NitroOptions {
|
|
19
|
+
openAPISchemas?: NitroOpenAPISchemasOptions;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Nitro v2 / Nuxt 4 line.
|
|
24
|
+
*
|
|
25
|
+
* Emits a virtual module that imports every scanned route handler *directly*
|
|
26
|
+
* (bypassing lazy wrappers), so the spec route can read the live `validate`
|
|
27
|
+
* schemas and `meta` attached to the handler function at runtime. h3 v1 has
|
|
28
|
+
* no defineValidatedHandler, so this package ships its own shim (see ./h3.ts)
|
|
29
|
+
* that attaches them.
|
|
30
|
+
*/
|
|
31
|
+
declare const nitroOpenAPISchemas: NitroModule;
|
|
32
|
+
export { NitroOpenAPISchemasOptions, nitroOpenAPISchemas as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { runtimeDir } from "nitropack/runtime/meta";
|
|
4
|
+
const nitroOpenAPISchemas = {
|
|
5
|
+
name: "nitro-openapi-schemas",
|
|
6
|
+
setup(nitro) {
|
|
7
|
+
const options = nitro.options.openAPISchemas || {};
|
|
8
|
+
const specRoute = options.route || "/_openapi.json";
|
|
9
|
+
nitro.options.virtual["#nitro-openapi-schemas"] = () => {
|
|
10
|
+
const entries = [...nitro.scannedHandlers, ...nitro.options.handlers].filter((h) => h.route && !h.middleware && h.route !== specRoute && !h.route.startsWith("/_") && !h.route.includes("**") && typeof h.handler === "string");
|
|
11
|
+
const files = [...new Set(entries.map((h) => h.handler))];
|
|
12
|
+
return [
|
|
13
|
+
...files.map((file, i) => `import h${i} from ${JSON.stringify(file)};`),
|
|
14
|
+
`export const config = ${JSON.stringify({ info: options.info })};`,
|
|
15
|
+
"export const routes = [",
|
|
16
|
+
...entries.map((h) => ` { route: ${JSON.stringify(h.route)}, method: ${JSON.stringify((h.method || "get").toLowerCase())}, handler: h${files.indexOf(h.handler)} },`),
|
|
17
|
+
"];"
|
|
18
|
+
].join("\n");
|
|
19
|
+
};
|
|
20
|
+
nitro.options.handlers.push({
|
|
21
|
+
route: specRoute,
|
|
22
|
+
handler: fileURLToPath(new URL("runtime/route", import.meta.url))
|
|
23
|
+
});
|
|
24
|
+
if (options.scalar !== false) nitro.options.handlers.push({
|
|
25
|
+
route: "/_scalar",
|
|
26
|
+
handler: join(runtimeDir, "internal/routes/scalar")
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export { nitroOpenAPISchemas as default };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export async function toOpenAPIDocument(entries, opts = {}) {
|
|
2
|
+
const registry = new SchemaRegistry();
|
|
3
|
+
const paths = {};
|
|
4
|
+
for (const entry of entries) {
|
|
5
|
+
const { route, parameters } = normalizeRoute(entry.route);
|
|
6
|
+
const { validate, meta } = entry.handler;
|
|
7
|
+
const { responses: metaResponses, ...openAPI } = meta?.openAPI || {};
|
|
8
|
+
if (validate?.query) {
|
|
9
|
+
parameters.push(...await schemaToParameters(registry, validate.query, "query"));
|
|
10
|
+
}
|
|
11
|
+
if (validate?.headers) {
|
|
12
|
+
parameters.push(...await schemaToParameters(registry, validate.headers, "header"));
|
|
13
|
+
}
|
|
14
|
+
const operation = {
|
|
15
|
+
...parameters.length > 0 ? { parameters } : {},
|
|
16
|
+
responses: { 200: { description: "OK" } },
|
|
17
|
+
...openAPI
|
|
18
|
+
};
|
|
19
|
+
if (validate?.body) {
|
|
20
|
+
operation.requestBody = {
|
|
21
|
+
required: true,
|
|
22
|
+
content: { "application/json": { schema: await registry.toJsonSchema(validate.body, "input") } }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (metaResponses) {
|
|
26
|
+
operation.responses = {};
|
|
27
|
+
for (const [status, res] of Object.entries(metaResponses)) {
|
|
28
|
+
const { schema, ...rest } = res;
|
|
29
|
+
operation.responses[status] = isStandardSchema(schema) ? {
|
|
30
|
+
...rest,
|
|
31
|
+
content: { "application/json": { schema: await registry.toJsonSchema(schema, "output") } }
|
|
32
|
+
} : res;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
(paths[route] ??= {})[entry.method] = operation;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
openapi: "3.1.0",
|
|
39
|
+
info: {
|
|
40
|
+
title: "Nitro Server Routes",
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
...opts.info
|
|
43
|
+
},
|
|
44
|
+
servers: opts.servers,
|
|
45
|
+
paths,
|
|
46
|
+
...registry.hasComponents() ? { components: { schemas: registry.components } } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isStandardSchema(value) {
|
|
51
|
+
return !!value && typeof value === "object" && "~standard" in value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class SchemaRegistry {
|
|
55
|
+
components = {};
|
|
56
|
+
|
|
57
|
+
#seen = {
|
|
58
|
+
input: new Map(),
|
|
59
|
+
output: new Map()
|
|
60
|
+
};
|
|
61
|
+
hasComponents() {
|
|
62
|
+
return Object.keys(this.components).length > 0;
|
|
63
|
+
}
|
|
64
|
+
async toJsonSchema(schema, io) {
|
|
65
|
+
const seen = this.#seen[io];
|
|
66
|
+
const cached = seen.get(schema);
|
|
67
|
+
if (cached) return cached;
|
|
68
|
+
const { json, name } = await convert(schema, io);
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if (json.$defs) {
|
|
72
|
+
for (const [defName, defSchema] of Object.entries(json.$defs)) {
|
|
73
|
+
this.components[defName] ??= rewriteDefsRefs(defSchema);
|
|
74
|
+
}
|
|
75
|
+
delete json.$defs;
|
|
76
|
+
rewriteDefsRefs(json);
|
|
77
|
+
}
|
|
78
|
+
let result = json;
|
|
79
|
+
if (name) {
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
const finalName = io === "input" && this.components[name] ? `${name}Input` : name;
|
|
83
|
+
this.components[finalName] = json;
|
|
84
|
+
result = { $ref: `#/components/schemas/${finalName}` };
|
|
85
|
+
}
|
|
86
|
+
seen.set(schema, result);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
function schemaId(schema, vendor) {
|
|
94
|
+
switch (vendor) {
|
|
95
|
+
case "zod": {
|
|
96
|
+
return schema.meta?.()?.id;
|
|
97
|
+
}
|
|
98
|
+
case "arktype": {
|
|
99
|
+
return schema.meta?.id;
|
|
100
|
+
}
|
|
101
|
+
default: {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function rewriteDefsRefs(node) {
|
|
107
|
+
if (Array.isArray(node)) {
|
|
108
|
+
for (const item of node) rewriteDefsRefs(item);
|
|
109
|
+
} else if (node && typeof node === "object") {
|
|
110
|
+
if (typeof node.$ref === "string" && node.$ref.startsWith("#/$defs/")) {
|
|
111
|
+
node.$ref = node.$ref.replace("#/$defs/", "#/components/schemas/");
|
|
112
|
+
}
|
|
113
|
+
for (const value of Object.values(node)) rewriteDefsRefs(value);
|
|
114
|
+
}
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
async function convert(schema, io) {
|
|
118
|
+
const std = schema["~standard"];
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if (std.jsonSchema) {
|
|
123
|
+
const json = std.jsonSchema[io]({ target: "draft-2020-12" });
|
|
124
|
+
delete json.$schema;
|
|
125
|
+
return {
|
|
126
|
+
json,
|
|
127
|
+
name: schemaId(schema, std.vendor)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
const vendor = std.vendor;
|
|
134
|
+
switch (vendor) {
|
|
135
|
+
case "valibot": {
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// @valibot/to-json-schema@1.5+ ships toStandardJsonSchema(), a
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
const { toStandardJsonSchema } = await import("@valibot/" + "to-json-schema");
|
|
144
|
+
const name = schema.pipe?.find((a) => a.kind === "metadata")?.metadata?.id;
|
|
145
|
+
const { json } = await convert(toStandardJsonSchema(schema), io);
|
|
146
|
+
return {
|
|
147
|
+
json,
|
|
148
|
+
name
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "arktype": {
|
|
152
|
+
return { json: schema.toJsonSchema() };
|
|
153
|
+
}
|
|
154
|
+
default: {
|
|
155
|
+
throw new Error(`[openapi] No JSON Schema converter for schema vendor "${vendor}". Supported: zod, valibot, arktype.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function schemaToParameters(registry, schema, location) {
|
|
160
|
+
const json = await registry.toJsonSchema(schema, "input");
|
|
161
|
+
const resolved = json.$ref ? registry.components[json.$ref.split("/").pop()] : json;
|
|
162
|
+
if (resolved?.type !== "object" || !resolved.properties) return [];
|
|
163
|
+
return Object.entries(resolved.properties).map(([name, propSchema]) => ({
|
|
164
|
+
name,
|
|
165
|
+
in: location,
|
|
166
|
+
required: resolved.required?.includes(name) ?? false,
|
|
167
|
+
schema: propSchema
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeRoute(_route) {
|
|
172
|
+
const parameters = [];
|
|
173
|
+
let anonymousCtr = 0;
|
|
174
|
+
const route = _route.replace(/:(\w+)/g, (_, name) => `{${name}}`).replace(/\/(\*)\//g, () => `/{param${++anonymousCtr}}/`).replace(/\*\*{/, "{").replace(/\/(\*\*)$/g, () => `/{*param${++anonymousCtr}}`);
|
|
175
|
+
for (const match of route.matchAll(/{(\*?\w+)}/g)) {
|
|
176
|
+
const name = match[1];
|
|
177
|
+
if (!parameters.some((p) => p.name === name)) {
|
|
178
|
+
parameters.push({
|
|
179
|
+
name,
|
|
180
|
+
in: "path",
|
|
181
|
+
required: true,
|
|
182
|
+
schema: { type: "string" }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
route,
|
|
188
|
+
parameters
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL } from "h3";
|
|
2
|
+
// @ts-expect-error virtual module provided by ../index.ts
|
|
3
|
+
import { routes, config } from "#nitro-openapi-schemas";
|
|
4
|
+
import { toOpenAPIDocument } from "./generator.mjs";
|
|
5
|
+
let cached;
|
|
6
|
+
export default defineEventHandler((event) => {
|
|
7
|
+
|
|
8
|
+
const origin = getRequestURL(event).origin;
|
|
9
|
+
return cached ??= toOpenAPIDocument(routes, {
|
|
10
|
+
info: config.info,
|
|
11
|
+
servers: [{ url: origin }]
|
|
12
|
+
});
|
|
13
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nitro-openapi-schemas",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Schema-driven OpenAPI for Nitro: the Zod, Valibot or ArkType schema that validates a route also documents it",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"api",
|
|
7
|
+
"arktype",
|
|
8
|
+
"h3",
|
|
9
|
+
"json-schema",
|
|
10
|
+
"nitro",
|
|
11
|
+
"nuxt",
|
|
12
|
+
"openapi",
|
|
13
|
+
"standard-schema",
|
|
14
|
+
"swagger",
|
|
15
|
+
"valibot",
|
|
16
|
+
"zod"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/oumarbarry/nitro-openapi-schemas#readme",
|
|
19
|
+
"bugs": "https://github.com/oumarbarry/nitro-openapi-schemas/issues",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Oumar Barry",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/oumarbarry/nitro-openapi-schemas.git"
|
|
25
|
+
},
|
|
26
|
+
"workspaces": [
|
|
27
|
+
"examples/*"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./h3": {
|
|
40
|
+
"types": "./dist/h3.d.mts",
|
|
41
|
+
"default": "./dist/h3.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "bun run --cwd examples/nitro dev",
|
|
49
|
+
"openapi": "bun run --cwd examples/nitro openapi",
|
|
50
|
+
"openapi:nuxt4": "bun run --cwd examples/nuxt4 openapi",
|
|
51
|
+
"sdk": "bun run --cwd examples/nitro sdk",
|
|
52
|
+
"build": "obuild",
|
|
53
|
+
"prepare": "obuild",
|
|
54
|
+
"lint": "oxlint",
|
|
55
|
+
"lint:fix": "oxlint --fix",
|
|
56
|
+
"format": "oxfmt",
|
|
57
|
+
"format:check": "oxfmt --check",
|
|
58
|
+
"check": "oxlint && oxfmt --check",
|
|
59
|
+
"fix": "oxlint --fix && oxfmt"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^26.1.0",
|
|
63
|
+
"jiti": "^2.7.0",
|
|
64
|
+
"obuild": "^0.4.37",
|
|
65
|
+
"oxfmt": "^0.57.0",
|
|
66
|
+
"oxlint": "^1.72.0",
|
|
67
|
+
"typescript": "^6.0.3"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@valibot/to-json-schema": "^1.5.0",
|
|
71
|
+
"arktype": "^2.1.28",
|
|
72
|
+
"h3": "^1.15.0",
|
|
73
|
+
"nitropack": "^2.12.0",
|
|
74
|
+
"zod": "^4.2.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"zod": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"arktype": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@valibot/to-json-schema": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|