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.

at feat/use-query-options 206 lines 6.8 kB view raw
1import path from 'node:path'; 2 3import { type Logger, Project } from '@hey-api/codegen-core'; 4import { $RefParser, ResolverError } from '@hey-api/json-schema-ref-parser'; 5import type { Input, OpenApi, WatchValues } from '@hey-api/shared'; 6import { 7 applyNaming, 8 buildGraph, 9 compileInputPath, 10 Context, 11 getSpec, 12 InputError, 13 logInputPaths, 14 parseOpenApiSpec, 15 patchOpenApiSpec, 16 postprocessOutput, 17} from '@hey-api/shared'; 18import colors from 'ansi-colors'; 19 20import { postProcessors } from './config/output/postprocess'; 21import type { Config } from './config/types'; 22import { generateOutput } from './generate/output'; 23import { PythonRenderer } from './py-dsl'; 24 25export async function createClient({ 26 config, 27 dependencies, 28 jobIndex, 29 logger, 30 watches: _watches, 31}: { 32 config: Config; 33 dependencies: Record<string, string>; 34 jobIndex: number; 35 logger: Logger; 36 /** 37 * Always undefined on the first run, defined on subsequent runs. 38 */ 39 watches?: ReadonlyArray<WatchValues>; 40}): Promise<Context | undefined> { 41 const watches: ReadonlyArray<WatchValues> = 42 _watches || 43 Array.from({ length: config.input.length }, () => ({ 44 headers: new Headers(), 45 })); 46 47 const inputPaths = config.input.map((input) => compileInputPath(input)); 48 49 // on first run, print the message as soon as possible 50 if (config.logs.level !== 'silent' && !_watches) { 51 logInputPaths(inputPaths, jobIndex); 52 } 53 54 const getSpecData = async (input: Input, index: number) => { 55 const eventSpec = logger.timeEvent('spec'); 56 const { arrayBuffer, error, resolvedInput, response } = await getSpec({ 57 fetchOptions: input.fetch, 58 inputPath: inputPaths[index]!.path, 59 timeout: input.watch.timeout, 60 watch: watches[index]!, 61 }); 62 eventSpec.timeEnd(); 63 64 // throw on first run if there's an error to preserve user experience 65 // if in watch mode, subsequent errors won't throw to gracefully handle 66 // cases where server might be reloading 67 if (error && !_watches) { 68 const text = await response.text().catch((): string => ''); 69 const message = `Request failed with status ${response.status}: ${text || response.statusText}`; 70 // Handle 4xx client errors as input errors (bad URL, bad API key, etc.) 71 if (response.status >= 400 && response.status < 500) { 72 const statusText = response.statusText || 'Unknown'; 73 const originalError = new Error(message) as Error & { source?: string }; 74 originalError.source = String(inputPaths[index]!.path); 75 throw new InputError( 76 `Input request failed: ${response.status} ${statusText}`, 77 originalError, 78 ); 79 } 80 // For 5xx server errors, keep the generic error (could be a bug) 81 throw new Error(message); 82 } 83 84 return { arrayBuffer, resolvedInput }; 85 }; 86 const specData = ( 87 await Promise.all(config.input.map((input, index) => getSpecData(input, index))) 88 ).filter((data) => data.arrayBuffer || data.resolvedInput); 89 90 let context: Context | undefined; 91 92 if (specData.length) { 93 const refParser = new $RefParser(); 94 let data: unknown; 95 try { 96 data = 97 specData.length > 1 98 ? await refParser.bundleMany({ 99 arrayBuffer: specData.map((data) => data.arrayBuffer!), 100 pathOrUrlOrSchemas: [], 101 resolvedInputs: specData.map((data) => data.resolvedInput!), 102 }) 103 : await refParser.bundle({ 104 arrayBuffer: specData[0]!.arrayBuffer, 105 pathOrUrlOrSchema: undefined, 106 resolvedInput: specData[0]!.resolvedInput!, 107 }); 108 } catch (err) { 109 if (err instanceof ResolverError && err.ioErrorCode === 'ENOENT') { 110 throw new InputError('Input file not found', err); 111 } 112 throw err; 113 } 114 115 // on subsequent runs in watch mode, print the message only if we know we're 116 // generating the output 117 if (config.logs.level !== 'silent' && _watches) { 118 console.clear(); 119 logInputPaths(inputPaths, jobIndex); 120 } 121 122 const eventInputPatch = logger.timeEvent('input.patch'); 123 await patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); 124 eventInputPatch.timeEnd(); 125 126 const eventParser = logger.timeEvent('parser'); 127 const header = config.output.header; 128 // TODO: allow overriding via config 129 const project = new Project({ 130 defaultFileName: '__init__', 131 fileName: (base) => { 132 const name = applyNaming(base, config.output.fileName); 133 const { suffix } = config.output.fileName; 134 if (!suffix) { 135 return name; 136 } 137 return name === '__init__' || name.endsWith(suffix) ? name : `${name}${suffix}`; 138 }, 139 nameConflictResolvers: config.output.nameConflictResolver 140 ? { 141 python: config.output.nameConflictResolver, 142 } 143 : undefined, 144 renderers: [ 145 new PythonRenderer({ 146 header: (ctx) => { 147 const defaultValue = ['# This file is auto-generated by @hey-api/openapi-python']; 148 const result = typeof header === 'function' ? header({ ...ctx, defaultValue }) : header; 149 return result === undefined ? defaultValue : result; 150 }, 151 module: config.output.module, 152 preferExportAll: config.output.preferExportAll, 153 }), 154 ], 155 root: config.output.path, 156 }); 157 context = new Context<OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, Config>({ 158 config, 159 dependencies, 160 logger, 161 project, 162 spec: data as OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, 163 }); 164 parseOpenApiSpec(context); 165 context.graph = buildGraph(context.ir, logger).graph; 166 eventParser.timeEnd(); 167 168 const eventGenerator = logger.timeEvent('generator'); 169 await generateOutput(context); 170 eventGenerator.timeEnd(); 171 172 const eventPostprocess = logger.timeEvent('postprocess'); 173 if (!config.dryRun) { 174 const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); 175 postprocessOutput(config.output, postProcessors, jobPrefix); 176 177 if (config.logs.level !== 'silent') { 178 const outputPath = process.env.INIT_CWD 179 ? `./${path.relative(process.env.INIT_CWD, config.output.path)}` 180 : config.output.path; 181 console.log( 182 `${jobPrefix}${colors.green('✅ Done!')} Your output is in ${colors.cyanBright(outputPath)}`, 183 ); 184 } 185 } 186 eventPostprocess.timeEnd(); 187 } 188 189 const watchedInput = config.input.find( 190 (input, index) => input.watch.enabled && typeof inputPaths[index]!.path === 'string', 191 ); 192 193 if (watchedInput) { 194 setTimeout(() => { 195 createClient({ 196 config, 197 dependencies, 198 jobIndex, 199 logger, 200 watches, 201 }); 202 }, watchedInput.watch.interval); 203 } 204 205 return context; 206}