@kubb/plugin-ts 5.0.0-beta.3 → 5.0.0-beta.31

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/package.json CHANGED
@@ -1,19 +1,16 @@
1
1
  {
2
2
  "name": "@kubb/plugin-ts",
3
- "version": "5.0.0-beta.3",
4
- "description": "TypeScript code generation plugin for Kubb, transforming OpenAPI schemas into TypeScript interfaces, types, and utility functions.",
3
+ "version": "5.0.0-beta.31",
4
+ "description": "Generate TypeScript types, interfaces, and enums from your OpenAPI specification. The foundational plugin that powers type safety across the entire Kubb ecosystem.",
5
5
  "keywords": [
6
- "code-generator",
6
+ "code-generation",
7
7
  "codegen",
8
+ "enums",
8
9
  "interfaces",
9
10
  "kubb",
10
- "oas",
11
11
  "openapi",
12
- "plugins",
13
12
  "swagger",
14
- "type-definitions",
15
13
  "type-generation",
16
- "type-safe",
17
14
  "types",
18
15
  "typescript"
19
16
  ],
@@ -27,7 +24,7 @@
27
24
  "files": [
28
25
  "src",
29
26
  "dist",
30
- "plugin.json",
27
+ "extension.yaml",
31
28
  "!/**/**.test.**",
32
29
  "!/**/__tests__/**",
33
30
  "!/**/__snapshots__/**"
@@ -49,17 +46,18 @@
49
46
  "registry": "https://registry.npmjs.org/"
50
47
  },
51
48
  "dependencies": {
52
- "@kubb/core": "5.0.0-beta.3",
53
- "@kubb/parser-ts": "5.0.0-beta.3",
54
- "@kubb/renderer-jsx": "5.0.0-beta.3",
55
- "remeda": "^2.34.0",
49
+ "@kubb/core": "5.0.0-beta.31",
50
+ "@kubb/parser-ts": "5.0.0-beta.31",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.31",
52
+ "remeda": "^2.34.1",
56
53
  "typescript": "^6.0.3"
57
54
  },
58
55
  "devDependencies": {
56
+ "@internals/shared": "0.0.0",
59
57
  "@internals/utils": "0.0.0"
60
58
  },
61
59
  "peerDependencies": {
62
- "@kubb/renderer-jsx": "5.0.0-beta.3"
60
+ "@kubb/renderer-jsx": "5.0.0-beta.31"
63
61
  },
64
62
  "size-limit": [
65
63
  {
@@ -1,6 +1,6 @@
1
1
  import { camelCase, trimQuotes } from '@internals/utils'
2
2
  import type { ast } from '@kubb/core'
3
- import { safePrint } from '@kubb/parser-ts'
3
+ import { parserTs } from '@kubb/parser-ts'
4
4
  import { File } from '@kubb/renderer-jsx'
5
5
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
6
6
  import { ENUM_TYPES_WITH_KEY_SUFFIX, ENUM_TYPES_WITH_RUNTIME_VALUE, ENUM_TYPES_WITH_TYPE_ONLY } from '../constants.ts'
@@ -72,11 +72,11 @@ export function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }
72
72
  <>
73
73
  {nameNode && (
74
74
  <File.Source name={enumName} isExportable isIndexable isTypeOnly={false}>
75
- {safePrint(nameNode)}
75
+ {parserTs.print(nameNode)}
76
76
  </File.Source>
77
77
  )}
78
78
  <File.Source name={typeName} isIndexable isExportable={ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumType)} isTypeOnly={ENUM_TYPES_WITH_TYPE_ONLY.has(enumType)}>
79
- {safePrint(typeNode)}
79
+ {parserTs.print(typeNode)}
80
80
  </File.Source>
81
81
  </>
82
82
  )
package/src/factory.ts CHANGED
@@ -27,6 +27,10 @@ export const syntaxKind = {
27
27
  stringLiteral: SyntaxKind.StringLiteral,
28
28
  } as const
29
29
 
