@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.
- package/dist/{chunk-N4MOEF32.js → chunk-C6B4RHXR.js} +36 -8
- package/dist/chunk-C6B4RHXR.js.map +1 -0
- package/dist/{chunk-QRDB4NLB.js → chunk-DTTIFYAM.js} +2 -2
- package/dist/{chunk-R4AB6GCT.js → chunk-S56JQJ4F.js} +5 -2
- package/dist/{chunk-R4AB6GCT.js.map → chunk-S56JQJ4F.js.map} +1 -1
- package/dist/{chunk-6V7565O2.js → chunk-WYYQK4D3.js} +3 -3
- package/dist/cli/bin.js +3 -3
- package/dist/cli/commands/audit.js +2 -2
- package/dist/configs/playwright-a11y.js +1 -1
- package/dist/configs/svelte.d.ts +30 -1
- package/dist/configs/svelte.js +9 -1
- package/dist/configs/svelte.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-TFVAN5OY.js → init-JN26NTWF.js} +5 -5
- package/dist/{launch-DWY2MF54.js → launch-JA7Z4CIM.js} +3 -3
- package/dist/{smoke-suite-OBWNPALQ.js → smoke-suite-TMCSOYRK.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-N4MOEF32.js.map +0 -1
- /package/dist/{chunk-QRDB4NLB.js.map → chunk-DTTIFYAM.js.map} +0 -0
- /package/dist/{chunk-6V7565O2.js.map → chunk-WYYQK4D3.js.map} +0 -0
- /package/dist/{init-TFVAN5OY.js.map → init-JN26NTWF.js.map} +0 -0
- /package/dist/{launch-DWY2MF54.js.map → launch-JA7Z4CIM.js.map} +0 -0
- /package/dist/{smoke-suite-OBWNPALQ.js.map → smoke-suite-TMCSOYRK.js.map} +0 -0
|
@@ -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
|
-
|
|
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
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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":";;;;;;;;;;;;AAAA,SAAS,QAAQ,OAAO,SAAS,UAAU,iBAAiB;AAC5D,SAAS,SAAS,YAAY;;;ACIvB,IAAM,wBAAwB;AAC9B,IAAM,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6B9B,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuE5B,IAAM,6BAA6B;AAInC,IAAM,6BAA6B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyCnC,IAAM,4BAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADhIzC,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,OAAO,IAAI;AACjB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,aAAa,MAAsC;AAChE,MAAI;AACF,WAAO,MAAM,SAAS,MAAM,OAAO;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,IAAM,yBACJ;AAEF,SAAS,uBAAuB,QAAwB;AACtD,SACE,0BAA0B,MAAM;AAAA;AAAA;AAIpC;AASA,eAAe,sBAAsB,KAAkD;AACrF,MAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,KAAK,KAAK,KAAK,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,cAAc,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,SAAS,CAAC;AAC/D,MAAI,YAAY,WAAW,EAAG,QAAO;AACrC,MAAI,UAAU;AACd,aAAW,OAAO,aAAa;AAC7B,UAAM,OAAQ,MAAM,aAAa,KAAK,KAAK,OAAO,GAAG,CAAC,KAAM;AAC5D,QAAI,gBAAgB,KAAK,IAAI,EAAG,QAAO;AACvC,QAAI,cAAc,KAAK,IAAI,EAAG,WAAU;AAAA,EAC1C;AACA,SAAO,UAAU,SAAS;AAC5B;AAWA,eAAsB,WACpB,MACA,OAAuB,EAAE,OAAO,aAAa,GACtB;AACvB,QAAM,UAAU,KAAK,KAAK,MAAM,cAAc;AAC9C,SAAO,WAAkD;AAAA,IACvD,MAAM;AAAA,IACN;AAAA,IACA,MAAM,YAAY;AAKhB,YAAM,MAAM,MAAM,aAAa,OAAO;AACtC,UAAI,QAAQ,MAAM;AAChB,eAAO,EAAE,MAAM,QAAQ,OAAO,uCAAuC;AAAA,MACvE;AACA,UAAI;AACJ,UAAI;AACF,cAAM,KAAK,MAAM,GAAG;AAAA,MACtB,QAAQ;AACN,eAAO,EAAE,MAAM,QAAQ,OAAO,0CAAqC;AAAA,MACrE;AACA,aAAO,EAAE,MAAM,SAAS,MAAM,EAAE,SAAS,IAAI,EAAE;AAAA,IACjD;AAAA,IACA,OAAO,OAAO,SAAS,EAAE,QAAQ,IAAI,MAAM;AACzC,YAAM,QAAkB,CAAC;AAGzB,YAAM,UAAoB,CAAC;AAM3B,UAAI,iBAAiB;AACrB,UAAI,CAAE,MAAM,WAAW,KAAK,KAAK,qBAAqB,CAAC,GAAI;AACzD,cAAM,SAAS,MAAM,sBAAsB,GAAG;AAC9C,YAAI,WAAW,UAAU;AACvB,2BAAiB,sBAAsB;AAAA,YACrC;AAAA,YACA,qBAAqB,MAAM;AAAA,UAC7B,EAAE,QAAQ,wBAAwB,uBAAuB,MAAM,CAAC;AAChE,gBAAM;AAAA,YACJ,0EAAqE,MAAM;AAAA,UAE7E;AAAA,QACF;AAAA,MACF;AACA,YAAM,YAAqC;AAAA,QACzC,CAAC,uBAAuB,cAAc;AAAA,QACtC,CAAC,qBAAqB,mBAAmB;AAAA,MAC3C;AACA,iBAAW,CAAC,KAAK,IAAI,KAAK,WAAW;AACnC,cAAM,SAAS,KAAK,KAAK,GAAG;AAC5B,YAAI,CAAE,MAAM,WAAW,MAAM,GAAI;AAC/B,gBAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,gBAAM,UAAU,QAAQ,MAAM,OAAO;AACrC,kBAAQ,KAAK,GAAG;AAAA,QAClB;AAAA,MACF;AAKA,YAAM,UAAU,KAAK,KAAK,0BAA0B;AACpD,YAAM,cAAc,MAAM,aAAa,OAAO;AAC9C,UAAI,gBAAgB,MAAM;AACxB,cAAM,UAAU,SAAS,4BAA4B,OAAO;AAC5D,gBAAQ,KAAK,0BAA0B;AAAA,MACzC,WAAW,YAAY,SAAS,oBAAoB,GAAG;AAAA,MAEvD,WAAW,YAAY,KAAK,MAAM,0BAA0B,KAAK,GAAG;AAClE,cAAM,UAAU,SAAS,4BAA4B,OAAO;AAC5D,gBAAQ,KAAK,0BAA0B;AAAA,MACzC,OAAO;AACL,cAAM;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAKA,YAAM,MAAM,QAAQ;AACpB,UAAI,aAAa;AACjB,UAAI,YAAY,CAAC;AACjB,UAAI,CAAC,IAAI,QAAQ,YAAY,GAAG;AAC9B,YAAI,QAAQ,YAAY,IAAI;AAC5B,qBAAa;AAAA,MACf;AAMA,YAAM,YAAY,CAAC,EAAE,IAAI,iBAAiB,UAAU,IAAI,cAAc;AACtE,YAAM,eAAe,IAAI,QAAQ,MAAM;AACvC,UAAI,aAAa,iBAAiB,QAAW;AAC3C,YAAI,CAAC,IAAI,QAAQ,WAAW,GAAG;AAC7B,cAAI,QAAQ,WAAW,IAAI,gBAAgB;AAC3C,uBAAa;AAAA,QACf;AACA,YAAI,IAAI,QAAQ,MAAM,MAAM,QAAW;AACrC,cAAI,QAAQ,MAAM,IAAI;AACtB,uBAAa;AAAA,QACf;AAAA,MACF;AACA,UAAI,cAAc;AAClB,YAAM,gBACJ,CAAC,CAAC,IAAI,kBAAkB,kBAAkB,KAAK,CAAC,CAAC,IAAI,eAAe,kBAAkB;AACxF,UAAI,CAAC,eAAe;AAClB,YAAI,oBAAoB,CAAC;AACzB,YAAI,gBAAgB,kBAAkB,IAAI;AAC1C,sBAAc;AACd,qBAAa;AAAA,MACf;AACA,UAAI,YAAY;AACd,cAAM,UAAU,QAAQ,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,MAAM,OAAO;AAC7E,gBAAQ,KAAK,cAAc;AAAA,MAC7B;AAIA,UAAI,aAAa;AACf,cAAM,MAAM,MAAM,KAAK,MAAM,QAAQ,CAAC,SAAS,GAAG,EAAE,KAAK,WAAW,KAAK,CAAC;AAC1E,YAAI,IAAI,SAAS,GAAG;AAClB,iBAAO,EAAE,MAAM,UAAU,OAAO,6BAA6B,IAAI,IAAI,IAAI;AAAA,QAC3E;AAAA,MACF;AAMA,UAAI,CAAE,MAAM,mBAAmB,KAAK,OAAO,KAAK,OAAO,GAAI;AACzD,cAAM,KAAK,kBAAkB;AAAA,MAC/B;AAIA,YAAM,OAAO,+EAA+E;AAC5F,aAAO,MAAM,SAAS,IAAI,EAAE,MAAM,MAAM,OAAO,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK;AAAA,IACnF;AAAA,EACF,CAAC;AACH;","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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
35
|
+
import "../../chunk-C6B4RHXR.js";
|
|
36
36
|
|
|
37
37
|
// src/cli/commands/audit.ts
|
|
38
38
|
import { resolve } from "path";
|
package/dist/configs/svelte.d.ts
CHANGED
|
@@ -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 };
|
package/dist/configs/svelte.js
CHANGED
|
@@ -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": [
|
|
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":"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","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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
54
|
+
//# sourceMappingURL=smoke-suite-TMCSOYRK.js.map
|
package/package.json
CHANGED
|
@@ -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":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|