swgto-ts 0.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,86 +1,147 @@
1
- # swgto
2
-
3
- `swgto` 是一个把 `OpenAPI 3.x` 文档转换成项目内请求函数的 Node.js 库和 CLI。
4
-
5
- 安装后会注册一个 `swgto` 命令。执行时它会在当前目录查找 `.swaggerts.config.ts` 或 `.swaggerts.config.js`,然后生成:
6
-
7
- - 按路径前缀分组的请求文件
8
- - 按文档模块输出请求文件。单文档默认输出到 `services/`,多文档时多个模块目录平级
9
- - `outputDir/index.ts` 或 `outputDir/index.js`
10
- - 聚合声明文件 `outputDir/api.d.ts`
11
- - 类型文件 `outputDir/types.ts` 或带 JSDoc 的 `outputDir/types.js`
12
-
13
- ## 安装
14
-
15
- ```bash
16
- npm install swgto
17
- ```
18
-
19
- ## 配置
20
-
21
- 在项目根目录创建 `.swaggerts.config.ts`:
22
-
23
- ```ts
24
- import type { SwaggerTsConfig } from 'swgto';
25
-
26
- const config: SwaggerTsConfig = {
27
- docUrls: 'https://example.com/openapi.json',
28
- httpClientPath: '@/utils/request',
29
- outputDir: 'src/api',
30
- outputType: 'ts',
31
- typeName: 'types',
32
- cleanOutput: true,
33
- renameMethod: (apiPath, method) => `${method}_${apiPath.replace(/[\\/{}]/g, '_')}`,
34
- resolveRequestPath: (apiPath, method) => `/proxy${apiPath}`,
35
- };
36
-
37
- export default config;
38
- ```
39
-
40
- 多文档模式下需要提供 `moduleName`:
41
-
42
- ```ts
43
- export default {
44
- docUrls: [
45
- 'https://example.com/user-openapi.json',
46
- 'https://example.com/order-openapi.json',
47
- ],
48
- httpClientPath: '@/utils/request',
49
- outputDir: 'src/api',
50
- moduleName: (docUrl) => docUrl.includes('user') ? 'user' : 'order',
51
- };
52
- ```
53
-
54
- ## 使用
55
-
56
- ```bash
57
- swgto
58
- ```
59
-
60
- ## 输出示例
61
-
62
- ```text
63
- src/api/
64
- api.d.ts
65
- index.ts
66
- types.ts
67
- services/
68
- get_user_list.ts
69
- post_user_create.ts
70
- ```
71
-
72
- ## 配置项
73
-
74
- ```ts
75
- export interface SwaggerTsConfig {
76
- docUrls: string | string[];
77
- httpClientPath: string;
78
- renameMethod?: (path: string, method: string) => string;
79
- resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
80
- outputDir?: string;
81
- moduleName?: (docUrl: string) => string;
82
- outputType?: 'ts' | 'js';
83
- typeName?: string;
84
- cleanOutput?: boolean;
85
- }
86
- ```
1
+ # swgto
2
+
3
+ `swgto` 是一个把 `OpenAPI 3.x` 文档转换成项目内请求函数的 Node.js 库和 CLI。
4
+
5
+ 安装后会注册一个 `swgto` 命令。执行时它会在当前目录查找 `.swaggerts.config.ts` 或 `.swaggerts.config.js`,然后生成:
6
+
7
+ - 按路径前缀分组的请求文件
8
+ - 按文档模块输出请求文件。单文档默认输出到 `services/`,多文档时多个模块目录平级
9
+ - `outputDir/index.ts` 或 `outputDir/index.js`
10
+ - 类型文件 `outputDir/types.ts` 或带 JSDoc 的 `outputDir/types.js`
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install swgto-ts
16
+ ```
17
+
18
+ ## 配置
19
+
20
+ 在项目根目录创建 `.swaggerts.config.ts`:
21
+
22
+ ```ts
23
+ import type { SwaggerTsConfig } from 'swgto-ts'
24
+
25
+ const config: SwaggerTsConfig = {
26
+ docUrls: 'https://example.com/openapi.json',
27
+ httpClientPath: '@/utils/request',
28
+ outputDir: 'src/api',
29
+ outputType: 'ts',
30
+ typeName: 'types',
31
+ cleanOutput: true,
32
+ renameMethod: (apiPath, method) => `${method}_${apiPath.replace(/[\\/{}]/g, '_')}`,
33
+ resolveRequestPath: (apiPath, method) => `/proxy${apiPath}`,
34
+ }
35
+
36
+ export default config
37
+ ```
38
+
39
+ 多文档模式下需要提供 `moduleName`:
40
+
41
+ ```ts
42
+ export default {
43
+ docUrls: ['https://example.com/user-openapi.json', 'https://example.com/order-openapi.json'],
44
+ httpClientPath: '@/utils/request',
45
+ outputDir: 'src/api',
46
+ moduleName: (docUrl) => (docUrl.includes('user') ? 'user' : 'order'),
47
+ }
48
+ ```
49
+
50
+ ## 使用
51
+
52
+ ```bash
53
+ swgto
54
+ ```
55
+
56
+ ## 输出示例
57
+
58
+ ```text
59
+ src/api/
60
+ index.ts
61
+ types.ts
62
+ services/
63
+ get_user_list.ts
64
+ post_user_create.ts
65
+ ```
66
+
67
+ ## 配置项
68
+
69
+ ```ts
70
+ export interface SwaggerTsConfig {
71
+ docUrls: string | string[]
72
+ httpClientPath: string
73
+ renameMethod?: (path: string, method: string) => string
74
+ resolveRequestPath?: (path: string, method: string, docUrl: string) => string
75
+ ignoreUrl?: (path: string, method: string, docUrl: string) => boolean
76
+ outputDir?: string
77
+ moduleName?: (docUrl: string) => string
78
+ outputType?: 'ts' | 'js'
79
+ typeName?: string
80
+ cleanOutput?: boolean
81
+ fileNaming?: 'module' | 'path'
82
+ flattenQueryParam?: boolean
83
+ mergeParams?: boolean
84
+ }
85
+ ```
86
+
87
+ ### 字段说明
88
+
89
+ | 字段 | 类型 | 默认值 | 说明 |
90
+ | -------------------- | ----------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `docUrls` | `string \| string[]` | (必填) | OpenAPI 3.x 文档的 URL,支持单文档字符串或多文档数组 |
92
+ | `httpClientPath` | `string` | (必填) | 项目内 HTTP 请求工具的引入路径,生成的文件会从中 `import request` |
93
+ | `outputDir` | `string` | `'src/api'` | 代码输出目录,相对项目根目录 |
94
+ | `outputType` | `'ts' \| 'js'` | `'ts'` | 输出文件类型,`'ts'` 生成 `.ts` 文件,`'js'` 生成 `.js` 文件(带 JSDoc 类型注释) |
95
+ | `typeName` | `string` | `'types'` | 类型文件名(不含后缀),如 `'types'` 生成 `types.ts` 或 `types.js` |
96
+ | `fileNaming` | `'module' \| 'path'` | `'path'` | 文件组织方式:`'path'` 按接口路径每个文件一个函数;`'module'` 按控制器合并到同一个文件 |
97
+ | `moduleName` | `(docUrl) => string` | `'services'` | 多文档时,为每个文档指定模块目录名。多文档模式下**必填** |
98
+ | `renameMethod` | `(path, method) => string` | 见说明 | 自定义函数名生成规则。默认根据路径和方法名自动生成(如 `get_user_list`) |
99
+ | `resolveRequestPath` | `(path, method, docUrl) => string` | 原路径 | 自定义请求路径转换规则,可用于添加统一前缀等,如 `(path) => \`/proxy\${path}\`` |
100
+ | `ignoreUrl` | `(path, method, docUrl) => boolean` | 不过滤 | 过滤不需要生成代码的接口,返回 `true` 跳过当前接口 |
101
+ | `cleanOutput` | `boolean` | `false` | 生成前是否清空输出目录 |
102
+ | `flattenQueryParam` | `boolean` | `false` | 当 query 参数只有一个且为 `$ref` 引用类型时,直接用引用类型代替 `{ query: RefType }` |
103
+ | `mergeParams` | `boolean` | `false` | 合并参数层级:`true` 时展平 `path/query/body` 嵌套,直接使用 `params: { field1, field2 }` 代替 `params: { path: {...}, query: {...} }` |
104
+
105
+ ### 函数名生成规则
106
+
107
+ 默认函数名格式为 `{method}_{path_segments}`,如:
108
+
109
+ - `GET /user/list` → `get_user_list`
110
+ - `POST /user/create` → `post_user_create`
111
+
112
+ 可通过 `renameMethod` 自定义:
113
+
114
+ ```ts
115
+ renameMethod: (path, method) => `${method}_${path.replace(/[\\/{}]/g, '_')}`,
116
+ ```
117
+
118
+ ### 文件组织方式
119
+
120
+ **`fileNaming: 'path'`(默认)**— 每个接口生成独立文件:
121
+
122
+ ```
123
+ src/api/moduleA/
124
+ get_user_list.ts
125
+ post_user_create.ts
126
+ get_user_detail.ts
127
+ ```
128
+
129
+ **`fileNaming: 'module'`**— 按控制器合并文件(自动识别 RESTful 资源路径分组):
130
+
131
+ ```
132
+ src/api/moduleA/
133
+ user.ts // 包含 get_user_list, post_user_create, get_user_detail
134
+ order.ts // 包含 get_order_list, post_order_create
135
+ ```
136
+
137
+ ### 参数合并模式
138
+
139
+ `mergeParams: true` 时展平参数层级,适用于不想嵌套 `path/query/body` 的场景:
140
+
141
+ ```ts
142
+ // mergeParams: false(默认),参数按来源嵌套
143
+ getUser({ path: { id: '1' }, query: { name: 'foo' } })
144
+
145
+ // mergeParams: true,参数展平
146
+ getUser({ id: '1', name: 'foo' })
147
+ ```
@@ -39,7 +39,10 @@ async function loadConfig(cwd) {
39
39
  outputDir: rawConfig.outputDir ?? "src/api",
40
40
  outputType: rawConfig.outputType ?? "ts",
41
41
  typeName: rawConfig.typeName ?? "types",
42
- cleanOutput: rawConfig.cleanOutput ?? false
42
+ cleanOutput: rawConfig.cleanOutput ?? false,
43
+ fileNaming: rawConfig.fileNaming ?? "path",
44
+ flattenQueryParam: rawConfig.flattenQueryParam ?? false,
45
+ mergeParams: rawConfig.mergeParams ?? false
43
46
  };
