@reddoorla/maintenance 0.1.1 → 0.1.3

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
  }
@@ -204,7 +207,7 @@ function classify(v) {
204
207
  }
205
208
  function normalizeSeverity(s) {
206
209
  if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
207
- return "moderate";
210
+ return "low";
208
211
  }
209
212
  function extractAdvisoriesFromPnpm(parsed) {
210
213
  const out = [];
@@ -220,83 +223,101 @@ function extractAdvisoriesFromPnpm(parsed) {
220
223
  }
221
224
  return out;
222
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
+ }
223
243
  function extractAdvisoriesFromNpm(parsed) {
224
- const out = [];
225
- for (const [name, v] of Object.entries(parsed.vulnerabilities ?? {})) {
244
+ const vulnerabilities = parsed.vulnerabilities ?? {};
245
+ const roots = /* @__PURE__ */ new Map();
246
+ for (const [name, v] of Object.entries(vulnerabilities)) {
226
247
  if (!v) continue;
227
- let title = name;
228
- let url;
229
- if (Array.isArray(v.via)) {
230
- const detailed = v.via.find(
231
- (entry) => typeof entry === "object" && entry !== null
232
- );
233
- if (detailed) {
234
- title = detailed.title ?? name;
235
- url = detailed.url;
236
- }
237
- }
238
- out.push({
239
- module: v.name ?? name,
240
- severity: normalizeSeverity(v.severity),
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,
241
257
  title,
242
258
  ...url ? { url } : {}
243
259
  });
244
260
  }
245
- return out;
261
+ return [...roots.values()];
246
262
  }
247
- async function tryRun(spawn2, cmd, args, cwd) {
263
+ async function runAuditTool(spawn2, cmd, args, cwd) {
264
+ let raw;
248
265
  try {
249
- return await spawn2(cmd, args, { cwd });
266
+ raw = await spawn2(cmd, args, { cwd });
250
267
  } catch (err) {
251
268
  const e = err;
252
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
253
- throw err;
254
- }
255
- }
256
- async function securityAudit(ctx) {
257
- const spawn2 = ctx.spawn ?? defaultSpawn;
258
- const site = ctx.site;
259
- const label = siteLabel3(site);
260
- let used = "pnpm audit";
261
- let raw = await tryRun(
262
- spawn2,
263
- "pnpm",
264
- ["audit", "--json", "--prod"],
265
- site.path
266
- );
267
- if ("missing" in raw) {
268
- used = "npm audit";
269
- raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
270
- }
271
- if ("missing" in raw) {
272
- return {
273
- audit: "security",
274
- site: label,
275
- status: "skip",
276
- summary: "neither pnpm nor npm is available on PATH"
277
- };
269
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { kind: "missing" };
270
+ return { kind: "error", reason: `spawn failed: ${String(err).slice(0, 200)}` };
278
271
  }
279
272
  if (raw.code !== 0 && raw.code !== 1) {
280
273
  return {
281
- audit: "security",
282
- site: label,
283
- status: "skip",
284
- summary: `${used} exited with code ${raw.code}`,
285
- details: { stderr: raw.stderr }
274
+ kind: "error",
275
+ reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : ""}`
286
276
  };
287
277
  }
288
278
  let parsed;
289
279
  try {
290
- parsed = JSON.parse(raw.stdout);
280
+ parsed = JSON.parse(raw.stdout || "{}");
291
281
  } catch (err) {
292
- return {
293
- audit: "security",
294
- site: label,
295
- status: "skip",
296
- summary: `${used} produced unparseable JSON`,
297
- details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
298
- };
282
+ return { kind: "error", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };
299
283
  }
284
+ const errEnvelope = parsed.error;
285
+ if (errEnvelope && typeof errEnvelope === "object") {
286
+ return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
287
+ }
288
+ if (!parsed.metadata?.vulnerabilities) {
289
+ return { kind: "error", reason: "no metadata.vulnerabilities in output" };
290
+ }
291
+ return { kind: "ok", parsed };
292
+ }
293
+ async function securityAudit(ctx) {
294
+ const spawn2 = ctx.spawn ?? defaultSpawn;
295
+ const site = ctx.site;
296
+ const label = siteLabel3(site);
297
+ let used = "pnpm audit";
298
+ let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
299
+ if (result.kind !== "ok") {
300
+ const pnpmReason = result.kind === "missing" ? "not installed" : result.reason;
301
+ const npmResult = await runAuditTool(
302
+ spawn2,
303
+ "npm",
304
+ ["audit", "--json", "--omit=dev"],
305
+ site.path
306
+ );
307
+ if (npmResult.kind === "ok") {
308
+ result = npmResult;
309
+ used = "npm audit";
310
+ } else {
311
+ const npmReason = npmResult.kind === "missing" ? "not installed" : npmResult.reason;
312
+ return {
313
+ audit: "security",
314
+ site: label,
315
+ status: "skip",
316
+ summary: `cannot run audit \u2014 pnpm: ${pnpmReason}; npm: ${npmReason}`
317
+ };
318
+ }
319
+ }
320
+ const parsed = result.parsed;
300
321
  const counts = {
301
322
  low: parsed.metadata?.vulnerabilities?.low ?? 0,
302
323
  moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
@@ -507,6 +528,10 @@ import { dirname } from "node:path";
507
528
  const pages = ${JSON.stringify(a11yRoutes)};
508
529
  const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
509
530
 
531
+ // Playwright's default per-test timeout is 30s. We loop through every
532
+ // configured route in a single test, so the budget needs to scale.
533
+ test.setTimeout(5 * 60_000);
534
+
510
535
  test("a11y across configured routes", async ({ page }) => {
511
536
  const violations = [];
512
537
  for (const { path, name } of pages) {
@@ -781,6 +806,7 @@ async function bumpDeps(site, opts = {}) {
781
806
  const label = siteLabel7(site);
782
807
  const group = opts.group ?? "minor";
783
808
  const spawn2 = opts.spawn ?? defaultSpawn;
809
+ await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
784
810
  const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
785
811
  cwd: site.path
786
812
  });
@@ -805,7 +831,10 @@ async function bumpDeps(site, opts = {}) {
805
831
  }
806
832
  const branch = branchName("bump-deps");
807
833
  await createBranch(site.path, branch);
808
- await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], { cwd: site.path });
834
+ await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
835
+ cwd: site.path,
836
+ streaming: true
837
+ });
809
838
  const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
810
839
  const shas = sha ? [sha] : [];
811
840
  return {
@@ -830,7 +859,8 @@ async function writePackageJson(path, pkg) {
830
859
  const content = JSON.stringify(pkg, null, 2) + "\n";
831
860
  await writeFile4(path, content, "utf-8");
832
861
  }
833
- function bumpDep(pkg, name, version) {
862
+ function bumpDep(pkg, name, version, opts = {}) {
863
+ const mode = opts.mode ?? "ensure";
834
864
  const next = {
835
865
  ...pkg
836
866
  };
@@ -850,6 +880,7 @@ function bumpDep(pkg, name, version) {
850
880
  next.devDependencies[name] = version;
851
881
  return next;
852
882
  }
883
+ if (mode === "bump-only") return pkg;
853
884
  next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
854
885
  return next;
855
886
  }
@@ -872,7 +903,7 @@ async function bumpToSvelte5Versions(cwd) {
872
903
  const pkg = await readPackageJson(pkgPath);
873
904
  let next = pkg;
874
905
  for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
875
- next = bumpDep(next, name, version);
906
+ next = bumpDep(next, name, version, { mode: "bump-only" });
876
907
  }
877
908
  if (next === pkg) return false;
878
909
  await writePackageJson(pkgPath, next);
@@ -882,6 +913,45 @@ async function bumpToSvelte5Versions(cwd) {
882
913
  // src/recipes/svelte-5/step-svelte-config.ts
883
914
  import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
884
915
  import { join as join7 } from "path";
916
+ var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
917
+ var IMPORT_FROM_VITE_PLUGIN = new RegExp(
918
+ String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
919
+ "m"
920
+ );
921
+ function dropVitePreprocessImport(source) {
922
+ return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names) => {
923
+ const remaining = names.split(",").map((n) => n.trim()).filter((n) => n.length > 0 && n !== "vitePreprocess");
924
+ if (remaining.length === 0) return "";
925
+ return `import { ${remaining.join(", ")} } from "${VITE_PLUGIN_PKG}";
926
+ `;
927
+ });
928
+ }
929
+ function findMatchingParen(source, openIdx) {
930
+ if (source[openIdx] !== "(") return -1;
931
+ let depth = 0;
932
+ for (let i = openIdx; i < source.length; i++) {
933
+ const ch = source[i];
934
+ if (ch === "(") depth++;
935
+ else if (ch === ")") {
936
+ depth--;
937
+ if (depth === 0) return i;
938
+ }
939
+ }
940
+ return -1;
941
+ }
942
+ function dropPreprocessKey(source) {
943
+ const startRe = /^(\s*)preprocess:\s*vitePreprocess\(/m;
944
+ const m = startRe.exec(source);
945
+ if (!m) return source;
946
+ const indent = m[1] ?? "";
947
+ const parenOpenAbs = m.index + m[0].length - 1;
948
+ const parenCloseAbs = findMatchingParen(source, parenOpenAbs);
949
+ if (parenCloseAbs < 0) return source;
950
+ let tailIdx = parenCloseAbs + 1;
951
+ while (tailIdx < source.length && /[ \t,]/.test(source[tailIdx] ?? "")) tailIdx++;
952
+ if (source[tailIdx] === "\n") tailIdx++;
953
+ return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
954
+ }
885
955
  async function migrateSvelteConfig(cwd) {
886
956
  const path = join7(cwd, "svelte.config.js");
887
957
  let src;
@@ -891,11 +961,8 @@ async function migrateSvelteConfig(cwd) {
891
961
  return false;
892
962
  }
893
963
  let next = src;
894
- next = next.replace(
895
- /^import\s+\{\s*vitePreprocess\s*\}\s+from\s+["']@sveltejs\/vite-plugin-svelte["'];\n/m,
896
- ""
897
- );
898
- next = next.replace(/^\s*preprocess:\s*vitePreprocess\(\)\s*,?\s*\n/m, "");
964
+ next = dropPreprocessKey(next);
965
+ next = dropVitePreprocessImport(next);
899
966
  if (next === src) return false;
900
967
  await writeFile5(path, next, "utf-8");
901
968
  return true;
@@ -1005,10 +1072,32 @@ function exportLetToProps(source) {
1005
1072
  }
1006
1073
 
1007
1074
  // src/recipes/svelte-5/codemods/dollar-restprops.ts
1075
+ function removeInterfaceBlock(source) {
1076
+ const re = /^\s*interface\s+\$\$Props\s*\{/m;
1077
+ let out = source;
1078
+ while (true) {
1079
+ const match = re.exec(out);
1080
+ if (!match) return out;
1081
+ const openBraceIdx = match.index + match[0].length - 1;
1082
+ let depth = 1;
1083
+ let i = openBraceIdx + 1;
1084
+ while (i < out.length && depth > 0) {
1085
+ const ch = out[i];
1086
+ if (ch === "{") depth++;
1087
+ else if (ch === "}") depth--;
1088
+ i++;
1089
+ }
1090
+ if (depth !== 0) return out;
1091
+ let endIdx = i;
1092
+ while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
1093
+ if (out[endIdx] === "\n") endIdx++;
1094
+ out = out.slice(0, match.index) + out.slice(endIdx);
1095
+ }
1096
+ }
1008
1097
  function removeDollarRestProps(source) {
1009
1098
  let next = source;
1010
1099
  next = next.replace(/\$\$restProps/g, "rest");
1011
- next = next.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
1100
+ next = removeInterfaceBlock(next);
1012
1101
  return next;
1013
1102
  }
1014
1103
 
@@ -1169,6 +1258,7 @@ function localPath(path, opts = {}) {
1169
1258
 
1170
1259
  // src/inventory/json.ts
1171
1260
  import { readFile as readFile9 } from "fs/promises";
1261
+ import { isAbsolute } from "path";
1172
1262
  function validate(raw) {
1173
1263
  if (!Array.isArray(raw)) {
1174
1264
  throw new Error("inventory JSON must be an array of sites");
@@ -1181,6 +1271,11 @@ function validate(raw) {
1181
1271
  if (typeof e.path !== "string" || e.path.length === 0) {
1182
1272
  throw new Error(`inventory entry ${i} is missing required field: path`);
1183
1273
  }
1274
+ if (!isAbsolute(e.path)) {
1275
+ throw new Error(
1276
+ `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.`
1277
+ );
1278
+ }
1184
1279
  const site = { path: e.path };
1185
1280
  if (typeof e.name === "string") site.name = e.name;
1186
1281
  if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;