@moznion/openapi-fetch-gen
Version:
Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript
285 lines (284 loc) • 11.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateClient = generateClient;
const node_child_process_1 = require("node:child_process");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const tmp_1 = __importDefault(require("tmp"));
const ts_morph_1 = require("ts-morph");
function generateClient(schemaFilePath, options = {}) {
const project = new ts_morph_1.Project({
compilerOptions: {
target: ts_morph_1.ScriptTarget.Latest,
module: ts_morph_1.ModuleKind.ESNext,
},
});
const schemaFile = project.addSourceFileAtPath(schemaFilePath);
const pathsInterface = findInterface(schemaFile, "paths");
if (!pathsInterface) {
throw new Error(`Interface "paths" not found in ${schemaFilePath}`);
}
const componentsInterface = findInterface(schemaFile, "components");
return generateClientCode(pathsInterface, componentsInterface, node_path_1.default.basename(schemaFilePath), options);
}
function findInterface(sourceFile, interfaceName) {
return sourceFile.getInterfaces().find((i) => i.getName() === interfaceName);
}
function generateClientCode(pathsInterface, componentsInterface, schemaFileName, options = {}) {
const endpoints = extractEndpointsInfo(pathsInterface);
const clientClass = generateClientClass(endpoints, options);
const code = [
"// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen.",
"// DO NOT EDIT THIS FILE MANUALLY.",
"// See Also: https://github.com/moznion/openapi-fetch-gen",
`import createClient, { type ClientOptions } from "openapi-fetch";`,
`import type { components, paths } from "./${schemaFileName}"; // generated by openapi-typescript`,
"",
clientClass,
"",
...generateSchemaTypes(componentsInterface),
].join("\n");
const tempFile = tmp_1.default.fileSync({ postfix: ".ts" }).name;
node_fs_1.default.writeFileSync(tempFile, code);
(0, node_child_process_1.execSync)(`npx biome check --write ${tempFile}`);
return node_fs_1.default.readFileSync(tempFile, "utf-8");
}
function generateSchemaTypes(componentsInterface) {
const schemasProperty = componentsInterface?.getProperty("schemas");
const types = [];
if (schemasProperty) {
const schemasType = schemasProperty.getType();
for (const schema of schemasType.getProperties()) {
const schemaType = schema.getValueDeclaration()?.getType();
if (schemaType) {
const schemaName = schema.getName();
const schemaTypeName = (schemaName + (schemaName === "Client" ? "Schema" : "")).replaceAll(/-/g, "_");
types.push(`export type ${schemaTypeName} = components["schemas"]["${schema.getName()}"];`);
}
}
}
return types;
}
function extractEndpointsInfo(pathsInterface) {
const endpoints = [];
for (const property of pathsInterface.getProperties()) {
const path = property.getName().replace(/['"]/g, "");
const propertyType = property.getType();
const httpMethods = [
"get",
"post",
"put",
"delete",
"patch",
"head",
"options",
];
for (const httpMethod of httpMethods) {
const methodProperty = propertyType.getProperty(httpMethod);
if (!methodProperty) {
continue;
}
if (methodProperty.getTypeAtLocation(property).getText() === "never") {
continue;
}
const commentLines = [];
let paramsType = null;
let requestBodyType = null;
const declarations = methodProperty.getDeclarations();
if (declarations.length <= 0) {
continue;
}
const decl = declarations[0];
if (!ts_morph_1.Node.isPropertySignature(decl)) {
continue;
}
const typeNode = decl.getTypeNodeOrThrow();
const operationId = (() => {
if (ts_morph_1.Node.isIndexedAccessTypeNode(typeNode) &&
typeNode.getObjectTypeNode().getText() === "operations") {
const indexTypeNode = typeNode.getIndexTypeNode();
if (ts_morph_1.Node.isLiteralTypeNode(indexTypeNode) &&
indexTypeNode.getLiteral().getKind() === ts_morph_1.SyntaxKind.StringLiteral) {
return sanitizeForOperation(indexTypeNode.getLiteral().getText().slice(1, -1));
}
}
return null;
})();
const paramProp = decl.getType().getPropertyOrThrow("parameters");
const paramTypes = paramProp
.getTypeAtLocation(decl)
.getProperties()
.map((prop) => {
const propType = prop.getTypeAtLocation(decl);
const text = propType.getText();
const name = prop.getName();
if (text === "never") {
return { name, text: "" };
}
return { name, text };
})
.filter((kv) => kv["text"] !== "");
const headerType = paramTypes.find((kv) => kv.name === "header");
if (headerType) {
const nonHeaderParams = paramTypes
.filter((kv) => kv.name !== "header")
.map((kv) => `${kv.name}: ${kv.text}`)
.join("\n");
paramsType =
`[
Exclude< // Missed Header Keys for default headers
keyof ${headerType["text"]},
Extract< // Provided header keys by default headers' keys
keyof HT, keyof ${headerType["text"]}
>
>,
] extends [never] ? ` +
`{
header?: ${headerType["text"]},
${nonHeaderParams}
} : ` +
`{
header:
| (Pick< // Pick the header keys that are not in the default headers
${headerType["text"]},
Exclude< // Missed Header Keys for default headers
keyof ${headerType["text"]},
Extract< // Provided header keys by default headers' keys
keyof HT, keyof ${headerType["text"]}
>
>
> &
Partial< // Disallow default headers' keys to be in the header param
Record<
Extract< // Provided header keys by default headers' keys
keyof HT, keyof ${headerType["text"]}
>,
never
>
>)
| ${headerType["text"]},
${nonHeaderParams}
}
`;
}
else {
const params = paramTypes
.map((kv) => `${kv.name}: ${kv.text}`)
.join("\n");
if (params !== "") {
paramsType = `{${params}}`;
}
}
const requestBodyProp = decl.getType().getProperty("requestBody");
if (requestBodyProp) {
const t = requestBodyProp.getTypeAtLocation(decl);
if (t.getText() !== "never") {
const contentProp = t.getPropertyOrThrow("content");
const contentType = contentProp.getTypeAtLocation(decl);
const contentTypeProps = contentType.getProperties();
if (contentTypeProps.length > 0 && contentTypeProps[0]) {
requestBodyType = contentTypeProps[0]
.getTypeAtLocation(decl)
.getText();
}
}
}
if ("getJsDocs" in decl && typeof decl.getJsDocs === "function") {
const jsDocs = decl.getJsDocs();
for (const d of jsDocs) {
const description = d.getDescription().trim();
if (description) {
commentLines.push(`* ${description}`);
}
}
}
const sanitizedPath = sanitizeForOperation(path);
const pathSegments = sanitizedPath.split("_");
const camelCasePath = pathSegments
.map((segment, index) => {
if (index === 0)
return segment.toLowerCase();
return (segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase());
})
.join("");
const defaultOperationName = `${httpMethod}${camelCasePath.charAt(0).toUpperCase()}${camelCasePath.slice(1)}`;
endpoints.push({
path,
httpMethod,
operationName: defaultOperationName,
operationId,
commentLines,
paramsType,
bodyType: requestBodyType,
headerType: headerType ? headerType["text"] : null,
});
}
}
return endpoints;
}
function generateClientClass(endpoints, options = {}) {
const classCode = [
`export class Client<HT extends Record<string, string>> {
private readonly client;
private readonly defaultHeaders: HT;
constructor(clientOptions: ClientOptions, defaultHeaders?: HT) {
this.client = createClient<paths>(clientOptions);
this.defaultHeaders = defaultHeaders ?? ({} as HT);
}
`,
];
for (const endpoint of endpoints) {
const { path, httpMethod, operationName, operationId, commentLines, paramsType, bodyType, } = endpoint;
const methodName = options.useOperationId && operationId ? operationId : operationName;
if (commentLines.length > 0) {
classCode.push(" /**");
for (const line of commentLines) {
classCode.push(` ${line}`);
}
classCode.push(" */");
}
classCode.push(` async ${methodName}(`);
const paramsList = [];
if (paramsType) {
paramsList.push(`params: ${paramsType}`);
}
if (bodyType) {
paramsList.push(`body: ${bodyType}`);
}
if (paramsList.length > 0) {
classCode.push(paramsList.join(",\n"));
}
classCode.push(" ) {");
classCode.push(` return await this.client.${httpMethod.toUpperCase()}("${path}", {`);
if (paramsType) {
if (endpoint.headerType) {
classCode.push(`params: {
...params,
header: {...this.defaultHeaders, ...params.header} as ${endpoint.headerType},
},`);
}
else {
classCode.push("params,");
}
}
if (bodyType) {
classCode.push(" body,");
}
classCode.push(" });");
classCode.push(" }");
classCode.push("");
}
classCode.push("}");
classCode.push("");
return classCode.join("\n");
}
function sanitizeForOperation(operation) {
return operation
.replace(/[{}]/g, "")
.replace(/[-/.]/g, "_")
.replace(/^_/, "");
}
//# sourceMappingURL=index.js.map