@decocms/start 1.6.2 → 1.6.3

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 (31) hide show
  1. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
  2. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
  3. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
  4. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
  5. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
  6. package/package.json +1 -1
  7. package/scripts/generate-blocks.ts +8 -5
  8. package/scripts/migrate/analyzers/island-classifier.ts +23 -0
  9. package/scripts/migrate/analyzers/section-metadata.ts +63 -7
  10. package/scripts/migrate/phase-analyze.ts +136 -11
  11. package/scripts/migrate/phase-cleanup.ts +1057 -6
  12. package/scripts/migrate/phase-scaffold.ts +294 -5
  13. package/scripts/migrate/phase-transform.ts +14 -3
  14. package/scripts/migrate/templates/app-css.ts +149 -2
  15. package/scripts/migrate/templates/commerce-loaders.ts +173 -68
  16. package/scripts/migrate/templates/lib-utils.ts +255 -0
  17. package/scripts/migrate/templates/package-json.ts +30 -22
  18. package/scripts/migrate/templates/routes.ts +81 -11
  19. package/scripts/migrate/templates/section-loaders.ts +365 -32
  20. package/scripts/migrate/templates/server-entry.ts +350 -80
  21. package/scripts/migrate/templates/setup.ts +78 -8
  22. package/scripts/migrate/templates/types-gen.ts +58 -0
  23. package/scripts/migrate/templates/ui-components.ts +47 -16
  24. package/scripts/migrate/templates/vite-config.ts +17 -6
  25. package/scripts/migrate/templates/wrangler.ts +3 -1
  26. package/scripts/migrate/transforms/dead-code.ts +330 -4
  27. package/scripts/migrate/transforms/deno-isms.ts +19 -0
  28. package/scripts/migrate/transforms/imports.ts +93 -30
  29. package/scripts/migrate/transforms/jsx.ts +79 -4
  30. package/scripts/migrate/transforms/section-conventions.ts +105 -3
  31. package/scripts/migrate/types.ts +6 -0
@@ -71,7 +71,6 @@ const SKIP_FILES = new Set([
71
71
  "yarn.lock",
72
72
  "bun.lock",
73
73
  "bun.lockb",
74
- "account.json",
75
74
  ]);
76
75
 
77
76
  /** Files that are generated and should be deleted */
