@kubb/plugin-ts 5.0.0-beta.4 → 5.0.0-beta.42

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.4",
4
- "description": "TypeScript code generation plugin for Kubb, transforming OpenAPI schemas into TypeScript interfaces, types, and utility functions.",
3
+ "version": "5.0.0-beta.42",
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
  ],
@@ -49,17 +46,17 @@
49
46
  "registry": "https://registry.npmjs.org/"
50
47
  },
51
48
  "dependencies": {
52
- "@kubb/core": "5.0.0-beta.4",
53
- "@kubb/parser-ts": "5.0.0-beta.4",
54
- "@kubb/renderer-jsx": "5.0.0-beta.4",
55
- "remeda": "^2.34.0",
49
+ "@kubb/core": "5.0.0-beta.42",
50
+ "@kubb/parser-ts": "5.0.0-beta.42",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.42",
56
52
  "typescript": "^6.0.3"
57
53
  },
58
54
  "devDependencies": {
55
+ "@internals/shared": "0.0.0",
59
56
  "@internals/utils": "0.0.0"
60
57
  },
61
58
  "peerDependencies": {
62
- "@kubb/renderer-jsx": "5.0.0-beta.4"
59
+ "@kubb/renderer-jsx": "5.0.0-beta.42"
63
60
  },
