@vibecheckai/cli 3.5.0 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. package/mcp-server/tools.js +0 -495
@@ -1,1077 +0,0 @@
1
- /**
2
- * Performance Regression Detection Engine
3
- *
4
- * ═══════════════════════════════════════════════════════════════════════════════
5
- * COMPETITIVE MOAT FEATURE - Automatic Performance Regression Detection
6
- * ═══════════════════════════════════════════════════════════════════════════════
7
- *
8
- * This engine tracks Core Web Vitals and performance metrics during Reality Mode
9
- * runs, comparing against baselines to detect regressions.
10
- *
11
- * Metrics Tracked:
12
- * - LCP (Largest Contentful Paint) - Loading performance
13
- * - FID (First Input Delay) - Interactivity
14
- * - CLS (Cumulative Layout Shift) - Visual stability
15
- * - TTFB (Time to First Byte) - Server response
16
- * - FCP (First Contentful Paint) - Initial render
17
- * - TTI (Time to Interactive) - Full interactivity
18
- * - TBT (Total Blocking Time) - Main thread blocking
19
- * - Memory usage - JS heap size
20
- * - Bundle impact - Resource loading
21
- *
22
- * Features:
23
- * - Baseline comparison with configurable thresholds
24
- * - Trend analysis (degradation over time)
25
- * - Budget enforcement (fail if exceeding budgets)
26
- * - Detailed breakdowns (by resource type, domain)
27
- * - Interaction performance (click-to-response)
28
- */
29
-
30
- "use strict";
31
-
32
- const fs = require("fs");
33
- const path = require("path");
34
-
35
- // ═══════════════════════════════════════════════════════════════════════════════
36
- // PERFORMANCE THRESHOLDS (based on Google's recommendations)
37
- // ═══════════════════════════════════════════════════════════════════════════════
38
-
39
- const THRESHOLDS = {
40
- // Core Web Vitals
41
- LCP: {
42
- good: 2500, // ms - Good
43
- needsImprovement: 4000, // ms - Needs improvement
44
- poor: Infinity // ms - Poor
45
- },
46
- FID: {
47
- good: 100,
48
- needsImprovement: 300,
49
- poor: Infinity
50
- },
51
- CLS: {
52
- good: 0.1,
53
- needsImprovement: 0.25,
54
- poor: Infinity
55
- },
56
-
57
- // Additional metrics
58
- TTFB: {
59
- good: 200,
60
- needsImprovement: 500,
61
- poor: Infinity
62
- },
63
- FCP: {
64
- good: 1800,
65
- needsImprovement: 3000,
66
- poor: Infinity
67
- },
68
- TTI: {
69
- good: 3800,
70
- needsImprovement: 7300,
71
- poor: Infinity
72
- },
73
- TBT: {
74
- good: 200,
75
- needsImprovement: 600,
76
- poor: Infinity
77
- },
78
-
79
- // Memory
80
- heapSize: {
81
- good: 50 * 1024 * 1024, // 50MB
82
- needsImprovement: 100 * 1024 * 1024, // 100MB
83
- poor: Infinity
84
- },
85
-
86
- // Bundle sizes
87
- totalTransfer: {
88
- good: 1024 * 1024, // 1MB
89
- needsImprovement: 3 * 1024 * 1024, // 3MB
90
- poor: Infinity
91
- },
92
- jsSize: {
93
- good: 300 * 1024, // 300KB
94
- needsImprovement: 500 * 1024, // 500KB
95
- poor: Infinity
96
- }
97
- };
98
-
99
- // Regression thresholds (% increase that triggers alert)
100
- const REGRESSION_THRESHOLDS = {
101
- LCP: 0.2, // 20% regression
102
- FID: 0.25, // 25% regression
103
- CLS: 0.5, // 50% regression (CLS is more variable)
104
- TTFB: 0.3, // 30% regression
105
- FCP: 0.2, // 20% regression
106
- TTI: 0.2, // 20% regression
107
- TBT: 0.25, // 25% regression
108
- heapSize: 0.3, // 30% regression
109
- totalTransfer: 0.15, // 15% regression
110
- jsSize: 0.1 // 10% regression (bundle bloat is bad)
111
- };
112
-
113
- // ═══════════════════════════════════════════════════════════════════════════════
114
- // PERFORMANCE TRACKER CLASS
115
- // ═══════════════════════════════════════════════════════════════════════════════
116
-
117
- class PerformanceTracker {
118
- constructor(options = {}) {
119
- this.projectRoot = options.projectRoot || process.cwd();
120
- this.baselinePath = options.baselinePath ||
121
- path.join(this.projectRoot, ".vibecheck", "performance-baselines");
122
- this.historyPath = options.historyPath ||
123
- path.join(this.projectRoot, ".vibecheck", "performance-history");
124
- this.thresholds = { ...THRESHOLDS, ...options.thresholds };
125
- this.regressionThresholds = { ...REGRESSION_THRESHOLDS, ...options.regressionThresholds };
126
- this.page = null;
127
- this.cdpSession = null;
128
- this.metrics = [];
129
- this.resources = [];
130
- }
131
-
132
- /**
133
- * Set the Playwright page and enable performance tracking
134
- */
135
- async setPage(page) {
136
- this.page = page;
137
-
138
- // Create CDP session for detailed metrics
139
- try {
140
- this.cdpSession = await page.context().newCDPSession(page);
141
- await this.cdpSession.send("Performance.enable");
142
- } catch {
143
- // CDP not available (e.g., Firefox)
144
- this.cdpSession = null;
145
- }
146
- }
147
-
148
- /**
149
- * Start tracking for a page load
150
- */
151
- async startTracking() {
152
- this.metrics = [];
153
- this.resources = [];
154
- this.startTime = Date.now();
155
-
156
- // Track resource loading
157
- if (this.page) {
158
- this.page.on("response", this.trackResource.bind(this));
159
- }
160
- }
161
-
162
- /**
163
- * Track individual resource
164
- */
165
- async trackResource(response) {
166
- try {
167
- const request = response.request();
168
- const timing = await response.timing();
169
-
170
- this.resources.push({
171
- url: request.url(),
172
- method: request.method(),
173
- resourceType: request.resourceType(),
174
- status: response.status(),
175
- transferSize: (await response.headers())["content-length"] || 0,
176
- timing: {
177
- dns: timing?.dnsEnd - timing?.dnsStart || 0,
178
- connect: timing?.connectEnd - timing?.connectStart || 0,
179
- ssl: timing?.sslEnd - timing?.sslStart || 0,
180
- ttfb: timing?.responseStart - timing?.requestStart || 0,
181
- download: timing?.responseEnd - timing?.responseStart || 0,
182
- total: timing?.responseEnd - timing?.requestStart || 0
183
- }
184
- });
185
- } catch {
186
- // Ignore tracking errors
187
- }
188
- }
189
-
190
- /**
191
- * Collect all performance metrics
192
- */
193
- async collectMetrics() {
194
- if (!this.page) {
195
- throw new Error("Page not set. Call setPage() first.");
196
- }
197
-
198
- const metrics = {
199
- timestamp: new Date().toISOString(),
200
- url: this.page.url(),
201
-
202
- // Core Web Vitals
203
- coreWebVitals: await this.collectCoreWebVitals(),
204
-
205
- // Navigation timing
206
- navigation: await this.collectNavigationTiming(),
207
-
208
- // Memory
209
- memory: await this.collectMemoryMetrics(),
210
-
211
- // Resources
212
- resources: this.analyzeResources(),
213
-
214
- // Long tasks (main thread blocking)
215
- longTasks: await this.collectLongTasks(),
216
-
217
- // Layout shifts
218
- layoutShifts: await this.collectLayoutShifts()
219
- };
220
-
221
- this.metrics.push(metrics);
222
- return metrics;
223
- }
224
-
225
- /**
226
- * Collect Core Web Vitals
227
- */
228
- async collectCoreWebVitals() {
229
- return await this.page.evaluate(() => {
230
- return new Promise((resolve) => {
231
- const vitals = {
232
- LCP: null,
233
- FID: null,
234
- CLS: 0
235
- };
236
-
237
- // LCP
238
- new PerformanceObserver((list) => {
239
- const entries = list.getEntries();
240
- const lastEntry = entries[entries.length - 1];
241
- vitals.LCP = lastEntry?.startTime || null;
242
- }).observe({ type: "largest-contentful-paint", buffered: true });
243
-
244
- // FID (approximated with first-input if available)
245
- new PerformanceObserver((list) => {
246
- const entries = list.getEntries();
247
- if (entries.length > 0) {
248
- vitals.FID = entries[0].processingStart - entries[0].startTime;
249
- }
250
- }).observe({ type: "first-input", buffered: true });
251
-
252
- // CLS
253
- new PerformanceObserver((list) => {
254
- for (const entry of list.getEntries()) {
255
- if (!entry.hadRecentInput) {
256
- vitals.CLS += entry.value;
257
- }
258
- }
259
- }).observe({ type: "layout-shift", buffered: true });
260
-
261
- // Wait a bit for metrics to populate
262
- setTimeout(() => resolve(vitals), 1000);
263
- });
264
- });
265
- }
266
-
267
- /**
268
- * Collect navigation timing metrics
269
- */
270
- async collectNavigationTiming() {
271
- return await this.page.evaluate(() => {
272
- const nav = performance.getEntriesByType("navigation")[0];
273
- if (!nav) return null;
274
-
275
- return {
276
- TTFB: nav.responseStart - nav.requestStart,
277
- FCP: performance.getEntriesByType("paint")
278
- .find(e => e.name === "first-contentful-paint")?.startTime || null,
279
- domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
280
- load: nav.loadEventEnd - nav.startTime,
281
-
282
- // Breakdown
283
- dns: nav.domainLookupEnd - nav.domainLookupStart,
284
- tcp: nav.connectEnd - nav.connectStart,
285
- ssl: nav.secureConnectionStart > 0
286
- ? nav.connectEnd - nav.secureConnectionStart
287
- : 0,
288
- request: nav.responseStart - nav.requestStart,
289
- response: nav.responseEnd - nav.responseStart,
290
- processing: nav.domComplete - nav.responseEnd,
291
-
292
- // Transfer sizes
293
- transferSize: nav.transferSize,
294
- encodedBodySize: nav.encodedBodySize,
295
- decodedBodySize: nav.decodedBodySize
296
- };
297
- });
298
- }
299
-
300
- /**
301
- * Collect memory metrics
302
- */
303
- async collectMemoryMetrics() {
304
- return await this.page.evaluate(() => {
305
- if (performance.memory) {
306
- return {
307
- usedJSHeapSize: performance.memory.usedJSHeapSize,
308
- totalJSHeapSize: performance.memory.totalJSHeapSize,
309
- jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
310
- };
311
- }
312
- return null;
313
- });
314
- }
315
-
316
- /**
317
- * Analyze collected resources
318
- */
319
- analyzeResources() {
320
- const byType = {};
321
- let totalTransfer = 0;
322
- let totalRequests = 0;
323
-
324
- for (const resource of this.resources) {
325
- const type = resource.resourceType;
326
- const size = parseInt(resource.transferSize) || 0;
327
-
328
- if (!byType[type]) {
329
- byType[type] = { count: 0, size: 0, avgTiming: 0 };
330
- }
331
-
332
- byType[type].count++;
333
- byType[type].size += size;
334
- byType[type].avgTiming += resource.timing.total;
335
-
336
- totalTransfer += size;
337
- totalRequests++;
338
- }
339
-
340
- // Calculate averages
341
- for (const type of Object.keys(byType)) {
342
- if (byType[type].count > 0) {
343
- byType[type].avgTiming /= byType[type].count;
344
- }
345
- }
346
-
347
- return {
348
- totalRequests,
349
- totalTransfer,
350
- byType,
351
- jsSize: byType.script?.size || 0,
352
- cssSize: byType.stylesheet?.size || 0,
353
- imageSize: byType.image?.size || 0,
354
- fontSize: byType.font?.size || 0
355
- };
356
- }
357
-
358
- /**
359
- * Collect long tasks (blocking main thread)
360
- */
361
- async collectLongTasks() {
362
- return await this.page.evaluate(() => {
363
- return new Promise((resolve) => {
364
- const longTasks = [];
365
-
366
- new PerformanceObserver((list) => {
367
- for (const entry of list.getEntries()) {
368
- longTasks.push({
369
- startTime: entry.startTime,
370
- duration: entry.duration,
371
- name: entry.name
372
- });
373
- }
374
- }).observe({ type: "longtask", buffered: true });
375
-
376
- setTimeout(() => {
377
- const totalBlockingTime = longTasks
378
- .filter(t => t.duration > 50)
379
- .reduce((sum, t) => sum + (t.duration - 50), 0);
380
-
381
- resolve({
382
- count: longTasks.length,
383
- totalBlockingTime,
384
- tasks: longTasks.slice(0, 10) // Top 10
385
- });
386
- }, 500);
387
- });
388
- });
389
- }
390
-
391
- /**
392
- * Collect layout shift details
393
- */
394
- async collectLayoutShifts() {
395
- return await this.page.evaluate(() => {
396
- return new Promise((resolve) => {
397
- const shifts = [];
398
- let cumulativeScore = 0;
399
-
400
- new PerformanceObserver((list) => {
401
- for (const entry of list.getEntries()) {
402
- if (!entry.hadRecentInput) {
403
- cumulativeScore += entry.value;
404
- shifts.push({
405
- value: entry.value,
406
- startTime: entry.startTime,
407
- sources: entry.sources?.map(s => ({
408
- node: s.node?.nodeName,
409
- previousRect: s.previousRect,
410
- currentRect: s.currentRect
411
- })) || []
412
- });
413
- }
414
- }
415
- }).observe({ type: "layout-shift", buffered: true });
416
-
417
- setTimeout(() => {
418
- resolve({
419
- cumulativeScore,
420
- shiftCount: shifts.length,
421
- largestShift: Math.max(...shifts.map(s => s.value), 0),
422
- shifts: shifts.slice(0, 5) // Top 5
423
- });
424
- }, 500);
425
- });
426
- });
427
- }
428
-
429
- // ═══════════════════════════════════════════════════════════════════════════
430
- // BASELINE COMPARISON
431
- // ═══════════════════════════════════════════════════════════════════════════
432
-
433
- /**
434
- * Compare metrics against baseline
435
- */
436
- async compareWithBaseline(metrics, pageName) {
437
- const baseline = this.loadBaseline(pageName);
438
-
439
- if (!baseline) {
440
- return {
441
- hasBaseline: false,
442
- message: "No baseline found. Current metrics will be saved as baseline."
443
- };
444
- }
445
-
446
- const comparison = {
447
- hasBaseline: true,
448
- regressions: [],
449
- improvements: [],
450
- unchanged: [],
451
- details: {}
452
- };
453
-
454
- // Compare Core Web Vitals
455
- this.compareMetric(comparison, "LCP",
456
- metrics.coreWebVitals?.LCP,
457
- baseline.coreWebVitals?.LCP
458
- );
459
-
460
- this.compareMetric(comparison, "FID",
461
- metrics.coreWebVitals?.FID,
462
- baseline.coreWebVitals?.FID
463
- );
464
-
465
- this.compareMetric(comparison, "CLS",
466
- metrics.coreWebVitals?.CLS,
467
- baseline.coreWebVitals?.CLS
468
- );
469
-
470
- // Compare navigation timing
471
- this.compareMetric(comparison, "TTFB",
472
- metrics.navigation?.TTFB,
473
- baseline.navigation?.TTFB
474
- );
475
-
476
- this.compareMetric(comparison, "FCP",
477
- metrics.navigation?.FCP,
478
- baseline.navigation?.FCP
479
- );
480
-
481
- // Compare memory
482
- this.compareMetric(comparison, "heapSize",
483
- metrics.memory?.usedJSHeapSize,
484
- baseline.memory?.usedJSHeapSize
485
- );
486
-
487
- // Compare bundle sizes
488
- this.compareMetric(comparison, "totalTransfer",
489
- metrics.resources?.totalTransfer,
490
- baseline.resources?.totalTransfer
491
- );
492
-
493
- this.compareMetric(comparison, "jsSize",
494
- metrics.resources?.jsSize,
495
- baseline.resources?.jsSize
496
- );
497
-
498
- // Compare TBT
499
- this.compareMetric(comparison, "TBT",
500
- metrics.longTasks?.totalBlockingTime,
501
- baseline.longTasks?.totalBlockingTime
502
- );
503
-
504
- return comparison;
505
- }
506
-
507
- /**
508
- * Compare individual metric
509
- */
510
- compareMetric(comparison, name, current, baseline) {
511
- if (current === null || current === undefined ||
512
- baseline === null || baseline === undefined) {
513
- return;
514
- }
515
-
516
- const threshold = this.regressionThresholds[name] || 0.2;
517
- const change = (current - baseline) / baseline;
518
- const changePercent = (change * 100).toFixed(1);
519
-
520
- const detail = {
521
- name,
522
- current,
523
- baseline,
524
- change: changePercent + "%",
525
- status: "unchanged"
526
- };
527
-
528
- // Check if regression
529
- if (change > threshold) {
530
- detail.status = "regression";
531
- detail.severity = this.getSeverity(name, current);
532
- comparison.regressions.push(detail);
533
- }
534
- // Check if improvement
535
- else if (change < -threshold) {
536
- detail.status = "improvement";
537
- comparison.improvements.push(detail);
538
- }
539
- else {
540
- comparison.unchanged.push(detail);
541
- }
542
-
543
- comparison.details[name] = detail;
544
- }
545
-
546
- /**
547
- * Get severity based on absolute value vs thresholds
548
- */
549
- getSeverity(metricName, value) {
550
- const threshold = this.thresholds[metricName];
551
- if (!threshold) return "WARN";
552
-
553
- if (value <= threshold.good) return "INFO";
554
- if (value <= threshold.needsImprovement) return "WARN";
555
- return "BLOCK";
556
- }
557
-
558
- // ═══════════════════════════════════════════════════════════════════════════
559
- // BASELINE MANAGEMENT
560
- // ═══════════════════════════════════════════════════════════════════════════
561
-
562
- /**
563
- * Save metrics as baseline
564
- */
565
- saveBaseline(metrics, pageName) {
566
- if (!fs.existsSync(this.baselinePath)) {
567
- fs.mkdirSync(this.baselinePath, { recursive: true });
568
- }
569
-
570
- const fileName = this.sanitizeName(pageName) + ".json";
571
- const filePath = path.join(this.baselinePath, fileName);
572
-
573
- fs.writeFileSync(filePath, JSON.stringify({
574
- ...metrics,
575
- savedAt: new Date().toISOString()
576
- }, null, 2));
577
-
578
- return filePath;
579
- }
580
-
581
- /**
582
- * Load baseline metrics
583
- */
584
- loadBaseline(pageName) {
585
- const fileName = this.sanitizeName(pageName) + ".json";
586
- const filePath = path.join(this.baselinePath, fileName);
587
-
588
- if (!fs.existsSync(filePath)) {
589
- return null;
590
- }
591
-
592
- try {
593
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
594
- } catch {
595
- return null;
596
- }
597
- }
598
-
599
- /**
600
- * Save to history for trend analysis
601
- */
602
- saveToHistory(metrics, pageName) {
603
- const historyDir = path.join(this.historyPath, this.sanitizeName(pageName));
604
-
605
- if (!fs.existsSync(historyDir)) {
606
- fs.mkdirSync(historyDir, { recursive: true });
607
- }
608
-
609
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
610
- const fileName = `${timestamp}.json`;
611
- const filePath = path.join(historyDir, fileName);
612
-
613
- fs.writeFileSync(filePath, JSON.stringify(metrics, null, 2));
614
-
615
- // Keep only last 30 entries
616
- this.pruneHistory(historyDir, 30);
617
-
618
- return filePath;
619
- }
620
-
621
- /**
622
- * Prune old history entries
623
- */
624
- pruneHistory(historyDir, keepCount) {
625
- const files = fs.readdirSync(historyDir)
626
- .filter(f => f.endsWith(".json"))
627
- .sort()
628
- .reverse();
629
-
630
- for (const file of files.slice(keepCount)) {
631
- fs.unlinkSync(path.join(historyDir, file));
632
- }
633
- }
634
-
635
- /**
636
- * Get trend analysis
637
- */
638
- getTrendAnalysis(pageName) {
639
- const historyDir = path.join(this.historyPath, this.sanitizeName(pageName));
640
-
641
- if (!fs.existsSync(historyDir)) {
642
- return null;
643
- }
644
-
645
- const files = fs.readdirSync(historyDir)
646
- .filter(f => f.endsWith(".json"))
647
- .sort()
648
- .reverse()
649
- .slice(0, 10); // Last 10 runs
650
-
651
- if (files.length < 2) {
652
- return null;
653
- }
654
-
655
- const history = files.map(f => {
656
- const data = JSON.parse(fs.readFileSync(path.join(historyDir, f), "utf8"));
657
- return {
658
- timestamp: data.timestamp,
659
- LCP: data.coreWebVitals?.LCP,
660
- FID: data.coreWebVitals?.FID,
661
- CLS: data.coreWebVitals?.CLS,
662
- TTFB: data.navigation?.TTFB,
663
- jsSize: data.resources?.jsSize,
664
- heapSize: data.memory?.usedJSHeapSize
665
- };
666
- });
667
-
668
- // Calculate trends
669
- const trends = {};
670
- const metrics = ["LCP", "FID", "CLS", "TTFB", "jsSize", "heapSize"];
671
-
672
- for (const metric of metrics) {
673
- const values = history.map(h => h[metric]).filter(v => v !== null && v !== undefined);
674
- if (values.length >= 2) {
675
- const recent = values.slice(0, 3).reduce((a, b) => a + b, 0) / Math.min(3, values.length);
676
- const older = values.slice(-3).reduce((a, b) => a + b, 0) / Math.min(3, values.length);
677
- const change = ((recent - older) / older) * 100;
678
-
679
- trends[metric] = {
680
- direction: change > 5 ? "degrading" : change < -5 ? "improving" : "stable",
681
- changePercent: change.toFixed(1),
682
- recentAvg: recent,
683
- historicalAvg: older
684
- };
685
- }
686
- }
687
-
688
- return {
689
- sampleCount: history.length,
690
- timeRange: {
691
- oldest: history[history.length - 1]?.timestamp,
692
- newest: history[0]?.timestamp
693
- },
694
- trends,
695
- history: history.slice(0, 5) // Last 5 for display
696
- };
697
- }
698
-
699
- // ═══════════════════════════════════════════════════════════════════════════
700
- // INTERACTION PERFORMANCE
701
- // ═══════════════════════════════════════════════════════════════════════════
702
-
703
- /**
704
- * Measure interaction performance (click-to-response)
705
- */
706
- async measureInteraction(selector, action = "click") {
707
- if (!this.page) {
708
- throw new Error("Page not set");
709
- }
710
-
711
- const startTime = Date.now();
712
-
713
- // Track network requests during interaction
714
- const requests = [];
715
- const requestHandler = (request) => {
716
- requests.push({
717
- url: request.url(),
718
- type: request.resourceType(),
719
- startTime: Date.now() - startTime
720
- });
721
- };
722
-
723
- this.page.on("request", requestHandler);
724
-
725
- // Track DOM changes
726
- const domChangePromise = this.page.evaluate(() => {
727
- return new Promise((resolve) => {
728
- const observer = new MutationObserver((mutations) => {
729
- observer.disconnect();
730
- resolve({
731
- changeCount: mutations.length,
732
- timestamp: performance.now()
733
- });
734
- });
735
-
736
- observer.observe(document.body, {
737
- childList: true,
738
- subtree: true,
739
- attributes: true
740
- });
741
-
742
- // Timeout after 5s
743
- setTimeout(() => {
744
- observer.disconnect();
745
- resolve({ changeCount: 0, timeout: true });
746
- }, 5000);
747
- });
748
- });
749
-
750
- // Perform interaction
751
- const element = await this.page.$(selector);
752
- if (!element) {
753
- throw new Error(`Element not found: ${selector}`);
754
- }
755
-
756
- if (action === "click") {
757
- await element.click();
758
- } else if (action === "hover") {
759
- await element.hover();
760
- }
761
-
762
- // Wait for response
763
- const domChange = await domChangePromise;
764
- const endTime = Date.now();
765
-
766
- this.page.off("request", requestHandler);
767
-
768
- return {
769
- selector,
770
- action,
771
- duration: endTime - startTime,
772
- domChangeTime: domChange.timestamp,
773
- domChangeCount: domChange.changeCount,
774
- networkRequests: requests.length,
775
- requests: requests.slice(0, 5),
776
- rating: this.rateInteractionSpeed(endTime - startTime)
777
- };
778
- }
779
-
780
- /**
781
- * Rate interaction speed
782
- */
783
- rateInteractionSpeed(duration) {
784
- if (duration < 100) return { rating: "instant", color: "green" };
785
- if (duration < 300) return { rating: "fast", color: "green" };
786
- if (duration < 1000) return { rating: "acceptable", color: "yellow" };
787
- if (duration < 3000) return { rating: "slow", color: "orange" };
788
- return { rating: "very slow", color: "red" };
789
- }
790
-
791
- // ═══════════════════════════════════════════════════════════════════════════
792
- // BUDGETS
793
- // ═══════════════════════════════════════════════════════════════════════════
794
-
795
- /**
796
- * Check against performance budgets
797
- */
798
- checkBudgets(metrics, budgets = {}) {
799
- const defaultBudgets = {
800
- LCP: 2500,
801
- FID: 100,
802
- CLS: 0.1,
803
- TTFB: 200,
804
- totalTransfer: 1024 * 1024, // 1MB
805
- jsSize: 300 * 1024, // 300KB
806
- requestCount: 50
807
- };
808
-
809
- const effectiveBudgets = { ...defaultBudgets, ...budgets };
810
- const violations = [];
811
- const passing = [];
812
-
813
- // Check each budget
814
- if (metrics.coreWebVitals?.LCP > effectiveBudgets.LCP) {
815
- violations.push({
816
- metric: "LCP",
817
- budget: effectiveBudgets.LCP,
818
- actual: metrics.coreWebVitals.LCP,
819
- overage: ((metrics.coreWebVitals.LCP / effectiveBudgets.LCP - 1) * 100).toFixed(0) + "%"
820
- });
821
- } else {
822
- passing.push({ metric: "LCP", within: true });
823
- }
824
-
825
- if (metrics.coreWebVitals?.CLS > effectiveBudgets.CLS) {
826
- violations.push({
827
- metric: "CLS",
828
- budget: effectiveBudgets.CLS,
829
- actual: metrics.coreWebVitals.CLS,
830
- overage: ((metrics.coreWebVitals.CLS / effectiveBudgets.CLS - 1) * 100).toFixed(0) + "%"
831
- });
832
- } else {
833
- passing.push({ metric: "CLS", within: true });
834
- }
835
-
836
- if (metrics.resources?.totalTransfer > effectiveBudgets.totalTransfer) {
837
- violations.push({
838
- metric: "Total Transfer",
839
- budget: this.formatBytes(effectiveBudgets.totalTransfer),
840
- actual: this.formatBytes(metrics.resources.totalTransfer),
841
- overage: ((metrics.resources.totalTransfer / effectiveBudgets.totalTransfer - 1) * 100).toFixed(0) + "%"
842
- });
843
- } else {
844
- passing.push({ metric: "Total Transfer", within: true });
845
- }
846
-
847
- if (metrics.resources?.jsSize > effectiveBudgets.jsSize) {
848
- violations.push({
849
- metric: "JS Size",
850
- budget: this.formatBytes(effectiveBudgets.jsSize),
851
- actual: this.formatBytes(metrics.resources.jsSize),
852
- overage: ((metrics.resources.jsSize / effectiveBudgets.jsSize - 1) * 100).toFixed(0) + "%"
853
- });
854
- } else {
855
- passing.push({ metric: "JS Size", within: true });
856
- }
857
-
858
- if (metrics.resources?.totalRequests > effectiveBudgets.requestCount) {
859
- violations.push({
860
- metric: "Request Count",
861
- budget: effectiveBudgets.requestCount,
862
- actual: metrics.resources.totalRequests,
863
- overage: ((metrics.resources.totalRequests / effectiveBudgets.requestCount - 1) * 100).toFixed(0) + "%"
864
- });
865
- } else {
866
- passing.push({ metric: "Request Count", within: true });
867
- }
868
-
869
- return {
870
- passed: violations.length === 0,
871
- violations,
872
- passing,
873
- summary: `${passing.length}/${passing.length + violations.length} budgets met`
874
- };
875
- }
876
-
877
- // ═══════════════════════════════════════════════════════════════════════════
878
- // REPORTING
879
- // ═══════════════════════════════════════════════════════════════════════════
880
-
881
- /**
882
- * Generate performance report
883
- */
884
- generateReport(metrics, comparison, budgetCheck) {
885
- return {
886
- timestamp: new Date().toISOString(),
887
- url: metrics.url,
888
-
889
- // Scores
890
- scores: this.calculateScores(metrics),
891
-
892
- // Core Web Vitals summary
893
- coreWebVitals: {
894
- LCP: this.formatMetric("LCP", metrics.coreWebVitals?.LCP),
895
- FID: this.formatMetric("FID", metrics.coreWebVitals?.FID),
896
- CLS: this.formatMetric("CLS", metrics.coreWebVitals?.CLS)
897
- },
898
-
899
- // Comparison with baseline
900
- comparison: comparison?.hasBaseline ? {
901
- regressions: comparison.regressions,
902
- improvements: comparison.improvements,
903
- summary: `${comparison.regressions.length} regressions, ${comparison.improvements.length} improvements`
904
- } : null,
905
-
906
- // Budget check
907
- budgets: budgetCheck,
908
-
909
- // Detailed metrics
910
- details: {
911
- navigation: metrics.navigation,
912
- resources: metrics.resources,
913
- memory: metrics.memory,
914
- longTasks: metrics.longTasks,
915
- layoutShifts: metrics.layoutShifts
916
- },
917
-
918
- // Recommendations
919
- recommendations: this.generateRecommendations(metrics)
920
- };
921
- }
922
-
923
- /**
924
- * Calculate overall scores
925
- */
926
- calculateScores(metrics) {
927
- const scores = {};
928
-
929
- // LCP score (0-100)
930
- const lcp = metrics.coreWebVitals?.LCP;
931
- if (lcp !== null) {
932
- if (lcp <= 2500) scores.LCP = 100;
933
- else if (lcp <= 4000) scores.LCP = Math.round(100 - ((lcp - 2500) / 1500) * 50);
934
- else scores.LCP = Math.max(0, Math.round(50 - ((lcp - 4000) / 4000) * 50));
935
- }
936
-
937
- // CLS score
938
- const cls = metrics.coreWebVitals?.CLS;
939
- if (cls !== null) {
940
- if (cls <= 0.1) scores.CLS = 100;
941
- else if (cls <= 0.25) scores.CLS = Math.round(100 - ((cls - 0.1) / 0.15) * 50);
942
- else scores.CLS = Math.max(0, Math.round(50 - (cls - 0.25) * 100));
943
- }
944
-
945
- // Overall score
946
- const validScores = Object.values(scores).filter(s => s !== undefined);
947
- scores.overall = validScores.length > 0
948
- ? Math.round(validScores.reduce((a, b) => a + b, 0) / validScores.length)
949
- : null;
950
-
951
- return scores;
952
- }
953
-
954
- /**
955
- * Format metric for display
956
- */
957
- formatMetric(name, value) {
958
- if (value === null || value === undefined) {
959
- return { value: "N/A", rating: "unknown" };
960
- }
961
-
962
- const threshold = this.thresholds[name];
963
- let rating = "unknown";
964
-
965
- if (threshold) {
966
- if (value <= threshold.good) rating = "good";
967
- else if (value <= threshold.needsImprovement) rating = "needs-improvement";
968
- else rating = "poor";
969
- }
970
-
971
- let formatted = value;
972
- if (name === "CLS") {
973
- formatted = value.toFixed(3);
974
- } else if (name === "LCP" || name === "FID" || name === "TTFB" || name === "FCP") {
975
- formatted = value.toFixed(0) + "ms";
976
- }
977
-
978
- return { value: formatted, rating };
979
- }
980
-
981
- /**
982
- * Generate recommendations based on metrics
983
- */
984
- generateRecommendations(metrics) {
985
- const recommendations = [];
986
-
987
- // LCP recommendations
988
- const lcp = metrics.coreWebVitals?.LCP;
989
- if (lcp > 4000) {
990
- recommendations.push({
991
- metric: "LCP",
992
- severity: "high",
993
- message: "Largest Contentful Paint is poor",
994
- suggestions: [
995
- "Optimize server response time",
996
- "Remove render-blocking resources",
997
- "Optimize images with lazy loading",
998
- "Use a CDN for static assets"
999
- ]
1000
- });
1001
- }
1002
-
1003
- // CLS recommendations
1004
- const cls = metrics.coreWebVitals?.CLS;
1005
- if (cls > 0.25) {
1006
- recommendations.push({
1007
- metric: "CLS",
1008
- severity: "high",
1009
- message: "Cumulative Layout Shift is poor",
1010
- suggestions: [
1011
- "Set explicit dimensions on images and videos",
1012
- "Reserve space for dynamic content",
1013
- "Avoid inserting content above existing content",
1014
- "Use CSS transform for animations"
1015
- ]
1016
- });
1017
- }
1018
-
1019
- // JS size recommendations
1020
- const jsSize = metrics.resources?.jsSize;
1021
- if (jsSize > 500 * 1024) {
1022
- recommendations.push({
1023
- metric: "JS Size",
1024
- severity: "medium",
1025
- message: `JavaScript bundle is large (${this.formatBytes(jsSize)})`,
1026
- suggestions: [
1027
- "Enable code splitting",
1028
- "Remove unused dependencies",
1029
- "Use tree shaking",
1030
- "Consider lazy loading non-critical JS"
1031
- ]
1032
- });
1033
- }
1034
-
1035
- // TBT recommendations
1036
- const tbt = metrics.longTasks?.totalBlockingTime;
1037
- if (tbt > 300) {
1038
- recommendations.push({
1039
- metric: "TBT",
1040
- severity: "medium",
1041
- message: `High Total Blocking Time (${tbt.toFixed(0)}ms)`,
1042
- suggestions: [
1043
- "Break up long tasks",
1044
- "Defer non-critical JavaScript",
1045
- "Use web workers for heavy computation",
1046
- "Optimize third-party scripts"
1047
- ]
1048
- });
1049
- }
1050
-
1051
- return recommendations;
1052
- }
1053
-
1054
- // ═══════════════════════════════════════════════════════════════════════════
1055
- // UTILITIES
1056
- // ═══════════════════════════════════════════════════════════════════════════
1057
-
1058
- sanitizeName(name) {
1059
- return name.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
1060
- }
1061
-
1062
- formatBytes(bytes) {
1063
- if (bytes < 1024) return bytes + " B";
1064
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
1065
- return (bytes / (1024 * 1024)).toFixed(2) + " MB";
1066
- }
1067
- }
1068
-
1069
- // ═══════════════════════════════════════════════════════════════════════════════
1070
- // EXPORTS
1071
- // ═══════════════════════════════════════════════════════════════════════════════
1072
-
1073
- module.exports = {
1074
- PerformanceTracker,
1075
- THRESHOLDS,
1076
- REGRESSION_THRESHOLDS
1077
- };