@godxjp/ui 16.10.0 → 17.0.1

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 (62) hide show
  1. package/README.md +30 -5
  2. package/dist/components/charts/chart-cartesian.js +1 -6
  3. package/dist/components/charts/pie-chart.js +1 -8
  4. package/dist/components/data-display/empty-state.d.ts +1 -1
  5. package/dist/components/data-display/empty-state.js +25 -7
  6. package/dist/components/data-entry/form-field.js +13 -4
  7. package/dist/components/data-entry/search-select.d.ts +1 -1
  8. package/dist/components/data-entry/search-select.js +97 -19
  9. package/dist/components/data-entry/select.js +15 -4
  10. package/dist/components/feedback/alert.d.ts +11 -2
  11. package/dist/components/feedback/alert.js +51 -9
  12. package/dist/components/general/button.js +2 -0
  13. package/dist/components/general/index.d.ts +2 -0
  14. package/dist/components/general/index.js +2 -0
  15. package/dist/components/general/reveal.d.ts +14 -0
  16. package/dist/components/general/reveal.js +21 -0
  17. package/dist/components/layout/auth-shell.d.ts +11 -0
  18. package/dist/components/layout/auth-shell.js +15 -0
  19. package/dist/components/layout/flex.js +1 -1
  20. package/dist/components/layout/index.d.ts +2 -0
  21. package/dist/components/layout/index.js +2 -0
  22. package/dist/components/layout/page-container.js +4 -4
  23. package/dist/components/navigation/app-setting-picker.js +19 -4
  24. package/dist/components/navigation/pagination.d.ts +1 -1
  25. package/dist/components/navigation/pagination.js +2 -1
  26. package/dist/components/query/data-state.d.ts +1 -1
  27. package/dist/components/query/data-state.js +27 -4
  28. package/dist/components/query/index.d.ts +2 -0
  29. package/dist/components/query/index.js +4 -1
  30. package/dist/i18n/messages/en.json +30 -2
  31. package/dist/i18n/messages/ja.json +30 -2
  32. package/dist/i18n/messages/vi.json +30 -2
  33. package/dist/lib/query-error.d.ts +17 -0
  34. package/dist/lib/query-error.js +58 -0
  35. package/dist/props/components/app.prop.d.ts +9 -1
  36. package/dist/props/components/data-display.prop.d.ts +15 -0
  37. package/dist/props/components/data-entry.prop.d.ts +8 -0
  38. package/dist/props/components/feedback.prop.d.ts +6 -0
  39. package/dist/props/components/general.prop.d.ts +23 -1
  40. package/dist/props/components/layout.prop.d.ts +17 -0
  41. package/dist/props/components/query.prop.d.ts +7 -1
  42. package/dist/props/registry.d.ts +31 -2
  43. package/dist/props/registry.js +44 -2
  44. package/dist/props/vocabulary/index.d.ts +1 -1
  45. package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
  46. package/dist/styles/alert-layout.css +1 -2
  47. package/dist/styles/base.css +1 -1
  48. package/dist/styles/card-layout.css +6 -6
  49. package/dist/styles/index.css +1 -0
  50. package/dist/styles/layout.css +28 -0
  51. package/dist/styles/motion.css +52 -0
  52. package/dist/styles/shell-layout.css +42 -0
  53. package/dist/styles/text-layout.css +1 -1
  54. package/dist/theme/famgia.service.css +15 -14
  55. package/dist/tokens/components/card.css +1 -1
  56. package/dist/tokens/components/feedback.css +4 -0
  57. package/dist/tokens/components/shell.css +10 -0
  58. package/dist/tokens/foundation.css +11 -5
  59. package/package.json +7 -3
  60. package/scripts/ui-audit.mjs +2 -2
  61. package/scripts/visual-audit-rules.mjs +2 -2
  62. package/scripts/visual-audit.mjs +293 -126
@@ -29,7 +29,7 @@ export const VISUAL_RULES = [
29
29
  id: "oversaturated-accent",
30
30
  severity: "warn",
31
31
  category: "color",
32
- standard: "@godxjp/ui dxs-kintai 渋み (OKLCH chroma ≤ 0.18)",
32
+ standard: "@godxjp/ui reference-design 渋み (OKLCH chroma ≤ 0.18)",
33
33
  fix: "Desaturate brand/primary surfaces — keep OKLCH chroma ≤ 0.18. Read --primary tokens; never paint a full-width bar in raw vivid blue.",
34
34
  },
