@kubb/plugin-ts 5.0.0-alpha.3 → 5.0.0-alpha.5

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.
@@ -1,45 +1,46 @@
1
+ import { applyParamsCasing } from '@kubb/ast'
1
2
  import type { SchemaNode } from '@kubb/ast/types'
3
+ import { defineGenerator } from '@kubb/core'
2
4
  import { useKubb } from '@kubb/core/hooks'
3
- import { createReactGenerator } from '@kubb/plugin-oas/generators'
4
5
  import { File } from '@kubb/react-fabric'
5
6
  import { Type } from '../../components/v2/Type.tsx'
6
7
  import type { PluginTs } from '../../types'
8
+ import { buildDataSchemaNode, buildResponsesSchemaNode, buildResponseUnionSchemaNode } from './utils.ts'
7
9
 
8
- export const typeGenerator = createReactGenerator<PluginTs, '2'>({
10
+ export const typeGenerator = defineGenerator<PluginTs>({
9
11
  name: 'typescript',
10
- version: '2',
12
+ type: 'react',
11
13
  Operation({ node, adapter, options }) {
12
- const { enumType, enumKeyCasing, optionalType, arrayType, syntaxType } = options
14
+ const { enumType, enumKeyCasing, optionalType, arrayType, syntaxType, paramsCasing, mapper } = options
15
+ const { mode, getFile, resolveName } = useKubb<PluginTs>()
13
16
 
14
- const { plugin, mode, getFile, resolveName } = useKubb<PluginTs>()
17
+ const file = getFile({ name: node.operationId, extname: '.ts', mode })
18
+ const params = applyParamsCasing(node.parameters, paramsCasing)
15
19
 
16
- const file = getFile({
17
- name: node.operationId,
18
- pluginName: plugin.name,
19
- extname: '.ts',
20
- mode,
21
- })
20
+ function renderSchemaType({
21
+ node: schemaNode,
22
+ name,
23
+ typedName,
24
+ description,
25
+ }: {
26
+ node: SchemaNode | null
27
+ name: string
28
+ typedName: string
29
+ description?: string
30
+ }) {
31
+ if (!schemaNode) {
32
+ return null
33
+ }
22
34
 
23
- function renderSchemaType({ node: schemaNode, name, typedName, description }: { node: SchemaNode; name: string; typedName: string; description?: string }) {
24
35
  const imports = adapter.getImports(schemaNode, (schemaName) => ({
25
- name: resolveName({
26
- name: schemaName,
27
- pluginName: plugin.name,
28
- type: 'type',
29
- }),
30
- path: getFile({
31
- name: schemaName,
32
- pluginName: plugin.name,
33
- extname: '.ts',
34
- mode,
35
- }).path,
36
+ name: resolveName({ name: schemaName, type: 'type' }),
37
+ path: getFile({ name: schemaName, extname: '.ts', mode }).path,
36
38
  }))
37
39
 
38
40
  return (
39
41
  <>
40
42
  {mode === 'split' &&
41
43
  imports.map((imp) => <File.Import key={[name, imp.path, imp.isTypeOnly].join('-')} root={file.path} path={imp.path} name={imp.name} isTypeOnly />)}
42
-
43
44
  <Type
44
45
  name={name}
45
46
  typedName={typedName}
@@ -50,127 +51,93 @@ export const typeGenerator = createReactGenerator<PluginTs, '2'>({
50
51
  optionalType={optionalType}
51
52
  arrayType={arrayType}
52
53
  syntaxType={syntaxType}
54
+ mapper={mapper}
53
55
  />
54
56
  </>
55
57
  )
56
58
  }
57
59
 
58
- // Parameter types each parameter rendered as its own type
59
- const paramTypes = node.parameters.map((param) => {
60
- const name = resolveName({
61
- name: `${node.operationId} ${param.name}`,
62
- pluginName: plugin.name,
63
- type: 'function',
64
- })
65
- const typedName = resolveName({
66
- name: `${node.operationId} ${param.name}`,
67
- pluginName: plugin.name,
68
- type: 'type',
69
- })
70
-
71
- return renderSchemaType({ node: param.schema, name, typedName })
72
- })
60
+ const paramTypes = params.map((param) =>
61
+ renderSchemaType({
62
+ node: param.schema,
63
+ name: resolveName({ name: `${node.operationId} ${param.name}`, type: 'function' }),
64
+ typedName: resolveName({ name: `${node.operationId} ${param.name}`, type: 'type' }),
65
+ }),
66
+ )
73
67
 
74
- // Response types
75
68
  const responseTypes = node.responses
76
69
  .filter((res) => res.schema)
77
- .map((res) => {
78
- const schemaNode = res.schema!
79
- const responseName = `${node.operationId} ${res.statusCode}`
80
- const resolvedName = resolveName({
81
- name: responseName,
82
- pluginName: plugin.name,
83
- type: 'function',
84
- })
85
- const typedName = resolveName({
86
- name: responseName,
87
- pluginName: plugin.name,
88
- type: 'type',
89
- })
90
-
91
- return renderSchemaType({ node: schemaNode, name: resolvedName, typedName, description: res.description })
92
- })
70
+ .map((res) =>
71
+ renderSchemaType({
72
+ node: res.schema!,
73
+ name: resolveName({ name: `${node.operationId} ${res.statusCode}`, type: 'function' }),
74
+ typedName: resolveName({ name: `${node.operationId} ${res.statusCode}`, type: 'type' }),
75
+ description: res.description,
76
+ }),
77
+ )
93
78
 
94
- // Request body type
95
79
  const requestType = node.requestBody
96
- ? (() => {
97
- const requestName = `${node.operationId} MutationRequest`
98
- const resolvedName = resolveName({
99
- name: requestName,
100
- pluginName: plugin.name,
101
- type: 'function',
102
- })
103
- const typedName = resolveName({
104
- name: requestName,
105
- pluginName: plugin.name,
106
- type: 'type',
107
- })
108
-
109
- return renderSchemaType({ node: node.requestBody, name: resolvedName, typedName, description: node.requestBody.description })
110
- })()
80
+ ? renderSchemaType({
81
+ node: node.requestBody,
82
+ name: resolveName({ name: `${node.operationId} MutationRequest`, type: 'function' }),
83
+ typedName: resolveName({ name: `${node.operationId} MutationRequest`, type: 'type' }),
84
+ description: node.requestBody.description,
85
+ })
111
86
  : null
112
87
 
88
+ const dataType = renderSchemaType({
89
+ node: buildDataSchemaNode({ node: { ...node, parameters: params }, resolveName }),
90
+ name: resolveName({ name: `${node.operationId} Data`, type: 'function' }),
91
+ typedName: resolveName({ name: `${node.operationId} Data`, type: 'type' }),
92
+ })
93
+
94
+ const responsesType = renderSchemaType({
95
+ node: buildResponsesSchemaNode({ node, resolveName }),
96
+ name: resolveName({ name: `${node.operationId} Responses`, type: 'function' }),
97
+ typedName: resolveName({ name: `${node.operationId} Responses`, type: 'type' }),
98
+ })
99
+
100
+ const responseType = renderSchemaType({
101
+ node: buildResponseUnionSchemaNode({ node, resolveName }),
102
+ name: resolveName({ name: `${node.operationId} Response`, type: 'function' }),
103
+ typedName: resolveName({ name: `${node.operationId} Response`, type: 'type' }),
104
+ })
105
+
113
106
  return (
114
107
  <File baseName={file.baseName} path={file.path} meta={file.meta}>
115
108
  {paramTypes}
116
109
  {responseTypes}
117
110
  {requestType}
111
+ {dataType}
112
+ {responsesType}
113
+ {responseType}
118
114
  </File>
119
115
  )
120
116
  },
121
117
  Schema({ node, adapter, options }) {
122
- const { enumType, enumKeyCasing, syntaxType, optionalType, arrayType } = options
123
- const { plugin, mode, resolveName, getFile } = useKubb<PluginTs>()
118
+ const { enumType, enumKeyCasing, syntaxType, optionalType, arrayType, mapper } = options
119
+ const { mode, resolveName, getFile } = useKubb<PluginTs>()
124
120
 
125
121
  if (!node.name) {
126
122
  return
127
123
  }
128
124
 
129
125
  const imports = adapter.getImports(node, (schemaName) => ({
130
- name: resolveName({
131
- name: schemaName,
132
- pluginName: plugin.name,
133
- type: 'type',
134
- }),
135
- path: getFile({
136
- name: schemaName,
137
- pluginName: plugin.name,
138
- extname: '.ts',
139
- mode,
140
- // options: {
141
- // group
142
- // },
143
- }).path,
126
+ name: resolveName({ name: schemaName, type: 'type' }),
127
+ path: getFile({ name: schemaName, extname: '.ts', mode }).path,
144
128
  }))
145
129
 
146
130
  const isEnumSchema = node.type === 'enum'
147
131
 
148
- let typedName = resolveName({
149
- name: node.name,
150
- pluginName: plugin.name,
151
- type: 'type',
152
- })
153
-
132
+ let typedName = resolveName({ name: node.name, type: 'type' })
154
133
  if (['asConst', 'asPascalConst'].includes(enumType) && isEnumSchema) {
155
- typedName = typedName += 'Key'
134
+ typedName += 'Key'
156
135
  }
157
136
 
158
137
  const type = {
159
- name: resolveName({
160
- name: node.name,
161
- pluginName: plugin.name,
162
- type: 'function',
163
- }),
138
+ name: resolveName({ name: node.name, type: 'function' }),
164
139
  typedName,
165
- file: getFile({
166
- name: node.name,
167
- pluginName: plugin.name,
168
- extname: '.ts',
169
- mode,
170
- // options: {
171
- // group
172
- // },
173
- }),
140
+ file: getFile({ name: node.name, extname: '.ts', mode }),
174
141
  } as const
175
142
 
176
143
  return (
@@ -179,7 +146,6 @@ export const typeGenerator = createReactGenerator<PluginTs, '2'>({
179
146
  imports.map((imp) => (
180
147
  <File.Import key={[node.name, imp.path, imp.isTypeOnly].join('-')} root={type.file.path} path={imp.path} name={imp.name} isTypeOnly />
181
148
  ))}
182
-
183
149
  <Type
184
150
  name={type.name}
185
151
  typedName={type.typedName}
@@ -189,6 +155,7 @@ export const typeGenerator = createReactGenerator<PluginTs, '2'>({
189
155
  optionalType={optionalType}
190
156
  arrayType={arrayType}
191
157
  syntaxType={syntaxType}
158
+ mapper={mapper}
192
159
  />
193
160
  </File>
194
161
  )
@@ -0,0 +1,145 @@
1
+ import { createProperty, createSchema } from '@kubb/ast'
2
+ import type { OperationNode, ParameterNode, SchemaNode } from '@kubb/ast/types'
3
+
4
+ type ResolveName = (opts: { name: string; type: 'type' | 'function' }) => string
5
+
6
+ type BuildParamsSchemaOptions = {
7
+ params: Array<ParameterNode>
8
+ operationId: string
9
+ resolveName: ResolveName
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
+ */
16
+ export function buildParamsSchema({ params, operationId, resolveName }: BuildParamsSchemaOptions): SchemaNode {
17
+ return createSchema({
18
+ type: 'object',
19
+ properties: params.map((param) =>
20
+ createProperty({
21
+ name: param.name,
22
+ schema: createSchema({
23
+ type: 'ref',
24
+ name: resolveName({ name: `${operationId} ${param.name}`, type: 'function' }),
25
+ optional: !param.required,
26
+ }),
27
+ }),
28
+ ),
29
+ })
30
+ }
31
+
32
+ type BuildOperationSchemaOptions = {
33
+ node: OperationNode
34
+ resolveName: ResolveName
35
+ }
36
+
37
+ /**
38
+ * Builds an `ObjectSchemaNode` representing the `<OperationId>Data` type:
39
+ * - `data` → request body ref (optional) or `never`
40
+ * - `pathParams` → inline object of path param refs, or `never`
41
+ * - `queryParams` → inline object of query param refs (optional), or `never`
42
+ * - `headerParams` → inline object of header param refs (optional), or `never`
43
+ * - `url` → Express-style template literal (plugin-ts extension, handled by printer)
44
+ */
45
+ export function buildDataSchemaNode({ node, resolveName }: BuildOperationSchemaOptions): SchemaNode {
46
+ const pathParams = node.parameters.filter((p) => p.in === 'path')
47
+ const queryParams = node.parameters.filter((p) => p.in === 'query')
48
+ const headerParams = node.parameters.filter((p) => p.in === 'header')
49
+
50
+ return createSchema({
51
+ type: 'object',
52
+ properties: [
53
+ createProperty({
54
+ name: 'data',
55
+ schema: node.requestBody
56
+ ? createSchema({
57
+ type: 'ref',
58
+ name: resolveName({ name: `${node.operationId} MutationRequest`, type: 'function' }),
59
+ optional: true,
60
+ })
61
+ : createSchema({ type: 'never', optional: true }),
62
+ }),
63
+ createProperty({
64
+ name: 'pathParams',
65
+ schema:
66
+ pathParams.length > 0
67
+ ? buildParamsSchema({ params: pathParams, operationId: node.operationId, resolveName })
68
+ : createSchema({ type: 'never', optional: true }),
69
+ }),
70
+ createProperty({
71
+ name: 'queryParams',
72
+ schema:
73
+ queryParams.length > 0
74
+ ? createSchema({ ...buildParamsSchema({ params: queryParams, operationId: node.operationId, resolveName }), 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, operationId: node.operationId, resolveName }), 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
+ *
95
+ * Example output:
96
+ * ```ts
97
+ * export type PlaceOrderPatchResponses = { 200: PlaceOrderPatch200; 405: PlaceOrderPatch405 }
98
+ * ```
99
+ */
100
+ export function buildResponsesSchemaNode({ node, resolveName }: BuildOperationSchemaOptions): SchemaNode | null {
101
+ const responsesWithSchema = node.responses.filter((res) => res.schema)
102
+
103
+ if (responsesWithSchema.length === 0) {
104
+ return null
105
+ }
106
+
107
+ return createSchema({
108
+ type: 'object',
109
+ properties: responsesWithSchema.map((res) =>
110
+ createProperty({
111
+ name: String(res.statusCode),
112
+ schema: createSchema({
113
+ type: 'ref',
114
+ name: resolveName({ name: `${node.operationId} ${res.statusCode}`, type: 'function' }),
115
+ }),
116
+ }),
117
+ ),
118
+ })
119
+ }
120
+
121
+ /**
122
+ * Builds a `UnionSchemaNode` representing `<OperationId>Response` — all response types in union format.
123
+ *
124
+ * Example output:
125
+ * ```ts
126
+ * export type PlaceOrderPatchResponse = PlaceOrderPatch200 | PlaceOrderPatch405
127
+ * ```
128
+ */
129
+ export function buildResponseUnionSchemaNode({ node, resolveName }: BuildOperationSchemaOptions): SchemaNode | null {
130
+ const responsesWithSchema = node.responses.filter((res) => res.schema)
131
+
132
+ if (responsesWithSchema.length === 0) {
133
+ return null
134
+ }
135
+
136
+ return createSchema({
137
+ type: 'union',
138
+ members: responsesWithSchema.map((res) =>
139
+ createSchema({
140
+ type: 'ref',
141
+ name: resolveName({ name: `${node.operationId} ${res.statusCode}`, type: 'function' }),
142
+ }),
143
+ ),
144
+ })
145
+ }
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { camelCase, pascalCase } from '@internals/utils'
3
3
  import { walk } from '@kubb/ast'
4
- import { definePlugin, type Group, getBarrelFiles, getMode } from '@kubb/core'
4
+ import { definePlugin, type Group, getBarrelFiles, getMode, resolveOptions } from '@kubb/core'
5
5
  import { buildOperation, buildSchema, OperationGenerator, pluginOasName, SchemaGenerator } from '@kubb/plugin-oas'
6
6
  import { typeGenerator, typeGeneratorV2 } from './generators'
7
7
  import type { PluginTs } from './types.ts'
@@ -58,7 +58,6 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
58
58
  usedEnumNames,
59
59
  },
60
60
  pre: [pluginOasName],
61
- //resolveOptions(operation|schema): ResolvedOptions
62
61
  resolvePath(baseName, pathMode, options) {
63
62
  const root = path.resolve(this.config.root, this.config.output.path)
64
63
  const mode = pathMode ?? getMode(path.resolve(root, output.path))
@@ -117,7 +116,14 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
117
116
  async schema(schemaNode) {
118
117
  const writeTasks = generators.map(async (generator) => {
119
118
  if (generator.type === 'react' && generator.version === '2') {
119
+ const options = resolveOptions(schemaNode, { options: plugin.options, exclude, include, override })
120
+
121
+ if (options === null) {
122
+ return
123
+ }
124
+
120
125
  await buildSchema(schemaNode, {
126
+ options,
121
127
  adapter,
122
128
  config,
123
129
  fabric,
@@ -130,12 +136,19 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
130
136
  }
131
137
  })
132
138
 
133
- await writeTasks
139
+ await Promise.all(writeTasks)
134
140
  },
135
141
  async operation(operationNode) {
136
142
  const writeTasks = generators.map(async (generator) => {
137
143
  if (generator.type === 'react' && generator.version === '2') {
144
+ const options = resolveOptions(operationNode, { options: plugin.options, exclude, include, override })
145
+
146
+ if (options === null) {
147
+ return
148
+ }
149
+
138
150
  await buildOperation(operationNode, {
151
+ options,
139
152
  adapter,
140
153
  config,
141
154
  fabric,
@@ -148,12 +161,23 @@ export const pluginTs = definePlugin<PluginTs>((options) => {
148
161
  }
149
162
  })
150
163
 
151
- await writeTasks
164
+ await Promise.all(writeTasks)
152
165
  },
153
166
  },
154
167
  { depth: 'shallow' },
155
168
  )
156
169
 
170
+ const barrelFiles = await getBarrelFiles(this.fabric.files, {
171
+ type: output.barrelType ?? 'named',
172
+ root,
173
+ output,
174
+ meta: {
175
+ pluginName: this.plugin.name,
176
+ },
177
+ })
178
+
179
+ await this.upsertFile(...barrelFiles)
180
+
157
181
  return
158
182
  }
159
183
 
package/src/printer.ts CHANGED
@@ -19,6 +19,10 @@ type TsOptions = {
19
19
  * @default `'inlineLiteral'`
20
20
  */
21
21
  enumType: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
22
+ /**
23
+ * Custom property signatures that override specific object properties by name.
24
+ */
25
+ mapper?: Record<string, ts.PropertySignature>
22
26
  }
23
27
 
24
28
  type TsPrinter = PrinterFactoryOptions<'typescript', TsOptions, ts.TypeNode>
@@ -143,13 +147,19 @@ export const printerTs = definePrinter<TsPrinter>((options) => ({
143
147
  any: () => factory.keywordTypeNodes.any,
144
148
  unknown: () => factory.keywordTypeNodes.unknown,
145
149
  void: () => factory.keywordTypeNodes.void,
150
+ never: () => factory.keywordTypeNodes.never,
146
151
  boolean: () => factory.keywordTypeNodes.boolean,
147
152
  null: () => factory.keywordTypeNodes.null,
148
153
  blob: () => factory.createTypeReferenceNode('Blob', []),
149
154
  string: () => factory.keywordTypeNodes.string,
150
155
  uuid: () => factory.keywordTypeNodes.string,
151
156
  email: () => factory.keywordTypeNodes.string,
152
- url: () => factory.keywordTypeNodes.string,
157
+ url: (node) => {
158
+ if (node.path) {
159
+ return factory.createUrlTemplateType(node.path)
160
+ }
161
+ return factory.keywordTypeNodes.string
162
+ },
153
163
  datetime: () => factory.keywordTypeNodes.string,
154
164
  number: () => factory.keywordTypeNodes.number,
155
165
  integer: () => factory.keywordTypeNodes.number,
@@ -219,6 +229,10 @@ export const printerTs = definePrinter<TsPrinter>((options) => ({
219
229
  const { print } = this
220
230
 
221
231
  const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => {
232
+ if (this.options.mapper && Object.hasOwn(this.options.mapper, prop.name)) {
233
+ return this.options.mapper[prop.name]!
234
+ }
235
+
222
236
  const baseType = (print(prop.schema) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode
223
237
  const type = buildPropertyType(prop.schema, baseType, this.options.optionalType)
224
238
 
package/src/types.ts CHANGED
@@ -63,6 +63,8 @@ export type Options = {
63
63
  /**
64
64
  * Set a suffix for the generated enums.
65
65
  * @default 'enum'
66
+ * @deprecated Set `enumSuffix` on the adapter (`adapterOas({ enumSuffix })`) instead.
67
+ * In v5, the adapter owns this decision at parse time; the plugin option is ignored.
66
68
  */
67
69
  enumSuffix?: string
68
70
  /**
@@ -70,6 +72,8 @@ export type Options = {
70
72
  * - 'string' represents dates as string values.
71
73
  * - 'date' represents dates as JavaScript Date objects.
72
74
  * @default 'string'
75
+ * @deprecated Set `dateType` on the adapter (`adapterOas({ dateType })`) instead.
76
+ * In v5, the adapter owns this decision at parse time; the plugin option is ignored.
73
77
  */
74
78
  dateType?: 'string' | 'date'
75
79
  /**
@@ -78,6 +82,8 @@ export type Options = {
78
82
  * - 'bigint' uses the TypeScript `bigint` type (accurate for values exceeding Number.MAX_SAFE_INTEGER).
79
83
  * @note in v5 of Kubb 'bigint' will become the default to better align with OpenAPI's int64 specification.
80
84
  * @default 'number'
85
+ * @deprecated Set `integerType` on the adapter (`adapterOas({ integerType })`) instead.
86
+ * In v5, the adapter owns this decision at parse time; the plugin option is ignored.
81
87
  */
82
88
  integerType?: 'number' | 'bigint'
83
89
  /**
@@ -86,6 +92,8 @@ export type Options = {
86
92
  * - 'unknown' requires type narrowing before use.
87
93
  * - 'void' represents no value.
88
94
  * @default 'any'
95
+ * @deprecated Set `unknownType` on the adapter (`adapterOas({ unknownType })`) instead.
96
+ * In v5, the adapter owns this decision at parse time; the plugin option is ignored.
89
97
  */
90
98
  unknownType?: 'any' | 'unknown' | 'void'
91
99
  /**
@@ -94,6 +102,8 @@ export type Options = {
94
102
  * - 'unknown' requires type narrowing before use.
95
103
  * - 'void' represents no value.
96
104
  * @default `unknownType`
105
+ * @deprecated Set `emptySchemaType` on the adapter (`adapterOas({ emptySchemaType })`) instead.
106
+ * In v5, the adapter owns this decision at parse time; the plugin option is ignored.
97
107
  */
98
108
  emptySchemaType?: 'any' | 'unknown' | 'void'
99
109
  /**