44
47
  return { configPath, config };
45
48
  }
@@ -53,6 +56,152 @@ function groupByPrefix(operations) {
53
56
  }, {});
54
57
  }
55
58
 
59
+ // src/core/groupByController.ts
60
+ function isPathParam(segment) {
61
+ return segment.startsWith("{") && segment.endsWith("}");
62
+ }
63
+ function resultName(segments, childIndex) {
64
+ if (childIndex >= 2) {
65
+ return segments[childIndex - 1] + "_" + segments[childIndex];
66
+ }
67
+ return segments[childIndex];
68
+ }
69
+ function getPathMethods(operations) {
70
+ const map = /* @__PURE__ */ new Map();
71
+ for (const op of operations) {
72
+ if (!map.has(op.path)) map.set(op.path, /* @__PURE__ */ new Set());
73
+ map.get(op.path).add(op.method);
74
+ }
75
+ return map;
76
+ }
77
+ function deriveControllerName(path4, allPaths, pathMethods) {
78
+ const segments = path4.split("/").filter(Boolean);
79
+ if (segments.length === 0) return "index";
80
+ if (segments.length === 1) return segments[0];
81
+ let effectiveLen = segments.length;
82
+ while (effectiveLen > 1 && isPathParam(segments[effectiveLen - 1])) {
83
+ effectiveLen--;
84
+ }
85
+ if (effectiveLen === 1) return segments[0];
86
+ for (let depth = effectiveLen - 1; depth >= 0; depth--) {
87
+ const ancestorPath = "/" + segments.slice(0, depth).join("/");
88
+ const ancestorMethods = pathMethods.get(ancestorPath);
89
+ if (ancestorMethods && ancestorMethods.size > 1 && depth >= 1) {
90
+ return segments[depth - 1];
91
+ }
92
+ if (depth >= 1) {
93
+ if (allPaths.has(ancestorPath)) {
94
+ const ancestorController = deriveControllerName(ancestorPath, allPaths, pathMethods);
95
+ if (ancestorController !== segments[depth - 1]) {
96
+ return ancestorController;
97
+ }
98
+ return segments[depth - 1];
99
+ }
100
+ const hasDirectSibling = [...allPaths].some((p) => {
101
+ if (p === path4) return false;
102
+ if (!p.startsWith(ancestorPath + "/")) return false;
103
+ const remaining = p.slice(ancestorPath.length + 1);
104
+ return !remaining.includes("/");
105
+ });
106
+ if (hasDirectSibling) {
107
+ const siblingIsRest = [...allPaths].some((p) => {
108
+ if (p === path4) return false;
109
+ if (!p.startsWith(ancestorPath + "/")) return false;
110
+ const remaining = p.slice(ancestorPath.length + 1);
111
+ if (remaining.includes("/")) return false;
112
+ const methods = pathMethods.get(p);
113
+ return methods != null && methods.size > 1;
114
+ });
115
+ if (siblingIsRest) {
116
+ return resultName(segments, depth);
117
+ }
118
+ return segments[depth - 1];
119
+ }
120
+ }
121
+ const currentPath = "/" + segments.slice(0, depth + 1).join("/");
122
+ const hasChildren = [...allPaths].some(
123
+ (p) => p !== path4 && p.startsWith(currentPath + "/")
124
+ );
125
+ if (hasChildren) {
126
+ return resultName(segments, depth);
127
+ }
128
+ }
129
+ for (let i = segments.length - 1; i >= 0; i--) {
130
+ if (!isPathParam(segments[i])) return resultName(segments, i);
131
+ }
132
+ return segments[segments.length - 1];
133
+ }
134
+ function groupByController(operations) {
135
+ const allPaths = new Set(operations.map((op) => op.path));
136
+ const pathMethods = getPathMethods(operations);
137
+ return operations.reduce((acc, operation) => {
138
+ const controllerName = deriveControllerName(operation.path, allPaths, pathMethods);
139
+ acc[controllerName] ??= [];
140
+ acc[controllerName].push(operation);
141
+ return acc;
142
+ }, {});
143
+ }
144
+
145
+ // src/generators/schemaToTs.ts
146
+ function formatPropertyName(name) {
147
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
148
+ }
149
+ function refToTypeName(ref) {
150
+ const parts = ref.split("/");
151
+ return parts[parts.length - 1] || "unknown";
152
+ }
153
+ function schemaToTs(schema) {
154
+ if (!schema) {
155
+ return "unknown";
156
+ }
157
+ if (schema.$ref) {
158
+ return refToTypeName(schema.$ref);
159
+ }
160
+ if (schema.enum?.length) {
161
+ return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
162
+ }
163
+ if (schema.anyOf?.length) {
164
+ return schema.anyOf.map((item) => schemaToTs(item)).join(" | ");
165
+ }
166
+ if (schema.oneOf?.length) {
167
+ return schema.oneOf.map((item) => schemaToTs(item)).join(" | ");
168
+ }
169
+ if (schema.allOf?.length) {
170
+ return schema.allOf.map((item) => schemaToTs(item)).join(" & ");
171
+ }
172
+ if (schema.type === "array") {
173
+ return `${schemaToTs(schema.items)}[]`;
174
+ }
175
+ if (schema.type === "object" || schema.properties) {
176
+ const requiredSet = new Set(schema.required ?? []);
177
+ const properties = Object.entries(schema.properties ?? {}).map(([key, value]) => {
178
+ const optional = requiredSet.has(key) ? "" : "?";
179
+ return `${formatPropertyName(key)}${optional}: ${schemaToTs(value)};`;
180
+ });
181
+ if (!properties.length && schema.additionalProperties) {
182
+ const valueType = schema.additionalProperties === true ? "unknown" : schemaToTs(schema.additionalProperties);
183
+ return `{ [key: string]: ${valueType} }`;
184
+ }
185
+ return `{ ${properties.join(" ")} }`;
186
+ }
187
+ switch (schema.type) {
188
+ case "integer":
189
+ case "number":
190
+ return "number";
191
+ case "boolean":
192
+ return "boolean";
193
+ case "string":
194
+ return "string";
195
+ case "null":
196
+ return "null";
197
+ default:
198
+ return "unknown";
199
+ }
200
+ }
201
+ function toTypePropertyName(name) {
202
+ return formatPropertyName(name);
203
+ }
204
+
56
205
  // src/utils/naming.ts
