uniapp-cross-package-analyzer 1.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 +102 -0
- package/package.json +42 -0
- package/src/index.js +421 -0
- package/src/template.js +436 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# UniApp Cross Package Analyzer
|
|
2
|
+
|
|
3
|
+
UniApp 跨包依赖分析 Webpack 插件,可视化分析分包之间的依赖关系,帮助优化小程序包体积。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 基于 `pages.json` 自动识别主包和分包配置
|
|
8
|
+
- 分析所有 Vue/JS 文件的跨包依赖关系
|
|
9
|
+
- 提供 Web 可视化界面,包含:
|
|
10
|
+
- 分包视图:按分包查看依赖详情,支持折叠展开
|
|
11
|
+
- 依赖关系图:力导向图展示分包间依赖,带箭头和引用数量
|
|
12
|
+
- 依赖矩阵:热力图直观展示包间依赖强度
|
|
13
|
+
- 完整列表:支持筛选和搜索的依赖列表
|
|
14
|
+
- 支持显示/隐藏主包相关依赖
|
|
15
|
+
- 控制台输出依赖统计摘要
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install uniapp-cross-package-analyzer --save-dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 使用方法
|
|
24
|
+
|
|
25
|
+
### Vue CLI 项目 (vue.config.js)
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const UniAppCrossPackageAnalyzer = require('uniapp-cross-package-analyzer');
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
configureWebpack: {
|
|
32
|
+
plugins: [
|
|
33
|
+
new UniAppCrossPackageAnalyzer({
|
|
34
|
+
port: 8889, // Web 服务端口
|
|
35
|
+
openBrowser: true, // 自动打开浏览器
|
|
36
|
+
analyzerMode: 'server', // 'server' | 'static' | 'json' | 'disabled'
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Webpack 配置
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const UniAppCrossPackageAnalyzer = require('uniapp-cross-package-analyzer');
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
plugins: [
|
|
50
|
+
new UniAppCrossPackageAnalyzer({
|
|
51
|
+
port: 8889,
|
|
52
|
+
openBrowser: true,
|
|
53
|
+
analyzerMode: 'server',
|
|
54
|
+
})
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 配置选项
|
|
60
|
+
|
|
61
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
62
|
+
|------|------|--------|------|
|
|
63
|
+
| `port` | number | 8889 | Web 服务端口 |
|
|
64
|
+
| `openBrowser` | boolean | true | 是否自动打开浏览器 |
|
|
65
|
+
| `analyzerMode` | string | 'server' | 报告模式:'server' 启动服务、'static' 生成 HTML、'json' 生成 JSON、'disabled' 禁用 |
|
|
66
|
+
| `reportFilename` | string | 'cross-package-report.html' | 静态报告文件名 |
|
|
67
|
+
| `statsFilename` | string | 'cross-package-stats.json' | JSON 报告文件名 |
|
|
68
|
+
| `pagesJsonPath` | string | 'pages.json' | pages.json 相对路径 |
|
|
69
|
+
| `inputDir` | string | process.env.UNI_INPUT_DIR 或 'src' | 源码目录 |
|
|
70
|
+
|
|
71
|
+
## 按需启用
|
|
72
|
+
|
|
73
|
+
建议仅在需要分析时启用,避免影响日常开发:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// vue.config.js
|
|
77
|
+
const plugins = [];
|
|
78
|
+
|
|
79
|
+
if (process.env.ANALYZE === 'true') {
|
|
80
|
+
const UniAppCrossPackageAnalyzer = require('uniapp-cross-package-analyzer');
|
|
81
|
+
plugins.push(new UniAppCrossPackageAnalyzer());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
configureWebpack: { plugins }
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
运行:
|
|
90
|
+
```bash
|
|
91
|
+
ANALYZE=true npm run build:mp-weixin
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 兼容性
|
|
95
|
+
|
|
96
|
+
- Webpack 4.x / 5.x
|
|
97
|
+
- UniApp Vue2 / Vue3
|
|
98
|
+
- CLI 创建的项目 / HBuilderX 项目
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uniapp-cross-package-analyzer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "UniApp 跨包依赖分析 Webpack 插件,可视化分析分包之间的依赖关系",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"uniapp",
|
|
8
|
+
"webpack",
|
|
9
|
+
"plugin",
|
|
10
|
+
"analyzer",
|
|
11
|
+
"subpackage",
|
|
12
|
+
"dependency",
|
|
13
|
+
"miniprogram",
|
|
14
|
+
"小程序",
|
|
15
|
+
"分包",
|
|
16
|
+
"依赖分析"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": ""
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": ""
|
|
26
|
+
},
|
|
27
|
+
"homepage": "",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=12.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"webpack": "^4.0.0 || ^5.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"json5": "^2.2.3",
|
|
36
|
+
"open": "^8.4.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"src",
|
|
40
|
+
"README.md"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniApp Cross Package Dependency Analyzer
|
|
3
|
+
*
|
|
4
|
+
* 分析 UniApp 项目中各分包之间的跨包依赖关系
|
|
5
|
+
* 提供类似 BundleAnalyzerPlugin 的 Web 可视化界面
|
|
6
|
+
*
|
|
7
|
+
* @author
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const json5 = require('json5');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
|
|
15
|
+
class UniAppCrossPackageAnalyzer {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
// 配置项
|
|
18
|
+
this.port = options.port || 8889;
|
|
19
|
+
this.openBrowser = options.openBrowser !== false;
|
|
20
|
+
this.analyzerMode = options.analyzerMode || 'server'; // 'server' | 'static' | 'json' | 'disabled'
|
|
21
|
+
this.reportFilename = options.reportFilename || 'cross-package-report.html';
|
|
22
|
+
this.statsFilename = options.statsFilename || 'cross-package-stats.json';
|
|
23
|
+
this.pagesJsonPath = options.pagesJsonPath || 'pages.json'; // 相对于 inputDir
|
|
24
|
+
this.inputDir = options.inputDir || process.env.UNI_INPUT_DIR || 'src';
|
|
25
|
+
|
|
26
|
+
// 内部状态
|
|
27
|
+
this.subPackageRoots = [];
|
|
28
|
+
this.mainPackagePrefixes = [];
|
|
29
|
+
this.dependencyGraph = {
|
|
30
|
+
nodes: [],
|
|
31
|
+
links: [],
|
|
32
|
+
packages: {},
|
|
33
|
+
crossDeps: [],
|
|
34
|
+
packageDeps: {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cleanPath(filePath) {
|
|
39
|
+
if (!filePath) return filePath;
|
|
40
|
+
let cleaned = filePath.split('?')[0];
|
|
41
|
+
try { cleaned = decodeURIComponent(cleaned); } catch (e) {}
|
|
42
|
+
return cleaned;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
extractPrefix(pagePath) {
|
|
46
|
+
const parts = pagePath.split('/');
|
|
47
|
+
if (parts.length >= 2) {
|
|
48
|
+
if (parts[0] === 'shares' && parts.length >= 2) {
|
|
49
|
+
return `${parts[0]}/${parts[1]}/`;
|
|
50
|
+
}
|
|
51
|
+
return `${parts[0]}/`;
|
|
52
|
+
}
|
|
53
|
+
return pagePath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
apply(compiler) {
|
|
57
|
+
const inputPath = this.inputDir.startsWith('/') ? this.inputDir :
|
|
58
|
+
path.resolve(compiler.context || process.cwd(), this.inputDir);
|
|
59
|
+
|
|
60
|
+
// 读取 pages.json 配置
|
|
61
|
+
try {
|
|
62
|
+
const pagesJsonFullPath = path.join(inputPath, this.pagesJsonPath);
|
|
63
|
+
const pagesJson = json5.parse(fs.readFileSync(pagesJsonFullPath, 'utf8'));
|
|
64
|
+
|
|
65
|
+
this.subPackageRoots = (pagesJson.subPackages || []).map(pkg => pkg.root);
|
|
66
|
+
|
|
67
|
+
const prefixSet = new Set();
|
|
68
|
+
(pagesJson.pages || []).forEach(p => {
|
|
69
|
+
const prefix = this.extractPrefix(p.path);
|
|
70
|
+
prefixSet.add(prefix);
|
|
71
|
+
});
|
|
72
|
+
this.mainPackagePrefixes = Array.from(prefixSet);
|
|
73
|
+
|
|
74
|
+
// 初始化主包
|
|
75
|
+
this.dependencyGraph.packages['main'] = {
|
|
76
|
+
name: '主包',
|
|
77
|
+
root: 'main',
|
|
78
|
+
files: [],
|
|
79
|
+
size: 0
|
|
80
|
+
};
|
|
81
|
+
this.dependencyGraph.packageDeps['main'] = {
|
|
82
|
+
imports: [],
|
|
83
|
+
importedBy: []
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 初始化分包
|
|
87
|
+
this.subPackageRoots.forEach(root => {
|
|
88
|
+
this.dependencyGraph.packages[root] = {
|
|
89
|
+
name: root,
|
|
90
|
+
root: root,
|
|
91
|
+
files: [],
|
|
92
|
+
size: 0
|
|
93
|
+
};
|
|
94
|
+
this.dependencyGraph.packageDeps[root] = {
|
|
95
|
+
imports: [],
|
|
96
|
+
importedBy: []
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn('[UniAppCrossPackageAnalyzer] 无法读取 pages.json:', e.message);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.subPackageRoots.length === 0) {
|
|
106
|
+
console.warn('[UniAppCrossPackageAnalyzer] 未检测到分包配置');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 分析模块依赖
|
|
111
|
+
compiler.hooks.compilation.tap('UniAppCrossPackageAnalyzer', (compilation) => {
|
|
112
|
+
compilation.hooks.finishModules.tap('UniAppCrossPackageAnalyzer', (modules) => {
|
|
113
|
+
this.analyzeModules(modules, inputPath);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 构建完成后生成报告
|
|
118
|
+
compiler.hooks.done.tap('UniAppCrossPackageAnalyzer', (stats) => {
|
|
119
|
+
this.generateReport(stats, compiler.outputPath);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
analyzeModules(modules, inputPath) {
|
|
124
|
+
const nodeMap = new Map();
|
|
125
|
+
const linkSet = new Set();
|
|
126
|
+
|
|
127
|
+
// 第一遍:收集所有模块
|
|
128
|
+
for (const module of modules) {
|
|
129
|
+
if (!module.resource || !module.resource.includes(inputPath)) continue;
|
|
130
|
+
|
|
131
|
+
const rawPath = module.resource.replace(inputPath, '').replace(/\\/g, '/');
|
|
132
|
+
const modulePath = this.cleanPath(rawPath);
|
|
133
|
+
|
|
134
|
+
if (modulePath.includes('node_modules')) continue;
|
|
135
|
+
|
|
136
|
+
const packageName = this.getPackageName(modulePath);
|
|
137
|
+
const size = this.getModuleSize(module);
|
|
138
|
+
|
|
139
|
+
const node = {
|
|
140
|
+
id: modulePath,
|
|
141
|
+
name: path.basename(modulePath),
|
|
142
|
+
path: modulePath,
|
|
143
|
+
package: packageName,
|
|
144
|
+
size: size,
|
|
145
|
+
imports: [],
|
|
146
|
+
importedBy: []
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
nodeMap.set(modulePath, node);
|
|
150
|
+
|
|
151
|
+
if (this.dependencyGraph.packages[packageName]) {
|
|
152
|
+
this.dependencyGraph.packages[packageName].files.push(modulePath);
|
|
153
|
+
this.dependencyGraph.packages[packageName].size += size;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 第二遍:分析依赖关系
|
|
158
|
+
for (const module of modules) {
|
|
159
|
+
if (!module.resource || !module.resource.includes(inputPath)) continue;
|
|
160
|
+
|
|
161
|
+
const rawPath = module.resource.replace(inputPath, '').replace(/\\/g, '/');
|
|
162
|
+
const sourcePath = this.cleanPath(rawPath);
|
|
163
|
+
|
|
164
|
+
if (sourcePath.includes('node_modules')) continue;
|
|
165
|
+
|
|
166
|
+
const sourceNode = nodeMap.get(sourcePath);
|
|
167
|
+
if (!sourceNode) continue;
|
|
168
|
+
|
|
169
|
+
if (module.dependencies) {
|
|
170
|
+
for (const dep of module.dependencies) {
|
|
171
|
+
const depModule = dep.module || dep._module;
|
|
172
|
+
if (!depModule || !depModule.resource) continue;
|
|
173
|
+
|
|
174
|
+
const rawDepPath = depModule.resource.replace(inputPath, '').replace(/\\/g, '/');
|
|
175
|
+
const targetPath = this.cleanPath(rawDepPath);
|
|
176
|
+
|
|
177
|
+
if (targetPath.includes('node_modules')) continue;
|
|
178
|
+
|
|
179
|
+
const targetNode = nodeMap.get(targetPath);
|
|
180
|
+
if (!targetNode) continue;
|
|
181
|
+
|
|
182
|
+
sourceNode.imports.push(targetPath);
|
|
183
|
+
targetNode.importedBy.push(sourcePath);
|
|
184
|
+
|
|
185
|
+
const sourcePackage = this.getPackageName(sourcePath);
|
|
186
|
+
const targetPackage = this.getPackageName(targetPath);
|
|
187
|
+
|
|
188
|
+
// 跨包依赖检测
|
|
189
|
+
if (sourcePackage !== targetPackage) {
|
|
190
|
+
const linkKey = `${sourcePath}|${targetPath}`;
|
|
191
|
+
if (!linkSet.has(linkKey)) {
|
|
192
|
+
linkSet.add(linkKey);
|
|
193
|
+
|
|
194
|
+
const location = this.getImportLocation(dep);
|
|
195
|
+
const depInfo = {
|
|
196
|
+
source: sourcePath,
|
|
197
|
+
target: targetPath,
|
|
198
|
+
sourcePackage: sourcePackage,
|
|
199
|
+
targetPackage: targetPackage,
|
|
200
|
+
size: targetNode.size,
|
|
201
|
+
location: location,
|
|
202
|
+
request: dep.request || dep.userRequest || targetPath
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
this.dependencyGraph.crossDeps.push(depInfo);
|
|
206
|
+
this.dependencyGraph.links.push({
|
|
207
|
+
source: sourcePath,
|
|
208
|
+
target: targetPath,
|
|
209
|
+
sourcePackage: sourcePackage,
|
|
210
|
+
targetPackage: targetPackage,
|
|
211
|
+
value: targetNode.size
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (this.dependencyGraph.packageDeps[sourcePackage]) {
|
|
215
|
+
this.dependencyGraph.packageDeps[sourcePackage].imports.push(depInfo);
|
|
216
|
+
}
|
|
217
|
+
if (this.dependencyGraph.packageDeps[targetPackage]) {
|
|
218
|
+
this.dependencyGraph.packageDeps[targetPackage].importedBy.push(depInfo);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.dependencyGraph.nodes = Array.from(nodeMap.values());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getPackageName(modulePath) {
|
|
230
|
+
const normalizedPath = modulePath.replace(/^\//, '');
|
|
231
|
+
for (const root of this.subPackageRoots) {
|
|
232
|
+
if (normalizedPath.startsWith(root + '/') || normalizedPath === root) {
|
|
233
|
+
return root;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return 'main';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getModuleSize(module) {
|
|
240
|
+
try {
|
|
241
|
+
if (typeof module.size === 'function') return module.size();
|
|
242
|
+
if (module._source && module._source._value) {
|
|
243
|
+
return Buffer.byteLength(module._source._value, 'utf8');
|
|
244
|
+
}
|
|
245
|
+
if (module.originalSource) {
|
|
246
|
+
const source = module.originalSource();
|
|
247
|
+
if (source && source.source) {
|
|
248
|
+
return Buffer.byteLength(source.source(), 'utf8');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getImportLocation(dep) {
|
|
256
|
+
let line = '?', column = '?';
|
|
257
|
+
if (dep.loc) {
|
|
258
|
+
if (dep.loc.start) {
|
|
259
|
+
line = dep.loc.start.line;
|
|
260
|
+
column = dep.loc.start.column;
|
|
261
|
+
} else if (typeof dep.loc.line === 'number') {
|
|
262
|
+
line = dep.loc.line;
|
|
263
|
+
column = dep.loc.column || 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { line, column };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
formatSize(bytes) {
|
|
270
|
+
if (bytes === 0) return '0 B';
|
|
271
|
+
if (bytes < 1024) return bytes + ' B';
|
|
272
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
273
|
+
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
generateReport(stats, outputPath) {
|
|
277
|
+
this.printSummary();
|
|
278
|
+
|
|
279
|
+
if (this.analyzerMode === 'disabled') return;
|
|
280
|
+
|
|
281
|
+
const reportData = this.prepareReportData();
|
|
282
|
+
|
|
283
|
+
if (this.analyzerMode === 'json') {
|
|
284
|
+
const jsonPath = path.join(outputPath, this.statsFilename);
|
|
285
|
+
fs.writeFileSync(jsonPath, JSON.stringify(reportData, null, 2));
|
|
286
|
+
console.log(`[UniAppCrossPackageAnalyzer] JSON 报告已生成: ${jsonPath}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const html = this.generateHTML(reportData);
|
|
291
|
+
|
|
292
|
+
if (this.analyzerMode === 'static') {
|
|
293
|
+
const htmlPath = path.join(outputPath, this.reportFilename);
|
|
294
|
+
fs.writeFileSync(htmlPath, html);
|
|
295
|
+
console.log(`[UniAppCrossPackageAnalyzer] HTML 报告已生成: ${htmlPath}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.startServer(html);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
prepareReportData() {
|
|
303
|
+
const crossDepsByPackage = {};
|
|
304
|
+
|
|
305
|
+
for (const dep of this.dependencyGraph.crossDeps) {
|
|
306
|
+
const key = `${dep.sourcePackage} -> ${dep.targetPackage}`;
|
|
307
|
+
if (!crossDepsByPackage[key]) {
|
|
308
|
+
crossDepsByPackage[key] = {
|
|
309
|
+
sourcePackage: dep.sourcePackage,
|
|
310
|
+
targetPackage: dep.targetPackage,
|
|
311
|
+
count: 0,
|
|
312
|
+
totalSize: 0,
|
|
313
|
+
deps: []
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
crossDepsByPackage[key].count++;
|
|
317
|
+
crossDepsByPackage[key].totalSize += dep.size;
|
|
318
|
+
crossDepsByPackage[key].deps.push(dep);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const allPackages = Object.keys(this.dependencyGraph.packages);
|
|
322
|
+
const subPackageOnly = allPackages.filter(p => p !== 'main');
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
packages: this.dependencyGraph.packages,
|
|
326
|
+
allPackages: allPackages,
|
|
327
|
+
subPackages: subPackageOnly,
|
|
328
|
+
packageDeps: this.dependencyGraph.packageDeps,
|
|
329
|
+
crossDeps: this.dependencyGraph.crossDeps,
|
|
330
|
+
crossDepsByPackage: Object.values(crossDepsByPackage),
|
|
331
|
+
links: this.dependencyGraph.links,
|
|
332
|
+
nodes: this.dependencyGraph.nodes,
|
|
333
|
+
summary: {
|
|
334
|
+
totalPackages: allPackages.length,
|
|
335
|
+
totalSubPackages: subPackageOnly.length,
|
|
336
|
+
totalCrossDeps: this.dependencyGraph.crossDeps.length,
|
|
337
|
+
totalCrossDepSize: this.dependencyGraph.crossDeps.reduce((sum, d) => sum + d.size, 0)
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
printSummary() {
|
|
343
|
+
console.log('\n' + '='.repeat(80));
|
|
344
|
+
console.log('[UniAppCrossPackageAnalyzer] 跨包依赖分析结果');
|
|
345
|
+
console.log('='.repeat(80));
|
|
346
|
+
|
|
347
|
+
const crossDeps = this.dependencyGraph.crossDeps;
|
|
348
|
+
|
|
349
|
+
if (crossDeps.length === 0) {
|
|
350
|
+
console.log('未检测到跨包依赖');
|
|
351
|
+
console.log('='.repeat(80) + '\n');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const groupedByRoute = {};
|
|
356
|
+
for (const dep of crossDeps) {
|
|
357
|
+
const key = `${dep.sourcePackage} -> ${dep.targetPackage}`;
|
|
358
|
+
if (!groupedByRoute[key]) {
|
|
359
|
+
groupedByRoute[key] = { count: 0, size: 0, files: new Set() };
|
|
360
|
+
}
|
|
361
|
+
groupedByRoute[key].count++;
|
|
362
|
+
groupedByRoute[key].size += dep.size;
|
|
363
|
+
groupedByRoute[key].files.add(dep.target);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const totalSize = crossDeps.reduce((sum, d) => sum + d.size, 0);
|
|
367
|
+
|
|
368
|
+
console.log(`共检测到 ${crossDeps.length} 处跨包依赖`);
|
|
369
|
+
console.log(`预估影响体积: ${this.formatSize(totalSize)}\n`);
|
|
370
|
+
|
|
371
|
+
console.log('--- 跨包依赖路径统计 ---');
|
|
372
|
+
const sortedRoutes = Object.entries(groupedByRoute)
|
|
373
|
+
.sort((a, b) => b[1].size - a[1].size);
|
|
374
|
+
|
|
375
|
+
for (const [route, stats] of sortedRoutes) {
|
|
376
|
+
console.log(` ${route}: ${stats.count} 处引用, ${stats.files.size} 个文件, ${this.formatSize(stats.size)}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log('\n' + '='.repeat(80) + '\n');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
startServer(html) {
|
|
383
|
+
const server = http.createServer((req, res) => {
|
|
384
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
385
|
+
res.end(html);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
server.listen(this.port, () => {
|
|
389
|
+
const url = `http://localhost:${this.port}`;
|
|
390
|
+
console.log(`[UniAppCrossPackageAnalyzer] 可视化报告服务已启动: ${url}`);
|
|
391
|
+
|
|
392
|
+
if (this.openBrowser) {
|
|
393
|
+
try {
|
|
394
|
+
const open = require('open');
|
|
395
|
+
open(url).catch(() => {
|
|
396
|
+
console.log(`请手动打开浏览器访问: ${url}`);
|
|
397
|
+
});
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log(`请手动打开浏览器访问: ${url}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
server.on('error', (err) => {
|
|
405
|
+
if (err.code === 'EADDRINUSE') {
|
|
406
|
+
console.log(`[UniAppCrossPackageAnalyzer] 端口 ${this.port} 已被占用,尝试端口 ${this.port + 1}`);
|
|
407
|
+
this.port++;
|
|
408
|
+
this.startServer(html);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
generateHTML(data) {
|
|
415
|
+
const htmlTemplate = require('./template');
|
|
416
|
+
return htmlTemplate(data, this.formatSize.bind(this));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = UniAppCrossPackageAnalyzer;
|
|
421
|
+
module.exports.UniAppCrossPackageAnalyzer = UniAppCrossPackageAnalyzer;
|
package/src/template.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML 报告模板
|
|
3
|
+
*/
|
|
4
|
+
module.exports = function generateHTML(data, formatSize) {
|
|
5
|
+
return `<!DOCTYPE html>
|
|
6
|
+
<html lang="zh-CN">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>UniApp 分包跨包依赖分析</title>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
|
|
15
|
+
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px 30px; }
|
|
16
|
+
.header h1 { font-size: 24px; font-weight: 600; }
|
|
17
|
+
.header p { opacity: 0.9; margin-top: 5px; font-size: 14px; }
|
|
18
|
+
.container { padding: 20px 30px; }
|
|
19
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
20
|
+
.summary-card { background: linear-gradient(145deg, #16213e, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid #333; }
|
|
21
|
+
.summary-card .label { color: #888; font-size: 13px; margin-bottom: 8px; }
|
|
22
|
+
.summary-card .value { font-size: 28px; font-weight: 700; color: #667eea; }
|
|
23
|
+
.tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px; flex-wrap: wrap; }
|
|
24
|
+
.tab { padding: 10px 20px; background: transparent; border: none; color: #888; cursor: pointer; font-size: 14px; border-radius: 8px; transition: all 0.3s; }
|
|
25
|
+
.tab:hover { background: #333; color: #fff; }
|
|
26
|
+
.tab.active { background: #667eea; color: #fff; }
|
|
27
|
+
.panel { display: none; }
|
|
28
|
+
.panel.active { display: block; }
|
|
29
|
+
.chart-container { background: #16213e; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid #333; }
|
|
30
|
+
.chart { height: 500px; }
|
|
31
|
+
.table-container { background: #16213e; border-radius: 12px; overflow: hidden; border: 1px solid #333; max-height: 600px; overflow-y: auto; }
|
|
32
|
+
table { width: 100%; border-collapse: collapse; }
|
|
33
|
+
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #333; }
|
|
34
|
+
th { background: #1a1a2e; color: #888; font-weight: 500; font-size: 13px; position: sticky; top: 0; z-index: 1; }
|
|
35
|
+
tr:hover { background: rgba(102, 126, 234, 0.1); }
|
|
36
|
+
.tag { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 12px; background: rgba(102, 126, 234, 0.2); color: #667eea; }
|
|
37
|
+
.tag.main { background: rgba(76, 175, 80, 0.2); color: #4caf50; }
|
|
38
|
+
.size { color: #f39c12; font-weight: 500; }
|
|
39
|
+
.path { color: #aaa; font-size: 12px; word-break: break-all; }
|
|
40
|
+
.filter-bar { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
|
|
41
|
+
.filter-bar select, .filter-bar input { padding: 10px 15px; border-radius: 8px; border: 1px solid #333; background: #16213e; color: #fff; font-size: 14px; min-width: 200px; }
|
|
42
|
+
.filter-bar select:focus, .filter-bar input:focus { outline: none; border-color: #667eea; }
|
|
43
|
+
.filter-bar label { color: #888; font-size: 13px; }
|
|
44
|
+
.package-selector { background: #16213e; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid #333; }
|
|
45
|
+
.package-selector h3 { margin-bottom: 15px; font-size: 16px; color: #667eea; }
|
|
46
|
+
.package-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
|
|
47
|
+
.package-item { padding: 12px 15px; background: #1a1a2e; border-radius: 8px; cursor: pointer; border: 2px solid transparent; transition: all 0.3s; }
|
|
48
|
+
.package-item:hover { border-color: #667eea; }
|
|
49
|
+
.package-item.selected { border-color: #667eea; background: rgba(102, 126, 234, 0.1); }
|
|
50
|
+
.package-item.main-pkg { border-color: #4caf50; background: rgba(76, 175, 80, 0.1); }
|
|
51
|
+
.package-item.main-pkg .name { color: #4caf50; }
|
|
52
|
+
.package-item .name { font-size: 13px; font-weight: 500; margin-bottom: 5px; word-break: break-all; }
|
|
53
|
+
.package-item .stats { font-size: 11px; color: #888; }
|
|
54
|
+
.dep-direction { display: flex; gap: 10px; margin-bottom: 15px; }
|
|
55
|
+
.dep-direction button { padding: 8px 16px; border-radius: 6px; border: 1px solid #333; background: #1a1a2e; color: #888; cursor: pointer; }
|
|
56
|
+
.dep-direction button.active { background: #667eea; color: #fff; border-color: #667eea; }
|
|
57
|
+
.file-tree { background: #0f0f1a; border-radius: 8px; padding: 15px; margin-top: 15px; }
|
|
58
|
+
.empty-state { text-align: center; padding: 40px; color: #666; }
|
|
59
|
+
.badge { background: #e74c3c; color: #fff; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-left: 5px; }
|
|
60
|
+
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
|
|
61
|
+
.switch input { opacity: 0; width: 0; height: 0; }
|
|
62
|
+
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #333; transition: .3s; border-radius: 24px; }
|
|
63
|
+
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: #888; transition: .3s; border-radius: 50%; }
|
|
64
|
+
input:checked + .slider { background-color: #667eea; }
|
|
65
|
+
input:checked + .slider:before { transform: translateX(20px); background-color: #fff; }
|
|
66
|
+
</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body>
|
|
69
|
+
<div class="header">
|
|
70
|
+
<h1>UniApp 分包跨包依赖分析</h1>
|
|
71
|
+
<p>分析各分包之间的相互依赖关系,帮助定位和优化跨包调用</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="container">
|
|
75
|
+
<div class="summary">
|
|
76
|
+
<div class="summary-card">
|
|
77
|
+
<div class="label">包总数 (含主包)</div>
|
|
78
|
+
<div class="value">${data.summary.totalPackages}</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="summary-card">
|
|
81
|
+
<div class="label">跨包依赖数</div>
|
|
82
|
+
<div class="value">${data.summary.totalCrossDeps}</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="summary-card">
|
|
85
|
+
<div class="label">跨包依赖体积</div>
|
|
86
|
+
<div class="value">${formatSize(data.summary.totalCrossDepSize)}</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="summary-card">
|
|
89
|
+
<div class="label" style="display: flex; align-items: center; gap: 10px;">
|
|
90
|
+
显示主包
|
|
91
|
+
<label class="switch">
|
|
92
|
+
<input type="checkbox" id="showMainToggle" checked>
|
|
93
|
+
<span class="slider"></span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="value" style="font-size: 14px; color: #888;" id="mainToggleHint">已显示主包相关依赖</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="tabs">
|
|
101
|
+
<button class="tab active" data-panel="packageView">分包视图</button>
|
|
102
|
+
<button class="tab" data-panel="graph">依赖关系图</button>
|
|
103
|
+
<button class="tab" data-panel="matrix">依赖矩阵</button>
|
|
104
|
+
<button class="tab" data-panel="list">完整列表</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div id="packageView" class="panel active">
|
|
108
|
+
<div class="package-selector">
|
|
109
|
+
<h3>选择包查看跨包依赖详情</h3>
|
|
110
|
+
<div class="package-grid" id="packageGrid"></div>
|
|
111
|
+
</div>
|
|
112
|
+
<div id="packageDetail" style="display: none;">
|
|
113
|
+
<div class="dep-direction">
|
|
114
|
+
<button class="active" data-dir="imports">该分包引用的其他分包</button>
|
|
115
|
+
<button data-dir="importedBy">被其他分包引用</button>
|
|
116
|
+
</div>
|
|
117
|
+
<div id="packageDepContent"></div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div id="graph" class="panel">
|
|
122
|
+
<div style="background: #16213e; border-radius: 12px; padding: 15px 20px; margin-bottom: 15px; border: 1px solid #333; display: flex; gap: 30px; flex-wrap: wrap; align-items: center;">
|
|
123
|
+
<div style="font-size: 13px; color: #888;"><span style="color: #667eea;">节点大小</span> = 跨包依赖数量</div>
|
|
124
|
+
<div style="font-size: 13px; color: #888;"><span style="color: #f39c12;">连线数字</span> = 引用次数</div>
|
|
125
|
+
<div style="font-size: 13px; color: #888;"><span style="color: #667eea;">箭头方向</span> = 依赖方向</div>
|
|
126
|
+
<div style="font-size: 13px; color: #4caf50;">点击节点查看详情</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="chart-container">
|
|
129
|
+
<div id="graphChart" class="chart" style="height: 600px;"></div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div id="matrix" class="panel">
|
|
134
|
+
<div class="chart-container">
|
|
135
|
+
<div id="matrixChart" class="chart" style="height: 600px;"></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div id="list" class="panel">
|
|
140
|
+
<div class="filter-bar">
|
|
141
|
+
<div>
|
|
142
|
+
<label>源包</label>
|
|
143
|
+
<select id="sourceFilter">
|
|
144
|
+
<option value="">全部</option>
|
|
145
|
+
<option value="main">主包</option>
|
|
146
|
+
${data.subPackages.map(p => `<option value="${p}">${p}</option>`).join('')}
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
<div>
|
|
150
|
+
<label>目标包</label>
|
|
151
|
+
<select id="targetFilter">
|
|
152
|
+
<option value="">全部</option>
|
|
153
|
+
<option value="main">主包</option>
|
|
154
|
+
${data.subPackages.map(p => `<option value="${p}">${p}</option>`).join('')}
|
|
155
|
+
</select>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<label>搜索文件</label>
|
|
159
|
+
<input type="text" id="searchInput" placeholder="输入文件路径关键词...">
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="table-container">
|
|
163
|
+
<table>
|
|
164
|
+
<thead>
|
|
165
|
+
<tr><th>源文件</th><th>源分包</th><th>目标文件</th><th>目标分包</th><th>体积</th><th>行号</th></tr>
|
|
166
|
+
</thead>
|
|
167
|
+
<tbody id="depTableBody"></tbody>
|
|
168
|
+
</table>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<script>
|
|
174
|
+
const reportData = ${JSON.stringify(data)};
|
|
175
|
+
let selectedPackage = null;
|
|
176
|
+
let depDirection = 'imports';
|
|
177
|
+
let showMain = true;
|
|
178
|
+
|
|
179
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
180
|
+
tab.addEventListener('click', () => {
|
|
181
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
182
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
183
|
+
tab.classList.add('active');
|
|
184
|
+
document.getElementById(tab.dataset.panel).classList.add('active');
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
if (tab.dataset.panel === 'graph') renderGraph();
|
|
187
|
+
if (tab.dataset.panel === 'matrix') renderMatrix();
|
|
188
|
+
}, 100);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function getDisplayPackages() { return showMain ? reportData.allPackages : reportData.subPackages; }
|
|
193
|
+
function filterDeps(deps) { return showMain ? deps : deps.filter(d => d.sourcePackage !== 'main' && d.targetPackage !== 'main'); }
|
|
194
|
+
|
|
195
|
+
function initPackageGrid() {
|
|
196
|
+
const grid = document.getElementById('packageGrid');
|
|
197
|
+
const packages = getDisplayPackages();
|
|
198
|
+
grid.innerHTML = packages.map(pkg => {
|
|
199
|
+
const deps = reportData.packageDeps[pkg] || { imports: [], importedBy: [] };
|
|
200
|
+
const importsCount = filterDeps(deps.imports).length;
|
|
201
|
+
const importedByCount = filterDeps(deps.importedBy).length;
|
|
202
|
+
const isMain = pkg === 'main';
|
|
203
|
+
return '<div class="package-item ' + (isMain ? 'main-pkg' : '') + '" data-pkg="' + pkg + '">' +
|
|
204
|
+
'<div class="name">' + (isMain ? '主包 (main)' : pkg) + '</div>' +
|
|
205
|
+
'<div class="stats">引用: ' + importsCount + ' | 被引用: ' + importedByCount + '</div></div>';
|
|
206
|
+
}).join('');
|
|
207
|
+
grid.querySelectorAll('.package-item').forEach(item => {
|
|
208
|
+
item.addEventListener('click', () => {
|
|
209
|
+
grid.querySelectorAll('.package-item').forEach(i => i.classList.remove('selected'));
|
|
210
|
+
item.classList.add('selected');
|
|
211
|
+
selectedPackage = item.dataset.pkg;
|
|
212
|
+
document.getElementById('packageDetail').style.display = 'block';
|
|
213
|
+
renderPackageDeps();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
document.getElementById('showMainToggle').addEventListener('change', function() {
|
|
219
|
+
showMain = this.checked;
|
|
220
|
+
document.getElementById('mainToggleHint').textContent = showMain ? '已显示主包相关依赖' : '已隐藏主包相关依赖';
|
|
221
|
+
if (!showMain && selectedPackage === 'main') {
|
|
222
|
+
selectedPackage = null;
|
|
223
|
+
document.getElementById('packageDetail').style.display = 'none';
|
|
224
|
+
}
|
|
225
|
+
initPackageGrid();
|
|
226
|
+
if (selectedPackage) {
|
|
227
|
+
const pkgItem = document.querySelector('[data-pkg="' + selectedPackage + '"]');
|
|
228
|
+
if (pkgItem) pkgItem.classList.add('selected');
|
|
229
|
+
renderPackageDeps();
|
|
230
|
+
}
|
|
231
|
+
renderDepList();
|
|
232
|
+
const activePanel = document.querySelector('.panel.active');
|
|
233
|
+
if (activePanel && activePanel.id === 'graph') renderGraph();
|
|
234
|
+
if (activePanel && activePanel.id === 'matrix') renderMatrix();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
function renderPackageDeps() {
|
|
239
|
+
const contentDiv = document.getElementById('packageDepContent');
|
|
240
|
+
const deps = reportData.packageDeps[selectedPackage] || { imports: [], importedBy: [] };
|
|
241
|
+
const rawDeps = depDirection === 'imports' ? deps.imports : deps.importedBy;
|
|
242
|
+
const currentDeps = filterDeps(rawDeps);
|
|
243
|
+
|
|
244
|
+
if (currentDeps.length === 0) {
|
|
245
|
+
contentDiv.innerHTML = '<div class="empty-state">' + (selectedPackage === 'main' ? '主包' : '该分包') + '没有' + (depDirection === 'imports' ? '引用其他包' : '被其他包引用') + '</div>';
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const grouped = {};
|
|
250
|
+
currentDeps.forEach(dep => {
|
|
251
|
+
const key = depDirection === 'imports' ? dep.targetPackage : dep.sourcePackage;
|
|
252
|
+
if (!grouped[key]) grouped[key] = { deps: [], totalSize: 0 };
|
|
253
|
+
grouped[key].deps.push(dep);
|
|
254
|
+
grouped[key].totalSize += dep.size;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
let html = '', idx = 0;
|
|
258
|
+
Object.entries(grouped).sort((a, b) => b[1].totalSize - a[1].totalSize).forEach(([pkg, info]) => {
|
|
259
|
+
const collapseId = 'collapse-' + idx++;
|
|
260
|
+
const isMain = pkg === 'main';
|
|
261
|
+
const bySource = {};
|
|
262
|
+
info.deps.forEach(dep => {
|
|
263
|
+
const sourceFile = depDirection === 'imports' ? dep.source : dep.target;
|
|
264
|
+
if (!bySource[sourceFile]) bySource[sourceFile] = [];
|
|
265
|
+
bySource[sourceFile].push(dep);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
html += '<div class="chart-container" style="margin-bottom: 15px;">' +
|
|
269
|
+
'<div class="collapse-header" data-target="' + collapseId + '" style="cursor: pointer; display: flex; align-items: center; justify-content: space-between;">' +
|
|
270
|
+
'<div><span class="collapse-icon" style="display: inline-block; width: 20px;">▶</span>' +
|
|
271
|
+
'<span class="tag ' + (isMain ? 'main' : '') + '">' + (isMain ? '主包' : pkg) + '</span>' +
|
|
272
|
+
'<span class="badge">' + info.deps.length + ' 处依赖</span>' +
|
|
273
|
+
'<span style="color: #888; font-size: 12px; margin-left: 8px;">' + Object.keys(bySource).length + ' 个文件</span>' +
|
|
274
|
+
'<span class="size" style="margin-left: 10px;">' + formatSize(info.totalSize) + '</span></div>' +
|
|
275
|
+
'<span style="color: #667eea; font-size: 12px;">点击展开</span></div>' +
|
|
276
|
+
'<div class="collapse-content" id="' + collapseId + '" style="display: none; margin-top: 15px;"><div class="file-tree">';
|
|
277
|
+
|
|
278
|
+
Object.entries(bySource).forEach(([file, fileDeps]) => {
|
|
279
|
+
const fileCollapseId = collapseId + '-' + Math.random().toString(36).substr(2, 9);
|
|
280
|
+
html += '<div style="border-bottom: 1px dashed #333; padding: 10px 0;">' +
|
|
281
|
+
'<div class="file-header" data-target="' + fileCollapseId + '" style="cursor: pointer;">' +
|
|
282
|
+
'<span class="file-collapse-icon" style="display: inline-block; width: 16px; font-size: 10px;">▶</span>' +
|
|
283
|
+
'<span style="color: #aaa; font-size: 12px;">' + file + '</span>' +
|
|
284
|
+
'<span class="badge" style="background: #667eea;">' + fileDeps.length + '</span></div>' +
|
|
285
|
+
'<div id="' + fileCollapseId + '" style="display: none; margin-top: 8px; margin-left: 20px; font-size: 11px; color: #667eea;">';
|
|
286
|
+
fileDeps.forEach(dep => {
|
|
287
|
+
const targetFile = depDirection === 'imports' ? dep.target : dep.source;
|
|
288
|
+
html += '<div style="padding: 4px 0;">→ ' + targetFile + ' <span class="size">(' + formatSize(dep.size) + ')</span> L' + dep.location.line + '</div>';
|
|
289
|
+
});
|
|
290
|
+
html += '</div></div>';
|
|
291
|
+
});
|
|
292
|
+
html += '</div></div></div>';
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
contentDiv.innerHTML = html;
|
|
296
|
+
|
|
297
|
+
contentDiv.querySelectorAll('.collapse-header').forEach(header => {
|
|
298
|
+
header.addEventListener('click', function() {
|
|
299
|
+
const content = document.getElementById(this.dataset.target);
|
|
300
|
+
const icon = this.querySelector('.collapse-icon');
|
|
301
|
+
const hint = this.querySelector('span:last-child');
|
|
302
|
+
if (content.style.display === 'none') {
|
|
303
|
+
content.style.display = 'block';
|
|
304
|
+
icon.textContent = '▼';
|
|
305
|
+
hint.textContent = '点击收起';
|
|
306
|
+
} else {
|
|
307
|
+
content.style.display = 'none';
|
|
308
|
+
icon.textContent = '▶';
|
|
309
|
+
hint.textContent = '点击展开';
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
contentDiv.querySelectorAll('.file-header').forEach(header => {
|
|
315
|
+
header.addEventListener('click', function(e) {
|
|
316
|
+
e.stopPropagation();
|
|
317
|
+
const content = document.getElementById(this.dataset.target);
|
|
318
|
+
const icon = this.querySelector('.file-collapse-icon');
|
|
319
|
+
if (content.style.display === 'none') {
|
|
320
|
+
content.style.display = 'block';
|
|
321
|
+
icon.textContent = '▼';
|
|
322
|
+
} else {
|
|
323
|
+
content.style.display = 'none';
|
|
324
|
+
icon.textContent = '▶';
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
document.querySelectorAll('.dep-direction button').forEach(btn => {
|
|
331
|
+
btn.addEventListener('click', () => {
|
|
332
|
+
document.querySelectorAll('.dep-direction button').forEach(b => b.classList.remove('active'));
|
|
333
|
+
btn.classList.add('active');
|
|
334
|
+
depDirection = btn.dataset.dir;
|
|
335
|
+
if (selectedPackage) renderPackageDeps();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
function renderGraph() {
|
|
340
|
+
const chart = echarts.init(document.getElementById('graphChart'));
|
|
341
|
+
const filteredCrossDeps = filterDeps(reportData.crossDeps);
|
|
342
|
+
const displayPackages = getDisplayPackages();
|
|
343
|
+
|
|
344
|
+
const pkgStats = {};
|
|
345
|
+
displayPackages.forEach(pkg => { pkgStats[pkg] = { imports: 0, importedBy: 0, importFiles: new Set(), importedByFiles: new Set() }; });
|
|
346
|
+
filteredCrossDeps.forEach(dep => {
|
|
347
|
+
if (pkgStats[dep.sourcePackage]) { pkgStats[dep.sourcePackage].imports++; pkgStats[dep.sourcePackage].importFiles.add(dep.target); }
|
|
348
|
+
if (pkgStats[dep.targetPackage]) { pkgStats[dep.targetPackage].importedBy++; pkgStats[dep.targetPackage].importedByFiles.add(dep.source); }
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const packageNodes = displayPackages.map((pkg, idx) => {
|
|
352
|
+
const stats = pkgStats[pkg] || { imports: 0, importedBy: 0, importFiles: new Set(), importedByFiles: new Set() };
|
|
353
|
+
const isMain = pkg === 'main';
|
|
354
|
+
return { id: pkg, name: isMain ? '主包' : pkg, symbolSize: Math.max(40, Math.min(90, 40 + (stats.imports + stats.importedBy) * 2)), category: idx, value: reportData.packages[pkg]?.size || 0, imports: stats.imports, importedBy: stats.importedBy, importFiles: stats.importFiles.size, importedByFiles: stats.importedByFiles.size, itemStyle: isMain ? { color: '#4caf50' } : undefined };
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const linkMap = {};
|
|
358
|
+
filteredCrossDeps.forEach(dep => {
|
|
359
|
+
const key = dep.sourcePackage + '|' + dep.targetPackage;
|
|
360
|
+
if (!linkMap[key]) linkMap[key] = { source: dep.sourcePackage, target: dep.targetPackage, value: 0, count: 0, files: [] };
|
|
361
|
+
linkMap[key].value += dep.size;
|
|
362
|
+
linkMap[key].count++;
|
|
363
|
+
linkMap[key].files.push({ source: dep.source, target: dep.target, size: dep.size, line: dep.location.line });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const links = Object.values(linkMap).map(l => ({ source: l.source, target: l.target, value: l.value, count: l.count, files: l.files, lineStyle: { width: Math.max(2, Math.min(12, l.count)) }, label: { show: true, formatter: l.count + '', fontSize: 11, color: '#f39c12', backgroundColor: 'rgba(0,0,0,0.6)', padding: [2, 6], borderRadius: 4 } }));
|
|
367
|
+
|
|
368
|
+
chart.setOption({
|
|
369
|
+
tooltip: { trigger: 'item', enterable: true, confine: true, extraCssText: 'max-width: 400px; max-height: 300px; overflow: auto;', formatter: params => {
|
|
370
|
+
if (params.dataType === 'node') { const d = params.data; return '<b>' + d.name + '</b><br/>引用其他包: ' + d.imports + ' 处<br/>被其他包引用: ' + d.importedBy + ' 处<br/>体积: ' + formatSize(d.value); }
|
|
371
|
+
const d = params.data; let html = '<b>' + d.source + ' → ' + d.target + '</b><br/>引用数: ' + d.count + ' 处<br/>总体积: ' + formatSize(d.value) + '<br/><div style="margin-top:8px;border-top:1px solid #444;padding-top:8px;max-height:150px;overflow:auto;">';
|
|
372
|
+
d.files.slice(0, 10).forEach(f => { html += '<div style="font-size:11px;margin:4px 0;color:#aaa;">' + f.source.split('/').pop() + ' → ' + f.target.split('/').pop() + ' (' + formatSize(f.size) + ') L' + f.line + '</div>'; });
|
|
373
|
+
if (d.files.length > 10) html += '<div style="color:#888;font-size:11px;">...还有 ' + (d.files.length - 10) + ' 处</div>';
|
|
374
|
+
return html + '</div>';
|
|
375
|
+
}},
|
|
376
|
+
legend: { data: displayPackages.map(p => p === 'main' ? '主包' : p), textStyle: { color: '#888' }, top: 10, type: 'scroll' },
|
|
377
|
+
series: [{ type: 'graph', layout: 'force', data: packageNodes, links: links, categories: displayPackages.map(p => ({ name: p === 'main' ? '主包' : p })), roam: true, label: { show: true, position: 'bottom', color: '#fff', fontSize: 10, formatter: params => params.data.name + '\\n[' + params.data.imports + '/' + params.data.importedBy + ']' }, force: { repulsion: 1000, edgeLength: [180, 450], gravity: 0.1 }, lineStyle: { color: 'source', curveness: 0.2 }, edgeSymbol: ['none', 'arrow'], edgeSymbolSize: [0, 12], emphasis: { focus: 'adjacency', lineStyle: { width: 8 } } }]
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
chart.on('click', params => {
|
|
381
|
+
if (params.dataType === 'node') {
|
|
382
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
383
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
384
|
+
document.querySelector('[data-panel="packageView"]').classList.add('active');
|
|
385
|
+
document.getElementById('packageView').classList.add('active');
|
|
386
|
+
const pkgItem = document.querySelector('[data-pkg="' + params.data.id + '"]');
|
|
387
|
+
if (pkgItem) { document.querySelectorAll('.package-item').forEach(i => i.classList.remove('selected')); pkgItem.classList.add('selected'); selectedPackage = params.data.id; document.getElementById('packageDetail').style.display = 'block'; renderPackageDeps(); }
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
window.addEventListener('resize', () => chart.resize());
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renderMatrix() {
|
|
394
|
+
const chart = echarts.init(document.getElementById('matrixChart'));
|
|
395
|
+
const packages = getDisplayPackages();
|
|
396
|
+
const filteredCrossDeps = filterDeps(reportData.crossDeps);
|
|
397
|
+
const matrix = packages.map(() => packages.map(() => 0));
|
|
398
|
+
filteredCrossDeps.forEach(dep => { const si = packages.indexOf(dep.sourcePackage), ti = packages.indexOf(dep.targetPackage); if (si >= 0 && ti >= 0) matrix[si][ti] += dep.size; });
|
|
399
|
+
const data = []; let maxValue = 0;
|
|
400
|
+
matrix.forEach((row, i) => { row.forEach((val, j) => { data.push([i, j, val]); if (val > 0) maxValue = Math.max(maxValue, val); }); });
|
|
401
|
+
chart.setOption({
|
|
402
|
+
tooltip: { formatter: params => { const [s, t, v] = params.data; return packages[s] + ' → ' + packages[t] + '<br/>' + (v === 0 ? '无依赖' : '依赖体积: ' + formatSize(v)); } },
|
|
403
|
+
grid: { left: 180, right: 50, top: 50, bottom: 120 },
|
|
404
|
+
xAxis: { type: 'category', data: packages, axisLabel: { rotate: 45, color: '#888', fontSize: 10 }, name: '目标包', nameLocation: 'middle', nameGap: 80 },
|
|
405
|
+
yAxis: { type: 'category', data: packages, axisLabel: { color: '#888', fontSize: 10 }, name: '源包', nameLocation: 'middle', nameGap: 140 },
|
|
406
|
+
visualMap: { min: 0, max: maxValue || 1, calculable: true, orient: 'horizontal', left: 'center', bottom: 10, inRange: { color: ['#1a1a2e', '#667eea', '#f39c12', '#e74c3c'] }, textStyle: { color: '#888' } },
|
|
407
|
+
series: [{ type: 'heatmap', data: data, label: { show: true, formatter: p => p.data[2] > 0 ? formatSize(p.data[2]) : '', fontSize: 9, color: '#fff' }, emphasis: { itemStyle: { shadowBlur: 10 } } }]
|
|
408
|
+
});
|
|
409
|
+
window.addEventListener('resize', () => chart.resize());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderDepList(filter = {}) {
|
|
413
|
+
const tbody = document.getElementById('depTableBody');
|
|
414
|
+
let deps = filterDeps(reportData.crossDeps);
|
|
415
|
+
if (filter.source) deps = deps.filter(d => d.sourcePackage === filter.source);
|
|
416
|
+
if (filter.target) deps = deps.filter(d => d.targetPackage === filter.target);
|
|
417
|
+
if (filter.search) { const s = filter.search.toLowerCase(); deps = deps.filter(d => d.source.toLowerCase().includes(s) || d.target.toLowerCase().includes(s)); }
|
|
418
|
+
tbody.innerHTML = deps.map(dep => {
|
|
419
|
+
const isMainS = dep.sourcePackage === 'main', isMainT = dep.targetPackage === 'main';
|
|
420
|
+
return '<tr><td class="path">' + dep.source + '</td><td><span class="tag ' + (isMainS ? 'main' : '') + '">' + (isMainS ? '主包' : dep.sourcePackage) + '</span></td><td class="path">' + dep.target + '</td><td><span class="tag ' + (isMainT ? 'main' : '') + '">' + (isMainT ? '主包' : dep.targetPackage) + '</span></td><td class="size">' + formatSize(dep.size) + '</td><td>L' + dep.location.line + '</td></tr>';
|
|
421
|
+
}).join('') || '<tr><td colspan="6" style="text-align: center; color: #666;">无匹配数据</td></tr>';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function formatSize(bytes) { if (!bytes || bytes === 0) return '0 B'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; }
|
|
425
|
+
|
|
426
|
+
document.getElementById('sourceFilter').addEventListener('change', updateFilter);
|
|
427
|
+
document.getElementById('targetFilter').addEventListener('change', updateFilter);
|
|
428
|
+
document.getElementById('searchInput').addEventListener('input', updateFilter);
|
|
429
|
+
function updateFilter() { renderDepList({ source: document.getElementById('sourceFilter').value, target: document.getElementById('targetFilter').value, search: document.getElementById('searchInput').value }); }
|
|
430
|
+
|
|
431
|
+
initPackageGrid();
|
|
432
|
+
renderDepList();
|
|
433
|
+
</script>
|
|
434
|
+
</body>
|
|
435
|
+
</html>`;
|
|
436
|
+
};
|