1
- import generate from "@babel/generator" ;
2
- import * as t from "@babel/types" ;
3
-
4
- import { SchemaTSContext , type SchemaTSOptions } from "./context" ;
5
- import type { JSONSchema } from "./types" ;
6
- import { isValidIdentifier , toCamelCase , toPascalCase } from "./utils" ;
7
-
8
- const identifier = ( name : string , typeAnnotation : t . TSTypeAnnotation ) => {
9
- const i = t . identifier ( name ) ;
10
- i . typeAnnotation = typeAnnotation ;
11
- return i ;
12
- } ;
13
-
14
- const anyOrObjectWithUnknownProps = ( ctx : SchemaTSContext ) => {
15
- return ctx . options . strictTypeSafety ? t . tsTypeLiteral ( [
16
- t . tsIndexSignature (
17
- [
18
- identifier ( 'key' , t . tsTypeAnnotation (
19
- t . tsStringKeyword ( )
20
- ) )
21
- ] ,
22
- t . tsTypeAnnotation ( t . tsUnknownKeyword ( ) )
23
- )
24
- ] ) : t . tsAnyKeyword ( ) ;
25
- }
26
-
27
- export function generateTypeScript ( schema : JSONSchema , options ?: Partial < SchemaTSOptions > ) : string {
28
- const interfaces = [ ] ;
29
- const ctx = new SchemaTSContext ( options , schema , schema , [ ] ) ;
30
-
31
- try {
32
- // Process both $defs and definitions
33
- const definitions = schema . $defs || schema . definitions || { } ;
34
- for ( const key in definitions ) {
35
- interfaces . push ( createInterfaceDeclaration ( ctx , toPascalCase ( key ) , definitions [ key ] ) ) ;
36
- }
37
- } catch ( e ) {
38
- console . error ( 'Error processing interfaces' ) ;
39
- throw e ;
40
- }
41
-
42
- // Process the main schema
43
- const title = schema . title ;
44
- if ( ! title ) {
45
- console . error ( 'schema or options require a title' ) ;
46
- return '' ; // Ensure there's a return on error condition
47
- }
48
- interfaces . push ( createInterfaceDeclaration ( ctx , toPascalCase ( title ) , schema ) ) ;
49
- return generate ( t . file ( t . program ( interfaces ) ) ) . code ;
50
- }
51
-
52
- function createInterfaceDeclaration (
53
- ctx : SchemaTSContext ,
54
- name : string ,
55
- schema : JSONSchema
56
- ) : t . ExportNamedDeclaration {
57
- // Handle standard properties if they exist
58
- let bodyElements : any = [ ] ;
59
- if ( schema . properties ) {
60
- const properties = schema . properties ;
61
- const required = schema . required || [ ] ;
62
- bodyElements = Object . keys ( properties ) . map ( key => {
63
- const prop = properties [ key ] ;
64
- return createPropertySignature ( ctx , key , prop , required , schema ) ;
65
- } ) ;
66
- }
67
-
68
- // Handling additionalProperties if they exist
69
- if ( schema . additionalProperties ) {
70
- const additionalType = typeof schema . additionalProperties === 'boolean' ?
71
- t . tsStringKeyword ( ) : getTypeForProp ( ctx , schema . additionalProperties , [ ] , schema ) ;
72
- const indexSignature = t . tsIndexSignature (
73
- [ t . identifier ( "key" ) ] , // index name, can be any valid name
74
- t . tsTypeAnnotation ( additionalType )
75
- ) ;
76
- indexSignature . parameters [ 0 ] . typeAnnotation = t . tsTypeAnnotation ( t . tsStringKeyword ( ) ) ;
77
- bodyElements . push ( indexSignature ) ;
78
- }
79
-
80
- // Handling oneOf, anyOf, allOf if properties are not defined
81
- if ( ! schema . properties && ( schema . oneOf || schema . anyOf || schema . allOf ) ) {
82
- const types = [ ] ;
83
- if ( schema . oneOf ) {
84
- types . push ( getTypeForProp ( ctx , { oneOf : schema . oneOf } , [ ] , schema ) ) ;
85
- }
86
- if ( schema . anyOf ) {
87
- types . push ( getTypeForProp ( ctx , { anyOf : schema . anyOf } , [ ] , schema ) ) ;
88
- }
89
- if ( schema . allOf ) {
90
- types . push ( getTypeForProp ( ctx , { allOf : schema . allOf } , [ ] , schema ) ) ;
91
- }
92
-
93
- // Create a union type if multiple types are generated
94
- const combinedType = types . length > 1 ? t . tsUnionType ( types ) : types [ 0 ] ;
95
-
96
- // Create a type alias instead of an interface if we're only handling these constructs
97
- const typeAlias = t . tsTypeAliasDeclaration ( t . identifier ( name ) , null , combinedType ) ;
98
- return t . exportNamedDeclaration ( typeAlias ) ;
99
- }
100
-
101
- // Finally, create the interface declaration if there are any body elements
102
- if ( bodyElements . length > 0 ) {
103
- const interfaceDeclaration = t . tsInterfaceDeclaration (
104
- t . identifier ( name ) ,
105
- null ,
106
- [ ] ,
107
- t . tsInterfaceBody ( bodyElements )
108
- ) ;
109
- return t . exportNamedDeclaration ( interfaceDeclaration ) ;
110
- }
111
-
112
- if ( schema . type ) {
113
- return t . exportNamedDeclaration ( t . tsTypeAliasDeclaration ( t . identifier ( name ) , null , getTypeForProp ( ctx , schema , [ ] , schema ) ) ) ;
114
- }
115
-
116
- // Fallback to exporting a basic type if nothing else is possible
117
- console . warn ( `No properties or type definitions found for ${ name } , defaulting to 'any'.` ) ;
118
- return t . exportNamedDeclaration ( t . tsTypeAliasDeclaration ( t . identifier ( name ) , null , t . tsAnyKeyword ( ) ) ) ;
119
- }
120
-
121
-
122
- function createPropertySignature (
123
- ctx : SchemaTSContext ,
124
- key : string ,
125
- prop : JSONSchema ,
126
- required : string [ ] ,
127
- schema : JSONSchema
128
- ) : t . TSPropertySignature {
129
-
130
- const isIdent = isValidIdentifier ( key ) ;
131
- const name = ctx . options . useCamelCase ? toCamelCase ( key ) : key ;
132
- const propType = getTypeForProp ( ctx , prop , required , schema ) ;
133
- const identifier = isIdent ? t . identifier ( name ) : t . stringLiteral ( key ) ;
134
- const propSig = t . tsPropertySignature (
135
- identifier ,
136
- t . tsTypeAnnotation ( propType )
137
- ) ;
138
- propSig . optional = ! required . includes ( key ) ;
139
- return propSig ;
140
- }
141
-
142
- function getTypeForProp ( ctx : SchemaTSContext , prop : JSONSchema , required : string [ ] , schema : JSONSchema ) : t . TSType {
143
- if ( prop . $ref ) {
144
- return resolveRefType ( ctx , prop . $ref , schema ) ;
145
- }
146
-
147
- if ( prop . enum ) {
148
- const enumType = prop . enum . map ( enumValue => t . tsLiteralType ( t . stringLiteral ( enumValue ) ) ) ;
149
- return t . tsUnionType ( enumType ) ;
150
- }
151
-
152
- if ( prop . const ) {
153
- return t . tsLiteralType ( t . stringLiteral ( prop . const ) ) ;
154
- }
155
-
156
- if ( prop . type ) {
157
- if ( Array . isArray ( prop . type ) ) {
158
- const arrayType = prop . type . map ( type => getTypeForProp ( ctx , { type, items : prop . items } , [ ] , schema ) ) ;
159
- return t . tsUnionType ( arrayType ) ;
160
- }
161
-
162
- switch ( prop . type ) {
163
- case 'string' :
164
- return t . tsStringKeyword ( ) ;
165
- case 'number' :
166
- case 'integer' :
167
- return t . tsNumberKeyword ( ) ;
168
- case 'boolean' :
169
- return t . tsBooleanKeyword ( ) ;
170
- case 'null' :
171
- return t . tsNullKeyword ( ) ;
172
- case 'array' :
173
- if ( prop . items ) {
174
- return t . tsArrayType ( getTypeForProp ( ctx , prop . items , required , schema ) ) ;
175
- } else {
176
- throw new Error ( 'Array items specification is missing' ) ;
177
- }
178
- case 'object' :
179
- if ( prop . properties ) {
180
- const nestedProperties = prop . properties ;
181
- const nestedRequired = prop . required || [ ] ;
182
- const typeElements = Object . keys ( nestedProperties ) . map ( nestedKey => {
183
- const nestedProp = nestedProperties [ nestedKey ] ;
184
- return createPropertySignature ( ctx , nestedKey , nestedProp , nestedRequired , schema ) ;
185
- } ) ;
186
- return t . tsTypeLiteral ( typeElements ) ;
187
- } else {
188
- // Handle dynamic properties with strict type safety option
189
- return anyOrObjectWithUnknownProps ( ctx ) ;
190
- }
191
- default :
192
- return t . tsAnyKeyword ( ) ;
193
- }
194
- }
195
-
196
- // Handling oneOf, anyOf, allOf
197
- if ( prop . anyOf ) {
198
- const types = prop . anyOf . map ( ( subProp ) => getTypeForProp ( ctx , subProp , required , schema ) ) ;
199
- return t . tsUnionType ( types ) ;
200
- }
201
- if ( prop . allOf ) {
202
- const types = prop . allOf . map ( ( subProp ) => getTypeForProp ( ctx , subProp , required , schema ) ) ;
203
- return t . tsIntersectionType ( types ) ;
204
- }
205
- if ( prop . oneOf ) {
206
- const types = prop . oneOf . map ( ( subProp ) => getTypeForProp ( ctx , subProp , required , schema ) ) ;
207
- return t . tsUnionType ( types ) ;
208
- }
209
-
210
- // Fallback when no types are defined
211
- return t . tsAnyKeyword ( )
212
-
213
- }
214
-
215
- function getTypeReferenceFromSchema ( schema : JSONSchema , definitionName : string ) : t . TSType | null {
216
- if ( definitionName ) {
217
- if ( schema . $defs && schema . $defs [ definitionName ] ) {
218
- return t . tsTypeReference ( t . identifier ( toPascalCase ( definitionName ) ) ) ;
219
- } else if ( schema . definitions && schema . definitions [ definitionName ] ) {
220
- return t . tsTypeReference ( t . identifier ( toPascalCase ( definitionName ) ) ) ;
221
- }
222
- }
223
- return null ; // Return null if no type reference is found
224
- }
225
-
226
-
227
- function resolveRefType ( ctx : SchemaTSContext , ref : string , schema : JSONSchema ) : t . TSType {
228
- const path = ref . split ( '/' ) ;
229
- const definitionName = path . pop ( ) ;
230
-
231
- // Try to resolve the type reference from the local schema
232
- const localTypeReference = getTypeReferenceFromSchema ( schema , definitionName ) ;
233
- if ( localTypeReference ) {
234
- return localTypeReference ;
235
- }
236
-
237
- // Try to resolve the type reference from the root schema
238
- const rootTypeReference = getTypeReferenceFromSchema ( ctx . root , definitionName ) ;
239
- if ( rootTypeReference ) {
240
- return rootTypeReference ;
241
- }
242
-
243
- // If no definitions are found in either schema, throw an error
244
- throw new Error ( `Reference ${ ref } not found in definitions or $defs.` ) ;
245
- }
1
+ export * from './context' ;
2
+ export * from './schema' ;
3
+ export * from './types' ;
4
+ export * from './utils' ;
0 commit comments