@shidesheng0218/dsh-plugin-lint 0.1.0 → 0.4.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/NOTICE +1 -1
- package/README.md +515 -81
- package/bin/dsh-plugin-analyze.mjs +378 -0
- package/bin/dsh-plugin-lint.mjs +315 -27
- package/bin/dsh-plugin-matrix.mjs +78 -0
- package/bin/dsh-plugin-upgrade-plan.mjs +180 -0
- package/package.json +12 -4
- package/schema/dsh-manifest.schema.json +134 -0
- package/scripts/lib/probe.mjs +28 -385
- package/scripts/probe-boot.mjs +103 -37
- package/src/composition.js +1 -0
- package/src/core/baseline.js +98 -0
- package/src/core/changed.js +41 -0
- package/src/core/load.js +145 -0
- package/src/core/mask.js +207 -0
- package/src/core/pack.js +95 -0
- package/src/core/packlist.js +186 -0
- package/src/core/peer.js +39 -0
- package/src/core/profile-scan.js +121 -0
- package/src/core/report.js +174 -0
- package/src/core/rules.js +581 -0
- package/src/core/semver.js +192 -0
- package/src/core/tarball.js +90 -0
- package/src/data/event-types.json +111 -9
- package/src/experimental/conflicts.js +315 -0
- package/src/experimental/index.js +4 -0
- package/src/experimental/performance.js +515 -0
- package/src/experimental/security.js +462 -0
- package/src/index.js +15 -17
- package/src/load.js +1 -116
- package/src/mask.js +1 -156
- package/src/matrix/cache.js +19 -0
- package/src/matrix/cell.js +79 -0
- package/src/matrix/cells.js +58 -0
- package/src/matrix/conflicts.js +210 -0
- package/src/matrix/export.js +155 -0
- package/src/matrix/index.js +71 -0
- package/src/matrix/manifest.js +17 -0
- package/src/matrix/migrate.js +41 -0
- package/src/matrix/run.js +241 -0
- package/src/matrix/sanitize.js +42 -0
- package/src/matrix/schema.js +178 -0
- package/src/matrix/site.js +61 -0
- package/src/matrix/sync.js +72 -0
- package/src/matrix/verify.js +152 -0
- package/src/performance.js +1 -0
- package/src/probe/artifact-resolver.js +161 -0
- package/src/probe/classify.js +8 -0
- package/src/probe/dsh-adapter.js +212 -0
- package/src/probe/environment/docker.js +270 -0
- package/src/probe/environment/host-unsafe.js +39 -0
- package/src/probe/environment/materialize.js +22 -0
- package/src/probe/environment.js +50 -0
- package/src/probe/installed-artifact.js +17 -0
- package/src/probe/probe.js +278 -0
- package/src/report.js +1 -79
- package/src/rules.js +1 -379
- package/src/security.js +1 -0
- package/src/shared/errors.js +7 -0
- package/src/shared/hash.js +31 -0
- package/src/shared/status.js +12 -0
- package/src/shared/validate.js +14 -0
- package/src/shared/version.js +4 -0
- package/src/upgrade/compatibility.js +14 -0
- package/src/upgrade/planner.js +119 -0
- package/src/upgrade/profile.js +82 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance analysis for plugins — EXPERIMENTAL.
|
|
3
|
+
*
|
|
4
|
+
* This module reads the files a plugin would publish and derives size,
|
|
5
|
+
* complexity, DOM and network indicators from source text. It does not measure
|
|
6
|
+
* startup time, memory, or runtime behaviour; the 0-100 score is a heuristic
|
|
7
|
+
* summary of the static indicators below.
|
|
8
|
+
*
|
|
9
|
+
* @module performance
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const LIMITS = [
|
|
13
|
+
'Static analysis of the files on disk: no startup time, memory, or runtime measurement is performed.',
|
|
14
|
+
'Bundle size approximates npm packing but does not resolve every npm files[] rule.',
|
|
15
|
+
'The 0-100 score is a heuristic summary; a high score is not proof of a fast plugin.',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
import { statSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { buildPacklist } from '../core/packlist.js';
|
|
21
|
+
import { maskSource } from '../core/mask.js';
|
|
22
|
+
import { EXPERIMENTAL_SCHEMA_VERSION } from '../shared/version.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze performance characteristics of a package.
|
|
26
|
+
* @param {object} pkg - loaded package from loadPackage()
|
|
27
|
+
* @returns {object} performance metrics
|
|
28
|
+
*/
|
|
29
|
+
export function analyzePerformance(pkg) {
|
|
30
|
+
const metrics = {
|
|
31
|
+
schemaVersion: EXPERIMENTAL_SCHEMA_VERSION,
|
|
32
|
+
bundleSize: calculateBundleSize(pkg),
|
|
33
|
+
complexity: analyzeComplexity(pkg),
|
|
34
|
+
domImpact: estimateDOMImpact(pkg),
|
|
35
|
+
networkCalls: detectNetworkCalls(pkg),
|
|
36
|
+
heavyDependencies: analyzeHeavyPatterns(pkg),
|
|
37
|
+
score: 0
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Calculate overall performance score (0-100, higher is better)
|
|
41
|
+
metrics.score = calculatePerformanceScore(metrics);
|
|
42
|
+
metrics.rating = ratePerformance(metrics.score);
|
|
43
|
+
metrics.recommendations = generateRecommendations(metrics);
|
|
44
|
+
metrics.experimental = true;
|
|
45
|
+
metrics.limits = LIMITS;
|
|
46
|
+
|
|
47
|
+
if (pkg.jsFiles.length === 0) {
|
|
48
|
+
// A package with no JavaScript is not a 100/100 plugin: there is nothing to
|
|
49
|
+
// judge, so say so instead of rewarding the absence of evidence.
|
|
50
|
+
metrics.score = 0;
|
|
51
|
+
metrics.rating = 'insufficient-data';
|
|
52
|
+
metrics.recommendations.unshift({
|
|
53
|
+
issue: 'No JavaScript files were analyzed',
|
|
54
|
+
suggestion: 'The score only describes files on disk; an empty analysis is not a clean bill of health.',
|
|
55
|
+
impact: 'info',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return metrics;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate total bundle size including all shipped files.
|
|
64
|
+
*/
|
|
65
|
+
function calculateBundleSize(pkg) {
|
|
66
|
+
let totalBytes = 0;
|
|
67
|
+
let jsBytes = 0;
|
|
68
|
+
const largestFiles = [];
|
|
69
|
+
const packlist = buildPacklist(pkg.dir, pkg.manifest);
|
|
70
|
+
|
|
71
|
+
for (const relPath of packlist.paths) {
|
|
72
|
+
const stat = statSync(join(pkg.dir, relPath));
|
|
73
|
+
totalBytes += stat.size;
|
|
74
|
+
if (/\.(js|mjs|cjs|jsx)$/.test(relPath)) jsBytes += stat.size;
|
|
75
|
+
largestFiles.push({ path: relPath, bytes: stat.size, kb: Math.round(stat.size / 1024) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
largestFiles.sort((a, b) => b.bytes - a.bytes);
|
|
79
|
+
return {
|
|
80
|
+
total: totalBytes,
|
|
81
|
+
totalMB: (totalBytes / (1024 * 1024)).toFixed(2),
|
|
82
|
+
js: jsBytes,
|
|
83
|
+
jsMB: (jsBytes / (1024 * 1024)).toFixed(2),
|
|
84
|
+
fileCount: packlist.paths.size,
|
|
85
|
+
largest: largestFiles.slice(0, 5),
|
|
86
|
+
thresholds: {
|
|
87
|
+
small: totalBytes < 100 * 1024,
|
|
88
|
+
medium: totalBytes < 1024 * 1024,
|
|
89
|
+
large: totalBytes >= 1024 * 1024,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Analyze code complexity indicators.
|
|
96
|
+
*/
|
|
97
|
+
function analyzeComplexity(pkg) {
|
|
98
|
+
let totalLines = 0;
|
|
99
|
+
let totalFunctions = 0;
|
|
100
|
+
let deepNesting = 0;
|
|
101
|
+
let longFunctions = 0;
|
|
102
|
+
const complexFiles = [];
|
|
103
|
+
|
|
104
|
+
for (const file of pkg.jsFiles) {
|
|
105
|
+
const lines = file.text.split('\n').length;
|
|
106
|
+
totalLines += lines;
|
|
107
|
+
// Comments and string interiors are blanked before patterns run, so a
|
|
108
|
+
// documented pattern in a comment is not counted as code.
|
|
109
|
+
const view = file.structural ?? maskSource(file.text).structural;
|
|
110
|
+
|
|
111
|
+
// Count function declarations
|
|
112
|
+
const funcMatches = view.match(/\bfunction\s+\w+|=>\s*\{|\bfunction\s*\(/g);
|
|
113
|
+
const funcCount = funcMatches ? funcMatches.length : 0;
|
|
114
|
+
totalFunctions += funcCount;
|
|
115
|
+
|
|
116
|
+
// Detect deep nesting (4+ levels)
|
|
117
|
+
const nestingLevel = maxNestingLevel(view);
|
|
118
|
+
if (nestingLevel >= 4) deepNesting++;
|
|
119
|
+
|
|
120
|
+
// Detect long functions (>100 lines)
|
|
121
|
+
const longFuncs = detectLongFunctions(view);
|
|
122
|
+
longFunctions += longFuncs;
|
|
123
|
+
|
|
124
|
+
const fileComplexity = calculateFileComplexity(file);
|
|
125
|
+
if (fileComplexity > 20) {
|
|
126
|
+
complexFiles.push({
|
|
127
|
+
path: file.path,
|
|
128
|
+
lines,
|
|
129
|
+
functions: funcCount,
|
|
130
|
+
complexity: fileComplexity
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
totalLines,
|
|
137
|
+
avgLinesPerFile: pkg.jsFiles.length > 0 ? Math.round(totalLines / pkg.jsFiles.length) : 0,
|
|
138
|
+
totalFunctions,
|
|
139
|
+
deepNesting,
|
|
140
|
+
longFunctions,
|
|
141
|
+
complexFiles: complexFiles.sort((a, b) => b.complexity - a.complexity).slice(0, 5),
|
|
142
|
+
maintainability: rateComplexity(totalLines, totalFunctions, deepNesting, longFunctions)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Estimate DOM manipulation impact (for client-side plugins).
|
|
148
|
+
*/
|
|
149
|
+
function estimateDOMImpact(pkg) {
|
|
150
|
+
if (!pkg.entries.client) {
|
|
151
|
+
return { hasClient: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let domMutations = 0;
|
|
155
|
+
let directBodyAccess = 0;
|
|
156
|
+
let heavySelectors = 0;
|
|
157
|
+
|
|
158
|
+
const clientFile = pkg.jsFiles.find(f => f.path === pkg.entries.client);
|
|
159
|
+
if (!clientFile) {
|
|
160
|
+
return { hasClient: true, analyzed: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const patterns = {
|
|
164
|
+
mutations: [
|
|
165
|
+
/\bappendChild\b/g,
|
|
166
|
+
/\binsertBefore\b/g,
|
|
167
|
+
/\bremoveChild\b/g,
|
|
168
|
+
/\binnerHTML\s*=/g,
|
|
169
|
+
/\bcloneNode\b/g
|
|
170
|
+
],
|
|
171
|
+
bodyAccess: [
|
|
172
|
+
/document\s*\.\s*body/g,
|
|
173
|
+
/document\s*\.\s*documentElement/g
|
|
174
|
+
],
|
|
175
|
+
heavySelectors: [
|
|
176
|
+
/querySelectorAll\s*\(\s*['"]\*['"]/g, // Select all
|
|
177
|
+
/getElementsByTagName\s*\(\s*['"]\*['"]/g
|
|
178
|
+
]
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const view = clientFile.masked ?? clientFile.text;
|
|
182
|
+
for (const pattern of patterns.mutations) {
|
|
183
|
+
const matches = view.match(pattern);
|
|
184
|
+
domMutations += matches ? matches.length : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const pattern of patterns.bodyAccess) {
|
|
188
|
+
const matches = view.match(pattern);
|
|
189
|
+
directBodyAccess += matches ? matches.length : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const pattern of patterns.heavySelectors) {
|
|
193
|
+
const matches = view.match(pattern);
|
|
194
|
+
heavySelectors += matches ? matches.length : 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
hasClient: true,
|
|
199
|
+
analyzed: true,
|
|
200
|
+
domMutations,
|
|
201
|
+
directBodyAccess,
|
|
202
|
+
heavySelectors,
|
|
203
|
+
risk: (directBodyAccess > 0 || heavySelectors > 0) ? 'high' :
|
|
204
|
+
(domMutations > 10) ? 'medium' : 'low'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detect network call patterns.
|
|
210
|
+
*/
|
|
211
|
+
function detectNetworkCalls(pkg) {
|
|
212
|
+
const calls = [];
|
|
213
|
+
const externalDomains = new Set();
|
|
214
|
+
|
|
215
|
+
const fetchPattern = /\bfetch\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
216
|
+
const axiosPattern = /axios\s*\.\s*(?:get|post|put|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
217
|
+
|
|
218
|
+
for (const file of pkg.jsFiles) {
|
|
219
|
+
// The masked view blanks comments, so a URL documented in a comment is not
|
|
220
|
+
// counted as a request.
|
|
221
|
+
const view = file.masked ?? file.text;
|
|
222
|
+
let m;
|
|
223
|
+
|
|
224
|
+
while ((m = fetchPattern.exec(view)) !== null) {
|
|
225
|
+
const url = m[1];
|
|
226
|
+
calls.push({
|
|
227
|
+
type: 'fetch',
|
|
228
|
+
url,
|
|
229
|
+
file: file.path,
|
|
230
|
+
line: lineOfIndex(file.text, m.index)
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
234
|
+
try {
|
|
235
|
+
const domain = new URL(url).hostname;
|
|
236
|
+
externalDomains.add(domain);
|
|
237
|
+
} catch {
|
|
238
|
+
// Invalid URL
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
while ((m = axiosPattern.exec(view)) !== null) {
|
|
244
|
+
const url = m[1];
|
|
245
|
+
calls.push({
|
|
246
|
+
type: 'axios',
|
|
247
|
+
url,
|
|
248
|
+
file: file.path,
|
|
249
|
+
line: lineOfIndex(file.text, m.index)
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
253
|
+
try {
|
|
254
|
+
const domain = new URL(url).hostname;
|
|
255
|
+
externalDomains.add(domain);
|
|
256
|
+
} catch {
|
|
257
|
+
// Invalid URL
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
total: calls.length,
|
|
265
|
+
externalDomains: Array.from(externalDomains),
|
|
266
|
+
calls: calls.slice(0, 10), // Sample
|
|
267
|
+
risk: externalDomains.size > 3 ? 'high' :
|
|
268
|
+
externalDomains.size > 0 ? 'medium' : 'low'
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Detect performance-heavy patterns.
|
|
274
|
+
*/
|
|
275
|
+
function analyzeHeavyPatterns(pkg) {
|
|
276
|
+
const issues = [];
|
|
277
|
+
|
|
278
|
+
for (const file of pkg.jsFiles) {
|
|
279
|
+
const view = file.masked ?? file.text;
|
|
280
|
+
// Synchronous fs operations
|
|
281
|
+
const syncFs = [
|
|
282
|
+
/\breadFileSync\b/g,
|
|
283
|
+
/\bwriteFileSync\b/g,
|
|
284
|
+
/\bexistsSync\b/g
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
for (const pattern of syncFs) {
|
|
288
|
+
const matches = view.match(pattern);
|
|
289
|
+
if (matches && matches.length > 5) {
|
|
290
|
+
issues.push({
|
|
291
|
+
type: 'sync-fs',
|
|
292
|
+
file: file.path,
|
|
293
|
+
count: matches.length,
|
|
294
|
+
severity: 'medium'
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Large JSON.parse in loops
|
|
301
|
+
if (/for\s*\([^)]*\)\s*\{[^}]*JSON\.parse/s.test(view)) {
|
|
302
|
+
issues.push({
|
|
303
|
+
type: 'json-parse-in-loop',
|
|
304
|
+
file: file.path,
|
|
305
|
+
severity: 'high'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Recursive directory walks without limits
|
|
310
|
+
if (/readdirSync.*recursive/i.test(view)) {
|
|
311
|
+
issues.push({
|
|
312
|
+
type: 'unbounded-recursion',
|
|
313
|
+
file: file.path,
|
|
314
|
+
severity: 'high'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Heavy crypto operations
|
|
319
|
+
if (/\bcrypto\s*\.\s*(?:pbkdf2|scrypt)/g.test(view)) {
|
|
320
|
+
issues.push({
|
|
321
|
+
type: 'heavy-crypto',
|
|
322
|
+
file: file.path,
|
|
323
|
+
severity: 'medium'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return issues;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Calculate cyclomatic complexity approximation for a file */
|
|
332
|
+
function calculateFileComplexity(file) {
|
|
333
|
+
const text = file.structural ?? maskSource(file.text).structural;
|
|
334
|
+
let complexity = 1; // Base complexity
|
|
335
|
+
|
|
336
|
+
// Count decision points
|
|
337
|
+
const patterns = [
|
|
338
|
+
/\bif\s*\(/g,
|
|
339
|
+
/\belse\s+if\s*\(/g,
|
|
340
|
+
/\bfor\s*\(/g,
|
|
341
|
+
/\bwhile\s*\(/g,
|
|
342
|
+
/\bcase\s+/g,
|
|
343
|
+
/\bcatch\s*\(/g,
|
|
344
|
+
/\?\s*[^:]+:/g, // Ternary
|
|
345
|
+
/&&/g,
|
|
346
|
+
/\|\|/g
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
for (const pattern of patterns) {
|
|
350
|
+
const matches = text.match(pattern);
|
|
351
|
+
complexity += matches ? matches.length : 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return complexity;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Find maximum nesting level in code */
|
|
358
|
+
function maxNestingLevel(text) {
|
|
359
|
+
let maxDepth = 0;
|
|
360
|
+
let currentDepth = 0;
|
|
361
|
+
|
|
362
|
+
for (const char of text) {
|
|
363
|
+
if (char === '{') {
|
|
364
|
+
currentDepth++;
|
|
365
|
+
maxDepth = Math.max(maxDepth, currentDepth);
|
|
366
|
+
} else if (char === '}') {
|
|
367
|
+
currentDepth--;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return maxDepth;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Detect functions longer than 100 lines */
|
|
375
|
+
function detectLongFunctions(text) {
|
|
376
|
+
const funcStarts = [];
|
|
377
|
+
const funcPattern = /\bfunction\s+\w+|=>\s*\{|\bfunction\s*\(/g;
|
|
378
|
+
let m;
|
|
379
|
+
|
|
380
|
+
while ((m = funcPattern.exec(text)) !== null) {
|
|
381
|
+
funcStarts.push(m.index);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let longCount = 0;
|
|
385
|
+
for (const start of funcStarts) {
|
|
386
|
+
const endBrace = findMatchingBrace(text, start);
|
|
387
|
+
// Count actual lines in the body; the previous version compared character
|
|
388
|
+
// counts against a 100*80 estimate and mislabelled short functions.
|
|
389
|
+
if (text.slice(start, endBrace).split('\n').length > 100) longCount++;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return longCount;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Find matching closing brace */
|
|
396
|
+
function findMatchingBrace(text, start) {
|
|
397
|
+
const openIdx = text.indexOf('{', start);
|
|
398
|
+
if (openIdx === -1) return start;
|
|
399
|
+
|
|
400
|
+
let depth = 1;
|
|
401
|
+
let i = openIdx + 1;
|
|
402
|
+
|
|
403
|
+
while (i < text.length && depth > 0) {
|
|
404
|
+
if (text[i] === '{') depth++;
|
|
405
|
+
else if (text[i] === '}') depth--;
|
|
406
|
+
i++;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return i;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Rate complexity level */
|
|
413
|
+
function rateComplexity(lines, functions, deepNesting, longFunctions) {
|
|
414
|
+
const avgLinesPerFunc = functions > 0 ? lines / functions : 0;
|
|
415
|
+
|
|
416
|
+
if (deepNesting > 5 || longFunctions > 3 || avgLinesPerFunc > 100) {
|
|
417
|
+
return 'poor';
|
|
418
|
+
} else if (deepNesting > 2 || longFunctions > 1 || avgLinesPerFunc > 50) {
|
|
419
|
+
return 'fair';
|
|
420
|
+
} else {
|
|
421
|
+
return 'good';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Calculate overall performance score */
|
|
426
|
+
function calculatePerformanceScore(metrics) {
|
|
427
|
+
let score = 100;
|
|
428
|
+
|
|
429
|
+
// Bundle size penalty
|
|
430
|
+
const sizeMB = parseFloat(metrics.bundleSize.totalMB);
|
|
431
|
+
if (sizeMB > 5) score -= 30;
|
|
432
|
+
else if (sizeMB > 2) score -= 20;
|
|
433
|
+
else if (sizeMB > 1) score -= 10;
|
|
434
|
+
|
|
435
|
+
// Complexity penalty
|
|
436
|
+
if (metrics.complexity.maintainability === 'poor') score -= 20;
|
|
437
|
+
else if (metrics.complexity.maintainability === 'fair') score -= 10;
|
|
438
|
+
|
|
439
|
+
// DOM impact penalty
|
|
440
|
+
if (metrics.domImpact.risk === 'high') score -= 15;
|
|
441
|
+
else if (metrics.domImpact.risk === 'medium') score -= 7;
|
|
442
|
+
|
|
443
|
+
// Network calls penalty
|
|
444
|
+
if (metrics.networkCalls.risk === 'high') score -= 15;
|
|
445
|
+
else if (metrics.networkCalls.risk === 'medium') score -= 7;
|
|
446
|
+
|
|
447
|
+
// Heavy patterns penalty
|
|
448
|
+
score -= metrics.heavyDependencies.length * 5;
|
|
449
|
+
|
|
450
|
+
return Math.max(0, Math.min(100, score));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Rate performance level */
|
|
454
|
+
function ratePerformance(score) {
|
|
455
|
+
if (score >= 90) return 'excellent';
|
|
456
|
+
if (score >= 75) return 'good';
|
|
457
|
+
if (score >= 60) return 'fair';
|
|
458
|
+
if (score >= 40) return 'poor';
|
|
459
|
+
return 'critical';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Generate actionable recommendations */
|
|
463
|
+
function generateRecommendations(metrics) {
|
|
464
|
+
const recommendations = [];
|
|
465
|
+
|
|
466
|
+
if (parseFloat(metrics.bundleSize.totalMB) > 1) {
|
|
467
|
+
recommendations.push({
|
|
468
|
+
issue: 'Large bundle size',
|
|
469
|
+
suggestion: 'Consider code splitting or removing unused dependencies',
|
|
470
|
+
impact: 'high'
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (metrics.complexity.longFunctions > 0) {
|
|
475
|
+
recommendations.push({
|
|
476
|
+
issue: `${metrics.complexity.longFunctions} long functions detected`,
|
|
477
|
+
suggestion: 'Break down large functions into smaller, testable units',
|
|
478
|
+
impact: 'medium'
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (metrics.domImpact.directBodyAccess > 0) {
|
|
483
|
+
recommendations.push({
|
|
484
|
+
issue: 'Direct document.body manipulation',
|
|
485
|
+
suggestion: 'Use DSH slot system instead of direct DOM access',
|
|
486
|
+
impact: 'high'
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (metrics.networkCalls.externalDomains.length > 3) {
|
|
491
|
+
recommendations.push({
|
|
492
|
+
issue: `Calls to ${metrics.networkCalls.externalDomains.length} external domains`,
|
|
493
|
+
suggestion: 'Reduce third-party dependencies or batch requests',
|
|
494
|
+
impact: 'medium'
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (metrics.heavyDependencies.length > 0) {
|
|
499
|
+
const highSeverity = metrics.heavyDependencies.filter(d => d.severity === 'high');
|
|
500
|
+
if (highSeverity.length > 0) {
|
|
501
|
+
recommendations.push({
|
|
502
|
+
issue: `${highSeverity.length} performance-heavy patterns found`,
|
|
503
|
+
suggestion: 'Review identified patterns for async alternatives',
|
|
504
|
+
impact: 'high'
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return recommendations;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Find line number from character index */
|
|
513
|
+
function lineOfIndex(text, index) {
|
|
514
|
+
return text.slice(0, index).split('\n').length;
|
|
515
|
+
}
|