@wx-sab/renkei 1.2.5 → 1.3.1
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 +166 -0
- package/lib/commands/api.js +67 -12
- package/lib/core/config-utils.d.ts +8 -0
- package/lib/core/config-utils.js +45 -0
- package/lib/core/config-utils.test.d.ts +1 -0
- package/lib/core/config-utils.test.js +204 -0
- package/lib/core/folder-selector.d.ts +10 -0
- package/lib/core/folder-selector.js +147 -0
- package/lib/core/generate.js +43 -49
- package/lib/core/interface.d.ts +16 -0
- package/lib/core/type-filter.d.ts +1 -0
- package/lib/core/type-filter.js +275 -0
- package/lib/core/utils.test.d.ts +1 -0
- package/lib/core/utils.test.js +192 -0
- package/package.json +11 -3
- package/templates/api.ejs +3 -3
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.selectFoldersForServices = exports.selectFolders = exports.extractFoldersFromSwagger = exports.fetchSwaggerDocument = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
18
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
19
|
+
const utils_1 = require("./utils");
|
|
20
|
+
const fetchSwaggerDocument = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
const finalConfig = (0, utils_1.generateConfig)(config);
|
|
23
|
+
if (!finalConfig.source) {
|
|
24
|
+
throw new Error('未配置文档源');
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = yield axios_1.default.get(finalConfig.source, {
|
|
28
|
+
timeout: 30000,
|
|
29
|
+
validateStatus: (status) => status < 400
|
|
30
|
+
});
|
|
31
|
+
return response.data;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = ((_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || error.message;
|
|
35
|
+
throw new Error(`获取文档失败: ${message}`, { cause: error });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
exports.fetchSwaggerDocument = fetchSwaggerDocument;
|
|
39
|
+
const extractFoldersFromSwagger = (swaggerDoc) => {
|
|
40
|
+
const folderMap = new Map();
|
|
41
|
+
if (swaggerDoc.paths) {
|
|
42
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
43
|
+
if (methods && typeof methods === 'object') {
|
|
44
|
+
Object.values(methods).forEach((methodInfo) => {
|
|
45
|
+
if (methodInfo && methodInfo['x-apifox-folder']) {
|
|
46
|
+
const folder = methodInfo['x-apifox-folder'];
|
|
47
|
+
const folderPath = folder.split('/')[0];
|
|
48
|
+
if (folderMap.has(folderPath)) {
|
|
49
|
+
const info = folderMap.get(folderPath);
|
|
50
|
+
info.count++;
|
|
51
|
+
info.paths.add(path);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
folderMap.set(folderPath, {
|
|
55
|
+
count: 1,
|
|
56
|
+
paths: new Set([path])
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const folders = [];
|
|
65
|
+
folderMap.forEach((info, name) => {
|
|
66
|
+
folders.push({
|
|
67
|
+
name,
|
|
68
|
+
path: name,
|
|
69
|
+
count: info.count
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return lodash_1.default.sortBy(folders, ['name']);
|
|
73
|
+
};
|
|
74
|
+
exports.extractFoldersFromSwagger = extractFoldersFromSwagger;
|
|
75
|
+
const selectFolders = (folders) => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
if (folders.length === 0) {
|
|
77
|
+
console.log('未找到任何文件夹,将生成所有接口');
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const totalCount = folders.reduce((sum, f) => sum + f.count, 0);
|
|
81
|
+
console.log('\n发现以下文件夹:');
|
|
82
|
+
folders.forEach((folder, index) => {
|
|
83
|
+
console.log(` ${index + 1}. ${folder.name} (${folder.count} 个接口)`);
|
|
84
|
+
});
|
|
85
|
+
const { selectedFolders } = yield inquirer_1.default.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'checkbox',
|
|
88
|
+
name: 'selectedFolders',
|
|
89
|
+
message: '请选择要生成的文件夹 (空格选择,回车确认)',
|
|
90
|
+
choices: [
|
|
91
|
+
{
|
|
92
|
+
name: `全部 (共 ${totalCount} 个接口)`,
|
|
93
|
+
value: '__ALL__',
|
|
94
|
+
checked: true
|
|
95
|
+
},
|
|
96
|
+
new inquirer_1.default.Separator('── 具体文件夹 ──'),
|
|
97
|
+
...folders.map(f => ({
|
|
98
|
+
name: `${f.name} (${f.count} 个接口)`,
|
|
99
|
+
value: f.name,
|
|
100
|
+
checked: false
|
|
101
|
+
}))
|
|
102
|
+
],
|
|
103
|
+
pageSize: 15,
|
|
104
|
+
loop: false
|
|
105
|
+
}
|
|
106
|
+
]);
|
|
107
|
+
// 如果选择了"全部"选项,返回空数组(表示生成所有接口)
|
|
108
|
+
if (selectedFolders.includes('__ALL__')) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return selectedFolders;
|
|
112
|
+
});
|
|
113
|
+
exports.selectFolders = selectFolders;
|
|
114
|
+
const selectFoldersForServices = (configs) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const folderSelections = new Map();
|
|
116
|
+
const failedServices = [];
|
|
117
|
+
for (const config of configs) {
|
|
118
|
+
console.log(`\n正在分析服务: ${config.serviceName}`);
|
|
119
|
+
try {
|
|
120
|
+
const swaggerDoc = yield (0, exports.fetchSwaggerDocument)(config);
|
|
121
|
+
const folders = (0, exports.extractFoldersFromSwagger)(swaggerDoc);
|
|
122
|
+
if (folders.length > 0) {
|
|
123
|
+
const selectedFolders = yield (0, exports.selectFolders)(folders);
|
|
124
|
+
folderSelections.set(config.serviceName, selectedFolders);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(` 服务 ${config.serviceName} 未找到文件夹信息,将生成所有接口`);
|
|
128
|
+
folderSelections.set(config.serviceName, []);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errorMessage = error.message || '未知错误';
|
|
133
|
+
console.error(` 获取服务 ${config.serviceName} 的文档失败: ${errorMessage}`);
|
|
134
|
+
failedServices.push({ serviceName: config.serviceName, error: errorMessage });
|
|
135
|
+
folderSelections.set(config.serviceName, []);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (failedServices.length > 0) {
|
|
139
|
+
console.log('\n⚠️ 部分服务获取失败:');
|
|
140
|
+
failedServices.forEach(({ serviceName, error }) => {
|
|
141
|
+
console.log(` - ${serviceName}: ${error}`);
|
|
142
|
+
});
|
|
143
|
+
console.log('这些服务将生成所有接口\n');
|
|
144
|
+
}
|
|
145
|
+
return folderSelections;
|
|
146
|
+
});
|
|
147
|
+
exports.selectFoldersForServices = selectFoldersForServices;
|
package/lib/core/generate.js
CHANGED
|
@@ -28,36 +28,27 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
28
28
|
const swagger_typescript_api_1 = require("swagger-typescript-api");
|
|
29
29
|
const cli_progress_1 = __importDefault(require("cli-progress"));
|
|
30
30
|
const utils_1 = require("./utils");
|
|
31
|
+
const config_utils_1 = require("./config-utils");
|
|
32
|
+
const type_filter_1 = require("./type-filter");
|
|
31
33
|
/**
|
|
32
34
|
* 单个配置生成
|
|
33
35
|
*/
|
|
34
|
-
const generate = (conf) => {
|
|
35
|
-
// 最终使用的配置项
|
|
36
|
+
const generate = (conf) => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
37
|
const config = (0, utils_1.generateConfig)(conf);
|
|
37
|
-
// 模型命名空间
|
|
38
38
|
const modelNamespace = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.modelNamespace));
|
|
39
|
-
// 服务名
|
|
40
39
|
const serviceName = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.serviceName));
|
|
41
|
-
// 存放已生成的方法名
|
|
42
40
|
const methodNameSet = new Set();
|
|
43
|
-
|
|
41
|
+
const skippedRoutes = new Set();
|
|
42
|
+
const result = yield (0, swagger_typescript_api_1.generateApi)({
|
|
44
43
|
url: config.source,
|
|
45
|
-
// 文件名
|
|
46
44
|
name: config.name,
|
|
47
|
-
// 输出目录
|
|
48
45
|
output: path_1.default.resolve(config.output, config.serviceName),
|
|
49
|
-
// 模块化?
|
|
50
46
|
modular: false,
|
|
51
|
-
// 静默
|
|
52
47
|
silent: true,
|
|
53
48
|
extractRequestParams: true,
|
|
54
|
-
// 清空输出目录?
|
|
55
49
|
cleanOutput: false,
|
|
56
|
-
// 生成请求客户端
|
|
57
50
|
generateClient: false,
|
|
58
|
-
// 模板文件
|
|
59
51
|
templates: path_1.default.resolve(__dirname, "../../templates"),
|
|
60
|
-
// 额外的,此处为了生成接口
|
|
61
52
|
extraTemplates: [
|
|
62
53
|
{
|
|
63
54
|
name: "index.ts",
|
|
@@ -67,7 +58,19 @@ const generate = (conf) => {
|
|
|
67
58
|
hooks: {
|
|
68
59
|
onCreateRouteName(routeNameInfo, rawRouteInfo) {
|
|
69
60
|
var _a, _b, _c;
|
|
70
|
-
|
|
61
|
+
if (config.folders && config.folders.length > 0) {
|
|
62
|
+
const extendedRouteInfo = rawRouteInfo;
|
|
63
|
+
const folder = extendedRouteInfo['x-apifox-folder'] || '';
|
|
64
|
+
const folderPath = folder.split('/')[0];
|
|
65
|
+
if (!config.folders.includes(folderPath)) {
|
|
66
|
+
skippedRoutes.add(routeNameInfo.original);
|
|
67
|
+
return {
|
|
68
|
+
duplicate: false,
|
|
69
|
+
original: config_utils_1.SKIP_ROUTE_MARKER,
|
|
70
|
+
usage: config_utils_1.SKIP_ROUTE_MARKER
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
71
74
|
const newRoute = (_a = config.customRequestUrl) === null || _a === void 0 ? void 0 : _a.call(config, rawRouteInfo, config);
|
|
72
75
|
if (typeof newRoute === "string") {
|
|
73
76
|
rawRouteInfo.route = newRoute;
|
|
@@ -77,12 +80,10 @@ const generate = (conf) => {
|
|
|
77
80
|
// @ts-ignore
|
|
78
81
|
rawRouteInfo.mockUrl = newRoute.mockUrl;
|
|
79
82
|
}
|
|
80
|
-
// 检测路径中的常见错误
|
|
81
83
|
if (/ /.test(rawRouteInfo.route)) {
|
|
82
84
|
throw Error(`语法错误,路径中存在空格,请核对,可以尝试通过配置 customRequestUrl 进行处理,当前处理节点为:${rawRouteInfo.route}`);
|
|
83
85
|
}
|
|
84
86
|
else if (/([\u4e00-\u9fa5])/.test(rawRouteInfo.route)) {
|
|
85
|
-
// 如果路径里面包含中文,警告用户,因为通常不会写带中文的api,但是直接抄配置文件很有可能由于大小项目的apifox目录组织结构不同,导致这个问题。
|
|
86
87
|
console.warn(`警告:检测到请求路径中存在中文,这通常是因为错误配置了 customRequestUrl,请进行核对,当前处理路径为:${rawRouteInfo.route} `);
|
|
87
88
|
}
|
|
88
89
|
const methodName = (_c = (_b = config === null || config === void 0 ? void 0 : config.customRouteName) === null || _b === void 0 ? void 0 : _b.call(config, routeNameInfo, rawRouteInfo)) !== null && _c !== void 0 ? _c : (0, utils_1.generateRouteName)(rawRouteInfo.method, rawRouteInfo.route);
|
|
@@ -96,10 +97,10 @@ const generate = (conf) => {
|
|
|
96
97
|
},
|
|
97
98
|
onPrepareConfig(currentConfiguration) {
|
|
98
99
|
return Object.assign(Object.assign({}, currentConfiguration), { modelNamespace,
|
|
99
|
-
serviceName, requestTempalte: config.requestTempalte
|
|
100
|
+
serviceName, requestTempalte: config.requestTempalte, skippedRoutes,
|
|
101
|
+
SKIP_ROUTE_MARKER: config_utils_1.SKIP_ROUTE_MARKER });
|
|
100
102
|
},
|
|
101
103
|
onParseSchema(originalSchema, parsedSchema) {
|
|
102
|
-
// 针对解析出的 any 做转换,主要是 swagger 3.0
|
|
103
104
|
if (parsedSchema.name === "any" &&
|
|
104
105
|
typeof parsedSchema.content === "string" &&
|
|
105
106
|
parsedSchema.$ref) {
|
|
@@ -109,8 +110,6 @@ const generate = (conf) => {
|
|
|
109
110
|
},
|
|
110
111
|
onCreateRoute(routeData) {
|
|
111
112
|
var _a;
|
|
112
|
-
// 将 /api/v1/order-mains/{orderId} 转成 /api/v1/order-mains/${orderId}
|
|
113
|
-
// routeData.raw.route 是进过 customRequestUrl 转换过的
|
|
114
113
|
// @ts-ignore
|
|
115
114
|
routeData.request.path = (_a = routeData.raw.route) === null || _a === void 0 ? void 0 : _a.replace(/\{([^}]+)\}/g, "${$1}");
|
|
116
115
|
return routeData;
|
|
@@ -120,38 +119,23 @@ const generate = (conf) => {
|
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
});
|
|
123
|
-
|
|
122
|
+
if (config.folders && config.folders.length > 0 && config.output) {
|
|
123
|
+
yield (0, type_filter_1.filterUnusedTypes)(config.output, config.serviceName, serviceName, modelNamespace);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
});
|
|
124
127
|
exports.generate = generate;
|
|
125
|
-
|
|
126
|
-
const generateApis = (config, services // 指定服务
|
|
127
|
-
) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const generateApis = (config, services) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
129
|
var _a, e_1, _b, _c;
|
|
129
|
-
|
|
130
|
-
const collectConfig = (conf, collects = []) => {
|
|
131
|
-
if (!conf)
|
|
132
|
-
return collects;
|
|
133
|
-
if (Array.isArray(conf)) {
|
|
134
|
-
conf.forEach((con) => collectConfig(con, collects));
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
collects.push(lodash_1.default.omit(conf, "projects"));
|
|
138
|
-
if (Array.isArray(conf.projects)) {
|
|
139
|
-
conf.projects.forEach((project) => collectConfig(Object.assign(Object.assign({}, lodash_1.default.omit(conf, "projects")), project), collects));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return collects;
|
|
143
|
-
};
|
|
144
|
-
const _configs = collectConfig(config).filter((conf) => {
|
|
145
|
-
// 只筛选指定的服务
|
|
130
|
+
const _configs = (0, config_utils_1.collectConfig)(config).filter((conf) => {
|
|
146
131
|
if (services.length > 0) {
|
|
147
132
|
return services.includes(conf.serviceName);
|
|
148
133
|
}
|
|
149
134
|
return true;
|
|
150
135
|
});
|
|
151
|
-
// 进度条控制
|
|
152
136
|
const progress = new cli_progress_1.default.SingleBar({});
|
|
153
137
|
progress.start(_configs.length, 0);
|
|
154
|
-
|
|
138
|
+
const failedServices = [];
|
|
155
139
|
try {
|
|
156
140
|
for (var _d = true, _configs_1 = __asyncValues(_configs), _configs_1_1; _configs_1_1 = yield _configs_1.next(), _a = _configs_1_1.done, !_a; _d = true) {
|
|
157
141
|
_c = _configs_1_1.value;
|
|
@@ -161,8 +145,12 @@ const generateApis = (config, services // 指定服务
|
|
|
161
145
|
yield (0, exports.generate)(conf);
|
|
162
146
|
}
|
|
163
147
|
catch (e) {
|
|
164
|
-
|
|
165
|
-
|
|
148
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
149
|
+
console.error(message);
|
|
150
|
+
failedServices.push({
|
|
151
|
+
serviceName: conf.serviceName,
|
|
152
|
+
error: message || '未知错误'
|
|
153
|
+
});
|
|
166
154
|
}
|
|
167
155
|
finally {
|
|
168
156
|
progress.increment(1);
|
|
@@ -177,8 +165,14 @@ const generateApis = (config, services // 指定服务
|
|
|
177
165
|
finally { if (e_1) throw e_1.error; }
|
|
178
166
|
}
|
|
179
167
|
progress.stop();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
168
|
+
if (failedServices.length > 0) {
|
|
169
|
+
console.info('✅ 执行完毕,以下服务生成失败:');
|
|
170
|
+
failedServices.forEach(({ serviceName, error }) => {
|
|
171
|
+
console.info(` - ${serviceName}: ${error}`);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.info('✅ 执行完毕');
|
|
176
|
+
}
|
|
183
177
|
});
|
|
184
178
|
exports.generateApis = generateApis;
|
package/lib/core/interface.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { GenerateApiParams, RawRouteInfo, RouteNameInfo } from "swagger-typescript-api";
|
|
2
|
+
export interface ExtendedRawRouteInfo extends RawRouteInfo {
|
|
3
|
+
'x-apifox-folder'?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ConfigWithProjects extends BasicConfig {
|
|
6
|
+
projects?: ConfigWithProjects[];
|
|
7
|
+
}
|
|
8
|
+
export interface ServiceConfig {
|
|
9
|
+
serviceName: string;
|
|
10
|
+
folders?: string[];
|
|
11
|
+
projects?: ServiceConfig[];
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* 基础配置
|
|
4
15
|
*/
|
|
@@ -60,6 +71,11 @@ export type BasicConfig = {
|
|
|
60
71
|
* 自定义生成的接口名
|
|
61
72
|
*/
|
|
62
73
|
customRouteName?(routeNameInfo: RouteNameInfo, rawRouteInfo: RawRouteInfo): string;
|
|
74
|
+
/**
|
|
75
|
+
* 要生成的文件夹列表(文件夹名称)
|
|
76
|
+
* 如果为空数组,则生成所有接口
|
|
77
|
+
*/
|
|
78
|
+
folders?: string[];
|
|
63
79
|
};
|
|
64
80
|
/**
|
|
65
81
|
* 如果 sourceType 是 apifox,则适用该配置项
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const filterUnusedTypes: (outputDir: string, serviceDirName: string, serviceTypeName: string, modelNamespace: string) => Promise<void>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.filterUnusedTypes = void 0;
|
|
16
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const extractTypeReferences = (content, modelNamespace) => {
|
|
19
|
+
const typeUsageMap = new Map();
|
|
20
|
+
const regex = new RegExp(`${modelNamespace}\\.(\\w+)\\.(\\w+)`, 'g');
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = regex.exec(content)) !== null) {
|
|
23
|
+
const serviceName = match[1];
|
|
24
|
+
const typeName = match[2];
|
|
25
|
+
if (!typeUsageMap.has(serviceName)) {
|
|
26
|
+
typeUsageMap.set(serviceName, new Set());
|
|
27
|
+
}
|
|
28
|
+
typeUsageMap.get(serviceName).add(typeName);
|
|
29
|
+
}
|
|
30
|
+
return typeUsageMap;
|
|
31
|
+
};
|
|
32
|
+
const stripComments = (content) => {
|
|
33
|
+
const result = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
const len = content.length;
|
|
36
|
+
while (i < len) {
|
|
37
|
+
if (content[i] === '"' || content[i] === "'" || content[i] === '`') {
|
|
38
|
+
const quote = content[i];
|
|
39
|
+
result.push(content[i]);
|
|
40
|
+
i++;
|
|
41
|
+
while (i < len && content[i] !== quote) {
|
|
42
|
+
if (content[i] === '\\' && i + 1 < len) {
|
|
43
|
+
result.push(content[i], content[i + 1]);
|
|
44
|
+
i += 2;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
result.push(content[i]);
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (i < len) {
|
|
52
|
+
result.push(content[i]);
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (content[i] === '/' && i + 1 < len && content[i + 1] === '/') {
|
|
58
|
+
while (i < len && content[i] !== '\n') {
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (content[i] === '/' && i + 1 < len && content[i + 1] === '*') {
|
|
64
|
+
i += 2;
|
|
65
|
+
while (i < len && !(content[i] === '*' && i + 1 < len && content[i + 1] === '/')) {
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
if (i < len)
|
|
69
|
+
i += 2;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
result.push(content[i]);
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
return result.join('');
|
|
76
|
+
};
|
|
77
|
+
const collectTypeDefBody = (lines, startIndex, initialLines) => {
|
|
78
|
+
const firstLine = lines[startIndex].trim();
|
|
79
|
+
const typeDefLines = [...initialLines];
|
|
80
|
+
if (!firstLine.includes('{')) {
|
|
81
|
+
typeDefLines.push(lines[startIndex]);
|
|
82
|
+
return { typeDefLines, nextIndex: startIndex + 1 };
|
|
83
|
+
}
|
|
84
|
+
let braceCount = (firstLine.match(/{/g) || []).length - (firstLine.match(/}/g) || []).length;
|
|
85
|
+
typeDefLines.push(lines[startIndex]);
|
|
86
|
+
let i = startIndex + 1;
|
|
87
|
+
while (i < lines.length && braceCount > 0) {
|
|
88
|
+
typeDefLines.push(lines[i]);
|
|
89
|
+
braceCount += (lines[i].match(/{/g) || []).length;
|
|
90
|
+
braceCount -= (lines[i].match(/}/g) || []).length;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
return { typeDefLines, nextIndex: i };
|
|
94
|
+
};
|
|
95
|
+
const extractAllTypeDefinitions = (content) => {
|
|
96
|
+
const typeDefs = new Map();
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < lines.length) {
|
|
100
|
+
const line = lines[i];
|
|
101
|
+
const trimmedLine = line.trim();
|
|
102
|
+
const isJSDoc = trimmedLine.startsWith('/**');
|
|
103
|
+
const isBlockComment = trimmedLine.startsWith('/*') && !isJSDoc;
|
|
104
|
+
if (isJSDoc || isBlockComment) {
|
|
105
|
+
const commentLines = [];
|
|
106
|
+
while (i < lines.length) {
|
|
107
|
+
commentLines.push(lines[i]);
|
|
108
|
+
if (lines[i].trim().endsWith('*/')) {
|
|
109
|
+
i++;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
if (i < lines.length) {
|
|
115
|
+
const nextLine = lines[i].trim();
|
|
116
|
+
const typeMatch = nextLine.match(/(?:export\s+)?(?:interface|type|enum)\s+(\w+)/);
|
|
117
|
+
if (typeMatch) {
|
|
118
|
+
const typeName = typeMatch[1];
|
|
119
|
+
const { typeDefLines, nextIndex } = collectTypeDefBody(lines, i, commentLines);
|
|
120
|
+
typeDefs.set(typeName, typeDefLines.join('\n'));
|
|
121
|
+
i = nextIndex;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const typeMatch = trimmedLine.match(/(?:export\s+)?(?:interface|type|enum)\s+(\w+)/);
|
|
128
|
+
if (typeMatch) {
|
|
129
|
+
const typeName = typeMatch[1];
|
|
130
|
+
const { typeDefLines, nextIndex } = collectTypeDefBody(lines, i, []);
|
|
131
|
+
typeDefs.set(typeName, typeDefLines.join('\n'));
|
|
132
|
+
i = nextIndex;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
return typeDefs;
|
|
138
|
+
};
|
|
139
|
+
const CHUNK_SIZE = 200;
|
|
140
|
+
const findShortNameRefs = (codeOnly, typeNameSet) => {
|
|
141
|
+
const found = new Set();
|
|
142
|
+
const typeNames = Array.from(typeNameSet);
|
|
143
|
+
for (let start = 0; start < typeNames.length; start += CHUNK_SIZE) {
|
|
144
|
+
const chunk = typeNames.slice(start, start + CHUNK_SIZE);
|
|
145
|
+
const pattern = chunk.map(n => `\\b${n}\\b`).join('|');
|
|
146
|
+
const regex = new RegExp(pattern, 'g');
|
|
147
|
+
let match;
|
|
148
|
+
while ((match = regex.exec(codeOnly)) !== null) {
|
|
149
|
+
found.add(match[0]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return found;
|
|
153
|
+
};
|
|
154
|
+
const findTypeDependencies = (rootTypes, typeDefs, modelNamespace, serviceName) => {
|
|
155
|
+
const visited = new Set();
|
|
156
|
+
const typeNameSet = new Set(typeDefs.keys());
|
|
157
|
+
const traverse = (typeName) => {
|
|
158
|
+
if (visited.has(typeName))
|
|
159
|
+
return;
|
|
160
|
+
visited.add(typeName);
|
|
161
|
+
const typeDef = typeDefs.get(typeName);
|
|
162
|
+
if (!typeDef)
|
|
163
|
+
return;
|
|
164
|
+
const fullRefRegex = new RegExp(`${modelNamespace}\\.${serviceName}\\.(\\w+)`, 'g');
|
|
165
|
+
let match;
|
|
166
|
+
while ((match = fullRefRegex.exec(typeDef)) !== null) {
|
|
167
|
+
const refTypeName = match[1];
|
|
168
|
+
if (!visited.has(refTypeName)) {
|
|
169
|
+
traverse(refTypeName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const codeOnly = stripComments(typeDef);
|
|
173
|
+
const shortRefs = findShortNameRefs(codeOnly, typeNameSet);
|
|
174
|
+
for (const refTypeName of shortRefs) {
|
|
175
|
+
if (refTypeName !== typeName && !visited.has(refTypeName) && typeDefs.has(refTypeName)) {
|
|
176
|
+
traverse(refTypeName);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
for (const typeName of rootTypes) {
|
|
181
|
+
traverse(typeName);
|
|
182
|
+
}
|
|
183
|
+
return visited;
|
|
184
|
+
};
|
|
185
|
+
const detectNamespaceIndent = (lines, serviceName) => {
|
|
186
|
+
var _a;
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
if (line.trim().startsWith(`export namespace ${serviceName}`)) {
|
|
189
|
+
const indent = ((_a = line.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1]) || '';
|
|
190
|
+
const innerIndent = indent + ' ';
|
|
191
|
+
return innerIndent;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return ' ';
|
|
195
|
+
};
|
|
196
|
+
const filterModelTypes = (modelContent, usedTypes, modelNamespace, serviceName) => {
|
|
197
|
+
const lines = modelContent.split('\n');
|
|
198
|
+
const allTypeDefs = extractAllTypeDefinitions(modelContent);
|
|
199
|
+
const allUsedTypes = findTypeDependencies(usedTypes, allTypeDefs, modelNamespace, serviceName);
|
|
200
|
+
const filteredTypeDefs = [];
|
|
201
|
+
allTypeDefs.forEach((typeDef, name) => {
|
|
202
|
+
if (allUsedTypes.has(name)) {
|
|
203
|
+
filteredTypeDefs.push(typeDef);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
let inModelNamespace = false;
|
|
207
|
+
let inServiceNamespace = false;
|
|
208
|
+
const header = [];
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const trimmedLine = line.trim();
|
|
211
|
+
if (trimmedLine.startsWith(`export namespace ${modelNamespace}`)) {
|
|
212
|
+
inModelNamespace = true;
|
|
213
|
+
header.push(line);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (trimmedLine.startsWith(`export namespace ${serviceName}`)) {
|
|
217
|
+
inServiceNamespace = true;
|
|
218
|
+
header.push(line);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (inServiceNamespace) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (inModelNamespace) {
|
|
225
|
+
if (trimmedLine === '}') {
|
|
226
|
+
inModelNamespace = false;
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
header.push(line);
|
|
231
|
+
}
|
|
232
|
+
const closingIndent = detectNamespaceIndent(lines, serviceName);
|
|
233
|
+
const result = [
|
|
234
|
+
...header,
|
|
235
|
+
...filteredTypeDefs,
|
|
236
|
+
closingIndent + '}',
|
|
237
|
+
'}'
|
|
238
|
+
];
|
|
239
|
+
return result.join('\n');
|
|
240
|
+
};
|
|
241
|
+
const filterUnusedTypes = (outputDir, serviceDirName, serviceTypeName, modelNamespace) => __awaiter(void 0, void 0, void 0, function* () {
|
|
242
|
+
try {
|
|
243
|
+
const serviceDir = path_1.default.join(outputDir, serviceDirName);
|
|
244
|
+
const indexFile = path_1.default.join(serviceDir, 'index.ts');
|
|
245
|
+
const modelFile = path_1.default.join(serviceDir, 'model.d.ts');
|
|
246
|
+
const indexContent = yield promises_1.default.readFile(indexFile, 'utf-8');
|
|
247
|
+
const modelContent = yield promises_1.default.readFile(modelFile, 'utf-8');
|
|
248
|
+
const typeUsageMap = extractTypeReferences(indexContent, modelNamespace);
|
|
249
|
+
if (!typeUsageMap.has(serviceTypeName)) {
|
|
250
|
+
console.log(`没有找到服务 ${serviceTypeName} 的类型引用,跳过过滤`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const usedTypes = typeUsageMap.get(serviceTypeName);
|
|
254
|
+
if (usedTypes.size === 0) {
|
|
255
|
+
console.log(`服务 ${serviceTypeName} 没有使用任何类型,跳过过滤`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const filteredModelContent = filterModelTypes(modelContent, usedTypes, modelNamespace, serviceTypeName);
|
|
259
|
+
yield promises_1.default.writeFile(modelFile, filteredModelContent, 'utf-8');
|
|
260
|
+
const originalLines = modelContent.split('\n').length;
|
|
261
|
+
const filteredLines = filteredModelContent.split('\n').length;
|
|
262
|
+
console.log(`✅ 服务 ${serviceTypeName} 已过滤类型`);
|
|
263
|
+
console.log(` 原始文件: ${originalLines} 行`);
|
|
264
|
+
console.log(` 过滤后: ${filteredLines} 行`);
|
|
265
|
+
console.log(` 减少: ${originalLines - filteredLines} 行 (${((1 - filteredLines / originalLines) * 100).toFixed(1)}%)`);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
269
|
+
const stack = error instanceof Error ? error.stack : '';
|
|
270
|
+
console.error(`过滤类型时出错: ${message}`);
|
|
271
|
+
if (stack)
|
|
272
|
+
console.error(stack);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
exports.filterUnusedTypes = filterUnusedTypes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|