rn-bundle-analyzer 1.1.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.
Files changed (54) hide show
  1. package/README.md +80 -0
  2. package/dist/builders/treeBuilder.d.ts +2 -0
  3. package/dist/builders/treeBuilder.js +47 -0
  4. package/dist/collectors/fileCollector.d.ts +2 -0
  5. package/dist/collectors/fileCollector.js +27 -0
  6. package/dist/collectors/inverseDependencyCollector.d.ts +2 -0
  7. package/dist/collectors/inverseDependencyCollector.js +29 -0
  8. package/dist/config.d.ts +12 -0
  9. package/dist/config.js +15 -0
  10. package/dist/fileInfoCollector.d.ts +4 -0
  11. package/dist/fileInfoCollector.js +44 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +48 -0
  14. package/dist/templates/analyse.html +1082 -0
  15. package/dist/templates/analyse.pug +321 -0
  16. package/dist/templates/analyseTemplate.d.ts +3 -0
  17. package/dist/templates/analyseTemplate.js +41 -0
  18. package/dist/templates/icons-inline.js +27 -0
  19. package/dist/templates/lucideIcons.d.ts +2 -0
  20. package/dist/templates/lucideIcons.js +28 -0
  21. package/dist/templates/tailwindcss.js +83 -0
  22. package/dist/types.d.ts +41 -0
  23. package/dist/types.js +2 -0
  24. package/dist/utils/envSetup.d.ts +1 -0
  25. package/dist/utils/envSetup.js +21 -0
  26. package/dist/utils/fileUtils.d.ts +3 -0
  27. package/dist/utils/fileUtils.js +27 -0
  28. package/dist/writers/fileInfoWriter.d.ts +12 -0
  29. package/dist/writers/fileInfoWriter.js +42 -0
  30. package/dist/writers/inverseDependencyWriter.d.ts +2 -0
  31. package/dist/writers/inverseDependencyWriter.js +48 -0
  32. package/dist/writers/treeWriter.d.ts +2 -0
  33. package/dist/writers/treeWriter.js +26 -0
  34. package/index.js +1 -0
  35. package/package.json +24 -0
  36. package/scripts/compile-pug.js +17 -0
  37. package/src/builders/treeBuilder.ts +57 -0
  38. package/src/collectors/fileCollector.ts +24 -0
  39. package/src/collectors/inverseDependencyCollector.ts +32 -0
  40. package/src/config.ts +14 -0
  41. package/src/fileInfoCollector.ts +50 -0
  42. package/src/index.ts +15 -0
  43. package/src/templates/analyse-app.js +988 -0
  44. package/src/templates/analyse.pug +321 -0
  45. package/src/templates/analyseTemplate.ts +45 -0
  46. package/src/templates/icons-inline.js +27 -0
  47. package/src/templates/tailwindcss.js +83 -0
  48. package/src/types.ts +43 -0
  49. package/src/utils/envSetup.ts +18 -0
  50. package/src/utils/fileUtils.ts +21 -0
  51. package/src/writers/fileInfoWriter.ts +44 -0
  52. package/src/writers/inverseDependencyWriter.ts +50 -0
  53. package/src/writers/treeWriter.ts +24 -0
  54. package/tsconfig.json +20 -0
