forked from Mike/freno-dev
nessa.freno.me/terms was a live 404 although AppSettings.termsOfServiceURL links it from Settings > Legal and the paywall. The document existed only on an unmerged branch and had no host-dispatch route, so it could not serve on the subdomain. Carry the Terms of Use page, add the /terms dispatcher, and publish nessa-support@freno.me on terms, privacy, contact and deletion. - src/routes/terms.tsx: host-aware dispatch for nessa; other sites 404. - src/routes/nessa/terms.tsx: the Terms page; its contact section names the support address. - privacy: add the controller clause and the support address. - contact/deletion: show the support address; deletion also accepts a request by email. - subdomain-dispatch.test.ts: list directories before filtering to .tsx files. The old predicate only ever received file names, so it asserted over an empty list and missed the missing /terms dispatch. - support-contact.test.ts: guard that every contact route renders the one address the app shows. App side unchanged: termsOfServiceURL stays https://nessa.freno.me/terms.
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
/**
|
|
* Subdomain routing invariant test.
|
|
*
|
|
* Verifies that every subdomain route (src/routes/<prefix>/<name>) has a
|
|
* corresponding top-level dispatch route (src/routes/<name>.tsx or
|
|
* src/routes/<name>/index.tsx). This is required because production Vercel
|
|
* deployments do not apply vercel.json host rewrites — the Nitro Build Output
|
|
* API config.json replaces them entirely, so host-aware root dispatch is the
|
|
* only mechanism that serves subdomain pages.
|
|
*
|
|
* See docs/subdomain-setup.md and AGENTS.md "Subdomain Routing" for context.
|
|
*/
|
|
import { describe, it, expect } from "bun:test";
|
|
import { readdirSync, existsSync, statSync } from "fs";
|
|
|
|
function isRouteFile(name: string): boolean {
|
|
// Page routes are components; the `.ts` files beside them are pure content /
|
|
// metadata modules (content.ts, meta.ts, *-content.ts) with no default
|
|
// export the router can render, so they are not dispatch targets.
|
|
return name.endsWith(".tsx");
|
|
}
|
|
|
|
function listRouteFiles(dir: string): string[] {
|
|
try {
|
|
return readdirSync(dir).filter(isRouteFile);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function listDirectories(dir: string): string[] {
|
|
try {
|
|
return readdirSync(dir).filter((name) => {
|
|
try {
|
|
return statSync(`${dir}/${name}`).isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function hasDispatchRoute(routeName: string, routesDir: string): boolean {
|
|
// Check for src/routes/<name>.tsx
|
|
if (existsSync(`${routesDir}/${routeName}.tsx`)) {
|
|
return true;
|
|
}
|
|
// Check for src/routes/<name>/index.tsx
|
|
if (existsSync(`${routesDir}/${routeName}/index.tsx`)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
describe("subdomain routing dispatch invariant", () => {
|
|
const routesDir = "./src/routes";
|
|
|
|
it("every subdomain route has a top-level dispatch route", () => {
|
|
// List directory entries before filtering to route files. The previous
|
|
// shape filtered with `listRouteFiles` first, which only ever returns
|
|
// `.tsx`/`.ts` names, so `statSync(...).isDirectory()` was never true
|
|
// and the assertion ran over an empty list — it passed while `/terms`
|
|
// had no dispatch file (FRE-1670).
|
|
const prefixDirs = listDirectories(routesDir)
|
|
// Exclude non-product subdomains
|
|
.filter((name) =>
|
|
["nessa", "lineage", "gaze", "inputhalo", "nook"].includes(name)
|
|
);
|
|
|
|
const missing = [];
|
|
|
|
for (const prefix of prefixDirs) {
|
|
const prefixFiles = listRouteFiles(`${routesDir}/${prefix}`);
|
|
for (const file of prefixFiles) {
|
|
// Skip test files and non-route files
|
|
if (file.includes("test") || file.includes("spec")) {
|
|
continue;
|
|
}
|
|
if (!isRouteFile(file)) {
|
|
continue;
|
|
}
|
|
|
|
// Determine route name (without extension)
|
|
const routeName = file.replace(/\.(tsx|ts)$/, "");
|
|
|
|
// index.tsx maps to the parent directory name
|
|
const effectiveRoute = routeName === "index" ? prefix : routeName;
|
|
|
|
// Skip routes that are intentionally not dispatched to root
|
|
// (these are subdomain-specific, not shared surfaces)
|
|
if (
|
|
effectiveRoute === "content" ||
|
|
effectiveRoute === "meta" ||
|
|
effectiveRoute.endsWith("-content") ||
|
|
effectiveRoute === "password-reset" ||
|
|
effectiveRoute === "request-password-reset" ||
|
|
effectiveRoute === "checkout" ||
|
|
effectiveRoute === "success" ||
|
|
effectiveRoute === "notifications"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (!hasDispatchRoute(effectiveRoute, routesDir)) {
|
|
missing.push(`${prefix}/${file} → missing dispatch for /${effectiveRoute}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(missing).toEqual([]);
|
|
});
|
|
});
|