UNPKG

@kubb/cli

Version:

Command-line interface for Kubb, enabling easy generation of TypeScript, React-Query, Zod, and other code from OpenAPI specifications.

205 lines (171 loc) • 6.44 kB
import { createHash } from 'node:crypto' import path from 'node:path' import process from 'node:process' import { styleText } from 'node:util' import { type Config, type KubbEvents, LogLevel, safeBuild, setup } from '@kubb/core' import type { AsyncEventEmitter } from '@kubb/core/utils' import { detectFormatter, detectLinter, formatters, linters } from '@kubb/core/utils' import { executeHooks } from '../utils/executeHooks.ts' type GenerateProps = { input?: string config: Config events: AsyncEventEmitter<KubbEvents> logLevel: number } export async function generate({ input, config: userConfig, events, logLevel }: GenerateProps): Promise<void> { const inputPath = input ?? ('path' in userConfig.input ? userConfig.input.path : undefined) const hrStart = process.hrtime() const config: Config = { ...userConfig, root: userConfig.root || process.cwd(), input: inputPath ? { ...userConfig.input, path: inputPath, } : userConfig.input, output: { write: true, barrelType: 'named', extension: { '.ts': '.ts', }, format: 'prettier', ...userConfig.output, }, } await events.emit('generation:start', config) await events.emit('info', config.name ? `Setup generation ${styleText('bold', config.name)}` : 'Setup generation', inputPath) const { sources, fabric, pluginManager } = await setup({ config, events, }) await events.emit('info', config.name ? `Build generation ${styleText('bold', config.name)}` : 'Build generation', inputPath) const { files, failedPlugins, pluginTimings, error } = await safeBuild( { config, events, }, { pluginManager, fabric, events, sources }, ) await events.emit('info', 'Load summary') // Handle build failures (either from failed plugins or general errors) const hasFailures = failedPlugins.size > 0 || error if (hasFailures) { // Collect all errors from failed plugins and general error const allErrors: Error[] = [ error, ...Array.from(failedPlugins) .filter((it) => it.error) .map((it) => it.error), ].filter(Boolean) allErrors.forEach((err) => { events.emit('error', err) }) await events.emit('generation:end', config, files, sources) await events.emit('generation:summary', config, { failedPlugins, filesCreated: files.length, status: failedPlugins.size > 0 || error ? 'failed' : 'success', hrStart, pluginTimings: logLevel >= LogLevel.verbose ? pluginTimings : undefined, }) process.exit(1) } await events.emit('success', 'Generation successfully', inputPath) await events.emit('generation:end', config, files, sources) // formatting if (config.output.format) { await events.emit('format:start') let formatter = config.output.format if (formatter === 'auto') { const detectedFormatter = await detectFormatter() if (!detectedFormatter) { await events.emit('warn', 'No formatter found (biome, prettier, or oxfmt). Skipping formatting.') } else { formatter = detectedFormatter await events.emit('info', `Auto-detected formatter: ${styleText('dim', formatter)}`) } } if (formatter && formatter !== 'auto' && formatter in formatters) { const formatterConfig = formatters[formatter as keyof typeof formatters] const outputPath = path.resolve(config.root, config.output.path) try { const hookId = createHash('sha256').update([config.name, formatter].filter(Boolean).join('-')).digest('hex') await events.emit('hook:start', { id: hookId, command: formatterConfig.command, args: formatterConfig.args(outputPath), }) await events.onOnce('hook:end', async ({ success, error }) => { if (!success) throw error await events.emit( 'success', [`Formatting with ${styleText('dim', formatter)}`, logLevel >= LogLevel.info ? `on ${styleText('dim', outputPath)}` : undefined, 'successfully'] .filter(Boolean) .join(' '), ) }) } catch (caughtError) { const error = new Error(formatterConfig.errorMessage) error.cause = caughtError await events.emit('error', error) } } await events.emit('format:end') } // linting if (config.output.lint) { await events.emit('lint:start') // Detect linter if set to 'auto' let linter = config.output.lint if (linter === 'auto') { const detectedLinter = await detectLinter() if (!detectedLinter) { await events.emit('warn', 'No linter found (biome, oxlint, or eslint). Skipping linting.') } else { linter = detectedLinter await events.emit('info', `Auto-detected linter: ${styleText('dim', linter)}`) } } // Only proceed with linting if we have a valid linter if (linter && linter !== 'auto' && linter in linters) { const linterConfig = linters[linter as keyof typeof linters] const outputPath = path.resolve(config.root, config.output.path) try { const hookId = createHash('sha256').update([config.name, linter].filter(Boolean).join('-')).digest('hex') await events.emit('hook:start', { id: hookId, command: linterConfig.command, args: linterConfig.args(outputPath), }) await events.onOnce('hook:end', async ({ success, error }) => { if (!success) throw error await events.emit( 'success', [`Linting with ${styleText('dim', linter)}`, logLevel >= LogLevel.info ? `on ${styleText('dim', outputPath)}` : undefined, 'successfully'] .filter(Boolean) .join(' '), ) }) } catch (caughtError) { const error = new Error(linterConfig.errorMessage) error.cause = caughtError await events.emit('error', error) } } await events.emit('lint:end') } if (config.hooks) { await events.emit('hooks:start') await executeHooks({ hooks: config.hooks, events }) await events.emit('hooks:end') } await events.emit('generation:summary', config, { failedPlugins, filesCreated: files.length, status: failedPlugins.size > 0 || error ? 'failed' : 'success', hrStart, pluginTimings, }) }