@@ -85,8 +84,8 @@ const GENERATED_FILES = new Set([
85
84
  const SDK_DELETE = new Set([
86
85
  "sdk/clx.ts",
87
86
  "sdk/useId.ts",
88
- "sdk/useOffer.ts",
89
- "sdk/useVariantPossiblities.ts",
87
+ // sdk/useOffer.ts — kept: sites often customize offer logic
88
+ // sdk/useVariantPossiblities.ts — kept: sites often customize variant logic
90
89
  "sdk/usePlatform.tsx",
91
90
  "sdk/signal.ts",
92
91
  "sdk/format.ts",
@@ -141,10 +140,27 @@ function extractInlineNpmDeps(content: string): Record<string, string> {
141
140
  while ((match = regex.exec(content)) !== null) {
142
141
  const name = match[1];
143
142
  const version = match[2] || "*";
144
- // Skip framework deps
145
143
  if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
146
144
  deps[name] = `^${version}`;
147
145
  }
146
+
147
+ // Detect well-known third-party packages imported without npm: prefix
148
+ const KNOWN_THIRD_PARTY: Record<string, string> = {
149
+ "@sentry/react": "^10.43.0",
150
+ "dompurify": "^3.3.3",
151
+ "fuse.js": "^7.0.0",
152
+ "swiper": "^11.2.6",
153
+ "lottie-web": "^5.12.2",
154
+ "class-variance-authority": "^0.7.1",
155
+ "clsx": "^2.1.1",
156
+ };
157
+ for (const [pkg, version] of Object.entries(KNOWN_THIRD_PARTY)) {
158
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ if (new RegExp(`from\\s+["']${escaped}`, "m").test(content)) {
160
+ deps[pkg] = version;
161
+ }
162
+ }
163
+
148
164
  return deps;
149
165
  }
150
166
 
@@ -218,6 +234,11 @@ function decideAction(
218
234
  return { action: "delete", notes: "Rewritten from scratch" };
219
235
  }
220
236
 
237
+ // All remaining apps/ files → delete (platform config is in setup.ts now)
238
+ if (relPath.startsWith("apps/")) {
239
+ return { action: "delete", notes: "Old app config — platform setup is in setup.ts" };
240
+ }
241
+
221
242
  // Loaders that depend on deleted admin tooling
222
243
  if (LOADER_DELETE.has(relPath)) {
223
244
  return {
@@ -247,6 +268,8 @@ function decideAction(
247
268
  return { action: "delete", notes: "Use @decocms/apps cart hooks" };
248
269
  }
249
270
 
271
+ // Non-platform cleanup is done in analyze() post-processing (needs ctx.platform)
272
+
250
273
  // Islands — classify and route to appropriate target
251
274
  if (category === "island") {
252
275
  const classification = (record as any).__islandClassification;
@@ -262,14 +285,19 @@ function decideAction(
262
285
  };
263
286
  }
264
287
 
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
288
+ // Sections that re-export from islands/ → TRANSFORM (rewrite to ~/components/)
289
+ // The CMS still references these section keys, so the file must exist.
290
+ // The island was moved to components/, so we rewrite the re-export target.
267
291
  if (category === "section" && isReExp) {
268
292
  const target = record.reExportTarget || "";
269
293
  const isIslandReExport = target.includes("islands/") ||
270
294
  target.includes("islands\\");
271
295
  if (isIslandReExport) {
272
- return { action: "delete", notes: "Re-export wrapper for island, island merged" };
296
+ return {
297
+ action: "transform",
298
+ targetPath: `src/${relPath}`,
299
+ notes: "Section re-export — island target rewritten to ~/components/",
300
+ };
273
301
  }
274
302
  // Section re-exports from components/ — keep and transform
275
303
  }
@@ -298,12 +326,13 @@ function decideAction(
298
326
  // Non-code root files that shouldn't go into src/
299
327
  const ext = path.extname(relPath);
300
328
  const nonCodeExts = new Set([".md", ".csv", ".json", ".sh", ".lock", ".yml", ".yaml", ".xml", ".html", ".txt", ".log"]);
301
- if (!relPath.includes("/") && nonCodeExts.has(ext)) {
329
+ const keepRootFiles = new Set(["account.json"]);
330
+ if (!relPath.includes("/") && nonCodeExts.has(ext) && !keepRootFiles.has(relPath)) {
302
331
  return { action: "delete", notes: "Root-level non-code file" };
303
332
  }
304
333
 
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"]);
334
+ // Root-level tooling files not app code
335
+ const rootToolingFiles = new Set(["islands.ts", "sync.sh"]);
307
336
  if (!relPath.includes("/") && rootToolingFiles.has(relPath)) {
308
337
  return { action: "delete", notes: "Root-level tooling file" };
309
338
  }
@@ -374,6 +403,47 @@ function extractGtmId(sourceDir: string): string | null {
374
403
  return match ? match[0] : null;
375
404
  }
376
405
 
406
+ function extractVtexAccount(sourceDir: string): string | null {
407
+ // Strategy 1: utils/sitename.ts — DICT_VTEX_AN mapping
408
+ for (const candidate of ["utils/sitename.ts", "sdk/sitename.ts"]) {
409
+ const fp = path.join(sourceDir, candidate);
410
+ if (!fs.existsSync(fp)) continue;
411
+ const content = fs.readFileSync(fp, "utf-8");
412
+ // Match patterns like: casaevideo: "casaevideonewio"
413
+ const match = content.match(/vtexAn|VTEX_AN/i);
414
+ if (match) {
415
+ const dictMatch = content.match(/["'](\w+)["']\s*:\s*["'](\w+myvtex|\w+newio|\w+)["']/);
416
+ if (dictMatch) return dictMatch[2];
417
+ }
418
+ }
419
+
420
+ // Strategy 2: .myvtex.com references in loaders/routes
421
+ const candidates = [
422
+ "routes/_app.tsx",
423
+ "loaders/reviews/productReviews.ts",
424
+ "apps/vtex.ts",
425
+ ];
426
+ for (const candidate of candidates) {
427
+ const fp = path.join(sourceDir, candidate);
428
+ if (!fs.existsSync(fp)) continue;
429
+ const content = fs.readFileSync(fp, "utf-8");
430
+ const match = content.match(/["'](\w+)\.myvtex\.com/);
431
+ if (match) return match[1];
432
+ const match2 = content.match(/account\s*[:=]\s*["'](\w+)["']/);
433
+ if (match2) return match2[1];
434
+ }
435
+
436
+ // Strategy 3: deno.json import map or apps/ config
437
+ const appsVtexPath = path.join(sourceDir, "apps", "vtex.ts");
438
+ if (fs.existsSync(appsVtexPath)) {
439
+ const content = fs.readFileSync(appsVtexPath, "utf-8");
440
+ const match = content.match(/account\s*:\s*["'](\w+)["']/);
441
+ if (match) return match[1];
442
+ }
443
+
444
+ return null;
445
+ }
446
+
377
447
  function extractPlatform(sourceDir: string): Platform {
378
448
  const platforms: Platform[] = ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"];
379
449
 
@@ -518,6 +588,7 @@ export function analyze(ctx: MigrationContext): void {
518
588
  // Extract metadata
519
589
  ctx.siteName = extractSiteName(ctx.sourceDir);
520
590
  ctx.platform = extractPlatform(ctx.sourceDir);
591
+ ctx.vtexAccount = ctx.platform === "vtex" ? extractVtexAccount(ctx.sourceDir) : null;
521
592
  ctx.gtmId = extractGtmId(ctx.sourceDir);
522
593
 
523
594
  // Extract theme colors and font from CMS
@@ -526,7 +597,7 @@ export function analyze(ctx: MigrationContext): void {
526
597
  ctx.fontFamily = theme.fontFamily;
527
598
 
528
599
  console.log(` Site: ${ctx.siteName}`);
529
- console.log(` Platform: ${ctx.platform}`);
600
+ console.log(` Platform: ${ctx.platform}${ctx.vtexAccount ? ` (account: ${ctx.vtexAccount})` : ""}`);
530
601
  console.log(` GTM ID: ${ctx.gtmId || "none"}`);
531
602
  if (Object.keys(ctx.themeColors).length > 0) {
532
603
  console.log(` Theme: ${Object.keys(ctx.themeColors).length} colors from CMS`);
@@ -581,10 +652,64 @@ export function analyze(ctx: MigrationContext): void {
581
652
  if (classification.type === "wrapper") {
582
653
  f.action = "delete";
583
654
  f.notes = "Island wrapper — imports repointed to component";
655
+ // Build redirect map: island path → wrapped component's migrated import path
656
+ if (classification.wrapsComponent) {
657
+ let wrappedImport = classification.wrapsComponent;
658
+ if (wrappedImport.startsWith("./") || wrappedImport.startsWith("../")) {
659
+ // Resolve relative to the island's directory
660
+ const islandDir = path.dirname(f.path);
661
+ wrappedImport = path.posix.normalize(path.posix.join(islandDir, wrappedImport));
662
+ wrappedImport = "~/" + wrappedImport;
663
+ } else {
664
+ wrappedImport = wrappedImport.replace(/^(\$store|site)\//, "~/");
665
+ }
666
+ wrappedImport = wrappedImport.replace(/\.tsx?$/, "");
667
+ ctx.islandWrapperTargets.set(f.path, wrappedImport);
668
+ }
584
669
  } else {
585
670
  f.action = "transform";
586
671
  f.targetPath = classification.suggestedTarget;
587
672
  f.notes = "Standalone island moved to components";
588
673
  }
589
674
  }
675
+
676
+ // Fix section re-exports from wrapper islands — rewrite to the wrapper's target component
677
+ if (ctx.islandWrapperTargets.size > 0) {
678
+ for (const f of ctx.files) {
679
+ if (f.category !== "section" || !f.isReExport || !f.reExportTarget) continue;
680
+ let target = f.reExportTarget;
681
+ // Resolve relative paths (../../islands/X) against section's directory
682
+ if (target.startsWith("./") || target.startsWith("../")) {
683
+ target = path.posix.normalize(path.posix.join(path.dirname(f.path), target));
684
+ }
685
+ target = target.replace(/^(\$store|site)\//, "").replace(/\.tsx?$/, "");
686
+ if (!target.startsWith("islands/")) continue;
687
+ const wrappedImport = ctx.islandWrapperTargets.get(target + ".tsx") ||
688
+ ctx.islandWrapperTargets.get(target);
689
+ if (wrappedImport) {
690
+ (f as any).__resolvedReExportTarget = wrappedImport;
691
+ f.notes = `Section re-export — island wrapper resolved to ${wrappedImport}`;
692
+ }
693
+ }
694
+ }
695
+
696
+ // Delete non-platform files (e.g. shopify/linx/vnda/wake/nuvemshop on VTEX sites)
697
+ const NON_PLATFORM_PATTERNS: Record<string, RegExp> = {
698
+ vtex: /\/(shopify|linx|vnda|wake|nuvemshop)\b/i,
699
+ shopify: /\/(vtex|linx|vnda|wake|nuvemshop)\b/i,
700
+ vnda: /\/(vtex|shopify|linx|wake|nuvemshop)\b/i,
701
+ wake: /\/(vtex|shopify|linx|vnda|nuvemshop)\b/i,
702
+ linx: /\/(vtex|shopify|vnda|wake|nuvemshop)\b/i,
703
+ nuvemshop: /\/(vtex|shopify|linx|vnda|wake)\b/i,
704
+ };
705
+ const nonPlatformRe = NON_PLATFORM_PATTERNS[ctx.platform];
706
+ if (nonPlatformRe) {
707
+ for (const f of ctx.files) {
708
+ if (f.action === "delete") continue;
709
+ if (nonPlatformRe.test("/" + f.path)) {
710
+ f.action = "delete";
711
+ f.notes = `Non-${ctx.platform} platform file`;
712
+ }
713
+ }
714
+ }
590
715
  }