@reddoorla/maintenance 0.6.5 → 0.6.7

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 CHANGED
@@ -41,6 +41,11 @@ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve9, reject) => {
41
41
  import { readFile } from "fs/promises";
42
42
  import { join } from "path";
43
43
 
44
+ // src/util/site.ts
45
+ function siteLabel(site) {
46
+ return site.name ?? site.path;
47
+ }
48
+
44
49
  // src/configs/baseline-versions.ts
45
50
  var baselineVersions = {
46
51
  // SvelteKit core
@@ -81,9 +86,6 @@ var baselineVersions = {
81
86
  };
82
87
 
83
88
  // src/audits/deps.ts
84
- function siteLabel(site) {
85
- return site.name ?? site.path;
86
- }
87
89
  function stripCaret(range) {
88
90
  return range.replace(/^[\^~]/, "");
89
91
  }
@@ -156,9 +158,6 @@ import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "
156
158
  import { glob } from "tinyglobby";
157
159
  var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
158
160
  var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
159
- function siteLabel2(site) {
160
- return site.name ?? site.path;
161
- }
162
161
  async function listFiles(cwd) {
163
162
  return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
164
163
  }
@@ -168,7 +167,7 @@ async function lintAudit(ctx) {
168
167
  if (!existsSync(configPath)) {
169
168
  return {
170
169
  audit: "lint",
171
- site: siteLabel2(site),
170
+ site: siteLabel(site),
172
171
  status: "skip",
173
172
  summary: "no eslint config at site root"
174
173
  };
@@ -194,7 +193,7 @@ async function lintAudit(ctx) {
194
193
  const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
195
194
  return {
196
195
  audit: "lint",
197
- site: siteLabel2(site),
196
+ site: siteLabel(site),
198
197
  status,
199
198
  summary,
200
199
  details: {
@@ -207,9 +206,6 @@ async function lintAudit(ctx) {
207
206
  }
208
207
 
209
208
  // src/audits/security.ts
210
- function siteLabel3(site) {
211
- return site.name ?? site.path;
212
- }
213
209
  function classify(v) {
214
210
  if (v.critical > 0 || v.high > 0) return "fail";
215
211
  if (v.moderate > 0 || v.low > 0) return "warn";
@@ -295,7 +291,8 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
295
291
  if (errEnvelope && typeof errEnvelope === "object") {
296
292
  return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
297
293
  }
298
- if (!parsed.metadata?.vulnerabilities) {
294
+ const vulnsMeta = parsed.metadata?.vulnerabilities;
295
+ if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {
299
296
  return { kind: "error", reason: "no metadata.vulnerabilities in output" };
300
297
  }
301
298
  return { kind: "ok", parsed };
@@ -303,7 +300,7 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
303
300
  async function securityAudit(ctx) {
304
301
  const spawn2 = ctx.spawn ?? defaultSpawn;
305
302
  const site = ctx.site;
306
- const label = siteLabel3(site);
303
+ const label = siteLabel(site);
307
304
  let used = "pnpm audit";
308
305
  let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
309
306
  if (result.kind !== "ok") {
@@ -384,9 +381,6 @@ var lighthouseConfig = {
384
381
  };
385
382
 
386
383
  // src/audits/lighthouse.ts
387
- function siteLabel4(site) {
388
- return site.name ?? site.path;
389
- }
390
384
  async function readJsonMaybe(path) {
391
385
  try {
392
386
  const raw = await readFile3(path, "utf-8");
@@ -424,7 +418,7 @@ function messageForAssertion(a) {
424
418
  async function lighthouseAudit(ctx) {
425
419
  const spawn2 = ctx.spawn ?? defaultSpawn;
426
420
  const site = ctx.site;
427
- const label = siteLabel4(site);
421
+ const label = siteLabel(site);
428
422
  const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
429
423
  const configPath = join3(configDir, "lighthouserc.json");
430
424
  await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
@@ -522,9 +516,6 @@ var playwrightA11yConfig = defineConfig({
522
516
 
523
517
  // src/audits/a11y.ts
524
518
  var RESULTS_REL = ".reddoor-a11y/results.json";
525
- function siteLabel5(site) {
526
- return site.name ?? site.path;
527
- }
528
519
  async function readJsonMaybe2(path) {
529
520
  try {
530
521
  const raw = await readFile4(path, "utf-8");
@@ -582,7 +573,7 @@ test("a11y across configured routes", async ({ page }) => {
582
573
  async function a11yAudit(ctx) {
583
574
  const spawn2 = ctx.spawn ?? defaultSpawn;
584
575
  const site = ctx.site;
585
- const label = siteLabel5(site);
576
+ const label = siteLabel(site);
586
577
  const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
587
578
  const specPath = join4(specDir, "a11y.spec.ts");
588
579
  await writeFile2(specPath, buildSpec(), "utf-8");
@@ -1007,9 +998,8 @@ async function git(cwd, args) {
1007
998
  return exec("git", args, { cwd, env: process.env });
1008
999
  }
1009
1000
  function branchName(recipe, when = /* @__PURE__ */ new Date()) {
1010
- const iso = when.toISOString().replace(/[-:.]/g, "").replace(/Z$/, "Z");
1011
- const trimmed = iso.replace(/(\d{8}T\d{6})\d+(Z)$/, "$1$2");
1012
- return `maint/${recipe}-${trimmed}`;
1001
+ const compact = when.toISOString().replace(/[-:.]/g, "");
1002
+ return `maint/${recipe}-${compact}`;
1013
1003
  }
1014
1004
  async function isWorkingTreeClean(cwd) {
1015
1005
  const { stdout } = await git(cwd, ["status", "--porcelain"]);
@@ -1040,8 +1030,16 @@ async function commit(cwd, message) {
1040
1030
 
1041
1031
  // src/recipes/sync-configs.ts
1042
1032
  var GITIGNORE_CONFIG = "gitignore";
1043
- function siteLabel6(site) {
1044
- return site.name ?? site.path;
1033
+ var ALL_CONFIG_NAMES = [
1034
+ "lighthouse",
1035
+ "eslint",
1036
+ "prettier",
1037
+ "playwright-a11y",
1038
+ "svelte",
1039
+ "gitignore"
1040
+ ];
1041
+ function isConfigName(value) {
1042
+ return ALL_CONFIG_NAMES.includes(value);
1045
1043
  }
1046
1044
  async function readMaybe(path) {
1047
1045
  try {
@@ -1073,7 +1071,7 @@ async function applyGitignore(cwd, plan) {
1073
1071
  }
1074
1072
  }
1075
1073
  async function syncConfigs(site, opts = {}) {
1076
- const label = siteLabel6(site);
1074
+ const label = siteLabel(site);
1077
1075
  const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);
1078
1076
  const templateNames = requested.filter((c) => c !== GITIGNORE_CONFIG);
1079
1077
  const templates = templatesByName(templateNames);
@@ -1119,12 +1117,34 @@ async function syncConfigs(site, opts = {}) {
1119
1117
 
1120
1118
  // src/cli/commands/sync-configs.ts
1121
1119
  function parseOnly2(value) {
1122
- return value ? value.split(",").map((s) => s.trim()) : void 0;
1120
+ if (!value) return void 0;
1121
+ const names = value.split(",").map((s) => s.trim());
1122
+ for (const n of names) {
1123
+ if (!isConfigName(n)) {
1124
+ throw Object.assign(
1125
+ new Error(`unknown config in --only: "${n}". Valid: ${ALL_CONFIG_NAMES.join(", ")}`),
1126
+ { exitCode: 2 }
1127
+ );
1128
+ }
1129
+ }
1130
+ return names;
1131
+ }
1132
+ async function dryPlanGitignore(cwd) {
1133
+ let existing;
1134
+ try {
1135
+ existing = await readFile7(join7(cwd, ".gitignore"), "utf-8");
1136
+ } catch {
1137
+ return "would create .gitignore";
1138
+ }
1139
+ const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
1140
+ if (merge.added.length === 0) return null;
1141
+ return `would update .gitignore (${merge.added.length} canonical entries to add)`;
1123
1142
  }
1124
1143
  async function dryPlan(cwd, which) {
1125
- const targets = which ? templatesByName(which) : ALL_TEMPLATES;
1144
+ const includeGitignore = which ? which.includes("gitignore") : true;
1145
+ const templateTargets = which ? templatesByName(which.filter((c) => c !== "gitignore")) : ALL_TEMPLATES;
1126
1146
  const lines = [];
1127
- for (const t of targets) {
1147
+ for (const t of templateTargets) {
1128
1148
  let existing = "";
1129
1149
  try {
1130
1150
  existing = await readFile7(join7(cwd, t.path), "utf-8");
@@ -1132,6 +1152,10 @@ async function dryPlan(cwd, which) {
1132
1152
  }
1133
1153
  if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
1134
1154
  }
1155
+ if (includeGitignore) {
1156
+ const gi = await dryPlanGitignore(cwd);
1157
+ if (gi) lines.push(gi);
1158
+ }
1135
1159
  return lines.length === 0 ? "no changes needed" : lines.join("\n");
1136
1160
  }
1137
1161
  function formatResult(r) {
@@ -1170,8 +1194,15 @@ async function runSyncConfigsCommand(site, opts) {
1170
1194
  import { resolve as resolve4 } from "path";
1171
1195
 
1172
1196
  // src/recipes/bump-deps.ts
1173
- function siteLabel7(site) {
1174
- return site.name ?? site.path;
1197
+ import { stat as stat2 } from "fs/promises";
1198
+ import { join as join8 } from "path";
1199
+ async function exists(path) {
1200
+ try {
1201
+ await stat2(path);
1202
+ return true;
1203
+ } catch {
1204
+ return false;
1205
+ }
1175
1206
  }
1176
1207
  function outdatedFlagsForGroup(group) {
1177
1208
  if (group === "major") return ["--latest"];
@@ -1183,9 +1214,27 @@ function upFlagsForGroup(group) {
1183
1214
  return [];
1184
1215
  }
1185
1216
  async function bumpDeps(site, opts = {}) {
1186
- const label = siteLabel7(site);
1217
+ const label = siteLabel(site);
1187
1218
  const group = opts.group ?? "minor";
1188
1219
  const spawn2 = opts.spawn ?? defaultSpawn;
1220
+ const hasPnpmLock = await exists(join8(site.path, "pnpm-lock.yaml"));
1221
+ if (!hasPnpmLock) {
1222
+ const hasNpmLock = await exists(join8(site.path, "package-lock.json"));
1223
+ const hasYarnLock = await exists(join8(site.path, "yarn.lock"));
1224
+ if (hasNpmLock || hasYarnLock) {
1225
+ const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
1226
+ return {
1227
+ recipe: "bump-deps",
1228
+ site: label,
1229
+ status: "failed",
1230
+ commits: [],
1231
+ notes: `site has ${competing} but no pnpm-lock.yaml \u2014 run convert-to-pnpm first`
1232
+ };
1233
+ }
1234
+ }
1235
+ if (!await isWorkingTreeClean(site.path)) {
1236
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1237
+ }
1189
1238
  await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
1190
1239
  const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
1191
1240
  cwd: site.path
@@ -1206,9 +1255,6 @@ async function bumpDeps(site, opts = {}) {
1206
1255
  notes: `pnpm outdated reported nothing for group=${group}`
1207
1256
  };
1208
1257
  }
1209
- if (!await isWorkingTreeClean(site.path)) {
1210
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1211
- }
1212
1258
  const branch = branchName("bump-deps");
1213
1259
  await createBranch(site.path, branch);
1214
1260
  await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
@@ -1262,7 +1308,7 @@ async function runBumpDepsCommand(site, opts) {
1262
1308
  import { resolve as resolve5 } from "path";
1263
1309
 
1264
1310
  // src/recipes/svelte-5/index.ts
1265
- import { join as join13 } from "path";
1311
+ import { join as join14 } from "path";
1266
1312
 
1267
1313
  // src/util/pkg.ts
1268
1314
  import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
@@ -1270,8 +1316,18 @@ async function readPackageJson(path) {
1270
1316
  const raw = await readFile8(path, "utf-8");
1271
1317
  return JSON.parse(raw);
1272
1318
  }
1319
+ function detectIndentFromContent(raw) {
1320
+ const match = raw.match(/\n([ \t]+)"/);
1321
+ return match ? match[1] ?? " " : " ";
1322
+ }
1273
1323
  async function writePackageJson(path, pkg) {
1274
- const content = JSON.stringify(pkg, null, 2) + "\n";
1324
+ let indent = " ";
1325
+ try {
1326
+ const existing = await readFile8(path, "utf-8");
1327
+ indent = detectIndentFromContent(existing);
1328
+ } catch {
1329
+ }
1330
+ const content = JSON.stringify(pkg, null, indent) + "\n";
1275
1331
  await writeFile4(path, content, "utf-8");
1276
1332
  }
1277
1333
  function bumpDep(pkg, name, version2, opts = {}) {
@@ -1301,7 +1357,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
1301
1357
  }
1302
1358
 
1303
1359
  // src/recipes/svelte-5/step-bump-versions.ts
1304
- import { join as join8 } from "path";
1360
+ import { join as join9 } from "path";
1305
1361
  var SVELTE_5_VERSIONS = {
1306
1362
  svelte: "^5.55.5",
1307
1363
  "@sveltejs/kit": "^2.59.0",
@@ -1314,7 +1370,7 @@ var SVELTE_5_VERSIONS = {
1314
1370
  "typescript-svelte-plugin": "^0.3.52"
1315
1371
  };
1316
1372
  async function bumpToSvelte5Versions(cwd) {
1317
- const pkgPath = join8(cwd, "package.json");
1373
+ const pkgPath = join9(cwd, "package.json");
1318
1374
  const pkg = await readPackageJson(pkgPath);
1319
1375
  let next = pkg;
1320
1376
  for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
@@ -1327,7 +1383,7 @@ async function bumpToSvelte5Versions(cwd) {
1327
1383
 
1328
1384
  // src/recipes/svelte-5/step-svelte-config.ts
1329
1385
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
1330
- import { join as join9 } from "path";
1386
+ import { join as join10 } from "path";
1331
1387
  var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
1332
1388
  var IMPORT_FROM_VITE_PLUGIN = new RegExp(
1333
1389
  String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
@@ -1368,7 +1424,7 @@ function dropPreprocessKey(source) {
1368
1424
  return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
1369
1425
  }
1370
1426
  async function migrateSvelteConfig(cwd) {
1371
- const path = join9(cwd, "svelte.config.js");
1427
+ const path = join10(cwd, "svelte.config.js");
1372
1428
  let src;
1373
1429
  try {
1374
1430
  src = await readFile9(path, "utf-8");
@@ -1405,9 +1461,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1405
1461
  }
1406
1462
 
1407
1463
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
1408
- import { join as join10 } from "path";
1464
+ import { join as join11 } from "path";
1409
1465
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1410
- const pkg = await readPackageJson(join10(cwd, "package.json"));
1466
+ const pkg = await readPackageJson(join11(cwd, "package.json"));
1411
1467
  const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
1412
1468
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1413
1469
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
@@ -1429,12 +1485,40 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1429
1485
 
1430
1486
  // src/recipes/svelte-5/step-gotchas.ts
1431
1487
  import { readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
1432
- import { join as join11 } from "path";
1488
+ import { join as join12 } from "path";
1433
1489
  import { glob as glob2 } from "tinyglobby";
1434
1490
 
1435
1491
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
1436
1492
  var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
1437
1493
  var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
1494
+ var MODIFIER_EVENT = /\bon:[a-z]+\|[a-zA-Z]+(?=\s*=)/g;
1495
+ function flagEventModifiers(source) {
1496
+ const insertions = [];
1497
+ let m;
1498
+ MODIFIER_EVENT.lastIndex = 0;
1499
+ while ((m = MODIFIER_EVENT.exec(source)) !== null) {
1500
+ const tagStart = source.lastIndexOf("<", m.index);
1501
+ if (tagStart === -1) continue;
1502
+ const prevLineEnd = tagStart - 1;
1503
+ if (prevLineEnd >= 0) {
1504
+ const prevLineStart = source.lastIndexOf("\n", prevLineEnd - 1) + 1;
1505
+ const prevLine = source.slice(prevLineStart, prevLineEnd + 1);
1506
+ if (/<!--\s*@migration-task/.test(prevLine)) continue;
1507
+ }
1508
+ const lineStart = source.lastIndexOf("\n", tagStart - 1) + 1;
1509
+ const indent = source.slice(lineStart, tagStart);
1510
+ const safeIndent = /^[ \t]*$/.test(indent) ? indent : "";
1511
+ insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });
1512
+ }
1513
+ let out = source;
1514
+ for (let i = insertions.length - 1; i >= 0; i--) {
1515
+ const { tagStart, indent, modifier } = insertions[i];
1516
+ const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\`${modifier}\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->
1517
+ ${indent}`;
1518
+ out = out.slice(0, tagStart) + comment + out.slice(tagStart);
1519
+ }
1520
+ return out;
1521
+ }
1438
1522
  function onEventToHandler(source) {
1439
1523
  const masked = [];
1440
1524
  const placeholder = (i) => ` SCRIPT_${i} `;
@@ -1442,8 +1526,9 @@ function onEventToHandler(source) {
1442
1526
  masked.push(match);
1443
1527
  return placeholder(masked.length - 1);
1444
1528
  });
1445
- const rewritten = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
1446
- let out = rewritten;
1529
+ let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
1530
+ processed = flagEventModifiers(processed);
1531
+ let out = processed;
1447
1532
  masked.forEach((blk, i) => {
1448
1533
  out = out.replace(placeholder(i), blk);
1449
1534
  });
@@ -1493,6 +1578,22 @@ function exportLetToProps(source) {
1493
1578
  return source.replace(SCRIPT_BLOCK2, (full) => full.replace(inner, body));
1494
1579
  }
1495
1580
 
1581
+ // src/util/svelte-source.ts
1582
+ function findStringEnd(source, openIdx) {
1583
+ const quote = source[openIdx];
1584
+ let i = openIdx + 1;
1585
+ while (i < source.length) {
1586
+ const ch = source[i];
1587
+ if (ch === "\\") {
1588
+ i += 2;
1589
+ continue;
1590
+ }
1591
+ if (ch === quote) return i;
1592
+ i++;
1593
+ }
1594
+ return -1;
1595
+ }
1596
+
1496
1597
  // src/recipes/svelte-5/codemods/dollar-restprops.ts
1497
1598
  function removeInterfaceBlock(source) {
1498
1599
  const re = /^\s*interface\s+\$\$Props\s*\{/m;
@@ -1516,20 +1617,6 @@ function removeInterfaceBlock(source) {
1516
1617
  out = out.slice(0, match.index) + out.slice(endIdx);
1517
1618
  }
1518
1619
  }
1519
- function findStringEnd(source, openIdx) {
1520
- const quote = source[openIdx];
1521
- let i = openIdx + 1;
1522
- while (i < source.length) {
1523
- const ch = source[i];
1524
- if (ch === "\\") {
1525
- i += 2;
1526
- continue;
1527
- }
1528
- if (ch === quote) return i;
1529
- i++;
1530
- }
1531
- return -1;
1532
- }
1533
1620
  function maskStringLiterals(source) {
1534
1621
  const strings = [];
1535
1622
  let out = "";
@@ -1606,7 +1693,8 @@ function stateEffectSyncToDerived(source) {
1606
1693
 
1607
1694
  // src/recipes/svelte-5/codemods/dollar-props-class.ts
1608
1695
  var PROPS_DESTRUCTURE = /let\s*\{([\s\S]*?)\}(\s*:\s*\{([\s\S]*?)\})?\s*=\s*\$props\(\)/;
1609
- var DOLLAR_PROPS_CLASS = /\$\$props\.class\b/g;
1696
+ var HAS_DOLLAR_PROPS_CLASS = /\$\$props\.class\b/;
1697
+ var DOLLAR_PROPS_CLASS_GLOBAL = /\$\$props\.class\b/g;
1610
1698
  var DOLLAR_PROPS_ANY = /\$\$props\b/;
1611
1699
  var SCRIPT_BLOCK4 = /<script\b[^>]*>[\s\S]*?<\/script>/g;
1612
1700
  var MIGRATION_TASK = /^<!--\s*@migration-task[\s\S]*?-->\s*\n?/gm;
@@ -1628,8 +1716,7 @@ function restoreScripts(masked, blocks) {
1628
1716
  }
1629
1717
  function dollarPropsClass(source) {
1630
1718
  const { masked } = maskScripts(source);
1631
- if (!DOLLAR_PROPS_CLASS.test(masked)) return source;
1632
- DOLLAR_PROPS_CLASS.lastIndex = 0;
1719
+ if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;
1633
1720
  if (!PROPS_DESTRUCTURE.test(source)) return source;
1634
1721
  let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {
1635
1722
  if (/\bclass\s*:/.test(body)) return full;
@@ -1643,7 +1730,7 @@ function dollarPropsClass(source) {
1643
1730
  return `let { ${newBody} } = $props()`;
1644
1731
  });
1645
1732
  const reMasked = maskScripts(updated);
1646
- const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS, IDENT);
1733
+ const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);
1647
1734
  updated = restoreScripts(templateRewritten, reMasked.blocks);
1648
1735
  const stripped = updated.replace(MIGRATION_TASK, "");
1649
1736
  if (!DOLLAR_PROPS_ANY.test(stripped)) {
@@ -1662,7 +1749,7 @@ function findMatchingClose(source, openIdx) {
1662
1749
  while (i < source.length) {
1663
1750
  const ch = source[i];
1664
1751
  if (ch === '"' || ch === "'" || ch === "`") {
1665
- const closeStr = findStringClose(source, i);
1752
+ const closeStr = findStringEnd(source, i);
1666
1753
  if (closeStr === -1) return -1;
1667
1754
  i = closeStr + 1;
1668
1755
  continue;
@@ -1676,20 +1763,6 @@ function findMatchingClose(source, openIdx) {
1676
1763
  }
1677
1764
  return -1;
1678
1765
  }
1679
- function findStringClose(source, openIdx) {
1680
- const quote = source[openIdx];
1681
- let i = openIdx + 1;
1682
- while (i < source.length) {
1683
- const ch = source[i];
1684
- if (ch === "\\") {
1685
- i += 2;
1686
- continue;
1687
- }
1688
- if (ch === quote) return i;
1689
- i++;
1690
- }
1691
- return -1;
1692
- }
1693
1766
  var MIGRATION_MARKER = "// @migration-task: $effect won't trigger UI updates on plain `let` bindings \u2014 refine mutated locals to $state or split into per-variable $derived.";
1694
1767
  function transformBlocks(body) {
1695
1768
  const out = [];
@@ -1744,7 +1817,7 @@ async function planGotchaCodemods(cwd) {
1744
1817
  const changes = [];
1745
1818
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
1746
1819
  for (const rel of relPaths) {
1747
- const path = join11(cwd, rel);
1820
+ const path = join12(cwd, rel);
1748
1821
  const before = await readFile10(path, "utf-8");
1749
1822
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
1750
1823
  if (after !== before) changes.push({ rel, after });
@@ -1754,7 +1827,7 @@ async function planGotchaCodemods(cwd) {
1754
1827
  async function applyGotchaCodemods(cwd) {
1755
1828
  const changes = await planGotchaCodemods(cwd);
1756
1829
  for (const c of changes) {
1757
- await writeFile6(join11(cwd, c.rel), c.after, "utf-8");
1830
+ await writeFile6(join12(cwd, c.rel), c.after, "utf-8");
1758
1831
  }
1759
1832
  return { filesChanged: changes.length };
1760
1833
  }
@@ -1778,7 +1851,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
1778
1851
 
1779
1852
  // src/recipes/svelte-5/step-summary.ts
1780
1853
  import { writeFile as writeFile7 } from "fs/promises";
1781
- import { join as join12 } from "path";
1854
+ import { join as join13 } from "path";
1782
1855
  async function writeMigrationSummary(input) {
1783
1856
  const lines = [
1784
1857
  `# Svelte 4 \u2192 5 migration summary`,
@@ -1795,18 +1868,15 @@ async function writeMigrationSummary(input) {
1795
1868
  `- Verify Playwright a11y tests still pass.`
1796
1869
  ];
1797
1870
  const content = lines.join("\n") + "\n";
1798
- const path = join12(input.cwd, "MIGRATION_SVELTE_5.md");
1871
+ const path = join13(input.cwd, "MIGRATION_SVELTE_5.md");
1799
1872
  await writeFile7(path, content, "utf-8");
1800
1873
  return path;
1801
1874
  }
1802
1875
 
1803
1876
  // src/recipes/svelte-5/index.ts
1804
- function siteLabel8(site) {
1805
- return site.name ?? site.path;
1806
- }
1807
1877
  async function alreadyOnSvelte5(cwd) {
1808
1878
  try {
1809
- const pkg = await readPackageJson(join13(cwd, "package.json"));
1879
+ const pkg = await readPackageJson(join14(cwd, "package.json"));
1810
1880
  const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
1811
1881
  return !!v && /^\^?5\./.test(v);
1812
1882
  } catch {
@@ -1814,7 +1884,7 @@ async function alreadyOnSvelte5(cwd) {
1814
1884
  }
1815
1885
  }
1816
1886
  async function upgradeSvelte4to5(site, opts = {}) {
1817
- const label = siteLabel8(site);
1887
+ const label = siteLabel(site);
1818
1888
  const spawn2 = opts.spawn ?? defaultSpawn;
1819
1889
  if (await alreadyOnSvelte5(site.path)) {
1820
1890
  return {
@@ -1923,8 +1993,8 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
1923
1993
  import { resolve as resolve6 } from "path";
1924
1994
 
1925
1995
  // src/recipes/convert-to-pnpm.ts
1926
- import { rm as rm3, stat as stat2 } from "fs/promises";
1927
- import { join as join14 } from "path";
1996
+ import { rm as rm3, stat as stat3 } from "fs/promises";
1997
+ import { join as join15 } from "path";
1928
1998
 
1929
1999
  // src/recipes/convert-to-pnpm/script-rewrites.ts
1930
2000
  function rewriteScriptForPnpm(script) {
@@ -1946,25 +2016,22 @@ function rewriteScriptsForPnpm(scripts) {
1946
2016
 
1947
2017
  // src/recipes/convert-to-pnpm.ts
1948
2018
  var DEFAULT_PNPM_VERSION = "10.33.1";
1949
- async function exists(path) {
2019
+ async function exists2(path) {
1950
2020
  try {
1951
- await stat2(path);
2021
+ await stat3(path);
1952
2022
  return true;
1953
2023
  } catch {
1954
2024
  return false;
1955
2025
  }
1956
2026
  }
1957
- function siteLabel9(site) {
1958
- return site.name ?? site.path;
1959
- }
1960
2027
  async function convertToPnpm(site, opts = {}) {
1961
- const label = siteLabel9(site);
2028
+ const label = siteLabel(site);
1962
2029
  const spawn2 = opts.spawn ?? defaultSpawn;
1963
2030
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
1964
- const pnpmLockPath = join14(site.path, "pnpm-lock.yaml");
1965
- const npmLockPath = join14(site.path, "package-lock.json");
1966
- const yarnLockPath = join14(site.path, "yarn.lock");
1967
- if (await exists(pnpmLockPath)) {
2031
+ const pnpmLockPath = join15(site.path, "pnpm-lock.yaml");
2032
+ const npmLockPath = join15(site.path, "package-lock.json");
2033
+ const yarnLockPath = join15(site.path, "yarn.lock");
2034
+ if (await exists2(pnpmLockPath)) {
1968
2035
  return {
1969
2036
  recipe: "convert-to-pnpm",
1970
2037
  site: label,
@@ -1973,8 +2040,8 @@ async function convertToPnpm(site, opts = {}) {
1973
2040
  notes: "site already has pnpm-lock.yaml"
1974
2041
  };
1975
2042
  }
1976
- const hasNpmLock = await exists(npmLockPath);
1977
- const hasYarnLock = await exists(yarnLockPath);
2043
+ const hasNpmLock = await exists2(npmLockPath);
2044
+ const hasYarnLock = await exists2(yarnLockPath);
1978
2045
  if (!hasNpmLock && !hasYarnLock) {
1979
2046
  return {
1980
2047
  recipe: "convert-to-pnpm",
@@ -1995,7 +2062,7 @@ async function convertToPnpm(site, opts = {}) {
1995
2062
  const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
1996
2063
  const lockSha = await commit(site.path, `chore(pnpm): remove ${sourceLock}`);
1997
2064
  if (lockSha) shas.push(lockSha);
1998
- const pkgPath = join14(site.path, "package.json");
2065
+ const pkgPath = join15(site.path, "package.json");
1999
2066
  const pkg = await readPackageJson(pkgPath);
2000
2067
  const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
2001
2068
  if (pkg.scripts && typeof pkg.scripts === "object") {
@@ -2009,7 +2076,7 @@ async function convertToPnpm(site, opts = {}) {
2009
2076
  await writePackageJson(pkgPath, next);
2010
2077
  const pkgSha = await commit(site.path, "chore(pnpm): pin packageManager + rewrite npm scripts");
2011
2078
  if (pkgSha) shas.push(pkgSha);
2012
- await rm3(join14(site.path, "node_modules"), { recursive: true, force: true });
2079
+ await rm3(join15(site.path, "node_modules"), { recursive: true, force: true });
2013
2080
  const installResult = await spawn2("pnpm", ["install"], {
2014
2081
  cwd: site.path,
2015
2082
  streaming: true
@@ -2063,17 +2130,17 @@ async function runConvertToPnpmCommand(site, opts) {
2063
2130
  import { resolve as resolve7 } from "path";
2064
2131
 
2065
2132
  // src/recipes/onboard.ts
2066
- import { stat as stat3 } from "fs/promises";
2067
- import { join as join16 } from "path";
2133
+ import { stat as stat4 } from "fs/promises";
2134
+ import { join as join17 } from "path";
2068
2135
 
2069
2136
  // src/util/self-version.ts
2070
2137
  import { readFileSync } from "fs";
2071
2138
  import { fileURLToPath } from "url";
2072
- import { dirname, join as join15 } from "path";
2139
+ import { dirname, join as join16 } from "path";
2073
2140
  function selfPackageVersion(callerImportMetaUrl) {
2074
2141
  try {
2075
2142
  const here2 = dirname(fileURLToPath(callerImportMetaUrl));
2076
- const raw = readFileSync(join15(here2, "..", "..", "package.json"), "utf-8");
2143
+ const raw = readFileSync(join16(here2, "..", "..", "package.json"), "utf-8");
2077
2144
  const pkg = JSON.parse(raw);
2078
2145
  return pkg.version ?? "0.0.0";
2079
2146
  } catch {
@@ -2086,33 +2153,41 @@ function selfCaretRange(callerImportMetaUrl) {
2086
2153
 
2087
2154
  // src/recipes/onboard.ts
2088
2155
  var PACKAGE_NAME = "@reddoorla/maintenance";
2089
- var AUDIT_DEPS = {
2090
- lighthouse: [{ name: "@lhci/cli", version: "^0.15.1" }],
2091
- a11y: [
2092
- { name: "@playwright/test", version: "^1.59.1" },
2093
- { name: "@axe-core/playwright", version: "^4.11.3" }
2094
- ]
2156
+ var AUDIT_DEP_NAMES = {
2157
+ lighthouse: ["@lhci/cli"],
2158
+ a11y: ["@playwright/test", "@axe-core/playwright"]
2095
2159
  };
2096
- async function exists2(path) {
2160
+ var AUDIT_DEPS = Object.fromEntries(
2161
+ Object.entries(AUDIT_DEP_NAMES).map(([audit, names]) => [
2162
+ audit,
2163
+ names.map((name) => {
2164
+ const version2 = baselineVersions[name];
2165
+ if (!version2) {
2166
+ throw new Error(
2167
+ `baseline-versions is missing audit dep "${name}" \u2014 add it to src/configs/baseline-versions.ts`
2168
+ );
2169
+ }
2170
+ return { name, version: version2 };
2171
+ })
2172
+ ])
2173
+ );
2174
+ async function exists3(path) {
2097
2175
  try {
2098
- await stat3(path);
2176
+ await stat4(path);
2099
2177
  return true;
2100
2178
  } catch {
2101
2179
  return false;
2102
2180
  }
2103
2181
  }
2104
- function siteLabel10(site) {
2105
- return site.name ?? site.path;
2106
- }
2107
2182
  function isDeclared(pkg, name) {
2108
2183
  return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
2109
2184
  }
2110
2185
  async function onboard(site, opts = {}) {
2111
- const label = siteLabel10(site);
2186
+ const label = siteLabel(site);
2112
2187
  const spawn2 = opts.spawn ?? defaultSpawn;
2113
2188
  const audits = opts.audits ?? ["lighthouse", "a11y"];
2114
2189
  const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);
2115
- if (!await exists2(join16(site.path, "pnpm-lock.yaml"))) {
2190
+ if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
2116
2191
  return {
2117
2192
  recipe: "onboard",
2118
2193
  site: label,
@@ -2121,7 +2196,7 @@ async function onboard(site, opts = {}) {
2121
2196
  notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
2122
2197
  };
2123
2198
  }
2124
- const pkgPath = join16(site.path, "package.json");
2199
+ const pkgPath = join17(site.path, "package.json");
2125
2200
  const pkg = await readPackageJson(pkgPath);
2126
2201
  const toAdd = [];
2127
2202
  if (!isDeclared(pkg, PACKAGE_NAME)) {
@@ -2225,12 +2300,9 @@ import { resolve as resolve8 } from "path";
2225
2300
 
2226
2301
  // src/recipes/svelte-codemods.ts
2227
2302
  import { writeFile as writeFile8 } from "fs/promises";
2228
- import { join as join17 } from "path";
2229
- function siteLabel11(site) {
2230
- return site.name ?? site.path;
2231
- }
2303
+ import { join as join18 } from "path";
2232
2304
  async function svelteCodemods(site) {
2233
- const label = siteLabel11(site);
2305
+ const label = siteLabel(site);
2234
2306
  const changes = await planGotchaCodemods(site.path);
2235
2307
  if (changes.length === 0) {
2236
2308
  return {
@@ -2247,7 +2319,7 @@ async function svelteCodemods(site) {
2247
2319
  const branch = branchName("svelte-codemods");
2248
2320
  await createBranch(site.path, branch);
2249
2321
  for (const c of changes) {
2250
- await writeFile8(join17(site.path, c.rel), c.after, "utf-8");
2322
+ await writeFile8(join18(site.path, c.rel), c.after, "utf-8");
2251
2323
  }
2252
2324
  const sha = await commit(
2253
2325
  site.path,
@@ -2289,10 +2361,10 @@ async function runSvelteCodemodsCommand(site, opts) {
2289
2361
 
2290
2362
  // src/cli/version.ts
2291
2363
  import { readFileSync as readFileSync2 } from "fs";
2292
- import { join as join18 } from "path";
2364
+ import { join as join19 } from "path";
2293
2365
  function resolvePackageVersion(fromDir) {
2294
2366
  try {
2295
- const raw = readFileSync2(join18(fromDir, "..", "..", "package.json"), "utf-8");
2367
+ const raw = readFileSync2(join19(fromDir, "..", "..", "package.json"), "utf-8");
2296
2368
  const pkg = JSON.parse(raw);
2297
2369
  return pkg.version ?? "unknown";
2298
2370
  } catch {
@@ -2318,6 +2390,17 @@ var RECIPE_DESCRIPTIONS = {
2318
2390
  "convert-to-pnpm": "Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).",
2319
2391
  onboard: "Install @reddoorla/maintenance + audit deps on a site (preferred first step)."
2320
2392
  };
2393
+ async function runOrExit(fn, opts) {
2394
+ try {
2395
+ const { output, code } = await fn();
2396
+ console.log(output);
2397
+ process.exit(code);
2398
+ } catch (err) {
2399
+ const e = err;
2400
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2401
+ process.exit(e.exitCode ?? 1);
2402
+ }
2403
+ }
2321
2404
  var cli = cac("reddoor-maint");
2322
2405
  cli.option("--cwd <path>", "Override working directory (default: process.cwd())");
2323
2406
  cli.option("--verbose", "Verbose output (full stack on errors)");
@@ -2332,101 +2415,31 @@ cli.command("list-recipes", "Print the available recipes.").action(() => {
2332
2415
  }
2333
2416
  });
2334
2417
  cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js); aggregates across sites").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2335
- async (site, opts) => {
2336
- try {
2337
- const { output, code } = await runAuditCommand(site, opts);
2338
- console.log(output);
2339
- process.exit(code);
2340
- } catch (err) {
2341
- const e = err;
2342
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2343
- process.exit(e.exitCode ?? 1);
2344
- }
2345
- }
2418
+ async (site, opts) => runOrExit(() => runAuditCommand(site, opts), opts)
2346
2419
  );
2347
2420
  cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2348
- async (site, opts) => {
2349
- try {
2350
- const { output, code } = await runSyncConfigsCommand(site, opts);
2351
- console.log(output);
2352
- process.exit(code);
2353
- } catch (err) {
2354
- const e = err;
2355
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2356
- process.exit(e.exitCode ?? 1);
2357
- }
2358
- }
2421
+ async (site, opts) => runOrExit(() => runSyncConfigsCommand(site, opts), opts)
2359
2422
  );
2360
2423
  cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2361
- async (site, opts) => {
2362
- try {
2363
- const { output, code } = await runBumpDepsCommand(site, opts);
2364
- console.log(output);
2365
- process.exit(code);
2366
- } catch (err) {
2367
- const e = err;
2368
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2369
- process.exit(e.exitCode ?? 1);
2370
- }
2371
- }
2424
+ async (site, opts) => runOrExit(() => runBumpDepsCommand(site, opts), opts)
2372
2425
  );
2373
2426
  cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2374
- async (upgrade, site, opts) => {
2375
- try {
2376
- const { output, code } = await runUpgradeCommand(upgrade, site, opts);
2377
- console.log(output);
2378
- process.exit(code);
2379
- } catch (err) {
2380
- const e = err;
2381
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2382
- process.exit(e.exitCode ?? 1);
2383
- }
2384
- }
2427
+ async (upgrade, site, opts) => runOrExit(() => runUpgradeCommand(upgrade, site, opts), opts)
2385
2428
  );
2386
2429
  cli.command(
2387
2430
  "convert-to-pnpm [site]",
2388
2431
  "Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts)."
2389
2432
  ).option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2390
- async (site, opts) => {
2391
- try {
2392
- const { output, code } = await runConvertToPnpmCommand(site, opts);
2393
- console.log(output);
2394
- process.exit(code);
2395
- } catch (err) {
2396
- const e = err;
2397
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2398
- process.exit(e.exitCode ?? 1);
2399
- }
2400
- }
2433
+ async (site, opts) => runOrExit(() => runConvertToPnpmCommand(site, opts), opts)
2401
2434
  );
2402
2435
  cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2403
- async (site, opts) => {
2404
- try {
2405
- const { output, code } = await runSvelteCodemodsCommand(site, opts);
2406
- console.log(output);
2407
- process.exit(code);
2408
- } catch (err) {
2409
- const e = err;
2410
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2411
- process.exit(e.exitCode ?? 1);
2412
- }
2413
- }
2436
+ async (site, opts) => runOrExit(() => runSvelteCodemodsCommand(site, opts), opts)
2414
2437
  );
2415
2438
  cli.command(
2416
2439
  "onboard [site]",
2417
2440
  "Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm)."
2418
2441
  ).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2419
- async (site, opts) => {
2420
- try {
2421
- const { output, code } = await runOnboardCommand(site, opts);
2422
- console.log(output);
2423
- process.exit(code);
2424
- } catch (err) {
2425
- const e = err;
2426
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2427
- process.exit(e.exitCode ?? 1);
2428
- }
2429
- }
2442
+ async (site, opts) => runOrExit(() => runOnboardCommand(site, opts), opts)
2430
2443
  );
2431
2444
  cli.help();
2432
2445
  cli.version(version);