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 throw new Error(`Request failed with status ${response.status}: ${response.statusText}`);
70 }
71
72 return { arrayBuffer, resolvedInput };
73 };
74 const specData = (
75 await Promise.all(config.input.map((input, index) => getSpecData(input, index)))
76 ).filter((data) => data.arrayBuffer || data.resolvedInput);
77
78 let context: Context | undefined;
79
80 if (specData.length) {
81 const refParser = new $RefParser();
82 const data =
83 specData.length > 1
84 ? await refParser.bundleMany({
85 arrayBuffer: specData.map((data) => data.arrayBuffer!),
86 pathOrUrlOrSchemas: [],
87 resolvedInputs: specData.map((data) => data.resolvedInput!),
88 })
89 : await refParser.bundle({
90 arrayBuffer: specData[0]!.arrayBuffer,
91 pathOrUrlOrSchema: undefined,
92 resolvedInput: specData[0]!.resolvedInput!,
93 });
94
95 // on subsequent runs in watch mode, print the message only if we know we're
96 // generating the output
97 if (config.logs.level !== 'silent' && _watches) {
98 console.clear();
99 logInputPaths(inputPaths, jobIndex);
100 }
101
102 const eventInputPatch = logger.timeEvent('input.patch');
103 await patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data });
104 eventInputPatch.timeEnd();
105
106 const eventParser = logger.timeEvent('parser');
107 // TODO: allow overriding via config
108 const project = new Project({
109 defaultFileName: '__init__',
110 fileName: (base) => {
111 const name = applyNaming(base, config.output.fileName);
112 const { suffix } = config.output.fileName;
113 if (!suffix) {
114 return name;
115 }
116 return name === '__init__' || name.endsWith(suffix) ? name : `${name}${suffix}`;
117 },
118 nameConflictResolvers: config.output.nameConflictResolver
119 ? {
120 typescript: config.output.nameConflictResolver,
121 }
122 : undefined,
123 renderers: [
124 new PythonRenderer({
125 header: config.output.header,
126 preferExportAll: config.output.preferExportAll,
127 preferFileExtension: config.output.importFileExtension || undefined,
128 resolveModuleName: config.output.resolveModuleName,
129 }),
130 ],
131 root: config.output.path,
132 });
133 context = new Context<OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X, Config>({
134 config,
135 dependencies,
136 logger,
137 project,
138 spec: data as OpenApi.V2_0_X | OpenApi.V3_0_X | OpenApi.V3_1_X,
139 });
140 parseOpenApiSpec(context);
141 context.graph = buildGraph(context.ir, logger).graph;
142 eventParser.timeEnd();
143
144 const eventGenerator = logger.timeEvent('generator');
145 await generateOutput(context);
146 eventGenerator.timeEnd();
147
148 const eventPostprocess = logger.timeEvent('postprocess');
149 if (!config.dryRun) {
150 const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `);
151 postprocessOutput(config.output, postProcessors, jobPrefix);
152
153 if (config.logs.level !== 'silent') {
154 const outputPath = process.env.INIT_CWD
155 ? `./${path.relative(process.env.INIT_CWD, config.output.path)}`
156 : config.output.path;
157 console.log(
158 `${jobPrefix}${colors.green('✅ Done!')} Your output is in ${colors.cyanBright(outputPath)}`,
159 );
160 }
161 }
162 eventPostprocess.timeEnd();
163 }
164
165 const watchedInput = config.input.find(
166 (input, index) => input.watch.enabled && typeof inputPaths[index]!.path === 'string',
167 );
168
169 if (watchedInput) {
170 setTimeout(() => {
171 createClient({
172 config,
173 dependencies,
174 jobIndex,
175 logger,
176 watches,
177 });
178 }, watchedInput.watch.interval);
179 }
180
181 return context;
182}