everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
173 lines (171 loc) • 6.6 kB
JavaScript
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