apex-auditor 0.3.6 → 0.3.7
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 +4 -4
- package/dist/cli.js +9 -16
- package/dist/lighthouse-runner.js +13 -1
- package/dist/lighthouse-worker.js +15 -2
- package/dist/shell-cli.js +183 -6
- package/dist/wizard-cli.js +50 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Notes:
|
|
|
20
20
|
|
|
21
21
|
- `init` can auto-detect your stack from `package.json` (Next.js, Nuxt, Remix/React Router, SvelteKit, SPA).
|
|
22
22
|
- In monorepos, `init` can prompt you to pick an app/package under `apps/` or `packages/`.
|
|
23
|
-
- `init` can auto-discover routes from the filesystem and top-up from `robots.txt`/`sitemap.xml`. You can optionally filter detected routes with include/exclude patterns and still add manual routes.
|
|
23
|
+
- `init` can auto-discover routes from the filesystem and top-up from `robots.txt`/`sitemap.xml`. You can optionally filter detected routes with include/exclude patterns and still add manual routes. For larger route sets, the wizard may default filtering to **Yes** and prefill common excludes (framework-specific).
|
|
24
24
|
- For static sites, `init` can discover routes from HTML files under `dist/`, `build/`, `out/`, `public/`, and `src/`.
|
|
25
25
|
- When using a localhost base URL (e.g. `http://localhost:3000`), make sure the dev server port matches the project you’re configuring (important when multiple projects are running).
|
|
26
26
|
|
|
@@ -34,6 +34,9 @@ Inside the interactive shell:
|
|
|
34
34
|
- **headers** (security headers check; writes `.apex-auditor/headers.json`)
|
|
35
35
|
- **console** (console errors + runtime exceptions; writes `.apex-auditor/console.json`)
|
|
36
36
|
- **open** (open the latest HTML report)
|
|
37
|
+
- **pages** / **routes** (print configured pages/routes from the current config)
|
|
38
|
+
- **add-page** (interactive: append a page to `apex.config.json`)
|
|
39
|
+
- **rm-page** (interactive: remove a page from `apex.config.json`)
|
|
37
40
|
- **init** (launch config wizard)
|
|
38
41
|
- **config <path>** (switch config file)
|
|
39
42
|
|
|
@@ -132,11 +135,8 @@ The docs in `docs/` reflect the current shell-based workflow:
|
|
|
132
135
|
|
|
133
136
|
## Known issues
|
|
134
137
|
|
|
135
|
-
- **Shell exits after init wizard**: in some environments, the process may exit after completing `init` in shell mode. Workaround: run `apex-auditor init` outside the shell, then re-run `apex-auditor shell`.
|
|
136
138
|
- **Large-run Lighthouse stability**: very large audits (many page/device combinations) may show higher score variance than manual Lighthouse runs and can intermittently hit worker/Chrome disconnects. Workaround: reduce parallelism (e.g. `--stable`) and retry.
|
|
137
139
|
|
|
138
|
-
The target for a truly stable release is after v0.3.7.
|
|
139
|
-
|
|
140
140
|
## License
|
|
141
141
|
|
|
142
142
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1140,15 +1140,17 @@ export async function runAuditCli(argv, options) {
|
|
|
1140
1140
|
const plannedRuns = overviewSample.config.runs ?? 1;
|
|
1141
1141
|
const plannedSteps = plannedCombos * plannedRuns;
|
|
1142
1142
|
const LARGE_RUN_COMBOS_THRESHOLD = 76;
|
|
1143
|
-
const LARGE_RUN_PARALLEL_CAP =
|
|
1143
|
+
const LARGE_RUN_PARALLEL_CAP = 4;
|
|
1144
1144
|
const usingDefaultParallel = args.parallelOverride === undefined && presetParallel === undefined && config.parallel === undefined;
|
|
1145
|
-
const
|
|
1145
|
+
const isLargeRun = plannedCombos >= LARGE_RUN_COMBOS_THRESHOLD;
|
|
1146
|
+
const autoStableLargeRun = isLargeRun && !args.stable && usingDefaultParallel;
|
|
1146
1147
|
const resolvedConfigForRun = autoStableLargeRun
|
|
1147
1148
|
? { ...overviewSample.config, parallel: Math.min(overviewSample.config.parallel ?? DEFAULT_PARALLEL, LARGE_RUN_PARALLEL_CAP) }
|
|
1148
1149
|
: overviewSample.config;
|
|
1149
1150
|
const maxSteps = args.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
1150
1151
|
const maxCombos = args.maxCombos ?? DEFAULT_MAX_COMBOS;
|
|
1151
1152
|
const isTty = typeof process !== "undefined" && process.stdout?.isTTY === true;
|
|
1153
|
+
const LARGE_RUN_HINT_COMBOS_THRESHOLD = 40;
|
|
1152
1154
|
const exceeds = plannedSteps > maxSteps || plannedCombos > maxCombos;
|
|
1153
1155
|
const useColor = shouldUseColor(args.ci, args.colorMode);
|
|
1154
1156
|
if (args.plan) {
|
|
@@ -1165,15 +1167,9 @@ export async function runAuditCli(argv, options) {
|
|
|
1165
1167
|
});
|
|
1166
1168
|
return;
|
|
1167
1169
|
}
|
|
1168
|
-
if (isTty && !args.ci && !args.jsonOutput) {
|
|
1169
|
-
const tipLines = [
|
|
1170
|
-
"Tip: use --plan to preview run size before starting.",
|
|
1171
|
-
"Note: runs-per-combo is always 1; rerun the same command to compare results.",
|
|
1172
|
-
"If parallel mode flakes (worker disconnects), retry with --stable (forces parallel=1).",
|
|
1173
|
-
];
|
|
1170
|
+
if (isTty && !args.ci && !args.jsonOutput && plannedCombos >= LARGE_RUN_HINT_COMBOS_THRESHOLD) {
|
|
1174
1171
|
// eslint-disable-next-line no-console
|
|
1175
|
-
console.log(
|
|
1176
|
-
printDivider();
|
|
1172
|
+
console.log(`Tip: large run (${plannedCombos} combos). Use --plan to preview, and retry with --stable if parallel mode flakes.`);
|
|
1177
1173
|
}
|
|
1178
1174
|
if (autoStableLargeRun && isTty && !args.ci && !args.jsonOutput) {
|
|
1179
1175
|
// eslint-disable-next-line no-console
|
|
@@ -1249,17 +1245,14 @@ export async function runAuditCli(argv, options) {
|
|
|
1249
1245
|
showParallel: args.showParallel,
|
|
1250
1246
|
onlyCategories,
|
|
1251
1247
|
signal: abortController.signal,
|
|
1252
|
-
onAfterWarmUp:
|
|
1248
|
+
onAfterWarmUp: startAuditSpinner,
|
|
1253
1249
|
onProgress: ({ completed, total, path, device, etaMs }) => {
|
|
1254
1250
|
if (!process.stdout.isTTY) {
|
|
1255
1251
|
return;
|
|
1256
1252
|
}
|
|
1257
1253
|
const etaText = etaMs !== undefined ? ` | ETA ${formatEtaText(etaMs)}` : "";
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
process.stdout.write(`\r${padded}`);
|
|
1261
|
-
lastProgressLine = padded;
|
|
1262
|
-
updateSpinnerMessage(`Running audit (Lighthouse) page ${completed}/${total}`);
|
|
1254
|
+
startAuditSpinner();
|
|
1255
|
+
updateSpinnerMessage(`Running audit (Lighthouse) page ${completed}/${total} — ${path} [${device}]${etaText}`);
|
|
1263
1256
|
},
|
|
1264
1257
|
});
|
|
1265
1258
|
}
|
|
@@ -347,6 +347,11 @@ async function createChromeSession(chromePort) {
|
|
|
347
347
|
"--safebrowsing-disable-auto-update",
|
|
348
348
|
"--password-store=basic",
|
|
349
349
|
"--use-mock-keychain",
|
|
350
|
+
// Stability flags for parallel runs
|
|
351
|
+
"--disable-hang-monitor",
|
|
352
|
+
"--disable-ipc-flooding-protection",
|
|
353
|
+
"--disable-domain-reliability",
|
|
354
|
+
"--disable-component-update",
|
|
350
355
|
],
|
|
351
356
|
});
|
|
352
357
|
return {
|
|
@@ -741,7 +746,14 @@ function isTransientLighthouseError(error) {
|
|
|
741
746
|
message.includes("LanternError") ||
|
|
742
747
|
message.includes("top level events") ||
|
|
743
748
|
message.includes("CDP") ||
|
|
744
|
-
message.includes("disconnected")
|
|
749
|
+
message.includes("disconnected") ||
|
|
750
|
+
// Additional transient errors for better retry handling
|
|
751
|
+
message.includes("WebSocket") ||
|
|
752
|
+
message.includes("webSocket") ||
|
|
753
|
+
message.includes("fetch failed") ||
|
|
754
|
+
message.includes("ECONNREFUSED") ||
|
|
755
|
+
message.includes("ECONNRESET") ||
|
|
756
|
+
message.includes("socket hang up"));
|
|
745
757
|
}
|
|
746
758
|
async function runSingleAuditWithRetry({ task, sessionRef, updateProgress, maxRetries, }) {
|
|
747
759
|
let attempt = 0;
|
|
@@ -13,7 +13,14 @@ function isTransientLighthouseError(error) {
|
|
|
13
13
|
message.includes("top level events") ||
|
|
14
14
|
message.includes("CDP") ||
|
|
15
15
|
message.includes("disconnected") ||
|
|
16
|
-
message.includes("ApexAuditor timeout")
|
|
16
|
+
message.includes("ApexAuditor timeout") ||
|
|
17
|
+
// Additional transient errors for better retry handling
|
|
18
|
+
message.includes("WebSocket") ||
|
|
19
|
+
message.includes("webSocket") ||
|
|
20
|
+
message.includes("fetch failed") ||
|
|
21
|
+
message.includes("ECONNREFUSED") ||
|
|
22
|
+
message.includes("ECONNRESET") ||
|
|
23
|
+
message.includes("socket hang up"));
|
|
17
24
|
}
|
|
18
25
|
async function createChromeSession() {
|
|
19
26
|
const userDataDir = await mkdtemp(join(tmpdir(), "apex-auditor-chrome-"));
|
|
@@ -39,6 +46,11 @@ async function createChromeSession() {
|
|
|
39
46
|
"--safebrowsing-disable-auto-update",
|
|
40
47
|
"--password-store=basic",
|
|
41
48
|
"--use-mock-keychain",
|
|
49
|
+
// Stability flags for parallel runs
|
|
50
|
+
"--disable-hang-monitor",
|
|
51
|
+
"--disable-ipc-flooding-protection",
|
|
52
|
+
"--disable-domain-reliability",
|
|
53
|
+
"--disable-component-update",
|
|
42
54
|
],
|
|
43
55
|
});
|
|
44
56
|
return {
|
|
@@ -213,6 +225,7 @@ function send(message) {
|
|
|
213
225
|
}
|
|
214
226
|
async function main() {
|
|
215
227
|
const maxRetries = 3;
|
|
228
|
+
// Recycle Chrome periodically to prevent memory leaks
|
|
216
229
|
const maxTasksPerChrome = 10;
|
|
217
230
|
const sessionRef = { session: await createChromeSession() };
|
|
218
231
|
let tasksSinceChromeStart = 0;
|
|
@@ -238,7 +251,7 @@ async function main() {
|
|
|
238
251
|
await sessionRef.session.close();
|
|
239
252
|
}
|
|
240
253
|
catch {
|
|
241
|
-
|
|
254
|
+
// Ignore close errors
|
|
242
255
|
}
|
|
243
256
|
sessionRef.session = await createChromeSession();
|
|
244
257
|
tasksSinceChromeStart = 0;
|
package/dist/shell-cli.js
CHANGED
|
@@ -13,8 +13,10 @@ import { runMeasureCli } from "./measure-cli.js";
|
|
|
13
13
|
import { runWizardCli } from "./wizard-cli.js";
|
|
14
14
|
import { runCleanCli } from "./clean-cli.js";
|
|
15
15
|
import { runUninstallCli } from "./uninstall-cli.js";
|
|
16
|
+
import { loadConfig } from "./config.js";
|
|
16
17
|
import { pathExists } from "./fs-utils.js";
|
|
17
18
|
import { renderPanel } from "./ui/render-panel.js";
|
|
19
|
+
import { renderTable } from "./ui/render-table.js";
|
|
18
20
|
import { startSpinner, stopSpinner } from "./spinner.js";
|
|
19
21
|
import { UiTheme } from "./ui/ui-theme.js";
|
|
20
22
|
const SESSION_DIR_NAME = ".apex-auditor";
|
|
@@ -124,6 +126,148 @@ async function runConsoleAuditFromShell(session, args) {
|
|
|
124
126
|
async function writeJsonFile(absolutePath, value) {
|
|
125
127
|
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
126
128
|
}
|
|
129
|
+
async function loadCurrentConfig(configPath) {
|
|
130
|
+
const loaded = await loadConfig({ configPath });
|
|
131
|
+
return { absolutePath: loaded.configPath, config: loaded.config };
|
|
132
|
+
}
|
|
133
|
+
function formatPagesTable(pages) {
|
|
134
|
+
const headers = ["#", "path", "label", "devices"];
|
|
135
|
+
const rows = pages.map((p, index) => {
|
|
136
|
+
return [String(index + 1), p.path, p.label, p.devices.join(",")];
|
|
137
|
+
});
|
|
138
|
+
return renderTable({ headers, rows });
|
|
139
|
+
}
|
|
140
|
+
async function printConfiguredPages(session) {
|
|
141
|
+
const exists = await pathExists(session.configPath);
|
|
142
|
+
if (!exists) {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.log(`Config not found at ${session.configPath}. Run 'init' to create a config, or use 'config <path>' to point to one.`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { absolutePath, config } = await loadCurrentConfig(session.configPath);
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push(`${theme.dim("Config")}: ${absolutePath}`);
|
|
150
|
+
lines.push(`${theme.dim("Base URL")}: ${config.baseUrl}`);
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push(formatPagesTable(config.pages));
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.log(renderPanel({ title: theme.bold("Pages"), lines }));
|
|
155
|
+
}
|
|
156
|
+
function parseDevices(raw) {
|
|
157
|
+
const parts = raw
|
|
158
|
+
.split(",")
|
|
159
|
+
.map((p) => p.trim().toLowerCase())
|
|
160
|
+
.filter((p) => p.length > 0);
|
|
161
|
+
if (parts.length === 0) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const devices = [];
|
|
165
|
+
for (const part of parts) {
|
|
166
|
+
if (part === "mobile" || part === "desktop") {
|
|
167
|
+
devices.push(part);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return [...new Set(devices)];
|
|
173
|
+
}
|
|
174
|
+
function askLine(rl, question) {
|
|
175
|
+
return new Promise((resolvePromise) => {
|
|
176
|
+
rl.question(question, (value) => resolvePromise(value));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function addPageInteractive(rl, session) {
|
|
180
|
+
const exists = await pathExists(session.configPath);
|
|
181
|
+
if (!exists) {
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.log(`Config not found at ${session.configPath}. Run 'init' first.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const { absolutePath, config } = await loadCurrentConfig(session.configPath);
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.log(formatPagesTable(config.pages));
|
|
189
|
+
const rawPath = (await askLine(rl, "New page path (must start with /): ")).trim();
|
|
190
|
+
if (!rawPath.startsWith("/")) {
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.log("Cancelled: path must start with '/'.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const label = (await askLine(rl, "Label (optional): ")).trim() || rawPath;
|
|
196
|
+
const rawDevices = (await askLine(rl, "Devices (comma-separated: mobile,desktop) [mobile,desktop]: ")).trim();
|
|
197
|
+
const devices = parseDevices(rawDevices.length > 0 ? rawDevices : "mobile,desktop") ?? ["mobile", "desktop"];
|
|
198
|
+
const nextPages = [...config.pages, { path: rawPath, label, devices }];
|
|
199
|
+
const nextConfig = { ...config, pages: nextPages };
|
|
200
|
+
await writeJsonFile(absolutePath, nextConfig);
|
|
201
|
+
// eslint-disable-next-line no-console
|
|
202
|
+
console.log(`Added page ${rawPath}.`);
|
|
203
|
+
}
|
|
204
|
+
async function removePageInteractive(rl, session, args) {
|
|
205
|
+
const exists = await pathExists(session.configPath);
|
|
206
|
+
if (!exists) {
|
|
207
|
+
// eslint-disable-next-line no-console
|
|
208
|
+
console.log(`Config not found at ${session.configPath}. Run 'init' first.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const { absolutePath, config } = await loadCurrentConfig(session.configPath);
|
|
212
|
+
if (config.pages.length === 0) {
|
|
213
|
+
// eslint-disable-next-line no-console
|
|
214
|
+
console.log("No pages configured.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const byPath = args[0]?.trim();
|
|
218
|
+
const resolvedIndex = (() => {
|
|
219
|
+
if (byPath && byPath.startsWith("/")) {
|
|
220
|
+
const index = config.pages.findIndex((p) => p.path === byPath);
|
|
221
|
+
return index >= 0 ? index : undefined;
|
|
222
|
+
}
|
|
223
|
+
if (byPath && /^\d+$/.test(byPath)) {
|
|
224
|
+
const idx = parseInt(byPath, 10) - 1;
|
|
225
|
+
return idx >= 0 && idx < config.pages.length ? idx : undefined;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
})();
|
|
229
|
+
const indexToRemove = resolvedIndex ?? (() => {
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.log(formatPagesTable(config.pages));
|
|
232
|
+
return undefined;
|
|
233
|
+
})();
|
|
234
|
+
const finalIndex = indexToRemove ?? (() => {
|
|
235
|
+
return undefined;
|
|
236
|
+
})();
|
|
237
|
+
let resolvedFinalIndex = finalIndex;
|
|
238
|
+
if (resolvedFinalIndex === undefined) {
|
|
239
|
+
const answer = (await askLine(rl, "Remove which page? Enter # or /path (blank to cancel): ")).trim();
|
|
240
|
+
if (answer.length === 0) {
|
|
241
|
+
// eslint-disable-next-line no-console
|
|
242
|
+
console.log("Cancelled.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (answer.startsWith("/")) {
|
|
246
|
+
const idx = config.pages.findIndex((p) => p.path === answer);
|
|
247
|
+
resolvedFinalIndex = idx >= 0 ? idx : undefined;
|
|
248
|
+
}
|
|
249
|
+
else if (/^\d+$/.test(answer)) {
|
|
250
|
+
const idx = parseInt(answer, 10) - 1;
|
|
251
|
+
resolvedFinalIndex = idx >= 0 && idx < config.pages.length ? idx : undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (resolvedFinalIndex === undefined) {
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.log("No matching page.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (config.pages.length <= 1) {
|
|
260
|
+
// eslint-disable-next-line no-console
|
|
261
|
+
console.log("Cancelled: config must contain at least one page.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const removed = config.pages[resolvedFinalIndex];
|
|
265
|
+
const nextPages = config.pages.filter((_p, i) => i !== resolvedFinalIndex);
|
|
266
|
+
const nextConfig = { ...config, pages: nextPages };
|
|
267
|
+
await writeJsonFile(absolutePath, nextConfig);
|
|
268
|
+
// eslint-disable-next-line no-console
|
|
269
|
+
console.log(`Removed page ${removed.path}.`);
|
|
270
|
+
}
|
|
127
271
|
function getSessionPaths(projectRoot) {
|
|
128
272
|
const dir = resolve(projectRoot, SESSION_DIR_NAME);
|
|
129
273
|
const file = join(dir, SESSION_FILE_NAME);
|
|
@@ -364,6 +508,10 @@ function printHelp() {
|
|
|
364
508
|
lines.push(`${theme.cyan("console")} Console errors + runtime exceptions audit (headless Chrome)`);
|
|
365
509
|
lines.push("");
|
|
366
510
|
lines.push(theme.bold("Other commands"));
|
|
511
|
+
lines.push(`${theme.cyan("pages")} Print configured pages/routes from the current config`);
|
|
512
|
+
lines.push(`${theme.cyan("routes")} Alias for pages`);
|
|
513
|
+
lines.push(`${theme.cyan("add-page")} Add a page to apex.config.json (interactive)`);
|
|
514
|
+
lines.push(`${theme.cyan("rm-page [#|/path]")} Remove a page from apex.config.json (interactive)`);
|
|
367
515
|
lines.push(`${theme.cyan("clean")} Remove ApexAuditor artifacts (reports/cache and optionally config)`);
|
|
368
516
|
lines.push(`${theme.cyan("uninstall")} Remove .apex-auditor and the current config file`);
|
|
369
517
|
lines.push(`${theme.cyan("open")} Open the last HTML report (or .apex-auditor/report.html)`);
|
|
@@ -431,6 +579,10 @@ function createCompleter() {
|
|
|
431
579
|
"links",
|
|
432
580
|
"headers",
|
|
433
581
|
"console",
|
|
582
|
+
"pages",
|
|
583
|
+
"routes",
|
|
584
|
+
"add-page",
|
|
585
|
+
"rm-page",
|
|
434
586
|
"clean",
|
|
435
587
|
"uninstall",
|
|
436
588
|
"open",
|
|
@@ -690,6 +842,10 @@ async function handleShellCommand(projectRoot, session, command) {
|
|
|
690
842
|
if (command.id === "exit" || command.id === "quit") {
|
|
691
843
|
return { session, shouldExit: true };
|
|
692
844
|
}
|
|
845
|
+
if (command.id === "pages" || command.id === "routes") {
|
|
846
|
+
await printConfiguredPages(session);
|
|
847
|
+
return { session, shouldExit: false };
|
|
848
|
+
}
|
|
693
849
|
if (command.id === "audit") {
|
|
694
850
|
const nextSession = await runAuditFromShell(projectRoot, session, command.args);
|
|
695
851
|
return { session: nextSession, shouldExit: false };
|
|
@@ -857,6 +1013,28 @@ export async function runShellCli(argv) {
|
|
|
857
1013
|
return;
|
|
858
1014
|
}
|
|
859
1015
|
let command = parseShellCommand(line);
|
|
1016
|
+
const isEditCommand = command.id === "add-page" || command.id === "rm-page";
|
|
1017
|
+
if (isEditCommand && process.stdin.isTTY) {
|
|
1018
|
+
suppressInput = true;
|
|
1019
|
+
rl.pause();
|
|
1020
|
+
try {
|
|
1021
|
+
if (command.id === "add-page") {
|
|
1022
|
+
await addPageInteractive(rl, session);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
await removePageInteractive(rl, session, command.args);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
finally {
|
|
1029
|
+
if (!rlClosed) {
|
|
1030
|
+
rl.resume();
|
|
1031
|
+
suppressInput = false;
|
|
1032
|
+
rl.setPrompt(buildPrompt(session));
|
|
1033
|
+
rl.prompt();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
860
1038
|
if (command.id === "clean" && !command.args.includes("--yes") && !command.args.includes("-y") && process.stdin.isTTY) {
|
|
861
1039
|
const targets = resolveCleanTargets({ projectRoot, session, args: command.args });
|
|
862
1040
|
const ok = await confirmCleanInShell({ rl, targets });
|
|
@@ -908,12 +1086,11 @@ export async function runShellCli(argv) {
|
|
|
908
1086
|
// eslint-disable-next-line no-console
|
|
909
1087
|
console.log("Starting config wizard...");
|
|
910
1088
|
await runWizardInShell();
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
1089
|
+
// eslint-disable-next-line no-console
|
|
1090
|
+
console.log(`Ready. Next: ${theme.cyan("measure")} or ${theme.cyan("audit")}.`);
|
|
1091
|
+
// Force readline recreation by closing current instance - this ensures
|
|
1092
|
+
// the shell stays alive after the wizard completes even if prompts closed stdin
|
|
1093
|
+
rl.close();
|
|
917
1094
|
return;
|
|
918
1095
|
}
|
|
919
1096
|
rl.pause();
|
package/dist/wizard-cli.js
CHANGED
|
@@ -82,13 +82,13 @@ const pageQuestions = [
|
|
|
82
82
|
const addFirstPageQuestion = {
|
|
83
83
|
type: "confirm",
|
|
84
84
|
name: "value",
|
|
85
|
-
message: "Add
|
|
85
|
+
message: "No pages were auto-detected. Add a page to audit now? (You can always edit apex.config.json later)",
|
|
86
86
|
initial: true,
|
|
87
87
|
};
|
|
88
88
|
const addMorePagesQuestion = {
|
|
89
89
|
type: "confirm",
|
|
90
90
|
name: "value",
|
|
91
|
-
message: "Add another page to audit?",
|
|
91
|
+
message: "Add another page to audit? (Optional — you can edit apex.config.json later)",
|
|
92
92
|
initial: false,
|
|
93
93
|
};
|
|
94
94
|
const projectRootQuestion = {
|
|
@@ -130,6 +130,39 @@ const routeFilterQuestions = [
|
|
|
130
130
|
initial: "",
|
|
131
131
|
},
|
|
132
132
|
];
|
|
133
|
+
const ROUTE_FILTER_DEFAULT_ON_THRESHOLD = 15;
|
|
134
|
+
function buildSuggestedExcludePatterns(profile) {
|
|
135
|
+
if (profile === "next") {
|
|
136
|
+
return ["/_next/*", "/api/*"];
|
|
137
|
+
}
|
|
138
|
+
if (profile === "nuxt") {
|
|
139
|
+
return ["/__nuxt/*", "/api/*"];
|
|
140
|
+
}
|
|
141
|
+
if (profile === "sveltekit") {
|
|
142
|
+
return ["/__data.json*", "/api/*"];
|
|
143
|
+
}
|
|
144
|
+
if (profile === "remix") {
|
|
145
|
+
return ["/api/*"];
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
function buildRouteFilterQuestions(params) {
|
|
150
|
+
const excludeInitial = params.suggestedExclude.join(",");
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
type: "text",
|
|
154
|
+
name: "include",
|
|
155
|
+
message: "Include patterns (comma-separated, optional)",
|
|
156
|
+
initial: "",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
name: "exclude",
|
|
161
|
+
message: "Exclude patterns (comma-separated, optional)",
|
|
162
|
+
initial: excludeInitial,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
}
|
|
133
166
|
function handleCancel() {
|
|
134
167
|
console.log("Wizard cancelled. No config written.");
|
|
135
168
|
throw new WizardAbortError("cancelled");
|
|
@@ -415,6 +448,10 @@ async function collectSinglePage() {
|
|
|
415
448
|
}
|
|
416
449
|
async function collectPages(initialPages) {
|
|
417
450
|
const pages = [...initialPages];
|
|
451
|
+
if (pages.length > 0) {
|
|
452
|
+
console.log("Tip: You can add/remove pages later by editing apex.config.json or re-running init.");
|
|
453
|
+
return pages;
|
|
454
|
+
}
|
|
418
455
|
while (true) {
|
|
419
456
|
const shouldAdd = await confirmAddPage(pages.length > 0);
|
|
420
457
|
if (!shouldAdd) {
|
|
@@ -452,8 +489,13 @@ async function maybeDetectPages(params) {
|
|
|
452
489
|
return [];
|
|
453
490
|
}
|
|
454
491
|
console.log(`Detected ${combined.length} route(s) using auto-discovery.`);
|
|
455
|
-
const
|
|
456
|
-
const
|
|
492
|
+
const suggestedExclude = buildSuggestedExcludePatterns(params.profile);
|
|
493
|
+
const initialFilter = combined.length >= ROUTE_FILTER_DEFAULT_ON_THRESHOLD || suggestedExclude.length > 0;
|
|
494
|
+
const confirmQuestion = { ...routeFilterConfirmQuestion, initial: initialFilter };
|
|
495
|
+
const shouldFilter = await ask(confirmQuestion);
|
|
496
|
+
const filtered = shouldFilter.value
|
|
497
|
+
? await filterDetectedRoutes({ routes: combined, profile: params.profile, suggestedExclude })
|
|
498
|
+
: combined;
|
|
457
499
|
if (filtered.length === 0) {
|
|
458
500
|
console.log("No routes remain after filtering. Add pages manually.");
|
|
459
501
|
return [];
|
|
@@ -463,13 +505,14 @@ async function maybeDetectPages(params) {
|
|
|
463
505
|
}
|
|
464
506
|
return selectDetectedRoutes(filtered);
|
|
465
507
|
}
|
|
466
|
-
async function filterDetectedRoutes(
|
|
467
|
-
const
|
|
508
|
+
async function filterDetectedRoutes(params) {
|
|
509
|
+
const questions = buildRouteFilterQuestions({ profile: params.profile, suggestedExclude: params.suggestedExclude });
|
|
510
|
+
const answers = await ask(questions);
|
|
468
511
|
const includePatterns = splitPatterns(answers.include);
|
|
469
512
|
const excludePatterns = splitPatterns(answers.exclude);
|
|
470
513
|
const includeRegexes = includePatterns.map((pattern) => compileGlob(pattern));
|
|
471
514
|
const excludeRegexes = excludePatterns.map((pattern) => compileGlob(pattern));
|
|
472
|
-
return routes.filter((route) => {
|
|
515
|
+
return params.routes.filter((route) => {
|
|
473
516
|
const included = includeRegexes.length === 0 ? true : includeRegexes.some((regex) => regex.test(route.path));
|
|
474
517
|
const excluded = excludeRegexes.some((regex) => regex.test(route.path));
|
|
475
518
|
return included && !excluded;
|