apex-auditor 0.2.9 → 0.3.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/README.md +3 -2
- package/dist/bin.js +2 -0
- package/dist/cli.js +75 -9
- package/dist/lighthouse-runner.js +62 -6
- package/dist/wizard-cli.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ apex-auditor audit --config apex.config.json
|
|
|
87
87
|
| `--warm-up` | Perform warm-up requests before auditing |
|
|
88
88
|
| `--open` | Auto-open HTML report in browser after audit |
|
|
89
89
|
| `--json` | Output JSON to stdout (for piping) |
|
|
90
|
+
| `--show-parallel` | Print the resolved parallel worker count before running |
|
|
90
91
|
| `--mobile-only` | Only audit mobile device configurations |
|
|
91
92
|
| `--desktop-only` | Only audit desktop device configurations |
|
|
92
93
|
|
|
@@ -97,8 +98,8 @@ apex-auditor audit --config apex.config.json
|
|
|
97
98
|
After each audit, results are saved to `.apex-auditor/`:
|
|
98
99
|
|
|
99
100
|
- `summary.json` – structured JSON results
|
|
100
|
-
- `summary.md` – Markdown table
|
|
101
|
-
- `report.html` – visual HTML report with score circles and
|
|
101
|
+
- `summary.md` – Markdown table plus a structured meta section (parallel, timings, throttling)
|
|
102
|
+
- `report.html` – visual HTML report with score circles, metrics, and a meta grid (resolved parallel, elapsed, avg/step)
|
|
102
103
|
|
|
103
104
|
---
|
|
104
105
|
|
package/dist/bin.js
CHANGED
|
@@ -35,6 +35,8 @@ function printHelp() {
|
|
|
35
35
|
" --log-level <lvl> Override Lighthouse log level: silent|error|info|verbose",
|
|
36
36
|
" --mobile-only Run audits only for 'mobile' devices defined in the config",
|
|
37
37
|
" --desktop-only Run audits only for 'desktop' devices defined in the config",
|
|
38
|
+
" --parallel <n> Override parallel workers (1-10). Default auto-tunes from CPU/memory.",
|
|
39
|
+
" --show-parallel Print the resolved parallel workers before running.",
|
|
38
40
|
].join("\n"));
|
|
39
41
|
}
|
|
40
42
|
export async function runBin(argv) {
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ function parseArgs(argv) {
|
|
|
31
31
|
let openReport = false;
|
|
32
32
|
let warmUp = false;
|
|
33
33
|
let jsonOutput = false;
|
|
34
|
+
let showParallel = false;
|
|
34
35
|
for (let i = 2; i < argv.length; i += 1) {
|
|
35
36
|
const arg = argv[i];
|
|
36
37
|
if ((arg === "--config" || arg === "-c") && i + 1 < argv.length) {
|
|
@@ -103,9 +104,12 @@ function parseArgs(argv) {
|
|
|
103
104
|
else if (arg === "--json") {
|
|
104
105
|
jsonOutput = true;
|
|
105
106
|
}
|
|
107
|
+
else if (arg === "--show-parallel") {
|
|
108
|
+
showParallel = true;
|
|
109
|
+
}
|
|
106
110
|
}
|
|
107
111
|
const finalConfigPath = configPath ?? "apex.config.json";
|
|
108
|
-
return { configPath: finalConfigPath, ci, colorMode, logLevelOverride, deviceFilter, throttlingMethodOverride, cpuSlowdownOverride, parallelOverride, openReport, warmUp, jsonOutput };
|
|
112
|
+
return { configPath: finalConfigPath, ci, colorMode, logLevelOverride, deviceFilter, throttlingMethodOverride, cpuSlowdownOverride, parallelOverride, openReport, warmUp, jsonOutput, showParallel };
|
|
109
113
|
}
|
|
110
114
|
/**
|
|
111
115
|
* Runs the ApexAuditor audit CLI.
|
|
@@ -136,13 +140,13 @@ export async function runAuditCli(argv) {
|
|
|
136
140
|
process.exitCode = 1;
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
139
|
-
const summary = await runAuditsForConfig({ config: filteredConfig, configPath });
|
|
143
|
+
const summary = await runAuditsForConfig({ config: filteredConfig, configPath, showParallel: args.showParallel });
|
|
140
144
|
const outputDir = resolve(".apex-auditor");
|
|
141
145
|
await mkdir(outputDir, { recursive: true });
|
|
142
146
|
await writeFile(resolve(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
143
|
-
const markdown = buildMarkdown(summary
|
|
147
|
+
const markdown = buildMarkdown(summary);
|
|
144
148
|
await writeFile(resolve(outputDir, "summary.md"), markdown, "utf8");
|
|
145
|
-
const html = buildHtmlReport(summary
|
|
149
|
+
const html = buildHtmlReport(summary);
|
|
146
150
|
const reportPath = resolve(outputDir, "report.html");
|
|
147
151
|
await writeFile(reportPath, html, "utf8");
|
|
148
152
|
// Open HTML report in browser if requested
|
|
@@ -160,6 +164,7 @@ export async function runAuditCli(argv) {
|
|
|
160
164
|
const consoleTable = buildConsoleTable(summary.results, useColor);
|
|
161
165
|
// eslint-disable-next-line no-console
|
|
162
166
|
console.log(consoleTable);
|
|
167
|
+
printRunMeta(summary.meta, useColor);
|
|
163
168
|
printSummaryStats(summary.results, useColor);
|
|
164
169
|
printRedIssues(summary.results);
|
|
165
170
|
printCiSummary(args, summary.results, effectiveConfig.budgets);
|
|
@@ -195,15 +200,34 @@ function filterPageDevices(page, deviceFilter) {
|
|
|
195
200
|
devices,
|
|
196
201
|
};
|
|
197
202
|
}
|
|
198
|
-
function buildMarkdown(
|
|
203
|
+
function buildMarkdown(summary) {
|
|
204
|
+
const meta = summary.meta;
|
|
205
|
+
const metaTable = [
|
|
206
|
+
"| Field | Value |",
|
|
207
|
+
"|-------|-------|",
|
|
208
|
+
`| Config | ${meta.configPath} |`,
|
|
209
|
+
`| Resolved parallel | ${meta.resolvedParallel} |`,
|
|
210
|
+
`| Warm-up | ${meta.warmUp ? "yes" : "no"} |`,
|
|
211
|
+
`| Throttling | ${meta.throttlingMethod} |`,
|
|
212
|
+
`| CPU slowdown | ${meta.cpuSlowdownMultiplier} |`,
|
|
213
|
+
`| Combos | ${meta.comboCount} |`,
|
|
214
|
+
`| Runs per combo | ${meta.runsPerCombo} |`,
|
|
215
|
+
`| Total steps | ${meta.totalSteps} |`,
|
|
216
|
+
`| Started | ${meta.startedAt} |`,
|
|
217
|
+
`| Completed | ${meta.completedAt} |`,
|
|
218
|
+
`| Elapsed | ${formatElapsedTime(meta.elapsedMs)} |`,
|
|
219
|
+
`| Avg per step | ${formatElapsedTime(meta.averageStepMs)} |`,
|
|
220
|
+
].join("\n");
|
|
199
221
|
const header = [
|
|
200
222
|
"| Label | Path | Device | P | A | BP | SEO | LCP (s) | FCP (s) | TBT (ms) | CLS | INP (ms) | Error | Top issues |",
|
|
201
223
|
"|-------|------|--------|---|---|----|-----|---------|---------|----------|-----|----------|-------|-----------|",
|
|
202
224
|
].join("\n");
|
|
203
|
-
const lines = results.map((result) => buildRow(result));
|
|
204
|
-
return `${header}\n${lines.join("\n")}`;
|
|
225
|
+
const lines = summary.results.map((result) => buildRow(result));
|
|
226
|
+
return `${metaTable}\n\n${header}\n${lines.join("\n")}`;
|
|
205
227
|
}
|
|
206
|
-
function buildHtmlReport(
|
|
228
|
+
function buildHtmlReport(summary) {
|
|
229
|
+
const results = summary.results;
|
|
230
|
+
const meta = summary.meta;
|
|
207
231
|
const timestamp = new Date().toISOString();
|
|
208
232
|
const rows = results.map((result) => buildHtmlRow(result)).join("\n");
|
|
209
233
|
return `<!DOCTYPE html>
|
|
@@ -218,6 +242,10 @@ function buildHtmlReport(results, configPath) {
|
|
|
218
242
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); padding: 2rem; }
|
|
219
243
|
h1 { margin-bottom: 0.5rem; }
|
|
220
244
|
.meta { color: #888; margin-bottom: 2rem; font-size: 0.9rem; }
|
|
245
|
+
.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
246
|
+
.meta-card { background: #16213e; border-radius: 10px; padding: 1rem; border: 1px solid #23304f; }
|
|
247
|
+
.meta-label { font-size: 0.8rem; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
248
|
+
.meta-value { font-size: 1rem; font-weight: 600; color: #e5e7eb; }
|
|
221
249
|
.cards { display: grid; gap: 1.5rem; }
|
|
222
250
|
.card { background: var(--card); border-radius: 12px; padding: 1.5rem; }
|
|
223
251
|
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid #333; padding-bottom: 1rem; }
|
|
@@ -246,7 +274,21 @@ function buildHtmlReport(results, configPath) {
|
|
|
246
274
|
</head>
|
|
247
275
|
<body>
|
|
248
276
|
<h1>ApexAuditor Report</h1>
|
|
249
|
-
<p class="meta">Generated: ${timestamp}
|
|
277
|
+
<p class="meta">Generated: ${timestamp}</p>
|
|
278
|
+
<div class="meta-grid">
|
|
279
|
+
${buildMetaCard("Config", escapeHtml(meta.configPath))}
|
|
280
|
+
${buildMetaCard("Resolved parallel", meta.resolvedParallel.toString())}
|
|
281
|
+
${buildMetaCard("Warm-up", meta.warmUp ? "Yes" : "No")}
|
|
282
|
+
${buildMetaCard("Throttling", meta.throttlingMethod)}
|
|
283
|
+
${buildMetaCard("CPU slowdown", meta.cpuSlowdownMultiplier.toString())}
|
|
284
|
+
${buildMetaCard("Combos", meta.comboCount.toString())}
|
|
285
|
+
${buildMetaCard("Runs per combo", meta.runsPerCombo.toString())}
|
|
286
|
+
${buildMetaCard("Total steps", meta.totalSteps.toString())}
|
|
287
|
+
${buildMetaCard("Elapsed", formatElapsedTime(meta.elapsedMs))}
|
|
288
|
+
${buildMetaCard("Avg / step", formatElapsedTime(meta.averageStepMs))}
|
|
289
|
+
${buildMetaCard("Started", meta.startedAt)}
|
|
290
|
+
${buildMetaCard("Completed", meta.completedAt)}
|
|
291
|
+
</div>
|
|
250
292
|
<div class="cards">
|
|
251
293
|
${rows}
|
|
252
294
|
</div>
|
|
@@ -291,6 +333,30 @@ function buildScoreCircle(label, score) {
|
|
|
291
333
|
function buildMetricBox(label, value, colorClass) {
|
|
292
334
|
return `<div class="metric"><div class="metric-value ${colorClass}">${value}</div><div class="metric-label">${label}</div></div>`;
|
|
293
335
|
}
|
|
336
|
+
function buildMetaCard(label, value) {
|
|
337
|
+
return `<div class="meta-card"><div class="meta-label">${escapeHtml(label)}</div><div class="meta-value">${escapeHtml(value)}</div></div>`;
|
|
338
|
+
}
|
|
339
|
+
function printRunMeta(meta, useColor) {
|
|
340
|
+
const rows = [
|
|
341
|
+
{ label: "Resolved parallel", value: meta.resolvedParallel.toString() },
|
|
342
|
+
{ label: "Warm-up", value: meta.warmUp ? "Yes" : "No" },
|
|
343
|
+
{ label: "Throttling", value: meta.throttlingMethod },
|
|
344
|
+
{ label: "CPU slowdown", value: meta.cpuSlowdownMultiplier.toString() },
|
|
345
|
+
{ label: "Combos", value: meta.comboCount.toString() },
|
|
346
|
+
{ label: "Runs per combo", value: meta.runsPerCombo.toString() },
|
|
347
|
+
{ label: "Total steps", value: meta.totalSteps.toString() },
|
|
348
|
+
{ label: "Elapsed", value: formatElapsedTime(meta.elapsedMs) },
|
|
349
|
+
{ label: "Avg / step", value: formatElapsedTime(meta.averageStepMs) },
|
|
350
|
+
];
|
|
351
|
+
const padLabel = (label) => label.padEnd(16, " ");
|
|
352
|
+
// eslint-disable-next-line no-console
|
|
353
|
+
console.log("\nMeta:");
|
|
354
|
+
for (const row of rows) {
|
|
355
|
+
const value = useColor ? `${ANSI_CYAN}${row.value}${ANSI_RESET}` : row.value;
|
|
356
|
+
// eslint-disable-next-line no-console
|
|
357
|
+
console.log(` ${padLabel(row.label)} ${value}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
294
360
|
function getMetricClass(value, good, warn) {
|
|
295
361
|
if (value === undefined)
|
|
296
362
|
return "";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { request as httpRequest } from "node:http";
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { cpus, freemem } from "node:os";
|
|
3
4
|
import lighthouse from "lighthouse";
|
|
4
5
|
import { launch as launchChrome } from "chrome-launcher";
|
|
5
6
|
async function createChromeSession(chromePort) {
|
|
@@ -112,9 +113,8 @@ async function fetchUrl(url) {
|
|
|
112
113
|
/**
|
|
113
114
|
* Run audits for all pages defined in the config and return a structured summary.
|
|
114
115
|
*/
|
|
115
|
-
export async function runAuditsForConfig({ config, configPath, }) {
|
|
116
|
+
export async function runAuditsForConfig({ config, configPath, showParallel, }) {
|
|
116
117
|
const runs = config.runs ?? 1;
|
|
117
|
-
const parallelCount = config.parallel ?? 1;
|
|
118
118
|
const firstPage = config.pages[0];
|
|
119
119
|
const healthCheckUrl = buildUrl({ baseUrl: config.baseUrl, path: firstPage.path, query: config.query });
|
|
120
120
|
await ensureUrlReachable(healthCheckUrl);
|
|
@@ -142,13 +142,20 @@ export async function runAuditsForConfig({ config, configPath, }) {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
const startedAtMs = Date.now();
|
|
146
|
+
const parallelCount = resolveParallelCount({ requested: config.parallel, chromePort: config.chromePort, taskCount: tasks.length });
|
|
147
|
+
if (showParallel === true) {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.log(`Resolved parallel workers: ${parallelCount}`);
|
|
150
|
+
}
|
|
145
151
|
const totalSteps = tasks.length * runs;
|
|
146
152
|
let completedSteps = 0;
|
|
147
153
|
const progressLock = { count: 0 };
|
|
148
154
|
const updateProgress = (path, device) => {
|
|
149
155
|
progressLock.count += 1;
|
|
150
156
|
completedSteps = progressLock.count;
|
|
151
|
-
|
|
157
|
+
const etaMs = computeEtaMs({ startedAtMs, completed: completedSteps, total: totalSteps });
|
|
158
|
+
logProgress({ completed: completedSteps, total: totalSteps, path, device, etaMs });
|
|
152
159
|
};
|
|
153
160
|
let results;
|
|
154
161
|
if (parallelCount <= 1 || config.chromePort !== undefined) {
|
|
@@ -159,7 +166,26 @@ export async function runAuditsForConfig({ config, configPath, }) {
|
|
|
159
166
|
// Parallel execution with multiple Chrome instances
|
|
160
167
|
results = await runParallel(tasks, parallelCount, updateProgress);
|
|
161
168
|
}
|
|
162
|
-
|
|
169
|
+
const completedAtMs = Date.now();
|
|
170
|
+
const elapsedMs = completedAtMs - startedAtMs;
|
|
171
|
+
const averageStepMs = totalSteps > 0 ? elapsedMs / totalSteps : 0;
|
|
172
|
+
return {
|
|
173
|
+
meta: {
|
|
174
|
+
configPath,
|
|
175
|
+
resolvedParallel: parallelCount,
|
|
176
|
+
totalSteps,
|
|
177
|
+
comboCount: tasks.length,
|
|
178
|
+
runsPerCombo: runs,
|
|
179
|
+
warmUp: config.warmUp === true,
|
|
180
|
+
throttlingMethod,
|
|
181
|
+
cpuSlowdownMultiplier,
|
|
182
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
183
|
+
completedAt: new Date(completedAtMs).toISOString(),
|
|
184
|
+
elapsedMs,
|
|
185
|
+
averageStepMs,
|
|
186
|
+
},
|
|
187
|
+
results,
|
|
188
|
+
};
|
|
163
189
|
}
|
|
164
190
|
async function runSequential(tasks, chromePort, updateProgress) {
|
|
165
191
|
const results = [];
|
|
@@ -242,9 +268,10 @@ function buildUrl({ baseUrl, path, query }) {
|
|
|
242
268
|
const queryPart = query && query.length > 0 ? query : "";
|
|
243
269
|
return `${cleanBase}${cleanPath}${queryPart}`;
|
|
244
270
|
}
|
|
245
|
-
function logProgress({ completed, total, path, device, }) {
|
|
271
|
+
function logProgress({ completed, total, path, device, etaMs, }) {
|
|
246
272
|
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
247
|
-
const
|
|
273
|
+
const etaText = etaMs !== undefined ? ` | ETA ${formatEta(etaMs)}` : "";
|
|
274
|
+
const message = `Running audits ${completed}/${total} (${percentage}%) – ${path} [${device}]${etaText}`;
|
|
248
275
|
if (typeof process !== "undefined" && process.stdout && typeof process.stdout.write === "function" && process.stdout.isTTY) {
|
|
249
276
|
const padded = message.padEnd(80, " ");
|
|
250
277
|
process.stdout.write(`\r${padded}`);
|
|
@@ -395,3 +422,32 @@ function averageOf(values) {
|
|
|
395
422
|
const total = defined.reduce((sum, value) => sum + value, 0);
|
|
396
423
|
return total / defined.length;
|
|
397
424
|
}
|
|
425
|
+
function resolveParallelCount({ requested, chromePort, taskCount, }) {
|
|
426
|
+
if (chromePort !== undefined) {
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
if (requested !== undefined) {
|
|
430
|
+
return requested;
|
|
431
|
+
}
|
|
432
|
+
const cpuBased = Math.max(1, Math.min(6, Math.floor(cpus().length / 2)));
|
|
433
|
+
const memoryBased = Math.max(1, Math.min(6, Math.floor(freemem() / 1_500_000_000)));
|
|
434
|
+
const suggested = Math.max(1, Math.min(cpuBased, memoryBased));
|
|
435
|
+
return Math.max(1, Math.min(10, Math.min(taskCount, suggested || 1)));
|
|
436
|
+
}
|
|
437
|
+
function computeEtaMs({ startedAtMs, completed, total, }) {
|
|
438
|
+
if (completed === 0 || total === 0 || completed > total) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
442
|
+
const averagePerStep = elapsedMs / completed;
|
|
443
|
+
const remainingSteps = total - completed;
|
|
444
|
+
return Math.max(0, Math.round(averagePerStep * remainingSteps));
|
|
445
|
+
}
|
|
446
|
+
function formatEta(etaMs) {
|
|
447
|
+
const totalSeconds = Math.ceil(etaMs / 1000);
|
|
448
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
449
|
+
const seconds = totalSeconds % 60;
|
|
450
|
+
const minutesPart = minutes > 0 ? `${minutes}m ` : "";
|
|
451
|
+
const secondsPart = `${seconds}s`;
|
|
452
|
+
return `${minutesPart}${secondsPart}`.trim();
|
|
453
|
+
}
|
package/dist/wizard-cli.js
CHANGED
|
@@ -278,6 +278,7 @@ function convertRouteToPage(route) {
|
|
|
278
278
|
async function buildConfig() {
|
|
279
279
|
const profileAnswer = await ask(profileQuestion);
|
|
280
280
|
const baseAnswers = await collectBaseAnswers();
|
|
281
|
+
console.log("Tip: parallel workers auto-tune from CPU/memory. Override later with --parallel <n> or inspect with --show-parallel.");
|
|
281
282
|
const detectedPages = await maybeDetectPages(profileAnswer.profile);
|
|
282
283
|
const pages = await collectPages(detectedPages);
|
|
283
284
|
return {
|