@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
JavaScript
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