@kubb/adapter-oas 5.0.0-beta.3 → 5.0.0-beta.30

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/extension.yaml ADDED
@@ -0,0 +1,431 @@
1
+ $schema: https://kubb.dev/schemas/extension.json
2
+ kind: adapter
3
+ id: adapter-oas
4
+ name: OpenAPI
5
+ description: Parse and convert OpenAPI 2.0, 3.0, and 3.1 specifications into Kubb's universal AST. Handles discriminators, date formats, and server URL resolution.
6
+ category: openapi
7
+ type: official
8
+ npmPackage: '@kubb/adapter-oas'
9
+ docsPath: /adapters/adapter-oas
10
+ repo: https://github.com/kubb-labs/kubb
11
+ maintainers:
12
+ - name: Stijn Van Hulle
13
+ github: stijnvanhulle
14
+ compatibility:
15
+ kubb: '>=5.0.0'
16
+ node: '>=22'
17
+ tags:
18
+ - openapi
19
+ - swagger
20
+ - api-spec
21
+ - parser
22
+ - converter
23
+ resources:
24
+ documentation: https://kubb.dev/adapters/adapter-oas
25
+ repository: https://github.com/kubb-labs/kubb
26
+ issues: https://github.com/kubb-labs/kubb/issues
27
+ changelog: https://github.com/kubb-labs/kubb/blob/main/packages/adapter-oas/CHANGELOG.md
28
+ featured: true
29
+ icon:
30
+ light: https://kubb.dev/feature/openapi.svg
31
+ intro: |-
32
+ The OpenAPI adapter is the bridge between your spec and every Kubb plugin. It reads the file at `input.path`, validates it, and converts every schema and operation into Kubb's universal AST that downstream plugins consume.
33
+
34
+ Configure it once on `defineConfig`. Its choices (date representation, integer width, server URL) apply to every plugin in the build.
35
+ options:
36
+ - name: validate
37
+ type: boolean
38
+ required: false
39
+ default: 'true'
40
+ description: |
41
+ Validates the OpenAPI spec with `@readme/openapi-parser` before parsing. Set to `false` only when you have a known-invalid spec that you still want to generate from.
42
+ examples:
43
+ - name: kubb.config.ts
44
+ files:
45
+ - lang: typescript
46
+ twoslash: false
47
+ code: |
48
+ import { defineConfig } from 'kubb'
49
+ import { adapterOas } from '@kubb/adapter-oas'
50
+
51
+ export default defineConfig({
52
+ input: { path: './petStore.yaml' },
53
+ output: { path: './src/gen' },
54
+ adapter: adapterOas({ validate: false }),
55
+ plugins: [],
56
+ })
57
+
58
+ - name: contentType
59
+ type: "'application/json' | string"
60
+ required: false
61
+ description: |
62
+ Preferred media type when extracting request and response schemas. Operations with multiple media types fall back to this one.
63
+
64
+ Defaults to the first JSON-compatible media type found in the spec (`application/json`, `application/vnd.api+json`, any `*+json`).
65
+ codeBlock:
66
+ lang: typescript
67
+ title: kubb.config.ts
68
+ twoslash: false
69
+ code: |-
70
+ import { defineConfig } from 'kubb'
71
+ import { adapterOas } from '@kubb/adapter-oas'
72
+
73
+ export default defineConfig({
74
+ input: { path: './petStore.yaml' },
75
+ output: { path: './src/gen' },
76
+ adapter: adapterOas({ contentType: 'application/vnd.api+json' }),
77
+ plugins: [],
78
+ })
79
+
80
+ - name: serverIndex
81
+ type: number
82
+ required: false
83
+ description: |
84
+ Index into the `servers` array from your OpenAPI spec, used to compute the base URL for plugins that need it (`@kubb/plugin-client`, `@kubb/plugin-msw`, ...).
85
+
86
+ Most projects pick `0` for the primary server. Use higher indices to point at staging or localhost when your spec defines multiple environments.
87
+ tip: |
88
+ Plugins read `baseURL` from this server unless they override it explicitly.
89
+ examples:
90
+ - name: OpenAPI spec
91
+ files:
92
+ - lang: yaml
93
+ twoslash: false
94
+ code: |-
95
+ openapi: 3.0.3
96
+ servers:
97
+ - url: http://petstore.swagger.io/api
98
+ - url: http://localhost:3000
99
+ - name: 'Use the production server'
100
+ files:
101
+ - lang: typescript
102
+ twoslash: false
103
+ code: |-
104
+ import { defineConfig } from 'kubb'
105
+ import { adapterOas } from '@kubb/adapter-oas'
106
+
107
+ export default defineConfig({
108
+ input: { path: './petStore.yaml' },
109
+ output: { path: './src/gen' },
110
+ adapter: adapterOas({ serverIndex: 0 }),
111
+ plugins: [],
112
+ })
113
+ - name: 'Use the localhost server'
114
+ files:
115
+ - lang: typescript
116
+ twoslash: false
117
+ code: |-
118
+ import { defineConfig } from 'kubb'
119
+ import { adapterOas } from '@kubb/adapter-oas'
120
+
121
+ export default defineConfig({
122
+ input: { path: './petStore.yaml' },
123
+ output: { path: './src/gen' },
124
+ adapter: adapterOas({ serverIndex: 1 }),
125
+ plugins: [],
126
+ })
127
+
128
+ - name: serverVariables
129
+ type: Record<string, string>
130
+ required: false
131
+ description: |
132
+ Values substituted into `{variable}` placeholders in the selected server URL. Only used when `serverIndex` is set. Variables you do not provide use their `default` value from the spec.
133
+ examples:
134
+ - name: OpenAPI spec
135
+ files:
136
+ - lang: yaml
137
+ twoslash: false
138
+ code: |-
139
+ openapi: 3.0.3
140
+ servers:
141
+ - url: https://api.{env}.example.com
142
+ variables:
143
+ env:
144
+ default: dev
145
+ enum: [dev, staging, prod]
146
+ - name: kubb.config.ts
147
+ files:
148
+ - lang: typescript
149
+ twoslash: false
150
+ code: |-
151
+ import { defineConfig } from 'kubb'
152
+ import { adapterOas } from '@kubb/adapter-oas'
153
+
154
+ export default defineConfig({
155
+ input: { path: './petStore.yaml' },
156
+ output: { path: './src/gen' },
157
+ adapter: adapterOas({
158
+ serverIndex: 0,
159
+ serverVariables: { env: 'prod' },
160
+ }),
161
+ plugins: [],
162
+ })
163
+ // baseURL becomes: https://api.prod.example.com
164
+
165
+ - name: discriminator
166
+ type: "'strict' | 'inherit'"
167
+ required: false
168
+ default: "'strict'"
169
+ description: |
170
+ How `discriminator` fields on `oneOf`/`anyOf` schemas are interpreted.
171
+
172
+ - `'strict'` (default) — child schemas stay exactly as written. The discriminator narrows types at the call site but child shapes are not modified.
173
+ - `'inherit'` — Kubb propagates the discriminator property with the appropriate literal value into each child schema, so each branch's `type` field is precisely typed.
174
+ details:
175
+ - title: strict
176
+ body: 'Child schemas are emitted verbatim. The discriminator property has whatever type the OpenAPI spec gave it.'
177
+ - title: inherit
178
+ body: "Each child schema gets the discriminator value as a literal (`type: 'cat'`, `type: 'dog'`). Catches more bugs at compile time."
179
+ examples:
180
+ - name: OpenAPI spec
181
+ files:
182
+ - lang: yaml
183
+ twoslash: false
184
+ code: |-
185
+ openapi: 3.0.3
186
+ components:
187
+ schemas:
188
+ Animal:
189
+ required: [type]
190
+ type: object
191
+ oneOf:
192
+ - $ref: '#/components/schemas/Cat'
193
+ - $ref: '#/components/schemas/Dog'
194
+ discriminator:
195
+ propertyName: type
196
+ mapping:
197
+ cat: '#/components/schemas/Cat'
198
+ dog: '#/components/schemas/Dog'
199
+ Cat:
200
+ type: object
201
+ properties:
202
+ type:
203
+ type: string
204
+ indoor:
205
+ type: boolean
206
+ Dog:
207
+ type: object
208
+ properties:
209
+ type:
210
+ type: string
211
+ name:
212
+ type: string
213
+ - name: "'strict' (default)"
214
+ files:
215
+ - lang: typescript
216
+ twoslash: false
217
+ code: |-
218
+ export type Cat = {
219
+ type: string
220
+ indoor?: boolean
221
+ }
222
+
223
+ export type Dog = {
224
+ type: string
225
+ name?: string
226
+ }
227
+
228
+ export type Animal = Cat | Dog
229
+ - name: "'inherit'"
230
+ files:
231
+ - lang: typescript
232
+ twoslash: false
233
+ code: |-
234
+ export type Cat = {
235
+ type: 'cat'
236
+ indoor?: boolean
237
+ }
238
+
239
+ export type Dog = {
240
+ type: 'dog'
241
+ name?: string
242
+ }
243
+
244
+ export type Animal = Cat | Dog
245
+
246
+ - name: dateType
247
+ type: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
248
+ required: false
249
+ default: "'string'"
250
+ description: |
251
+ How `format: date-time` schemas are represented downstream.
252
+
253
+ - `false` — fall through to a plain `string` (no validation).
254
+ - `'string'` (default) — datetime string (`z.string().datetime()`, ISO 8601).
255
+ - `'stringOffset'` — datetime string with timezone offset.
256
+ - `'stringLocal'` — local datetime string (no timezone).
257
+ - `'date'` — JavaScript `Date` object. Best for client code; requires JSON parsing to revive.
258
+ examples:
259
+ - name: 'false'
260
+ files:
261
+ - lang: typescript
262
+ twoslash: false
263
+ code: |-
264
+ // format: date-time → plain string
265
+ type CreatedAt = string
266
+ - name: "'string' (default)"
267
+ files:
268
+ - lang: typescript
269
+ twoslash: false
270
+ code: |-
271
+ // format: date-time → ISO 8601 datetime string
272
+ type CreatedAt = string
273
+ - name: "'stringOffset'"
274
+ files:
275
+ - lang: typescript
276
+ twoslash: false
277
+ code: |-
278
+ // format: date-time → ISO 8601 datetime with offset
279
+ type CreatedAt = string
280
+ - name: "'stringLocal'"
281
+ files:
282
+ - lang: typescript
283
+ twoslash: false
284
+ code: |-
285
+ // format: date-time → local ISO 8601 datetime
286
+ type CreatedAt = string
287
+ - name: "'date'"
288
+ files:
289
+ - lang: typescript
290
+ twoslash: false
291
+ code: |-
292
+ // format: date-time → JavaScript Date
293
+ type CreatedAt = Date
294
+
295
+ - name: integerType
296
+ type: "'number' | 'bigint'"
297
+ required: false
298
+ default: "'number'"
299
+ description: |
300
+ How `type: integer` (and `format: int64`) maps to TypeScript.
301
+
302
+ - `'number'` (default) — fits most JSON APIs; loses precision above `Number.MAX_SAFE_INTEGER`.
303
+ - `'bigint'` — exact for 64-bit IDs, but `JSON.stringify`/`JSON.parse` cannot round-trip it. Use only when you also handle bigint serialization explicitly.
304
+ examples:
305
+ - name: "'number' (default)"
306
+ files:
307
+ - lang: typescript
308
+ twoslash: false
309
+ code: |-
310
+ type Pet = {
311
+ id: number
312
+ }
313
+ - name: "'bigint'"
314
+ files:
315
+ - lang: typescript
316
+ twoslash: false
317
+ code: |-
318
+ type Pet = {
319
+ id: bigint
320
+ }
321
+
322
+ - name: unknownType
323
+ type: "'any' | 'unknown' | 'void'"
324
+ required: false
325
+ default: "'any'"
326
+ description: |
327
+ AST type used when a schema's type cannot be inferred from the spec (`additionalProperties: true`, missing `type`, etc.).
328
+
329
+ Pick `'unknown'` to force callers to narrow before using the value. `'any'` is the loosest; `'void'` is rarely useful but matches some legacy APIs.
330
+ examples:
331
+ - name: "'any' (default)"
332
+ files:
333
+ - lang: typescript
334
+ twoslash: false
335
+ code: |-
336
+ type Pet = {
337
+ extra: any
338
+ }
339
+ - name: "'unknown'"
340
+ files:
341
+ - lang: typescript
342
+ twoslash: false
343
+ code: |-
344
+ type Pet = {
345
+ extra: unknown
346
+ }
347
+ - name: "'void'"
348
+ files:
349
+ - lang: typescript
350
+ twoslash: false
351
+ code: |-
352
+ type Pet = {
353
+ extra: void
354
+ }
355
+
356
+ - name: emptySchemaType
357
+ type: "'any' | 'unknown' | 'void'"
358
+ required: false
359
+ default: unknownType | 'any'
360
+ description: |
361
+ AST type used for fully empty schemas (`{}`). Defaults to the value of `unknownType`. Override only when empty schemas should be treated differently from unresolvable ones.
362
+ tip: |
363
+ A common pattern: `unknownType: 'unknown'` for safety, `emptySchemaType: 'any'` to keep empty 204 response bodies frictionless to use.
364
+ examples:
365
+ - name: "'any' (default)"
366
+ files:
367
+ - lang: typescript
368
+ twoslash: false
369
+ code: |-
370
+ // empty schema {} → any
371
+ type EmptyModel = any
372
+ - name: "'unknown'"
373
+ files:
374
+ - lang: typescript
375
+ twoslash: false
376
+ code: |-
377
+ // empty schema {} → unknown
378
+ type EmptyModel = unknown
379
+
380
+ - name: enumSuffix
381
+ type: string
382
+ required: false
383
+ default: "'enum'"
384
+ description: |
385
+ Suffix appended to derived enum names when Kubb has to invent one (typically for inline enums on object properties).
386
+
387
+ Inline enums on a `status` property would be named `statusEnum` with the default. Change this to align with your project's naming convention.
388
+ examples:
389
+ - name: "'enum' (default)"
390
+ files:
391
+ - lang: typescript
392
+ twoslash: false
393
+ code: |-
394
+ // Property `status` with inline enum values
395
+ const statusEnum = { available: 'available', pending: 'pending' } as const
396
+ type StatusEnum = (typeof statusEnum)[keyof typeof statusEnum]
397
+ - name: "'type'"
398
+ files:
399
+ - lang: typescript
400
+ twoslash: false
401
+ code: |-
402
+ const statusType = { available: 'available', pending: 'pending' } as const
403
+ type StatusType = (typeof statusType)[keyof typeof statusType]
404
+ examples:
405
+ - name: kubb.config.ts
406
+ files:
407
+ - lang: typescript
408
+ twoslash: false
409
+ code: |-
410
+ import { defineConfig } from 'kubb'
411
+ import { adapterOas } from '@kubb/adapter-oas'
412
+ import { pluginTs } from '@kubb/plugin-ts'
413
+
414
+ export default defineConfig({
415
+ input: { path: './petStore.yaml' },
416
+ output: { path: './src/gen' },
417
+ adapter: adapterOas({
418
+ validate: true,
419
+ serverIndex: 0,
420
+ serverVariables: { env: 'prod' },
421
+ discriminator: 'inherit',
422
+ dateType: 'date',
423
+ integerType: 'number',
424
+ unknownType: 'unknown',
425
+ emptySchemaType: 'unknown',
426
+ enumSuffix: 'enum',
427
+ }),
428
+ plugins: [
429
+ pluginTs(),
430
+ ],
431
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-beta.3",
4
- "description": "OpenAPI / Swagger adapter for Kubb converts OAS input into a @kubb/ast RootNode.",
3
+ "version": "5.0.0-beta.30",
4
+ "description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
5
5
  "keywords": [
6
6
  "adapter",
7
7
  "codegen",
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "src",
23
23
  "dist",
24
+ "extension.yaml",
24
25
  "!/**/**.test.**",
25
26
  "!/**/__tests__/**",
26
27
  "!/**/__snapshots__/**"
@@ -42,11 +43,11 @@
42
43
  "registry": "https://registry.npmjs.org/"
43
44
  },
44
45
  "dependencies": {
45
- "@redocly/openapi-core": "^2.30.3",
46
+ "@redocly/openapi-core": "^2.31.2",
46
47
  "oas": "^32.1.18",
47
48
  "oas-normalize": "^16.0.4",
48
49
  "swagger2openapi": "^7.0.8",
49
- "@kubb/core": "5.0.0-beta.3"
50
+ "@kubb/core": "5.0.0-beta.30"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/swagger2openapi": "^7.0.4",
@@ -67,6 +68,7 @@
67
68
  "release": "pnpm publish --no-git-check",
68
69
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
69
70
  "start": "tsdown --watch",
71
+ "bench": "vitest bench",
70
72
  "test": "vitest --passWithNoTests",
71
73
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
72
74
  }
@@ -0,0 +1,64 @@
1
+ import path from 'node:path'
2
+ import { existsSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { bench, describe } from 'vitest'
5
+ import { adapterOas } from './adapter.ts'
6
+ import { parseDocument } from './factory.ts'
7
+ import { parseOas } from './parser.ts'
8
+ import type { Document } from './types.ts'
9
+ import type { AdapterSource } from '@kubb/core'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+
14
+ const petStorePath = path.resolve(__dirname, '../mocks/petStore.yaml')
15
+ const stripeSpecPath = '/tmp/kubb-stripe-spec3.json'
16
+ const hasStripe = existsSync(stripeSpecPath)
17
+
18
+ let petStoreDoc: Document | undefined
19
+
20
+ async function getPetStoreDocument(): Promise<Document> {
21
+ if (!petStoreDoc) {
22
+ petStoreDoc = await parseDocument(petStorePath)
23
+ }
24
+ return petStoreDoc
25
+ }
26
+
27
+ describe('parseOas() performance', () => {
28
+ bench(
29
+ 'petStore spec',
30
+ async () => {
31
+ const doc = await getPetStoreDocument()
32
+ parseOas(doc)
33
+ },
34
+ { iterations: 5, warmupIterations: 1 },
35
+ )
36
+ })
37
+
38
+ describe.skipIf(!hasStripe)('Stripe spec — batch vs streaming (1,385 schemas)', () => {
39
+ const stripeSource: AdapterSource = { type: 'path', path: stripeSpecPath }
40
+
41
+ bench(
42
+ 'batch — adapter.parse()',
43
+ async () => {
44
+ const adapter = adapterOas({ validate: false })
45
+ await adapter.parse(stripeSource)
46
+ },
47
+ { iterations: 3, warmupIterations: 1 },
48
+ )
49
+
50
+ bench(
51
+ 'streaming — adapter.stream() drain',
52
+ async () => {
53
+ const adapter = adapterOas({ validate: false })
54
+ const stream = await adapter.stream!(stripeSource)
55
+ for await (const _ of stream.schemas) {
56
+ /* drain */
57
+ }
58
+ for await (const _ of stream.operations) {
59
+ /* drain */
60
+ }
61
+ },
62
+ { iterations: 3, warmupIterations: 1 },
63
+ )
64
+ })