@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/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
- child.stdout.on("data", (chunk) => stdout += String(chunk));
12
- child.stderr.on("data", (chunk) => stderr += String(chunk));
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, relative } from "path";
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 filesToLint = relFiles.map((f) => join2(site.path, f));
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 file of filesToLint) {
175
- const source = await readFile2(file, "utf-8");
176
- const options = await prettierResolveConfig(file) ?? {};
177
- const ok = await prettierCheck(source, { ...options, filepath: file });
178
- if (!ok) prettierUnformatted.push(relative(site.path, file));
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 ${filesToLint.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
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: filesToLint.length
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 vuln = {
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 status = classify(vuln);
265
- const total = vuln.low + vuln.moderate + vuln.high + vuln.critical;
266
- const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${vuln.critical}C/${vuln.high}H/${vuln.moderate}M/${vuln.low}L)`;
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: vuln
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 isFakeShape(stdout) {
372
+ async function readJsonMaybe(path) {
314
373
  try {
315
- const parsed = JSON.parse(stdout);
316
- if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
374
+ const raw = await readFile3(path, "utf-8");
375
+ return JSON.parse(raw);
317
376
  } catch {
318
377
  return null;
319
378
  }
320
- return null;
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 dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
327
- const configPath = join3(dir, "lighthouserc.json");
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(dir, { recursive: true, force: true });
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(dir, { recursive: true, force: true });
348
- const fake = isFakeShape(raw.stdout);
349
- const normalized = fake ?? {
350
- summary: {},
351
- assertionsFailed: raw.code === 0 ? 0 : 1,
352
- assertions: raw.code === 0 ? [] : [{ category: "unknown", level: "error", message: raw.stderr.slice(0, 200) }]
353
- };
354
- const anyError = (normalized.assertions ?? []).some((a) => a.level === "error");
355
- const anyWarn = (normalized.assertions ?? []).some((a) => a.level === "warn");
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 summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
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 isFakeShape2(stdout) {
509
+ async function readJsonMaybe2(path) {
408
510
  try {
409
- const parsed = JSON.parse(stdout);
410
- if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
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
- for (const { path, name } of pages) {
422
- test(\`\${name} has no axe violations\`, async ({ page }) => {
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
- expect(results.violations).toEqual([]);
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 dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
437
- const specPath = join4(dir, "a11y.spec.ts");
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=json", specPath], {
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(dir, { recursive: true, force: true });
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(dir, { recursive: true, force: true });
458
- const fake = isFakeShape2(raw.stdout);
459
- const normalized = fake ?? {
460
- totalViolations: raw.code === 0 ? 0 : 1,
461
- byImpact: raw.code === 0 ? {} : { moderate: 1 }
462
- };
463
- const hasSerious = (normalized.byImpact.serious ?? 0) > 0 || (normalized.byImpact.critical ?? 0) > 0;
464
- const hasAny = normalized.totalViolations > 0;
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: ${normalized.totalViolations} violations`;
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: normalized
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 readFile3, writeFile as writeFile3 } from "fs/promises";
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 readFile3(path, "utf-8");
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)], { cwd: site.path });
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 readFile4, writeFile as writeFile4 } from "fs/promises";
848
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
707
849
  async function readPackageJson(path) {
708
- const raw = await readFile4(path, "utf-8");
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 readFile5, writeFile as writeFile5 } from "fs/promises";
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 readFile5(path, "utf-8");
954
+ src = await readFile7(path, "utf-8");
772
955
  } catch {
773
956
  return false;
774
957
  }
775
958
  let next = src;
776
- next = next.replace(
777
- /^import\s+\{\s*vitePreprocess\s*\}\s+from\s+["']@sveltejs\/vite-plugin-svelte["'];\n/m,
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 readFile6, writeFile as writeFile6 } from "fs/promises";
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.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
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 readFile6(path, "utf-8");
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 readFile7 } from "fs/promises";
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 readFile7(path, "utf-8"));
1285
+ const raw = JSON.parse(await readFile9(path, "utf-8"));
1078
1286
  return validate(raw);
1079
1287
  };
1080
1288
  }