@@ -0,0 +1,988 @@
1
+ // 全局数据存储
2
+ let packageData = null;
3
+ let treeData = null;
4
+ let inverseDependencyData = null;
5
+ let filteredFiles = [];
6
+ let filteredDependencies = [];
7
+ let currentPage = 1;
8
+ let currentDepPage = 1;
9
+ const itemsPerPage = 50;
10
+ const supportedLanguages = ['zh', 'en'];
11
+ let currentLanguage = localStorage.getItem('analyse-language') || 'zh';
12
+
13
+ const emptyPackageData = {
14
+ buildInfo: { totalFiles: 0, totalSize: 0 },
15
+ fileTypeStats: {},
16
+ files: [],
17
+ };
18
+ const emptyTreeData = { pathAnalyse: [] };
19
+ const emptyInverseData = {
20
+ summary: { totalFiles: 0, totalDependencies: 0, averageDependencies: 0, mostDependedFiles: [] },
21
+ inverseDependencies: [],
22
+ };
23
+
24
+ const i18nMessages = {
25
+ zh: {
26
+ pageTitle: 'RN 包分析结果展示',
27
+ headerTitle: 'RN 包分析结果',
28
+ headerSubtitle: 'React Native 项目包大小分析与可视化',
29
+ totalFiles: '总文件数',
30
+ totalSize: '总大小',
31
+ tabFiles: '文件列表视图',
32
+ tabTree: '目录树视图',
33
+ tabDependencies: '反向依赖分析',
34
+ allFileTypes: '所有文件类型',
35
+ sortSizeDesc: '大小 (大到小)',
36
+ sortSizeAsc: '大小 (小到大)',
37
+ sortNameAsc: '名称 (A-Z)',
38
+ sortNameDesc: '名称 (Z-A)',
39
+ colFilePath: '文件路径',
40
+ colSize: '大小',
41
+ colType: '类型',
42
+ colModified: '修改时间',
43
+ colInverseDeps: '反向依赖',
44
+ show: '显示',
45
+ itemsSepA: ' 项,共 ',
46
+ itemsSepB: ' 项',
47
+ prevPage: '上一页',
48
+ nextPage: '下一页',
49
+ expandAll: '全部展开',
50
+ collapseAll: '全部折叠',
51
+ totalDependencyRelations: '总依赖关系数',
52
+ avgDependencies: '平均依赖数',
53
+ maxDependencies: '最高依赖数',
54
+ topDepsTitle: '热门依赖文件 Top 10',
55
+ sortDepsDesc: '依赖数 (高到低)',
56
+ sortDepsAsc: '依赖数 (低到高)',
57
+ sortFileNameAsc: '文件名 (A-Z)',
58
+ sortFileNameDesc: '文件名 (Z-A)',
59
+ allFiles: '所有文件',
60
+ srcFiles: '源码文件',
61
+ dependencyPackageFiles: '依赖包文件',
62
+ highDependencyFiles: '高依赖文件 (>100)',
63
+ colDependentCount: '被依赖次数',
64
+ colAction: '操作',
65
+ searchFilesPlaceholder: '搜索文件名或路径...',
66
+ searchTreePlaceholder: '搜索目录或文件...',
67
+ searchDepPlaceholder: '搜索文件路径...',
68
+ unknown: '未知',
69
+ viewInverseDependencies: '查看反向依赖',
70
+ noDependencies: '无依赖',
71
+ viewDependencies: '查看依赖',
72
+ modalTitle: '{fileName} 的反向依赖关系',
73
+ noDependentsMessage: '此文件没有被其他文件依赖',
74
+ dependedByCount: '共 {count} 个文件依赖此文件',
75
+ moreDependents: '还有 {count} 个依赖文件未显示...',
76
+ },
77
+ en: {
78
+ pageTitle: 'RN Package Analysis Report',
79
+ headerTitle: 'RN Package Analysis',
80
+ headerSubtitle: 'Bundle size analysis and visualization for React Native projects',
81
+ totalFiles: 'Total Files',
82
+ totalSize: 'Total Size',
83
+ tabFiles: 'File List View',
84
+ tabTree: 'Directory Tree View',
85
+ tabDependencies: 'Inverse Dependencies',
86
+ allFileTypes: 'All File Types',
87
+ sortSizeDesc: 'Size (High to Low)',
88
+ sortSizeAsc: 'Size (Low to High)',
89
+ sortNameAsc: 'Name (A-Z)',
90
+ sortNameDesc: 'Name (Z-A)',
91
+ colFilePath: 'File Path',
92
+ colSize: 'Size',
93
+ colType: 'Type',
94
+ colModified: 'Modified Time',
95
+ colInverseDeps: 'Inverse Dependencies',
96
+ show: 'Showing',
97
+ itemsSepA: ' items, total ',
98
+ itemsSepB: ' items',
99
+ prevPage: 'Prev',
100
+ nextPage: 'Next',
101
+ expandAll: 'Expand All',
102
+ collapseAll: 'Collapse All',
103
+ totalDependencyRelations: 'Total Dependency Relations',
104
+ avgDependencies: 'Average Dependencies',
105
+ maxDependencies: 'Max Dependencies',
106
+ topDepsTitle: 'Top 10 Most Depended Files',
107
+ sortDepsDesc: 'Dependents (High to Low)',
108
+ sortDepsAsc: 'Dependents (Low to High)',
109
+ sortFileNameAsc: 'Filename (A-Z)',
110
+ sortFileNameDesc: 'Filename (Z-A)',
111
+ allFiles: 'All Files',
112
+ srcFiles: 'Source Files',
113
+ dependencyPackageFiles: 'Dependency Package Files',
114
+ highDependencyFiles: 'High Dependency Files (>100)',
115
+ colDependentCount: 'Dependents',
116
+ colAction: 'Action',
117
+ searchFilesPlaceholder: 'Search by file name or path...',
118
+ searchTreePlaceholder: 'Search directories or files...',
119
+ searchDepPlaceholder: 'Search file path...',
120
+ unknown: 'Unknown',
121
+ viewInverseDependencies: 'View inverse dependencies',
122
+ noDependencies: 'No dependencies',
123
+ viewDependencies: 'View dependencies',
124
+ modalTitle: 'Inverse dependencies of {fileName}',
125
+ noDependentsMessage: 'No files depend on this file',
126
+ dependedByCount: '{count} files depend on this file',
127
+ moreDependents: '{count} more dependent files are hidden...',
128
+ }
129
+ };
130
+
131
+ // 工具函数
132
+ function t(key, params = {}) {
133
+ const lang = supportedLanguages.includes(currentLanguage) ? currentLanguage : 'zh';
134
+ const fallback = i18nMessages.zh[key] || key;
135
+ const message = (i18nMessages[lang] && i18nMessages[lang][key]) || fallback;
136
+ return message.replace(/\{(\w+)\}/g, (_, token) => {
137
+ return params[token] !== undefined ? String(params[token]) : '';
138
+ });
139
+ }
140
+
141
+ function updateLanguageToggleState() {
142
+ document.querySelectorAll('.lang-toggle-btn').forEach((button) => {
143
+ button.classList.toggle('active', button.dataset.lang === currentLanguage);
144
+ });
145
+ }
146
+
147
+ function applyI18n() {
148
+ document.documentElement.lang = currentLanguage === 'en' ? 'en' : 'zh-CN';
149
+
150
+ document.querySelectorAll('[data-i18n]').forEach((node) => {
151
+ const key = node.getAttribute('data-i18n');
152
+ if (!key) return;
153
+ node.textContent = t(key);
154
+ });
155
+
156
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
157
+ const key = node.getAttribute('data-i18n-placeholder');
158
+ if (!key) return;
159
+ node.setAttribute('placeholder', t(key));
160
+ });
161
+ }
162
+
163
+ function setLanguage(lang) {
164
+ if (!supportedLanguages.includes(lang)) return;
165
+ currentLanguage = lang;
166
+ localStorage.setItem('analyse-language', lang);
167
+ updateLanguageToggleState();
168
+ applyI18n();
169
+ updateFilesTable();
170
+ updateDependenciesTable();
171
+ }
172
+
173
+ function formatFileSize(bytes) {
174
+ if (bytes === 0) return '0 B';
175
+ const k = 1024;
176
+ const sizes = ['B', 'KB', 'MB', 'GB'];
177
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
178
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
179
+ }
180
+
181
+ function formatDate(dateString) {
182
+ const date = new Date(dateString);
183
+ const locale = currentLanguage === 'en' ? 'en-US' : 'zh-CN';
184
+ return date.toLocaleDateString(locale) + ' ' + date.toLocaleTimeString(locale, { hour12: false });
185
+ }
186
+
187
+ function getFileExtension(filename) {
188
+ const lastDot = filename.lastIndexOf('.');
189
+ return lastDot > 0 ? filename.substring(lastDot) : '';
190
+ }
191
+
192
+ function getFileName(path) {
193
+ return path.split('/').pop();
194
+ }
195
+
196
+ function escapeForSingleQuoteJsString(value) {
197
+ return String(value)
198
+ .replace(/\\/g, '\\\\')
199
+ .replace(/'/g, "\\'");
200
+ }
201
+
202
+ // 数据加载和初始化
203
+ async function loadData() {
204
+ try {
205
+ packageData = window.packageAnalyseData || null;
206
+ treeData = window.treeAnalyseData || null;
207
+
208
+ // 加载反向依赖数据
209
+ inverseDependencyData = window.inverseDependencyData;
210
+
211
+ if (!packageData || !packageData.buildInfo || !Array.isArray(packageData.files)) {
212
+ throw new Error('packageAnalyseData 数据缺失或格式不正确');
213
+ }
214
+ if (!treeData || !Array.isArray(treeData.pathAnalyse)) {
215
+ throw new Error('treeAnalyseData 数据缺失或格式不正确');
216
+ }
217
+
218
+ initializeApp();
219
+ } catch (error) {
220
+ console.error('Error loading data:', error);
221
+ packageData = emptyPackageData;
222
+ treeData = emptyTreeData;
223
+ inverseDependencyData = emptyInverseData;
224
+ initializeApp();
225
+ }
226
+ }
227
+
228
+ // 应用初始化
229
+ function initializeApp() {
230
+ if (!supportedLanguages.includes(currentLanguage)) {
231
+ currentLanguage = 'zh';
232
+ }
233
+
234
+ applyI18n();
235
+ updateLanguageToggleState();
236
+ updateHeader();
237
+ setupEventListeners();
238
+ initializeFilesView();
239
+ initializeTreeView();
240
+ initializeDependenciesView();
241
+
242
+
243
+ // 初始化图标
244
+ lucide.createIcons();
245
+ }
246
+
247
+ // 更新头部信息
248
+ function updateHeader() {
249
+ const totalFiles = packageData?.buildInfo?.totalFiles || 0;
250
+ const totalSize = packageData?.buildInfo?.totalSize || 0;
251
+ document.getElementById('total-files').textContent = totalFiles.toLocaleString();
252
+ document.getElementById('total-size').textContent = formatFileSize(totalSize);
253
+ }
254
+
255
+ // 设置事件监听器
256
+ function setupEventListeners() {
257
+ // Tab切换
258
+ document.querySelectorAll('.tab-button').forEach(button => {
259
+ button.addEventListener('click', () => {
260
+ const tabName = button.dataset.tab;
261
+ switchTab(tabName);
262
+ });
263
+ });
264
+
265
+ // 搜索功能
266
+ document.getElementById('search-input').addEventListener('input', debounce(handleSearch, 300));
267
+ document.getElementById('tree-search').addEventListener('input', debounce(handleTreeSearch, 300));
268
+
269
+ // 树形控制按钮
270
+ document.getElementById('expand-all-btn').addEventListener('click', expandAllNodes);
271
+ document.getElementById('collapse-all-btn').addEventListener('click', collapseAllNodes);
272
+
273
+ // 筛选和排序
274
+ document.getElementById('extension-filter').addEventListener('change', handleFilter);
275
+ document.getElementById('sort-select').addEventListener('change', handleSort);
276
+
277
+ // 分页
278
+ document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
279
+ document.getElementById('next-page').addEventListener('click', () => changePage(1));
280
+
281
+ // 反向依赖功能
282
+ document.getElementById('dep-search-input').addEventListener('input', debounce(handleDepSearch, 300));
283
+ document.getElementById('dep-sort-select').addEventListener('change', handleDepSort);
284
+ document.getElementById('dep-filter-select').addEventListener('change', handleDepFilter);
285
+ document.getElementById('dep-prev-page').addEventListener('click', () => changeDepPage(-1));
286
+ document.getElementById('dep-next-page').addEventListener('click', () => changeDepPage(1));
287
+
288
+ document.querySelectorAll('.lang-toggle-btn').forEach((button) => {
289
+ button.addEventListener('click', () => {
290
+ setLanguage(button.dataset.lang);
291
+ });
292
+ });
293
+ }
294
+
295
+ // 防抖函数
296
+ function debounce(func, wait) {
297
+ let timeout;
298
+ return function executedFunction(...args) {
299
+ const later = () => {
300
+ clearTimeout(timeout);
301
+ func(...args);
302
+ };
303
+ clearTimeout(timeout);
304
+ timeout = setTimeout(later, wait);
305
+ };
306
+ }
307
+
308
+ // Tab切换
309
+ function switchTab(tabName) {
310
+ if (!tabName) return;
311
+ // 更新按钮状态
312
+ document.querySelectorAll('.tab-button').forEach(btn => {
313
+ btn.classList.remove('active', 'border-primary', 'text-primary');
314
+ btn.classList.add('border-transparent', 'text-muted-foreground');
315
+ });
316
+
317
+ const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
318
+ if (!activeBtn) return;
319
+ activeBtn.classList.add('active', 'border-primary', 'text-primary');
320
+ activeBtn.classList.remove('border-transparent', 'text-muted-foreground');
321
+
322
+ // 切换内容
323
+ document.querySelectorAll('.tab-content').forEach(content => {
324
+ content.classList.add('hidden');
325
+ });
326
+ const activeTab = document.getElementById(`${tabName}-tab`);
327
+ if (activeTab) activeTab.classList.remove('hidden');
328
+
329
+ // 重新初始化图标
330
+ lucide.createIcons();
331
+ }
332
+
333
+ // 文件视图初始化
334
+ function initializeFilesView() {
335
+ // 初始化扩展名过滤器
336
+ const extensions = Object.keys(packageData?.fileTypeStats || {});
337
+ const filterSelect = document.getElementById('extension-filter');
338
+ filterSelect.innerHTML = `<option value="" data-i18n="allFileTypes">${t('allFileTypes')}</option>`;
339
+ extensions.forEach(ext => {
340
+ const option = document.createElement('option');
341
+ option.value = ext;
342
+ option.textContent = `${ext} (${packageData.fileTypeStats[ext].count})`;
343
+ filterSelect.appendChild(option);
344
+ });
345
+
346
+ // 初始化文件列表
347
+ filteredFiles = Array.isArray(packageData?.files) ? [...packageData.files] : [];
348
+ updateFilesTable();
349
+ }
350
+
351
+ // 搜索处理
352
+ function handleSearch() {
353
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
354
+ const extensionFilter = document.getElementById('extension-filter').value;
355
+
356
+ const files = Array.isArray(packageData?.files) ? packageData.files : [];
357
+ filteredFiles = files.filter(file => {
358
+ const matchesSearch = file.path.toLowerCase().includes(searchTerm);
359
+ const matchesExtension = !extensionFilter || file.extension === extensionFilter;
360
+ return matchesSearch && matchesExtension;
361
+ });
362
+
363
+ currentPage = 1;
364
+ updateFilesTable();
365
+ }
366
+
367
+ // 筛选处理
368
+ function handleFilter() {
369
+ handleSearch(); // 重新应用所有筛选条件
370
+ }
371
+
372
+ // 排序处理
373
+ function handleSort() {
374
+ const sortValue = document.getElementById('sort-select').value;
375
+
376
+ filteredFiles.sort((a, b) => {
377
+ switch (sortValue) {
378
+ case 'size-desc':
379
+ return b.size - a.size;
380
+ case 'size-asc':
381
+ return a.size - b.size;
382
+ case 'name-asc':
383
+ return a.path.localeCompare(b.path);
384
+ case 'name-desc':
385
+ return b.path.localeCompare(a.path);
386
+ default:
387
+ return 0;
388
+ }
389
+ });
390
+
391
+ updateFilesTable();
392
+ }
393
+
394
+ // 更新文件表格
395
+ function updateFilesTable() {
396
+ const tbody = document.getElementById('files-table-body');
397
+ const startIndex = (currentPage - 1) * itemsPerPage;
398
+ const endIndex = startIndex + itemsPerPage;
399
+ const pageFiles = filteredFiles.slice(startIndex, endIndex);
400
+ const maxFileSize = filteredFiles[0]?.size || packageData?.files?.[0]?.size || 1;
401
+
402
+ if (pageFiles.length === 0) {
403
+ tbody.innerHTML = `<tr><td colspan="5" class="px-6 py-12 text-center text-sm text-muted-foreground">${t('noDependencies')}</td></tr>`;
404
+ updatePagination();
405
+ return;
406
+ }
407
+
408
+ tbody.innerHTML = pageFiles.map(file => {
409
+ const dependencyCount = getDependencyCount(file.path);
410
+ return `
411
+ <tr class="hover:bg-accent">
412
+ <td class="px-6 py-4">
413
+ <div class="flex items-center">
414
+ <i data-lucide="file" class="w-4 h-4 mr-2 text-muted-foreground"></i>
415
+ <span class="font-mono text-sm break-all">${file.path}</span>
416
+ </div>
417
+ </td>
418
+ <td class="px-6 py-4">
419
+ <div class="flex items-center">
420
+ <span class="font-medium">${formatFileSize(file.size)}</span>
421
+ <div class="ml-2 w-16 h-2 bg-muted rounded-full overflow-hidden">
422
+ <div class="progress-bar h-full" style="width: ${Math.min(100, (file.size / maxFileSize) * 100)}%"></div>
423
+ </div>
424
+ </div>
425
+ </td>
426
+ <td class="px-6 py-4">
427
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground">
428
+ ${file.extension || t('unknown')}
429
+ </span>
430
+ </td>
431
+ <td class="px-6 py-4 text-sm text-muted-foreground">
432
+ ${formatDate(file.lastModified)}
433
+ </td>
434
+ <td class="px-6 py-4">
435
+ <div class="flex items-center space-x-2">
436
+ ${dependencyCount > 0 ? `
437
+ <button title="${t('viewInverseDependencies')}" class="px-2 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600 transition-colors flex items-center"
438
+ onclick="showDependentsList('${escapeForSingleQuoteJsString(file.path)}')">
439
+ <i data-lucide="git-fork" class="w-3 h-3 mr-1"></i>
440
+ ${dependencyCount}
441
+ </button>
442
+ ` : `
443
+ <span class="text-xs text-muted-foreground">${t('noDependencies')}</span>
444
+ `}
445
+ </div>
446
+ </td>
447
+ </tr>
448
+ `;
449
+ }).join('');
450
+
451
+ // 更新分页信息
452
+ updatePagination();
453
+
454
+ // 重新初始化图标
455
+ lucide.createIcons();
456
+ }
457
+
458
+ // 更新分页
459
+ function updatePagination() {
460
+ const totalPages = Math.ceil(filteredFiles.length / itemsPerPage);
461
+ if (filteredFiles.length === 0) {
462
+ document.getElementById('files-range').textContent = '0-0';
463
+ document.getElementById('files-total').textContent = '0';
464
+ document.getElementById('page-info').textContent = '0 / 0';
465
+ document.getElementById('prev-page').disabled = true;
466
+ document.getElementById('next-page').disabled = true;
467
+ return;
468
+ }
469
+ const startItem = (currentPage - 1) * itemsPerPage + 1;
470
+ const endItem = Math.min(currentPage * itemsPerPage, filteredFiles.length);
471
+
472
+ document.getElementById('files-range').textContent = `${startItem}-${endItem}`;
473
+ document.getElementById('files-total').textContent = filteredFiles.length;
474
+ document.getElementById('page-info').textContent = `${currentPage} / ${totalPages}`;
475
+
476
+ document.getElementById('prev-page').disabled = currentPage === 1;
477
+ document.getElementById('next-page').disabled = currentPage === totalPages;
478
+ }
479
+
480
+ // 翻页
481
+ function changePage(direction) {
482
+ const totalPages = Math.ceil(filteredFiles.length / itemsPerPage);
483
+ const newPage = currentPage + direction;
484
+
485
+ if (newPage >= 1 && newPage <= totalPages) {
486
+ currentPage = newPage;
487
+ updateFilesTable();
488
+ }
489
+ }
490
+
491
+ // 树形视图初始化
492
+ function initializeTreeView() {
493
+ renderTree();
494
+ }
495
+
496
+ // 渲染树形结构
497
+ function renderTree() {
498
+ const container = document.getElementById('tree-container');
499
+ container.innerHTML = renderTreeNode(treeData.pathAnalyse, 0);
500
+
501
+ // 添加点击事件
502
+ container.addEventListener('click', handleTreeClick);
503
+
504
+ // 重新初始化图标
505
+ lucide.createIcons();
506
+ }
507
+
508
+ // 渲染树节点
509
+ function renderTreeNode(nodes, level) {
510
+ if (!nodes || !Array.isArray(nodes)) return '';
511
+
512
+ return nodes.map(node => {
513
+ const hasChildren = node.child && node.child.length > 0;
514
+ const sizePercent = (node.ratio * 100).toFixed(1);
515
+ const pathName = node.path.split('/').pop();
516
+ const dependencyCount = !hasChildren ? getDependencyCount(node.path) : 0;
517
+
518
+ return `
519
+ <div class="tree-node collapsed" data-path="${node.path}">
520
+ <div class="tree-item ${hasChildren ? 'tree-toggle' : ''} flex items-center">
521
+ <span class="tree-expand-icon w-4 h-4 flex items-center justify-center mr-1">
522
+ ${hasChildren ? '<i data-lucide="chevron-right" class="w-3 h-3"></i>' : '<span class="w-3"></span>'}
523
+ </span>
524
+ <i data-lucide="${hasChildren ? 'folder' : 'file'}" class="w-4 h-4 mr-2 ${hasChildren ? 'tree-folder-icon' : 'tree-file-icon'}"></i>
525
+ <span class="flex-1 font-medium">${pathName}</span>
526
+ <span class="text-sm text-muted-foreground ml-2 font-mono">${formatFileSize(node.size)}</span>
527
+ <span class="text-xs text-muted-foreground ml-2 opacity-75">(${sizePercent}%)</span>
528
+ ${!hasChildren && dependencyCount > 0 ? `
529
+ <button title="${t('viewInverseDependencies')}" class="ml-2 px-1 py-0.5 bg-blue-500 text-white rounded text-xs hover:bg-blue-600 transition-colors flex items-center"
530
+ onclick="event.stopPropagation(); showDependentsList('${escapeForSingleQuoteJsString(node.path)}')">
531
+ <i data-lucide="git-fork" class="w-2.5 h-2.5 mr-1"></i>
532
+ ${dependencyCount}
533
+ </button>
534
+ ` : !hasChildren ? `
535
+ <span class="ml-2 text-xs text-muted-foreground opacity-50">${t('noDependencies')}</span>
536
+ ` : ''}
537
+ </div>
538
+ ${hasChildren ? `<div class="tree-children">${renderTreeNode(node.child, level + 1)}</div>` : ''}
539
+ </div>
540
+ `;
541
+ }).join('');
542
+ }
543
+
544
+ // 处理树形结构点击
545
+ function handleTreeClick(e) {
546
+ const toggle = e.target.closest('.tree-toggle');
547
+ if (!toggle) return;
548
+
549
+ e.preventDefault();
550
+ e.stopPropagation();
551
+
552
+ const node = toggle.closest('.tree-node');
553
+ const children = node.querySelector('.tree-children');
554
+
555
+ if (!children) return;
556
+
557
+ if (node.classList.contains('collapsed')) {
558
+ // 展开节点
559
+ node.classList.remove('collapsed');
560
+ children.style.display = 'block';
561
+ } else {
562
+ // 折叠节点
563
+ node.classList.add('collapsed');
564
+ children.style.display = 'none';
565
+ }
566
+
567
+ // 重新初始化图标以确保正确显示
568
+ lucide.createIcons();
569
+ }
570
+
571
+ // 树形搜索
572
+ function handleTreeSearch() {
573
+ const searchTerm = document.getElementById('tree-search').value.toLowerCase();
574
+ const nodes = document.querySelectorAll('.tree-node');
575
+
576
+ if (searchTerm === '') {
577
+ // 重置所有节点显示状态
578
+ nodes.forEach(node => {
579
+ node.style.display = 'block';
580
+ node.classList.add('collapsed'); // 重新折叠所有节点
581
+ const children = node.querySelector('.tree-children');
582
+ if (children) {
583
+ children.style.display = 'none';
584
+ }
585
+ });
586
+ } else {
587
+ // 搜索匹配
588
+ nodes.forEach(node => {
589
+ const path = node.dataset.path.toLowerCase();
590
+ const pathName = node.dataset.path.split('/').pop().toLowerCase();
591
+ const matches = path.includes(searchTerm) || pathName.includes(searchTerm);
592
+
593
+ if (matches) {
594
+ node.style.display = 'block';
595
+ // 展开所有父节点
596
+ expandParentNodes(node);
597
+ } else {
598
+ node.style.display = 'none';
599
+ }
600
+ });
601
+ }
602
+
603
+ // 重新初始化图标
604
+ lucide.createIcons();
605
+ }
606
+
607
+ // 展开父节点的辅助函数
608
+ function expandParentNodes(node) {
609
+ let parent = node.parentElement ? node.parentElement.closest('.tree-node') : null;
610
+ while (parent) {
611
+ parent.style.display = 'block';
612
+ parent.classList.remove('collapsed');
613
+ const children = parent.querySelector('.tree-children');
614
+ if (children) {
615
+ children.style.display = 'block';
616
+ }
617
+ parent = parent.parentElement.closest('.tree-node');
618
+ }
619
+ }
620
+
621
+ // 全部展开
622
+ function expandAllNodes() {
623
+ const nodes = document.querySelectorAll('.tree-node');
624
+ nodes.forEach(node => {
625
+ node.classList.remove('collapsed');
626
+ node.style.display = 'block';
627
+ const children = node.querySelector('.tree-children');
628
+ if (children) {
629
+ children.style.display = 'block';
630
+ }
631
+ });
632
+ lucide.createIcons();
633
+ }
634
+
635
+ // 全部折叠
636
+ function collapseAllNodes() {
637
+ const nodes = document.querySelectorAll('.tree-node');
638
+ nodes.forEach(node => {
639
+ node.classList.add('collapsed');
640
+ const children = node.querySelector('.tree-children');
641
+ if (children) {
642
+ children.style.display = 'none';
643
+ }
644
+ });
645
+ lucide.createIcons();
646
+ }
647
+
648
+
649
+ // ===== 反向依赖功能函数 =====
650
+
651
+ // 获取文件的依赖数量
652
+ function getDependencyCount(filePath) {
653
+ if (!inverseDependencyData || !inverseDependencyData.inverseDependencies) {
654
+ return 0;
655
+ }
656
+
657
+ const dependency = inverseDependencyData.inverseDependencies.find(dep => dep.path === filePath);
658
+ return dependency ? dependency.dependents.length : 0;
659
+ }
660
+
661
+ // 反向依赖视图初始化
662
+ function initializeDependenciesView() {
663
+ if (!inverseDependencyData || !Array.isArray(inverseDependencyData.inverseDependencies)) {
664
+ console.warn('反向依赖数据未加载');
665
+ inverseDependencyData = emptyInverseData;
666
+ return;
667
+ }
668
+
669
+ // 更新统计信息
670
+ updateDependenciesStats();
671
+
672
+ // 更新热门依赖列表
673
+ updateTopDependencies();
674
+
675
+ // 初始化依赖关系表格
676
+ filteredDependencies = [...inverseDependencyData.inverseDependencies];
677
+ updateDependenciesTable();
678
+ }
679
+
680
+ // 更新反向依赖统计信息
681
+ function updateDependenciesStats() {
682
+ const summary = inverseDependencyData.summary;
683
+ document.getElementById('dep-total-files').textContent = summary.totalFiles.toLocaleString();
684
+ document.getElementById('dep-total-dependencies').textContent = summary.totalDependencies.toLocaleString();
685
+ document.getElementById('dep-average-dependencies').textContent = summary.averageDependencies.toFixed(2);
686
+ document.getElementById('dep-most-depended').textContent = summary.mostDependedFiles[0]?.dependentCount || 0;
687
+ }
688
+
689
+ // 更新热门依赖文件列表
690
+ function updateTopDependencies() {
691
+ const topList = document.getElementById('top-dependencies-list');
692
+ const topFiles = inverseDependencyData.summary.mostDependedFiles.slice(0, 10);
693
+ const totalDependencies = inverseDependencyData.summary.totalDependencies || 0;
694
+
695
+ if (topFiles.length === 0) {
696
+ topList.innerHTML = `<div class="text-sm text-muted-foreground py-6 text-center">${t('noDependencies')}</div>`;
697
+ return;
698
+ }
699
+
700
+ topList.innerHTML = topFiles.map((file, index) => {
701
+ const fileName = getFileName(file.path);
702
+ const isNodeModule = file.path.includes('node_modules');
703
+ const percentage = totalDependencies > 0
704
+ ? ((file.dependentCount / totalDependencies) * 100).toFixed(2)
705
+ : '0.00';
706
+
707
+ return `
708
+ <div class="flex items-center justify-between p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors">
709
+ <div class="flex items-center flex-1 min-w-0">
710
+ <div class="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-bold mr-3">
711
+ ${index + 1}
712
+ </div>
713
+ <div class="flex-1 min-w-0">
714
+ <div class="font-medium text-sm truncate" title="${file.path}">
715
+ ${fileName}
716
+ </div>
717
+ <div class="text-xs text-muted-foreground truncate">
718
+ ${isNodeModule ? '📦 ' : '📄 '}${file.path}
719
+ </div>
720
+ </div>
721
+ </div>
722
+ <div class="text-right ml-4">
723
+ <div class="text-lg font-bold text-primary">${file.dependentCount}</div>
724
+ <div class="text-xs text-muted-foreground">${percentage}%</div>
725
+ </div>
726
+ </div>
727
+ `;
728
+ }).join('');
729
+ }
730
+
731
+ // 反向依赖搜索处理
732
+ function handleDepSearch() {
733
+ const searchTerm = document.getElementById('dep-search-input').value.toLowerCase();
734
+ const filterValue = document.getElementById('dep-filter-select').value;
735
+
736
+ filteredDependencies = inverseDependencyData.inverseDependencies.filter(dep => {
737
+ const matchesSearch = dep.path.toLowerCase().includes(searchTerm);
738
+ let matchesFilter = true;
739
+
740
+ switch (filterValue) {
741
+ case 'src':
742
+ matchesFilter = dep.path.startsWith('src/');
743
+ break;
744
+ case 'node_modules':
745
+ matchesFilter = dep.path.includes('node_modules');
746
+ break;
747
+ case 'high-deps':
748
+ matchesFilter = dep.dependents.length > 100;
749
+ break;
750
+ }
751
+
752
+ return matchesSearch && matchesFilter;
753
+ });
754
+
755
+ currentDepPage = 1;
756
+ updateDependenciesTable();
757
+ }
758
+
759
+ // 反向依赖筛选处理
760
+ function handleDepFilter() {
761
+ handleDepSearch(); // 重新应用所有筛选条件
762
+ }
763
+
764
+ // 反向依赖排序处理
765
+ function handleDepSort() {
766
+ const sortValue = document.getElementById('dep-sort-select').value;
767
+
768
+ filteredDependencies.sort((a, b) => {
769
+ switch (sortValue) {
770
+ case 'dependents-desc':
771
+ return b.dependents.length - a.dependents.length;
772
+ case 'dependents-asc':
773
+ return a.dependents.length - b.dependents.length;
774
+ case 'name-asc':
775
+ return a.path.localeCompare(b.path);
776
+ case 'name-desc':
777
+ return b.path.localeCompare(a.path);
778
+ default:
779
+ return 0;
780
+ }
781
+ });
782
+
783
+ updateDependenciesTable();
784
+ }
785
+
786
+ // 更新反向依赖表格
787
+ function updateDependenciesTable() {
788
+ const tbody = document.getElementById('dependencies-table-body');
789
+ const startIndex = (currentDepPage - 1) * itemsPerPage;
790
+ const endIndex = startIndex + itemsPerPage;
791
+ const pageDeps = filteredDependencies.slice(startIndex, endIndex);
792
+ const maxDependentCount = inverseDependencyData?.summary?.mostDependedFiles?.[0]?.dependentCount || 1;
793
+
794
+ if (pageDeps.length === 0) {
795
+ tbody.innerHTML = `<tr><td colspan="3" class="px-6 py-12 text-center text-sm text-muted-foreground">${t('noDependencies')}</td></tr>`;
796
+ updateDepPagination();
797
+ return;
798
+ }
799
+
800
+ tbody.innerHTML = pageDeps.map(dep => {
801
+ const fileName = getFileName(dep.path);
802
+ const isNodeModule = dep.path.includes('node_modules');
803
+ const dependentCount = dep.dependents.length;
804
+
805
+ return `
806
+ <tr class="hover:bg-accent">
807
+ <td class="px-6 py-4">
808
+ <div class="flex items-center">
809
+ <i data-lucide="${isNodeModule ? 'package' : 'file'}" class="w-4 h-4 mr-2 text-muted-foreground"></i>
810
+ <div>
811
+ <div class="font-medium text-sm">${fileName}</div>
812
+ <div class="text-xs text-muted-foreground font-mono break-all">${dep.path}</div>
813
+ </div>
814
+ </div>
815
+ </td>
816
+ <td class="px-6 py-4">
817
+ <div class="flex items-center">
818
+ <span class="text-lg font-bold text-primary mr-2">${dependentCount}</span>
819
+ <div class="flex-1 bg-muted rounded-full h-2 overflow-hidden">
820
+ <div class="bg-gradient-to-r from-blue-500 to-purple-500 h-full transition-all duration-300"
821
+ style="width: ${Math.min(100, (dependentCount / maxDependentCount) * 100)}%"></div>
822
+ </div>
823
+ </div>
824
+ </td>
825
+ <td class="px-6 py-4">
826
+ <button class="px-3 py-1 bg-primary text-primary-foreground rounded text-sm hover:bg-primary/90 transition-colors"
827
+ onclick="showDependentsList('${escapeForSingleQuoteJsString(dep.path)}')">
828
+ ${t('viewDependencies')}
829
+ </button>
830
+ </td>
831
+ </tr>
832
+ `;
833
+ }).join('');
834
+
835
+ // 更新分页信息
836
+ updateDepPagination();
837
+
838
+ // 重新初始化图标
839
+ lucide.createIcons();
840
+ }
841
+
842
+ // 更新反向依赖分页
843
+ function updateDepPagination() {
844
+ const totalPages = Math.ceil(filteredDependencies.length / itemsPerPage);
845
+ if (filteredDependencies.length === 0) {
846
+ document.getElementById('dep-range').textContent = '0-0';
847
+ document.getElementById('dep-total').textContent = '0';
848
+ document.getElementById('dep-page-info').textContent = '0 / 0';
849
+ document.getElementById('dep-prev-page').disabled = true;
850
+ document.getElementById('dep-next-page').disabled = true;
851
+ return;
852
+ }
853
+ const startItem = (currentDepPage - 1) * itemsPerPage + 1;
854
+ const endItem = Math.min(currentDepPage * itemsPerPage, filteredDependencies.length);
855
+
856
+ document.getElementById('dep-range').textContent = `${startItem}-${endItem}`;
857
+ document.getElementById('dep-total').textContent = filteredDependencies.length;
858
+ document.getElementById('dep-page-info').textContent = `${currentDepPage} / ${totalPages}`;
859
+
860
+ document.getElementById('dep-prev-page').disabled = currentDepPage === 1;
861
+ document.getElementById('dep-next-page').disabled = currentDepPage === totalPages;
862
+ }
863
+
864
+ // 反向依赖翻页
865
+ function changeDepPage(direction) {
866
+ const totalPages = Math.ceil(filteredDependencies.length / itemsPerPage);
867
+ const newPage = currentDepPage + direction;
868
+
869
+ if (newPage >= 1 && newPage <= totalPages) {
870
+ currentDepPage = newPage;
871
+ updateDependenciesTable();
872
+ }
873
+ }
874
+
875
+ // 显示依赖列表弹窗
876
+ function showDependentsList(filePath, existingModal = null) {
877
+ const dep = inverseDependencyData.inverseDependencies.find(d => d.path === filePath);
878
+ if (!dep) {
879
+ if (existingModal) {
880
+ // 如果文件没有依赖关系,显示提示
881
+ updateModalContent(existingModal, filePath, [], 0, true);
882
+ }
883
+ return;
884
+ }
885
+
886
+ const fileName = getFileName(filePath);
887
+ const dependentsList = dep.dependents.slice(0, 500); // 限制显示前500个
888
+
889
+ if (existingModal) {
890
+ // 更新现有弹窗内容
891
+ updateModalContent(existingModal, filePath, dependentsList, dep.dependents.length, false);
892
+ } else {
893
+ // 创建新弹窗
894
+ const modal = document.createElement('div');
895
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
896
+ modal.innerHTML = createModalHTML(filePath, dependentsList, dep.dependents.length, false);
897
+
898
+ document.body.appendChild(modal);
899
+ lucide.createIcons();
900
+
901
+ // 点击背景关闭
902
+ modal.addEventListener('click', (e) => {
903
+ if (e.target === modal) {
904
+ modal.remove();
905
+ }
906
+ });
907
+ }
908
+ }
909
+
910
+ // 创建弹窗HTML内容
911
+ function createModalHTML(filePath, dependentsList, totalCount, noDependencies) {
912
+ const fileName = getFileName(filePath);
913
+
914
+ return `
915
+ <div class="bg-card rounded-lg border border-border max-w-4xl w-full max-h-[80vh] overflow-hidden">
916
+ <div class="p-6 border-b border-border">
917
+ <div class="flex items-center justify-between">
918
+ <div>
919
+ <h3 class="text-lg font-semibold">${t('modalTitle', { fileName })}</h3>
920
+ <p class="text-sm text-muted-foreground mt-1">
921
+ ${filePath}
922
+ </p>
923
+ <p class="text-sm text-muted-foreground mt-1">
924
+ ${noDependencies ? t('noDependentsMessage') : t('dependedByCount', { count: totalCount })}
925
+ </p>
926
+ </div>
927
+ <button onclick="this.closest('.fixed').remove()" class="text-muted-foreground hover:text-foreground">
928
+ <i data-lucide="x" class="w-5 h-5"></i>
929
+ </button>
930
+ </div>
931
+ </div>
932
+ <div class="p-6 overflow-y-auto max-h-96">
933
+ ${noDependencies ? `
934
+ <div class="text-center py-8">
935
+ <i data-lucide="info" class="w-12 h-12 text-muted-foreground mx-auto mb-4"></i>
936
+ <p class="text-muted-foreground">${t('noDependentsMessage')}</p>
937
+ </div>
938
+ ` : `
939
+ <div class="space-y-2">
940
+ ${dependentsList.map(dependent => {
941
+ const depFileName = getFileName(dependent);
942
+ const isNodeModule = dependent.includes('node_modules');
943
+ const depCount = getDependencyCount(dependent);
944
+ return `
945
+ <div class="flex items-center p-2 bg-muted/50 rounded hover:bg-muted transition-colors">
946
+ <i data-lucide="${isNodeModule ? 'package' : 'file'}" class="w-4 h-4 mr-2 text-muted-foreground"></i>
947
+ <div class="flex-1 min-w-0">
948
+ <div class="font-medium text-sm">${depFileName}</div>
949
+ <div class="text-xs text-muted-foreground font-mono truncate">${dependent}</div>
950
+ </div>
951
+ ${depCount > 0 ? `
952
+ <button title="${t('viewInverseDependencies')}" class="ml-2 px-2 py-1 bg-blue-500 text-white rounded text-xs hover:bg-blue-600 transition-colors flex items-center"
953
+ onclick="showDependentsList('${escapeForSingleQuoteJsString(dependent)}', this.closest('.fixed'))">
954
+ <i data-lucide="git-fork" class="w-3 h-3 mr-1"></i>
955
+ ${depCount}
956
+ </button>
957
+ ` : `
958
+ <span class="ml-2 text-xs text-muted-foreground">${t('noDependencies')}</span>
959
+ `}
960
+ </div>
961
+ `;
962
+ }).join('')}
963
+ ${totalCount > 500 ? `
964
+ <div class="text-center text-muted-foreground text-sm py-2">
965
+ ${t('moreDependents', { count: totalCount - 500 })}
966
+ </div>
967
+ ` : ''}
968
+ </div>
969
+ `}
970
+ </div>
971
+ </div>
972
+ `;
973
+ }
974
+
975
+ // 更新弹窗内容
976
+ function updateModalContent(modal, filePath, dependentsList, totalCount, noDependencies) {
977
+ const modalContent = modal.querySelector('.bg-card');
978
+ const tempDiv = document.createElement('div');
979
+ tempDiv.innerHTML = createModalHTML(filePath, dependentsList, totalCount, noDependencies);
980
+ const newContent = tempDiv.querySelector('.bg-card');
981
+ modalContent.innerHTML = newContent.innerHTML;
982
+ lucide.createIcons();
983
+ }
984
+
985
+ // 页面加载完成后初始化
986
+ document.addEventListener('DOMContentLoaded', () => {
987
+ loadData();
988
+ });