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

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,66 +1,69 @@
1
- import { pascalCase } from '@internals/utils'
1
+ import { ensureValidVarName, pascalCase, toFilePath } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginTs } from '../types.ts'
4
4
 
5
5
  /**
6
- * Resolver for `@kubb/plugin-ts` that provides the default naming and path-resolution
7
- * helpers used by the plugin. Import this in other plugins to resolve the exact names and
8
- * paths that `plugin-ts` generates without hardcoding the conventions.
6
+ * Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
7
+ * for every generated TypeScript type. Import this in other plugins that need
8
+ * to reference the exact names `plugin-ts` produces without duplicating the
9
+ * casing/file-layout rules.
9
10
  *
10
- * The `default` method is automatically injected by `defineResolver` it uses `camelCase`
11
- * for identifiers/files and `pascalCase` for type names.
11
+ * The `default` method is supplied by `defineResolver`. It uses PascalCase for
12
+ * type names and PascalCase file paths (dotted names become `/`-joined) for files.
12
13
  *
13
- * @example
14
+ * @example Resolve a type and file name
14
15
  * ```ts
15
- * import { resolver } from '@kubb/plugin-ts'
16
+ * import { resolverTs } from '@kubb/plugin-ts'
16
17
  *
17
- * resolver.default('list pets', 'type') // 'ListPets'
18
- * resolver.resolveName('list pets status 200') // 'ListPetsStatus200'
19
- * resolver.resolvePathName('list pets', 'file') // 'listPets'
18
+ * resolverTs.default('list pets', 'type') // 'ListPets'
19
+ * resolverTs.resolvePathName('list pets', 'file') // 'ListPets'
20
+ * resolverTs.resolveResponseStatusName(node, 200) // 'ListPetsStatus200'
20
21
  * ```
21
22
  */
