@kubb/plugin-ts 5.0.0-alpha.11 → 5.0.0-alpha.12

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.
Files changed (43) hide show
  1. package/dist/{components-CRu8IKY3.js → Type-CX1HRooG.js} +377 -365
  2. package/dist/Type-CX1HRooG.js.map +1 -0
  3. package/dist/Type-Cat0_htq.cjs +808 -0
  4. package/dist/Type-Cat0_htq.cjs.map +1 -0
  5. package/dist/components.cjs +3 -2
  6. package/dist/components.d.ts +40 -9
  7. package/dist/components.js +2 -2
  8. package/dist/generators-CLuCmfUz.js +532 -0
  9. package/dist/generators-CLuCmfUz.js.map +1 -0
  10. package/dist/generators-DWBU-MuW.cjs +536 -0
  11. package/dist/generators-DWBU-MuW.cjs.map +1 -0
  12. package/dist/generators.cjs +2 -3
  13. package/dist/generators.d.ts +3 -503
  14. package/dist/generators.js +2 -2
  15. package/dist/index.cjs +308 -4
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.ts +26 -21
  18. package/dist/index.js +305 -2
  19. package/dist/index.js.map +1 -0
  20. package/dist/{types-mSXmB8WU.d.ts → types-BA1ZCQ5p.d.ts} +73 -57
  21. package/package.json +5 -5
  22. package/src/components/{v2/Enum.tsx → Enum.tsx} +27 -11
  23. package/src/components/Type.tsx +23 -141
  24. package/src/components/index.ts +1 -0
  25. package/src/generators/index.ts +0 -1
  26. package/src/generators/typeGenerator.tsx +189 -413
  27. package/src/generators/utils.ts +298 -0
  28. package/src/index.ts +1 -1
  29. package/src/plugin.ts +80 -126
  30. package/src/printer.ts +15 -4
  31. package/src/resolverTs.ts +109 -1
  32. package/src/types.ts +68 -52
  33. package/dist/components-CRu8IKY3.js.map +0 -1
  34. package/dist/components-DeNDKlzf.cjs +0 -982
  35. package/dist/components-DeNDKlzf.cjs.map +0 -1
  36. package/dist/plugin-CJ29AwE2.cjs +0 -1320
  37. package/dist/plugin-CJ29AwE2.cjs.map +0 -1
  38. package/dist/plugin-D60XNJSD.js +0 -1267
  39. package/dist/plugin-D60XNJSD.js.map +0 -1
  40. package/src/components/v2/Type.tsx +0 -59
  41. package/src/generators/v2/typeGenerator.tsx +0 -167
  42. package/src/generators/v2/utils.ts +0 -140
  43. package/src/parser.ts +0 -389
