@kubb/plugin-ts 5.0.0-alpha.24 → 5.0.0-alpha.25

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-ts",
3
- "version": "5.0.0-alpha.24",
3
+ "version": "5.0.0-alpha.25",
4
4
  "description": "TypeScript code generation plugin for Kubb, transforming OpenAPI schemas into TypeScript interfaces, types, and utility functions.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -51,10 +51,10 @@
51
51
  "dependencies": {
52
52
  "@kubb/fabric-core": "0.15.1",
53
53
  "@kubb/react-fabric": "0.15.1",
54
- "remeda": "^2.33.6",
54
+ "remeda": "^2.33.7",
55
55
  "typescript": "5.9.3",
56
- "@kubb/ast": "5.0.0-alpha.24",
57
- "@kubb/core": "5.0.0-alpha.24"
56
+ "@kubb/ast": "5.0.0-alpha.25",
57
+ "@kubb/core": "5.0.0-alpha.25"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@kubb/react-fabric": "0.15.1"
@@ -62,7 +62,7 @@ export function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }
62
62
  typeName,
63
63
  enums: (node.namedEnumValues?.map((v) => [trimQuotes(v.name.toString()), v.value]) ??
64
64
  node.enumValues?.filter((v): v is NonNullable<typeof v> => v !== null && v !== undefined).map((v) => [trimQuotes(v.toString()), v]) ??
65
- []) as unknown as Array<[string, string]>,
65
+ []) as Array<[string | number, string | number | boolean]>,
66
66
  type: enumType,
67
67
  enumKeyCasing,
68
68
  })
