@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/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// src/audits/util/spawn.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve, reject) => {
|
|
4
|
+
const streaming = opts.streaming === true;
|
|
4
5
|
const child = spawn(cmd, [...args], {
|
|
5
6
|
cwd: opts.cwd,
|
|
6
7
|
env: opts.env ?? process.env,
|
|
7
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
8
|
+
stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
|
|
8
9
|
});
|
|
9
10
|
let stdout = "";
|
|
10
11
|
let stderr = "";
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
if (!streaming) {
|
|
13
|
+
child.stdout?.on("data", (chunk) => stdout += String(chunk));
|
|
14
|
+
child.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
15
|
+
}
|
|
13
16
|
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
14
17
|
child.kill("SIGTERM");
|
|
15
18
|
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
@@ -137,7 +140,7 @@ async function depsAudit(ctx) {
|
|
|
137
140
|
// src/audits/lint.ts
|
|
138
141
|
import { existsSync } from "fs";
|
|
139
142
|
import { readFile as readFile2 } from "fs/promises";
|
|
140
|
-
import { join as join2
|
|
143
|
+
import { join as join2 } from "path";
|
|
141
144
|
import { ESLint } from "eslint";
|
|
142
145
|
import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
|
|
143
146
|
import { glob } from "tinyglobby";
|
|
@@ -166,19 +169,19 @@ async function lintAudit(ctx) {
|
|
|
166
169
|
errorOnUnmatchedPattern: false
|
|
167
170
|
});
|
|
168
171
|
const relFiles = await listFiles(site.path);
|
|
169
|
-
const
|
|
170
|
-
const eslintResults = await eslint2.lintFiles(filesToLint);
|
|
172
|
+
const eslintResults = await eslint2.lintFiles(relFiles);
|
|
171
173
|
const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
|
|
172
174
|
const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
|
|
173
175
|
const prettierUnformatted = [];
|
|
174
|
-
for (const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
176
|
+
for (const rel of relFiles) {
|
|
177
|
+
const absForResolve = join2(site.path, rel);
|
|
178
|
+
const source = await readFile2(absForResolve, "utf-8");
|
|
179
|
+
const options = await prettierResolveConfig(absForResolve) ?? {};
|
|
180
|
+
const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
|
|
181
|
+
if (!ok) prettierUnformatted.push(rel);
|
|
179
182
|
}
|
|
180
183
|
const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
|
|
181
|
-
const summary = status === "pass" ? `lint clean across ${
|
|
184
|
+
const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
|
|
182
185
|
return {
|
|
183
186
|
audit: "lint",
|
|
184
187
|
site: siteLabel2(site),
|
|
@@ -188,7 +191,7 @@ async function lintAudit(ctx) {
|
|
|
188
191
|
eslintErrors,
|
|
189
192
|
eslintWarnings,
|
|
190
193
|
prettierUnformatted,
|
|
191
|
-
files:
|
|
194
|
+
files: relFiles.length
|
|
192
195
|
}
|
|
193
196
|
};
|
|
194
197
|
}
|
|
@@ -202,6 +205,61 @@ function classify(v) {
|
|
|
202
205
|
if (v.moderate > 0 || v.low > 0) return "warn";
|
|
203
206
|
return "pass";
|
|
204
207
|
}
|
|
208
|
+
function normalizeSeverity(s) {
|
|
209
|
+
if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
|
|
210
|
+
return "low";
|
|
211
|
+
}
|
|
212
|
+
function extractAdvisoriesFromPnpm(parsed) {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const a of Object.values(parsed.advisories ?? {})) {
|
|
215
|
+
if (!a) continue;
|
|
216
|
+
out.push({
|
|
217
|
+
module: a.module_name ?? "unknown",
|
|
218
|
+
severity: normalizeSeverity(a.severity),
|
|
219
|
+
title: a.title ?? "(no title)",
|
|
220
|
+
...a.cves ? { cves: a.cves } : {},
|
|
221
|
+
...a.url ? { url: a.url } : {}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
function resolveNpmAdvisoryRoot(startName, vulnerabilities) {
|
|
227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
228
|
+
let current = startName;
|
|
229
|
+
while (!seen.has(current)) {
|
|
230
|
+
seen.add(current);
|
|
231
|
+
const entry = vulnerabilities[current];
|
|
232
|
+
if (!entry || !Array.isArray(entry.via)) return { rootName: current };
|
|
233
|
+
const detailed = entry.via.find(
|
|
234
|
+
(e) => typeof e === "object" && e !== null
|
|
235
|
+
);
|
|
236
|
+
if (detailed) return { rootName: current, detail: detailed };
|
|
237
|
+
const next = entry.via.find((e) => typeof e === "string");
|
|
238
|
+
if (!next || next === current) return { rootName: current };
|
|
239
|
+
current = next;
|
|
240
|
+
}
|
|
241
|
+
return { rootName: current };
|
|
242
|
+
}
|
|
243
|
+
function extractAdvisoriesFromNpm(parsed) {
|
|
244
|
+
const vulnerabilities = parsed.vulnerabilities ?? {};
|
|
245
|
+
const roots = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const [name, v] of Object.entries(vulnerabilities)) {
|
|
247
|
+
if (!v) continue;
|
|
248
|
+
const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);
|
|
249
|
+
if (roots.has(rootName)) continue;
|
|
250
|
+
const rootEntry = vulnerabilities[rootName];
|
|
251
|
+
const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);
|
|
252
|
+
const title = detail?.title ?? rootName;
|
|
253
|
+
const url = detail?.url;
|
|
254
|
+
roots.set(rootName, {
|
|
255
|
+
module: rootEntry?.name ?? rootName,
|
|
256
|
+
severity,
|
|
257
|
+
title,
|
|
258
|
+
...url ? { url } : {}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return [...roots.values()];
|
|
262
|
+
}
|
|
205
263
|
async function tryRun(spawn2, cmd, args, cwd) {
|
|
206
264
|
try {
|
|
207
265
|
return await spawn2(cmd, args, { cwd });
|
|
@@ -255,26 +313,27 @@ async function securityAudit(ctx) {
|
|
|
255
313
|
details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
|
|
256
314
|
};
|
|
257
315
|
}
|
|
258
|
-
const
|
|
316
|
+
const counts = {
|
|
259
317
|
low: parsed.metadata?.vulnerabilities?.low ?? 0,
|
|
260
318
|
moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
|
|
261
319
|
high: parsed.metadata?.vulnerabilities?.high ?? 0,
|
|
262
320
|
critical: parsed.metadata?.vulnerabilities?.critical ?? 0
|
|
263
321
|
};
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
322
|
+
const advisories = used === "pnpm audit" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);
|
|
323
|
+
const status = classify(counts);
|
|
324
|
+
const total = counts.low + counts.moderate + counts.high + counts.critical;
|
|
325
|
+
const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;
|
|
267
326
|
return {
|
|
268
327
|
audit: "security",
|
|
269
328
|
site: label,
|
|
270
329
|
status,
|
|
271
330
|
summary,
|
|
272
|
-
details:
|
|
331
|
+
details: { counts, advisories }
|
|
273
332
|
};
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
// src/audits/lighthouse.ts
|
|
277
|
-
import { writeFile, mkdtemp, rm } from "fs/promises";
|
|
336
|
+
import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
|
|
278
337
|
import { tmpdir } from "os";
|
|
279
338
|
import { join as join3 } from "path";
|
|
280
339
|
|
|
@@ -310,29 +369,56 @@ var lighthouseConfig = {
|
|
|
310
369
|
function siteLabel4(site) {
|
|
311
370
|
return site.name ?? site.path;
|
|
312
371
|
}
|
|
313
|
-
function
|
|
372
|
+
async function readJsonMaybe(path) {
|
|
314
373
|
try {
|
|
315
|
-
const
|
|
316
|
-
|
|
374
|
+
const raw = await readFile3(path, "utf-8");
|
|
375
|
+
return JSON.parse(raw);
|
|
317
376
|
} catch {
|
|
318
377
|
return null;
|
|
319
378
|
}
|
|
320
|
-
|
|
379
|
+
}
|
|
380
|
+
function averageSummaries(entries) {
|
|
381
|
+
if (entries.length === 0) return {};
|
|
382
|
+
const sums = {};
|
|
383
|
+
const counts = {};
|
|
384
|
+
for (const e of entries) {
|
|
385
|
+
for (const [k, v] of Object.entries(e.summary ?? {})) {
|
|
386
|
+
if (typeof v !== "number") continue;
|
|
387
|
+
sums[k] = (sums[k] ?? 0) + v;
|
|
388
|
+
counts[k] = (counts[k] ?? 0) + 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const out = {};
|
|
392
|
+
for (const k of Object.keys(sums)) {
|
|
393
|
+
const total = sums[k] ?? 0;
|
|
394
|
+
const count = counts[k] ?? 1;
|
|
395
|
+
out[k] = total / count;
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
function categoryFromAssertion(a) {
|
|
400
|
+
const colonIdx = a.name.indexOf(":");
|
|
401
|
+
return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
|
|
402
|
+
}
|
|
403
|
+
function messageForAssertion(a) {
|
|
404
|
+
return `${a.name} ${a.operator} ${a.expected} (actual: ${a.actual.toFixed(2)})`;
|
|
321
405
|
}
|
|
322
406
|
async function lighthouseAudit(ctx) {
|
|
323
407
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
324
408
|
const site = ctx.site;
|
|
325
409
|
const label = siteLabel4(site);
|
|
326
|
-
const
|
|
327
|
-
const configPath = join3(
|
|
410
|
+
const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
|
|
411
|
+
const configPath = join3(configDir, "lighthouserc.json");
|
|
328
412
|
await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
|
|
413
|
+
const resultsDir = join3(site.path, ".lighthouseci");
|
|
414
|
+
await rm(resultsDir, { recursive: true, force: true });
|
|
329
415
|
let raw;
|
|
330
416
|
try {
|
|
331
417
|
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
332
418
|
cwd: site.path
|
|
333
419
|
});
|
|
334
420
|
} catch (err) {
|
|
335
|
-
await rm(
|
|
421
|
+
await rm(configDir, { recursive: true, force: true });
|
|
336
422
|
const e = err;
|
|
337
423
|
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
338
424
|
return {
|
|
@@ -344,17 +430,32 @@ async function lighthouseAudit(ctx) {
|
|
|
344
430
|
}
|
|
345
431
|
throw err;
|
|
346
432
|
}
|
|
347
|
-
await rm(
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
433
|
+
await rm(configDir, { recursive: true, force: true });
|
|
434
|
+
const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
|
|
435
|
+
if (!manifest || manifest.length === 0) {
|
|
436
|
+
return {
|
|
437
|
+
audit: "lighthouse",
|
|
438
|
+
site: label,
|
|
439
|
+
status: "fail",
|
|
440
|
+
summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
|
|
444
|
+
const failed = assertionResults.filter((a) => !a.passed);
|
|
445
|
+
const assertions = failed.map((a) => ({
|
|
446
|
+
category: categoryFromAssertion(a),
|
|
447
|
+
level: a.level,
|
|
448
|
+
message: messageForAssertion(a)
|
|
449
|
+
}));
|
|
450
|
+
const anyError = assertions.some((a) => a.level === "error");
|
|
451
|
+
const anyWarn = assertions.some((a) => a.level === "warn");
|
|
356
452
|
const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
|
|
357
|
-
const
|
|
453
|
+
const normalized = {
|
|
454
|
+
summary: averageSummaries(manifest),
|
|
455
|
+
assertionsFailed: failed.length,
|
|
456
|
+
assertions
|
|
457
|
+
};
|
|
458
|
+
const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${failed.length} assertion(s) failed`;
|
|
358
459
|
return {
|
|
359
460
|
audit: "lighthouse",
|
|
360
461
|
site: label,
|
|
@@ -365,7 +466,7 @@ async function lighthouseAudit(ctx) {
|
|
|
365
466
|
}
|
|
366
467
|
|
|
367
468
|
// src/audits/a11y.ts
|
|
368
|
-
import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
469
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
369
470
|
import { tmpdir as tmpdir2 } from "os";
|
|
370
471
|
import { join as join4 } from "path";
|
|
371
472
|
|
|
@@ -401,48 +502,81 @@ var playwrightA11yConfig = defineConfig({
|
|
|
401
502
|
});
|
|
402
503
|
|
|
403
504
|
// src/audits/a11y.ts
|
|
505
|
+
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
404
506
|
function siteLabel5(site) {
|
|
405
507
|
return site.name ?? site.path;
|
|
406
508
|
}
|
|
407
|
-
function
|
|
509
|
+
async function readJsonMaybe2(path) {
|
|
408
510
|
try {
|
|
409
|
-
const
|
|
410
|
-
|
|
511
|
+
const raw = await readFile4(path, "utf-8");
|
|
512
|
+
return JSON.parse(raw);
|
|
411
513
|
} catch {
|
|
412
514
|
return null;
|
|
413
515
|
}
|
|
414
|
-
return null;
|
|
415
516
|
}
|
|
416
517
|
function buildSpec() {
|
|
417
|
-
return `
|
|
418
|
-
import { test, expect } from "@playwright/test";
|
|
518
|
+
return `import { test, expect } from "@playwright/test";
|
|
419
519
|
import AxeBuilder from "@axe-core/playwright";
|
|
520
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
521
|
+
import { dirname } from "node:path";
|
|
522
|
+
|
|
420
523
|
const pages = ${JSON.stringify(a11yRoutes)};
|
|
421
|
-
|
|
422
|
-
|
|
524
|
+
const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
|
|
525
|
+
|
|
526
|
+
// Playwright's default per-test timeout is 30s. We loop through every
|
|
527
|
+
// configured route in a single test, so the budget needs to scale.
|
|
528
|
+
test.setTimeout(5 * 60_000);
|
|
529
|
+
|
|
530
|
+
test("a11y across configured routes", async ({ page }) => {
|
|
531
|
+
const violations = [];
|
|
532
|
+
for (const { path, name } of pages) {
|
|
423
533
|
await page.goto(path);
|
|
424
534
|
const results = await new AxeBuilder({ page })
|
|
425
535
|
.withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
|
|
426
536
|
.analyze();
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
537
|
+
for (const v of results.violations) {
|
|
538
|
+
violations.push({
|
|
539
|
+
id: v.id,
|
|
540
|
+
impact: v.impact ?? "moderate",
|
|
541
|
+
route: name,
|
|
542
|
+
help: v.help,
|
|
543
|
+
helpUrl: v.helpUrl,
|
|
544
|
+
nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const byImpact = {};
|
|
549
|
+
for (const v of violations) {
|
|
550
|
+
byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;
|
|
551
|
+
}
|
|
552
|
+
if (OUTPUT) {
|
|
553
|
+
await mkdir(dirname(OUTPUT), { recursive: true });
|
|
554
|
+
await writeFile(
|
|
555
|
+
OUTPUT,
|
|
556
|
+
JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
expect(violations).toEqual([]);
|
|
560
|
+
});
|
|
430
561
|
`;
|
|
431
562
|
}
|
|
432
563
|
async function a11yAudit(ctx) {
|
|
433
564
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
434
565
|
const site = ctx.site;
|
|
435
566
|
const label = siteLabel5(site);
|
|
436
|
-
const
|
|
437
|
-
const specPath = join4(
|
|
567
|
+
const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
|
|
568
|
+
const specPath = join4(specDir, "a11y.spec.ts");
|
|
438
569
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
570
|
+
const resultsPath = join4(site.path, RESULTS_REL);
|
|
571
|
+
await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
439
572
|
let raw;
|
|
440
573
|
try {
|
|
441
|
-
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=
|
|
442
|
-
cwd: site.path
|
|
574
|
+
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
|
|
575
|
+
cwd: site.path,
|
|
576
|
+
env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath }
|
|
443
577
|
});
|
|
444
578
|
} catch (err) {
|
|
445
|
-
await rm2(
|
|
579
|
+
await rm2(specDir, { recursive: true, force: true });
|
|
446
580
|
const e = err;
|
|
447
581
|
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
448
582
|
return {
|
|
@@ -454,22 +588,26 @@ async function a11yAudit(ctx) {
|
|
|
454
588
|
}
|
|
455
589
|
throw err;
|
|
456
590
|
}
|
|
457
|
-
await rm2(
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
591
|
+
await rm2(specDir, { recursive: true, force: true });
|
|
592
|
+
const artifact = await readJsonMaybe2(resultsPath);
|
|
593
|
+
if (!artifact) {
|
|
594
|
+
return {
|
|
595
|
+
audit: "a11y",
|
|
596
|
+
site: label,
|
|
597
|
+
status: "fail",
|
|
598
|
+
summary: `a11y: no results written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const hasSerious = (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;
|
|
602
|
+
const hasAny = artifact.totalViolations > 0;
|
|
465
603
|
const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
|
|
466
|
-
const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${
|
|
604
|
+
const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${artifact.totalViolations} violations`;
|
|
467
605
|
return {
|
|
468
606
|
audit: "a11y",
|
|
469
607
|
site: label,
|
|
470
608
|
status,
|
|
471
609
|
summary,
|
|
472
|
-
details:
|
|
610
|
+
details: artifact
|
|
473
611
|
};
|
|
474
612
|
}
|
|
475
613
|
|
|
@@ -512,7 +650,7 @@ async function runAuditsAcross(sites, which) {
|
|
|
512
650
|
}
|
|
513
651
|
|
|
514
652
|
// src/recipes/sync-configs.ts
|
|
515
|
-
import { readFile as
|
|
653
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
516
654
|
import { join as join5 } from "path";
|
|
517
655
|
|
|
518
656
|
// src/recipes/sync-configs/templates.ts
|
|
@@ -597,7 +735,7 @@ function siteLabel6(site) {
|
|
|
597
735
|
}
|
|
598
736
|
async function readMaybe(path) {
|
|
599
737
|
try {
|
|
600
|
-
return await
|
|
738
|
+
return await readFile5(path, "utf-8");
|
|
601
739
|
} catch {
|
|
602
740
|
return null;
|
|
603
741
|
}
|
|
@@ -663,6 +801,7 @@ async function bumpDeps(site, opts = {}) {
|
|
|
663
801
|
const label = siteLabel7(site);
|
|
664
802
|
const group = opts.group ?? "minor";
|
|
665
803
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
804
|
+
await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
|
|
666
805
|
const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
|
|
667
806
|
cwd: site.path
|
|
668
807
|
});
|
|
@@ -687,7 +826,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
687
826
|
}
|
|
688
827
|
const branch = branchName("bump-deps");
|
|
689
828
|
await createBranch(site.path, branch);
|
|
690
|
-
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
|
|
829
|
+
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
|
|
830
|
+
cwd: site.path,
|
|
831
|
+
streaming: true
|
|
832
|
+
});
|
|
691
833
|
const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
|
|
692
834
|
const shas = sha ? [sha] : [];
|
|
693
835
|
return {
|
|
@@ -703,16 +845,17 @@ async function bumpDeps(site, opts = {}) {
|
|
|
703
845
|
import { join as join11 } from "path";
|
|
704
846
|
|
|
705
847
|
// src/util/pkg.ts
|
|
706
|
-
import { readFile as
|
|
848
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
707
849
|
async function readPackageJson(path) {
|
|
708
|
-
const raw = await
|
|
850
|
+
const raw = await readFile6(path, "utf-8");
|
|
709
851
|
return JSON.parse(raw);
|
|
710
852
|
}
|
|
711
853
|
async function writePackageJson(path, pkg) {
|
|
712
854
|
const content = JSON.stringify(pkg, null, 2) + "\n";
|
|
713
855
|
await writeFile4(path, content, "utf-8");
|
|
714
856
|
}
|
|
715
|
-
function bumpDep(pkg, name, version) {
|
|
857
|
+
function bumpDep(pkg, name, version, opts = {}) {
|
|
858
|
+
const mode = opts.mode ?? "ensure";
|
|
716
859
|
const next = {
|
|
717
860
|
...pkg
|
|
718
861
|
};
|
|
@@ -732,6 +875,7 @@ function bumpDep(pkg, name, version) {
|
|
|
732
875
|
next.devDependencies[name] = version;
|
|
733
876
|
return next;
|
|
734
877
|
}
|
|
878
|
+
if (mode === "bump-only") return pkg;
|
|
735
879
|
next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
|
|
736
880
|
return next;
|
|
737
881
|
}
|
|
@@ -754,7 +898,7 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
754
898
|
const pkg = await readPackageJson(pkgPath);
|
|
755
899
|
let next = pkg;
|
|
756
900
|
for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
757
|
-
next = bumpDep(next, name, version);
|
|
901
|
+
next = bumpDep(next, name, version, { mode: "bump-only" });
|
|
758
902
|
}
|
|
759
903
|
if (next === pkg) return false;
|
|
760
904
|
await writePackageJson(pkgPath, next);
|
|
@@ -762,22 +906,58 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
762
906
|
}
|
|
763
907
|
|
|
764
908
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
765
|
-
import { readFile as
|
|
909
|
+
import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
766
910
|
import { join as join7 } from "path";
|
|
911
|
+
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
912
|
+
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
913
|
+
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
914
|
+
"m"
|
|
915
|
+
);
|
|
916
|
+
function dropVitePreprocessImport(source) {
|
|
917
|
+
return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names) => {
|
|
918
|
+
const remaining = names.split(",").map((n) => n.trim()).filter((n) => n.length > 0 && n !== "vitePreprocess");
|
|
919
|
+
if (remaining.length === 0) return "";
|
|
920
|
+
return `import { ${remaining.join(", ")} } from "${VITE_PLUGIN_PKG}";
|
|
921
|
+
`;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
function findMatchingParen(source, openIdx) {
|
|
925
|
+
if (source[openIdx] !== "(") return -1;
|
|
926
|
+
let depth = 0;
|
|
927
|
+
for (let i = openIdx; i < source.length; i++) {
|
|
928
|
+
const ch = source[i];
|
|
929
|
+
if (ch === "(") depth++;
|
|
930
|
+
else if (ch === ")") {
|
|
931
|
+
depth--;
|
|
932
|
+
if (depth === 0) return i;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return -1;
|
|
936
|
+
}
|
|
937
|
+
function dropPreprocessKey(source) {
|
|
938
|
+
const startRe = /^(\s*)preprocess:\s*vitePreprocess\(/m;
|
|
939
|
+
const m = startRe.exec(source);
|
|
940
|
+
if (!m) return source;
|
|
941
|
+
const indent = m[1] ?? "";
|
|
942
|
+
const parenOpenAbs = m.index + m[0].length - 1;
|
|
943
|
+
const parenCloseAbs = findMatchingParen(source, parenOpenAbs);
|
|
944
|
+
if (parenCloseAbs < 0) return source;
|
|
945
|
+
let tailIdx = parenCloseAbs + 1;
|
|
946
|
+
while (tailIdx < source.length && /[ \t,]/.test(source[tailIdx] ?? "")) tailIdx++;
|
|
947
|
+
if (source[tailIdx] === "\n") tailIdx++;
|
|
948
|
+
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
949
|
+
}
|
|
767
950
|
async function migrateSvelteConfig(cwd) {
|
|
768
951
|
const path = join7(cwd, "svelte.config.js");
|
|
769
952
|
let src;
|
|
770
953
|
try {
|
|
771
|
-
src = await
|
|
954
|
+
src = await readFile7(path, "utf-8");
|
|
772
955
|
} catch {
|
|
773
956
|
return false;
|
|
774
957
|
}
|
|
775
958
|
let next = src;
|
|
776
|
-
next = next
|
|
777
|
-
|
|
778
|
-
""
|
|
779
|
-
);
|
|
780
|
-
next = next.replace(/^\s*preprocess:\s*vitePreprocess\(\)\s*,?\s*\n/m, "");
|
|
959
|
+
next = dropPreprocessKey(next);
|
|
960
|
+
next = dropVitePreprocessImport(next);
|
|
781
961
|
if (next === src) return false;
|
|
782
962
|
await writeFile5(path, next, "utf-8");
|
|
783
963
|
return true;
|
|
@@ -828,7 +1008,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
828
1008
|
}
|
|
829
1009
|
|
|
830
1010
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
831
|
-
import { readFile as
|
|
1011
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
832
1012
|
import { join as join9 } from "path";
|
|
833
1013
|
import { glob as glob2 } from "tinyglobby";
|
|
834
1014
|
|
|
@@ -887,10 +1067,32 @@ function exportLetToProps(source) {
|
|
|
887
1067
|
}
|
|
888
1068
|
|
|
889
1069
|
// src/recipes/svelte-5/codemods/dollar-restprops.ts
|
|
1070
|
+
function removeInterfaceBlock(source) {
|
|
1071
|
+
const re = /^\s*interface\s+\$\$Props\s*\{/m;
|
|
1072
|
+
let out = source;
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const match = re.exec(out);
|
|
1075
|
+
if (!match) return out;
|
|
1076
|
+
const openBraceIdx = match.index + match[0].length - 1;
|
|
1077
|
+
let depth = 1;
|
|
1078
|
+
let i = openBraceIdx + 1;
|
|
1079
|
+
while (i < out.length && depth > 0) {
|
|
1080
|
+
const ch = out[i];
|
|
1081
|
+
if (ch === "{") depth++;
|
|
1082
|
+
else if (ch === "}") depth--;
|
|
1083
|
+
i++;
|
|
1084
|
+
}
|
|
1085
|
+
if (depth !== 0) return out;
|
|
1086
|
+
let endIdx = i;
|
|
1087
|
+
while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
|
|
1088
|
+
if (out[endIdx] === "\n") endIdx++;
|
|
1089
|
+
out = out.slice(0, match.index) + out.slice(endIdx);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
890
1092
|
function removeDollarRestProps(source) {
|
|
891
1093
|
let next = source;
|
|
892
1094
|
next = next.replace(/\$\$restProps/g, "rest");
|
|
893
|
-
next = next
|
|
1095
|
+
next = removeInterfaceBlock(next);
|
|
894
1096
|
return next;
|
|
895
1097
|
}
|
|
896
1098
|
|
|
@@ -903,7 +1105,7 @@ async function applyGotchaCodemods(cwd) {
|
|
|
903
1105
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
904
1106
|
for (const rel of relPaths) {
|
|
905
1107
|
const path = join9(cwd, rel);
|
|
906
|
-
const before = await
|
|
1108
|
+
const before = await readFile8(path, "utf-8");
|
|
907
1109
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
908
1110
|
if (after !== before) {
|
|
909
1111
|
await writeFile6(path, after, "utf-8");
|
|
@@ -1050,7 +1252,8 @@ function localPath(path, opts = {}) {
|
|
|
1050
1252
|
}
|
|
1051
1253
|
|
|
1052
1254
|
// src/inventory/json.ts
|
|
1053
|
-
import { readFile as
|
|
1255
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1256
|
+
import { isAbsolute } from "path";
|
|
1054
1257
|
function validate(raw) {
|
|
1055
1258
|
if (!Array.isArray(raw)) {
|
|
1056
1259
|
throw new Error("inventory JSON must be an array of sites");
|
|
@@ -1063,6 +1266,11 @@ function validate(raw) {
|
|
|
1063
1266
|
if (typeof e.path !== "string" || e.path.length === 0) {
|
|
1064
1267
|
throw new Error(`inventory entry ${i} is missing required field: path`);
|
|
1065
1268
|
}
|
|
1269
|
+
if (!isAbsolute(e.path)) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`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.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1066
1274
|
const site = { path: e.path };
|
|
1067
1275
|
if (typeof e.name === "string") site.name = e.name;
|
|
1068
1276
|
if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
|
|
@@ -1074,7 +1282,7 @@ function validate(raw) {
|
|
|
1074
1282
|
}
|
|
1075
1283
|
function fromJsonFile(path) {
|
|
1076
1284
|
return async () => {
|
|
1077
|
-
const raw = JSON.parse(await
|
|
1285
|
+
const raw = JSON.parse(await readFile9(path, "utf-8"));
|
|
1078
1286
|
return validate(raw);
|
|
1079
1287
|
};
|
|
1080
1288
|
}
|