@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/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
|
|
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
|
|
276
|
-
const
|
|
277
|
-
const
|
|
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:
|
|
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
|
|
367
|
+
async function readJsonMaybe(path) {
|
|
325
368
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
369
|
+
const raw = await readFile3(path, "utf-8");
|
|
370
|
+
return JSON.parse(raw);
|
|
328
371
|
} catch {
|
|
329
372
|
return null;
|
|
330
373
|
}
|
|
331
|
-
|
|
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
|
|
338
|
-
const configPath = join3(
|
|
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(
|
|
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(
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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
|
|
504
|
+
async function readJsonMaybe2(path) {
|
|
419
505
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
|
448
|
-
const specPath = join4(
|
|
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=
|
|
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(
|
|
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(
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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: ${
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1073
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
956
1074
|
async function readPackageJson(path) {
|
|
957
|
-
const raw = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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");
|