@decocms/start 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,409 @@
1
+ import type { TransformResult } from "../types.ts";
2
+
3
+ /**
4
+ * Tailwind v3 → v4 class migration transform.
5
+ *
6
+ * Handles:
7
+ * 1. Renamed/removed utility classes
8
+ * 2. DaisyUI v4 → v5 class changes
9
+ * 3. Responsive class ordering (base → sm → md → lg → xl → 2xl)
10
+ * 4. Arbitrary values → native equivalents (px-[16px] → px-4)
11
+ * 5. Deprecated patterns
12
+ */
13
+
14
+ // ── Breakpoint order (mobile-first) ─────────────────────────────
15
+ const BREAKPOINT_ORDER = ["sm", "md", "lg", "xl", "2xl"] as const;
16
+ const BP_INDEX: Record<string, number> = {};
17
+ BREAKPOINT_ORDER.forEach((bp, i) => {
18
+ BP_INDEX[bp] = i + 1; // base = 0
19
+ });
20
+
21
+ // ── Tailwind v3 → v4 class renames ──────────────────────────────
22
+ // These are direct 1:1 replacements
23
+ const CLASS_RENAMES: Record<string, string> = {
24
+ // Flexbox/Grid
25
+ "flex-grow-0": "grow-0",
26
+ "flex-grow": "grow",
27
+ "flex-shrink-0": "shrink-0",
28
+ "flex-shrink": "shrink",
29
+
30
+ // Overflow
31
+ "overflow-ellipsis": "text-ellipsis",
32
+
33
+ // Decoration
34
+ "decoration-clone": "box-decoration-clone",
35
+ "decoration-slice": "box-decoration-slice",
36
+
37
+ // Transforms (v4 applies transforms automatically)
38
+ "transform": "", // remove — v4 applies automatically
39
+ "transform-gpu": "",
40
+ "transform-none": "transform-none", // this one stays
41
+
42
+ // Blur/filter (v4 applies automatically)
43
+ "filter": "", // remove
44
+ "backdrop-filter": "", // remove
45
+
46
+ // Ring width default
47
+ "ring": "ring-3", // v4 changed default from 3px to 1px
48
+ };
49
+
50
+ // ── DaisyUI v4 → v5 class renames ──────────────────────────────
51
+ const DAISYUI_RENAMES: Record<string, string> = {
52
+ // Button changes
53
+ "btn-ghost": "btn-ghost", // kept
54
+ "btn-outline": "btn-outline", // kept
55
+ "btn-active": "btn-active", // kept
56
+
57
+ // Alert/Badge
58
+ "badge-ghost": "badge-soft",
59
+ "alert-info": "alert-info",
60
+ "alert-success": "alert-success",
61
+ "alert-warning": "alert-warning",
62
+ "alert-error": "alert-error",
63
+
64
+ // Card
65
+ "card-compact": "card-sm",
66
+
67
+ // Modal
68
+ "modal-open": "modal-open",
69
+
70
+ // Drawer
71
+ "drawer-end": "drawer-end",
72
+
73
+ // Menu
74
+ "menu-horizontal": "menu-horizontal",
75
+
76
+ // Toast position classes (daisy v5 uses different system)
77
+ "toast-top": "toast-top",
78
+ "toast-bottom": "toast-bottom",
79
+ "toast-center": "toast-center",
80
+ "toast-end": "toast-end",
81
+ "toast-start": "toast-start",
82
+ "toast-middle": "toast-middle",
83
+
84
+ // Loading
85
+ "loading-spinner": "loading-spinner",
86
+ "loading-dots": "loading-dots",
87
+ "loading-ring": "loading-ring",
88
+ "loading-ball": "loading-ball",
89
+ "loading-bars": "loading-bars",
90
+ "loading-infinity": "loading-infinity",
91
+
92
+ // Sizes (daisy v5 naming)
93
+ "btn-xs": "btn-xs",
94
+ "btn-sm": "btn-sm",
95
+ "btn-md": "btn-md",
96
+ "btn-lg": "btn-lg",
97
+ };
98
+
99
+ // ── Spacing scale: px → Tailwind unit ───────────────────────────
100
+ const PX_TO_SPACING: Record<number, string> = {};
101
+ for (let i = 0; i <= 96; i++) {
102
+ PX_TO_SPACING[i * 4] = String(i);
103
+ }
104
+ PX_TO_SPACING[2] = "0.5";
105
+ PX_TO_SPACING[6] = "1.5";
106
+ PX_TO_SPACING[10] = "2.5";
107
+ PX_TO_SPACING[14] = "3.5";
108
+
109
+ // Text size: px → native class
110
+ const TEXT_SIZE_MAP: Record<string, string> = {
111
+ "12": "xs",
112
+ "14": "sm",
113
+ "16": "base",
114
+ "18": "lg",
115
+ "20": "xl",
116
+ "24": "2xl",
117
+ "30": "3xl",
118
+ "36": "4xl",
119
+ "48": "5xl",
120
+ "60": "6xl",
121
+ "72": "7xl",
122
+ "96": "8xl",
123
+ "128": "9xl",
124
+ };
125
+
126
+ // Properties that accept spacing values
127
+ const SPACING_PROPS = new Set([
128
+ "p", "px", "py", "pt", "pb", "pl", "pr",
129
+ "m", "mx", "my", "mt", "mb", "ml", "mr",
130
+ "gap", "gap-x", "gap-y", "space-x", "space-y",
131
+ "w", "h", "min-w", "min-h", "max-w", "max-h",
132
+ "top", "right", "bottom", "left", "inset", "inset-x", "inset-y",
133
+ "rounded", "rounded-t", "rounded-b", "rounded-l", "rounded-r",
134
+ "rounded-tl", "rounded-tr", "rounded-bl", "rounded-br",
135
+ "border", "border-t", "border-b", "border-l", "border-r",
136
+ "text",
137
+ ]);
138
+
139
+ // ── CSS category resolution (avoid false positives) ─────────────
140
+ const TEXT_SIZE_VALUES = new Set([
141
+ "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl",
142
+ "7xl", "8xl", "9xl",
143
+ ]);
144
+ const TEXT_ALIGN_VALUES = new Set([
145
+ "left", "center", "right", "justify", "start", "end",
146
+ ]);
147
+
148
+ function getCssCategory(prop: string, value: string): string {
149
+ if (prop === "text" || prop === "-text") {
150
+ if (TEXT_SIZE_VALUES.has(value) || /^\[\d/.test(value)) return "text-size";
151
+ if (TEXT_ALIGN_VALUES.has(value)) return "text-align";
152
+ return "text-color";
153
+ }
154
+ if (prop === "flex") {
155
+ if (value === "") return "display";
156
+ if (["1", "auto", "initial", "none"].includes(value)) return "flex-grow";
157
+ if (["row", "col", "row-reverse", "col-reverse"].includes(value)) return "flex-direction";
158
+ if (["wrap", "nowrap", "wrap-reverse"].includes(value)) return "flex-wrap";
159
+ return "flex";
160
+ }
161
+ if (prop === "font") {
162
+ if (["bold", "semibold", "medium", "normal", "light", "thin", "extrabold", "black", "extralight"].includes(value)) return "font-weight";
163
+ return "font-family";
164
+ }
165
+ return prop;
166
+ }
167
+
168
+ // ── Parse class ─────────────────────────────────────────────────
169
+ interface ParsedClass {
170
+ raw: string;
171
+ modifiers: string[];
172
+ bpIndex: number;
173
+ property: string;
174
+ value: string;
175
+ cssCategory: string;
176
+ }
177
+
178
+ function parseClass(cls: string): ParsedClass {
179
+ const parts = cls.split(":");
180
+ const utility = parts.pop()!;
181
+ const modifiers = parts;
182
+
183
+ let bpIndex = 0;
184
+ for (const mod of modifiers) {
185
+ if (BP_INDEX[mod] !== undefined && BP_INDEX[mod] > bpIndex) {
186
+ bpIndex = BP_INDEX[mod];
187
+ }
188
+ }
189
+
190
+ const negMatch = utility.match(/^(-?)(.+)-(.+)$/);
191
+ let property = utility;
192
+ let value = "";
193
+ if (negMatch) {
194
+ property = negMatch[1] + negMatch[2];
195
+ value = negMatch[3];
196
+ }
197
+
198
+ return { raw: cls, modifiers, bpIndex, property, value, cssCategory: getCssCategory(property, value) };
199
+ }
200
+
201
+ // ── Fix class renames ───────────────────────────────────────────
202
+ function fixRenames(cls: string): string {
203
+ const parts = cls.split(":");
204
+ const utility = parts.pop()!;
205
+
206
+ // Check direct rename
207
+ if (CLASS_RENAMES[utility] !== undefined) {
208
+ const renamed = CLASS_RENAMES[utility];
209
+ if (renamed === "") return ""; // Remove class entirely
210
+ parts.push(renamed);
211
+ return parts.join(":");
212
+ }
213
+
214
+ // Check DaisyUI rename
215
+ if (DAISYUI_RENAMES[utility] && DAISYUI_RENAMES[utility] !== utility) {
216
+ parts.push(DAISYUI_RENAMES[utility]);
217
+ return parts.join(":");
218
+ }
219
+
220
+ return cls;
221
+ }
222
+
223
+ // ── Fix arbitrary values ────────────────────────────────────────
224
+ function fixArbitrary(cls: string): string {
225
+ const parsed = parseClass(cls);
226
+ const arbMatch = parsed.value.match(/^\[(-?\d+(?:\.\d+)?)(px|rem|%)?\]$/);
227
+ if (!arbMatch) {
228
+ // w-[100%] → w-full, h-[100%] → h-full
229
+ if (parsed.value === "[100%]" && (parsed.property === "w" || parsed.property === "h")) {
230
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
231
+ return `${prefix}${parsed.property}-full`;
232
+ }
233
+ if (parsed.value === "[auto]" && (parsed.property === "w" || parsed.property === "h")) {
234
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
235
+ return `${prefix}${parsed.property}-auto`;
236
+ }
237
+ return cls;
238
+ }
239
+
240
+ const num = parseFloat(arbMatch[1]);
241
+ const unit = arbMatch[2] || "px";
242
+ const baseProp = parsed.property.replace(/^-/, "");
243
+ const isNeg = parsed.property.startsWith("-");
244
+ const prefix = parsed.modifiers.length ? parsed.modifiers.join(":") + ":" : "";
245
+ const negPrefix = isNeg ? "-" : "";
246
+
247
+ // text-[Npx] → text-{size}
248
+ if (baseProp === "text" && unit === "px") {
249
+ const native = TEXT_SIZE_MAP[String(num)];
250
+ if (native) return `${prefix}text-${native}`;
251
+ return cls;
252
+ }
253
+
254
+ // Spacing: px-[16px] → px-4
255
+ if (SPACING_PROPS.has(baseProp)) {
256
+ let pxValue: number | null = null;
257
+ if (unit === "px") pxValue = num;
258
+ else if (unit === "rem") pxValue = num * 16;
259
+
260
+ if (pxValue !== null && PX_TO_SPACING[pxValue] !== undefined) {
261
+ return `${prefix}${negPrefix}${baseProp}-${PX_TO_SPACING[pxValue]}`;
262
+ }
263
+ }
264
+
265
+ return cls;
266
+ }
267
+
268
+ // ── Fix responsive ordering ─────────────────────────────────────
269
+ function fixResponsiveOrder(classes: string[]): string[] {
270
+ const parsed = classes.map((cls, i) => ({ ...parseClass(cls), idx: i }));
271
+
272
+ // Group by CSS category
273
+ const groups: Record<string, typeof parsed> = {};
274
+ for (const p of parsed) {
275
+ if (!groups[p.cssCategory]) groups[p.cssCategory] = [];
276
+ groups[p.cssCategory].push(p);
277
+ }
278
+
279
+ const result = [...classes];
280
+ for (const group of Object.values(groups)) {
281
+ if (group.length < 2) continue;
282
+ const positions = group.map((g) => g.idx).sort((a, b) => a - b);
283
+ const sorted = [...group].sort((a, b) => a.bpIndex - b.bpIndex);
284
+ for (let i = 0; i < positions.length; i++) {
285
+ result[positions[i]] = sorted[i].raw;
286
+ }
287
+ }
288
+
289
+ return result;
290
+ }
291
+
292
+ // ── Check if ordering is wrong ──────────────────────────────────
293
+ function hasOrderIssues(classes: string[]): boolean {
294
+ const parsed = classes.map((cls, i) => ({ ...parseClass(cls), idx: i }));
295
+ const groups: Record<string, typeof parsed> = {};
296
+ for (const p of parsed) {
297
+ if (!groups[p.cssCategory]) groups[p.cssCategory] = [];
298
+ groups[p.cssCategory].push(p);
299
+ }
300
+
301
+ for (const group of Object.values(groups)) {
302
+ if (group.length < 2) continue;
303
+ for (let i = 0; i < group.length; i++) {
304
+ for (let j = i + 1; j < group.length; j++) {
305
+ const a = group[i];
306
+ const b = group[j];
307
+ if (a.idx < b.idx && a.bpIndex > b.bpIndex) return true;
308
+ if (b.idx < a.idx && b.bpIndex > a.bpIndex) return true;
309
+ }
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+
315
+ // ── Fix a className string ──────────────────────────────────────
316
+ function fixClassNameString(classes: string): { fixed: string; changes: string[] } {
317
+ const changes: string[] = [];
318
+ let classList = classes.split(/\s+/).filter(Boolean);
319
+
320
+ // 1. Apply renames
321
+ classList = classList.map((cls) => {
322
+ const renamed = fixRenames(cls);
323
+ if (renamed !== cls) {
324
+ if (renamed === "") {
325
+ changes.push(`Removed deprecated: ${cls}`);
326
+ } else {
327
+ changes.push(`Renamed: ${cls} → ${renamed}`);
328
+ }
329
+ }
330
+ return renamed;
331
+ }).filter(Boolean); // Remove empty strings (deleted classes)
332
+
333
+ // 2. Fix arbitrary values
334
+ classList = classList.map((cls) => {
335
+ if (!cls.includes("[")) return cls;
336
+ const fixed = fixArbitrary(cls);
337
+ if (fixed !== cls) {
338
+ changes.push(`Arbitrary: ${cls} → ${fixed}`);
339
+ }
340
+ return fixed;
341
+ });
342
+
343
+ // 3. Fix responsive ordering
344
+ if (hasOrderIssues(classList)) {
345
+ const reordered = fixResponsiveOrder(classList);
346
+ if (reordered.join(" ") !== classList.join(" ")) {
347
+ changes.push("Reordered responsive classes (mobile-first)");
348
+ classList = reordered;
349
+ }
350
+ }
351
+
352
+ return { fixed: classList.join(" "), changes };
353
+ }
354
+
355
+ /**
356
+ * Transform Tailwind classes in a file.
357
+ *
358
+ * Finds all className="..." and class="..." attributes and applies:
359
+ * - v3→v4 class renames
360
+ * - DaisyUI v4→v5 renames
361
+ * - Arbitrary value → native equivalent
362
+ * - Responsive class ordering fix
363
+ */
364
+ export function transformTailwind(content: string): TransformResult {
365
+ const notes: string[] = [];
366
+ let changed = false;
367
+ let result = content;
368
+
369
+ // Match className="...", className={`...`}, class="..."
370
+ const patterns = [
371
+ /(?<=className\s*=\s*")([^"]+)(?=")/g,
372
+ /(?<=className\s*=\s*{`)([^`]+)(?=`})/g,
373
+ /(?<=className\s*=\s*{\s*")([^"]+)(?="\s*})/g,
374
+ /(?<=class\s*=\s*")([^"]+)(?=")/g,
375
+ ];
376
+
377
+ for (const pattern of patterns) {
378
+ result = result.replace(pattern, (match) => {
379
+ // Handle multiline class strings
380
+ if (match.includes("\n")) {
381
+ const lines = match.split("\n");
382
+ const fixedLines = lines.map((line) => {
383
+ const trimmed = line.trim();
384
+ if (!trimmed) return line;
385
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
386
+ const { fixed, changes } = fixClassNameString(trimmed);
387
+ if (changes.length > 0) {
388
+ changed = true;
389
+ notes.push(...changes);
390
+ }
391
+ return indent + fixed;
392
+ });
393
+ return fixedLines.join("\n");
394
+ }
395
+
396
+ const { fixed, changes } = fixClassNameString(match);
397
+ if (changes.length > 0) {
398
+ changed = true;
399
+ notes.push(...changes);
400
+ }
401
+ return fixed;
402
+ });
403
+ }
404
+
405
+ // Deduplicate notes
406
+ const uniqueNotes = [...new Set(notes)];
407
+
408
+ return { content: result, changed, notes: uniqueNotes };
409
+ }
@@ -0,0 +1,137 @@
1
+ export type Platform =
2
+ | "vtex"
3
+ | "vnda"
4
+ | "shopify"
5
+ | "wake"
6
+ | "linx"
7
+ | "nuvemshop"
8
+ | "custom";
9
+
10
+ export interface FileRecord {
11
+ /** Relative path from source root */
12
+ path: string;
13
+ /** Absolute path */
14
+ absPath: string;
15
+ /** File category */
16
+ category:
17
+ | "section"
18
+ | "island"
19
+ | "component"
20
+ | "sdk"
21
+ | "loader"
22
+ | "action"
23
+ | "route"
24
+ | "app"
25
+ | "static"
26
+ | "config"
27
+ | "generated"
28
+ | "other";
29
+ /** Whether this file is a re-export wrapper */
30
+ isReExport?: boolean;
31
+ /** The target of the re-export if applicable */
32
+ reExportTarget?: string;
33
+ /** Detected patterns in this file */
34
+ patterns: DetectedPattern[];
35
+ /** Action to take */
36
+ action: "transform" | "delete" | "move" | "scaffold" | "manual-review";
37
+ /** Target path in new structure (relative to project root) */
38
+ targetPath?: string;
39
+ /** Notes for the report */
40
+ notes?: string;
41
+ }
42
+
43
+ export type DetectedPattern =
44
+ | "preact-hooks"
45
+ | "preact-signals"
46
+ | "fresh-runtime"
47
+ | "fresh-server"
48
+ | "deco-hooks"
49
+ | "deco-context"
50
+ | "deco-web"
51
+ | "deco-blocks"
52
+ | "apps-imports"
53
+ | "site-imports"
54
+ | "class-attr"
55
+ | "onInput-handler"
56
+ | "deno-lint-ignore"
57
+ | "npm-prefix"
58
+ | "ts-extension-import"
59
+ | "component-children"
60
+ | "jsx-types"
61
+ | "asset-function"
62
+ | "head-component"
63
+ | "define-app"
64
+ | "invoke-proxy";
65
+
66
+ export interface MigrationContext {
67
+ sourceDir: string;
68
+ siteName: string;
69
+ platform: Platform;
70
+ gtmId: string | null;
71
+
72
+ /** deno.json import map entries */
73
+ importMap: Record<string, string>;
74
+
75
+ /** All categorized source files */
76
+ files: FileRecord[];
77
+
78
+ /** Files created by scaffold phase */
79
+ scaffoldedFiles: string[];
80
+ /** Files transformed */
81
+ transformedFiles: string[];
82
+ /** Files deleted */
83
+ deletedFiles: string[];
84
+ /** Files moved */
85
+ movedFiles: Array<{ from: string; to: string }>;
86
+ /** Items requiring manual review */
87
+ manualReviewItems: ReviewItem[];
88
+ /** Framework findings */
89
+ frameworkFindings: string[];
90
+
91
+ dryRun: boolean;
92
+ verbose: boolean;
93
+ }
94
+
95
+ export interface ReviewItem {
96
+ file: string;
97
+ reason: string;
98
+ severity: "info" | "warning" | "error";
99
+ }
100
+
101
+ export interface TransformResult {
102
+ content: string;
103
+ changed: boolean;
104
+ notes: string[];
105
+ }
106
+
107
+ export function createContext(
108
+ sourceDir: string,
109
+ opts: { dryRun?: boolean; verbose?: boolean } = {},
110
+ ): MigrationContext {
111
+ return {
112
+ sourceDir,
113
+ siteName: "",
114
+ platform: "custom",
115
+ gtmId: null,
116
+ importMap: {},
117
+ files: [],
118
+ scaffoldedFiles: [],
119
+ transformedFiles: [],
120
+ deletedFiles: [],
121
+ movedFiles: [],
122
+ manualReviewItems: [],
123
+ frameworkFindings: [],
124
+ dryRun: opts.dryRun ?? false,
125
+ verbose: opts.verbose ?? false,
126
+ };
127
+ }
128
+
129
+ export function log(ctx: MigrationContext, msg: string) {
130
+ if (ctx.verbose) console.log(` ${msg}`);
131
+ }
132
+
133
+ export function logPhase(phase: string) {
134
+ console.log(`\n${"=".repeat(60)}`);
135
+ console.log(` Phase: ${phase}`);
136
+ console.log(`${"=".repeat(60)}\n`);
137
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Migration Script: Fresh/Deno/Preact → TanStack Start/React/Cloudflare Workers
4
+ *
5
+ * Converts a Deco storefront from the old Fresh/Deno stack to the new TanStack Start stack.
6
+ * Part of the @decocms/start framework — run from a site's root directory.
7
+ *
8
+ * Usage (from site root):
9
+ * npx tsx node_modules/@decocms/start/scripts/migrate.ts [options]
10
+ *
11
+ * Options:
12
+ * --source <dir> Source directory (default: current directory)
13
+ * --dry-run Preview changes without writing files
14
+ * --verbose Show detailed output
15
+ * --help Show this help message
16
+ *
17
+ * Phases:
18
+ * 1. Analyze — Scan source site, categorize files, detect patterns
19
+ * 2. Scaffold — Create target structure (configs, routes, infra files)
20
+ * 3. Transform — Convert source files (imports, JSX, Fresh APIs, Deno-isms, Tailwind)
21
+ * 4. Cleanup — Delete old artifacts, move static → public
22
+ * 5. Report — Generate MIGRATION_REPORT.md with findings
23
+ * 6. Verify — Smoke test the migrated output
24
+ */
25
+
26
+ import * as path from "node:path";
27
+ import { createContext } from "./migrate/types.ts";
28
+ import { analyze } from "./migrate/phase-analyze.ts";
29
+ import { scaffold } from "./migrate/phase-scaffold.ts";
30
+ import { transform } from "./migrate/phase-transform.ts";
31
+ import { cleanup } from "./migrate/phase-cleanup.ts";
32
+ import { report } from "./migrate/phase-report.ts";
33
+ import { verify } from "./migrate/phase-verify.ts";
34
+ import { banner, stat, red, green, yellow } from "./migrate/colors.ts";
35
+
36
+ function parseArgs(args: string[]): {
37
+ source: string;
38
+ dryRun: boolean;
39
+ verbose: boolean;
40
+ help: boolean;
41
+ } {
42
+ let source = ".";
43
+ let dryRun = false;
44
+ let verbose = false;
45
+ let help = false;
46
+
47
+ for (let i = 0; i < args.length; i++) {
48
+ switch (args[i]) {
49
+ case "--source":
50
+ source = args[++i];
51
+ break;
52
+ case "--dry-run":
53
+ dryRun = true;
54
+ break;
55
+ case "--verbose":
56
+ verbose = true;
57
+ break;
58
+ case "--help":
59
+ case "-h":
60
+ help = true;
61
+ break;
62
+ }
63
+ }
64
+
65
+ return { source, dryRun, verbose, help };
66
+ }
67
+
68
+ function showHelp() {
69
+ console.log(`
70
+ @decocms/start — Migration Script: Fresh/Deno → TanStack Start
71
+
72
+ Usage:
73
+ npx tsx node_modules/@decocms/start/scripts/migrate.ts [options]
74
+
75
+ Options:
76
+ --source <dir> Source directory (default: .)
77
+ --dry-run Preview changes without writing files
78
+ --verbose Show detailed output for every file
79
+ --help, -h Show this help message
80
+
81
+ Examples:
82
+ npx tsx node_modules/@decocms/start/scripts/migrate.ts --dry-run --verbose
83
+ npx tsx node_modules/@decocms/start/scripts/migrate.ts --source ./my-site
84
+ npx tsx node_modules/@decocms/start/scripts/migrate.ts
85
+ `);
86
+ }
87
+
88
+ async function main() {
89
+ const opts = parseArgs(process.argv.slice(2));
90
+
91
+ if (opts.help) {
92
+ showHelp();
93
+ process.exit(0);
94
+ }
95
+
96
+ const sourceDir = path.resolve(opts.source);
97
+
98
+ banner("@decocms/start — Migrate: Fresh/Deno → TanStack Start");
99
+ stat("Source", sourceDir);
100
+ stat("Mode", opts.dryRun ? yellow("DRY RUN") : green("EXECUTE"));
101
+ stat("Verbose", opts.verbose ? "yes" : "no");
102
+
103
+ const ctx = createContext(sourceDir, {
104
+ dryRun: opts.dryRun,
105
+ verbose: opts.verbose,
106
+ });
107
+
108
+ try {
109
+ // Phase 1: Analyze source
110
+ analyze(ctx);
111
+
112
+ // Phase 2: Scaffold target structure
113
+ scaffold(ctx);
114
+
115
+ // Phase 3: Transform source files
116
+ transform(ctx);
117
+
118
+ // Phase 4: Cleanup old artifacts
119
+ cleanup(ctx);
120
+
121
+ // Phase 5: Generate report
122
+ report(ctx);
123
+
124
+ // Phase 6: Verify (smoke test)
125
+ const ok = verify(ctx);
126
+ if (!ok) {
127
+ process.exit(2);
128
+ }
129
+ } catch (error) {
130
+ console.error(`\n ${red("Migration failed:")}`, error);
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ main();