@reddoorla/maintenance 0.1.0
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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +1408 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +13 -0
- package/dist/cli/commands/audit.js +666 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/configs/eslint.d.ts +8 -0
- package/dist/configs/eslint.js +67 -0
- package/dist/configs/eslint.js.map +1 -0
- package/dist/configs/lighthouse.d.ts +36 -0
- package/dist/configs/lighthouse.js +33 -0
- package/dist/configs/lighthouse.js.map +1 -0
- package/dist/configs/playwright-a11y.d.ts +10 -0
- package/dist/configs/playwright-a11y.js +37 -0
- package/dist/configs/playwright-a11y.js.map +1 -0
- package/dist/configs/prettier.d.ts +5 -0
- package/dist/configs/prettier.js +14 -0
- package/dist/configs/prettier.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +1098 -0
- package/dist/index.js.map +1 -0
- package/dist/recipes/sync-configs.d.ts +1 -0
- package/dist/recipes/sync-configs.js +138 -0
- package/dist/recipes/sync-configs.js.map +1 -0
- package/dist/sync-configs-Z5a0iaDd.d.ts +31 -0
- package/dist/util/git.d.ts +12 -0
- package/dist/util/git.js +43 -0
- package/dist/util/git.js.map +1 -0
- package/dist/util/pkg.d.ts +12 -0
- package/dist/util/pkg.js +39 -0
- package/dist/util/pkg.js.map +1 -0
- package/package.json +81 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
// src/audits/util/spawn.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve, reject) => {
|
|
4
|
+
const child = spawn(cmd, [...args], {
|
|
5
|
+
cwd: opts.cwd,
|
|
6
|
+
env: opts.env ?? process.env,
|
|
7
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8
|
+
});
|
|
9
|
+
let stdout = "";
|
|
10
|
+
let stderr = "";
|
|
11
|
+
child.stdout.on("data", (chunk) => stdout += String(chunk));
|
|
12
|
+
child.stderr.on("data", (chunk) => stderr += String(chunk));
|
|
13
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
14
|
+
child.kill("SIGTERM");
|
|
15
|
+
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
16
|
+
}, opts.timeoutMs) : void 0;
|
|
17
|
+
child.on("error", (err) => {
|
|
18
|
+
if (timer) clearTimeout(timer);
|
|
19
|
+
reject(err);
|
|
20
|
+
});
|
|
21
|
+
child.on("close", (code) => {
|
|
22
|
+
if (timer) clearTimeout(timer);
|
|
23
|
+
resolve({ code: code ?? -1, stdout, stderr });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// src/audits/deps.ts
|
|
28
|
+
import { readFile } from "fs/promises";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
|
|
31
|
+
// src/configs/baseline-versions.ts
|
|
32
|
+
var baselineVersions = {
|
|
33
|
+
// SvelteKit core
|
|
34
|
+
svelte: "^5.55.5",
|
|
35
|
+
"@sveltejs/kit": "^2.59.0",
|
|
36
|
+
"@sveltejs/adapter-netlify": "^6.0.4",
|
|
37
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
38
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
39
|
+
"svelte-check": "^4.4.7",
|
|
40
|
+
// Build tooling
|
|
41
|
+
vite: "^8.0.10",
|
|
42
|
+
vitest: "^4.1.1",
|
|
43
|
+
typescript: "^6.0.3",
|
|
44
|
+
// Tailwind 4
|
|
45
|
+
tailwindcss: "^4.0.14",
|
|
46
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
47
|
+
// Prismic
|
|
48
|
+
"@prismicio/client": "^7.3.1",
|
|
49
|
+
"@prismicio/svelte": "^2.0.0",
|
|
50
|
+
"@slicemachine/adapter-sveltekit": "^0.3.36",
|
|
51
|
+
"slice-machine-ui": "^2.11.1",
|
|
52
|
+
// Test tooling
|
|
53
|
+
"@playwright/test": "^1.59.1",
|
|
54
|
+
"@axe-core/playwright": "^4.11.3",
|
|
55
|
+
"@lhci/cli": "^0.15.1",
|
|
56
|
+
// Lint
|
|
57
|
+
eslint: "^10.3.0",
|
|
58
|
+
"eslint-plugin-svelte": "^3.1.0",
|
|
59
|
+
"eslint-config-prettier": "^10.1.1",
|
|
60
|
+
prettier: "^3.1.1",
|
|
61
|
+
"prettier-plugin-svelte": "^3.2.6",
|
|
62
|
+
"typescript-eslint": "^8.59.1",
|
|
63
|
+
"@eslint/js": "^10.0.1",
|
|
64
|
+
globals: "^17.6.0",
|
|
65
|
+
// Misc
|
|
66
|
+
"@lucide/svelte": "^1.14.0",
|
|
67
|
+
"@zerodevx/svelte-img": "^2.1.2"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/audits/deps.ts
|
|
71
|
+
function siteLabel(site) {
|
|
72
|
+
return site.name ?? site.path;
|
|
73
|
+
}
|
|
74
|
+
function stripCaret(range) {
|
|
75
|
+
return range.replace(/^[\^~]/, "");
|
|
76
|
+
}
|
|
77
|
+
function parseSemver(v) {
|
|
78
|
+
const cleaned = stripCaret(v).split("-")[0] ?? "0.0.0";
|
|
79
|
+
const parts = cleaned.split(".").map((n) => Number.parseInt(n, 10));
|
|
80
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
81
|
+
}
|
|
82
|
+
function compareSemver(actual, baseline) {
|
|
83
|
+
const [aMajor, aMinor, aPatch] = parseSemver(actual);
|
|
84
|
+
const [bMajor, bMinor, bPatch] = parseSemver(baseline);
|
|
85
|
+
if (aMajor > bMajor) return "newer";
|
|
86
|
+
if (aMajor < bMajor) return "major";
|
|
87
|
+
if (aMinor > bMinor) return "newer";
|
|
88
|
+
if (aMinor < bMinor) return "minor";
|
|
89
|
+
if (aPatch > bPatch) return "newer";
|
|
90
|
+
if (aPatch < bPatch) return "patch";
|
|
91
|
+
return "same";
|
|
92
|
+
}
|
|
93
|
+
async function depsAudit(ctx) {
|
|
94
|
+
const pkgPath = join(ctx.site.path, "package.json");
|
|
95
|
+
let pkgRaw;
|
|
96
|
+
try {
|
|
97
|
+
pkgRaw = await readFile(pkgPath, "utf-8");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
audit: "deps",
|
|
101
|
+
site: siteLabel(ctx.site),
|
|
102
|
+
status: "skip",
|
|
103
|
+
summary: `no package.json at ${pkgPath}`,
|
|
104
|
+
details: { error: String(err) }
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const pkg = JSON.parse(pkgRaw);
|
|
108
|
+
const installed = {
|
|
109
|
+
...pkg.dependencies ?? {},
|
|
110
|
+
...pkg.devDependencies ?? {}
|
|
111
|
+
};
|
|
112
|
+
const details = [];
|
|
113
|
+
for (const [name, baseline] of Object.entries(baselineVersions)) {
|
|
114
|
+
const actual = installed[name];
|
|
115
|
+
if (!actual) continue;
|
|
116
|
+
details.push({
|
|
117
|
+
pkg: name,
|
|
118
|
+
baseline,
|
|
119
|
+
actual,
|
|
120
|
+
drift: compareSemver(actual, baseline)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const anyMajor = details.some((d) => d.drift === "major");
|
|
124
|
+
const anyMinor = details.some((d) => d.drift === "minor");
|
|
125
|
+
const anyNewer = details.some((d) => d.drift === "newer");
|
|
126
|
+
const status = anyMajor ? "fail" : anyMinor || anyNewer ? "warn" : "pass";
|
|
127
|
+
const summary = status === "pass" ? `all ${details.length} tracked deps in line with baseline` : status === "warn" ? `${details.filter((d) => d.drift !== "same").length} of ${details.length} tracked deps drifted` : `${details.filter((d) => d.drift === "major").length} deps lagging by a major version`;
|
|
128
|
+
return {
|
|
129
|
+
audit: "deps",
|
|
130
|
+
site: siteLabel(ctx.site),
|
|
131
|
+
status,
|
|
132
|
+
summary,
|
|
133
|
+
details
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/audits/lint.ts
|
|
138
|
+
import { existsSync } from "fs";
|
|
139
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
140
|
+
import { join as join2, relative } from "path";
|
|
141
|
+
import { ESLint } from "eslint";
|
|
142
|
+
import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
|
|
143
|
+
import { glob } from "tinyglobby";
|
|
144
|
+
var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
|
|
145
|
+
var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
|
|
146
|
+
function siteLabel2(site) {
|
|
147
|
+
return site.name ?? site.path;
|
|
148
|
+
}
|
|
149
|
+
async function listFiles(cwd) {
|
|
150
|
+
return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
|
|
151
|
+
}
|
|
152
|
+
async function lintAudit(ctx) {
|
|
153
|
+
const { site } = ctx;
|
|
154
|
+
const configPath = join2(site.path, "eslint.config.js");
|
|
155
|
+
if (!existsSync(configPath)) {
|
|
156
|
+
return {
|
|
157
|
+
audit: "lint",
|
|
158
|
+
site: siteLabel2(site),
|
|
159
|
+
status: "skip",
|
|
160
|
+
summary: "no eslint config at site root"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const eslint2 = new ESLint({
|
|
164
|
+
cwd: site.path,
|
|
165
|
+
overrideConfigFile: configPath,
|
|
166
|
+
errorOnUnmatchedPattern: false
|
|
167
|
+
});
|
|
168
|
+
const relFiles = await listFiles(site.path);
|
|
169
|
+
const filesToLint = relFiles.map((f) => join2(site.path, f));
|
|
170
|
+
const eslintResults = await eslint2.lintFiles(filesToLint);
|
|
171
|
+
const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
|
|
172
|
+
const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
|
|
173
|
+
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));
|
|
179
|
+
}
|
|
180
|
+
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`;
|
|
182
|
+
return {
|
|
183
|
+
audit: "lint",
|
|
184
|
+
site: siteLabel2(site),
|
|
185
|
+
status,
|
|
186
|
+
summary,
|
|
187
|
+
details: {
|
|
188
|
+
eslintErrors,
|
|
189
|
+
eslintWarnings,
|
|
190
|
+
prettierUnformatted,
|
|
191
|
+
files: filesToLint.length
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/audits/security.ts
|
|
197
|
+
function siteLabel3(site) {
|
|
198
|
+
return site.name ?? site.path;
|
|
199
|
+
}
|
|
200
|
+
function classify(v) {
|
|
201
|
+
if (v.critical > 0 || v.high > 0) return "fail";
|
|
202
|
+
if (v.moderate > 0 || v.low > 0) return "warn";
|
|
203
|
+
return "pass";
|
|
204
|
+
}
|
|
205
|
+
async function tryRun(spawn2, cmd, args, cwd) {
|
|
206
|
+
try {
|
|
207
|
+
return await spawn2(cmd, args, { cwd });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const e = err;
|
|
210
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function securityAudit(ctx) {
|
|
215
|
+
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
216
|
+
const site = ctx.site;
|
|
217
|
+
const label = siteLabel3(site);
|
|
218
|
+
let used = "pnpm audit";
|
|
219
|
+
let raw = await tryRun(
|
|
220
|
+
spawn2,
|
|
221
|
+
"pnpm",
|
|
222
|
+
["audit", "--json", "--prod"],
|
|
223
|
+
site.path
|
|
224
|
+
);
|
|
225
|
+
if ("missing" in raw) {
|
|
226
|
+
used = "npm audit";
|
|
227
|
+
raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
|
|
228
|
+
}
|
|
229
|
+
if ("missing" in raw) {
|
|
230
|
+
return {
|
|
231
|
+
audit: "security",
|
|
232
|
+
site: label,
|
|
233
|
+
status: "skip",
|
|
234
|
+
summary: "neither pnpm nor npm is available on PATH"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (raw.code !== 0 && raw.code !== 1) {
|
|
238
|
+
return {
|
|
239
|
+
audit: "security",
|
|
240
|
+
site: label,
|
|
241
|
+
status: "skip",
|
|
242
|
+
summary: `${used} exited with code ${raw.code}`,
|
|
243
|
+
details: { stderr: raw.stderr }
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
let parsed;
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(raw.stdout);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
audit: "security",
|
|
252
|
+
site: label,
|
|
253
|
+
status: "skip",
|
|
254
|
+
summary: `${used} produced unparseable JSON`,
|
|
255
|
+
details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const vuln = {
|
|
259
|
+
low: parsed.metadata?.vulnerabilities?.low ?? 0,
|
|
260
|
+
moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
|
|
261
|
+
high: parsed.metadata?.vulnerabilities?.high ?? 0,
|
|
262
|
+
critical: parsed.metadata?.vulnerabilities?.critical ?? 0
|
|
263
|
+
};
|
|
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)`;
|
|
267
|
+
return {
|
|
268
|
+
audit: "security",
|
|
269
|
+
site: label,
|
|
270
|
+
status,
|
|
271
|
+
summary,
|
|
272
|
+
details: vuln
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/audits/lighthouse.ts
|
|
277
|
+
import { writeFile, mkdtemp, rm } from "fs/promises";
|
|
278
|
+
import { tmpdir } from "os";
|
|
279
|
+
import { join as join3 } from "path";
|
|
280
|
+
|
|
281
|
+
// src/configs/lighthouse.ts
|
|
282
|
+
var lighthouseConfig = {
|
|
283
|
+
ci: {
|
|
284
|
+
collect: {
|
|
285
|
+
url: ["http://localhost:5173/dev/a11y-fixtures"],
|
|
286
|
+
startServerCommand: "pnpm vite:dev",
|
|
287
|
+
startServerReadyPattern: "ready in",
|
|
288
|
+
startServerReadyTimeout: 12e4,
|
|
289
|
+
numberOfRuns: 1,
|
|
290
|
+
settings: {
|
|
291
|
+
preset: "desktop",
|
|
292
|
+
skipAudits: ["uses-http2"]
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
assert: {
|
|
296
|
+
assertions: {
|
|
297
|
+
"categories:accessibility": ["error", { minScore: 0.95 }],
|
|
298
|
+
"categories:best-practices": ["error", { minScore: 0.9 }],
|
|
299
|
+
"categories:seo": ["error", { minScore: 0.9 }],
|
|
300
|
+
"categories:performance": ["warn", { minScore: 0.7 }]
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
upload: {
|
|
304
|
+
target: "temporary-public-storage"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/audits/lighthouse.ts
|
|
310
|
+
function siteLabel4(site) {
|
|
311
|
+
return site.name ?? site.path;
|
|
312
|
+
}
|
|
313
|
+
function isFakeShape(stdout) {
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(stdout);
|
|
316
|
+
if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
async function lighthouseAudit(ctx) {
|
|
323
|
+
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
324
|
+
const site = ctx.site;
|
|
325
|
+
const label = siteLabel4(site);
|
|
326
|
+
const dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
|
|
327
|
+
const configPath = join3(dir, "lighthouserc.json");
|
|
328
|
+
await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
|
|
329
|
+
let raw;
|
|
330
|
+
try {
|
|
331
|
+
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
332
|
+
cwd: site.path
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
await rm(dir, { recursive: true, force: true });
|
|
336
|
+
const e = err;
|
|
337
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
338
|
+
return {
|
|
339
|
+
audit: "lighthouse",
|
|
340
|
+
site: label,
|
|
341
|
+
status: "skip",
|
|
342
|
+
summary: "npx/@lhci/cli not available"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
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");
|
|
356
|
+
const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
|
|
357
|
+
const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
|
|
358
|
+
return {
|
|
359
|
+
audit: "lighthouse",
|
|
360
|
+
site: label,
|
|
361
|
+
status,
|
|
362
|
+
summary,
|
|
363
|
+
details: normalized
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/audits/a11y.ts
|
|
368
|
+
import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
369
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
370
|
+
import { join as join4 } from "path";
|
|
371
|
+
|
|
372
|
+
// src/configs/playwright-a11y.ts
|
|
373
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
374
|
+
var a11yRoutes = [
|
|
375
|
+
{ path: "/dev/a11y-fixtures", name: "a11y fixtures" },
|
|
376
|
+
{ path: "/dev/animate-in", name: "animate-in demo" }
|
|
377
|
+
];
|
|
378
|
+
var playwrightA11yConfig = defineConfig({
|
|
379
|
+
testDir: "tests",
|
|
380
|
+
testMatch: /.*\.spec\.ts$/,
|
|
381
|
+
fullyParallel: true,
|
|
382
|
+
forbidOnly: !!process.env.CI,
|
|
383
|
+
retries: process.env.CI ? 2 : 0,
|
|
384
|
+
reporter: process.env.CI ? "github" : "list",
|
|
385
|
+
use: {
|
|
386
|
+
baseURL: "http://localhost:5173",
|
|
387
|
+
trace: "on-first-retry"
|
|
388
|
+
},
|
|
389
|
+
projects: [
|
|
390
|
+
{
|
|
391
|
+
name: "chromium",
|
|
392
|
+
use: { ...devices["Desktop Chrome"] }
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
webServer: {
|
|
396
|
+
command: "pnpm vite:dev",
|
|
397
|
+
url: "http://localhost:5173/dev/a11y-fixtures",
|
|
398
|
+
reuseExistingServer: !process.env.CI,
|
|
399
|
+
timeout: 12e4
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// src/audits/a11y.ts
|
|
404
|
+
function siteLabel5(site) {
|
|
405
|
+
return site.name ?? site.path;
|
|
406
|
+
}
|
|
407
|
+
function isFakeShape2(stdout) {
|
|
408
|
+
try {
|
|
409
|
+
const parsed = JSON.parse(stdout);
|
|
410
|
+
if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function buildSpec() {
|
|
417
|
+
return `
|
|
418
|
+
import { test, expect } from "@playwright/test";
|
|
419
|
+
import AxeBuilder from "@axe-core/playwright";
|
|
420
|
+
const pages = ${JSON.stringify(a11yRoutes)};
|
|
421
|
+
for (const { path, name } of pages) {
|
|
422
|
+
test(\`\${name} has no axe violations\`, async ({ page }) => {
|
|
423
|
+
await page.goto(path);
|
|
424
|
+
const results = await new AxeBuilder({ page })
|
|
425
|
+
.withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
|
|
426
|
+
.analyze();
|
|
427
|
+
expect(results.violations).toEqual([]);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
async function a11yAudit(ctx) {
|
|
433
|
+
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
434
|
+
const site = ctx.site;
|
|
435
|
+
const label = siteLabel5(site);
|
|
436
|
+
const dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
|
|
437
|
+
const specPath = join4(dir, "a11y.spec.ts");
|
|
438
|
+
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
439
|
+
let raw;
|
|
440
|
+
try {
|
|
441
|
+
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=json", specPath], {
|
|
442
|
+
cwd: site.path
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
await rm2(dir, { recursive: true, force: true });
|
|
446
|
+
const e = err;
|
|
447
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
448
|
+
return {
|
|
449
|
+
audit: "a11y",
|
|
450
|
+
site: label,
|
|
451
|
+
status: "skip",
|
|
452
|
+
summary: "npx/playwright not available"
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
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;
|
|
465
|
+
const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
|
|
466
|
+
const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${normalized.totalViolations} violations`;
|
|
467
|
+
return {
|
|
468
|
+
audit: "a11y",
|
|
469
|
+
site: label,
|
|
470
|
+
status,
|
|
471
|
+
summary,
|
|
472
|
+
details: normalized
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/audits/index.ts
|
|
477
|
+
var REGISTRY = {
|
|
478
|
+
deps: depsAudit,
|
|
479
|
+
lint: lintAudit,
|
|
480
|
+
security: securityAudit,
|
|
481
|
+
lighthouse: lighthouseAudit,
|
|
482
|
+
a11y: a11yAudit
|
|
483
|
+
};
|
|
484
|
+
var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
|
|
485
|
+
var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
486
|
+
function timedSpawn(timeoutMs) {
|
|
487
|
+
return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
|
|
488
|
+
}
|
|
489
|
+
async function runAudits(site, which) {
|
|
490
|
+
const names = which ?? ALL_AUDIT_NAMES;
|
|
491
|
+
for (const n of names) {
|
|
492
|
+
if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
|
|
493
|
+
}
|
|
494
|
+
const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
|
|
495
|
+
const label = site.name ?? site.path;
|
|
496
|
+
return Promise.all(
|
|
497
|
+
names.map(
|
|
498
|
+
(n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
|
|
499
|
+
(err) => ({
|
|
500
|
+
audit: n,
|
|
501
|
+
site: label,
|
|
502
|
+
status: "fail",
|
|
503
|
+
summary: `${n}: unexpected error \u2014 ${String(err)}`
|
|
504
|
+
})
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
async function runAuditsAcross(sites, which) {
|
|
510
|
+
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
511
|
+
return all.flat();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/recipes/sync-configs.ts
|
|
515
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
516
|
+
import { join as join5 } from "path";
|
|
517
|
+
|
|
518
|
+
// src/recipes/sync-configs/templates.ts
|
|
519
|
+
var eslint = {
|
|
520
|
+
config: "eslint",
|
|
521
|
+
path: "eslint.config.js",
|
|
522
|
+
contents: `import { createEslintConfig } from "@reddoorla/maintenance/configs/eslint";
|
|
523
|
+
import svelteConfig from "./svelte.config.js";
|
|
524
|
+
|
|
525
|
+
export default createEslintConfig({ svelteConfig });
|
|
526
|
+
`
|
|
527
|
+
};
|
|
528
|
+
var prettier = {
|
|
529
|
+
config: "prettier",
|
|
530
|
+
path: ".prettierrc.json",
|
|
531
|
+
contents: `{
|
|
532
|
+
"trailingComma": "all",
|
|
533
|
+
"singleQuote": false,
|
|
534
|
+
"printWidth": 100,
|
|
535
|
+
"plugins": ["prettier-plugin-svelte"]
|
|
536
|
+
}
|
|
537
|
+
`
|
|
538
|
+
};
|
|
539
|
+
var lighthouse = {
|
|
540
|
+
config: "lighthouse",
|
|
541
|
+
path: "lighthouserc.json",
|
|
542
|
+
contents: `${JSON.stringify(
|
|
543
|
+
{
|
|
544
|
+
$note: "Generated by @reddoorla/maintenance sync-configs; edit src/configs/lighthouse.ts in the package instead.",
|
|
545
|
+
extends: "@reddoorla/maintenance/configs/lighthouse"
|
|
546
|
+
},
|
|
547
|
+
null,
|
|
548
|
+
2
|
|
549
|
+
)}
|
|
550
|
+
`
|
|
551
|
+
};
|
|
552
|
+
var playwrightA11y = {
|
|
553
|
+
config: "playwright-a11y",
|
|
554
|
+
path: "playwright.config.ts",
|
|
555
|
+
contents: `export { default } from "@reddoorla/maintenance/configs/playwright-a11y";
|
|
556
|
+
`
|
|
557
|
+
};
|
|
558
|
+
var ALL_TEMPLATES = [eslint, prettier, lighthouse, playwrightA11y];
|
|
559
|
+
function templatesByName(which) {
|
|
560
|
+
return ALL_TEMPLATES.filter((t) => which.includes(t.config));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/util/git.ts
|
|
564
|
+
import { execFile } from "child_process";
|
|
565
|
+
import { promisify } from "util";
|
|
566
|
+
var exec = promisify(execFile);
|
|
567
|
+
async function git(cwd, args) {
|
|
568
|
+
return exec("git", args, { cwd, env: process.env });
|
|
569
|
+
}
|
|
570
|
+
function branchName(recipe, when = /* @__PURE__ */ new Date()) {
|
|
571
|
+
const iso = when.toISOString().replace(/[-:.]/g, "").replace(/Z$/, "Z");
|
|
572
|
+
const trimmed = iso.replace(/(\d{8}T\d{6})\d+(Z)$/, "$1$2");
|
|
573
|
+
return `maint/${recipe}-${trimmed}`;
|
|
574
|
+
}
|
|
575
|
+
async function isWorkingTreeClean(cwd) {
|
|
576
|
+
const { stdout } = await git(cwd, ["status", "--porcelain"]);
|
|
577
|
+
return stdout.trim().length === 0;
|
|
578
|
+
}
|
|
579
|
+
async function createBranch(cwd, name) {
|
|
580
|
+
await git(cwd, ["checkout", "-b", name]);
|
|
581
|
+
}
|
|
582
|
+
async function stageAll(cwd) {
|
|
583
|
+
await git(cwd, ["add", "-A"]);
|
|
584
|
+
}
|
|
585
|
+
async function commit(cwd, message) {
|
|
586
|
+
await stageAll(cwd);
|
|
587
|
+
const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
|
|
588
|
+
if (status.trim().length === 0) return null;
|
|
589
|
+
await git(cwd, ["commit", "-m", message]);
|
|
590
|
+
const { stdout: sha } = await git(cwd, ["rev-parse", "HEAD"]);
|
|
591
|
+
return sha.trim();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/recipes/sync-configs.ts
|
|
595
|
+
function siteLabel6(site) {
|
|
596
|
+
return site.name ?? site.path;
|
|
597
|
+
}
|
|
598
|
+
async function readMaybe(path) {
|
|
599
|
+
try {
|
|
600
|
+
return await readFile3(path, "utf-8");
|
|
601
|
+
} catch {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function planDiffs(cwd, templates) {
|
|
606
|
+
const diffs = [];
|
|
607
|
+
for (const t of templates) {
|
|
608
|
+
const existing = await readMaybe(join5(cwd, t.path));
|
|
609
|
+
if (existing !== t.contents) diffs.push(t);
|
|
610
|
+
}
|
|
611
|
+
return diffs;
|
|
612
|
+
}
|
|
613
|
+
async function syncConfigs(site, opts = {}) {
|
|
614
|
+
const label = siteLabel6(site);
|
|
615
|
+
const targets = opts.which ? templatesByName(opts.which) : ALL_TEMPLATES;
|
|
616
|
+
const diffs = await planDiffs(site.path, targets);
|
|
617
|
+
if (diffs.length === 0) {
|
|
618
|
+
return {
|
|
619
|
+
recipe: "sync-configs",
|
|
620
|
+
site: label,
|
|
621
|
+
status: "noop",
|
|
622
|
+
commits: [],
|
|
623
|
+
notes: "all targeted configs already match"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (!await isWorkingTreeClean(site.path)) {
|
|
627
|
+
throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
|
|
628
|
+
}
|
|
629
|
+
const branch = branchName("sync-configs");
|
|
630
|
+
await createBranch(site.path, branch);
|
|
631
|
+
const shas = [];
|
|
632
|
+
for (const t of diffs) {
|
|
633
|
+
await writeFile3(join5(site.path, t.path), t.contents, "utf-8");
|
|
634
|
+
const sha = await commit(
|
|
635
|
+
site.path,
|
|
636
|
+
`chore: sync ${t.config} config from @reddoorla/maintenance`
|
|
637
|
+
);
|
|
638
|
+
if (sha) shas.push(sha);
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
recipe: "sync-configs",
|
|
642
|
+
site: label,
|
|
643
|
+
status: "applied",
|
|
644
|
+
commits: shas,
|
|
645
|
+
notes: `branch: ${branch}`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/recipes/bump-deps.ts
|
|
650
|
+
function siteLabel7(site) {
|
|
651
|
+
return site.name ?? site.path;
|
|
652
|
+
}
|
|
653
|
+
function outdatedFlagsForGroup(group) {
|
|
654
|
+
if (group === "major") return ["--latest"];
|
|
655
|
+
if (group === "minor") return [];
|
|
656
|
+
return ["--depth", "0"];
|
|
657
|
+
}
|
|
658
|
+
function upFlagsForGroup(group) {
|
|
659
|
+
if (group === "major") return ["--latest"];
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
async function bumpDeps(site, opts = {}) {
|
|
663
|
+
const label = siteLabel7(site);
|
|
664
|
+
const group = opts.group ?? "minor";
|
|
665
|
+
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
666
|
+
const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
|
|
667
|
+
cwd: site.path
|
|
668
|
+
});
|
|
669
|
+
let parsed;
|
|
670
|
+
try {
|
|
671
|
+
parsed = JSON.parse(outdated.stdout || "{}");
|
|
672
|
+
} catch {
|
|
673
|
+
parsed = {};
|
|
674
|
+
}
|
|
675
|
+
const nothingToDo = Object.keys(parsed).length === 0;
|
|
676
|
+
if (nothingToDo) {
|
|
677
|
+
return {
|
|
678
|
+
recipe: "bump-deps",
|
|
679
|
+
site: label,
|
|
680
|
+
status: "noop",
|
|
681
|
+
commits: [],
|
|
682
|
+
notes: `pnpm outdated reported nothing for group=${group}`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (!await isWorkingTreeClean(site.path)) {
|
|
686
|
+
throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
|
|
687
|
+
}
|
|
688
|
+
const branch = branchName("bump-deps");
|
|
689
|
+
await createBranch(site.path, branch);
|
|
690
|
+
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], { cwd: site.path });
|
|
691
|
+
const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
|
|
692
|
+
const shas = sha ? [sha] : [];
|
|
693
|
+
return {
|
|
694
|
+
recipe: "bump-deps",
|
|
695
|
+
site: label,
|
|
696
|
+
status: shas.length > 0 ? "applied" : "noop",
|
|
697
|
+
commits: shas,
|
|
698
|
+
notes: `branch: ${branch}`
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/recipes/svelte-5/index.ts
|
|
703
|
+
import { join as join11 } from "path";
|
|
704
|
+
|
|
705
|
+
// src/util/pkg.ts
|
|
706
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
707
|
+
async function readPackageJson(path) {
|
|
708
|
+
const raw = await readFile4(path, "utf-8");
|
|
709
|
+
return JSON.parse(raw);
|
|
710
|
+
}
|
|
711
|
+
async function writePackageJson(path, pkg) {
|
|
712
|
+
const content = JSON.stringify(pkg, null, 2) + "\n";
|
|
713
|
+
await writeFile4(path, content, "utf-8");
|
|
714
|
+
}
|
|
715
|
+
function bumpDep(pkg, name, version) {
|
|
716
|
+
const next = {
|
|
717
|
+
...pkg
|
|
718
|
+
};
|
|
719
|
+
if (pkg.dependencies) {
|
|
720
|
+
next.dependencies = { ...pkg.dependencies };
|
|
721
|
+
}
|
|
722
|
+
if (pkg.devDependencies) {
|
|
723
|
+
next.devDependencies = { ...pkg.devDependencies };
|
|
724
|
+
}
|
|
725
|
+
if (next.dependencies && name in next.dependencies) {
|
|
726
|
+
if (next.dependencies[name] === version) return pkg;
|
|
727
|
+
next.dependencies[name] = version;
|
|
728
|
+
return next;
|
|
729
|
+
}
|
|
730
|
+
if (next.devDependencies && name in next.devDependencies) {
|
|
731
|
+
if (next.devDependencies[name] === version) return pkg;
|
|
732
|
+
next.devDependencies[name] = version;
|
|
733
|
+
return next;
|
|
734
|
+
}
|
|
735
|
+
next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
|
|
736
|
+
return next;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/recipes/svelte-5/step-bump-versions.ts
|
|
740
|
+
import { join as join6 } from "path";
|
|
741
|
+
var SVELTE_5_VERSIONS = {
|
|
742
|
+
svelte: "^5.55.5",
|
|
743
|
+
"@sveltejs/kit": "^2.59.0",
|
|
744
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
745
|
+
"@sveltejs/adapter-netlify": "^6.0.4",
|
|
746
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
747
|
+
vite: "^8.0.10",
|
|
748
|
+
"svelte-check": "^4.4.7",
|
|
749
|
+
typescript: "^6.0.3",
|
|
750
|
+
"typescript-svelte-plugin": "^0.3.52"
|
|
751
|
+
};
|
|
752
|
+
async function bumpToSvelte5Versions(cwd) {
|
|
753
|
+
const pkgPath = join6(cwd, "package.json");
|
|
754
|
+
const pkg = await readPackageJson(pkgPath);
|
|
755
|
+
let next = pkg;
|
|
756
|
+
for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
757
|
+
next = bumpDep(next, name, version);
|
|
758
|
+
}
|
|
759
|
+
if (next === pkg) return false;
|
|
760
|
+
await writePackageJson(pkgPath, next);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/recipes/svelte-5/step-svelte-config.ts
|
|
765
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
766
|
+
import { join as join7 } from "path";
|
|
767
|
+
async function migrateSvelteConfig(cwd) {
|
|
768
|
+
const path = join7(cwd, "svelte.config.js");
|
|
769
|
+
let src;
|
|
770
|
+
try {
|
|
771
|
+
src = await readFile5(path, "utf-8");
|
|
772
|
+
} catch {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
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, "");
|
|
781
|
+
if (next === src) return false;
|
|
782
|
+
await writeFile5(path, next, "utf-8");
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/recipes/svelte-5/step-svelte-migrate.ts
|
|
787
|
+
async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
788
|
+
try {
|
|
789
|
+
const { code, stderr } = await spawn2(
|
|
790
|
+
"npx",
|
|
791
|
+
["--yes", "svelte-migrate", "svelte-5", "--no-install"],
|
|
792
|
+
{ cwd, timeoutMs: 5 * 6e4 }
|
|
793
|
+
);
|
|
794
|
+
if (code !== 0) {
|
|
795
|
+
return { ran: false, stderr };
|
|
796
|
+
}
|
|
797
|
+
return { ran: true, stderr };
|
|
798
|
+
} catch (err) {
|
|
799
|
+
const e = err;
|
|
800
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
801
|
+
return { ran: false, stderr: "npx unavailable" };
|
|
802
|
+
}
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
808
|
+
import { join as join8 } from "path";
|
|
809
|
+
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
810
|
+
const pkg = await readPackageJson(join8(cwd, "package.json"));
|
|
811
|
+
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
812
|
+
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
813
|
+
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
814
|
+
try {
|
|
815
|
+
const { code, stderr } = await spawn2("npx", ["--yes", "@tailwindcss/upgrade", "--force"], {
|
|
816
|
+
cwd,
|
|
817
|
+
timeoutMs: 5 * 6e4
|
|
818
|
+
});
|
|
819
|
+
if (code !== 0) return { ran: false, reason: stderr.slice(0, 200) };
|
|
820
|
+
return { ran: true };
|
|
821
|
+
} catch (err) {
|
|
822
|
+
const e = err;
|
|
823
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
824
|
+
return { ran: false, reason: "npx unavailable" };
|
|
825
|
+
}
|
|
826
|
+
throw err;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/recipes/svelte-5/step-gotchas.ts
|
|
831
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
832
|
+
import { join as join9 } from "path";
|
|
833
|
+
import { glob as glob2 } from "tinyglobby";
|
|
834
|
+
|
|
835
|
+
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
836
|
+
var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
|
|
837
|
+
var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
|
|
838
|
+
function onEventToHandler(source) {
|
|
839
|
+
const masked = [];
|
|
840
|
+
const placeholder = (i) => ` SCRIPT_${i} `;
|
|
841
|
+
const intermediate = source.replace(SCRIPT_BLOCK, (match) => {
|
|
842
|
+
masked.push(match);
|
|
843
|
+
return placeholder(masked.length - 1);
|
|
844
|
+
});
|
|
845
|
+
const rewritten = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
|
|
846
|
+
let out = rewritten;
|
|
847
|
+
masked.forEach((blk, i) => {
|
|
848
|
+
out = out.replace(placeholder(i), blk);
|
|
849
|
+
});
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/recipes/svelte-5/codemods/dollar-props.ts
|
|
854
|
+
var SCRIPT_TS = /<script\b[^>]*lang=["']ts["'][^>]*>([\s\S]*?)<\/script>/;
|
|
855
|
+
var EXPORT_LET = /^\s*export\s+let\s+(\w+)\s*(?::\s*([^=;\n]+))?\s*(?:=\s*([^;\n]+))?;?\s*$/gm;
|
|
856
|
+
function transformScript(scriptBody) {
|
|
857
|
+
const props = [];
|
|
858
|
+
const cleaned = scriptBody.replace(
|
|
859
|
+
EXPORT_LET,
|
|
860
|
+
(_full, name, type, defaultExpr) => {
|
|
861
|
+
props.push({
|
|
862
|
+
name,
|
|
863
|
+
type: type?.trim(),
|
|
864
|
+
defaultExpr: defaultExpr?.trim()
|
|
865
|
+
});
|
|
866
|
+
return "";
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
if (props.length === 0) return { body: scriptBody, changed: false };
|
|
870
|
+
const typeSig = props.map((p) => {
|
|
871
|
+
const optional = p.defaultExpr ? "?" : "";
|
|
872
|
+
return `${p.name}${optional}: ${p.type ?? "unknown"}`;
|
|
873
|
+
}).join("; ");
|
|
874
|
+
const destructured = props.map((p) => p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name).join(", ");
|
|
875
|
+
const decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;
|
|
876
|
+
const next = cleaned.replace(/^(\s*)/, (m) => `${m}${decl}
|
|
877
|
+
`);
|
|
878
|
+
return { body: next, changed: true };
|
|
879
|
+
}
|
|
880
|
+
function exportLetToProps(source) {
|
|
881
|
+
const match = source.match(SCRIPT_TS);
|
|
882
|
+
if (!match) return source;
|
|
883
|
+
const inner = match[1] ?? "";
|
|
884
|
+
const { body, changed } = transformScript(inner);
|
|
885
|
+
if (!changed) return source;
|
|
886
|
+
return source.replace(SCRIPT_TS, (full) => full.replace(inner, body));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/recipes/svelte-5/codemods/dollar-restprops.ts
|
|
890
|
+
function removeDollarRestProps(source) {
|
|
891
|
+
let next = source;
|
|
892
|
+
next = next.replace(/\$\$restProps/g, "rest");
|
|
893
|
+
next = next.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
|
|
894
|
+
return next;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/recipes/svelte-5/step-gotchas.ts
|
|
898
|
+
var SVELTE_GLOBS = ["src/**/*.svelte"];
|
|
899
|
+
var IGNORE2 = ["node_modules/**", ".svelte-kit/**", "build/**"];
|
|
900
|
+
var CODEMODS = [onEventToHandler, exportLetToProps, removeDollarRestProps];
|
|
901
|
+
async function applyGotchaCodemods(cwd) {
|
|
902
|
+
let filesChanged = 0;
|
|
903
|
+
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
904
|
+
for (const rel of relPaths) {
|
|
905
|
+
const path = join9(cwd, rel);
|
|
906
|
+
const before = await readFile6(path, "utf-8");
|
|
907
|
+
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
908
|
+
if (after !== before) {
|
|
909
|
+
await writeFile6(path, after, "utf-8");
|
|
910
|
+
filesChanged += 1;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return { filesChanged };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/recipes/svelte-5/step-verify.ts
|
|
917
|
+
async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
918
|
+
let install;
|
|
919
|
+
try {
|
|
920
|
+
install = await spawn2("pnpm", ["install"], { cwd, timeoutMs: 10 * 6e4 });
|
|
921
|
+
} catch {
|
|
922
|
+
install = { skipped: true };
|
|
923
|
+
}
|
|
924
|
+
let check;
|
|
925
|
+
try {
|
|
926
|
+
check = await spawn2("pnpm", ["run", "check"], { cwd, timeoutMs: 5 * 6e4 });
|
|
927
|
+
} catch {
|
|
928
|
+
check = { skipped: true };
|
|
929
|
+
}
|
|
930
|
+
return { install, check };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/recipes/svelte-5/step-summary.ts
|
|
934
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
935
|
+
import { join as join10 } from "path";
|
|
936
|
+
async function writeMigrationSummary(input) {
|
|
937
|
+
const lines = [
|
|
938
|
+
`# Svelte 4 \u2192 5 migration summary`,
|
|
939
|
+
``,
|
|
940
|
+
`Generated by @reddoorla/maintenance.`,
|
|
941
|
+
``,
|
|
942
|
+
`- svelte-migrate run: ${input.svelteMigrateRan ? "yes" : "no"}`,
|
|
943
|
+
`- @tailwindcss/upgrade run: ${input.tailwindUpgraded ? "yes" : "no"}`,
|
|
944
|
+
`- .svelte files touched by gotcha codemods: ${input.filesChangedByCodemods}`,
|
|
945
|
+
``,
|
|
946
|
+
`Next steps:`,
|
|
947
|
+
`- Run \`pnpm run check\` and resolve any remaining warnings.`,
|
|
948
|
+
`- Spot-check rune migrations in components that use \`reactive\` statements.`,
|
|
949
|
+
`- Verify Playwright a11y tests still pass.`
|
|
950
|
+
];
|
|
951
|
+
const content = lines.join("\n") + "\n";
|
|
952
|
+
const path = join10(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
953
|
+
await writeFile7(path, content, "utf-8");
|
|
954
|
+
return path;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/recipes/svelte-5/index.ts
|
|
958
|
+
function siteLabel8(site) {
|
|
959
|
+
return site.name ?? site.path;
|
|
960
|
+
}
|
|
961
|
+
async function alreadyOnSvelte5(cwd) {
|
|
962
|
+
try {
|
|
963
|
+
const pkg = await readPackageJson(join11(cwd, "package.json"));
|
|
964
|
+
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
965
|
+
return !!v && /^\^?5\./.test(v);
|
|
966
|
+
} catch {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function upgradeSvelte4to5(site, opts = {}) {
|
|
971
|
+
const label = siteLabel8(site);
|
|
972
|
+
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
973
|
+
if (await alreadyOnSvelte5(site.path)) {
|
|
974
|
+
return {
|
|
975
|
+
recipe: "svelte-4-to-5",
|
|
976
|
+
site: label,
|
|
977
|
+
status: "noop",
|
|
978
|
+
commits: [],
|
|
979
|
+
notes: "site already declares svelte ^5.x"
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (!await isWorkingTreeClean(site.path)) {
|
|
983
|
+
throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
|
|
984
|
+
}
|
|
985
|
+
const branch = branchName("svelte-4-to-5");
|
|
986
|
+
await createBranch(site.path, branch);
|
|
987
|
+
const shas = [];
|
|
988
|
+
const bumped = await bumpToSvelte5Versions(site.path);
|
|
989
|
+
if (bumped) {
|
|
990
|
+
const sha = await commit(site.path, "chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte");
|
|
991
|
+
if (sha) shas.push(sha);
|
|
992
|
+
}
|
|
993
|
+
const configChanged = await migrateSvelteConfig(site.path);
|
|
994
|
+
if (configChanged) {
|
|
995
|
+
const sha = await commit(
|
|
996
|
+
site.path,
|
|
997
|
+
"refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)"
|
|
998
|
+
);
|
|
999
|
+
if (sha) shas.push(sha);
|
|
1000
|
+
}
|
|
1001
|
+
const migrate = await runSvelteMigrate(site.path, spawn2);
|
|
1002
|
+
if (migrate.ran) {
|
|
1003
|
+
const sha = await commit(site.path, "refactor(svelte5): run official svelte-migrate codemod");
|
|
1004
|
+
if (sha) shas.push(sha);
|
|
1005
|
+
}
|
|
1006
|
+
const tw = await upgradeTailwind(site.path, spawn2);
|
|
1007
|
+
if (tw.ran) {
|
|
1008
|
+
const sha = await commit(site.path, "chore(svelte5): tailwindcss 3 \u2192 4 upgrade");
|
|
1009
|
+
if (sha) shas.push(sha);
|
|
1010
|
+
}
|
|
1011
|
+
const codemods = await applyGotchaCodemods(site.path);
|
|
1012
|
+
if (codemods.filesChanged > 0) {
|
|
1013
|
+
const sha = await commit(
|
|
1014
|
+
site.path,
|
|
1015
|
+
`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`
|
|
1016
|
+
);
|
|
1017
|
+
if (sha) shas.push(sha);
|
|
1018
|
+
}
|
|
1019
|
+
await verifyMigration(site.path, spawn2);
|
|
1020
|
+
const verifySha = await commit(site.path, "chore(svelte5): pnpm install + check");
|
|
1021
|
+
if (verifySha) shas.push(verifySha);
|
|
1022
|
+
await writeMigrationSummary({
|
|
1023
|
+
cwd: site.path,
|
|
1024
|
+
filesChangedByCodemods: codemods.filesChanged,
|
|
1025
|
+
svelteMigrateRan: migrate.ran,
|
|
1026
|
+
tailwindUpgraded: tw.ran
|
|
1027
|
+
});
|
|
1028
|
+
const summarySha = await commit(site.path, "docs(svelte5): add MIGRATION_SVELTE_5.md summary");
|
|
1029
|
+
if (summarySha) shas.push(summarySha);
|
|
1030
|
+
return {
|
|
1031
|
+
recipe: "svelte-4-to-5",
|
|
1032
|
+
site: label,
|
|
1033
|
+
status: shas.length > 0 ? "applied" : "noop",
|
|
1034
|
+
commits: shas,
|
|
1035
|
+
notes: `branch: ${branch}`
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/recipes/index.ts
|
|
1040
|
+
var ALL_RECIPE_NAMES = ["sync-configs", "bump-deps", "svelte-4-to-5"];
|
|
1041
|
+
function isRecipeName(value) {
|
|
1042
|
+
return ALL_RECIPE_NAMES.includes(value);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/inventory/local.ts
|
|
1046
|
+
import { basename } from "path";
|
|
1047
|
+
function localPath(path, opts = {}) {
|
|
1048
|
+
const site = { path, name: opts.name ?? basename(path) };
|
|
1049
|
+
return async () => [site];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/inventory/json.ts
|
|
1053
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1054
|
+
function validate(raw) {
|
|
1055
|
+
if (!Array.isArray(raw)) {
|
|
1056
|
+
throw new Error("inventory JSON must be an array of sites");
|
|
1057
|
+
}
|
|
1058
|
+
return raw.map((entry, i) => {
|
|
1059
|
+
if (!entry || typeof entry !== "object") {
|
|
1060
|
+
throw new Error(`inventory entry ${i} is not an object`);
|
|
1061
|
+
}
|
|
1062
|
+
const e = entry;
|
|
1063
|
+
if (typeof e.path !== "string" || e.path.length === 0) {
|
|
1064
|
+
throw new Error(`inventory entry ${i} is missing required field: path`);
|
|
1065
|
+
}
|
|
1066
|
+
const site = { path: e.path };
|
|
1067
|
+
if (typeof e.name === "string") site.name = e.name;
|
|
1068
|
+
if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
|
|
1069
|
+
if (typeof e.meta === "object" && e.meta !== null) {
|
|
1070
|
+
site.meta = e.meta;
|
|
1071
|
+
}
|
|
1072
|
+
return site;
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function fromJsonFile(path) {
|
|
1076
|
+
return async () => {
|
|
1077
|
+
const raw = JSON.parse(await readFile7(path, "utf-8"));
|
|
1078
|
+
return validate(raw);
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
export {
|
|
1082
|
+
ALL_AUDIT_NAMES,
|
|
1083
|
+
ALL_RECIPE_NAMES,
|
|
1084
|
+
a11yAudit,
|
|
1085
|
+
bumpDeps,
|
|
1086
|
+
depsAudit,
|
|
1087
|
+
fromJsonFile,
|
|
1088
|
+
isRecipeName,
|
|
1089
|
+
lighthouseAudit,
|
|
1090
|
+
lintAudit,
|
|
1091
|
+
localPath,
|
|
1092
|
+
runAudits,
|
|
1093
|
+
runAuditsAcross,
|
|
1094
|
+
securityAudit,
|
|
1095
|
+
syncConfigs,
|
|
1096
|
+
upgradeSvelte4to5
|
|
1097
|
+
};
|
|
1098
|
+
//# sourceMappingURL=index.js.map
|