22
- export const resolverTs = defineResolver<PluginTs>((ctx) => {
23
+ export const resolverTs = defineResolver<PluginTs>(() => {
23
24
  return {
24
25
  name: 'default',
25
26
  pluginName: 'plugin-ts',
26
27
  default(name, type) {
27
- return pascalCase(name, { isFile: type === 'file' })
28
+ if (type === 'file') return toFilePath(name, pascalCase)
29
+ return ensureValidVarName(pascalCase(name))
28
30
  },
29
31
  resolveTypeName(name) {
30
- return pascalCase(name)
32
+ return ensureValidVarName(pascalCase(name))
31
33
  },
32
34
  resolvePathName(name, type) {
33
- return pascalCase(name, { isFile: type === 'file' })
35
+ if (type === 'file') return toFilePath(name, pascalCase)
36
+ return ensureValidVarName(pascalCase(name))
34
37
  },
35
38
  resolveParamName(node, param) {
36
- return ctx.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`)
39
+ return this.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`)
37
40
  },
38
41
  resolveResponseStatusName(node, statusCode) {
39
- return ctx.resolveTypeName(`${node.operationId} Status ${statusCode}`)
42
+ return this.resolveTypeName(`${node.operationId} Status ${statusCode}`)
40
43
  },
41
44
  resolveDataName(node) {
42
- return ctx.resolveTypeName(`${node.operationId} Data`)
45
+ return this.resolveTypeName(`${node.operationId} Data`)
43
46
  },
44
47
  resolveRequestConfigName(node) {
45
- return ctx.resolveTypeName(`${node.operationId} RequestConfig`)
48
+ return this.resolveTypeName(`${node.operationId} RequestConfig`)
46
49
  },
47
50
  resolveResponsesName(node) {
48
- return ctx.resolveTypeName(`${node.operationId} Responses`)
51
+ return this.resolveTypeName(`${node.operationId} Responses`)
49
52
  },
50
53
  resolveResponseName(node) {
51
- return ctx.resolveTypeName(`${node.operationId} Response`)
54
+ return this.resolveTypeName(`${node.operationId} Response`)
52
55
  },
53
56
  resolveEnumKeyName(node, enumTypeSuffix = 'key') {
54
- return `${ctx.resolveTypeName(node.name ?? '')}${enumTypeSuffix}`
57
+ return `${this.resolveTypeName(node.name ?? '')}${enumTypeSuffix}`
55
58
  },
56
59
  resolvePathParamsName(node, param) {
57
- return ctx.resolveParamName(node, param)
60
+ return this.resolveParamName(node, param)
58
61
  },
59
62
  resolveQueryParamsName(node, param) {
60
- return ctx.resolveParamName(node, param)
63
+ return this.resolveParamName(node, param)
61
64
  },
62
65
  resolveHeaderParamsName(node, param) {
63
- return ctx.resolveParamName(node, param)
66
+ return this.resolveParamName(node, param)
64
67
  },
65
68
  }
66
69
  })
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, OutputOptions, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
2
2
  import type { PrinterTsNodes } from './printers/printerTs.ts'
3
3
  /**
4
4
  * The concrete resolver type for `@kubb/plugin-ts`.
@@ -93,34 +93,49 @@ export type ResolverTs = Resolver &
93
93
 
94
94
  type EnumKeyCasing = 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none'
95
95
 
96
+ type EnumConstCasing = 'camelCase' | 'pascalCase'
97
+
96
98
  /**
97
- * Discriminated union that ties `enumTypeSuffix` and `enumKeyCasing` to the enum types that actually use them.
99
+ * Grouped enum settings. Each `type` uses only some of the other fields.
100
+ *
101
+ * - `'asConst'` emits a `const` object plus a `typeof` type alias, so `constCasing`, `typeSuffix`, and `keyCasing` all apply.
102
+ * - `'enum'` and `'constEnum'` emit a TypeScript enum, so only `keyCasing` (the member names) applies.
103
+ * - `'literal'` and `'inlineLiteral'` emit union literals and drop the keys, so none of the other fields apply.
98
104
  *
99
- * - `'asConst'` / `'asPascalConst'` emit a `const` object; both `enumTypeSuffix` (type-alias suffix) and
100
- * `enumKeyCasing` (key formatting) are meaningful.
101
- * - `'enum'` / `'constEnum'` emit a TypeScript enum; `enumKeyCasing` applies to member names,
102
- * but there is no separate type alias so `enumTypeSuffix` is not used.
103
- * - `'literal'` / `'inlineLiteral'` emit only union literals; keys are discarded entirely,
104
- * so neither `enumTypeSuffix` nor `enumKeyCasing` have any effect.
105
+ * @example Share one name between the const and the type
106
+ * ```ts
107
+ * enum: { type: 'asConst', constCasing: 'pascalCase', typeSuffix: '' }
108
+ * // export const VehicleType = { } as const
109
+ * // export type VehicleType = (typeof VehicleType)[keyof typeof VehicleType]
110
+ * ```
105
111
  */
106
- type EnumTypeOptions =
112
+ type EnumOptions =
107
113
  | {
108
114
  /**
109
- * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
110
- * - 'asConst' generates const objects with camelCase names and as const assertion.
111
- * - 'asPascalConst' generates const objects with PascalCase names and as const assertion.
115
+ * Emit a `const` object asserted with `as const`, paired with a `typeof` type alias.
116
+ * This is tree-shakeable and adds no enum runtime.
117
+ *
112
118
  * @default 'asConst'
113
119
  */
114
- enumType?: 'asConst' | 'asPascalConst'
120
+ type?: 'asConst'
115
121
  /**
116
- * Suffix appended to the generated type alias name.
122
+ * Casing of the generated const variable.
123
+ * - 'camelCase' names the const `vehicleType`.
124
+ * - 'pascalCase' names the const `VehicleType`, matching the schema name.
117
125
  *
118
- * Only affects the type alias — the const object name is unchanged.
126
+ * @default 'camelCase'
127
+ */
128
+ constCasing?: EnumConstCasing
129
+ /**
130
+ * Suffix appended to the generated type alias name. Only the type alias is renamed. The const
131
+ * object name stays the same. Set it to `''` together with `constCasing: 'pascalCase'` to merge
132
+ * the const and type under the schema's exact name.
119
133
  *
120
134
  * @default 'Key'
121
- * @example enumTypeSuffix: 'Value' → `export type PetStatusValue = …`
135
+ * @example A custom suffix
136
+ * `typeSuffix: 'Value'` renames the alias to `PetStatusValue`
122
137
  */
123
- enumTypeSuffix?: string
138
+ typeSuffix?: string
124
139
  /**
125
140
  * Choose the casing for enum key names.
126
141
  * - 'screamingSnakeCase' generates keys in SCREAMING_SNAKE_CASE format.
@@ -130,23 +145,25 @@ type EnumTypeOptions =
130
145
  * - 'none' uses the enum value as-is without transformation.
131
146
  * @default 'none'
132
147
  */
133
- enumKeyCasing?: EnumKeyCasing
148
+ keyCasing?: EnumKeyCasing
134
149
  }
135
150
  | {
136
151
  /**
137
- * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
138
- * - 'enum' generates TypeScript enum declarations.
139
- * - 'constEnum' generates TypeScript const enum declarations.
152
+ * Emit a TypeScript `enum` (`'enum'`) or `const enum` (`'constEnum'`) declaration.
153
+ *
140
154
  * @default 'asConst'
141
155
  */
142
- enumType?: 'enum' | 'constEnum'
156
+ type?: 'enum' | 'constEnum'
143
157
  /**
144
- * `enumTypeSuffix` has no effect for this `enumType`.
145
- * It is only used when `enumType` is `'asConst'` or `'asPascalConst'`.
158
+ * `constCasing` has no effect for this `type`. Only `'asConst'` emits a const object.
146
159
  */
147
- enumTypeSuffix?: never
160
+ constCasing?: never
148
161
  /**
149
- * Choose the casing for enum key names.
162
+ * `typeSuffix` has no effect for this `type`. Only `'asConst'` emits a separate type alias.
163
+ */
164
+ typeSuffix?: never
165
+ /**
166
+ * Choose the casing for enum member names.
150
167
  * - 'screamingSnakeCase' generates keys in SCREAMING_SNAKE_CASE format.
151
168
  * - 'snakeCase' generates keys in snake_case format.
152
169
  * - 'pascalCase' generates keys in PascalCase format.
@@ -154,99 +171,101 @@ type EnumTypeOptions =
154
171
  * - 'none' uses the enum value as-is without transformation.
155
172
  * @default 'none'
156
173
  */
157
- enumKeyCasing?: EnumKeyCasing
174
+ keyCasing?: EnumKeyCasing
158
175
  }
159
176
  | {
160
177
  /**
161
- * Choose to use enum, asConst, asPascalConst, constEnum, literal, or inlineLiteral for enums.
162
- * - 'literal' generates literal union types.
163
- * - 'inlineLiteral' will inline enum values directly into the type (default in v5).
178
+ * Emit a union of literals as a named alias (`'literal'`) or inline the union at every usage
179
+ * site (`'inlineLiteral'`).
180
+ *
164
181
  * @default 'asConst'
165
182
  * @note In Kubb v5, 'inlineLiteral' becomes the default.
166
183
  */
167
- enumType?: 'literal' | 'inlineLiteral'
184
+ type?: 'literal' | 'inlineLiteral'
185
+ /**
186
+ * `constCasing` has no effect for this `type`; literal modes emit no const object.
187
+ */
188
+ constCasing?: never
168
189
  /**
169
- * `enumTypeSuffix` has no effect for this `enumType`.
170
- * It is only used when `enumType` is `'asConst'` or `'asPascalConst'`.
190
+ * `typeSuffix` has no effect for this `type`; literal modes emit no separate type alias.
171
191
  */
172
- enumTypeSuffix?: never
192
+ typeSuffix?: never
173
193
  /**
174
- * `enumKeyCasing` has no effect for this `enumType`.
175
- * Literal and inlineLiteral modes emit only values — keys are discarded entirely.
194
+ * `keyCasing` has no effect for this `type`. Literal and inlineLiteral modes emit only values,
195
+ * so the keys are discarded.
176
196
  */
177
- enumKeyCasing?: never
197
+ keyCasing?: never
178
198
  }
179
199
 
180
- export type Options = {
181
- /**
182
- * Specify the export location for the files and define the behavior of the output
183
- * @default { path: 'types', barrelType: 'named' }
184
- */
185
- output?: Output
186
- /**
187
- * Define which contentType should be used.
188
- * By default, uses the first valid JSON media type.
189
- */
190
- contentType?: 'application/json' | (string & {})
191
- /**
192
- * Group the clients based on the provided name.
193
- */
194
- group?: Group
200
+ /**
201
+ * Where the generated `.ts` files are written and how they are exported, plus the optional
202
+ * `group` strategy. The `group` option organizes `output.mode: 'directory'` output into per-tag or per-path subdirectories.
203
+ *
204
+ * @default { path: 'types', barrel: { type: 'named' } }
205
+ */
206
+ export type Options = OutputOptions & {
195
207
  /**
196
- * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
208
+ * Skip operations matching at least one entry in the list.
197
209
  */
198
210
  exclude?: Array<Exclude>
199
211
  /**
200
- * Array containing include parameters to include tags/operations/methods/paths.
212
+ * Restrict generation to operations matching at least one entry in the list.
201
213
  */
202
214
  include?: Array<Include>
203
215
  /**
204
- * Array containing override parameters to override `options` based on tags/operations/methods/paths.
216
+ * Apply a different options object to operations matching a pattern.
205
217
  */
206
218
  override?: Array<Override<ResolvedOptions>>
207
219
  /**
208
- * Switch between type or interface for creating TypeScript types.
209
- * - 'type' generates type alias declarations.
210
- * - 'interface' generates interface declarations.
220
+ * Whether object schemas are emitted as `type` aliases or `interface` declarations.
221
+ * - `'type'` emits closed type aliases. Safer default for generated code.
222
+ * - `'interface'` emits interfaces. Useful when consumers rely on declaration merging.
223
+ *
211
224
  * @default 'type'
225
+ * @see https://www.totaltypescript.com/type-vs-interface-which-should-you-use
212
226
  */
213
227
  syntaxType?: 'type' | 'interface'
214
228
  /**
215
- * Choose what to use as mode for an optional value.
216
- * - 'questionToken' marks the property as optional with ? (e.g., type?: string).
217
- * - 'undefined' adds undefined to the type union (e.g., type: string | undefined).
218
- * - 'questionTokenAndUndefined' combines both approaches (e.g., type?: string | undefined).
229
+ * How optional properties are written in generated types.
230
+ * - `'questionToken'` `type?: string`. The property may be missing.
231
+ * - `'undefined'` `type: string | undefined`. Required to exist, may be `undefined`.
232
+ * - `'questionTokenAndUndefined'` `type?: string | undefined`. Strictest.
233
+ *
219
234
  * @default 'questionToken'
235
+ * @note Pick `'questionTokenAndUndefined'` when `exactOptionalPropertyTypes` is on in `tsconfig.json`.
220
236
  */
221
237
  optionalType?: 'questionToken' | 'undefined' | 'questionTokenAndUndefined'
222
238
  /**
223
- * Choose between Array<string> or string[] for array types.
224
- * - 'generic' generates Array<Type> syntax.
225
- * - 'array' generates Type[] syntax.
239
+ * Syntax used for array types.
240
+ * - `'array'` `Type[]`. Shorter.
241
+ * - `'generic'` `Array<Type>`. More readable for complex element types.
242
+ *
226
243
  * @default 'array'
227
244
  */
228
245
  arrayType?: 'generic' | 'array'
229
246
  /**
230
- * How to style your params, by default no casing is applied
231
- * - 'camelcase' uses camelCase for pathParams, queryParams and headerParams property names
232
- * @default undefined
233
- * @note response types (data/body) are NOT affected by this option
247
+ * Rename properties inside `PathParams`, `QueryParams`, and `HeaderParams` types.
248
+ * Response and request body types are not affected.
249
+ *
250
+ * @note Every plugin that touches operation parameters must use the same value.
234
251
  */
235
252
  paramsCasing?: 'camelcase'
236
253
  /**
237
- * Define some generators next to the ts generators
254
+ * Custom generators that run alongside the built-in TypeScript generators.
238
255
  */
239
256
  generators?: Array<Generator<PluginTs>>
240
257
  /**
241
- * Override naming conventions. When a method returns `null` or `undefined`, the preset
242
- * resolver (`resolverTs`) is used as fallback.
258
+ * Override how names and file paths are built for generated symbols.
259
+ * Methods you omit fall back to the default `resolverTs`. `this` is bound to the
260
+ * full resolver, so `this.default(name, 'function')` delegates to the built-in
261
+ * implementation.
243
262
  */
244
263
  resolver?: Partial<ResolverTs> & ThisType<ResolverTs>
245
264
  /**
246
- * AST visitor applied to each schema/operation node before printing.
247
- * Returning `null` or `undefined` from a visitor method falls back to the preset transformer.
265
+ * AST visitor applied to each schema or operation node before printing.
266
+ * Methods you omit fall back to the preset transformer.
248
267
  *
249
- * @example Remove writeOnly properties from response types
268
+ * @example Drop writeOnly properties from response types
250
269
  * ```ts
251
270
  * transformer: {
252
271
  * property(node) {
@@ -257,18 +276,17 @@ export type Options = {
257
276
  */
258
277
  transformer?: ast.Visitor
259
278
  /**
260
- * Override individual printer node handlers to customize rendering of specific schema types.
261
- *
262
- * Each key is a `SchemaType` (e.g. `'date'`, `'string'`). The function replaces the
263
- * built-in handler for that type. Use `this.transform` to recurse into nested schema nodes.
279
+ * Replace the TypeScript handler for a specific schema type (`'integer'`, `'date'`, ...).
280
+ * Each handler returns a TypeScript AST node for that schema type. Use `this.transform`
281
+ * to recurse into nested schema nodes.
264
282
  *
265
- * @example Override the `date` node to use the `Date` object type
283
+ * @example Use the JavaScript `Date` object for date schemas
266
284
  * ```ts
267
285
  * import ts from 'typescript'
268
286
  * pluginTs({
269
287
  * printer: {
270
288
  * nodes: {
271
- * date(node) {
289
+ * date() {
272
290
  * return ts.factory.createTypeReferenceNode('Date', [])
273
291
  * },
274
292
  * },
@@ -279,17 +297,26 @@ export type Options = {
279
297
  printer?: {
280
298
  nodes?: PrinterTsNodes
281
299
  }
282
- } & EnumTypeOptions
300
+ /**
301
+ * How OpenAPI enums are represented in the generated TypeScript, and how their names are cased.
302
+ */
303
+ enum?: EnumOptions
304
+ }
305
+
306
+ type ResolvedEnumOptions = {
307
+ type: NonNullable<EnumOptions['type']>
308
+ constCasing: EnumConstCasing
309
+ typeSuffix: string
310
+ keyCasing: EnumKeyCasing
311
+ }
283
312
 
284
313
  type ResolvedOptions = {
285
314
  output: Output
286
315
  exclude: Array<Exclude>
287
316
  include: Array<Include> | undefined
288
317
  override: Array<Override<ResolvedOptions>>
289
- group: Group | undefined
290
- enumType: NonNullable<Options['enumType']>
291
- enumTypeSuffix: NonNullable<Options['enumTypeSuffix']>
292
- enumKeyCasing: EnumKeyCasing
318
+ group: Group | null
319
+ enum: ResolvedEnumOptions
293
320
  optionalType: NonNullable<Options['optionalType']>
294
321
  arrayType: NonNullable<Options['arrayType']>
295
322
  syntaxType: NonNullable<Options['syntaxType']>
package/src/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { jsStringEscape, stringify } from '@internals/utils'
1
+ import { jsStringEscape, stringify } from '@kubb/ast/utils'
2
+ import { getOperationParameters } from '@internals/shared'
2
3
  import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from './types.ts'
4
5
 
@@ -14,20 +15,31 @@ export function buildPropertyJSDocComments(schema: ast.SchemaNode): Array<string
14
15
 
15
16
  const isArray = meta?.primitive === 'array'
16
17
 
18
+ const hasDescription = meta && 'description' in meta && meta.description
19
+
20
+ const formatComment =
21
+ meta && 'format' in meta && meta.format
22
+ ? hasDescription
23
+ ? // Empty line between description and format
24
+ [' ', `Format: \`${meta.format}\``]
25
+ : ['@description', `Format: \`${meta.format}\``]
26
+ : []
27
+
17
28
  return [
18
- meta && 'description' in meta && meta.description ? `@description ${jsStringEscape(meta.description)}` : undefined,
19
- meta && 'deprecated' in meta && meta.deprecated ? '@deprecated' : undefined,
29
+ hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
30
+ ...formatComment,
31
+ meta && 'deprecated' in meta && meta.deprecated ? '@deprecated' : null,
20
32
  // minItems/maxItems on arrays should not be emitted as @minLength/@maxLength
21
- !isArray && meta && 'min' in meta && meta.min !== undefined ? `@minLength ${meta.min}` : undefined,
22
- !isArray && meta && 'max' in meta && meta.max !== undefined ? `@maxLength ${meta.max}` : undefined,
23
- meta && 'pattern' in meta && meta.pattern ? `@pattern ${meta.pattern}` : undefined,
33
+ !isArray && meta && 'min' in meta && meta.min !== undefined ? `@minLength ${meta.min}` : null,
34
+ !isArray && meta && 'max' in meta && meta.max !== undefined ? `@maxLength ${meta.max}` : null,
35
+ meta && 'pattern' in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
24
36
  meta && 'default' in meta && meta.default !== undefined
25
37
  ? `@default ${'primitive' in meta && meta.primitive === 'string' ? stringify(meta.default as string) : meta.default}`
26
- : undefined,
27
- meta && 'example' in meta && meta.example !== undefined ? `@example ${meta.example}` : undefined,
38
+ : null,
39
+ meta && 'example' in meta && meta.example !== undefined ? `@example ${meta.example}` : null,
28
40
  meta && 'primitive' in meta && meta.primitive
29
- ? [`@type ${meta.primitive}`, 'optional' in schema && schema.optional ? ' | undefined' : undefined].filter(Boolean).join('')
30
- : undefined,
41
+ ? [`@type ${meta.primitive}`, 'optional' in schema && schema.optional ? ' | undefined' : null].filter(Boolean).join('')
42
+ : null,
31
43
  ].filter(Boolean)
32
44
  }
33
45
 
@@ -57,9 +69,7 @@ export function buildParams(node: ast.OperationNode, { params, resolver }: Build
57
69
  }
58
70
 
59
71
  export function buildData(node: ast.OperationNode, { resolver }: BuildOperationSchemaOptions): ast.SchemaNode {
60
- const pathParams = node.parameters.filter((p) => p.in === 'path')
61
- const queryParams = node.parameters.filter((p) => p.in === 'query')
62
- const headerParams = node.parameters.filter((p) => p.in === 'header')
72
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node)
63
73
 
64
74
  return ast.createSchema({
65
75
  type: 'object',
@@ -117,7 +127,7 @@ export function buildResponses(node: ast.OperationNode, { resolver }: BuildOpera
117
127
  }
118
128
 
119
129
  export function buildResponseUnion(node: ast.OperationNode, { resolver }: BuildOperationSchemaOptions): ast.SchemaNode | null {
120
- const responsesWithSchema = node.responses.filter((res) => res.schema)
130
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))
121
131
 
122
132
  if (responsesWithSchema.length === 0) {
123
133
  return null