@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/index.js CHANGED
@@ -202,6 +202,48 @@ function classify(v) {
202
202
  if (v.moderate > 0 || v.low > 0) return "warn";
203
203
  return "pass";
204
204
  }
205
+ function normalizeSeverity(s) {
206
+ if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
207
+ return "moderate";
208
+ }
209
+ function extractAdvisoriesFromPnpm(parsed) {
210
+ const out = [];
211
+ for (const a of Object.values(parsed.advisories ?? {})) {
212
+ if (!a) continue;
213
+ out.push({
214
+ module: a.module_name ?? "unknown",
215
+ severity: normalizeSeverity(a.severity),
216
+ title: a.title ?? "(no title)",
217
+ ...a.cves ? { cves: a.cves } : {},
218
+ ...a.url ? { url: a.url } : {}
219
+ });
220
+ }
221
+ return out;
222
+ }
223
+ function extractAdvisoriesFromNpm(parsed) {
224
+ const out = [];
225
+ for (const [name, v] of Object.entries(parsed.vulnerabilities ?? {})) {
226
+ if (!v) continue;
227
+ let title = name;
228
+ let url;
229
+ if (Array.isArray(v.via)) {
230
+ const detailed = v.via.find(
231
+ (entry) => typeof entry === "object" && entry !== null
232
+ );
233
+ if (detailed) {
234
+ title = detailed.title ?? name;
235
+ url = detailed.url;
236
+ }
237
+ }
238
+ out.push({
239
+ module: v.name ?? name,
240
+ severity: normalizeSeverity(v.severity),
241
+ title,
242
+ ...url ? { url } : {}
243
+ });
244
+ }
245
+ return out;
246
+ }
205
247
  async function tryRun(spawn2, cmd, args, cwd) {
206
248
  try {
207
249
  return await spawn2(cmd, args, { cwd });
@@ -255,26 +297,27 @@ async function securityAudit(ctx) {
255
297
  details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
256
298
  };
257
299
  }
258
- const vuln = {
300
+ const counts = {
259
301
  low: parsed.metadata?.vulnerabilities?.low ?? 0,
260
302
  moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
261
303
  high: parsed.metadata?.vulnerabilities?.high ?? 0,
262
304
  critical: parsed.metadata?.vulnerabilities?.critical ?? 0
263
305
  };
264
- const status = classify(vuln);
265
- const total = vuln.low + vuln.moderate + vuln.high + vuln.critical;
266
- const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${vuln.critical}C/${vuln.high}H/${vuln.moderate}M/${vuln.low}L)`;
306
+ const advisories = used === "pnpm audit" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);
307
+ const status = classify(counts);
308
+ const total = counts.low + counts.moderate + counts.high + counts.critical;
309
+ const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;
267
310
  return {
268
311
  audit: "security",
269
312
  site: label,
270
313
  status,
271
314
  summary,
272
- details: vuln
315
+ details: { counts, advisories }
273
316
  };
274
317
  }
275
318
 
276
319
  // src/audits/lighthouse.ts
277
- import { writeFile, mkdtemp, rm } from "fs/promises";
320
+ import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
278
321
  import { tmpdir } from "os";
279
322
  import { join as join3 } from "path";
280
323
 
@@ -310,29 +353,56 @@ var lighthouseConfig = {
310
353
  function siteLabel4(site) {
311
354
  return site.name ?? site.path;
312
355
  }
313
- function isFakeShape(stdout) {
356
+ async function readJsonMaybe(path) {
314
357
  try {
315
- const parsed = JSON.parse(stdout);
316
- if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
358
+ const raw = await readFile3(path, "utf-8");
359
+ return JSON.parse(raw);
317
360
  } catch {
318
361
  return null;
319
362
  }
320
- return null;
363
+ }
364
+ function averageSummaries(entries) {
365
+ if (entries.length === 0) return {};
366
+ const sums = {};
367
+ const counts = {};
368
+ for (const e of entries) {
369
+ for (const [k, v] of Object.entries(e.summary ?? {})) {
370
+ if (typeof v !== "number") continue;
371
+ sums[k] = (sums[k] ?? 0) + v;
372
+ counts[k] = (counts[k] ?? 0) + 1;
373
+ }
374
+ }
375
+ const out = {};
376
+ for (const k of Object.keys(sums)) {
377
+ const total = sums[k] ?? 0;
378
+ const count = counts[k] ?? 1;
379
+ out[k] = total / count;
380
+ }
381
+ return out;
382
+ }
383
+ function categoryFromAssertion(a) {
384
+ const colonIdx = a.name.indexOf(":");
385
+ return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
386
+ }
387
+ function messageForAssertion(a) {
388
+ return `${a.name} ${a.operator} ${a.expected} (actual: ${a.actual.toFixed(2)})`;
321
389
  }
322
390
  async function lighthouseAudit(ctx) {
323
391
  const spawn2 = ctx.spawn ?? defaultSpawn;
324
392
  const site = ctx.site;
325
393
  const label = siteLabel4(site);
326
- const dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
327
- const configPath = join3(dir, "lighthouserc.json");
394
+ const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
395
+ const configPath = join3(configDir, "lighthouserc.json");
328
396
  await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
397
+ const resultsDir = join3(site.path, ".lighthouseci");
398
+ await rm(resultsDir, { recursive: true, force: true });
329
399
  let raw;
330
400
  try {
331
401
  raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
332
402
  cwd: site.path
333
403
  });
334
404
  } catch (err) {
335
- await rm(dir, { recursive: true, force: true });
405
+ await rm(configDir, { recursive: true, force: true });
336
406
  const e = err;
337
407
  if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
338
408
  return {
@@ -344,17 +414,32 @@ async function lighthouseAudit(ctx) {
344
414
  }
345
415
  throw err;
346
416
  }
347
- await rm(dir, { recursive: true, force: true });
348
- const fake = isFakeShape(raw.stdout);
349
- const normalized = fake ?? {
350
- summary: {},
351
- assertionsFailed: raw.code === 0 ? 0 : 1,
352
- assertions: raw.code === 0 ? [] : [{ category: "unknown", level: "error", message: raw.stderr.slice(0, 200) }]
353
- };
354
- const anyError = (normalized.assertions ?? []).some((a) => a.level === "error");
355
- const anyWarn = (normalized.assertions ?? []).some((a) => a.level === "warn");
417
+ await rm(configDir, { recursive: true, force: true });
418
+ const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
419
+ if (!manifest || manifest.length === 0) {
420
+ return {
421
+ audit: "lighthouse",
422
+ site: label,
423
+ status: "fail",
424
+ summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
425
+ };
426
+ }
427
+ const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
428
+ const failed = assertionResults.filter((a) => !a.passed);
429
+ const assertions = failed.map((a) => ({
430
+ category: categoryFromAssertion(a),
431
+ level: a.level,
432
+ message: messageForAssertion(a)
433
+ }));
434
+ const anyError = assertions.some((a) => a.level === "error");
435
+ const anyWarn = assertions.some((a) => a.level === "warn");
356
436
  const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
357
- const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
437
+ const normalized = {
438
+ summary: averageSummaries(manifest),
439
+ assertionsFailed: failed.length,
440
+ assertions
441
+ };
442
+ const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
358
443
  return {
359
444
  audit: "lighthouse",
360
445
  site: label,
@@ -365,7 +450,7 @@ async function lighthouseAudit(ctx) {
365
450
  }
366
451
 
367
452
  // src/audits/a11y.ts
368
- import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
453
+ import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
369
454
  import { tmpdir as tmpdir2 } from "os";
370
455
  import { join as join4 } from "path";
371
456
 
@@ -401,48 +486,77 @@ var playwrightA11yConfig = defineConfig({
401
486
  });
402
487
 
403
488
  // src/audits/a11y.ts
489
+ var RESULTS_REL = ".reddoor-a11y/results.json";
404
490
  function siteLabel5(site) {
405
491
  return site.name ?? site.path;
406
492
  }
407
- function isFakeShape2(stdout) {
493
+ async function readJsonMaybe2(path) {
408
494
  try {
409
- const parsed = JSON.parse(stdout);
410
- if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
495
+ const raw = await readFile4(path, "utf-8");
496
+ return JSON.parse(raw);
411
497
  } catch {
412
498
  return null;
413
499
  }
414
- return null;
415
500
  }
416
501
  function buildSpec() {
417
- return `
418
- import { test, expect } from "@playwright/test";
502
+ return `import { test, expect } from "@playwright/test";
419
503
  import AxeBuilder from "@axe-core/playwright";
504
+ import { mkdir, writeFile } from "node:fs/promises";
505
+ import { dirname } from "node:path";
506
+
420
507
  const pages = ${JSON.stringify(a11yRoutes)};
421
- for (const { path, name } of pages) {
422
- test(\`\${name} has no axe violations\`, async ({ page }) => {
508
+ const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
509
+
510
+ test("a11y across configured routes", async ({ page }) => {
511
+ const violations = [];
512
+ for (const { path, name } of pages) {
423
513
  await page.goto(path);
424
514
  const results = await new AxeBuilder({ page })
425
515
  .withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
426
516
  .analyze();
427
- expect(results.violations).toEqual([]);
428
- });
429
- }
517
+ for (const v of results.violations) {
518
+ violations.push({
519
+ id: v.id,
520
+ impact: v.impact ?? "moderate",
521
+ route: name,
522
+ help: v.help,
523
+ helpUrl: v.helpUrl,
524
+ nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),
525
+ });
526
+ }
527
+ }
528
+ const byImpact = {};
529
+ for (const v of violations) {
530
+ byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;
531
+ }
532
+ if (OUTPUT) {
533
+ await mkdir(dirname(OUTPUT), { recursive: true });
534
+ await writeFile(
535
+ OUTPUT,
536
+ JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),
537
+ );
538
+ }
539
+ expect(violations).toEqual([]);
540
+ });
430
541
  `;
431
542
  }
