apex-auditor 0.1.6 → 0.2.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/dist/bin.js +15 -2
- package/dist/cli.js +138 -11
- package/dist/config.js +85 -0
- package/dist/quickstart-cli.js +118 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runAuditCli } from "./cli.js";
|
|
3
3
|
import { runWizardCli } from "./wizard-cli.js";
|
|
4
|
+
import { runQuickstartCli } from "./quickstart-cli.js";
|
|
4
5
|
function parseBinArgs(argv) {
|
|
5
6
|
const rawCommand = argv[2];
|
|
6
7
|
if (rawCommand === undefined || rawCommand === "help" || rawCommand === "--help" || rawCommand === "-h") {
|
|
7
8
|
return { command: "help", argv };
|
|
8
9
|
}
|
|
9
|
-
if (rawCommand === "audit" || rawCommand === "wizard") {
|
|
10
|
+
if (rawCommand === "audit" || rawCommand === "wizard" || rawCommand === "quickstart") {
|
|
10
11
|
const commandArgv = ["node", "apex-auditor", ...argv.slice(3)];
|
|
11
12
|
return { command: rawCommand, argv: commandArgv };
|
|
12
13
|
}
|
|
@@ -17,13 +18,21 @@ function printHelp() {
|
|
|
17
18
|
"ApexAuditor CLI",
|
|
18
19
|
"",
|
|
19
20
|
"Usage:",
|
|
21
|
+
" apex-auditor quickstart --base-url <url> [--project-root <path>]",
|
|
20
22
|
" apex-auditor wizard [--config <path>]",
|
|
21
|
-
" apex-auditor audit [--config <path>]",
|
|
23
|
+
" apex-auditor audit [--config <path>] [--ci] [--no-color|--color] [--log-level <level>]",
|
|
22
24
|
"",
|
|
23
25
|
"Commands:",
|
|
26
|
+
" quickstart Detect routes and run a one-off audit with sensible defaults",
|
|
24
27
|
" wizard Run interactive config wizard",
|
|
25
28
|
" audit Run Lighthouse audits using apex.config.json",
|
|
26
29
|
" help Show this help message",
|
|
30
|
+
"",
|
|
31
|
+
"Options (audit):",
|
|
32
|
+
" --ci Enable CI mode with budgets and non-zero exit code on failure",
|
|
33
|
+
" --no-color Disable ANSI colours in console output (default in CI mode)",
|
|
34
|
+
" --color Force ANSI colours in console output",
|
|
35
|
+
" --log-level <lvl> Override Lighthouse log level: silent|error|info|verbose",
|
|
27
36
|
].join("\n"));
|
|
28
37
|
}
|
|
29
38
|
export async function runBin(argv) {
|
|
@@ -32,6 +41,10 @@ export async function runBin(argv) {
|
|
|
32
41
|
printHelp();
|
|
33
42
|
return;
|
|
34
43
|
}
|
|
44
|
+
if (parsed.command === "quickstart") {
|
|
45
|
+
await runQuickstartCli(parsed.argv);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
35
48
|
if (parsed.command === "audit") {
|
|
36
49
|
await runAuditCli(parsed.argv);
|
|
37
50
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -8,15 +8,37 @@ const ANSI_YELLOW = "\u001B[33m";
|
|
|
8
8
|
const ANSI_GREEN = "\u001B[32m";
|
|
9
9
|
function parseArgs(argv) {
|
|
10
10
|
let configPath;
|
|
11
|
+
let ci = false;
|
|
12
|
+
let colorMode = "auto";
|
|
13
|
+
let logLevelOverride;
|
|
11
14
|
for (let i = 2; i < argv.length; i += 1) {
|
|
12
15
|
const arg = argv[i];
|
|
13
16
|
if ((arg === "--config" || arg === "-c") && i + 1 < argv.length) {
|
|
14
17
|
configPath = argv[i + 1];
|
|
15
18
|
i += 1;
|
|
16
19
|
}
|
|
20
|
+
else if (arg === "--ci") {
|
|
21
|
+
ci = true;
|
|
22
|
+
}
|
|
23
|
+
else if (arg === "--no-color") {
|
|
24
|
+
colorMode = "off";
|
|
25
|
+
}
|
|
26
|
+
else if (arg === "--color") {
|
|
27
|
+
colorMode = "on";
|
|
28
|
+
}
|
|
29
|
+
else if (arg === "--log-level" && i + 1 < argv.length) {
|
|
30
|
+
const value = argv[i + 1];
|
|
31
|
+
if (value === "silent" || value === "error" || value === "info" || value === "verbose") {
|
|
32
|
+
logLevelOverride = value;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new Error(`Invalid --log-level value: ${value}`);
|
|
36
|
+
}
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
17
39
|
}
|
|
18
40
|
const finalConfigPath = configPath ?? "apex.config.json";
|
|
19
|
-
return { configPath: finalConfigPath };
|
|
41
|
+
return { configPath: finalConfigPath, ci, colorMode, logLevelOverride };
|
|
20
42
|
}
|
|
21
43
|
/**
|
|
22
44
|
* Runs the ApexAuditor audit CLI.
|
|
@@ -26,17 +48,24 @@ function parseArgs(argv) {
|
|
|
26
48
|
export async function runAuditCli(argv) {
|
|
27
49
|
const args = parseArgs(argv);
|
|
28
50
|
const { configPath, config } = await loadConfig({ configPath: args.configPath });
|
|
29
|
-
const
|
|
51
|
+
const effectiveLogLevel = args.logLevelOverride ?? config.logLevel;
|
|
52
|
+
const effectiveConfig = {
|
|
53
|
+
...config,
|
|
54
|
+
logLevel: effectiveLogLevel,
|
|
55
|
+
};
|
|
56
|
+
const summary = await runAuditsForConfig({ config: effectiveConfig, configPath });
|
|
30
57
|
const outputDir = resolve(".apex-auditor");
|
|
31
58
|
await mkdir(outputDir, { recursive: true });
|
|
32
59
|
await writeFile(resolve(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
33
60
|
const markdown = buildMarkdown(summary.results);
|
|
34
61
|
await writeFile(resolve(outputDir, "summary.md"), markdown, "utf8");
|
|
35
62
|
// Also echo a compact, colourised table to stdout for quick viewing.
|
|
36
|
-
const
|
|
63
|
+
const useColor = shouldUseColor(args.ci, args.colorMode);
|
|
64
|
+
const consoleTable = buildConsoleTable(summary.results, useColor);
|
|
37
65
|
// eslint-disable-next-line no-console
|
|
38
66
|
console.log(consoleTable);
|
|
39
67
|
printRedIssues(summary.results);
|
|
68
|
+
printCiSummary(args, summary.results, effectiveConfig.budgets);
|
|
40
69
|
}
|
|
41
70
|
function buildMarkdown(results) {
|
|
42
71
|
const header = [
|
|
@@ -46,12 +75,12 @@ function buildMarkdown(results) {
|
|
|
46
75
|
const lines = results.map((result) => buildRow(result));
|
|
47
76
|
return `${header}\n${lines.join("\n")}`;
|
|
48
77
|
}
|
|
49
|
-
function buildConsoleTable(results) {
|
|
78
|
+
function buildConsoleTable(results, useColor) {
|
|
50
79
|
const header = [
|
|
51
80
|
"| Label | Path | Device | P | A | BP | SEO | LCP (s) | FCP (s) | TBT (ms) | CLS | Error | Top issues |",
|
|
52
81
|
"|-------|------|--------|---|---|----|-----|---------|---------|----------|-----|-------|-----------|",
|
|
53
82
|
].join("\n");
|
|
54
|
-
const lines = results.map((result) => buildConsoleRow(result));
|
|
83
|
+
const lines = results.map((result) => buildConsoleRow(result, useColor));
|
|
55
84
|
return `${header}\n${lines.join("\n")}`;
|
|
56
85
|
}
|
|
57
86
|
function buildRow(result) {
|
|
@@ -65,7 +94,7 @@ function buildRow(result) {
|
|
|
65
94
|
const error = result.runtimeErrorCode ?? (result.runtimeErrorMessage !== undefined ? result.runtimeErrorMessage : "");
|
|
66
95
|
return `| ${result.label} | ${result.path} | ${result.device} | ${scores.performance ?? "-"} | ${scores.accessibility ?? "-"} | ${scores.bestPractices ?? "-"} | ${scores.seo ?? "-"} | ${lcpSeconds} | ${fcpSeconds} | ${tbtMs} | ${cls} | ${error} | ${issues} |`;
|
|
67
96
|
}
|
|
68
|
-
function buildConsoleRow(result) {
|
|
97
|
+
function buildConsoleRow(result, useColor) {
|
|
69
98
|
const scores = result.scores;
|
|
70
99
|
const metrics = result.metrics;
|
|
71
100
|
const lcpSeconds = metrics.lcpMs !== undefined ? (metrics.lcpMs / 1000).toFixed(1) : "-";
|
|
@@ -74,10 +103,10 @@ function buildConsoleRow(result) {
|
|
|
74
103
|
const cls = metrics.cls !== undefined ? metrics.cls.toFixed(3) : "-";
|
|
75
104
|
const issues = formatTopIssues(result.opportunities);
|
|
76
105
|
const error = result.runtimeErrorCode ?? (result.runtimeErrorMessage !== undefined ? result.runtimeErrorMessage : "");
|
|
77
|
-
const performanceText = colourScore(scores.performance);
|
|
78
|
-
const accessibilityText = colourScore(scores.accessibility);
|
|
79
|
-
const bestPracticesText = colourScore(scores.bestPractices);
|
|
80
|
-
const seoText = colourScore(scores.seo);
|
|
106
|
+
const performanceText = colourScore(scores.performance, useColor);
|
|
107
|
+
const accessibilityText = colourScore(scores.accessibility, useColor);
|
|
108
|
+
const bestPracticesText = colourScore(scores.bestPractices, useColor);
|
|
109
|
+
const seoText = colourScore(scores.seo, useColor);
|
|
81
110
|
return `| ${result.label} | ${result.path} | ${result.device} | ${performanceText} | ${accessibilityText} | ${bestPracticesText} | ${seoText} | ${lcpSeconds} | ${fcpSeconds} | ${tbtMs} | ${cls} | ${error} | ${issues} |`;
|
|
82
111
|
}
|
|
83
112
|
function formatTopIssues(opportunities) {
|
|
@@ -93,12 +122,15 @@ function formatTopIssues(opportunities) {
|
|
|
93
122
|
});
|
|
94
123
|
return items.join("; ");
|
|
95
124
|
}
|
|
96
|
-
function colourScore(score) {
|
|
125
|
+
function colourScore(score, useColor) {
|
|
97
126
|
if (score === undefined) {
|
|
98
127
|
return "-";
|
|
99
128
|
}
|
|
100
129
|
const value = score;
|
|
101
130
|
const text = value.toString();
|
|
131
|
+
if (!useColor) {
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
102
134
|
let colour;
|
|
103
135
|
if (value < 50) {
|
|
104
136
|
colour = ANSI_RED;
|
|
@@ -147,3 +179,98 @@ function printRedIssues(results) {
|
|
|
147
179
|
console.log(`- ${result.label} ${result.path} [${result.device}] – ${badParts.join(", ")} – ${issues}`);
|
|
148
180
|
}
|
|
149
181
|
}
|
|
182
|
+
function shouldUseColor(ci, colorMode) {
|
|
183
|
+
if (colorMode === "on") {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (colorMode === "off") {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (ci) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
|
|
193
|
+
}
|
|
194
|
+
function printCiSummary(args, results, budgets) {
|
|
195
|
+
if (!args.ci) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!budgets) {
|
|
199
|
+
// eslint-disable-next-line no-console
|
|
200
|
+
console.log("\nCI mode: no budgets configured. Skipping threshold checks.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const violations = collectBudgetViolations(results, budgets);
|
|
204
|
+
if (violations.length === 0) {
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.log("\nCI budgets PASSED.");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// eslint-disable-next-line no-console
|
|
210
|
+
console.log(`\nCI budgets FAILED (${violations.length} violations):`);
|
|
211
|
+
for (const violation of violations) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.log(`- ${violation.pageLabel} ${violation.path} [${violation.device}] – ${violation.kind} ${violation.id}: ${violation.value} vs limit ${violation.limit}`);
|
|
214
|
+
}
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
function collectBudgetViolations(results, budgets) {
|
|
218
|
+
const violations = [];
|
|
219
|
+
for (const result of results) {
|
|
220
|
+
if (budgets.categories) {
|
|
221
|
+
collectCategoryViolations(result, budgets.categories, violations);
|
|
222
|
+
}
|
|
223
|
+
if (budgets.metrics) {
|
|
224
|
+
collectMetricViolations(result, budgets.metrics, violations);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return violations;
|
|
228
|
+
}
|
|
229
|
+
function collectCategoryViolations(result, categories, allViolations) {
|
|
230
|
+
const scores = result.scores;
|
|
231
|
+
addCategoryViolation("performance", scores.performance, categories.performance, result, allViolations);
|
|
232
|
+
addCategoryViolation("accessibility", scores.accessibility, categories.accessibility, result, allViolations);
|
|
233
|
+
addCategoryViolation("bestPractices", scores.bestPractices, categories.bestPractices, result, allViolations);
|
|
234
|
+
addCategoryViolation("seo", scores.seo, categories.seo, result, allViolations);
|
|
235
|
+
}
|
|
236
|
+
function addCategoryViolation(id, actual, limit, result, allViolations) {
|
|
237
|
+
if (limit === undefined || actual === undefined) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (actual >= limit) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
allViolations.push({
|
|
244
|
+
pageLabel: result.label,
|
|
245
|
+
path: result.path,
|
|
246
|
+
device: result.device,
|
|
247
|
+
kind: "category",
|
|
248
|
+
id,
|
|
249
|
+
value: actual,
|
|
250
|
+
limit,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function collectMetricViolations(result, metricsBudgets, allViolations) {
|
|
254
|
+
const metrics = result.metrics;
|
|
255
|
+
addMetricViolation("lcpMs", metrics.lcpMs, metricsBudgets.lcpMs, result, allViolations);
|
|
256
|
+
addMetricViolation("fcpMs", metrics.fcpMs, metricsBudgets.fcpMs, result, allViolations);
|
|
257
|
+
addMetricViolation("tbtMs", metrics.tbtMs, metricsBudgets.tbtMs, result, allViolations);
|
|
258
|
+
addMetricViolation("cls", metrics.cls, metricsBudgets.cls, result, allViolations);
|
|
259
|
+
}
|
|
260
|
+
function addMetricViolation(id, actual, limit, result, allViolations) {
|
|
261
|
+
if (limit === undefined || actual === undefined) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (actual <= limit) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
allViolations.push({
|
|
268
|
+
pageLabel: result.label,
|
|
269
|
+
path: result.path,
|
|
270
|
+
device: result.device,
|
|
271
|
+
kind: "metric",
|
|
272
|
+
id,
|
|
273
|
+
value: actual,
|
|
274
|
+
limit,
|
|
275
|
+
});
|
|
276
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -31,6 +31,7 @@ function normaliseConfig(input, absolutePath) {
|
|
|
31
31
|
const logLevel = rawLogLevel === "silent" || rawLogLevel === "error" || rawLogLevel === "info" || rawLogLevel === "verbose"
|
|
32
32
|
? rawLogLevel
|
|
33
33
|
: undefined;
|
|
34
|
+
const budgets = normaliseBudgets(maybeConfig.budgets, absolutePath);
|
|
34
35
|
return {
|
|
35
36
|
baseUrl,
|
|
36
37
|
query,
|
|
@@ -38,6 +39,7 @@ function normaliseConfig(input, absolutePath) {
|
|
|
38
39
|
runs,
|
|
39
40
|
logLevel,
|
|
40
41
|
pages,
|
|
42
|
+
budgets,
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
function normalisePage(page, index, absolutePath) {
|
|
@@ -66,3 +68,86 @@ function normalisePage(page, index, absolutePath) {
|
|
|
66
68
|
devices,
|
|
67
69
|
};
|
|
68
70
|
}
|
|
71
|
+
function normaliseBudgets(input, absolutePath) {
|
|
72
|
+
if (input === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (!input || typeof input !== "object") {
|
|
76
|
+
throw new Error(`Invalid budgets in ${absolutePath}: expected object`);
|
|
77
|
+
}
|
|
78
|
+
const maybeBudgets = input;
|
|
79
|
+
const categories = normaliseCategoryBudgets(maybeBudgets.categories, absolutePath);
|
|
80
|
+
const metrics = normaliseMetricBudgets(maybeBudgets.metrics, absolutePath);
|
|
81
|
+
if (!categories && !metrics) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
categories,
|
|
86
|
+
metrics,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function normaliseCategoryBudgets(input, absolutePath) {
|
|
90
|
+
if (input === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!input || typeof input !== "object") {
|
|
94
|
+
throw new Error(`Invalid budgets.categories in ${absolutePath}: expected object`);
|
|
95
|
+
}
|
|
96
|
+
const maybeCategories = input;
|
|
97
|
+
const performance = normaliseScoreBudget(maybeCategories.performance, "performance", absolutePath);
|
|
98
|
+
const accessibility = normaliseScoreBudget(maybeCategories.accessibility, "accessibility", absolutePath);
|
|
99
|
+
const bestPractices = normaliseScoreBudget(maybeCategories.bestPractices, "bestPractices", absolutePath);
|
|
100
|
+
const seo = normaliseScoreBudget(maybeCategories.seo, "seo", absolutePath);
|
|
101
|
+
if (performance === undefined &&
|
|
102
|
+
accessibility === undefined &&
|
|
103
|
+
bestPractices === undefined &&
|
|
104
|
+
seo === undefined) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
performance,
|
|
109
|
+
accessibility,
|
|
110
|
+
bestPractices,
|
|
111
|
+
seo,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function normaliseMetricBudgets(input, absolutePath) {
|
|
115
|
+
if (input === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (!input || typeof input !== "object") {
|
|
119
|
+
throw new Error(`Invalid budgets.metrics in ${absolutePath}: expected object`);
|
|
120
|
+
}
|
|
121
|
+
const maybeMetrics = input;
|
|
122
|
+
const lcpMs = normaliseMetricBudget(maybeMetrics.lcpMs, "lcpMs", absolutePath);
|
|
123
|
+
const fcpMs = normaliseMetricBudget(maybeMetrics.fcpMs, "fcpMs", absolutePath);
|
|
124
|
+
const tbtMs = normaliseMetricBudget(maybeMetrics.tbtMs, "tbtMs", absolutePath);
|
|
125
|
+
const cls = normaliseMetricBudget(maybeMetrics.cls, "cls", absolutePath);
|
|
126
|
+
if (lcpMs === undefined && fcpMs === undefined && tbtMs === undefined && cls === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
lcpMs,
|
|
131
|
+
fcpMs,
|
|
132
|
+
tbtMs,
|
|
133
|
+
cls,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function normaliseScoreBudget(value, key, absolutePath) {
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value !== "number" || value < 0 || value > 100) {
|
|
141
|
+
throw new Error(`Invalid budgets.categories.${key} in ${absolutePath}: expected number between 0 and 100`);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function normaliseMetricBudget(value, key, absolutePath) {
|
|
146
|
+
if (value === undefined) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== "number" || value < 0) {
|
|
150
|
+
throw new Error(`Invalid budgets.metrics.${key} in ${absolutePath}: expected non-negative number`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { detectRoutes } from "./route-detectors.js";
|
|
5
|
+
import { pathExists } from "./fs-utils.js";
|
|
6
|
+
import { runAuditCli } from "./cli.js";
|
|
7
|
+
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
8
|
+
const DEFAULT_RUNS = 1;
|
|
9
|
+
const DEFAULT_DEVICES = ["mobile", "desktop"];
|
|
10
|
+
const DEFAULT_ROUTE_LIMIT = 5;
|
|
11
|
+
const QUICKSTART_CONFIG_NAME = "quickstart.config.json";
|
|
12
|
+
const PROMPT_OPTIONS = { onCancel: handleCancel };
|
|
13
|
+
const baseUrlQuestion = {
|
|
14
|
+
type: "text",
|
|
15
|
+
name: "baseUrl",
|
|
16
|
+
message: "Base URL of the running app",
|
|
17
|
+
initial: DEFAULT_BASE_URL,
|
|
18
|
+
validate: (value) => (value.startsWith("http") ? true : "Enter a full http(s) URL."),
|
|
19
|
+
};
|
|
20
|
+
function handleCancel() {
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
console.log("Quickstart cancelled. No audits run.");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
async function ask(question) {
|
|
27
|
+
const answers = await prompts(question, PROMPT_OPTIONS);
|
|
28
|
+
return answers;
|
|
29
|
+
}
|
|
30
|
+
function parseQuickstartArgs(argv) {
|
|
31
|
+
let baseUrl;
|
|
32
|
+
let projectRoot = ".";
|
|
33
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
34
|
+
const arg = argv[index];
|
|
35
|
+
if ((arg === "--base-url" || arg === "-b") && index + 1 < argv.length) {
|
|
36
|
+
baseUrl = argv[index + 1];
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
else if ((arg === "--project-root" || arg === "-p") && index + 1 < argv.length) {
|
|
40
|
+
projectRoot = argv[index + 1];
|
|
41
|
+
index += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { baseUrl, projectRoot };
|
|
45
|
+
}
|
|
46
|
+
async function resolveBaseUrl(cliBaseUrl) {
|
|
47
|
+
if (cliBaseUrl && cliBaseUrl.length > 0) {
|
|
48
|
+
return cliBaseUrl;
|
|
49
|
+
}
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.error("Quickstart requires --base-url <url>. No base URL was provided.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
return DEFAULT_BASE_URL;
|
|
54
|
+
}
|
|
55
|
+
async function resolveProjectRoot(rawProjectRoot) {
|
|
56
|
+
const absolutePath = resolve(rawProjectRoot);
|
|
57
|
+
if (await pathExists(absolutePath)) {
|
|
58
|
+
return absolutePath;
|
|
59
|
+
}
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.log(`Project root ${absolutePath} does not exist. Falling back to current directory.`);
|
|
62
|
+
return process.cwd();
|
|
63
|
+
}
|
|
64
|
+
async function detectQuickstartPages(projectRoot) {
|
|
65
|
+
const routes = await detectRoutes({ projectRoot, limit: DEFAULT_ROUTE_LIMIT });
|
|
66
|
+
if (routes.length === 0) {
|
|
67
|
+
return buildFallbackPages();
|
|
68
|
+
}
|
|
69
|
+
return routes.map((route) => convertRouteToPage(route));
|
|
70
|
+
}
|
|
71
|
+
function buildFallbackPages() {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
path: "/",
|
|
75
|
+
label: "home",
|
|
76
|
+
devices: DEFAULT_DEVICES,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
function convertRouteToPage(route) {
|
|
81
|
+
return {
|
|
82
|
+
path: route.path,
|
|
83
|
+
label: route.label,
|
|
84
|
+
devices: DEFAULT_DEVICES,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function writeQuickstartConfig(config) {
|
|
88
|
+
const outputDir = resolve(".apex-auditor");
|
|
89
|
+
await mkdir(outputDir, { recursive: true });
|
|
90
|
+
const configPath = join(outputDir, QUICKSTART_CONFIG_NAME);
|
|
91
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
92
|
+
return configPath;
|
|
93
|
+
}
|
|
94
|
+
async function buildQuickstartConfig(args) {
|
|
95
|
+
const baseUrl = await resolveBaseUrl(args.baseUrl);
|
|
96
|
+
const projectRoot = await resolveProjectRoot(args.projectRoot);
|
|
97
|
+
const pages = await detectQuickstartPages(projectRoot);
|
|
98
|
+
const config = {
|
|
99
|
+
baseUrl,
|
|
100
|
+
pages,
|
|
101
|
+
runs: DEFAULT_RUNS,
|
|
102
|
+
};
|
|
103
|
+
return writeQuickstartConfig(config);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Run the ApexAuditor quickstart flow.
|
|
107
|
+
*
|
|
108
|
+
* This command discovers common routes from the current project, asks for a base URL
|
|
109
|
+
* if needed, writes a temporary config file, and then delegates to the audit CLI.
|
|
110
|
+
*
|
|
111
|
+
* @param argv - The process arguments array.
|
|
112
|
+
*/
|
|
113
|
+
export async function runQuickstartCli(argv) {
|
|
114
|
+
const args = parseQuickstartArgs(argv);
|
|
115
|
+
const configPath = await buildQuickstartConfig(args);
|
|
116
|
+
const auditArgv = ["node", "apex-auditor", "--config", configPath];
|
|
117
|
+
await runAuditCli(auditArgv);
|
|
118
|
+
}
|