morphit/apps/web/scripts/dev-routes-prod-gated-smoke.ts

158 lines
6 KiB
TypeScript

/**
* dev-routes-prod-gated smoke.
*
* Pins the production gate on the maintainer-only `/dev` route subtree
* (icon catalog, responsive-viewport preview, WebHID YubiKey transport
* probe). These are contributor/maintainer diagnostics — the
* yubikey-probe page is literally headed "DEV ONLY" — and used to be
* reachable in a production build via the SPA fallback.
*
* The gate lives in `[lang]/dev/+layout.ts`: a layout `load` that
* throws `error(404)` when `!import.meta.env.DEV`. Vite statically
* replaces `import.meta.env.DEV` with `false` in the prod bundle, so
* the whole subtree 404s in production while staying fully available
* under `npm run dev`.
*
* This smoke fails if that gate is removed, weakened, or if a new
* `/dev/*` page is added OUTSIDE the gated subtree (which would slip
* past the layout guard).
*/
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
const ANSI_GREEN = '\x1b[32m';
const ANSI_RED = '\x1b[31m';
const ANSI_RESET = '\x1b[0m';
// repo root is two levels up from apps/web/scripts/
const WEB = resolve(fileURLToPath(new URL('..', import.meta.url)));
const DEV_ROOT = resolve(WEB, 'src/routes/[lang]/dev');
const LAYOUT = resolve(DEV_ROOT, '+layout.ts');
interface Result {
name: string;
passed: boolean;
detail?: string;
}
const results: Result[] = [];
function pass(name: string) {
results.push({ name, passed: true });
}
function fail(name: string, detail: string) {
results.push({ name, passed: false, detail });
}
/* ---------------- 1. the gate layout exists ---------------- */
if (existsSync(LAYOUT)) {
pass('[lang]/dev/+layout.ts exists (the subtree gate)');
} else {
fail(
'[lang]/dev/+layout.ts exists',
`no layout at ${LAYOUT} — the /dev subtree has no production gate, so the dev tools are reachable in prod via the SPA fallback`
);
}
const layoutSrc = existsSync(LAYOUT) ? readFileSync(LAYOUT, 'utf8') : '';
/* ---------------- 2. imports SvelteKit's error ---------------- */
if (/import\s*\{[^}]*\berror\b[^}]*\}\s*from\s*['"]@sveltejs\/kit['"]/.test(layoutSrc)) {
pass("imports `error` from '@sveltejs/kit'");
} else {
fail("imports `error` from '@sveltejs/kit'", 'the gate must import SvelteKit `error` to throw a 404');
}
/* ---------------- 3. the load throws 404 gated on !import.meta.env.DEV ---------------- */
// Normalize whitespace so the assertion is robust to formatting.
const flat = layoutSrc.replace(/\s+/g, ' ');
const hasDevGuard = /if\s*\(\s*!\s*import\.meta\.env\.DEV\s*\)/.test(flat);
const throws404 = /throw\s+error\s*\(\s*404/.test(flat);
const hasLoad = /export\s+(async\s+)?function\s+load\b/.test(flat) || /export\s+const\s+load\b/.test(flat);
if (hasLoad && hasDevGuard && throws404) {
pass('load() throws error(404) when !import.meta.env.DEV (404 in prod, open in `npm run dev`)');
} else {
fail(
'load() throws error(404) when !import.meta.env.DEV',
`hasLoad=${hasLoad} hasDevGuard=${hasDevGuard} throws404=${throws404} — the gate must be a load that throws error(404) under !import.meta.env.DEV`
);
}
/* ---------------- 4. prerender disabled for the subtree ---------------- */
if (/export\s+const\s+prerender\s*=\s*false/.test(flat)) {
pass('prerender = false on the /dev subtree (dev-only pages never emitted as prerendered HTML)');
} else {
fail(
'prerender = false on the /dev subtree',
'the dev layout should explicitly set `prerender = false` so the dev-only pages are never prerendered into the release build'
);
}
/* ---------------- 5. every /dev page lives under the gated subtree ---------------- */
// Enumerate all +page.svelte files anywhere under routes/[lang]/dev/.
// (They all sit under DEV_ROOT by construction, so the layout covers
// them — but a future page added at a sibling path like
// routes/[lang]/dev-tools/ would NOT be gated. This asserts the
// known dev pages are all genuinely inside the gated subtree.)
let devPages: string[] = [];
try {
const out = execSync(`find "${DEV_ROOT}" -name '+page.svelte'`, { encoding: 'utf8' });
devPages = out.split('\n').map((s) => s.trim()).filter(Boolean);
} catch {
devPages = [];
}
const allUnderGate = devPages.length > 0 && devPages.every((p) => p.startsWith(DEV_ROOT));
if (allUnderGate) {
pass(`all ${devPages.length} /dev page(s) live under the gated subtree (covered by +layout.ts)`);
} else {
fail(
'all /dev pages live under the gated subtree',
`found ${devPages.length} dev page(s); the gate only covers files under ${DEV_ROOT}. ` +
`If a new diagnostic route was added elsewhere, move it under [lang]/dev/ or gate it too.`
);
}
/* ---------------- 6. the gated subtree actually contains the known dev tools ---------------- */
// Sanity that the gate sits above the real tools (not an empty dir):
// the original three diagnostic pages must be present and gated.
const expected = ['+page.svelte', 'icons/+page.svelte', 'responsive/+page.svelte', 'yubikey-probe/+page.svelte'];
const missing = expected.filter((rel) => !existsSync(resolve(DEV_ROOT, rel)));
if (missing.length === 0) {
pass('the gated subtree contains the dev index + icons + responsive + yubikey-probe pages');
} else {
fail(
'the gated subtree contains the known dev tools',
`missing under [lang]/dev/: ${missing.join(', ')} (if a tool was intentionally removed, update this list)`
);
}
/* ---------------- report ---------------- */
let failed = 0;
for (const r of results) {
if (r.passed) {
console.log(' ' + ANSI_GREEN + '✓' + ANSI_RESET + ' ' + r.name);
} else {
console.log(' ' + ANSI_RED + '✗' + ANSI_RESET + ' ' + r.name);
if (r.detail) console.log(' ' + r.detail);
failed++;
}
}
console.log();
console.log('──────────────────────────────────────────────────────');
if (failed > 0) {
console.log('✗ ' + failed + ' of ' + results.length + ' scenarios failed');
process.exit(1);
} else {
console.log('✓ all ' + results.length + ' scenarios passed');
}