@decocms/start 1.2.5 → 1.2.7

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/scripts/deco-migrate-cli.ts +444 -0
  3. package/scripts/migrate/analyzers/island-classifier.ts +73 -0
  4. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  5. package/scripts/migrate/analyzers/section-metadata.ts +91 -0
  6. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  7. package/scripts/migrate/phase-analyze.ts +147 -17
  8. package/scripts/migrate/phase-cleanup.ts +124 -2
  9. package/scripts/migrate/phase-report.ts +44 -16
  10. package/scripts/migrate/phase-scaffold.ts +38 -132
  11. package/scripts/migrate/phase-transform.ts +28 -3
  12. package/scripts/migrate/phase-verify.ts +127 -5
  13. package/scripts/migrate/templates/app-css.ts +204 -0
  14. package/scripts/migrate/templates/cache-config.ts +26 -0
  15. package/scripts/migrate/templates/commerce-loaders.ts +124 -0
  16. package/scripts/migrate/templates/hooks.ts +358 -0
  17. package/scripts/migrate/templates/package-json.ts +29 -6
  18. package/scripts/migrate/templates/routes.ts +41 -136
  19. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  20. package/scripts/migrate/templates/section-loaders.ts +108 -0
  21. package/scripts/migrate/templates/server-entry.ts +174 -67
  22. package/scripts/migrate/templates/setup.ts +64 -55
  23. package/scripts/migrate/templates/types-gen.ts +119 -0
  24. package/scripts/migrate/templates/ui-components.ts +113 -0
  25. package/scripts/migrate/templates/vite-config.ts +18 -1
  26. package/scripts/migrate/templates/wrangler.ts +4 -1
  27. package/scripts/migrate/transforms/dead-code.ts +23 -2
  28. package/scripts/migrate/transforms/imports.ts +40 -10
  29. package/scripts/migrate/transforms/jsx.ts +9 -0
  30. package/scripts/migrate/transforms/section-conventions.ts +83 -0
  31. package/scripts/migrate/types.ts +74 -0
  32. package/src/cms/resolve.ts +10 -0
  33. package/src/routes/cmsRoute.ts +13 -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 — if the section is a re-export of this island, island becomes section
250
+ // Islands — classify and route to appropriate target
218
251
  if (category === "island") {
219
- const sectionPath = relPath.replace("islands/", "sections/");
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/${sectionPath}`,
223
- notes: "Island merged into section",
260
+ targetPath: `src/${componentPath}`,
261
+ notes: "Standalone island moved to components",
224
262
  };
225
263
  }
226
264
 
227
- // Sections that are re-exports of islands → delete (island takes their place)
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
- return { action: "delete", notes: "Re-export wrapper, island merged" };
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 sitePath = path.join(sourceDir, "apps", "site.ts");
321
- if (!fs.existsSync(sitePath)) return "custom";
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
- const content = fs.readFileSync(sitePath, "utf-8");
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 in Props or default
326
- for (const p of ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"] as const) {
327
- if (content.includes(`"${p}"`) && content.includes("_platform")) {
328
- // This is just detecting what's available, default is usually "custom"
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/ → public/...");
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
- "- [ ] `FormEmail.tsx` — `invoke.resend.actions.emails.send()` needs new server function pattern",
101
- );
102
- lines.push(
103
- "- [ ] `Slider.tsx` verify `scriptAsDataURI` pattern works with React",
104
- );
105
- lines.push(
106
- "- [ ] `Theme.tsx` verify `SiteTheme` mapping to `@decocms/start`",
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