@kubb/plugin-oas 4.18.5 → 4.19.1
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/dist/{SchemaGenerator-DExEah4y.js → SchemaGenerator-CK_Mv0RW.js} +90 -39
- package/dist/SchemaGenerator-CK_Mv0RW.js.map +1 -0
- package/dist/{SchemaGenerator-B18-jWu7.cjs → SchemaGenerator-D2ZpjZgn.cjs} +91 -40
- package/dist/SchemaGenerator-D2ZpjZgn.cjs.map +1 -0
- package/dist/{SchemaMapper-oZdVAbXF.d.ts → SchemaMapper-DI2vHHE0.d.ts} +24 -7
- package/dist/{SchemaMapper-BWZ2ZjPQ.d.cts → SchemaMapper-DmB5NyNo.d.cts} +24 -7
- package/dist/{createGenerator-Bq76G0TY.d.ts → createGenerator-3zJdjpOn.d.ts} +38 -2
- package/dist/{createGenerator-RL6jHKJ-.d.cts → createGenerator-cYz-kExA.d.cts} +38 -2
- package/dist/generators.d.cts +2 -2
- package/dist/generators.d.ts +2 -2
- package/dist/getSchemaFactory-CBp1me72.cjs +29 -0
- package/dist/getSchemaFactory-CBp1me72.cjs.map +1 -0
- package/dist/getSchemaFactory-DsoVRgxV.js +24 -0
- package/dist/getSchemaFactory-DsoVRgxV.js.map +1 -0
- package/dist/hooks.cjs +1 -1
- package/dist/hooks.d.cts +2 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +1 -1
- package/dist/index.cjs +4 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/mocks.d.cts +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/utils.cjs +26 -3
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +25 -5
- package/dist/utils.d.ts +25 -5
- package/dist/utils.js +24 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/SchemaGenerator.ts +90 -32
- package/src/plugin.ts +2 -0
- package/src/types.ts +30 -0
- package/src/utils/getSchemas.ts +27 -105
- package/dist/SchemaGenerator-B18-jWu7.cjs.map +0 -1
- package/dist/SchemaGenerator-DExEah4y.js.map +0 -1
- package/dist/getSchemas-BUXPwm-5.js +0 -101
- package/dist/getSchemas-BUXPwm-5.js.map +0 -1
- package/dist/getSchemas-D3YweIFO.cjs +0 -112
- package/dist/getSchemas-D3YweIFO.cjs.map +0 -1
package/src/SchemaGenerator.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { BaseGenerator, type FileMetaBase } from '@kubb/core'
|
|
|
3
3
|
import transformers, { pascalCase } from '@kubb/core/transformers'
|
|
4
4
|
import { type AsyncEventEmitter, getUniqueName } from '@kubb/core/utils'
|
|
5
5
|
import type { KubbFile } from '@kubb/fabric-core/types'
|
|
6
|
-
import type { contentType, Oas, OpenAPIV3, SchemaObject } from '@kubb/oas'
|
|
6
|
+
import type { contentType, Oas, OasTypes, OpenAPIV3, SchemaObject } from '@kubb/oas'
|
|
7
7
|
import { isDiscriminator, isNullable, isReference } from '@kubb/oas'
|
|
8
8
|
import type { Fabric } from '@kubb/react-fabric'
|
|
9
9
|
import pLimit from 'p-limit'
|
|
@@ -12,7 +12,6 @@ import type { Generator } from './generators/types.ts'
|
|
|
12
12
|
import { isKeyword, type Schema, type SchemaKeywordMapper, schemaKeywords } from './SchemaMapper.ts'
|
|
13
13
|
import type { OperationSchema, Override, Refs } from './types.ts'
|
|
14
14
|
import { getSchemaFactory } from './utils/getSchemaFactory.ts'
|
|
15
|
-
import { getSchemas } from './utils/getSchemas.ts'
|
|
16
15
|
import { buildSchema } from './utils.tsx'
|
|
17
16
|
|
|
18
17
|
export type GetSchemaGeneratorOptions<T extends SchemaGenerator<any, any, any>> = T extends SchemaGenerator<infer Options, any, any> ? Options : never
|
|
@@ -41,6 +40,11 @@ export type SchemaGeneratorOptions = {
|
|
|
41
40
|
emptySchemaType: 'any' | 'unknown' | 'void'
|
|
42
41
|
enumType?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
|
|
43
42
|
enumSuffix?: string
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated Will be removed in v5. Use `collisionDetection: true` instead to prevent enum name collisions.
|
|
45
|
+
* When `collisionDetection` is enabled, the rootName-based approach eliminates the need for numeric suffixes.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
44
48
|
usedEnumNames?: Record<string, number>
|
|
45
49
|
mapper?: Record<string, string>
|
|
46
50
|
typed?: boolean
|
|
@@ -64,6 +68,7 @@ type SchemaProps = {
|
|
|
64
68
|
schema: SchemaObject | null
|
|
65
69
|
name: string | null
|
|
66
70
|
parentName: string | null
|
|
71
|
+
rootName?: string | null
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
export class SchemaGenerator<
|
|
@@ -74,13 +79,31 @@ export class SchemaGenerator<
|
|
|
74
79
|
// Collect the types of all referenced schemas, so we can export them later
|
|
75
80
|
refs: Refs = {}
|
|
76
81
|
|
|
77
|
-
//
|
|
78
|
-
|
|
82
|
+
// Map from original component paths to resolved schema names (after collision resolution)
|
|
83
|
+
// e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
|
|
84
|
+
#schemaNameMapping: Map<string, string> = new Map()
|
|
85
|
+
|
|
86
|
+
// Flag to track if nameMapping has been initialized
|
|
87
|
+
#nameMappingInitialized = false
|
|
79
88
|
|
|
80
89
|
// Cache for parsed schemas to avoid redundant parsing
|
|
81
90
|
// Using WeakMap for automatic garbage collection when schemas are no longer referenced
|
|
82
91
|
#parseCache: Map<string, Schema[]> = new Map()
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Ensure the name mapping is initialized (lazy initialization)
|
|
95
|
+
*/
|
|
96
|
+
#ensureNameMapping() {
|
|
97
|
+
if (this.#nameMappingInitialized) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { oas, contentType, include } = this.context
|
|
102
|
+
const { nameMapping } = oas.getSchemas({ contentType, includes: include })
|
|
103
|
+
this.#schemaNameMapping = nameMapping
|
|
104
|
+
this.#nameMappingInitialized = true
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
/**
|
|
85
108
|
* Creates a type node from a given schema.
|
|
86
109
|
* Delegates to getBaseTypeFromSchema internally and
|
|
@@ -102,6 +125,7 @@ export class SchemaGenerator<
|
|
|
102
125
|
schema: props.schema,
|
|
103
126
|
name: props.name,
|
|
104
127
|
parentName: props.parentName,
|
|
128
|
+
rootName: props.rootName,
|
|
105
129
|
})
|
|
106
130
|
|
|
107
131
|
const cached = this.#parseCache.get(cacheKey)
|
|
@@ -321,7 +345,7 @@ export class SchemaGenerator<
|
|
|
321
345
|
/**
|
|
322
346
|
* Recursively creates a type literal with the given props.
|
|
323
347
|
*/
|
|
324
|
-
#parseProperties(name: string | null, schemaObject: SchemaObject): Schema[] {
|
|
348
|
+
#parseProperties(name: string | null, schemaObject: SchemaObject, rootName?: string | null): Schema[] {
|
|
325
349
|
const properties = schemaObject?.properties || {}
|
|
326
350
|
const additionalProperties = schemaObject?.additionalProperties
|
|
327
351
|
const required = schemaObject?.required
|
|
@@ -335,7 +359,7 @@ export class SchemaGenerator<
|
|
|
335
359
|
const isRequired = Array.isArray(required) ? required?.includes(propertyName) : !!required
|
|
336
360
|
const nullable = isNullable(propertySchema)
|
|
337
361
|
|
|
338
|
-
validationFunctions.push(...this.parse({ schema: propertySchema, name: propertyName, parentName: name }))
|
|
362
|
+
validationFunctions.push(...this.parse({ schema: propertySchema, name: propertyName, parentName: name, rootName: rootName || name }))
|
|
339
363
|
|
|
340
364
|
validationFunctions.push({
|
|
341
365
|
keyword: schemaKeywords.name,
|
|
@@ -359,7 +383,7 @@ export class SchemaGenerator<
|
|
|
359
383
|
additionalPropertiesSchemas =
|
|
360
384
|
additionalProperties === true || !Object.keys(additionalProperties).length
|
|
361
385
|
? [{ keyword: this.#getUnknownType(name) }]
|
|
362
|
-
: this.parse({ schema: additionalProperties as SchemaObject, name: null, parentName: name })
|
|
386
|
+
: this.parse({ schema: additionalProperties as SchemaObject, name: null, parentName: name, rootName: rootName || name })
|
|
363
387
|
}
|
|
364
388
|
|
|
365
389
|
let patternPropertiesSchemas: Record<string, Schema[]> = {}
|
|
@@ -369,7 +393,7 @@ export class SchemaGenerator<
|
|
|
369
393
|
const schemas =
|
|
370
394
|
patternSchema === true || !Object.keys(patternSchema as object).length
|
|
371
395
|
? [{ keyword: this.#getUnknownType(name) }]
|
|
372
|
-
: this.parse({ schema: patternSchema, name: null, parentName: name })
|
|
396
|
+
: this.parse({ schema: patternSchema, name: null, parentName: name, rootName: rootName || name })
|
|
373
397
|
|
|
374
398
|
return {
|
|
375
399
|
...acc,
|
|
@@ -453,15 +477,21 @@ export class SchemaGenerator<
|
|
|
453
477
|
]
|
|
454
478
|
}
|
|
455
479
|
|
|
456
|
-
|
|
480
|
+
// Ensure name mapping is initialized before resolving names
|
|
481
|
+
this.#ensureNameMapping()
|
|
482
|
+
|
|
483
|
+
const originalName = $ref.replace(/.+\//, '')
|
|
484
|
+
// Use the full $ref path to look up the collision-resolved name
|
|
485
|
+
const resolvedName = this.#schemaNameMapping.get($ref) || originalName
|
|
486
|
+
|
|
457
487
|
const propertyName = this.context.pluginManager.resolveName({
|
|
458
|
-
name:
|
|
488
|
+
name: resolvedName,
|
|
459
489
|
pluginKey: this.context.plugin.key,
|
|
460
490
|
type: 'function',
|
|
461
491
|
})
|
|
462
492
|
|
|
463
493
|
const fileName = this.context.pluginManager.resolveName({
|
|
464
|
-
name:
|
|
494
|
+
name: resolvedName,
|
|
465
495
|
pluginKey: this.context.plugin.key,
|
|
466
496
|
type: 'file',
|
|
467
497
|
})
|
|
@@ -473,7 +503,7 @@ export class SchemaGenerator<
|
|
|
473
503
|
|
|
474
504
|
this.refs[$ref] = {
|
|
475
505
|
propertyName,
|
|
476
|
-
originalName,
|
|
506
|
+
originalName: resolvedName,
|
|
477
507
|
path: file.path,
|
|
478
508
|
}
|
|
479
509
|
|
|
@@ -504,7 +534,7 @@ export class SchemaGenerator<
|
|
|
504
534
|
return schema
|
|
505
535
|
}
|
|
506
536
|
|
|
507
|
-
const objectPropertySchema = SchemaGenerator.find(this.parse({ schema: schemaObject, name: null, parentName: null }), schemaKeywords.object)
|
|
537
|
+
const objectPropertySchema = SchemaGenerator.find(this.parse({ schema: schemaObject, name: null, parentName: null, rootName: null }), schemaKeywords.object)
|
|
508
538
|
|
|
509
539
|
return {
|
|
510
540
|
...schema,
|
|
@@ -605,7 +635,7 @@ export class SchemaGenerator<
|
|
|
605
635
|
* This is the very core of the OpenAPI to TS conversion - it takes a
|
|
606
636
|
* schema and returns the appropriate type.
|
|
607
637
|
*/
|
|
608
|
-
#parseSchemaObject({ schema: _schemaObject, name, parentName }: SchemaProps): Schema[] {
|
|
638
|
+
#parseSchemaObject({ schema: _schemaObject, name, parentName, rootName }: SchemaProps): Schema[] {
|
|
609
639
|
const normalizedSchema = this.context.oas.flattenSchema(_schemaObject)
|
|
610
640
|
|
|
611
641
|
const { schemaObject, version } = this.#getParsedSchemaObject(normalizedSchema)
|
|
@@ -710,6 +740,7 @@ export class SchemaGenerator<
|
|
|
710
740
|
schema: { ...schemaObject, type: item },
|
|
711
741
|
name,
|
|
712
742
|
parentName,
|
|
743
|
+
rootName,
|
|
713
744
|
})[0],
|
|
714
745
|
)
|
|
715
746
|
.filter(Boolean)
|
|
@@ -764,7 +795,7 @@ export class SchemaGenerator<
|
|
|
764
795
|
args: (schemaObject.oneOf || schemaObject.anyOf)!
|
|
765
796
|
.map((item) => {
|
|
766
797
|
// first item, this is ref
|
|
767
|
-
return item && this.parse({ schema: item as SchemaObject, name, parentName })[0]
|
|
798
|
+
return item && this.parse({ schema: item as SchemaObject, name, parentName, rootName })[0]
|
|
768
799
|
})
|
|
769
800
|
.filter(Boolean),
|
|
770
801
|
}
|
|
@@ -778,7 +809,7 @@ export class SchemaGenerator<
|
|
|
778
809
|
}
|
|
779
810
|
|
|
780
811
|
if (schemaWithoutOneOf.properties) {
|
|
781
|
-
const propertySchemas = this.parse({ schema: schemaWithoutOneOf, name, parentName })
|
|
812
|
+
const propertySchemas = this.parse({ schema: schemaWithoutOneOf, name, parentName, rootName })
|
|
782
813
|
|
|
783
814
|
union.args = [
|
|
784
815
|
...union.args.map((arg) => {
|
|
@@ -808,7 +839,7 @@ export class SchemaGenerator<
|
|
|
808
839
|
return []
|
|
809
840
|
}
|
|
810
841
|
|
|
811
|
-
return item ? this.parse({ schema: item, name, parentName }) : []
|
|
842
|
+
return item ? this.parse({ schema: item, name, parentName, rootName }) : []
|
|
812
843
|
})
|
|
813
844
|
.filter(Boolean),
|
|
814
845
|
}
|
|
@@ -848,7 +879,7 @@ export class SchemaGenerator<
|
|
|
848
879
|
}
|
|
849
880
|
|
|
850
881
|
for (const item of parsedItems) {
|
|
851
|
-
const parsed = this.parse({ schema: item, name, parentName })
|
|
882
|
+
const parsed = this.parse({ schema: item, name, parentName, rootName })
|
|
852
883
|
|
|
853
884
|
if (Array.isArray(parsed)) {
|
|
854
885
|
and.args = and.args ? and.args.concat(parsed) : parsed
|
|
@@ -857,7 +888,7 @@ export class SchemaGenerator<
|
|
|
857
888
|
}
|
|
858
889
|
|
|
859
890
|
if (schemaWithoutAllOf.properties) {
|
|
860
|
-
and.args = [...(and.args || []), ...this.parse({ schema: schemaWithoutAllOf, name, parentName })]
|
|
891
|
+
and.args = [...(and.args || []), ...this.parse({ schema: schemaWithoutAllOf, name, parentName, rootName })]
|
|
861
892
|
}
|
|
862
893
|
|
|
863
894
|
return SchemaGenerator.combineObjects([and, ...baseItems])
|
|
@@ -880,12 +911,22 @@ export class SchemaGenerator<
|
|
|
880
911
|
items: normalizedItems,
|
|
881
912
|
} as SchemaObject
|
|
882
913
|
|
|
883
|
-
return this.parse({ schema: normalizedSchema, name, parentName })
|
|
914
|
+
return this.parse({ schema: normalizedSchema, name, parentName, rootName })
|
|
884
915
|
}
|
|
885
916
|
|
|
886
917
|
// Removed verbose enum parsing debug log - too noisy for hundreds of enums
|
|
887
918
|
|
|
888
|
-
|
|
919
|
+
// Include rootName in enum naming to avoid collisions for nested enums with same path
|
|
920
|
+
// Only add rootName if it differs from parentName to avoid duplication
|
|
921
|
+
// This is controlled by the collisionDetection flag to maintain backward compatibility
|
|
922
|
+
const useCollisionDetection = this.context.oas.options.collisionDetection ?? false
|
|
923
|
+
const enumNameParts =
|
|
924
|
+
useCollisionDetection && rootName && rootName !== parentName ? [rootName, parentName, name, options.enumSuffix] : [parentName, name, options.enumSuffix]
|
|
925
|
+
|
|
926
|
+
// @deprecated usedEnumNames will be removed in v5 - collisionDetection with rootName-based naming eliminates the need for numeric suffixes
|
|
927
|
+
const enumName = useCollisionDetection
|
|
928
|
+
? pascalCase(enumNameParts.join(' '))
|
|
929
|
+
: getUniqueName(pascalCase(enumNameParts.join(' ')), this.options.usedEnumNames || {})
|
|
889
930
|
const typeName = this.context.pluginManager.resolveName({
|
|
890
931
|
name: enumName,
|
|
891
932
|
pluginKey: this.context.plugin.key,
|
|
@@ -1016,13 +1057,14 @@ export class SchemaGenerator<
|
|
|
1016
1057
|
max,
|
|
1017
1058
|
items: prefixItems
|
|
1018
1059
|
.map((item) => {
|
|
1019
|
-
return this.parse({ schema: item, name, parentName })[0]
|
|
1060
|
+
return this.parse({ schema: item, name, parentName, rootName })[0]
|
|
1020
1061
|
})
|
|
1021
1062
|
.filter(Boolean),
|
|
1022
1063
|
rest: this.parse({
|
|
1023
1064
|
schema: items,
|
|
1024
1065
|
name,
|
|
1025
1066
|
parentName,
|
|
1067
|
+
rootName,
|
|
1026
1068
|
})[0],
|
|
1027
1069
|
},
|
|
1028
1070
|
},
|
|
@@ -1168,7 +1210,7 @@ export class SchemaGenerator<
|
|
|
1168
1210
|
if ('items' in schemaObject || schemaObject.type === ('array' as 'string')) {
|
|
1169
1211
|
const min = schemaObject.minimum ?? schemaObject.minLength ?? schemaObject.minItems ?? undefined
|
|
1170
1212
|
const max = schemaObject.maximum ?? schemaObject.maxLength ?? schemaObject.maxItems ?? undefined
|
|
1171
|
-
const items = this.parse({ schema: 'items' in schemaObject ? (schemaObject.items as SchemaObject) : [], name, parentName })
|
|
1213
|
+
const items = this.parse({ schema: 'items' in schemaObject ? (schemaObject.items as SchemaObject) : [], name, parentName, rootName })
|
|
1172
1214
|
const unique = !!schemaObject.uniqueItems
|
|
1173
1215
|
|
|
1174
1216
|
return [
|
|
@@ -1205,10 +1247,10 @@ export class SchemaGenerator<
|
|
|
1205
1247
|
return acc
|
|
1206
1248
|
}, schemaObject || {}) as SchemaObject
|
|
1207
1249
|
|
|
1208
|
-
return [...this.#parseProperties(name, schemaObjectOverridden), ...baseItems]
|
|
1250
|
+
return [...this.#parseProperties(name, schemaObjectOverridden, rootName), ...baseItems]
|
|
1209
1251
|
}
|
|
1210
1252
|
|
|
1211
|
-
return [...this.#parseProperties(name, schemaObject), ...baseItems]
|
|
1253
|
+
return [...this.#parseProperties(name, schemaObject, rootName), ...baseItems]
|
|
1212
1254
|
}
|
|
1213
1255
|
|
|
1214
1256
|
if (schemaObject.type) {
|
|
@@ -1244,13 +1286,29 @@ export class SchemaGenerator<
|
|
|
1244
1286
|
|
|
1245
1287
|
async build(...generators: Array<Generator<TPluginOptions>>): Promise<Array<KubbFile.File<TFileMeta>>> {
|
|
1246
1288
|
const { oas, contentType, include } = this.context
|
|
1247
|
-
const schemas = getSchemas({ oas, contentType, includes: include })
|
|
1248
|
-
const schemaEntries = Object.entries(schemas)
|
|
1249
1289
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1290
|
+
// Initialize the name mapping if not already done
|
|
1291
|
+
if (!this.#nameMappingInitialized) {
|
|
1292
|
+
const { schemas, nameMapping } = oas.getSchemas({ contentType, includes: include })
|
|
1293
|
+
this.#schemaNameMapping = nameMapping
|
|
1294
|
+
this.#nameMappingInitialized = true
|
|
1295
|
+
const schemaEntries = Object.entries(schemas)
|
|
1296
|
+
|
|
1297
|
+
this.context.events?.emit('debug', {
|
|
1298
|
+
date: new Date(),
|
|
1299
|
+
logs: [`Building ${schemaEntries.length} schemas`, ` • Content Type: ${contentType || 'application/json'}`, ` • Generators: ${generators.length}`],
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
// Continue with build using the schemas
|
|
1303
|
+
return this.#doBuild(schemas, generators)
|
|
1304
|
+
}
|
|
1305
|
+
// If already initialized, just get the schemas (without mapping)
|
|
1306
|
+
const { schemas } = oas.getSchemas({ contentType, includes: include })
|
|
1307
|
+
return this.#doBuild(schemas, generators)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async #doBuild(schemas: Record<string, OasTypes.SchemaObject>, generators: Array<Generator<TPluginOptions>>): Promise<Array<KubbFile.File<TFileMeta>>> {
|
|
1311
|
+
const schemaEntries = Object.entries(schemas)
|
|
1254
1312
|
|
|
1255
1313
|
// Increased parallelism for better performance
|
|
1256
1314
|
// - generatorLimit increased from 1 to 3 to allow parallel generator processing
|
|
@@ -1263,7 +1321,7 @@ export class SchemaGenerator<
|
|
|
1263
1321
|
const schemaTasks = schemaEntries.map(([name, schemaObject]) =>
|
|
1264
1322
|
schemaLimit(async () => {
|
|
1265
1323
|
const options = this.#getOptions(name)
|
|
1266
|
-
const tree = this.parse({ schema: schemaObject, name, parentName: null })
|
|
1324
|
+
const tree = this.parse({ schema: schemaObject, name, parentName: null, rootName: name })
|
|
1267
1325
|
|
|
1268
1326
|
if (generator.type === 'react') {
|
|
1269
1327
|
await buildSchema(
|
package/src/plugin.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const pluginOas = definePlugin<PluginOas>((options) => {
|
|
|
23
23
|
contentType,
|
|
24
24
|
oasClass,
|
|
25
25
|
discriminator = 'strict',
|
|
26
|
+
collisionDetection = false,
|
|
26
27
|
} = options
|
|
27
28
|
|
|
28
29
|
const getOas = async ({ validate, config, events }: { validate: boolean; config: Config; events: AsyncEventEmitter<KubbEvents> }): Promise<Oas> => {
|
|
@@ -32,6 +33,7 @@ export const pluginOas = definePlugin<PluginOas>((options) => {
|
|
|
32
33
|
oas.setOptions({
|
|
33
34
|
contentType,
|
|
34
35
|
discriminator,
|
|
36
|
+
collisionDetection,
|
|
35
37
|
})
|
|
36
38
|
|
|
37
39
|
try {
|
package/src/types.ts
CHANGED
|
@@ -71,6 +71,36 @@ export type Options = {
|
|
|
71
71
|
* Define some generators next to the JSON generation
|
|
72
72
|
*/
|
|
73
73
|
generators?: Array<Generator<PluginOas>>
|
|
74
|
+
/**
|
|
75
|
+
* Resolve name collisions when schemas from different components share the same name (case-insensitive).
|
|
76
|
+
*
|
|
77
|
+
* When enabled, Kubb automatically detects and resolves collisions using intelligent suffixes:
|
|
78
|
+
* - Cross-component collisions: Adds semantic suffixes based on the component type (Schema/Response/Request)
|
|
79
|
+
* - Same-component collisions: Adds numeric suffixes (2, 3, ...) for case-insensitive duplicates
|
|
80
|
+
* - Nested enum collisions: Includes root schema name in enum names to prevent duplicates across schemas
|
|
81
|
+
*
|
|
82
|
+
* When disabled (legacy behavior), collisions may result in duplicate files or overwrite issues.
|
|
83
|
+
*
|
|
84
|
+
* **Cross-component collision example:**
|
|
85
|
+
* If you have "Order" in both schemas and requestBodies:
|
|
86
|
+
* - With `collisionDetection: true`: Generates `OrderSchema.ts`, `OrderRequest.ts`
|
|
87
|
+
* - With `collisionDetection: false`: May generate duplicate `Order.ts` files
|
|
88
|
+
*
|
|
89
|
+
* **Same-component collision example:**
|
|
90
|
+
* If you have "Variant" and "variant" in schemas:
|
|
91
|
+
* - With `collisionDetection: true`: Generates `Variant.ts`, `Variant2.ts`
|
|
92
|
+
* - With `collisionDetection: false`: May overwrite or create duplicates
|
|
93
|
+
*
|
|
94
|
+
* **Nested enum collision example:**
|
|
95
|
+
* If you have "params.channel" enum in both "NotificationTypeA" and "NotificationTypeB":
|
|
96
|
+
* - With `collisionDetection: true`: Generates `notificationTypeAParamsChannelEnum`, `notificationTypeBParamsChannelEnum`
|
|
97
|
+
* - With `collisionDetection: false`: Generates duplicate `paramsChannelEnum` in both files
|
|
98
|
+
*
|
|
99
|
+
* @default false (will be `true` in v5)
|
|
100
|
+
* @see https://github.com/kubb-labs/kubb/issues/1999
|
|
101
|
+
* @note In Kubb v5, this will be enabled by default and the deprecated `usedEnumNames` mechanism will be removed
|
|
102
|
+
*/
|
|
103
|
+
collisionDetection?: boolean
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
/**
|
package/src/utils/getSchemas.ts
CHANGED
|
@@ -1,119 +1,41 @@
|
|
|
1
1
|
import type { contentType, Oas, OasTypes } from '@kubb/oas'
|
|
2
2
|
|
|
3
|
+
export type GetSchemasResult = {
|
|
4
|
+
schemas: Record<string, OasTypes.SchemaObject>
|
|
5
|
+
/**
|
|
6
|
+
* Mapping from original component name to resolved name after collision handling
|
|
7
|
+
* e.g., { 'Order': 'OrderSchema', 'variant': 'variant2' }
|
|
8
|
+
*/
|
|
9
|
+
nameMapping: Map<string, string>
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
type Mode = 'schemas' | 'responses' | 'requestBodies'
|
|
4
13
|
|
|
5
14
|
type GetSchemasProps = {
|
|
6
15
|
oas: Oas
|
|
7
16
|
contentType?: contentType
|
|
8
17
|
includes?: Mode[]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (Array.isArray(schema)) {
|
|
16
|
-
for (const item of schema) {
|
|
17
|
-
collectRefs(item, refs)
|
|
18
|
-
}
|
|
19
|
-
return refs
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (schema && typeof schema === 'object') {
|
|
23
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
24
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
25
|
-
const match = value.match(/^#\/components\/schemas\/(.+)$/)
|
|
26
|
-
if (match) {
|
|
27
|
-
refs.add(match[1]!)
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
collectRefs(value, refs)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return refs
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Sort schemas topologically so referenced schemas appear first.
|
|
40
|
-
*/
|
|
41
|
-
function sortSchemas(schemas: Record<string, OasTypes.SchemaObject>): Record<string, OasTypes.SchemaObject> {
|
|
42
|
-
const deps = new Map<string, string[]>()
|
|
43
|
-
|
|
44
|
-
for (const [name, schema] of Object.entries(schemas)) {
|
|
45
|
-
deps.set(name, Array.from(collectRefs(schema)))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const sorted: string[] = []
|
|
49
|
-
const visited = new Set<string>()
|
|
50
|
-
|
|
51
|
-
function visit(name: string, stack = new Set<string>()) {
|
|
52
|
-
if (visited.has(name)) {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
if (stack.has(name)) {
|
|
56
|
-
return
|
|
57
|
-
} // circular refs, ignore
|
|
58
|
-
stack.add(name)
|
|
59
|
-
const children = deps.get(name) || []
|
|
60
|
-
for (const child of children) {
|
|
61
|
-
if (deps.has(child)) {
|
|
62
|
-
visit(child, stack)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
stack.delete(name)
|
|
66
|
-
visited.add(name)
|
|
67
|
-
sorted.push(name)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const name of Object.keys(schemas)) {
|
|
71
|
-
visit(name)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const sortedSchemas: Record<string, OasTypes.SchemaObject> = {}
|
|
75
|
-
for (const name of sorted) {
|
|
76
|
-
sortedSchemas[name] = schemas[name]!
|
|
77
|
-
}
|
|
78
|
-
return sortedSchemas
|
|
18
|
+
/**
|
|
19
|
+
* Whether to resolve name collisions with suffixes.
|
|
20
|
+
* If not provided, uses oas.options.collisionDetection
|
|
21
|
+
* @default false (from oas.options or fallback)
|
|
22
|
+
*/
|
|
23
|
+
collisionDetection?: boolean
|
|
79
24
|
}
|
|
80
25
|
|
|
81
26
|
/**
|
|
82
27
|
* Collect schemas from OpenAPI components (schemas, responses, requestBodies)
|
|
83
|
-
* and return them in dependency order.
|
|
28
|
+
* and return them in dependency order along with name mapping for collision resolution.
|
|
29
|
+
*
|
|
30
|
+
* This function is a wrapper around the oas.getSchemas() method for backward compatibility.
|
|
31
|
+
* New code should use oas.getSchemas() directly.
|
|
32
|
+
*
|
|
33
|
+
* @deprecated Use oas.getSchemas() instead
|
|
84
34
|
*/
|
|
85
|
-
export function getSchemas({ oas, contentType, includes = ['schemas', 'requestBodies', 'responses'] }: GetSchemasProps):
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
...schemas,
|
|
92
|
-
...((components?.schemas as Record<string, OasTypes.SchemaObject>) || {}),
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (includes.includes('responses')) {
|
|
97
|
-
const responses = components?.responses || {}
|
|
98
|
-
for (const [name, response] of Object.entries(responses)) {
|
|
99
|
-
const responseObject = response as OasTypes.ResponseObject
|
|
100
|
-
if (responseObject.content && !schemas[name]) {
|
|
101
|
-
const firstContentType = Object.keys(responseObject.content)[0] || 'application/json'
|
|
102
|
-
schemas[name] = responseObject.content?.[contentType || firstContentType]?.schema as OasTypes.SchemaObject
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (includes.includes('requestBodies')) {
|
|
108
|
-
const requestBodies = components?.requestBodies || {}
|
|
109
|
-
for (const [name, request] of Object.entries(requestBodies)) {
|
|
110
|
-
const requestObject = request as OasTypes.RequestBodyObject
|
|
111
|
-
if (requestObject.content && !schemas[name]) {
|
|
112
|
-
const firstContentType = Object.keys(requestObject.content)[0] || 'application/json'
|
|
113
|
-
schemas[name] = requestObject.content?.[contentType || firstContentType]?.schema as OasTypes.SchemaObject
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return sortSchemas(schemas)
|
|
35
|
+
export function getSchemas({ oas, contentType, includes = ['schemas', 'requestBodies', 'responses'], collisionDetection }: GetSchemasProps): GetSchemasResult {
|
|
36
|
+
return oas.getSchemas({
|
|
37
|
+
contentType,
|
|
38
|
+
includes,
|
|
39
|
+
collisionDetection,
|
|
40
|
+
})
|
|
119
41
|
}
|