#!/usr/bin/env tsx /** * Upgrade a site off the frozen pre-split `@decocms/start@6.30.0` / * `@decocms/apps@5.4.0` packages onto the current 7.x split * (`@decocms/tanstack` + `@decocms/blocks` + `@decocms/blocks-admin` + * `@decocms/apps-*`). See #367. * * This is NOT the Fresh→TanStack migrator (`deco-migrate`) — it assumes the * site is already on TanStack Start and only needs its package dependency * moved to the current split. Rewrites import specifiers (including the * `@decocms/start/routes` / `@decocms/start/hooks` symbol fan-out and the * `decoMetaRoute`/`RenderSection` renames — see scripts/lib/upgrade-6-to-7.ts * for the full mapping) and updates package.json dependencies. * * Usage (from the site root): * # dry-run (default): prints every file that would change, no writes * npx -p @decocms/blocks-cli deco-upgrade-6-to-7 * # apply: * npx -p @decocms/blocks-cli deco-upgrade-6-to-7 --write * * Options: * --src-dir Source dir to scan (default: src) * --write Apply changes (otherwise dry-run, exit 0) * --help, -h Show this help * * Exit codes: 0 ok / dry-run; 2 error (bad dir) */ import * as fs from "node:fs"; import * as path from "node:path"; import { upgradeFileToV7 } from "./lib/upgrade-6-to-7"; const FRAMEWORK_DEPS = [ "@decocms/blocks", "@decocms/blocks-admin", "@decocms/tanstack", "@decocms/apps-commerce", ]; const PLATFORM_APPS_PREFIX = "@decocms/apps-"; function parseArgs(argv: string[]) { const has = (f: string) => argv.includes(f); const val = (f: string, d: string) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; }; return { help: has("--help") || has("-h"), write: has("--write"), srcDir: val("--src-dir", "src"), }; } function walkTsFiles(dir: string, out: string[] = []): string[] { if (!fs.existsSync(dir)) return out; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name === "node_modules" || entry.name === ".git") continue; walkTsFiles(full, out); } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) { out.push(full); } } return out; } /** Removes @decocms/start and @decocms/apps (bare, monolith) from a deps * object, and ensures the current split packages are present. Site- and * platform-specific version pins are left to `bun install`/`npm install` * against the fallback "latest" this writes — sites should pin exact * versions afterward the same way the scaffolder does. */ function upgradeDeps(deps: Record | undefined): { next: Record; removed: string[]; added: string[]; } { const next: Record = { ...(deps ?? {}) }; const removed: string[] = []; for (const key of Object.keys(next)) { if (key === "@decocms/start" || key === "@decocms/apps") { delete next[key]; removed.push(key); } } const added: string[] = []; if (removed.length > 0) { for (const dep of FRAMEWORK_DEPS) { if (!next[dep]) { next[dep] = "latest"; added.push(dep); } } } return { next, removed, added }; } function upgradePackageJson(pkgPath: string, write: boolean): void { if (!fs.existsSync(pkgPath)) return; const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); const depsResult = upgradeDeps(pkg.dependencies); const devDepsResult = upgradeDeps(pkg.devDependencies); const removed = [...depsResult.removed, ...devDepsResult.removed]; if (removed.length === 0) return; console.log(` package.json: removing ${removed.join(", ")}`); const added = [...new Set([...depsResult.added, ...devDepsResult.added])]; if (added.length > 0) { console.log(` package.json: adding ${added.join(", ")} at "latest" — pin exact versions after install`); } if (!write) return; pkg.dependencies = depsResult.next; pkg.devDependencies = devDepsResult.next; fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); } function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { console.log( "Usage: deco-upgrade-6-to-7 [--src-dir ] [--write]\n\n" + "Upgrades a site off @decocms/start@6.30.0 / @decocms/apps@5.4.0 onto the\n" + "current 7.x split. Dry-run by default; pass --write to apply.", ); return; } const cwd = process.cwd(); const srcDir = path.resolve(cwd, args.srcDir); if (!fs.existsSync(srcDir)) { console.error(`Source dir not found: ${srcDir}`); process.exitCode = 2; return; } const files = walkTsFiles(srcDir); let changedCount = 0; const allNotes: Array<{ file: string; note: string }> = []; for (const abs of files) { const content = fs.readFileSync(abs, "utf-8"); const result = upgradeFileToV7(content); if (!result.changed) continue; changedCount++; const rel = path.relative(cwd, abs); console.log(`${args.write ? "upgraded" : "would upgrade"}: ${rel}`); for (const note of result.notes) allNotes.push({ file: rel, note }); if (args.write) fs.writeFileSync(abs, result.content); } upgradePackageJson(path.join(cwd, "package.json"), args.write); if (allNotes.length > 0) { console.log("\nManual review needed:"); for (const { file, note } of allNotes) console.log(` [${file}] ${note}`); } console.log( `\n${args.write ? "Upgraded" : "Would upgrade"} ${changedCount} file(s).` + (args.write ? "" : " Re-run with --write to apply."), ); } main();