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