fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 259 lines 8.0 kB view raw
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}