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