@reddoorla/maintenance 0.1.0 → 0.1.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.
package/dist/cli/bin.js CHANGED
@@ -213,6 +213,48 @@ function classify(v) {
213
213
  if (v.moderate > 0 || v.low > 0) return "warn";
214
214
  return "pass";
215
215
  }
216
+ function normalizeSeverity(s) {
217
+ if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
218
+ return "moderate";
219
+ }
220
+ function extractAdvisoriesFromPnpm(parsed) {
221
+ const out = [];
222
+ for (const a of Object.values(parsed.advisories ?? {})) {
223
+ if (!a) continue;
224
+ out.push({
225
+ module: a.module_name ?? "unknown",
226
+ severity: normalizeSeverity(a.severity),
227
+ title: a.title ?? "(no title)",
228
+ ...a.cves ? { cves: a.cves } : {},
229
+ ...a.url ? { url: a.url } : {}
230
+ });
231
+ }
232
+ return out;
233
+ }
234
+ function extractAdvisoriesFromNpm(parsed) {
235
+ const out = [];
236
+ for (const [name, v] of Object.entries(parsed.vulnerabilities ?? {})) {
237
+ if (!v) continue;
238
+ let title = name;
239
+ let url;
240
+ if (Array.isArray(v.via)) {
241
+ const detailed = v.via.find(
242
+ (entry) => typeof entry === "object" && entry !== null
243
+ );
244
+ if (detailed) {
245
+ title = detailed.title ?? name;
246
+ url = detailed.url;
247
+ }
248
+ }
249
+ out.push({
250
+ module: v.name ?? name,
251
+ severity: normalizeSeverity(v.severity),
252
+ title,
253
+ ...url ? { url } : {}
254
+ });
255
+ }
256
+ return out;
257
+ }
216
258
  async function tryRun(spawn2, cmd, args, cwd) {
217
259
  try {
218
260
  return await spawn2(cmd, args, { cwd });
@@ -266,26 +308,27 @@ async function securityAudit(ctx) {
266
308
  details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
267
309
  };
268
310
  }
269
- const vuln = {
311
+ const counts = {
270
312
  low: parsed.metadata?.vulnerabilities?.low ?? 0,
271
313
  moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
272
314
  high: parsed.metadata?.vulnerabilities?.high ?? 0,
273
315
  critical: parsed.metadata?.vulnerabilities?.critical ?? 0
274
316
  };
275
- const status = classify(vuln);
276
- const total = vuln.low + vuln.moderate + vuln.high + vuln.critical;
277
- const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${vuln.critical}C/${vuln.high}H/${vuln.moderate}M/${vuln.low}L)`;
317
+ const advisories = used === "pnpm audit" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);
318
+ const status = classify(counts);
319
+ const total = counts.low + counts.moderate + counts.high + counts.critical;
320
+ const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;
278
321
  return {
279
322
  audit: "security",
280
323
  site: label,
281
324
  status,
282
325
  summary,
283
- details: vuln
326
+ details: { counts, advisories }
284
327
  };
285
328
  }
286
329
 
287
330
  // src/audits/lighthouse.ts
288
- import { writeFile, mkdtemp, rm } from "fs/promises";
331
+ import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
289
332
  import { tmpdir } from "os";
290
333
  import { join as join3 } from "path";
291
334
 
@@ -321,29 +364,56 @@ var lighthouseConfig = {
321
364
  function siteLabel4(site) {
322
365
  return site.name ?? site.path;
323
366
  }
324
- function isFakeShape(stdout) {
367
+ async function readJsonMaybe(path) {
325
368
  try {
326
- const parsed = JSON.parse(stdout);
327
- if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
369
+ const raw = await readFile3(path, "utf-8");
370
+ return JSON.parse(raw);
328
371
  } catch {
329
372
  return null;
330
373
  }
331
- return null;
374
+ }
375
+ function averageSummaries(entries) {
376
+ if (entries.length === 0) return {};
377
+ const sums = {};
378
+ const counts = {};
379
+ for (const e of entries) {
380
+ for (const [k, v] of Object.entries(e.summary ?? {})) {
381
+ if (typeof v !== "number") continue;
382
+ sums[k] = (sums[k] ?? 0) + v;
383
+ counts[k] = (counts[k] ?? 0) + 1;
384
+ }
385
+ }
386
+ const out = {};
387
+ for (const k of Object.keys(sums)) {
388
+ const total = sums[k] ?? 0;
389
+ const count = counts[k] ?? 1;
390
+ out[k] = total / count;
391
+ }
392
+ return out;
393
+ }
394
+ function categoryFromAssertion(a) {
395
+ const colonIdx = a.name.indexOf(":");
396
+ return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
397
+ }
398
+ function messageForAssertion(a) {
399
+ return `${a.name} ${a.operator} ${a.expected} (actual: ${a.actual.toFixed(2)})`;
332
400
  }
333
401
  async function lighthouseAudit(ctx) {
334
402
  const spawn2 = ctx.spawn ?? defaultSpawn;
335
403
  const site = ctx.site;
336
404
  const label = siteLabel4(site);
337
- const dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
338
- const configPath = join3(dir, "lighthouserc.json");
405
+ const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
406
+ const configPath = join3(configDir, "lighthouserc.json");
339
407
  await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
408
+ const resultsDir = join3(site.path, ".lighthouseci");
409
+ await rm(resultsDir, { recursive: true, force: true });
340
410
  let raw;
341
411
  try {
342
412
  raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
343
413
  cwd: site.path
344
414
  });
345
415
  } catch (err) {
346
- await rm(dir, { recursive: true, force: true });
416
+ await rm(configDir, { recursive: true, force: true });
347
417
  const e = err;
348
418
  if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
349
419
  return {
@@ -355,17 +425,32 @@ async function lighthouseAudit(ctx) {
355
425
  }
356
426
  throw err;
357
427
  }
358
- await rm(dir, { recursive: true, force: true });
359
- const fake = isFakeShape(raw.stdout);
360
- const normalized = fake ?? {
361
- summary: {},
362
- assertionsFailed: raw.code === 0 ? 0 : 1,
363
- assertions: raw.code === 0 ? [] : [{ category: "unknown", level: "error", message: raw.stderr.slice(0, 200) }]
364
- };
365
- const anyError = (normalized.assertions ?? []).some((a) => a.level === "error");
366
- const anyWarn = (normalized.assertions ?? []).some((a) => a.level === "warn");
428
+ await rm(configDir, { recursive: true, force: true });
429
+ const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
430
+ if (!manifest || manifest.length === 0) {
431
+ return {
432
+ audit: "lighthouse",
433
+ site: label,
434
+ status: "fail",
435
+ summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
436
+ };
437
+ }
438
+ const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
439
+ const failed = assertionResults.filter((a) => !a.passed);
440
+ const assertions = failed.map((a) => ({
441
+ category: categoryFromAssertion(a),
442
+ level: a.level,
443
+ message: messageForAssertion(a)
444
+ }));
445
+ const anyError = assertions.some((a) => a.level === "error");
446
+ const anyWarn = assertions.some((a) => a.level === "warn");
367
447
  const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
368
- const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
448
+ const normalized = {
449
+ summary: averageSummaries(manifest),
450
+ assertionsFailed: failed.length,
451
+ assertions
452
+ };
453
+ const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
369
454
  return {
370
455
  audit: "lighthouse",
371
456
  site: label,
@@ -376,7 +461,7 @@ async function lighthouseAudit(ctx) {
376
461
  }
377
462
 
378
463
  // src/audits/a11y.ts
379
- import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
464
+ import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
380
465
  import { tmpdir as tmpdir2 } from "os";
381
466
  import { join as join4 } from "path";
382
467
 
@@ -412,48 +497,77 @@ var playwrightA11yConfig = defineConfig({
412
497
  });
413
498
 
414
499
  // src/audits/a11y.ts
500
+ var RESULTS_REL = ".reddoor-a11y/results.json";
415
501
  function siteLabel5(site) {
416
502
  return site.name ?? site.path;
417
503
  }
418
- function isFakeShape2(stdout) {
504
+ async function readJsonMaybe2(path) {
419
505
  try {
420
- const parsed = JSON.parse(stdout);
421
- if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
506
+ const raw = await readFile4(path, "utf-8");
507
+ return JSON.parse(raw);
422
508
  } catch {
423
509
  return null;
424
510
  }
425
- return null;
426
511
  }
427
512
  function buildSpec() {
428
- return `
429
- import { test, expect } from "@playwright/test";
513
+ return `import { test, expect } from "@playwright/test";
430
514
  import AxeBuilder from "@axe-core/playwright";
515
+ import { mkdir, writeFile } from "node:fs/promises";
516
+ import { dirname } from "node:path";
517
+
431
518
  const pages = ${JSON.stringify(a11yRoutes)};
432
- for (const { path, name } of pages) {
433
- test(\`\${name} has no axe violations\`, async ({ page }) => {
519
+ const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
520
+
521
+ test("a11y across configured routes", async ({ page }) => {
522
+ const violations = [];
523
+ for (const { path, name } of pages) {
434
524
  await page.goto(path);
435
525
  const results = await new AxeBuilder({ page })
436
526
  .withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
437
527
  .analyze();
438
- expect(results.violations).toEqual([]);
439
- });
440
- }
528
+ for (const v of results.violations) {
529
+ violations.push({
530
+ id: v.id,
531
+ impact: v.impact ?? "moderate",
532
+ route: name,
533
+ help: v.help,
534
+ helpUrl: v.helpUrl,
535
+ nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),
536
+ });
537
+ }
538
+ }
539
+ const byImpact = {};
540
+ for (const v of violations) {
541
+ byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;
542
+ }
543
+ if (OUTPUT) {
544
+ await mkdir(dirname(OUTPUT), { recursive: true });
545
+ await writeFile(
546
+ OUTPUT,
547
+ JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),
548
+ );
549
+ }
550
+ expect(violations).toEqual([]);
551
+ });
441
552
  `;
442
553
  }
