fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move schemaToType to plugin utils from @hey-api/types

+766 -772
+5 -3
packages/openapi-ts/src/compiler/typedef.ts
··· 22 22 comment?: Comments; 23 23 isReadOnly?: boolean; 24 24 isRequired?: boolean; 25 - name: string; 25 + name: string | ts.PropertyName; 26 26 type: any | ts.TypeNode; 27 27 }; 28 28 ··· 87 87 const signature = ts.factory.createPropertySignature( 88 88 modifiers, 89 89 useLegacyResolution || 90 - property.name.match(validTypescriptIdentifierRegExp) 90 + (typeof property.name === 'string' && 91 + property.name.match(validTypescriptIdentifierRegExp)) || 92 + (typeof property.name !== 'string' && ts.isPropertyName(property.name)) 91 93 ? property.name 92 94 : createStringLiteral({ text: property.name }), 93 95 questionToken, ··· 111 113 modifiers, 112 114 [ 113 115 createParameterDeclaration({ 114 - name: createIdentifier({ text: indexProperty.name }), 116 + name: createIdentifier({ text: String(indexProperty.name) }), 115 117 type: createKeywordTypeNode({ keyword: 'string' }), 116 118 }), 117 119 ],
+36 -1
packages/openapi-ts/src/ir/schema.ts
··· 1 - import type { IRSchemaObject } from './ir'; 1 + import type { IRParameterObject, IRSchemaObject } from './ir'; 2 2 3 3 /** 4 4 * Ensure we don't produce redundant types, e.g. string | string. ··· 70 70 71 71 return schema; 72 72 }; 73 + 74 + export const irParametersToIrSchema = ({ 75 + parameters, 76 + }: { 77 + parameters: Record<string, IRParameterObject>; 78 + }): IRSchemaObject => { 79 + const irSchema: IRSchemaObject = { 80 + type: 'object', 81 + }; 82 + 83 + if (parameters) { 84 + const properties: Record<string, IRSchemaObject> = {}; 85 + const required: Array<string> = []; 86 + 87 + for (const name in parameters) { 88 + const parameter = parameters[name]; 89 + 90 + properties[name] = deduplicateSchema({ 91 + schema: parameter.schema, 92 + }); 93 + 94 + if (parameter.required) { 95 + required.push(name); 96 + } 97 + } 98 + 99 + irSchema.properties = properties; 100 + 101 + if (required.length) { 102 + irSchema.required = required; 103 + } 104 + } 105 + 106 + return irSchema; 107 + };
+23 -766
packages/openapi-ts/src/plugins/@hey-api/types/plugin.ts
··· 1 - import type ts from 'typescript'; 2 - 3 - import type { Property } from '../../../compiler'; 4 1 import { compiler } from '../../../compiler'; 5 2 import type { IRContext } from '../../../ir/context'; 6 3 import type { 7 4 IROperationObject, 8 - IRParameterObject, 9 5 IRPathItemObject, 10 6 IRPathsObject, 11 7 IRSchemaObject, 12 8 } from '../../../ir/ir'; 13 9 import { operationResponsesMap } from '../../../ir/operation'; 14 - import { deduplicateSchema } from '../../../ir/schema'; 15 - import { ensureValidTypeScriptJavaScriptIdentifier } from '../../../openApi'; 16 - import { escapeComment } from '../../../utils/escape'; 17 - import { irRef, isRefOpenApiComponent } from '../../../utils/ref'; 10 + import { irParametersToIrSchema } from '../../../ir/schema'; 18 11 import type { PluginHandler } from '../../types'; 19 12 import { 13 + componentsToType, 14 + schemaToType, 15 + type SchemaToTypeOptions, 16 + } from '../../utils/types'; 17 + import { 20 18 operationDataRef, 21 19 operationErrorRef, 22 20 operationResponseRef, 23 21 } from '../services/plugin'; 24 22 import type { Config } from './types'; 25 23 26 - interface SchemaWithType<T extends Required<IRSchemaObject>['type']> 27 - extends Omit<IRSchemaObject, 'type'> { 28 - type: Extract<Required<IRSchemaObject>['type'], T>; 29 - } 30 - 31 - const typesId = 'types'; 32 - 33 - const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => { 34 - const comments = [ 35 - schema.description && escapeComment(schema.description), 36 - schema.deprecated && '@deprecated', 37 - ]; 38 - return comments; 39 - }; 40 - 41 - const addJavaScriptEnum = ({ 42 - $ref, 43 - context, 44 - schema, 45 - }: { 46 - $ref: string; 47 - context: IRContext; 48 - schema: SchemaWithType<'enum'>; 49 - }) => { 50 - const identifier = context.file({ id: typesId })!.identifier({ 51 - $ref, 52 - create: true, 53 - namespace: 'value', 54 - }); 55 - 56 - // TODO: parser - this is the old parser behavior where we would NOT 57 - // print nested enum identifiers if they already exist. This is a 58 - // blocker for referencing these identifiers within the file as 59 - // we cannot guarantee just because they have a duplicate identifier, 60 - // they have a duplicate value. 61 - if (!identifier.created) { 62 - return; 63 - } 64 - 65 - const enumObject = schemaToEnumObject({ schema }); 66 - 67 - const expression = compiler.objectExpression({ 68 - multiLine: true, 69 - obj: enumObject.obj, 70 - }); 71 - const node = compiler.constVariable({ 72 - assertion: 'const', 73 - comment: parseSchemaJsDoc({ schema }), 74 - exportConst: true, 75 - expression, 76 - name: identifier.name || '', 77 - }); 78 - return node; 79 - }; 80 - 81 - const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => { 82 - const typeofItems: Array< 83 - | 'string' 84 - | 'number' 85 - | 'bigint' 86 - | 'boolean' 87 - | 'symbol' 88 - | 'undefined' 89 - | 'object' 90 - | 'function' 91 - > = []; 92 - 93 - const obj = (schema.items ?? []).map((item) => { 94 - const typeOfItemConst = typeof item.const; 95 - 96 - if (!typeofItems.includes(typeOfItemConst)) { 97 - typeofItems.push(typeOfItemConst); 98 - } 99 - 100 - let key; 101 - if (item.title) { 102 - key = item.title; 103 - } else if (typeOfItemConst === 'number') { 104 - key = `_${item.const}`; 105 - } else if (typeOfItemConst === 'boolean') { 106 - const valid = typeOfItemConst ? 'true' : 'false'; 107 - key = valid.toLocaleUpperCase(); 108 - } else { 109 - let valid = ensureValidTypeScriptJavaScriptIdentifier( 110 - item.const as string, 111 - ); 112 - if (!valid) { 113 - // TODO: parser - abstract empty string handling 114 - valid = 'empty_string'; 115 - } 116 - key = valid.toLocaleUpperCase(); 117 - } 118 - return { 119 - comments: parseSchemaJsDoc({ schema: item }), 120 - key, 121 - value: item.const, 122 - }; 123 - }); 124 - 125 - return { 126 - obj, 127 - typeofItems, 128 - }; 129 - }; 130 - 131 - const addTypeEnum = ({ 132 - $ref, 133 - context, 134 - schema, 135 - }: { 136 - $ref: string; 137 - context: IRContext; 138 - schema: SchemaWithType<'enum'>; 139 - }) => { 140 - const identifier = context.file({ id: typesId })!.identifier({ 141 - $ref, 142 - create: true, 143 - namespace: 'type', 144 - }); 145 - 146 - // TODO: parser - this is the old parser behavior where we would NOT 147 - // print nested enum identifiers if they already exist. This is a 148 - // blocker for referencing these identifiers within the file as 149 - // we cannot guarantee just because they have a duplicate identifier, 150 - // they have a duplicate value. 151 - if ( 152 - !identifier.created && 153 - !isRefOpenApiComponent($ref) && 154 - context.config.plugins['@hey-api/types']?.enums !== 'typescript+namespace' 155 - ) { 156 - return; 157 - } 158 - 159 - const node = compiler.typeAliasDeclaration({ 160 - comment: parseSchemaJsDoc({ schema }), 161 - exportType: true, 162 - name: identifier.name || '', 163 - type: schemaToType({ 164 - context, 165 - schema: { 166 - ...schema, 167 - type: undefined, 168 - }, 169 - }), 170 - }); 171 - return node; 172 - }; 173 - 174 - const addTypeScriptEnum = ({ 175 - $ref, 176 - context, 177 - schema, 178 - }: { 179 - $ref: string; 180 - context: IRContext; 181 - schema: SchemaWithType<'enum'>; 182 - }) => { 183 - const identifier = context.file({ id: typesId })!.identifier({ 184 - $ref, 185 - create: true, 186 - namespace: 'value', 187 - }); 188 - 189 - // TODO: parser - this is the old parser behavior where we would NOT 190 - // print nested enum identifiers if they already exist. This is a 191 - // blocker for referencing these identifiers within the file as 192 - // we cannot guarantee just because they have a duplicate identifier, 193 - // they have a duplicate value. 194 - if ( 195 - !identifier.created && 196 - context.config.plugins['@hey-api/types']?.enums !== 'typescript+namespace' 197 - ) { 198 - return; 199 - } 200 - 201 - const enumObject = schemaToEnumObject({ schema }); 202 - 203 - // TypeScript enums support only string and number values so we need to fallback to types 204 - if ( 205 - enumObject.typeofItems.filter( 206 - (type) => type !== 'number' && type !== 'string', 207 - ).length 208 - ) { 209 - const node = addTypeEnum({ 210 - $ref, 211 - context, 212 - schema, 213 - }); 214 - return node; 215 - } 216 - 217 - const node = compiler.enumDeclaration({ 218 - leadingComment: parseSchemaJsDoc({ schema }), 219 - name: identifier.name || '', 220 - obj: enumObject.obj, 221 - }); 222 - return node; 223 - }; 224 - 225 - const arrayTypeToIdentifier = ({ 226 - context, 227 - namespace, 228 - schema, 229 - }: { 230 - context: IRContext; 231 - namespace: Array<ts.Statement>; 232 - schema: SchemaWithType<'array'>; 233 - }) => { 234 - if (!schema.items) { 235 - return compiler.typeArrayNode( 236 - compiler.keywordTypeNode({ 237 - keyword: 'unknown', 238 - }), 239 - ); 240 - } 241 - 242 - schema = deduplicateSchema({ schema }); 243 - 244 - // at least one item is guaranteed 245 - const itemTypes = schema.items!.map((item) => 246 - schemaToType({ 247 - context, 248 - namespace, 249 - schema: item, 250 - }), 251 - ); 252 - 253 - if (itemTypes.length === 1) { 254 - return compiler.typeArrayNode(itemTypes[0]); 255 - } 256 - 257 - if (schema.logicalOperator === 'and') { 258 - return compiler.typeArrayNode( 259 - compiler.typeIntersectionNode({ types: itemTypes }), 260 - ); 261 - } 262 - 263 - return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes })); 264 - }; 265 - 266 - const booleanTypeToIdentifier = ({ 267 - schema, 268 - }: { 269 - context: IRContext; 270 - namespace: Array<ts.Statement>; 271 - schema: SchemaWithType<'boolean'>; 272 - }) => { 273 - if (schema.const !== undefined) { 274 - return compiler.literalTypeNode({ 275 - literal: compiler.ots.boolean(schema.const as boolean), 276 - }); 277 - } 278 - 279 - return compiler.keywordTypeNode({ 280 - keyword: 'boolean', 281 - }); 282 - }; 283 - 284 - const enumTypeToIdentifier = ({ 285 - $ref, 286 - context, 287 - namespace, 288 - schema, 289 - }: { 290 - $ref?: string; 291 - context: IRContext; 292 - namespace: Array<ts.Statement>; 293 - schema: SchemaWithType<'enum'>; 294 - }): ts.TypeNode => { 295 - // TODO: parser - add option to inline enums 296 - if ($ref) { 297 - const isRefComponent = isRefOpenApiComponent($ref); 298 - 299 - // when enums are disabled (default), emit only reusable components 300 - // as types, otherwise the output would be broken if we skipped all enums 301 - if (!context.config.plugins['@hey-api/types']?.enums && isRefComponent) { 302 - const typeNode = addTypeEnum({ 303 - $ref, 304 - context, 305 - schema, 306 - }); 307 - if (typeNode) { 308 - context.file({ id: typesId })!.add(typeNode); 309 - } 310 - } 311 - 312 - if (context.config.plugins['@hey-api/types']?.enums === 'javascript') { 313 - const typeNode = addTypeEnum({ 314 - $ref, 315 - context, 316 - schema, 317 - }); 318 - if (typeNode) { 319 - context.file({ id: typesId })!.add(typeNode); 320 - } 321 - 322 - const objectNode = addJavaScriptEnum({ 323 - $ref, 324 - context, 325 - schema, 326 - }); 327 - if (objectNode) { 328 - context.file({ id: typesId })!.add(objectNode); 329 - } 330 - } 331 - 332 - if (context.config.plugins['@hey-api/types']?.enums === 'typescript') { 333 - const enumNode = addTypeScriptEnum({ 334 - $ref, 335 - context, 336 - schema, 337 - }); 338 - if (enumNode) { 339 - context.file({ id: typesId })!.add(enumNode); 340 - } 341 - } 342 - 343 - if ( 344 - context.config.plugins['@hey-api/types']?.enums === 'typescript+namespace' 345 - ) { 346 - const enumNode = addTypeScriptEnum({ 347 - $ref, 348 - context, 349 - schema, 350 - }); 351 - if (enumNode) { 352 - if (isRefComponent) { 353 - context.file({ id: typesId })!.add(enumNode); 354 - } else { 355 - // emit enum inside TypeScript namespace 356 - namespace.push(enumNode); 357 - } 358 - } 359 - } 360 - } 361 - 362 - const type = schemaToType({ 363 - context, 364 - schema: { 365 - ...schema, 366 - type: undefined, 367 - }, 368 - }); 369 - return type; 370 - }; 371 - 372 - const numberTypeToIdentifier = ({ 373 - schema, 374 - }: { 375 - context: IRContext; 376 - namespace: Array<ts.Statement>; 377 - schema: SchemaWithType<'number'>; 378 - }) => { 379 - if (schema.const !== undefined) { 380 - return compiler.literalTypeNode({ 381 - literal: compiler.ots.number(schema.const as number), 382 - }); 383 - } 384 - 385 - return compiler.keywordTypeNode({ 386 - keyword: 'number', 387 - }); 388 - }; 389 - 390 - const objectTypeToIdentifier = ({ 391 - context, 392 - namespace, 393 - schema, 394 - }: { 395 - context: IRContext; 396 - namespace: Array<ts.Statement>; 397 - schema: SchemaWithType<'object'>; 398 - }) => { 399 - let indexProperty: Property | undefined; 400 - const schemaProperties: Array<Property> = []; 401 - let indexPropertyItems: Array<IRSchemaObject> = []; 402 - const required = schema.required ?? []; 403 - let hasOptionalProperties = false; 404 - 405 - for (const name in schema.properties) { 406 - const property = schema.properties[name]; 407 - const isRequired = required.includes(name); 408 - schemaProperties.push({ 409 - comment: parseSchemaJsDoc({ schema: property }), 410 - isReadOnly: property.accessScope === 'read', 411 - isRequired, 412 - name, 413 - type: schemaToType({ 414 - $ref: `${irRef}${name}`, 415 - context, 416 - namespace, 417 - schema: property, 418 - }), 419 - }); 420 - indexPropertyItems.push(property); 421 - 422 - if (!isRequired) { 423 - hasOptionalProperties = true; 424 - } 425 - } 426 - 427 - if ( 428 - schema.additionalProperties && 429 - (schema.additionalProperties.type !== 'never' || !indexPropertyItems.length) 430 - ) { 431 - if (schema.additionalProperties.type === 'never') { 432 - indexPropertyItems = [schema.additionalProperties]; 433 - } else { 434 - indexPropertyItems.unshift(schema.additionalProperties); 435 - } 436 - 437 - if (hasOptionalProperties) { 438 - indexPropertyItems.push({ 439 - type: 'undefined', 440 - }); 441 - } 442 - 443 - indexProperty = { 444 - isRequired: true, 445 - name: 'key', 446 - type: schemaToType({ 447 - context, 448 - namespace, 449 - schema: 450 - indexPropertyItems.length === 1 451 - ? indexPropertyItems[0] 452 - : { 453 - items: indexPropertyItems, 454 - logicalOperator: 'or', 455 - }, 456 - }), 457 - }; 458 - } 459 - 460 - return compiler.typeInterfaceNode({ 461 - indexProperty, 462 - properties: schemaProperties, 463 - useLegacyResolution: false, 464 - }); 465 - }; 466 - 467 - const stringTypeToIdentifier = ({ 468 - context, 469 - schema, 470 - }: { 471 - context: IRContext; 472 - namespace: Array<ts.Statement>; 473 - schema: SchemaWithType<'string'>; 474 - }) => { 475 - if (schema.const !== undefined) { 476 - return compiler.literalTypeNode({ 477 - literal: compiler.stringLiteral({ text: schema.const as string }), 478 - }); 479 - } 480 - 481 - if (schema.format) { 482 - if (schema.format === 'binary') { 483 - return compiler.typeUnionNode({ 484 - types: [ 485 - compiler.typeReferenceNode({ 486 - typeName: 'Blob', 487 - }), 488 - compiler.typeReferenceNode({ 489 - typeName: 'File', 490 - }), 491 - ], 492 - }); 493 - } 494 - 495 - if (schema.format === 'date-time' || schema.format === 'date') { 496 - // TODO: parser - add ability to skip type transformers 497 - if (context.config.plugins['@hey-api/transformers']?.dates) { 498 - return compiler.typeReferenceNode({ typeName: 'Date' }); 499 - } 500 - } 501 - } 502 - 503 - return compiler.keywordTypeNode({ 504 - keyword: 'string', 505 - }); 506 - }; 507 - 508 - const tupleTypeToIdentifier = ({ 509 - context, 510 - namespace, 511 - schema, 512 - }: { 513 - context: IRContext; 514 - namespace: Array<ts.Statement>; 515 - schema: SchemaWithType<'tuple'>; 516 - }) => { 517 - const itemTypes: Array<ts.TypeNode> = []; 518 - 519 - for (const item of schema.items ?? []) { 520 - itemTypes.push( 521 - schemaToType({ 522 - context, 523 - namespace, 524 - schema: item, 525 - }), 526 - ); 527 - } 528 - 529 - return compiler.typeTupleNode({ 530 - types: itemTypes, 531 - }); 532 - }; 533 - 534 - const schemaTypeToIdentifier = ({ 535 - $ref, 536 - context, 537 - namespace, 538 - schema, 539 - }: { 540 - $ref?: string; 541 - context: IRContext; 542 - namespace: Array<ts.Statement>; 543 - schema: IRSchemaObject; 544 - }): ts.TypeNode => { 545 - switch (schema.type as Required<IRSchemaObject>['type']) { 546 - case 'array': 547 - return arrayTypeToIdentifier({ 548 - context, 549 - namespace, 550 - schema: schema as SchemaWithType<'array'>, 551 - }); 552 - case 'boolean': 553 - return booleanTypeToIdentifier({ 554 - context, 555 - namespace, 556 - schema: schema as SchemaWithType<'boolean'>, 557 - }); 558 - case 'enum': 559 - return enumTypeToIdentifier({ 560 - $ref, 561 - context, 562 - namespace, 563 - schema: schema as SchemaWithType<'enum'>, 564 - }); 565 - case 'never': 566 - return compiler.keywordTypeNode({ 567 - keyword: 'never', 568 - }); 569 - case 'null': 570 - return compiler.literalTypeNode({ 571 - literal: compiler.null(), 572 - }); 573 - case 'number': 574 - return numberTypeToIdentifier({ 575 - context, 576 - namespace, 577 - schema: schema as SchemaWithType<'number'>, 578 - }); 579 - case 'object': 580 - return objectTypeToIdentifier({ 581 - context, 582 - namespace, 583 - schema: schema as SchemaWithType<'object'>, 584 - }); 585 - case 'string': 586 - return stringTypeToIdentifier({ 587 - context, 588 - namespace, 589 - schema: schema as SchemaWithType<'string'>, 590 - }); 591 - case 'tuple': 592 - return tupleTypeToIdentifier({ 593 - context, 594 - namespace, 595 - schema: schema as SchemaWithType<'tuple'>, 596 - }); 597 - case 'undefined': 598 - return compiler.keywordTypeNode({ 599 - keyword: 'undefined', 600 - }); 601 - case 'unknown': 602 - return compiler.keywordTypeNode({ 603 - keyword: 'unknown', 604 - }); 605 - case 'void': 606 - return compiler.keywordTypeNode({ 607 - keyword: 'void', 608 - }); 609 - } 610 - }; 611 - 612 - const irParametersToIrSchema = ({ 613 - parameters, 614 - }: { 615 - parameters: Record<string, IRParameterObject>; 616 - }): IRSchemaObject => { 617 - const irSchema: IRSchemaObject = { 618 - type: 'object', 619 - }; 620 - 621 - if (parameters) { 622 - const properties: Record<string, IRSchemaObject> = {}; 623 - const required: Array<string> = []; 624 - 625 - for (const name in parameters) { 626 - const parameter = parameters[name]; 627 - 628 - properties[name] = deduplicateSchema({ 629 - schema: parameter.schema, 630 - }); 631 - 632 - if (parameter.required) { 633 - required.push(name); 634 - } 635 - } 636 - 637 - irSchema.properties = properties; 638 - 639 - if (required.length) { 640 - irSchema.required = required; 641 - } 642 - } 643 - 644 - return irSchema; 645 - }; 24 + export const typesId = 'types'; 646 25 647 26 const operationToDataType = ({ 648 27 context, 649 28 operation, 29 + options, 650 30 }: { 651 31 context: IRContext; 652 32 operation: IROperationObject; 33 + options: SchemaToTypeOptions; 653 34 }) => { 654 35 const data: IRSchemaObject = { 655 36 type: 'object', ··· 732 113 exportType: true, 733 114 name: identifier.name || '', 734 115 type: schemaToType({ 735 - context, 116 + options, 736 117 schema: data, 737 118 }), 738 119 }); ··· 743 124 const operationToType = ({ 744 125 context, 745 126 operation, 127 + options, 746 128 }: { 747 129 context: IRContext; 748 130 operation: IROperationObject; 131 + options: SchemaToTypeOptions; 749 132 }) => { 750 133 operationToDataType({ 751 134 context, 752 135 operation, 136 + options, 753 137 }); 754 138 755 139 const { error, response } = operationResponsesMap(operation); ··· 764 148 exportType: true, 765 149 name: identifier.name || '', 766 150 type: schemaToType({ 767 - context, 151 + options, 768 152 schema: error, 769 153 }), 770 154 }); ··· 781 165 exportType: true, 782 166 name: identifier.name || '', 783 167 type: schemaToType({ 784 - context, 168 + options, 785 169 schema: response, 786 170 }), 787 171 }); ··· 789 173 } 790 174 }; 791 175 792 - export const schemaToType = ({ 793 - $ref, 794 - context, 795 - namespace = [], 796 - schema, 797 - }: { 798 - $ref?: string; 799 - context: IRContext; 800 - namespace?: Array<ts.Statement>; 801 - schema: IRSchemaObject; 802 - }): ts.TypeNode => { 803 - let type: ts.TypeNode | undefined; 804 - 805 - if (schema.$ref) { 806 - const identifier = context.file({ id: typesId })!.identifier({ 807 - $ref: schema.$ref, 808 - create: true, 809 - namespace: 'type', 810 - }); 811 - type = compiler.typeReferenceNode({ 812 - typeName: identifier.name || '', 813 - }); 814 - } else if (schema.type) { 815 - type = schemaTypeToIdentifier({ 816 - $ref, 817 - context, 818 - namespace, 819 - schema, 820 - }); 821 - } else if (schema.items) { 822 - schema = deduplicateSchema({ schema }); 823 - if (schema.items) { 824 - const itemTypes = schema.items.map((item) => 825 - schemaToType({ 826 - context, 827 - namespace, 828 - schema: item, 829 - }), 830 - ); 831 - type = 832 - schema.logicalOperator === 'and' 833 - ? compiler.typeIntersectionNode({ types: itemTypes }) 834 - : compiler.typeUnionNode({ types: itemTypes }); 835 - } else { 836 - type = schemaToType({ 837 - context, 838 - namespace, 839 - schema, 840 - }); 841 - } 842 - } else { 843 - // catch-all fallback for failed schemas 844 - type = schemaTypeToIdentifier({ 845 - context, 846 - namespace, 847 - schema: { 848 - type: 'unknown', 849 - }, 850 - }); 851 - } 852 - 853 - // emit nodes only if $ref points to a reusable component 854 - if ($ref && isRefOpenApiComponent($ref)) { 855 - // emit namespace if it has any members 856 - if (namespace.length) { 857 - const identifier = context.file({ id: typesId })!.identifier({ 858 - $ref, 859 - create: true, 860 - namespace: 'value', 861 - }); 862 - const node = compiler.namespaceDeclaration({ 863 - name: identifier.name || '', 864 - statements: namespace, 865 - }); 866 - context.file({ id: typesId })!.add(node); 867 - } 868 - 869 - // enum handler emits its own artifacts 870 - if (schema.type !== 'enum') { 871 - const identifier = context.file({ id: typesId })!.identifier({ 872 - $ref, 873 - create: true, 874 - namespace: 'type', 875 - }); 876 - const node = compiler.typeAliasDeclaration({ 877 - comment: parseSchemaJsDoc({ schema }), 878 - exportType: true, 879 - name: identifier.name || '', 880 - type, 881 - }); 882 - context.file({ id: typesId })!.add(node); 883 - } 884 - } 885 - 886 - return type; 887 - }; 888 - 889 176 export const handler: PluginHandler<Config> = ({ context }) => { 890 - context.createFile({ 177 + const file = context.createFile({ 891 178 id: typesId, 892 179 path: 'types', 893 180 }); 181 + const options: SchemaToTypeOptions = { 182 + enums: context.config.plugins['@hey-api/types']?.enums, 183 + file, 184 + useTransformersDate: context.config.plugins['@hey-api/transformers']?.dates, 185 + }; 894 186 895 - if (context.ir.components) { 896 - for (const name in context.ir.components.schemas) { 897 - const schema = context.ir.components.schemas[name]; 898 - const $ref = `#/components/schemas/${name}`; 899 - 900 - try { 901 - schemaToType({ 902 - $ref, 903 - context, 904 - schema, 905 - }); 906 - } catch (error) { 907 - console.error( 908 - `🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(schema, null, 2)}`, 909 - ); 910 - throw error; 911 - } 912 - } 913 - 914 - for (const name in context.ir.components.parameters) { 915 - const parameter = context.ir.components.parameters[name]; 916 - const $ref = `#/components/parameters/${name}`; 917 - 918 - try { 919 - schemaToType({ 920 - $ref, 921 - context, 922 - schema: parameter.schema, 923 - }); 924 - } catch (error) { 925 - console.error( 926 - `🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(parameter.schema, null, 2)}`, 927 - ); 928 - throw error; 929 - } 930 - } 931 - } 187 + componentsToType({ context, options }); 932 188 933 189 // TODO: parser - once types are a plugin, this logic can be simplified 934 190 // provide config option on types to generate path types and services ··· 947 203 operationToType({ 948 204 context, 949 205 operation, 206 + options, 950 207 }); 951 208 } 952 209 }
+8 -2
packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts
··· 33 33 operationOptionsType, 34 34 serviceFunctionIdentifier, 35 35 } from '../../@hey-api/services/plugin-legacy'; 36 - import { schemaToType } from '../../@hey-api/types/plugin'; 36 + import { typesId } from '../../@hey-api/types/plugin'; 37 37 import type { PluginHandler } from '../../types'; 38 38 import type { Config as AngularQueryConfig } from '../angular-query-experimental'; 39 39 import type { Config as ReactQueryConfig } from '../react-query'; ··· 890 890 891 891 const typeQueryKey = `${queryKeyName}<${typeData}>`; 892 892 const typePageObjectParam = `Pick<${typeQueryKey}[0], 'body' | 'headers' | 'path' | 'query'>`; 893 + const options: SchemaToTypeOptions = { 894 + enums: context.config.plugins['@hey-api/types']?.enums, 895 + file: context.file({ id: typesId })!, 896 + useTransformersDate: 897 + context.config.plugins['@hey-api/transformers']?.dates, 898 + }; 893 899 // TODO: parser - this is a bit clunky, need to compile type to string because 894 900 // `compiler.returnFunctionCall()` accepts only strings, should be cleaned up 895 901 const typePageParam = `${tsNodeToString({ 896 902 node: schemaToType({ 897 - context, 903 + options, 898 904 schema: pagination.schema, 899 905 }), 900 906 unescape: true,
+694
packages/openapi-ts/src/plugins/utils/types.ts
··· 1 + import type ts from 'typescript'; 2 + 3 + import type { Property } from '../../compiler'; 4 + import { compiler } from '../../compiler'; 5 + import type { TypeScriptFile } from '../../generate/files'; 6 + import type { IRContext } from '../../ir/context'; 7 + import type { IRSchemaObject } from '../../ir/ir'; 8 + import { deduplicateSchema } from '../../ir/schema'; 9 + import { ensureValidTypeScriptJavaScriptIdentifier } from '../../openApi'; 10 + import { escapeComment } from '../../utils/escape'; 11 + import { irRef, isRefOpenApiComponent } from '../../utils/ref'; 12 + 13 + export type SchemaToTypeOptions = { 14 + enums?: 'javascript' | 'typescript' | 'typescript+namespace' | false; 15 + file: TypeScriptFile; 16 + useTransformersDate?: boolean; 17 + }; 18 + 19 + interface SchemaWithType<T extends Required<IRSchemaObject>['type']> 20 + extends Omit<IRSchemaObject, 'type'> { 21 + type: Extract<Required<IRSchemaObject>['type'], T>; 22 + } 23 + 24 + const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => { 25 + const comments = [ 26 + schema.description && escapeComment(schema.description), 27 + schema.deprecated && '@deprecated', 28 + ]; 29 + return comments; 30 + }; 31 + 32 + const addJavaScriptEnum = ({ 33 + $ref, 34 + schema, 35 + options, 36 + }: { 37 + $ref: string; 38 + options: SchemaToTypeOptions; 39 + schema: SchemaWithType<'enum'>; 40 + }) => { 41 + const identifier = options.file.identifier({ 42 + $ref, 43 + create: true, 44 + namespace: 'value', 45 + }); 46 + 47 + // TODO: parser - this is the old parser behavior where we would NOT 48 + // print nested enum identifiers if they already exist. This is a 49 + // blocker for referencing these identifiers within the file as 50 + // we cannot guarantee just because they have a duplicate identifier, 51 + // they have a duplicate value. 52 + if (!identifier.created) { 53 + return; 54 + } 55 + 56 + const enumObject = schemaToEnumObject({ schema }); 57 + 58 + const expression = compiler.objectExpression({ 59 + multiLine: true, 60 + obj: enumObject.obj, 61 + }); 62 + const node = compiler.constVariable({ 63 + assertion: 'const', 64 + comment: parseSchemaJsDoc({ schema }), 65 + exportConst: true, 66 + expression, 67 + name: identifier.name || '', 68 + }); 69 + return node; 70 + }; 71 + 72 + const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => { 73 + const typeofItems: Array< 74 + | 'string' 75 + | 'number' 76 + | 'bigint' 77 + | 'boolean' 78 + | 'symbol' 79 + | 'undefined' 80 + | 'object' 81 + | 'function' 82 + > = []; 83 + 84 + const obj = (schema.items ?? []).map((item) => { 85 + const typeOfItemConst = typeof item.const; 86 + 87 + if (!typeofItems.includes(typeOfItemConst)) { 88 + typeofItems.push(typeOfItemConst); 89 + } 90 + 91 + let key; 92 + if (item.title) { 93 + key = item.title; 94 + } else if (typeOfItemConst === 'number') { 95 + key = `_${item.const}`; 96 + } else if (typeOfItemConst === 'boolean') { 97 + const valid = typeOfItemConst ? 'true' : 'false'; 98 + key = valid.toLocaleUpperCase(); 99 + } else { 100 + let valid = ensureValidTypeScriptJavaScriptIdentifier( 101 + item.const as string, 102 + ); 103 + if (!valid) { 104 + // TODO: parser - abstract empty string handling 105 + valid = 'empty_string'; 106 + } 107 + key = valid.toLocaleUpperCase(); 108 + } 109 + return { 110 + comments: parseSchemaJsDoc({ schema: item }), 111 + key, 112 + value: item.const, 113 + }; 114 + }); 115 + 116 + return { 117 + obj, 118 + typeofItems, 119 + }; 120 + }; 121 + 122 + const addTypeEnum = ({ 123 + $ref, 124 + schema, 125 + options, 126 + }: { 127 + $ref: string; 128 + options: SchemaToTypeOptions; 129 + schema: SchemaWithType<'enum'>; 130 + }) => { 131 + const identifier = options.file.identifier({ 132 + $ref, 133 + create: true, 134 + namespace: 'type', 135 + }); 136 + 137 + // TODO: parser - this is the old parser behavior where we would NOT 138 + // print nested enum identifiers if they already exist. This is a 139 + // blocker for referencing these identifiers within the file as 140 + // we cannot guarantee just because they have a duplicate identifier, 141 + // they have a duplicate value. 142 + if (!identifier.created && options.enums !== 'typescript+namespace') { 143 + return; 144 + } 145 + 146 + const node = compiler.typeAliasDeclaration({ 147 + comment: parseSchemaJsDoc({ schema }), 148 + exportType: true, 149 + name: identifier.name || '', 150 + type: schemaToType({ 151 + options, 152 + schema: { 153 + ...schema, 154 + type: undefined, 155 + }, 156 + }), 157 + }); 158 + return node; 159 + }; 160 + 161 + const addTypeScriptEnum = ({ 162 + $ref, 163 + schema, 164 + options, 165 + }: { 166 + $ref: string; 167 + options: SchemaToTypeOptions; 168 + schema: SchemaWithType<'enum'>; 169 + }) => { 170 + const identifier = options.file.identifier({ 171 + $ref, 172 + create: true, 173 + namespace: 'value', 174 + }); 175 + 176 + // TODO: parser - this is the old parser behavior where we would NOT 177 + // print nested enum identifiers if they already exist. This is a 178 + // blocker for referencing these identifiers within the file as 179 + // we cannot guarantee just because they have a duplicate identifier, 180 + // they have a duplicate value. 181 + if (!identifier.created && options.enums !== 'typescript+namespace') { 182 + return; 183 + } 184 + 185 + const enumObject = schemaToEnumObject({ schema }); 186 + 187 + // TypeScript enums support only string and number values so we need to fallback to types 188 + if ( 189 + enumObject.typeofItems.filter( 190 + (type) => type !== 'number' && type !== 'string', 191 + ).length 192 + ) { 193 + const node = addTypeEnum({ 194 + $ref, 195 + options, 196 + schema, 197 + }); 198 + return node; 199 + } 200 + 201 + const node = compiler.enumDeclaration({ 202 + leadingComment: parseSchemaJsDoc({ schema }), 203 + name: identifier.name || '', 204 + obj: enumObject.obj, 205 + }); 206 + return node; 207 + }; 208 + 209 + const arrayTypeToIdentifier = ({ 210 + namespace, 211 + schema, 212 + options, 213 + }: { 214 + namespace: Array<ts.Statement>; 215 + options: SchemaToTypeOptions; 216 + schema: SchemaWithType<'array'>; 217 + }) => { 218 + if (!schema.items) { 219 + return compiler.typeArrayNode( 220 + compiler.keywordTypeNode({ 221 + keyword: 'unknown', 222 + }), 223 + ); 224 + } 225 + 226 + schema = deduplicateSchema({ schema }); 227 + 228 + // at least one item is guaranteed 229 + const itemTypes = schema.items!.map((item) => 230 + schemaToType({ 231 + namespace, 232 + options, 233 + schema: item, 234 + }), 235 + ); 236 + 237 + if (itemTypes.length === 1) { 238 + return compiler.typeArrayNode(itemTypes[0]); 239 + } 240 + 241 + if (schema.logicalOperator === 'and') { 242 + return compiler.typeArrayNode( 243 + compiler.typeIntersectionNode({ types: itemTypes }), 244 + ); 245 + } 246 + 247 + return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes })); 248 + }; 249 + 250 + const booleanTypeToIdentifier = ({ 251 + schema, 252 + }: { 253 + schema: SchemaWithType<'boolean'>; 254 + }) => { 255 + if (schema.const !== undefined) { 256 + return compiler.literalTypeNode({ 257 + literal: compiler.ots.boolean(schema.const as boolean), 258 + }); 259 + } 260 + 261 + return compiler.keywordTypeNode({ 262 + keyword: 'boolean', 263 + }); 264 + }; 265 + 266 + const enumTypeToIdentifier = ({ 267 + $ref, 268 + namespace, 269 + schema, 270 + options, 271 + }: { 272 + $ref?: string; 273 + namespace: Array<ts.Statement>; 274 + options: SchemaToTypeOptions; 275 + schema: SchemaWithType<'enum'>; 276 + }): ts.TypeNode => { 277 + // TODO: parser - add option to inline enums 278 + if ($ref) { 279 + const isRefComponent = isRefOpenApiComponent($ref); 280 + 281 + // when enums are disabled (default), emit only reusable components 282 + // as types, otherwise the output would be broken if we skipped all enums 283 + if (!options.enums && isRefComponent) { 284 + const typeNode = addTypeEnum({ 285 + $ref, 286 + options, 287 + schema, 288 + }); 289 + if (typeNode) { 290 + options.file.add(typeNode); 291 + } 292 + } 293 + 294 + if (options.enums === 'javascript') { 295 + const typeNode = addTypeEnum({ 296 + $ref, 297 + options, 298 + schema, 299 + }); 300 + if (typeNode) { 301 + options.file.add(typeNode); 302 + } 303 + 304 + const objectNode = addJavaScriptEnum({ 305 + $ref, 306 + options, 307 + schema, 308 + }); 309 + if (objectNode) { 310 + options.file.add(objectNode); 311 + } 312 + } 313 + 314 + if (options.enums === 'typescript') { 315 + const enumNode = addTypeScriptEnum({ 316 + $ref, 317 + options, 318 + schema, 319 + }); 320 + if (enumNode) { 321 + options.file.add(enumNode); 322 + } 323 + } 324 + 325 + if (options.enums === 'typescript+namespace') { 326 + const enumNode = addTypeScriptEnum({ 327 + $ref, 328 + options, 329 + schema, 330 + }); 331 + if (enumNode) { 332 + if (isRefComponent) { 333 + options.file.add(enumNode); 334 + } else { 335 + // emit enum inside TypeScript namespace 336 + namespace.push(enumNode); 337 + } 338 + } 339 + } 340 + } 341 + 342 + const type = schemaToType({ 343 + options, 344 + schema: { 345 + ...schema, 346 + type: undefined, 347 + }, 348 + }); 349 + return type; 350 + }; 351 + 352 + const numberTypeToIdentifier = ({ 353 + schema, 354 + }: { 355 + schema: SchemaWithType<'number'>; 356 + }) => { 357 + if (schema.const !== undefined) { 358 + return compiler.literalTypeNode({ 359 + literal: compiler.ots.number(schema.const as number), 360 + }); 361 + } 362 + 363 + return compiler.keywordTypeNode({ 364 + keyword: 'number', 365 + }); 366 + }; 367 + 368 + const objectTypeToIdentifier = ({ 369 + namespace, 370 + schema, 371 + options, 372 + }: { 373 + namespace: Array<ts.Statement>; 374 + options: SchemaToTypeOptions; 375 + schema: SchemaWithType<'object'>; 376 + }) => { 377 + let indexProperty: Property | undefined; 378 + const schemaProperties: Array<Property> = []; 379 + const indexPropertyItems: Array<IRSchemaObject> = []; 380 + const required = schema.required ?? []; 381 + let hasOptionalProperties = false; 382 + 383 + for (const name in schema.properties) { 384 + const property = schema.properties[name]; 385 + const isRequired = required.includes(name); 386 + schemaProperties.push({ 387 + comment: parseSchemaJsDoc({ schema: property }), 388 + isReadOnly: property.accessScope === 'read', 389 + isRequired, 390 + name, 391 + type: schemaToType({ 392 + $ref: `${irRef}${name}`, 393 + namespace, 394 + options, 395 + schema: property, 396 + }), 397 + }); 398 + indexPropertyItems.push(property); 399 + 400 + if (!isRequired) { 401 + hasOptionalProperties = true; 402 + } 403 + } 404 + 405 + if (schema.additionalProperties) { 406 + indexPropertyItems.unshift(schema.additionalProperties); 407 + 408 + if (hasOptionalProperties) { 409 + indexPropertyItems.push({ 410 + type: 'void', 411 + }); 412 + } 413 + 414 + indexProperty = { 415 + isRequired: true, 416 + name: 'key', 417 + type: schemaToType({ 418 + namespace, 419 + options, 420 + schema: 421 + indexPropertyItems.length === 1 422 + ? indexPropertyItems[0] 423 + : { 424 + items: indexPropertyItems, 425 + logicalOperator: 'or', 426 + }, 427 + }), 428 + }; 429 + } 430 + 431 + return compiler.typeInterfaceNode({ 432 + indexProperty, 433 + properties: schemaProperties, 434 + useLegacyResolution: false, 435 + }); 436 + }; 437 + 438 + const stringTypeToIdentifier = ({ 439 + schema, 440 + options, 441 + }: { 442 + options: SchemaToTypeOptions; 443 + schema: SchemaWithType<'string'>; 444 + }) => { 445 + if (schema.const !== undefined) { 446 + return compiler.literalTypeNode({ 447 + literal: compiler.stringLiteral({ text: schema.const as string }), 448 + }); 449 + } 450 + 451 + if (schema.format) { 452 + if (schema.format === 'binary') { 453 + return compiler.typeUnionNode({ 454 + types: [ 455 + compiler.typeReferenceNode({ 456 + typeName: 'Blob', 457 + }), 458 + compiler.typeReferenceNode({ 459 + typeName: 'File', 460 + }), 461 + ], 462 + }); 463 + } 464 + 465 + if (schema.format === 'date-time' || schema.format === 'date') { 466 + // TODO: parser - add ability to skip type transformers 467 + if (options.useTransformersDate) { 468 + return compiler.typeReferenceNode({ typeName: 'Date' }); 469 + } 470 + } 471 + } 472 + 473 + return compiler.keywordTypeNode({ 474 + keyword: 'string', 475 + }); 476 + }; 477 + 478 + const tupleTypeToIdentifier = ({ 479 + namespace, 480 + schema, 481 + options, 482 + }: { 483 + namespace: Array<ts.Statement>; 484 + options: SchemaToTypeOptions; 485 + schema: SchemaWithType<'tuple'>; 486 + }) => { 487 + const itemTypes: Array<ts.TypeNode> = []; 488 + 489 + for (const item of schema.items ?? []) { 490 + itemTypes.push( 491 + schemaToType({ 492 + namespace, 493 + options, 494 + schema: item, 495 + }), 496 + ); 497 + } 498 + 499 + return compiler.typeTupleNode({ 500 + types: itemTypes, 501 + }); 502 + }; 503 + 504 + const schemaTypeToIdentifier = ({ 505 + $ref, 506 + namespace, 507 + schema, 508 + options, 509 + }: { 510 + $ref?: string; 511 + namespace: Array<ts.Statement>; 512 + options: SchemaToTypeOptions; 513 + schema: IRSchemaObject; 514 + }): ts.TypeNode => { 515 + switch (schema.type as Required<IRSchemaObject>['type']) { 516 + case 'array': 517 + return arrayTypeToIdentifier({ 518 + namespace, 519 + options, 520 + schema: schema as SchemaWithType<'array'>, 521 + }); 522 + case 'boolean': 523 + return booleanTypeToIdentifier({ 524 + schema: schema as SchemaWithType<'boolean'>, 525 + }); 526 + case 'enum': 527 + return enumTypeToIdentifier({ 528 + $ref, 529 + namespace, 530 + options, 531 + schema: schema as SchemaWithType<'enum'>, 532 + }); 533 + case 'null': 534 + return compiler.literalTypeNode({ 535 + literal: compiler.null(), 536 + }); 537 + case 'number': 538 + return numberTypeToIdentifier({ 539 + schema: schema as SchemaWithType<'number'>, 540 + }); 541 + case 'object': 542 + return objectTypeToIdentifier({ 543 + namespace, 544 + options, 545 + schema: schema as SchemaWithType<'object'>, 546 + }); 547 + case 'string': 548 + return stringTypeToIdentifier({ 549 + options, 550 + schema: schema as SchemaWithType<'string'>, 551 + }); 552 + case 'tuple': 553 + return tupleTypeToIdentifier({ 554 + namespace, 555 + options, 556 + schema: schema as SchemaWithType<'tuple'>, 557 + }); 558 + case 'unknown': 559 + return compiler.keywordTypeNode({ 560 + keyword: 'unknown', 561 + }); 562 + case 'void': 563 + return compiler.keywordTypeNode({ 564 + keyword: 'undefined', 565 + }); 566 + } 567 + }; 568 + 569 + export const schemaToType = ({ 570 + $ref, 571 + namespace = [], 572 + schema, 573 + options, 574 + }: { 575 + $ref?: string; 576 + namespace?: Array<ts.Statement>; 577 + options: SchemaToTypeOptions; 578 + schema: IRSchemaObject; 579 + }): ts.TypeNode => { 580 + let type: ts.TypeNode | undefined; 581 + 582 + if (schema.$ref) { 583 + const identifier = options.file.identifier({ 584 + $ref: schema.$ref, 585 + create: true, 586 + namespace: 'type', 587 + }); 588 + type = compiler.typeReferenceNode({ 589 + typeName: identifier.name || '', 590 + }); 591 + } else if (schema.type) { 592 + type = schemaTypeToIdentifier({ 593 + $ref, 594 + namespace, 595 + options, 596 + schema, 597 + }); 598 + } else if (schema.items) { 599 + schema = deduplicateSchema({ schema }); 600 + if (schema.items) { 601 + const itemTypes = schema.items.map((item) => 602 + schemaToType({ 603 + namespace, 604 + options, 605 + schema: item, 606 + }), 607 + ); 608 + type = 609 + schema.logicalOperator === 'and' 610 + ? compiler.typeIntersectionNode({ types: itemTypes }) 611 + : compiler.typeUnionNode({ types: itemTypes }); 612 + } else { 613 + type = schemaToType({ 614 + namespace, 615 + options, 616 + schema, 617 + }); 618 + } 619 + } else { 620 + // catch-all fallback for failed schemas 621 + type = schemaTypeToIdentifier({ 622 + namespace, 623 + options, 624 + schema: { 625 + type: 'unknown', 626 + }, 627 + }); 628 + } 629 + 630 + // emit nodes only if $ref points to a reusable component 631 + if ($ref && isRefOpenApiComponent($ref)) { 632 + // emit namespace if it has any members 633 + if (namespace.length) { 634 + const identifier = options.file.identifier({ 635 + $ref, 636 + create: true, 637 + namespace: 'value', 638 + }); 639 + const node = compiler.namespaceDeclaration({ 640 + name: identifier.name || '', 641 + statements: namespace, 642 + }); 643 + options.file.add(node); 644 + } 645 + 646 + // enum handler emits its own artifacts 647 + if (schema.type !== 'enum') { 648 + const identifier = options.file.identifier({ 649 + $ref, 650 + create: true, 651 + namespace: 'type', 652 + }); 653 + const node = compiler.typeAliasDeclaration({ 654 + comment: parseSchemaJsDoc({ schema }), 655 + exportType: true, 656 + name: identifier.name || '', 657 + type, 658 + }); 659 + options.file.add(node); 660 + } 661 + } 662 + 663 + return type; 664 + }; 665 + 666 + export const componentsToType = ({ 667 + context, 668 + options, 669 + }: { 670 + context: IRContext; 671 + options: SchemaToTypeOptions; 672 + }) => { 673 + if (context.ir.components) { 674 + for (const name in context.ir.components.schemas) { 675 + const schema = context.ir.components.schemas[name]; 676 + 677 + schemaToType({ 678 + $ref: `#/components/schemas/${name}`, 679 + options, 680 + schema, 681 + }); 682 + } 683 + 684 + for (const name in context.ir.components.parameters) { 685 + const parameter = context.ir.components.parameters[name]; 686 + 687 + schemaToType({ 688 + $ref: `#/components/parameters/${name}`, 689 + options, 690 + schema: parameter.schema, 691 + }); 692 + } 693 + } 694 + };