fork of hey-api/openapi-ts because I need some additional things
1import fs from 'node:fs';
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
4
5import type { IProject } from '@hey-api/codegen-core';
6import type { DefinePlugin, OutputHeader } from '@hey-api/shared';
7import { ensureDirSync, isEnvironment, outputHeaderToPrefix } from '@hey-api/shared';
8
9import type { Config } from '../config/types';
10import type { Client } from '../plugins/@hey-api/client-core/types';
11import { getClientPlugin } from '../plugins/@hey-api/client-core/utils';
12
13const __filename = fileURLToPath(import.meta.url);
14const __dirname = path.dirname(__filename);
15
16/**
17 * Returns paths to client bundle files based on execution context
18 */
19function getClientBundlePaths(pluginName: string): {
20 clientPath: string;
21 corePath: string;
22} {
23 const clientName = pluginName.slice('@hey-api/client-'.length);
24
25 if (isEnvironment('development')) {
26 // Dev: source bundle folders at src/plugins/@hey-api/{client}/bundle
27 const pluginsDir = path.resolve(__dirname, '..', 'plugins', '@hey-api');
28 return {
29 clientPath: path.resolve(pluginsDir, `client-${clientName}`, 'bundle'),
30 corePath: path.resolve(pluginsDir, 'client-core', 'bundle'),
31 };
32 }
33
34 // Prod: copied to dist/clients/{clientName}
35 return {
36 clientPath: path.resolve(__dirname, 'clients', clientName),
37 corePath: path.resolve(__dirname, 'clients', 'core'),
38 };
39}
40
41/**
42 * Returns absolute path to the client folder. This is hard-coded for now.
43 */
44export function clientFolderAbsolutePath(config: Config): string {
45 const client = getClientPlugin(config);
46
47 if ('bundle' in client.config && client.config.bundle) {
48 // not proud of this one
49 const renamed: Map<string, string> | undefined =
50 // @ts-expect-error
51 config._FRAGILE_CLIENT_BUNDLE_RENAMED;
52 return path.resolve(config.output.path, 'client', `${renamed?.get('index') ?? 'index'}.py`);
53 }
54
55 return client.name;
56}
57
58/**
59 * Recursively copies files and directories.
60 * This is a PnP-compatible alternative to fs.cpSync that works with Yarn PnP's
61 * virtualized filesystem.
62 */
63function copyRecursivePnP(src: string, dest: string): void {
64 const stat = fs.statSync(src);
65
66 if (stat.isDirectory()) {
67 if (!fs.existsSync(dest)) {
68 fs.mkdirSync(dest, { recursive: true });
69 }
70
71 const files = fs.readdirSync(src);
72 for (const file of files) {
73 copyRecursivePnP(path.join(src, file), path.join(dest, file));
74 }
75 } else {
76 const content = fs.readFileSync(src);
77 fs.writeFileSync(dest, content);
78 }
79}
80
81function renameFile({
82 filePath,
83 project,
84 renamed,
85}: {
86 filePath: string;
87 project: IProject;
88 renamed: Map<string, string>;
89}): void {
90 const extension = path.extname(filePath);
91 const name = path.basename(filePath, extension);
92 const renamedName = project.fileName?.(name) || name;
93 if (renamedName !== name) {
94 const outputPath = path.dirname(filePath);
95 fs.renameSync(filePath, path.resolve(outputPath, `${renamedName}${extension}`));
96 renamed.set(name, renamedName);
97 }
98}
99
100function replaceImports({
101 filePath,
102 header,
103 renamed,
104}: {
105 filePath: string;
106 header?: string;
107 renamed: Map<string, string>;
108}): void {
109 let content = fs.readFileSync(filePath, 'utf8');
110
111 // Dev mode: rewrite source bundle imports to match output structure
112 if (isEnvironment('development')) {
113 // ...client_core.bundle.foo -> ..core.foo
114 content = content.replace(
115 /from\s+(\.{3,})\.?client_core\.bundle\./g,
116 (match, dots) => `from ${dots === '...' ? '..' : dots.slice(0, -1)}core.`,
117 );
118 // ...client_core.bundle (index import) -> ..core
119 content = content.replace(
120 /from\s+(\.{3,})\.?client_core\.bundle['"]?/g,
121 (match, dots) => `from ${dots === '...' ? '..' : dots.slice(0, -1)}core`,
122 );
123 }
124
125 content = content.replace(/from\s+(.+?)\s+import/g, (match, importPath) => {
126 const cleanPath = importPath.replace(/^['"]|['"]$/g, '');
127 if (!cleanPath.startsWith('.')) return match;
128 const leadingDots = cleanPath.match(/^\.+/)?.[0] || '';
129 const moduleName = cleanPath.replace(/^\.+/, '');
130 if (!moduleName) return match;
131 const replacedName = renamed.get(moduleName) ?? moduleName;
132 return `from ${leadingDots}${replacedName} import`;
133 });
134
135 const fileHeader = header ?? '';
136
137 content = `${fileHeader}${content}`;
138
139 fs.writeFileSync(filePath, content, 'utf8');
140}
141
142/**
143 * Creates a `client` folder containing the same modules as the client package.
144 */
145export function generateClientBundle({
146 header,
147 outputPath,
148 plugin,
149 project,
150}: {
151 header?: OutputHeader;
152 outputPath: string;
153 plugin: DefinePlugin<Client.Config & { name: string }>['Config'];
154 project: IProject;
155}): Map<string, string> | undefined {
156 const renamed = new Map<string, string>();
157 const headerPrefix = outputHeaderToPrefix({
158 defaultValue: ['# This file is auto-generated by @hey-api/openapi-python'],
159 header,
160 project,
161 });
162
163 // copy Hey API clients to output
164 const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-');
165 if (isHeyApiClientPlugin) {
166 const { clientPath } = getClientBundlePaths(plugin.name);
167 // const { clientPath, corePath } = getClientBundlePaths(plugin.name);
168
169 // copy client core
170 // const coreOutputPath = path.resolve(outputPath, 'core');
171 // ensureDirSync(coreOutputPath);
172 // copyRecursivePnP(corePath, coreOutputPath);
173
174 // copy client bundle
175 const clientOutputPath = path.resolve(outputPath, 'client');
176 ensureDirSync(clientOutputPath);
177 copyRecursivePnP(clientPath, clientOutputPath);
178
179 if (project) {
180 // const copiedCoreFiles = fs.readdirSync(coreOutputPath);
181 // for (const file of copiedCoreFiles) {
182 // renameFile({
183 // filePath: path.resolve(coreOutputPath, file),
184 // project,
185 // renamed,
186 // });
187 // }
188
189 const copiedClientFiles = fs.readdirSync(clientOutputPath);
190 for (const file of copiedClientFiles) {
191 renameFile({
192 filePath: path.resolve(clientOutputPath, file),
193 project,
194 renamed,
195 });
196 }
197 }
198
199 // const coreFiles = fs.readdirSync(coreOutputPath);
200 // for (const file of coreFiles) {
201 // replaceImports({
202 // filePath: path.resolve(coreOutputPath, file),
203 // renamed,
204 // });
205 // }
206
207 const clientFiles = fs.readdirSync(clientOutputPath);
208 for (const file of clientFiles) {
209 replaceImports({
210 filePath: path.resolve(clientOutputPath, file),
211 header: headerPrefix,
212 renamed,
213 });
214 }
215 return renamed;
216 }
217
218 const clientSrcPath = path.isAbsolute(plugin.name) ? path.dirname(plugin.name) : undefined;
219
220 // copy custom local client to output
221 if (clientSrcPath) {
222 const dirPath = path.resolve(outputPath, 'client');
223 ensureDirSync(dirPath);
224 copyRecursivePnP(clientSrcPath, dirPath);
225 return;
226 }
227
228 // copy third-party client to output
229 const clientModulePath = path.normalize(require.resolve(plugin.name));
230 const clientModulePathComponents = clientModulePath.split(path.sep);
231 const clientDistPath = clientModulePathComponents
232 .slice(0, clientModulePathComponents.indexOf('dist') + 1)
233 .join(path.sep);
234
235 const indexJsFile = clientModulePathComponents[clientModulePathComponents.length - 1];
236 const distFiles = [indexJsFile!, 'index.d.mts', 'index.d.cts'];
237 const dirPath = path.resolve(outputPath, 'client');
238 ensureDirSync(dirPath);
239 for (const file of distFiles) {
240 fs.copyFileSync(path.resolve(clientDistPath, file), path.resolve(dirPath, file));
241 }
242
243 return;
244}