@reddoorla/maintenance 0.10.4 → 0.10.5
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/README.md +34 -16
- package/dist/cli/bin.js +77 -12
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +77 -12
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical maintenance configs, audits, and recipes for the reddoor SvelteKit + Prismic fleet.
|
|
4
4
|
|
|
5
|
-
A single CLI (`reddoor-maint`)
|
|
5
|
+
A single CLI (`reddoor-maint`) covering the full operator workflow:
|
|
6
|
+
|
|
7
|
+
- **Audits** inspect a site (lighthouse, a11y, security, lint, deps).
|
|
8
|
+
- **Recipes** mutate one (branch-isolated, idempotent, never on a dirty tree) — start with [`init`](#init) for fresh sites.
|
|
9
|
+
- **Reports** ([per-site monthly/quarterly emails](#reports-per-site-maintenancetesting-emails)) draft + send client-facing maintenance + testing summaries, backed by Airtable as the source of truth and Resend for delivery.
|
|
10
|
+
- **Canonical configs** (eslint, prettier, lighthouserc, playwright-a11y, svelte) ship as importable presets every reddoor site shares.
|
|
11
|
+
|
|
12
|
+
Designed to run against either a single local site or a fleet declared in an inventory file.
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
pnpm add -D @reddoorla/maintenance
|
|
@@ -46,6 +53,7 @@ reddoor-maint list-recipes # recipe descriptions
|
|
|
46
53
|
|
|
47
54
|
reddoor-maint init [site] # full onboarding chain (preferred entrypoint)
|
|
48
55
|
reddoor-maint audit [site] # run audits
|
|
56
|
+
reddoor-maint report [site] # draft / send per-site maintenance + testing emails (see Reports)
|
|
49
57
|
reddoor-maint sync-configs [site]
|
|
50
58
|
reddoor-maint bump-deps [site]
|
|
51
59
|
reddoor-maint convert-to-pnpm [site]
|
|
@@ -76,7 +84,7 @@ reddoor-maint upgrade svelte-4-to-5 [site]
|
|
|
76
84
|
|
|
77
85
|
## Recipes
|
|
78
86
|
|
|
79
|
-
Each recipe is `(site, opts?) => Promise<RecipeResult>` and is exported from the package entry as a library function
|
|
87
|
+
Each recipe is `(site, opts?) => Promise<RecipeResult>` and is also exported from the package entry as a library function:
|
|
80
88
|
|
|
81
89
|
```ts
|
|
82
90
|
import {
|
|
@@ -99,13 +107,11 @@ Shared contract — every recipe:
|
|
|
99
107
|
- **Is idempotent** — re-running on the already-applied state returns `{ status: "noop", commits: [] }` without creating a branch.
|
|
100
108
|
- **Returns a `RecipeResult`** — `{ recipe, site, status: "applied" | "noop" | "failed", commits: string[], notes?: string }`.
|
|
101
109
|
|
|
102
|
-
### `
|
|
103
|
-
|
|
104
|
-
Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges the canonical entries into `.gitignore`. Each config is its own commit. `--dry` reports the planned diff (including gitignore drift) without writing. `--only <name>[,<name>]` restricts to a subset.
|
|
110
|
+
### `init`
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
One-shot guided onboarding — the recommended entrypoint for a fresh site. Runs `convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → audit` in sequence. Each underlying recipe still creates its own branch — `init` is a thin orchestrator, not a branch-collapser. Stops the chain on the first `failed` recipe or uncaught error; `noop` results continue the chain. Exit code is 1 if any step failed _or_ the final audit pass reports a `fail`.
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
The remaining sections describe each step `init` invokes (in chain order), plus the standalone recipes that aren't part of onboarding.
|
|
109
115
|
|
|
110
116
|
### `convert-to-pnpm`
|
|
111
117
|
|
|
@@ -115,21 +121,25 @@ Removes the npm or yarn lockfile, pins `packageManager: pnpm@X.Y.Z` in `package.
|
|
|
115
121
|
|
|
116
122
|
Adds `@reddoorla/maintenance` + the audit deps (`@lhci/cli`, `@playwright/test`, `@axe-core/playwright`) to the site's `devDependencies` if they're missing. Audit dep versions come from `src/configs/baseline-versions.ts` so they can't drift from the rest of the package. The maintenance dep is pinned to a caret range against this package's own version at runtime — no manual syncing required at each minor bump. Refuses with `{ status: "failed", notes: "run convert-to-pnpm first" }` if the site has no `pnpm-lock.yaml`.
|
|
117
123
|
|
|
118
|
-
### `
|
|
124
|
+
### `sync-configs`
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges the canonical entries into `.gitignore`. Each config is its own commit. `--dry` reports the planned diff (including gitignore drift) without writing. `--only <name>[,<name>]` restricts to a subset.
|
|
121
127
|
|
|
122
|
-
### `
|
|
128
|
+
### `svelte-codemods`
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
Standalone codemod pass for sites already on Svelte 5. Applies the same gotcha codemods the full `svelte-4-to-5` recipe runs (`export let` → `$props()`, `on:event` → `onevent`, `$:` → `$derived`/`$effect`, `$$props.class` rewrite, `$$restProps` → destructured `...rest`, `$state` + `$effect` → `$derived`). Useful when post-upgrade Svelte 5 surfaces new strictness warnings and the fleet needs a clean re-application.
|
|
125
131
|
|
|
126
132
|
### `a11y-fixtures-page`
|
|
127
133
|
|
|
128
134
|
Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route doesn't already exist. The `lighthouse` and `playwright-a11y` configs both target this URL — newly-onboarded sites need the route to exist for either audit to pass. The template is intentionally generic (semantic landmarks + headings + a relative link); operator edits to an existing page are never clobbered.
|
|
129
135
|
|
|
130
|
-
### `init`
|
|
136
|
+
### `bump-deps` _(standalone — not part of `init`)_
|
|
137
|
+
|
|
138
|
+
Pre-flights that the site is on pnpm (refuses with a clear remediation if `package-lock.json` or `yarn.lock` is present without `pnpm-lock.yaml`), runs `pnpm install` to ensure the lockfile is current, then `pnpm outdated --json` to decide whether anything needs upgrading. If yes: creates a branch and runs `pnpm up` scoped to the requested `--group` (patch / minor / major), then commits the result. `noop` if nothing's out of date.
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
### `upgrade svelte-4-to-5` _(standalone — not part of `init`)_
|
|
141
|
+
|
|
142
|
+
The full 7-step Svelte 4 → 5 migration: bump framework versions, migrate `svelte.config.js`, run the official `svelte-migrate` codemod, run `@tailwindcss/upgrade`, apply gotcha codemods over `src/**/*.svelte`, verify with `pnpm install` + `pnpm run check`, and write a `MIGRATION_SVELTE_5.md` summary. Each step is its own commit; the file leaves a record of what ran and what may need manual review.
|
|
133
143
|
|
|
134
144
|
---
|
|
135
145
|
|
|
@@ -190,22 +200,30 @@ Pass `--fleet <path>` to run a command against multiple sites declared in an inv
|
|
|
190
200
|
|
|
191
201
|
## Library usage
|
|
192
202
|
|
|
193
|
-
The package's main entry exports every recipe and
|
|
203
|
+
The package's main entry exports every recipe, audit, and report function so you can wire them into custom tooling (CI jobs, scheduled scripts, alternative CLIs):
|
|
194
204
|
|
|
195
205
|
```ts
|
|
196
206
|
import {
|
|
197
207
|
// recipes
|
|
198
|
-
|
|
199
|
-
bumpDeps,
|
|
208
|
+
init,
|
|
200
209
|
convertToPnpm,
|
|
201
210
|
onboard,
|
|
211
|
+
syncConfigs,
|
|
202
212
|
svelteCodemods,
|
|
213
|
+
a11yFixturesPage,
|
|
214
|
+
bumpDeps,
|
|
203
215
|
upgradeSvelte4to5,
|
|
204
216
|
|
|
205
217
|
// audits
|
|
206
218
|
runAudits,
|
|
207
219
|
ALL_AUDIT_NAMES,
|
|
208
220
|
|
|
221
|
+
// reports (per-site maintenance / testing emails)
|
|
222
|
+
draftReportForSite,
|
|
223
|
+
sendApprovedReports,
|
|
224
|
+
renderReportHtml,
|
|
225
|
+
findDueReports,
|
|
226
|
+
|
|
209
227
|
// recipe registry
|
|
210
228
|
ALL_RECIPE_NAMES,
|
|
211
229
|
isRecipeName,
|
package/dist/cli/bin.js
CHANGED
|
@@ -1166,6 +1166,32 @@ async function readSiteConfig(sitePath) {
|
|
|
1166
1166
|
return out;
|
|
1167
1167
|
}
|
|
1168
1168
|
|
|
1169
|
+
// src/util/free-port.ts
|
|
1170
|
+
import { createServer } from "net";
|
|
1171
|
+
async function findFreePort() {
|
|
1172
|
+
return new Promise((resolve10, reject) => {
|
|
1173
|
+
const server = createServer();
|
|
1174
|
+
server.unref();
|
|
1175
|
+
server.on("error", reject);
|
|
1176
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1177
|
+
const addr = server.address();
|
|
1178
|
+
if (typeof addr === "object" && addr) {
|
|
1179
|
+
const port = addr.port;
|
|
1180
|
+
server.close(() => resolve10(port));
|
|
1181
|
+
} else {
|
|
1182
|
+
server.close();
|
|
1183
|
+
reject(new Error("findFreePort: could not determine assigned port from socket"));
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
function withFreePort(url, port) {
|
|
1189
|
+
const u = new URL(url);
|
|
1190
|
+
u.hostname = "localhost";
|
|
1191
|
+
u.port = String(port);
|
|
1192
|
+
return u.toString();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1169
1195
|
// src/audits/lighthouse.ts
|
|
1170
1196
|
async function readJsonMaybe(path) {
|
|
1171
1197
|
try {
|
|
@@ -1206,13 +1232,19 @@ async function lighthouseAudit(ctx) {
|
|
|
1206
1232
|
const site = ctx.site;
|
|
1207
1233
|
const label = siteLabel(site);
|
|
1208
1234
|
const siteCfg = await readSiteConfig(site.path);
|
|
1209
|
-
const
|
|
1235
|
+
const port = await findFreePort();
|
|
1236
|
+
const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];
|
|
1237
|
+
const resolvedConfig = {
|
|
1210
1238
|
...lighthouseConfig,
|
|
1211
1239
|
ci: {
|
|
1212
1240
|
...lighthouseConfig.ci,
|
|
1213
|
-
collect: {
|
|
1241
|
+
collect: {
|
|
1242
|
+
...lighthouseConfig.ci.collect,
|
|
1243
|
+
url: [withFreePort(baseUrl, port)],
|
|
1244
|
+
startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`
|
|
1245
|
+
}
|
|
1214
1246
|
}
|
|
1215
|
-
}
|
|
1247
|
+
};
|
|
1216
1248
|
const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
|
|
1217
1249
|
const configPath = join4(configDir, "lighthouserc.json");
|
|
1218
1250
|
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
@@ -1323,6 +1355,32 @@ async function readJsonMaybe2(path) {
|
|
|
1323
1355
|
return null;
|
|
1324
1356
|
}
|
|
1325
1357
|
}
|
|
1358
|
+
function buildPlaywrightConfig(port) {
|
|
1359
|
+
return `import { defineConfig } from "@playwright/test";
|
|
1360
|
+
|
|
1361
|
+
export default defineConfig({
|
|
1362
|
+
testDir: ".",
|
|
1363
|
+
testMatch: /.*\\.spec\\.ts$/,
|
|
1364
|
+
fullyParallel: true,
|
|
1365
|
+
forbidOnly: !!process.env.CI,
|
|
1366
|
+
retries: process.env.CI ? 2 : 0,
|
|
1367
|
+
reporter: process.env.CI ? "github" : "list",
|
|
1368
|
+
use: {
|
|
1369
|
+
baseURL: "http://localhost:${port}",
|
|
1370
|
+
trace: "on-first-retry",
|
|
1371
|
+
},
|
|
1372
|
+
webServer: {
|
|
1373
|
+
// --strictPort: refuse to bump to a different port if ours is taken,
|
|
1374
|
+
// so the audit fails loudly instead of probing a zombie.
|
|
1375
|
+
// reuseExistingServer:false: never reuse \u2014 we control the lifecycle.
|
|
1376
|
+
command: "npm run vite:dev -- --port ${port} --strictPort",
|
|
1377
|
+
url: "http://localhost:${port}/dev/a11y-fixtures",
|
|
1378
|
+
reuseExistingServer: false,
|
|
1379
|
+
timeout: 120_000,
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
`;
|
|
1383
|
+
}
|
|
1326
1384
|
function buildSpec() {
|
|
1327
1385
|
return `import { test, expect } from "@playwright/test";
|
|
1328
1386
|
import AxeBuilder from "@axe-core/playwright";
|
|
@@ -1376,19 +1434,26 @@ async function a11yAudit(ctx) {
|
|
|
1376
1434
|
const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
|
|
1377
1435
|
const specPath = join5(specDir, "a11y.spec.ts");
|
|
1378
1436
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
1437
|
+
const port = await findFreePort();
|
|
1438
|
+
const configPath = join5(specDir, "playwright.config.ts");
|
|
1439
|
+
await writeFile2(configPath, buildPlaywrightConfig(port), "utf-8");
|
|
1379
1440
|
const resultsPath = join5(site.path, RESULTS_REL);
|
|
1380
1441
|
await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
1381
1442
|
let raw;
|
|
1382
1443
|
try {
|
|
1383
|
-
raw = await spawn2(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1444
|
+
raw = await spawn2(
|
|
1445
|
+
"npx",
|
|
1446
|
+
["--yes", "playwright", "test", `--config=${configPath}`, "--reporter=line", specPath],
|
|
1447
|
+
{
|
|
1448
|
+
cwd: site.path,
|
|
1449
|
+
env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },
|
|
1450
|
+
// playwright on a cold tree downloads Chrome, boots the site's dev
|
|
1451
|
+
// server, and runs axe over every configured route. The shared 30 s
|
|
1452
|
+
// default in runAudits is fine for deps/lint/security but starves
|
|
1453
|
+
// playwright (mirrors the lighthouse fix shipped earlier).
|
|
1454
|
+
timeoutMs: 5 * 6e4
|
|
1455
|
+
}
|
|
1456
|
+
);
|
|
1392
1457
|
} catch (err) {
|
|
1393
1458
|
await rm2(specDir, { recursive: true, force: true });
|
|
1394
1459
|
const e = err;
|