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