@cushin/api-codegen
Advanced tools
+630
-2
@@ -31,2 +31,3 @@ #!/usr/bin/env node | ||
| const outputDir = path.resolve(rootDir, userConfig.output); | ||
| const swaggerSourcePath = userConfig.swaggerSource ? path.resolve(rootDir, userConfig.swaggerSource) : void 0; | ||
| const generateHooks = userConfig.generateHooks ?? true; | ||
@@ -42,2 +43,3 @@ const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs"; | ||
| outputDir, | ||
| swaggerSourcePath, | ||
| generateHooks, | ||
@@ -1016,2 +1018,564 @@ generateServerActions, | ||
| import { fileURLToPath } from "url"; | ||
| import fs11 from "fs/promises"; | ||
| import path12 from "path"; | ||
| // src/swagger/parser.ts | ||
| import fs10 from "fs"; | ||
| import path11 from "path"; | ||
| async function parseOpenAPISpec(filePath) { | ||
| const content = await fs10.promises.readFile(filePath, "utf-8"); | ||
| const ext = path11.extname(filePath).toLowerCase(); | ||
| let spec; | ||
| if (ext === ".json") { | ||
| spec = JSON.parse(content); | ||
| } else if (ext === ".yaml" || ext === ".yml") { | ||
| const yaml = await import("yaml"); | ||
| spec = yaml.parse(content); | ||
| } else { | ||
| throw new Error( | ||
| `Unsupported file format: ${ext}. Only .json, .yaml, and .yml are supported.` | ||
| ); | ||
| } | ||
| if (!spec.openapi || !spec.openapi.startsWith("3.")) { | ||
| throw new Error( | ||
| `Unsupported OpenAPI version: ${spec.openapi}. Only OpenAPI 3.0 and 3.1 are supported.` | ||
| ); | ||
| } | ||
| return spec; | ||
| } | ||
| function extractEndpoints(spec) { | ||
| const endpoints = []; | ||
| for (const [path14, pathItem] of Object.entries(spec.paths)) { | ||
| const methods = [ | ||
| "get", | ||
| "post", | ||
| "put", | ||
| "delete", | ||
| "patch", | ||
| "head", | ||
| "options" | ||
| ]; | ||
| for (const method of methods) { | ||
| const operation = pathItem[method]; | ||
| if (!operation) continue; | ||
| const endpoint = parseOperation( | ||
| path14, | ||
| method, | ||
| operation, | ||
| pathItem, | ||
| spec | ||
| ); | ||
| endpoints.push(endpoint); | ||
| } | ||
| } | ||
| return endpoints; | ||
| } | ||
| function parseOperation(path14, method, operation, pathItem, spec) { | ||
| const allParameters = [ | ||
| ...pathItem.parameters || [], | ||
| ...operation.parameters || [] | ||
| ]; | ||
| const pathParams = []; | ||
| const queryParams = []; | ||
| for (const param of allParameters) { | ||
| const resolved = resolveParameter(param, spec); | ||
| const parsed = { | ||
| name: resolved.name, | ||
| type: schemaToTypeString(resolved.schema), | ||
| required: resolved.required ?? resolved.in === "path", | ||
| description: resolved.description, | ||
| schema: resolved.schema | ||
| // Keep original schema | ||
| }; | ||
| if (resolved.in === "path") { | ||
| pathParams.push(parsed); | ||
| } else if (resolved.in === "query") { | ||
| queryParams.push(parsed); | ||
| } | ||
| } | ||
| let requestBody; | ||
| if (operation.requestBody) { | ||
| const resolved = resolveRequestBody(operation.requestBody, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| if (content) { | ||
| requestBody = { | ||
| type: schemaToTypeString(content.schema), | ||
| required: resolved.required ?? false, | ||
| description: resolved.description, | ||
| schema: content.schema | ||
| }; | ||
| } | ||
| } | ||
| let response; | ||
| const responses = operation.responses; | ||
| const successStatus = responses["200"] || responses["201"] || responses["204"]; | ||
| if (successStatus) { | ||
| const statusCode = responses["200"] ? "200" : responses["201"] ? "201" : "204"; | ||
| const resolved = resolveResponse(successStatus, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| if (content || statusCode === "204") { | ||
| response = { | ||
| statusCode, | ||
| type: content ? schemaToTypeString(content.schema) : "void", | ||
| description: resolved.description, | ||
| schema: content?.schema || { type: "null" } | ||
| }; | ||
| } | ||
| } | ||
| if (!response) { | ||
| for (const [statusCode, res] of Object.entries(responses)) { | ||
| if (statusCode.startsWith("2")) { | ||
| const resolved = resolveResponse(res, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| response = { | ||
| statusCode, | ||
| type: content ? schemaToTypeString(content.schema) : "void", | ||
| description: resolved.description, | ||
| schema: content?.schema || { type: "null" } | ||
| }; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| path: path14, | ||
| method: method.toUpperCase(), | ||
| operationId: operation.operationId, | ||
| summary: operation.summary, | ||
| description: operation.description, | ||
| tags: operation.tags, | ||
| pathParams: pathParams.length > 0 ? pathParams : void 0, | ||
| queryParams: queryParams.length > 0 ? queryParams : void 0, | ||
| requestBody, | ||
| response | ||
| }; | ||
| } | ||
| function resolveParameter(param, spec) { | ||
| if ("$ref" in param && param.$ref) { | ||
| const refPath = param.$ref.replace("#/components/parameters/", ""); | ||
| const resolved = spec.components?.parameters?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve parameter reference: ${param.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return param; | ||
| } | ||
| function resolveRequestBody(requestBody, spec) { | ||
| if ("$ref" in requestBody && requestBody.$ref) { | ||
| const refPath = requestBody.$ref.replace("#/components/requestBodies/", ""); | ||
| const resolved = spec.components?.requestBodies?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve request body reference: ${requestBody.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return requestBody; | ||
| } | ||
| function resolveResponse(response, spec) { | ||
| if ("$ref" in response && response.$ref) { | ||
| const refPath = response.$ref.replace("#/components/responses/", ""); | ||
| const resolved = spec.components?.responses?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve response reference: ${response.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return response; | ||
| } | ||
| function schemaToTypeString(schema) { | ||
| if (!schema) return "any"; | ||
| if (schema.$ref) { | ||
| const parts = schema.$ref.split("/"); | ||
| return parts[parts.length - 1]; | ||
| } | ||
| if (schema.type) { | ||
| switch (schema.type) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "array": | ||
| if (schema.items) { | ||
| return `${schemaToTypeString(schema.items)}[]`; | ||
| } | ||
| return "any[]"; | ||
| case "object": | ||
| return "object"; | ||
| default: | ||
| return "any"; | ||
| } | ||
| } | ||
| if (schema.allOf) { | ||
| return schema.allOf.map(schemaToTypeString).join(" & "); | ||
| } | ||
| if (schema.oneOf || schema.anyOf) { | ||
| const schemas = schema.oneOf || schema.anyOf; | ||
| return schemas.map(schemaToTypeString).join(" | "); | ||
| } | ||
| return "any"; | ||
| } | ||
| // src/swagger/config-generator.ts | ||
| function groupEndpointsByTags(endpoints) { | ||
| const grouped = /* @__PURE__ */ new Map(); | ||
| for (const endpoint of endpoints) { | ||
| const tag = endpoint.tags && endpoint.tags.length > 0 ? endpoint.tags[0] : "default"; | ||
| if (!grouped.has(tag)) { | ||
| grouped.set(tag, []); | ||
| } | ||
| grouped.get(tag).push(endpoint); | ||
| } | ||
| return grouped; | ||
| } | ||
| function generateConfigFile(endpoints, spec, baseUrl) { | ||
| const lines = []; | ||
| lines.push('import { defineConfig, defineEndpoint } from "@cushin/api-runtime";'); | ||
| lines.push('import { z } from "zod";'); | ||
| lines.push(""); | ||
| if (spec.components?.schemas) { | ||
| lines.push("// Schema definitions from OpenAPI components"); | ||
| for (const [name, schema] of Object.entries(spec.components.schemas)) { | ||
| const zodSchema = convertSchemaToZod(schema, spec); | ||
| lines.push(`const ${name}Schema = ${zodSchema};`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push("// Endpoint definitions"); | ||
| const endpointNames = []; | ||
| for (const endpoint of endpoints) { | ||
| const name = generateEndpointName(endpoint); | ||
| endpointNames.push(name); | ||
| const path14 = convertPathFormat(endpoint.path); | ||
| lines.push(`const ${name} = defineEndpoint({`); | ||
| lines.push(` path: "${path14}",`); | ||
| lines.push(` method: "${endpoint.method}",`); | ||
| if (endpoint.pathParams && endpoint.pathParams.length > 0) { | ||
| const paramsSchema = generateParamsSchema(endpoint.pathParams, spec); | ||
| lines.push(` params: ${paramsSchema},`); | ||
| } | ||
| if (endpoint.queryParams && endpoint.queryParams.length > 0) { | ||
| const querySchema = generateQuerySchema(endpoint.queryParams, spec); | ||
| lines.push(` query: ${querySchema},`); | ||
| } | ||
| if (endpoint.requestBody) { | ||
| const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec); | ||
| lines.push(` body: ${bodySchema},`); | ||
| } | ||
| if (endpoint.response) { | ||
| const responseSchema = convertSchemaToZod(endpoint.response.schema, spec); | ||
| lines.push(` response: ${responseSchema},`); | ||
| } else { | ||
| lines.push(` response: z.any(),`); | ||
| } | ||
| if (endpoint.tags && endpoint.tags.length > 0) { | ||
| const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", "); | ||
| lines.push(` tags: [${tagsStr}],`); | ||
| } | ||
| if (endpoint.description || endpoint.summary) { | ||
| const desc = endpoint.description || endpoint.summary; | ||
| lines.push(` description: "${escapeString(desc)}",`); | ||
| } | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| } | ||
| lines.push("// API Configuration"); | ||
| lines.push("export const apiConfig = defineConfig({"); | ||
| if (baseUrl) { | ||
| lines.push(` baseUrl: "${baseUrl}",`); | ||
| } else if (spec.servers && spec.servers.length > 0) { | ||
| lines.push(` baseUrl: "${spec.servers[0].url}",`); | ||
| } | ||
| lines.push(" endpoints: {"); | ||
| for (const name of endpointNames) { | ||
| lines.push(` ${name},`); | ||
| } | ||
| lines.push(" },"); | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| return lines.join("\n"); | ||
| } | ||
| function generateEndpointName(endpoint) { | ||
| if (endpoint.operationId) { | ||
| return toCamelCase(endpoint.operationId); | ||
| } | ||
| const method = endpoint.method.toLowerCase(); | ||
| const pathParts = endpoint.path.split("/").filter((p) => p && !p.startsWith(":")).map((p) => p.replace(/[^a-zA-Z0-9]/g, "")); | ||
| const parts = [method, ...pathParts]; | ||
| return toCamelCase(parts.join("_")); | ||
| } | ||
| function toCamelCase(str) { | ||
| return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toLowerCase()); | ||
| } | ||
| function generateParamsSchema(params, spec) { | ||
| const props = []; | ||
| for (const param of params) { | ||
| const zodType = convertSchemaToZod(param.schema, spec); | ||
| props.push(` ${param.name}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| function generateQuerySchema(params, spec) { | ||
| const props = []; | ||
| for (const param of params) { | ||
| let zodType = convertSchemaToZod(param.schema, spec); | ||
| if (!param.required) { | ||
| zodType += ".optional()"; | ||
| } | ||
| props.push(` ${param.name}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| function convertSchemaToZod(schema, spec) { | ||
| if (!schema) return "z.any()"; | ||
| if (schema.$ref) { | ||
| const refName = schema.$ref.split("/").pop(); | ||
| if (refName && spec.components?.schemas?.[refName]) { | ||
| return `${refName}Schema`; | ||
| } | ||
| return "z.any()"; | ||
| } | ||
| if (schema.type) { | ||
| switch (schema.type) { | ||
| case "string": | ||
| if (schema.enum) { | ||
| const values = schema.enum.map((v) => `"${v}"`).join(", "); | ||
| return `z.enum([${values}])`; | ||
| } | ||
| return "z.string()"; | ||
| case "number": | ||
| case "integer": | ||
| return "z.number()"; | ||
| case "boolean": | ||
| return "z.boolean()"; | ||
| case "array": | ||
| if (schema.items) { | ||
| const itemSchema = convertSchemaToZod(schema.items, spec); | ||
| return `z.array(${itemSchema})`; | ||
| } | ||
| return "z.array(z.any())"; | ||
| case "object": | ||
| if (schema.properties) { | ||
| const props = []; | ||
| const required = schema.required || []; | ||
| for (const [propName, propSchema] of Object.entries( | ||
| schema.properties | ||
| )) { | ||
| let zodType = convertSchemaToZod(propSchema, spec); | ||
| if (!required.includes(propName)) { | ||
| zodType += ".optional()"; | ||
| } | ||
| props.push(` ${propName}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| return "z.object({})"; | ||
| case "null": | ||
| return "z.null()"; | ||
| default: | ||
| return "z.any()"; | ||
| } | ||
| } | ||
| if (schema.allOf) { | ||
| const schemas = schema.allOf.map((s) => convertSchemaToZod(s, spec)); | ||
| return schemas.join(".and("); | ||
| } | ||
| if (schema.oneOf || schema.anyOf) { | ||
| const schemas = (schema.oneOf || schema.anyOf).map( | ||
| (s) => convertSchemaToZod(s, spec) | ||
| ); | ||
| return `z.union([${schemas.join(", ")}])`; | ||
| } | ||
| return "z.any()"; | ||
| } | ||
| function escapeString(str) { | ||
| return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); | ||
| } | ||
| function convertPathFormat(path14) { | ||
| return path14.replace(/\{([^}]+)\}/g, ":$1"); | ||
| } | ||
| function generateModuleFile(tag, endpoints, spec) { | ||
| const lines = []; | ||
| lines.push('import { defineEndpoint } from "@cushin/api-runtime";'); | ||
| lines.push('import { z } from "zod";'); | ||
| lines.push(""); | ||
| const usedSchemas = /* @__PURE__ */ new Set(); | ||
| for (const endpoint of endpoints) { | ||
| collectUsedSchemas(endpoint, usedSchemas, spec); | ||
| } | ||
| if (usedSchemas.size > 0 && spec.components?.schemas) { | ||
| lines.push("// Schema definitions"); | ||
| const sortedSchemas = sortSchemasByDependencies(Array.from(usedSchemas), spec); | ||
| for (const schemaName of sortedSchemas) { | ||
| const schema = spec.components.schemas[schemaName]; | ||
| if (schema) { | ||
| const zodSchema = convertSchemaToZod(schema, spec); | ||
| lines.push(`const ${schemaName}Schema = ${zodSchema};`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push("// Endpoint definitions"); | ||
| const endpointNames = []; | ||
| for (const endpoint of endpoints) { | ||
| const name = generateEndpointName(endpoint); | ||
| endpointNames.push(name); | ||
| const path14 = convertPathFormat(endpoint.path); | ||
| lines.push(`export const ${name} = defineEndpoint({`); | ||
| lines.push(` path: "${path14}",`); | ||
| lines.push(` method: "${endpoint.method}",`); | ||
| if (endpoint.pathParams && endpoint.pathParams.length > 0) { | ||
| const paramsSchema = generateParamsSchema(endpoint.pathParams, spec); | ||
| lines.push(` params: ${paramsSchema},`); | ||
| } | ||
| if (endpoint.queryParams && endpoint.queryParams.length > 0) { | ||
| const querySchema = generateQuerySchema(endpoint.queryParams, spec); | ||
| lines.push(` query: ${querySchema},`); | ||
| } | ||
| if (endpoint.requestBody) { | ||
| const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec); | ||
| lines.push(` body: ${bodySchema},`); | ||
| } | ||
| if (endpoint.response) { | ||
| const responseSchema = convertSchemaToZod(endpoint.response.schema, spec); | ||
| lines.push(` response: ${responseSchema},`); | ||
| } else { | ||
| lines.push(` response: z.any(),`); | ||
| } | ||
| if (endpoint.tags && endpoint.tags.length > 0) { | ||
| const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", "); | ||
| lines.push(` tags: [${tagsStr}],`); | ||
| } | ||
| if (endpoint.description || endpoint.summary) { | ||
| const desc = endpoint.description || endpoint.summary; | ||
| lines.push(` description: "${escapeString(desc)}",`); | ||
| } | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function sortSchemasByDependencies(schemaNames, spec) { | ||
| const sorted = []; | ||
| const visited = /* @__PURE__ */ new Set(); | ||
| const visiting = /* @__PURE__ */ new Set(); | ||
| function visit(name) { | ||
| if (visited.has(name)) return; | ||
| if (visiting.has(name)) { | ||
| return; | ||
| } | ||
| visiting.add(name); | ||
| const schema = spec.components?.schemas?.[name]; | ||
| if (schema) { | ||
| const deps = /* @__PURE__ */ new Set(); | ||
| extractSchemaNames(schema, deps, spec); | ||
| for (const dep of deps) { | ||
| if (dep !== name && schemaNames.includes(dep)) { | ||
| visit(dep); | ||
| } | ||
| } | ||
| } | ||
| visiting.delete(name); | ||
| if (!visited.has(name)) { | ||
| visited.add(name); | ||
| sorted.push(name); | ||
| } | ||
| } | ||
| for (const name of schemaNames) { | ||
| visit(name); | ||
| } | ||
| return sorted; | ||
| } | ||
| function collectUsedSchemas(endpoint, usedSchemas, spec) { | ||
| if (endpoint.requestBody?.schema) { | ||
| extractSchemaNames(endpoint.requestBody.schema, usedSchemas, spec); | ||
| } | ||
| if (endpoint.response?.schema) { | ||
| extractSchemaNames(endpoint.response.schema, usedSchemas, spec); | ||
| } | ||
| if (endpoint.pathParams) { | ||
| for (const param of endpoint.pathParams) { | ||
| extractSchemaNames(param.schema, usedSchemas, spec); | ||
| } | ||
| } | ||
| if (endpoint.queryParams) { | ||
| for (const param of endpoint.queryParams) { | ||
| extractSchemaNames(param.schema, usedSchemas, spec); | ||
| } | ||
| } | ||
| } | ||
| function extractSchemaNames(schema, names, spec) { | ||
| if (!schema) return; | ||
| if (schema.$ref) { | ||
| const schemaName = schema.$ref.split("/").pop(); | ||
| if (schemaName) { | ||
| names.add(schemaName); | ||
| const referencedSchema = spec.components?.schemas?.[schemaName]; | ||
| if (referencedSchema) { | ||
| extractSchemaNames(referencedSchema, names, spec); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (schema.properties) { | ||
| for (const prop of Object.values(schema.properties)) { | ||
| extractSchemaNames(prop, names, spec); | ||
| } | ||
| } | ||
| if (schema.items) { | ||
| extractSchemaNames(schema.items, names, spec); | ||
| } | ||
| if (schema.allOf) { | ||
| for (const s of schema.allOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| if (schema.oneOf) { | ||
| for (const s of schema.oneOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| if (schema.anyOf) { | ||
| for (const s of schema.anyOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| } | ||
| function generateIndexFile(tagModules, spec, baseUrl) { | ||
| const lines = []; | ||
| lines.push('import { defineConfig } from "@cushin/api-runtime";'); | ||
| lines.push(""); | ||
| for (const [tag] of tagModules) { | ||
| const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-"); | ||
| lines.push(`import * as ${toCamelCase(tag)}Module from "./${moduleFileName}.js";`); | ||
| } | ||
| lines.push(""); | ||
| lines.push("export const apiConfig = defineConfig({"); | ||
| const url = baseUrl || (spec.servers && spec.servers.length > 0 ? spec.servers[0].url : void 0); | ||
| if (url) { | ||
| lines.push(` baseUrl: "${url}",`); | ||
| } | ||
| lines.push(" endpoints: {"); | ||
| for (const [tag] of tagModules) { | ||
| lines.push(` ...${toCamelCase(tag)}Module,`); | ||
| } | ||
| lines.push(" },"); | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| for (const [tag] of tagModules) { | ||
| lines.push(`export * from "./${tag.toLowerCase().replace(/[^a-z0-9]/g, "-")}.js";`); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| // src/core/codegen.ts | ||
| var CodegenCore = class { | ||
@@ -1022,2 +1586,5 @@ constructor(config) { | ||
| async execute() { | ||
| if (this.config.swaggerSourcePath) { | ||
| await this.generateConfigFromSwagger(); | ||
| } | ||
| const apiConfig = await this.loadAPIConfig(); | ||
@@ -1031,2 +1598,63 @@ this.config.apiConfig = apiConfig; | ||
| } | ||
| /** | ||
| * Generate single config file (no split) | ||
| */ | ||
| async generateSingleConfigFile(endpoints, spec) { | ||
| const configContent = generateConfigFile(endpoints, spec, this.config.baseUrl); | ||
| const endpointsDir = path12.dirname(this.config.endpointsPath); | ||
| await fs11.mkdir(endpointsDir, { recursive: true }); | ||
| await fs11.writeFile(this.config.endpointsPath, configContent, "utf-8"); | ||
| console.log(`\u2713 Generated endpoint config at ${this.config.endpointsPath}`); | ||
| } | ||
| /** | ||
| * Generate multiple module files split by tags | ||
| */ | ||
| async generateMultipleModuleFiles(endpoints, spec) { | ||
| const grouped = groupEndpointsByTags(endpoints); | ||
| console.log(`\u2713 Grouped into ${grouped.size} modules by tags`); | ||
| const endpointsDir = path12.dirname(this.config.endpointsPath); | ||
| const modulesDir = path12.join(endpointsDir, "modules"); | ||
| await fs11.mkdir(modulesDir, { recursive: true }); | ||
| for (const [tag, tagEndpoints] of grouped.entries()) { | ||
| const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-"); | ||
| const moduleFilePath = path12.join(modulesDir, `${moduleFileName}.ts`); | ||
| const moduleContent = generateModuleFile(tag, tagEndpoints, spec); | ||
| await fs11.writeFile(moduleFilePath, moduleContent, "utf-8"); | ||
| console.log(` \u2713 ${tag}: ${tagEndpoints.length} endpoints \u2192 ${moduleFileName}.ts`); | ||
| } | ||
| const indexContent = generateIndexFile(grouped, spec, this.config.baseUrl); | ||
| const indexPath = path12.join(modulesDir, "index.ts"); | ||
| await fs11.writeFile(indexPath, indexContent, "utf-8"); | ||
| console.log(`\u2713 Generated index.ts at ${modulesDir}/index.ts`); | ||
| const mainExportContent = `export * from "./modules/index.js"; | ||
| `; | ||
| await fs11.mkdir(endpointsDir, { recursive: true }); | ||
| await fs11.writeFile(this.config.endpointsPath, mainExportContent, "utf-8"); | ||
| console.log(`\u2713 Generated main export at ${this.config.endpointsPath}`); | ||
| } | ||
| /** | ||
| * Generate endpoint config file from Swagger/OpenAPI spec | ||
| */ | ||
| async generateConfigFromSwagger() { | ||
| if (!this.config.swaggerSourcePath) { | ||
| return; | ||
| } | ||
| try { | ||
| console.log(`\u{1F4C4} Parsing Swagger spec from ${this.config.swaggerSourcePath}...`); | ||
| const spec = await parseOpenAPISpec(this.config.swaggerSourcePath); | ||
| console.log(`\u2713 Found OpenAPI ${spec.openapi} specification`); | ||
| console.log(` Title: ${spec.info.title} (v${spec.info.version})`); | ||
| const endpoints = extractEndpoints(spec); | ||
| console.log(`\u2713 Extracted ${endpoints.length} endpoints`); | ||
| if (this.config.splitByTags) { | ||
| await this.generateMultipleModuleFiles(endpoints, spec); | ||
| } else { | ||
| await this.generateSingleConfigFile(endpoints, spec); | ||
| } | ||
| } catch (error) { | ||
| throw new Error( | ||
| `Failed to generate config from Swagger: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| } | ||
| } | ||
| async loadAPIConfig() { | ||
@@ -1057,3 +1685,3 @@ try { | ||
| // src/cli.ts | ||
| import path11 from "path"; | ||
| import path13 from "path"; | ||
| var program = new Command(); | ||
@@ -1065,3 +1693,3 @@ setupCLIProgram(program); | ||
| CodegenCore, | ||
| pathToFileURL: (filePath) => new URL(`file://${path11.resolve(filePath)}`) | ||
| pathToFileURL: (filePath) => new URL(`file://${path13.resolve(filePath)}`) | ||
| }; | ||
@@ -1068,0 +1696,0 @@ setupGenerateCommand(program, context); |
+25
-0
@@ -20,2 +20,14 @@ import { APIConfig } from '@cushin/api-runtime'; | ||
| /** | ||
| * Path to Swagger/OpenAPI specification file (JSON or YAML) | ||
| * When provided, endpoints will be generated from this spec | ||
| * @optional | ||
| */ | ||
| swaggerSource?: string; | ||
| /** | ||
| * Split generated endpoints into separate files by tags | ||
| * When true with swaggerSource, each tag becomes a separate module file | ||
| * @default false | ||
| */ | ||
| splitByTags?: boolean; | ||
| /** | ||
| * Provider type: 'vite' | 'nextjs' | ||
@@ -83,2 +95,3 @@ */ | ||
| outputDir: string; | ||
| swaggerSourcePath?: string; | ||
| apiConfig?: APIConfig; | ||
@@ -96,2 +109,14 @@ } | ||
| execute(): Promise<void>; | ||
| /** | ||
| * Generate single config file (no split) | ||
| */ | ||
| private generateSingleConfigFile; | ||
| /** | ||
| * Generate multiple module files split by tags | ||
| */ | ||
| private generateMultipleModuleFiles; | ||
| /** | ||
| * Generate endpoint config file from Swagger/OpenAPI spec | ||
| */ | ||
| private generateConfigFromSwagger; | ||
| private loadAPIConfig; | ||
@@ -98,0 +123,0 @@ } |
+628
-0
@@ -28,2 +28,3 @@ // src/index.ts | ||
| const outputDir = path.resolve(rootDir, userConfig.output); | ||
| const swaggerSourcePath = userConfig.swaggerSource ? path.resolve(rootDir, userConfig.swaggerSource) : void 0; | ||
| const generateHooks = userConfig.generateHooks ?? true; | ||
@@ -39,2 +40,3 @@ const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs"; | ||
| outputDir, | ||
| swaggerSourcePath, | ||
| generateHooks, | ||
@@ -1013,2 +1015,564 @@ generateServerActions, | ||
| import { fileURLToPath } from "url"; | ||
| import fs11 from "fs/promises"; | ||
| import path12 from "path"; | ||
| // src/swagger/parser.ts | ||
| import fs10 from "fs"; | ||
| import path11 from "path"; | ||
| async function parseOpenAPISpec(filePath) { | ||
| const content = await fs10.promises.readFile(filePath, "utf-8"); | ||
| const ext = path11.extname(filePath).toLowerCase(); | ||
| let spec; | ||
| if (ext === ".json") { | ||
| spec = JSON.parse(content); | ||
| } else if (ext === ".yaml" || ext === ".yml") { | ||
| const yaml = await import("yaml"); | ||
| spec = yaml.parse(content); | ||
| } else { | ||
| throw new Error( | ||
| `Unsupported file format: ${ext}. Only .json, .yaml, and .yml are supported.` | ||
| ); | ||
| } | ||
| if (!spec.openapi || !spec.openapi.startsWith("3.")) { | ||
| throw new Error( | ||
| `Unsupported OpenAPI version: ${spec.openapi}. Only OpenAPI 3.0 and 3.1 are supported.` | ||
| ); | ||
| } | ||
| return spec; | ||
| } | ||
| function extractEndpoints(spec) { | ||
| const endpoints = []; | ||
| for (const [path13, pathItem] of Object.entries(spec.paths)) { | ||
| const methods = [ | ||
| "get", | ||
| "post", | ||
| "put", | ||
| "delete", | ||
| "patch", | ||
| "head", | ||
| "options" | ||
| ]; | ||
| for (const method of methods) { | ||
| const operation = pathItem[method]; | ||
| if (!operation) continue; | ||
| const endpoint = parseOperation( | ||
| path13, | ||
| method, | ||
| operation, | ||
| pathItem, | ||
| spec | ||
| ); | ||
| endpoints.push(endpoint); | ||
| } | ||
| } | ||
| return endpoints; | ||
| } | ||
| function parseOperation(path13, method, operation, pathItem, spec) { | ||
| const allParameters = [ | ||
| ...pathItem.parameters || [], | ||
| ...operation.parameters || [] | ||
| ]; | ||
| const pathParams = []; | ||
| const queryParams = []; | ||
| for (const param of allParameters) { | ||
| const resolved = resolveParameter(param, spec); | ||
| const parsed = { | ||
| name: resolved.name, | ||
| type: schemaToTypeString(resolved.schema), | ||
| required: resolved.required ?? resolved.in === "path", | ||
| description: resolved.description, | ||
| schema: resolved.schema | ||
| // Keep original schema | ||
| }; | ||
| if (resolved.in === "path") { | ||
| pathParams.push(parsed); | ||
| } else if (resolved.in === "query") { | ||
| queryParams.push(parsed); | ||
| } | ||
| } | ||
| let requestBody; | ||
| if (operation.requestBody) { | ||
| const resolved = resolveRequestBody(operation.requestBody, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| if (content) { | ||
| requestBody = { | ||
| type: schemaToTypeString(content.schema), | ||
| required: resolved.required ?? false, | ||
| description: resolved.description, | ||
| schema: content.schema | ||
| }; | ||
| } | ||
| } | ||
| let response; | ||
| const responses = operation.responses; | ||
| const successStatus = responses["200"] || responses["201"] || responses["204"]; | ||
| if (successStatus) { | ||
| const statusCode = responses["200"] ? "200" : responses["201"] ? "201" : "204"; | ||
| const resolved = resolveResponse(successStatus, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| if (content || statusCode === "204") { | ||
| response = { | ||
| statusCode, | ||
| type: content ? schemaToTypeString(content.schema) : "void", | ||
| description: resolved.description, | ||
| schema: content?.schema || { type: "null" } | ||
| }; | ||
| } | ||
| } | ||
| if (!response) { | ||
| for (const [statusCode, res] of Object.entries(responses)) { | ||
| if (statusCode.startsWith("2")) { | ||
| const resolved = resolveResponse(res, spec); | ||
| const content = resolved.content?.["application/json"]; | ||
| response = { | ||
| statusCode, | ||
| type: content ? schemaToTypeString(content.schema) : "void", | ||
| description: resolved.description, | ||
| schema: content?.schema || { type: "null" } | ||
| }; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| path: path13, | ||
| method: method.toUpperCase(), | ||
| operationId: operation.operationId, | ||
| summary: operation.summary, | ||
| description: operation.description, | ||
| tags: operation.tags, | ||
| pathParams: pathParams.length > 0 ? pathParams : void 0, | ||
| queryParams: queryParams.length > 0 ? queryParams : void 0, | ||
| requestBody, | ||
| response | ||
| }; | ||
| } | ||
| function resolveParameter(param, spec) { | ||
| if ("$ref" in param && param.$ref) { | ||
| const refPath = param.$ref.replace("#/components/parameters/", ""); | ||
| const resolved = spec.components?.parameters?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve parameter reference: ${param.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return param; | ||
| } | ||
| function resolveRequestBody(requestBody, spec) { | ||
| if ("$ref" in requestBody && requestBody.$ref) { | ||
| const refPath = requestBody.$ref.replace("#/components/requestBodies/", ""); | ||
| const resolved = spec.components?.requestBodies?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve request body reference: ${requestBody.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return requestBody; | ||
| } | ||
| function resolveResponse(response, spec) { | ||
| if ("$ref" in response && response.$ref) { | ||
| const refPath = response.$ref.replace("#/components/responses/", ""); | ||
| const resolved = spec.components?.responses?.[refPath]; | ||
| if (!resolved) { | ||
| throw new Error(`Cannot resolve response reference: ${response.$ref}`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return response; | ||
| } | ||
| function schemaToTypeString(schema) { | ||
| if (!schema) return "any"; | ||
| if (schema.$ref) { | ||
| const parts = schema.$ref.split("/"); | ||
| return parts[parts.length - 1]; | ||
| } | ||
| if (schema.type) { | ||
| switch (schema.type) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "array": | ||
| if (schema.items) { | ||
| return `${schemaToTypeString(schema.items)}[]`; | ||
| } | ||
| return "any[]"; | ||
| case "object": | ||
| return "object"; | ||
| default: | ||
| return "any"; | ||
| } | ||
| } | ||
| if (schema.allOf) { | ||
| return schema.allOf.map(schemaToTypeString).join(" & "); | ||
| } | ||
| if (schema.oneOf || schema.anyOf) { | ||
| const schemas = schema.oneOf || schema.anyOf; | ||
| return schemas.map(schemaToTypeString).join(" | "); | ||
| } | ||
| return "any"; | ||
| } | ||
| // src/swagger/config-generator.ts | ||
| function groupEndpointsByTags(endpoints) { | ||
| const grouped = /* @__PURE__ */ new Map(); | ||
| for (const endpoint of endpoints) { | ||
| const tag = endpoint.tags && endpoint.tags.length > 0 ? endpoint.tags[0] : "default"; | ||
| if (!grouped.has(tag)) { | ||
| grouped.set(tag, []); | ||
| } | ||
| grouped.get(tag).push(endpoint); | ||
| } | ||
| return grouped; | ||
| } | ||
| function generateConfigFile(endpoints, spec, baseUrl) { | ||
| const lines = []; | ||
| lines.push('import { defineConfig, defineEndpoint } from "@cushin/api-runtime";'); | ||
| lines.push('import { z } from "zod";'); | ||
| lines.push(""); | ||
| if (spec.components?.schemas) { | ||
| lines.push("// Schema definitions from OpenAPI components"); | ||
| for (const [name, schema] of Object.entries(spec.components.schemas)) { | ||
| const zodSchema = convertSchemaToZod(schema, spec); | ||
| lines.push(`const ${name}Schema = ${zodSchema};`); | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push("// Endpoint definitions"); | ||
| const endpointNames = []; | ||
| for (const endpoint of endpoints) { | ||
| const name = generateEndpointName(endpoint); | ||
| endpointNames.push(name); | ||
| const path13 = convertPathFormat(endpoint.path); | ||
| lines.push(`const ${name} = defineEndpoint({`); | ||
| lines.push(` path: "${path13}",`); | ||
| lines.push(` method: "${endpoint.method}",`); | ||
| if (endpoint.pathParams && endpoint.pathParams.length > 0) { | ||
| const paramsSchema = generateParamsSchema(endpoint.pathParams, spec); | ||
| lines.push(` params: ${paramsSchema},`); | ||
| } | ||
| if (endpoint.queryParams && endpoint.queryParams.length > 0) { | ||
| const querySchema = generateQuerySchema(endpoint.queryParams, spec); | ||
| lines.push(` query: ${querySchema},`); | ||
| } | ||
| if (endpoint.requestBody) { | ||
| const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec); | ||
| lines.push(` body: ${bodySchema},`); | ||
| } | ||
| if (endpoint.response) { | ||
| const responseSchema = convertSchemaToZod(endpoint.response.schema, spec); | ||
| lines.push(` response: ${responseSchema},`); | ||
| } else { | ||
| lines.push(` response: z.any(),`); | ||
| } | ||
| if (endpoint.tags && endpoint.tags.length > 0) { | ||
| const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", "); | ||
| lines.push(` tags: [${tagsStr}],`); | ||
| } | ||
| if (endpoint.description || endpoint.summary) { | ||
| const desc = endpoint.description || endpoint.summary; | ||
| lines.push(` description: "${escapeString(desc)}",`); | ||
| } | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| } | ||
| lines.push("// API Configuration"); | ||
| lines.push("export const apiConfig = defineConfig({"); | ||
| if (baseUrl) { | ||
| lines.push(` baseUrl: "${baseUrl}",`); | ||
| } else if (spec.servers && spec.servers.length > 0) { | ||
| lines.push(` baseUrl: "${spec.servers[0].url}",`); | ||
| } | ||
| lines.push(" endpoints: {"); | ||
| for (const name of endpointNames) { | ||
| lines.push(` ${name},`); | ||
| } | ||
| lines.push(" },"); | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| return lines.join("\n"); | ||
| } | ||
| function generateEndpointName(endpoint) { | ||
| if (endpoint.operationId) { | ||
| return toCamelCase(endpoint.operationId); | ||
| } | ||
| const method = endpoint.method.toLowerCase(); | ||
| const pathParts = endpoint.path.split("/").filter((p) => p && !p.startsWith(":")).map((p) => p.replace(/[^a-zA-Z0-9]/g, "")); | ||
| const parts = [method, ...pathParts]; | ||
| return toCamelCase(parts.join("_")); | ||
| } | ||
| function toCamelCase(str) { | ||
| return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toLowerCase()); | ||
| } | ||
| function generateParamsSchema(params, spec) { | ||
| const props = []; | ||
| for (const param of params) { | ||
| const zodType = convertSchemaToZod(param.schema, spec); | ||
| props.push(` ${param.name}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| function generateQuerySchema(params, spec) { | ||
| const props = []; | ||
| for (const param of params) { | ||
| let zodType = convertSchemaToZod(param.schema, spec); | ||
| if (!param.required) { | ||
| zodType += ".optional()"; | ||
| } | ||
| props.push(` ${param.name}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| function convertSchemaToZod(schema, spec) { | ||
| if (!schema) return "z.any()"; | ||
| if (schema.$ref) { | ||
| const refName = schema.$ref.split("/").pop(); | ||
| if (refName && spec.components?.schemas?.[refName]) { | ||
| return `${refName}Schema`; | ||
| } | ||
| return "z.any()"; | ||
| } | ||
| if (schema.type) { | ||
| switch (schema.type) { | ||
| case "string": | ||
| if (schema.enum) { | ||
| const values = schema.enum.map((v) => `"${v}"`).join(", "); | ||
| return `z.enum([${values}])`; | ||
| } | ||
| return "z.string()"; | ||
| case "number": | ||
| case "integer": | ||
| return "z.number()"; | ||
| case "boolean": | ||
| return "z.boolean()"; | ||
| case "array": | ||
| if (schema.items) { | ||
| const itemSchema = convertSchemaToZod(schema.items, spec); | ||
| return `z.array(${itemSchema})`; | ||
| } | ||
| return "z.array(z.any())"; | ||
| case "object": | ||
| if (schema.properties) { | ||
| const props = []; | ||
| const required = schema.required || []; | ||
| for (const [propName, propSchema] of Object.entries( | ||
| schema.properties | ||
| )) { | ||
| let zodType = convertSchemaToZod(propSchema, spec); | ||
| if (!required.includes(propName)) { | ||
| zodType += ".optional()"; | ||
| } | ||
| props.push(` ${propName}: ${zodType}`); | ||
| } | ||
| return `z.object({ | ||
| ${props.join(",\n")} | ||
| })`; | ||
| } | ||
| return "z.object({})"; | ||
| case "null": | ||
| return "z.null()"; | ||
| default: | ||
| return "z.any()"; | ||
| } | ||
| } | ||
| if (schema.allOf) { | ||
| const schemas = schema.allOf.map((s) => convertSchemaToZod(s, spec)); | ||
| return schemas.join(".and("); | ||
| } | ||
| if (schema.oneOf || schema.anyOf) { | ||
| const schemas = (schema.oneOf || schema.anyOf).map( | ||
| (s) => convertSchemaToZod(s, spec) | ||
| ); | ||
| return `z.union([${schemas.join(", ")}])`; | ||
| } | ||
| return "z.any()"; | ||
| } | ||
| function escapeString(str) { | ||
| return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); | ||
| } | ||
| function convertPathFormat(path13) { | ||
| return path13.replace(/\{([^}]+)\}/g, ":$1"); | ||
| } | ||
| function generateModuleFile(tag, endpoints, spec) { | ||
| const lines = []; | ||
| lines.push('import { defineEndpoint } from "@cushin/api-runtime";'); | ||
| lines.push('import { z } from "zod";'); | ||
| lines.push(""); | ||
| const usedSchemas = /* @__PURE__ */ new Set(); | ||
| for (const endpoint of endpoints) { | ||
| collectUsedSchemas(endpoint, usedSchemas, spec); | ||
| } | ||
| if (usedSchemas.size > 0 && spec.components?.schemas) { | ||
| lines.push("// Schema definitions"); | ||
| const sortedSchemas = sortSchemasByDependencies(Array.from(usedSchemas), spec); | ||
| for (const schemaName of sortedSchemas) { | ||
| const schema = spec.components.schemas[schemaName]; | ||
| if (schema) { | ||
| const zodSchema = convertSchemaToZod(schema, spec); | ||
| lines.push(`const ${schemaName}Schema = ${zodSchema};`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| } | ||
| lines.push("// Endpoint definitions"); | ||
| const endpointNames = []; | ||
| for (const endpoint of endpoints) { | ||
| const name = generateEndpointName(endpoint); | ||
| endpointNames.push(name); | ||
| const path13 = convertPathFormat(endpoint.path); | ||
| lines.push(`export const ${name} = defineEndpoint({`); | ||
| lines.push(` path: "${path13}",`); | ||
| lines.push(` method: "${endpoint.method}",`); | ||
| if (endpoint.pathParams && endpoint.pathParams.length > 0) { | ||
| const paramsSchema = generateParamsSchema(endpoint.pathParams, spec); | ||
| lines.push(` params: ${paramsSchema},`); | ||
| } | ||
| if (endpoint.queryParams && endpoint.queryParams.length > 0) { | ||
| const querySchema = generateQuerySchema(endpoint.queryParams, spec); | ||
| lines.push(` query: ${querySchema},`); | ||
| } | ||
| if (endpoint.requestBody) { | ||
| const bodySchema = convertSchemaToZod(endpoint.requestBody.schema, spec); | ||
| lines.push(` body: ${bodySchema},`); | ||
| } | ||
| if (endpoint.response) { | ||
| const responseSchema = convertSchemaToZod(endpoint.response.schema, spec); | ||
| lines.push(` response: ${responseSchema},`); | ||
| } else { | ||
| lines.push(` response: z.any(),`); | ||
| } | ||
| if (endpoint.tags && endpoint.tags.length > 0) { | ||
| const tagsStr = endpoint.tags.map((t) => `"${t}"`).join(", "); | ||
| lines.push(` tags: [${tagsStr}],`); | ||
| } | ||
| if (endpoint.description || endpoint.summary) { | ||
| const desc = endpoint.description || endpoint.summary; | ||
| lines.push(` description: "${escapeString(desc)}",`); | ||
| } | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function sortSchemasByDependencies(schemaNames, spec) { | ||
| const sorted = []; | ||
| const visited = /* @__PURE__ */ new Set(); | ||
| const visiting = /* @__PURE__ */ new Set(); | ||
| function visit(name) { | ||
| if (visited.has(name)) return; | ||
| if (visiting.has(name)) { | ||
| return; | ||
| } | ||
| visiting.add(name); | ||
| const schema = spec.components?.schemas?.[name]; | ||
| if (schema) { | ||
| const deps = /* @__PURE__ */ new Set(); | ||
| extractSchemaNames(schema, deps, spec); | ||
| for (const dep of deps) { | ||
| if (dep !== name && schemaNames.includes(dep)) { | ||
| visit(dep); | ||
| } | ||
| } | ||
| } | ||
| visiting.delete(name); | ||
| if (!visited.has(name)) { | ||
| visited.add(name); | ||
| sorted.push(name); | ||
| } | ||
| } | ||
| for (const name of schemaNames) { | ||
| visit(name); | ||
| } | ||
| return sorted; | ||
| } | ||
| function collectUsedSchemas(endpoint, usedSchemas, spec) { | ||
| if (endpoint.requestBody?.schema) { | ||
| extractSchemaNames(endpoint.requestBody.schema, usedSchemas, spec); | ||
| } | ||
| if (endpoint.response?.schema) { | ||
| extractSchemaNames(endpoint.response.schema, usedSchemas, spec); | ||
| } | ||
| if (endpoint.pathParams) { | ||
| for (const param of endpoint.pathParams) { | ||
| extractSchemaNames(param.schema, usedSchemas, spec); | ||
| } | ||
| } | ||
| if (endpoint.queryParams) { | ||
| for (const param of endpoint.queryParams) { | ||
| extractSchemaNames(param.schema, usedSchemas, spec); | ||
| } | ||
| } | ||
| } | ||
| function extractSchemaNames(schema, names, spec) { | ||
| if (!schema) return; | ||
| if (schema.$ref) { | ||
| const schemaName = schema.$ref.split("/").pop(); | ||
| if (schemaName) { | ||
| names.add(schemaName); | ||
| const referencedSchema = spec.components?.schemas?.[schemaName]; | ||
| if (referencedSchema) { | ||
| extractSchemaNames(referencedSchema, names, spec); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (schema.properties) { | ||
| for (const prop of Object.values(schema.properties)) { | ||
| extractSchemaNames(prop, names, spec); | ||
| } | ||
| } | ||
| if (schema.items) { | ||
| extractSchemaNames(schema.items, names, spec); | ||
| } | ||
| if (schema.allOf) { | ||
| for (const s of schema.allOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| if (schema.oneOf) { | ||
| for (const s of schema.oneOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| if (schema.anyOf) { | ||
| for (const s of schema.anyOf) { | ||
| extractSchemaNames(s, names, spec); | ||
| } | ||
| } | ||
| } | ||
| function generateIndexFile(tagModules, spec, baseUrl) { | ||
| const lines = []; | ||
| lines.push('import { defineConfig } from "@cushin/api-runtime";'); | ||
| lines.push(""); | ||
| for (const [tag] of tagModules) { | ||
| const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-"); | ||
| lines.push(`import * as ${toCamelCase(tag)}Module from "./${moduleFileName}.js";`); | ||
| } | ||
| lines.push(""); | ||
| lines.push("export const apiConfig = defineConfig({"); | ||
| const url = baseUrl || (spec.servers && spec.servers.length > 0 ? spec.servers[0].url : void 0); | ||
| if (url) { | ||
| lines.push(` baseUrl: "${url}",`); | ||
| } | ||
| lines.push(" endpoints: {"); | ||
| for (const [tag] of tagModules) { | ||
| lines.push(` ...${toCamelCase(tag)}Module,`); | ||
| } | ||
| lines.push(" },"); | ||
| lines.push("});"); | ||
| lines.push(""); | ||
| for (const [tag] of tagModules) { | ||
| lines.push(`export * from "./${tag.toLowerCase().replace(/[^a-z0-9]/g, "-")}.js";`); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| // src/core/codegen.ts | ||
| var CodegenCore = class { | ||
@@ -1019,2 +1583,5 @@ constructor(config) { | ||
| async execute() { | ||
| if (this.config.swaggerSourcePath) { | ||
| await this.generateConfigFromSwagger(); | ||
| } | ||
| const apiConfig = await this.loadAPIConfig(); | ||
@@ -1028,2 +1595,63 @@ this.config.apiConfig = apiConfig; | ||
| } | ||
| /** | ||
| * Generate single config file (no split) | ||
| */ | ||
| async generateSingleConfigFile(endpoints, spec) { | ||
| const configContent = generateConfigFile(endpoints, spec, this.config.baseUrl); | ||
| const endpointsDir = path12.dirname(this.config.endpointsPath); | ||
| await fs11.mkdir(endpointsDir, { recursive: true }); | ||
| await fs11.writeFile(this.config.endpointsPath, configContent, "utf-8"); | ||
| console.log(`\u2713 Generated endpoint config at ${this.config.endpointsPath}`); | ||
| } | ||
| /** | ||
| * Generate multiple module files split by tags | ||
| */ | ||
| async generateMultipleModuleFiles(endpoints, spec) { | ||
| const grouped = groupEndpointsByTags(endpoints); | ||
| console.log(`\u2713 Grouped into ${grouped.size} modules by tags`); | ||
| const endpointsDir = path12.dirname(this.config.endpointsPath); | ||
| const modulesDir = path12.join(endpointsDir, "modules"); | ||
| await fs11.mkdir(modulesDir, { recursive: true }); | ||
| for (const [tag, tagEndpoints] of grouped.entries()) { | ||
| const moduleFileName = tag.toLowerCase().replace(/[^a-z0-9]/g, "-"); | ||
| const moduleFilePath = path12.join(modulesDir, `${moduleFileName}.ts`); | ||
| const moduleContent = generateModuleFile(tag, tagEndpoints, spec); | ||
| await fs11.writeFile(moduleFilePath, moduleContent, "utf-8"); | ||
| console.log(` \u2713 ${tag}: ${tagEndpoints.length} endpoints \u2192 ${moduleFileName}.ts`); | ||
| } | ||
| const indexContent = generateIndexFile(grouped, spec, this.config.baseUrl); | ||
| const indexPath = path12.join(modulesDir, "index.ts"); | ||
| await fs11.writeFile(indexPath, indexContent, "utf-8"); | ||
| console.log(`\u2713 Generated index.ts at ${modulesDir}/index.ts`); | ||
| const mainExportContent = `export * from "./modules/index.js"; | ||
| `; | ||
| await fs11.mkdir(endpointsDir, { recursive: true }); | ||
| await fs11.writeFile(this.config.endpointsPath, mainExportContent, "utf-8"); | ||
| console.log(`\u2713 Generated main export at ${this.config.endpointsPath}`); | ||
| } | ||
| /** | ||
| * Generate endpoint config file from Swagger/OpenAPI spec | ||
| */ | ||
| async generateConfigFromSwagger() { | ||
| if (!this.config.swaggerSourcePath) { | ||
| return; | ||
| } | ||
| try { | ||
| console.log(`\u{1F4C4} Parsing Swagger spec from ${this.config.swaggerSourcePath}...`); | ||
| const spec = await parseOpenAPISpec(this.config.swaggerSourcePath); | ||
| console.log(`\u2713 Found OpenAPI ${spec.openapi} specification`); | ||
| console.log(` Title: ${spec.info.title} (v${spec.info.version})`); | ||
| const endpoints = extractEndpoints(spec); | ||
| console.log(`\u2713 Extracted ${endpoints.length} endpoints`); | ||
| if (this.config.splitByTags) { | ||
| await this.generateMultipleModuleFiles(endpoints, spec); | ||
| } else { | ||
| await this.generateSingleConfigFile(endpoints, spec); | ||
| } | ||
| } catch (error) { | ||
| throw new Error( | ||
| `Failed to generate config from Swagger: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| } | ||
| } | ||
| async loadAPIConfig() { | ||
@@ -1030,0 +1658,0 @@ try { |
+3
-2
| { | ||
| "name": "@cushin/api-codegen", | ||
| "version": "5.0.8", | ||
| "version": "6.0.0", | ||
| "description": "Type-safe API client generator for React/Next.js with automatic hooks and server actions generation", | ||
@@ -63,3 +63,4 @@ "type": "module", | ||
| "ora": "^8.0.0", | ||
| "@cushin/api-runtime": "5.0.8", | ||
| "yaml": "^2.8.2", | ||
| "@cushin/api-runtime": "5.0.9", | ||
| "@cushin/codegen-cli": "5.0.0" | ||
@@ -66,0 +67,0 @@ }, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
348019
57.78%3417
59.52%13
8.33%27
17.39%+ Added
+ Added
+ Added
- Removed
Updated