@reddoorla/maintenance 0.1.0 → 0.1.2
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 +344 -112
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +222 -66
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +290 -82
- package/dist/index.js.map +1 -1
- package/dist/util/pkg.d.ts +6 -2
- package/dist/util/pkg.js +3 -1
- package/dist/util/pkg.js.map +1 -1
- package/package.json +14 -16
package/dist/cli/bin.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/bin.ts
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join as join14 } from "path";
|
|
4
|
+
import { dirname } from "path";
|
|
6
5
|
import { fileURLToPath } from "url";
|
|
7
6
|
import { cac } from "cac";
|
|
8
7
|
|
|
@@ -12,15 +11,18 @@ import { resolve as resolve2 } from "path";
|
|
|
12
11
|
// src/audits/util/spawn.ts
|
|
13
12
|
import { spawn } from "child_process";
|
|
14
13
|
var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve6, reject) => {
|
|
14
|
+
const streaming = opts.streaming === true;
|
|
15
15
|
const child = spawn(cmd, [...args], {
|
|
16
16
|
cwd: opts.cwd,
|
|
17
17
|
env: opts.env ?? process.env,
|
|
18
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
18
|
+
stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
|
|
19
19
|
});
|
|
20
20
|
let stdout = "";
|
|
21
21
|
let stderr = "";
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (!streaming) {
|
|
23
|
+
child.stdout?.on("data", (chunk) => stdout += String(chunk));
|
|
24
|
+
child.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
25
|
+
}
|
|
24
26
|
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
25
27
|
child.kill("SIGTERM");
|
|
26
28
|
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
@@ -115,10 +117,10 @@ async function depsAudit(ctx) {
|
|
|
115
117
|
details: { error: String(err) }
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
|
-
const
|
|
120
|
+
const pkg = JSON.parse(pkgRaw);
|
|
119
121
|
const installed = {
|
|
120
|
-
...
|
|
121
|
-
...
|
|
122
|
+
...pkg.dependencies ?? {},
|
|
123
|
+
...pkg.devDependencies ?? {}
|
|
122
124
|
};
|
|
123
125
|
const details = [];
|
|
124
126
|
for (const [name, baseline] of Object.entries(baselineVersions)) {
|
|
@@ -148,7 +150,7 @@ async function depsAudit(ctx) {
|
|
|
148
150
|
// src/audits/lint.ts
|
|
149
151
|
import { existsSync } from "fs";
|
|
150
152
|
import { readFile as readFile2 } from "fs/promises";
|
|
151
|
-
import { join as join2
|
|
153
|
+
import { join as join2 } from "path";
|
|
152
154
|
import { ESLint } from "eslint";
|
|
153
155
|
import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
|
|
154
156
|
import { glob } from "tinyglobby";
|
|
@@ -177,19 +179,19 @@ async function lintAudit(ctx) {
|
|
|
177
179
|
errorOnUnmatchedPattern: false
|
|
178
180
|
});
|
|
179
181
|
const relFiles = await listFiles(site.path);
|
|
180
|
-
const
|
|
181
|
-
const eslintResults = await eslint2.lintFiles(filesToLint);
|
|
182
|
+
const eslintResults = await eslint2.lintFiles(relFiles);
|
|
182
183
|
const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
|
|
183
184
|
const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
|
|
184
185
|
const prettierUnformatted = [];
|
|
185
|
-
for (const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
186
|
+
for (const rel of relFiles) {
|
|
187
|
+
const absForResolve = join2(site.path, rel);
|
|
188
|
+
const source = await readFile2(absForResolve, "utf-8");
|
|
189
|
+
const options = await prettierResolveConfig(absForResolve) ?? {};
|
|
190
|
+
const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
|
|
191
|
+
if (!ok) prettierUnformatted.push(rel);
|
|
190
192
|
}
|
|
191
193
|
const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
|
|
192
|
-
const summary = status === "pass" ? `lint clean across ${
|
|
194
|
+
const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
|
|
193
195
|
return {
|
|
194
196
|
audit: "lint",
|
|
195
197
|
site: siteLabel2(site),
|
|
@@ -199,7 +201,7 @@ async function lintAudit(ctx) {
|
|
|
199
201
|
eslintErrors,
|
|
200
202
|
eslintWarnings,
|
|
201
203
|
prettierUnformatted,
|
|
202
|
-
files:
|
|
204
|
+
files: relFiles.length
|
|
203
205
|
}
|
|
204
206
|
};
|
|
205
207
|
}
|
|
@@ -213,6 +215,61 @@ function classify(v) {
|
|
|
213
215
|
if (v.moderate > 0 || v.low > 0) return "warn";
|
|
214
216
|
return "pass";
|
|
215
217
|
}
|
|
218
|
+
function normalizeSeverity(s) {
|
|
219
|
+
if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
|
|
220
|
+
return "low";
|
|
221
|
+
}
|
|
222
|
+
function extractAdvisoriesFromPnpm(parsed) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const a of Object.values(parsed.advisories ?? {})) {
|
|
225
|
+
if (!a) continue;
|
|
226
|
+
out.push({
|
|
227
|
+
module: a.module_name ?? "unknown",
|
|
228
|
+
severity: normalizeSeverity(a.severity),
|
|
229
|
+
title: a.title ?? "(no title)",
|
|
230
|
+
...a.cves ? { cves: a.cves } : {},
|
|
231
|
+
...a.url ? { url: a.url } : {}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function resolveNpmAdvisoryRoot(startName, vulnerabilities) {
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
let current = startName;
|
|
239
|
+
while (!seen.has(current)) {
|
|
240
|
+
seen.add(current);
|
|
241
|
+
const entry = vulnerabilities[current];
|
|
242
|
+
if (!entry || !Array.isArray(entry.via)) return { rootName: current };
|
|
243
|
+
const detailed = entry.via.find(
|
|
244
|
+
(e) => typeof e === "object" && e !== null
|
|
245
|
+
);
|
|
246
|
+
if (detailed) return { rootName: current, detail: detailed };
|
|
247
|
+
const next = entry.via.find((e) => typeof e === "string");
|
|
248
|
+
if (!next || next === current) return { rootName: current };
|
|
249
|
+
current = next;
|
|
250
|
+
}
|
|
251
|
+
return { rootName: current };
|
|
252
|
+
}
|
|
253
|
+
function extractAdvisoriesFromNpm(parsed) {
|
|
254
|
+
const vulnerabilities = parsed.vulnerabilities ?? {};
|
|
255
|
+
const roots = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const [name, v] of Object.entries(vulnerabilities)) {
|
|
257
|
+
if (!v) continue;
|
|
258
|
+
const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);
|
|
259
|
+
if (roots.has(rootName)) continue;
|
|
260
|
+
const rootEntry = vulnerabilities[rootName];
|
|
261
|
+
const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);
|
|
262
|
+
const title = detail?.title ?? rootName;
|
|
263
|
+
const url = detail?.url;
|
|
264
|
+
roots.set(rootName, {
|
|
265
|
+
module: rootEntry?.name ?? rootName,
|
|
266
|
+
severity,
|
|
267
|
+
title,
|
|
268
|
+
...url ? { url } : {}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return [...roots.values()];
|
|
272
|
+
}
|
|
216
273
|
async function tryRun(spawn2, cmd, args, cwd) {
|
|
217
274
|
try {
|
|
218
275
|
return await spawn2(cmd, args, { cwd });
|
|
@@ -266,26 +323,27 @@ async function securityAudit(ctx) {
|
|
|
266
323
|
details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
|
|
267
324
|
};
|
|
268
325
|
}
|
|
269
|
-
const
|
|
326
|
+
const counts = {
|
|
270
327
|
low: parsed.metadata?.vulnerabilities?.low ?? 0,
|
|
271
328
|
moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
|
|
272
329
|
high: parsed.metadata?.vulnerabilities?.high ?? 0,
|
|
273
330
|
critical: parsed.metadata?.vulnerabilities?.critical ?? 0
|
|
274
331
|
};
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const
|
|
332
|
+
const advisories = used === "pnpm audit" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);
|
|
333
|
+
const status = classify(counts);
|
|
334
|
+
const total = counts.low + counts.moderate + counts.high + counts.critical;
|
|
335
|
+
const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;
|
|
278
336
|
return {
|
|
279
337
|
audit: "security",
|
|
280
338
|
site: label,
|
|
281
339
|
status,
|
|
282
340
|
summary,
|
|
283
|
-
details:
|
|
341
|
+
details: { counts, advisories }
|
|
284
342
|
};
|
|
285
343
|
}
|
|
286
344
|
|
|
287
345
|
// src/audits/lighthouse.ts
|
|
288
|
-
import { writeFile, mkdtemp, rm } from "fs/promises";
|
|
346
|
+
import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
|
|
289
347
|
import { tmpdir } from "os";
|
|
290
348
|
import { join as join3 } from "path";
|
|
291
349
|
|
|
@@ -321,29 +379,56 @@ var lighthouseConfig = {
|
|
|
321
379
|
function siteLabel4(site) {
|
|
322
380
|
return site.name ?? site.path;
|
|
323
381
|
}
|
|
324
|
-
function
|
|
382
|
+
async function readJsonMaybe(path) {
|
|
325
383
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
384
|
+
const raw = await readFile3(path, "utf-8");
|
|
385
|
+
return JSON.parse(raw);
|
|
328
386
|
} catch {
|
|
329
387
|
return null;
|
|
330
388
|
}
|
|
331
|
-
|
|
389
|
+
}
|
|
390
|
+
function averageSummaries(entries) {
|
|
391
|
+
if (entries.length === 0) return {};
|
|
392
|
+
const sums = {};
|
|
393
|
+
const counts = {};
|
|
394
|
+
for (const e of entries) {
|
|
395
|
+
for (const [k, v] of Object.entries(e.summary ?? {})) {
|
|
396
|
+
if (typeof v !== "number") continue;
|
|
397
|
+
sums[k] = (sums[k] ?? 0) + v;
|
|
398
|
+
counts[k] = (counts[k] ?? 0) + 1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const out = {};
|
|
402
|
+
for (const k of Object.keys(sums)) {
|
|
403
|
+
const total = sums[k] ?? 0;
|
|
404
|
+
const count = counts[k] ?? 1;
|
|
405
|
+
out[k] = total / count;
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
function categoryFromAssertion(a) {
|
|
410
|
+
const colonIdx = a.name.indexOf(":");
|
|
411
|
+
return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
|
|
412
|
+
}
|
|
413
|
+
function messageForAssertion(a) {
|
|
414
|
+
return `${a.name} ${a.operator} ${a.expected} (actual: ${a.actual.toFixed(2)})`;
|
|
332
415
|
}
|
|
333
416
|
async function lighthouseAudit(ctx) {
|
|
334
417
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
335
418
|
const site = ctx.site;
|
|
336
419
|
const label = siteLabel4(site);
|
|
337
|
-
const
|
|
338
|
-
const configPath = join3(
|
|
420
|
+
const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
|
|
421
|
+
const configPath = join3(configDir, "lighthouserc.json");
|
|
339
422
|
await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
|
|
423
|
+
const resultsDir = join3(site.path, ".lighthouseci");
|
|
424
|
+
await rm(resultsDir, { recursive: true, force: true });
|
|
340
425
|
let raw;
|
|
341
426
|
try {
|
|
342
427
|
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
343
428
|
cwd: site.path
|
|
344
429
|
});
|
|
345
430
|
} catch (err) {
|
|
346
|
-
await rm(
|
|
431
|
+
await rm(configDir, { recursive: true, force: true });
|
|
347
432
|
const e = err;
|
|
348
433
|
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
349
434
|
return {
|
|
@@ -355,17 +440,32 @@ async function lighthouseAudit(ctx) {
|
|
|
355
440
|
}
|
|
356
441
|
throw err;
|
|
357
442
|
}
|
|
358
|
-
await rm(
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
443
|
+
await rm(configDir, { recursive: true, force: true });
|
|
444
|
+
const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
|
|
445
|
+
if (!manifest || manifest.length === 0) {
|
|
446
|
+
return {
|
|
447
|
+
audit: "lighthouse",
|
|
448
|
+
site: label,
|
|
449
|
+
status: "fail",
|
|
450
|
+
summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
|
|
454
|
+
const failed = assertionResults.filter((a) => !a.passed);
|
|
455
|
+
const assertions = failed.map((a) => ({
|
|
456
|
+
category: categoryFromAssertion(a),
|
|
457
|
+
level: a.level,
|
|
458
|
+
message: messageForAssertion(a)
|
|
459
|
+
}));
|
|
460
|
+
const anyError = assertions.some((a) => a.level === "error");
|
|
461
|
+
const anyWarn = assertions.some((a) => a.level === "warn");
|
|
367
462
|
const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
|
|
368
|
-
const
|
|
463
|
+
const normalized = {
|
|
464
|
+
summary: averageSummaries(manifest),
|
|
465
|
+
assertionsFailed: failed.length,
|
|
466
|
+
assertions
|
|
467
|
+
};
|
|
468
|
+
const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
|
|
369
469
|
return {
|
|
370
470
|
audit: "lighthouse",
|
|
371
471
|
site: label,
|
|
@@ -376,7 +476,7 @@ async function lighthouseAudit(ctx) {
|
|
|
376
476
|
}
|
|
377
477
|
|
|
378
478
|
// src/audits/a11y.ts
|
|
379
|
-
import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
479
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
380
480
|
import { tmpdir as tmpdir2 } from "os";
|
|
381
481
|
import { join as join4 } from "path";
|
|
382
482
|
|
|
@@ -412,48 +512,81 @@ var playwrightA11yConfig = defineConfig({
|
|
|
412
512
|
});
|
|
413
513
|
|
|
414
514
|
// src/audits/a11y.ts
|
|
515
|
+
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
415
516
|
function siteLabel5(site) {
|
|
416
517
|
return site.name ?? site.path;
|
|
417
518
|
}
|
|
418
|
-
function
|
|
519
|
+
async function readJsonMaybe2(path) {
|
|
419
520
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
521
|
+
const raw = await readFile4(path, "utf-8");
|
|
522
|
+
return JSON.parse(raw);
|
|
422
523
|
} catch {
|
|
423
524
|
return null;
|
|
424
525
|
}
|
|
425
|
-
return null;
|
|
426
526
|
}
|
|
427
527
|
function buildSpec() {
|
|
428
|
-
return `
|
|
429
|
-
import { test, expect } from "@playwright/test";
|
|
528
|
+
return `import { test, expect } from "@playwright/test";
|
|
430
529
|
import AxeBuilder from "@axe-core/playwright";
|
|
530
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
531
|
+
import { dirname } from "node:path";
|
|
532
|
+
|
|
431
533
|
const pages = ${JSON.stringify(a11yRoutes)};
|
|
432
|
-
|
|
433
|
-
|
|
534
|
+
const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
|
|
535
|
+
|
|
536
|
+
// Playwright's default per-test timeout is 30s. We loop through every
|
|
537
|
+
// configured route in a single test, so the budget needs to scale.
|
|
538
|
+
test.setTimeout(5 * 60_000);
|
|
539
|
+
|
|
540
|
+
test("a11y across configured routes", async ({ page }) => {
|
|
541
|
+
const violations = [];
|
|
542
|
+
for (const { path, name } of pages) {
|
|
434
543
|
await page.goto(path);
|
|
435
544
|
const results = await new AxeBuilder({ page })
|
|
436
545
|
.withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
|
|
437
546
|
.analyze();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
547
|
+
for (const v of results.violations) {
|
|
548
|
+
violations.push({
|
|
549
|
+
id: v.id,
|
|
550
|
+
impact: v.impact ?? "moderate",
|
|
551
|
+
route: name,
|
|
552
|
+
help: v.help,
|
|
553
|
+
helpUrl: v.helpUrl,
|
|
554
|
+
nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const byImpact = {};
|
|
559
|
+
for (const v of violations) {
|
|
560
|
+
byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;
|
|
561
|
+
}
|
|
562
|
+
if (OUTPUT) {
|
|
563
|
+
await mkdir(dirname(OUTPUT), { recursive: true });
|
|
564
|
+
await writeFile(
|
|
565
|
+
OUTPUT,
|
|
566
|
+
JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
expect(violations).toEqual([]);
|
|
570
|
+
});
|
|
441
571
|
`;
|
|
442
572
|
}
|
|
443
573
|
async function a11yAudit(ctx) {
|
|
444
574
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
445
575
|
const site = ctx.site;
|
|
446
576
|
const label = siteLabel5(site);
|
|
447
|
-
const
|
|
448
|
-
const specPath = join4(
|
|
577
|
+
const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
|
|
578
|
+
const specPath = join4(specDir, "a11y.spec.ts");
|
|
449
579
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
580
|
+
const resultsPath = join4(site.path, RESULTS_REL);
|
|
581
|
+
await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
450
582
|
let raw;
|
|
451
583
|
try {
|
|
452
|
-
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=
|
|
453
|
-
cwd: site.path
|
|
584
|
+
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
|
|
585
|
+
cwd: site.path,
|
|
586
|
+
env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath }
|
|
454
587
|
});
|
|
455
588
|
} catch (err) {
|
|
456
|
-
await rm2(
|
|
589
|
+
await rm2(specDir, { recursive: true, force: true });
|
|
457
590
|
const e = err;
|
|
458
591
|
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
459
592
|
return {
|
|
@@ -465,22 +598,26 @@ async function a11yAudit(ctx) {
|
|
|
465
598
|
}
|
|
466
599
|
throw err;
|
|
467
600
|
}
|
|
468
|
-
await rm2(
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
601
|
+
await rm2(specDir, { recursive: true, force: true });
|
|
602
|
+
const artifact = await readJsonMaybe2(resultsPath);
|
|
603
|
+
if (!artifact) {
|
|
604
|
+
return {
|
|
605
|
+
audit: "a11y",
|
|
606
|
+
site: label,
|
|
607
|
+
status: "fail",
|
|
608
|
+
summary: `a11y: no results written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const hasSerious = (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;
|
|
612
|
+
const hasAny = artifact.totalViolations > 0;
|
|
476
613
|
const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
|
|
477
|
-
const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${
|
|
614
|
+
const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${artifact.totalViolations} violations`;
|
|
478
615
|
return {
|
|
479
616
|
audit: "a11y",
|
|
480
617
|
site: label,
|
|
481
618
|
status,
|
|
482
619
|
summary,
|
|
483
|
-
details:
|
|
620
|
+
details: artifact
|
|
484
621
|
};
|
|
485
622
|
}
|
|
486
623
|
|
|
@@ -530,7 +667,8 @@ function localPath(path, opts = {}) {
|
|
|
530
667
|
}
|
|
531
668
|
|
|
532
669
|
// src/inventory/json.ts
|
|
533
|
-
import { readFile as
|
|
670
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
671
|
+
import { isAbsolute } from "path";
|
|
534
672
|
function validate(raw) {
|
|
535
673
|
if (!Array.isArray(raw)) {
|
|
536
674
|
throw new Error("inventory JSON must be an array of sites");
|
|
@@ -543,6 +681,11 @@ function validate(raw) {
|
|
|
543
681
|
if (typeof e.path !== "string" || e.path.length === 0) {
|
|
544
682
|
throw new Error(`inventory entry ${i} is missing required field: path`);
|
|
545
683
|
}
|
|
684
|
+
if (!isAbsolute(e.path)) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`inventory entry ${i}: path must be absolute (got "${e.path}"). Relative paths are rejected so cwd at invocation can't change which site is targeted.`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
546
689
|
const site = { path: e.path };
|
|
547
690
|
if (typeof e.name === "string") site.name = e.name;
|
|
548
691
|
if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
|
|
@@ -554,7 +697,7 @@ function validate(raw) {
|
|
|
554
697
|
}
|
|
555
698
|
function fromJsonFile(path) {
|
|
556
699
|
return async () => {
|
|
557
|
-
const raw = JSON.parse(await
|
|
700
|
+
const raw = JSON.parse(await readFile5(path, "utf-8"));
|
|
558
701
|
return validate(raw);
|
|
559
702
|
};
|
|
560
703
|
}
|
|
@@ -593,11 +736,22 @@ async function resolveSites(input) {
|
|
|
593
736
|
|
|
594
737
|
// src/cli/fleet/clone-if-needed.ts
|
|
595
738
|
import { stat, readdir, mkdir } from "fs/promises";
|
|
596
|
-
import { join as join5 } from "path";
|
|
739
|
+
import { isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
597
740
|
function deriveNameFromRepoUrl(repoUrl) {
|
|
598
741
|
const slash = repoUrl.split("/").pop() ?? repoUrl;
|
|
599
742
|
return slash.replace(/\.git$/, "");
|
|
600
743
|
}
|
|
744
|
+
function assertSafeName(name) {
|
|
745
|
+
if (isAbsolute2(name)) {
|
|
746
|
+
throw new Error(`unsafe site name (absolute path not allowed): ${name}`);
|
|
747
|
+
}
|
|
748
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
749
|
+
throw new Error(`unsafe site name (path separator not allowed): ${name}`);
|
|
750
|
+
}
|
|
751
|
+
if (name.split(/[\\/]/).some((seg) => seg === "..")) {
|
|
752
|
+
throw new Error(`unsafe site name (traversal segment not allowed): ${name}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
601
755
|
async function isNonEmptyDir(path) {
|
|
602
756
|
try {
|
|
603
757
|
const s = await stat(path);
|
|
@@ -614,6 +768,7 @@ async function cloneIfNeeded(site, opts) {
|
|
|
614
768
|
throw new Error(`site path does not exist (${site.path}) and no repoUrl is set \u2014 cannot clone`);
|
|
615
769
|
}
|
|
616
770
|
const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
|
|
771
|
+
assertSafeName(name);
|
|
617
772
|
const target = join5(opts.workdir, name);
|
|
618
773
|
await mkdir(opts.workdir, { recursive: true });
|
|
619
774
|
if (await isNonEmptyDir(target)) {
|
|
@@ -670,11 +825,11 @@ async function runAuditCommand(site, opts) {
|
|
|
670
825
|
}
|
|
671
826
|
|
|
672
827
|
// src/cli/commands/sync-configs.ts
|
|
673
|
-
import { readFile as
|
|
828
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
674
829
|
import { join as join7, resolve as resolve3 } from "path";
|
|
675
830
|
|
|
676
831
|
// src/recipes/sync-configs.ts
|
|
677
|
-
import { readFile as
|
|
832
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
678
833
|
import { join as join6 } from "path";
|
|
679
834
|
|
|
680
835
|
// src/recipes/sync-configs/templates.ts
|
|
@@ -759,7 +914,7 @@ function siteLabel6(site) {
|
|
|
759
914
|
}
|
|
760
915
|
async function readMaybe(path) {
|
|
761
916
|
try {
|
|
762
|
-
return await
|
|
917
|
+
return await readFile6(path, "utf-8");
|
|
763
918
|
} catch {
|
|
764
919
|
return null;
|
|
765
920
|
}
|
|
@@ -818,7 +973,7 @@ async function dryPlan(cwd, which) {
|
|
|
818
973
|
for (const t of targets) {
|
|
819
974
|
let existing = "";
|
|
820
975
|
try {
|
|
821
|
-
existing = await
|
|
976
|
+
existing = await readFile7(join7(cwd, t.path), "utf-8");
|
|
822
977
|
} catch {
|
|
823
978
|
}
|
|
824
979
|
if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
|
|
@@ -877,6 +1032,7 @@ async function bumpDeps(site, opts = {}) {
|
|
|
877
1032
|
const label = siteLabel7(site);
|
|
878
1033
|
const group = opts.group ?? "minor";
|
|
879
1034
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
1035
|
+
await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
|
|
880
1036
|
const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
|
|
881
1037
|
cwd: site.path
|
|
882
1038
|
});
|
|
@@ -901,7 +1057,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
901
1057
|
}
|
|
902
1058
|
const branch = branchName("bump-deps");
|
|
903
1059
|
await createBranch(site.path, branch);
|
|
904
|
-
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
|
|
1060
|
+
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
|
|
1061
|
+
cwd: site.path,
|
|
1062
|
+
streaming: true
|
|
1063
|
+
});
|
|
905
1064
|
const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
|
|
906
1065
|
const shas = sha ? [sha] : [];
|
|
907
1066
|
return {
|
|
@@ -952,36 +1111,38 @@ import { resolve as resolve5 } from "path";
|
|
|
952
1111
|
import { join as join13 } from "path";
|
|
953
1112
|
|
|
954
1113
|
// src/util/pkg.ts
|
|
955
|
-
import { readFile as
|
|
1114
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
956
1115
|
async function readPackageJson(path) {
|
|
957
|
-
const raw = await
|
|
1116
|
+
const raw = await readFile8(path, "utf-8");
|
|
958
1117
|
return JSON.parse(raw);
|
|
959
1118
|
}
|
|
960
|
-
async function writePackageJson(path,
|
|
961
|
-
const content = JSON.stringify(
|
|
1119
|
+
async function writePackageJson(path, pkg) {
|
|
1120
|
+
const content = JSON.stringify(pkg, null, 2) + "\n";
|
|
962
1121
|
await writeFile4(path, content, "utf-8");
|
|
963
1122
|
}
|
|
964
|
-
function bumpDep(
|
|
1123
|
+
function bumpDep(pkg, name, version2, opts = {}) {
|
|
1124
|
+
const mode = opts.mode ?? "ensure";
|
|
965
1125
|
const next = {
|
|
966
|
-
...
|
|
1126
|
+
...pkg
|
|
967
1127
|
};
|
|
968
|
-
if (
|
|
969
|
-
next.dependencies = { ...
|
|
1128
|
+
if (pkg.dependencies) {
|
|
1129
|
+
next.dependencies = { ...pkg.dependencies };
|
|
970
1130
|
}
|
|
971
|
-
if (
|
|
972
|
-
next.devDependencies = { ...
|
|
1131
|
+
if (pkg.devDependencies) {
|
|
1132
|
+
next.devDependencies = { ...pkg.devDependencies };
|
|
973
1133
|
}
|
|
974
1134
|
if (next.dependencies && name in next.dependencies) {
|
|
975
|
-
if (next.dependencies[name] ===
|
|
976
|
-
next.dependencies[name] =
|
|
1135
|
+
if (next.dependencies[name] === version2) return pkg;
|
|
1136
|
+
next.dependencies[name] = version2;
|
|
977
1137
|
return next;
|
|
978
1138
|
}
|
|
979
1139
|
if (next.devDependencies && name in next.devDependencies) {
|
|
980
|
-
if (next.devDependencies[name] ===
|
|
981
|
-
next.devDependencies[name] =
|
|
1140
|
+
if (next.devDependencies[name] === version2) return pkg;
|
|
1141
|
+
next.devDependencies[name] = version2;
|
|
982
1142
|
return next;
|
|
983
1143
|
}
|
|
984
|
-
|
|
1144
|
+
if (mode === "bump-only") return pkg;
|
|
1145
|
+
next.devDependencies = { ...next.devDependencies ?? {}, [name]: version2 };
|
|
985
1146
|
return next;
|
|
986
1147
|
}
|
|
987
1148
|
|
|
@@ -1000,33 +1161,69 @@ var SVELTE_5_VERSIONS = {
|
|
|
1000
1161
|
};
|
|
1001
1162
|
async function bumpToSvelte5Versions(cwd) {
|
|
1002
1163
|
const pkgPath = join8(cwd, "package.json");
|
|
1003
|
-
const
|
|
1004
|
-
let next =
|
|
1005
|
-
for (const [name,
|
|
1006
|
-
next = bumpDep(next, name,
|
|
1164
|
+
const pkg = await readPackageJson(pkgPath);
|
|
1165
|
+
let next = pkg;
|
|
1166
|
+
for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
1167
|
+
next = bumpDep(next, name, version2, { mode: "bump-only" });
|
|
1007
1168
|
}
|
|
1008
|
-
if (next ===
|
|
1169
|
+
if (next === pkg) return false;
|
|
1009
1170
|
await writePackageJson(pkgPath, next);
|
|
1010
1171
|
return true;
|
|
1011
1172
|
}
|
|
1012
1173
|
|
|
1013
1174
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
1014
|
-
import { readFile as
|
|
1175
|
+
import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
1015
1176
|
import { join as join9 } from "path";
|
|
1177
|
+
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
1178
|
+
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
1179
|
+
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
1180
|
+
"m"
|
|
1181
|
+
);
|
|
1182
|
+
function dropVitePreprocessImport(source) {
|
|
1183
|
+
return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names) => {
|
|
1184
|
+
const remaining = names.split(",").map((n) => n.trim()).filter((n) => n.length > 0 && n !== "vitePreprocess");
|
|
1185
|
+
if (remaining.length === 0) return "";
|
|
1186
|
+
return `import { ${remaining.join(", ")} } from "${VITE_PLUGIN_PKG}";
|
|
1187
|
+
`;
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function findMatchingParen(source, openIdx) {
|
|
1191
|
+
if (source[openIdx] !== "(") return -1;
|
|
1192
|
+
let depth = 0;
|
|
1193
|
+
for (let i = openIdx; i < source.length; i++) {
|
|
1194
|
+
const ch = source[i];
|
|
1195
|
+
if (ch === "(") depth++;
|
|
1196
|
+
else if (ch === ")") {
|
|
1197
|
+
depth--;
|
|
1198
|
+
if (depth === 0) return i;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return -1;
|
|
1202
|
+
}
|
|
1203
|
+
function dropPreprocessKey(source) {
|
|
1204
|
+
const startRe = /^(\s*)preprocess:\s*vitePreprocess\(/m;
|
|
1205
|
+
const m = startRe.exec(source);
|
|
1206
|
+
if (!m) return source;
|
|
1207
|
+
const indent = m[1] ?? "";
|
|
1208
|
+
const parenOpenAbs = m.index + m[0].length - 1;
|
|
1209
|
+
const parenCloseAbs = findMatchingParen(source, parenOpenAbs);
|
|
1210
|
+
if (parenCloseAbs < 0) return source;
|
|
1211
|
+
let tailIdx = parenCloseAbs + 1;
|
|
1212
|
+
while (tailIdx < source.length && /[ \t,]/.test(source[tailIdx] ?? "")) tailIdx++;
|
|
1213
|
+
if (source[tailIdx] === "\n") tailIdx++;
|
|
1214
|
+
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
1215
|
+
}
|
|
1016
1216
|
async function migrateSvelteConfig(cwd) {
|
|
1017
1217
|
const path = join9(cwd, "svelte.config.js");
|
|
1018
1218
|
let src;
|
|
1019
1219
|
try {
|
|
1020
|
-
src = await
|
|
1220
|
+
src = await readFile9(path, "utf-8");
|
|
1021
1221
|
} catch {
|
|
1022
1222
|
return false;
|
|
1023
1223
|
}
|
|
1024
1224
|
let next = src;
|
|
1025
|
-
next = next
|
|
1026
|
-
|
|
1027
|
-
""
|
|
1028
|
-
);
|
|
1029
|
-
next = next.replace(/^\s*preprocess:\s*vitePreprocess\(\)\s*,?\s*\n/m, "");
|
|
1225
|
+
next = dropPreprocessKey(next);
|
|
1226
|
+
next = dropVitePreprocessImport(next);
|
|
1030
1227
|
if (next === src) return false;
|
|
1031
1228
|
await writeFile5(path, next, "utf-8");
|
|
1032
1229
|
return true;
|
|
@@ -1056,8 +1253,8 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
1056
1253
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
1057
1254
|
import { join as join10 } from "path";
|
|
1058
1255
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
1059
|
-
const
|
|
1060
|
-
const tailwindVersion =
|
|
1256
|
+
const pkg = await readPackageJson(join10(cwd, "package.json"));
|
|
1257
|
+
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
1061
1258
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
1062
1259
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
1063
1260
|
try {
|
|
@@ -1077,7 +1274,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
1077
1274
|
}
|
|
1078
1275
|
|
|
1079
1276
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
1080
|
-
import { readFile as
|
|
1277
|
+
import { readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
1081
1278
|
import { join as join11 } from "path";
|
|
1082
1279
|
import { glob as glob2 } from "tinyglobby";
|
|
1083
1280
|
|
|
@@ -1136,10 +1333,32 @@ function exportLetToProps(source) {
|
|
|
1136
1333
|
}
|
|
1137
1334
|
|
|
1138
1335
|
// src/recipes/svelte-5/codemods/dollar-restprops.ts
|
|
1336
|
+
function removeInterfaceBlock(source) {
|
|
1337
|
+
const re = /^\s*interface\s+\$\$Props\s*\{/m;
|
|
1338
|
+
let out = source;
|
|
1339
|
+
while (true) {
|
|
1340
|
+
const match = re.exec(out);
|
|
1341
|
+
if (!match) return out;
|
|
1342
|
+
const openBraceIdx = match.index + match[0].length - 1;
|
|
1343
|
+
let depth = 1;
|
|
1344
|
+
let i = openBraceIdx + 1;
|
|
1345
|
+
while (i < out.length && depth > 0) {
|
|
1346
|
+
const ch = out[i];
|
|
1347
|
+
if (ch === "{") depth++;
|
|
1348
|
+
else if (ch === "}") depth--;
|
|
1349
|
+
i++;
|
|
1350
|
+
}
|
|
1351
|
+
if (depth !== 0) return out;
|
|
1352
|
+
let endIdx = i;
|
|
1353
|
+
while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
|
|
1354
|
+
if (out[endIdx] === "\n") endIdx++;
|
|
1355
|
+
out = out.slice(0, match.index) + out.slice(endIdx);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1139
1358
|
function removeDollarRestProps(source) {
|
|
1140
1359
|
let next = source;
|
|
1141
1360
|
next = next.replace(/\$\$restProps/g, "rest");
|
|
1142
|
-
next = next
|
|
1361
|
+
next = removeInterfaceBlock(next);
|
|
1143
1362
|
return next;
|
|
1144
1363
|
}
|
|
1145
1364
|
|
|
@@ -1152,7 +1371,7 @@ async function applyGotchaCodemods(cwd) {
|
|
|
1152
1371
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
1153
1372
|
for (const rel of relPaths) {
|
|
1154
1373
|
const path = join11(cwd, rel);
|
|
1155
|
-
const before = await
|
|
1374
|
+
const before = await readFile10(path, "utf-8");
|
|
1156
1375
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
1157
1376
|
if (after !== before) {
|
|
1158
1377
|
await writeFile6(path, after, "utf-8");
|
|
@@ -1209,8 +1428,8 @@ function siteLabel8(site) {
|
|
|
1209
1428
|
}
|
|
1210
1429
|
async function alreadyOnSvelte5(cwd) {
|
|
1211
1430
|
try {
|
|
1212
|
-
const
|
|
1213
|
-
const v =
|
|
1431
|
+
const pkg = await readPackageJson(join13(cwd, "package.json"));
|
|
1432
|
+
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
1214
1433
|
return !!v && /^\^?5\./.test(v);
|
|
1215
1434
|
} catch {
|
|
1216
1435
|
return false;
|
|
@@ -1322,9 +1541,22 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
|
|
|
1322
1541
|
return { output, code };
|
|
1323
1542
|
}
|
|
1324
1543
|
|
|
1544
|
+
// src/cli/version.ts
|
|
1545
|
+
import { readFileSync } from "fs";
|
|
1546
|
+
import { join as join14 } from "path";
|
|
1547
|
+
function resolvePackageVersion(fromDir) {
|
|
1548
|
+
try {
|
|
1549
|
+
const raw = readFileSync(join14(fromDir, "..", "..", "package.json"), "utf-8");
|
|
1550
|
+
const pkg = JSON.parse(raw);
|
|
1551
|
+
return pkg.version ?? "unknown";
|
|
1552
|
+
} catch {
|
|
1553
|
+
return "unknown";
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1325
1557
|
// src/cli/bin.ts
|
|
1326
1558
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
1327
|
-
var
|
|
1559
|
+
var version = resolvePackageVersion(here);
|
|
1328
1560
|
var AUDIT_DESCRIPTIONS = {
|
|
1329
1561
|
deps: "Diff site package.json against the bundled baseline version map.",
|
|
1330
1562
|
lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
|
|
@@ -1403,6 +1635,6 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
|
|
|
1403
1635
|
}
|
|
1404
1636
|
);
|
|
1405
1637
|
cli.help();
|
|
1406
|
-
cli.version(
|
|
1638
|
+
cli.version(version);
|
|
1407
1639
|
cli.parse();
|
|
1408
1640
|
//# sourceMappingURL=bin.js.map
|