@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 +180 -62
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +170 -52
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.js +178 -60
- package/dist/index.js.map +1 -1
- package/package.json +14 -16
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
|
|
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
|
|
265
|
-
const
|
|
266
|
-
const
|
|
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:
|
|
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
|
|
356
|
+
async function readJsonMaybe(path) {
|
|
314
357
|
try {
|
|
315
|
-
const
|
|
316
|
-
|
|
358
|
+
const raw = await readFile3(path, "utf-8");
|
|
359
|
+
return JSON.parse(raw);
|
|
317
360
|
} catch {
|
|
318
361
|
return null;
|
|
319
362
|
}
|
|
320
|
-
|
|
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
|
|
327
|
-
const configPath = join3(
|
|
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(
|
|
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(
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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
|
|
493
|
+
async function readJsonMaybe2(path) {
|
|
408
494
|
try {
|
|
409
|
-
const
|
|
410
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
|
437
|
-
const specPath = join4(
|
|
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=
|
|
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(
|
|
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(
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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: ${
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
824
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
707
825
|
async function readPackageJson(path) {
|
|
708
|
-
const raw = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1195
|
+
const raw = JSON.parse(await readFile9(path, "utf-8"));
|
|
1078
1196
|
return validate(raw);
|
|
1079
1197
|
};
|
|
1080
1198
|
}
|