axigen 0.1.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/index.mjs ADDED
@@ -0,0 +1,454 @@
1
+ // src/generate.ts
2
+ import fs2 from "fs";
3
+ import path2 from "path";
4
+
5
+ // src/parser/openapi.ts
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import yaml from "js-yaml";
9
+ function parseOpenAPIFile(filePath) {
10
+ if (!fs.existsSync(filePath)) {
11
+ throw new Error(`OpenAPI file not found: ${filePath}`);
12
+ }
13
+ const content = fs.readFileSync(filePath, "utf-8");
14
+ const ext = path.extname(filePath).toLowerCase();
15
+ let spec;
16
+ if (ext === ".json") {
17
+ spec = JSON.parse(content);
18
+ } else {
19
+ spec = yaml.load(content);
20
+ }
21
+ return validateSpec(spec, filePath);
22
+ }
23
+ function validateSpec(raw, filePath) {
24
+ if (!raw || typeof raw !== "object") {
25
+ throw new Error(`Invalid OpenAPI file: ${filePath}`);
26
+ }
27
+ const s = raw;
28
+ const isOpenAPI3 = typeof s.openapi === "string" && s.openapi.startsWith("3.");
29
+ const isSwagger2 = typeof s.swagger === "string" && s.swagger.startsWith("2.");
30
+ if (!isOpenAPI3 && !isSwagger2) {
31
+ throw new Error(`Unsupported spec format in ${filePath}.
32
+ Expected OpenAPI 3.x or Swagger 2.x`);
33
+ }
34
+ if (!s.paths || typeof s.paths !== "object") {
35
+ throw new Error(`OpenAPI spec has no "paths" defined: ${filePath}`);
36
+ }
37
+ return raw;
38
+ }
39
+ function extractEndpoints(spec, filterTags) {
40
+ const endpoints = [];
41
+ for (const [urlPath, pathItem] of Object.entries(spec.paths)) {
42
+ const methods = ["get", "post", "put", "patch", "delete", "head", "options"];
43
+ for (const method of methods) {
44
+ const operation = pathItem[method];
45
+ if (!operation) continue;
46
+ const tags = operation.tags ?? [];
47
+ if (filterTags && filterTags.length > 0) {
48
+ if (!tags.some((t) => filterTags.includes(t))) continue;
49
+ }
50
+ const operationId = operation.operationId ?? buildOperationId(method, urlPath);
51
+ const allParams = [...pathItem.parameters ?? [], ...operation.parameters ?? []];
52
+ const pathParams = allParams.filter((p) => !("$ref" in p) && p.in === "path").map(normalizeParam);
53
+ const queryParams = allParams.filter((p) => !("$ref" in p) && p.in === "query").map(normalizeParam);
54
+ let bodySchema;
55
+ let bodyRequired = false;
56
+ if (operation.requestBody && !("$ref" in operation.requestBody)) {
57
+ bodyRequired = operation.requestBody.required ?? false;
58
+ const content = operation.requestBody.content;
59
+ const jsonContent = content?.["application/json"];
60
+ bodySchema = jsonContent?.schema;
61
+ }
62
+ let responseSchema;
63
+ for (const [status, response] of Object.entries(operation.responses)) {
64
+ if (status.startsWith("2") && !("$ref" in response)) {
65
+ const jsonContent = response.content?.["application/json"];
66
+ responseSchema = jsonContent?.schema;
67
+ break;
68
+ }
69
+ }
70
+ endpoints.push({
71
+ operationId,
72
+ method,
73
+ path: urlPath,
74
+ summary: operation.summary,
75
+ description: operation.description,
76
+ tags,
77
+ pathParams,
78
+ queryParams,
79
+ bodySchema,
80
+ bodyRequired,
81
+ responseSchema
82
+ });
83
+ }
84
+ }
85
+ return endpoints;
86
+ }
87
+ function normalizeParam(p) {
88
+ return {
89
+ name: p.name,
90
+ required: p.required ?? false,
91
+ schema: p.schema,
92
+ description: p.description
93
+ };
94
+ }
95
+ function buildOperationId(method, urlPath) {
96
+ const parts = urlPath.split("/").filter(Boolean).map((segment) => {
97
+ if (segment.startsWith("{") && segment.endsWith("}")) {
98
+ const name = segment.slice(1, -1);
99
+ return "By" + capitalize(name);
100
+ }
101
+ return capitalize(segment);
102
+ });
103
+ return method.toLowerCase() + parts.join("");
104
+ }
105
+ function capitalize(s) {
106
+ return s.charAt(0).toUpperCase() + s.slice(1);
107
+ }
108
+
109
+ // src/generator/types.ts
110
+ function schemaToTSType(schema, indent = 0) {
111
+ if (!schema) return "unknown";
112
+ if (schema.$ref) {
113
+ return refToTypeName(schema.$ref);
114
+ }
115
+ if (schema.allOf && schema.allOf.length > 0) {
116
+ return schema.allOf.map((s) => schemaToTSType(s, indent)).join(" & ");
117
+ }
118
+ if (schema.anyOf || schema.oneOf) {
119
+ const variants = schema.anyOf ?? schema.oneOf;
120
+ return variants.map((s) => schemaToTSType(s, indent)).join(" | ");
121
+ }
122
+ if (schema.enum) {
123
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
124
+ }
125
+ const nullable = schema.nullable ? " | null" : "";
126
+ switch (schema.type) {
127
+ case "string":
128
+ return `string${nullable}`;
129
+ case "number":
130
+ case "integer":
131
+ return `number${nullable}`;
132
+ case "boolean":
133
+ return `boolean${nullable}`;
134
+ case "null":
135
+ return "null";
136
+ case "array":
137
+ return `Array<${schemaToTSType(schema.items, indent)}>${nullable}`;
138
+ case "object":
139
+ return buildObjectType(schema, indent) + nullable;
140
+ default:
141
+ if (schema.properties) {
142
+ return buildObjectType(schema, indent) + nullable;
143
+ }
144
+ return `unknown${nullable}`;
145
+ }
146
+ }
147
+ function buildObjectType(schema, indent) {
148
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
149
+ return "Record<string, unknown>";
150
+ }
151
+ const required = new Set(schema.required ?? []);
152
+ const pad = " ".repeat(indent + 1);
153
+ const closePad = " ".repeat(indent);
154
+ const fields = Object.entries(schema.properties).map(([key, val]) => {
155
+ const optional = !required.has(key) ? "?" : "";
156
+ const comment = val.description ? `${pad}/** ${val.description} */
157
+ ` : "";
158
+ const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
159
+ return `${comment}${pad}${safeName}${optional}: ${schemaToTSType(val, indent + 1)}`;
160
+ }).join("\n");
161
+ return `{
162
+ ${fields}
163
+ ${closePad}}`;
164
+ }
165
+ function refToTypeName(ref) {
166
+ const parts = ref.split("/");
167
+ return parts[parts.length - 1] ?? "unknown";
168
+ }
169
+ function generateTypesFile(endpoints, schemas = {}) {
170
+ const lines = [];
171
+ lines.push(`// This file is auto-generated by axigen. DO NOT EDIT.`);
172
+ lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
173
+ lines.push("");
174
+ if (Object.keys(schemas).length > 0) {
175
+ lines.push("// \u2500\u2500\u2500 Component Schemas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
176
+ lines.push("");
177
+ for (const [name, schema] of Object.entries(schemas)) {
178
+ if (schema.description) {
179
+ lines.push(`/** ${schema.description} */`);
180
+ }
181
+ lines.push(`export type ${name} = ${schemaToTSType(schema)}`);
182
+ lines.push("");
183
+ }
184
+ }
185
+ lines.push("// \u2500\u2500\u2500 Endpoint Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
186
+ lines.push("");
187
+ for (const ep of endpoints) {
188
+ const baseName = operationToTypeName(ep.operationId);
189
+ if (ep.pathParams.length > 0) {
190
+ lines.push(`export interface ${baseName}PathParams {`);
191
+ for (const p of ep.pathParams) {
192
+ const type = schemaToTSType(p.schema);
193
+ const comment = p.description ? ` /** ${p.description} */
194
+ ` : "";
195
+ lines.push(`${comment} ${p.name}: ${type}`);
196
+ }
197
+ lines.push(`}`);
198
+ lines.push("");
199
+ }
200
+ if (ep.queryParams.length > 0) {
201
+ lines.push(`export interface ${baseName}QueryParams {`);
202
+ for (const p of ep.queryParams) {
203
+ const optional = !p.required ? "?" : "";
204
+ const type = schemaToTSType(p.schema);
205
+ const comment = p.description ? ` /** ${p.description} */
206
+ ` : "";
207
+ lines.push(`${comment} ${p.name}${optional}: ${type}`);
208
+ }
209
+ lines.push(`}`);
210
+ lines.push("");
211
+ }
212
+ if (ep.bodySchema) {
213
+ lines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
214
+ lines.push("");
215
+ }
216
+ if (ep.responseSchema) {
217
+ lines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
218
+ lines.push("");
219
+ }
220
+ }
221
+ return lines.join("\n");
222
+ }
223
+ function operationToTypeName(operationId) {
224
+ return operationId.charAt(0).toUpperCase() + operationId.slice(1);
225
+ }
226
+
227
+ // src/generator/axios.ts
228
+ function generateClientFile(opts) {
229
+ const { endpoints, config, typesRelativePath } = opts;
230
+ const isTS = config.language !== "js";
231
+ const instanceExport = config.axiosInstanceExport ?? "axiosInstance";
232
+ const lines = [];
233
+ lines.push(`// This file is auto-generated by axigen. DO NOT EDIT.`);
234
+ lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
235
+ lines.push("");
236
+ if (isTS) {
237
+ lines.push(`import type { AxiosResponse } from 'axios'`);
238
+ }
239
+ lines.push(`import { ${instanceExport} } from '${config.axiosInstancePath}'`);
240
+ if (isTS && typesRelativePath) {
241
+ const typeImports = collectTypeImports(endpoints);
242
+ if (typeImports.length > 0) {
243
+ lines.push(`import type { ${typeImports.join(", ")} } from '${typesRelativePath}'`);
244
+ }
245
+ }
246
+ lines.push("");
247
+ lines.push("");
248
+ for (const ep of endpoints) {
249
+ const fn = buildFunction(ep, isTS, instanceExport, config.jsdoc !== false);
250
+ lines.push(fn);
251
+ lines.push("");
252
+ }
253
+ return lines.join("\n");
254
+ }
255
+ function buildFunction(ep, isTS, instanceExport, jsdoc) {
256
+ const typeName = operationToTypeName(ep.operationId);
257
+ const lines = [];
258
+ if (jsdoc) {
259
+ lines.push(`/**`);
260
+ if (ep.summary) lines.push(` * ${ep.summary}`);
261
+ if (ep.description && ep.description !== ep.summary) {
262
+ lines.push(` * ${ep.description}`);
263
+ }
264
+ lines.push(` * \`${ep.method.toUpperCase()} ${ep.path}\``);
265
+ if (ep.tags.length > 0) lines.push(` * @tags ${ep.tags.join(", ")}`);
266
+ lines.push(` */`);
267
+ }
268
+ const params = buildParams(ep, typeName, isTS);
269
+ const returnType = isTS ? buildReturnType(ep, typeName) : "";
270
+ const asyncKw = "async ";
271
+ lines.push(`export ${asyncKw}function ${ep.operationId}(${params})${returnType} {`);
272
+ const callLines = buildAxiosCall(ep, instanceExport, typeName, isTS);
273
+ for (const l of callLines) {
274
+ lines.push(` ${l}`);
275
+ }
276
+ lines.push(`}`);
277
+ return lines.join("\n");
278
+ }
279
+ function buildParams(ep, typeName, isTS) {
280
+ const parts = [];
281
+ for (const p of ep.pathParams) {
282
+ const type = isTS ? `: ${typeName}PathParams['${p.name}']` : "";
283
+ parts.push(`${p.name}${type}`);
284
+ }
285
+ if (ep.bodySchema) {
286
+ const optional = !ep.bodyRequired ? "?" : "";
287
+ const type = isTS ? `: ${typeName}Body` : "";
288
+ parts.push(`data${optional}${type}`);
289
+ }
290
+ if (ep.queryParams.length > 0) {
291
+ const optional = ep.queryParams.every((p) => !p.required) ? "?" : "";
292
+ const type = isTS ? `: ${typeName}QueryParams` : "";
293
+ parts.push(`params${optional}${type}`);
294
+ }
295
+ return parts.join(", ");
296
+ }
297
+ function buildReturnType(ep, typeName) {
298
+ const responseType = ep.responseSchema ? `${typeName}Response` : "unknown";
299
+ return `: Promise<AxiosResponse<${responseType}>>`;
300
+ }
301
+ function buildAxiosCall(ep, instanceExport, _typeName, _isTS) {
302
+ const lines = [];
303
+ const interpolatedPath = ep.pathParams.length > 0 ? "`" + ep.path.replace(/\{(\w+)\}/g, "${$1}") + "`" : `'${ep.path}'`;
304
+ const hasBody = !!ep.bodySchema;
305
+ const hasQuery = ep.queryParams.length > 0;
306
+ const method = ep.method.toLowerCase();
307
+ if (method === "get" || method === "delete" || method === "head") {
308
+ if (hasQuery) {
309
+ lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, { params })`);
310
+ } else {
311
+ lines.push(`return ${instanceExport}.${method}(${interpolatedPath})`);
312
+ }
313
+ } else {
314
+ const bodyArg = hasBody ? "data" : "undefined";
315
+ if (hasQuery) {
316
+ lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg}, { params })`);
317
+ } else {
318
+ lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg})`);
319
+ }
320
+ }
321
+ return lines;
322
+ }
323
+ function collectTypeImports(endpoints) {
324
+ const names = /* @__PURE__ */ new Set();
325
+ for (const ep of endpoints) {
326
+ const typeName = operationToTypeName(ep.operationId);
327
+ if (ep.pathParams.length > 0) names.add(`${typeName}PathParams`);
328
+ if (ep.queryParams.length > 0) names.add(`${typeName}QueryParams`);
329
+ if (ep.bodySchema) names.add(`${typeName}Body`);
330
+ if (ep.responseSchema) names.add(`${typeName}Response`);
331
+ }
332
+ return [...names].sort();
333
+ }
334
+
335
+ // src/generate.ts
336
+ async function generate(config, cwd) {
337
+ const specPath = path2.resolve(cwd, config.input);
338
+ const spec = parseOpenAPIFile(specPath);
339
+ const endpoints = extractEndpoints(spec, config.tags);
340
+ if (endpoints.length === 0) {
341
+ throw new Error("No endpoints found in the OpenAPI spec. Check your input file or tags filter.");
342
+ }
343
+ const clientPath = path2.resolve(cwd, config.output.client);
344
+ const typesPath = config.output.types ? path2.resolve(cwd, config.output.types) : void 0;
345
+ let typesRelativePath;
346
+ if (typesPath) {
347
+ typesRelativePath = toRelativeImportPath(path2.dirname(clientPath), typesPath);
348
+ }
349
+ const schemas = spec.components?.schemas ?? {};
350
+ let typesContent;
351
+ if (config.language !== "js" && typesPath) {
352
+ typesContent = generateTypesFile(endpoints, schemas);
353
+ }
354
+ const clientContent = generateClientFile({
355
+ endpoints,
356
+ config,
357
+ typesRelativePath
358
+ });
359
+ ensureDir(path2.dirname(clientPath));
360
+ fs2.writeFileSync(clientPath, clientContent, "utf-8");
361
+ if (typesPath && typesContent) {
362
+ ensureDir(path2.dirname(typesPath));
363
+ fs2.writeFileSync(typesPath, typesContent, "utf-8");
364
+ }
365
+ return {
366
+ clientPath,
367
+ typesPath,
368
+ endpointCount: endpoints.length
369
+ };
370
+ }
371
+ function ensureDir(dir) {
372
+ if (!fs2.existsSync(dir)) {
373
+ fs2.mkdirSync(dir, { recursive: true });
374
+ }
375
+ }
376
+ function toRelativeImportPath(fromDir, toFile) {
377
+ let rel = path2.relative(fromDir, toFile);
378
+ rel = rel.replace(/\.(ts|js|d\.ts)$/, "");
379
+ if (!rel.startsWith(".")) {
380
+ rel = "./" + rel;
381
+ }
382
+ return rel.replace(/\\/g, "/");
383
+ }
384
+
385
+ // src/config/loader.ts
386
+ import path3 from "path";
387
+ import fs3 from "fs";
388
+ var CONFIG_FILES = ["axigen.config.js", "axigen.config.cjs", "axigen.config.mjs", "axigen.config.ts"];
389
+ async function loadConfig(cwd, configPath) {
390
+ if (configPath) {
391
+ const abs = path3.resolve(cwd, configPath);
392
+ if (!fs3.existsSync(abs)) {
393
+ throw new Error(`Config file not found: ${abs}`);
394
+ }
395
+ return importConfig(abs);
396
+ }
397
+ for (const filename of CONFIG_FILES) {
398
+ const abs = path3.join(cwd, filename);
399
+ if (fs3.existsSync(abs)) {
400
+ return importConfig(abs);
401
+ }
402
+ }
403
+ throw new Error(`No config file found. Create one of: ${CONFIG_FILES.join(", ")}
404
+ Or run: axigen init`);
405
+ }
406
+ async function importConfig(filePath) {
407
+ let raw;
408
+ try {
409
+ const mod = await import(filePath);
410
+ raw = mod.default ?? mod;
411
+ } catch {
412
+ throw new Error(`Failed to load config: ${filePath}`);
413
+ }
414
+ return validateConfig(raw, filePath);
415
+ }
416
+ function validateConfig(raw, filePath) {
417
+ if (!raw || typeof raw !== "object") {
418
+ throw new Error(`Invalid config in ${filePath}: must export an object`);
419
+ }
420
+ const cfg = raw;
421
+ if (!cfg.input || typeof cfg.input !== "string") {
422
+ throw new Error(`Config error: "input" must be a string path to your OpenAPI file`);
423
+ }
424
+ if (!cfg.output || typeof cfg.output !== "object") {
425
+ throw new Error(`Config error: "output" must be an object with at least "client" path`);
426
+ }
427
+ const output = cfg.output;
428
+ if (!output.client || typeof output.client !== "string") {
429
+ throw new Error(`Config error: "output.client" is required`);
430
+ }
431
+ if (!cfg.axiosInstancePath || typeof cfg.axiosInstancePath !== "string") {
432
+ throw new Error(`Config error: "axiosInstancePath" is required (path to your axios instance)`);
433
+ }
434
+ return {
435
+ input: cfg.input,
436
+ output: {
437
+ client: output.client,
438
+ types: typeof output.types === "string" ? output.types : void 0
439
+ },
440
+ axiosInstancePath: cfg.axiosInstancePath,
441
+ axiosInstanceExport: typeof cfg.axiosInstanceExport === "string" ? cfg.axiosInstanceExport : "axiosInstance",
442
+ language: cfg.language === "js" ? "js" : "ts",
443
+ jsdoc: cfg.jsdoc !== false,
444
+ tags: Array.isArray(cfg.tags) ? cfg.tags : void 0
445
+ };
446
+ }
447
+ export {
448
+ extractEndpoints,
449
+ generate,
450
+ generateClientFile,
451
+ generateTypesFile,
452
+ loadConfig,
453
+ parseOpenAPIFile
454
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "axigen",
3
+ "version": "0.1.0",
4
+ "description": "Generate typed Axios client functions from OpenAPI / Swagger specs",
5
+ "keywords": [
6
+ "openapi",
7
+ "swagger",
8
+ "axios",
9
+ "axigen",
10
+ "typescript",
11
+ "cli"
12
+ ],
13
+ "author": "",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/HamidrezaSafarpour/axigen.git"
18
+ },
19
+ "bin": {
20
+ "axigen": "./dist/cli/index.js"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs",
28
+ "types": "./dist/index.d.ts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "build:watch": "tsup --watch",
37
+ "dev": "tsx src/cli/index.ts",
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "eslint src --ext .ts",
40
+ "prepublishOnly": "npm run build && npm run typecheck"
41
+ },
42
+ "dependencies": {
43
+ "chalk": "^5.3.0",
44
+ "commander": "^12.0.0",
45
+ "js-yaml": "^4.1.0",
46
+ "ora": "^8.0.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/js-yaml": "^4.0.9",
50
+ "@types/node": "^20.19.43",
51
+ "tsup": "^8.0.2",
52
+ "tsx": "^4.7.0",
53
+ "typescript": "^5.3.3"
54
+ },
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ }
58
+ }