@decocms/start 0.31.1 → 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.
- package/package.json +3 -1
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/phase-analyze.ts +335 -0
- package/scripts/migrate/phase-cleanup.ts +203 -0
- package/scripts/migrate/phase-report.ts +171 -0
- package/scripts/migrate/phase-scaffold.ts +133 -0
- package/scripts/migrate/phase-transform.ts +102 -0
- package/scripts/migrate/phase-verify.ts +248 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/package-json.ts +59 -0
- package/scripts/migrate/templates/routes.ts +280 -0
- package/scripts/migrate/templates/server-entry.ts +148 -0
- package/scripts/migrate/templates/setup.ts +32 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/vite-config.ts +108 -0
- package/scripts/migrate/templates/wrangler.ts +25 -0
- package/scripts/migrate/transforms/deno-isms.ts +64 -0
- package/scripts/migrate/transforms/fresh-apis.ts +111 -0
- package/scripts/migrate/transforms/imports.ts +132 -0
- package/scripts/migrate/transforms/jsx.ts +109 -0
- package/scripts/migrate/transforms/tailwind.ts +409 -0
- package/scripts/migrate/types.ts +137 -0
- package/scripts/migrate.ts +135 -0
- package/scripts/tailwind-lint.ts +518 -0
|
@@ -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();
|