@reddoorla/maintenance 0.85.1 → 0.85.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  // src/configs/playwright-a11y.ts
2
+ import { execFileSync } from "child_process";
2
3
  import { defineConfig, devices } from "@playwright/test";
3
4
  var a11yRoutes = [
4
5
  { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
@@ -6,7 +7,22 @@ var a11yRoutes = [
6
7
  ];
7
8
  var smokeRoutes = [{ path: "/", name: "home" }];
8
9
  var smokePort = process.env.REDDOOR_SMOKE_PORT;
9
- var port = smokePort || "5173";
10
+ function allocateFreePortSync() {
11
+ try {
12
+ const out = execFileSync(
13
+ process.execPath,
14
+ [
15
+ "-e",
16
+ 'const s=require("node:net").createServer();s.on("error",()=>process.exit(1));s.listen(0,"127.0.0.1",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});'
17
+ ],
18
+ { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
19
+ ).trim();
20
+ return /^\d+$/.test(out) ? out : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ var port = smokePort || allocateFreePortSync() || "5173";
10
26
  var playwrightA11yConfig = defineConfig({
11
27
  testDir: "tests",
12
28
  testMatch: /.*\.spec\.ts$/,
@@ -41,14 +57,26 @@ var playwrightA11yConfig = defineConfig({
41
57
  // nothing, naming neither the port nor the squatter. With --strictPort it
42
58
  // is an immediate "Port 5173 is already in use".
43
59
  //
44
- // This does NOT overlap with `reuseExistingServer`: that check runs first,
45
- // so a dev server already serving the probe URL is still reused and the
46
- // command never executes. --strictPort only bites when 5173 is held by
47
- // something that is not the server under test, which is exactly the case
48
- // worth failing on.
60
+ // --strictPort now only bites if the allocated port is taken in the window
61
+ // between releasing and binding it, which is exactly the case worth failing
62
+ // on.
49
63
  command: `npm run vite:dev -- --port ${port} --strictPort`,
50
64
  url: `http://localhost:${port}/dev/a11y-fixtures`,
51
- reuseExistingServer: !process.env.CI,
65
+ // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs
66
+ // reused whatever answered the probe URL. The probe only asks "does this
67
+ // respond?" — never "is this serving the code I am about to test?" — so a
68
+ // dev server left open, or one whose tree changed under it after a
69
+ // checkout, silently became the system under test. That fails in both
70
+ // directions: a false red blamed on the code (beachfront 2026-08-12, where
71
+ // it was investigated as a macOS-vs-Linux difference and written up as one
72
+ // before being caught), and a false green where a passing suite ran against
73
+ // an old build. CI already had it false, and that asymmetry is precisely
74
+ // what made the failure read as a platform bug.
75
+ //
76
+ // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).
77
+ // Because the port above is allocated rather than fixed, your own dev
78
+ // server on 5173 keeps running untouched.
79
+ reuseExistingServer: false,
52
80
  timeout: 12e4
53
81
  }
54
82
  });
@@ -59,4 +87,4 @@ export {
59
87
  smokeRoutes,
60
88
  playwright_a11y_default
61
89
  };
62
- //# sourceMappingURL=chunk-N4MOEF32.js.map
90
+ //# sourceMappingURL=chunk-C6B4RHXR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\n/**\n * Allocate a free port SYNCHRONOUSLY, for the local path where nothing handed\n * us one. Same trick as src/util/free-port.ts (bind :0, read the assigned port,\n * release it) — but that is async, and this value is needed at module scope\n * while Playwright is still building the config object.\n *\n * It cannot be an async default export instead: sites consume this base by\n * SPREADING it (`{ ...base, use: { ...base.use } }` — see the smoke-suite\n * recipe template). Spreading a Promise yields none of its properties, so the\n * site would get a silently empty config — the exact false-green this whole\n * change exists to remove. The export must stay a plain object.\n *\n * A subprocess is the cost of that constraint: ~30-50ms, once per Playwright\n * run. On any failure we return null and the caller falls back to 5173, which\n * (with reuseExistingServer now false) degrades to a loud \"port already in use\"\n * rather than a silent wrong-server run.\n */\nfunction allocateFreePortSync(): string | null {\n try {\n const out = execFileSync(\n process.execPath,\n [\n \"-e\",\n 'const s=require(\"node:net\").createServer();s.on(\"error\",()=>process.exit(1));' +\n 's.listen(0,\"127.0.0.1\",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});',\n ],\n { encoding: \"utf8\", timeout: 5_000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim();\n return /^\\d+$/.test(out) ? out : null;\n } catch {\n return null;\n }\n}\n\n// REDDOOR_SMOKE_PORT (the central audit already allocated one) wins; otherwise\n// this run allocates its own. The old fallback was the fixed 5173 — the same\n// port a dev server sits on, which is what let `reuseExistingServer` silently\n// hijack local runs (#524).\nconst port = smokePort || allocateFreePortSync() || \"5173\";\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: `http://localhost:${port}`,\n trace: \"on-first-retry\",\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n //\n // `--port ... --strictPort` in BOTH cases. It used to be applied only when\n // REDDOOR_SMOKE_PORT allocated one, on the reasoning that we should \"fail\n // loudly rather than let vite drift to a free port the baseURL doesn't\n // point at\" — but that argument covers the unset case just as well. 5173 is\n // equally a fixed port that `baseURL` and the readiness probe below are\n // pinned to, and vite left to itself drifts off it whenever something else\n // holds it.\n //\n // The symptom that exposed this: a non-vite process on 5173 sends vite to\n // 5174 while the probe keeps polling 5173, so the run dies on\n // \"Timed out waiting 120000ms from config.webServer\" — 120 seconds of\n // nothing, naming neither the port nor the squatter. With --strictPort it\n // is an immediate \"Port 5173 is already in use\".\n //\n // --strictPort now only bites if the allocated port is taken in the window\n // between releasing and binding it, which is exactly the case worth failing\n // on.\n command: `npm run vite:dev -- --port ${port} --strictPort`,\n url: `http://localhost:${port}/dev/a11y-fixtures`,\n // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs\n // reused whatever answered the probe URL. The probe only asks \"does this\n // respond?\" — never \"is this serving the code I am about to test?\" — so a\n // dev server left open, or one whose tree changed under it after a\n // checkout, silently became the system under test. That fails in both\n // directions: a false red blamed on the code (beachfront 2026-08-12, where\n // it was investigated as a macOS-vs-Linux difference and written up as one\n // before being caught), and a false green where a passing suite ran against\n // an old build. CI already had it false, and that asymmetry is precisely\n // what made the failure read as a platform bug.\n //\n // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).\n // Because the port above is allocated rather than fixed, your own dev\n // server on 5173 keeps running untouched.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAmB9B,SAAS,uBAAsC;AAC7C,MAAI;AACF,UAAM,MAAM;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,QACE;AAAA,QACA;AAAA,MAEF;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAO,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IAC1E,EAAE,KAAK;AACP,WAAO,QAAQ,KAAK,GAAG,IAAI,MAAM;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,IAAM,OAAO,aAAa,qBAAqB,KAAK;AAKpD,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAoBT,SAAS,8BAA8B,IAAI;AAAA,IAC3C,KAAK,oBAAoB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAe7B,qBAAqB;AAAA,IACrB,SAAS;AAAA,EACX;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}
@@ -22,7 +22,7 @@ import {
22
22
  import {
23
23
  a11yRoutes,
24
24
  smokeRoutes
25
- } from "./chunk-N4MOEF32.js";
25
+ } from "./chunk-C6B4RHXR.js";
26
26
 
27
27
  // src/audits/deps.ts
28
28
  import { readFile } from "fs/promises";
@@ -2263,4 +2263,4 @@ export {
2263
2263
  runAudits,
2264
2264
  runAuditsAcross
2265
2265
  };
2266
- //# sourceMappingURL=chunk-QRDB4NLB.js.map
2266
+ //# sourceMappingURL=chunk-DTTIFYAM.js.map
@@ -144,7 +144,10 @@ export default defineConfig({
144
144
  webServer: {
145
145
  command: \`npm run vite:dev -- --port \${smokePort} --strictPort\`,
146
146
  url: \`http://localhost:\${smokePort}/dev/a11y-fixtures\`,
147
- reuseExistingServer: !process.env.CI,
147
+ // Never reuse: the readiness probe cannot tell the server under test
148
+ // from any other server answering that URL. The shared base does the
149
+ // same, and allocates its own port when REDDOOR_SMOKE_PORT is unset.
150
+ reuseExistingServer: false,
148
151
  timeout: 120_000,
149
152
  },
150
153
  }
@@ -312,4 +315,4 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
312
315
  export {
313
316
  smokeSuite
314
317
  };
315
- //# sourceMappingURL=chunk-R4AB6GCT.js.map
318
+ //# sourceMappingURL=chunk-S56JQJ4F.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/recipes/smoke-suite/index.ts","../src/recipes/smoke-suite/template.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { formatWithPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n SMOKE_ROUTES_RELATIVE,\n SMOKE_ROUTES_TEMPLATE,\n SMOKE_SPEC_RELATIVE,\n SMOKE_SPEC_TEMPLATE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n PLAYWRIGHT_CONFIG_TEMPLATE,\n PLAYWRIGHT_CONFIG_PRE_R11,\n} from \"./template.js\";\n\nexport type SmokeSuiteDeps = { spawn: SpawnFn };\n\ntype PackageJson = {\n scripts?: Record<string, string>;\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n};\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/** The template sentence explaining the default `footer` marker — swapped for a\n * fallback explanation when the marker deviates. Must match template.ts. */\nconst FOOTER_MARKER_SENTENCE =\n \"The hydration marker `footer` is the shared\\n// layout footer, present on every page including the error page.\";\n\nfunction fallbackMarkerSentence(marker: string): string {\n return (\n `The hydration marker \\`${marker}\\` is a\\n` +\n \"// fallback: no <footer> element exists in this site's Svelte source. Add a\\n\" +\n \"// semantic <footer> landmark and point the marker back at it when possible.\"\n );\n}\n\n/** The starter's default `footer` hydration marker needs the element to exist,\n * or EVERY route check false-fails (la-homelessness-initiative red'd the first\n * fleet-smoke run exactly this way). Deviate only on positive evidence of a\n * bespoke build: svelte files exist under src/ and none renders a literal\n * lowercase `<footer` element (a capital-F `<Footer` component tag proves\n * nothing — the element the browser paints lives inside that component). No\n * svelte files at all → no signal → keep the starter default. */\nasync function detectHydrationMarker(cwd: string): Promise<\"footer\" | \"main\" | \"body\"> {\n let entries: string[];\n try {\n entries = (await readdir(join(cwd, \"src\"), { recursive: true })) as string[];\n } catch {\n return \"footer\";\n }\n const svelteFiles = entries.filter((p) => p.endsWith(\".svelte\"));\n if (svelteFiles.length === 0) return \"footer\";\n let sawMain = false;\n for (const rel of svelteFiles) {\n const text = (await readIfExists(join(cwd, \"src\", rel))) ?? \"\";\n if (/<footer[\\s>/]/.test(text)) return \"footer\";\n if (/<main[\\s>/]/.test(text)) sawMain = true;\n }\n return sawMain ? \"main\" : \"body\";\n}\n\n/**\n * Adds the smoke suite to a site: the `tests/smoke/*` specs, a `test:smoke` /\n * `test:unit` script split, `@playwright/test`, and a `playwright.config.ts`\n * that honors `REDDOOR_SMOKE_PORT` (R1.1). Conservative + partial-apply — every\n * file is noop-if-exists, every `package.json` script/dep is add-if-absent, and\n * an existing config that isn't a recognizable shared-base shape is left\n * untouched and flagged for a manual R1.1 patch (never a destructive write).\n * A site with no `package.json` noops (not a node project). Branch+commit per site.\n */\nexport async function smokeSuite(\n site: Site,\n deps: SmokeSuiteDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const pkgPath = join(site.path, \"package.json\");\n return withRecipe<{ pkgPath: string; pkg: PackageJson }>({\n name: \"smoke-suite\",\n site,\n plan: async () => {\n // Parse in the read-only plan phase so a missing OR unparseable\n // package.json noops cleanly (spec: structurally unrecognizable → noop),\n // BEFORE apply writes any file. A bad JSON that only threw in apply would\n // leave the (safely-restored) branch as a `failed`, not the graceful noop.\n const raw = await readIfExists(pkgPath);\n if (raw === null) {\n return { kind: \"noop\", notes: \"no package.json (not a node project)\" };\n }\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(raw) as PackageJson;\n } catch {\n return { kind: \"noop\", notes: \"unparseable package.json — skipped\" };\n }\n return { kind: \"apply\", plan: { pkgPath, pkg } };\n },\n apply: async (planned, { commit, cwd }) => {\n const notes: string[] = [];\n // Relative paths this run actually wrote/changed — prettier-formatted to the\n // site's own config before committing (never operator files we left alone).\n const written: string[] = [];\n\n // 1. Spec files — write if absent (never clobber operator edits). The\n // routes manifest ships the starter-verbatim `footer` marker only when\n // the site actually renders one; bespoke builds fall back to `main`,\n // then `body`, so the suite proves paint instead of false-failing.\n let routesTemplate = SMOKE_ROUTES_TEMPLATE;\n if (!(await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE)))) {\n const marker = await detectHydrationMarker(cwd);\n if (marker !== \"footer\") {\n routesTemplate = SMOKE_ROUTES_TEMPLATE.replace(\n 'hydrationMarker: \"footer\"',\n `hydrationMarker: \"${marker}\"`,\n ).replace(FOOTER_MARKER_SENTENCE, fallbackMarkerSentence(marker));\n notes.push(\n `no <footer> element in src/**/*.svelte — hydration marker set to \"${marker}\" ` +\n \"(add a semantic <footer> landmark to restore the default)\",\n );\n }\n }\n const specFiles: Array<[string, string]> = [\n [SMOKE_ROUTES_RELATIVE, routesTemplate],\n [SMOKE_SPEC_RELATIVE, SMOKE_SPEC_TEMPLATE],\n ];\n for (const [rel, tmpl] of specFiles) {\n const target = join(cwd, rel);\n if (!(await fileExists(target))) {\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, tmpl, \"utf-8\");\n written.push(rel);\n }\n }\n\n // 2. playwright.config.ts — four cases, never an in-place edit: absent →\n // write R1.1; already has REDDOOR_SMOKE_PORT → leave; exact pre-R1.1\n // shared-base → safe-replace wholesale; anything else → flag for manual.\n const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);\n const existingCfg = await readIfExists(cfgPath);\n if (existingCfg === null) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else if (existingCfg.includes(\"REDDOOR_SMOKE_PORT\")) {\n // Already R1.1-aware; leave it.\n } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else {\n notes.push(\n \"playwright.config.ts exists without REDDOOR_SMOKE_PORT — add the R1.1 port block manually\",\n );\n }\n\n // 3. package.json — add-if-absent scripts + @playwright/test. Only rewrite\n // when something changed, so a re-run of a fully-adopted site noops\n // instead of churning the file. (Parsed in plan; see above.)\n const pkg = planned.pkg;\n let pkgChanged = false;\n pkg.scripts ??= {};\n if (!pkg.scripts[\"test:smoke\"]) {\n pkg.scripts[\"test:smoke\"] = \"playwright install chromium && playwright test\";\n pkgChanged = true;\n }\n // Unit-test scripts (`test:unit` / `test` → \"vitest run\") only when the site\n // actually runs vitest — either it depends on vitest, or it already defines a\n // `test` script. Adding `test: \"vitest run\"` to a site WITHOUT vitest makes\n // the shared CI's \"run the test script if present\" step fail with\n // `vitest: not found` (the whole fleet-smoke batch red'd on exactly this).\n const hasVitest = !!(pkg.devDependencies?.vitest || pkg.dependencies?.vitest);\n const existingTest = pkg.scripts[\"test\"];\n if (hasVitest || existingTest !== undefined) {\n if (!pkg.scripts[\"test:unit\"]) {\n pkg.scripts[\"test:unit\"] = existingTest ?? \"vitest run\";\n pkgChanged = true;\n }\n if (pkg.scripts[\"test\"] === undefined) {\n pkg.scripts[\"test\"] = \"vitest run\";\n pkgChanged = true;\n }\n }\n let depsChanged = false;\n const hasPlaywright =\n !!pkg.devDependencies?.[\"@playwright/test\"] || !!pkg.dependencies?.[\"@playwright/test\"];\n if (!hasPlaywright) {\n pkg.devDependencies ??= {};\n pkg.devDependencies[\"@playwright/test\"] = \"^1.60.0\";\n depsChanged = true;\n pkgChanged = true;\n }\n if (pkgChanged) {\n await writeFile(planned.pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n written.push(\"package.json\");\n }\n\n // 4. Install only when a dep was added (streaming, so the operator sees\n // progress; matches bump-deps). A failed install aborts the recipe.\n if (depsChanged) {\n const res = await deps.spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (res.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${res.code})` };\n }\n }\n\n // 5. Format everything this run wrote to the site's own prettier config, so\n // fleet CI's format check stays green across heterogeneous configs\n // (quotes/tabs/printWidth vary). Best-effort — a site without prettier\n // just commits unformatted with a flag note.\n if (!(await formatWithPrettier(deps.spawn, cwd, written))) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n\n // 6. Commit. If nothing was written/changed the commit stages nothing and\n // withRecipe reports noop (the flag note, if any, is still surfaced).\n await commit(\"feat: add smoke suite (test:smoke + playwright config + /health smoke routes)\");\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","// GENERATED verbatim from the reddoor-starter working tree (tests/smoke/*,\n// playwright.config.ts). Byte-fidelity matters — the smoke spec + config are\n// written into each site unchanged. Regenerate with scratchpad/gen-smoke-template.mjs\n// if the starter's smoke suite changes. Do NOT hand-edit the string bodies.\n\nexport const SMOKE_ROUTES_RELATIVE = \"tests/smoke/routes.ts\";\nexport const SMOKE_ROUTES_TEMPLATE = `// Committed per-site smoke manifest. \\`tests/smoke/pages.spec.ts\\` iterates this\n// list, asserting each route returns its expected status and paints a hydration\n// marker with no console errors. This ships the SAFE DEFAULT every reddoor-starter\n// clone inherits; each site's figma-slices build grows the list as real routes\n// land (add \\`{ path, name, hydrationMarker }\\` entries).\n//\n// NOTE on the default \\`/\\` entry: it expects 200, which holds once the clone is\n// wired to a real Prismic repo (getByUID(\"page\",\"home\") resolves). On the bare\n// placeholder starter, \\`/\\` returns 404 (the Prismic lookup throws → error(404)),\n// so the \\`/\\` case only goes green after Prismic is wired — by design, since the\n// gate is about real site health. The hydration marker \\`footer\\` is the shared\n// layout footer, present on every page including the error page.\n\nexport type SmokeRoute = {\n /** Route path to visit, e.g. \"/\" or \"/about\". */\n path: string;\n /** Human-readable label used in the test title. */\n name: string;\n /** CSS selector asserted visible after load (hydration proof). Default: skip. */\n hydrationMarker?: string;\n /** Expected HTTP status. Default: 200. */\n expectStatus?: number;\n};\n\nexport const smokeRoutes: SmokeRoute[] = [\n { path: \"/\", name: \"home\", hydrationMarker: \"footer\" },\n];\n`;\n\nexport const SMOKE_SPEC_RELATIVE = \"tests/smoke/pages.spec.ts\";\nexport const SMOKE_SPEC_TEMPLATE = `import { test, expect, type Page, type ConsoleMessage } from \"@playwright/test\";\nimport { smokeRoutes } from \"./routes\";\n\n// Console messages we don't care about. Add patterns here only after seeing them\n// in CI and confirming they aren't actionable. Patterns are matched against both\n// the message text and the offending resource URL — Chromium's \"Failed to load\n// resource\" text omits the URL, so URL matching catches third-party network noise.\nconst ALLOWED_CONSOLE_PATTERNS: RegExp[] = [\n // Vimeo iframe embeds + their CDN telemetry endpoints occasionally 403 from\n // cloud IPs due to bot detection.\n /vimeo/i,\n // Turnstile (Cloudflare) telemetry occasionally surfaces in console.\n /turnstile|challenges\\\\.cloudflare/i,\n];\n\nfunction attachConsoleWatcher(page: Page, extraAllowed: RegExp[] = []) {\n const errors: string[] = [];\n const allowed = [...ALLOWED_CONSOLE_PATTERNS, ...extraAllowed];\n const isAllowed = (s: string) => !!s && allowed.some((re) => re.test(s));\n\n page.on(\"console\", (msg: ConsoleMessage) => {\n if (msg.type() !== \"error\") return;\n const text = msg.text();\n const url = msg.location()?.url ?? \"\";\n if (isAllowed(text) || isAllowed(url)) return;\n errors.push(\\`[console.error] \\${text}\\${url ? \\` (\\${url})\\` : \"\"}\\`);\n });\n\n page.on(\"pageerror\", (err) => {\n if (isAllowed(err.message)) return;\n errors.push(\\`[pageerror] \\${err.message}\\`);\n });\n\n return errors;\n}\n\nfor (const route of smokeRoutes) {\n test(\\`\\${route.path} (\\${route.name}) loads with no console errors\\`, async ({\n page,\n }) => {\n const errors = attachConsoleWatcher(page);\n const response = await page.goto(route.path, {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status(), \\`HTTP status for \\${route.path}\\`).toBe(\n route.expectStatus ?? 200,\n );\n if (route.hydrationMarker) {\n await expect(\n page.locator(route.hydrationMarker),\n \\`hydration marker \"\\${route.hydrationMarker}\" on \\${route.path}\\`,\n ).toBeVisible();\n }\n expect(errors, \\`console errors on \\${route.path}\\`).toEqual([]);\n });\n}\n\ntest(\"404 page renders the custom error component\", async ({ page }) => {\n // The browser logs a top-level \"Failed to load resource: 404\" for the page\n // itself — expected on a 404 route, not a bug. Allow it here.\n const errors = attachConsoleWatcher(page, [/Failed to load resource.*404/i]);\n const response = await page.goto(\"/this-uid-does-not-exist\", {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status()).toBe(404);\n // src/routes/+error.svelte renders \\`<h1>{page.status}</h1>\\` → \"404\".\n await expect(page.getByText(\"404\", { exact: false }).first()).toBeVisible();\n expect(errors).toEqual([]);\n});\n`;\n\nexport const PLAYWRIGHT_CONFIG_RELATIVE = \"playwright.config.ts\";\n\n/** The R1.1 config: reads REDDOOR_SMOKE_PORT and binds --strictPort. Written\n * when a site has no playwright.config.ts. */\nexport const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\n//\n// R1.1 (health-gate): the central \\`smoke\\` audit (reddoor-maintenance\n// src/audits/smoke.ts) allocates a free port and passes it as\n// REDDOOR_SMOKE_PORT so a zombie vite already squatting the default 5173 can't\n// silently hijack the run and green a stale build. When it's set, bind vite to\n// exactly that port with --strictPort (forwarded through \\`npm run vite:dev\\` so\n// it stays portable across pnpm/npm) and aim Playwright's baseURL + readiness\n// probe at it. Unset (local \\`pnpm test:smoke\\`) → the shared base's fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\nexport default defineConfig({\n ...base,\n use: {\n ...base.use,\n reducedMotion: \"reduce\",\n ...(smokePort ? { baseURL: \\`http://localhost:\\${smokePort}\\` } : {}),\n },\n ...(smokePort\n ? {\n webServer: {\n command: \\`npm run vite:dev -- --port \\${smokePort} --strictPort\\`,\n url: \\`http://localhost:\\${smokePort}/dev/a11y-fixtures\\`,\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n }\n : {}),\n});\n`;\n\n/** The pre-R1.1 shared-base config (no port block). Used ONLY to recognize a\n * site that adopted the shared base before R1.1, so it can be safely replaced\n * wholesale with PLAYWRIGHT_CONFIG_TEMPLATE. Any other existing config is left\n * untouched and flagged for manual patch. */\nexport const PLAYWRIGHT_CONFIG_PRE_R11 = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\nexport default defineConfig({\n ...base,\n use: { ...base.use, reducedMotion: \"reduce\" },\n});\n`;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/recipes/smoke-suite/index.ts","../src/recipes/smoke-suite/template.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { formatWithPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n SMOKE_ROUTES_RELATIVE,\n SMOKE_ROUTES_TEMPLATE,\n SMOKE_SPEC_RELATIVE,\n SMOKE_SPEC_TEMPLATE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n PLAYWRIGHT_CONFIG_TEMPLATE,\n PLAYWRIGHT_CONFIG_PRE_R11,\n} from \"./template.js\";\n\nexport type SmokeSuiteDeps = { spawn: SpawnFn };\n\ntype PackageJson = {\n scripts?: Record<string, string>;\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n};\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/** The template sentence explaining the default `footer` marker — swapped for a\n * fallback explanation when the marker deviates. Must match template.ts. */\nconst FOOTER_MARKER_SENTENCE =\n \"The hydration marker `footer` is the shared\\n// layout footer, present on every page including the error page.\";\n\nfunction fallbackMarkerSentence(marker: string): string {\n return (\n `The hydration marker \\`${marker}\\` is a\\n` +\n \"// fallback: no <footer> element exists in this site's Svelte source. Add a\\n\" +\n \"// semantic <footer> landmark and point the marker back at it when possible.\"\n );\n}\n\n/** The starter's default `footer` hydration marker needs the element to exist,\n * or EVERY route check false-fails (la-homelessness-initiative red'd the first\n * fleet-smoke run exactly this way). Deviate only on positive evidence of a\n * bespoke build: svelte files exist under src/ and none renders a literal\n * lowercase `<footer` element (a capital-F `<Footer` component tag proves\n * nothing — the element the browser paints lives inside that component). No\n * svelte files at all → no signal → keep the starter default. */\nasync function detectHydrationMarker(cwd: string): Promise<\"footer\" | \"main\" | \"body\"> {\n let entries: string[];\n try {\n entries = (await readdir(join(cwd, \"src\"), { recursive: true })) as string[];\n } catch {\n return \"footer\";\n }\n const svelteFiles = entries.filter((p) => p.endsWith(\".svelte\"));\n if (svelteFiles.length === 0) return \"footer\";\n let sawMain = false;\n for (const rel of svelteFiles) {\n const text = (await readIfExists(join(cwd, \"src\", rel))) ?? \"\";\n if (/<footer[\\s>/]/.test(text)) return \"footer\";\n if (/<main[\\s>/]/.test(text)) sawMain = true;\n }\n return sawMain ? \"main\" : \"body\";\n}\n\n/**\n * Adds the smoke suite to a site: the `tests/smoke/*` specs, a `test:smoke` /\n * `test:unit` script split, `@playwright/test`, and a `playwright.config.ts`\n * that honors `REDDOOR_SMOKE_PORT` (R1.1). Conservative + partial-apply — every\n * file is noop-if-exists, every `package.json` script/dep is add-if-absent, and\n * an existing config that isn't a recognizable shared-base shape is left\n * untouched and flagged for a manual R1.1 patch (never a destructive write).\n * A site with no `package.json` noops (not a node project). Branch+commit per site.\n */\nexport async function smokeSuite(\n site: Site,\n deps: SmokeSuiteDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const pkgPath = join(site.path, \"package.json\");\n return withRecipe<{ pkgPath: string; pkg: PackageJson }>({\n name: \"smoke-suite\",\n site,\n plan: async () => {\n // Parse in the read-only plan phase so a missing OR unparseable\n // package.json noops cleanly (spec: structurally unrecognizable → noop),\n // BEFORE apply writes any file. A bad JSON that only threw in apply would\n // leave the (safely-restored) branch as a `failed`, not the graceful noop.\n const raw = await readIfExists(pkgPath);\n if (raw === null) {\n return { kind: \"noop\", notes: \"no package.json (not a node project)\" };\n }\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(raw) as PackageJson;\n } catch {\n return { kind: \"noop\", notes: \"unparseable package.json — skipped\" };\n }\n return { kind: \"apply\", plan: { pkgPath, pkg } };\n },\n apply: async (planned, { commit, cwd }) => {\n const notes: string[] = [];\n // Relative paths this run actually wrote/changed — prettier-formatted to the\n // site's own config before committing (never operator files we left alone).\n const written: string[] = [];\n\n // 1. Spec files — write if absent (never clobber operator edits). The\n // routes manifest ships the starter-verbatim `footer` marker only when\n // the site actually renders one; bespoke builds fall back to `main`,\n // then `body`, so the suite proves paint instead of false-failing.\n let routesTemplate = SMOKE_ROUTES_TEMPLATE;\n if (!(await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE)))) {\n const marker = await detectHydrationMarker(cwd);\n if (marker !== \"footer\") {\n routesTemplate = SMOKE_ROUTES_TEMPLATE.replace(\n 'hydrationMarker: \"footer\"',\n `hydrationMarker: \"${marker}\"`,\n ).replace(FOOTER_MARKER_SENTENCE, fallbackMarkerSentence(marker));\n notes.push(\n `no <footer> element in src/**/*.svelte — hydration marker set to \"${marker}\" ` +\n \"(add a semantic <footer> landmark to restore the default)\",\n );\n }\n }\n const specFiles: Array<[string, string]> = [\n [SMOKE_ROUTES_RELATIVE, routesTemplate],\n [SMOKE_SPEC_RELATIVE, SMOKE_SPEC_TEMPLATE],\n ];\n for (const [rel, tmpl] of specFiles) {\n const target = join(cwd, rel);\n if (!(await fileExists(target))) {\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, tmpl, \"utf-8\");\n written.push(rel);\n }\n }\n\n // 2. playwright.config.ts — four cases, never an in-place edit: absent →\n // write R1.1; already has REDDOOR_SMOKE_PORT → leave; exact pre-R1.1\n // shared-base → safe-replace wholesale; anything else → flag for manual.\n const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);\n const existingCfg = await readIfExists(cfgPath);\n if (existingCfg === null) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else if (existingCfg.includes(\"REDDOOR_SMOKE_PORT\")) {\n // Already R1.1-aware; leave it.\n } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else {\n notes.push(\n \"playwright.config.ts exists without REDDOOR_SMOKE_PORT — add the R1.1 port block manually\",\n );\n }\n\n // 3. package.json — add-if-absent scripts + @playwright/test. Only rewrite\n // when something changed, so a re-run of a fully-adopted site noops\n // instead of churning the file. (Parsed in plan; see above.)\n const pkg = planned.pkg;\n let pkgChanged = false;\n pkg.scripts ??= {};\n if (!pkg.scripts[\"test:smoke\"]) {\n pkg.scripts[\"test:smoke\"] = \"playwright install chromium && playwright test\";\n pkgChanged = true;\n }\n // Unit-test scripts (`test:unit` / `test` → \"vitest run\") only when the site\n // actually runs vitest — either it depends on vitest, or it already defines a\n // `test` script. Adding `test: \"vitest run\"` to a site WITHOUT vitest makes\n // the shared CI's \"run the test script if present\" step fail with\n // `vitest: not found` (the whole fleet-smoke batch red'd on exactly this).\n const hasVitest = !!(pkg.devDependencies?.vitest || pkg.dependencies?.vitest);\n const existingTest = pkg.scripts[\"test\"];\n if (hasVitest || existingTest !== undefined) {\n if (!pkg.scripts[\"test:unit\"]) {\n pkg.scripts[\"test:unit\"] = existingTest ?? \"vitest run\";\n pkgChanged = true;\n }\n if (pkg.scripts[\"test\"] === undefined) {\n pkg.scripts[\"test\"] = \"vitest run\";\n pkgChanged = true;\n }\n }\n let depsChanged = false;\n const hasPlaywright =\n !!pkg.devDependencies?.[\"@playwright/test\"] || !!pkg.dependencies?.[\"@playwright/test\"];\n if (!hasPlaywright) {\n pkg.devDependencies ??= {};\n pkg.devDependencies[\"@playwright/test\"] = \"^1.60.0\";\n depsChanged = true;\n pkgChanged = true;\n }\n if (pkgChanged) {\n await writeFile(planned.pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n written.push(\"package.json\");\n }\n\n // 4. Install only when a dep was added (streaming, so the operator sees\n // progress; matches bump-deps). A failed install aborts the recipe.\n if (depsChanged) {\n const res = await deps.spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (res.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${res.code})` };\n }\n }\n\n // 5. Format everything this run wrote to the site's own prettier config, so\n // fleet CI's format check stays green across heterogeneous configs\n // (quotes/tabs/printWidth vary). Best-effort — a site without prettier\n // just commits unformatted with a flag note.\n if (!(await formatWithPrettier(deps.spawn, cwd, written))) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n\n // 6. Commit. If nothing was written/changed the commit stages nothing and\n // withRecipe reports noop (the flag note, if any, is still surfaced).\n await commit(\"feat: add smoke suite (test:smoke + playwright config + /health smoke routes)\");\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","// GENERATED verbatim from the reddoor-starter working tree (tests/smoke/*,\n// playwright.config.ts). Byte-fidelity matters — the smoke spec + config are\n// written into each site unchanged. Regenerate with scratchpad/gen-smoke-template.mjs\n// if the starter's smoke suite changes. Do NOT hand-edit the string bodies.\n\nexport const SMOKE_ROUTES_RELATIVE = \"tests/smoke/routes.ts\";\nexport const SMOKE_ROUTES_TEMPLATE = `// Committed per-site smoke manifest. \\`tests/smoke/pages.spec.ts\\` iterates this\n// list, asserting each route returns its expected status and paints a hydration\n// marker with no console errors. This ships the SAFE DEFAULT every reddoor-starter\n// clone inherits; each site's figma-slices build grows the list as real routes\n// land (add \\`{ path, name, hydrationMarker }\\` entries).\n//\n// NOTE on the default \\`/\\` entry: it expects 200, which holds once the clone is\n// wired to a real Prismic repo (getByUID(\"page\",\"home\") resolves). On the bare\n// placeholder starter, \\`/\\` returns 404 (the Prismic lookup throws → error(404)),\n// so the \\`/\\` case only goes green after Prismic is wired — by design, since the\n// gate is about real site health. The hydration marker \\`footer\\` is the shared\n// layout footer, present on every page including the error page.\n\nexport type SmokeRoute = {\n /** Route path to visit, e.g. \"/\" or \"/about\". */\n path: string;\n /** Human-readable label used in the test title. */\n name: string;\n /** CSS selector asserted visible after load (hydration proof). Default: skip. */\n hydrationMarker?: string;\n /** Expected HTTP status. Default: 200. */\n expectStatus?: number;\n};\n\nexport const smokeRoutes: SmokeRoute[] = [\n { path: \"/\", name: \"home\", hydrationMarker: \"footer\" },\n];\n`;\n\nexport const SMOKE_SPEC_RELATIVE = \"tests/smoke/pages.spec.ts\";\nexport const SMOKE_SPEC_TEMPLATE = `import { test, expect, type Page, type ConsoleMessage } from \"@playwright/test\";\nimport { smokeRoutes } from \"./routes\";\n\n// Console messages we don't care about. Add patterns here only after seeing them\n// in CI and confirming they aren't actionable. Patterns are matched against both\n// the message text and the offending resource URL — Chromium's \"Failed to load\n// resource\" text omits the URL, so URL matching catches third-party network noise.\nconst ALLOWED_CONSOLE_PATTERNS: RegExp[] = [\n // Vimeo iframe embeds + their CDN telemetry endpoints occasionally 403 from\n // cloud IPs due to bot detection.\n /vimeo/i,\n // Turnstile (Cloudflare) telemetry occasionally surfaces in console.\n /turnstile|challenges\\\\.cloudflare/i,\n];\n\nfunction attachConsoleWatcher(page: Page, extraAllowed: RegExp[] = []) {\n const errors: string[] = [];\n const allowed = [...ALLOWED_CONSOLE_PATTERNS, ...extraAllowed];\n const isAllowed = (s: string) => !!s && allowed.some((re) => re.test(s));\n\n page.on(\"console\", (msg: ConsoleMessage) => {\n if (msg.type() !== \"error\") return;\n const text = msg.text();\n const url = msg.location()?.url ?? \"\";\n if (isAllowed(text) || isAllowed(url)) return;\n errors.push(\\`[console.error] \\${text}\\${url ? \\` (\\${url})\\` : \"\"}\\`);\n });\n\n page.on(\"pageerror\", (err) => {\n if (isAllowed(err.message)) return;\n errors.push(\\`[pageerror] \\${err.message}\\`);\n });\n\n return errors;\n}\n\nfor (const route of smokeRoutes) {\n test(\\`\\${route.path} (\\${route.name}) loads with no console errors\\`, async ({\n page,\n }) => {\n const errors = attachConsoleWatcher(page);\n const response = await page.goto(route.path, {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status(), \\`HTTP status for \\${route.path}\\`).toBe(\n route.expectStatus ?? 200,\n );\n if (route.hydrationMarker) {\n await expect(\n page.locator(route.hydrationMarker),\n \\`hydration marker \"\\${route.hydrationMarker}\" on \\${route.path}\\`,\n ).toBeVisible();\n }\n expect(errors, \\`console errors on \\${route.path}\\`).toEqual([]);\n });\n}\n\ntest(\"404 page renders the custom error component\", async ({ page }) => {\n // The browser logs a top-level \"Failed to load resource: 404\" for the page\n // itself — expected on a 404 route, not a bug. Allow it here.\n const errors = attachConsoleWatcher(page, [/Failed to load resource.*404/i]);\n const response = await page.goto(\"/this-uid-does-not-exist\", {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status()).toBe(404);\n // src/routes/+error.svelte renders \\`<h1>{page.status}</h1>\\` → \"404\".\n await expect(page.getByText(\"404\", { exact: false }).first()).toBeVisible();\n expect(errors).toEqual([]);\n});\n`;\n\nexport const PLAYWRIGHT_CONFIG_RELATIVE = \"playwright.config.ts\";\n\n/** The R1.1 config: reads REDDOOR_SMOKE_PORT and binds --strictPort. Written\n * when a site has no playwright.config.ts. */\nexport const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\n//\n// R1.1 (health-gate): the central \\`smoke\\` audit (reddoor-maintenance\n// src/audits/smoke.ts) allocates a free port and passes it as\n// REDDOOR_SMOKE_PORT so a zombie vite already squatting the default 5173 can't\n// silently hijack the run and green a stale build. When it's set, bind vite to\n// exactly that port with --strictPort (forwarded through \\`npm run vite:dev\\` so\n// it stays portable across pnpm/npm) and aim Playwright's baseURL + readiness\n// probe at it. Unset (local \\`pnpm test:smoke\\`) → the shared base's fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\nexport default defineConfig({\n ...base,\n use: {\n ...base.use,\n reducedMotion: \"reduce\",\n ...(smokePort ? { baseURL: \\`http://localhost:\\${smokePort}\\` } : {}),\n },\n ...(smokePort\n ? {\n webServer: {\n command: \\`npm run vite:dev -- --port \\${smokePort} --strictPort\\`,\n url: \\`http://localhost:\\${smokePort}/dev/a11y-fixtures\\`,\n // Never reuse: the readiness probe cannot tell the server under test\n // from any other server answering that URL. The shared base does the\n // same, and allocates its own port when REDDOOR_SMOKE_PORT is unset.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n }\n : {}),\n});\n`;\n\n/** The pre-R1.1 shared-base config (no port block). Used ONLY to recognize a\n * site that adopted the shared base before R1.1, so it can be safely replaced\n * wholesale with PLAYWRIGHT_CONFIG_TEMPLATE. Any other existing config is left\n * untouched and flagged for manual patch. */\nexport const PLAYWRIGHT_CONFIG_PRE_R11 = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\nexport default defineConfig({\n ...base,\n use: { ...base.use, reducedMotion: \"reduce\" },\n});\n`;\n"],"mappings":"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","names":[]}
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-R6FPFMOS.js";
13
13
  import {
14
14
  smokeSuite
15
- } from "./chunk-R4AB6GCT.js";
15
+ } from "./chunk-S56JQJ4F.js";
16
16
  import {
17
17
  syncConfigs
18
18
  } from "./chunk-Q276MZ7E.js";
@@ -21,7 +21,7 @@ import {
21
21
  } from "./chunk-ZTPPCHDB.js";
22
22
  import {
23
23
  runAudits
24
- } from "./chunk-QRDB4NLB.js";
24
+ } from "./chunk-DTTIFYAM.js";
25
25
  import {
26
26
  siteLabel
27
27
  } from "./chunk-XXTZBPUY.js";
@@ -143,4 +143,4 @@ export {
143
143
  DEFAULT_INIT_STEPS,
144
144
  init
145
145
  };
146
- //# sourceMappingURL=chunk-6V7565O2.js.map
146
+ //# sourceMappingURL=chunk-WYYQK4D3.js.map
package/dist/cli/bin.js CHANGED
@@ -207,7 +207,7 @@ cli.command(
207
207
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
208
208
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
209
209
  async (site, opts) => runOrExit(
210
- async () => (await import("../smoke-suite-OBWNPALQ.js")).runSmokeSuiteCommand(site, opts),
210
+ async () => (await import("../smoke-suite-TMCSOYRK.js")).runSmokeSuiteCommand(site, opts),
211
211
  opts
212
212
  )
213
213
  );
@@ -230,14 +230,14 @@ cli.command(
230
230
  "--fleet <inventory>",
231
231
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
232
232
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
233
- async (site, opts) => runOrExit(async () => (await import("../init-TFVAN5OY.js")).runInitCommand(site, opts), opts)
233
+ async (site, opts) => runOrExit(async () => (await import("../init-JN26NTWF.js")).runInitCommand(site, opts), opts)
234
234
  );
235
235
  cli.command(
236
236
  "launch <site>",
237
237
  "Bootstrap + first-audit a site, then draft its launch email for approval."
238
238
  ).action(
239
239
  async (site, opts) => runOrExit(
240
- async () => (await import("../launch-DWY2MF54.js")).runLaunchCommand(site, opts),
240
+ async () => (await import("../launch-JA7Z4CIM.js")).runLaunchCommand(site, opts),
241
241
  opts
242
242
  )
243
243
  );
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  ALL_AUDIT_NAMES,
14
14
  runOneAudit
15
- } from "../../chunk-QRDB4NLB.js";
15
+ } from "../../chunk-DTTIFYAM.js";
16
16
  import "../../chunk-LKGVSM2O.js";
17
17
  import "../../chunk-GASBX52O.js";
18
18
  import "../../chunk-VRWUL4TR.js";
@@ -32,7 +32,7 @@ import {
32
32
  } from "../../chunk-U5SZ4FOP.js";
33
33
  import "../../chunk-6VAL7XJT.js";
34
34
  import "../../chunk-NMAWXBJA.js";
35
- import "../../chunk-N4MOEF32.js";
35
+ import "../../chunk-C6B4RHXR.js";
36
36
 
37
37
  // src/cli/commands/audit.ts
38
38
  import { resolve } from "path";
@@ -2,7 +2,7 @@ import {
2
2
  a11yRoutes,
3
3
  playwright_a11y_default,
4
4
  smokeRoutes
5
- } from "../chunk-N4MOEF32.js";
5
+ } from "../chunk-C6B4RHXR.js";
6
6
  export {
7
7
  a11yRoutes,
8
8
  playwright_a11y_default as default,
@@ -23,6 +23,35 @@ type CspObject = {
23
23
  directives?: CspDirectives;
24
24
  [k: string]: unknown;
25
25
  };
26
+ /**
27
+ * SHA-256 of Svelte's SSR event-replay stub, `this.__e=event`.
28
+ *
29
+ * Svelte emits `onload`/`onerror="this.__e=event"` on any load/error element
30
+ * (`<img>`, `<iframe>`, …) that carries a spread attribute or a `use:`
31
+ * directive — i.e. every `<img {...getImageProps(field)} />` the Prismic
32
+ * helpers produce. The stub stashes an event that fires BEFORE hydration so the
33
+ * component can replay it once it is alive.
34
+ *
35
+ * Hashes do not apply to inline event handlers unless `'unsafe-hashes'` is
36
+ * present, so without both of these the browser refuses to run the stub: the
37
+ * pre-hydration `load`/`error` is silently dropped (anything keyed on it — a
38
+ * fade-in, a fallback swap — can strand) and a `script-src-attr` violation is
39
+ * reported per image on every page view, burying real violations and hammering
40
+ * the report endpoint. Measured on beachfront-dentistry 2026-08-13: 12
41
+ * violations on `/` alone.
42
+ *
43
+ * `'unsafe-hashes'` widens hash matching to event handlers; it does NOT permit
44
+ * arbitrary inline handlers, so only this exact one-liner is allowed. Pair it
45
+ * with `'unsafe-inline'` and that guarantee is gone — the test asserts we do
46
+ * not. The stub itself only assigns the event to a property, so an injected
47
+ * element carrying identical text achieves nothing.
48
+ *
49
+ * Verified in Chrome 2026-08-17 against a page served with this policy: the
50
+ * handler runs and `img.__e.type === "error"`; under the previous baseline it
51
+ * did not run at all. Re-derive with:
52
+ * node -e 'console.log(require("crypto").createHash("sha256").update("this.__e=event").digest("base64"))'
53
+ */
54
+ declare const SVELTE_EVENT_REPLAY_HASH = "sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=";
26
55
  /** reddoor-specific options that get transformed into `kit.*`, not passed through. */
27
56
  type ReddoorSvelteOptions = {
28
57
  /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */
@@ -65,4 +94,4 @@ declare function createSvelteConfig(siteConfig?: SvelteConfigLike & ReddoorSvelt
65
94
  compilerOptions: NonNullable<SvelteConfigLike["compilerOptions"]>;
66
95
  };
67
96
 
68
- export { type ReddoorSvelteOptions, type SvelteConfigLike, createSvelteConfig, createSvelteConfig as default };
97
+ export { type ReddoorSvelteOptions, SVELTE_EVENT_REPLAY_HASH, type SvelteConfigLike, createSvelteConfig, createSvelteConfig as default };
@@ -18,11 +18,18 @@ var CANONICAL_ALIASES = {
18
18
  $assets: "src/lib/assets",
19
19
  "$assets/*": "src/lib/assets/*"
20
20
  };
21
+ var SVELTE_EVENT_REPLAY_HASH = "sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=";
21
22
  var BASELINE_CSP = {
22
23
  mode: "auto",
23
24
  directives: {
24
25
  "default-src": ["self"],
25
- "script-src": ["self", "https://static.cdn.prismic.io", "https://player.vimeo.com"],
26
+ "script-src": [
27
+ "self",
28
+ "https://static.cdn.prismic.io",
29
+ "https://player.vimeo.com",
30
+ "unsafe-hashes",
31
+ SVELTE_EVENT_REPLAY_HASH
32
+ ],
26
33
  "style-src": ["self", "unsafe-inline"],
27
34
  "img-src": ["self", "data:", "https://images.prismic.io", "https://*.prismic.io"],
28
35
  "media-src": ["self", "https://*.vimeocdn.com"],
@@ -97,6 +104,7 @@ function createSvelteConfig(siteConfig = {}) {
97
104
  }
98
105
  var svelte_default = createSvelteConfig;
99
106
  export {
107
+ SVELTE_EVENT_REPLAY_HASH,
100
108
  createSvelteConfig,
101
109
  svelte_default as default
102
110
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/configs/svelte.ts"],"sourcesContent":["/**\n * Minimal shape we touch — the full type lives in @sveltejs/kit's `Config`.\n * We don't import it directly to avoid a peer dependency for what amounts\n * to a small config helper. Sites get full type-checking from their own\n * `@sveltejs/kit` install when they invoke createSvelteConfig.\n */\ntype WarningFilter = (warning: { code?: string; message?: string }) => boolean;\n\nexport type SvelteConfigLike = {\n kit?: unknown;\n preprocess?: unknown;\n compilerOptions?: {\n warningFilter?: WarningFilter;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\n/** Compiler-level warnings the canonical reddoor stack treats as noise. */\nconst SILENCED_WARNING_CODES = new Set<string>([\n // `<div ... />` shorthand is widely used across reddoor codebases; the\n // Svelte 5 strictness change for non-void self-closing tags would flood\n // dev logs with no actionable signal.\n \"element_invalid_self_closing_tag\",\n]);\n\nfunction isSilenced(warning: { code?: string }): boolean {\n return warning.code !== undefined && SILENCED_WARNING_CODES.has(warning.code);\n}\n\n/**\n * Canonical `$lib` aliases shared across the reddoor fleet. Injecting these as\n * defaults means a synced site no longer has to redeclare them (and the\n * sync-configs svelte template no longer clobbers them with a thinner config).\n * Sites can override any entry or add their own — see the merge in\n * createSvelteConfig. Override an alias's bare and `/*` forms together: setting\n * only `$components` while leaving the canonical `$components/*` in place would\n * split `import \"$components\"` and `import \"$components/Foo\"` across two roots.\n */\nconst CANONICAL_ALIASES: Record<string, string> = {\n $components: \"src/lib/components\",\n \"$components/*\": \"src/lib/components/*\",\n $utils: \"src/lib/utils\",\n \"$utils/*\": \"src/lib/utils/*\",\n $stores: \"src/lib/stores\",\n \"$stores/*\": \"src/lib/stores/*\",\n $assets: \"src/lib/assets\",\n \"$assets/*\": \"src/lib/assets/*\",\n};\n\ntype CspDirectives = Record<string, string[]>;\ntype CspObject = { mode?: string; directives?: CspDirectives; [k: string]: unknown };\n\n/**\n * Baseline CSP for the reddoor stack (Prismic + Vimeo). Opt-in only — a CSP is\n * breakage-prone, so it is never injected unless a site asks via `csp`. Extend\n * per project by passing `csp: { directives: { ... } }`; named directives\n * replace the baseline entry, unnamed ones are kept. SvelteKit adds\n * nonces/hashes for the inline scripts/styles it emits.\n */\nconst BASELINE_CSP = {\n mode: \"auto\",\n directives: {\n \"default-src\": [\"self\"],\n \"script-src\": [\"self\", \"https://static.cdn.prismic.io\", \"https://player.vimeo.com\"],\n \"style-src\": [\"self\", \"unsafe-inline\"],\n \"img-src\": [\"self\", \"data:\", \"https://images.prismic.io\", \"https://*.prismic.io\"],\n \"media-src\": [\"self\", \"https://*.vimeocdn.com\"],\n \"frame-src\": [\"self\", \"https://player.vimeo.com\"],\n \"connect-src\": [\"self\", \"https://*.prismic.io\", \"https://static.cdn.prismic.io\"],\n \"font-src\": [\"self\", \"data:\"],\n \"base-uri\": [\"self\"],\n \"form-action\": [\"self\"],\n \"frame-ancestors\": [\"self\"],\n \"report-uri\": [\"/api/csp-report\"],\n },\n} satisfies CspObject;\n\n/**\n * Copy a directives map and each of its arrays, so the returned config shares\n * no array reference with the module-level BASELINE_CSP (or with a caller's\n * input). Without this, mutating one config's directive array would poison the\n * shared baseline for every later call.\n */\nfunction cloneDirectives(directives: CspDirectives): CspDirectives {\n return Object.fromEntries(\n Object.entries(directives).map(([k, v]) => [k, Array.isArray(v) ? [...v] : v]),\n );\n}\n\n/** Build a `kit.csp` block from the `csp` option, layering over the baseline. */\nfunction buildCsp(option: true | CspObject): CspObject {\n const baseDirectives = BASELINE_CSP.directives ?? {};\n if (option === true) {\n return { mode: BASELINE_CSP.mode, directives: cloneDirectives(baseDirectives) };\n }\n const { directives: siteDirectives, ...rest } = option;\n return {\n mode: BASELINE_CSP.mode,\n ...rest, // allow `mode` override, `reportOnly`, etc.\n directives: cloneDirectives({ ...baseDirectives, ...(siteDirectives ?? {}) }),\n };\n}\n\ntype PrerenderErrorDetails = {\n path?: string;\n status?: number;\n message?: string;\n referrer?: string;\n};\n\n/**\n * Prerender error handler for an un-wired placeholder clone: every Prismic-backed\n * route 404s until the clone points at a real repo, so tolerate 404 to let\n * `pnpm build` / Netlify CI pass. Any other status still throws loudly, and a\n * real site never opts in (so its 404s fail the build as they should).\n */\nfunction placeholderHttpErrorHandler({\n path,\n status,\n message,\n referrer,\n}: PrerenderErrorDetails): void {\n if (status === 404) return;\n throw new Error(`${status} ${path}${referrer ? ` (linked from ${referrer})` : \"\"}: ${message}`);\n}\n\n/** reddoor-specific options that get transformed into `kit.*`, not passed through. */\nexport type ReddoorSvelteOptions = {\n /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */\n csp?: true | CspObject;\n /** Tolerate 404s during prerender — for un-wired placeholder clones only. */\n placeholder?: boolean;\n};\n\n/**\n * Compose a Svelte/Kit config with the reddoor fleet's canonical pieces layered\n * in. Sites pass their site-specific bits (`kit.adapter`, `preprocess`, etc.)\n * and get back a complete config.\n *\n * Always applied:\n * - the canonical `compilerOptions.warningFilter` (composes with a site's own:\n * a warning shows only when both filters allow it);\n * - the canonical `$components/$utils/$stores/$assets` `kit.alias` entries\n * (a site's own `kit.alias` overrides per key and may add more).\n *\n * Opt-in (NOT applied unless requested, so adoption never silently changes a\n * site's behavior):\n * - `csp: true` injects the baseline Prismic+Vimeo CSP; `csp: { directives }`\n * extends it per-directive. A CSP is breakage-prone, so it is opt-in — a site\n * that wants the starter's CSP parity must pass `csp`. An explicit `kit.csp`\n * always wins as an escape hatch.\n * - `placeholder: true` tolerates 404s during prerender (for an un-wired\n * placeholder clone only) — like `csp`, a site that wants the starter's\n * prerender-tolerance parity must pass it; the site computes the signal itself.\n *\n * @example\n * import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\n * import adapter from \"@sveltejs/adapter-netlify\";\n *\n * export default createSvelteConfig({\n * kit: { adapter: adapter() },\n * csp: true,\n * placeholder: process.env.VITE_PRISMIC_ENVIRONMENT === \"your-prismic-repo-name\",\n * });\n */\n\nexport function createSvelteConfig(\n siteConfig: SvelteConfigLike & ReddoorSvelteOptions = {},\n): SvelteConfigLike & {\n compilerOptions: NonNullable<SvelteConfigLike[\"compilerOptions\"]>;\n} {\n // Strip the reddoor-only options so they never leak onto the returned config.\n const { csp, placeholder, ...rest } = siteConfig;\n\n const siteCompiler = rest.compilerOptions ?? {};\n const siteFilter = siteCompiler.warningFilter;\n\n const siteKit = (rest.kit ?? {}) as Record<string, unknown>;\n const siteAlias = (siteKit.alias ?? {}) as Record<string, string>;\n\n const kit: Record<string, unknown> = {\n ...siteKit,\n // Canonical aliases first, site entries last so a site can override any\n // single alias or add its own without losing the fleet defaults.\n alias: { ...CANONICAL_ALIASES, ...siteAlias },\n };\n\n // CSP: opt-in. An explicit `kit.csp` always wins as an escape hatch.\n if (siteKit.csp !== undefined) {\n kit.csp = siteKit.csp;\n } else if (csp) {\n kit.csp = buildCsp(csp);\n }\n\n // Prerender placeholder tolerance: opt-in. A site-provided handler wins.\n if (placeholder) {\n const sitePrerender = (siteKit.prerender ?? {}) as Record<string, unknown>;\n kit.prerender = { handleHttpError: placeholderHttpErrorHandler, ...sitePrerender };\n }\n\n return {\n ...rest,\n kit,\n compilerOptions: {\n ...siteCompiler,\n warningFilter: (warning) => {\n if (isSilenced(warning)) return false;\n return siteFilter ? siteFilter(warning) : true;\n },\n },\n };\n}\n\nexport default createSvelteConfig;\n"],"mappings":";AAmBA,IAAM,yBAAyB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA,EAI7C;AACF,CAAC;AAED,SAAS,WAAW,SAAqC;AACvD,SAAO,QAAQ,SAAS,UAAa,uBAAuB,IAAI,QAAQ,IAAI;AAC9E;AAWA,IAAM,oBAA4C;AAAA,EAChD,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,EACT,aAAa;AACf;AAYA,IAAM,eAAe;AAAA,EACnB,MAAM;AAAA,EACN,YAAY;AAAA,IACV,eAAe,CAAC,MAAM;AAAA,IACtB,cAAc,CAAC,QAAQ,iCAAiC,0BAA0B;AAAA,IAClF,aAAa,CAAC,QAAQ,eAAe;AAAA,IACrC,WAAW,CAAC,QAAQ,SAAS,6BAA6B,sBAAsB;AAAA,IAChF,aAAa,CAAC,QAAQ,wBAAwB;AAAA,IAC9C,aAAa,CAAC,QAAQ,0BAA0B;AAAA,IAChD,eAAe,CAAC,QAAQ,wBAAwB,+BAA+B;AAAA,IAC/E,YAAY,CAAC,QAAQ,OAAO;AAAA,IAC5B,YAAY,CAAC,MAAM;AAAA,IACnB,eAAe,CAAC,MAAM;AAAA,IACtB,mBAAmB,CAAC,MAAM;AAAA,IAC1B,cAAc,CAAC,iBAAiB;AAAA,EAClC;AACF;AAQA,SAAS,gBAAgB,YAA0C;AACjE,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,UAAU,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAAA,EAC/E;AACF;AAGA,SAAS,SAAS,QAAqC;AACrD,QAAM,iBAAiB,aAAa,cAAc,CAAC;AACnD,MAAI,WAAW,MAAM;AACnB,WAAO,EAAE,MAAM,aAAa,MAAM,YAAY,gBAAgB,cAAc,EAAE;AAAA,EAChF;AACA,QAAM,EAAE,YAAY,gBAAgB,GAAG,KAAK,IAAI;AAChD,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,GAAG;AAAA;AAAA,IACH,YAAY,gBAAgB,EAAE,GAAG,gBAAgB,GAAI,kBAAkB,CAAC,EAAG,CAAC;AAAA,EAC9E;AACF;AAeA,SAAS,4BAA4B;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,MAAI,WAAW,IAAK;AACpB,QAAM,IAAI,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,iBAAiB,QAAQ,MAAM,EAAE,KAAK,OAAO,EAAE;AAChG;AA0CO,SAAS,mBACd,aAAsD,CAAC,GAGvD;AAEA,QAAM,EAAE,KAAK,aAAa,GAAG,KAAK,IAAI;AAEtC,QAAM,eAAe,KAAK,mBAAmB,CAAC;AAC9C,QAAM,aAAa,aAAa;AAEhC,QAAM,UAAW,KAAK,OAAO,CAAC;AAC9B,QAAM,YAAa,QAAQ,SAAS,CAAC;AAErC,QAAM,MAA+B;AAAA,IACnC,GAAG;AAAA;AAAA;AAAA,IAGH,OAAO,EAAE,GAAG,mBAAmB,GAAG,UAAU;AAAA,EAC9C;AAGA,MAAI,QAAQ,QAAQ,QAAW;AAC7B,QAAI,MAAM,QAAQ;AAAA,EACpB,WAAW,KAAK;AACd,QAAI,MAAM,SAAS,GAAG;AAAA,EACxB;AAGA,MAAI,aAAa;AACf,UAAM,gBAAiB,QAAQ,aAAa,CAAC;AAC7C,QAAI,YAAY,EAAE,iBAAiB,6BAA6B,GAAG,cAAc;AAAA,EACnF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,iBAAiB;AAAA,MACf,GAAG;AAAA,MACH,eAAe,CAAC,YAAY;AAC1B,YAAI,WAAW,OAAO,EAAG,QAAO;AAChC,eAAO,aAAa,WAAW,OAAO,IAAI;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/configs/svelte.ts"],"sourcesContent":["/**\n * Minimal shape we touch — the full type lives in @sveltejs/kit's `Config`.\n * We don't import it directly to avoid a peer dependency for what amounts\n * to a small config helper. Sites get full type-checking from their own\n * `@sveltejs/kit` install when they invoke createSvelteConfig.\n */\ntype WarningFilter = (warning: { code?: string; message?: string }) => boolean;\n\nexport type SvelteConfigLike = {\n kit?: unknown;\n preprocess?: unknown;\n compilerOptions?: {\n warningFilter?: WarningFilter;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\n/** Compiler-level warnings the canonical reddoor stack treats as noise. */\nconst SILENCED_WARNING_CODES = new Set<string>([\n // `<div ... />` shorthand is widely used across reddoor codebases; the\n // Svelte 5 strictness change for non-void self-closing tags would flood\n // dev logs with no actionable signal.\n \"element_invalid_self_closing_tag\",\n]);\n\nfunction isSilenced(warning: { code?: string }): boolean {\n return warning.code !== undefined && SILENCED_WARNING_CODES.has(warning.code);\n}\n\n/**\n * Canonical `$lib` aliases shared across the reddoor fleet. Injecting these as\n * defaults means a synced site no longer has to redeclare them (and the\n * sync-configs svelte template no longer clobbers them with a thinner config).\n * Sites can override any entry or add their own — see the merge in\n * createSvelteConfig. Override an alias's bare and `/*` forms together: setting\n * only `$components` while leaving the canonical `$components/*` in place would\n * split `import \"$components\"` and `import \"$components/Foo\"` across two roots.\n */\nconst CANONICAL_ALIASES: Record<string, string> = {\n $components: \"src/lib/components\",\n \"$components/*\": \"src/lib/components/*\",\n $utils: \"src/lib/utils\",\n \"$utils/*\": \"src/lib/utils/*\",\n $stores: \"src/lib/stores\",\n \"$stores/*\": \"src/lib/stores/*\",\n $assets: \"src/lib/assets\",\n \"$assets/*\": \"src/lib/assets/*\",\n};\n\ntype CspDirectives = Record<string, string[]>;\ntype CspObject = { mode?: string; directives?: CspDirectives; [k: string]: unknown };\n\n/**\n * SHA-256 of Svelte's SSR event-replay stub, `this.__e=event`.\n *\n * Svelte emits `onload`/`onerror=\"this.__e=event\"` on any load/error element\n * (`<img>`, `<iframe>`, …) that carries a spread attribute or a `use:`\n * directive — i.e. every `<img {...getImageProps(field)} />` the Prismic\n * helpers produce. The stub stashes an event that fires BEFORE hydration so the\n * component can replay it once it is alive.\n *\n * Hashes do not apply to inline event handlers unless `'unsafe-hashes'` is\n * present, so without both of these the browser refuses to run the stub: the\n * pre-hydration `load`/`error` is silently dropped (anything keyed on it — a\n * fade-in, a fallback swap — can strand) and a `script-src-attr` violation is\n * reported per image on every page view, burying real violations and hammering\n * the report endpoint. Measured on beachfront-dentistry 2026-08-13: 12\n * violations on `/` alone.\n *\n * `'unsafe-hashes'` widens hash matching to event handlers; it does NOT permit\n * arbitrary inline handlers, so only this exact one-liner is allowed. Pair it\n * with `'unsafe-inline'` and that guarantee is gone — the test asserts we do\n * not. The stub itself only assigns the event to a property, so an injected\n * element carrying identical text achieves nothing.\n *\n * Verified in Chrome 2026-08-17 against a page served with this policy: the\n * handler runs and `img.__e.type === \"error\"`; under the previous baseline it\n * did not run at all. Re-derive with:\n * node -e 'console.log(require(\"crypto\").createHash(\"sha256\").update(\"this.__e=event\").digest(\"base64\"))'\n */\nexport const SVELTE_EVENT_REPLAY_HASH = \"sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=\";\n\n/**\n * Baseline CSP for the reddoor stack (Prismic + Vimeo). Opt-in only — a CSP is\n * breakage-prone, so it is never injected unless a site asks via `csp`. Extend\n * per project by passing `csp: { directives: { ... } }`; named directives\n * replace the baseline entry, unnamed ones are kept. SvelteKit adds\n * nonces/hashes for the inline scripts/styles it emits.\n *\n * NOTE: a site overriding `script-src` replaces this entry wholesale, so it must\n * carry `'unsafe-hashes'` + SVELTE_EVENT_REPLAY_HASH itself or it reintroduces\n * the violation above.\n */\nconst BASELINE_CSP = {\n mode: \"auto\",\n directives: {\n \"default-src\": [\"self\"],\n \"script-src\": [\n \"self\",\n \"https://static.cdn.prismic.io\",\n \"https://player.vimeo.com\",\n \"unsafe-hashes\",\n SVELTE_EVENT_REPLAY_HASH,\n ],\n \"style-src\": [\"self\", \"unsafe-inline\"],\n \"img-src\": [\"self\", \"data:\", \"https://images.prismic.io\", \"https://*.prismic.io\"],\n \"media-src\": [\"self\", \"https://*.vimeocdn.com\"],\n \"frame-src\": [\"self\", \"https://player.vimeo.com\"],\n \"connect-src\": [\"self\", \"https://*.prismic.io\", \"https://static.cdn.prismic.io\"],\n \"font-src\": [\"self\", \"data:\"],\n \"base-uri\": [\"self\"],\n \"form-action\": [\"self\"],\n \"frame-ancestors\": [\"self\"],\n \"report-uri\": [\"/api/csp-report\"],\n },\n} satisfies CspObject;\n\n/**\n * Copy a directives map and each of its arrays, so the returned config shares\n * no array reference with the module-level BASELINE_CSP (or with a caller's\n * input). Without this, mutating one config's directive array would poison the\n * shared baseline for every later call.\n */\nfunction cloneDirectives(directives: CspDirectives): CspDirectives {\n return Object.fromEntries(\n Object.entries(directives).map(([k, v]) => [k, Array.isArray(v) ? [...v] : v]),\n );\n}\n\n/** Build a `kit.csp` block from the `csp` option, layering over the baseline. */\nfunction buildCsp(option: true | CspObject): CspObject {\n const baseDirectives = BASELINE_CSP.directives ?? {};\n if (option === true) {\n return { mode: BASELINE_CSP.mode, directives: cloneDirectives(baseDirectives) };\n }\n const { directives: siteDirectives, ...rest } = option;\n return {\n mode: BASELINE_CSP.mode,\n ...rest, // allow `mode` override, `reportOnly`, etc.\n directives: cloneDirectives({ ...baseDirectives, ...(siteDirectives ?? {}) }),\n };\n}\n\ntype PrerenderErrorDetails = {\n path?: string;\n status?: number;\n message?: string;\n referrer?: string;\n};\n\n/**\n * Prerender error handler for an un-wired placeholder clone: every Prismic-backed\n * route 404s until the clone points at a real repo, so tolerate 404 to let\n * `pnpm build` / Netlify CI pass. Any other status still throws loudly, and a\n * real site never opts in (so its 404s fail the build as they should).\n */\nfunction placeholderHttpErrorHandler({\n path,\n status,\n message,\n referrer,\n}: PrerenderErrorDetails): void {\n if (status === 404) return;\n throw new Error(`${status} ${path}${referrer ? ` (linked from ${referrer})` : \"\"}: ${message}`);\n}\n\n/** reddoor-specific options that get transformed into `kit.*`, not passed through. */\nexport type ReddoorSvelteOptions = {\n /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */\n csp?: true | CspObject;\n /** Tolerate 404s during prerender — for un-wired placeholder clones only. */\n placeholder?: boolean;\n};\n\n/**\n * Compose a Svelte/Kit config with the reddoor fleet's canonical pieces layered\n * in. Sites pass their site-specific bits (`kit.adapter`, `preprocess`, etc.)\n * and get back a complete config.\n *\n * Always applied:\n * - the canonical `compilerOptions.warningFilter` (composes with a site's own:\n * a warning shows only when both filters allow it);\n * - the canonical `$components/$utils/$stores/$assets` `kit.alias` entries\n * (a site's own `kit.alias` overrides per key and may add more).\n *\n * Opt-in (NOT applied unless requested, so adoption never silently changes a\n * site's behavior):\n * - `csp: true` injects the baseline Prismic+Vimeo CSP; `csp: { directives }`\n * extends it per-directive. A CSP is breakage-prone, so it is opt-in — a site\n * that wants the starter's CSP parity must pass `csp`. An explicit `kit.csp`\n * always wins as an escape hatch.\n * - `placeholder: true` tolerates 404s during prerender (for an un-wired\n * placeholder clone only) — like `csp`, a site that wants the starter's\n * prerender-tolerance parity must pass it; the site computes the signal itself.\n *\n * @example\n * import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\n * import adapter from \"@sveltejs/adapter-netlify\";\n *\n * export default createSvelteConfig({\n * kit: { adapter: adapter() },\n * csp: true,\n * placeholder: process.env.VITE_PRISMIC_ENVIRONMENT === \"your-prismic-repo-name\",\n * });\n */\n\nexport function createSvelteConfig(\n siteConfig: SvelteConfigLike & ReddoorSvelteOptions = {},\n): SvelteConfigLike & {\n compilerOptions: NonNullable<SvelteConfigLike[\"compilerOptions\"]>;\n} {\n // Strip the reddoor-only options so they never leak onto the returned config.\n const { csp, placeholder, ...rest } = siteConfig;\n\n const siteCompiler = rest.compilerOptions ?? {};\n const siteFilter = siteCompiler.warningFilter;\n\n const siteKit = (rest.kit ?? {}) as Record<string, unknown>;\n const siteAlias = (siteKit.alias ?? {}) as Record<string, string>;\n\n const kit: Record<string, unknown> = {\n ...siteKit,\n // Canonical aliases first, site entries last so a site can override any\n // single alias or add its own without losing the fleet defaults.\n alias: { ...CANONICAL_ALIASES, ...siteAlias },\n };\n\n // CSP: opt-in. An explicit `kit.csp` always wins as an escape hatch.\n if (siteKit.csp !== undefined) {\n kit.csp = siteKit.csp;\n } else if (csp) {\n kit.csp = buildCsp(csp);\n }\n\n // Prerender placeholder tolerance: opt-in. A site-provided handler wins.\n if (placeholder) {\n const sitePrerender = (siteKit.prerender ?? {}) as Record<string, unknown>;\n kit.prerender = { handleHttpError: placeholderHttpErrorHandler, ...sitePrerender };\n }\n\n return {\n ...rest,\n kit,\n compilerOptions: {\n ...siteCompiler,\n warningFilter: (warning) => {\n if (isSilenced(warning)) return false;\n return siteFilter ? siteFilter(warning) : true;\n },\n },\n };\n}\n\nexport default createSvelteConfig;\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  DEFAULT_INIT_STEPS,
8
8
  a11yFixturesPage,
9
9
  init
10
- } from "./chunk-6V7565O2.js";
10
+ } from "./chunk-WYYQK4D3.js";
11
11
  import {
12
12
  onboard,
13
13
  selfCaretRange,
@@ -36,7 +36,7 @@ import {
36
36
  } from "./chunk-LS6QPANO.js";
37
37
  import "./chunk-J7SZQUCW.js";
38
38
  import "./chunk-R6FPFMOS.js";
39
- import "./chunk-R4AB6GCT.js";
39
+ import "./chunk-S56JQJ4F.js";
40
40
  import "./chunk-R53VL3RI.js";
41
41
  import {
42
42
  parseAddresses,
@@ -78,7 +78,7 @@ import {
78
78
  runAudits,
79
79
  runAuditsAcross,
80
80
  securityAudit
81
- } from "./chunk-QRDB4NLB.js";
81
+ } from "./chunk-DTTIFYAM.js";
82
82
  import "./chunk-LKGVSM2O.js";
83
83
  import "./chunk-GASBX52O.js";
84
84
  import "./chunk-VRWUL4TR.js";
@@ -107,7 +107,7 @@ import {
107
107
  import "./chunk-HN6I2XWI.js";
108
108
  import "./chunk-6VAL7XJT.js";
109
109
  import "./chunk-NMAWXBJA.js";
110
- import "./chunk-N4MOEF32.js";
110
+ import "./chunk-C6B4RHXR.js";
111
111
 
112
112
  // src/recipes/index.ts
113
113
  var ALL_RECIPE_NAMES = [
@@ -8,19 +8,19 @@ import {
8
8
  } from "./chunk-7J53SUT6.js";
9
9
  import {
10
10
  init
11
- } from "./chunk-6V7565O2.js";
11
+ } from "./chunk-WYYQK4D3.js";
12
12
  import "./chunk-UYCAF77E.js";
13
13
  import "./chunk-RJB3QUSP.js";
14
14
  import "./chunk-LS6QPANO.js";
15
15
  import "./chunk-J7SZQUCW.js";
16
16
  import "./chunk-R6FPFMOS.js";
17
- import "./chunk-R4AB6GCT.js";
17
+ import "./chunk-S56JQJ4F.js";
18
18
  import "./chunk-R53VL3RI.js";
19
19
  import "./chunk-Q276MZ7E.js";
20
20
  import "./chunk-73UFRZKY.js";
21
21
  import "./chunk-ZTPPCHDB.js";
22
22
  import "./chunk-OTGB6YQO.js";
23
- import "./chunk-QRDB4NLB.js";
23
+ import "./chunk-DTTIFYAM.js";
24
24
  import "./chunk-LKGVSM2O.js";
25
25
  import "./chunk-GASBX52O.js";
26
26
  import "./chunk-VRWUL4TR.js";
@@ -34,7 +34,7 @@ import {
34
34
  import "./chunk-XTK5VIQB.js";
35
35
  import "./chunk-U5SZ4FOP.js";
36
36
  import "./chunk-NMAWXBJA.js";
37
- import "./chunk-N4MOEF32.js";
37
+ import "./chunk-C6B4RHXR.js";
38
38
 
39
39
  // src/cli/commands/init.ts
40
40
  import { resolve } from "path";
@@ -107,4 +107,4 @@ async function runInitCommand(site, opts) {
107
107
  export {
108
108
  runInitCommand
109
109
  };
110
- //# sourceMappingURL=init-TFVAN5OY.js.map
110
+ //# sourceMappingURL=init-JN26NTWF.js.map
@@ -30,7 +30,7 @@ import "./chunk-SEEA6RXF.js";
30
30
  import "./chunk-73UFRZKY.js";
31
31
  import {
32
32
  runAudits
33
- } from "./chunk-QRDB4NLB.js";
33
+ } from "./chunk-DTTIFYAM.js";
34
34
  import "./chunk-LKGVSM2O.js";
35
35
  import "./chunk-GASBX52O.js";
36
36
  import "./chunk-VRWUL4TR.js";
@@ -53,7 +53,7 @@ import {
53
53
  } from "./chunk-HN6I2XWI.js";
54
54
  import "./chunk-6VAL7XJT.js";
55
55
  import "./chunk-NMAWXBJA.js";
56
- import "./chunk-N4MOEF32.js";
56
+ import "./chunk-C6B4RHXR.js";
57
57
 
58
58
  // src/cli/commands/launch.ts
59
59
  import { resolve } from "path";
@@ -216,4 +216,4 @@ async function runLaunchCommand(site, opts) {
216
216
  export {
217
217
  runLaunchCommand
218
218
  };
219
- //# sourceMappingURL=launch-DWY2MF54.js.map
219
+ //# sourceMappingURL=launch-JA7Z4CIM.js.map
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-7J53SUT6.js";
12
12
  import {
13
13
  smokeSuite
14
- } from "./chunk-R4AB6GCT.js";
14
+ } from "./chunk-S56JQJ4F.js";
15
15
  import "./chunk-R53VL3RI.js";
16
16
  import "./chunk-ZTPPCHDB.js";
17
17
  import "./chunk-FDIFTIXC.js";
@@ -51,4 +51,4 @@ async function runSmokeSuiteCommand(site, opts) {
51
51
  export {
52
52
  runSmokeSuiteCommand
53
53
  };
54
- //# sourceMappingURL=smoke-suite-OBWNPALQ.js.map
54
+ //# sourceMappingURL=smoke-suite-TMCSOYRK.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddoorla/maintenance",
3
- "version": "0.85.1",
3
+ "version": "0.85.2",
4
4
  "description": "Canonical maintenance configs, audits, and recipes for the reddoor stack.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\nconst port = smokePort || \"5173\";\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: `http://localhost:${port}`,\n trace: \"on-first-retry\",\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n //\n // `--port ... --strictPort` in BOTH cases. It used to be applied only when\n // REDDOOR_SMOKE_PORT allocated one, on the reasoning that we should \"fail\n // loudly rather than let vite drift to a free port the baseURL doesn't\n // point at\" — but that argument covers the unset case just as well. 5173 is\n // equally a fixed port that `baseURL` and the readiness probe below are\n // pinned to, and vite left to itself drifts off it whenever something else\n // holds it.\n //\n // The symptom that exposed this: a non-vite process on 5173 sends vite to\n // 5174 while the probe keeps polling 5173, so the run dies on\n // \"Timed out waiting 120000ms from config.webServer\" — 120 seconds of\n // nothing, naming neither the port nor the squatter. With --strictPort it\n // is an immediate \"Port 5173 is already in use\".\n //\n // This does NOT overlap with `reuseExistingServer`: that check runs first,\n // so a dev server already serving the probe URL is still reused and the\n // command never executes. --strictPort only bites when 5173 is held by\n // something that is not the server under test, which is exactly the case\n // worth failing on.\n command: `npm run vite:dev -- --port ${port} --strictPort`,\n url: `http://localhost:${port}/dev/a11y-fixtures`,\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAC9B,IAAM,OAAO,aAAa;AAK1B,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAsBT,SAAS,8BAA8B,IAAI;AAAA,IAC3C,KAAK,oBAAoB,IAAI;AAAA,IAC7B,qBAAqB,CAAC,QAAQ,IAAI;AAAA,IAClC,SAAS;AAAA,EACX;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}