432
543
  async function a11yAudit(ctx) {
433
544
  const spawn2 = ctx.spawn ?? defaultSpawn;
434
545
  const site = ctx.site;
435
546
  const label = siteLabel5(site);
436
- const dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
437
- const specPath = join4(dir, "a11y.spec.ts");
547
+ const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
548
+ const specPath = join4(specDir, "a11y.spec.ts");
438
549
  await writeFile2(specPath, buildSpec(), "utf-8");
550
+ const resultsPath = join4(site.path, RESULTS_REL);
551
+ await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
439
552
  let raw;
440
553
  try {
441
- raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=json", specPath], {
442
- cwd: site.path
554
+ raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
555
+ cwd: site.path,
556
+ env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath }
443
557
  });
444
558
  } catch (err) {
445
- await rm2(dir, { recursive: true, force: true });
559
+ await rm2(specDir, { recursive: true, force: true });
446
560
  const e = err;
447
561
  if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
448
562
  return {
@@ -454,22 +568,26 @@ async function a11yAudit(ctx) {
454
568
  }
455
569
  throw err;
456
570
  }
457
- await rm2(dir, { recursive: true, force: true });
458
- const fake = isFakeShape2(raw.stdout);
459
- const normalized = fake ?? {
460
- totalViolations: raw.code === 0 ? 0 : 1,
461
- byImpact: raw.code === 0 ? {} : { moderate: 1 }
462
- };
463
- const hasSerious = (normalized.byImpact.serious ?? 0) > 0 || (normalized.byImpact.critical ?? 0) > 0;
464
- const hasAny = normalized.totalViolations > 0;
571
+ await rm2(specDir, { recursive: true, force: true });
572
+ const artifact = await readJsonMaybe2(resultsPath);
573
+ if (!artifact) {
574
+ return {
575
+ audit: "a11y",
576
+ site: label,
577
+ status: "fail",
578
+ summary: `a11y: no results written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
579
+ };
580
+ }
581
+ const hasSerious = (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;
582
+ const hasAny = artifact.totalViolations > 0;
465
583
  const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
466
- const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${normalized.totalViolations} violations`;
584
+ const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${artifact.totalViolations} violations`;
467
585
  return {
468
586
  audit: "a11y",
469
587
  site: label,
470
588
  status,
471
589
  summary,
472
- details: normalized
590
+ details: artifact
473
591
  };
474
592
  }
475
593
 
@@ -512,7 +630,7 @@ async function runAuditsAcross(sites, which) {
512
630
  }
513
631
 
514
632
  // src/recipes/sync-configs.ts
515
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
633
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
516
634
  import { join as join5 } from "path";
517
635
 
518
636
  // src/recipes/sync-configs/templates.ts
@@ -597,7 +715,7 @@ function siteLabel6(site) {
597
715
  }
598
716
  async function readMaybe(path) {
599
717
  try {
600
- return await readFile3(path, "utf-8");
718
+ return await readFile5(path, "utf-8");
601
719
  } catch {
602
720
  return null;
603
721
  }
@@ -703,9 +821,9 @@ async function bumpDeps(site, opts = {}) {
703
821
  import { join as join11 } from "path";
704
822
 
705
823
  // src/util/pkg.ts
706
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
824
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
707
825
  async function readPackageJson(path) {
708
- const raw = await readFile4(path, "utf-8");
826
+ const raw = await readFile6(path, "utf-8");
709
827
  return JSON.parse(raw);
710
828
  }
711
829
  async function writePackageJson(path, pkg) {
@@ -762,13 +880,13 @@ async function bumpToSvelte5Versions(cwd) {
762
880
  }
763
881
 
764
882
  // src/recipes/svelte-5/step-svelte-config.ts
765
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
883
+ import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
766
884
  import { join as join7 } from "path";
767
885
  async function migrateSvelteConfig(cwd) {
768
886
  const path = join7(cwd, "svelte.config.js");
769
887
  let src;
770
888
  try {
771
- src = await readFile5(path, "utf-8");
889
+ src = await readFile7(path, "utf-8");
772
890
  } catch {
773
891
  return false;
774
892
  }
@@ -828,7 +946,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
828
946
  }
829
947
 
830
948
  // src/recipes/svelte-5/step-gotchas.ts
831
- import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
949
+ import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
832
950
  import { join as join9 } from "path";
833
951
  import { glob as glob2 } from "tinyglobby";
834
952
 
@@ -903,7 +1021,7 @@ async function applyGotchaCodemods(cwd) {
903
1021
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
904
1022
  for (const rel of relPaths) {
905
1023
  const path = join9(cwd, rel);
906
- const before = await readFile6(path, "utf-8");
1024
+ const before = await readFile8(path, "utf-8");
907
1025
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
908
1026
  if (after !== before) {
909
1027
  await writeFile6(path, after, "utf-8");
@@ -1050,7 +1168,7 @@ function localPath(path, opts = {}) {
1050
1168
  }
1051
1169
 
1052
1170
  // src/inventory/json.ts
1053
- import { readFile as readFile7 } from "fs/promises";
1171
+ import { readFile as readFile9 } from "fs/promises";
1054
1172
  function validate(raw) {
1055
1173
  if (!Array.isArray(raw)) {
1056
1174
  throw new Error("inventory JSON must be an array of sites");
@@ -1074,7 +1192,7 @@ function validate(raw) {
1074
1192
  }
1075
1193
  function fromJsonFile(path) {
1076
1194
  return async () => {
1077
- const raw = JSON.parse(await readFile7(path, "utf-8"));
1195
+ const raw = JSON.parse(await readFile9(path, "utf-8"));
1078
1196
  return validate(raw);
1079
1197
  };
1080
1198
  }