#!/usr/bin/env tsx
/**
* Migration Script: Fresh/Deno/Preact → TanStack Start/React/Cloudflare Workers
*
* Converts a Deco storefront from the old Fresh/Deno stack to the new TanStack Start stack.
* Ships as part of @decocms/blocks-cli — run from a site's root directory.
*
* Usage (from your Fresh site directory):
* npx -p @decocms/blocks-cli deco-migrate [options]
*
* Options:
* --source
Source directory (default: current directory)
* --dry-run Preview changes without writing files
* --verbose Show detailed output
* --help Show this help message
*
* Phases:
* 1. Analyze — Scan source site, categorize files, detect patterns
* 2. Scaffold — Create target structure (configs, routes, infra files)
* 3. Transform — Convert source files (imports, JSX, Fresh APIs, Deno-isms, Tailwind)
* 4. Cleanup — Delete old artifacts, move static → public
* 5. Report — Generate MIGRATION_REPORT.md with findings
* 6. Verify — Smoke test the migrated output
*/
import { execSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { banner, green, red, stat, yellow } from "./migrate/colors";
import { loadConfig, validateConfig } from "./migrate/config";
import { analyze } from "./migrate/phase-analyze";
import { cleanup } from "./migrate/phase-cleanup";
import { cleanupAudit } from "./migrate/phase-cleanup-audit";
import { compile } from "./migrate/phase-compile";
import { report } from "./migrate/phase-report";
import { scaffold } from "./migrate/phase-scaffold";
import { transform } from "./migrate/phase-transform";
import { verify } from "./migrate/phase-verify";
import { detectSourceLayout, explainNonClassicLayout } from "./migrate/source-layout";
import { createContext, logPhase } from "./migrate/types";
function parseArgs(args: string[]): {
source: string;
dryRun: boolean;
verbose: boolean;
help: boolean;
strict: boolean;
withBuild: boolean;
noCompile: boolean;
noCleanupAudit: boolean;
} {
let source = ".";
let dryRun = false;
let verbose = false;
let help = false;
let strict = false;
let withBuild = false;
let noCompile = false;
let noCleanupAudit = false;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--source":
source = args[++i];
break;
case "--dry-run":
dryRun = true;
break;
case "--verbose":
verbose = true;
break;
case "--strict":
strict = true;
break;
case "--with-build":
withBuild = true;
break;
case "--no-compile":
noCompile = true;
break;
case "--no-cleanup-audit":
noCleanupAudit = true;
break;
case "--help":
case "-h":
help = true;
break;
}
}
return {
source,
dryRun,
verbose,
help,
strict,
withBuild,
noCompile,
noCleanupAudit,
};
}
function showHelp() {
console.log(`
@decocms/blocks-cli — Migration Script: Fresh/Deno → TanStack Start
Usage:
npx -p @decocms/blocks-cli deco-migrate [options]
Options:
--source Source directory (default: .)
--dry-run Preview changes without writing files
--verbose Show detailed output for every file
--strict Fail (exit 2) when typecheck/build report errors
--with-build Also run \`vite build\` after typecheck (slower)
--no-compile Skip the post-bootstrap compile phase entirely
--no-cleanup-audit Skip the post-migration cleanup audit (run separately
via \`deco-post-cleanup\` if needed)
--help, -h Show this help message
Examples:
npx -p @decocms/blocks-cli deco-migrate --dry-run --verbose
npx -p @decocms/blocks-cli deco-migrate --source ./my-site
npx -p @decocms/blocks-cli deco-migrate --strict --with-build
npx -p @decocms/blocks-cli deco-migrate
`);
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (opts.help) {
showHelp();
process.exit(0);
}
const sourceDir = path.resolve(opts.source);
banner("@decocms/blocks-cli — Migrate: Fresh/Deno → TanStack Start");
stat("Source", sourceDir);
stat("Mode", opts.dryRun ? yellow("DRY RUN") : green("EXECUTE"));
stat("Verbose", opts.verbose ? "yes" : "no");
// Load optional per-site config from `.deco-migrate.config.json`. Drives
// section-conventions hardcoded lists today; future fields will tune
// import rewrites, scaffolding, etc.
const siteConfig = loadConfig(sourceDir);
if (siteConfig) {
validateConfig(siteConfig);
stat("Config", green(".deco-migrate.config.json (loaded)"));
}
const ctx = createContext(sourceDir, {
dryRun: opts.dryRun,
verbose: opts.verbose,
config: siteConfig,
});
// Phase 0: Source-layout detection. `classic` (root sections/) and `modern`
// (src/sections/) are both supported — the analyzer scans src/ natively for
// modern layouts. Only genuinely ambiguous (`mixed`) or unrecognizable
// (`empty`) layouts abort with an actionable error before we touch files.
const layout = detectSourceLayout(sourceDir);
ctx.layout = layout;
if (layout === "mixed" || layout === "empty") {
console.error(red(`Error: ${layout} source layout`));
console.error("");
console.error(explainNonClassicLayout(layout, sourceDir));
process.exit(2);
}
if (layout === "modern") {
console.log(` Source layout: modern (src/) — scanning src/ natively`);
}
try {
// Phase 1: Analyze source
analyze(ctx);
// Phase 2: Scaffold target structure
scaffold(ctx);
// Phase 3: Transform source files
transform(ctx);
// Phase 4: Cleanup old artifacts
cleanup(ctx);
// Phase 5: Generate report
report(ctx);
// Phase 6: Verify (smoke test)
const ok = verify(ctx);
if (!ok) {
process.exit(2);
}
// Phase 7: Bootstrap (install + generate)
if (!ctx.dryRun) {
bootstrap(ctx);
await provisionAnalytics(ctx.sourceDir);
}
// Phase 8: Compile (typecheck + optional build)
// Skipped in dry-run, when --no-compile is passed, or when bootstrap
// didn't install dependencies (handled inside `compile`).
if (!opts.noCompile) {
const compileResult = compile(ctx, {
strict: opts.strict,
withBuild: opts.withBuild,
});
if (compileResult.shouldFail) {
process.exit(2);
}
}
// Phase 9: Post-migration cleanup audit
// Read-only scan that catches residual debt the migration script
// can't (or won't) fix. Always informational unless --strict is on,
// in which case warning-severity findings exit 2.
if (!opts.noCleanupAudit) {
const auditFailed = cleanupAudit(ctx, { strict: opts.strict });
if (auditFailed) {
process.exit(2);
}
}
} catch (error) {
console.error(`\n ${red("Migration failed:")}`, error);
process.exit(1);
}
}
function bootstrap(ctx: { sourceDir: string }) {
logPhase("Bootstrap (install + generate)");
let failures = 0;
const run = (cmd: string, label: string, critical = false) => {
console.log(` Running: ${label}...`);
try {
execSync(cmd, { cwd: ctx.sourceDir, stdio: "pipe" });
console.log(` ${green("✓")} ${label}`);
} catch (e: any) {
failures++;
const icon = critical ? red("✗") : yellow("⚠");
console.log(` ${icon} ${label} failed: ${e.message?.split("\n")[0]}`);
if (critical) {
console.log(`\n ${red("Bootstrap aborted.")} Fix the error above and run manually.\n`);
return false;
}
}
return true;
};
// bun is the fleet-wide canonical package manager for decocms storefronts.
// We hardcode it here (instead of sniffing process.env.npm_execpath) so a
// freshly-migrated site always commits a bun.lock and never accidentally
// ships a package-lock.json that drifts vs bun.lock under CF Workers Builds.
// See MIGRATION_TOOLING_PLAN.md and the package-json template for the
// matching `packageManager` field that pins the version.
const pm = "bun";
if (!run(`${pm} install`, "Install dependencies", true)) return;
run("bunx tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks.ts", "Generate CMS blocks");
// generate-invoke emits src/server/invoke.gen.ts with top-level
// createServerFn declarations + the forwardResponseCookies bridge that
// propagates VTEX Set-Cookie headers (orderFormId, segment, sc…) to the
// browser. Without this file, the site falls back to the proxy
// `~/runtime.ts` route which hits /deco/invoke and used to drop cookies,
// making the cart appear empty at /checkout after addItemToCart. The
// upstream invoke handler now also forwards cookies correctly, but
// running the generator gives every freshly-migrated site the canonical
// RPC path so VTEX hooks (useCart, useUser, useWishlist) work end-to-end.
run(
"bunx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts",
"Generate VTEX invoke server functions",
);
run("bunx tsr generate", "Generate TanStack routes");
if (failures > 0) {
console.log(
`\n ${yellow("Bootstrap completed with warnings.")} Check errors above before running dev.\n`,
);
} else {
console.log(`\n ${green("Ready!")} Run \`${pm} run dev\` to start the dev server.\n`);
}
}
// Supabase project ref for decocms — the central platform DB that tracks all sites.
const DECOCMS_SUPABASE_REF = "ozksgdmyrqcxcwhnbepg";
async function provisionAnalytics(sourceDir: string): Promise {
logPhase("Provision analytics (decocms Supabase)");
let siteName: string;
try {
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
siteName = pkg.name;
} catch {
console.log(` ${yellow("⚠")} Could not read package.json — skipping analytics provision`);
return;
}
const token = process.env.SUPABASE_ACCESS_TOKEN;
if (!token) {
console.log(` ${yellow("⚠")} SUPABASE_ACCESS_TOKEN not set — skipping analytics provision`);
printAnalyticsSQL(siteName);
return;
}
const sql = `UPDATE public.sites SET metadata = metadata || '{"analytics": "onedollarstats"}'::jsonb WHERE name = '${siteName}'`;
try {
const res = await fetch(
`https://api.supabase.com/v1/projects/${DECOCMS_SUPABASE_REF}/database/query`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: sql }),
},
);
if (res.ok) {
console.log(` ${green("✓")} ${siteName} → analytics: onedollarstats`);
} else {
const body = await res.text();
console.log(` ${yellow("⚠")} Supabase update failed (${res.status}): ${body.slice(0, 120)}`);
printAnalyticsSQL(siteName);
}
} catch (err: any) {
console.log(` ${yellow("⚠")} Supabase request failed: ${err.message}`);
printAnalyticsSQL(siteName);
}
}
function printAnalyticsSQL(siteName: string): void {
console.log(` Run manually on decocms Supabase (${DECOCMS_SUPABASE_REF}):`);
console.log(
` UPDATE public.sites SET metadata = metadata || '{"analytics": "onedollarstats"}'::jsonb WHERE name = '${siteName}';`,
);
}
main();