fork of hey-api/openapi-ts because I need some additional things
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}