443
554
  async function a11yAudit(ctx) {
444
555
  const spawn2 = ctx.spawn ?? defaultSpawn;
445
556
  const site = ctx.site;
446
557
  const label = siteLabel5(site);
447
- const dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
448
- const specPath = join4(dir, "a11y.spec.ts");
558
+ const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
559
+ const specPath = join4(specDir, "a11y.spec.ts");
449
560
  await writeFile2(specPath, buildSpec(), "utf-8");
561
+ const resultsPath = join4(site.path, RESULTS_REL);
562
+ await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
450
563
  let raw;
451
564
  try {
452
- raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=json", specPath], {
453
- cwd: site.path
565
+ raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
566
+ cwd: site.path,
567
+ env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath }
454
568
  });
455
569
  } catch (err) {
456
- await rm2(dir, { recursive: true, force: true });
570
+ await rm2(specDir, { recursive: true, force: true });
457
571
  const e = err;
458
572
  if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
459
573
  return {
@@ -465,22 +579,26 @@ async function a11yAudit(ctx) {
465
579
  }
466
580
  throw err;
467
581
  }
468
- await rm2(dir, { recursive: true, force: true });
469
- const fake = isFakeShape2(raw.stdout);
470
- const normalized = fake ?? {
471
- totalViolations: raw.code === 0 ? 0 : 1,
472
- byImpact: raw.code === 0 ? {} : { moderate: 1 }
473
- };
474
- const hasSerious = (normalized.byImpact.serious ?? 0) > 0 || (normalized.byImpact.critical ?? 0) > 0;
475
- const hasAny = normalized.totalViolations > 0;
582
+ await rm2(specDir, { recursive: true, force: true });
583
+ const artifact = await readJsonMaybe2(resultsPath);
584
+ if (!artifact) {
585
+ return {
586
+ audit: "a11y",
587
+ site: label,
588
+ status: "fail",
589
+ summary: `a11y: no results written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
590
+ };
591
+ }
592
+ const hasSerious = (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;
593
+ const hasAny = artifact.totalViolations > 0;
476
594
  const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
477
- const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${normalized.totalViolations} violations`;
595
+ const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${artifact.totalViolations} violations`;
478
596
  return {
479
597
  audit: "a11y",
480
598
  site: label,
481
599
  status,
482
600
  summary,
483
- details: normalized
601
+ details: artifact
484
602
  };
485
603
  }
486
604
 
@@ -530,7 +648,7 @@ function localPath(path, opts = {}) {
530
648
  }
531
649
 
532
650
  // src/inventory/json.ts
533
- import { readFile as readFile3 } from "fs/promises";
651
+ import { readFile as readFile5 } from "fs/promises";
534
652
  function validate(raw) {
535
653
  if (!Array.isArray(raw)) {
536
654
  throw new Error("inventory JSON must be an array of sites");
@@ -554,7 +672,7 @@ function validate(raw) {
554
672
  }
555
673
  function fromJsonFile(path) {
556
674
  return async () => {
557
- const raw = JSON.parse(await readFile3(path, "utf-8"));
675
+ const raw = JSON.parse(await readFile5(path, "utf-8"));
558
676
  return validate(raw);
559
677
  };
560
678
  }
@@ -670,11 +788,11 @@ async function runAuditCommand(site, opts) {
670
788
  }
671
789
 
672
790
  // src/cli/commands/sync-configs.ts
673
- import { readFile as readFile5 } from "fs/promises";
791
+ import { readFile as readFile7 } from "fs/promises";
674
792
  import { join as join7, resolve as resolve3 } from "path";
675
793
 
676
794
  // src/recipes/sync-configs.ts
677
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
795
+ import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
678
796
  import { join as join6 } from "path";
679
797
 
680
798
  // src/recipes/sync-configs/templates.ts
@@ -759,7 +877,7 @@ function siteLabel6(site) {
759
877
  }
760
878
  async function readMaybe(path) {
761
879
  try {
762
- return await readFile4(path, "utf-8");
880
+ return await readFile6(path, "utf-8");
763
881
  } catch {
764
882
  return null;
765
883
  }
@@ -818,7 +936,7 @@ async function dryPlan(cwd, which) {
818
936
  for (const t of targets) {
819
937
  let existing = "";
820
938
  try {
821
- existing = await readFile5(join7(cwd, t.path), "utf-8");
939
+ existing = await readFile7(join7(cwd, t.path), "utf-8");
822
940
  } catch {
823
941
  }
824
942
  if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
@@ -952,9 +1070,9 @@ import { resolve as resolve5 } from "path";
952
1070
  import { join as join13 } from "path";
953
1071
 
954
1072
  // src/util/pkg.ts
955
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1073
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
956
1074
  async function readPackageJson(path) {
957
- const raw = await readFile6(path, "utf-8");
1075
+ const raw = await readFile8(path, "utf-8");
958
1076
  return JSON.parse(raw);
959
1077
  }
960
1078
  async function writePackageJson(path, pkg2) {
@@ -1011,13 +1129,13 @@ async function bumpToSvelte5Versions(cwd) {
1011
1129
  }
1012
1130
 
1013
1131
  // src/recipes/svelte-5/step-svelte-config.ts
1014
- import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
1132
+ import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
1015
1133
  import { join as join9 } from "path";
1016
1134
  async function migrateSvelteConfig(cwd) {
1017
1135
  const path = join9(cwd, "svelte.config.js");
1018
1136
  let src;
1019
1137
  try {
1020
- src = await readFile7(path, "utf-8");
1138
+ src = await readFile9(path, "utf-8");
1021
1139
  } catch {
1022
1140
  return false;
1023
1141
  }
@@ -1077,7 +1195,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1077
1195
  }
1078
1196
 
1079
1197
  // src/recipes/svelte-5/step-gotchas.ts
1080
- import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
1198
+ import { readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
1081
1199
  import { join as join11 } from "path";
1082
1200
  import { glob as glob2 } from "tinyglobby";
1083
1201
 
@@ -1152,7 +1270,7 @@ async function applyGotchaCodemods(cwd) {
1152
1270
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
1153
1271
  for (const rel of relPaths) {
1154
1272
  const path = join11(cwd, rel);
1155
- const before = await readFile8(path, "utf-8");
1273
+ const before = await readFile10(path, "utf-8");
1156
1274
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
1157
1275
  if (after !== before) {
1158
1276
  await writeFile6(path, after, "utf-8");