uniapp-cross-package-analyzer 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +129 -5
- package/src/template.js +12 -5
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -124,6 +124,69 @@ class UniAppCrossPackageAnalyzer {
|
|
|
124
124
|
const nodeMap = new Map();
|
|
125
125
|
const linkSet = new Set();
|
|
126
126
|
|
|
127
|
+
// 缓存文件内容用于读取代码行
|
|
128
|
+
const fileContentCache = {};
|
|
129
|
+
const getFileLines = (filePath) => {
|
|
130
|
+
try {
|
|
131
|
+
const fullPath = path.join(inputPath, filePath);
|
|
132
|
+
if (!fileContentCache[fullPath]) {
|
|
133
|
+
if (fs.existsSync(fullPath)) {
|
|
134
|
+
fileContentCache[fullPath] = fs.readFileSync(fullPath, 'utf8').split('\n');
|
|
135
|
+
} else {
|
|
136
|
+
fileContentCache[fullPath] = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return fileContentCache[fullPath];
|
|
140
|
+
} catch (e) {}
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const getLineContent = (filePath, lineNum) => {
|
|
145
|
+
if (typeof lineNum !== 'number' || lineNum < 1) return null;
|
|
146
|
+
const lines = getFileLines(filePath);
|
|
147
|
+
if (lines && lines[lineNum - 1] !== undefined) {
|
|
148
|
+
return lines[lineNum - 1].trim();
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// 在源文件中搜索 import 语句的实际行号
|
|
154
|
+
const findImportLine = (sourcePath, request, targetPath) => {
|
|
155
|
+
const lines = getFileLines(sourcePath);
|
|
156
|
+
if (!lines || !request) return null;
|
|
157
|
+
|
|
158
|
+
// 清理 request,去掉查询参数
|
|
159
|
+
const cleanRequest = request.replace(/\?.*$/, '');
|
|
160
|
+
|
|
161
|
+
// 从 targetPath 提取更精确的匹配关键词
|
|
162
|
+
// 例如 /shares/subMain/services/visitCard/index.js -> visitCard/index 或 visitCard
|
|
163
|
+
const targetParts = targetPath.replace(/^\//, '').split('/');
|
|
164
|
+
const targetFileName = targetParts[targetParts.length - 1].replace(/\.(js|ts|vue|json)$/, '');
|
|
165
|
+
const targetDir = targetParts.length >= 2 ? targetParts[targetParts.length - 2] : '';
|
|
166
|
+
|
|
167
|
+
// 构建多个匹配模式,按优先级排序
|
|
168
|
+
const patterns = [
|
|
169
|
+
cleanRequest, // 完整的 request 路径
|
|
170
|
+
targetDir && targetFileName === 'index' ? targetDir : null, // 如果是 index.js,用目录名匹配
|
|
171
|
+
targetDir ? `${targetDir}/${targetFileName}` : null, // 目录/文件名
|
|
172
|
+
targetDir ? `${targetDir}/index` : null, // 目录/index
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
const line = lines[i];
|
|
177
|
+
// 必须是 import/require 语句
|
|
178
|
+
if (!line.includes('import ') && !line.includes('require(')) continue;
|
|
179
|
+
|
|
180
|
+
// 按优先级匹配
|
|
181
|
+
for (const pattern of patterns) {
|
|
182
|
+
if (line.includes(pattern)) {
|
|
183
|
+
return { line: i + 1, code: line.trim() };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
|
|
127
190
|
// 第一遍:收集所有模块
|
|
128
191
|
for (const module of modules) {
|
|
129
192
|
if (!module.resource || !module.resource.includes(inputPath)) continue;
|
|
@@ -191,7 +254,19 @@ class UniAppCrossPackageAnalyzer {
|
|
|
191
254
|
if (!linkSet.has(linkKey)) {
|
|
192
255
|
linkSet.add(linkKey);
|
|
193
256
|
|
|
194
|
-
|
|
257
|
+
// 优先通过搜索源文件找到 import 语句的真实位置
|
|
258
|
+
const importInfo = findImportLine(sourcePath, dep.request || dep.userRequest, targetPath);
|
|
259
|
+
let location, code;
|
|
260
|
+
|
|
261
|
+
if (importInfo) {
|
|
262
|
+
location = { line: importInfo.line, column: 0 };
|
|
263
|
+
code = importInfo.code;
|
|
264
|
+
} else {
|
|
265
|
+
// 回退到 dep.loc(可能不准确)
|
|
266
|
+
location = this.getImportLocation(dep, module);
|
|
267
|
+
code = getLineContent(sourcePath, location.line);
|
|
268
|
+
}
|
|
269
|
+
|
|
195
270
|
const depInfo = {
|
|
196
271
|
source: sourcePath,
|
|
197
272
|
target: targetPath,
|
|
@@ -199,6 +274,7 @@ class UniAppCrossPackageAnalyzer {
|
|
|
199
274
|
targetPackage: targetPackage,
|
|
200
275
|
size: targetNode.size,
|
|
201
276
|
location: location,
|
|
277
|
+
code: code,
|
|
202
278
|
request: dep.request || dep.userRequest || targetPath
|
|
203
279
|
};
|
|
204
280
|
|
|
@@ -252,8 +328,10 @@ class UniAppCrossPackageAnalyzer {
|
|
|
252
328
|
return 0;
|
|
253
329
|
}
|
|
254
330
|
|
|
255
|
-
getImportLocation(dep) {
|
|
331
|
+
getImportLocation(dep, sourceModule) {
|
|
256
332
|
let line = '?', column = '?';
|
|
333
|
+
|
|
334
|
+
// 优先从依赖对象本身获取位置(import 语句在源文件中的位置)
|
|
257
335
|
if (dep.loc) {
|
|
258
336
|
if (dep.loc.start) {
|
|
259
337
|
line = dep.loc.start.line;
|
|
@@ -263,6 +341,24 @@ class UniAppCrossPackageAnalyzer {
|
|
|
263
341
|
column = dep.loc.column || 0;
|
|
264
342
|
}
|
|
265
343
|
}
|
|
344
|
+
|
|
345
|
+
// 如果 dep.loc 不可用,尝试从源模块的依赖信息中获取
|
|
346
|
+
if (line === '?' && sourceModule && sourceModule.blocks) {
|
|
347
|
+
for (const block of sourceModule.blocks) {
|
|
348
|
+
if (block.dependencies) {
|
|
349
|
+
for (const blockDep of block.dependencies) {
|
|
350
|
+
if (blockDep === dep && blockDep.loc) {
|
|
351
|
+
if (blockDep.loc.start) {
|
|
352
|
+
line = blockDep.loc.start.line;
|
|
353
|
+
column = blockDep.loc.start.column;
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
266
362
|
return { line, column };
|
|
267
363
|
}
|
|
268
364
|
|
|
@@ -280,10 +376,10 @@ class UniAppCrossPackageAnalyzer {
|
|
|
280
376
|
|
|
281
377
|
const reportData = this.prepareReportData();
|
|
282
378
|
|
|
379
|
+
// 始终导出详细的 JSON 报告(包含每行跨包引用点位)
|
|
380
|
+
this.exportDetailedJson(reportData, outputPath);
|
|
381
|
+
|
|
283
382
|
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
383
|
return;
|
|
288
384
|
}
|
|
289
385
|
|
|
@@ -299,6 +395,34 @@ class UniAppCrossPackageAnalyzer {
|
|
|
299
395
|
this.startServer(html);
|
|
300
396
|
}
|
|
301
397
|
|
|
398
|
+
exportDetailedJson(reportData, outputPath) {
|
|
399
|
+
// 简化的跨包引用报告
|
|
400
|
+
const crossRefs = [];
|
|
401
|
+
|
|
402
|
+
// 只保留分包跨分包的调用
|
|
403
|
+
for (const dep of reportData.crossDeps) {
|
|
404
|
+
if (dep.sourcePackage === 'main' || dep.targetPackage === 'main') {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
crossRefs.push({
|
|
409
|
+
from: dep.source,
|
|
410
|
+
to: dep.target,
|
|
411
|
+
pkg: `${dep.sourcePackage} -> ${dep.targetPackage}`,
|
|
412
|
+
line: dep.location.line
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const result = {
|
|
417
|
+
total: crossRefs.length,
|
|
418
|
+
refs: crossRefs
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const jsonPath = path.join(outputPath, this.statsFilename);
|
|
422
|
+
fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2));
|
|
423
|
+
console.log(`[UniAppCrossPackageAnalyzer] JSON 报告已生成: ${jsonPath}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
302
426
|
prepareReportData() {
|
|
303
427
|
const crossDepsByPackage = {};
|
|
304
428
|
|
package/src/template.js
CHANGED
|
@@ -162,7 +162,7 @@ module.exports = function generateHTML(data, formatSize) {
|
|
|
162
162
|
<div class="table-container">
|
|
163
163
|
<table>
|
|
164
164
|
<thead>
|
|
165
|
-
<tr><th>源文件</th><th>源分包</th><th>目标文件</th><th>目标分包</th><th>体积</th><th>行号</th></tr>
|
|
165
|
+
<tr><th>源文件</th><th>源分包</th><th>目标文件</th><th>目标分包</th><th>体积</th><th>行号</th><th>代码</th></tr>
|
|
166
166
|
</thead>
|
|
167
167
|
<tbody id="depTableBody"></tbody>
|
|
168
168
|
</table>
|
|
@@ -282,10 +282,14 @@ module.exports = function generateHTML(data, formatSize) {
|
|
|
282
282
|
'<span class="file-collapse-icon" style="display: inline-block; width: 16px; font-size: 10px;">▶</span>' +
|
|
283
283
|
'<span style="color: #aaa; font-size: 12px;">' + file + '</span>' +
|
|
284
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;
|
|
285
|
+
'<div id="' + fileCollapseId + '" style="display: none; margin-top: 8px; margin-left: 20px; font-size: 11px;">';
|
|
286
286
|
fileDeps.forEach(dep => {
|
|
287
287
|
const targetFile = depDirection === 'imports' ? dep.target : dep.source;
|
|
288
|
-
|
|
288
|
+
const codeSnippet = dep.code ? ('<div style="background: #0d0d15; padding: 6px 10px; border-radius: 4px; margin-top: 4px; font-family: monospace; color: #e0e0e0; white-space: pre-wrap; word-break: break-all; border-left: 3px solid #667eea;">' + escapeHtml(dep.code) + '</div>') : '';
|
|
289
|
+
html += '<div style="padding: 8px 0; border-bottom: 1px dotted #2a2a3e;">' +
|
|
290
|
+
'<div style="color: #667eea;">→ ' + targetFile + ' <span class="size">(' + formatSize(dep.size) + ')</span> <span style="color: #888;">L' + dep.location.line + '</span></div>' +
|
|
291
|
+
codeSnippet +
|
|
292
|
+
'</div>';
|
|
289
293
|
});
|
|
290
294
|
html += '</div></div>';
|
|
291
295
|
});
|
|
@@ -417,12 +421,15 @@ module.exports = function generateHTML(data, formatSize) {
|
|
|
417
421
|
if (filter.search) { const s = filter.search.toLowerCase(); deps = deps.filter(d => d.source.toLowerCase().includes(s) || d.target.toLowerCase().includes(s)); }
|
|
418
422
|
tbody.innerHTML = deps.map(dep => {
|
|
419
423
|
const isMainS = dep.sourcePackage === 'main', isMainT = dep.targetPackage === 'main';
|
|
420
|
-
|
|
421
|
-
|
|
424
|
+
const codeCell = dep.code ? '<code style="background: #0d0d15; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #e0e0e0;">' + escapeHtml(dep.code) + '</code>' : '-';
|
|
425
|
+
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><td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">' + codeCell + '</td></tr>';
|
|
426
|
+
}).join('') || '<tr><td colspan="7" style="text-align: center; color: #666;">无匹配数据</td></tr>';
|
|
422
427
|
}
|
|
423
428
|
|
|
424
429
|
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
430
|
|
|
431
|
+
function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
432
|
+
|
|
426
433
|
document.getElementById('sourceFilter').addEventListener('change', updateFilter);
|
|
427
434
|
document.getElementById('targetFilter').addEventListener('change', updateFilter);
|
|
428
435
|
document.getElementById('searchInput').addEventListener('input', updateFilter);
|