From 210cadce6f310ee8527be0709e12cdf50e7de7a6 Mon Sep 17 00:00:00 2001 From: snadn Date: Fri, 30 Jan 2026 15:11:23 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20dedupeApiPrefix=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 #189 添加 dedupeApiPrefix 配置选项用于控制 API 前缀的去重行为: - 当路径已包含指定前缀时,设置为 true 会进行去重防止重复添加前缀 - 设置为 false 时不进行检查,前缀作为变量引用处理 - 默认值为 true,向后兼容现有行为 - 补充完整的测试用例,演示 true 和 false 两种场景 - 更新 README 文档,添加英文和中文详细说明及使用示例 --- README.md | 52 +++++++- src/index.ts | 1 + src/serviceGenerator.ts | 3 +- test/apispe/api/api0.ts | 2 +- .../swagger-dedupe-api-prefix.json | 111 ++++++++++++++++++ test/test.js | 72 +++++++++++- 6 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 test/example-files/swagger-dedupe-api-prefix.json diff --git a/README.md b/README.md index 3972d0c..a8f846b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ npm run openapi2ts | requestLibPath | No | Custom request method path | string | - | | requestOptionsType | No | Custom request options type | string | {[key: string]: any} | | requestImportStatement | No | Custom request import statement | string | - | -| apiPrefix | No | API prefix | string | - | +| apiPrefix | No | API prefix | string \| function | - | +| dedupeApiPrefix | No | Whether to deduplicate API prefix when it already exists in the path | boolean | true | | serversPath | No | Output directory path | string | - | | schemaPath | No | Swagger 2.0 or OpenAPI 3.0 URL | string | - | | projectName | No | Project name | string | - | @@ -83,6 +84,29 @@ npm run openapi2ts | declareType | No | Interface declaration type | type/interface | type | | splitDeclare | No | Generate a separate .d.ts file for each tag group. | boolean | - | +#### About `apiPrefix` and `dedupeApiPrefix` + +The `apiPrefix` option allows you to add a prefix to all generated API paths. It can be: +- A string literal: `apiPrefix: "'/api'"` - The prefix will be processed as a string value +- A function that returns a string: `apiPrefix: (data) => "/api"` - Dynamically determine the prefix + +The `dedupeApiPrefix` option controls how to handle prefixes: +- `true` (default): When the API path already contains the specified prefix, it will be deduplicated (not added again) + - Example: path `/api/user/list` with `apiPrefix: "'/api'"` → remains `/api/user/list` (not `/api/api/user/list`) +- `false`: No deduplication, the prefix will be treated as a variable reference + - Example: path `/user/list` with `apiPrefix: "'/api'"` and `dedupeApiPrefix: false` → becomes `${'/api'}/user/list` + +Example configuration: + +```typescript +export default { + schemaPath: 'http://petstore.swagger.io/v2/swagger.json', + serversPath: './servers', + apiPrefix: "'/api'", // or apiPrefix: (data) => "/api" + dedupeApiPrefix: true, // Deduplicate prefix if it already exists +} +``` + ### Custom Hooks | Property | Type | Description | @@ -159,7 +183,8 @@ npm run openapi2ts | requestLibPath | 否 | 自定义请求方法路径 | string | - | | requestOptionsType | 否 | 自定义请求方法 options 参数类型 | string | {[key: string]: any} | | requestImportStatement | 否 | 自定义请求方法表达式 | string | - | -| apiPrefix | 否 | API 前缀 | string | - | +| apiPrefix | 否 | API 前缀 | string \| function | - | +| dedupeApiPrefix | 否 | 当路径中已存在前缀时是否进行去重 | boolean | true | | serversPath | 否 | 生成文件夹的路径 | string | - | | schemaPath | 否 | Swagger 2.0 或 OpenAPI 3.0 的地址 | string | - | | projectName | 否 | 项目名称 | string | - | @@ -173,6 +198,29 @@ npm run openapi2ts | declareType | 否 | interface 声明类型 | type/interface | type | | splitDeclare | 否 | 每个tag组一个独立的.d.ts. | boolean | - | +#### 关于 `apiPrefix` 和 `dedupeApiPrefix` + +`apiPrefix` 选项用于为生成的所有 API 路径添加前缀,支持两种形式: +- 字符串字面量:`apiPrefix: "'/api'"` - 前缀将被作为字符串值处理 +- 函数:`apiPrefix: (data) => "/api"` - 动态决定前缀 + +`dedupeApiPrefix` 选项控制如何处理前缀: +- `true`(默认):当 API 路径已包含指定的前缀时,会进行去重处理(不再添加) + - 示例:路径 `/api/user/list`,配置 `apiPrefix: "'/api'"` → 保持 `/api/user/list`(不会变成 `/api/api/user/list`) +- `false`:不进行去重,前缀将被作为变量引用处理 + - 示例:路径 `/user/list`,配置 `apiPrefix: "'/api'"` 和 `dedupeApiPrefix: false` → 生成 `${'/api'}/user/list` + +配置示例: + +```typescript +export default { + schemaPath: 'http://petstore.swagger.io/v2/swagger.json', + serversPath: './servers', + apiPrefix: "'/api'", // 或者 apiPrefix: (data) => "/api" + dedupeApiPrefix: true, // 当前缀已存在时进行去重 +} +``` + ### 自定义钩子 | 属性 | 类型 | 说明 | diff --git a/src/index.ts b/src/index.ts index f9a6d4a..04bc556 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export type GenerateServiceProps = { functionName: string; autoExclude?: boolean; }) => string); + dedupeApiPrefix?: boolean /** * 生成的文件夹的路径 */ diff --git a/src/serviceGenerator.ts b/src/serviceGenerator.ts index eddb6a3..e8426e2 100644 --- a/src/serviceGenerator.ts +++ b/src/serviceGenerator.ts @@ -323,6 +323,7 @@ class ServiceGenerator { this.config = { projectName: 'api', templatesFolder: join(__dirname, '../', 'templates'), + dedupeApiPrefix: true, ...config, }; if (this.config.hook?.afterOpenApiDataInited) { @@ -554,7 +555,7 @@ class ServiceGenerator { return formattedPath; } - if (prefix.startsWith("'") || prefix.startsWith('"') || prefix.startsWith('`')) { + if (this.config.dedupeApiPrefix && (prefix.startsWith("'") || prefix.startsWith('"') || prefix.startsWith('`'))) { const finalPrefix = prefix.slice(1, prefix.length - 1); if ( formattedPath.startsWith(finalPrefix) || diff --git a/test/apispe/api/api0.ts b/test/apispe/api/api0.ts index 32fead0..0e288b6 100644 --- a/test/apispe/api/api0.ts +++ b/test/apispe/api/api0.ts @@ -26,7 +26,7 @@ export async function postLicenceActive(body: {}, file?: File, options?: { [key: if (item instanceof Array) { item.forEach((f) => formData.append(ele, f || '')); } else { - formData.append(ele, JSON.stringify(item)); + formData.append(ele, new Blob([JSON.stringify(item)], { type: 'application/json' })); } } else { formData.append(ele, item); diff --git a/test/example-files/swagger-dedupe-api-prefix.json b/test/example-files/swagger-dedupe-api-prefix.json new file mode 100644 index 0000000..2d1f75b --- /dev/null +++ b/test/example-files/swagger-dedupe-api-prefix.json @@ -0,0 +1,111 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Test Dedupe API Prefix True", + "version": "1.0.0" + }, + "paths": { + "/api/apiInfo/get": { + "get": { + "operationId": "getApiInfo", + "tags": ["api"], + "summary": "Get API Info", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/apiInfo/update": { + "post": { + "operationId": "updateApiInfo", + "tags": ["api"], + "summary": "Update API Info", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/user/profile": { + "get": { + "operationId": "getUserProfile", + "tags": ["user"], + "summary": "Get User Profile", + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/user/info": { + "post": { + "operationId": "getUserInfo", + "tags": ["user"], + "summary": "Get User Info", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/product/list": { + "get": { + "operationId": "getProductList", + "tags": ["product"], + "summary": "Get Product List", + "responses": { + "200": { + "description": "Success" + } + } + } + } + } +} diff --git a/test/test.js b/test/test.js index 7b0583a..3b4dea3 100644 --- a/test/test.js +++ b/test/test.js @@ -139,10 +139,76 @@ const gen = async () => { }, }); + // await openAPI.generateService({ + // schemaPath: `${__dirname}/example-files/swagger-splitdeclare.json`, + // serversPath: './splitDeclare', + // splitDeclare:true + // }); + + // Test dedupeApiPrefix 配置的两种场景 + // 使用同一个 swagger 文件,通过不同的 dedupeApiPrefix 配置演示不同的行为 + + // 场景1:dedupeApiPrefix: true - 去重模式 + // 当 apiPrefix 为 '/api',路径已经包含 '/api' 前缀时(如 /api/apiInfo/xxx), + // 设置 dedupeApiPrefix: true 会去重,生成的路径仍为 /api/apiInfo/xxx(不重复添加) + await openAPI.generateService({ + schemaPath: `${__dirname}/example-files/swagger-dedupe-api-prefix.json`, + serversPath: './servers/dedupe-api-prefix/true', + apiPrefix: `'/api'`, + dedupeApiPrefix: true, + }); + + const dedupeApiPrefixTrueApiControllerStr = fs.readFileSync( + path.join(__dirname, 'servers/dedupe-api-prefix/true/api/api.ts'), + 'utf8', + ); + const dedupeApiPrefixTrueUserControllerStr = fs.readFileSync( + path.join(__dirname, 'servers/dedupe-api-prefix/true/api/user.ts'), + 'utf8', + ); + + // /api/apiInfo/get 已经有 /api 前缀,dedupeApiPrefix: true 会去重,保持 /api/apiInfo/get + assert(dedupeApiPrefixTrueApiControllerStr.indexOf(`'/api/apiInfo/get'`) > 0 || + dedupeApiPrefixTrueApiControllerStr.indexOf('`/api/apiInfo/get`') > 0, + 'dedupeApiPrefix=true: /api/apiInfo/get should remain as /api/apiInfo/get (not /api/api/apiInfo/get)'); + + assert(dedupeApiPrefixTrueApiControllerStr.indexOf(`'/api/apiInfo/update'`) > 0 || + dedupeApiPrefixTrueApiControllerStr.indexOf('`/api/apiInfo/update`') > 0, + 'dedupeApiPrefix=true: /api/apiInfo/update should remain as /api/apiInfo/update'); + + assert(dedupeApiPrefixTrueUserControllerStr.indexOf(`'/api/user/profile'`) > 0 || + dedupeApiPrefixTrueUserControllerStr.indexOf('`/api/user/profile`') > 0, + 'dedupeApiPrefix=true: /api/user/profile should remain as /api/user/profile'); + + + // 场景2:dedupeApiPrefix: false - 非去重模式 + // 同一个 swagger 文件,设置 dedupeApiPrefix: false 时,不检查前缀,直接作为变量引用 + // 导致前缀被重复添加 await openAPI.generateService({ - schemaPath: `${__dirname}/example-files/swagger-splitdeclare.json`, - serversPath: './splitDeclare', - splitDeclare:true + schemaPath: `${__dirname}/example-files/swagger-dedupe-api-prefix.json`, + serversPath: './servers/dedupe-api-prefix/false', + apiPrefix: `'/api'`, + dedupeApiPrefix: false, }); + + const dedupeApiPrefixFalseApiControllerStr = fs.readFileSync( + path.join(__dirname, 'servers/dedupe-api-prefix/false/api/api.ts'), + 'utf8', + ); + const dedupeApiPrefixFalseUserControllerStr = fs.readFileSync( + path.join(__dirname, 'servers/dedupe-api-prefix/false/api/user.ts'), + 'utf8', + ); + + // 当 dedupeApiPrefix: false 时,同样的路径会被作为变量引用,导致前缀被拼接 + // /api/apiInfo/get 会变成 ${'/api'}/api/apiInfo/get + assert(dedupeApiPrefixFalseApiControllerStr.indexOf("${'/api'}/api/apiInfo/get") > 0, + 'dedupeApiPrefix=false: /api/apiInfo/get should become ${' + "'" + '/api' + "'" + '}/api/apiInfo/get (duplication)'); + + assert(dedupeApiPrefixFalseApiControllerStr.indexOf("${'/api'}/api/apiInfo/update") > 0, + 'dedupeApiPrefix=false: /api/apiInfo/update should become ${' + "'" + '/api' + "'" + '}/api/apiInfo/update (duplication)'); + + assert(dedupeApiPrefixFalseUserControllerStr.indexOf("${'/api'}/api/user/profile") > 0, + 'dedupeApiPrefix=false: /api/user/profile should become ${' + "'" + '/api' + "'" + '}/api/user/profile (duplication)'); }; gen();