30
+ function isNonNullable<T>(value: T | null | undefined): value is T {
31
+ return value !== null && value !== undefined
32
+ }
33
+
30
34
  function isValidIdentifier(str: string): boolean {
31
35
  if (!str.length || str.trim() !== str) {
32
36
  return false
@@ -158,7 +162,9 @@ export function createPropertySignature({
158
162
  type?: ts.TypeNode
159
163
  }) {
160
164
  return factory.createPropertySignature(
161
- [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(Boolean),
165
+ [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
166
+ (modifier): modifier is ts.Modifier => modifier !== undefined,
167
+ ),
162
168
  propertyName(name),
163
169
  createQuestionToken(questionToken),
164
170
  type,
@@ -192,7 +198,7 @@ export function createParameterSignature(
192
198
  * Creates a JSDoc comment node from an array of comment strings.
193
199
  * Returns null if no comments are provided.
194
200
  */
195
- export function createJSDoc({ comments }: { comments: string[] }) {
201
+ export function createJSDoc({ comments }: { comments: Array<string> }) {
196
202
  if (!comments.length) {
197
203
  return null
198
204
  }
@@ -332,7 +338,7 @@ export function createTypeDeclaration({
332
338
  /**
333
339
  * Creates a TypeScript namespace declaration (exported module).
334
340
  */
335
- export function createNamespaceDeclaration({ statements, name }: { name: string; statements: ts.Statement[] }) {
341
+ export function createNamespaceDeclaration({ statements, name }: { name: string; statements: Array<ts.Statement> }) {
336
342
  return factory.createModuleDeclaration(
337
343
  [factory.createToken(ts.SyntaxKind.ExportKeyword)],
338
344
  factory.createIdentifier(name),
@@ -366,13 +372,8 @@ export function createImportDeclaration({
366
372
  isNameSpace?: boolean
367
373
  }) {
368
374
  if (!Array.isArray(name)) {
369
- let importPropertyName: ts.Identifier | undefined = factory.createIdentifier(name)
370
- let importName: ts.NamedImportBindings | undefined
371
-
372
- if (isNameSpace) {
373
- importPropertyName = undefined
374
- importName = factory.createNamespaceImport(factory.createIdentifier(name))
375
- }
375
+ const importPropertyName = isNameSpace ? undefined : factory.createIdentifier(name)
376
+ const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : undefined
376
377
 
377
378
  return factory.createImportDeclaration(
378
379
  undefined,
@@ -518,7 +519,7 @@ export function createEnumDeclaration({
518
519
  * Enum name in PascalCase.
519
520
  */
520
521
  typeName: string
521
- enums: [key: string | number, value: string | number | boolean][]
522
+ enums: Array<[key: string | number, value: string | number | boolean]>
522
523
  /**
523
524
  * Choose the casing for enum key names.
524
525
  * @default 'none'
@@ -553,7 +554,7 @@ export function createEnumDeclaration({
553
554
 
554
555
  return undefined
555
556
  })
556
- .filter(Boolean),
557
+ .filter((node): node is ts.LiteralTypeNode => node !== undefined),
557
558
  ),
558
559
  ),
559
560
  ]
@@ -563,7 +564,9 @@ export function createEnumDeclaration({
563
564
  return [
564
565
  undefined,
565
566
  factory.createEnumDeclaration(
566
- [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(Boolean),
567
+ [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
568
+ (modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
569
+ ),
567
570
  factory.createIdentifier(typeName),
568
571
  enums
569
572
  .map(([key, value]) => {
@@ -594,7 +597,7 @@ export function createEnumDeclaration({
594
597
 
595
598
  return undefined
596
599
  })
597
- .filter(Boolean),
600
+ .filter((member): member is ts.EnumMember => member !== undefined),
598
601
  ),
599
602
  ]
600
603
  }
@@ -657,7 +660,7 @@ export function createEnumDeclaration({
657
660
 
658
661
  return undefined
659
662
  })
660
- .filter(Boolean),
663
+ .filter((property): property is ts.PropertyAssignment => property !== undefined),
661
664
  true,
662
665
  ),
663
666
  factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
@@ -737,8 +740,8 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
737
740
  }
738
741
 
739
742
  const segments = normalized.split(/(\{[^}]+\})/)
740
- const parts: string[] = []
741
- const parameterIndices: number[] = []
743
+ const parts: Array<string> = []
744
+ const parameterIndices: Array<number> = []
742
745
 
743
746
  segments.forEach((segment) => {
744
747
  if (segment.startsWith('{') && segment.endsWith('}')) {
@@ -750,7 +753,7 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
750
753
  })
751
754
 
752
755
  const head = ts.factory.createTemplateHead(parts[0] || '')
753
- const templateSpans: ts.TemplateLiteralTypeSpan[] = []
756
+ const templateSpans: Array<ts.TemplateLiteralTypeSpan> = []
754
757
 
755
758
  parameterIndices.forEach((paramIndex, i) => {
756
759
  const isLast = i === parameterIndices.length - 1
@@ -885,7 +888,7 @@ export function buildMemberNodes(
885
888
  members: Array<ast.SchemaNode> | undefined,
886
889
  print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
887
890
  ): Array<ts.TypeNode> {
888
- return (members ?? []).map(print).filter(Boolean)
891
+ return (members ?? []).map(print).filter(isNonNullable)
889
892
  }
890
893
 
891
894
  /**
@@ -893,7 +896,7 @@ export function buildMemberNodes(
893
896
  * applying min/max slice and optional/rest element rules.
894
897
  */
895
898
  export function buildTupleNode(node: ast.ArraySchemaNode, print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
896
- let items = (node.items ?? []).map(print).filter(Boolean)
899
+ let items = (node.items ?? []).map(print).filter(isNonNullable)
897
900
 
898
901
  const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
899
902
  const { min, max } = node
@@ -1,14 +1,21 @@
1
+ import { resolveContentTypeVariants } from '@internals/shared'
1
2
  import { ast, defineGenerator } from '@kubb/core'
2
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
3
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
3
4
  import { Type } from '../components/Type.tsx'
4
5
  import { ENUM_TYPES_WITH_KEY_SUFFIX } from '../constants.ts'
5
6
  import { printerTs } from '../printers/printerTs.ts'
6
7
  import type { PluginTs } from '../types'
7
8
  import { buildData, buildResponses, buildResponseUnion } from '../utils.ts'
8
9
 
10
+ /**
11
+ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
12
+ * schema in the spec plus per-operation request, response, and parameter
13
+ * types. Drop-replace with a custom `Generator<PluginTs>` to change how
14
+ * TypeScript output is produced.
15
+ */
9
16
  export const typeGenerator = defineGenerator<PluginTs>({
10
17
  name: 'typescript',
11
- renderer: jsxRenderer,
18
+ renderer: jsxRendererSync,
12
19
  schema(node, ctx) {
13
20
  const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options
14
21
  const { adapter, config, resolver, root } = ctx
@@ -19,7 +26,7 @@ export const typeGenerator = defineGenerator<PluginTs>({
19
26
  const mode = ctx.getMode(output)
20
27
  // Build a set of schema names that are enums so the ref handler and getImports
21
28
  // callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
22
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name!))
29
+ const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
23
30
 
24
31
  function resolveImportName(schemaName: string): string {
25
32
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
@@ -30,14 +37,14 @@ export const typeGenerator = defineGenerator<PluginTs>({
30
37
 
31
38
  const imports = adapter.getImports(node, (schemaName) => ({
32
39
  name: resolveImportName(schemaName),
33
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
40
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
34
41
  }))
35
42
 
36
43
  const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
37
44
 
38
45
  const meta = {
39
46
  name: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && isEnumSchema ? resolver.resolveEnumKeyName(node, enumTypeSuffix) : resolver.resolveTypeName(node.name),
40
- file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
47
+ file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
41
48
  } as const
42
49
 
43
50
  const schemaPrinter = printerTs({
@@ -58,8 +65,8 @@ export const typeGenerator = defineGenerator<PluginTs>({
58
65
  baseName={meta.file.baseName}
59
66
  path={meta.file.path}
60
67
  meta={meta.file.meta}
61
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
62
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
68
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
69
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
63
70
  >
64
71
  {mode === 'split' &&
65
72
  imports.map((imp) => (
@@ -86,12 +93,15 @@ export const typeGenerator = defineGenerator<PluginTs>({
86
93
  const params = ast.caseParams(node.parameters, paramsCasing)
87
94
 
88
95
  const meta = {
89
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
96
+ file: resolver.resolveFile(
97
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
98
+ { root, output, group: group ?? undefined },
99
+ ),
90
100
  } as const
91
101
 
92
102
  // Build a set of schema names that are enums so the ref handler and getImports
93
103
  // callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
94
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name!))
104
+ const enumSchemaNames = new Set<string>(ctx.meta.enumNames)
95
105
 
96
106
  function resolveImportName(schemaName: string): string {
97
107
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
@@ -100,12 +110,12 @@ export const typeGenerator = defineGenerator<PluginTs>({
100
110
  return resolver.resolveTypeName(schemaName)
101
111
  }
102
112
 
103
- function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
113
+ function renderSchemaType({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
104
114
  if (!schema) return null
105
115
 
106
116
  const imports = adapter.getImports(schema, (schemaName) => ({
107
117
  name: resolveImportName(schemaName),
108
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
118
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
109
119
  }))
110
120
 
111
121
  const schemaPrinter = printerTs({
@@ -141,6 +151,34 @@ export const typeGenerator = defineGenerator<PluginTs>({
141
151
  )
142
152
  }
143
153
 
154
+ /**
155
+ * Emits an individual type per content type plus a union alias under `baseName`.
156
+ * Shared by the request body and multi-content-type responses.
157
+ */
158
+ function buildContentTypeVariants(
159
+ entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
160
+ baseName: string,
161
+ decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
162
+ ) {
163
+ const variants = resolveContentTypeVariants(entries, baseName)
164
+ const unionSchema = ast.createSchema({
165
+ type: 'union',
166
+ members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
167
+ })
168
+ return (
169
+ <>
170
+ {variants.map((variant) =>
171
+ renderSchemaType({
172
+ schema: decorate ? decorate(variant.schema) : variant.schema,
173
+ name: variant.name,
174
+ keysToOmit: variant.keysToOmit,
175
+ }),
176
+ )}
177
+ {renderSchemaType({ schema: unionSchema, name: baseName })}
178
+ </>
179
+ )
180
+ }
181
+
144
182
  const paramTypes = params.map((param) =>
145
183
  renderSchemaType({
146
184
  schema: param.schema,
@@ -148,24 +186,44 @@ export const typeGenerator = defineGenerator<PluginTs>({
148
186
  }),
149
187
  )
150
188
 
151
- const requestType = node.requestBody?.content?.[0]?.schema
152
- ? renderSchemaType({
189
+ const requestBodyContent = node.requestBody?.content ?? []
190
+
191
+ function buildRequestType() {
192
+ if (requestBodyContent.length === 0) return null
193
+ if (requestBodyContent.length === 1) {
194
+ const entry = requestBodyContent[0]!
195
+ if (!entry.schema) return null
196
+ return renderSchemaType({
153
197
  schema: {
154
- ...node.requestBody.content![0]!.schema!,
155
- description: node.requestBody.description ?? node.requestBody.content![0]!.schema!.description,
198
+ ...entry.schema,
199
+ description: node.requestBody!.description ?? entry.schema.description,
156
200
  },
157
201
  name: resolver.resolveDataName(node),
158
- keysToOmit: node.requestBody.content![0]!.keysToOmit,
202
+ keysToOmit: entry.keysToOmit,
159
203
  })
160
- : null
204
+ }
205
+ // Multiple content types — generate individual types + union alias
206
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
207
+ ...schema,
208
+ description: node.requestBody!.description ?? schema.description,
209
+ }))
210
+ }
161
211
 
162
- const responseTypes = node.responses.map((res) =>
163
- renderSchemaType({
164
- schema: res.schema,
212
+ const requestType = buildRequestType()
213
+
214
+ const responseTypes = node.responses.map((res) => {
215
+ const variants = (res.content ?? []).filter((entry) => entry.schema)
216
+ // Multiple content types for a single status code — generate a union of the variants.
217
+ if (variants.length > 1) {
218
+ return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode))
219
+ }
220
+ const primary = variants[0] ?? res.content?.[0]
221
+ return renderSchemaType({
222
+ schema: primary?.schema ?? null,
165
223
  name: resolver.resolveResponseStatusName(node, res.statusCode),
166
- keysToOmit: res.keysToOmit,
167
- }),
168
- )
224
+ keysToOmit: primary?.keysToOmit,
225
+ })
226
+ })
169
227
 
170
228
  const dataType = renderSchemaType({
171
229
  schema: buildData({ ...node, parameters: params }, { resolver }),
@@ -177,26 +235,27 @@ export const typeGenerator = defineGenerator<PluginTs>({
177
235
  name: resolver.resolveResponsesName(node),
178
236
  })
179
237
 
180
- const responseType = (() => {
181
- if (!node.responses.some((res) => res.schema)) {
238
+ function buildResponseType() {
239
+ const hasSchema = (res: ast.ResponseNode) => (res.content ?? []).some((entry) => entry.schema)
240
+ if (!node.responses.some(hasSchema)) {
182
241
  return null
183
242
  }
184
243
 
185
244
  const responseName = resolver.resolveResponseName(node)
186
245
 
187
- // Skip generating the response union type when an imported component schema
188
- // has the same resolved name to avoid redeclaration errors.
189
- const responsesWithSchema = node.responses.filter((res) => res.schema)
246
+ const responsesWithSchema = node.responses.filter(hasSchema)
190
247
  const importedNames = new Set(
191
248
  responsesWithSchema.flatMap((res) =>
192
- res.schema
193
- ? adapter
194
- .getImports(res.schema, (schemaName) => ({
195
- name: resolveImportName(schemaName),
196
- path: '',
197
- }))
198
- .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
199
- : [],
249
+ (res.content ?? []).flatMap((entry) =>
250
+ entry.schema
251
+ ? adapter
252
+ .getImports(entry.schema, (schemaName) => ({
253
+ name: resolveImportName(schemaName),
254
+ path: '',
255
+ }))
256
+ .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
257
+ : [],
258
+ ),
200
259
  ),
201
260
  )
202
261
 
@@ -211,15 +270,17 @@ export const typeGenerator = defineGenerator<PluginTs>({
211
270
  },
212
271
  name: responseName,
213
272
  })
214
- })()
273
+ }
274
+
275
+ const responseType = buildResponseType()
215
276
 
216
277
  return (
217
278
  <File
218
279
  baseName={meta.file.baseName}
219
280
  path={meta.file.path}
220
281
  meta={meta.file.meta}
221
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
222
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
282
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
283
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
223
284
  >
224
285
  {paramTypes}
225
286
  {responseTypes}
package/src/plugin.ts CHANGED
@@ -1,27 +1,36 @@
1
- import { camelCase } from '@internals/utils'
2
- import { definePlugin, type Group } from '@kubb/core'
1
+ import { createGroupConfig } from '@internals/shared'
2
+ import { definePlugin } from '@kubb/core'
3
3
  import { typeGenerator } from './generators/typeGenerator.tsx'
4
4
  import { resolverTs } from './resolvers/resolverTs.ts'
5
5
  import type { PluginTs } from './types.ts'
6
6
 
7
7
  /**
8
- * Canonical plugin name for `@kubb/plugin-ts`, used to identify the plugin in driver lookups and warnings.
8
+ * Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
9
+ * cross-plugin dependency references.
9
10
  */
10
11
  export const pluginTsName = 'plugin-ts' satisfies PluginTs['name']
11
12
 
12
13
  /**
13
- * The `@kubb/plugin-ts` plugin factory.
14
- *
15
- * Generates TypeScript type declarations from an OpenAPI/AST `RootNode`.
16
- * Walks schemas and operations, delegates rendering to the active generators,
17
- * and writes barrel files based on `output.barrelType`.
14
+ * Generates TypeScript `type` aliases and `interface` declarations from an
15
+ * OpenAPI spec. The foundation that every other Kubb plugin builds on:
16
+ * clients, query hooks, mocks, and validators all reference the names this
17
+ * plugin produces.
18
18
  *
19
19
  * @example
20
20
  * ```ts
21
- * import pluginTs from '@kubb/plugin-ts'
21
+ * import { defineConfig } from 'kubb'
22
+ * import { pluginTs } from '@kubb/plugin-ts'
22
23
  *
23
24
  * export default defineConfig({
24
- * plugins: [pluginTs({ output: { path: 'types' }, enumType: 'asConst' })],
25
+ * input: { path: './petStore.yaml' },
26
+ * output: { path: './src/gen' },
27
+ * plugins: [
28
+ * pluginTs({
29
+ * output: { path: './types' },
30
+ * enumType: 'asConst',
31
+ * optionalType: 'questionTokenAndUndefined',
32
+ * }),
33
+ * ],
25
34
  * })
26
35
  * ```
27
36
  */
@@ -45,17 +54,7 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
45
54
  generators: userGenerators = [],
46
55
  } = options
47
56
 
48
- const groupConfig = group
49
- ? ({
50
- ...group,
51
- name: (ctx) => {
52
- if (group.type === 'path') {
53
- return `${ctx.group.split('/')[1]}`
54
- }
55
- return `${camelCase(ctx.group)}Controller`
56
- },
57
- } satisfies Group)
58
- : undefined
57
+ const groupConfig = createGroupConfig(group, { suffix: 'Controller' })
59
58
 
60
59
  return {
61
60
  name: pluginTsName,
@@ -1,11 +1,15 @@
1
1
  import { ast } from '@kubb/core'
2
- import { safePrint } from '@kubb/parser-ts'
2
+ import { parserTs } from '@kubb/parser-ts'
3
3
  import type ts from 'typescript'
4
4
  import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from '../constants.ts'
5
5
  import * as factory from '../factory.ts'
6
6
  import type { PluginTs, ResolverTs } from '../types.ts'
7
7
  import { buildPropertyJSDocComments } from '../utils.ts'
8
8
 
9
+ function isNonNullable<T>(value: T | null | undefined): value is T {
10
+ return value !== null && value !== undefined
11
+ }
12
+
9
13
  /**
10
14
  * Partial map of node-type overrides for the TypeScript printer.
11
15
  *
@@ -85,7 +89,7 @@ export type PrinterTsOptions = {
85
89
  * Properties to exclude using `Omit<Type, Keys>`.
86
90
  * Forces type alias syntax regardless of `syntaxType` setting.
87
91
  */
88
- keysToOmit?: Array<string>
92
+ keysToOmit?: Array<string> | null
89
93
  /**
90
94
  * Transforms raw schema names into valid TypeScript identifiers.
91
95
  */
@@ -163,7 +167,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
163
167
  time: factory.dateOrStringNode,
164
168
  ref(node) {
165
169
  if (!node.name) {
166
- return undefined
170
+ return null
167
171
  }
168
172
  // Parser-generated refs (with $ref) carry raw schema names that need resolving.
169
173
  // Use the canonical name from the $ref path — node.name may have been overridden
@@ -191,7 +195,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
191
195
  const literalNodes = values
192
196
  .filter((v): v is string | number | boolean => v !== null && v !== undefined)
193
197
  .map((value) => factory.constToTypeNode(value, typeof value as 'string' | 'number' | 'boolean'))
194
- .filter(Boolean)
198
+ .filter(isNonNullable)
195
199
 
196
200
  return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
197
201
  }
@@ -224,7 +228,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
224
228
 
225
229
  return this.transform(m)
226
230
  })
227
- .filter(Boolean)
231
+ .filter(isNonNullable)
228
232
 
229
233
  return factory.createUnionDeclaration({ withParentheses: true, nodes: memberNodes }) ?? undefined
230
234
  }
@@ -232,15 +236,15 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
232
236
  return factory.createUnionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(members, this.transform) }) ?? undefined
233
237
  },
234
238
  intersection(node) {
235
- return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? undefined
239
+ return factory.createIntersectionDeclaration({ withParentheses: true, nodes: factory.buildMemberNodes(node.members, this.transform) }) ?? null
236
240
  },
237
241
  array(node) {
238
- const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
242
+ const itemNodes = (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable)
239
243
 
240
- return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined
244
+ return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? null
241
245
  },
242
246
  tuple(node) {
243
- return factory.buildTupleNode(node, this.transform)
247
+ return factory.buildTupleNode(node, this.transform) ?? null
244
248
  },
245
249
  object(node) {
246
250
  const { transform, options } = this
@@ -275,36 +279,33 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
275
279
  print(node) {
276
280
  const { name, syntaxType = 'type', description, keysToOmit } = this.options
277
281
 
278
- let base = this.transform(node)
279
- if (!base) return null
282
+ const transformed = this.transform(node)
283
+ if (!transformed) return null
280
284
 
281
285
  // For ref nodes, structural metadata lives on node.schema rather than the ref node itself.
282
286
  const meta = ast.syncSchemaRef(node)
283
287
 
284
288
  // Without name, apply modifiers inline and return.
285
289
  if (!name) {
286
- if (meta.nullable) {
287
- base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.null] })
288
- }
289
- if ((meta.nullish || meta.optional) && addsUndefined) {
290
- base = factory.createUnionDeclaration({ nodes: [base, factory.keywordTypeNodes.undefined] })
291
- }
292
- return safePrint(base)
290
+ const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [transformed, factory.keywordTypeNodes.null] }) : transformed
291
+ const result =
292
+ (meta.nullish || meta.optional) && addsUndefined
293
+ ? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] })
294
+ : withNullable
295
+ return parserTs.print(result)
293
296
  }
294
297
 
295
298
  // When keysToOmit is present, wrap with Omit first, then apply nullable/optional
296
299
  // modifiers so they are not swallowed by NonNullable inside createOmitDeclaration.
297
- let inner: ts.TypeNode = keysToOmit?.length ? factory.createOmitDeclaration({ keys: keysToOmit, type: base, nonNullable: true }) : base
298
-
299
- if (meta.nullable) {
300
- inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.null] })
301
- }
302
-
303
- // For named type declarations (type aliases), optional/nullish always produces | undefined
304
- // regardless of optionalType the questionToken ? modifier only applies to object properties.
305
- if (meta.nullish || meta.optional) {
306
- inner = factory.createUnionDeclaration({ nodes: [inner, factory.keywordTypeNodes.undefined] })
307
- }
300
+ const inner = (() => {
301
+ const omitted: ts.TypeNode = keysToOmit?.length
302
+ ? factory.createOmitDeclaration({ keys: keysToOmit, type: transformed, nonNullable: true })
303
+ : transformed
304
+ const withNullable = meta.nullable ? factory.createUnionDeclaration({ nodes: [omitted, factory.keywordTypeNodes.null] }) : omitted
305
+ // For named type declarations (type aliases), optional/nullish always produces | undefined
306
+ // regardless of optionalType the questionToken ? modifier only applies to object properties.
307
+ return meta.nullish || meta.optional ? factory.createUnionDeclaration({ nodes: [withNullable, factory.keywordTypeNodes.undefined] }) : withNullable
308
+ })()
308
309
 
309
310
  const useTypeGeneration = syntaxType === 'type' || inner.kind === factory.syntaxKind.union || !!keysToOmit?.length
310
311
 
@@ -319,7 +320,7 @@ export const printerTs = ast.definePrinter<PrinterTs>((options) => {
319
320
  }),
320
321
  })
321
322
 
322
- return safePrint(typeNode)
323
+ return parserTs.print(typeNode)
323
324
  },
324
325
  }
325
326
  })