57
206
  function toPascalCase(value) {
58
207
  return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join("");
@@ -77,6 +226,36 @@ function buildTypeName(functionName, suffix) {
77
226
 
78
227
  // src/core/parsePaths.ts
79
228
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
229
+ function collectReferencedTypeNames(schema, collected = /* @__PURE__ */ new Set()) {
230
+ if (!schema) {
231
+ return [...collected];
232
+ }
233
+ if (schema.$ref) {
234
+ const typeName = schema.$ref.split("/").pop();
235
+ if (typeName) {
236
+ collected.add(typeName);
237
+ }
238
+ }
239
+ for (const child of schema.anyOf ?? []) {
240
+ collectReferencedTypeNames(child, collected);
241
+ }
242
+ for (const child of schema.oneOf ?? []) {
243
+ collectReferencedTypeNames(child, collected);
244
+ }
245
+ for (const child of schema.allOf ?? []) {
246
+ collectReferencedTypeNames(child, collected);
247
+ }
248
+ if (schema.items) {
249
+ collectReferencedTypeNames(schema.items, collected);
250
+ }
251
+ for (const property of Object.values(schema.properties ?? {})) {
252
+ collectReferencedTypeNames(property, collected);
253
+ }
254
+ if (schema.additionalProperties && schema.additionalProperties !== true) {
255
+ collectReferencedTypeNames(schema.additionalProperties, collected);
256
+ }
257
+ return [...collected];
258
+ }
80
259
  function getPrimarySchema(content) {
81
260
  if (!content) {
82
261
  return void 0;
@@ -99,6 +278,9 @@ function parsePaths(document, docUrl, config) {
99
278
  continue;
100
279
  }
101
280
  const moduleName = config.moduleName?.(docUrl) ?? "services";
281
+ if (config.ignoreUrl?.(apiPath, method, docUrl)) {
282
+ continue;
283
+ }
102
284
  const functionName = sanitizeIdentifier(
103
285
  config.renameMethod?.(apiPath, method) ?? buildDefaultMethodName(apiPath, method)
104
286
  );
@@ -108,7 +290,43 @@ function parsePaths(document, docUrl, config) {
108
290
  const requestBodySchema = getPrimarySchema(operation.requestBody?.content);
109
291
  const responseSchema = getSuccessResponse(operation.responses);
110
292
  const hasRequestParams = queryParams.length > 0 || pathParams.length > 0 || Boolean(requestBodySchema);
111
- const requestTypeName = hasRequestParams ? buildTypeName(functionName, "Request") : void 0;
293
+ const bodyType = requestBodySchema ? schemaToTs(requestBodySchema) : void 0;
294
+ const requestImportTypes = collectReferencedTypeNames(requestBodySchema);
295
+ for (const param of queryParams) {
296
+ requestImportTypes.push(...collectReferencedTypeNames(param.schema));
297
+ }
298
+ for (const param of pathParams) {
299
+ requestImportTypes.push(...collectReferencedTypeNames(param.schema));
300
+ }
301
+ const pathFields = pathParams.map((param) => `${param.name}: ${schemaToTs(param.schema)};`);
302
+ const queryFields = queryParams.map((param) => `${param.name}${param.required ? "" : "?"}: ${schemaToTs(param.schema)};`);
303
+ const requestTypeParts = [];
304
+ if (bodyType) {
305
+ requestTypeParts.push(bodyType);
306
+ }
307
+ if (pathFields.length) {
308
+ if (config.mergeParams) {
309
+ requestTypeParts.push(`{ ${pathFields.join(" ")} }`);
310
+ } else {
311
+ requestTypeParts.push(`{ path: { ${pathFields.join(" ")} } }`);
312
+ }
313
+ }
314
+ if (queryFields.length) {
315
+ if (config.flattenQueryParam && queryParams.length === 1 && queryParams[0].schema?.$ref) {
316
+ if (config.mergeParams) {
317
+ requestTypeParts.push(schemaToTs(queryParams[0].schema));
318
+ } else {
319
+ requestTypeParts.push(`{ query: ${schemaToTs(queryParams[0].schema)} }`);
320
+ }
321
+ } else {
322
+ if (config.mergeParams) {
323
+ requestTypeParts.push(`{ ${queryFields.join(" ")} }`);
324
+ } else {
325
+ requestTypeParts.push(`{ query: { ${queryFields.join(" ")} } }`);
326
+ }
327
+ }
328
+ }
329
+ const requestTypeExpression = hasRequestParams ? requestTypeParts.join(" & ") || "unknown" : void 0;
112
330
  operations.push({
113
331
  docUrl,
114
332
  moduleName,
@@ -123,7 +341,8 @@ function parsePaths(document, docUrl, config) {
123
341
  pathParams,
124
342
  requestBodySchema,
125
343
  responseSchema,
126
- requestTypeName,
344
+ requestTypeExpression,
345
+ requestImportTypes,
127
346
  responseTypeName: responseSchema ? buildTypeName(functionName, "Response") : void 0,
128
347
  fileBaseName: functionName
129
348
  });
@@ -149,13 +368,14 @@ async function loadOpenApiDocument(docUrl) {
149
368
  }
150
369
 
151
370
  // src/generators/genIndex.ts
152
- function generateIndexFile(operations, config) {
153
- const exports = operations.map((operation) => {
154
- return `export * from "./${operation.moduleName}/${operation.fileBaseName}";`;
155
- });
371
+ function generateIndexFile(operations, _config) {
372
+ const exportSet = /* @__PURE__ */ new Set();
373
+ for (const operation of operations) {
374
+ exportSet.add(`export * from "./${operation.moduleName}/${operation.fileBaseName}";`);
375
+ }
156
376
  return `/* eslint-disable */
157
377
  // Auto-generated by swgto.
158
- ${exports.join("\n")}
378
+ ${[...exportSet].join("\n")}
159
379
  `;
160
380
  }
161
381
 
@@ -171,17 +391,43 @@ function buildFunctionDoc(operation, typeImportPath, requestParamType) {
171
391
  lines.push(`@path ${operation.path}`);
172
392
  lines.push(`@requestPath ${operation.requestPath}`);
173
393
  lines.push(`@method ${operation.method.toUpperCase()}`);
174
- lines.push("@template T");
175
394
  lines.push(`@param ${requestParamType === "void" ? "[params]" : `{import(${JSON.stringify(typeImportPath)}).${requestParamType}} params`}`);
176
395
  lines.push("@param [config]");
177
- lines.push("@returns {Promise<T>}");
396
+ if (operation.responseTypeName) {
397
+ lines.push(`@returns {Promise<import(${JSON.stringify(typeImportPath)}).${operation.responseTypeName}>}`);
398
+ } else {
399
+ lines.push("@template T");
400
+ lines.push("@returns {Promise<T>}");
401
+ }
178
402
  return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
179
403
  }
180
- function generateJsRequestFile(operation, httpClientPath, typeImportPath) {
181
- const requestParamType = operation.requestTypeName ?? "void";
182
- const queryLine = operation.queryParams.length ? " params: params?.query,\n" : "";
183
- const bodyLine = operation.requestBodySchema ? " data: params?.body,\n" : "";
184
- const pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
404
+ function renderFunction(operation, useGenericUrl, mergeParams) {
405
+ const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
406
+ ` : "";
407
+ const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
408
+ const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
409
+ ` : "";
410
+ const pathArg = mergeParams ? "params" : "params?.path";
411
+ const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
412
+ const pathLine = ` url: ${urlValue},
413
+ `;
414
+ return `export async function ${operation.functionName}(params, config) {
415
+ return request({
416
+ ${pathLine} method: ${JSON.stringify(operation.method)},
417
+ ${queryLine}${bodyLine} ...config,
418
+ });
419
+ }`;
420
+ }
421
+ function generateJsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
422
+ const requestParamType = operation.requestTypeExpression ?? "void";
423
+ const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
424
+ ` : "";
425
+ const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
426
+ const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
427
+ ` : "";
428
+ const pathArg = mergeParams ? "params" : "params?.path";
429
+ const pathLine = operation.pathParams.length ? ` url: buildUrl(${pathArg}),
430
+ ` : ` url: ${JSON.stringify(operation.requestPath)},
185
431
  `;
186
432
  const buildUrlHelper = operation.pathParams.length ? `
187
433
  function buildUrl(path) {
@@ -202,6 +448,26 @@ ${queryLine}${bodyLine} ...config,
202
448
  }
203
449
  `;
204
450
  }
451
+ function generateJsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
452
+ const needsBuildUrl = operations.some((op) => op.pathParams.length > 0);
453
+ const buildUrlHelper = needsBuildUrl ? `
454
+ function buildUrl(url, path) {
455
+ return url.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
456
+ }
457
+ ` : "";
458
+ const functions = operations.map((op) => {
459
+ const doc = buildFunctionDoc(op, typeImportPath, op.requestTypeExpression ?? "void");
460
+ return `${doc}
461
+
462
+ ${renderFunction(op, true, mergeParams)}`;
463
+ }).join("\n\n");
464
+ return `/* eslint-disable */
465
+ // Auto-generated by swgto.
466
+ import request from ${JSON.stringify(httpClientPath)};
467
+ ${buildUrlHelper}
468
+ ${functions}
469
+ `;
470
+ }
205
471
 
206
472
  // src/generators/genRequestTs.ts
207
473
  function buildFunctionDoc2(operation) {
@@ -217,20 +483,33 @@ function buildFunctionDoc2(operation) {
217
483
  }
218
484
  return ["/**", ...lines.map((line) => ` * ${line}`), " */", ""].join("\n");
219
485
  }
220
- function generateTsRequestFile(operation, httpClientPath, typeImportPath) {
486
+ function renderFunction2(operation, useGenericUrl, mergeParams) {
487
+ const requestArg = operation.requestTypeExpression ? `params: ${operation.requestTypeExpression}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
488
+ const defaultResponseType = operation.responseTypeName ?? "unknown";
489
+ const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
490
+ const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
491
+ ` : "";
492
+ const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
493
+ ` : "";
494
+ const pathArg = mergeParams ? "params" : "params?.path";
495
+ const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
496
+ const pathLine = ` url: ${urlValue},
497
+ `;
498
+ return `${buildFunctionDoc2(operation)}export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
499
+ return request<T>({
500
+ ${pathLine} method: ${JSON.stringify(operation.method)},
501
+ ${queryLine}${bodyLine} ...config,
502
+ });
503
+ }`;
504
+ }
505
+ function generateTsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
221
506
  const importTypes = [
222
- operation.requestTypeName,
507
+ ...operation.requestImportTypes,
223
508
  operation.responseTypeName,
224
509
  "RequestConfig"
225
510
  ].filter((value, index, array) => Boolean(value) && array.indexOf(value) === index);
226
511
  const importLine = importTypes.length ? `import type { ${importTypes.join(", ")} } from ${JSON.stringify(typeImportPath)};
227
512
  ` : "";
228
- const requestArg = operation.requestTypeName ? `params: ${operation.requestTypeName}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
229
- const defaultResponseType = operation.responseTypeName ?? "unknown";
230
- const bodyLine = operation.requestBodySchema ? " data: params?.body,\n" : "";
231
- const queryLine = operation.queryParams.length ? " params: params?.query,\n" : "";
232
- const pathLine = operation.pathParams.length ? " url: buildUrl(params?.path),\n" : ` url: ${JSON.stringify(operation.requestPath)},
233
- `;
234
513
  const buildUrlHelper = operation.pathParams.length ? `
235
514
  function buildUrl(path?: Record<string, unknown>): string {
236
515
  return ${JSON.stringify(operation.requestPath)}.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
@@ -241,74 +520,31 @@ function buildUrl(path?: Record<string, unknown>): string {
241
520
  import request from ${JSON.stringify(httpClientPath)};
242
521
  ${importLine}
243
522
  ${buildUrlHelper}
244
- ${buildFunctionDoc2(operation)}
245
- export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
246
- return request<T>({
247
- ${pathLine} method: ${JSON.stringify(operation.method)},
248
- ${queryLine}${bodyLine} ...config,
249
- });
250
- }
523
+ ${renderFunction2(operation, false, mergeParams)}
251
524
  `;
252
525
  }
253
-
254
- // src/generators/schemaToTs.ts
255
- function formatPropertyName(name) {
256
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
257
- }
258
- function refToTypeName(ref) {
259
- const parts = ref.split("/");
260
- return parts[parts.length - 1] || "unknown";
261
- }
262
- function schemaToTs(schema) {
263
- if (!schema) {
264
- return "unknown";
265
- }
266
- if (schema.$ref) {
267
- return refToTypeName(schema.$ref);
268
- }
269
- if (schema.enum?.length) {
270
- return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
271
- }
272
- if (schema.anyOf?.length) {
273
- return schema.anyOf.map((item) => schemaToTs(item)).join(" | ");
274
- }
275
- if (schema.oneOf?.length) {
276
- return schema.oneOf.map((item) => schemaToTs(item)).join(" | ");
277
- }
278
- if (schema.allOf?.length) {
279
- return schema.allOf.map((item) => schemaToTs(item)).join(" & ");
280
- }
281
- if (schema.type === "array") {
282
- return `${schemaToTs(schema.items)}[]`;
283
- }
284
- if (schema.type === "object" || schema.properties) {
285
- const requiredSet = new Set(schema.required ?? []);
286
- const properties = Object.entries(schema.properties ?? {}).map(([key, value]) => {
287
- const optional = requiredSet.has(key) ? "" : "?";
288
- return `${formatPropertyName(key)}${optional}: ${schemaToTs(value)};`;
289
- });
290
- if (!properties.length && schema.additionalProperties) {
291
- const valueType = schema.additionalProperties === true ? "unknown" : schemaToTs(schema.additionalProperties);
292
- return `{ [key: string]: ${valueType} }`;
293
- }
294
- return `{ ${properties.join(" ")} }`;
295
- }
296
- switch (schema.type) {
297
- case "integer":
298
- case "number":
299
- return "number";
300
- case "boolean":
301
- return "boolean";
302
- case "string":
303
- return "string";
304
- case "null":
305
- return "null";
306
- default:
307
- return "unknown";
526
+ function generateTsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
527
+ const importTypeSet = /* @__PURE__ */ new Set();
528
+ for (const op of operations) {
529
+ for (const t of op.requestImportTypes) importTypeSet.add(t);
530
+ if (op.responseTypeName) importTypeSet.add(op.responseTypeName);
308
531
  }
532
+ importTypeSet.add("RequestConfig");
533
+ const importLine = `import type { ${[...importTypeSet].join(", ")} } from ${JSON.stringify(typeImportPath)};
534
+ `;
535
+ const needsBuildUrl = operations.some((op) => op.pathParams.length > 0);
536
+ const buildUrlHelper = needsBuildUrl ? `
537
+ function buildUrl(url: string, path?: Record<string, unknown>): string {
538
+ return url.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
309
539
  }
310
- function toTypePropertyName(name) {
311
- return formatPropertyName(name);
540
+ ` : "";
541
+ const functions = operations.map((op) => renderFunction2(op, true, mergeParams)).join("\n\n");
542
+ return `/* eslint-disable */
543
+ // Auto-generated by swgto.
544
+ import request from ${JSON.stringify(httpClientPath)};
545
+ ${importLine}${buildUrlHelper}
546
+ ${functions}
547
+ `;
312
548
  }
313
549
 
314
550
  // src/generators/genTypes.ts
@@ -328,21 +564,17 @@ function buildSchemaDoc(schema) {
328
564
  }
329
565
  return formatDocLines(lines);
330
566
  }
331
- function buildParameterDoc(parameter, fallback) {
332
- const lines = [parameter.description ?? fallback];
333
- if (parameter.required) {
334
- lines.push("@required");
335
- }
336
- if (parameter.example !== void 0) {
337
- lines.push(`@example ${JSON.stringify(parameter.example)}`);
338
- }
339
- return formatDocLines(lines);
567
+ function jsdocToInline(doc, indent) {
568
+ return doc.split("\n").filter((line) => {
569
+ const t = line.trim();
570
+ return t !== "/**" && t !== "*/" && !t.startsWith("/**");
571
+ }).map((line) => `${indent}// ${line.replace(/^\s*\* ?/, "")}`);
340
572
  }
341
573
  function renderObjectFields(fields, indent = "") {
342
574
  const lines = [];
343
575
  for (const field of fields) {
344
576
  if (field.doc) {
345
- lines.push(...field.doc.split("\n").map((line) => `${indent}${line}`));
577
+ lines.push(...jsdocToInline(field.doc, indent));
346
578
  }
347
579
  lines.push(`${indent}${field.name}${field.optional ? "?" : ""}: ${field.type};`);
348
580
  }
@@ -370,56 +602,6 @@ ${body}
370
602
  }
371
603
  function renderOperationTypes(operation) {
372
604
  const blocks = [];
373
- if (operation.requestTypeName) {
374
- const fields = [];
375
- const pathFields = [];
376
- const queryFields = [];
377
- for (const param of operation.pathParams) {
378
- pathFields.push({
379
- name: toTypePropertyName(param.name),
380
- type: schemaToTs(param.schema),
381
- optional: false,
382
- doc: buildParameterDoc(param, `Path parameter: ${param.name}`)
383
- });
384
- }
385
- for (const param of operation.queryParams) {
386
- queryFields.push({
387
- name: toTypePropertyName(param.name),
388
- type: schemaToTs(param.schema),
389
- optional: !param.required,
390
- doc: buildParameterDoc(param, `Query parameter: ${param.name}`)
391
- });
392
- }
393
- if (operation.requestBodySchema) {
394
- const doc = buildSchemaDoc(operation.requestBodySchema);
395
- if (doc) {
396
- fields.push(...doc.split("\n"));
397
- }
398
- fields.push(`body${operation.requestBodySchema.nullable ? "?" : ""}: ${schemaToTs(operation.requestBodySchema)};`);
399
- }
400
- if (pathFields.length) {
401
- fields.push("/** Path parameters */");
402
- fields.push("path: {");
403
- fields.push(...renderObjectFields(pathFields, " "));
404
- fields.push("};");
405
- }
406
- if (queryFields.length) {
407
- fields.push("/** Query parameters */");
408
- fields.push("query: {");
409
- fields.push(...renderObjectFields(queryFields, " "));
410
- fields.push("};");
411
- }
412
- const requestDoc = formatDocLines(
413
- [
414
- operation.summary,
415
- operation.description
416
- ].filter((value) => Boolean(value))
417
- );
418
- blocks.push(`${requestDoc ? `${requestDoc}
419
- ` : ""}export interface ${operation.requestTypeName} {
420
- ${fields.map((line) => ` ${line}`).join("\n")}
421
- }`);
422
- }
423
605
  if (operation.responseTypeName) {
424
606
  const responseDoc = buildSchemaDoc(operation.responseSchema);
425
607
  blocks.push(`${responseDoc ? `${responseDoc}
@@ -433,7 +615,7 @@ function renderComponentSchemas(document) {
433
615
  });
434
616
  }
435
617
  function toJSDocType(typeText) {
436
- return typeText.replace(/;/g, "").replace(/\?/g, "=");
618
+ return typeText.replace(/;\s*/g, ", ").replace(/,\s*\}/g, " }");
437
619
  }
438
620
  function generateTypesFile(documentMap, operations, config) {
439
621
  if (config.outputType === "js") {
@@ -450,13 +632,6 @@ function generateTypesFile(documentMap, operations, config) {
450
632
  }
451
633
  }
452
634
  for (const operation of operations) {
453
- if (operation.requestTypeName) {
454
- const rendered = renderOperationTypes(operation).find((line) => line.startsWith(`export interface ${operation.requestTypeName}`));
455
- if (rendered) {
456
- const body = rendered.replace(`export interface ${operation.requestTypeName} `, "").trim();
457
- parts2.push(`/** @typedef ${body} ${operation.requestTypeName} */`);
458
- }
459
- }
460
635
  if (operation.responseTypeName) {
461
636
  parts2.push(`/** @typedef {${toJSDocType(schemaToTs(operation.responseSchema))}} ${operation.responseTypeName} */`);
462
637
  }
@@ -481,16 +656,6 @@ function generateTypesFile(documentMap, operations, config) {
481
656
  return `${parts.filter(Boolean).join("\n\n")}
482
657
  `;
483
658
  }
484
- function generateApiDtsContent(operations, config) {
485
- const lines = ["// Auto-generated by swgto.", 'export * from "./index";', `export * from "./${config.typeName}";`];
486
- for (const operation of operations) {
487
- const paramsType = operation.requestTypeName ?? "void";
488
- const responseType = operation.responseTypeName ?? "unknown";
489
- lines.push(`export declare function ${operation.functionName}<T = ${responseType}>(params${paramsType === "void" ? "?" : ""}: ${paramsType}, config?: import("./index").RequestConfig): Promise<T>;`);
490
- }
491
- return `${lines.join("\n")}
492
- `;
493
- }
494
659
 
495
660
  // src/utils/fs.ts
496
661
  import { mkdir, rm, writeFile } from "fs/promises";
@@ -503,12 +668,17 @@ async function writeTextFile(filePath, content) {
503
668
  await writeFile(filePath, content, "utf8");
504
669
  }
505
670
  async function removeDir(dirPath) {
506
- await rm(dirPath, { recursive: true, force: true });
671
+ try {
672
+ await rm(dirPath, { recursive: true, force: true });
673
+ } catch {
674
+ await new Promise((resolve) => setTimeout(resolve, 100));
675
+ await rm(dirPath, { recursive: true, force: true });
676
+ }
507
677
  }
508
678
 
509
679
  // src/generate.ts
510
- function getRootImportPath(fileKind, typeName) {
511
- return fileKind === "types" ? `../${typeName}` : "../api";
680
+ function getRootImportPath(typeName) {
681
+ return `../${typeName}`;
512
682
  }
513
683
  async function generateFromConfig(cwd = process.cwd()) {
514
684
  const { configPath, config } = await loadConfig(cwd);
@@ -516,6 +686,7 @@ async function generateFromConfig(cwd = process.cwd()) {
516
686
  const operations = [];
517
687
  if (config.cleanOutput) {
518
688
  await removeDir(path3.join(cwd, config.outputDir));
689
+ console.log(`Cleaned output directory: ${config.outputDir}`);
519
690
  }
520
691
  for (const docUrl of config.docUrls) {
521
692
  const document = await loadOpenApiDocument(docUrl);
@@ -525,33 +696,45 @@ async function generateFromConfig(cwd = process.cwd()) {
525
696
  const grouped = groupByPrefix(operations);
526
697
  const files = [];
527
698
  for (const [moduleName, moduleOperations] of Object.entries(grouped)) {
528
- for (const operation of moduleOperations) {
529
- const relativeFile = path3.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
530
- const absoluteFile = path3.join(cwd, relativeFile);
531
- const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath("types", config.typeName)) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath("api", config.typeName));
532
- await writeTextFile(absoluteFile, content);
533
- files.push(relativeFile);
699
+ if (config.fileNaming === "module") {
700
+ const controllerMap = groupByController(moduleOperations);
701
+ for (const [controllerName, controllerOperations] of Object.entries(controllerMap)) {
702
+ const relativeFile = path3.join(config.outputDir, moduleName, `${controllerName}.${config.outputType}`);
703
+ const absoluteFile = path3.join(cwd, relativeFile);
704
+ const content = config.outputType === "ts" ? generateTsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
705
+ for (const op of controllerOperations) {
706
+ op.fileBaseName = controllerName;
707
+ }
708
+ await writeTextFile(absoluteFile, content);
709
+ files.push(relativeFile);
710
+ }
711
+ } else {
712
+ for (const operation of moduleOperations) {
713
+ const relativeFile = path3.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
714
+ const absoluteFile = path3.join(cwd, relativeFile);
715
+ const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
716
+ await writeTextFile(absoluteFile, content);
717
+ files.push(relativeFile);
718
+ }
534
719
  }
535
720
  }
536
721
  const typesContent = generateTypesFile(documentMap, operations, config);
537
- const apiDtsContent = generateApiDtsContent(operations, config);
538
722
  const indexContent = generateIndexFile(operations, config);
539
723
  const typesFile = path3.join(cwd, config.outputDir, `${config.typeName}.${config.outputType === "ts" ? "ts" : "js"}`);
540
- const apiDtsFile = path3.join(cwd, config.outputDir, "api.d.ts");
541
724
  const indexFile = path3.join(cwd, config.outputDir, `index.${config.outputType}`);
542
725
  await writeTextFile(typesFile, typesContent);
543
- await writeTextFile(apiDtsFile, apiDtsContent);
544
726
  await writeTextFile(indexFile, indexContent);
545
727
  files.push(
546
728
  path3.relative(cwd, typesFile),
547
- path3.relative(cwd, apiDtsFile),
548
729
  path3.relative(cwd, indexFile)
549
730
  );
550
731
  return {
551
732
  configPath,
552
733
  files,
553
734
  operationCount: operations.length,
554
- moduleCount: Object.keys(grouped).length
735
+ moduleCount: Object.keys(grouped).length,
736
+ apiFileCount: files.length - 2
737
+ // exclude types + index
555
738
  };
556
739
  }
557
740
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generateFromConfig
4
- } from "./chunk-RRFCLHNU.js";
4
+ } from "./chunk-4OK6R2K5.js";
5
5
 
6
6
  // src/cli.ts
7
7
  async function main() {
@@ -10,8 +10,8 @@ async function main() {
10
10
  const result = await generateFromConfig(process.cwd());
11
11
  const elapsedMs = Date.now() - startedAt;
12
12
  console.log(`swgto loaded config: ${result.configPath}`);
13
- console.log(`Generated ${result.operationCount} API files in ${result.moduleCount} module(s).`);
14
- console.log(`Wrote ${result.files.length} files, including index and api.d.ts.`);
13
+ console.log(`Generated ${result.apiFileCount} api file(s), ${result.operationCount} operation(s) in ${result.moduleCount} module(s).`);
14
+ console.log(`Wrote ${result.files.length} file(s) total (including types, index).`);
15
15
  console.log(`Done in ${elapsedMs}ms.`);
16
16
  } catch (error) {
17
17
  const message = error instanceof Error ? error.message : String(error);
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ interface GenerateResult {
3
3
  files: string[];
4
4
  operationCount: number;
5
5
  moduleCount: number;
6
+ apiFileCount: number;
6
7
  }
7
8
  declare function generateFromConfig(cwd?: string): Promise<GenerateResult>;
8
9
 
@@ -13,11 +14,15 @@ interface SwaggerTsConfig {
13
14
  httpClientPath: string;
14
15
  renameMethod?: (path: string, method: string) => string;
15
16
  resolveRequestPath?: (path: string, method: string, docUrl: string) => string;
17
+ ignoreUrl?: (path: string, method: string, docUrl: string) => boolean;
16
18
  outputDir?: string;
17
19
  moduleName?: (docUrl: string) => string;
18
20
  outputType?: OutputType;
19
21
  typeName?: string;
20
22
  cleanOutput?: boolean;
23
+ fileNaming?: 'module' | 'path';
24
+ flattenQueryParam?: boolean;
25
+ mergeParams?: boolean;
21
26
  }
22
27
 
23
28
  export { type GenerateResult, type RequestConfig, type SwaggerTsConfig, generateFromConfig };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  generateFromConfig
3
- } from "./chunk-RRFCLHNU.js";
3
+ } from "./chunk-4OK6R2K5.js";
4
4
  export {
5
5
  generateFromConfig
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swgto-ts",
3
- "version": "0.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Generate API request files from OpenAPI 3.x documents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
18
18
  "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
19
- "test": "vitest run"
19
+ "test": "tsx ./src/cli.ts"
20
20
  },
21
21
  "keywords": [
22
22
  "openapi",
@@ -29,7 +29,9 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "dependencies": {
32
- "jiti": "^2.4.2"
32
+ "jiti": "^2.4.2",
33
+ "ts-node": "^10.9.2",
34
+ "tsx": "^4.21.0"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/node": "^24.6.0",