import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core'; import type { DefinePlugin } from '@hey-api/shared'; import { ensureDirSync } from '@hey-api/shared'; import type { Config } from '../config/types'; import type { Client } from '../plugins/@hey-api/client-core/types'; import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Dev mode: 'src' appears after 'dist' (or dist doesn't exist), and 'generate' follows 'src' */ function isDevMode(): boolean { const normalized = __dirname.split(path.sep); const srcIndex = normalized.lastIndexOf('src'); const distIndex = normalized.lastIndexOf('dist'); return ( srcIndex !== -1 && srcIndex > distIndex && srcIndex === normalized.length - 2 && normalized[srcIndex + 1] === 'generate' ); } /** * Returns paths to client bundle files based on execution context */ function getClientBundlePaths(pluginName: string): { clientPath: string; corePath: string; } { const clientName = pluginName.slice('@hey-api/client-'.length); if (isDevMode()) { // Dev: source bundle folders at src/plugins/@hey-api/{client}/bundle const pluginsDir = path.resolve(__dirname, '..', 'plugins', '@hey-api'); return { clientPath: path.resolve(pluginsDir, `client-${clientName}`, 'bundle'), corePath: path.resolve(pluginsDir, 'client-core', 'bundle'), }; } // Prod: copied to dist/clients/{clientName} return { clientPath: path.resolve(__dirname, 'clients', clientName), corePath: path.resolve(__dirname, 'clients', 'core'), }; } /** * Returns absolute path to the client folder. This is hard-coded for now. */ export function clientFolderAbsolutePath(config: Config): string { const client = getClientPlugin(config); if ('bundle' in client.config && client.config.bundle) { // not proud of this one const renamed: Map | undefined = // @ts-expect-error config._FRAGILE_CLIENT_BUNDLE_RENAMED; return path.resolve(config.output.path, 'client', `${renamed?.get('index') ?? 'index'}.py`); } return client.name; } /** * Recursively copies files and directories. * This is a PnP-compatible alternative to fs.cpSync that works with Yarn PnP's * virtualized filesystem. */ function copyRecursivePnP(src: string, dest: string): void { const stat = fs.statSync(src); if (stat.isDirectory()) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const files = fs.readdirSync(src); for (const file of files) { copyRecursivePnP(path.join(src, file), path.join(dest, file)); } } else { const content = fs.readFileSync(src); fs.writeFileSync(dest, content); } } function renameFile({ filePath, project, renamed, }: { filePath: string; project: IProject; renamed: Map; }): void { const extension = path.extname(filePath); const name = path.basename(filePath, extension); const renamedName = project.fileName?.(name) || name; if (renamedName !== name) { const outputPath = path.dirname(filePath); fs.renameSync(filePath, path.resolve(outputPath, `${renamedName}${extension}`)); renamed.set(name, renamedName); } } function replaceImports({ filePath, isDevMode, meta, renamed, }: { filePath: string; isDevMode?: boolean; meta: ProjectRenderMeta; renamed: Map; }): void { let content = fs.readFileSync(filePath, 'utf8'); // Dev mode: rewrite source bundle imports to match output structure if (isDevMode) { // ../../client-core/bundle/foo -> ../core/foo content = content.replace(/from\s+['"]\.\.\/\.\.\/client-core\/bundle\//g, "from '../core/"); // ../../client-core/bundle' (index import) content = content.replace(/from\s+['"]\.\.\/\.\.\/client-core\/bundle['"]/g, "from '../core'"); } content = content.replace(/from\s+['"](\.\.?\/[^'"]*?)['"]/g, (match, importPath) => { const importIndex = match.indexOf(importPath); const extension = path.extname(importPath); const fileName = path.basename(importPath, extension); const importDir = path.dirname(importPath); const replacedName = (renamed.get(fileName) ?? fileName) + (meta.importFileExtension ? meta.importFileExtension : extension); const replacedMatch = match.slice(0, importIndex) + [importDir, replacedName].filter(Boolean).join('/') + match.slice(importIndex + importPath.length); return replacedMatch; }); const header = '# This file is auto-generated by @hey-api/openapi-python\n\n'; content = `${header}${content}`; fs.writeFileSync(filePath, content, 'utf8'); } /** * Creates a `client` folder containing the same modules as the client package. */ export function generateClientBundle({ meta, outputPath, plugin, project, }: { meta: ProjectRenderMeta; outputPath: string; plugin: DefinePlugin['Config']; project?: IProject; }): Map | undefined { const renamed = new Map(); const devMode = isDevMode(); // copy Hey API clients to output const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-'); if (isHeyApiClientPlugin) { const { clientPath } = getClientBundlePaths(plugin.name); // const { clientPath, corePath } = getClientBundlePaths(plugin.name); // copy client core // const coreOutputPath = path.resolve(outputPath, 'core'); // ensureDirSync(coreOutputPath); // copyRecursivePnP(corePath, coreOutputPath); // copy client bundle const clientOutputPath = path.resolve(outputPath, 'client'); ensureDirSync(clientOutputPath); copyRecursivePnP(clientPath, clientOutputPath); if (project) { // const copiedCoreFiles = fs.readdirSync(coreOutputPath); // for (const file of copiedCoreFiles) { // renameFile({ // filePath: path.resolve(coreOutputPath, file), // project, // renamed, // }); // } const copiedClientFiles = fs.readdirSync(clientOutputPath); for (const file of copiedClientFiles) { renameFile({ filePath: path.resolve(clientOutputPath, file), project, renamed, }); } } // const coreFiles = fs.readdirSync(coreOutputPath); // for (const file of coreFiles) { // replaceImports({ // filePath: path.resolve(coreOutputPath, file), // isDevMode: devMode, // meta, // renamed, // }); // } const clientFiles = fs.readdirSync(clientOutputPath); for (const file of clientFiles) { replaceImports({ filePath: path.resolve(clientOutputPath, file), isDevMode: devMode, meta, renamed, }); } return renamed; } const clientSrcPath = path.isAbsolute(plugin.name) ? path.dirname(plugin.name) : undefined; // copy custom local client to output if (clientSrcPath) { const dirPath = path.resolve(outputPath, 'client'); ensureDirSync(dirPath); copyRecursivePnP(clientSrcPath, dirPath); return; } // copy third-party client to output const clientModulePath = path.normalize(require.resolve(plugin.name)); const clientModulePathComponents = clientModulePath.split(path.sep); const clientDistPath = clientModulePathComponents .slice(0, clientModulePathComponents.indexOf('dist') + 1) .join(path.sep); const indexJsFile = clientModulePathComponents[clientModulePathComponents.length - 1]; const distFiles = [indexJsFile!, 'index.d.mts', 'index.d.cts']; const dirPath = path.resolve(outputPath, 'client'); ensureDirSync(dirPath); for (const file of distFiles) { fs.copyFileSync(path.resolve(clientDistPath, file), path.resolve(dirPath, file)); } return; }