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: typescript plugin

Lubos 476546c9 2e138020

+1029 -845
+4 -4
dev/typescript/presets.ts
··· 10 10 zod({ metadata: true }), 11 11 tanstackReactQuery({ queryKeys: { tags: true } }), 12 12 ], 13 - minimal: () => [ 14 - /** Just types, nothing else */ 15 - typescript(), 16 - ], 17 13 sdk: () => [ 18 14 /** SDK with types */ 19 15 typescript(), ··· 30 26 typescript(), 31 27 sdk(), 32 28 tanstackReactQuery({ queryKeys: { tags: true } }), 29 + ], 30 + types: () => [ 31 + /** Just types, nothing else */ 32 + typescript(), 33 33 ], 34 34 validated: () => [ 35 35 /** SDK + Zod validation */
-2
packages/openapi-python/src/plugins/pydantic/shared/meta.ts
··· 17 17 /** 18 18 * Composes metadata from child results. 19 19 * 20 - * Automatically propagates hasForwardReference, nullable, readonly from children. 21 - * 22 20 * @param children - Results from walking child schemas 23 21 * @param overrides - Explicit overrides (e.g., from parent schema) 24 22 */
+7 -7
packages/openapi-python/src/plugins/pydantic/shared/processor.ts
··· 1 - import type { 2 - IR, 3 - NamingConfig, 4 - SchemaProcessorContext, 5 - SchemaProcessorResult, 6 - } from '@hey-api/shared'; 1 + import type { IR, NamingConfig, SchemaProcessorContext } from '@hey-api/shared'; 7 2 8 3 import type { PydanticPlugin } from '../types'; 4 + import type { PydanticFinal } from './types'; 9 5 10 6 export type ProcessorContext = SchemaProcessorContext & { 7 + /** Whether to export the result (default: true) */ 8 + export?: boolean; 11 9 naming: NamingConfig; 12 10 /** The plugin instance. */ 13 11 plugin: PydanticPlugin['Instance']; 14 12 schema: IR.SchemaObject; 15 13 }; 16 14 17 - export type ProcessorResult = SchemaProcessorResult<ProcessorContext>; 15 + export type ProcessorResult = { 16 + process: (ctx: ProcessorContext) => PydanticFinal | void; 17 + };
+10 -3
packages/openapi-python/src/plugins/pydantic/v2/processor.ts
··· 42 42 return ctx.schema; 43 43 } 44 44 45 - function process(ctx: ProcessorContext): void { 45 + function process(ctx: ProcessorContext): PydanticFinal | void { 46 46 if (!processor.markEmitted(ctx.path)) return; 47 47 48 - processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 48 + const shouldExport = ctx.export !== false; 49 + 50 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 49 51 const visitor = createVisitor({ schemaExtractor: extractor }); 50 52 const walk = createSchemaWalker(visitor); 51 53 ··· 59 61 plugin, 60 62 }) as PydanticFinal; 61 63 62 - exportAst({ ...ctx, final, plugin }); 64 + if (shouldExport) { 65 + exportAst({ ...ctx, final, plugin }); 66 + return; 67 + } 68 + 69 + return final; 63 70 }); 64 71 } 65 72
+3 -10
packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { refs } from '@hey-api/codegen-core'; 3 2 import type { IR } from '@hey-api/shared'; 4 3 import { statusCodeToGroup } from '@hey-api/shared'; 5 4 ··· 108 107 isParametersRequired = true; 109 108 } 110 109 flatParams.prop(parameter.name, (p) => 111 - p.required(parameter.isRequired).type( 112 - pluginTypeScript.api.schemaToType({ 113 - plugin: pluginTypeScript, 114 - schema: parameter.schema, 115 - state: refs({ 116 - path: [], 117 - }), 118 - }), 119 - ), 110 + p 111 + .required(parameter.isRequired) 112 + .type(pluginTypeScript.api.schemaToType(pluginTypeScript, parameter.schema)), 120 113 ); 121 114 } 122 115
+31 -5
packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts
··· 1 - import type { MaybeTsDsl, TypeTsDsl } from '../../../ts-dsl'; 2 - import { irSchemaToAstV1 } from './v1/api'; 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import { $ } from '../../../ts-dsl'; 4 + import type { TypeScriptResult } from './shared/types'; 5 + import type { HeyApiTypeScriptPlugin } from './types'; 6 + import { createProcessor } from './v1/processor'; 3 7 4 8 export type IApi = { 5 - schemaToType: (args: Parameters<typeof irSchemaToAstV1>[0]) => MaybeTsDsl<TypeTsDsl>; 9 + schemaToType: ( 10 + plugin: HeyApiTypeScriptPlugin['Instance'], 11 + schema: IR.SchemaObject, 12 + ) => TypeScriptResult['type']; 6 13 }; 7 14 8 15 export class Api implements IApi { 9 - schemaToType(args: Parameters<typeof irSchemaToAstV1>[0]): MaybeTsDsl<TypeTsDsl> { 10 - return irSchemaToAstV1(args); 16 + schemaToType( 17 + plugin: HeyApiTypeScriptPlugin['Instance'], 18 + schema: IR.SchemaObject, 19 + ): TypeScriptResult['type'] { 20 + const processor = createProcessor(plugin); 21 + const result = processor.process({ 22 + export: false, 23 + meta: { 24 + resource: 'definition', 25 + resourceId: '', 26 + }, 27 + naming: plugin.config.definitions, 28 + path: [], 29 + plugin, 30 + schema, 31 + }); 32 + 33 + if (!result) { 34 + return $.type(plugin.config.topType); 35 + } 36 + return result.type; 11 37 } 12 38 }
+181 -167
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/export.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming, toCase } from '@hey-api/shared'; 4 - import { pathToJsonPointer, refToName } from '@hey-api/shared'; 2 + import { applyNaming, pathToName, toCase } from '@hey-api/shared'; 3 + import { pathToJsonPointer } from '@hey-api/shared'; 5 4 6 5 import { createSchemaComment } from '../../../../plugins/shared/utils/schema'; 7 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../ts-dsl'; 8 6 import { $, regexp } from '../../../../ts-dsl'; 9 7 import type { HeyApiTypeScriptPlugin } from '../types'; 10 - import type { IrSchemaToAstOptions } from './types'; 8 + import type { ProcessorContext } from './processor'; 9 + import type { TypeScriptFinal } from './types'; 11 10 12 - const schemaToEnumObject = ({ 11 + function resolveEnumKey({ 12 + baseName, 13 + duplicateAttempt, 13 14 plugin, 14 - schema, 15 15 }: { 16 + baseName: string; 17 + duplicateAttempt: number; 16 18 plugin: HeyApiTypeScriptPlugin['Instance']; 17 - schema: IR.SchemaObject; 18 - }) => { 19 - const keyCounts: Record<string, number> = {}; 20 - const typeofItems: Array< 21 - 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined' 22 - > = []; 19 + }): string { 20 + let key = toCase(baseName, plugin.config.enums.case, { 21 + stripLeadingSeparators: false, 22 + }); 23 23 24 - const obj = (schema.items ?? []).map((item, index) => { 25 - const typeOfItemConst = typeof item.const; 24 + regexp.number.lastIndex = 0; 25 + if ( 26 + regexp.number.test(key) && 27 + plugin.config.enums.enabled && 28 + (plugin.config.enums.mode === 'typescript' || plugin.config.enums.mode === 'typescript-const') 29 + ) { 30 + key = `_${key}`; 31 + } 26 32 27 - if (!typeofItems.includes(typeOfItemConst)) { 28 - // track types of enum values because some modes support 29 - // only enums with string and number types 30 - typeofItems.push(typeOfItemConst); 31 - } 32 - 33 - let key: string | undefined; 34 - if (item.title) { 35 - key = item.title; 36 - } else if (typeOfItemConst === 'number' || typeOfItemConst === 'string') { 37 - key = `${item.const}`; 38 - } else if (typeOfItemConst === 'boolean') { 39 - key = item.const ? 'true' : 'false'; 40 - } else if (item.const === null) { 41 - key = 'null'; 33 + if (duplicateAttempt > 0) { 34 + const nameConflictResolver = plugin.context.config.output?.nameConflictResolver; 35 + if (nameConflictResolver) { 36 + const resolvedName = nameConflictResolver({ 37 + attempt: duplicateAttempt, 38 + baseName: key, 39 + }); 40 + if (resolvedName !== null) { 41 + key = resolvedName; 42 + } else { 43 + key = `${key}${duplicateAttempt + 1}`; 44 + } 42 45 } else { 43 - key = `${index}`; 46 + key = `${key}${duplicateAttempt + 1}`; 44 47 } 48 + } 45 49 46 - if (key) { 47 - key = toCase(key, plugin.config.enums.case, { 48 - stripLeadingSeparators: false, 49 - }); 50 + return key; 51 + } 50 52 51 - regexp.number.lastIndex = 0; 52 - // TypeScript enum keys cannot be numbers 53 - if ( 54 - regexp.number.test(key) && 55 - plugin.config.enums.enabled && 56 - (plugin.config.enums.mode === 'typescript' || 57 - plugin.config.enums.mode === 'typescript-const') 58 - ) { 59 - key = `_${key}`; 60 - } 53 + function buildEnumExport({ 54 + enumData, 55 + name, 56 + plugin, 57 + resourceId, 58 + schema, 59 + }: { 60 + enumData: TypeScriptFinal['enumData']; 61 + name: string; 62 + plugin: HeyApiTypeScriptPlugin['Instance']; 63 + resourceId: string; 64 + schema: IR.SchemaObject; 65 + }): boolean { 66 + if (!enumData || enumData.mode === 'type') return false; 61 67 62 - const keyCount = (keyCounts[key] ?? 0) + 1; 63 - keyCounts[key] = keyCount; 68 + const mode = enumData.mode; 69 + const items = enumData.items; 70 + const duplicateCounts: Record<string, number> = {}; 64 71 65 - // avoid collision 66 - if (keyCount > 1) { 67 - const nameConflictResolver = plugin.context.config.output?.nameConflictResolver; 68 - if (nameConflictResolver) { 69 - const resolvedName = nameConflictResolver({ 70 - attempt: keyCount - 1, // 0-based index 71 - baseName: key, 72 - }); 73 - if (resolvedName !== null) { 74 - key = resolvedName; 75 - } else { 76 - key = `${key}${keyCount}`; 77 - } 78 - } else { 79 - key = `${key}${keyCount}`; 80 - } 81 - } 82 - } 72 + const itemsWithAttempts = items.map((item, index) => { 73 + const candidateKey = toCase(item.key, plugin.config.enums.case, { 74 + stripLeadingSeparators: false, 75 + }); 76 + 77 + regexp.number.lastIndex = 0; 78 + const baseKey = 79 + regexp.number.test(candidateKey) && 80 + plugin.config.enums.enabled && 81 + (plugin.config.enums.mode === 'typescript' || plugin.config.enums.mode === 'typescript-const') 82 + ? `_${candidateKey}` 83 + : candidateKey; 84 + 85 + const duplicateAttempt = duplicateCounts[baseKey] ?? 0; 86 + duplicateCounts[baseKey] = duplicateAttempt + 1; 87 + 83 88 return { 84 - key, 85 - schema: item, 89 + duplicateAttempt, 90 + index, 91 + item, 86 92 }; 87 93 }); 88 94 89 - return { 90 - obj, 91 - typeofItems, 92 - }; 93 - }; 95 + if (mode === 'javascript') { 96 + const filteredItems = 97 + plugin.config.enums.constantsIgnoreNull && items.some((item) => item.schema.const === null) 98 + ? items.filter((item) => item.schema.const !== null) 99 + : items; 94 100 95 - export const exportType = ({ 96 - plugin, 97 - schema, 98 - state, 99 - type, 100 - }: IrSchemaToAstOptions & { 101 - schema: IR.SchemaObject; 102 - type: MaybeTsDsl<TypeTsDsl>; 103 - }) => { 104 - const $ref = pathToJsonPointer(fromRef(state.path)); 105 - 106 - // root enums have an additional export 107 - if (schema.type === 'enum' && plugin.config.enums.enabled) { 108 - const enumObject = schemaToEnumObject({ plugin, schema }); 109 - 110 - if (plugin.config.enums.mode === 'javascript') { 111 - // JavaScript enums might want to ignore null values 112 - if (plugin.config.enums.constantsIgnoreNull && enumObject.typeofItems.includes('object')) { 113 - enumObject.obj = enumObject.obj.filter((item) => item.schema.const !== null); 114 - } 101 + const symbolObject = plugin.symbol(applyNaming(name, plugin.config.definitions), { 102 + meta: { 103 + category: 'utility', 104 + resource: 'definition', 105 + resourceId, 106 + tool: 'typescript', 107 + }, 108 + }); 115 109 116 - const symbolObject = plugin.symbol(applyNaming(refToName($ref), plugin.config.definitions), { 117 - meta: { 118 - category: 'utility', 119 - path: fromRef(state.path), 120 - resource: 'definition', 121 - resourceId: $ref, 122 - tags: fromRef(state.tags), 123 - tool: 'typescript', 124 - }, 125 - }); 126 - const objectNode = $.const(symbolObject) 127 - .export() 128 - .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 129 - .assign( 130 - $.object( 131 - ...enumObject.obj.map((item) => 132 - $.prop({ kind: 'prop', name: item.key }) 110 + const objectNode = $.const(symbolObject) 111 + .export() 112 + .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 113 + .assign( 114 + $.object( 115 + ...itemsWithAttempts 116 + .filter(({ item }) => filteredItems.includes(item)) 117 + .map(({ duplicateAttempt, item }) => 118 + $.prop({ 119 + kind: 'prop' as const, 120 + name: resolveEnumKey({ baseName: item.key, duplicateAttempt, plugin }), 121 + }) 133 122 .$if(plugin.config.comments && createSchemaComment(item.schema), (p, v) => p.doc(v)) 134 123 .value($.fromValue(item.schema.const)), 135 124 ), 136 - ).as('const'), 137 - ); 138 - plugin.node(objectNode); 125 + ).as('const'), 126 + ); 127 + plugin.node(objectNode); 128 + 129 + const symbol = plugin.symbol(applyNaming(name, plugin.config.definitions), { 130 + meta: { 131 + category: 'type', 132 + resource: 'definition', 133 + resourceId, 134 + tool: 'typescript', 135 + }, 136 + }); 137 + const node = $.type 138 + .alias(symbol) 139 + .export() 140 + .$if(plugin.config.comments && createSchemaComment(schema), (t, v) => t.doc(v)) 141 + .type($.type(symbolObject).idx($.type(symbolObject).typeofType().keyof()).typeofType()); 142 + plugin.node(node); 143 + return true; 144 + } 139 145 140 - const symbol = plugin.symbol(applyNaming(refToName($ref), plugin.config.definitions), { 141 - meta: { 142 - category: 'type', 143 - path: fromRef(state.path), 144 - resource: 'definition', 145 - resourceId: $ref, 146 - tags: fromRef(state.tags), 147 - tool: 'typescript', 148 - }, 149 - }); 150 - const node = $.type 151 - .alias(symbol) 152 - .export() 153 - .$if(plugin.config.comments && createSchemaComment(schema), (t, v) => t.doc(v)) 154 - .type($.type(symbolObject).idx($.type(symbolObject).typeofType().keyof()).typeofType()); 155 - plugin.node(node); 156 - return; 157 - } else if ( 158 - plugin.config.enums.mode === 'typescript' || 159 - plugin.config.enums.mode === 'typescript-const' 160 - ) { 161 - // TypeScript enums support only string and number values 162 - const shouldCreateTypeScriptEnum = !enumObject.typeofItems.some( 163 - (type) => type !== 'number' && type !== 'string', 146 + if (mode === 'typescript' || mode === 'typescript-const') { 147 + const hasInvalidTypes = items.some( 148 + (item) => typeof item.schema.const !== 'number' && typeof item.schema.const !== 'string', 149 + ); 150 + if (hasInvalidTypes) return false; 151 + 152 + const symbol = plugin.symbol(applyNaming(name, plugin.config.definitions), { 153 + meta: { 154 + category: 'type', 155 + resource: 'definition', 156 + resourceId, 157 + tool: 'typescript', 158 + }, 159 + }); 160 + const enumNode = $.enum(symbol) 161 + .export() 162 + .$if(plugin.config.comments && createSchemaComment(schema), (e, v) => e.doc(v)) 163 + .const(mode === 'typescript-const') 164 + .members( 165 + ...itemsWithAttempts.map(({ duplicateAttempt, item }) => 166 + $.member(resolveEnumKey({ baseName: item.key, duplicateAttempt, plugin })) 167 + .$if(plugin.config.comments && createSchemaComment(item.schema), (m, v) => m.doc(v)) 168 + .value($.fromValue(item.schema.const)), 169 + ), 164 170 ); 165 - if (shouldCreateTypeScriptEnum) { 166 - const symbol = plugin.symbol(applyNaming(refToName($ref), plugin.config.definitions), { 167 - meta: { 168 - category: 'type', 169 - path: fromRef(state.path), 170 - resource: 'definition', 171 - resourceId: $ref, 172 - tags: fromRef(state.tags), 173 - tool: 'typescript', 174 - }, 175 - }); 176 - const enumNode = $.enum(symbol) 177 - .export() 178 - .$if(plugin.config.comments && createSchemaComment(schema), (e, v) => e.doc(v)) 179 - .const(plugin.config.enums.mode === 'typescript-const') 180 - .members( 181 - ...enumObject.obj.map((item) => 182 - $.member(item.key) 183 - .$if(plugin.config.comments && createSchemaComment(item.schema), (m, v) => m.doc(v)) 184 - .value($.fromValue(item.schema.const)), 185 - ), 186 - ); 187 - plugin.node(enumNode); 188 - return; 189 - } 190 - } 171 + plugin.node(enumNode); 172 + return true; 173 + } 174 + 175 + return false; 176 + } 177 + 178 + export function exportAst({ 179 + final, 180 + meta, 181 + naming, 182 + namingAnchor, 183 + path, 184 + plugin, 185 + schema, 186 + tags, 187 + }: ProcessorContext & { 188 + final: TypeScriptFinal; 189 + }): void { 190 + const $ref = meta.resourceId || pathToJsonPointer(path); 191 + const name = pathToName(path, { anchor: namingAnchor }); 192 + 193 + const hasEnumExport = buildEnumExport({ 194 + enumData: final.enumData, 195 + name, 196 + plugin, 197 + resourceId: $ref, 198 + schema, 199 + }); 200 + 201 + // If enum declaration/const object has been emitted, do not emit fallback type alias. 202 + if (hasEnumExport) { 203 + return; 191 204 } 192 205 193 - const symbol = plugin.symbol(applyNaming(refToName($ref), plugin.config.definitions), { 206 + const symbol = plugin.symbol(applyNaming(name, naming), { 194 207 meta: { 195 208 category: 'type', 196 - path: fromRef(state.path), 209 + path, 197 210 resource: 'definition', 198 211 resourceId: $ref, 199 - tags: fromRef(state.tags), 212 + tags, 200 213 tool: 'typescript', 201 214 }, 202 215 }); 216 + 203 217 const node = $.type 204 218 .alias(symbol) 205 219 .export() 206 220 .$if(plugin.config.comments && createSchemaComment(schema), (t, v) => t.doc(v)) 207 - .type(type); 221 + .type(final.type); 208 222 plugin.node(node); 209 - }; 223 + }
+45
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/meta.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import type { TypeScriptMeta, TypeScriptResult } from './types'; 4 + 5 + /** 6 + * Creates default metadata from a schema. 7 + */ 8 + export function defaultMeta(schema: IR.SchemaObject): TypeScriptMeta { 9 + return { 10 + default: schema.default, 11 + readonly: schema.accessScope === 'read', 12 + }; 13 + } 14 + 15 + /** 16 + * Composes metadata from child results. 17 + * 18 + * @param children - Results from walking child schemas 19 + * @param overrides - Explicit overrides (e.g., from parent schema) 20 + */ 21 + export function composeMeta( 22 + children: ReadonlyArray<TypeScriptResult>, 23 + overrides?: Partial<TypeScriptMeta>, 24 + ): TypeScriptMeta { 25 + return { 26 + default: overrides?.default, 27 + readonly: overrides?.readonly ?? children.some((c) => c.meta.readonly), 28 + }; 29 + } 30 + 31 + /** 32 + * Merges parent schema metadata with composed child metadata. 33 + * 34 + * @param parent - The parent schema 35 + * @param children - Results from walking child schemas 36 + */ 37 + export function inheritMeta( 38 + parent: IR.SchemaObject, 39 + children: ReadonlyArray<TypeScriptResult>, 40 + ): TypeScriptMeta { 41 + return composeMeta(children, { 42 + default: parent.default, 43 + readonly: parent.accessScope === 'read', 44 + }); 45 + }
+115 -123
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/operation.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 2 import { applyNaming } from '@hey-api/shared'; 4 3 import { operationResponsesMap } from '@hey-api/shared'; 5 4 import { deduplicateSchema } from '@hey-api/shared'; 6 5 7 6 import { $ } from '../../../../ts-dsl'; 8 - import { irSchemaToAst } from '../v1/plugin'; 9 - import type { IrSchemaToAstOptions } from './types'; 7 + import type { HeyApiTypeScriptPlugin } from '../types'; 8 + import { createProcessor } from '../v1/processor'; 10 9 11 10 const irParametersToIrSchema = ({ 12 11 parameters, ··· 44 43 return irSchema; 45 44 }; 46 45 47 - const operationToDataType = ({ 46 + export const operationToType = ({ 48 47 operation, 48 + path, 49 49 plugin, 50 - state, 51 - }: IrSchemaToAstOptions & { 50 + tags, 51 + }: { 52 52 operation: IR.OperationObject; 53 - }) => { 53 + path: ReadonlyArray<string | number>; 54 + plugin: HeyApiTypeScriptPlugin['Instance']; 55 + tags?: ReadonlyArray<string>; 56 + }): void => { 57 + const processor = createProcessor(plugin); 58 + 54 59 const data: IR.SchemaObject = { 60 + properties: { 61 + body: operation.body?.schema ?? { type: 'never' }, 62 + ...(operation.parameters?.header 63 + ? { 64 + headers: irParametersToIrSchema({ 65 + parameters: operation.parameters.header, 66 + }), 67 + } 68 + : {}), 69 + path: operation.parameters?.path 70 + ? irParametersToIrSchema({ parameters: operation.parameters.path }) 71 + : { type: 'never' }, 72 + query: operation.parameters?.query 73 + ? irParametersToIrSchema({ parameters: operation.parameters.query }) 74 + : { type: 'never' }, 75 + url: { 76 + const: operation.path, 77 + type: 'string', 78 + }, 79 + }, 55 80 type: 'object', 56 81 }; 82 + 57 83 const dataRequired: Array<string> = []; 58 84 59 - if (!data.properties) { 60 - data.properties = {}; 85 + if (operation.body?.required) { 86 + dataRequired.push('body'); 61 87 } 62 88 63 - if (operation.body) { 64 - data.properties.body = operation.body.schema; 65 - 66 - if (operation.body.required) { 67 - dataRequired.push('body'); 68 - } 69 - } else { 70 - data.properties.body = { 71 - type: 'never', 72 - }; 89 + // do not set headers to never so we can always pass arbitrary values 90 + if (data.properties!.headers?.required) { 91 + dataRequired.push('headers'); 73 92 } 74 93 75 - // TODO: parser - handle cookie parameters 76 - 77 - // do not set headers to never so we can always pass arbitrary values 78 - if (operation.parameters?.header) { 79 - data.properties.headers = irParametersToIrSchema({ 80 - parameters: operation.parameters.header, 81 - }); 82 - 83 - if (data.properties.headers.required) { 84 - dataRequired.push('headers'); 85 - } 94 + if (data.properties!.path!.required) { 95 + dataRequired.push('path'); 86 96 } 87 97 88 - if (operation.parameters?.path) { 89 - data.properties.path = irParametersToIrSchema({ 90 - parameters: operation.parameters.path, 91 - }); 92 - 93 - if (data.properties.path.required) { 94 - dataRequired.push('path'); 95 - } 96 - } else { 97 - data.properties.path = { 98 - type: 'never', 99 - }; 98 + if (data.properties!.query!.required) { 99 + dataRequired.push('query'); 100 100 } 101 101 102 - if (operation.parameters?.query) { 103 - data.properties.query = irParametersToIrSchema({ 104 - parameters: operation.parameters.query, 105 - }); 102 + dataRequired.push('url'); 106 103 107 - if (data.properties.query.required) { 108 - dataRequired.push('query'); 109 - } 110 - } else { 111 - data.properties.query = { 112 - type: 'never', 113 - }; 104 + if (dataRequired.length > 0) { 105 + data.required = dataRequired; 114 106 } 115 107 116 - data.properties.url = { 117 - const: operation.path, 118 - type: 'string', 119 - }; 120 - dataRequired.push('url'); 121 - 122 - data.required = dataRequired; 108 + const dataResult = processor.process({ 109 + export: false, 110 + meta: { 111 + resource: 'operation', 112 + resourceId: operation.id, 113 + }, 114 + naming: plugin.config.definitions, 115 + path: [...path, operation.id, 'data'], 116 + plugin, 117 + schema: data, 118 + }); 123 119 124 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.requests), { 120 + const dataSymbol = plugin.symbol(applyNaming(operation.id, plugin.config.requests), { 125 121 meta: { 126 122 category: 'type', 127 - path: fromRef(state.path), 123 + path, 128 124 resource: 'operation', 129 125 resourceId: operation.id, 130 126 role: 'data', 131 - tags: fromRef(state.tags), 127 + tags, 132 128 tool: 'typescript', 133 129 }, 134 130 }); 135 - const node = $.type 136 - .alias(symbol) 131 + const dataNode = $.type 132 + .alias(dataSymbol) 137 133 .export() 138 - .type( 139 - irSchemaToAst({ 140 - plugin, 141 - schema: data, 142 - state, 143 - }), 144 - ); 145 - plugin.node(node); 146 - }; 147 - 148 - export const operationToType = ({ 149 - operation, 150 - plugin, 151 - state, 152 - }: IrSchemaToAstOptions & { 153 - operation: IR.OperationObject; 154 - }) => { 155 - operationToDataType({ operation, plugin, state }); 134 + .type(dataResult?.type ?? $.type('never')); 135 + plugin.node(dataNode); 156 136 157 137 const { error, errors, response, responses } = operationResponsesMap(operation); 158 138 159 139 if (errors) { 160 - const symbolErrors = plugin.symbol(applyNaming(operation.id, plugin.config.errors), { 140 + const errorsResult = processor.process({ 141 + export: false, 142 + meta: { 143 + resource: 'operation', 144 + resourceId: operation.id, 145 + }, 146 + naming: plugin.config.definitions, 147 + path: [...path, operation.id, 'errors'], 148 + plugin, 149 + schema: errors, 150 + }); 151 + 152 + const errorsSymbol = plugin.symbol(applyNaming(operation.id, plugin.config.errors), { 161 153 meta: { 162 154 category: 'type', 163 - path: fromRef(state.path), 155 + path, 164 156 resource: 'operation', 165 157 resourceId: operation.id, 166 158 role: 'errors', 167 - tags: fromRef(state.tags), 159 + tags, 168 160 tool: 'typescript', 169 161 }, 170 162 }); 171 - const node = $.type 172 - .alias(symbolErrors) 163 + const errorsNode = $.type 164 + .alias(errorsSymbol) 173 165 .export() 174 - .type( 175 - irSchemaToAst({ 176 - plugin, 177 - schema: errors, 178 - state, 179 - }), 180 - ); 181 - plugin.node(node); 166 + .type(errorsResult?.type ?? $.type('never')); 167 + plugin.node(errorsNode); 182 168 183 169 if (error) { 184 - const symbol = plugin.symbol( 170 + const errorSymbol = plugin.symbol( 185 171 applyNaming(operation.id, { 186 172 case: plugin.config.errors.case, 187 173 name: plugin.config.errors.error, ··· 189 175 { 190 176 meta: { 191 177 category: 'type', 192 - path: fromRef(state.path), 178 + path, 193 179 resource: 'operation', 194 180 resourceId: operation.id, 195 181 role: 'error', 196 - tags: fromRef(state.tags), 182 + tags, 197 183 tool: 'typescript', 198 184 }, 199 185 }, 200 186 ); 201 - const node = $.type 202 - .alias(symbol) 187 + const errorNode = $.type 188 + .alias(errorSymbol) 203 189 .export() 204 - .type($.type(symbolErrors).idx($.type(symbolErrors).keyof())); 205 - plugin.node(node); 190 + .type($.type(errorsSymbol).idx($.type(errorsSymbol).keyof())); 191 + plugin.node(errorNode); 206 192 } 207 193 } 208 194 209 195 if (responses) { 210 - const symbolResponses = plugin.symbol(applyNaming(operation.id, plugin.config.responses), { 196 + const responsesResult = processor.process({ 197 + export: false, 198 + meta: { 199 + resource: 'operation', 200 + resourceId: operation.id, 201 + }, 202 + naming: plugin.config.definitions, 203 + path: [...path, operation.id, 'responses'], 204 + plugin, 205 + schema: responses, 206 + }); 207 + 208 + const responsesSymbol = plugin.symbol(applyNaming(operation.id, plugin.config.responses), { 211 209 meta: { 212 210 category: 'type', 213 - path: fromRef(state.path), 211 + path, 214 212 resource: 'operation', 215 213 resourceId: operation.id, 216 214 role: 'responses', 217 - tags: fromRef(state.tags), 215 + tags, 218 216 tool: 'typescript', 219 217 }, 220 218 }); 221 - const node = $.type 222 - .alias(symbolResponses) 219 + const responsesNode = $.type 220 + .alias(responsesSymbol) 223 221 .export() 224 - .type( 225 - irSchemaToAst({ 226 - plugin, 227 - schema: responses, 228 - state, 229 - }), 230 - ); 231 - plugin.node(node); 222 + .type(responsesResult?.type ?? $.type('never')); 223 + plugin.node(responsesNode); 232 224 233 225 if (response) { 234 - const symbol = plugin.symbol( 226 + const responseSymbol = plugin.symbol( 235 227 applyNaming(operation.id, { 236 228 case: plugin.config.responses.case, 237 229 name: plugin.config.responses.response, ··· 239 231 { 240 232 meta: { 241 233 category: 'type', 242 - path: fromRef(state.path), 234 + path, 243 235 resource: 'operation', 244 236 resourceId: operation.id, 245 237 role: 'response', 246 - tags: fromRef(state.tags), 238 + tags, 247 239 tool: 'typescript', 248 240 }, 249 241 }, 250 242 ); 251 - const node = $.type 252 - .alias(symbol) 243 + const responseNode = $.type 244 + .alias(responseSymbol) 253 245 .export() 254 - .type($.type(symbolResponses).idx($.type(symbolResponses).keyof())); 255 - plugin.node(node); 246 + .type($.type(responsesSymbol).idx($.type(responsesSymbol).keyof())); 247 + plugin.node(responseNode); 256 248 } 257 249 } 258 250 };
+17
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/processor.ts
··· 1 + import type { IR, NamingConfig, SchemaProcessorContext } from '@hey-api/shared'; 2 + 3 + import type { HeyApiTypeScriptPlugin } from '../types'; 4 + import type { TypeScriptFinal } from './types'; 5 + 6 + export type ProcessorContext = SchemaProcessorContext & { 7 + /** Whether to export the result (default: true) */ 8 + export?: boolean; 9 + naming: NamingConfig; 10 + /** The plugin instance. */ 11 + plugin: HeyApiTypeScriptPlugin['Instance']; 12 + schema: IR.SchemaObject; 13 + }; 14 + 15 + export type ProcessorResult = { 16 + process: (ctx: ProcessorContext) => TypeScriptFinal | void; 17 + };
+32 -12
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/types.ts
··· 1 - import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { SchemaExtractor } from '@hey-api/shared'; 1 + import type { IR } from '@hey-api/shared'; 3 2 4 - import type { HeyApiTypeScriptPlugin } from '../types'; 3 + import type { MaybeTsDsl, TypeTsDsl } from '../../../../ts-dsl'; 5 4 6 - export type IrSchemaToAstOptions = { 7 - /** The plugin instance. */ 8 - plugin: HeyApiTypeScriptPlugin['Instance']; 9 - /** Optional schema extractor function. */ 10 - schemaExtractor?: SchemaExtractor; 11 - /** The plugin state references. */ 12 - state: Refs<PluginState>; 13 - }; 5 + export type { HeyApiTypeScriptPlugin } from '../types'; 14 6 15 - export type PluginState = Pick<Required<SymbolMeta>, 'path'> & Pick<Partial<SymbolMeta>, 'tags'>; 7 + /** 8 + * Metadata that flows through schema walking. 9 + */ 10 + export interface TypeScriptMeta { 11 + /** Default value from schema. */ 12 + default?: unknown; 13 + /** Is this schema read-only? */ 14 + readonly: boolean; 15 + } 16 + 17 + export interface TypeScriptEnumData { 18 + items: Array<{ key: string; schema: IR.SchemaObject }>; 19 + mode: 'javascript' | 'typescript' | 'typescript-const' | 'type'; 20 + } 21 + 22 + /** 23 + * Result from walking a schema node. 24 + */ 25 + export interface TypeScriptResult { 26 + enumData?: TypeScriptEnumData; 27 + meta: TypeScriptMeta; 28 + type: MaybeTsDsl<TypeTsDsl>; 29 + } 30 + 31 + /** 32 + * Finalized result after applyModifiers. 33 + */ 34 + // eslint-disable-next-line @typescript-eslint/no-empty-object-type 35 + export interface TypeScriptFinal extends Pick<TypeScriptResult, 'enumData' | 'type'> {}
+48 -70
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/webhook.ts
··· 1 1 import type { Symbol } from '@hey-api/codegen-core'; 2 - import { fromRef } from '@hey-api/codegen-core'; 3 2 import type { IR } from '@hey-api/shared'; 4 3 import { applyNaming } from '@hey-api/shared'; 5 4 6 5 import { createSchemaComment } from '../../../../plugins/shared/utils/schema'; 7 6 import { $ } from '../../../../ts-dsl'; 8 - import { irSchemaToAst } from '../v1/plugin'; 9 - import type { IrSchemaToAstOptions } from './types'; 7 + import { createProcessor } from '../v1/processor'; 8 + import type { HeyApiTypeScriptPlugin } from './types'; 10 9 11 - const operationToDataType = ({ 10 + export function webhookToType({ 12 11 operation, 12 + path, 13 13 plugin, 14 - state, 15 - }: IrSchemaToAstOptions & { 14 + tags, 15 + }: { 16 16 operation: IR.OperationObject; 17 - }): Symbol => { 18 - const data: IR.SchemaObject = { 19 - type: 'object', 20 - }; 21 - const dataRequired: Array<string> = []; 22 - 23 - if (!data.properties) { 24 - data.properties = {}; 25 - } 17 + path: ReadonlyArray<string | number>; 18 + plugin: HeyApiTypeScriptPlugin['Instance']; 19 + tags?: ReadonlyArray<string>; 20 + }): Symbol { 21 + const processor = createProcessor(plugin); 22 + let symbolWebhookPayload: Symbol | undefined; 26 23 27 24 if (operation.body) { 28 - const symbolWebhookPayload = plugin.symbol( 25 + symbolWebhookPayload = plugin.symbol( 29 26 applyNaming(operation.id, { 30 27 case: plugin.config.webhooks.case, 31 28 name: plugin.config.webhooks.payload, ··· 33 30 { 34 31 meta: { 35 32 category: 'type', 36 - path: fromRef(state.path), 33 + path, 37 34 resource: 'webhook', 38 35 resourceId: operation.id, 39 - role: 'data', 40 - tags: fromRef(state.tags), 36 + role: 'payload', 37 + tags, 41 38 tool: 'typescript', 42 39 }, 43 40 }, 44 41 ); 45 - const node = $.type 42 + 43 + const payloadResult = processor.process({ 44 + export: false, 45 + meta: { 46 + resource: 'webhook', 47 + resourceId: operation.id, 48 + }, 49 + naming: plugin.config.definitions, 50 + path: [...path, operation.id, 'payload'], 51 + plugin, 52 + schema: operation.body.schema, 53 + }); 54 + 55 + const payloadNode = $.type 46 56 .alias(symbolWebhookPayload) 47 57 .export() 48 58 .$if(plugin.config.comments && createSchemaComment(operation.body.schema), (t, v) => t.doc(v)) 49 - .type( 50 - irSchemaToAst({ 51 - plugin, 52 - schema: operation.body.schema, 53 - state, 54 - }), 55 - ); 56 - plugin.node(node); 57 - 58 - data.properties.body = { symbolRef: symbolWebhookPayload }; 59 - dataRequired.push('body'); 60 - } else { 61 - data.properties.body = { type: 'never' }; 59 + .type(payloadResult?.type ?? $.type('never')); 60 + plugin.node(payloadNode); 62 61 } 63 62 64 - data.properties.key = { 65 - const: operation.path, 66 - type: 'string', 67 - }; 68 - dataRequired.push('key'); 69 - 70 - data.properties.path = { type: 'never' }; 71 - data.properties.query = { type: 'never' }; 72 - 73 - data.required = dataRequired; 63 + const requestType = $.type 64 + .object() 65 + .prop('body', (p) => 66 + p 67 + .required(Boolean(symbolWebhookPayload)) 68 + .type(symbolWebhookPayload ? $.type(symbolWebhookPayload) : $.type('never')), 69 + ) 70 + .prop('key', (p) => p.required(true).type($.type.literal(operation.path))) 71 + .prop('path', (p) => p.required(false).type($.type('never'))) 72 + .prop('query', (p) => p.required(false).type($.type('never'))); 74 73 75 - const symbolWebhookRequest = plugin.symbol(applyNaming(operation.id, plugin.config.webhooks), { 74 + const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.webhooks), { 76 75 meta: { 77 76 category: 'type', 78 - path: fromRef(state.path), 77 + path, 79 78 resource: 'webhook', 80 79 resourceId: operation.id, 81 80 role: 'data', 82 - tags: fromRef(state.tags), 81 + tags, 83 82 tool: 'typescript', 84 83 }, 85 84 }); 86 - const node = $.type 87 - .alias(symbolWebhookRequest) 88 - .export() 89 - .type( 90 - irSchemaToAst({ 91 - plugin, 92 - schema: data, 93 - state, 94 - }), 95 - ); 85 + 86 + const node = $.type.alias(symbol).export().type(requestType); 96 87 plugin.node(node); 97 88 98 - return symbolWebhookRequest; 99 - }; 100 - 101 - export const webhookToType = ({ 102 - operation, 103 - plugin, 104 - state, 105 - }: IrSchemaToAstOptions & { 106 - operation: IR.OperationObject; 107 - }): Symbol => { 108 - const symbol = operationToDataType({ operation, plugin, state }); 109 89 return symbol; 110 - 111 - // don't handle webhook responses for now, users only need requestBody 112 - }; 90 + }
-1
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/api.ts
··· 1 - export { irSchemaToAst as irSchemaToAstV1 } from './plugin';
+33 -116
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts
··· 1 1 import type { Symbol } from '@hey-api/codegen-core'; 2 - import { fromRef, refs } from '@hey-api/codegen-core'; 3 - import type { IR, SchemaWithType } from '@hey-api/shared'; 4 - import { applyNaming, deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 2 + import type { IR } from '@hey-api/shared'; 3 + import { applyNaming, pathToJsonPointer } from '@hey-api/shared'; 5 4 6 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../ts-dsl'; 7 5 import { $ } from '../../../../ts-dsl'; 8 6 import { createClientOptions } from '../shared/clientOptions'; 9 - import { exportType } from '../shared/export'; 10 7 import { operationToType } from '../shared/operation'; 11 - import type { IrSchemaToAstOptions, PluginState } from '../shared/types'; 12 8 import { webhookToType } from '../shared/webhook'; 13 9 import type { HeyApiTypeScriptPlugin } from '../types'; 14 - import { irSchemaWithTypeToAst } from './toAst'; 15 - 16 - export function irSchemaToAst({ 17 - plugin, 18 - schema, 19 - schemaExtractor, 20 - state, 21 - }: IrSchemaToAstOptions & { 22 - schema: IR.SchemaObject; 23 - }): MaybeTsDsl<TypeTsDsl> { 24 - if (schemaExtractor && !schema.$ref) { 25 - const extracted = schemaExtractor({ 26 - meta: { 27 - resource: 'definition', 28 - resourceId: pathToJsonPointer(fromRef(state.path)), 29 - }, 30 - path: fromRef(state.path), 31 - schema, 32 - }); 33 - if (extracted !== schema) schema = extracted; 34 - } 35 - 36 - if (schema.symbolRef) { 37 - const baseType = $.type(schema.symbolRef); 38 - if (schema.omit && schema.omit.length > 0) { 39 - // Render as Omit<Type, 'prop1' | 'prop2'> 40 - const omittedKeys = 41 - schema.omit.length === 1 42 - ? $.type.literal(schema.omit[0]!) 43 - : $.type.or(...schema.omit.map((key) => $.type.literal(key))); 44 - return $.type('Omit').generics(baseType, omittedKeys); 45 - } 46 - return baseType; 47 - } 48 - 49 - if (schema.$ref) { 50 - const symbol = plugin.referenceSymbol({ 51 - category: 'type', 52 - resource: 'definition', 53 - resourceId: schema.$ref, 54 - }); 55 - const baseType = $.type(symbol); 56 - if (schema.omit && schema.omit.length > 0) { 57 - // Render as Omit<Type, 'prop1' | 'prop2'> 58 - const omittedKeys = 59 - schema.omit.length === 1 60 - ? $.type.literal(schema.omit[0]!) 61 - : $.type.or(...schema.omit.map((key) => $.type.literal(key))); 62 - return $.type('Omit').generics(baseType, omittedKeys); 63 - } 64 - return baseType; 65 - } 66 - 67 - if (schema.type) { 68 - return irSchemaWithTypeToAst({ 69 - plugin, 70 - schema: schema as SchemaWithType, 71 - state, 72 - }); 73 - } 74 - 75 - if (schema.items) { 76 - schema = deduplicateSchema({ detectFormat: false, schema }); 77 - if (schema.items) { 78 - const itemTypes = schema.items.map((item) => irSchemaToAst({ plugin, schema: item, state })); 79 - return schema.logicalOperator === 'and' ? $.type.and(...itemTypes) : $.type.or(...itemTypes); 80 - } 81 - 82 - return irSchemaToAst({ plugin, schema, state }); 83 - } 84 - 85 - // catch-all fallback for failed schemas 86 - return irSchemaWithTypeToAst({ 87 - plugin, 88 - schema: { 89 - type: 'unknown', 90 - }, 91 - state, 92 - }); 93 - } 94 - 95 - function handleComponent({ 96 - plugin, 97 - schema, 98 - state, 99 - }: IrSchemaToAstOptions & { 100 - schema: IR.SchemaObject; 101 - }) { 102 - const type = irSchemaToAst({ plugin, schema, state }); 103 - exportType({ 104 - plugin, 105 - schema, 106 - state, 107 - type, 108 - }); 109 - } 10 + import { createProcessor } from './processor'; 110 11 111 12 export const handlerV1: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { 112 - // reserve node for ClientOptions 113 13 const nodeClientIndex = plugin.node(null); 114 - // reserve node for Webhooks 115 14 const nodeWebhooksIndex = plugin.node(null); 116 15 117 16 const servers: Array<IR.ServerObject> = []; 118 17 const webhooks: Array<Symbol> = []; 119 18 19 + const processor = createProcessor(plugin); 20 + 120 21 plugin.forEach( 121 22 'operation', 122 23 'parameter', ··· 125 26 'server', 126 27 'webhook', 127 28 (event) => { 128 - const state = refs<PluginState>({ 129 - path: event._path, 130 - tags: event.tags, 131 - }); 132 29 switch (event.type) { 133 30 case 'operation': 134 31 operationToType({ 135 32 operation: event.operation, 33 + path: event._path, 136 34 plugin, 137 - state, 35 + tags: event.tags, 138 36 }); 139 37 break; 140 38 case 'parameter': 141 - handleComponent({ 39 + processor.process({ 40 + meta: { 41 + resource: 'definition', 42 + resourceId: pathToJsonPointer(event._path), 43 + }, 44 + naming: plugin.config.definitions, 45 + path: event._path, 142 46 plugin, 143 47 schema: event.parameter.schema, 144 - state, 48 + tags: event.tags, 145 49 }); 146 50 break; 147 51 case 'requestBody': 148 - handleComponent({ 52 + processor.process({ 53 + meta: { 54 + resource: 'definition', 55 + resourceId: pathToJsonPointer(event._path), 56 + }, 57 + naming: plugin.config.definitions, 58 + path: event._path, 149 59 plugin, 150 60 schema: event.requestBody.schema, 151 - state, 61 + tags: event.tags, 152 62 }); 153 63 break; 154 64 case 'schema': 155 - handleComponent({ 65 + processor.process({ 66 + meta: { 67 + resource: 'definition', 68 + resourceId: pathToJsonPointer(event._path), 69 + }, 70 + naming: plugin.config.definitions, 71 + path: event._path, 156 72 plugin, 157 73 schema: event.schema, 158 - state, 74 + tags: event.tags, 159 75 }); 160 76 break; 161 77 case 'server': ··· 165 81 webhooks.push( 166 82 webhookToType({ 167 83 operation: event.operation, 84 + path: event._path, 168 85 plugin, 169 - state, 86 + tags: event.tags, 170 87 }), 171 88 ); 172 89 break;
+68
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/processor.ts
··· 1 + import { ref } from '@hey-api/codegen-core'; 2 + import type { Hooks, IR } from '@hey-api/shared'; 3 + import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { exportAst } from '../shared/export'; 6 + import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 + import type { TypeScriptFinal } from '../shared/types'; 8 + import type { HeyApiTypeScriptPlugin } from '../types'; 9 + import { createVisitor } from './walker'; 10 + 11 + export function createProcessor(plugin: HeyApiTypeScriptPlugin['Instance']): ProcessorResult { 12 + const processor = createSchemaProcessor(); 13 + 14 + const extractorHooks: ReadonlyArray<NonNullable<Hooks['schemas']>['shouldExtract']> = [ 15 + plugin.config['~hooks']?.schemas?.shouldExtract, 16 + plugin.context.config.parser.hooks.schemas?.shouldExtract, 17 + ]; 18 + 19 + function extractor(ctx: ProcessorContext): IR.SchemaObject { 20 + if (processor.hasEmitted(ctx.path)) { 21 + return ctx.schema; 22 + } 23 + 24 + for (const hook of extractorHooks) { 25 + const result = hook?.(ctx); 26 + if (result) { 27 + process({ 28 + namingAnchor: processor.context.anchor, 29 + tags: processor.context.tags, 30 + ...ctx, 31 + }); 32 + return { $ref: pathToJsonPointer(ctx.path) }; 33 + } 34 + } 35 + 36 + return ctx.schema; 37 + } 38 + 39 + function process(ctx: ProcessorContext): TypeScriptFinal | void { 40 + if (!processor.markEmitted(ctx.path)) return; 41 + 42 + const shouldExport = ctx.export !== false; 43 + 44 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 45 + const visitor = createVisitor({ schemaExtractor: extractor }); 46 + const walk = createSchemaWalker(visitor); 47 + 48 + const result = walk(ctx.schema, { 49 + path: ref(ctx.path), 50 + plugin, 51 + }); 52 + 53 + const final = visitor.applyModifiers(result, { 54 + path: ref(ctx.path), 55 + plugin, 56 + }) as TypeScriptFinal; 57 + 58 + if (shouldExport) { 59 + exportAst({ ...ctx, final, plugin }); 60 + return; 61 + } 62 + 63 + return final; 64 + }); 65 + } 66 + 67 + return { process }; 68 + }
+23 -28
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/array.ts
··· 1 - import { fromRef, ref } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { SchemaWithType } from '@hey-api/shared'; 3 + import type { Walker } from '@hey-api/shared'; 3 4 import { deduplicateSchema } from '@hey-api/shared'; 4 5 5 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../../ts-dsl'; 6 6 import { $ } from '../../../../../ts-dsl'; 7 - import type { IrSchemaToAstOptions } from '../../shared/types'; 8 - import { irSchemaToAst } from '../plugin'; 7 + import type { HeyApiTypeScriptPlugin } from '../../shared/types'; 8 + import type { TypeScriptResult } from '../../shared/types'; 9 9 10 10 export function arrayToAst({ 11 11 plugin, 12 12 schema, 13 - state, 14 - }: IrSchemaToAstOptions & { 13 + walk, 14 + }: { 15 + plugin: HeyApiTypeScriptPlugin['Instance']; 15 16 schema: SchemaWithType<'array'>; 16 - }): TypeTsDsl { 17 + walk: Walker<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']>; 18 + }): TypeScriptResult['type'] { 17 19 if (!schema.items) { 18 20 return $.type('Array').generic($.type(plugin.config.topType)); 19 21 } 20 22 21 - schema = deduplicateSchema({ detectFormat: true, schema }); 22 - 23 - const itemTypes: Array<MaybeTsDsl<TypeTsDsl>> = []; 24 - 25 - if (schema.items) { 26 - schema.items.forEach((item, index) => { 27 - const type = irSchemaToAst({ 28 - plugin, 29 - schema: item, 30 - state: { 31 - ...state, 32 - path: ref([...fromRef(state.path), 'items', index]), 33 - }, 34 - }); 35 - itemTypes.push(type); 36 - }); 23 + const dedupedSchema = deduplicateSchema({ detectFormat: true, schema }); 24 + if (!dedupedSchema.items) { 25 + return $.type('Array').generic($.type(plugin.config.topType)); 37 26 } 38 27 39 - if (itemTypes.length === 1) { 40 - return $.type('Array').generic(itemTypes[0]!); 28 + const itemResults: Array<TypeScriptResult> = dedupedSchema.items.map((item) => 29 + walk(item, { 30 + path: ref([]), 31 + plugin, 32 + }), 33 + ); 34 + if (itemResults.length === 1) { 35 + return $.type('Array').generic(itemResults[0]!.type); 41 36 } 42 37 43 - return schema.logicalOperator === 'and' 44 - ? $.type('Array').generic($.type.and(...itemTypes)) 45 - : $.type('Array').generic($.type.or(...itemTypes)); 38 + return dedupedSchema.logicalOperator === 'and' 39 + ? $.type('Array').generic($.type.and(...itemResults.map((r) => r.type))) 40 + : $.type('Array').generic($.type.or(...itemResults.map((r) => r.type))); 46 41 }
+5 -5
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/boolean.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 6 export function booleanToAst({ 8 7 schema, 9 - }: IrSchemaToAstOptions & { 8 + }: { 9 + plugin: HeyApiTypeScriptPlugin['Instance']; 10 10 schema: SchemaWithType<'boolean'>; 11 - }): TypeTsDsl { 11 + }): TypeScriptResult['type'] { 12 12 if (schema.const !== undefined) { 13 - return $.type.literal(schema.const as boolean); 13 + return $.type.fromValue(schema.const); 14 14 } 15 15 16 16 return $.type('boolean');
+58 -15
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/enum.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../../ts-dsl'; 4 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 - import { irSchemaToAst } from '../plugin'; 3 + import { $ } from '../../../../../ts-dsl'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 5 + import type { TypeScriptEnumData } from '../../shared/types'; 6 + 7 + function buildEnumData( 8 + plugin: HeyApiTypeScriptPlugin['Instance'], 9 + schema: SchemaWithType<'enum'>, 10 + ): TypeScriptEnumData | undefined { 11 + if (!plugin.config.enums.enabled) { 12 + return undefined; 13 + } 14 + 15 + const items = schema.items ?? []; 16 + const mode = plugin.config.enums.mode; 17 + 18 + return { 19 + items: items.map((item, index) => { 20 + let key: string; 21 + if (item.title) { 22 + key = item.title; 23 + } else if (typeof item.const === 'number' || typeof item.const === 'string') { 24 + key = `${item.const}`; 25 + } else if (typeof item.const === 'boolean') { 26 + key = item.const ? 'true' : 'false'; 27 + } else if (item.const === null) { 28 + key = 'null'; 29 + } else { 30 + key = `${index}`; 31 + } 32 + return { key, schema: item }; 33 + }), 34 + mode, 35 + }; 36 + } 6 37 7 38 export function enumToAst({ 8 39 plugin, 9 40 schema, 10 - state, 11 - }: IrSchemaToAstOptions & { 41 + }: { 42 + plugin: HeyApiTypeScriptPlugin['Instance']; 12 43 schema: SchemaWithType<'enum'>; 13 - }): MaybeTsDsl<TypeTsDsl> { 14 - const type = irSchemaToAst({ 15 - plugin, 16 - schema: { 17 - ...schema, 18 - type: undefined, 19 - }, 20 - state, 21 - }); 22 - return type; 44 + }): { 45 + enumData?: TypeScriptEnumData; 46 + type: TypeScriptResult['type']; 47 + } { 48 + const items = schema.items ?? []; 49 + const enumData = buildEnumData(plugin, schema); 50 + 51 + let type: TypeScriptResult['type']; 52 + 53 + if (items.length === 0) { 54 + type = $.type('never'); 55 + } else { 56 + const literalTypes = items 57 + .filter((item) => item.const !== undefined) 58 + .map((item) => $.type.fromValue(item.const)); 59 + type = literalTypes.length > 0 ? $.type.or(...literalTypes) : $.type('string'); 60 + } 61 + 62 + return { 63 + enumData, 64 + type, 65 + }; 23 66 }
-97
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/index.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 2 - 3 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../../ts-dsl'; 4 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 - import { arrayToAst } from './array'; 6 - import { booleanToAst } from './boolean'; 7 - import { enumToAst } from './enum'; 8 - import { neverToAst } from './never'; 9 - import { nullToAst } from './null'; 10 - import { numberToAst } from './number'; 11 - import { objectToAst } from './object'; 12 - import { stringToAst } from './string'; 13 - import { tupleToAst } from './tuple'; 14 - import { undefinedToAst } from './undefined'; 15 - import { unknownToAst } from './unknown'; 16 - import { voidToAst } from './void'; 17 - 18 - export function irSchemaWithTypeToAst({ 19 - schema, 20 - ...args 21 - }: IrSchemaToAstOptions & { 22 - schema: SchemaWithType; 23 - }): MaybeTsDsl<TypeTsDsl> { 24 - const transformersPlugin = args.plugin.getPlugin('@hey-api/transformers'); 25 - if (transformersPlugin?.config.typeTransformers) { 26 - for (const typeTransformer of transformersPlugin.config.typeTransformers) { 27 - const typeNode = typeTransformer({ schema }); 28 - if (typeNode) { 29 - return typeNode; 30 - } 31 - } 32 - } 33 - 34 - switch (schema.type) { 35 - case 'array': 36 - return arrayToAst({ 37 - ...args, 38 - schema: schema as SchemaWithType<'array'>, 39 - }); 40 - case 'boolean': 41 - return booleanToAst({ 42 - ...args, 43 - schema: schema as SchemaWithType<'boolean'>, 44 - }); 45 - case 'enum': 46 - return enumToAst({ 47 - ...args, 48 - schema: schema as SchemaWithType<'enum'>, 49 - }); 50 - case 'integer': 51 - case 'number': 52 - return numberToAst({ 53 - ...args, 54 - schema: schema as SchemaWithType<'integer' | 'number'>, 55 - }); 56 - case 'never': 57 - return neverToAst({ 58 - ...args, 59 - schema: schema as SchemaWithType<'never'>, 60 - }); 61 - case 'null': 62 - return nullToAst({ 63 - ...args, 64 - schema: schema as SchemaWithType<'null'>, 65 - }); 66 - case 'object': 67 - return objectToAst({ 68 - ...args, 69 - schema: schema as SchemaWithType<'object'>, 70 - }); 71 - case 'string': 72 - return stringToAst({ 73 - ...args, 74 - schema: schema as SchemaWithType<'string'>, 75 - }); 76 - case 'tuple': 77 - return tupleToAst({ 78 - ...args, 79 - schema: schema as SchemaWithType<'tuple'>, 80 - }); 81 - case 'undefined': 82 - return undefinedToAst({ 83 - ...args, 84 - schema: schema as SchemaWithType<'undefined'>, 85 - }); 86 - case 'unknown': 87 - return unknownToAst({ 88 - ...args, 89 - schema: schema as SchemaWithType<'unknown'>, 90 - }); 91 - case 'void': 92 - return voidToAst({ 93 - ...args, 94 - schema: schema as SchemaWithType<'void'>, 95 - }); 96 - } 97 - }
+8 -11
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/never.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 - export const neverToAst = ( 8 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 - _args: IrSchemaToAstOptions & { 10 - schema: SchemaWithType<'never'>; 11 - }, 12 - ): TypeTsDsl => { 13 - const node = $.type('never'); 14 - return node; 15 - }; 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function neverToAst(args: { 8 + plugin: HeyApiTypeScriptPlugin['Instance']; 9 + schema: SchemaWithType<'never'>; 10 + }): TypeScriptResult['type'] { 11 + return $.type('never'); 12 + }
+8 -11
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/null.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 - export const nullToAst = ( 8 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 - _args: IrSchemaToAstOptions & { 10 - schema: SchemaWithType<'null'>; 11 - }, 12 - ): TypeTsDsl => { 13 - const node = $.type.literal(null); 14 - return node; 15 - }; 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function nullToAst(args: { 8 + plugin: HeyApiTypeScriptPlugin['Instance']; 9 + schema: SchemaWithType<'null'>; 10 + }): TypeScriptResult['type'] { 11 + return $.type.literal(null); 12 + }
+7 -7
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/number.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 - export const numberToAst = ({ 6 + export function numberToAst({ 8 7 plugin, 9 8 schema, 10 - }: IrSchemaToAstOptions & { 9 + }: { 10 + plugin: HeyApiTypeScriptPlugin['Instance']; 11 11 schema: SchemaWithType<'integer' | 'number'>; 12 - }): TypeTsDsl => { 12 + }): TypeScriptResult['type'] { 13 13 if (schema.const !== undefined) { 14 - return $.type.literal(schema.const as number); 14 + return $.type.fromValue(schema.const); 15 15 } 16 16 17 17 if (schema.type === 'integer' && schema.format === 'int64') { ··· 22 22 } 23 23 24 24 return $.type('number'); 25 - }; 25 + }
+28 -56
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/object.ts
··· 1 - import { fromRef, ref } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 3 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import { ref } from '@hey-api/codegen-core'; 2 + import type { IR, SchemaWithType, Walker } from '@hey-api/shared'; 3 + import { deduplicateSchema } from '@hey-api/shared'; 4 4 5 5 import { createSchemaComment } from '../../../../../plugins/shared/utils/schema'; 6 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 7 6 import { $ } from '../../../../../ts-dsl'; 8 - import type { IrSchemaToAstOptions } from '../../shared/types'; 9 - import { irSchemaToAst } from '../plugin'; 7 + import type { HeyApiTypeScriptPlugin } from '../../shared/types'; 8 + import type { TypeScriptResult } from '../../shared/types'; 10 9 11 10 export function objectToAst({ 12 11 plugin, 13 12 schema, 14 - state, 15 - }: IrSchemaToAstOptions & { 13 + walk, 14 + }: { 15 + plugin: HeyApiTypeScriptPlugin['Instance']; 16 16 schema: SchemaWithType<'object'>; 17 - }): TypeTsDsl { 18 - // TODO: parser - handle constants 17 + walk: Walker<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']>; 18 + }): TypeScriptResult['type'] { 19 19 const shape = $.type.object(); 20 20 const required = schema.required ?? []; 21 21 let indexSchemas: Array<IR.SchemaObject> = []; ··· 23 23 24 24 for (const name in schema.properties) { 25 25 const property = schema.properties[name]!; 26 - const propertyType = irSchemaToAst({ 27 - plugin, 28 - schema: property, 29 - state: { 30 - ...state, 31 - path: ref([...fromRef(state.path), 'properties', name]), 32 - }, 33 - }); 26 + const propertyResult = walk(property, { path: ref([]), plugin }); 34 27 const isRequired = required.includes(name); 35 28 shape.prop(name, (p) => 36 29 p 37 30 .$if(plugin.config.comments && createSchemaComment(property), (p, v) => p.doc(v)) 38 31 .readonly(property.accessScope === 'read') 39 32 .required(isRequired) 40 - .type(propertyType), 33 + .type(propertyResult.type), 41 34 ); 42 35 indexSchemas.push(property); 43 36 ··· 46 39 } 47 40 } 48 41 49 - // include pattern value schemas into the index union 50 42 if (schema.patternProperties) { 51 43 for (const pattern in schema.patternProperties) { 52 44 const ir = schema.patternProperties[pattern]!; ··· 67 59 const addProps = addPropsObj; 68 60 if (addProps && addProps.type !== 'never') { 69 61 if (addProps.type === 'unknown') { 70 - // When additionalProperties is unknown (e.g. `{}` or `true`), it already subsumes all 71 - // named property types, so we only need the additionalProperties schema itself (plus any 72 - // patternProperties) in the index signature. Including named property types would produce 73 - // a redundant, noisy union like `unknown | string | null | ...`. 74 62 const patternSchemas: Array<IR.SchemaObject> = schema.patternProperties 75 63 ? Object.values(schema.patternProperties) 76 64 : []; 77 65 indexSchemas = [addProps, ...patternSchemas]; 78 66 } else { 79 - // For typed additionalProperties (e.g. `{ type: 'string' }`), named property types must 80 - // be included so that TypeScript's index signature constraint is satisfied. 81 67 indexSchemas.unshift(addProps); 82 68 } 83 69 } else if (!hasPatterns && !indexSchemas.length && addProps && addProps.type === 'never') { 84 - // keep "never" only when there are NO patterns and NO explicit properties 85 70 indexSchemas = [addProps]; 86 71 } 87 72 88 - // `unknown` already subsumes `undefined`, so no need to add it explicitly 89 73 if (hasOptionalProperties && addProps?.type !== 'unknown') { 90 74 indexSchemas.push({ type: 'undefined' }); 91 75 } 92 76 93 - const type = 94 - indexSchemas.length === 1 95 - ? irSchemaToAst({ 96 - plugin, 97 - schema: indexSchemas[0]!, 98 - state, 99 - }) 100 - : irSchemaToAst({ 101 - plugin, 102 - schema: { items: indexSchemas, logicalOperator: 'or' }, 103 - state, 104 - }); 77 + if (indexSchemas.length > 0) { 78 + const unionSchema: IR.SchemaObject = 79 + indexSchemas.length === 1 80 + ? indexSchemas[0]! 81 + : deduplicateSchema({ schema: { items: indexSchemas, logicalOperator: 'or' } }); 105 82 106 - if (schema.propertyNames?.$ref) { 107 - return $.type 108 - .mapped('key') 109 - .key( 110 - irSchemaToAst({ 111 - plugin, 112 - schema: { 113 - $ref: schema.propertyNames.$ref, 114 - }, 115 - state, 116 - }), 117 - ) 118 - .optional() 119 - .type(type); 83 + const indexType = walk(unionSchema, { path: ref([]), plugin }).type; 84 + 85 + if (schema.propertyNames?.$ref) { 86 + const propertyNamesResult = walk( 87 + { $ref: schema.propertyNames.$ref }, 88 + { path: ref([]), plugin }, 89 + ); 90 + return $.type.mapped('key').key(propertyNamesResult.type).optional().type(indexType); 91 + } 92 + 93 + shape.idxSig('key', (i) => i.key('string').type(indexType)); 120 94 } 121 - 122 - shape.idxSig('key', (i) => i.key('string').type(type)); 123 95 } 124 96 125 97 return shape;
+19 -20
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/string.ts
··· 2 2 import type { SchemaWithType } from '@hey-api/shared'; 3 3 import { toCase } from '@hey-api/shared'; 4 4 5 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 6 5 import { $ } from '../../../../../ts-dsl'; 7 - import type { IrSchemaToAstOptions } from '../../shared/types'; 6 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 8 7 9 - export const stringToAst = ({ 8 + export function stringToAst({ 10 9 plugin, 11 10 schema, 12 - }: IrSchemaToAstOptions & { 11 + }: { 12 + plugin: HeyApiTypeScriptPlugin['Instance']; 13 13 schema: SchemaWithType<'string'>; 14 - }): TypeTsDsl => { 14 + }): TypeScriptResult['type'] { 15 15 if (schema.const !== undefined) { 16 - return $.type.literal(schema.const as string); 16 + return $.type.fromValue(schema.const); 17 17 } 18 18 19 19 if (schema.format) { ··· 31 31 if (schema.format === 'typeid' && typeof schema.example === 'string') { 32 32 const parts = String(schema.example).split('_'); 33 33 parts.pop(); // remove the ID part 34 - const type = parts.join('_'); 34 + const typeidBase = parts.join('_'); 35 35 36 - const query: SymbolMeta = { 36 + const typeidQuery: SymbolMeta = { 37 37 category: 'type', 38 38 resource: 'type-id', 39 - resourceId: type, 39 + resourceId: typeidBase, 40 40 tool: 'typescript', 41 41 }; 42 - if (!plugin.getSymbol(query)) { 43 - const queryTypeId: SymbolMeta = { 42 + if (!plugin.getSymbol(typeidQuery)) { 43 + const containerQuery: SymbolMeta = { 44 44 category: 'type', 45 45 resource: 'type-id', 46 46 tool: 'typescript', 47 47 variant: 'container', 48 48 }; 49 49 50 - if (!plugin.getSymbol(queryTypeId)) { 50 + if (!plugin.getSymbol(containerQuery)) { 51 51 const symbolTypeId = plugin.symbol('TypeID', { 52 - meta: queryTypeId, 52 + meta: containerQuery, 53 53 }); 54 54 const nodeTypeId = $.type 55 55 .alias(symbolTypeId) ··· 59 59 plugin.node(nodeTypeId); 60 60 } 61 61 62 - const symbolTypeId = plugin.referenceSymbol(queryTypeId); 63 - const symbolTypeName = plugin.symbol(toCase(`${type}_id`, plugin.config.case), { 64 - meta: query, 62 + const refSymbol = plugin.referenceSymbol(containerQuery); 63 + const symbolTypeName = plugin.symbol(toCase(`${typeidBase}_id`, plugin.config.case), { 64 + meta: typeidQuery, 65 65 }); 66 66 const node = $.type 67 67 .alias(symbolTypeName) 68 68 .export() 69 - .type($.type(symbolTypeId).generic($.type.literal(type))); 69 + .type($.type(refSymbol).generic($.type.literal(typeidBase))); 70 70 plugin.node(node); 71 71 } 72 - const symbol = plugin.referenceSymbol(query); 73 - return $.type(symbol); 72 + return $.type(plugin.referenceSymbol(typeidQuery)); 74 73 } 75 74 } 76 75 77 76 return $.type('string'); 78 - }; 77 + }
+13 -18
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/tuple.ts
··· 1 - import { fromRef, ref } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { SchemaWithType } from '@hey-api/shared'; 3 + import type { Walker } from '@hey-api/shared'; 3 4 4 - import type { MaybeTsDsl, TypeTsDsl } from '../../../../../ts-dsl'; 5 5 import { $ } from '../../../../../ts-dsl'; 6 - import type { IrSchemaToAstOptions } from '../../shared/types'; 7 - import { irSchemaToAst } from '../plugin'; 6 + import type { HeyApiTypeScriptPlugin } from '../../shared/types'; 7 + import type { TypeScriptResult } from '../../shared/types'; 8 8 9 9 export function tupleToAst({ 10 10 plugin, 11 11 schema, 12 - state, 13 - }: IrSchemaToAstOptions & { 12 + walk, 13 + }: { 14 + plugin: HeyApiTypeScriptPlugin['Instance']; 14 15 schema: SchemaWithType<'tuple'>; 15 - }): MaybeTsDsl<TypeTsDsl> { 16 - let itemTypes: Array<MaybeTsDsl<TypeTsDsl>> = []; 16 + walk: Walker<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']>; 17 + }): TypeScriptResult['type'] { 18 + let itemTypes: Array<TypeScriptResult['type']> = []; 17 19 18 20 if (schema.const && Array.isArray(schema.const)) { 19 21 itemTypes = schema.const.map((value) => $.type.fromValue(value)); 20 22 } else if (schema.items) { 21 - schema.items.forEach((item, index) => { 22 - const type = irSchemaToAst({ 23 - plugin, 24 - schema: item, 25 - state: { 26 - ...state, 27 - path: ref([...fromRef(state.path), 'items', index]), 28 - }, 29 - }); 30 - itemTypes.push(type); 23 + schema.items.forEach((item) => { 24 + const result = walk(item, { path: ref([]), plugin }); 25 + itemTypes.push(result.type); 31 26 }); 32 27 } 33 28
+8 -11
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/undefined.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 - export const undefinedToAst = ( 8 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 - _args: IrSchemaToAstOptions & { 10 - schema: SchemaWithType<'undefined'>; 11 - }, 12 - ): TypeTsDsl => { 13 - const node = $.type('undefined'); 14 - return node; 15 - }; 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function undefinedToAst(args: { 8 + plugin: HeyApiTypeScriptPlugin['Instance']; 9 + schema: SchemaWithType<'undefined'>; 10 + }): TypeScriptResult['type'] { 11 + return $.type('undefined'); 12 + }
+5 -6
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/unknown.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 6 export function unknownToAst({ 8 7 plugin, 9 - }: IrSchemaToAstOptions & { 8 + }: { 9 + plugin: HeyApiTypeScriptPlugin['Instance']; 10 10 schema: SchemaWithType<'unknown'>; 11 - }): TypeTsDsl { 12 - const node = $.type(plugin.config.topType); 13 - return node; 11 + }): TypeScriptResult['type'] { 12 + return $.type(plugin.config.topType); 14 13 }
+8 -11
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/void.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import type { TypeTsDsl } from '../../../../../ts-dsl'; 4 3 import { $ } from '../../../../../ts-dsl'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 4 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types'; 6 5 7 - export const voidToAst = ( 8 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 - _args: IrSchemaToAstOptions & { 10 - schema: SchemaWithType<'void'>; 11 - }, 12 - ): TypeTsDsl => { 13 - const node = $.type('void'); 14 - return node; 15 - }; 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function voidToAst(args: { 8 + plugin: HeyApiTypeScriptPlugin['Instance']; 9 + schema: SchemaWithType<'void'>; 10 + }): TypeScriptResult['type'] { 11 + return $.type('void'); 12 + }
+214
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/walker.ts
··· 1 + import { fromRef } from '@hey-api/codegen-core'; 2 + import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 3 + import { pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { $ } from '../../../../ts-dsl'; 6 + import { defaultMeta, inheritMeta } from '../shared/meta'; 7 + import type { ProcessorContext } from '../shared/processor'; 8 + import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../shared/types'; 9 + import { arrayToAst } from './toAst/array'; 10 + import { booleanToAst } from './toAst/boolean'; 11 + import { enumToAst } from './toAst/enum'; 12 + import { neverToAst } from './toAst/never'; 13 + import { nullToAst } from './toAst/null'; 14 + import { numberToAst } from './toAst/number'; 15 + import { objectToAst } from './toAst/object'; 16 + import { stringToAst } from './toAst/string'; 17 + import { tupleToAst } from './toAst/tuple'; 18 + import { undefinedToAst } from './toAst/undefined'; 19 + import { unknownToAst } from './toAst/unknown'; 20 + import { voidToAst } from './toAst/void'; 21 + 22 + export interface VisitorConfig { 23 + /** Optional schema extractor function. */ 24 + schemaExtractor?: SchemaExtractor<ProcessorContext>; 25 + } 26 + 27 + export function createVisitor( 28 + config: VisitorConfig, 29 + ): SchemaVisitor<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']> { 30 + const { schemaExtractor } = config; 31 + 32 + return { 33 + applyModifiers(result) { 34 + return { 35 + enumData: result.enumData, 36 + type: result.type, 37 + }; 38 + }, 39 + array(schema, ctx, walk) { 40 + const type = arrayToAst({ 41 + plugin: ctx.plugin, 42 + schema, 43 + walk, 44 + }); 45 + return { 46 + meta: defaultMeta(schema), 47 + type, 48 + }; 49 + }, 50 + boolean(schema, ctx) { 51 + const type = booleanToAst({ plugin: ctx.plugin, schema }); 52 + return { 53 + meta: defaultMeta(schema), 54 + type, 55 + }; 56 + }, 57 + enum(schema, ctx) { 58 + const { enumData, type } = enumToAst({ plugin: ctx.plugin, schema }); 59 + return { 60 + enumData, 61 + meta: defaultMeta(schema), 62 + type, 63 + }; 64 + }, 65 + integer(schema, ctx) { 66 + const type = numberToAst({ plugin: ctx.plugin, schema }); 67 + return { 68 + meta: defaultMeta(schema), 69 + type, 70 + }; 71 + }, 72 + intercept(schema, ctx, walk) { 73 + if (schemaExtractor && !schema.$ref) { 74 + const extracted = schemaExtractor({ 75 + meta: { 76 + resource: 'definition', 77 + resourceId: pathToJsonPointer(fromRef(ctx.path)), 78 + }, 79 + naming: ctx.plugin.config.definitions, 80 + path: fromRef(ctx.path), 81 + plugin: ctx.plugin, 82 + schema, 83 + }); 84 + 85 + if (extracted !== schema) { 86 + return walk(extracted, ctx); 87 + } 88 + } 89 + 90 + const transformersPlugin = ctx.plugin.getPlugin('@hey-api/transformers'); 91 + if (transformersPlugin?.config.typeTransformers) { 92 + for (const typeTransformer of transformersPlugin.config.typeTransformers) { 93 + const typeNode = typeTransformer({ schema }); 94 + if (typeNode) { 95 + return { meta: defaultMeta(schema), type: typeNode }; 96 + } 97 + } 98 + } 99 + }, 100 + intersection(items, schemas, parentSchema) { 101 + const type = items.length === 1 ? items[0]!.type : $.type.and(...items.map((r) => r.type)); 102 + 103 + return { 104 + meta: inheritMeta(parentSchema, items), 105 + type, 106 + }; 107 + }, 108 + never(schema, ctx) { 109 + const type = neverToAst({ plugin: ctx.plugin, schema }); 110 + return { 111 + meta: defaultMeta(schema), 112 + type, 113 + }; 114 + }, 115 + null(schema, ctx) { 116 + const type = nullToAst({ plugin: ctx.plugin, schema }); 117 + return { 118 + meta: defaultMeta(schema), 119 + type, 120 + }; 121 + }, 122 + number(schema, ctx) { 123 + const type = numberToAst({ plugin: ctx.plugin, schema }); 124 + return { 125 + meta: defaultMeta(schema), 126 + type, 127 + }; 128 + }, 129 + object(schema, ctx, walk) { 130 + const type = objectToAst({ 131 + plugin: ctx.plugin, 132 + schema, 133 + walk, 134 + }); 135 + return { 136 + meta: defaultMeta(schema), 137 + type, 138 + }; 139 + }, 140 + postProcess(result) { 141 + return result; 142 + }, 143 + reference($ref, schema, ctx) { 144 + const symbol = ctx.plugin.referenceSymbol({ 145 + category: 'type', 146 + resource: 'definition', 147 + resourceId: $ref, 148 + }); 149 + 150 + if (schema.omit && schema.omit.length > 0) { 151 + const omittedKeys = 152 + schema.omit.length === 1 153 + ? $.type.literal(schema.omit[0]!) 154 + : $.type.or(...schema.omit.map((key) => $.type.literal(key))); 155 + return { 156 + meta: defaultMeta(schema), 157 + type: $.type('Omit').generics($.type(symbol), omittedKeys), 158 + }; 159 + } 160 + 161 + return { 162 + meta: defaultMeta(schema), 163 + type: $.type(symbol), 164 + }; 165 + }, 166 + string(schema, ctx) { 167 + const type = stringToAst({ plugin: ctx.plugin, schema }); 168 + return { 169 + meta: defaultMeta(schema), 170 + type, 171 + }; 172 + }, 173 + tuple(schema, ctx, walk) { 174 + const type = tupleToAst({ 175 + plugin: ctx.plugin, 176 + schema, 177 + walk, 178 + }); 179 + return { 180 + meta: defaultMeta(schema), 181 + type, 182 + }; 183 + }, 184 + undefined(schema, ctx) { 185 + const type = undefinedToAst({ plugin: ctx.plugin, schema }); 186 + return { 187 + meta: defaultMeta(schema), 188 + type, 189 + }; 190 + }, 191 + union(items, schemas, parentSchema) { 192 + const type = items.length === 1 ? items[0]!.type : $.type.or(...items.map((r) => r.type)); 193 + 194 + return { 195 + meta: inheritMeta(parentSchema, items), 196 + type, 197 + }; 198 + }, 199 + unknown(schema, ctx) { 200 + const type = unknownToAst({ plugin: ctx.plugin, schema }); 201 + return { 202 + meta: defaultMeta(schema), 203 + type, 204 + }; 205 + }, 206 + void(schema, ctx) { 207 + const type = voidToAst({ plugin: ctx.plugin, schema }); 208 + return { 209 + meta: defaultMeta(schema), 210 + type, 211 + }; 212 + }, 213 + }; 214 + }
+1 -8
packages/openapi-ts/src/plugins/@tanstack/query-core/v5/infiniteQueryOptions.ts
··· 1 - import { ref } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 2 import { applyNaming, operationPagination } from '@hey-api/shared'; 4 3 ··· 156 155 ), 157 156 ); 158 157 const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); 159 - const type = pluginTypeScript.api.schemaToType({ 160 - plugin: pluginTypeScript, 161 - schema: pagination.schema, 162 - state: { 163 - path: ref([]), 164 - }, 165 - }); 158 + const type = pluginTypeScript.api.schemaToType(pluginTypeScript, pagination.schema); 166 159 167 160 const symbolInfiniteQueryKey = plugin.symbol( 168 161 applyNaming(operation.id, plugin.config.infiniteQueryKeys),
-2
packages/openapi-ts/src/plugins/valibot/shared/meta.ts
··· 18 18 /** 19 19 * Composes metadata from child results. 20 20 * 21 - * Automatically propagates hasLazy, nullable, readonly from children. 22 - * 23 21 * @param children - Results from walking child schemas 24 22 * @param overrides - Explicit overrides (e.g., from parent schema) 25 23 */
+7 -7
packages/openapi-ts/src/plugins/valibot/shared/processor.ts
··· 1 - import type { 2 - IR, 3 - NamingConfig, 4 - SchemaProcessorContext, 5 - SchemaProcessorResult, 6 - } from '@hey-api/shared'; 1 + import type { IR, NamingConfig, SchemaProcessorContext } from '@hey-api/shared'; 7 2 8 3 import type { ValibotPlugin } from '../types'; 4 + import type { ValibotFinal } from './types'; 9 5 10 6 export type ProcessorContext = SchemaProcessorContext & { 7 + /** Whether to export the result (default: true) */ 8 + export?: boolean; 11 9 naming: NamingConfig; 12 10 /** The plugin instance. */ 13 11 plugin: ValibotPlugin['Instance']; 14 12 schema: IR.SchemaObject; 15 13 }; 16 14 17 - export type ProcessorResult = SchemaProcessorResult<ProcessorContext>; 15 + export type ProcessorResult = { 16 + process: (ctx: ProcessorContext) => ValibotFinal | void; 17 + };
+1 -2
packages/openapi-ts/src/plugins/valibot/shared/types.ts
··· 37 37 /** 38 38 * Finalized result after applyModifiers. 39 39 */ 40 - export interface ValibotFinal { 41 - pipes: Pipes; 40 + export interface ValibotFinal extends Pick<ValibotResult, 'pipes'> { 42 41 /** Type annotation for schemas requiring explicit typing (e.g., lazy). */ 43 42 typeName?: string | ts.Identifier; 44 43 }
+10 -3
packages/openapi-ts/src/plugins/valibot/v1/processor.ts
··· 36 36 return ctx.schema; 37 37 } 38 38 39 - function process(ctx: ProcessorContext): void { 39 + function process(ctx: ProcessorContext): ValibotFinal | void { 40 40 if (!processor.markEmitted(ctx.path)) return; 41 41 42 - processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 42 + const shouldExport = ctx.export !== false; 43 + 44 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 43 45 const visitor = createVisitor({ schemaExtractor: extractor }); 44 46 const walk = createSchemaWalker(visitor); 45 47 ··· 53 55 plugin, 54 56 }) as ValibotFinal; 55 57 56 - exportAst({ ...ctx, final, plugin }); 58 + if (shouldExport) { 59 + exportAst({ ...ctx, final, plugin }); 60 + return; 61 + } 62 + 63 + return final; 57 64 }); 58 65 } 59 66
+1 -1
packages/openapi-ts/src/plugins/zod/mini/processor.ts
··· 36 36 function process(ctx: ProcessorContext): void { 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 - processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 39 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 40 const state = refs<PluginState>({ 41 41 hasLazyExpression: false, 42 42 path: ctx.path,
+1 -1
packages/openapi-ts/src/plugins/zod/v3/processor.ts
··· 36 36 function process(ctx: ProcessorContext): void { 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 - processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 39 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 40 const state = refs<PluginState>({ 41 41 hasLazyExpression: false, 42 42 path: ctx.path,
+1 -1
packages/openapi-ts/src/plugins/zod/v4/processor.ts
··· 36 36 function process(ctx: ProcessorContext): void { 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 - processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 39 + return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 40 const state = refs<PluginState>({ 41 41 hasLazyExpression: false, 42 42 path: ctx.path,
+6 -1
packages/openapi-ts/src/ts-dsl/type/fromValue.ts
··· 15 15 return new TypeLiteralTsDsl(input); 16 16 } 17 17 18 - if (typeof input === 'number' || typeof input === 'boolean' || typeof input === 'string') { 18 + if ( 19 + typeof input === 'number' || 20 + typeof input === 'boolean' || 21 + typeof input === 'string' || 22 + typeof input === 'bigint' 23 + ) { 19 24 return new TypeLiteralTsDsl(input); 20 25 } 21 26
+3 -3
packages/openapi-ts/src/ts-dsl/type/literal.ts
··· 2 2 import ts from 'typescript'; 3 3 4 4 import { TsDsl } from '../base'; 5 - import { LiteralTsDsl } from '../expr/literal'; 5 + import { LiteralTsDsl, type LiteralValue } from '../expr/literal'; 6 6 7 7 const Mixed = TsDsl<ts.LiteralTypeNode>; 8 8 ··· 10 10 readonly '~dsl' = 'TypeLiteralTsDsl'; 11 11 override scope: NodeScope = 'type'; 12 12 13 - protected value: string | number | boolean | null; 13 + protected value: LiteralValue; 14 14 15 - constructor(value: string | number | boolean | null) { 15 + constructor(value: LiteralValue) { 16 16 super(); 17 17 this.value = value; 18 18 }