@hey-api/openapi-ts
Version:
🌀 OpenAPI to TypeScript codegen. Production-grade SDKs, Zod schemas, TanStack Query hooks, and 20+ plugins. Used by Vercel, OpenCode, and PayPal.
257 lines (256 loc) • 10.7 kB
JavaScript
import { _ as postProcessors, c as TypeScriptRenderer, g as getTypedConfig, h as getClientPlugin, i as generateClientBundle, t as resolveJobs } from "./init-D3VuY80Z.mjs";
import { Logger, Logger as Logger$1, Project } from "@hey-api/codegen-core";
import { ConfigValidationError, Context, InputError, IntentContext, JobError, OperationPath as OperationPath$1, OperationStrategy as OperationStrategy$1, applyNaming, buildGraph, checkNodeVersion, compileInputPath, defaultPaginationKeywords, definePluginConfig as definePluginConfig$1, getInputError, getLogs, getSpec, logCrashReport, logInputPaths, openGitHubIssueWithCrashReport, parseOpenApiSpec, patchOpenApiSpec, postprocessOutput, printCliIntro, printCrashReport, shouldReportCrash, utils } from "@hey-api/shared";
import colors from "ansi-colors";
import colorSupport from "color-support";
import path from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import { $RefParser, ResolverError } from "@hey-api/json-schema-ref-parser";
import { format } from "@lukeed/ms";
import fsPromises from "node:fs/promises";
//#region src/generate/output.ts
async function generateOutput(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 config = getTypedConfig(context);
const client = getClientPlugin(config);
if ("bundle" in client.config && client.config.bundle && !config.dryRun) config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({
header: config.output.header,
module: config.output.module,
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);
let fileCount = 0;
const writes = [];
for (const file of context.gen.render()) {
const filePath = path.resolve(outputPath, file.path);
const dir = path.dirname(filePath);
if (!context.config.dryRun) writes.push(fsPromises.mkdir(dir, { recursive: true }).then(() => fsPromises.writeFile(filePath, file.content, { encoding: "utf8" })));
fileCount++;
}
await Promise.all(writes);
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) await fsPromises.mkdir(sourcePath, { recursive: true });
const serialized = await source.serialize(context.spec);
if (!context.config.dryRun && sourcePath) {
await fsPromises.writeFile(path.resolve(sourcePath, `${source.fileName}.${source.extension}`), serialized, { encoding: "utf8" });
fileCount++;
}
if (source.callback) await source.callback(serialized);
}
return { fileCount };
}
//#endregion
//#region src/createClient.ts
async function createClient$1({ config, dependencies, jobIndex, logger, watches: _watches }) {
const watches = _watches || Array.from({ length: config.input.length }, () => ({ headers: new Headers() }));
const jobStart = Date.now();
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) {
const text = await response.text().catch(() => "");
const message = `Request failed with status ${response.status}: ${text || response.statusText}`;
if (response.status >= 400 && response.status < 500) {
const statusText = response.statusText || "Unknown";
const originalError = new Error(message);
originalError.source = String(inputPaths[index].path);
throw new InputError(`Input request failed: ${response.status} ${statusText}`, originalError);
}
throw new Error(message);
}
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();
let data;
try {
data = specData.length > 1 ? await refParser.bundleMany({
arrayBuffer: specData.map((data) => data.arrayBuffer),
pathOrUrlOrSchemas: [],
resolvedInputs: specData.map((data) => data.resolvedInput)
}) : await refParser.bundle({
arrayBuffer: specData[0].arrayBuffer,
pathOrUrlOrSchema: void 0,
resolvedInput: specData[0].resolvedInput
});
} catch (err) {
if (err instanceof ResolverError && err.ioErrorCode === "ENOENT") throw new InputError("Input file not found", err);
throw err;
}
if (config.logs.level !== "silent" && _watches) {
console.clear();
logInputPaths(inputPaths, jobIndex);
}
const eventInputPatch = logger.timeEvent("input.patch");
await patchOpenApiSpec({
patchOptions: config.parser.patch,
spec: data
});
eventInputPatch.timeEnd();
const eventParser = logger.timeEvent("parser");
const header = config.output.header;
context = new Context({
config,
dependencies,
logger,
project: new Project({
defaultFileName: "index",
fileName: (base) => {
const name = applyNaming(base, config.output.fileName);
const { suffix } = config.output.fileName;
if (!suffix) return name;
return name === "index" || name.endsWith(suffix) ? name : `${name}${suffix}`;
},
nameConflictResolvers: config.output.nameConflictResolver ? { typescript: config.output.nameConflictResolver } : void 0,
renderers: [new TypeScriptRenderer({
header: (ctx) => {
const defaultValue = ["// This file is auto-generated by @hey-api/openapi-ts"];
const result = typeof header === "function" ? header({
...ctx,
defaultValue
}) : header;
return result === void 0 ? defaultValue : result;
},
module: config.output.module,
preferExportAll: config.output.preferExportAll
})],
root: config.output.path
}),
spec: data
});
parseOpenApiSpec(context);
context.graph = buildGraph(context.ir, logger).graph;
eventParser.timeEnd();
const eventGenerator = logger.timeEvent("generator");
const { fileCount } = await generateOutput(context);
eventGenerator.timeEnd();
const totalMs = Date.now() - jobStart;
const eventPostprocess = logger.timeEvent("postprocess");
if (!config.dryRun) {
const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `);
postprocessOutput(config.output, postProcessors, jobPrefix);
if (config.logs.level !== "silent") {
const outputPath = process.env.INIT_CWD ? `./${path.relative(process.env.INIT_CWD, config.output.path)}` : config.output.path;
console.log(`${jobPrefix}${colors.green("✅ Done!")} Your output is in ${colors.cyanBright(outputPath)} ${colors.gray(`(${fileCount} ${fileCount === 1 ? "file" : "files"} in ${format(totalMs)})`)}`);
}
}
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/generate.ts
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Generate a client from the provided configuration.
*
* @param userConfig User provided {@link UserConfig} configuration(s).
*/
async function createClient(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.logs).level !== "silent")?.logs;
if (typeof rawLogs === "string") rawLogs = getLogs(rawLogs);
let jobs = [];
try {
checkNodeVersion();
const eventCreateClient = logger.timeEvent("createClient");
const eventConfig = logger.timeEvent("config");
const resolved = await resolveJobs({
logger,
userConfigs
});
const dependencies = resolved.dependencies;
jobs = resolved.jobs;
if (jobs.some((job) => job.config.logs.level !== "silent")) printCliIntro(__dirname);
eventConfig.timeEnd();
const configErrors = jobs.flatMap((job) => job.errors.map((error) => ({
error,
jobIndex: job.index
})));
if (configErrors.length) throw new ConfigValidationError(configErrors);
const contexts = (await Promise.all(jobs.map(async (job) => {
try {
return await createClient$1({
config: job.config,
dependencies,
jobIndex: job.index,
logger
});
} catch (error) {
if (error instanceof Error) throw new JobError("", {
error,
jobIndex: job.index
});
}
}))).filter((ctx) => ctx !== void 0);
eventCreateClient.timeEnd();
logger.report(jobs.some((job) => job.config.logs.level === "debug"));
return contexts;
} catch (error) {
const logs = jobs.find((job) => job.config.logs.level !== "silent")?.config.logs ?? jobs[0]?.config.logs ?? rawLogs;
const dryRun = jobs.some((job) => job.config.dryRun) ?? userConfigs.some((config) => config.dryRun) ?? false;
const inputError = getInputError(error);
const normalizedError = inputError ?? error;
const logPath = logs?.file && !dryRun ? logCrashReport(normalizedError, logs.path ?? "") : void 0;
if (!logs || logs.level !== "silent") {
printCrashReport({
error,
logPath
});
if (await shouldReportCrash({
error: normalizedError,
isInteractive: jobs.some((job) => job.config.interactive) ?? userConfigs.some((config) => config.interactive) ?? false
})) await openGitHubIssueWithCrashReport(error, __dirname);
}
if (inputError) return [];
throw error;
}
}
//#endregion
//#region src/index.ts
colors.enabled = colorSupport().hasBasic;
async function defineConfig(config) {
return typeof config === "function" ? await config() : config;
}
//#endregion
export { defineConfig as a, createClient as c, defaultPaginationKeywords as i, OperationPath$1 as n, definePluginConfig$1 as o, OperationStrategy$1 as r, utils as s, Logger$1 as t };
//# sourceMappingURL=src-BeNy9O9X.mjs.map