apex-auditor 0.2.2 → 0.2.4

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 (2) hide show
  1. package/dist/cli.js +125 -26
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,6 +6,16 @@ const ANSI_RESET = "\u001B[0m";
6
6
  const ANSI_RED = "\u001B[31m";
7
7
  const ANSI_YELLOW = "\u001B[33m";
8
8
  const ANSI_GREEN = "\u001B[32m";
9
+ const ANSI_CYAN = "\u001B[36m";
10
+ const ANSI_BLUE = "\u001B[34m";
11
+ const LCP_GOOD_MS = 2500;
12
+ const LCP_WARN_MS = 4000;
13
+ const FCP_GOOD_MS = 1800;
14
+ const FCP_WARN_MS = 3000;
15
+ const TBT_GOOD_MS = 200;
16
+ const TBT_WARN_MS = 600;
17
+ const CLS_GOOD = 0.1;
18
+ const CLS_WARN = 0.25;
9
19
  function parseArgs(argv) {
10
20
  let configPath;
11
21
  let ci = false;
@@ -70,11 +80,12 @@ export async function runAuditCli(argv) {
70
80
  printLowestPerformancePages(summary.results, useColor);
71
81
  const elapsedMs = Date.now() - startTimeMs;
72
82
  const elapsedText = formatElapsedTime(elapsedMs);
83
+ const elapsedDisplay = useColor ? `${ANSI_CYAN}${elapsedText}${ANSI_RESET}` : elapsedText;
73
84
  const runsPerTarget = effectiveConfig.runs ?? 1;
74
85
  const comboCount = summary.results.length;
75
86
  const totalRuns = comboCount * runsPerTarget;
76
87
  // eslint-disable-next-line no-console
77
- console.log(`\nCompleted in ${elapsedText} (${comboCount} page/device combinations x ${runsPerTarget} runs = ${totalRuns} Lighthouse runs).`);
88
+ console.log(`\nCompleted in ${elapsedDisplay} (${comboCount} page/device combinations x ${runsPerTarget} runs = ${totalRuns} Lighthouse runs).`);
78
89
  }
79
90
  function buildMarkdown(results) {
80
91
  const header = [
@@ -89,7 +100,16 @@ function buildConsoleTable(results, useColor) {
89
100
  "| Label | Path | Device | P | A | BP | SEO | LCP (s) | FCP (s) | TBT (ms) | CLS |",
90
101
  "|-------|------|--------|---|---|----|-----|---------|---------|----------|-----|",
91
102
  ].join("\n");
92
- const rows = results.map((result) => buildConsoleRow(result, useColor));
103
+ const rows = [];
104
+ let previousKey;
105
+ for (const result of results) {
106
+ const key = `${result.label}:::${result.path}`;
107
+ if (previousKey !== undefined && key !== previousKey) {
108
+ rows.push("");
109
+ }
110
+ rows.push(buildConsoleRow(result, useColor));
111
+ previousKey = key;
112
+ }
93
113
  return `${header}\n${rows.join("\n")}`;
94
114
  }
95
115
  function buildRow(result) {
@@ -104,32 +124,37 @@ function buildRow(result) {
104
124
  return `| ${result.label} | ${result.path} | ${result.device} | ${scores.performance ?? "-"} | ${scores.accessibility ?? "-"} | ${scores.bestPractices ?? "-"} | ${scores.seo ?? "-"} | ${lcpSeconds} | ${fcpSeconds} | ${tbtMs} | ${cls} | ${error} | ${issues} |`;
105
125
  }
106
126
  function buildConsoleRow(result, useColor) {
107
- const line1 = buildConsoleRowLine1(result, useColor);
108
- const line2 = buildConsoleRowLine2(result, useColor);
109
- const line3 = buildConsoleRowLine3(result);
110
- const lines = [line1];
111
- if (line2.length > 0) {
112
- lines.push(line2);
127
+ const scoreLine = buildConsoleScoreLine(result, useColor);
128
+ const metricsLine = buildConsoleMetricsLine(result, useColor);
129
+ const errorLine = buildConsoleErrorLine(result, useColor);
130
+ const issuesLine = buildConsoleIssuesLine(result);
131
+ const lines = [scoreLine, metricsLine];
132
+ if (errorLine.length > 0) {
133
+ lines.push(errorLine);
113
134
  }
114
- if (line3.length > 0) {
115
- lines.push(line3);
135
+ if (issuesLine.length > 0) {
136
+ lines.push(issuesLine);
116
137
  }
117
138
  return lines.join("\n");
118
139
  }
119
- function buildConsoleRowLine1(result, useColor) {
140
+ function buildConsoleScoreLine(result, useColor) {
120
141
  const scores = result.scores;
121
- const metrics = result.metrics;
122
- const lcpSeconds = metrics.lcpMs !== undefined ? (metrics.lcpMs / 1000).toFixed(1) : "-";
123
- const fcpSeconds = metrics.fcpMs !== undefined ? (metrics.fcpMs / 1000).toFixed(1) : "-";
124
- const tbtMs = metrics.tbtMs !== undefined ? Math.round(metrics.tbtMs).toString() : "-";
125
- const cls = metrics.cls !== undefined ? metrics.cls.toFixed(3) : "-";
126
142
  const performanceText = colourScore(scores.performance, useColor);
127
143
  const accessibilityText = colourScore(scores.accessibility, useColor);
128
144
  const bestPracticesText = colourScore(scores.bestPractices, useColor);
129
145
  const seoText = colourScore(scores.seo, useColor);
130
- return `| ${result.label} | ${result.path} | ${result.device} | ${performanceText} | ${accessibilityText} | ${bestPracticesText} | ${seoText} | ${lcpSeconds} | ${fcpSeconds} | ${tbtMs} | ${cls} |`;
146
+ const deviceText = formatDeviceLabel(result.device, useColor);
147
+ return `| ${result.label} | ${result.path} | ${deviceText} | ${performanceText} | ${accessibilityText} | ${bestPracticesText} | ${seoText} | | | | |`;
131
148
  }
132
- function buildConsoleRowLine2(result, useColor) {
149
+ function buildConsoleMetricsLine(result, useColor) {
150
+ const metrics = result.metrics;
151
+ const lcpText = formatMetricSeconds(metrics.lcpMs, LCP_GOOD_MS, LCP_WARN_MS, useColor);
152
+ const fcpText = formatMetricSeconds(metrics.fcpMs, FCP_GOOD_MS, FCP_WARN_MS, useColor);
153
+ const tbtText = formatMetricMilliseconds(metrics.tbtMs, TBT_GOOD_MS, TBT_WARN_MS, useColor);
154
+ const clsText = formatMetricRatio(metrics.cls, CLS_GOOD, CLS_WARN, useColor);
155
+ return `| | | | | | | | ${lcpText} | ${fcpText} | ${tbtText} | ${clsText} |`;
156
+ }
157
+ function buildConsoleErrorLine(result, useColor) {
133
158
  const errorCode = result.runtimeErrorCode;
134
159
  const errorMessage = result.runtimeErrorMessage;
135
160
  if (!errorCode && !errorMessage) {
@@ -139,7 +164,7 @@ function buildConsoleRowLine2(result, useColor) {
139
164
  const prefix = useColor ? `${ANSI_RED}↳ Error:${ANSI_RESET}` : "↳ Error:";
140
165
  return ` ${prefix} ${errorText}`;
141
166
  }
142
- function buildConsoleRowLine3(result) {
167
+ function buildConsoleIssuesLine(result) {
143
168
  const issues = formatTopIssues(result.opportunities);
144
169
  if (issues.length === 0) {
145
170
  return "";
@@ -150,15 +175,89 @@ function formatTopIssues(opportunities) {
150
175
  if (opportunities.length === 0) {
151
176
  return "";
152
177
  }
153
- const items = opportunities.map((opp) => {
154
- const savingsMs = opp.estimatedSavingsMs !== undefined ? `${Math.round(opp.estimatedSavingsMs)}ms` : "";
155
- const savingsBytes = opp.estimatedSavingsBytes !== undefined ? `${Math.round(opp.estimatedSavingsBytes / 1024)}KB` : "";
156
- const parts = [savingsMs, savingsBytes].filter((p) => p.length > 0);
157
- const suffix = parts.length > 0 ? ` (${parts.join(", ")})` : "";
158
- return `${opp.id}${suffix}`;
159
- });
178
+ const meaningful = opportunities.filter((opp) => hasMeaningfulSavings(opp));
179
+ const source = meaningful.length > 0 ? meaningful : opportunities;
180
+ const sorted = [...source].sort(compareOpportunitiesByImpact);
181
+ const limit = 2;
182
+ const top = sorted.slice(0, limit);
183
+ const items = top.map((opp) => formatOpportunityLabel(opp));
160
184
  return items.join("; ");
161
185
  }
186
+ function hasMeaningfulSavings(opportunity) {
187
+ const savingsMs = opportunity.estimatedSavingsMs ?? 0;
188
+ const savingsBytes = opportunity.estimatedSavingsBytes ?? 0;
189
+ return savingsMs > 0 || savingsBytes > 0;
190
+ }
191
+ function compareOpportunitiesByImpact(a, b) {
192
+ const aMs = a.estimatedSavingsMs ?? 0;
193
+ const bMs = b.estimatedSavingsMs ?? 0;
194
+ if (aMs !== bMs) {
195
+ return bMs - aMs;
196
+ }
197
+ const aBytes = a.estimatedSavingsBytes ?? 0;
198
+ const bBytes = b.estimatedSavingsBytes ?? 0;
199
+ return bBytes - aBytes;
200
+ }
201
+ function formatOpportunityLabel(opportunity) {
202
+ const savingsMs = opportunity.estimatedSavingsMs !== undefined ? `${Math.round(opportunity.estimatedSavingsMs)}ms` : "";
203
+ const savingsBytes = opportunity.estimatedSavingsBytes !== undefined
204
+ ? `${Math.round(opportunity.estimatedSavingsBytes / 1024)}KB`
205
+ : "";
206
+ const parts = [savingsMs, savingsBytes].filter((part) => part.length > 0);
207
+ const suffix = parts.length > 0 ? ` (${parts.join(", ")})` : "";
208
+ return `${opportunity.id}${suffix}`;
209
+ }
210
+ function formatMetricSeconds(valueMs, goodThresholdMs, warnThresholdMs, useColor) {
211
+ if (valueMs === undefined) {
212
+ return "-";
213
+ }
214
+ const seconds = valueMs / 1000;
215
+ const text = `${seconds.toFixed(1)}s`;
216
+ if (!useColor) {
217
+ return text;
218
+ }
219
+ const colour = selectColourForThreshold(valueMs, goodThresholdMs, warnThresholdMs);
220
+ return `${colour}${text}${ANSI_RESET}`;
221
+ }
222
+ function formatMetricMilliseconds(valueMs, goodThresholdMs, warnThresholdMs, useColor) {
223
+ if (valueMs === undefined) {
224
+ return "-";
225
+ }
226
+ const rounded = Math.round(valueMs);
227
+ const text = `${rounded}ms`;
228
+ if (!useColor) {
229
+ return text;
230
+ }
231
+ const colour = selectColourForThreshold(valueMs, goodThresholdMs, warnThresholdMs);
232
+ return `${colour}${text}${ANSI_RESET}`;
233
+ }
234
+ function formatMetricRatio(value, goodThreshold, warnThreshold, useColor) {
235
+ if (value === undefined) {
236
+ return "-";
237
+ }
238
+ const text = value.toFixed(3);
239
+ if (!useColor) {
240
+ return text;
241
+ }
242
+ const colour = selectColourForThreshold(value, goodThreshold, warnThreshold);
243
+ return `${colour}${text}${ANSI_RESET}`;
244
+ }
245
+ function selectColourForThreshold(value, goodThreshold, warnThreshold) {
246
+ if (value <= goodThreshold) {
247
+ return ANSI_GREEN;
248
+ }
249
+ if (value <= warnThreshold) {
250
+ return ANSI_YELLOW;
251
+ }
252
+ return ANSI_RED;
253
+ }
254
+ function formatDeviceLabel(device, useColor) {
255
+ if (!useColor) {
256
+ return device;
257
+ }
258
+ const colour = device === "mobile" ? ANSI_CYAN : ANSI_BLUE;
259
+ return `${colour}${device}${ANSI_RESET}`;
260
+ }
162
261
  function colourScore(score, useColor) {
163
262
  if (score === undefined) {
164
263
  return "-";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-auditor",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "description": "CLI to run structured Lighthouse audits (Performance, Accessibility, Best Practices, SEO) across routes.",
6
6
  "type": "module",