@wx-sab/renkei 1.3.0 → 1.3.2
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/lib/core/config-utils.test.d.ts +1 -0
- package/lib/core/config-utils.test.js +204 -0
- package/lib/core/generate.js +17 -5
- 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 +9 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const config_utils_1 = require("./config-utils");
|
|
5
|
+
(0, vitest_1.describe)('config-utils', () => {
|
|
6
|
+
(0, vitest_1.describe)('collectConfig', () => {
|
|
7
|
+
(0, vitest_1.it)('should return empty array for null/undefined config', () => {
|
|
8
|
+
// 测试边界情况:null 和 undefined 输入
|
|
9
|
+
(0, vitest_1.expect)((0, config_utils_1.collectConfig)(null)).toEqual([]);
|
|
10
|
+
(0, vitest_1.expect)((0, config_utils_1.collectConfig)(undefined)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
(0, vitest_1.it)('should collect single config', () => {
|
|
13
|
+
const config = {
|
|
14
|
+
serviceName: 'test-service',
|
|
15
|
+
sourceType: 'apifox',
|
|
16
|
+
apifoxProjectId: '12345'
|
|
17
|
+
};
|
|
18
|
+
const result = (0, config_utils_1.collectConfig)(config);
|
|
19
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
20
|
+
(0, vitest_1.expect)(result[0].serviceName).toBe('test-service');
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)('should collect array of configs', () => {
|
|
23
|
+
const configs = [
|
|
24
|
+
{ serviceName: 'service1', sourceType: 'apifox', apifoxProjectId: '123' },
|
|
25
|
+
{ serviceName: 'service2', sourceType: 'swagger' }
|
|
26
|
+
];
|
|
27
|
+
const result = (0, config_utils_1.collectConfig)(configs);
|
|
28
|
+
(0, vitest_1.expect)(result).toHaveLength(2);
|
|
29
|
+
(0, vitest_1.expect)(result[0].serviceName).toBe('service1');
|
|
30
|
+
(0, vitest_1.expect)(result[1].serviceName).toBe('service2');
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)('should flatten nested projects', () => {
|
|
33
|
+
const config = {
|
|
34
|
+
serviceName: 'parent-service',
|
|
35
|
+
sourceType: 'apifox',
|
|
36
|
+
apifoxProjectId: '123',
|
|
37
|
+
projects: [
|
|
38
|
+
{ serviceName: 'child-service-1' },
|
|
39
|
+
{ serviceName: 'child-service-2' }
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
const result = (0, config_utils_1.collectConfig)(config);
|
|
43
|
+
(0, vitest_1.expect)(result).toHaveLength(3);
|
|
44
|
+
(0, vitest_1.expect)(result[0].serviceName).toBe('parent-service');
|
|
45
|
+
(0, vitest_1.expect)(result[1].serviceName).toBe('child-service-1');
|
|
46
|
+
(0, vitest_1.expect)(result[2].serviceName).toBe('child-service-2');
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)('should omit projects field from collected configs', () => {
|
|
49
|
+
const config = {
|
|
50
|
+
serviceName: 'parent-service',
|
|
51
|
+
sourceType: 'apifox',
|
|
52
|
+
apifoxProjectId: '123',
|
|
53
|
+
projects: [
|
|
54
|
+
{ serviceName: 'child-service' }
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
const result = (0, config_utils_1.collectConfig)(config);
|
|
58
|
+
(0, vitest_1.expect)(result[0]).not.toHaveProperty('projects');
|
|
59
|
+
(0, vitest_1.expect)(result[1]).not.toHaveProperty('projects');
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)('should merge parent config with child projects', () => {
|
|
62
|
+
const config = {
|
|
63
|
+
serviceName: 'parent-service',
|
|
64
|
+
sourceType: 'apifox',
|
|
65
|
+
apifoxProjectId: '123',
|
|
66
|
+
output: '/custom/output',
|
|
67
|
+
projects: [
|
|
68
|
+
{ serviceName: 'child-service' }
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
const result = (0, config_utils_1.collectConfig)(config);
|
|
72
|
+
(0, vitest_1.expect)(result[1].serviceName).toBe('child-service');
|
|
73
|
+
(0, vitest_1.expect)(result[1].sourceType).toBe('apifox');
|
|
74
|
+
(0, vitest_1.expect)(result[1].apifoxProjectId).toBe('123');
|
|
75
|
+
(0, vitest_1.expect)(result[1].output).toBe('/custom/output');
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('should handle deeply nested projects', () => {
|
|
78
|
+
const config = {
|
|
79
|
+
serviceName: 'root-service',
|
|
80
|
+
sourceType: 'swagger',
|
|
81
|
+
projects: [
|
|
82
|
+
{
|
|
83
|
+
serviceName: 'level-1',
|
|
84
|
+
projects: [
|
|
85
|
+
{ serviceName: 'level-2' }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
const result = (0, config_utils_1.collectConfig)(config);
|
|
91
|
+
(0, vitest_1.expect)(result).toHaveLength(3);
|
|
92
|
+
(0, vitest_1.expect)(result[0].serviceName).toBe('root-service');
|
|
93
|
+
(0, vitest_1.expect)(result[1].serviceName).toBe('level-1');
|
|
94
|
+
(0, vitest_1.expect)(result[2].serviceName).toBe('level-2');
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('should preserve existing collects array', () => {
|
|
97
|
+
const config = {
|
|
98
|
+
serviceName: 'new-service'
|
|
99
|
+
};
|
|
100
|
+
const existing = [
|
|
101
|
+
{ serviceName: 'existing-service' }
|
|
102
|
+
];
|
|
103
|
+
const result = (0, config_utils_1.collectConfig)(config, existing);
|
|
104
|
+
(0, vitest_1.expect)(result).toHaveLength(2);
|
|
105
|
+
(0, vitest_1.expect)(result[0].serviceName).toBe('existing-service');
|
|
106
|
+
(0, vitest_1.expect)(result[1].serviceName).toBe('new-service');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.describe)('applyFoldersToConfig', () => {
|
|
110
|
+
(0, vitest_1.it)('should apply folders to single config', () => {
|
|
111
|
+
const config = {
|
|
112
|
+
serviceName: 'test-service'
|
|
113
|
+
};
|
|
114
|
+
const folderSelections = new Map();
|
|
115
|
+
folderSelections.set('test-service', ['folder1', 'folder2']);
|
|
116
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
117
|
+
(0, vitest_1.expect)(config.folders).toEqual(['folder1', 'folder2']);
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)('should not modify config if no folders selected', () => {
|
|
120
|
+
const config = {
|
|
121
|
+
serviceName: 'test-service'
|
|
122
|
+
};
|
|
123
|
+
const folderSelections = new Map();
|
|
124
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
125
|
+
(0, vitest_1.expect)(config.folders).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.it)('should apply folders to array of configs', () => {
|
|
128
|
+
const configs = [
|
|
129
|
+
{ serviceName: 'service1' },
|
|
130
|
+
{ serviceName: 'service2' }
|
|
131
|
+
];
|
|
132
|
+
const folderSelections = new Map();
|
|
133
|
+
folderSelections.set('service1', ['folder1']);
|
|
134
|
+
folderSelections.set('service2', ['folder2']);
|
|
135
|
+
(0, config_utils_1.applyFoldersToConfig)(configs, folderSelections);
|
|
136
|
+
(0, vitest_1.expect)(configs[0].folders).toEqual(['folder1']);
|
|
137
|
+
(0, vitest_1.expect)(configs[1].folders).toEqual(['folder2']);
|
|
138
|
+
});
|
|
139
|
+
(0, vitest_1.it)('should apply folders to nested projects', () => {
|
|
140
|
+
const config = {
|
|
141
|
+
serviceName: 'parent-service',
|
|
142
|
+
projects: [
|
|
143
|
+
{ serviceName: 'child-service' }
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
const folderSelections = new Map();
|
|
147
|
+
folderSelections.set('parent-service', ['parent-folder']);
|
|
148
|
+
folderSelections.set('child-service', ['child-folder']);
|
|
149
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
150
|
+
(0, vitest_1.expect)(config.folders).toEqual(['parent-folder']);
|
|
151
|
+
(0, vitest_1.expect)(config.projects[0].folders).toEqual(['child-folder']);
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.it)('should use parent serviceName for projects without serviceName', () => {
|
|
154
|
+
const config = {
|
|
155
|
+
serviceName: 'parent-service',
|
|
156
|
+
projects: [
|
|
157
|
+
{}
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
const folderSelections = new Map();
|
|
161
|
+
folderSelections.set('parent-service', ['shared-folder']);
|
|
162
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
163
|
+
(0, vitest_1.expect)(config.folders).toEqual(['shared-folder']);
|
|
164
|
+
(0, vitest_1.expect)(config.projects[0].folders).toEqual(['shared-folder']);
|
|
165
|
+
});
|
|
166
|
+
(0, vitest_1.it)('should not apply empty folder selection to projects', () => {
|
|
167
|
+
const config = {
|
|
168
|
+
serviceName: 'parent-service',
|
|
169
|
+
projects: [
|
|
170
|
+
{ serviceName: 'child-service', folders: ['existing-folder'] }
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
const folderSelections = new Map();
|
|
174
|
+
folderSelections.set('child-service', []);
|
|
175
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
176
|
+
(0, vitest_1.expect)(config.projects[0].folders).toEqual(['existing-folder']);
|
|
177
|
+
});
|
|
178
|
+
(0, vitest_1.it)('should not modify config when folder selection is empty', () => {
|
|
179
|
+
const config = {
|
|
180
|
+
serviceName: 'test-service',
|
|
181
|
+
folders: ['existing-folder']
|
|
182
|
+
};
|
|
183
|
+
const folderSelections = new Map();
|
|
184
|
+
folderSelections.set('test-service', []);
|
|
185
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections);
|
|
186
|
+
(0, vitest_1.expect)(config.folders).toEqual(['existing-folder']);
|
|
187
|
+
});
|
|
188
|
+
(0, vitest_1.it)('should preserve services array parameter', () => {
|
|
189
|
+
const config = {
|
|
190
|
+
serviceName: 'test-service'
|
|
191
|
+
};
|
|
192
|
+
const folderSelections = new Map();
|
|
193
|
+
folderSelections.set('test-service', ['folder1']);
|
|
194
|
+
const services = [];
|
|
195
|
+
(0, config_utils_1.applyFoldersToConfig)(config, folderSelections, services);
|
|
196
|
+
(0, vitest_1.expect)(config.folders).toEqual(['folder1']);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
(0, vitest_1.describe)('SKIP_ROUTE_MARKER', () => {
|
|
200
|
+
(0, vitest_1.it)('should be defined as constant', () => {
|
|
201
|
+
(0, vitest_1.expect)(config_utils_1.SKIP_ROUTE_MARKER).toBe('__SKIP__');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
package/lib/core/generate.js
CHANGED
|
@@ -29,16 +29,17 @@ 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
31
|
const config_utils_1 = require("./config-utils");
|
|
32
|
+
const type_filter_1 = require("./type-filter");
|
|
32
33
|
/**
|
|
33
34
|
* 单个配置生成
|
|
34
35
|
*/
|
|
35
|
-
const generate = (conf) => {
|
|
36
|
+
const generate = (conf) => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
37
|
const config = (0, utils_1.generateConfig)(conf);
|
|
37
38
|
const modelNamespace = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.modelNamespace));
|
|
38
39
|
const serviceName = lodash_1.default.upperFirst(lodash_1.default.camelCase(config.serviceName));
|
|
39
40
|
const methodNameSet = new Set();
|
|
40
41
|
const skippedRoutes = new Set();
|
|
41
|
-
|
|
42
|
+
const result = yield (0, swagger_typescript_api_1.generateApi)({
|
|
42
43
|
url: config.source,
|
|
43
44
|
name: config.name,
|
|
44
45
|
output: path_1.default.resolve(config.output, config.serviceName),
|
|
@@ -100,6 +101,12 @@ const generate = (conf) => {
|
|
|
100
101
|
SKIP_ROUTE_MARKER: config_utils_1.SKIP_ROUTE_MARKER });
|
|
101
102
|
},
|
|
102
103
|
onParseSchema(originalSchema, parsedSchema) {
|
|
104
|
+
if (originalSchema.format === 'int64' ||
|
|
105
|
+
originalSchema.format === 'long') {
|
|
106
|
+
if (typeof parsedSchema.content === 'string') {
|
|
107
|
+
return Object.assign(Object.assign({}, parsedSchema), { content: parsedSchema.content.replace(/\bnumber\b/g, 'number | string') });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
103
110
|
if (parsedSchema.name === "any" &&
|
|
104
111
|
typeof parsedSchema.content === "string" &&
|
|
105
112
|
parsedSchema.$ref) {
|
|
@@ -118,7 +125,11 @@ const generate = (conf) => {
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
});
|
|
121
|
-
|
|
128
|
+
if (config.folders && config.folders.length > 0 && config.output) {
|
|
129
|
+
yield (0, type_filter_1.filterUnusedTypes)(config.output, config.serviceName, serviceName, modelNamespace);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
});
|
|
122
133
|
exports.generate = generate;
|
|
123
134
|
const generateApis = (config, services) => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
135
|
var _a, e_1, _b, _c;
|
|
@@ -140,10 +151,11 @@ const generateApis = (config, services) => __awaiter(void 0, void 0, void 0, fun
|
|
|
140
151
|
yield (0, exports.generate)(conf);
|
|
141
152
|
}
|
|
142
153
|
catch (e) {
|
|
143
|
-
|
|
154
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
155
|
+
console.error(message);
|
|
144
156
|
failedServices.push({
|
|
145
157
|
serviceName: conf.serviceName,
|
|
146
|
-
error:
|
|
158
|
+
error: message || '未知错误'
|
|
147
159
|
});
|
|
148
160
|
}
|
|
149
161
|
finally {
|
|
@@ -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 {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
(0, vitest_1.describe)('utils', () => {
|
|
6
|
+
(0, vitest_1.describe)('generateConfig', () => {
|
|
7
|
+
(0, vitest_1.it)('should generate config with default values', () => {
|
|
8
|
+
const conf = {
|
|
9
|
+
serviceName: 'test-service'
|
|
10
|
+
};
|
|
11
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
12
|
+
(0, vitest_1.expect)(result.sourceType).toBe('apifox');
|
|
13
|
+
(0, vitest_1.expect)(result.output).toContain('src/services');
|
|
14
|
+
(0, vitest_1.expect)(result.name).toBe('model.d.ts');
|
|
15
|
+
(0, vitest_1.expect)(result.modular).toBe(false);
|
|
16
|
+
(0, vitest_1.expect)(result.modelNamespace).toBe('Model');
|
|
17
|
+
(0, vitest_1.expect)(result.requestTempalte).toBe(`import { request } from '@wx-sab/renkei'`);
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.it)('should merge user config with default config', () => {
|
|
20
|
+
const conf = {
|
|
21
|
+
serviceName: 'test-service',
|
|
22
|
+
sourceType: 'swagger',
|
|
23
|
+
output: '/custom/output',
|
|
24
|
+
name: 'custom.d.ts',
|
|
25
|
+
modelNamespace: 'CustomModel'
|
|
26
|
+
};
|
|
27
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
28
|
+
(0, vitest_1.expect)(result.sourceType).toBe('swagger');
|
|
29
|
+
(0, vitest_1.expect)(result.output).toBe('/custom/output');
|
|
30
|
+
(0, vitest_1.expect)(result.name).toBe('custom.d.ts');
|
|
31
|
+
(0, vitest_1.expect)(result.modelNamespace).toBe('CustomModel');
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)('should generate apifox source when sourceType is apifox', () => {
|
|
34
|
+
const conf = {
|
|
35
|
+
serviceName: 'test-service',
|
|
36
|
+
sourceType: 'apifox',
|
|
37
|
+
apifoxProjectId: '12345'
|
|
38
|
+
};
|
|
39
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
40
|
+
(0, vitest_1.expect)(result.source).toContain('projectId=12345');
|
|
41
|
+
(0, vitest_1.expect)(result.source).toContain('127.0.0.1:4523');
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)('should generate swagger source when sourceType is swagger', () => {
|
|
44
|
+
const conf = {
|
|
45
|
+
serviceName: 'my-service',
|
|
46
|
+
sourceType: 'swagger'
|
|
47
|
+
};
|
|
48
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
49
|
+
(0, vitest_1.expect)(result.source).toContain('my-service');
|
|
50
|
+
(0, vitest_1.expect)(result.source).toContain('api-docs');
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)('should use custom source if provided', () => {
|
|
53
|
+
const conf = {
|
|
54
|
+
serviceName: 'test-service',
|
|
55
|
+
source: 'https://custom.api.com/swagger'
|
|
56
|
+
};
|
|
57
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
58
|
+
(0, vitest_1.expect)(result.source).toBe('https://custom.api.com/swagger');
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('should apply customResponseType function', () => {
|
|
61
|
+
const conf = {
|
|
62
|
+
serviceName: 'test-service',
|
|
63
|
+
customResponseType: (type) => `CustomResponse<${type}>`
|
|
64
|
+
};
|
|
65
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
66
|
+
(0, vitest_1.expect)(result.customResponseType).toBeDefined();
|
|
67
|
+
(0, vitest_1.expect)(result.customResponseType('User')).toBe('CustomResponse<User>');
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('should apply customMockService function', () => {
|
|
70
|
+
const conf = {
|
|
71
|
+
serviceName: 'test-service',
|
|
72
|
+
apifoxProjectId: '12345',
|
|
73
|
+
customMockService: (config) => `https://mock.example.com/${config.apifoxProjectId}`
|
|
74
|
+
};
|
|
75
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
76
|
+
(0, vitest_1.expect)(result.customMockService).toBeDefined();
|
|
77
|
+
(0, vitest_1.expect)(result.customMockService(result)).toBe('https://mock.example.com/12345');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('should use default customResponseType when not provided', () => {
|
|
80
|
+
const conf = {
|
|
81
|
+
serviceName: 'test-service'
|
|
82
|
+
};
|
|
83
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
84
|
+
(0, vitest_1.expect)(result.customResponseType).toBeDefined();
|
|
85
|
+
(0, vitest_1.expect)(result.customResponseType('User')).toBe('AxiosResponse<User>');
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)('should use default customMockService when apifoxProjectId is provided', () => {
|
|
88
|
+
const conf = {
|
|
89
|
+
serviceName: 'test-service',
|
|
90
|
+
apifoxProjectId: '12345'
|
|
91
|
+
};
|
|
92
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
93
|
+
(0, vitest_1.expect)(result.customMockService).toBeDefined();
|
|
94
|
+
(0, vitest_1.expect)(result.customMockService(result)).toContain('mock.apifox.cn');
|
|
95
|
+
(0, vitest_1.expect)(result.customMockService(result)).toContain('12345');
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)('should return empty string for default customMockService without apifoxProjectId', () => {
|
|
98
|
+
const conf = {
|
|
99
|
+
serviceName: 'test-service'
|
|
100
|
+
};
|
|
101
|
+
const result = (0, utils_1.generateConfig)(conf);
|
|
102
|
+
(0, vitest_1.expect)(result.customMockService).toBeDefined();
|
|
103
|
+
(0, vitest_1.expect)(result.customMockService(result)).toBe('');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
(0, vitest_1.describe)('generateRouteName', () => {
|
|
107
|
+
(0, vitest_1.it)('should generate route name from method and route', () => {
|
|
108
|
+
const result = (0, utils_1.generateRouteName)('GET', '/api/users');
|
|
109
|
+
(0, vitest_1.expect)(result).toBe('get_api_users');
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.it)('should handle route with path parameters', () => {
|
|
112
|
+
const result = (0, utils_1.generateRouteName)('POST', '/api/users/{id}');
|
|
113
|
+
(0, vitest_1.expect)(result).toBe('post_api_users_$p');
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.it)('should convert to lowercase', () => {
|
|
116
|
+
const result = (0, utils_1.generateRouteName)('GET', '/API/Users');
|
|
117
|
+
(0, vitest_1.expect)(result).toBe('get_api_users');
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)('should replace hyphens with underscores', () => {
|
|
120
|
+
const result = (0, utils_1.generateRouteName)('GET', '/api/user-profiles');
|
|
121
|
+
(0, vitest_1.expect)(result).toBe('get_api_user_profiles');
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.it)('should replace dots with underscores', () => {
|
|
124
|
+
const result = (0, utils_1.generateRouteName)('GET', '/api/user.profiles');
|
|
125
|
+
(0, vitest_1.expect)(result).toBe('get_api_user_profiles');
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.it)('should replace asterisks with underscores', () => {
|
|
128
|
+
const result = (0, utils_1.generateRouteName)('GET', '/api/user*profiles');
|
|
129
|
+
(0, vitest_1.expect)(result).toBe('get_api_user_profiles');
|
|
130
|
+
});
|
|
131
|
+
(0, vitest_1.it)('should handle complex route patterns', () => {
|
|
132
|
+
const result = (0, utils_1.generateRouteName)('DELETE', '/api/users/{userId}/posts/{postId}');
|
|
133
|
+
(0, vitest_1.expect)(result).toBe('delete_api_users_$p_posts_$p');
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.it)('should handle empty route segments', () => {
|
|
136
|
+
const result = (0, utils_1.generateRouteName)('GET', '/api//users');
|
|
137
|
+
(0, vitest_1.expect)(result).toBe('get_api_users');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.describe)('getValidName', () => {
|
|
141
|
+
(0, vitest_1.it)('should return null for empty string', () => {
|
|
142
|
+
const result = (0, utils_1.getValidName)('');
|
|
143
|
+
(0, vitest_1.expect)(result).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.it)('should return null for null/undefined', () => {
|
|
146
|
+
// 测试边界情况:null 和 undefined 输入
|
|
147
|
+
(0, vitest_1.expect)((0, utils_1.getValidName)(null)).toBeNull();
|
|
148
|
+
(0, vitest_1.expect)((0, utils_1.getValidName)(undefined)).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.it)('should return the same string if no special characters', () => {
|
|
151
|
+
const result = (0, utils_1.getValidName)('UserVO');
|
|
152
|
+
(0, vitest_1.expect)(result).toBe('UserVO');
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.it)('should handle nested generic types', () => {
|
|
155
|
+
const result = (0, utils_1.getValidName)('ResponseVO«AgentTraceVO»');
|
|
156
|
+
(0, vitest_1.expect)(result).toBe('ResponseVOAgentTraceVO');
|
|
157
|
+
});
|
|
158
|
+
(0, vitest_1.it)('should handle path with slashes', () => {
|
|
159
|
+
const result = (0, utils_1.getValidName)('path/to/Component');
|
|
160
|
+
(0, vitest_1.expect)(result).toBe('Component');
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)('should remove all non-word characters', () => {
|
|
163
|
+
const result = (0, utils_1.getValidName)('User@VO#Test');
|
|
164
|
+
(0, vitest_1.expect)(result).toBe('UserVOTest');
|
|
165
|
+
});
|
|
166
|
+
(0, vitest_1.it)('should handle complex nested types', () => {
|
|
167
|
+
const result = (0, utils_1.getValidName)('List«Map«String,Object»»');
|
|
168
|
+
(0, vitest_1.expect)(result).toBe('ListMapStringObject');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.describe)('defineConfig', () => {
|
|
172
|
+
(0, vitest_1.it)('should return single config', () => {
|
|
173
|
+
const config = {
|
|
174
|
+
serviceName: 'test-service',
|
|
175
|
+
sourceType: 'apifox',
|
|
176
|
+
apifoxProjectId: '12345'
|
|
177
|
+
};
|
|
178
|
+
const result = (0, utils_1.defineConfig)(config);
|
|
179
|
+
(0, vitest_1.expect)(result).toEqual(config);
|
|
180
|
+
});
|
|
181
|
+
(0, vitest_1.it)('should return array of configs', () => {
|
|
182
|
+
const configs = [
|
|
183
|
+
{ serviceName: 'service1', sourceType: 'apifox', apifoxProjectId: '123' },
|
|
184
|
+
{ serviceName: 'service2', sourceType: 'swagger' }
|
|
185
|
+
];
|
|
186
|
+
const result = (0, utils_1.defineConfig)(configs);
|
|
187
|
+
(0, vitest_1.expect)(result).toEqual(configs);
|
|
188
|
+
(0, vitest_1.expect)(Array.isArray(result)).toBe(true);
|
|
189
|
+
(0, vitest_1.expect)(result).toHaveLength(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wx-sab/renkei",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "解析swagger转为ts代码,日本动漫《游戏王》中的‘Renkei’,意为连携或协作。",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"clean": "rm -rf lib",
|
|
12
12
|
"build": "npm run clean & tsc --outDir lib",
|
|
13
13
|
"version:alpha": "npm version premajor --preid alpha",
|
|
14
|
-
"api": "ts-node --transpile-only ./src/cli.ts api"
|
|
14
|
+
"api": "ts-node --transpile-only ./src/cli.ts api",
|
|
15
|
+
"test": "vitest",
|
|
16
|
+
"test:ui": "vitest --ui",
|
|
17
|
+
"test:coverage": "vitest --coverage"
|
|
15
18
|
},
|
|
16
19
|
"repository": {
|
|
17
20
|
"type": "git",
|
|
@@ -24,7 +27,10 @@
|
|
|
24
27
|
"@types/inquirer": "9.0.7",
|
|
25
28
|
"@types/lodash": "^4.14.196",
|
|
26
29
|
"@types/node": "^20.4.7",
|
|
27
|
-
"
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
31
|
+
"@vitest/ui": "^4.1.4",
|
|
32
|
+
"typescript": "^5.1.6",
|
|
33
|
+
"vitest": "^4.1.4"
|
|
28
34
|
},
|
|
29
35
|
"files": [
|
|
30
36
|
"lib",
|