import * as fs from "node:fs"; import * as path from "node:path"; import type { MigrationContext, ReviewItem } from "./types"; import { logPhase } from "./types"; // Word-only keywords are matched with \b boundaries so a generic English // word like "collapse" doesn't misfire on an unrelated finding's reason // text (e.g. a JS/UX note that happens to mention "collapse" in a // non-DaisyUI sense). Path-/symbol-like keywords (containing non-word // characters) are matched as plain substrings since \b doesn't apply to them. const CSS_REVIEW_KEYWORDS = [ "tailwind.config.ts", "safelist", "oklch", "css-styling.md", "@theme", "@apply", "@utility", "DaisyUI", "collapse", ]; const CSS_REVIEW_KEYWORD_MATCHERS = CSS_REVIEW_KEYWORDS.map((k) => /^\w+$/.test(k) ? new RegExp(`\\b${k}\\b`, "i") : k, ); function isCssReviewItem(item: ReviewItem): boolean { return ( item.file.endsWith("app.css") || item.file === "tailwind.config.ts" || CSS_REVIEW_KEYWORD_MATCHERS.some((k) => typeof k === "string" ? item.reason.includes(k) : k.test(item.reason), ) ); } const FRAMEWORK_FINDINGS = [ "Session/analytics SDK is boilerplate duplicated across all sites — should be a single framework function", "GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/blocks", "Route files (__root.tsx, index.tsx, $.tsx, deco/*) are near-identical across sites — should be generated by framework", "server.ts, worker-entry.ts, router.tsx are pure boilerplate — should be a single createSite() call", "setup.ts section registration via import.meta.glob is 100% boilerplate — framework should handle this", "runtime.ts invoke proxy is identical across sites — already in @decocms/blocks/sdk/invoke but sites still have local copies", "apps/site.ts is mostly empty after migration — platform config should be in a config file, not code", ]; export function report(ctx: MigrationContext): void { logPhase("Report"); ctx.frameworkFindings = FRAMEWORK_FINDINGS; const lines: string[] = []; lines.push("# Migration Report"); lines.push(""); lines.push(`**Site:** ${ctx.siteName}`); lines.push(`**Platform:** ${ctx.platform}`); lines.push(`**GTM ID:** ${ctx.gtmId || "none"}`); lines.push(`**Date:** ${new Date().toISOString().split("T")[0]}`); lines.push(`**Mode:** ${ctx.dryRun ? "DRY RUN" : "EXECUTED"}`); lines.push(""); // Summary lines.push("## Summary"); lines.push(""); lines.push(`| Metric | Count |`); lines.push(`|--------|-------|`); lines.push(`| Files analyzed | ${ctx.files.length} |`); lines.push(`| Files scaffolded | ${ctx.scaffoldedFiles.length} |`); lines.push(`| Files transformed | ${ctx.transformedFiles.length} |`); lines.push(`| Files deleted | ${ctx.deletedFiles.length} |`); lines.push(`| Files moved | ${ctx.movedFiles.length} |`); lines.push( `| Manual review items | ${ctx.manualReviewItems.length} |`, ); lines.push(""); // Scaffolded files lines.push("## Scaffolded Files (new)"); lines.push(""); for (const f of ctx.scaffoldedFiles) { lines.push(`- \`${f}\``); } lines.push(""); // Transformed files lines.push("## Transformed Files"); lines.push(""); for (const f of ctx.transformedFiles) { lines.push(`- \`${f}\``); } lines.push(""); // Deleted files lines.push("## Deleted Files"); lines.push(""); for (const f of ctx.deletedFiles) { lines.push(`- \`${f}\``); } lines.push(""); // Moved files if (ctx.movedFiles.length > 0) { lines.push("## Moved Files"); lines.push(""); for (const { from, to } of ctx.movedFiles) { lines.push(`- \`${from}\` → \`${to}\``); } lines.push(""); } // Manual review if (ctx.manualReviewItems.length > 0) { lines.push("## Manual Review Required"); lines.push(""); for (const item of ctx.manualReviewItems) { const icon = item.severity === "error" ? "🔴" : item.severity === "warning" ? "🟡" : "🔵"; lines.push(`${icon} **\`${item.file}\`**: ${item.reason}`); } lines.push(""); } // Analyzer summaries if (ctx.sectionMetas.length > 0) { lines.push("## Section Analysis"); lines.push(""); const withLoader = ctx.sectionMetas.filter((m) => m.hasLoader).length; const layouts = ctx.sectionMetas.filter((m) => m.isHeader || m.isFooter || m.isTheme).length; const listings = ctx.sectionMetas.filter((m) => m.isListing).length; lines.push(`- **${ctx.sectionMetas.length}** sections analyzed`); lines.push(`- **${withLoader}** have loaders (extracted to \`setup/section-loaders.ts\`)`); lines.push(`- **${layouts}** are layout sections (eager + sync + layout)`); lines.push(`- **${listings}** are listing sections (cache = "listing")`); lines.push(""); } if (ctx.islandClassifications.length > 0) { const wrappers = ctx.islandClassifications.filter((c) => c.type === "wrapper").length; const standalone = ctx.islandClassifications.filter((c) => c.type === "standalone").length; lines.push("## Island Elimination"); lines.push(""); lines.push(`- **${ctx.islandClassifications.length}** islands classified`); lines.push(`- **${wrappers}** wrappers (deleted, imports repointed)`); lines.push(`- **${standalone}** standalone (moved to \`src/components/\`)`); lines.push(""); } if (ctx.loaderInventory.length > 0) { const custom = ctx.loaderInventory.filter((l) => l.isCustom).length; const mapped = ctx.loaderInventory.filter((l) => l.appsEquivalent).length; lines.push("## Loader Inventory"); lines.push(""); lines.push(`- **${ctx.loaderInventory.length}** loaders inventoried`); lines.push(`- **${mapped}** mapped to \`@decocms/apps-*\` equivalents`); lines.push(`- **${custom}** custom (included in \`setup/commerce-loaders.ts\`)`); lines.push(""); } // CSS Migration — replaces the old generic "DaisyUI/Tailwind" checkboxes // with what was actually ported/detected for THIS site, so a human // doesn't have to re-derive it from scratch. lines.push("## CSS Migration"); lines.push(""); const twColorCount = Object.keys(ctx.tailwindConfig.colors).length; const twFontCount = Object.keys(ctx.tailwindConfig.fontFamily).length; const twScreenCount = Object.keys(ctx.tailwindConfig.screens).length; const twSafelistCount = ctx.tailwindConfig.safelist.length; lines.push( `- **tailwind.config.ts porting**: ${twColorCount} color(s), ${twFontCount} font stack(s), ${twScreenCount} breakpoint(s) ported into \`src/styles/app.css\` \`@theme\`; ${twSafelistCount} safelist entrie(s) ported via \`@source inline(...)\`.`, ); lines.push( "- **Real CSS compile check**: runs in the compile phase (`npx @tailwindcss/cli`) against `src/styles/app.css` — catches unknown-utility-class errors before they ship. Re-run any time with `bun run tailwind:lint`.", ); const cssReviewItems = ctx.manualReviewItems.filter(isCssReviewItem); if (cssReviewItems.length > 0) { lines.push(""); lines.push(`**${cssReviewItems.length} CSS-specific finding(s) requiring manual review:**`); lines.push(""); for (const item of cssReviewItems) { const icon = item.severity === "error" ? "🔴" : item.severity === "warning" ? "🟡" : "🔵"; lines.push(`${icon} **\`${item.file}\`**: ${item.reason}`); } } else { lines.push(""); lines.push("- No CSS-specific findings from this run."); } lines.push(""); // Always-present manual review items lines.push("## Always Check (site-specific)"); lines.push(""); lines.push("- [ ] `src/setup/commerce-loaders.ts` — verify each loader mapping is correct"); lines.push("- [ ] `src/setup/section-loaders.ts` — verify extracted loaders work correctly"); lines.push("- [ ] `src/hooks/useCart.ts` — wire to actual server functions for your platform"); lines.push("- [ ] `src/worker-entry.ts` — verify CSP, proxy, and segment builder"); lines.push("- [ ] See the **CSS Migration** section above for DaisyUI v4→v5 / Tailwind v3→v4 findings specific to this site"); lines.push("- [ ] Run `npm run generate` (unified: blocks, sections, loaders, schema) after migration"); lines.push(""); // Known Issues lines.push("## Known Issues (Tailwind v3 → v4 + React)"); lines.push(""); lines.push("### Negative z-index on background images"); lines.push(""); lines.push("The migration script automatically converts `-z-{n}` to `z-0` on `` and `` elements."); lines.push("However, if you have **non-image elements** with negative z-index (e.g. `-z-10` on a `
` used as a background layer), they may become invisible."); lines.push(""); lines.push("**Why it breaks:** In TanStack Start/React, section wrappers (`
`) or parent elements can create"); lines.push("CSS stacking contexts (via `animation`, `transform`, `will-change`, `filter`, `isolation`, etc.)."); lines.push("A child with negative z-index gets trapped inside that stacking context and renders behind the parent's background — making it invisible."); lines.push(""); lines.push("**How to fix:**"); lines.push("1. Replace `-z-{n}` with `z-0` on the background element"); lines.push("2. Content siblings render on top naturally via DOM order (they come after in the HTML)"); lines.push("3. If needed, add `relative z-10` to content siblings to ensure they stay above"); lines.push(""); lines.push("**How to detect:** Search for remaining negative z-index: `grep -rn '\\-z-' src/ --include='*.tsx'`"); lines.push(""); lines.push("### Opacity utility classes"); lines.push(""); lines.push("Tailwind v4 removed `bg-opacity-{n}`, `text-opacity-{n}`, etc. The script converts them to"); lines.push("the modifier syntax (e.g. `bg-black bg-opacity-20` → `bg-black/20`). If a color and its opacity"); lines.push("are not in the same className string (e.g. set via different conditional branches), the script"); lines.push("flags them for manual review."); lines.push(""); lines.push("**How to detect:** `grep -rn 'opacity-' src/ --include='*.tsx'`"); lines.push(""); // Framework findings lines.push("## Framework Findings"); lines.push(""); lines.push( "> These are patterns found during migration that should eventually be handled by `@decocms/blocks` instead of being duplicated in every site.", ); lines.push(""); for (const finding of ctx.frameworkFindings) { lines.push(`- ${finding}`); } lines.push(""); // Next steps lines.push("## Next Steps"); lines.push(""); lines.push("```bash"); lines.push("# 1. Install dependencies (bun is the canonical PM)"); lines.push("bun install"); lines.push(""); lines.push("# 2. Generate CMS artifacts (blocks, sections, loaders, schema)"); lines.push("bun run generate"); lines.push(""); lines.push("# 3. Generate routes"); lines.push("bunx tsr generate"); lines.push(""); lines.push("# 4. Type check"); lines.push("bunx tsc --noEmit"); lines.push(""); lines.push("# 5. Find unused code"); lines.push("bun run knip"); lines.push(""); lines.push("# 6. Run dev server"); lines.push("npm run dev"); lines.push(""); lines.push("# 7. (Once wrangler.jsonc is wired) enable CF-native observability"); lines.push("# Adds the canonical `observability` block to wrangler.jsonc so the"); lines.push("# Cloudflare runtime captures console.* logs and auto-instrumented"); lines.push("# traces directly into the CF dashboard (Workers & Pages -> "); lines.push("# -> Observability). No external destination required — the CF"); lines.push("# dashboard is the destination."); lines.push("#"); lines.push("# Forwarding to a future ClickHouse-backed OTel collector is on the"); lines.push("# roadmap (placeholder lives at"); lines.push("# @decocms/blocks/sdk/otelAdapters/clickhouseCollector); when it"); lines.push("# ships, the codemod gains a `--destination-logs` /"); lines.push("# `--destination-traces` flag to point at it."); lines.push("npx -p @decocms/blocks-cli deco-cf-observability # dry-run"); lines.push("npx -p @decocms/blocks-cli deco-cf-observability --write # apply"); lines.push("```"); lines.push(""); const content = lines.join("\n"); if (ctx.dryRun) { console.log("\n" + content); } else { const reportPath = path.join(ctx.sourceDir, "MIGRATION_REPORT.md"); fs.writeFileSync(reportPath, content, "utf-8"); console.log(` Report written to MIGRATION_REPORT.md`); } // Print summary to console console.log(`\n === Migration ${ctx.dryRun ? "(DRY RUN)" : "COMPLETE"} ===`); console.log(` Scaffolded: ${ctx.scaffoldedFiles.length}`); console.log(` Transformed: ${ctx.transformedFiles.length}`); console.log(` Deleted: ${ctx.deletedFiles.length}`); console.log(` Moved: ${ctx.movedFiles.length}`); console.log(` Manual review: ${ctx.manualReviewItems.length}`); }