@decocms/start 1.2.6 → 1.2.8
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 +1 -1
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/migrate/analyzers/island-classifier.ts +73 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +91 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/phase-analyze.ts +147 -17
- package/scripts/migrate/phase-cleanup.ts +124 -2
- package/scripts/migrate/phase-report.ts +44 -16
- package/scripts/migrate/phase-scaffold.ts +38 -132
- package/scripts/migrate/phase-transform.ts +28 -3
- package/scripts/migrate/phase-verify.ts +127 -5
- package/scripts/migrate/templates/app-css.ts +204 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +124 -0
- package/scripts/migrate/templates/hooks.ts +358 -0
- package/scripts/migrate/templates/package-json.ts +29 -6
- package/scripts/migrate/templates/routes.ts +41 -136
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +108 -0
- package/scripts/migrate/templates/server-entry.ts +174 -67
- package/scripts/migrate/templates/setup.ts +64 -55
- package/scripts/migrate/templates/types-gen.ts +119 -0
- package/scripts/migrate/templates/ui-components.ts +113 -0
- package/scripts/migrate/templates/vite-config.ts +18 -1
- package/scripts/migrate/templates/wrangler.ts +4 -1
- package/scripts/migrate/transforms/dead-code.ts +23 -2
- package/scripts/migrate/transforms/imports.ts +40 -10
- package/scripts/migrate/transforms/jsx.ts +9 -0
- package/scripts/migrate/transforms/section-conventions.ts +83 -0
- package/scripts/migrate/types.ts +74 -0
- package/src/routes/cmsRoute.ts +26 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { MigrationContext } from "../types.ts";
|
|
4
|
+
import { log } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export interface ExtractedTheme {
|
|
7
|
+
/** Raw CSS variable -> hex color map from DEFAULT_THEME */
|
|
8
|
+
variables: Record<string, string>;
|
|
9
|
+
/** Font family string (from Theme.tsx or default_theme) */
|
|
10
|
+
fontFamily: string | null;
|
|
11
|
+
/** DaisyUI semantic colors derived from the brand palette */
|
|
12
|
+
daisyUiColors: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DAISYUI_MAPPING: Record<string, string[]> = {
|
|
16
|
+
"--color-primary": ["--brand-primary-1"],
|
|
17
|
+
"--color-secondary": ["--brand-secondary-1"],
|
|
18
|
+
"--color-accent": ["--brand-terciary-1", "--brand-terciary-base"],
|
|
19
|
+
"--color-neutral": ["--neutral-900", "--neutral-1"],
|
|
20
|
+
"--color-base-100": ["--neutral-0", "--neutral-50"],
|
|
21
|
+
"--color-base-200": ["--brand-secondary-50", "--neutral-100"],
|
|
22
|
+
"--color-base-300": ["--brand-secondary-500", "--neutral-500"],
|
|
23
|
+
"--color-info": ["--information"],
|
|
24
|
+
"--color-success": ["--success"],
|
|
25
|
+
"--color-warning": ["--warning"],
|
|
26
|
+
"--color-error": ["--error"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function extractDefaultTheme(sourceDir: string): Record<string, string> | null {
|
|
30
|
+
const candidates = [
|
|
31
|
+
"styles/default_theme.ts",
|
|
32
|
+
"styles/defaultTheme.ts",
|
|
33
|
+
"sdk/default_theme.ts",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const candidate of candidates) {
|
|
37
|
+
const filePath = path.join(sourceDir, candidate);
|
|
38
|
+
if (!fs.existsSync(filePath)) continue;
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
41
|
+
|
|
42
|
+
const vars: Record<string, string> = {};
|
|
43
|
+
const entryRe = /["'](--.+?)["']\s*:\s*["'](.+?)["']/g;
|
|
44
|
+
let match: RegExpExecArray | null;
|
|
45
|
+
while ((match = entryRe.exec(content)) !== null) {
|
|
46
|
+
vars[match[1]] = match[2];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Object.keys(vars).length > 0) return vars;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractFontFamily(sourceDir: string): string | null {
|
|
56
|
+
const candidates = [
|
|
57
|
+
"sections/Theme/Theme.tsx",
|
|
58
|
+
"sections/theme/Theme.tsx",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const filePath = path.join(sourceDir, candidate);
|
|
63
|
+
if (!fs.existsSync(filePath)) continue;
|
|
64
|
+
|
|
65
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
66
|
+
|
|
67
|
+
const fontMatch = content.match(
|
|
68
|
+
/["']--font-family["']\s*,\s*\n?\s*["'](.*?)["']/,
|
|
69
|
+
);
|
|
70
|
+
if (fontMatch) {
|
|
71
|
+
return fontMatch[1].split(",")[0].trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fontMatch2 = content.match(
|
|
75
|
+
/font.*?["']([\w\s]+(?:,\s*[\w\s-]+)*)/i,
|
|
76
|
+
);
|
|
77
|
+
if (fontMatch2) {
|
|
78
|
+
const family = fontMatch2[1].split(",")[0].trim();
|
|
79
|
+
if (family && family !== "sans-serif") return family;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deriveDaisyUiColors(vars: Record<string, string>): Record<string, string> {
|
|
87
|
+
const result: Record<string, string> = {};
|
|
88
|
+
|
|
89
|
+
for (const [daisyKey, sourceKeys] of Object.entries(DAISYUI_MAPPING)) {
|
|
90
|
+
for (const sourceKey of sourceKeys) {
|
|
91
|
+
if (vars[sourceKey]) {
|
|
92
|
+
result[daisyKey] = vars[sourceKey];
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function extractTheme(ctx: MigrationContext): ExtractedTheme {
|
|
102
|
+
const vars = extractDefaultTheme(ctx.sourceDir);
|
|
103
|
+
|
|
104
|
+
if (!vars) {
|
|
105
|
+
log(ctx, "No styles/default_theme.ts found — using placeholder theme");
|
|
106
|
+
return {
|
|
107
|
+
variables: {},
|
|
108
|
+
fontFamily: ctx.fontFamily,
|
|
109
|
+
daisyUiColors: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fontFamily = extractFontFamily(ctx.sourceDir) || ctx.fontFamily;
|
|
114
|
+
const daisyUiColors = deriveDaisyUiColors(vars);
|
|
115
|
+
|
|
116
|
+
log(
|
|
117
|
+
ctx,
|
|
118
|
+
`Theme extracted: ${Object.keys(vars).length} variables, ${Object.keys(daisyUiColors).length} DaisyUI colors, font: ${fontFamily || "none"}`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return { variables: vars, fontFamily, daisyUiColors };
|
|
122
|
+
}
|
|
@@ -7,6 +7,9 @@ import type {
|
|
|
7
7
|
Platform,
|
|
8
8
|
} from "./types.ts";
|
|
9
9
|
import { log, logPhase } from "./types.ts";
|
|
10
|
+
import { extractSectionMetadata } from "./analyzers/section-metadata.ts";
|
|
11
|
+
import { classifyIslands } from "./analyzers/island-classifier.ts";
|
|
12
|
+
import { inventoryLoaders } from "./analyzers/loader-inventory.ts";
|
|
10
13
|
|
|
11
14
|
const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
12
15
|
["preact-hooks", /from\s+["']preact\/hooks["']/],
|
|
@@ -39,6 +42,8 @@ const SKIP_DIRS = new Set([
|
|
|
39
42
|
".deco",
|
|
40
43
|
".devcontainer",
|
|
41
44
|
".vscode",
|
|
45
|
+
".claude",
|
|
46
|
+
".cursor",
|
|
42
47
|
"_fresh",
|
|
43
48
|
"static",
|
|
44
49
|
".context",
|
|
@@ -46,17 +51,27 @@ const SKIP_DIRS = new Set([
|
|
|
46
51
|
"src",
|
|
47
52
|
"public",
|
|
48
53
|
".tanstack",
|
|
54
|
+
"tests",
|
|
55
|
+
"bin",
|
|
56
|
+
"fonts",
|
|
57
|
+
".pilot",
|
|
49
58
|
]);
|
|
50
59
|
|
|
51
60
|
const SKIP_FILES = new Set([
|
|
52
61
|
"deno.lock",
|
|
53
62
|
".gitignore",
|
|
54
63
|
"README.md",
|
|
64
|
+
"AGENTS.md",
|
|
55
65
|
"LICENSE",
|
|
56
66
|
"browserslist",
|
|
57
67
|
"bw_stats.json",
|
|
68
|
+
"biome.json",
|
|
58
69
|
"package.json",
|
|
59
70
|
"package-lock.json",
|
|
71
|
+
"yarn.lock",
|
|
72
|
+
"bun.lock",
|
|
73
|
+
"bun.lockb",
|
|
74
|
+
"account.json",
|
|
60
75
|
]);
|
|
61
76
|
|
|
62
77
|
/** Files that are generated and should be deleted */
|
|
@@ -66,13 +81,22 @@ const GENERATED_FILES = new Set([
|
|
|
66
81
|
"fresh.config.ts",
|
|
67
82
|
]);
|
|
68
83
|
|
|
69
|
-
/** SDK files that have framework equivalents */
|
|
84
|
+
/** SDK files that have framework equivalents or are scaffolded fresh */
|
|
70
85
|
const SDK_DELETE = new Set([
|
|
71
86
|
"sdk/clx.ts",
|
|
72
87
|
"sdk/useId.ts",
|
|
73
88
|
"sdk/useOffer.ts",
|
|
74
89
|
"sdk/useVariantPossiblities.ts",
|
|
75
90
|
"sdk/usePlatform.tsx",
|
|
91
|
+
"sdk/signal.ts",
|
|
92
|
+
"sdk/format.ts",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/** Component files that are scaffolded fresh (old versions must not overwrite) */
|
|
96
|
+
const COMPONENT_DELETE = new Set([
|
|
97
|
+
"components/ui/Image.tsx",
|
|
98
|
+
"components/ui/Picture.tsx",
|
|
99
|
+
"components/ui/Video.tsx",
|
|
76
100
|
]);
|
|
77
101
|
|
|
78
102
|
/** Loaders that depend on deleted admin tooling */
|
|
@@ -96,6 +120,7 @@ const ROOT_DELETE = new Set([
|
|
|
96
120
|
"fresh.config.ts",
|
|
97
121
|
"browserslist",
|
|
98
122
|
"bw_stats.json",
|
|
123
|
+
"islands.ts",
|
|
99
124
|
]);
|
|
100
125
|
|
|
101
126
|
/** Static files that are code/tooling, not assets — should be deleted */
|
|
@@ -152,7 +177,7 @@ function categorizeFile(
|
|
|
152
177
|
if (relPath.startsWith("actions/")) return "action";
|
|
153
178
|
if (relPath.startsWith("routes/")) return "route";
|
|
154
179
|
if (relPath.startsWith("apps/")) return "app";
|
|
155
|
-
if (relPath.startsWith("static/")) return "static";
|
|
180
|
+
if (relPath.startsWith("static/") || relPath.startsWith("static-")) return "static";
|
|
156
181
|
if (GENERATED_FILES.has(relPath)) return "generated";
|
|
157
182
|
if (
|
|
158
183
|
relPath === "deno.json" || relPath === "tsconfig.json" ||
|
|
@@ -201,7 +226,7 @@ function decideAction(
|
|
|
201
226
|
};
|
|
202
227
|
}
|
|
203
228
|
|
|
204
|
-
// SDK files to delete
|
|
229
|
+
// SDK files to delete (replaced by scaffolded or framework equivalents)
|
|
205
230
|
if (SDK_DELETE.has(relPath)) {
|
|
206
231
|
return {
|
|
207
232
|
action: "delete",
|
|
@@ -209,24 +234,44 @@ function decideAction(
|
|
|
209
234
|
};
|
|
210
235
|
}
|
|
211
236
|
|
|
237
|
+
// Component files replaced by scaffolded versions
|
|
238
|
+
if (COMPONENT_DELETE.has(relPath)) {
|
|
239
|
+
return {
|
|
240
|
+
action: "delete",
|
|
241
|
+
notes: "Scaffolded fresh from @decocms/apps re-exports",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
212
245
|
// cart/ directory → delete
|
|
213
246
|
if (relPath.startsWith("sdk/cart/")) {
|
|
214
247
|
return { action: "delete", notes: "Use @decocms/apps cart hooks" };
|
|
215
248
|
}
|
|
216
249
|
|
|
217
|
-
// Islands —
|
|
250
|
+
// Islands — classify and route to appropriate target
|
|
218
251
|
if (category === "island") {
|
|
219
|
-
const
|
|
252
|
+
const classification = (record as any).__islandClassification;
|
|
253
|
+
if (classification?.type === "wrapper") {
|
|
254
|
+
return { action: "delete", notes: "Island wrapper — imports repointed to component" };
|
|
255
|
+
}
|
|
256
|
+
// Standalone islands go to components/, not sections/
|
|
257
|
+
const componentPath = relPath.replace("islands/", "components/");
|
|
220
258
|
return {
|
|
221
259
|
action: "transform",
|
|
222
|
-
targetPath: `src/${
|
|
223
|
-
notes: "
|
|
260
|
+
targetPath: `src/${componentPath}`,
|
|
261
|
+
notes: "Standalone island moved to components",
|
|
224
262
|
};
|
|
225
263
|
}
|
|
226
264
|
|
|
227
|
-
// Sections that
|
|
265
|
+
// Sections that re-export from islands/ → delete (island takes their place)
|
|
266
|
+
// But sections that re-export from components/ or other dirs should be KEPT
|
|
228
267
|
if (category === "section" && isReExp) {
|
|
229
|
-
|
|
268
|
+
const target = record.reExportTarget || "";
|
|
269
|
+
const isIslandReExport = target.includes("islands/") ||
|
|
270
|
+
target.includes("islands\\");
|
|
271
|
+
if (isIslandReExport) {
|
|
272
|
+
return { action: "delete", notes: "Re-export wrapper for island, island merged" };
|
|
273
|
+
}
|
|
274
|
+
// Section re-exports from components/ — keep and transform
|
|
230
275
|
}
|
|
231
276
|
|
|
232
277
|
// Session component → delete (analytics moves to __root.tsx)
|
|
@@ -250,6 +295,19 @@ function decideAction(
|
|
|
250
295
|
return { action: "move", targetPath: publicPath };
|
|
251
296
|
}
|
|
252
297
|
|
|
298
|
+
// Non-code root files that shouldn't go into src/
|
|
299
|
+
const ext = path.extname(relPath);
|
|
300
|
+
const nonCodeExts = new Set([".md", ".csv", ".json", ".sh", ".lock", ".yml", ".yaml", ".xml", ".html", ".txt", ".log"]);
|
|
301
|
+
if (!relPath.includes("/") && nonCodeExts.has(ext)) {
|
|
302
|
+
return { action: "delete", notes: "Root-level non-code file" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Root-level loose TS/TSX files that are tooling, not app code
|
|
306
|
+
const rootToolingFiles = new Set(["islands.ts", "order-status.ts", "sync.sh"]);
|
|
307
|
+
if (!relPath.includes("/") && rootToolingFiles.has(relPath)) {
|
|
308
|
+
return { action: "delete", notes: "Root-level tooling file" };
|
|
309
|
+
}
|
|
310
|
+
|
|
253
311
|
// Everything else → transform into src/
|
|
254
312
|
return { action: "transform", targetPath: `src/${relPath}` };
|
|
255
313
|
}
|
|
@@ -266,7 +324,7 @@ function scanDir(
|
|
|
266
324
|
const relPath = path.relative(baseDir, fullPath);
|
|
267
325
|
|
|
268
326
|
if (entry.isDirectory()) {
|
|
269
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
327
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || entry.name.startsWith("static-")) continue;
|
|
270
328
|
scanDir(fullPath, baseDir, files);
|
|
271
329
|
continue;
|
|
272
330
|
}
|
|
@@ -317,15 +375,65 @@ function extractGtmId(sourceDir: string): string | null {
|
|
|
317
375
|
}
|
|
318
376
|
|
|
319
377
|
function extractPlatform(sourceDir: string): Platform {
|
|
320
|
-
const
|
|
321
|
-
|
|
378
|
+
const platforms: Platform[] = ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"];
|
|
379
|
+
|
|
380
|
+
// Strategy 1: Check deno.json imports for platform-specific app imports
|
|
381
|
+
const denoPath = path.join(sourceDir, "deno.json");
|
|
382
|
+
if (fs.existsSync(denoPath)) {
|
|
383
|
+
try {
|
|
384
|
+
const deno = JSON.parse(fs.readFileSync(denoPath, "utf-8"));
|
|
385
|
+
const imports = deno.imports || {};
|
|
386
|
+
for (const p of platforms) {
|
|
387
|
+
// e.g. "apps/vtex/" or direct app import containing the platform name
|
|
388
|
+
const hasAppImport = Object.keys(imports).some(
|
|
389
|
+
(k) => k === `apps/${p}/` || k.includes(`/${p}/mod.ts`) || k.includes(`deco-apps`) && imports[k].includes(`/${p}/`),
|
|
390
|
+
);
|
|
391
|
+
const hasAppValue = Object.values(imports).some(
|
|
392
|
+
(v) => typeof v === "string" && (v as string).includes(`/${p}/`),
|
|
393
|
+
);
|
|
394
|
+
if (hasAppImport || hasAppValue) return p;
|
|
395
|
+
}
|
|
396
|
+
// Check if the import map value for "apps/" contains a platform hint
|
|
397
|
+
const appsUrl = imports["apps/"];
|
|
398
|
+
if (typeof appsUrl === "string") {
|
|
399
|
+
for (const p of platforms) {
|
|
400
|
+
// The apps/ URL itself doesn't indicate platform, but let's check apps/vtex.ts
|
|
401
|
+
const vtexAppPath = path.join(sourceDir, "apps", `${p}.ts`);
|
|
402
|
+
if (fs.existsSync(vtexAppPath)) return p;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
}
|
|
322
407
|
|
|
323
|
-
|
|
408
|
+
// Strategy 2: Check for apps/{platform}.ts file existence
|
|
409
|
+
for (const p of platforms) {
|
|
410
|
+
if (fs.existsSync(path.join(sourceDir, "apps", `${p}.ts`))) return p;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Strategy 3: Check apps/site.ts for platform type and default value
|
|
414
|
+
const sitePath = path.join(sourceDir, "apps", "site.ts");
|
|
415
|
+
if (fs.existsSync(sitePath)) {
|
|
416
|
+
const content = fs.readFileSync(sitePath, "utf-8");
|
|
417
|
+
// Look for platform default in state or props: state.platform || "vtex"
|
|
418
|
+
const defaultMatch = content.match(/(?:state\.platform|props\.platform)\s*\|\|\s*["'](\w+)["']/);
|
|
419
|
+
if (defaultMatch) {
|
|
420
|
+
const p = defaultMatch[1] as Platform;
|
|
421
|
+
if (platforms.includes(p)) return p;
|
|
422
|
+
}
|
|
423
|
+
// Look for platform in the Props type
|
|
424
|
+
for (const p of platforms) {
|
|
425
|
+
if (content.includes(`"${p}"`) && (content.includes("Platform") || content.includes("platform"))) {
|
|
426
|
+
return p;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
324
430
|
|
|
325
|
-
// Check for platform
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
431
|
+
// Strategy 4: Check .deco/blocks for platform-specific block files
|
|
432
|
+
const blocksDir = path.join(sourceDir, ".deco", "blocks");
|
|
433
|
+
if (fs.existsSync(blocksDir)) {
|
|
434
|
+
const blockFiles = fs.readdirSync(blocksDir);
|
|
435
|
+
for (const p of platforms) {
|
|
436
|
+
if (blockFiles.some((f) => f.includes(`deco-${p}`) || f === `${p}.json`)) return p;
|
|
329
437
|
}
|
|
330
438
|
}
|
|
331
439
|
|
|
@@ -457,4 +565,26 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
457
565
|
console.log(`\n Files found: ${ctx.files.length}`);
|
|
458
566
|
console.log(` By category: ${JSON.stringify(byCategory)}`);
|
|
459
567
|
console.log(` By action: ${JSON.stringify(byAction)}`);
|
|
568
|
+
|
|
569
|
+
// Run analyzers
|
|
570
|
+
extractSectionMetadata(ctx);
|
|
571
|
+
classifyIslands(ctx);
|
|
572
|
+
inventoryLoaders(ctx);
|
|
573
|
+
|
|
574
|
+
// Apply island classifications to file records
|
|
575
|
+
const classMap = new Map(ctx.islandClassifications.map((c) => [c.path, c]));
|
|
576
|
+
for (const f of ctx.files) {
|
|
577
|
+
if (f.category !== "island") continue;
|
|
578
|
+
const classification = classMap.get(f.path);
|
|
579
|
+
if (!classification) continue;
|
|
580
|
+
|
|
581
|
+
if (classification.type === "wrapper") {
|
|
582
|
+
f.action = "delete";
|
|
583
|
+
f.notes = "Island wrapper — imports repointed to component";
|
|
584
|
+
} else {
|
|
585
|
+
f.action = "transform";
|
|
586
|
+
f.targetPath = classification.suggestedTarget;
|
|
587
|
+
f.notes = "Standalone island moved to components";
|
|
588
|
+
}
|
|
589
|
+
}
|
|
460
590
|
}
|
|
@@ -134,6 +134,73 @@ function moveStaticFiles(ctx: MigrationContext) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Handle multi-brand static directories (static-cv/, static-lb/, etc.).
|
|
139
|
+
* The "primary" brand's assets go to public/.
|
|
140
|
+
*/
|
|
141
|
+
function moveMultiBrandStaticFiles(ctx: MigrationContext) {
|
|
142
|
+
const entries = fs.readdirSync(ctx.sourceDir, { withFileTypes: true });
|
|
143
|
+
const staticDirs = entries.filter(
|
|
144
|
+
(e) => e.isDirectory() && e.name.startsWith("static-"),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (staticDirs.length === 0) return;
|
|
148
|
+
|
|
149
|
+
// Use the first one as primary (or match by site name)
|
|
150
|
+
const primaryDir = staticDirs[0];
|
|
151
|
+
const primaryPath = path.join(ctx.sourceDir, primaryDir.name);
|
|
152
|
+
const publicDir = path.join(ctx.sourceDir, "public");
|
|
153
|
+
|
|
154
|
+
log(ctx, `Found multi-brand static dirs: ${staticDirs.map((d) => d.name).join(", ")}`);
|
|
155
|
+
log(ctx, `Using ${primaryDir.name} as primary → public/`);
|
|
156
|
+
|
|
157
|
+
function copyRecursive(dir: string, base: string) {
|
|
158
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
const srcPath = path.join(dir, item.name);
|
|
161
|
+
const relFromBase = path.relative(base, srcPath);
|
|
162
|
+
const destPath = path.join(publicDir, relFromBase);
|
|
163
|
+
|
|
164
|
+
if (item.name === "tailwind.css" || item.name === "adminIcons.ts") continue;
|
|
165
|
+
// Skip partytown (not needed in Workers)
|
|
166
|
+
if (item.name === "~partytown" || item.name === "partytown") continue;
|
|
167
|
+
|
|
168
|
+
if (item.isDirectory()) {
|
|
169
|
+
copyRecursive(srcPath, base);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (ctx.dryRun) {
|
|
174
|
+
log(ctx, `[DRY] Would copy: ${primaryDir.name}/${relFromBase} → public/${relFromBase}`);
|
|
175
|
+
ctx.movedFiles.push({ from: `${primaryDir.name}/${relFromBase}`, to: `public/${relFromBase}` });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
180
|
+
fs.copyFileSync(srcPath, destPath);
|
|
181
|
+
ctx.movedFiles.push({ from: `${primaryDir.name}/${relFromBase}`, to: `public/${relFromBase}` });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
copyRecursive(primaryPath, primaryPath);
|
|
186
|
+
|
|
187
|
+
// Clean up all static-* dirs (both root and src/)
|
|
188
|
+
if (!ctx.dryRun) {
|
|
189
|
+
for (const d of staticDirs) {
|
|
190
|
+
const rootDir = path.join(ctx.sourceDir, d.name);
|
|
191
|
+
if (fs.existsSync(rootDir)) {
|
|
192
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
193
|
+
log(ctx, `Deleted: ${d.name}/`);
|
|
194
|
+
}
|
|
195
|
+
const srcDir = path.join(ctx.sourceDir, "src", d.name);
|
|
196
|
+
if (fs.existsSync(srcDir)) {
|
|
197
|
+
fs.rmSync(srcDir, { recursive: true, force: true });
|
|
198
|
+
log(ctx, `Deleted: src/${d.name}/`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
137
204
|
function cleanupOldSourceDirs(ctx: MigrationContext) {
|
|
138
205
|
// After transforms, the original top-level dirs have been copied to src/.
|
|
139
206
|
// Delete the old top-level copies if they still exist and src/ has them.
|
|
@@ -173,12 +240,66 @@ function cleanupReExportSections(ctx: MigrationContext) {
|
|
|
173
240
|
}
|
|
174
241
|
}
|
|
175
242
|
|
|
243
|
+
/** Remove non-code files and directories that shouldn't be under src/ */
|
|
244
|
+
function cleanupJunkFromSrc(ctx: MigrationContext) {
|
|
245
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
246
|
+
if (!fs.existsSync(srcDir)) return;
|
|
247
|
+
|
|
248
|
+
// Remove dirs that don't belong in src/
|
|
249
|
+
const junkDirs = ["bin", "fonts", "tests", ".pilot", ".deco"];
|
|
250
|
+
for (const dir of junkDirs) {
|
|
251
|
+
const dirPath = path.join(srcDir, dir);
|
|
252
|
+
if (fs.existsSync(dirPath)) {
|
|
253
|
+
if (ctx.dryRun) {
|
|
254
|
+
log(ctx, `[DRY] Would delete junk dir: src/${dir}/`);
|
|
255
|
+
} else {
|
|
256
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
257
|
+
log(ctx, `Deleted junk from src/: ${dir}/`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Remove static-* dirs from src/
|
|
263
|
+
if (fs.existsSync(srcDir)) {
|
|
264
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
265
|
+
if (entry.isDirectory() && entry.name.startsWith("static-")) {
|
|
266
|
+
const dirPath = path.join(srcDir, entry.name);
|
|
267
|
+
if (ctx.dryRun) {
|
|
268
|
+
log(ctx, `[DRY] Would delete: src/${entry.name}/`);
|
|
269
|
+
} else {
|
|
270
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
271
|
+
log(ctx, `Deleted from src/: ${entry.name}/`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Remove non-code root files from src/
|
|
278
|
+
const junkFiles = [
|
|
279
|
+
"AGENTS.md", "account.json", "biome.json", "blockedQs.ts", "islands.ts",
|
|
280
|
+
"lint-changed.sh", "redirects-vtex.csv", "search-urls-cvlb.csv",
|
|
281
|
+
"search.csv", "sync.sh", "yarn.lock",
|
|
282
|
+
];
|
|
283
|
+
for (const file of junkFiles) {
|
|
284
|
+
const filePath = path.join(srcDir, file);
|
|
285
|
+
if (fs.existsSync(filePath)) {
|
|
286
|
+
if (ctx.dryRun) {
|
|
287
|
+
log(ctx, `[DRY] Would delete: src/${file}`);
|
|
288
|
+
} else {
|
|
289
|
+
fs.unlinkSync(filePath);
|
|
290
|
+
log(ctx, `Deleted from src/: ${file}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
176
296
|
export function cleanup(ctx: MigrationContext): void {
|
|
177
297
|
logPhase("Cleanup");
|
|
178
298
|
|
|
179
|
-
// 1. Move static → public
|
|
180
|
-
console.log(" Moving static
|
|
299
|
+
// 1. Move static → public (handles static/, static-cv/, static-lb/, etc.)
|
|
300
|
+
console.log(" Moving static assets → public/...");
|
|
181
301
|
moveStaticFiles(ctx);
|
|
302
|
+
moveMultiBrandStaticFiles(ctx);
|
|
182
303
|
|
|
183
304
|
// 2. Delete specific files
|
|
184
305
|
console.log(" Deleting old files...");
|
|
@@ -205,6 +326,7 @@ export function cleanup(ctx: MigrationContext): void {
|
|
|
205
326
|
console.log(" Cleaning up old source dirs...");
|
|
206
327
|
cleanupOldSourceDirs(ctx);
|
|
207
328
|
cleanupReExportSections(ctx);
|
|
329
|
+
cleanupJunkFromSrc(ctx);
|
|
208
330
|
|
|
209
331
|
console.log(
|
|
210
332
|
` Deleted ${ctx.deletedFiles.length} files/dirs, moved ${ctx.movedFiles.length} files`,
|
|
@@ -93,25 +93,53 @@ export function report(ctx: MigrationContext): void {
|
|
|
93
93
|
lines.push("");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Analyzer summaries
|
|
97
|
+
if (ctx.sectionMetas.length > 0) {
|
|
98
|
+
lines.push("## Section Analysis");
|
|
99
|
+
lines.push("");
|
|
100
|
+
const withLoader = ctx.sectionMetas.filter((m) => m.hasLoader).length;
|
|
101
|
+
const layouts = ctx.sectionMetas.filter((m) => m.isHeader || m.isFooter || m.isTheme).length;
|
|
102
|
+
const listings = ctx.sectionMetas.filter((m) => m.isListing).length;
|
|
103
|
+
lines.push(`- **${ctx.sectionMetas.length}** sections analyzed`);
|
|
104
|
+
lines.push(`- **${withLoader}** have loaders (extracted to \`setup/section-loaders.ts\`)`);
|
|
105
|
+
lines.push(`- **${layouts}** are layout sections (eager + sync + layout)`);
|
|
106
|
+
lines.push(`- **${listings}** are listing sections (cache = "listing")`);
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (ctx.islandClassifications.length > 0) {
|
|
111
|
+
const wrappers = ctx.islandClassifications.filter((c) => c.type === "wrapper").length;
|
|
112
|
+
const standalone = ctx.islandClassifications.filter((c) => c.type === "standalone").length;
|
|
113
|
+
lines.push("## Island Elimination");
|
|
114
|
+
lines.push("");
|
|
115
|
+
lines.push(`- **${ctx.islandClassifications.length}** islands classified`);
|
|
116
|
+
lines.push(`- **${wrappers}** wrappers (deleted, imports repointed)`);
|
|
117
|
+
lines.push(`- **${standalone}** standalone (moved to \`src/components/\`)`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ctx.loaderInventory.length > 0) {
|
|
122
|
+
const custom = ctx.loaderInventory.filter((l) => l.isCustom).length;
|
|
123
|
+
const mapped = ctx.loaderInventory.filter((l) => l.appsEquivalent).length;
|
|
124
|
+
lines.push("## Loader Inventory");
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(`- **${ctx.loaderInventory.length}** loaders inventoried`);
|
|
127
|
+
lines.push(`- **${mapped}** mapped to \`@decocms/apps\` equivalents`);
|
|
128
|
+
lines.push(`- **${custom}** custom (included in \`setup/commerce-loaders.ts\`)`);
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
// Always-present manual review items
|
|
97
133
|
lines.push("## Always Check (site-specific)");
|
|
98
134
|
lines.push("");
|
|
99
|
-
lines.push(
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
lines.push(
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
lines.push(
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
-
lines.push("- [ ] DaisyUI v4 → v5 class name changes");
|
|
109
|
-
lines.push(
|
|
110
|
-
"- [ ] Tailwind v3 → v4: verify all utility classes still work",
|
|
111
|
-
);
|
|
112
|
-
lines.push(
|
|
113
|
-
"- [ ] Check `src/styles/app.css` theme colors match the original design",
|
|
114
|
-
);
|
|
135
|
+
lines.push("- [ ] `src/setup/commerce-loaders.ts` — verify each loader mapping is correct");
|
|
136
|
+
lines.push("- [ ] `src/setup/section-loaders.ts` — verify extracted loaders work correctly");
|
|
137
|
+
lines.push("- [ ] `src/hooks/useCart.ts` — wire to actual server functions for your platform");
|
|
138
|
+
lines.push("- [ ] `src/worker-entry.ts` — verify CSP, proxy, and segment builder");
|
|
139
|
+
lines.push("- [ ] DaisyUI v4 to v5 class name changes");
|
|
140
|
+
lines.push("- [ ] Tailwind v3 to v4: verify all utility classes still work");
|
|
141
|
+
lines.push("- [ ] Check `src/styles/app.css` theme colors match the original design");
|
|
142
|
+
lines.push("- [ ] Run `npm run generate:blocks` and `npm run generate:schema` after migration");
|
|
115
143
|
lines.push("");
|
|
116
144
|
|
|
117
145
|
// Known Issues
|