#!/usr/bin/env bun /** * Lint: the workspace not-found route stays INSIDE the `/w/:slug` element. * * React Router ranks branches by specificity, not source order. A `` at the top level therefore outranks the `/w/:slug` branch for any * unmatched `/w//…` URL — the branch is never entered, so * `WorkspaceRouteGuard` never mounts. * * The guard is what projects the route slug onto the ambient workspace. Skip it * and the shell keeps serving the previously-focused workspace's placements, so * an app that IS installed in the workspace the URL names gets reported as gone. * Reachable by pressing Back after a workspace switch, or by opening a shared * `/w//app/` link. * * This shipped wrong once already, which is why it is a gate rather than a * comment. It cannot be a unit test: React Router rejects a custom component * that returns `` elements, so the route table can't be mounted in * isolation without restructuring it into a `useRoutes` config. * * What this asserts, against `web/src/App.tsx`: * - the `` element has a direct `` child * - that child renders `WorkspaceNotFoundPage` (the slug-derived settledness * wrapper — the bare `NotFoundPage` would compare against lagging ambient * state and paint a false "not available" mid-switch) * - a `` also exists OUTSIDE the workspace element, so a path * with no workspace at all (`/nonsense`) still lands somewhere * * Position is only asserted for the nested one: the top-level splat is free to * move within the table, but deleting it restores the white screen for every * non-workspace URL, so its existence is checked. */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import * as ts from "typescript"; const ROOT = join(import.meta.dirname ?? __dirname, ".."); const TARGET = join(ROOT, "web/src/App.tsx"); const WORKSPACE_PATH = "/w/:slug"; const SPLAT = "*"; const REQUIRED_ELEMENT = "WorkspaceNotFoundPage"; /** The literal value of a JSX attribute, or undefined when it isn't a plain string. */ function stringAttr(node: ts.JsxOpeningLikeElement, name: string): string | undefined { for (const prop of node.attributes.properties) { if (!ts.isJsxAttribute(prop) || prop.name.getText() !== name) continue; const init = prop.initializer; if (init && ts.isStringLiteral(init)) return init.text; } return undefined; } /** Does this ``'s `element={...}` mention `WorkspaceNotFoundPage`? */ function elementMentions(node: ts.JsxOpeningLikeElement, name: string): boolean { for (const prop of node.attributes.properties) { if (!ts.isJsxAttribute(prop) || prop.name.getText() !== "element") continue; return prop.initializer?.getText().includes(name) ?? false; } return false; } function openingOf(node: ts.Node): ts.JsxOpeningLikeElement | undefined { if (ts.isJsxElement(node)) return node.openingElement; if (ts.isJsxSelfClosingElement(node)) return node; return undefined; } const source = ts.createSourceFile( TARGET, readFileSync(TARGET, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); let workspaceRoute: ts.JsxElement | undefined; (function find(node: ts.Node): void { if (ts.isJsxElement(node)) { const opening = node.openingElement; if (opening.tagName.getText() === "Route" && stringAttr(opening, "path") === WORKSPACE_PATH) { workspaceRoute = node; return; } } ts.forEachChild(node, find); })(source); if (!workspaceRoute) { console.error( `✗ check:workspace-splat-nesting — no element found in web/src/App.tsx.\n` + " If the workspace route was renamed, update this check with it.", ); process.exit(1); } const nested = workspaceRoute.children .map(openingOf) .filter((o): o is ts.JsxOpeningLikeElement => !!o) .filter((o) => o.tagName.getText() === "Route" && stringAttr(o, "path") === SPLAT); if (nested.length === 0) { console.error( `✗ check:workspace-splat-nesting — has no child.\n\n` + " A top-level splat cannot serve workspace-scoped misses: React Router ranks by\n" + " specificity, so it swallows /w//… whole and WorkspaceRouteGuard never\n" + " mounts. Without the guard the shell stays on the previously-focused workspace\n" + " and reports an installed app as gone. Keep the nested splat.", ); process.exit(1); } const wrong = nested.filter((o) => !elementMentions(o, REQUIRED_ELEMENT)); if (wrong.length > 0) { console.error( `✗ check:workspace-splat-nesting — the nested must render ${REQUIRED_ELEMENT}.\n\n` + " It derives settledness from the route slug. The bare NotFoundPage takes a\n" + " `settled` prop computed from ambient workspace state, which lags a switch by a\n" + " render — reporting settled while both sides still name the previous workspace,\n" + ' and painting "not available" for an app that is installed in the one the URL names.', ); process.exit(1); } // A splat outside the workspace element — the terminal route for everything // else. Position is irrelevant (React Router ranks by specificity), existence is // not: without it an unmatched non-workspace path renders `null` into the main // area, which is the white screen this whole gate exists to keep closed. const wsStart = workspaceRoute.getStart(); const wsEnd = workspaceRoute.getEnd(); let hasTopLevelSplat = false; (function findSplat(node: ts.Node): void { if (hasTopLevelSplat) return; const opening = openingOf(node); if ( opening && opening.tagName.getText() === "Route" && stringAttr(opening, "path") === SPLAT && (node.getStart() < wsStart || node.getEnd() > wsEnd) ) { hasTopLevelSplat = true; return; } ts.forEachChild(node, findSplat); })(source); if (!hasTopLevelSplat) { console.error( `✗ check-workspace-splat-nesting — no outside .\n\n` + " Workspace-scoped misses are covered by the nested splat, but a path with no\n" + " workspace at all then matches nothing and renders null into the main\n" + " area — a blank page that reads as a broken app. Keep a terminal splat outside\n" + " the workspace element.", ); process.exit(1); } console.log("✓ workspace not-found route is nested under /w/:slug and slug-derived");