import * as fs from "node:fs"; import * as path from "node:path"; import type { MigrationContext } from "../types"; function discoverFonts(ctx: MigrationContext): string[] { const fontsDir = path.join(ctx.sourceDir, "public", "fonts"); if (!fs.existsSync(fontsDir)) { const staticFonts = path.join(ctx.sourceDir, "static", "fonts"); if (!fs.existsSync(staticFonts)) return []; return scanFontDir(staticFonts); } return scanFontDir(fontsDir); } function scanFontDir(dir: string): string[] { try { return fs.readdirSync(dir) .filter((f) => /\.(woff2)$/i.test(f)) .map((f) => `/fonts/${f}`); } catch { return []; } } export function generateRoutes( ctx: MigrationContext, ): Record { const siteName = ctx.siteName; const siteTitle = siteName.charAt(0).toUpperCase() + siteName.slice(1); const vtexAccount = ctx.vtexAccount || siteName; return { "src/routes/__root.tsx": generateRoot(ctx, siteTitle, vtexAccount), "src/routes/index.tsx": generateIndex(ctx, siteTitle), "src/routes/$.tsx": generateCatchAll(ctx, siteTitle), "src/routes/deco/meta.ts": generateDecoMeta(), "src/routes/deco/invoke.$.ts": generateDecoInvoke(), "src/routes/deco/render.ts": generateDecoRender(), }; } function generateRoot(ctx: MigrationContext, siteTitle: string, vtexAccount: string): string { const fonts = discoverFonts(ctx); const isVtex = ctx.platform === "vtex"; const deployedSiteName = `${ctx.siteName}-tanstack`; // Build preconnect list based on platform const preconnects: string[] = []; preconnects.push(` { rel: "preconnect", href: "https://decoims.com", crossOrigin: "anonymous" as const },`); if (isVtex) { preconnects.push(` { rel: "preconnect", href: "https://${vtexAccount}.vtexassets.com", crossOrigin: "anonymous" as const },`); } const googleFontLinks = [ ...ctx.googleFonts.preconnects.map((href) => ` { rel: "preconnect", href: "${href}"${href.includes("gstatic") ? ', crossOrigin: "anonymous" as const' : ""} },` ), ...ctx.googleFonts.stylesheets.map((href) => ` { rel: "stylesheet", href: "${href}" },` ), ]; const fontPreloads = fonts.map((f) => ` { rel: "preload", href: "${f}", as: "font", type: "font/woff2", crossOrigin: "anonymous" as const },` ); const dnsPrefetch: string[] = []; if (isVtex) { dnsPrefetch.push(` { rel: "dns-prefetch", href: "https://sp.vtex.com" },`); } const linkLines = [...preconnects, ...googleFontLinks, ...fontPreloads].filter(Boolean); return `import { createRootRoute } from "@tanstack/react-router"; import { DecoRootLayout } from "@decocms/tanstack"; // @ts-ignore Vite ?url import import appCss from "../styles/app.css?url"; // MIGRATION TODO: customize description, OG image, and locale for ${siteTitle}. // The migration scaffold leaves a generic default so it never falls through; // CMS \`Site.seo\` overrides this once block resolution kicks in. const DEFAULT_DESCRIPTION = "${siteTitle}"; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: "${siteTitle}" }, { name: "description", content: DEFAULT_DESCRIPTION }, { property: "og:site_name", content: "${siteTitle}" }, { property: "og:locale", content: "pt_BR" }, ], links: [ ${linkLines.join("\n")} { rel: "stylesheet", href: appCss }, { rel: "icon", href: "/favicon.ico" }, ${dnsPrefetch.join("\n")} ], }), component: RootLayout, }); function RootLayout() { return ( ); } `; } function generateIndex(ctx: MigrationContext, siteTitle: string): string { return `import { createFileRoute } from "@tanstack/react-router"; import { cmsHomeRouteConfig, DecoPageRenderer } from "@decocms/tanstack"; import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader"; // MIGRATION TODO: customize defaultTitle / defaultDescription / fallback // copy below for ${siteTitle}. CMS \`Site.seo\` overrides these once block // resolution kicks in, so leaving the migration scaffold defaults is safe // but visible in pre-block-resolution states. export const Route = createFileRoute("/")({ ...cmsHomeRouteConfig({ defaultTitle: "${siteTitle}", defaultDescription: "${siteTitle}", siteName: "${siteTitle}", }), component: HomePage, }); function HomePage() { const data = Route.useLoaderData() as Record | null; if (!data) { return (

${siteTitle}

No CMS page registered for /

); } return ( ); } `; } function generateCatchAll(ctx: MigrationContext, siteTitle: string): string { return `import { createFileRoute } from "@tanstack/react-router"; import { cmsRouteConfig, DecoPageRenderer } from "@decocms/tanstack"; import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader"; // MIGRATION TODO: customize defaultTitle / defaultDescription for ${siteTitle} // (CMS \`Site.seo\` overrides these per-page once block resolution kicks in). const routeConfig = cmsRouteConfig({ siteName: "${siteTitle}", defaultTitle: "${siteTitle}", defaultDescription: "${siteTitle}", ignoreSearchParams: ["skuId"], }); export const Route = createFileRoute("/$")({ ...routeConfig, component: CmsPage, notFoundComponent: NotFoundPage, }); function CmsPage() { const data = Route.useLoaderData() as Record | null; if (!data) return ; return ( ); } function PendingPage() { return (
); } function NotFoundPage() { return (

404

Page Not Found

No CMS page block matches this URL.

Go Home
); } `; } // The deco admin routes use the `*RouteConfig()` factories — the only form // @decocms/tanstack exports since 7.10.0. The pre-7.10.0 module-scope // literals (decoMetaRoute/decoRenderRoute/decoInvokeRoute) were removed: // passed by reference, router-core's update() mutated the shared object // (injecting id/path), and any dev-HMR re-execution of the route file then // threw "Route cannot have both an 'id' and a 'path' option", 500ing every // route until dev restart. function generateDecoMeta(): string { return `import { createFileRoute } from "@tanstack/react-router"; import { decoMetaRouteConfig } from "@decocms/tanstack"; export const Route = createFileRoute("/deco/meta")(decoMetaRouteConfig()); `; } function generateDecoInvoke(): string { return `import { createFileRoute } from "@tanstack/react-router"; import { decoInvokeRouteConfig } from "@decocms/tanstack"; export const Route = createFileRoute("/deco/invoke/$")(decoInvokeRouteConfig()); `; } function generateDecoRender(): string { return `import { createFileRoute } from "@tanstack/react-router"; import { decoRenderRouteConfig } from "@decocms/tanstack"; export const Route = createFileRoute("/deco/render")(decoRenderRouteConfig()); `; }