UNPKG

everything-dev

Version:

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

173 lines (171 loc) • 6.6 kB
import { KNOWN_MOUNTS, MOUNT_ALIASES } from "../domain.mjs"; import { fileExists, listFiles, readText, removePath, writeText } from "../fs.mjs"; import { UPSTREAM_REF } from "../upstream-ref.mjs"; import { Effect } from "effect"; //#region src/services/scaffold/upgrade-audit/steps/migrate-route-mounts.ts const ROUTES_GLOB = "ui/src/routes/**/*.{ts,tsx}"; const ROUTE_FILE_RE = /createFileRoute(?:<[^>]*>)?\(\s*["'`]([^"'`]+)["'`]/; /** * Unknown mount id → registered mount id. Child apps commonly root their UI * under `_layout` (v1 convention) which is not in the host mount registry. * Map it to `_public` by default; individual entries can be overridden. */ const MOUNT_REDIRECTS = { layout: "public" }; const mountIdOf = (routePath) => { const segments = routePath.split("/").filter(Boolean); if (segments.length === 0) return null; const first = segments[0]; if (!first.startsWith("_")) return null; return first.slice(1); }; const canonicalMount = (mountId) => MOUNT_ALIASES[mountId] ?? mountId; const isRegistered = (mountId) => KNOWN_MOUNTS.includes(mountId); const mountLayoutFile = (mountId) => `ui/src/routes/_${mountId}.tsx`; const renderMountLayout = (mountId) => { const canonical = canonicalMount(mountId); const needsGate = canonical === "authenticated"; return [ "/**", " * GENERATED mount layout — a minimal Outlet shell (plus the requireSession", " * gate on authed mounts) so rehomed routes sit on a registered mount.", " *", ` * Upstream canonical shapes live in everything-dev's`, ` * upgrade-audit fixtures/upstream/routes/ at UPSTREAM_REF ${UPSTREAM_REF}`, " * — conform to those when adding chrome (headers, session context) to", " * this mount. Chrome from the old layout was NOT carried over; port it", " * manually.", " */", "", `import { createFileRoute, Outlet${needsGate ? ", redirect" : ""} } from "@tanstack/react-router";`, needsGate ? `import { requireSession } from "@/lib/auth";` : "", "", "export const Route = createFileRoute(", ` "/_${canonical}",`, needsGate ? " { beforeLoad: requireSession, component: Outlet }," : " { component: Outlet },", "});", "" ].filter(Boolean).join("\n"); }; const rehomeRoute = (file, source, targetMount) => { const match = source.match(ROUTE_FILE_RE); if (!match) return null; const oldPath = match[1]; const segments = oldPath.split("/").filter(Boolean); if (segments.length === 0) return null; const oldMountDir = `_${segments[0].slice(1)}`; let newPathSegments; if (segments.length >= 2 && segments[1].startsWith("_")) { const segMountId = segments[1].slice(1); const canonical = canonicalMount(segMountId); if (KNOWN_MOUNTS.includes(canonical)) newPathSegments = [`_${canonical}`, ...segments.slice(2)]; else newPathSegments = [`_${targetMount}`, ...segments.slice(1)]; } else newPathSegments = [`_${targetMount}`, ...segments.slice(1)]; const newPath = "/" + newPathSegments.join("/") + (oldPath.endsWith("/") && newPathSegments.length >= 1 ? "/" : ""); const next = source.replace(match[0], match[0].replace(oldPath, newPath)); const parts = file.split("/"); const mountIdx = parts.indexOf(oldMountDir); if (mountIdx === -1) return null; let insertTargetMount = true; if (mountIdx + 1 < parts.length) { const nextSeg = parts[mountIdx + 1]; if (nextSeg.startsWith("_")) { const nextMountId = nextSeg.slice(1); const canonical = canonicalMount(nextMountId); if (KNOWN_MOUNTS.includes(canonical)) insertTargetMount = false; } } return { next, newFile: (insertTargetMount ? [ ...parts.slice(0, mountIdx), `_${targetMount}`, ...parts.slice(mountIdx + 1) ] : [...parts.slice(0, mountIdx), ...parts.slice(mountIdx + 1)]).join("/") }; }; const migrateRouteMounts = (projectDir, dryRun) => Effect.gen(function* () { const writes = []; const deletes = []; const notes = []; const files = (yield* listFiles(projectDir, ROUTES_GLOB)).filter((f) => !f.endsWith(".test.ts")); const rehome = []; for (const file of files) { const source = yield* readText(projectDir, file); const match = source.match(ROUTE_FILE_RE); if (!match) continue; const segments = match[1].split("/").filter(Boolean); if (segments.length === 0) continue; const mountId = mountIdOf(match[1]); if (!mountId || isRegistered(mountId)) continue; let targetMount = "public"; const ownCanonical = canonicalMount(mountId); if (KNOWN_MOUNTS.includes(ownCanonical)) targetMount = ownCanonical; else { for (let i = 1; i < segments.length; i++) { const seg = segments[i]; if (!seg.startsWith("_")) continue; const segMountId = seg.slice(1); const canonical = canonicalMount(segMountId); if (KNOWN_MOUNTS.includes(canonical)) { targetMount = canonical; break; } } if (targetMount === "public" && MOUNT_REDIRECTS[mountId]) targetMount = MOUNT_REDIRECTS[mountId]; } rehome.push({ file, source, targetMount }); } if (rehome.length === 0) { notes.push("no routes on unknown mounts — nothing to rehome"); return { writes, deletes, notes }; } const targetMounts = [...new Set(rehome.map((r) => r.targetMount))]; for (const targetMount of targetMounts) { const path = mountLayoutFile(targetMount); if (yield* fileExists(projectDir, path)) { notes.push(`${path} exists — left untouched`); continue; } if (!dryRun) yield* writeText(projectDir, path, renderMountLayout(targetMount)); writes.push(path); notes.push(`created mount layout ${path}`); } for (const { file, source, targetMount } of rehome) { const result = rehomeRoute(file, source, targetMount); if (!result) { notes.push(`${file}: could not parse route path — left for manual rehome`); continue; } const { next, newFile } = result; if (next !== source && !dryRun) yield* writeText(projectDir, newFile, next); writes.push(newFile); if (!dryRun) yield* removePath(projectDir, file); deletes.push(file); notes.push(`rehomed ${file} → ${newFile}`); } for (const mountId of Object.keys(MOUNT_REDIRECTS)) { const layoutPath = mountLayoutFile(mountId); if (yield* fileExists(projectDir, layoutPath)) { if (!dryRun) yield* removePath(projectDir, layoutPath); deletes.push(layoutPath); notes.push(`removed obsolete mount layout ${layoutPath}`); } } notes.push("mount layouts are minimal Outlet shells — migrate any chrome (Header/Footer, session) from the old layout into the new mount manually"); return { writes, deletes, notes }; }); //#endregion export { migrateRouteMounts }; //# sourceMappingURL=migrate-route-mounts.mjs.map