everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
192 lines (190 loc) • 7.82 kB
JavaScript
import { fileExists, readText, writeText } from "../fs.mjs";
import { detectRouterSeam } from "../checks/ui-seam.mjs";
import { Effect } from "effect";
//#region src/services/scaffold/upgrade-audit/steps/migrate-ui-seam.ts
const ROUTER_PATH = "ui/src/router.tsx";
const ROUTER_CLIENT_PATH = "ui/src/router-client.ts";
const TSCONFIG_PATH = "ui/tsconfig.json";
const CREATE_QUERY_CLIENT_RE = /export\s+function\s+createQueryClient\s*\(\s*\)\s*:\s*QueryClient\s*\{/;
const DEFAULT_COMPONENT_MARKERS = [
["defaultErrorComponent", "Oops!"],
["defaultNotFoundComponent", "Not Found"],
["defaultPendingComponent", "Loading..."]
];
const extractBlock = (source, openIdx) => {
let depth = 0;
for (let i = openIdx; i < source.length; i++) {
const ch = source[i];
if (ch === "{") depth++;
else if (ch === "}") {
depth--;
if (depth === 0) return source.slice(openIdx, i + 1);
}
}
return null;
};
const extractCreateQueryClient = (source) => {
const match = source.match(CREATE_QUERY_CLIENT_RE);
if (!match || match.index === void 0) return null;
const openIdx = source.indexOf("{", match.index + match[0].length - 1);
if (openIdx === -1) return null;
const block = extractBlock(source, openIdx);
if (!block) return null;
return source.slice(match.index, openIdx + block.length).trimEnd();
};
const customComponentNotes = (source) => {
const notes = [];
for (const [name, marker] of DEFAULT_COMPONENT_MARKERS) if (new RegExp(`function\\s+${name}\\s*\\(`).test(source) && !source.includes(marker)) notes.push(`custom ${name} detected — the core router hardcodes its defaults until router-client spreads opts; re-apply it after the spread on a framework version that supports it`);
return notes;
};
const renderAuthoredRouter = (queryClientFn) => [
"/**",
" * Client router — the app's authored router factory, the single customization",
" * seam for router policy (notFound/pending/error components, scroll behavior,",
" * query timings via the `createQueryClient` export). Minted through the",
" * framework's client factory; the composed manifest tree arrives at call",
" * time and wins over the bundled core-only tree.",
" *",
" * This file is yours — scaffolded once, never overwritten by `bos sync`",
" * (ADR 0023). Edit the policy defaults after the `...opts` spread freely.",
" */",
"",
...queryClientFn ? ["import { QueryClient } from \"@tanstack/react-query\";"] : [],
"import { createRouter as createCoreRouter } from \"everything-dev/ui/router-client\";",
"import type { ApiClient, CreateRouterOptions, SessionData } from \"./app\";",
"import { routeTree } from \"./routeTree.gen\";",
"",
"export type {",
" ClientRuntimeConfig,",
" CreateRouterOptions,",
" RouterContext,",
" RouterModule,",
"} from \"./app\";",
"",
"export function createRouter(opts: CreateRouterOptions) {",
" return createCoreRouter<ApiClient, SessionData, typeof routeTree>({",
" ...opts,",
" defaultRouteTree: routeTree,",
" });",
"}",
...queryClientFn ? ["", queryClientFn] : [],
"",
"export { routeTree };",
"",
"declare module \"@tanstack/react-router\" {",
" interface Register {",
" router: ReturnType<typeof createRouter>[\"router\"];",
" }",
"}",
""
].join("\n");
const renderRouterClientShim = () => [
"/**",
" * LOCAL SHIM — stand-in for `everything-dev/ui/router-client`.",
" *",
" * Why this exists: the pinned framework release predates the v2 train and",
" * does not publish `ui/router-client`. This shim lets the authored router",
" * seam (router.tsx importing createCoreRouter from",
" * `everything-dev/ui/router-client`) resolve and typecheck regardless.",
" *",
" * Upstream canonical shape to conform to:",
" * createCoreRouter<ApiClient, SessionData, typeof routeTree>({",
" ...opts,",
" defaultRouteTree: routeTree,",
" })",
" * Conformance rules this shim must keep:",
" * 1. Accept `defaultRouteTree` and forward it as `routeTree`.",
" * 2. Spread caller opts LAST so overrides win over shim defaults.",
" * 3. Preserve the <TApiClient, TSession, TRouteTree> generic signature.",
" *",
" * Detecting native availability (upgrade off this shim):",
" * ls node_modules/everything-dev/dist/ui/router-client.d.mts",
" * v2 releases of everything-dev publish that module.",
" *",
" * Exit path (once on a framework version that publishes ui/router-client):",
" * 1. Delete this file.",
" * 2. Remove the \"everything-dev/ui/router-client\" entry from",
" * ui/tsconfig.json compilerOptions.paths.",
" * 3. Run `pnpm run typecheck` — resolution must fall through to the",
" * framework module with no type errors.",
" *",
" * NOTE: the upgrade audit classifies router.tsx as authored-seam by its",
" * import string, so re-running `bos upgrade` will never revisit this",
" * shim — migration off it is manual by design.",
" */",
"",
"import { createRouter as createTanStackRouter } from \"@tanstack/react-router\";",
"import type { CreateRouterOptions } from \"@tanstack/react-router\";",
"",
"export function createCoreRouter<TApiClient, TSession, TRouteTree extends object>(",
" opts: Omit<CreateRouterOptions<TApiClient, TSession>, \"routeTree\"> & {",
" defaultRouteTree?: TRouteTree;",
" },",
") {",
" return createTanStackRouter({",
" ...opts,",
" ...(opts.defaultRouteTree ? { routeTree: opts.defaultRouteTree } : {}),",
" });",
"}",
""
].join("\n");
const renderTsconfigPathMapping = (existing) => {
const parsed = JSON.parse(existing);
const paths = parsed.compilerOptions?.paths ?? {};
if (paths["everything-dev/ui/router-client"]) return existing;
paths["everything-dev/ui/router-client"] = ["./src/router-client"];
parsed.compilerOptions.paths = paths;
return `${JSON.stringify(parsed, null, 2)}\n`;
};
const migrateUiSeam = (projectDir, dryRun) => Effect.gen(function* () {
if (!(yield* fileExists(projectDir, ROUTER_PATH))) return {
writes: [],
deletes: [],
notes: ["ui/src/router.tsx absent — nothing to migrate"]
};
const source = yield* readText(projectDir, ROUTER_PATH);
const seam = detectRouterSeam(source);
if (seam === "authored-seam") return {
writes: [],
deletes: [],
notes: ["router.tsx already on the authored seam"]
};
if (seam === "unknown") return {
writes: [],
deletes: [],
notes: ["router.tsx is not the known monolithic factory — left untouched, inspect manually"]
};
const queryClientFn = extractCreateQueryClient(source);
const next = renderAuthoredRouter(queryClientFn);
if (!dryRun) yield* writeText(projectDir, ROUTER_PATH, next);
const shimExists = yield* fileExists(projectDir, ROUTER_CLIENT_PATH);
let shimWritten = false;
if (!shimExists) {
if (!dryRun) yield* writeText(projectDir, ROUTER_CLIENT_PATH, renderRouterClientShim());
shimWritten = true;
}
let tsconfigWritten = false;
if (yield* fileExists(projectDir, TSCONFIG_PATH)) {
const tsconfigSource = yield* readText(projectDir, TSCONFIG_PATH);
const nextTsconfig = renderTsconfigPathMapping(tsconfigSource);
if (nextTsconfig !== tsconfigSource && !dryRun) {
yield* writeText(projectDir, TSCONFIG_PATH, nextTsconfig);
tsconfigWritten = true;
}
}
const allWrites = [ROUTER_PATH];
if (shimWritten) allWrites.push(ROUTER_CLIENT_PATH);
if (tsconfigWritten) allWrites.push(TSCONFIG_PATH);
return {
writes: allWrites,
deletes: [],
notes: [
"router factory migrated to the createCoreRouter authored seam (ADR 0023) — the composed manifest tree now wins over the bundled core-only tree",
...queryClientFn ? [] : ["no createQueryClient found in the old factory — the core default query timings apply"],
...customComponentNotes(source)
]
};
});
//#endregion
export { extractCreateQueryClient, migrateUiSeam };
//# sourceMappingURL=migrate-ui-seam.mjs.map