35
35
  {
@@ -75,7 +75,7 @@ export function oklchChroma({ r, g, b }) {
75
75
  return Math.hypot(oa, ob);
76
76
  }
77
77
 
78
- /** dxs-kintai restraint bound. A signal/brand surface above this chroma "screams". */
78
+ /** reference-design restraint bound. A signal/brand surface above this chroma "screams". */
79
79
  export const CHROMA_LIMIT = 0.18;
80
80
 
81
81
  /** @returns {boolean} true when an accent surface exceeds the 渋み chroma limit. */
@@ -12,12 +12,27 @@
12
12
  * Playwright + @axe-core/playwright are OPTIONAL peer deps — installed only by apps that
13
13
  * run this audit, so the static audit and the library stay browser-free.
14
14
  *
15
+ * TESTED peer range (see TESTED_VERSIONS + README "Runtime visual audit"):
16
+ * playwright >=1.55 <2 (tested 1.61.1)
17
+ * @axe-core/playwright >=4.10 <5 (tested 4.12.1)
18
+ * axe-core >=4.10 <5 (tested 4.12.1)
19
+ *
15
20
  * Usage (from the consuming app, against its running dev/preview server):
16
21
  * node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
17
- * node node_modules/@godxjp/ui/scripts/visual-audit.mjs --format json http://localhost:5173
22
+ * node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --format json
18
23
  * node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
19
24
  * PLAYWRIGHT_CHROMIUM_EXECUTABLE=/usr/bin/chromium node …/visual-audit.mjs http://… # system browser
25
+ *
26
+ * JSON contract (--format json ALWAYS emits valid JSON, even on bootstrap failure):
27
+ * { status: "ok" | "partial" | "error",
28
+ * tool: <TESTED_VERSIONS>,
29
+ * summary: { errors, warnings, pages, infrastructureErrors } | null,
30
+ * findings: [ … product findings … ],
31
+ * errors: [ … infrastructure errors (bootstrap / navigate / analyze) … ] }
32
+ * `status: "ok"` ONLY when every page was audited with zero infrastructure errors —
33
+ * so a tool/bootstrap failure can NEVER be misread as "zero violations".
20
34
  */
35
+ import { pathToFileURL } from "node:url";
21
36
  import {
22
37
  VISUAL_RULES,
23
38
  isOversaturated,
@@ -27,55 +42,79 @@ import {
27
42
  alertControlIssues,
28
43
  } from "./visual-audit-rules.mjs";
29
44
 
30
- const args = process.argv.slice(2);
31
- const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
32
- const strict = args.includes("--strict");
33
- const listRules = args.includes("--rules");
45
+ /**
46
+ * Peer versions this audit is tested against. Surfaced in the JSON `tool` field, the
47
+ * README, and the MCP `list_visual_checks` command so agents pin a compatible range.
48
+ * Playwright 1.55+ is required for the `browser.newContext()` / `context.newPage()`
49
+ * flow that @axe-core/playwright 4.10+ expects (older `browser.newPage()` throws
50
+ * "Please use browser.newContext()").
51
+ */
52
+ export const TESTED_VERSIONS = {
53
+ playwright: { range: ">=1.55 <2", tested: "1.61.1" },
54
+ "@axe-core/playwright": { range: ">=4.10 <5", tested: "4.12.1" },
55
+ "axe-core": { range: ">=4.10 <5", tested: "4.12.1" },
56
+ };
34
57
 
35
- if (listRules) {
36
- process.stdout.write(JSON.stringify(VISUAL_RULES, null, 2) + "\n");
37
- process.exit(0);
58
+ /** Shape one product finding from a rule id (pure — unit-tested without a browser). */
59
+ export function buildFinding(ruleId, url, message) {
60
+ const rule = VISUAL_RULES.find((r) => r.id === ruleId);
61
+ return {
62
+ url,
63
+ rule: ruleId,
64
+ severity: rule?.severity ?? "warn",
65
+ standard: rule?.standard,
66
+ message,
67
+ };
38
68
  }
39
69
 
40
- const C = {
41
- red: "\x1b[31m",
42
- yellow: "\x1b[33m",
43
- dim: "\x1b[2m",
44
- bold: "\x1b[1m",
45
- reset: "\x1b[0m",
46
- };
70
+ /**
71
+ * Assemble the machine-readable result (pure). `findings` are PRODUCT results; `errors`
72
+ * are INFRASTRUCTURE failures (a page that would not load, axe that would not inject).
73
+ * `status` is "ok" only when no infra error occurred, "error" when nothing could be
74
+ * audited, "partial" when some pages ran and some failed.
75
+ */
76
+ export function buildResult({ findings = [], errors = [], pages = 0 } = {}) {
77
+ const productErrors = findings.filter((f) => f.severity === "error");
78
+ const warnings = findings.filter((f) => f.severity === "warn");
79
+ const failedToLoad = errors.filter((e) => e.stage === "navigate").length;
80
+ const audited = pages - failedToLoad;
81
+ const status = errors.length === 0 ? "ok" : audited > 0 ? "partial" : "error";
82
+ return {
83
+ status,
84
+ tool: TESTED_VERSIONS,
85
+ summary: {
86
+ errors: productErrors.length,
87
+ warnings: warnings.length,
88
+ pages,
89
+ infrastructureErrors: errors.length,
90
+ },
91
+ findings,
92
+ errors,
93
+ };
94
+ }
47
95
 
48
- const urls = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--format");
49
- if (urls.length === 0) {
50
- console.error(
51
- "usage: visual-audit.mjs [--format json] [--strict] <baseUrl> [route …]\n" +
52
- " (baseUrl is the running app; extra args are routes appended to it)",
53
- );
54
- process.exit(2);
96
+ /**
97
+ * Assemble a BOOTSTRAP failure result (pure) — missing peers, no browser, bad args.
98
+ * `summary` is null (never `{ warnings: 0 }`) so a consumer cannot read a bootstrap
99
+ * failure as a clean pass.
100
+ */
101
+ export function buildBootstrapError(message, hint) {
102
+ return {
103
+ status: "error",
104
+ tool: TESTED_VERSIONS,
105
+ summary: null,
106
+ findings: [],
107
+ errors: [{ stage: "bootstrap", message, ...(hint ? { hint } : {}) }],
108
+ };
55
109
  }
56
- const base = urls[0].replace(/\/$/, "");
57
- const routes = urls.length > 1 ? urls.slice(1) : ["/"];
58
- const targets = routes.map((r) =>
59
- r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
60
- );
61
110
 
62
- /** Optional-peer loader print a clear install hint instead of a stack trace. */
63
- async function loadDeps() {
64
- try {
65
- const [{ chromium }, axe] = await Promise.all([
66
- import("playwright"),
67
- import("@axe-core/playwright"),
68
- ]);
69
- return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
70
- } catch {
71
- console.error(
72
- "visual-audit needs the optional peers `playwright` and `@axe-core/playwright`:\n" +
73
- " pnpm add -D playwright @axe-core/playwright\n" +
74
- " pnpm exec playwright install chromium\n" +
75
- "(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).",
76
- );
77
- process.exit(2);
78
- }
111
+ /** Exit code for a result: 2 = could not audit, 1 = findings/partial gate, 0 = clean. */
112
+ export function exitCodeFor(result, { strict = false } = {}) {
113
+ if (result.status === "error") return 2;
114
+ if (result.status === "partial") return 1;
115
+ if (strict && result.findings.length > 0) return 1;
116
+ if (result.summary && result.summary.errors > 0) return 1;
117
+ return 0;
79
118
  }
80
119
 
81
120
  /** Collect raw measurements from the rendered page; the PURE rules run back in node. */
@@ -160,102 +199,230 @@ function collectInPage() {
160
199
  }
161
200
  /* c8 ignore stop */
162
201
 
163
- async function run() {
164
- const { chromium, AxeBuilder } = await loadDeps();
165
- const browser = await chromium.launch({
166
- executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined,
167
- });
168
- const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
169
- const findings = [];
170
- const add = (ruleId, url, message) => {
171
- const rule = VISUAL_RULES.find((r) => r.id === ruleId);
172
- findings.push({
173
- url,
174
- rule: ruleId,
175
- severity: rule?.severity ?? "warn",
176
- standard: rule?.standard,
177
- message,
178
- });
179
- };
202
+ /** Optional-peer loader — throws a tagged bootstrap error instead of a raw stack. */
203
+ async function loadDeps() {
204
+ try {
205
+ const [{ chromium }, axe] = await Promise.all([
206
+ import("playwright"),
207
+ import("@axe-core/playwright"),
208
+ ]);
209
+ return { chromium, AxeBuilder: axe.default ?? axe.AxeBuilder };
210
+ } catch (cause) {
211
+ const err = new Error(
212
+ "visual-audit needs the optional peers `playwright` and `@axe-core/playwright`",
213
+ );
214
+ err.bootstrap = true;
215
+ err.hint =
216
+ "pnpm add -D playwright @axe-core/playwright && pnpm exec playwright install chromium " +
217
+ "(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
218
+ err.cause = cause;
219
+ throw err;
220
+ }
221
+ }
180
222
 
181
- for (const url of targets) {
223
+ /**
224
+ * Drive the browser over every target and gather findings + infra errors. Cleanup
225
+ * (page/context/browser) is guaranteed via finally on BOTH success and failure.
226
+ * A launch failure is re-tagged as a bootstrap error so it surfaces as `status:error`.
227
+ */
228
+ /* c8 ignore start — the browser-driving glue; exercised by scripts/visual-audit-smoke.mjs in CI. */
229
+ async function audit(targets, { chromium, AxeBuilder }) {
230
+ const findings = [];
231
+ const errors = [];
232
+ let browser;
233
+ let context;
234
+ try {
182
235
  try {
183
- await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
184
- } catch (e) {
185
- add("axe-violations", url, `could not load page: ${e.message}`);
186
- continue;
236
+ browser = await chromium.launch({
237
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined,
238
+ });
239
+ } catch (cause) {
240
+ const err = new Error(`could not launch Chromium: ${cause.message}`);
241
+ err.bootstrap = true;
242
+ err.hint =
243
+ "install a browser: pnpm exec playwright install chromium " +
244
+ "(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
245
+ throw err;
187
246
  }
247
+ context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
248
+ const page = await context.newPage();
188
249
 
189
- // 1) axe-core real WCAG/ARIA engine.
190
- const { violations } = await new AxeBuilder({ page }).analyze();
191
- for (const v of violations) {
192
- add(
193
- "axe-violations",
194
- url,
195
- `${v.id} (${v.impact}) — ${v.help} [${v.nodes.length} node(s)] ${v.helpUrl}`,
196
- );
197
- }
250
+ for (const url of targets) {
251
+ try {
252
+ await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
253
+ } catch (e) {
254
+ errors.push({ url, stage: "navigate", message: `could not load page: ${e.message}` });
255
+ continue;
256
+ }
198
257
 
199
- // 2) computed-style / layout heuristics via the PURE rules.
200
- const m = await page.evaluate(collectInPage);
201
- for (const a of m.accents) {
202
- if (isOversaturated(a.rgb))
203
- add(
204
- "oversaturated-accent",
205
- url,
206
- `<${a.tag}> background OKLCH chroma ${oklchChroma(a.rgb).toFixed(3)} > 0.18 (rgb ${a.rgb.r},${a.rgb.g},${a.rgb.b})`,
207
- );
208
- }
209
- for (const t of m.targets) {
210
- if (isUndersizedTarget(t))
211
- add(
212
- "target-size-min",
213
- url,
214
- `target "${t.name || "(unnamed)"}" is ${t.width}×${t.height}px (< 24×24)`,
215
- );
216
- }
217
- if (hasEmoji(m.text)) {
218
- const found = (m.text.match(/\p{Extended_Pictographic}/gu) || []).slice(0, 8).join(" ");
219
- add("emoji-rendered", url, `emoji rendered in product text: ${found}`);
258
+ try {
259
+ // 1) axe-core real WCAG/ARIA engine.
260
+ const { violations } = await new AxeBuilder({ page }).analyze();
261
+ for (const v of violations) {
262
+ findings.push(
263
+ buildFinding(
264
+ "axe-violations",
265
+ url,
266
+ `${v.id} (${v.impact}) — ${v.help} [${v.nodes.length} node(s)] ${v.helpUrl}`,
267
+ ),
268
+ );
269
+ }
270
+
271
+ // 2) computed-style / layout heuristics via the PURE rules.
272
+ const m = await page.evaluate(collectInPage);
273
+ for (const a of m.accents) {
274
+ if (isOversaturated(a.rgb))
275
+ findings.push(
276
+ buildFinding(
277
+ "oversaturated-accent",
278
+ url,
279
+ `<${a.tag}> background OKLCH chroma ${oklchChroma(a.rgb).toFixed(3)} > 0.18 (rgb ${a.rgb.r},${a.rgb.g},${a.rgb.b})`,
280
+ ),
281
+ );
282
+ }
283
+ for (const t of m.targets) {
284
+ if (isUndersizedTarget(t))
285
+ findings.push(
286
+ buildFinding(
287
+ "target-size-min",
288
+ url,
289
+ `target "${t.name || "(unnamed)"}" is ${t.width}×${t.height}px (< 24×24)`,
290
+ ),
291
+ );
292
+ }
293
+ if (hasEmoji(m.text)) {
294
+ const found = (m.text.match(/\p{Extended_Pictographic}/gu) || []).slice(0, 8).join(" ");
295
+ findings.push(
296
+ buildFinding("emoji-rendered", url, `emoji rendered in product text: ${found}`),
297
+ );
298
+ }
299
+ m.alerts.forEach((al, i) => {
300
+ for (const issue of alertControlIssues(al))
301
+ findings.push(
302
+ buildFinding("alert-controls-misplaced", url, `banner #${i + 1}: ${issue}`),
303
+ );
304
+ });
305
+ } catch (e) {
306
+ errors.push({ url, stage: "analyze", message: `audit engine failed: ${e.message}` });
307
+ }
220
308
  }
221
- m.alerts.forEach((al, i) => {
222
- for (const issue of alertControlIssues(al))
223
- add("alert-controls-misplaced", url, `banner #${i + 1}: ${issue}`);
224
- });
309
+ } finally {
310
+ await context?.close().catch(() => {});
311
+ await browser?.close().catch(() => {});
225
312
  }
226
313
 
227
- await browser.close();
228
- report(findings);
314
+ return buildResult({ findings, errors, pages: targets.length });
229
315
  }
316
+ /* c8 ignore stop */
230
317
 
231
- function report(findings) {
232
- const errors = findings.filter((f) => f.severity === "error");
233
- const warnings = findings.filter((f) => f.severity === "warn");
318
+ const C = {
319
+ red: "\x1b[31m",
320
+ yellow: "\x1b[33m",
321
+ dim: "\x1b[2m",
322
+ bold: "\x1b[1m",
323
+ reset: "\x1b[0m",
324
+ };
325
+
326
+ /** Render a result to stdout in the requested format, then exit with the right code. */
327
+ /* c8 ignore start — thin process-exit glue, driven by the smoke/CLI paths. */
328
+ function finish(result, { asJson, strict, pages }) {
329
+ const code = exitCodeFor(result, { strict });
234
330
  if (asJson) {
235
- process.stdout.write(
236
- JSON.stringify(
237
- { summary: { errors: errors.length, warnings: warnings.length }, findings },
238
- null,
239
- 2,
240
- ) + "\n",
241
- );
242
- } else {
243
- for (const f of findings) {
244
- const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
245
- console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
246
- console.log(` ${f.message}`);
247
- if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
331
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
332
+ process.exit(code);
333
+ }
334
+
335
+ // Bootstrap failure — no page was audited.
336
+ if (result.summary === null) {
337
+ for (const e of result.errors) {
338
+ process.stderr.write(`visual-audit failed: ${e.message}\n`);
339
+ if (e.hint) process.stderr.write(` ${e.hint}\n`);
248
340
  }
341
+ process.exit(code);
342
+ }
343
+
344
+ for (const f of result.findings) {
345
+ const tag = f.severity === "error" ? `${C.red}error${C.reset}` : `${C.yellow}warn ${C.reset}`;
346
+ console.log(`${tag} ${C.bold}${f.url}${C.reset} ${C.dim}[${f.rule}]${C.reset}`);
347
+ console.log(` ${f.message}`);
348
+ if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
349
+ }
350
+ for (const e of result.errors) {
249
351
  console.log(
250
- `\ngodxjp-ui visual-audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset} across ${targets.length} page(s).`,
352
+ `${C.red}infra${C.reset} ${C.bold}${e.url ?? "(bootstrap)"}${C.reset} ${C.dim}[${e.stage}]${C.reset}`,
251
353
  );
252
- if (findings.length === 0) console.log("✓ No visual/runtime a11y violations found.");
354
+ console.log(` ${e.message}`);
253
355
  }
254
- // Warnings never block; --strict turns any finding into a non-zero exit (CI gate).
255
- process.exit(strict && findings.length > 0 ? 1 : errors.length > 0 ? 1 : 0);
356
+ const { errors, warnings } = result.summary;
357
+ console.log(
358
+ `\ngodxjp-ui visual-audit [${result.status}]: ${C.red}${errors} error(s)${C.reset}, ` +
359
+ `${C.yellow}${warnings} warning(s)${C.reset}` +
360
+ (result.errors.length ? `, ${C.red}${result.errors.length} infra error(s)${C.reset}` : "") +
361
+ ` across ${pages} page(s).`,
362
+ );
363
+ if (result.status === "ok" && result.findings.length === 0)
364
+ console.log("✓ No visual/runtime a11y violations found.");
365
+ process.exit(code);
256
366
  }
367
+ /* c8 ignore stop */
368
+
369
+ /** CLI entry — parse args, run the audit, print, exit. */
370
+ /* c8 ignore start — CLI driver, exercised via subprocess by the smoke test. */
371
+ async function main() {
372
+ const args = process.argv.slice(2);
373
+ const fmtIdx = args.indexOf("--format");
374
+ const asJson = fmtIdx !== -1 && args[fmtIdx + 1] === "json";
375
+ const strict = args.includes("--strict");
376
+
377
+ if (args.includes("--rules")) {
378
+ process.stdout.write(JSON.stringify(VISUAL_RULES, null, 2) + "\n");
379
+ process.exit(0);
380
+ }
381
+
382
+ const urls = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--format");
383
+ if (urls.length === 0) {
384
+ const msg =
385
+ "no baseUrl given — usage: visual-audit.mjs [--format json] [--strict] <baseUrl> [route …] " +
386
+ "(baseUrl is the running app; extra args are routes appended to it)";
387
+ finish(buildBootstrapError(msg), { asJson, strict, pages: 0 });
388
+ return;
389
+ }
257
390
 
258
- run().catch((e) => {
259
- console.error(`visual-audit failed: ${e.message}`);
260
- process.exit(2);
261
- });
391
+ const base = urls[0].replace(/\/$/, "");
392
+ const routes = urls.length > 1 ? urls.slice(1) : ["/"];
393
+ const targets = routes.map((r) =>
394
+ r.startsWith("http") ? r : `${base}${r.startsWith("/") ? r : `/${r}`}`,
395
+ );
396
+
397
+ let deps;
398
+ try {
399
+ deps = await loadDeps();
400
+ } catch (e) {
401
+ finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
402
+ return;
403
+ }
404
+
405
+ try {
406
+ const result = await audit(targets, deps);
407
+ finish(result, { asJson, strict, pages: targets.length });
408
+ } catch (e) {
409
+ // A launch/setup failure = infrastructure error (never "zero violations").
410
+ finish(buildBootstrapError(e.message, e.hint), { asJson, strict, pages: targets.length });
411
+ }
412
+ }
413
+ /* c8 ignore stop */
414
+
415
+ // Run only as a CLI — importing this module (for unit tests) must NOT execute.
416
+ /* c8 ignore start */
417
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
418
+ main().catch((e) => {
419
+ const asJson =
420
+ process.argv.indexOf("--format") !== -1 &&
421
+ process.argv[process.argv.indexOf("--format") + 1] === "json";
422
+ if (asJson) {
423
+ process.stdout.write(JSON.stringify(buildBootstrapError(e.message, e.hint)) + "\n");
424
+ } else console.error(`visual-audit failed: ${e.message}`);
425
+ process.exit(2);
426
+ });
427
+ }
428
+ /* c8 ignore stop */