64
61
  "size-limit": [
65
62
  {
@@ -78,6 +75,7 @@
78
75
  "lint:fix": "oxlint --fix .",
79
76
  "release": "pnpm publish --no-git-check",
80
77
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
78
+ "release:stage": "pnpm stage publish --no-git-check",
81
79
  "start": "tsdown --watch",
82
80
  "test": "vitest --passWithNoTests",
83
81
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
@@ -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
@@ -1,11 +1,24 @@
1
1
  import { camelCase, pascalCase, screamingSnakeCase, snakeCase } from '@internals/utils'
2
2
  import { ast } from '@kubb/core'
3
- import { isNumber, sortBy } from 'remeda'
4
3
  import ts from 'typescript'
5
4
  import { OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
6
5
 
7
6
  const { SyntaxKind, factory } = ts
8
7
 
8
+ /**
9
+ * Compares two strings by UTF-16 code unit, keeping sorted output identical across platforms
10
+ * regardless of locale.
11
+ */
12
+ function compareStrings(a: string, b: string): number {
13
+ if (a < b) return -1
14
+ if (a > b) return 1
15
+ return 0
16
+ }
17
+
18
+ function isNumber(value: unknown): value is number {
19
+ return typeof value === 'number' && !Number.isNaN(value)
20
+ }
21
+
9
22
  // https://ts-ast-viewer.com/
10
23
 
11
24
  /**
@@ -27,13 +40,30 @@ export const syntaxKind = {
27
40
  stringLiteral: SyntaxKind.StringLiteral,
28
41
  } as const
29
42
 
43
+ function isNonNullable<T>(value: T | null | undefined): value is T {
44
+ return value !== null && value !== undefined
45
+ }
46
+
30
47
  function isValidIdentifier(str: string): boolean {
31
48
  if (!str.length || str.trim() !== str) {
32
49
  return false
33
50
  }
34
- const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest)
35
51
 
36
- return !!node && node.kind === ts.SyntaxKind.Identifier && ts.identifierToKeywordKind(node.kind as unknown as ts.Identifier) === undefined
52
+ // Mirrors `ts.isIdentifierText`, which is not in the public type declarations.
53
+ // Walking by code point with `isIdentifierStart`/`isIdentifierPart` rejects
54
+ // invalid names such as private identifiers (`#FOO`), forcing `propertyName`
55
+ // to quote them.
56
+ let ch = str.codePointAt(0)!
57
+ if (!ts.isIdentifierStart(ch, ts.ScriptTarget.Latest)) {
58
+ return false
59
+ }
60
+ for (let i = ch > 0xffff ? 2 : 1; i < str.length; i += ch > 0xffff ? 2 : 1) {
61
+ ch = str.codePointAt(i)!
62
+ if (!ts.isIdentifierPart(ch, ts.ScriptTarget.Latest)) {
63
+ return false
64
+ }
65
+ }
66
+ return true
37
67
  }
38
68
 
39
69
  function propertyName(name: string | ts.PropertyName): ts.PropertyName {
@@ -158,7 +188,9 @@ export function createPropertySignature({
158
188
  type?: ts.TypeNode
159
189
  }) {
160
190
  return factory.createPropertySignature(
161
- [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(Boolean),
191
+ [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
192
+ (modifier): modifier is ts.Modifier => modifier !== undefined,
193
+ ),
162
194
  propertyName(name),
163
195
  createQuestionToken(questionToken),
164
196
  type,
@@ -192,7 +224,7 @@ export function createParameterSignature(
192
224
  * Creates a JSDoc comment node from an array of comment strings.
193
225
  * Returns null if no comments are provided.
194
226
  */
195
- export function createJSDoc({ comments }: { comments: string[] }) {
227
+ export function createJSDoc({ comments }: { comments: Array<string> }) {
196
228
  if (!comments.length) {
197
229
  return null
198
230
  }
@@ -332,7 +364,7 @@ export function createTypeDeclaration({
332
364
  /**
333
365
  * Creates a TypeScript namespace declaration (exported module).
334
366
  */
335
- export function createNamespaceDeclaration({ statements, name }: { name: string; statements: ts.Statement[] }) {
367
+ export function createNamespaceDeclaration({ statements, name }: { name: string; statements: Array<ts.Statement> }) {
336
368
  return factory.createModuleDeclaration(
337
369
  [factory.createToken(ts.SyntaxKind.ExportKeyword)],
338
370
  factory.createIdentifier(name),
@@ -366,13 +398,8 @@ export function createImportDeclaration({
366
398
  isNameSpace?: boolean
367
399
  }) {
368
400
  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
- }
401
+ const importPropertyName = isNameSpace ? undefined : factory.createIdentifier(name)
402
+ const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : undefined
376
403
 
377
404
  return factory.createImportDeclaration(
378
405
  undefined,
@@ -383,7 +410,7 @@ export function createImportDeclaration({
383
410
  }
384
411
 
385
412
  // Sort the imports alphabetically for consistent output across platforms
386
- const sortedName = sortBy(name, [(item) => (typeof item === 'object' ? item.propertyName : item), 'asc'])
413
+ const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'object' ? a.propertyName : a, typeof b === 'object' ? b.propertyName : b))
387
414
 
388
415
  return factory.createImportDeclaration(
389
416
  undefined,
@@ -442,7 +469,7 @@ export function createExportDeclaration({
442
469
  }
443
470
 
444
471
  // Sort the exports alphabetically for consistent output across platforms
445
- const sortedName = sortBy(name, [(propertyName) => (typeof propertyName === 'string' ? propertyName : propertyName.text), 'asc'])
472
+ const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'string' ? a : a.text, typeof b === 'string' ? b : b.text))
446
473
 
447
474
  return factory.createExportDeclaration(
448
475
  undefined,
@@ -518,7 +545,7 @@ export function createEnumDeclaration({
518
545
  * Enum name in PascalCase.
519
546
  */
520
547
  typeName: string
521
- enums: [key: string | number, value: string | number | boolean][]
548
+ enums: Array<[key: string | number, value: string | number | boolean]>
522
549
  /**
523
550
  * Choose the casing for enum key names.
524
551
  * @default 'none'
@@ -553,7 +580,7 @@ export function createEnumDeclaration({
553
580
 
554
581
  return undefined
555
582
  })
556
- .filter(Boolean),
583
+ .filter((node): node is ts.LiteralTypeNode => node !== undefined),
557
584
  ),
558
585
  ),
559
586
  ]
@@ -563,7 +590,9 @@ export function createEnumDeclaration({
563
590
  return [
564
591
  undefined,
565
592
  factory.createEnumDeclaration(
566
- [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(Boolean),
593
+ [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
594
+ (modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
595
+ ),
567
596
  factory.createIdentifier(typeName),
568
597
  enums
569
598
  .map(([key, value]) => {
@@ -594,7 +623,7 @@ export function createEnumDeclaration({
594
623
 
595
624
  return undefined
596
625
  })
597
- .filter(Boolean),
626
+ .filter((member): member is ts.EnumMember => member !== undefined),
598
627
  ),
599
628
  ]
600
629
  }
@@ -657,7 +686,7 @@ export function createEnumDeclaration({
657
686
 
658
687
  return undefined
659
688
  })
660
- .filter(Boolean),
689
+ .filter((property): property is ts.PropertyAssignment => property !== undefined),
661
690
  true,
662
691
  ),
663
692
  factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
@@ -737,8 +766,8 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
737
766
  }
738
767
 
739
768
  const segments = normalized.split(/(\{[^}]+\})/)
740
- const parts: string[] = []
741
- const parameterIndices: number[] = []
769
+ const parts: Array<string> = []
770
+ const parameterIndices: Array<number> = []
742
771
 
743
772
  segments.forEach((segment) => {
744
773
  if (segment.startsWith('{') && segment.endsWith('}')) {
@@ -750,7 +779,7 @@ export function createUrlTemplateType(path: string): ts.TypeNode {
750
779
  })
751
780
 
752
781
  const head = ts.factory.createTemplateHead(parts[0] || '')
753
- const templateSpans: ts.TemplateLiteralTypeSpan[] = []
782
+ const templateSpans: Array<ts.TemplateLiteralTypeSpan> = []
754
783
 
755
784
  parameterIndices.forEach((paramIndex, i) => {
756
785
  const isLast = i === parameterIndices.length - 1
@@ -885,7 +914,7 @@ export function buildMemberNodes(
885
914
  members: Array<ast.SchemaNode> | undefined,
886
915
  print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
887
916
  ): Array<ts.TypeNode> {
888
- return (members ?? []).map(print).filter(Boolean)
917
+ return (members ?? []).map(print).filter(isNonNullable)
889
918
  }
890
919
 
891
920
  /**
@@ -893,7 +922,7 @@ export function buildMemberNodes(
893
922
  * applying min/max slice and optional/rest element rules.
894
923
  */
895
924
  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)
925
+ let items = (node.items ?? []).map(print).filter(isNonNullable)
897
926
 
898
927
  const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
899
928
  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,
@@ -64,11 +64,11 @@ function rank(param: ast.FunctionParameterNode | ast.ParameterGroupNode): number
64
64
  }
65
65
 
66
66
  function sortParams(params: ReadonlyArray<ast.FunctionParameterNode | ast.ParameterGroupNode>): Array<ast.FunctionParameterNode | ast.ParameterGroupNode> {
67
- return [...params].sort((a, b) => rank(a) - rank(b))
67
+ return params.toSorted((a, b) => rank(a) - rank(b))
68
68
  }
69
69
 
70
70
  function sortChildParams(params: Array<ast.FunctionParameterNode>): Array<ast.FunctionParameterNode> {
71
- return [...params].sort((a, b) => rank(a) - rank(b))
71
+ return params.toSorted((a, b) => rank(a) - rank(b))
72
72
  }
73
73
 
74
74
  /**