@@ -0,0 +1,298 @@
1
+ import { pascalCase } from '@internals/utils'
2
+ import { createProperty, createSchema, narrowSchema, transform } from '@kubb/ast'
3
+ import type { OperationNode, ParameterNode, SchemaNode } from '@kubb/ast/types'
4
+ import type { ResolverTs } from '../types.ts'
5
+
6
+ type BuildParamsSchemaOptions = {
7
+ params: Array<ParameterNode>
8
+ node: OperationNode
9
+ resolver: ResolverTs
10
+ }
11
+
12
+ /**
13
+ * Builds an `ObjectSchemaNode` for a group of parameters (path/query/header).
14
+ * Each property is a `ref` schema pointing to the individually-resolved parameter type.
15
+ * The ref name includes the parameter location so generated type names follow
16
+ * the `<OperationId><Location><ParamName>` convention.
17
+ */
18
+ export function buildParamsSchema({ params, node, resolver }: BuildParamsSchemaOptions): SchemaNode {
19
+ return createSchema({
20
+ type: 'object',
21
+ properties: params.map((param) =>
22
+ createProperty({
23
+ name: param.name,
24
+ schema: createSchema({
25
+ type: 'ref',
26
+ name: resolver.resolveParamName(node, param),
27
+ optional: !param.required,
28
+ }),
29
+ }),
30
+ ),
31
+ })
32
+ }
33
+
34
+ type BuildOperationSchemaOptions = {
35
+ node: OperationNode
36
+ resolver: ResolverTs
37
+ }
38
+
39
+ /**
40
+ * Builds an `ObjectSchemaNode` representing the `<OperationId>RequestConfig` type:
41
+ * - `data` → request body ref (optional) or `never`
42
+ * - `pathParams` → inline object of path param refs, or `never`
43
+ * - `queryParams` → inline object of query param refs (optional), or `never`
44
+ * - `headerParams` → inline object of header param refs (optional), or `never`
45
+ * - `url` → Express-style template literal (plugin-ts extension, handled by printer)
46
+ */
47
+ export function buildDataSchemaNode({ node, resolver }: BuildOperationSchemaOptions): SchemaNode {
48
+ const pathParams = node.parameters.filter((p) => p.in === 'path')
49
+ const queryParams = node.parameters.filter((p) => p.in === 'query')
50
+ const headerParams = node.parameters.filter((p) => p.in === 'header')
51
+
52
+ return createSchema({
53
+ type: 'object',
54
+ deprecated: node.deprecated,
55
+ properties: [
56
+ createProperty({
57
+ name: 'data',
58
+ schema: node.requestBody?.schema
59
+ ? createSchema({
60
+ type: 'ref',
61
+ name: resolver.resolveDataTypedName(node),
62
+ optional: true,
63
+ })
64
+ : createSchema({ type: 'never', optional: true }),
65
+ }),
66
+ createProperty({
67
+ name: 'pathParams',
68
+ schema: pathParams.length > 0 ? buildParamsSchema({ params: pathParams, node, resolver }) : createSchema({ type: 'never', optional: true }),
69
+ }),
70
+ createProperty({
71
+ name: 'queryParams',
72
+ schema:
73
+ queryParams.length > 0
74
+ ? createSchema({ ...buildParamsSchema({ params: queryParams, node, resolver }), optional: true })
75
+ : createSchema({ type: 'never', optional: true }),
76
+ }),
77
+ createProperty({
78
+ name: 'headerParams',
79
+ schema:
80
+ headerParams.length > 0
81
+ ? createSchema({ ...buildParamsSchema({ params: headerParams, node, resolver }), optional: true })
82
+ : createSchema({ type: 'never', optional: true }),
83
+ }),
84
+ createProperty({
85
+ name: 'url',
86
+ schema: createSchema({ type: 'url', path: node.path }),
87
+ }),
88
+ ],
89
+ })
90
+ }
91
+
92
+ /**
93
+ * Builds an `ObjectSchemaNode` representing `<OperationId>Responses` — keyed by HTTP status code.
94
+ * Numeric status codes produce unquoted numeric keys (e.g. `200:`).
95
+ * All responses are included; those without a schema are represented as a ref to a `never` type.
96
+ */
97
+ export function buildResponsesSchemaNode({ node, resolver }: BuildOperationSchemaOptions): SchemaNode | null {
98
+ if (node.responses.length === 0) {
99
+ return null
100
+ }
101
+
102
+ return createSchema({
103
+ type: 'object',
104
+ properties: node.responses.map((res) =>
105
+ createProperty({
106
+ name: String(res.statusCode),
107
+ schema: createSchema({
108
+ type: 'ref',
109
+ name: resolver.resolveResponseStatusTypedName(node, res.statusCode),
110
+ }),
111
+ }),
112
+ ),
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Builds a `UnionSchemaNode` representing `<OperationId>Response` — all response types in union format.
118
+ * Returns `null` when the operation has no responses with schemas.
119
+ */
120
+ export function buildResponseUnionSchemaNode({ node, resolver }: BuildOperationSchemaOptions): SchemaNode | null {
121
+ const responsesWithSchema = node.responses.filter((res) => res.schema)
122
+
123
+ if (responsesWithSchema.length === 0) {
124
+ return null
125
+ }
126
+
127
+ return createSchema({
128
+ type: 'union',
129
+ members: responsesWithSchema.map((res) =>
130
+ createSchema({
131
+ type: 'ref',
132
+ name: resolver.resolveResponseStatusTypedName(node, res.statusCode),
133
+ }),
134
+ ),
135
+ })
136
+ }
137
+
138
+ type BuildGroupedParamsSchemaOptions = {
139
+ params: Array<ParameterNode>
140
+ /**
141
+ * Parent type name (e.g. `FindPetsByStatusQueryParams`) used to derive enum names
142
+ * for inline enum properties (e.g. `FindPetsByStatusQueryParamsStatusEnum`).
143
+ */
144
+ parentName?: string
145
+ }
146
+
147
+ /**
148
+ * Builds an `ObjectSchemaNode` for a grouped parameters type (path/query/header) in legacy mode.
149
+ * Each property directly embeds the parameter's schema inline (not a ref).
150
+ * Used to generate `<OperationId>PathParams`, `<OperationId>QueryParams`, `<OperationId>HeaderParams`.
151
+ * @deprecated Legacy only — will be removed in v6.
152
+ */
153
+ export function buildGroupedParamsSchema({ params, parentName }: BuildGroupedParamsSchemaOptions): SchemaNode {
154
+ return createSchema({
155
+ type: 'object',
156
+ properties: params.map((param) => {
157
+ let schema = { ...param.schema, optional: !param.required } as SchemaNode
158
+ // Name unnamed enum properties so they are emitted as enum declarations
159
+ if (narrowSchema(schema, 'enum') && !schema.name && parentName) {
160
+ schema = { ...schema, name: pascalCase([parentName, param.name, 'enum'].join(' ')) }
161
+ }
162
+ return createProperty({
163
+ name: param.name,
164
+ schema,
165
+ })
166
+ }),
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Builds the legacy wrapper `ObjectSchemaNode` for `<OperationId>Mutation` / `<OperationId>Query`.
172
+ * Structure: `{ Response, Request (mutation) | QueryParams (query), Errors }`.
173
+ * Mirrors the v4 naming convention where this type acts as a namespace for the operation's shapes.
174
+ *
175
+ * @deprecated Legacy only — will be removed in v6.
176
+ */
177
+ export function buildLegacyResponsesSchemaNode({ node, resolver }: BuildOperationSchemaOptions): SchemaNode | null {
178
+ const isGet = node.method.toLowerCase() === 'get'
179
+ const successResponses = node.responses.filter((res) => {
180
+ const code = Number(res.statusCode)
181
+ return !Number.isNaN(code) && code >= 200 && code < 300
182
+ })
183
+ const errorResponses = node.responses.filter((res) => res.statusCode === 'default' || Number(res.statusCode) >= 400)
184
+
185
+ const responseSchema =
186
+ successResponses.length > 0
187
+ ? successResponses.length === 1
188
+ ? createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, successResponses[0]!.statusCode) })
189
+ : createSchema({
190
+ type: 'union',
191
+ members: successResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, res.statusCode) })),
192
+ })
193
+ : createSchema({ type: 'any' })
194
+
195
+ const errorsSchema =
196
+ errorResponses.length > 0
197
+ ? errorResponses.length === 1
198
+ ? createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, errorResponses[0]!.statusCode) })
199
+ : createSchema({
200
+ type: 'union',
201
+ members: errorResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, res.statusCode) })),
202
+ })
203
+ : createSchema({ type: 'any' })
204
+
205
+ const properties = [createProperty({ name: 'Response', schema: responseSchema })]
206
+
207
+ if (!isGet && node.requestBody?.schema) {
208
+ properties.push(
209
+ createProperty({
210
+ name: 'Request',
211
+ schema: createSchema({ type: 'ref', name: resolver.resolveDataTypedName(node) }),
212
+ }),
213
+ )
214
+ } else if (isGet && node.parameters.some((p) => p.in === 'query')) {
215
+ properties.push(
216
+ createProperty({
217
+ name: 'QueryParams',
218
+ schema: createSchema({ type: 'ref', name: resolver.resolveQueryParamsTypedName!(node) }),
219
+ }),
220
+ )
221
+ }
222
+
223
+ if (node.parameters.some((p) => p.in === 'path') && resolver.resolvePathParamsTypedName) {
224
+ properties.push(
225
+ createProperty({
226
+ name: 'PathParams',
227
+ schema: createSchema({ type: 'ref', name: resolver.resolvePathParamsTypedName(node) }),
228
+ }),
229
+ )
230
+ }
231
+
232
+ if (node.parameters.some((p) => p.in === 'header') && resolver.resolveHeaderParamsTypedName) {
233
+ properties.push(
234
+ createProperty({
235
+ name: 'HeaderParams',
236
+ schema: createSchema({ type: 'ref', name: resolver.resolveHeaderParamsTypedName(node) }),
237
+ }),
238
+ )
239
+ }
240
+
241
+ properties.push(createProperty({ name: 'Errors', schema: errorsSchema }))
242
+
243
+ return createSchema({ type: 'object', properties })
244
+ }
245
+
246
+ /**
247
+ * Builds the legacy response union for `<OperationId>MutationResponse` / `<OperationId>QueryResponse`.
248
+ * In legacy mode this is the **success** response only (not the full union including errors).
249
+ * Returns an `any` schema when there is no success response, matching v4 behavior.
250
+ * @deprecated Legacy only — will be removed in v6.
251
+ */
252
+ export function buildLegacyResponseUnionSchemaNode({ node, resolver }: BuildOperationSchemaOptions): SchemaNode {
253
+ const successResponses = node.responses.filter((res) => {
254
+ const code = Number(res.statusCode)
255
+ return !Number.isNaN(code) && code >= 200 && code < 300
256
+ })
257
+
258
+ if (successResponses.length === 0) {
259
+ return createSchema({ type: 'any' })
260
+ }
261
+
262
+ if (successResponses.length === 1) {
263
+ return createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, successResponses[0]!.statusCode) })
264
+ }
265
+
266
+ return createSchema({
267
+ type: 'union',
268
+ members: successResponses.map((res) => createSchema({ type: 'ref', name: resolver.resolveResponseStatusTypedName(node, res.statusCode) })),
269
+ })
270
+ }
271
+
272
+ /**
273
+ * Names unnamed enum nodes within a schema tree based on the parent type name.
274
+ * Used in legacy mode to ensure inline enums in response/request schemas get
275
+ * extracted as named enum declarations (e.g. `DeletePet200Enum`).
276
+ *
277
+ * @deprecated Legacy only — will be removed in v6.
278
+ */
279
+ export function nameUnnamedEnums(node: SchemaNode, parentName: string): SchemaNode {
280
+ return transform(node, {
281
+ schema(n) {
282
+ if (n.type === 'enum' && !n.name) {
283
+ return { ...n, name: pascalCase([parentName, 'enum'].join(' ')) }
284
+ }
285
+ return undefined
286
+ },
287
+ property(p) {
288
+ const enumNode = narrowSchema(p.schema, 'enum')
289
+ if (enumNode && !enumNode.name) {
290
+ return {
291
+ ...p,
292
+ schema: { ...enumNode, name: pascalCase([parentName, p.name, 'enum'].join(' ')) },
293
+ }
294
+ }
295
+ return undefined
296
+ },
297
+ })
298
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { pluginTs, pluginTsName } from './plugin.ts'
2
- export { resolverTs } from './resolverTs.ts'
2
+ export { resolverTs, resolverTsLegacy } from './resolverTs.ts'
3
3
  export type { PluginTs } from './types.ts'
package/src/plugin.ts CHANGED
@@ -2,9 +2,8 @@ import path from 'node:path'
2
2
  import { camelCase } from '@internals/utils'
3
3
  import { walk } from '@kubb/ast'
4
4
  import { createPlugin, type Group, getBarrelFiles, getMode, renderOperation, renderSchema } from '@kubb/core'
5
- import { OperationGenerator, pluginOasName, SchemaGenerator } from '@kubb/plugin-oas'
6
- import { typeGenerator, typeGeneratorV2 } from './generators'
7
- import { resolverTs } from './resolverTs.ts'
5
+ import { typeGenerator } from './generators'
6
+ import { resolverTs, resolverTsLegacy } from './resolverTs.ts'
8
7
  import type { PluginTs } from './types.ts'
9
8
 
10
9
  export const pluginTsName = 'plugin-ts' satisfies PluginTs['name']
@@ -18,45 +17,49 @@ export const pluginTs = createPlugin<PluginTs>((options) => {
18
17
  override = [],
19
18
  enumType = 'asConst',
20
19
  enumKeyCasing = 'none',
21
- enumSuffix = 'enum',
22
- dateType = 'string',
23
- integerType = 'number',
24
- unknownType = 'any',
25
20
  optionalType = 'questionToken',
26
21
  arrayType = 'array',
27
- emptySchemaType = unknownType,
28
22
  syntaxType = 'type',
29
23
  transformers = {},
30
24
  paramsCasing,
31
- generators = [typeGenerator, typeGeneratorV2].filter(Boolean),
32
- contentType,
33
- UNSTABLE_NAMING,
25
+ generators = [typeGenerator].filter(Boolean),
26
+ legacy = false,
34
27
  } = options
35
28
 
36
- // @deprecated Will be removed in v5 when collisionDetection defaults to true
37
- const usedEnumNames = {}
29
+ const baseResolver = legacy ? resolverTsLegacy : resolverTs
30
+
31
+ // When a `transformers.name` callback is provided, wrap the resolver so that
32
+ // every name produced by `default()` (and therefore by every helper that calls
33
+ // `this.default(...)`) flows through the user's transformer.
34
+ const resolver: typeof baseResolver = transformers?.name
35
+ ? {
36
+ ...baseResolver,
37
+ default(name, type) {
38
+ const resolved = baseResolver.default(name, type)
39
+
40
+ return transformers.name!(resolved, type) || resolved
41
+ },
42
+ }
43
+ : baseResolver
44
+
45
+ let resolveNameWarning = false
38
46
 
39
47
  return {
40
48
  name: pluginTsName,
41
49
  options: {
42
50
  output,
43
51
  transformers,
44
- dateType,
45
- integerType,
46
52
  optionalType,
47
53
  arrayType,
48
54
  enumType,
49
55
  enumKeyCasing,
50
- enumSuffix,
51
- unknownType,
52
- emptySchemaType,
53
56
  syntaxType,
54
57
  group,
55
58
  override,
56
59
  paramsCasing,
57
- usedEnumNames,
60
+ legacy,
61
+ resolver,
58
62
  },
59
- pre: [pluginOasName],
60
63
  resolvePath(baseName, pathMode, options) {
61
64
  const root = path.resolve(this.config.root, this.config.output.path)
62
65
  const mode = pathMode ?? getMode(path.resolve(root, output.path))
@@ -92,13 +95,12 @@ export const pluginTs = createPlugin<PluginTs>((options) => {
92
95
  return path.resolve(root, output.path, baseName)
93
96
  },
94
97
  resolveName(name, type) {
95
- const resolvedName = resolverTs.default(name, type)
96
-
97
- if (type) {
98
- return transformers?.name?.(resolvedName, type) || resolvedName
98
+ if (!resolveNameWarning) {
99
+ this.driver.events.emit('warn', 'Do not use resolveName for pluginTs, use resolverTs instead')
100
+ resolveNameWarning = true
99
101
  }
100
102
 
101
- return resolvedName
103
+ return resolver.default(name, type)
102
104
  },
103
105
  async install() {
104
106
  const { config, fabric, plugin, adapter, rootNode, driver, openInStudio } = this
@@ -106,114 +108,66 @@ export const pluginTs = createPlugin<PluginTs>((options) => {
106
108
  const root = path.resolve(config.root, config.output.path)
107
109
  const mode = getMode(path.resolve(root, output.path))
108
110
 
109
- if (adapter) {
110
- await openInStudio({ ast: true })
111
-
112
- await walk(
113
- rootNode,
114
- {
115
- async schema(schemaNode) {
116
- const writeTasks = generators.map(async (generator) => {
117
- if (generator.type === 'react' && generator.version === '2') {
118
- const options = resolverTs.resolveOptions(schemaNode, { options: plugin.options, exclude, include, override })
119
-
120
- if (options === null) {
121
- return
122
- }
123
-
124
- await renderSchema(schemaNode, {
125
- options,
126
- adapter,
127
- config,
128
- fabric,
129
- Component: generator.Schema,
130
- plugin,
131
- driver,
132
- mode,
133
- })
134
- }
135
- })
136
-
137
- await Promise.all(writeTasks)
138
- },
139
- async operation(operationNode) {
140
- const writeTasks = generators.map(async (generator) => {
141
- if (generator.type === 'react' && generator.version === '2') {
142
- const options = resolverTs.resolveOptions(operationNode, { options: plugin.options, exclude, include, override })
143
-
144
- if (options === null) {
145
- return
146
- }
147
-
148
- await renderOperation(operationNode, {
149
- options,
150
- adapter,
151
- config,
152
- fabric,
153
- Component: generator.Operation,
154
- plugin,
155
- driver,
156
- mode,
157
- })
158
- }
159
- })
111
+ if (!adapter) {
112
+ throw new Error('Plugin cannot work without adapter being set')
113
+ }
160
114
 
161
- await Promise.all(writeTasks)
162
- },
163
- },
164
- { depth: 'shallow' },
165
- )
115
+ await openInStudio({ ast: true })
166
116
 
167
- const barrelFiles = await getBarrelFiles(this.fabric.files, {
168
- type: output.barrelType ?? 'named',
169
- root,
170
- output,
171
- meta: {
172
- pluginName: this.plugin.name,
173
- },
174
- })
117
+ await walk(
118
+ rootNode,
119
+ {
120
+ async schema(schemaNode) {
121
+ const writeTasks = generators.map(async (generator) => {
122
+ if (generator.type === 'react' && generator.version === '2') {
123
+ const options = resolver.resolveOptions(schemaNode, { options: plugin.options, exclude, include, override })
175
124
 
176
- await this.upsertFile(...barrelFiles)
125
+ if (options === null) {
126
+ return
127
+ }
177
128
 
178
- return
179
- }
129
+ await renderSchema(schemaNode, {
130
+ options,
131
+ adapter,
132
+ config,
133
+ fabric,
134
+ Component: generator.Schema,
135
+ plugin,
136
+ driver,
137
+ mode,
138
+ })
139
+ }
140
+ })
180
141
 
181
- // v1 flow
182
-
183
- const oas = await this.getOas()
184
-
185
- const schemaGenerator = new SchemaGenerator(this.plugin.options, {
186
- fabric: this.fabric,
187
- oas,
188
- driver: this.driver,
189
- events: this.events,
190
- plugin: this.plugin,
191
- contentType,
192
- include: undefined,
193
- override,
194
- mode,
195
- output: output.path,
196
- })
142
+ await Promise.all(writeTasks)
143
+ },
144
+ async operation(operationNode) {
145
+ const writeTasks = generators.map(async (generator) => {
146
+ if (generator.type === 'react' && generator.version === '2') {
147
+ const options = resolver.resolveOptions(operationNode, { options: plugin.options, exclude, include, override })
197
148
 
198
- const schemaFiles = await schemaGenerator.build(...generators)
199
- await this.upsertFile(...schemaFiles)
200
-
201
- const operationGenerator = new OperationGenerator(this.plugin.options, {
202
- fabric: this.fabric,
203
- oas,
204
- driver: this.driver,
205
- events: this.events,
206
- plugin: this.plugin,
207
- contentType,
208
- exclude,
209
- include,
210
- override,
211
- mode,
212
- UNSTABLE_NAMING,
213
- })
149
+ if (options === null) {
150
+ return
151
+ }
152
+
153
+ await renderOperation(operationNode, {
154
+ options,
155
+ adapter,
156
+ config,
157
+ fabric,
158
+ Component: generator.Operation,
159
+ plugin,
160
+ driver,
161
+ mode,
162
+ })
163
+ }
164
+ })
214
165
 
215
- const operationFiles = await operationGenerator.build(...generators)
216
- await this.upsertFile(...operationFiles)
166
+ await Promise.all(writeTasks)
167
+ },
168
+ },
169
+ { depth: 'shallow' },
170
+ )
217
171
 
218
172
  const barrelFiles = await getBarrelFiles(this.fabric.files, {
219
173
  type: output.barrelType ?? 'named',
package/src/printer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { jsStringEscape, pascalCase, stringify } from '@internals/utils'
1
+ import { jsStringEscape, stringify } from '@internals/utils'
2
2
  import { isPlainStringType } from '@kubb/ast'
3
3
  import type { ArraySchemaNode, SchemaNode } from '@kubb/ast/types'
4
4
  import type { PrinterFactoryOptions } from '@kubb/core'
@@ -6,7 +6,7 @@ import { definePrinter } from '@kubb/core'
6
6
  import type ts from 'typescript'
7
7
  import { ENUM_TYPES_WITH_KEY_SUFFIX, OPTIONAL_ADDS_QUESTION_TOKEN, OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
8
8
  import * as factory from './factory.ts'
9
- import type { PluginTs } from './types.ts'
9
+ import type { PluginTs, ResolverTs } from './types.ts'
10
10
 
11
11
  type TsOptions = {
12
12
  /**
@@ -41,6 +41,10 @@ type TsOptions = {
41
41
  * Forces type-alias syntax even when `syntaxType` is `'interface'`.
42
42
  */
43
43
  keysToOmit?: Array<string>
44
+ /**
45
+ * Resolver used to transform raw schema names into valid TypeScript identifiers.
46
+ */
47
+ resolver: ResolverTs
44
48
  }
45
49
 
46
50
  /**
@@ -235,7 +239,14 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
235
239
  if (!node.name) {
236
240
  return undefined
237
241
  }
238
- return factory.createTypeReferenceNode(node.name, undefined)
242
+ // Parser-generated refs (with $ref) carry raw schema names that need resolving.
243
+ // Use the canonical name from the $ref path — node.name may have been overridden
244
+ // (e.g. by single-member allOf flatten using the property-derived child name).
245
+ // Inline refs (without $ref) from utils already carry resolved type names.
246
+ const refName = node.ref ? (node.ref.split('/').at(-1) ?? node.name) : node.name
247
+ const name = node.ref ? this.options.resolver.default(refName, 'type') : refName
248
+
249
+ return factory.createTypeReferenceNode(name, undefined)
239
250
  },
240
251
  enum(node) {
241
252
  const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
@@ -249,7 +260,7 @@ export const printerTs = definePrinter<TsPrinter>((options) => {
249
260
  return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined
250
261
  }
251
262
 
252
- const resolvedName = pascalCase(node.name)
263
+ const resolvedName = this.options.resolver.default(node.name, 'type')
253
264
  const typeName = ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) ? `${resolvedName}Key` : resolvedName
254
265
 
255
266
  return factory.createTypeReferenceNode(typeName, undefined)