typed-openapi
Version:
898 lines (863 loc) • 34.3 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/cli.ts
var import_swagger_parser = __toESM(require("@apidevtools/swagger-parser"), 1);
var import_cac = require("cac");
var import_pathe = require("pathe");
var import_arktype2 = require("arktype");
var import_promises = require("fs/promises");
// package.json
var name = "typed-openapi";
var version = "0.10.1";
// src/generator.ts
var import_server2 = require("pastable/server");
// src/asserts.ts
var isPrimitiveType = (type3) => primitiveTypeList.includes(type3);
var primitiveTypeList = ["string", "number", "integer", "boolean", "null"];
// src/box-factory.ts
var unwrap = (param) => typeof param === "string" ? param : param.value;
var createFactory = (f) => f;
var createBoxFactory = (schema, ctx) => {
const f = typeof ctx.factory === "function" ? ctx.factory(schema, ctx) : ctx.factory;
const callback = (box2) => {
if (f.callback) {
box2 = f.callback(box2);
}
if (ctx?.onBox) {
box2 = ctx.onBox?.(box2);
}
return box2;
};
const box = {
union: (types) => callback(new Box({ ctx, schema, type: "union", params: { types }, value: f.union(types) })),
intersection: (types) => callback(new Box({ ctx, schema, type: "intersection", params: { types }, value: f.intersection(types) })),
array: (type3) => callback(new Box({ ctx, schema, type: "array", params: { type: type3 }, value: f.array(type3) })),
optional: (type3) => callback(new Box({ ctx, schema, type: "optional", params: { type: type3 }, value: f.optional(type3) })),
reference: (name2, generics) => callback(
new Box({
ctx,
schema,
type: "ref",
params: generics ? { name: name2, generics } : { name: name2 },
value: f.reference(name2, generics)
})
),
literal: (value) => callback(new Box({ ctx, schema, type: "literal", params: {}, value: f.literal(value) })),
string: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "string" }, value: f.string() })),
number: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "number" }, value: f.number() })),
boolean: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "boolean" }, value: f.boolean() })),
unknown: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "unknown" }, value: f.unknown() })),
any: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "any" }, value: f.any() })),
never: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "never" }, value: f.never() })),
object: (props) => callback(new Box({ ctx, schema, type: "object", params: { props }, value: f.object(props) }))
};
return box;
};
// src/is-reference-object.ts
function isReferenceObject(obj) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, "$ref");
}
// src/string-utils.ts
var import_server = require("pastable/server");
function normalizeString(text) {
const prefixed = prefixStringStartingWithNumberIfNeeded(text);
return prefixed.normalize("NFKD").trim().replace(/\s+/g, "_").replace(/-+/g, "_").replace(/[^\w\-]+/g, "_").replace(/--+/g, "-");
}
var onlyWordRegex = /^\w+$/;
var wrapWithQuotesIfNeeded = (str) => {
if (str[0] === '"' && str[str.length - 1] === '"')
return str;
if (onlyWordRegex.test(str)) {
return str;
}
return `"${str}"`;
};
var prefixStringStartingWithNumberIfNeeded = (str) => {
const firstAsNumber = Number(str[0]);
if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) {
return "_" + str;
}
return str;
};
var pathParamWithBracketsRegex = /({\w+})/g;
var wordPrecededByNonWordCharacter = /[^\w\-]+/g;
var pathToVariableName = (path) => (0, import_server.capitalize)((0, import_server.kebabToCamel)(path).replaceAll("/", "")).replace(pathParamWithBracketsRegex, (group) => (0, import_server.capitalize)(group.slice(1, -1))).replace(wordPrecededByNonWordCharacter, "_");
// src/openapi-schema-to-ts.ts
var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
const meta = {};
if (!schema) {
throw new Error("Schema is required");
}
const t = createBoxFactory(schema, ctx);
const getTs = () => {
if (isReferenceObject(schema)) {
const refInfo = ctx.refs.getInfosByRef(schema.$ref);
return t.reference(refInfo.normalized);
}
if (Array.isArray(schema.type)) {
if (schema.type.length === 1) {
return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0] }, ctx, meta });
}
return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta })));
}
if (schema.type === "null") {
return t.reference("null");
}
if (schema.oneOf) {
if (schema.oneOf.length === 1) {
return openApiSchemaToTs({ schema: schema.oneOf[0], ctx, meta });
}
return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
}
if (schema.anyOf) {
if (schema.anyOf.length === 1) {
return openApiSchemaToTs({ schema: schema.anyOf[0], ctx, meta });
}
const oneOf = t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
return t.union([oneOf, t.array(oneOf)]);
}
if (schema.allOf) {
if (schema.allOf.length === 1) {
return openApiSchemaToTs({ schema: schema.allOf[0], ctx, meta });
}
const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
return t.intersection(types);
}
const schemaType = schema.type ? schema.type.toLowerCase() : void 0;
if (schemaType && isPrimitiveType(schemaType)) {
if (schema.enum) {
if (schema.enum.length === 1) {
const value = schema.enum[0];
return t.literal(value === null ? "null" : `"${value}"`);
}
if (schemaType === "string") {
return t.union(schema.enum.map((value) => t.literal(`"${value}"`)));
}
if (schema.enum.some((e) => typeof e === "string")) {
return t.never();
}
return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value)));
}
if (schemaType === "string")
return t.string();
if (schemaType === "boolean")
return t.boolean();
if (schemaType === "number" || schemaType === "integer")
return t.number();
if (schemaType === "null")
return t.reference("null");
}
if (schemaType === "array") {
if (schema.items) {
let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta });
if (typeof arrayOfType === "string") {
arrayOfType = t.reference(arrayOfType);
}
return t.array(arrayOfType);
}
return t.array(t.any());
}
if (schemaType === "object" || schema.properties || schema.additionalProperties) {
if (!schema.properties) {
return t.unknown();
}
let additionalProperties;
if (schema.additionalProperties) {
let additionalPropertiesType;
if (typeof schema.additionalProperties === "boolean" && schema.additionalProperties || typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0) {
additionalPropertiesType = t.any();
} else if (typeof schema.additionalProperties === "object") {
additionalPropertiesType = openApiSchemaToTs({
schema: schema.additionalProperties,
ctx,
meta
});
}
additionalProperties = t.object({ [t.string().value]: additionalPropertiesType });
}
const hasRequiredArray = schema.required && schema.required.length > 0;
const isPartial = !schema.required?.length;
const props = Object.fromEntries(
Object.entries(schema.properties).map(([prop, propSchema]) => {
let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta });
if (typeof propType === "string") {
propType = t.reference(propType);
}
const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false);
const isOptional = !isPartial && !isRequired;
return [`${wrapWithQuotesIfNeeded(prop)}`, isOptional ? t.optional(propType) : propType];
})
);
const objectType = additionalProperties ? t.intersection([t.object(props), additionalProperties]) : t.object(props);
return isPartial ? t.reference("Partial", [objectType]) : objectType;
}
if (!schemaType)
return t.unknown();
throw new Error(`Unsupported schema type: ${schemaType}`);
};
let output = getTs();
if (!isReferenceObject(schema)) {
if (schema.nullable) {
output = t.union([output, t.reference("null")]);
}
}
return output;
};
// src/box.ts
var Box = class _Box {
constructor(definition) {
this.definition = definition;
this.definition = definition;
this.type = definition.type;
this.value = definition.value;
this.params = definition.params;
this.schema = definition.schema;
this.ctx = definition.ctx;
}
type;
value;
params;
schema;
ctx;
toJSON() {
return { type: this.type, value: this.value };
}
toString() {
return JSON.stringify(this.toJSON(), null, 2);
}
recompute(callback) {
return openApiSchemaToTs({ schema: this.schema, ctx: { ...this.ctx, onBox: callback } });
}
static fromJSON(json) {
return new _Box(JSON.parse(json));
}
static isBox(box) {
return box instanceof _Box;
}
static isUnion(box) {
return box.type === "union";
}
static isIntersection(box) {
return box.type === "intersection";
}
static isArray(box) {
return box.type === "array";
}
static isOptional(box) {
return box.type === "optional";
}
static isReference(box) {
return box.type === "ref";
}
static isKeyword(box) {
return box.type === "keyword";
}
static isObject(box) {
return box.type === "object";
}
static isLiteral(box) {
return box.type === "literal";
}
};
// src/format.ts
var import_prettier = __toESM(require("prettier"), 1);
var import_parser_typescript = __toESM(require("prettier/parser-typescript"), 1);
function maybePretty(input, options) {
try {
return import_prettier.default.format(input, {
parser: "typescript",
plugins: [import_parser_typescript.default],
...options
});
} catch (err) {
console.warn("Failed to format code");
console.warn(err);
return input;
}
}
var prettify = (str, options) => maybePretty(str, { printWidth: 120, trailingComma: "all", ...options });
// src/generator.ts
var Codegen = __toESM(require("@sinclair/typebox-codegen"), 1);
var import_ts_pattern = require("ts-pattern");
var import_arktype = require("arktype");
var allowedRuntimes = (0, import_arktype.type)("'none' | 'arktype' | 'io-ts' | 'typebox' | 'valibot' | 'yup' | 'zod'");
var runtimeValidationGenerator = {
arktype: Codegen.ModelToArkType.Generate,
"io-ts": Codegen.ModelToIoTs.Generate,
typebox: Codegen.ModelToTypeBox.Generate,
valibot: Codegen.ModelToValibot.Generate,
yup: Codegen.ModelToYup.Generate,
zod: Codegen.ModelToZod.Generate
};
var inferByRuntime = {
none: (input) => input,
arktype: (input) => `${input}["infer"]`,
"io-ts": (input) => `t.TypeOf<${input}>`,
typebox: (input) => `Static<${input}>`,
valibot: (input) => `v.InferOutput<${input}>`,
yup: (input) => `y.InferType<${input}>`,
zod: (input) => `z.infer<${input}>`
};
var methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
var methodsRegex = new RegExp(`(?:${methods.join("|")})_`);
var endpointExport = new RegExp(`export (?:type|const) (?:${methodsRegex.source})`);
var replacerByRuntime = {
yup: (line) => line.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"), "$1$2("),
zod: (line) => line.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"), "$1$2(")
};
var generateFile = (options) => {
const ctx = { ...options, runtime: options.runtime ?? "none" };
const schemaList = generateSchemaList(ctx);
const endpointSchemaList = generateEndpointSchemaList(ctx);
const apiClient = generateApiClient(ctx);
const transform = ctx.runtime === "none" ? (file2) => file2 : (file2) => {
const model = Codegen.TypeScriptToModel.Generate(file2);
const transformer = runtimeValidationGenerator[ctx.runtime];
const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file2) : transformer(model);
let converted = "";
const match3 = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
const content = match3?.[2];
if (content && ctx.runtime in replacerByRuntime) {
const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
converted = before + replacerByRuntime[ctx.runtime](
content.slice(content.indexOf("export"))
);
} else {
converted = generated;
}
return converted;
};
const file = `
${transform(schemaList + endpointSchemaList)}
${apiClient}
`;
return prettify(file);
};
var generateSchemaList = ({ refs, runtime }) => {
let file = `
${runtime === "none" ? "export namespace Schemas {" : ""}
// <Schemas>
`;
refs.getOrderedSchemas().forEach(([schema, infos]) => {
if (!infos?.name)
return;
if (infos.kind !== "schemas")
return;
file += `export type ${infos.normalized} = ${schema.value}
`;
});
return file + `
// </Schemas>
${runtime === "none" ? "}" : ""}
`;
};
var parameterObjectToString = (parameters) => {
if (parameters instanceof Box)
return parameters.value;
let str = "{";
for (const [key, box] of Object.entries(parameters)) {
str += `${wrapWithQuotesIfNeeded(key)}: ${box.value},
`;
}
return str + "}";
};
var generateEndpointSchemaList = (ctx) => {
let file = `
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
// <Endpoints>
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_START__ = {}"}
`;
ctx.endpointList.map((endpoint) => {
const parameters = endpoint.parameters ?? {};
file += `export type ${endpoint.meta.alias} = {
method: "${endpoint.method.toUpperCase()}",
path: "${endpoint.path}",
requestFormat: "${endpoint.requestFormat}",
${endpoint.meta.hasParameters ? `parameters: {
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
${parameters.body ? `body: ${parameterObjectToString(
ctx.runtime === "none" ? parameters.body.recompute((box) => {
if (Box.isReference(box) && !box.params.generics) {
box.value = `Schemas.${box.value}`;
}
return box;
}) : parameters.body
)},` : ""}
}` : "parameters: never,"}
response: ${ctx.runtime === "none" ? endpoint.response.recompute((box) => {
if (Box.isReference(box) && !box.params.generics) {
box.value = `Schemas.${box.value}`;
}
return box;
}).value : endpoint.response.value},
}
`;
});
return file + `
// </Endpoints>
${ctx.runtime === "none" ? "}" : ""}
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_END__ = {}"}
`;
};
var generateEndpointByMethod = (ctx) => {
const { endpointList } = ctx;
const byMethods = (0, import_server2.groupBy)(endpointList, "method");
const endpointByMethod = `
// <EndpointByMethod>
export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = {
${Object.entries(byMethods).map(([method, list]) => {
return `${method}: {
${list.map(
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`
)}
}`;
}).join(",\n")}
}
${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"}
// </EndpointByMethod>
`;
const shorthands = `
// <EndpointByMethod.Shorthands>
${Object.keys(byMethods).map((method) => `export type ${(0, import_server2.capitalize)(method)}Endpoints = EndpointByMethod["${method}"]`).join("\n")}
${endpointList.length ? `export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod];` : ""}
// </EndpointByMethod.Shorthands>
`;
return endpointByMethod + shorthands;
};
var generateApiClient = (ctx) => {
const { endpointList } = ctx;
const byMethods = (0, import_server2.groupBy)(endpointList, "method");
const endpointSchemaList = generateEndpointByMethod(ctx);
const apiClientTypes = `
// <ApiClientTypes>
export type EndpointParameters = {
body?: unknown;
query?: Record<string, unknown>;
header?: Record<string, unknown>;
path?: Record<string, unknown>;
};
export type MutationMethod = "post" | "put" | "patch" | "delete";
export type Method = "get" | "head" | "options" | MutationMethod;
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
export type DefaultEndpoint = {
parameters?: EndpointParameters | undefined;
response: unknown;
};
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
operationId: string;
method: Method;
path: string;
requestFormat: RequestFormat;
parameters?: TConfig["parameters"];
meta: {
alias: string;
hasParameters: boolean;
areParametersRequired: boolean;
};
response: TConfig["response"];
};
type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Endpoint["response"]>;
type RequiredKeys<T> = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
// </ApiClientTypes>
`;
const apiClient = `
// <ApiClient>
export class ApiClient {
baseUrl: string = "";
constructor(public fetcher: Fetcher) {}
setBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl;
return this;
}
${Object.entries(byMethods).map(([method, endpointByMethod]) => {
const capitalizedMethod = (0, import_server2.capitalize)(method);
const infer = inferByRuntime[ctx.runtime];
return endpointByMethod.length ? `// <ApiClient.${method}>
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<${(0, import_ts_pattern.match)(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)}>
): Promise<${(0, import_ts_pattern.match)(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}> {
return this.fetcher("${method}", this.baseUrl + path, params[0])${(0, import_ts_pattern.match)(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`).otherwise(() => `as Promise<TEndpoint["response"]>`)};
}
// </ApiClient.${method}>
` : "";
}).join("\n")}
}
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
}
/**
Example usage:
const api = createApiClient((method, url, params) =>
fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
);
api.get("/users").then((users) => console.log(users));
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
*/
// </ApiClient
`;
return endpointSchemaList + apiClientTypes + apiClient;
};
// src/map-openapi-endpoints.ts
var import_server4 = require("pastable/server");
// src/ref-resolver.ts
var import_server3 = require("pastable/server");
// src/topological-sort.ts
function topologicalSort(graph) {
const sorted = [], visited = {};
function visit(name2, ancestors) {
if (!Array.isArray(ancestors))
ancestors = [];
ancestors.push(name2);
visited[name2] = true;
const deps = graph.get(name2);
if (deps) {
deps.forEach((dep) => {
if (ancestors.includes(dep)) {
return;
}
if (visited[dep])
return;
visit(dep, ancestors.slice(0));
});
}
if (!sorted.includes(name2))
sorted.push(name2);
}
graph.forEach((_, name2) => visit(name2, []));
return sorted;
}
// src/ref-resolver.ts
var autocorrectRef = (ref) => ref[1] === "/" ? ref : "#/" + ref.slice(1);
var componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
var createRefResolver = (doc, factory2) => {
const nameByRef = /* @__PURE__ */ new Map();
const refByName = /* @__PURE__ */ new Map();
const byRef = /* @__PURE__ */ new Map();
const byNormalized = /* @__PURE__ */ new Map();
const boxByRef = /* @__PURE__ */ new Map();
const getSchemaByRef = (ref) => {
const correctRef = autocorrectRef(ref);
const split = correctRef.split("/");
const path = split.slice(1, -1).join("/");
const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
const map = (0, import_server3.get)(doc, normalizedPath) ?? {};
const name2 = split[split.length - 1];
const normalized = normalizeString(name2);
nameByRef.set(correctRef, normalized);
refByName.set(normalized, correctRef);
const infos = { ref: correctRef, name: name2, normalized, kind: normalizedPath.split(".")[1] };
byRef.set(infos.ref, infos);
byNormalized.set(infos.normalized, infos);
const schema = map[name2];
if (!schema) {
throw new Error(`Unresolved ref "${name2}" not found in "${path}"`);
}
return schema;
};
const getInfosByRef = (ref) => byRef.get(autocorrectRef(ref));
const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key));
schemaEntries.forEach(([key, component]) => {
Object.keys(component).map((name2) => {
const ref = `#/components/${key}/${name2}`;
getSchemaByRef(ref);
});
});
const directDependencies = /* @__PURE__ */ new Map();
schemaEntries.forEach(([key, component]) => {
Object.keys(component).map((name2) => {
const ref = `#/components/${key}/${name2}`;
const schema = getSchemaByRef(ref);
boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory: factory2, refs: { getInfosByRef } } }));
if (!directDependencies.has(ref)) {
directDependencies.set(ref, /* @__PURE__ */ new Set());
}
setSchemaDependencies(schema, directDependencies.get(ref));
});
});
const transitiveDependencies = getTransitiveDependencies(directDependencies);
return {
get: getSchemaByRef,
unwrap: (component) => {
return isReferenceObject(component) ? getSchemaByRef(component.$ref) : component;
},
getInfosByRef,
infos: byRef,
/**
* Get the schemas in the order they should be generated, depending on their dependencies
* so that a schema is generated before the ones that depend on it
*/
getOrderedSchemas: () => {
const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => {
const infos = getInfosByRef(ref);
return [boxByRef.get(infos.ref), infos];
});
return schemaOrderedByDependencies;
},
directDependencies,
transitiveDependencies
};
};
var setSchemaDependencies = (schema, deps) => {
const visit = (schema2) => {
if (!schema2)
return;
if (isReferenceObject(schema2)) {
deps.add(schema2.$ref);
return;
}
if (schema2.allOf) {
for (const allOf of schema2.allOf) {
visit(allOf);
}
return;
}
if (schema2.oneOf) {
for (const oneOf of schema2.oneOf) {
visit(oneOf);
}
return;
}
if (schema2.anyOf) {
for (const anyOf of schema2.anyOf) {
visit(anyOf);
}
return;
}
if (schema2.type === "array") {
if (!schema2.items)
return;
return void visit(schema2.items);
}
if (schema2.type === "object" || schema2.properties || schema2.additionalProperties) {
if (schema2.properties) {
for (const property in schema2.properties) {
visit(schema2.properties[property]);
}
}
if (schema2.additionalProperties && typeof schema2.additionalProperties === "object") {
visit(schema2.additionalProperties);
}
}
};
visit(schema);
};
var getTransitiveDependencies = (directDependencies) => {
const transitiveDependencies = /* @__PURE__ */ new Map();
const visitedsDeepRefs = /* @__PURE__ */ new Set();
directDependencies.forEach((deps, ref) => {
if (!transitiveDependencies.has(ref)) {
transitiveDependencies.set(ref, /* @__PURE__ */ new Set());
}
const visit = (depRef) => {
transitiveDependencies.get(ref).add(depRef);
const deps2 = directDependencies.get(depRef);
if (deps2 && ref !== depRef) {
deps2.forEach((transitive) => {
const key = ref + "__" + transitive;
if (visitedsDeepRefs.has(key))
return;
visitedsDeepRefs.add(key);
visit(transitive);
});
}
};
deps.forEach((dep) => visit(dep));
});
return transitiveDependencies;
};
// src/ts-factory.ts
var tsFactory = createFactory({
union: (types) => types.map(unwrap).join(" | "),
intersection: (types) => types.map(unwrap).join(" & "),
array: (type3) => `Array<${unwrap(type3)}>`,
optional: (type3) => `${unwrap(type3)} | undefined`,
reference: (name2, typeArgs) => `${name2}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`,
literal: (value) => value.toString(),
string: () => "string",
number: () => "number",
boolean: () => "boolean",
unknown: () => "unknown",
any: () => "any",
never: () => "never",
object: (props) => {
const propsString = Object.entries(props).map(
([prop, type3]) => `${wrapWithQuotesIfNeeded(prop)}${typeof type3 !== "string" && Box.isOptional(type3) ? "?" : ""}: ${unwrap(
type3
)}`
).join(", ");
return `{ ${propsString} }`;
}
});
// src/map-openapi-endpoints.ts
var import_ts_pattern2 = require("ts-pattern");
var factory = tsFactory;
var mapOpenApiEndpoints = (doc) => {
const refs = createRefResolver(doc, factory);
const ctx = { refs, factory };
const endpointList = [];
Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => {
const pathItem = (0, import_server4.pick)(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
Object.entries(pathItem).forEach(([method, operation]) => {
if (operation.deprecated)
return;
const endpoint = {
operation,
method,
path,
requestFormat: "json",
response: openApiSchemaToTs({ schema: {}, ctx }),
meta: {
alias: getAlias({ path, method, operation }),
areParametersRequired: false,
hasParameters: false
}
};
const lists = { query: [], path: [], header: [] };
const paramObjects = [...pathItemObj.parameters ?? [], ...operation.parameters ?? []].reduce(
(acc, paramOrRef) => {
const param = refs.unwrap(paramOrRef);
const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx });
if (param.required)
endpoint.meta.areParametersRequired = true;
endpoint.meta.hasParameters = true;
if (param.in === "query") {
lists.query.push(param);
acc.query[param.name] = schema;
}
if (param.in === "path") {
lists.path.push(param);
acc.path[param.name] = schema;
}
if (param.in === "header") {
lists.header.push(param);
acc.header[param.name] = schema;
}
return acc;
},
{ query: {}, path: {}, header: {} }
);
const params = Object.entries(paramObjects).reduce((acc, [key, value]) => {
if (Object.keys(value).length) {
acc[key] = value;
}
return acc;
}, {});
if (operation.requestBody) {
endpoint.meta.hasParameters = true;
const requestBody = refs.unwrap(operation.requestBody ?? {});
const content2 = requestBody.content;
const matchingMediaType = Object.keys(content2).find(isAllowedParamMediaTypes);
if (matchingMediaType && content2[matchingMediaType]) {
params.body = openApiSchemaToTs({
schema: content2[matchingMediaType]?.schema ?? {} ?? {},
ctx
});
}
endpoint.requestFormat = (0, import_ts_pattern2.match)(matchingMediaType).with("application/octet-stream", () => "binary").with("multipart/form-data", () => "form-data").with("application/x-www-form-urlencoded", () => "form-url").with(import_ts_pattern2.P.string.includes("json"), () => "json").otherwise(() => "text");
}
if (params) {
const t = createBoxFactory({}, ctx);
const filtered_params = ["query", "path", "header"];
for (const k of filtered_params) {
if (params[k] && lists[k].length) {
if (lists[k].every((param) => !param.required)) {
params[k] = t.reference("Partial", [t.object(params[k])]);
} else {
for (const p of lists[k]) {
if (!p.required) {
params[k][p.name] = t.optional(params[k][p.name]);
}
}
}
}
}
endpoint.parameters = Object.keys(params).length ? params : void 0;
}
let responseObject;
Object.entries(operation.responses).map(([status, responseOrRef]) => {
const statusCode = Number(status);
if (statusCode >= 200 && statusCode < 300) {
responseObject = refs.unwrap(responseOrRef);
}
});
if (!responseObject && operation.responses.default) {
responseObject = refs.unwrap(operation.responses.default);
}
const content = responseObject?.content;
if (content) {
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
if (matchingMediaType && content[matchingMediaType]) {
endpoint.response = openApiSchemaToTs({
schema: content[matchingMediaType]?.schema ?? {} ?? {},
ctx
});
}
}
endpointList.push(endpoint);
});
});
return { doc, refs, endpointList, factory };
};
var allowedParamMediaTypes = [
"application/octet-stream",
"multipart/form-data",
"application/x-www-form-urlencoded",
"*/*"
];
var isAllowedParamMediaTypes = (mediaType) => mediaType.includes("application/") && mediaType.includes("json") || allowedParamMediaTypes.includes(mediaType) || mediaType.includes("text/");
var isResponseMediaType = (mediaType) => mediaType === "application/json";
var getAlias = ({ path, method, operation }) => (method + "_" + (0, import_server4.capitalize)(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__");
// src/cli.ts
var cwd = process.cwd();
var cli = (0, import_cac.cac)(name);
var now = /* @__PURE__ */ new Date();
var optionsSchema = (0, import_arktype2.type)({ "output?": "string", runtime: allowedRuntimes });
cli.command("<input>", "Generate").option("-o, --output <path>", "Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)").option(
"-r, --runtime <name>",
`Runtime to use for validation; defaults to \`none\`; available: ${allowedRuntimes.definition}`,
{ default: "none" }
).action(async (input, _options) => {
const options = optionsSchema.assert(_options);
const openApiDoc = await import_swagger_parser.default.bundle(input);
const ctx = mapOpenApiEndpoints(openApiDoc);
console.log(`Found ${ctx.endpointList.length} endpoints`);
const content = generateFile({ ...ctx, runtime: options.runtime });
const output = (0, import_pathe.join)(
cwd,
options.output ?? input + `.${options.runtime === "none" ? "client" : options.runtime}.ts`
);
console.log("Generating...", output);
await (0, import_promises.writeFile)(output, content);
console.log(`Done in ${(/* @__PURE__ */ new Date()).getTime() - now.getTime()}ms !`);
});
cli.help();
cli.version(version);
cli.parse();