package/src/constants.ts CHANGED
@@ -27,3 +27,13 @@ export const ENUM_TYPES_WITH_RUNTIME_VALUE = new Set<EnumType | undefined>(['enu
27
27
  * `enumType` values whose type declaration is type-only (no runtime value emitted for the type alias).
28
28
  */
29
29
  export const ENUM_TYPES_WITH_TYPE_ONLY = new Set<EnumType | undefined>(['asConst', 'asPascalConst', 'literal', undefined] as const)
30
+
31
+ /**
32
+ * Ordering priority for function parameters: lower = sorted earlier.
33
+ */
34
+ export const PARAM_RANK = {
35
+ required: 0,
36
+ optional: 1,
37
+ withDefault: 2,
38
+ rest: 3,
39
+ } as const
package/src/factory.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { camelCase, pascalCase, screamingSnakeCase, snakeCase } from '@internals/utils'
2
+ import { syncSchemaRef } from '@kubb/ast'
3
+ import type { ArraySchemaNode, SchemaNode } from '@kubb/ast/types'
2
4
  import { isNumber, sortBy } from 'remeda'
3
5
  import ts from 'typescript'
6
+ import { OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
4
7
 
5
8
  const { SyntaxKind, factory } = ts
6
9
 
@@ -249,7 +252,7 @@ export function createTypeDeclaration({
249
252
  }) {
250
253
  if (syntax === 'interface' && 'members' in type) {
251
254
  const node = createInterfaceDeclaration({
252
- members: type.members as Array<ts.TypeElement>,
255
+ members: [...(type as ts.TypeLiteralNode).members],
253
256
  modifiers: isExportable ? [modifiers.export] : [],
254
257
  name,
255
258
  typeParameters: undefined,
@@ -692,3 +695,121 @@ export const createTypeOperatorNode = factory.createTypeOperatorNode
692
695
  export const createPrefixUnaryExpression = factory.createPrefixUnaryExpression
693
696
 
694
697
  export { SyntaxKind }
698
+
699
+ // ─── Printer helpers ──────────────────────────────────────────────────────────
700
+
701
+ /**
702
+ * Converts a primitive const value to a TypeScript literal type node.
703
+ * Handles negative numbers via a prefix unary expression.
704
+ */
705
+ export function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined {
706
+ if (format === 'boolean') {
707
+ return createLiteralTypeNode(value === true ? createTrue() : createFalse())
708
+ }
709
+ if (format === 'number' && typeof value === 'number') {
710
+ if (value < 0) {
711
+ return createLiteralTypeNode(createPrefixUnaryExpression(SyntaxKind.MinusToken, createNumericLiteral(Math.abs(value))))
712
+ }
713
+ return createLiteralTypeNode(createNumericLiteral(value))
714
+ }
715
+ return createLiteralTypeNode(createStringLiteral(String(value)))
716
+ }
717
+
718
+ /**
719
+ * Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
720
+ */
721
+ export function dateOrStringNode(node: { representation?: string }): ts.TypeNode {
722
+ return node.representation === 'date' ? createTypeReferenceNode(createIdentifier('Date')) : keywordTypeNodes.string
723
+ }
724
+
725
+ /**
726
+ * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
727
+ */
728
+ export function buildMemberNodes(members: Array<SchemaNode> | undefined, print: (node: SchemaNode) => ts.TypeNode | null | undefined): Array<ts.TypeNode> {
729
+ return (members ?? []).map(print).filter(Boolean)
730
+ }
731
+
732
+ /**
733
+ * Builds a TypeScript tuple type node from an array schema's `items`,
734
+ * applying min/max slice and optional/rest element rules.
735
+ */
736
+ export function buildTupleNode(node: ArraySchemaNode, print: (node: SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
737
+ let items = (node.items ?? []).map(print).filter(Boolean)
738
+
739
+ const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
740
+ const { min, max } = node
741
+
742
+ if (max !== undefined) {
743
+ items = items.slice(0, max)
744
+ if (items.length < max && restNode) {
745
+ items = [...items, ...Array(max - items.length).fill(restNode)]
746
+ }
747
+ }
748
+
749
+ if (min !== undefined) {
750
+ items = items.map((item, i) => (i >= min ? createOptionalTypeNode(item) : item))
751
+ }
752
+
753
+ if (max === undefined && restNode) {
754
+ items.push(createRestTypeNode(createArrayTypeNode(restNode)))
755
+ }
756
+
757
+ return createTupleTypeNode(items)
758
+ }
759
+
760
+ /**
761
+ * Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
762
+ */
763
+ export function buildPropertyType(
764
+ schema: SchemaNode,
765
+ baseType: ts.TypeNode,
766
+ optionalType: 'questionToken' | 'undefined' | 'questionTokenAndUndefined',
767
+ ): ts.TypeNode {
768
+ const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType)
769
+ const meta = syncSchemaRef(schema)
770
+
771
+ let type = baseType
772
+
773
+ if (meta.nullable) {
774
+ type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] })
775
+ }
776
+
777
+ if ((meta.nullish || meta.optional) && addsUndefined) {
778
+ type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.undefined] })
779
+ }
780
+
781
+ return type
782
+ }
783
+
784
+ /**
785
+ * Creates TypeScript index signatures for `additionalProperties` and `patternProperties` on an object schema node.
786
+ */
787
+ export function buildIndexSignatures(
788
+ node: { additionalProperties?: SchemaNode | boolean; patternProperties?: Record<string, SchemaNode> },
789
+ propertyCount: number,
790
+ print: (node: SchemaNode) => ts.TypeNode | null | undefined,
791
+ ): Array<ts.TypeElement> {
792
+ const elements: Array<ts.TypeElement> = []
793
+
794
+ if (node.additionalProperties && node.additionalProperties !== true) {
795
+ const additionalType = print(node.additionalProperties) ?? keywordTypeNodes.unknown
796
+
797
+ elements.push(createIndexSignature(propertyCount > 0 ? keywordTypeNodes.unknown : additionalType))
798
+ } else if (node.additionalProperties === true) {
799
+ elements.push(createIndexSignature(keywordTypeNodes.unknown))
800
+ }
801
+
802
+ if (node.patternProperties) {
803
+ const first = Object.values(node.patternProperties)[0]
804
+ if (first) {
805
+ let patternType = print(first) ?? keywordTypeNodes.unknown
806
+
807
+ if (first.nullable) {
808
+ patternType = createUnionDeclaration({ nodes: [patternType, keywordTypeNodes.null] })
809
+ }
810
+ elements.push(createIndexSignature(patternType))
811
+ }
812
+ }
813
+
814
+ return elements
815
+ }
@@ -11,15 +11,86 @@ import { buildData, buildResponses, buildResponseUnion } from '../utils.ts'
11
11
  export const typeGenerator = defineGenerator<PluginTs>({
12
12
  name: 'typescript',
13
13
  type: 'react',
14
+ Schema({ node, adapter, options, config, resolver }) {
15
+ const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, transformers = [] } = options
16
+
17
+ const transformedNode = transform(node, composeTransformers(...transformers))
18
+
19
+ if (!transformedNode.name) {
20
+ return
21
+ }
22
+
23
+ const root = path.resolve(config.root, config.output.path)
24
+ const mode = getMode(path.resolve(root, output.path))
25
+ // Build a set of schema names that are enums so the ref handler and getImports
26
+ // callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
27
+ const enumSchemaNames = new Set((adapter.rootNode?.schemas ?? []).filter((s) => narrowSchema(s, schemaTypes.enum) && s.name).map((s) => s.name!))
28
+
29
+ function resolveImportName(schemaName: string): string {
30
+ if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
31
+ return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix)
32
+ }
33
+ return resolver.default(schemaName, 'type')
34
+ }
35
+
36
+ const imports = adapter.getImports(transformedNode, (schemaName) => ({
37
+ name: resolveImportName(schemaName),
38
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
39
+ }))
40
+
41
+ const isEnumSchema = !!narrowSchema(transformedNode, schemaTypes.enum)
42
+
43
+ const meta = {
44
+ name:
45
+ ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && isEnumSchema
46
+ ? resolver.resolveEnumKeyName(transformedNode, enumTypeSuffix)
47
+ : resolver.resolveName(transformedNode.name),
48
+ file: resolver.resolveFile({ name: transformedNode.name, extname: '.ts' }, { root, output, group }),
49
+ } as const
50
+
51
+ return (
52
+ <File
53
+ baseName={meta.file.baseName}
54
+ path={meta.file.path}
55
+ meta={meta.file.meta}
56
+ banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
57
+ footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
58
+ >
59
+ {mode === 'split' &&
60
+ imports.map((imp) => (
61
+ <File.Import key={[transformedNode.name, imp.path, imp.isTypeOnly].join('-')} root={meta.file.path} path={imp.path} name={imp.name} isTypeOnly />
62
+ ))}
63
+ <Type
64
+ name={meta.name}
65
+ node={transformedNode}
66
+ enumType={enumType}
67
+ enumTypeSuffix={enumTypeSuffix}
68
+ enumKeyCasing={enumKeyCasing}
69
+ optionalType={optionalType}
70
+ arrayType={arrayType}
71
+ syntaxType={syntaxType}
72
+ resolver={resolver}
73
+ enumSchemaNames={enumSchemaNames}
74
+ />
75
+ </File>
76
+ )
77
+ },
14
78
  Operation({ node, adapter, options, config, resolver }) {
15
79
  const { enumType, enumTypeSuffix, enumKeyCasing, optionalType, arrayType, syntaxType, paramsCasing, group, output, transformers = [] } = options
16
80
 
81
+ const transformedNode = transform(node, composeTransformers(...transformers))
82
+
17
83
  const root = path.resolve(config.root, config.output.path)
18
84
  const mode = getMode(path.resolve(root, output.path))
19
85
 
20
- const file = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
86
+ const params = caseParams(transformedNode.parameters, paramsCasing)
21
87
 
22
- const params = caseParams(node.parameters, paramsCasing)
88
+ const meta = {
89
+ file: resolver.resolveFile(
90
+ { name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
91
+ { root, output, group },
92
+ ),
93
+ } as const
23
94
 
24
95
  // Build a set of schema names that are enums so the ref handler and getImports
25
96
  // callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
@@ -27,29 +98,15 @@ export const typeGenerator = defineGenerator<PluginTs>({
27
98
 
28
99
  function resolveImportName(schemaName: string): string {
29
100
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
30
- return resolver.resolveEnumKeyName({ name: schemaName } as SchemaNode, enumTypeSuffix)
101
+ return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix)
31
102
  }
32
103
  return resolver.default(schemaName, 'type')
33
104
  }
34
105
 
35
- function renderSchemaType({
36
- node: schemaNode,
37
- name,
38
- description,
39
- keysToOmit,
40
- }: {
41
- node: SchemaNode | null
42
- name: string
43
- description?: string
44
- keysToOmit?: Array<string>
45
- }) {
46
- if (!schemaNode) {
47
- return null
48
- }
106
+ function renderSchemaType({ schema, name, keysToOmit }: { schema: SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
107
+ if (!schema) return null
49
108
 
50
- const transformedNode = transform(schemaNode, composeTransformers(...transformers))
51
-
52
- const imports = adapter.getImports(transformedNode, (schemaName) => ({
109
+ const imports = adapter.getImports(schema, (schemaName) => ({
53
110
  name: resolveImportName(schemaName),
54
111
  path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
55
112
  }))
@@ -57,11 +114,12 @@ export const typeGenerator = defineGenerator<PluginTs>({
57
114
  return (
58
115
  <>
59
116
  {mode === 'split' &&
60
- imports.map((imp) => <File.Import key={[name, imp.path, imp.isTypeOnly].join('-')} root={file.path} path={imp.path} name={imp.name} isTypeOnly />)}
117
+ imports.map((imp) => (
118
+ <File.Import key={[name, imp.path, imp.isTypeOnly].join('-')} root={meta.file.path} path={imp.path} name={imp.name} isTypeOnly />
119
+ ))}
61
120
  <Type
62
121
  name={name}
63
- node={transformedNode}
64
- description={description}
122
+ node={schema}
65
123
  enumType={enumType}
66
124
  enumTypeSuffix={enumTypeSuffix}
67
125
  enumKeyCasing={enumKeyCasing}
@@ -78,50 +136,55 @@ export const typeGenerator = defineGenerator<PluginTs>({
78
136
 
79
137
  const paramTypes = params.map((param) =>
80
138
  renderSchemaType({
81
- node: param.schema,
82
- name: resolver.resolveParamName(node, param),
139
+ schema: param.schema,
140
+ name: resolver.resolveParamName(transformedNode, param),
83
141
  }),
84
142
  )
85
143
 
86
- const requestType = node.requestBody?.schema
144
+ const requestType = transformedNode.requestBody?.schema
87
145
  ? renderSchemaType({
88
- node: node.requestBody.schema,
89
- name: resolver.resolveDataName(node),
90
- description: node.requestBody.description ?? node.requestBody.schema.description,
91
- keysToOmit: node.requestBody.keysToOmit,
146
+ schema: {
147
+ ...transformedNode.requestBody.schema,
148
+ description: transformedNode.requestBody.description ?? transformedNode.requestBody.schema.description,
149
+ },
150
+ name: resolver.resolveDataName(transformedNode),
151
+ keysToOmit: transformedNode.requestBody.keysToOmit,
92
152
  })
93
153
  : null
94
154
 
95
- const responseTypes = node.responses.map((res) =>
155
+ const responseTypes = transformedNode.responses.map((res) =>
96
156
  renderSchemaType({
97
- node: res.schema,
98
- name: resolver.resolveResponseStatusName(node, res.statusCode),
99
- description: res.description,
157
+ schema: res.schema,
158
+ name: resolver.resolveResponseStatusName(transformedNode, res.statusCode),
100
159
  keysToOmit: res.keysToOmit,
101
160
  }),
102
161
  )
103
162
 
104
163
  const dataType = renderSchemaType({
105
- node: buildData({ node: { ...node, parameters: params }, resolver }),
106
- name: resolver.resolveRequestConfigName(node),
164
+ schema: buildData({ ...transformedNode, parameters: params }, { resolver }),
165
+ name: resolver.resolveRequestConfigName(transformedNode),
107
166
  })
108
167
 
109
168
  const responsesType = renderSchemaType({
110
- node: buildResponses({ node, resolver }),
111
- name: resolver.resolveResponsesName(node),
169
+ schema: buildResponses(transformedNode, { resolver }),
170
+ name: resolver.resolveResponsesName(transformedNode),
112
171
  })
113
172
 
114
173
  const responseType = renderSchemaType({
115
- node: buildResponseUnion({ node, resolver }),
116
- name: resolver.resolveResponseName(node),
117
- description: 'Union of all possible responses',
174
+ schema: transformedNode.responses.some((res) => res.schema)
175
+ ? {
176
+ ...buildResponseUnion(transformedNode, { resolver })!,
177
+ description: 'Union of all possible responses',
178
+ }
179
+ : null,
180
+ name: resolver.resolveResponseName(transformedNode),
118
181
  })
119
182
 
120
183
  return (
121
184
  <File
122
- baseName={file.baseName}
123
- path={file.path}
124
- meta={file.meta}
185
+ baseName={meta.file.baseName}
186
+ path={meta.file.path}
187
+ meta={meta.file.meta}
125
188
  banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
126
189
  footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
127
190
  >
@@ -134,68 +197,4 @@ export const typeGenerator = defineGenerator<PluginTs>({
134
197
  </File>
135
198
  )
136
199
  },
137
- Schema({ node, adapter, options, config, resolver }) {
138
- const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, transformers = [] } = options
139
-
140
- const root = path.resolve(config.root, config.output.path)
141
- const mode = getMode(path.resolve(root, output.path))
142
-
143
- if (!node.name) {
144
- return
145
- }
146
-
147
- const transformedNode = transform(node, composeTransformers(...transformers))
148
-
149
- // Build a set of schema names that are enums so the ref handler and getImports
150
- // callback can use the suffixed type name (e.g. `StatusKey`) for those refs.
151
- const enumSchemaNames = new Set((adapter.rootNode?.schemas ?? []).filter((s) => narrowSchema(s, schemaTypes.enum) && s.name).map((s) => s.name!))
152
-
153
- function resolveImportName(schemaName: string): string {
154
- if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) {
155
- return resolver.resolveEnumKeyName({ name: schemaName } as SchemaNode, enumTypeSuffix)
156
- }
157
- return resolver.default(schemaName, 'type')
158
- }
159
-
160
- const imports = adapter.getImports(transformedNode, (schemaName) => ({
161
- name: resolveImportName(schemaName),
162
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
163
- }))
164
-
165
- const isEnumSchema = !!narrowSchema(node, schemaTypes.enum)
166
-
167
- const name = ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && isEnumSchema ? resolver.resolveEnumKeyName(node, enumTypeSuffix) : resolver.resolveName(node.name)
168
-
169
- const type = {
170
- name,
171
- file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
172
- } as const
173
-
174
- return (
175
- <File
176
- baseName={type.file.baseName}
177
- path={type.file.path}
178
- meta={type.file.meta}
179
- banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
180
- footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
181
- >
182
- {mode === 'split' &&
183
- imports.map((imp) => (
184
- <File.Import key={[node.name, imp.path, imp.isTypeOnly].join('-')} root={type.file.path} path={imp.path} name={imp.name} isTypeOnly />
185
- ))}
186
- <Type
187
- name={type.name}
188
- node={transformedNode}
189
- enumType={enumType}
190
- enumTypeSuffix={enumTypeSuffix}
191
- enumKeyCasing={enumKeyCasing}
192
- optionalType={optionalType}
193
- arrayType={arrayType}
194
- syntaxType={syntaxType}
195
- resolver={resolver}
196
- enumSchemaNames={enumSchemaNames}
197
- />
198
- </File>
199
- )
200
- },
201
200
  })