UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

227 lines (223 loc) • 9.4 kB
const require_fs = require('../fs.cjs'); const require_upstream_ref = require('../upstream-ref.cjs'); let effect = require("effect"); //#region src/services/scaffold/upgrade-audit/steps/port-route-guards.ts /** * Ports the upstream route-guard patterns onto a v2 child app. Upstream * route files already delegate session resolution to the framework guard * surface — requireSession on authed mounts, resolveSessionFromCache in * the root — which is what keeps every guard on one session semantics and * prevents the /login ↔ guarded-route redirect ping-pong ("Too many * redirects" is the router's redirect-budget tripwire). * * Unlike the v1→v2 apply pipeline (gated by the pre-upgrade audit), this * step is a maintenance port for post-upgrade trees and runs standalone: * `pnpm port-route-guards [--dry-run]` * * Upstream snapshots: fixtures/upstream/routes/<file> at UPSTREAM_REF. * * Scope decisions, deliberate divergences from upstream main/HEAD: * - `_authenticated`/`_admin` are rewritten whole (tiny, pure delegation). * - `__root.tsx` is patched surgically (beforeLoad/loader session wiring * only) — child roots commonly carry app-owned chrome that upstream * lacks, and bos sync does not overwrite hand-placed route bodies. * - `_public.tsx` is reference-only (upstream's focused-login / * signed-in-shell form needs a PublicShell API this repo may not have) — * noted, not applied. */ const MOUNT_PATH = "ui/src/routes/_authenticated.tsx"; const ADMIN_PATH = "ui/src/routes/_admin.tsx"; const ROOT_PATH = "ui/src/routes/__root.tsx"; const renderAuthenticated = () => [ "import { createFileRoute, Outlet } from \"@tanstack/react-router\";", "import { requireSession } from \"@/lib/auth\";", "", "export const Route = createFileRoute(\"/_authenticated\")({", " beforeLoad: requireSession,", " component: AuthenticatedLayout,", "});", "", "// The shell is rendered once by the root route.", "function AuthenticatedLayout() {", " return <Outlet />;", "}", "" ].join("\n"); const renderAdmin = () => [ "import { createFileRoute, Outlet, redirect } from \"@tanstack/react-router\";", "import { requireSession } from \"@/lib/auth\";", "", "export const Route = createFileRoute(\"/_admin\")({", " beforeLoad: async (args) => {", " const result = await requireSession(args);", " if (result.session.user?.role !== \"admin\") {", " throw redirect({ to: \"/dashboard\", search: { restricted: \"admin\" } });", " }", " return result;", " },", " component: AdminGate,", "});", "", "function AdminGate() {", " return <Outlet />;", "}", "" ].join("\n"); /** Detects the old raw pass-through wiring that must be replaced. */ const hasRawRootSession = (source) => /const session = context\.session;/.test(source); const resolveSessionCacheImport = "import { resolveSessionFromCache } from \"@/lib/auth\";\n"; const AUTH_LIB_PATH = "ui/src/lib/auth.ts"; const RESOLVE_SESSION_SOURCE = `/** * LOCAL COPY — stand-in for \`resolveSessionFromCache\` in * \`everything-dev/ui/auth\` (the upstream canonical source; the main app's * ui/src/lib/auth.ts is a pure re-export of it). This local copy exists * because the pinned framework release predates the v2 train and does not * publish \`ui/auth\`. * * Semantics to preserve when conforming to upstream: * 1. Cache-first — the query cache entry wins over the server-passed * session. * 2. An explicit signed-out \`null\` cache entry must be honored (return * null, not the stale context session) — this is the fix for the * sign-out-then-bounce-back redirect-loop class. * * Exit path (once on a framework version that publishes ui/auth): replace * this function and requireSession below with * export { requireSession, resolveSessionFromCache } from "everything-dev/ui/auth"; * then run \`pnpm run typecheck\`. */ export function resolveSessionFromCache( queryClient: { getQueryData: (key: unknown[]) => unknown }, session: SessionData | null | undefined ): SessionData | null { const cached = queryClient.getQueryData(sessionQueryKey); if (cached !== undefined) return cached as SessionData | null; return session ?? null; } `; const REQUIRE_SESSION_SOURCE = `/** * LOCAL COPY — stand-in for \`requireSession\` in \`everything-dev/ui/auth\`. * See resolveSessionFromCache above for why this local copy exists and how * to migrate off it. * * Semantics to preserve when conforming to upstream: this is the single * shared session gate for authed mounts — every guard must delegate here * (rather than inlining sessionQueryOptions/authClient.getSession) so all * guards share one resolution semantics and cannot disagree into a * /login <-> protected-route redirect ping-pong. */ export function requireSession(args: { context: { session: SessionData | null | undefined }; }): { session: SessionData | null } { const session = args.context.session; if (!session?.user) { throw new Error("No active session"); } return { session }; } `; /** Ensures ui/src/lib/auth.ts exports resolveSessionFromCache and requireSession. */ const ensureAuthSurface = (projectDir, dryRun) => effect.Effect.gen(function* () { const writes = []; const notes = []; if (!(yield* require_fs.fileExists(projectDir, AUTH_LIB_PATH))) { notes.push(`${AUTH_LIB_PATH} absent — auth surface not injected`); return { writes, notes }; } let source = yield* require_fs.readText(projectDir, AUTH_LIB_PATH); const hasResolve = /export\s+function\s+resolveSessionFromCache/.test(source); const hasRequire = /export\s+function\s+requireSession/.test(source); if (hasResolve && hasRequire) { notes.push(`${AUTH_LIB_PATH} already exports resolveSessionFromCache and requireSession`); return { writes, notes }; } if (!/SessionData/.test(source)) { notes.push(`${AUTH_LIB_PATH}: SessionData type not found — auth surface injection skipped`); return { writes, notes }; } if (!hasResolve) { source = RESOLVE_SESSION_SOURCE + source; writes.push(AUTH_LIB_PATH); notes.push(`${AUTH_LIB_PATH}: added resolveSessionFromCache export`); } if (!hasRequire) { source = REQUIRE_SESSION_SOURCE + source; writes.push(AUTH_LIB_PATH); notes.push(`${AUTH_LIB_PATH}: added requireSession export`); } if (!dryRun) yield* require_fs.writeText(projectDir, AUTH_LIB_PATH, source); return { writes, notes }; }); const portRouteGuards = (projectDir, dryRun) => effect.Effect.gen(function* () { const writes = []; const notes = []; const surface = yield* ensureAuthSurface(projectDir, dryRun); writes.push(...surface.writes); notes.push(...surface.notes); for (const [path, render, guardId] of [[ MOUNT_PATH, renderAuthenticated, "sessionQueryOptions(" ], [ ADMIN_PATH, renderAdmin, "sessionQueryOptions(" ]]) { if (!(yield* require_fs.fileExists(projectDir, path))) { notes.push(`${path} absent — nothing to port`); continue; } const source = yield* require_fs.readText(projectDir, path); if (source.includes("requireSession")) { notes.push(`${path} already delegates to the framework guard`); continue; } if (!source.includes(guardId)) notes.push(`${path}: session guard pattern not recognized — replaced anyway`); if (!dryRun) yield* require_fs.writeText(projectDir, path, render()); writes.push(path); } if (!(yield* require_fs.fileExists(projectDir, ROOT_PATH))) notes.push(`${ROOT_PATH} absent — nothing to patch`); else { let source = yield* require_fs.readText(projectDir, ROOT_PATH); if (/resolveSessionFromCache/.test(source)) notes.push("root session wiring already uses resolveSessionFromCache"); else if (!hasRawRootSession(source)) notes.push("root session wiring unrecognized (no `const session = context.session;`) — inspect manually"); else { source = source.replace("const session = context.session;", "const session = resolveSessionFromCache(context.queryClient, context.session);"); const loaderPattern = /const session = context\.session;\s*if \(session && queryClient\) \{\n\s*queryClient\.setQueryData\(sessionQueryKey, session\);/; const loaderReplacement = [ "const session = resolveSessionFromCache(queryClient, context.session);", "", " if (session !== undefined && queryClient) {", " queryClient.setQueryData(sessionQueryKey, session);" ].join("\n"); if (loaderPattern.test(source)) { source = source.replace(loaderPattern, loaderReplacement); notes.push("root loader: raw pass-through → resolveSessionFromCache with explicit signed-out seeding"); } else notes.push("root loader pattern not matched — only beforeLoad patched; inspect loader wiring manually"); if (!/resolveSessionFromCache/.test(source.split("\n").filter((l) => l.startsWith("import")).join("\n"))) source = `${resolveSessionCacheImport}${source}`; if (!dryRun) yield* require_fs.writeText(projectDir, ROOT_PATH, source); writes.push(ROOT_PATH); } } notes.push("upstream reference-only: _public.tsx focused-login / signed-in-shell semantics were NOT applied — port separately if the PublicShell API supports it", `snapshots pinned at ${require_upstream_ref.UPSTREAM_REF} under fixtures/upstream/routes/`); return { writes, deletes: [], notes }; }); //#endregion exports.portRouteGuards = portRouteGuards; //# sourceMappingURL=port-route-guards.cjs.map