UNPKG

@hey-api/openapi-ts

Version:

🌀 OpenAPI to TypeScript codegen. Production-ready SDKs, Zod schemas, TanStack Query hooks, and 20+ plugins. Used by Vercel, OpenCode, and PayPal.

589 lines (577 loc) 22.4 kB
import { A as openGitHubIssueWithCrashReport, D as ConfigValidationError, E as ConfigError, M as shouldReportCrash, N as loadPackageJson, O as JobError, T as getLogs, i as initConfigs, j as printCrashReport, k as logCrashReport, n as buildGraph, r as getSpec, s as generateClientBundle, t as parseOpenApiSpec, u as toCase, w as postprocessOutput, y as getClientPlugin } from "./openApi-PX3rDrOF.mjs"; import "@hey-api/codegen-core"; import colors from "ansi-colors"; import colorSupport from "color-support"; import fs from "node:fs"; import path from "node:path"; import { $RefParser } from "@hey-api/json-schema-ref-parser"; //#region src/config/engine.ts const checkNodeVersion = () => { if (typeof Bun !== "undefined") { const [major] = Bun.version.split(".").map(Number); if (major < 1) throw new ConfigError(`Unsupported Bun version ${Bun.version}. Please use Bun 1.0.0 or newer.`); } else if (typeof process !== "undefined" && process.versions?.node) { const [major] = process.versions.node.split(".").map(Number); if (major < 20) throw new ConfigError(`Unsupported Node version ${process.versions.node}. Please use Node 20 or newer.`); } }; //#endregion //#region src/ir/intents.ts var IntentContext = class { spec; constructor(spec) { this.spec = spec; } getOperation(path$1, method) { const paths = this.spec.paths; if (!paths) return; return paths[path$1]?.[method]; } setExample(operation, example) { const source = this.getOperation(operation.path, operation.method); if (!source) return; source["x-codeSamples"] ||= []; source["x-codeSamples"].push(example); } }; //#endregion //#region src/generate/output.ts const generateOutput = async ({ context }) => { const outputPath = path.resolve(context.config.output.path); if (context.config.output.clean) { if (fs.existsSync(outputPath)) fs.rmSync(outputPath, { force: true, recursive: true }); } const client = getClientPlugin(context.config); if ("bundle" in client.config && client.config.bundle && !context.config.dryRun) context.config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({ meta: { importFileExtension: context.config.output.importFileExtension }, outputPath, plugin: client, project: context.gen }); for (const plugin of context.registerPlugins()) await plugin.run(); context.gen.plan(); const ctx = new IntentContext(context.spec); for (const intent of context.intents) await intent.run(ctx); for (const file of context.gen.render()) { const filePath = path.resolve(outputPath, file.path); const dir = path.dirname(filePath); if (!context.config.dryRun) { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, file.content, { encoding: "utf8" }); } } const { source } = context.config.output; if (source.enabled) { const sourcePath = source.path === null ? void 0 : path.resolve(outputPath, source.path); if (!context.config.dryRun && sourcePath && sourcePath !== outputPath) fs.mkdirSync(sourcePath, { recursive: true }); const serialized = await source.serialize(context.spec); if (!context.config.dryRun && sourcePath) fs.writeFileSync(path.resolve(sourcePath, `${source.fileName}.${source.extension}`), serialized, { encoding: "utf8" }); if (source.callback) await source.callback(serialized); } }; //#endregion //#region src/openApi/shared/utils/patch.ts const patchOpenApiSpec = ({ patchOptions, spec: _spec }) => { if (!patchOptions) return; const spec = _spec; if ("swagger" in spec) { if (patchOptions.version && spec.swagger) spec.swagger = typeof patchOptions.version === "string" ? patchOptions.version : patchOptions.version(spec.swagger); if (patchOptions.meta && spec.info) patchOptions.meta(spec.info); if (patchOptions.schemas && spec.definitions) for (const key in patchOptions.schemas) { const schema = spec.definitions[key]; if (!schema || typeof schema !== "object") continue; const patchFn = patchOptions.schemas[key]; patchFn(schema); } if (patchOptions.operations && spec.paths) for (const key in patchOptions.operations) { const [method, path$1] = key.split(" "); if (!method || !path$1) continue; const pathItem = spec.paths[path$1]; if (!pathItem) continue; const operation = pathItem[method.toLocaleLowerCase()] || pathItem[method.toLocaleUpperCase()]; if (!operation || typeof operation !== "object") continue; const patchFn = patchOptions.operations[key]; patchFn(operation); } return; } if (patchOptions.version && spec.openapi) spec.openapi = typeof patchOptions.version === "string" ? patchOptions.version : patchOptions.version(spec.openapi); if (patchOptions.meta && spec.info) patchOptions.meta(spec.info); if (spec.components) { if (patchOptions.schemas && spec.components.schemas) for (const key in patchOptions.schemas) { const schema = spec.components.schemas[key]; if (!schema || typeof schema !== "object") continue; const patchFn = patchOptions.schemas[key]; patchFn(schema); } if (patchOptions.parameters && spec.components.parameters) for (const key in patchOptions.parameters) { const schema = spec.components.parameters[key]; if (!schema || typeof schema !== "object") continue; const patchFn = patchOptions.parameters[key]; patchFn(schema); } if (patchOptions.requestBodies && spec.components.requestBodies) for (const key in patchOptions.requestBodies) { const schema = spec.components.requestBodies[key]; if (!schema || typeof schema !== "object") continue; const patchFn = patchOptions.requestBodies[key]; patchFn(schema); } if (patchOptions.responses && spec.components.responses) for (const key in patchOptions.responses) { const schema = spec.components.responses[key]; if (!schema || typeof schema !== "object") continue; const patchFn = patchOptions.responses[key]; patchFn(schema); } } if (patchOptions.operations && spec.paths) for (const key in patchOptions.operations) { const [method, path$1] = key.split(" "); if (!method || !path$1) continue; const pathItem = spec.paths[path$1]; if (!pathItem) continue; const operation = pathItem[method.toLocaleLowerCase()] || pathItem[method.toLocaleUpperCase()]; if (!operation || typeof operation !== "object") continue; const patchFn = patchOptions.operations[key]; patchFn(operation); } }; //#endregion //#region src/createClient.ts const compileInputPath = (input) => { const result = { ...input, path: "" }; if (input.path && (typeof input.path !== "string" || input.registry !== "hey-api")) { result.path = input.path; return result; } const [basePath, baseQuery] = input.path.split("?"); const queryPath = (baseQuery || "").split("&").map((part) => part.split("=")); let path$1 = basePath || ""; if (path$1.endsWith("/")) path$1 = path$1.slice(0, path$1.length - 1); const [, pathUrl] = path$1.split("://"); const [baseUrl, organization, project] = (pathUrl || "").split("/"); result.organization = organization || input.organization; result.project = project || input.project; const queryParams = []; const kApiKey = "api_key"; result.api_key = queryPath.find(([key]) => key === kApiKey)?.[1] || input.api_key || process.env.HEY_API_TOKEN; if (result.api_key) queryParams.push(`${kApiKey}=${result.api_key}`); const kBranch = "branch"; result.branch = queryPath.find(([key]) => key === kBranch)?.[1] || input.branch; if (result.branch) queryParams.push(`${kBranch}=${result.branch}`); const kCommitSha = "commit_sha"; result.commit_sha = queryPath.find(([key]) => key === kCommitSha)?.[1] || input.commit_sha; if (result.commit_sha) queryParams.push(`${kCommitSha}=${result.commit_sha}`); const kTags = "tags"; result.tags = queryPath.find(([key]) => key === kTags)?.[1]?.split(",") || input.tags; if (result.tags?.length) queryParams.push(`${kTags}=${result.tags.join(",")}`); const kVersion = "version"; result.version = queryPath.find(([key]) => key === kVersion)?.[1] || input.version; if (result.version) queryParams.push(`${kVersion}=${result.version}`); if (!result.organization) throw new Error("missing organization - from which Hey API Platform organization do you want to generate your output?"); if (!result.project) throw new Error("missing project - from which Hey API Platform project do you want to generate your output?"); const query = queryParams.join("&"); const platformUrl = baseUrl || "get.heyapi.dev"; const isLocalhost = platformUrl.startsWith("localhost"); const platformUrlWithProtocol = [isLocalhost ? "http" : "https", platformUrl].join("://"); const compiledPath = isLocalhost ? [ platformUrlWithProtocol, "v1", "get", result.organization, result.project ].join("/") : [ platformUrlWithProtocol, result.organization, result.project ].join("/"); result.path = query ? `${compiledPath}?${query}` : compiledPath; return result; }; const logInputPaths = (inputPaths, jobIndex) => { const lines = []; const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); const count = inputPaths.length; const baseString = colors.cyan(`Generating from ${count} ${count === 1 ? "input" : "inputs"}:`); lines.push(`${jobPrefix}${baseString}`); inputPaths.forEach((inputPath, index) => { const itemPrefixStr = ` [${index + 1}] `; const itemPrefix = colors.cyan(itemPrefixStr); const detailIndent = " ".repeat(itemPrefixStr.length); if (typeof inputPath.path !== "string") { lines.push(`${jobPrefix}${itemPrefix}raw OpenAPI specification`); return; } switch (inputPath.registry) { case "hey-api": { const baseInput = [inputPath.organization, inputPath.project].filter(Boolean).join("/"); lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); if (inputPath.branch) lines.push(`${jobPrefix}${detailIndent}${colors.gray("branch:")} ${colors.green(inputPath.branch)}`); if (inputPath.commit_sha) lines.push(`${jobPrefix}${detailIndent}${colors.gray("commit:")} ${colors.green(inputPath.commit_sha)}`); if (inputPath.tags?.length) lines.push(`${jobPrefix}${detailIndent}${colors.gray("tags:")} ${colors.green(inputPath.tags.join(", "))}`); if (inputPath.version) lines.push(`${jobPrefix}${detailIndent}${colors.gray("version:")} ${colors.green(inputPath.version)}`); lines.push(`${jobPrefix}${detailIndent}${colors.gray("registry:")} ${colors.green("Hey API")}`); break; } case "readme": { const baseInput = [inputPath.organization, inputPath.project].filter(Boolean).join("/"); if (!baseInput) lines.push(`${jobPrefix}${itemPrefix}${inputPath.path}`); else lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); if (inputPath.uuid) lines.push(`${jobPrefix}${detailIndent}${colors.gray("uuid:")} ${colors.green(inputPath.uuid)}`); lines.push(`${jobPrefix}${detailIndent}${colors.gray("registry:")} ${colors.green("ReadMe")}`); break; } case "scalar": { const baseInput = [inputPath.organization, inputPath.project].filter(Boolean).join("/"); lines.push(`${jobPrefix}${itemPrefix}${baseInput}`); lines.push(`${jobPrefix}${detailIndent}${colors.gray("registry:")} ${colors.green("Scalar")}`); break; } default: lines.push(`${jobPrefix}${itemPrefix}${inputPath.path}`); break; } }); for (const line of lines) console.log(line); }; const createClient$1 = async ({ config, dependencies, jobIndex, logger, watches: _watches }) => { const watches = _watches || Array.from({ length: config.input.length }, () => ({ headers: new Headers() })); const inputPaths = config.input.map((input) => compileInputPath(input)); if (config.logs.level !== "silent" && !_watches) logInputPaths(inputPaths, jobIndex); const getSpecData = async (input, index) => { const eventSpec = logger.timeEvent("spec"); const { arrayBuffer, error, resolvedInput, response } = await getSpec({ fetchOptions: input.fetch, inputPath: inputPaths[index].path, timeout: input.watch.timeout, watch: watches[index] }); eventSpec.timeEnd(); if (error && !_watches) throw new Error(`Request failed with status ${response.status}: ${response.statusText}`); return { arrayBuffer, resolvedInput }; }; const specData = (await Promise.all(config.input.map((input, index) => getSpecData(input, index)))).filter((data) => data.arrayBuffer || data.resolvedInput); let context; if (specData.length) { const refParser = new $RefParser(); const data = specData.length > 1 ? await refParser.bundleMany({ arrayBuffer: specData.map((data$1) => data$1.arrayBuffer), pathOrUrlOrSchemas: [], resolvedInputs: specData.map((data$1) => data$1.resolvedInput) }) : await refParser.bundle({ arrayBuffer: specData[0].arrayBuffer, pathOrUrlOrSchema: void 0, resolvedInput: specData[0].resolvedInput }); if (config.logs.level !== "silent" && _watches) { console.clear(); logInputPaths(inputPaths, jobIndex); } const eventInputPatch = logger.timeEvent("input.patch"); patchOpenApiSpec({ patchOptions: config.parser.patch, spec: data }); eventInputPatch.timeEnd(); const eventParser = logger.timeEvent("parser"); context = parseOpenApiSpec({ config, dependencies, logger, spec: data }); context.graph = buildGraph(context.ir, logger).graph; eventParser.timeEnd(); const eventGenerator = logger.timeEvent("generator"); await generateOutput({ context }); eventGenerator.timeEnd(); const eventPostprocess = logger.timeEvent("postprocess"); if (!config.dryRun) { postprocessOutput(config.output); if (config.logs.level !== "silent") { const outputPath = process.env.INIT_CWD ? `./${path.relative(process.env.INIT_CWD, config.output.path)}` : config.output.path; const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); console.log(`${jobPrefix}${colors.green("✅ Done!")} Your output is in ${colors.cyanBright(outputPath)}`); } } eventPostprocess.timeEnd(); } const watchedInput = config.input.find((input, index) => input.watch.enabled && typeof inputPaths[index].path === "string"); if (watchedInput) setTimeout(() => { createClient$1({ config, dependencies, jobIndex, logger, watches }); }, watchedInput.watch.interval); return context; }; //#endregion //#region src/utils/cli.ts const textAscii = ` 888 | e 888~-_ 888 888___| e88~~8e Y88b / d8b 888 \\ 888 888 | d888 88b Y888/ /Y88b 888 | 888 888 | 8888__888 Y8/ / Y88b 888 / 888 888 | Y888 , Y /____Y88b 888_-~ 888 888 | "88___/ / / Y88b 888 888 _/ `; const asciiToLines = (ascii, options) => { const lines = []; const padding = Array.from({ length: options?.padding ?? 0 }).fill(""); lines.push(...padding); let maxLineLength = 0; let line = ""; for (const char of ascii) if (char === "\n") { if (line) { lines.push(line); maxLineLength = Math.max(maxLineLength, line.length); line = ""; } } else line += char; lines.push(...padding); return { lines, maxLineLength }; }; function printCliIntro() { const packageJson = loadPackageJson(); const text = asciiToLines(textAscii, { padding: 1 }); for (const line of text.lines) console.log(colors.cyan(line)); console.log(colors.gray(`${packageJson.name} v${packageJson.version}`)); console.log(""); } //#endregion //#region src/utils/logger.ts let loggerCounter = 0; const nameToId = (name) => `${name}-${loggerCounter++}`; const idEnd = (id) => `${id}-end`; const idLength = (id) => `${id}-length`; const idStart = (id) => `${id}-start`; const getSeverity = (duration, percentage) => { if (duration > 200) return { color: colors.red, type: "duration" }; if (percentage > 30) return { color: colors.red, type: "percentage" }; if (duration > 50) return { color: colors.yellow, type: "duration" }; if (percentage > 10) return { color: colors.yellow, type: "percentage" }; }; var Logger = class { events = []; end(result) { let event; let events = this.events; for (const index of result.position) { event = events[index]; if (event?.events) events = event.events; } if (event && !event.end) event.end = performance.mark(idEnd(event.id)); } /** * Recursively end all unended events in the event tree. * This ensures all events have end marks before measuring. */ endAllEvents(events) { for (const event of events) { if (!event.end) event.end = performance.mark(idEnd(event.id)); if (event.events.length > 0) this.endAllEvents(event.events); } } report(print = true) { const firstEvent = this.events[0]; if (!firstEvent) return; this.endAllEvents(this.events); const lastEvent = this.events[this.events.length - 1]; const name = "root"; const id = nameToId(name); try { const measure = performance.measure(idLength(id), idStart(firstEvent.id), idEnd(lastEvent.id)); if (print) this.reportEvent({ end: lastEvent.end, events: this.events, id, indent: 0, measure, name, start: firstEvent.start }); return measure; } catch { return; } } reportEvent({ indent, ...parent }) { const color = !indent ? colors.cyan : colors.gray; const lastIndex = parent.events.length - 1; parent.events.forEach((event, index) => { try { const measure = performance.measure(idLength(event.id), idStart(event.id), idEnd(event.id)); const duration = Math.ceil(measure.duration * 100) / 100; const percentage = Math.ceil(measure.duration / parent.measure.duration * 100 * 100) / 100; const severity = indent ? getSeverity(duration, percentage) : void 0; let durationLabel = `${duration.toFixed(2).padStart(8)}ms`; if (severity?.type === "duration") durationLabel = severity.color(durationLabel); const branch = index === lastIndex ? "└─ " : "├─ "; const prefix = !indent ? "" : "│ ".repeat(indent - 1) + branch; const maxLength = 38 - prefix.length; const percentageBranch = !indent ? "" : "↳ "; let percentageLabel = `${indent ? " ".repeat(indent - 1) + percentageBranch : ""}${percentage.toFixed(2)}%`; if (severity?.type === "percentage") percentageLabel = severity.color(percentageLabel); const jobPrefix = colors.gray("[root] "); console.log(`${jobPrefix}${colors.gray(prefix)}${color(`${event.name.padEnd(maxLength)} ${durationLabel} (${percentageLabel})`)}`); this.reportEvent({ ...event, indent: indent + 1, measure }); } catch {} }); } start(id) { return performance.mark(idStart(id)); } storeEvent({ result, ...event }) { const lastEventIndex = event.events.length - 1; const lastEvent = event.events[lastEventIndex]; if (lastEvent && !lastEvent.end) { result.position = [...result.position, lastEventIndex]; this.storeEvent({ ...event, events: lastEvent.events, result }); return; } const length = event.events.push({ ...event, events: [] }); result.position = [...result.position, length - 1]; } timeEvent(name) { const id = nameToId(name); const start = this.start(id); const event = { events: this.events, id, name, start }; const result = { position: [] }; this.storeEvent({ ...event, result }); return { mark: start, timeEnd: () => this.end(result) }; } }; //#endregion //#region src/generate.ts /** * Generate a client from the provided configuration. * * @param userConfig User provided {@link UserConfig} configuration(s). */ const createClient = async (userConfig, logger = new Logger()) => { const resolvedConfig = typeof userConfig === "function" ? await userConfig() : userConfig; const userConfigs = resolvedConfig ? resolvedConfig instanceof Array ? resolvedConfig : [resolvedConfig] : []; let rawLogs = userConfigs.find((config) => getLogs(config).level !== "silent")?.logs; if (typeof rawLogs === "string") rawLogs = getLogs({ logs: rawLogs }); let configs; try { checkNodeVersion(); const eventCreateClient = logger.timeEvent("createClient"); const eventConfig = logger.timeEvent("config"); configs = await initConfigs({ logger, userConfigs }); if (configs.results.some((result$1) => result$1.config.logs.level !== "silent")) printCliIntro(); eventConfig.timeEnd(); const allConfigErrors = configs.results.flatMap((result$1) => result$1.errors.map((error) => ({ error, jobIndex: result$1.jobIndex }))); if (allConfigErrors.length) throw new ConfigValidationError(allConfigErrors); const result = (await Promise.all(configs.results.map(async (result$1) => { try { return await createClient$1({ config: result$1.config, dependencies: configs.dependencies, jobIndex: result$1.jobIndex, logger }); } catch (error) { throw new JobError("", { error, jobIndex: result$1.jobIndex }); } }))).filter((client) => Boolean(client)); eventCreateClient.timeEnd(); const printLogs = configs.results.some((result$1) => result$1.config.logs.level === "debug"); logger.report(printLogs); return result; } catch (error) { const results = configs?.results ?? []; const logs = results.find((result) => result.config.logs.level !== "silent")?.config.logs ?? results[0]?.config.logs ?? rawLogs; const dryRun = results.some((result) => result.config.dryRun) ?? userConfigs.some((config) => config.dryRun) ?? false; const logPath = logs?.file && !dryRun ? logCrashReport(error, logs.path ?? "") : void 0; if (!logs || logs.level !== "silent") { printCrashReport({ error, logPath }); if (await shouldReportCrash({ error, isInteractive: results.some((result) => result.config.interactive) ?? userConfigs.some((config) => config.interactive) ?? false })) await openGitHubIssueWithCrashReport(error); } throw error; } }; //#endregion //#region src/utils/exports.ts /** * Utilities shared across the package. */ const utils = { stringCase({ case: casing, stripLeadingSeparators, value }) { return toCase(value, casing, { stripLeadingSeparators }); }, toCase }; //#endregion //#region src/index.ts colors.enabled = colorSupport().hasBasic; /** * Type helper for openapi-ts.config.ts, returns {@link MaybeArray<UserConfig>} object(s) */ const defineConfig = async (config) => typeof config === "function" ? await config() : config; //#endregion export { Logger as i, utils as n, createClient as r, defineConfig as t }; //# sourceMappingURL=src-BCi83sdM.mjs.map