axigen 0.1.0 → 1.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/README.md CHANGED
@@ -1,109 +1,239 @@
1
1
  # axigen
2
2
 
3
- > Generate typed Axios client functions from OpenAPI / Swagger specs
3
+ > Generate typed Axios client functions from your OpenAPI / Swagger spec — in one command.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/axigen)](https://www.npmjs.com/package/axigen)
6
6
  [![license](https://img.shields.io/npm/l/axigen)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/axigen)](https://nodejs.org)
7
8
 
8
- ## نصب
9
+ ## Overview
10
+
11
+ **axigen** reads your OpenAPI (or Swagger) spec and generates fully-typed Axios wrapper functions, so you never have to write boilerplate API calls by hand again.
12
+
13
+ - ✅ Supports OpenAPI 3.x and Swagger 2.x (YAML or JSON)
14
+ - ✅ Generates typed request/response interfaces
15
+ - ✅ Works with your own Axios instance
16
+ - ✅ TypeScript and JavaScript output
17
+ - ✅ JSDoc comments from your spec's summaries
18
+ - ✅ Filter endpoints by tag
19
+ - ✅ Zero runtime dependencies in generated code
20
+
21
+ ## Installation
9
22
 
10
23
  ```bash
11
24
  npm install -D axigen
12
- # یا
25
+ # or
13
26
  pnpm add -D axigen
27
+ # or
28
+ yarn add -D axigen
14
29
  ```
15
30
 
16
- ## شروع سریع
31
+ ## Quick Start
17
32
 
18
- **۱. ساخت فایل کانفیگ:**
33
+ **1. Create a config file:**
19
34
 
20
35
  ```bash
21
36
  npx axigen init
22
37
  ```
23
38
 
24
- **۲. ویرایش `axigen.config.js`:**
39
+ **2. Edit `axigen.config.js`:**
25
40
 
26
41
  ```js
27
42
  /** @type {import('axigen').AxigenConfig} */
28
43
  module.exports = {
44
+ // Path to your OpenAPI spec (YAML or JSON)
29
45
  input: "./openapi.yaml",
46
+
30
47
  output: {
48
+ // Generated Axios client functions
31
49
  client: "./src/api/client.ts",
50
+ // Generated TypeScript types
32
51
  types: "./src/api/types.ts",
33
52
  },
53
+
54
+ // Import path to your Axios instance inside your project
34
55
  axiosInstancePath: "../lib/axios",
56
+
35
57
  language: "ts",
36
58
  jsdoc: true,
37
59
  };
38
60
  ```
39
61
 
40
- **۳. Generate:**
62
+ **3. Run:**
41
63
 
42
64
  ```bash
43
65
  npx axigen generate
44
- # یا کوتاه‌تر:
66
+ # or simply:
45
67
  npx axigen
46
68
  ```
47
69
 
48
- ## خروجی
70
+ ---
49
71
 
50
- از این OpenAPI:
72
+ ## Example
73
+
74
+ Given this OpenAPI spec:
51
75
 
52
76
  ```yaml
53
77
  paths:
78
+ /users:
79
+ get:
80
+ operationId: getUsers
81
+ summary: List all users
82
+ parameters:
83
+ - name: page
84
+ in: query
85
+ schema:
86
+ type: integer
87
+ - name: limit
88
+ in: query
89
+ schema:
90
+ type: integer
91
+
54
92
  /users/{userId}:
55
93
  get:
56
94
  operationId: getUserById
95
+ summary: Get a user by ID
96
+ parameters:
97
+ - name: userId
98
+ in: path
99
+ required: true
100
+ schema:
101
+ type: string
102
+
103
+ put:
104
+ operationId: updateUser
105
+ summary: Update a user
57
106
  parameters:
58
107
  - name: userId
59
108
  in: path
60
109
  required: true
110
+ schema:
111
+ type: string
112
+ requestBody:
113
+ required: true
114
+ content:
115
+ application/json:
116
+ schema:
117
+ $ref: "#/components/schemas/UpdateUserBody"
61
118
  ```
62
119
 
63
- این کد generate میشه:
120
+ axigen generates:
64
121
 
65
122
  ```ts
123
+ // src/api/client.ts — auto-generated by axigen, do not edit
124
+
125
+ import type { AxiosResponse } from "axios";
126
+ import { axiosInstance } from "../lib/axios";
127
+ import type {
128
+ GetUsersQueryParams,
129
+ GetUsersResponse,
130
+ GetUserByIdPathParams,
131
+ GetUserByIdResponse,
132
+ UpdateUserPathParams,
133
+ UpdateUserBody,
134
+ UpdateUserResponse,
135
+ } from "./types";
136
+
66
137
  /**
67
- * دریافت کاربر با ID
138
+ * List all users
139
+ * `GET /users`
140
+ */
141
+ export async function getUsers(params?: GetUsersQueryParams): Promise<AxiosResponse<GetUsersResponse>> {
142
+ return axiosInstance.get("/users", { params });
143
+ }
144
+
145
+ /**
146
+ * Get a user by ID
68
147
  * `GET /users/{userId}`
69
148
  */
70
149
  export async function getUserById(userId: GetUserByIdPathParams["userId"]): Promise<AxiosResponse<GetUserByIdResponse>> {
71
150
  return axiosInstance.get(`/users/${userId}`);
72
151
  }
152
+
153
+ /**
154
+ * Update a user
155
+ * `PUT /users/{userId}`
156
+ */
157
+ export async function updateUser(userId: UpdateUserPathParams["userId"], data: UpdateUserBody): Promise<AxiosResponse<UpdateUserResponse>> {
158
+ return axiosInstance.put(`/users/${userId}`, data);
159
+ }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Configuration
165
+
166
+ | Option | Type | Default | Description |
167
+ | --------------------- | -------------- | --------------- | ----------------------------------------------------- |
168
+ | `input` | `string` | — | Path to your OpenAPI spec file (YAML or JSON) |
169
+ | `output.client` | `string` | — | Output path for the generated client functions |
170
+ | `output.types` | `string` | — | Output path for generated TypeScript types (optional) |
171
+ | `axiosInstancePath` | `string` | — | Import path to your Axios instance |
172
+ | `axiosInstanceExport` | `string` | `axiosInstance` | Named export of your Axios instance |
173
+ | `language` | `'ts' \| 'js'` | `'ts'` | Output language |
174
+ | `jsdoc` | `boolean` | `true` | Add JSDoc comments to generated functions |
175
+ | `tags` | `string[]` | — | Only generate endpoints matching these tags |
176
+
177
+ ### Axios Instance
178
+
179
+ axigen does not create an Axios instance for you — it imports the one you already have in your project. For example:
180
+
181
+ ```ts
182
+ // src/lib/axios.ts
183
+ import axios from "axios";
184
+
185
+ export const axiosInstance = axios.create({
186
+ baseURL: "https://api.example.com/v1",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ },
190
+ });
73
191
  ```
74
192
 
75
- ## گزینه‌های کانفیگ
193
+ Then in your config:
194
+
195
+ ```js
196
+ axiosInstancePath: '../lib/axios',
197
+ axiosInstanceExport: 'axiosInstance', // default, can be omitted
198
+ ```
76
199
 
77
- | گزینه | نوع | پیش‌فرض | توضیح |
78
- | --------------------- | -------------- | --------------- | ----------------------------------- |
79
- | `input` | `string` | — | مسیر فایل OpenAPI (yaml یا json) |
80
- | `output.client` | `string` | — | مسیر خروجی فایل توابع |
81
- | `output.types` | `string` | — | مسیر خروجی فایل types (اختیاری) |
82
- | `axiosInstancePath` | `string` | — | مسیر import مربوط به axios instance |
83
- | `axiosInstanceExport` | `string` | `axiosInstance` | نام export |
84
- | `language` | `'ts' \| 'js'` | `'ts'` | زبان خروجی |
85
- | `jsdoc` | `boolean` | `true` | اضافه کردن JSDoc |
86
- | `tags` | `string[]` | — | فیلتر بر اساس تگ |
200
+ ---
87
201
 
88
- ## دستورات CLI
202
+ ## CLI
89
203
 
90
204
  ```bash
205
+ # Generate client from config (default command)
91
206
  axigen generate [--config <path>] [--cwd <path>]
207
+
208
+ # Create a starter config file
92
209
  axigen init
210
+
211
+ # Show version
93
212
  axigen --version
213
+
214
+ # Show help
94
215
  axigen --help
95
216
  ```
96
217
 
97
- ## استفاده programmatic
218
+ ---
219
+
220
+ ## Programmatic Usage
221
+
222
+ You can also use axigen directly in Node.js scripts:
98
223
 
99
224
  ```ts
100
225
  import { generate, loadConfig } from "axigen";
101
226
 
102
227
  const config = await loadConfig(process.cwd());
103
228
  const result = await generate(config, process.cwd());
104
- console.log(`Generated ${result.endpointCount} endpoints`);
229
+
230
+ console.log(`✓ Generated ${result.endpointCount} endpoints`);
231
+ console.log(` client → ${result.clientPath}`);
232
+ console.log(` types → ${result.typesPath}`);
105
233
  ```
106
234
 
107
- ## لایسنس
235
+ ---
236
+
237
+ ## License
108
238
 
109
239
  MIT
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/cli/index.ts
5
4
  import { Command } from "commander";
@@ -67,9 +66,52 @@ function validateConfig(raw, filePath) {
67
66
  axiosInstanceExport: typeof cfg.axiosInstanceExport === "string" ? cfg.axiosInstanceExport : "axiosInstance",
68
67
  language: cfg.language === "js" ? "js" : "ts",
69
68
  jsdoc: cfg.jsdoc !== false,
70
- tags: Array.isArray(cfg.tags) ? cfg.tags : void 0
69
+ tags: Array.isArray(cfg.tags) ? cfg.tags : void 0,
70
+ functionName: validateFunctionNameConfig(cfg.functionName, filePath)
71
71
  };
72
72
  }
73
+ function validateFunctionNameConfig(raw, filePath) {
74
+ if (raw === void 0 || raw === null) return void 0;
75
+ if (typeof raw !== "object") {
76
+ throw new Error(`Config error: "functionName" must be an object`);
77
+ }
78
+ const fn = raw;
79
+ const result = {};
80
+ if (fn.transforms !== void 0) {
81
+ if (!Array.isArray(fn.transforms)) {
82
+ throw new Error(`Config error: "functionName.transforms" must be an array`);
83
+ }
84
+ result.transforms = fn.transforms.map((t, i) => {
85
+ if (!t || typeof t !== "object") {
86
+ throw new Error(`Config error: "functionName.transforms[${i}]" must be an object`);
87
+ }
88
+ const transform = t;
89
+ if (typeof transform.match !== "string") {
90
+ throw new Error(`Config error: "functionName.transforms[${i}].match" must be a string regex pattern`);
91
+ }
92
+ if (typeof transform.replacement !== "string") {
93
+ throw new Error(`Config error: "functionName.transforms[${i}].replacement" must be a string`);
94
+ }
95
+ try {
96
+ new RegExp(transform.match, transform.flags ?? "g");
97
+ } catch {
98
+ throw new Error(`Config error: "functionName.transforms[${i}].match" is not a valid regex: ${transform.match}`);
99
+ }
100
+ return {
101
+ match: transform.match,
102
+ flags: typeof transform.flags === "string" ? transform.flags : "g",
103
+ replacement: transform.replacement
104
+ };
105
+ });
106
+ }
107
+ if (fn.appendMethod !== void 0) {
108
+ if (typeof fn.appendMethod !== "boolean" && !Array.isArray(fn.appendMethod)) {
109
+ throw new Error(`Config error: "functionName.appendMethod" must be a boolean or an array of HTTP methods`);
110
+ }
111
+ result.appendMethod = fn.appendMethod;
112
+ }
113
+ return result;
114
+ }
73
115
 
74
116
  // src/generate.ts
75
117
  import fs3 from "fs";
@@ -244,6 +286,7 @@ function generateTypesFile(endpoints, schemas = {}) {
244
286
  lines.push(`// This file is auto-generated by axigen. DO NOT EDIT.`);
245
287
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
246
288
  lines.push("");
289
+ const declaredInComponents = new Set(Object.keys(schemas));
247
290
  if (Object.keys(schemas).length > 0) {
248
291
  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");
249
292
  lines.push("");
@@ -255,48 +298,118 @@ function generateTypesFile(endpoints, schemas = {}) {
255
298
  lines.push("");
256
299
  }
257
300
  }
258
- 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");
259
- lines.push("");
301
+ const endpointLines = [];
260
302
  for (const ep of endpoints) {
261
303
  const baseName = operationToTypeName(ep.operationId);
262
304
  if (ep.pathParams.length > 0) {
263
- lines.push(`export interface ${baseName}PathParams {`);
305
+ endpointLines.push(`export interface ${baseName}PathParams {`);
264
306
  for (const p of ep.pathParams) {
265
307
  const type = schemaToTSType(p.schema);
266
308
  const comment = p.description ? ` /** ${p.description} */
267
309
  ` : "";
268
- lines.push(`${comment} ${p.name}: ${type}`);
310
+ endpointLines.push(`${comment} ${p.name}: ${type}`);
269
311
  }
270
- lines.push(`}`);
271
- lines.push("");
312
+ endpointLines.push(`}`);
313
+ endpointLines.push("");
272
314
  }
273
315
  if (ep.queryParams.length > 0) {
274
- lines.push(`export interface ${baseName}QueryParams {`);
316
+ endpointLines.push(`export interface ${baseName}QueryParams {`);
275
317
  for (const p of ep.queryParams) {
276
318
  const optional = !p.required ? "?" : "";
277
319
  const type = schemaToTSType(p.schema);
278
320
  const comment = p.description ? ` /** ${p.description} */
279
321
  ` : "";
280
- lines.push(`${comment} ${p.name}${optional}: ${type}`);
322
+ endpointLines.push(`${comment} ${p.name}${optional}: ${type}`);
281
323
  }
282
- lines.push(`}`);
283
- lines.push("");
324
+ endpointLines.push(`}`);
325
+ endpointLines.push("");
284
326
  }
285
327
  if (ep.bodySchema) {
286
- lines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
287
- lines.push("");
328
+ const refName = getRefName(ep.bodySchema);
329
+ if (refName && declaredInComponents.has(refName)) {
330
+ if (`${baseName}Body` !== refName) {
331
+ endpointLines.push(`export type ${baseName}Body = ${refName}`);
332
+ endpointLines.push("");
333
+ }
334
+ } else {
335
+ endpointLines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
336
+ endpointLines.push("");
337
+ }
288
338
  }
289
339
  if (ep.responseSchema) {
290
- lines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
291
- lines.push("");
340
+ const refName = getRefName(ep.responseSchema);
341
+ if (refName && declaredInComponents.has(refName)) {
342
+ if (`${baseName}Response` !== refName) {
343
+ endpointLines.push(`export type ${baseName}Response = ${refName}`);
344
+ endpointLines.push("");
345
+ }
346
+ } else {
347
+ endpointLines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
348
+ endpointLines.push("");
349
+ }
292
350
  }
293
351
  }
352
+ if (endpointLines.length > 0) {
353
+ 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");
354
+ lines.push("");
355
+ lines.push(...endpointLines);
356
+ }
294
357
  return lines.join("\n");
295
358
  }
359
+ function getRefName(schema) {
360
+ if (schema.$ref) return refToTypeName(schema.$ref);
361
+ return void 0;
362
+ }
296
363
  function operationToTypeName(operationId) {
297
364
  return operationId.charAt(0).toUpperCase() + operationId.slice(1);
298
365
  }
299
366
 
367
+ // src/generator/function-name.ts
368
+ function resolveFunctionName(operationId, method, config) {
369
+ if (!config) return operationId;
370
+ let name = operationId;
371
+ if (config.transforms && config.transforms.length > 0) {
372
+ for (const transform of config.transforms) {
373
+ name = applyTransform(name, transform);
374
+ }
375
+ }
376
+ if (shouldAppendMethod(method, config.appendMethod)) {
377
+ const suffix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
378
+ name = name + suffix;
379
+ }
380
+ return name;
381
+ }
382
+ function applyTransform(input, transform) {
383
+ const flags = transform.flags ?? "g";
384
+ const regex = new RegExp(transform.match, flags);
385
+ const { replacement } = transform;
386
+ const upperMatch = replacement.match(/^upper:\$(\d+)$/);
387
+ if (upperMatch) {
388
+ const groupIndex = parseInt(upperMatch[1], 10);
389
+ return input.replace(regex, (...args) => {
390
+ const captured = args[groupIndex];
391
+ return captured ? captured.toUpperCase() : "";
392
+ });
393
+ }
394
+ const lowerMatch = replacement.match(/^lower:\$(\d+)$/);
395
+ if (lowerMatch) {
396
+ const groupIndex = parseInt(lowerMatch[1], 10);
397
+ return input.replace(regex, (...args) => {
398
+ const captured = args[groupIndex];
399
+ return captured ? captured.toLowerCase() : "";
400
+ });
401
+ }
402
+ return input.replace(regex, replacement);
403
+ }
404
+ function shouldAppendMethod(method, appendMethod) {
405
+ if (!appendMethod) return false;
406
+ if (appendMethod === true) return true;
407
+ if (Array.isArray(appendMethod)) {
408
+ return appendMethod.map((m) => m.toLowerCase()).includes(method.toLowerCase());
409
+ }
410
+ return false;
411
+ }
412
+
300
413
  // src/generator/axios.ts
301
414
  function generateClientFile(opts) {
302
415
  const { endpoints, config, typesRelativePath } = opts;
@@ -307,7 +420,7 @@ function generateClientFile(opts) {
307
420
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
308
421
  lines.push("");
309
422
  if (isTS) {
310
- lines.push(`import type { AxiosResponse } from 'axios'`);
423
+ lines.push(`import type { AxiosRequestConfig, AxiosResponse } from 'axios'`);
311
424
  }
312
425
  lines.push(`import { ${instanceExport} } from '${config.axiosInstancePath}'`);
313
426
  if (isTS && typesRelativePath) {
@@ -319,13 +432,15 @@ function generateClientFile(opts) {
319
432
  lines.push("");
320
433
  lines.push("");
321
434
  for (const ep of endpoints) {
322
- const fn = buildFunction(ep, isTS, instanceExport, config.jsdoc !== false);
435
+ const fn = buildFunction(ep, isTS, instanceExport, config);
323
436
  lines.push(fn);
324
437
  lines.push("");
325
438
  }
326
439
  return lines.join("\n");
327
440
  }
328
- function buildFunction(ep, isTS, instanceExport, jsdoc) {
441
+ function buildFunction(ep, isTS, instanceExport, config) {
442
+ const jsdoc = config.jsdoc !== false;
443
+ const fnName = resolveFunctionName(ep.operationId, ep.method, config.functionName);
329
444
  const typeName = operationToTypeName(ep.operationId);
330
445
  const lines = [];
331
446
  if (jsdoc) {
@@ -340,9 +455,8 @@ function buildFunction(ep, isTS, instanceExport, jsdoc) {
340
455
  }
341
456
  const params = buildParams(ep, typeName, isTS);
342
457
  const returnType = isTS ? buildReturnType(ep, typeName) : "";
343
- const asyncKw = "async ";
344
- lines.push(`export ${asyncKw}function ${ep.operationId}(${params})${returnType} {`);
345
- const callLines = buildAxiosCall(ep, instanceExport, typeName, isTS);
458
+ lines.push(`export async function ${fnName}(${params})${returnType} {`);
459
+ const callLines = buildAxiosCall(ep, instanceExport);
346
460
  for (const l of callLines) {
347
461
  lines.push(` ${l}`);
348
462
  }
@@ -365,31 +479,36 @@ function buildParams(ep, typeName, isTS) {
365
479
  const type = isTS ? `: ${typeName}QueryParams` : "";
366
480
  parts.push(`params${optional}${type}`);
367
481
  }
482
+ if (isTS) {
483
+ parts.push(`config?: AxiosRequestConfig`);
484
+ parts.push(`options?: AxiosRequestConfig`);
485
+ } else {
486
+ parts.push(`config`);
487
+ parts.push(`options`);
488
+ }
368
489
  return parts.join(", ");
369
490
  }
370
491
  function buildReturnType(ep, typeName) {
371
492
  const responseType = ep.responseSchema ? `${typeName}Response` : "unknown";
372
493
  return `: Promise<AxiosResponse<${responseType}>>`;
373
494
  }
374
- function buildAxiosCall(ep, instanceExport, _typeName, _isTS) {
495
+ function buildAxiosCall(ep, instanceExport) {
375
496
  const lines = [];
376
497
  const interpolatedPath = ep.pathParams.length > 0 ? "`" + ep.path.replace(/\{(\w+)\}/g, "${$1}") + "`" : `'${ep.path}'`;
377
498
  const hasBody = !!ep.bodySchema;
378
499
  const hasQuery = ep.queryParams.length > 0;
379
- const method = ep.method.toLowerCase();
380
- if (method === "get" || method === "delete" || method === "head") {
381
- if (hasQuery) {
382
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, { params })`);
383
- } else {
384
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath})`);
385
- }
500
+ if (hasQuery) {
501
+ lines.push(`const mergedConfig: AxiosRequestConfig = { ...config, params: { ...params, ...config?.params } }`);
502
+ }
503
+ const configArg = hasQuery ? "mergedConfig" : "config";
504
+ if (hasBody) {
505
+ lines.push(
506
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, data, ...${configArg} }, options)`
507
+ );
386
508
  } else {
387
- const bodyArg = hasBody ? "data" : "undefined";
388
- if (hasQuery) {
389
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg}, { params })`);
390
- } else {
391
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg})`);
392
- }
509
+ lines.push(
510
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, ...${configArg} }, options)`
511
+ );
393
512
  }
394
513
  return lines;
395
514
  }
package/dist/index.d.mts CHANGED
@@ -74,17 +74,66 @@ interface SchemaObject {
74
74
  default?: unknown;
75
75
  example?: unknown;
76
76
  }
77
+ interface FunctionNameTransform {
78
+ /**
79
+ * Regex pattern to match parts of the operationId.
80
+ * Example: to convert "product-variant-create" to "productVariantCreate"
81
+ * use: { match: '-([a-z])', replacement: (_, c) => c.toUpperCase() }
82
+ *
83
+ * Defined as a string pattern (flags supported via `flags` field).
84
+ */
85
+ match: string;
86
+ /** Regex flags (e.g. 'g', 'gi'). Defaults to 'g'. */
87
+ flags?: string;
88
+ /**
89
+ * Replacement string. Supports capture groups via $1, $2, etc.
90
+ * Example: to capitalize the first char after a dash: use a replacer function
91
+ * defined as a template string like "upper:$1" — see docs for special tokens.
92
+ *
93
+ * Special tokens:
94
+ * "upper:$1" → uppercase capture group 1
95
+ * "lower:$1" → lowercase capture group 1
96
+ * any other string is used as-is (standard String.replace replacement)
97
+ */
98
+ replacement: string;
99
+ }
100
+ interface FunctionNameConfig {
101
+ /**
102
+ * One or more regex transforms applied in order to the raw operationId.
103
+ * Each transform is applied to the result of the previous one.
104
+ */
105
+ transforms?: FunctionNameTransform[];
106
+ /**
107
+ * Append the HTTP method to the end of the function name.
108
+ * Can be set globally or per-method.
109
+ *
110
+ * Examples:
111
+ * appendMethod: true → always append
112
+ * appendMethod: ['post','put'] → append only for these methods
113
+ */
114
+ appendMethod?: boolean | HttpMethod[];
115
+ }
77
116
  interface AxigenConfig {
117
+ /** Path to the OpenAPI spec file (YAML or JSON) */
78
118
  input: string;
79
119
  output: {
120
+ /** Output path for generated Axios client functions */
80
121
  client: string;
122
+ /** Output path for generated TypeScript types (optional) */
81
123
  types?: string;
82
124
  };
125
+ /** Import path to the user's Axios instance */
83
126
  axiosInstancePath: string;
127
+ /** Named export of the Axios instance (default: "axiosInstance") */
84
128
  axiosInstanceExport?: string;
129
+ /** Output language (default: "ts") */
85
130
  language?: "ts" | "js";
131
+ /** Add JSDoc comments to generated functions (default: true) */
86
132
  jsdoc?: boolean;
133
+ /** Only generate endpoints matching these tags */
87
134
  tags?: string[];
135
+ /** Controls how generated function names are derived from operationIds */
136
+ functionName?: FunctionNameConfig;
88
137
  }
89
138
  interface ParsedEndpoint {
90
139
  operationId: string;
@@ -121,7 +170,7 @@ declare function extractEndpoints(spec: OpenAPISpec, filterTags?: string[]): Par
121
170
  interface GenerateClientOptions {
122
171
  endpoints: ParsedEndpoint[];
123
172
  config: AxigenConfig;
124
- /** مسیر نسبی فایل types نسبت به فایل client */
173
+ /** Relative path to the types file from the client file */
125
174
  typesRelativePath?: string;
126
175
  }
127
176
  declare function generateClientFile(opts: GenerateClientOptions): string;
package/dist/index.d.ts CHANGED
@@ -74,17 +74,66 @@ interface SchemaObject {
74
74
  default?: unknown;
75
75
  example?: unknown;
76
76
  }
77
+ interface FunctionNameTransform {
78
+ /**
79
+ * Regex pattern to match parts of the operationId.
80
+ * Example: to convert "product-variant-create" to "productVariantCreate"
81
+ * use: { match: '-([a-z])', replacement: (_, c) => c.toUpperCase() }
82
+ *
83
+ * Defined as a string pattern (flags supported via `flags` field).
84
+ */
85
+ match: string;
86
+ /** Regex flags (e.g. 'g', 'gi'). Defaults to 'g'. */
87
+ flags?: string;
88
+ /**
89
+ * Replacement string. Supports capture groups via $1, $2, etc.
90
+ * Example: to capitalize the first char after a dash: use a replacer function
91
+ * defined as a template string like "upper:$1" — see docs for special tokens.
92
+ *
93
+ * Special tokens:
94
+ * "upper:$1" → uppercase capture group 1
95
+ * "lower:$1" → lowercase capture group 1
96
+ * any other string is used as-is (standard String.replace replacement)
97
+ */
98
+ replacement: string;
99
+ }
100
+ interface FunctionNameConfig {
101
+ /**
102
+ * One or more regex transforms applied in order to the raw operationId.
103
+ * Each transform is applied to the result of the previous one.
104
+ */
105
+ transforms?: FunctionNameTransform[];
106
+ /**
107
+ * Append the HTTP method to the end of the function name.
108
+ * Can be set globally or per-method.
109
+ *
110
+ * Examples:
111
+ * appendMethod: true → always append
112
+ * appendMethod: ['post','put'] → append only for these methods
113
+ */
114
+ appendMethod?: boolean | HttpMethod[];
115
+ }
77
116
  interface AxigenConfig {
117
+ /** Path to the OpenAPI spec file (YAML or JSON) */
78
118
  input: string;
79
119
  output: {
120
+ /** Output path for generated Axios client functions */
80
121
  client: string;
122
+ /** Output path for generated TypeScript types (optional) */
81
123
  types?: string;
82
124
  };
125
+ /** Import path to the user's Axios instance */
83
126
  axiosInstancePath: string;
127
+ /** Named export of the Axios instance (default: "axiosInstance") */
84
128
  axiosInstanceExport?: string;
129
+ /** Output language (default: "ts") */
85
130
  language?: "ts" | "js";
131
+ /** Add JSDoc comments to generated functions (default: true) */
86
132
  jsdoc?: boolean;
133
+ /** Only generate endpoints matching these tags */
87
134
  tags?: string[];
135
+ /** Controls how generated function names are derived from operationIds */
136
+ functionName?: FunctionNameConfig;
88
137
  }
89
138
  interface ParsedEndpoint {
90
139
  operationId: string;
@@ -121,7 +170,7 @@ declare function extractEndpoints(spec: OpenAPISpec, filterTags?: string[]): Par
121
170
  interface GenerateClientOptions {
122
171
  endpoints: ParsedEndpoint[];
123
172
  config: AxigenConfig;
124
- /** مسیر نسبی فایل types نسبت به فایل client */
173
+ /** Relative path to the types file from the client file */
125
174
  typesRelativePath?: string;
126
175
  }
127
176
  declare function generateClientFile(opts: GenerateClientOptions): string;
package/dist/index.js CHANGED
@@ -212,6 +212,7 @@ function generateTypesFile(endpoints, schemas = {}) {
212
212
  lines.push(`// This file is auto-generated by axigen. DO NOT EDIT.`);
213
213
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
214
214
  lines.push("");
215
+ const declaredInComponents = new Set(Object.keys(schemas));
215
216
  if (Object.keys(schemas).length > 0) {
216
217
  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");
217
218
  lines.push("");
@@ -223,48 +224,118 @@ function generateTypesFile(endpoints, schemas = {}) {
223
224
  lines.push("");
224
225
  }
225
226
  }
226
- 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");
227
- lines.push("");
227
+ const endpointLines = [];
228
228
  for (const ep of endpoints) {
229
229
  const baseName = operationToTypeName(ep.operationId);
230
230
  if (ep.pathParams.length > 0) {
231
- lines.push(`export interface ${baseName}PathParams {`);
231
+ endpointLines.push(`export interface ${baseName}PathParams {`);
232
232
  for (const p of ep.pathParams) {
233
233
  const type = schemaToTSType(p.schema);
234
234
  const comment = p.description ? ` /** ${p.description} */
235
235
  ` : "";
236
- lines.push(`${comment} ${p.name}: ${type}`);
236
+ endpointLines.push(`${comment} ${p.name}: ${type}`);
237
237
  }
238
- lines.push(`}`);
239
- lines.push("");
238
+ endpointLines.push(`}`);
239
+ endpointLines.push("");
240
240
  }
241
241
  if (ep.queryParams.length > 0) {
242
- lines.push(`export interface ${baseName}QueryParams {`);
242
+ endpointLines.push(`export interface ${baseName}QueryParams {`);
243
243
  for (const p of ep.queryParams) {
244
244
  const optional = !p.required ? "?" : "";
245
245
  const type = schemaToTSType(p.schema);
246
246
  const comment = p.description ? ` /** ${p.description} */
247
247
  ` : "";
248
- lines.push(`${comment} ${p.name}${optional}: ${type}`);
248
+ endpointLines.push(`${comment} ${p.name}${optional}: ${type}`);
249
249
  }
250
- lines.push(`}`);
251
- lines.push("");
250
+ endpointLines.push(`}`);
251
+ endpointLines.push("");
252
252
  }
253
253
  if (ep.bodySchema) {
254
- lines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
255
- lines.push("");
254
+ const refName = getRefName(ep.bodySchema);
255
+ if (refName && declaredInComponents.has(refName)) {
256
+ if (`${baseName}Body` !== refName) {
257
+ endpointLines.push(`export type ${baseName}Body = ${refName}`);
258
+ endpointLines.push("");
259
+ }
260
+ } else {
261
+ endpointLines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
262
+ endpointLines.push("");
263
+ }
256
264
  }
257
265
  if (ep.responseSchema) {
258
- lines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
259
- lines.push("");
266
+ const refName = getRefName(ep.responseSchema);
267
+ if (refName && declaredInComponents.has(refName)) {
268
+ if (`${baseName}Response` !== refName) {
269
+ endpointLines.push(`export type ${baseName}Response = ${refName}`);
270
+ endpointLines.push("");
271
+ }
272
+ } else {
273
+ endpointLines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
274
+ endpointLines.push("");
275
+ }
260
276
  }
261
277
  }
278
+ if (endpointLines.length > 0) {
279
+ 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");
280
+ lines.push("");
281
+ lines.push(...endpointLines);
282
+ }
262
283
  return lines.join("\n");
263
284
  }
285
+ function getRefName(schema) {
286
+ if (schema.$ref) return refToTypeName(schema.$ref);
287
+ return void 0;
288
+ }
264
289
  function operationToTypeName(operationId) {
265
290
  return operationId.charAt(0).toUpperCase() + operationId.slice(1);
266
291
  }
267
292
 
293
+ // src/generator/function-name.ts
294
+ function resolveFunctionName(operationId, method, config) {
295
+ if (!config) return operationId;
296
+ let name = operationId;
297
+ if (config.transforms && config.transforms.length > 0) {
298
+ for (const transform of config.transforms) {
299
+ name = applyTransform(name, transform);
300
+ }
301
+ }
302
+ if (shouldAppendMethod(method, config.appendMethod)) {
303
+ const suffix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
304
+ name = name + suffix;
305
+ }
306
+ return name;
307
+ }
308
+ function applyTransform(input, transform) {
309
+ const flags = transform.flags ?? "g";
310
+ const regex = new RegExp(transform.match, flags);
311
+ const { replacement } = transform;
312
+ const upperMatch = replacement.match(/^upper:\$(\d+)$/);
313
+ if (upperMatch) {
314
+ const groupIndex = parseInt(upperMatch[1], 10);
315
+ return input.replace(regex, (...args) => {
316
+ const captured = args[groupIndex];
317
+ return captured ? captured.toUpperCase() : "";
318
+ });
319
+ }
320
+ const lowerMatch = replacement.match(/^lower:\$(\d+)$/);
321
+ if (lowerMatch) {
322
+ const groupIndex = parseInt(lowerMatch[1], 10);
323
+ return input.replace(regex, (...args) => {
324
+ const captured = args[groupIndex];
325
+ return captured ? captured.toLowerCase() : "";
326
+ });
327
+ }
328
+ return input.replace(regex, replacement);
329
+ }
330
+ function shouldAppendMethod(method, appendMethod) {
331
+ if (!appendMethod) return false;
332
+ if (appendMethod === true) return true;
333
+ if (Array.isArray(appendMethod)) {
334
+ return appendMethod.map((m) => m.toLowerCase()).includes(method.toLowerCase());
335
+ }
336
+ return false;
337
+ }
338
+
268
339
  // src/generator/axios.ts
269
340
  function generateClientFile(opts) {
270
341
  const { endpoints, config, typesRelativePath } = opts;
@@ -275,7 +346,7 @@ function generateClientFile(opts) {
275
346
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
276
347
  lines.push("");
277
348
  if (isTS) {
278
- lines.push(`import type { AxiosResponse } from 'axios'`);
349
+ lines.push(`import type { AxiosRequestConfig, AxiosResponse } from 'axios'`);
279
350
  }
280
351
  lines.push(`import { ${instanceExport} } from '${config.axiosInstancePath}'`);
281
352
  if (isTS && typesRelativePath) {
@@ -287,13 +358,15 @@ function generateClientFile(opts) {
287
358
  lines.push("");
288
359
  lines.push("");
289
360
  for (const ep of endpoints) {
290
- const fn = buildFunction(ep, isTS, instanceExport, config.jsdoc !== false);
361
+ const fn = buildFunction(ep, isTS, instanceExport, config);
291
362
  lines.push(fn);
292
363
  lines.push("");
293
364
  }
294
365
  return lines.join("\n");
295
366
  }
296
- function buildFunction(ep, isTS, instanceExport, jsdoc) {
367
+ function buildFunction(ep, isTS, instanceExport, config) {
368
+ const jsdoc = config.jsdoc !== false;
369
+ const fnName = resolveFunctionName(ep.operationId, ep.method, config.functionName);
297
370
  const typeName = operationToTypeName(ep.operationId);
298
371
  const lines = [];
299
372
  if (jsdoc) {
@@ -308,9 +381,8 @@ function buildFunction(ep, isTS, instanceExport, jsdoc) {
308
381
  }
309
382
  const params = buildParams(ep, typeName, isTS);
310
383
  const returnType = isTS ? buildReturnType(ep, typeName) : "";
311
- const asyncKw = "async ";
312
- lines.push(`export ${asyncKw}function ${ep.operationId}(${params})${returnType} {`);
313
- const callLines = buildAxiosCall(ep, instanceExport, typeName, isTS);
384
+ lines.push(`export async function ${fnName}(${params})${returnType} {`);
385
+ const callLines = buildAxiosCall(ep, instanceExport);
314
386
  for (const l of callLines) {
315
387
  lines.push(` ${l}`);
316
388
  }
@@ -333,31 +405,36 @@ function buildParams(ep, typeName, isTS) {
333
405
  const type = isTS ? `: ${typeName}QueryParams` : "";
334
406
  parts.push(`params${optional}${type}`);
335
407
  }
408
+ if (isTS) {
409
+ parts.push(`config?: AxiosRequestConfig`);
410
+ parts.push(`options?: AxiosRequestConfig`);
411
+ } else {
412
+ parts.push(`config`);
413
+ parts.push(`options`);
414
+ }
336
415
  return parts.join(", ");
337
416
  }
338
417
  function buildReturnType(ep, typeName) {
339
418
  const responseType = ep.responseSchema ? `${typeName}Response` : "unknown";
340
419
  return `: Promise<AxiosResponse<${responseType}>>`;
341
420
  }
342
- function buildAxiosCall(ep, instanceExport, _typeName, _isTS) {
421
+ function buildAxiosCall(ep, instanceExport) {
343
422
  const lines = [];
344
423
  const interpolatedPath = ep.pathParams.length > 0 ? "`" + ep.path.replace(/\{(\w+)\}/g, "${$1}") + "`" : `'${ep.path}'`;
345
424
  const hasBody = !!ep.bodySchema;
346
425
  const hasQuery = ep.queryParams.length > 0;
347
- const method = ep.method.toLowerCase();
348
- if (method === "get" || method === "delete" || method === "head") {
349
- if (hasQuery) {
350
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, { params })`);
351
- } else {
352
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath})`);
353
- }
426
+ if (hasQuery) {
427
+ lines.push(`const mergedConfig: AxiosRequestConfig = { ...config, params: { ...params, ...config?.params } }`);
428
+ }
429
+ const configArg = hasQuery ? "mergedConfig" : "config";
430
+ if (hasBody) {
431
+ lines.push(
432
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, data, ...${configArg} }, options)`
433
+ );
354
434
  } else {
355
- const bodyArg = hasBody ? "data" : "undefined";
356
- if (hasQuery) {
357
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg}, { params })`);
358
- } else {
359
- lines.push(`return ${instanceExport}.${method}(${interpolatedPath}, ${bodyArg})`);
360
- }
435
+ lines.push(
436
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, ...${configArg} }, options)`
437
+ );
361
438
  }
362
439
  return lines;
363
440
  }
@@ -482,9 +559,52 @@ function validateConfig(raw, filePath) {
482
559
  axiosInstanceExport: typeof cfg.axiosInstanceExport === "string" ? cfg.axiosInstanceExport : "axiosInstance",
483
560
  language: cfg.language === "js" ? "js" : "ts",
484
561
  jsdoc: cfg.jsdoc !== false,
485
- tags: Array.isArray(cfg.tags) ? cfg.tags : void 0
562
+ tags: Array.isArray(cfg.tags) ? cfg.tags : void 0,
563
+ functionName: validateFunctionNameConfig(cfg.functionName, filePath)
486
564
  };
487
565
  }
566
+ function validateFunctionNameConfig(raw, filePath) {
567
+ if (raw === void 0 || raw === null) return void 0;
568
+ if (typeof raw !== "object") {
569
+ throw new Error(`Config error: "functionName" must be an object`);
570
+ }
571
+ const fn = raw;
572
+ const result = {};
573
+ if (fn.transforms !== void 0) {
574
+ if (!Array.isArray(fn.transforms)) {
575
+ throw new Error(`Config error: "functionName.transforms" must be an array`);
576
+ }
577
+ result.transforms = fn.transforms.map((t, i) => {
578
+ if (!t || typeof t !== "object") {
579
+ throw new Error(`Config error: "functionName.transforms[${i}]" must be an object`);
580
+ }
581
+ const transform = t;
582
+ if (typeof transform.match !== "string") {
583
+ throw new Error(`Config error: "functionName.transforms[${i}].match" must be a string regex pattern`);
584
+ }
585
+ if (typeof transform.replacement !== "string") {
586
+ throw new Error(`Config error: "functionName.transforms[${i}].replacement" must be a string`);
587
+ }
588
+ try {
589
+ new RegExp(transform.match, transform.flags ?? "g");
590
+ } catch {
591
+ throw new Error(`Config error: "functionName.transforms[${i}].match" is not a valid regex: ${transform.match}`);
592
+ }
593
+ return {
594
+ match: transform.match,
595
+ flags: typeof transform.flags === "string" ? transform.flags : "g",
596
+ replacement: transform.replacement
597
+ };
598
+ });
599
+ }
600
+ if (fn.appendMethod !== void 0) {
601
+ if (typeof fn.appendMethod !== "boolean" && !Array.isArray(fn.appendMethod)) {
602
+ throw new Error(`Config error: "functionName.appendMethod" must be a boolean or an array of HTTP methods`);
603
+ }
604
+ result.appendMethod = fn.appendMethod;
605
+ }
606
+ return result;
607
+ }
488
608
  // Annotate the CommonJS export names for ESM import in node:
489
609
  0 && (module.exports = {
490
610
  extractEndpoints,
package/dist/index.mjs CHANGED
@@ -171,6 +171,7 @@ function generateTypesFile(endpoints, schemas = {}) {
171
171
  lines.push(`// This file is auto-generated by axigen. DO NOT EDIT.`);
172
172
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
173
173
  lines.push("");
174
+ const declaredInComponents = new Set(Object.keys(schemas));
174
175
  if (Object.keys(schemas).length > 0) {
175
176
  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
177
  lines.push("");
@@ -182,48 +183,118 @@ function generateTypesFile(endpoints, schemas = {}) {
182
183
  lines.push("");
183
184
  }
184
185
  }
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("");
186
+ const endpointLines = [];
187
187
  for (const ep of endpoints) {
188
188
  const baseName = operationToTypeName(ep.operationId);
189
189
  if (ep.pathParams.length > 0) {
190
- lines.push(`export interface ${baseName}PathParams {`);
190
+ endpointLines.push(`export interface ${baseName}PathParams {`);
191
191
  for (const p of ep.pathParams) {
192
192
  const type = schemaToTSType(p.schema);
193
193
  const comment = p.description ? ` /** ${p.description} */
194
194
  ` : "";
195
- lines.push(`${comment} ${p.name}: ${type}`);
195
+ endpointLines.push(`${comment} ${p.name}: ${type}`);
196
196
  }
197
- lines.push(`}`);
198
- lines.push("");
197
+ endpointLines.push(`}`);
198
+ endpointLines.push("");
199
199
  }
200
200
  if (ep.queryParams.length > 0) {
201
- lines.push(`export interface ${baseName}QueryParams {`);
201
+ endpointLines.push(`export interface ${baseName}QueryParams {`);
202
202
  for (const p of ep.queryParams) {
203
203
  const optional = !p.required ? "?" : "";
204
204
  const type = schemaToTSType(p.schema);
205
205
  const comment = p.description ? ` /** ${p.description} */
206
206
  ` : "";
207
- lines.push(`${comment} ${p.name}${optional}: ${type}`);
207
+ endpointLines.push(`${comment} ${p.name}${optional}: ${type}`);
208
208
  }
209
- lines.push(`}`);
210
- lines.push("");
209
+ endpointLines.push(`}`);
210
+ endpointLines.push("");
211
211
  }
212
212
  if (ep.bodySchema) {
213
- lines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
214
- lines.push("");
213
+ const refName = getRefName(ep.bodySchema);
214
+ if (refName && declaredInComponents.has(refName)) {
215
+ if (`${baseName}Body` !== refName) {
216
+ endpointLines.push(`export type ${baseName}Body = ${refName}`);
217
+ endpointLines.push("");
218
+ }
219
+ } else {
220
+ endpointLines.push(`export type ${baseName}Body = ${schemaToTSType(ep.bodySchema)}`);
221
+ endpointLines.push("");
222
+ }
215
223
  }
216
224
  if (ep.responseSchema) {
217
- lines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
218
- lines.push("");
225
+ const refName = getRefName(ep.responseSchema);
226
+ if (refName && declaredInComponents.has(refName)) {
227
+ if (`${baseName}Response` !== refName) {
228
+ endpointLines.push(`export type ${baseName}Response = ${refName}`);
229
+ endpointLines.push("");
230
+ }
231
+ } else {
232
+ endpointLines.push(`export type ${baseName}Response = ${schemaToTSType(ep.responseSchema)}`);
233
+ endpointLines.push("");
234
+ }
219
235
  }
220
236
  }
237
+ if (endpointLines.length > 0) {
238
+ 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");
239
+ lines.push("");
240
+ lines.push(...endpointLines);
241
+ }
221
242
  return lines.join("\n");
222
243
  }
244
+ function getRefName(schema) {
245
+ if (schema.$ref) return refToTypeName(schema.$ref);
246
+ return void 0;
247
+ }
223
248
  function operationToTypeName(operationId) {
224
249
  return operationId.charAt(0).toUpperCase() + operationId.slice(1);
225
250
  }
226
251
 
252
+ // src/generator/function-name.ts
253
+ function resolveFunctionName(operationId, method, config) {
254
+ if (!config) return operationId;
255
+ let name = operationId;
256
+ if (config.transforms && config.transforms.length > 0) {
257
+ for (const transform of config.transforms) {
258
+ name = applyTransform(name, transform);
259
+ }
260
+ }
261
+ if (shouldAppendMethod(method, config.appendMethod)) {
262
+ const suffix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
263
+ name = name + suffix;
264
+ }
265
+ return name;
266
+ }
267
+ function applyTransform(input, transform) {
268
+ const flags = transform.flags ?? "g";
269
+ const regex = new RegExp(transform.match, flags);
270
+ const { replacement } = transform;
271
+ const upperMatch = replacement.match(/^upper:\$(\d+)$/);
272
+ if (upperMatch) {
273
+ const groupIndex = parseInt(upperMatch[1], 10);
274
+ return input.replace(regex, (...args) => {
275
+ const captured = args[groupIndex];
276
+ return captured ? captured.toUpperCase() : "";
277
+ });
278
+ }
279
+ const lowerMatch = replacement.match(/^lower:\$(\d+)$/);
280
+ if (lowerMatch) {
281
+ const groupIndex = parseInt(lowerMatch[1], 10);
282
+ return input.replace(regex, (...args) => {
283
+ const captured = args[groupIndex];
284
+ return captured ? captured.toLowerCase() : "";
285
+ });
286
+ }
287
+ return input.replace(regex, replacement);
288
+ }
289
+ function shouldAppendMethod(method, appendMethod) {
290
+ if (!appendMethod) return false;
291
+ if (appendMethod === true) return true;
292
+ if (Array.isArray(appendMethod)) {
293
+ return appendMethod.map((m) => m.toLowerCase()).includes(method.toLowerCase());
294
+ }
295
+ return false;
296
+ }
297
+
227
298
  // src/generator/axios.ts
228
299
  function generateClientFile(opts) {
229
300
  const { endpoints, config, typesRelativePath } = opts;
@@ -234,7 +305,7 @@ function generateClientFile(opts) {
234
305
  lines.push(`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
235
306
  lines.push("");
236
307
  if (isTS) {
237
- lines.push(`import type { AxiosResponse } from 'axios'`);
308
+ lines.push(`import type { AxiosRequestConfig, AxiosResponse } from 'axios'`);
238
309
  }
239
310
  lines.push(`import { ${instanceExport} } from '${config.axiosInstancePath}'`);
240
311
  if (isTS && typesRelativePath) {
@@ -246,13 +317,15 @@ function generateClientFile(opts) {
246
317
  lines.push("");
247
318
  lines.push("");
248
319
  for (const ep of endpoints) {
249
- const fn = buildFunction(ep, isTS, instanceExport, config.jsdoc !== false);
320
+ const fn = buildFunction(ep, isTS, instanceExport, config);
250
321
  lines.push(fn);
251
322
  lines.push("");
252
323
  }
253
324
  return lines.join("\n");
254
325
  }
255
- function buildFunction(ep, isTS, instanceExport, jsdoc) {
326
+ function buildFunction(ep, isTS, instanceExport, config) {
327
+ const jsdoc = config.jsdoc !== false;
328
+ const fnName = resolveFunctionName(ep.operationId, ep.method, config.functionName);
256
329
  const typeName = operationToTypeName(ep.operationId);
257
330
  const lines = [];
258
331
  if (jsdoc) {
@@ -267,9 +340,8 @@ function buildFunction(ep, isTS, instanceExport, jsdoc) {
267
340
  }
268
341
  const params = buildParams(ep, typeName, isTS);
269
342
  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);
343
+ lines.push(`export async function ${fnName}(${params})${returnType} {`);
344
+ const callLines = buildAxiosCall(ep, instanceExport);
273
345
  for (const l of callLines) {
274
346
  lines.push(` ${l}`);
275
347
  }
@@ -292,31 +364,36 @@ function buildParams(ep, typeName, isTS) {
292
364
  const type = isTS ? `: ${typeName}QueryParams` : "";
293
365
  parts.push(`params${optional}${type}`);
294
366
  }
367
+ if (isTS) {
368
+ parts.push(`config?: AxiosRequestConfig`);
369
+ parts.push(`options?: AxiosRequestConfig`);
370
+ } else {
371
+ parts.push(`config`);
372
+ parts.push(`options`);
373
+ }
295
374
  return parts.join(", ");
296
375
  }
297
376
  function buildReturnType(ep, typeName) {
298
377
  const responseType = ep.responseSchema ? `${typeName}Response` : "unknown";
299
378
  return `: Promise<AxiosResponse<${responseType}>>`;
300
379
  }
301
- function buildAxiosCall(ep, instanceExport, _typeName, _isTS) {
380
+ function buildAxiosCall(ep, instanceExport) {
302
381
  const lines = [];
303
382
  const interpolatedPath = ep.pathParams.length > 0 ? "`" + ep.path.replace(/\{(\w+)\}/g, "${$1}") + "`" : `'${ep.path}'`;
304
383
  const hasBody = !!ep.bodySchema;
305
384
  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
- }
385
+ if (hasQuery) {
386
+ lines.push(`const mergedConfig: AxiosRequestConfig = { ...config, params: { ...params, ...config?.params } }`);
387
+ }
388
+ const configArg = hasQuery ? "mergedConfig" : "config";
389
+ if (hasBody) {
390
+ lines.push(
391
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, data, ...${configArg} }, options)`
392
+ );
313
393
  } 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
- }
394
+ lines.push(
395
+ `return ${instanceExport}({ method: '${ep.method.toUpperCase()}', url: ${interpolatedPath}, ...${configArg} }, options)`
396
+ );
320
397
  }
321
398
  return lines;
322
399
  }
@@ -441,9 +518,52 @@ function validateConfig(raw, filePath) {
441
518
  axiosInstanceExport: typeof cfg.axiosInstanceExport === "string" ? cfg.axiosInstanceExport : "axiosInstance",
442
519
  language: cfg.language === "js" ? "js" : "ts",
443
520
  jsdoc: cfg.jsdoc !== false,
444
- tags: Array.isArray(cfg.tags) ? cfg.tags : void 0
521
+ tags: Array.isArray(cfg.tags) ? cfg.tags : void 0,
522
+ functionName: validateFunctionNameConfig(cfg.functionName, filePath)
445
523
  };
446
524
  }
525
+ function validateFunctionNameConfig(raw, filePath) {
526
+ if (raw === void 0 || raw === null) return void 0;
527
+ if (typeof raw !== "object") {
528
+ throw new Error(`Config error: "functionName" must be an object`);
529
+ }
530
+ const fn = raw;
531
+ const result = {};
532
+ if (fn.transforms !== void 0) {
533
+ if (!Array.isArray(fn.transforms)) {
534
+ throw new Error(`Config error: "functionName.transforms" must be an array`);
535
+ }
536
+ result.transforms = fn.transforms.map((t, i) => {
537
+ if (!t || typeof t !== "object") {
538
+ throw new Error(`Config error: "functionName.transforms[${i}]" must be an object`);
539
+ }
540
+ const transform = t;
541
+ if (typeof transform.match !== "string") {
542
+ throw new Error(`Config error: "functionName.transforms[${i}].match" must be a string regex pattern`);
543
+ }
544
+ if (typeof transform.replacement !== "string") {
545
+ throw new Error(`Config error: "functionName.transforms[${i}].replacement" must be a string`);
546
+ }
547
+ try {
548
+ new RegExp(transform.match, transform.flags ?? "g");
549
+ } catch {
550
+ throw new Error(`Config error: "functionName.transforms[${i}].match" is not a valid regex: ${transform.match}`);
551
+ }
552
+ return {
553
+ match: transform.match,
554
+ flags: typeof transform.flags === "string" ? transform.flags : "g",
555
+ replacement: transform.replacement
556
+ };
557
+ });
558
+ }
559
+ if (fn.appendMethod !== void 0) {
560
+ if (typeof fn.appendMethod !== "boolean" && !Array.isArray(fn.appendMethod)) {
561
+ throw new Error(`Config error: "functionName.appendMethod" must be a boolean or an array of HTTP methods`);
562
+ }
563
+ result.appendMethod = fn.appendMethod;
564
+ }
565
+ return result;
566
+ }
447
567
  export {
448
568
  extractEndpoints,
449
569
  generate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axigen",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Generate typed Axios client functions from OpenAPI / Swagger specs",
5
5
  "keywords": [
6
6
  "openapi",
@@ -37,7 +37,11 @@
37
37
  "dev": "tsx src/cli/index.ts",
38
38
  "typecheck": "tsc --noEmit",
39
39
  "lint": "eslint src --ext .ts",
40
- "prepublishOnly": "npm run build && npm run typecheck"
40
+ "prepublishOnly": "npm run build && npm run typecheck",
41
+ "release": "standard-version",
42
+ "release:minor": "standard-version --release-as minor",
43
+ "release:major": "standard-version --release-as major",
44
+ "release:patch": "standard-version --release-as patch"
41
45
  },
42
46
  "dependencies": {
43
47
  "chalk": "^5.3.0",
@@ -48,6 +52,7 @@
48
52
  "devDependencies": {
49
53
  "@types/js-yaml": "^4.0.9",
50
54
  "@types/node": "^20.19.43",
55
+ "standard-version": "^9.5.0",
51
56
  "tsup": "^8.0.2",
52
57
  "tsx": "^4.7.0",
53
58
  "typescript": "^5.3.3"