@cushin/api-codegen 5.0.7 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +650 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.js +648 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ async function loadConfig(configPath) {
|
|
|
25
25
|
const rootDir = path.dirname(result.filepath);
|
|
26
26
|
const endpointsPath = path.resolve(rootDir, userConfig.endpoints);
|
|
27
27
|
const outputDir = path.resolve(rootDir, userConfig.output);
|
|
28
|
+
const swaggerSourcePath = userConfig.swaggerSource ? path.resolve(rootDir, userConfig.swaggerSource) : void 0;
|
|
28
29
|
const generateHooks = userConfig.generateHooks ?? true;
|
|
29
30
|
const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs";
|
|
30
31
|
const generateServerQueries = userConfig.generateServerQueries ?? userConfig.provider === "nextjs";
|
|
@@ -35,6 +36,7 @@ async function loadConfig(configPath) {
|
|
|
35
36
|
rootDir,
|
|
36
37
|
endpointsPath,
|
|
37
38
|
outputDir,
|
|
39
|
+
swaggerSourcePath,
|
|
38
40
|
generateHooks,
|
|
39
41
|
generateServerActions,
|
|
40
42
|
generateServerQueries,
|
|
@@ -561,7 +563,7 @@ ${this.generateApiClientMethods()}
|
|
|
561
563
|
};
|
|
562
564
|
|
|
563
565
|
/**
|
|
564
|
-
* Initialize API client with auth callbacks
|
|
566
|
+
* Initialize API client with auth callbacks and optional global headers
|
|
565
567
|
* Call this function in your auth provider setup
|
|
566
568
|
*
|
|
567
569
|
* @example
|
|
@@ -573,10 +575,16 @@ ${this.generateApiClientMethods()}
|
|
|
573
575
|
* },
|
|
574
576
|
* };
|
|
575
577
|
*
|
|
576
|
-
* initializeAPIClient(authCallbacks
|
|
578
|
+
* initializeAPIClient(authCallbacks, {
|
|
579
|
+
* 'X-Client-ID': 'my-app',
|
|
580
|
+
* 'X-Environment': 'production',
|
|
581
|
+
* });
|
|
577
582
|
*/
|
|
578
|
-
export const initializeAPIClient = (
|
|
579
|
-
|
|
583
|
+
export const initializeAPIClient = (
|
|
584
|
+
authCallbacks: AuthCallbacks,
|
|
585
|
+
globalHeaders?: Record<string, string>,
|
|
586
|
+
) => {
|
|
587
|
+
baseClient = createAPIClient(apiConfig, authCallbacks, globalHeaders) as any;
|
|
580
588
|
return baseClient;
|
|
581
589
|
};
|
|
582
590
|
|
|
@@ -609,7 +617,7 @@ export let serverClient: APIClientMethods & {
|
|
|
609
617
|
serverClient = createAPIClient(apiConfig) as any;
|
|
610
618
|
|
|
611
619
|
/**
|
|
612
|
-
* Initialize server-side API client with auth callbacks
|
|
620
|
+
* Initialize server-side API client with auth callbacks and optional global headers
|
|
613
621
|
* Call this in Server Actions or Route Handlers where you need authentication
|
|
614
622
|
*
|
|
615
623
|
* @example
|
|
@@ -626,10 +634,16 @@ serverClient = createAPIClient(apiConfig) as any;
|
|
|
626
634
|
* }
|
|
627
635
|
* };
|
|
628
636
|
*
|
|
629
|
-
* initializeServerClient(authCallbacks
|
|
637
|
+
* initializeServerClient(authCallbacks, {
|
|
638
|
+
* 'X-Server-ID': 'server-1',
|
|
639
|
+
* 'X-Request-ID': requestId,
|
|
640
|
+
* });
|
|
630
641
|
*/
|
|
631
|
-
export const initializeServerClient = (
|
|
632
|
-
|
|
642
|
+
export const initializeServerClient = (
|
|
643
|
+
authCallbacks: AuthCallbacks,
|
|
644
|
+
globalHeaders?: Record<string, string>,
|
|
645
|
+
) => {
|
|
646
|
+
serverClient = createAPIClient(apiConfig, authCallbacks, globalHeaders) as any;
|
|
633
647
|
return serverClient;
|
|
634
648
|
};
|
|
635
649
|
|
|
@@ -996,11 +1010,576 @@ var CodeGenerator = class {
|
|
|
996
1010
|
|
|
997
1011
|
// src/core/codegen.ts
|
|
998
1012
|
import { fileURLToPath } from "url";
|
|
1013
|
+
import fs11 from "fs/promises";
|
|
1014
|
+
import path12 from "path";
|
|
1015
|
+
|
|
1016
|
+
// src/swagger/parser.ts
|
|
1017
|
+
import fs10 from "fs";
|
|
1018
|
+
import path11 from "path";
|
|
1019
|
+
async function parseOpenAPISpec(filePath) {
|
|
1020
|
+
const content = await fs10.promises.readFile(filePath, "utf-8");
|
|
1021
|
+
const ext = path11.extname(filePath).toLowerCase();
|
|
1022
|
+
let spec;
|
|
1023
|
+
if (ext === ".json") {
|
|
1024
|
+
spec = JSON.parse(content);
|
|
1025
|
+
} else if (ext === ".yaml" || ext === ".yml") {
|
|
1026
|
+
const yaml = await import("yaml");
|
|
1027
|
+
spec = yaml.parse(content);
|
|
1028
|
+
} else {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`Unsupported file format: ${ext}. Only .json, .yaml, and .yml are supported.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
if (!spec.openapi || !spec.openapi.startsWith("3.")) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`Unsupported OpenAPI version: ${spec.openapi}. Only OpenAPI 3.0 and 3.1 are supported.`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
return spec;
|
|
1039
|
+
}
|
|
1040
|
+
function extractEndpoints(spec) {
|
|
1041
|
+
const endpoints = [];
|
|
1042
|
+
for (const [path13, pathItem] of Object.entries(spec.paths)) {
|
|
1043
|
+
const methods = [
|
|
1044
|
+
"get",
|
|
1045
|
+
"post",
|
|
1046
|
+
"put",
|
|
1047
|
+
"delete",
|
|
1048
|
+
"patch",
|
|
1049
|
+
"head",
|
|
1050
|
+
"options"
|
|
1051
|
+
];
|
|
1052
|
+
for (const method of methods) {
|
|
1053
|
+
const operation = pathItem[method];
|
|
1054
|
+
if (!operation) continue;
|
|
1055
|
+
const endpoint = parseOperation(
|
|
1056
|
+
path13,
|
|
1057
|
+
method,
|
|
1058
|
+
operation,
|
|
1059
|
+
pathItem,
|
|
1060
|
+
spec
|
|
1061
|
+
);
|
|
1062
|
+
endpoints.push(endpoint);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return endpoints;
|
|
1066
|
+
}
|
|
1067
|
+
function parseOperation(path13, method, operation, pathItem, spec) {
|
|
1068
|
+
const allParameters = [
|
|
1069
|
+
...pathItem.parameters || [],
|
|
1070
|
+
...operation.parameters || []
|
|
1071
|
+
];
|
|
1072
|
+
const pathParams = [];
|
|
1073
|
+
const queryParams = [];
|
|
1074
|
+
for (const param of allParameters) {
|
|
1075
|
+
const resolved = resolveParameter(param, spec);
|
|
1076
|
+
const parsed = {
|
|
1077
|
+
name: resolved.name,
|
|
1078
|
+
type: schemaToTypeString(resolved.schema),
|
|
1079
|
+
required: resolved.required ?? resolved.in === "path",
|
|
1080
|
+
description: resolved.description,
|
|
1081
|
+
schema: resolved.schema
|
|
1082
|
+
// Keep original schema
|
|
1083
|
+
};
|
|
1084
|
+
if (resolved.in === "path") {
|
|
1085
|
+
pathParams.push(parsed);
|
|
1086
|
+
} else if (resolved.in === "query") {
|
|
1087
|
+
queryParams.push(parsed);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
let requestBody;
|
|
1091
|
+
if (operation.requestBody) {
|
|
1092
|
+
const resolved = resolveRequestBody(operation.requestBody, spec);
|
|
1093
|
+
const content = resolved.content?.["application/json"];
|
|
1094
|
+
if (content) {
|
|
1095
|
+
requestBody = {
|
|
1096
|
+
type: schemaToTypeString(content.schema),
|
|
1097
|
+
required: resolved.required ?? false,
|
|
1098
|
+
description: resolved.description,
|
|
1099
|
+
schema: content.schema
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
let response;
|
|
1104
|
+
const responses = operation.responses;
|
|
1105
|
+
const successStatus = responses["200"] || responses["201"] || responses["204"];
|
|
1106
|
+
if (successStatus) {
|
|
1107
|
+
const statusCode = responses["200"] ? "200" : responses["201"] ? "201" : "204";
|
|
1108
|
+
const resolved = resolveResponse(successStatus, spec);
|
|
1109
|
+
const content = resolved.content?.["application/json"];
|
|
1110
|
+
if (content || statusCode === "204") {
|
|
1111
|
+
response = {
|
|
1112
|
+
statusCode,
|
|
1113
|
+
type: content ? schemaToTypeString(content.schema) : "void",
|
|
1114
|
+
description: resolved.description,
|
|
1115
|
+
schema: content?.schema || { type: "null" }
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (!response) {
|
|
1120
|
+
for (const [statusCode, res] of Object.entries(responses)) {
|
|
1121
|
+
if (statusCode.startsWith("2")) {
|
|
1122
|
+
const resolved = resolveResponse(res, spec);
|
|
1123
|
+
const content = resolved.content?.["application/json"];
|
|
1124
|
+
response = {
|
|
1125
|
+
statusCode,
|
|
1126
|
+
type: content ? schemaToTypeString(content.schema) : "void",
|
|
1127
|
+
description: resolved.description,
|
|
1128
|
+
schema: content?.schema || { type: "null" }
|
|
1129
|
+
};
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
path: path13,
|
|
1136
|
+
method: method.toUpperCase(),
|
|
1137
|
+
operationId: operation.operationId,
|
|
1138
|
+
summary: operation.summary,
|
|
1139
|
+
description: operation.description,
|
|
1140
|
+
tags: operation.tags,
|
|
1141
|
+
pathParams: pathParams.length > 0 ? pathParams : void 0,
|
|
1142
|
+
queryParams: queryParams.length > 0 ? queryParams : void 0,
|
|
1143
|
+
requestBody,
|
|
1144
|
+
response
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function resolveParameter(param, spec) {
|
|
1148
|
+
if ("$ref" in param && param.$ref) {
|
|
1149
|
+
const refPath = param.$ref.replace("#/components/parameters/", "");
|
|
1150
|
+
const resolved = spec.components?.parameters?.[refPath];
|
|
1151
|
+
if (!resolved) {
|
|
1152
|
+
throw new Error(`Cannot resolve parameter reference: ${param.$ref}`);
|
|
1153
|
+
}
|
|
1154
|
+
return resolved;
|
|
1155
|
+
}
|
|
1156
|
+
return param;
|
|
1157
|
+
}
|
|
1158
|
+
function resolveRequestBody(requestBody, spec) {
|
|
1159
|
+
if ("$ref" in requestBody && requestBody.$ref) {
|
|
1160
|
+
const refPath = requestBody.$ref.replace("#/components/requestBodies/", "");
|
|
1161
|
+
const resolved = spec.components?.requestBodies?.[refPath];
|
|
1162
|
+
if (!resolved) {
|
|
1163
|
+
throw new Error(`Cannot resolve request body reference: ${requestBody.$ref}`);
|
|
1164
|
+
}
|
|
1165
|
+
return resolved;
|
|
1166
|
+
}
|
|
1167
|
+
return requestBody;
|
|
1168
|
+
}
|
|
1169
|
+
function resolveResponse(response, spec) {
|
|
1170
|
+
if ("$ref" in response && response.$ref) {
|
|
1171
|
+
const refPath = response.$ref.replace("#/components/responses/", "");
|
|
1172
|
+
const resolved = spec.components?.responses?.[refPath];
|
|
1173
|
+
if (!resolved) {
|
|
1174
|
+
throw new Error(`Cannot resolve response reference: ${response.$ref}`);
|
|
1175
|
+
}
|
|
1176
|
+
return resolved;
|
|
1177
|
+
}
|
|
1178
|
+
return response;
|
|
1179
|
+
}
|
|
1180
|
+
function schemaToTypeString(schema) {
|
|
1181
|
+
if (!schema) return "any";
|
|
1182
|
+
if (schema.$ref) {
|
|
1183
|
+
const parts = schema.$ref.split("/");
|
|
1184
|
+
return parts[parts.length - 1];
|
|
1185
|
+
}
|
|
1186
|
+
if (schema.type) {
|
|
1187
|
+
switch (schema.type) {
|
|
1188
|
+
case "string":
|
|
1189
|
+
return "string";
|
|
1190
|
+
case "number":
|
|
1191
|
+
case "integer":
|
|
1192
|
+
return "number";
|
|
1193
|
+
case "boolean":
|
|
1194
|
+
return "boolean";
|
|
1195
|
+
case "array":
|
|
1196
|
+
if (schema.items) {
|
|
1197
|
+
return `${schemaToTypeString(schema.items)}[]`;
|
|
1198
|
+
}
|
|
1199
|
+
return "any[]";
|
|
1200
|
+
case "object":
|
|
1201
|
+
return "object";
|
|
1202
|
+
default:
|
|
1203
|
+
return "any";
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (schema.allOf) {
|
|
1207
|
+
return schema.allOf.map(schemaToTypeString).join(" & ");
|
|
1208
|
+
}
|
|
1209
|
+
if (schema.oneOf || schema.anyOf) {
|
|
1210
|
+
const schemas = schema.oneOf || schema.anyOf;
|
|
1211
|
+
return schemas.map(schemaToTypeString).join(" | ");
|
|
1212
|
+
}
|
|
1213
|
+
return "any";
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/swagger/config-generator.ts
|
|
1217
|
+
function groupEndpointsByTags(endpoints) {
|
|
1218
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const endpoint of endpoints) {
|
|
1220
|
+
const tag = endpoint.tags && endpoint.tags.length > 0 ? endpoint.tags[0] : "default";
|
|
1221
|
+
if (!grouped.has(tag)) {
|
|
1222
|
+
grouped.set(tag, []);
|
|
1223
|
+
}
|
|
1224
|
+
grouped.get(tag).push(endpoint);
|
|
1225
|
+
}
|
|
1226
|
+
return grouped;
|
|
1227
|
+
}
|
|
1228
|
+
function generateConfigFile(endpoints, spec, baseUrl) {
|
|
1229
|
+
const lines = [];
|
|
1230
|
+
lines.push('import { defineConfig, defineEndpoint } from "@cushin/api-runtime";');
|
|
1231
|
+
lines.push('import { z } from "zod";');
|
|
1232
|
+
lines.push("");
|
|
1233
|
+
if (spec.components?.schemas) {
|
|
1234
|
+
lines.push("// Schema definitions from OpenAPI components");
|
|
1235
|
+
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
1236
|
+
const zodSchema = convertSchemaToZod(schema, spec);
|
|
1237
|
+
lines.push(`const ${name}Schema = ${zodSchema};`);
|
|
1238
|
+
}
|
|
1239
|
+
lines.push("");
|
|
1240
|
+
}
|
|
1241
|
+
lines.push("// Endpoint definitions");
|
|
1242
|
+
const endpointNames = [];
|
|
1243
|
+
for (const endpoint of endpoints) {
|
|
1244
|
+
const name = generateEndpointName(endpoint);
|
|
1245
|
+
endpointNames.push(name);
|
|
1246
|
+
const path13 = convertPathFormat(endpoint.path);
|
|
1247
|
+
lines.push(`const ${name} = defineEndpoint({`);
|
|
1248
|
+
lines.push(` path: "${path13}",`);
|
|
1249
|
+
lines.push(` method: "${endpoint.method}",`);
|
|
1250
|
+
if (endpoint.pathParams && endpoint.pathParams.length > 0) {
|
|
1251
|
+
const paramsSchema = generateParamsSchema(endpoint.pathParams, spec);
|
|
1252
|
+
lines.push(` params: ${paramsSchema},`);
|
|
1253
|
+
}
|
|
1254
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
1255
|
+
const querySchema = generateQuerySchema(endpoint.queryParams, spec);
|
|
1256
|
+
lines.push(` query: ${querySchema},`);
|
|
1257
|
+
}
|
|
1258
|
+
if (endpoint.requestBody) {
|
|
1259
|
+
const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec);
|
|
1260
|
+
lines.push(` body: ${bodySchema},`);
|
|
1261
|
+
}
|
|
1262
|
+
if (endpoint.response) {
|
|
1263
|
+
const responseSchema = convertSchemaToZod(endpoint.response.schema, spec);
|
|
1264
|
+
lines.push(` response: ${responseSchema},`);
|
|
1265
|
+
} else {
|
|
1266
|
+
lines.push(` response: z.any(),`);
|
|
1267
|
+
}
|
|
1268
|
+
if (endpoint.tags && endpoint.tags.length > 0) {
|
|
1269
|
+
const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", ");
|
|
1270
|
+
lines.push(` tags: [${tagsStr}],`);
|
|
1271
|
+
}
|
|
1272
|
+
if (endpoint.description || endpoint.summary) {
|
|
1273
|
+
const desc = endpoint.description || endpoint.summary;
|
|
1274
|
+
lines.push(` description: "${escapeString(desc)}",`);
|
|
1275
|
+
}
|
|
1276
|
+
lines.push("});");
|
|
1277
|
+
lines.push("");
|
|
1278
|
+
}
|
|
1279
|
+
lines.push("// API Configuration");
|
|
1280
|
+
lines.push("export const apiConfig = defineConfig({");
|
|
1281
|
+
if (baseUrl) {
|
|
1282
|
+
lines.push(` baseUrl: "${baseUrl}",`);
|
|
1283
|
+
} else if (spec.servers && spec.servers.length > 0) {
|
|
1284
|
+
lines.push(` baseUrl: "${spec.servers[0].url}",`);
|
|
1285
|
+
}
|
|
1286
|
+
lines.push(" endpoints: {");
|
|
1287
|
+
for (const name of endpointNames) {
|
|
1288
|
+
lines.push(` ${name},`);
|
|
1289
|
+
}
|
|
1290
|
+
lines.push(" },");
|
|
1291
|
+
lines.push("});");
|
|
1292
|
+
lines.push("");
|
|
1293
|
+
return lines.join("\n");
|
|
1294
|
+
}
|
|
1295
|
+
function generateEndpointName(endpoint) {
|
|
1296
|
+
if (endpoint.operationId) {
|
|
1297
|
+
return toCamelCase(endpoint.operationId);
|
|
1298
|
+
}
|
|
1299
|
+
const method = endpoint.method.toLowerCase();
|
|
1300
|
+
const pathParts = endpoint.path.split("/").filter((p) => p && !p.startsWith(":")).map((p) => p.replace(/[^a-zA-Z0-9]/g, ""));
|
|
1301
|
+
const parts = [method, ...pathParts];
|
|
1302
|
+
return toCamelCase(parts.join("_"));
|
|
1303
|
+
}
|
|
1304
|
+
function toCamelCase(str) {
|
|
1305
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toLowerCase());
|
|
1306
|
+
}
|
|
1307
|
+
function generateParamsSchema(params, spec) {
|
|
1308
|
+
const props = [];
|
|
1309
|
+
for (const param of params) {
|
|
1310
|
+
const zodType = convertSchemaToZod(param.schema, spec);
|
|
1311
|
+
props.push(` ${param.name}: ${zodType}`);
|
|
1312
|
+
}
|
|
1313
|
+
return `z.object({
|
|
1314
|
+
${props.join(",\n")}
|
|
1315
|
+
})`;
|
|
1316
|
+
}
|
|
1317
|
+
function generateQuerySchema(params, spec) {
|
|
1318
|
+
const props = [];
|
|
1319
|
+
for (const param of params) {
|
|
1320
|
+
let zodType = convertSchemaToZod(param.schema, spec);
|
|
1321
|
+
if (!param.required) {
|
|
1322
|
+
zodType += ".optional()";
|
|
1323
|
+
}
|
|
1324
|
+
props.push(` ${param.name}: ${zodType}`);
|
|
1325
|
+
}
|
|
1326
|
+
return `z.object({
|
|
1327
|
+
${props.join(",\n")}
|
|
1328
|
+
})`;
|
|
1329
|
+
}
|
|
1330
|
+
function convertSchemaToZod(schema, spec) {
|
|
1331
|
+
if (!schema) return "z.any()";
|
|
1332
|
+
if (schema.$ref) {
|
|
1333
|
+
const refName = schema.$ref.split("/").pop();
|
|
1334
|
+
if (refName && spec.components?.schemas?.[refName]) {
|
|
1335
|
+
return `${refName}Schema`;
|
|
1336
|
+
}
|
|
1337
|
+
return "z.any()";
|
|
1338
|
+
}
|
|
1339
|
+
if (schema.type) {
|
|
1340
|
+
switch (schema.type) {
|
|
1341
|
+
case "string":
|
|
1342
|
+
if (schema.enum) {
|
|
1343
|
+
const values = schema.enum.map((v) => `"${v}"`).join(", ");
|
|
1344
|
+
return `z.enum([${values}])`;
|
|
1345
|
+
}
|
|
1346
|
+
return "z.string()";
|
|
1347
|
+
case "number":
|
|
1348
|
+
case "integer":
|
|
1349
|
+
return "z.number()";
|
|
1350
|
+
case "boolean":
|
|
1351
|
+
return "z.boolean()";
|
|
1352
|
+
case "array":
|
|
1353
|
+
if (schema.items) {
|
|
1354
|
+
const itemSchema = convertSchemaToZod(schema.items, spec);
|
|
1355
|
+
return `z.array(${itemSchema})`;
|
|
1356
|
+
}
|
|
1357
|
+
return "z.array(z.any())";
|
|
1358
|
+
case "object":
|
|
1359
|
+
if (schema.properties) {
|
|
1360
|
+
const props = [];
|
|
1361
|
+
const required = schema.required || [];
|
|
1362
|
+
for (const [propName, propSchema] of Object.entries(
|
|
1363
|
+
schema.properties
|
|
1364
|
+
)) {
|
|
1365
|
+
let zodType = convertSchemaToZod(propSchema, spec);
|
|
1366
|
+
if (!required.includes(propName)) {
|
|
1367
|
+
zodType += ".optional()";
|
|
1368
|
+
}
|
|
1369
|
+
props.push(` ${propName}: ${zodType}`);
|
|
1370
|
+
}
|
|
1371
|
+
return `z.object({
|
|
1372
|
+
${props.join(",\n")}
|
|
1373
|
+
})`;
|
|
1374
|
+
}
|
|
1375
|
+
return "z.object({})";
|
|
1376
|
+
case "null":
|
|
1377
|
+
return "z.null()";
|
|
1378
|
+
default:
|
|
1379
|
+
return "z.any()";
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (schema.allOf) {
|
|
1383
|
+
const schemas = schema.allOf.map((s) => convertSchemaToZod(s, spec));
|
|
1384
|
+
return schemas.join(".and(");
|
|
1385
|
+
}
|
|
1386
|
+
if (schema.oneOf || schema.anyOf) {
|
|
1387
|
+
const schemas = (schema.oneOf || schema.anyOf).map(
|
|
1388
|
+
(s) => convertSchemaToZod(s, spec)
|
|
1389
|
+
);
|
|
1390
|
+
return `z.union([${schemas.join(", ")}])`;
|
|
1391
|
+
}
|
|
1392
|
+
return "z.any()";
|
|
1393
|
+
}
|
|
1394
|
+
function escapeString(str) {
|
|
1395
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1396
|
+
}
|
|
1397
|
+
function convertPathFormat(path13) {
|
|
1398
|
+
return path13.replace(/\{([^}]+)\}/g, ":$1");
|
|
1399
|
+
}
|
|
1400
|
+
function generateModuleFile(tag, endpoints, spec) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
lines.push('import { defineEndpoint } from "@cushin/api-runtime";');
|
|
1403
|
+
lines.push('import { z } from "zod";');
|
|
1404
|
+
lines.push("");
|
|
1405
|
+
const usedSchemas = /* @__PURE__ */ new Set();
|
|
1406
|
+
for (const endpoint of endpoints) {
|
|
1407
|
+
collectUsedSchemas(endpoint, usedSchemas, spec);
|
|
1408
|
+
}
|
|
1409
|
+
if (usedSchemas.size > 0 && spec.components?.schemas) {
|
|
1410
|
+
lines.push("// Schema definitions");
|
|
1411
|
+
const sortedSchemas = sortSchemasByDependencies(Array.from(usedSchemas), spec);
|
|
1412
|
+
for (const schemaName of sortedSchemas) {
|
|
1413
|
+
const schema = spec.components.schemas[schemaName];
|
|
1414
|
+
if (schema) {
|
|
1415
|
+
const zodSchema = convertSchemaToZod(schema, spec);
|
|
1416
|
+
lines.push(`const ${schemaName}Schema = ${zodSchema};`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
lines.push("");
|
|
1420
|
+
}
|
|
1421
|
+
lines.push("// Endpoint definitions");
|
|
1422
|
+
const endpointNames = [];
|
|
1423
|
+
for (const endpoint of endpoints) {
|
|
1424
|
+
const name = generateEndpointName(endpoint);
|
|
1425
|
+
endpointNames.push(name);
|
|
1426
|
+
const path13 = convertPathFormat(endpoint.path);
|
|
1427
|
+
lines.push(`export const ${name} = defineEndpoint({`);
|
|
1428
|
+
lines.push(` path: "${path13}",`);
|
|
1429
|
+
lines.push(` method: "${endpoint.method}",`);
|
|
1430
|
+
if (endpoint.pathParams && endpoint.pathParams.length > 0) {
|
|
1431
|
+
const paramsSchema = generateParamsSchema(endpoint.pathParams, spec);
|
|
1432
|
+
lines.push(` params: ${paramsSchema},`);
|
|
1433
|
+
}
|
|
1434
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
1435
|
+
const querySchema = generateQuerySchema(endpoint.queryParams, spec);
|
|
1436
|
+
lines.push(` query: ${querySchema},`);
|
|
1437
|
+
}
|
|
1438
|
+
if (endpoint.requestBody) {
|
|
1439
|
+
const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec);
|
|
1440
|
+
lines.push(` body: ${bodySchema},`);
|
|
1441
|
+
}
|
|
1442
|
+
if (endpoint.response) {
|
|
1443
|
+
const responseSchema = convertSchemaToZod(endpoint.response.schema, spec);
|
|
1444
|
+
lines.push(` response: ${responseSchema},`);
|
|
1445
|
+
} else {
|
|
1446
|
+
lines.push(` response: z.any(),`);
|
|
1447
|
+
}
|
|
1448
|
+
if (endpoint.tags && endpoint.tags.length > 0) {
|
|
1449
|
+
const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", ");
|
|
1450
|
+
lines.push(` tags: [${tagsStr}],`);
|
|
1451
|
+
}
|
|
1452
|
+
if (endpoint.description || endpoint.summary) {
|
|
1453
|
+
const desc = endpoint.description || endpoint.summary;
|
|
1454
|
+
lines.push(` description: "${escapeString(desc)}",`);
|
|
1455
|
+
}
|
|
1456
|
+
lines.push("});");
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
}
|
|
1459
|
+
return lines.join("\n");
|
|
1460
|
+
}
|
|
1461
|
+
function sortSchemasByDependencies(schemaNames, spec) {
|
|
1462
|
+
const sorted = [];
|
|
1463
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1464
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1465
|
+
function visit(name) {
|
|
1466
|
+
if (visited.has(name)) return;
|
|
1467
|
+
if (visiting.has(name)) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
visiting.add(name);
|
|
1471
|
+
const schema = spec.components?.schemas?.[name];
|
|
1472
|
+
if (schema) {
|
|
1473
|
+
const deps = /* @__PURE__ */ new Set();
|
|
1474
|
+
extractSchemaNames(schema, deps, spec);
|
|
1475
|
+
for (const dep of deps) {
|
|
1476
|
+
if (dep !== name && schemaNames.includes(dep)) {
|
|
1477
|
+
visit(dep);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
visiting.delete(name);
|
|
1482
|
+
if (!visited.has(name)) {
|
|
1483
|
+
visited.add(name);
|
|
1484
|
+
sorted.push(name);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
for (const name of schemaNames) {
|
|
1488
|
+
visit(name);
|
|
1489
|
+
}
|
|
1490
|
+
return sorted;
|
|
1491
|
+
}
|
|
1492
|
+
function collectUsedSchemas(endpoint, usedSchemas, spec) {
|
|
1493
|
+
if (endpoint.requestBody?.schema) {
|
|
1494
|
+
extractSchemaNames(endpoint.requestBody.schema, usedSchemas, spec);
|
|
1495
|
+
}
|
|
1496
|
+
if (endpoint.response?.schema) {
|
|
1497
|
+
extractSchemaNames(endpoint.response.schema, usedSchemas, spec);
|
|
1498
|
+
}
|
|
1499
|
+
if (endpoint.pathParams) {
|
|
1500
|
+
for (const param of endpoint.pathParams) {
|
|
1501
|
+
extractSchemaNames(param.schema, usedSchemas, spec);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (endpoint.queryParams) {
|
|
1505
|
+
for (const param of endpoint.queryParams) {
|
|
1506
|
+
extractSchemaNames(param.schema, usedSchemas, spec);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function extractSchemaNames(schema, names, spec) {
|
|
1511
|
+
if (!schema) return;
|
|
1512
|
+
if (schema.$ref) {
|
|
1513
|
+
const schemaName = schema.$ref.split("/").pop();
|
|
1514
|
+
if (schemaName) {
|
|
1515
|
+
names.add(schemaName);
|
|
1516
|
+
const referencedSchema = spec.components?.schemas?.[schemaName];
|
|
1517
|
+
if (referencedSchema) {
|
|
1518
|
+
extractSchemaNames(referencedSchema, names, spec);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (schema.properties) {
|
|
1524
|
+
for (const prop of Object.values(schema.properties)) {
|
|
1525
|
+
extractSchemaNames(prop, names, spec);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (schema.items) {
|
|
1529
|
+
extractSchemaNames(schema.items, names, spec);
|
|
1530
|
+
}
|
|
1531
|
+
if (schema.allOf) {
|
|
1532
|
+
for (const s of schema.allOf) {
|
|
1533
|
+
extractSchemaNames(s, names, spec);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (schema.oneOf) {
|
|
1537
|
+
for (const s of schema.oneOf) {
|
|
1538
|
+
extractSchemaNames(s, names, spec);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (schema.anyOf) {
|
|
1542
|
+
for (const s of schema.anyOf) {
|
|
1543
|
+
extractSchemaNames(s, names, spec);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function generateIndexFile(tagModules, spec, baseUrl) {
|
|
1548
|
+
const lines = [];
|
|
1549
|
+
lines.push('import { defineConfig } from "@cushin/api-runtime";');
|
|
1550
|
+
lines.push("");
|
|
1551
|
+
for (const [tag] of tagModules) {
|
|
1552
|
+
const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
1553
|
+
lines.push(`import * as ${toCamelCase(tag)}Module from "./${moduleFileName}.js";`);
|
|
1554
|
+
}
|
|
1555
|
+
lines.push("");
|
|
1556
|
+
lines.push("export const apiConfig = defineConfig({");
|
|
1557
|
+
const url = baseUrl || (spec.servers && spec.servers.length > 0 ? spec.servers[0].url : void 0);
|
|
1558
|
+
if (url) {
|
|
1559
|
+
lines.push(` baseUrl: "${url}",`);
|
|
1560
|
+
}
|
|
1561
|
+
lines.push(" endpoints: {");
|
|
1562
|
+
for (const [tag] of tagModules) {
|
|
1563
|
+
lines.push(` ...${toCamelCase(tag)}Module,`);
|
|
1564
|
+
}
|
|
1565
|
+
lines.push(" },");
|
|
1566
|
+
lines.push("});");
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
for (const [tag] of tagModules) {
|
|
1569
|
+
lines.push(`export * from "./${tag.toLowerCase().replace(/[^a-z0-9]/g, "-")}.js";`);
|
|
1570
|
+
}
|
|
1571
|
+
return lines.join("\n");
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/core/codegen.ts
|
|
999
1575
|
var CodegenCore = class {
|
|
1000
1576
|
constructor(config) {
|
|
1001
1577
|
this.config = config;
|
|
1002
1578
|
}
|
|
1003
1579
|
async execute() {
|
|
1580
|
+
if (this.config.swaggerSourcePath) {
|
|
1581
|
+
await this.generateConfigFromSwagger();
|
|
1582
|
+
}
|
|
1004
1583
|
const apiConfig = await this.loadAPIConfig();
|
|
1005
1584
|
this.config.apiConfig = apiConfig;
|
|
1006
1585
|
const generator = new CodeGenerator({
|
|
@@ -1009,6 +1588,67 @@ var CodegenCore = class {
|
|
|
1009
1588
|
});
|
|
1010
1589
|
await generator.generate();
|
|
1011
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Generate single config file (no split)
|
|
1593
|
+
*/
|
|
1594
|
+
async generateSingleConfigFile(endpoints, spec) {
|
|
1595
|
+
const configContent = generateConfigFile(endpoints, spec, this.config.baseUrl);
|
|
1596
|
+
const endpointsDir = path12.dirname(this.config.endpointsPath);
|
|
1597
|
+
await fs11.mkdir(endpointsDir, { recursive: true });
|
|
1598
|
+
await fs11.writeFile(this.config.endpointsPath, configContent, "utf-8");
|
|
1599
|
+
console.log(`\u2713 Generated endpoint config at ${this.config.endpointsPath}`);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Generate multiple module files split by tags
|
|
1603
|
+
*/
|
|
1604
|
+
async generateMultipleModuleFiles(endpoints, spec) {
|
|
1605
|
+
const grouped = groupEndpointsByTags(endpoints);
|
|
1606
|
+
console.log(`\u2713 Grouped into ${grouped.size} modules by tags`);
|
|
1607
|
+
const endpointsDir = path12.dirname(this.config.endpointsPath);
|
|
1608
|
+
const modulesDir = path12.join(endpointsDir, "modules");
|
|
1609
|
+
await fs11.mkdir(modulesDir, { recursive: true });
|
|
1610
|
+
for (const [tag, tagEndpoints] of grouped.entries()) {
|
|
1611
|
+
const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
1612
|
+
const moduleFilePath = path12.join(modulesDir, `${moduleFileName}.ts`);
|
|
1613
|
+
const moduleContent = generateModuleFile(tag, tagEndpoints, spec);
|
|
1614
|
+
await fs11.writeFile(moduleFilePath, moduleContent, "utf-8");
|
|
1615
|
+
console.log(` \u2713 ${tag}: ${tagEndpoints.length} endpoints \u2192 ${moduleFileName}.ts`);
|
|
1616
|
+
}
|
|
1617
|
+
const indexContent = generateIndexFile(grouped, spec, this.config.baseUrl);
|
|
1618
|
+
const indexPath = path12.join(modulesDir, "index.ts");
|
|
1619
|
+
await fs11.writeFile(indexPath, indexContent, "utf-8");
|
|
1620
|
+
console.log(`\u2713 Generated index.ts at ${modulesDir}/index.ts`);
|
|
1621
|
+
const mainExportContent = `export * from "./modules/index.js";
|
|
1622
|
+
`;
|
|
1623
|
+
await fs11.mkdir(endpointsDir, { recursive: true });
|
|
1624
|
+
await fs11.writeFile(this.config.endpointsPath, mainExportContent, "utf-8");
|
|
1625
|
+
console.log(`\u2713 Generated main export at ${this.config.endpointsPath}`);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Generate endpoint config file from Swagger/OpenAPI spec
|
|
1629
|
+
*/
|
|
1630
|
+
async generateConfigFromSwagger() {
|
|
1631
|
+
if (!this.config.swaggerSourcePath) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
console.log(`\u{1F4C4} Parsing Swagger spec from ${this.config.swaggerSourcePath}...`);
|
|
1636
|
+
const spec = await parseOpenAPISpec(this.config.swaggerSourcePath);
|
|
1637
|
+
console.log(`\u2713 Found OpenAPI ${spec.openapi} specification`);
|
|
1638
|
+
console.log(` Title: ${spec.info.title} (v${spec.info.version})`);
|
|
1639
|
+
const endpoints = extractEndpoints(spec);
|
|
1640
|
+
console.log(`\u2713 Extracted ${endpoints.length} endpoints`);
|
|
1641
|
+
if (this.config.splitByTags) {
|
|
1642
|
+
await this.generateMultipleModuleFiles(endpoints, spec);
|
|
1643
|
+
} else {
|
|
1644
|
+
await this.generateSingleConfigFile(endpoints, spec);
|
|
1645
|
+
}
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`Failed to generate config from Swagger: ${error instanceof Error ? error.message : String(error)}`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1012
1652
|
async loadAPIConfig() {
|
|
1013
1653
|
try {
|
|
1014
1654
|
const jiti = createJiti(fileURLToPath(import.meta.url), {
|