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

chore: revert context argument

Lubos 4a939d16 cebf3270

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