@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
@@ -34,17 +34,25 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
34
34
  [/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
35
35
  [/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
36
36
  [/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
37
+ [/^"apps\/website\/components\/_seo\/[^"]+?"$/, null], // SEO preview — framework-only, remove
37
38
  [/^"apps\/website\/components\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
38
39
  [/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
39
40
  [/^"apps\/commerce\/mod\.ts"$/, `"~/types/commerce-app"`],
40
41
  [/^"apps\/commerce\/types"$/, `"@decocms/apps/commerce/types"`],
41
42
 
42
- // Apps — VTEX (hooks, utils, actions, loaders, types)
43
+ // Apps — VTEX hooks: useUser/useCart/useWishlist → local hooks (react-query based @decocms/apps hooks crash Workers SSR)
44
+ [/^"apps\/vtex\/hooks\/useUser(?:\.ts)?"$/, `"~/hooks/useUser"`],
45
+ [/^"apps\/vtex\/hooks\/useCart(?:\.ts)?"$/, `"~/hooks/useCart"`],
46
+ [/^"apps\/vtex\/hooks\/useWishlist(?:\.ts)?"$/, `"~/hooks/useWishlist"`],
43
47
  [/^"apps\/vtex\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/hooks/$1"`],
48
+ // Specific VTEX utils that moved to different paths in @decocms/apps
49
+ [/^"apps\/vtex\/utils\/fetchVTEX(?:\.ts)?"$/, `"@decocms/apps/vtex/client"`],
50
+ [/^"apps\/vtex\/utils\/client(?:\.ts)?"$/, `"@decocms/apps/vtex/client"`],
44
51
  [/^"apps\/vtex\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/utils/$1"`],
45
52
  [/^"apps\/vtex\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/actions/$1"`],
46
53
  [/^"apps\/vtex\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/loaders/$1"`],
47
54
  [/^"apps\/vtex\/types(?:\.ts)?"$/, `"@decocms/apps/vtex/types"`],
55
+ [/^"apps\/vtex\/mod(?:\.ts)?"$/, `"~/types/vtex-app"`],
48
56
  // Apps — Shopify (hooks, utils, actions, loaders)
49
57
  [/^"apps\/shopify\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/hooks/$1"`],
50
58
  [/^"apps\/shopify\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/utils/$1"`],
@@ -54,48 +62,72 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
54
62
  [/^"apps\/commerce\/sdk\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/sdk/$1"`],
55
63
  [/^"apps\/commerce\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/utils/$1"`],
56
64
 
65
+ // Apps — shared utils (STALE, fetchSafe, createHttpClient, etc.)
66
+ [/^"apps\/utils\/fetch(?:\.ts)?"$/, `"~/lib/fetch-utils"`],
67
+ [/^"apps\/utils\/http(?:\.ts)?"$/, `"~/lib/http-utils"`],
68
+ [/^"apps\/utils\/graphql(?:\.ts)?"$/, `"~/lib/graphql-utils"`],
69
+
57
70
  // Apps — catch-all (things like apps/website/mod.ts, apps/analytics/mod.ts, etc.)
58
71
  [/^"apps\/([^"]+)"$/, null], // Remove — site.ts is rewritten
59
72
 
60
73
  // Deco old CDN imports
61
74
  [/^"deco\/([^"]+)"$/, null],
62
75
 
76
+ // Remote URL imports (esm.sh, cdn.esm.sh, skypack, etc.) — remove
77
+ [/^"https?:\/\/esm\.sh\/[^"]*"$/, null],
78
+ [/^"https?:\/\/cdn\.esm\.sh\/[^"]*"$/, null],
79
+ [/^"https?:\/\/cdn\.skypack\.dev\/[^"]*"$/, null],
80
+ [/^"https?:\/\/deno\.land\/[^"]*"$/, null],
81
+
63
82
  // Std lib — redirect useful utils, remove the rest
64
83
  [/^"std\/async\/debounce(?:\.ts)?"$/, `"~/sdk/debounce"`],
65
84
  [/^"std\/([^"]+)"$/, null],
66
85
  [/^"@std\/crypto"$/, null], // Use globalThis.crypto instead
67
86
 
68
87
  // site/sdk/* → framework equivalents (before the catch-all site/ → ~/ rule)
69
- [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
88
+ [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
70
89
  [/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
71
- [/^"site\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
72
- [/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
90
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
73
91
  [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
74
92
 
75
- // $store/account.json → ~/account.json (JSON import with assertion)
76
- [/^"\$store\/account\.json"$/, `"~/account.json"`],
77
- [/^"site\/account\.json"$/, `"~/account.json"`],
93
+ // account.json → constants/account (JSON file replaced with TS module in cleanup)
94
+ [/^"\$store\/account\.json"$/, `"~/constants/account"`],
95
+ [/^"site\/account\.json"$/, `"~/constants/account"`],
96
+ [/^"~\/account\.json"$/, `"~/constants/account"`],
78
97
 
79
98
  // $store/ → ~/ (common Deno import map alias for project root)
80
- [/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
99
+ [/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
81
100
  [/^"\$store\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
82
- [/^"\$store\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
101
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
83
102
  [/^"\$store\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
84
- [/^"\$store\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
85
103
  [/^"\$store\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
104
+ // islands → components (must be before $store catch-all)
105
+ [/^"\$store\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
106
+ [/^"\$store\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
86
107
  [/^"\$store\/(.+)"$/, `"~/$1"`],
87
108
 
88
109
  // $home/ → ~/ (another common alias)
89
110
  [/^"\$home\/(.+)"$/, `"~/$1"`],
90
111
 
91
112
  // site/ → ~/
92
- [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
113
+ [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
93
114
  [/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
94
- [/^"site\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
115
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
95
116
  [/^"site\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
96
- [/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
97
117
  [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
118
+ // islands → components (must be before site/ catch-all)
119
+ [/^"site\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
120
+ [/^"site\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
98
121
  [/^"site\/(.+)"$/, `"~/$1"`],
122
+
123
+ // ~/islands/* → ~/components/* (catch any that slipped through)
124
+ [/^"~\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
125
+ [/^"~\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
126
+
127
+ // @decocms/apps hooks → local hooks (react-query hooks crash Workers SSR at module eval)
128
+ [/^"@decocms\/apps\/vtex\/hooks\/useUser"$/, `"~/hooks/useUser"`],
129
+ [/^"@decocms\/apps\/vtex\/hooks\/useCart"$/, `"~/hooks/useCart"`],
130
+ [/^"@decocms\/apps\/vtex\/hooks\/useWishlist"$/, `"~/hooks/useWishlist"`],
99
131
  ];
100
132
 
101
133
  /**
@@ -104,20 +136,21 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
104
136
  * The key is the ending of the import path, the value is the replacement specifier.
105
137
  */
106
138
  const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
107
- // sdk/clx → ~/sdk/clx (scaffolded locally with default export)
108
- [/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "~/sdk/clx"],
139
+ // sdk/clx → @decocms/start/sdk/clx (framework utility)
140
+ [/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "@decocms/start/sdk/clx"],
109
141
  // sdk/useId → react (useId is built-in in React 19)
110
142
  [/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
111
- // sdk/useOffer @decocms/apps/commerce/sdk/useOffer
112
- [/(?:\.\.\/)*sdk\/useOffer(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useOffer"],
143
+ // sdk/useOffer kept as-is (sites customize offer logic)
144
+ // sdk/useVariantPossiblities — kept as-is (sites customize variant logic)
113
145
  // sdk/format → @decocms/apps/commerce/sdk/formatPrice
114
146
  [/(?:\.\.\/)*sdk\/format(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/formatPrice"],
115
- // sdk/useVariantPossiblities → @decocms/apps/commerce/sdk/useVariantPossibilities
116
- [/(?:\.\.\/)*sdk\/useVariantPossiblities(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useVariantPossibilities"],
117
147
  // sdk/usePlatform → remove entirely
118
148
  [/(?:\.\.\/)*sdk\/usePlatform(?:\.tsx?)?$/, ""],
119
149
  // static/adminIcons → deleted (icon loaders need rewriting)
120
150
  [/(?:\.\.\/)*static\/adminIcons(?:\.ts)?$/, ""],
151
+ // islands/ui/* → components/ui/* (islands are merged into components)
152
+ [/(?:\.\.\/)*islands\/ui\/([^"]+?)(?:\.tsx?)?$/, "~/components/ui/$1"],
153
+ [/(?:\.\.\/)*islands\/([^"]+?)(?:\.tsx?)?$/, "~/components/$1"],
121
154
  ];
122
155
 
123
156
  /**
@@ -132,10 +165,27 @@ const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
132
165
  *
133
166
  * When a rule maps to null, the entire import line is removed.
134
167
  */
135
- export function transformImports(content: string): TransformResult {
168
+ export function transformImports(
169
+ content: string,
170
+ islandWrapperTargets?: Map<string, string>,
171
+ ): TransformResult {
136
172
  const notes: string[] = [];
137
173
  let changed = false;
138
174
 
175
+ // Build dynamic rules from island wrapper targets (wrapper island → actual component)
176
+ const dynamicRules: Array<[RegExp, string]> = [];
177
+ if (islandWrapperTargets) {
178
+ for (const [islandPath, targetImport] of islandWrapperTargets) {
179
+ const componentPath = islandPath.replace("islands/", "components/").replace(/\.tsx?$/, "");
180
+ // Match ~/components/X and rewrite to the wrapper's actual target
181
+ const escaped = componentPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
+ dynamicRules.push([
183
+ new RegExp(`^"~/${escaped}"$`),
184
+ `"${targetImport}"`,
185
+ ]);
186
+ }
187
+ }
188
+
139
189
  // Strip BOM that prevents ^ matching on the first line
140
190
  if (content.charCodeAt(0) === 0xfeff) {
141
191
  content = content.slice(1);
@@ -180,6 +230,13 @@ export function transformImports(content: string): TransformResult {
180
230
  );
181
231
  }
182
232
 
233
+ function applyDynamicRules(result: string): string {
234
+ for (const [dynPattern, dynReplacement] of dynamicRules) {
235
+ if (dynPattern.test(result)) return dynReplacement;
236
+ }
237
+ return result;
238
+ }
239
+
183
240
  function rewriteSpecifier(specifier: string): string | null {
184
241
  // Remove quotes for matching
185
242
  const inner = specifier.slice(1, -1);
@@ -187,17 +244,16 @@ export function transformImports(content: string): TransformResult {
187
244
  for (const [pattern, replacement] of IMPORT_RULES) {
188
245
  if (pattern.test(`"${inner}"`)) {
189
246
  if (replacement === null) return null;
190
- // Apply regex replacement
191
247
  let result = `"${inner}"`.replace(pattern, replacement);
192
- // Strip .ts/.tsx extensions from the rewritten path if it's a relative/alias import
193
- const resultInner = result.slice(1, -1);
248
+ let resultInner = result.slice(1, -1);
194
249
  if (
195
250
  (resultInner.startsWith("~/") || resultInner.startsWith("./") || resultInner.startsWith("../")) &&
196
251
  (resultInner.endsWith(".ts") || resultInner.endsWith(".tsx"))
197
252
  ) {
198
- result = `"${resultInner.replace(/\.tsx?$/, "")}"`;
253
+ resultInner = resultInner.replace(/\.tsx?$/, "");
254
+ result = `"${resultInner}"`;
199
255
  }
200
- return result;
256
+ return applyDynamicRules(result);
201
257
  }
202
258
  }
203
259
 
@@ -205,8 +261,9 @@ export function transformImports(content: string): TransformResult {
205
261
  if (inner.startsWith("./") || inner.startsWith("../")) {
206
262
  for (const [pattern, replacement] of RELATIVE_SDK_REWRITES) {
207
263
  if (pattern.test(inner)) {
208
- if (replacement === "") return null; // remove the import
209
- return `"${replacement}"`;
264
+ if (replacement === "") return null;
265
+ const resolved = inner.replace(pattern, replacement);
266
+ return applyDynamicRules(`"${resolved}"`);
210
267
  }
211
268
  }
212
269
  }
@@ -215,7 +272,7 @@ export function transformImports(content: string): TransformResult {
215
272
  if (inner.startsWith("npm:")) {
216
273
  const cleaned = inner
217
274
  .slice(4)
218
- .replace(/@[\d^~>=<.*]+$/, ""); // strip version
275
+ .replace(/@[\d^~>=<.*]+$/, "");
219
276
  return `"${cleaned}"`;
220
277
  }
221
278
 
@@ -226,7 +283,7 @@ export function transformImports(content: string): TransformResult {
226
283
  (inner.endsWith(".ts") || inner.endsWith(".tsx"))
227
284
  ) {
228
285
  const stripped = inner.replace(/\.tsx?$/, "");
229
- return `"${stripped}"`;
286
+ return applyDynamicRules(`"${stripped}"`);
230
287
  }
231
288
 
232
289
  return specifier;
@@ -247,7 +304,13 @@ export function transformImports(content: string): TransformResult {
247
304
  if (newSpec !== specifier) {
248
305
  changed = true;
249
306
  notes.push(`Rewrote: ${specifier} → ${newSpec}`);
250
- return `${prefix}${newSpec}${suffix}`;
307
+ // Strip import assertions (with/assert { type: "json" }) when the
308
+ // specifier no longer points to a JSON file (e.g. account.json → constants/account)
309
+ let cleanSuffix = suffix;
310
+ if (specifier.includes(".json") && !newSpec.includes(".json")) {
311
+ cleanSuffix = cleanSuffix.replace(/\s*(?:with|assert)\s*\{[^}]*\}\s*/, "");
312
+ }
313
+ return `${prefix}${newSpec}${cleanSuffix}`;
251
314
  }
252
315
  return `${prefix}${specifier}${suffix}`;
253
316
  }
@@ -1,5 +1,17 @@
1
1
  import type { TransformResult } from "../types.ts";
2
2
 
3
+ /**
4
+ * Convert JSX attribute syntax to object literal entries.
5
+ * e.g. `page={page}` → `page`, `foo={bar.baz}` → `foo: bar.baz`, `label="hi"` → `label: "hi"`
6
+ */
7
+ function jsxAttrsToObjectEntries(jsxAttrs: string): string {
8
+ return jsxAttrs
9
+ .replace(/(\w+)=\{(\1)\}/g, "$1") // shorthand: name={name} → name
10
+ .replace(/(\w+)=\{([^}]+)\}/g, "$1: $2") // name={expr} → name: expr
11
+ .replace(/(\w+)="([^"]*)"/g, '$1: "$2"') // name="str" → name: "str"
12
+ .replace(/(\w+)='([^']*)'/g, "$1: '$2'"); // name='str' → name: 'str'
13
+ }
14
+
3
15
  /**
4
16
  * Transforms Preact JSX patterns to React JSX patterns.
5
17
  *
@@ -32,11 +44,23 @@ export function transformJsx(content: string): TransformResult {
32
44
  changed = true;
33
45
  }
34
46
 
35
- // onInput= → onChange=
47
+ // onInput= → onChange= (React onChange fires on every keystroke, like Preact onInput)
36
48
  if (result.includes("onInput=")) {
37
- result = result.replace(/onInput=/g, "onChange=");
38
- changed = true;
39
- notes.push("Replaced onInput= with onChange=");
49
+ if (result.includes("onChange=") && result.includes("onInput=")) {
50
+ // Both exist — remove onInput blocks to avoid duplicate JSX attributes.
51
+ // onInput is redundant in React since onChange already fires on every keystroke.
52
+ // Match onInput={...handler...} including multi-line arrow functions
53
+ result = result.replace(
54
+ /\s*onInput=\{[^{}]*(?:\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}[^{}]*)*\}/g,
55
+ ""
56
+ );
57
+ changed = true;
58
+ notes.push("Removed onInput= (redundant with existing onChange= in React)");
59
+ } else {
60
+ result = result.replace(/onInput=/g, "onChange=");
61
+ changed = true;
62
+ notes.push("Replaced onInput= with onChange=");
63
+ }
40
64
  }
41
65
 
42
66
  // for= → htmlFor= in JSX (label elements)
@@ -229,6 +253,57 @@ export function transformJsx(content: string): TransformResult {
229
253
  notes.push("Removed data-fresh-disable-lock attribute (Fresh-specific)");
230
254
  }
231
255
 
256
+ // Replace <x.Component {...x.props} /> with <SectionRenderer section={x} />
257
+ // In TanStack Start, nested sections have Component as a string key, not a function.
258
+ // SectionRenderer from @decocms/start/hooks handles the lazy registry lookup.
259
+ //
260
+ // Gate on ANY variant of the .Component/.props pattern (simple, extra props, or multi-line).
261
+ const sectionPatternGate = /\.\s*Component[\s\n]+\{\.\.\.(\w+)\.props\}/;
262
+ if (sectionPatternGate.test(result)) {
263
+ // 1. Simple: <x.Component {...x.props} />
264
+ result = result.replace(
265
+ /<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s*\/>/g,
266
+ (_, v1) => `<SectionRenderer section={${v1}} />`,
267
+ );
268
+
269
+ // 2. Extra props: <x.Component {...x.props} page={page} />
270
+ // Convert JSX attrs to object entries: name={expr} → name: expr
271
+ result = result.replace(
272
+ /<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s+([^/]+?)\s*\/>/g,
273
+ (_, varName, _v2, extraJsx) => {
274
+ const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
275
+ return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
276
+ },
277
+ );
278
+
279
+ // 3. Multi-line: <x.Component\n {...x.props}\n/>
280
+ result = result.replace(
281
+ /<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*\/>/g,
282
+ (_, v1) => `<SectionRenderer section={${v1}} />`,
283
+ );
284
+
285
+ // 4. Multi-line with extra props: <x.Component\n {...x.props}\n extra={val}\n/>
286
+ result = result.replace(
287
+ /<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*([^/]+?)\s*\n\s*\/>/g,
288
+ (_, varName, _v2, extraJsx) => {
289
+ const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
290
+ return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
291
+ },
292
+ );
293
+
294
+ if (!result.match(/^import\s.*SectionRenderer/m)) {
295
+ const hooksImportRe = /^import\s+\{([^}]*)\}\s+from\s+["']@decocms\/start\/hooks["'];?$/m;
296
+ const hooksMatch = result.match(hooksImportRe);
297
+ if (hooksMatch) {
298
+ result = result.replace(hooksImportRe, `import { ${hooksMatch[1].trim()}, SectionRenderer } from "@decocms/start/hooks";`);
299
+ } else {
300
+ result = `import { SectionRenderer } from "@decocms/start/hooks";\n${result}`;
301
+ }
302
+ }
303
+ changed = true;
304
+ notes.push("Replaced .Component/.props section pattern with SectionRenderer");
305
+ }
306
+
232
307
  // Ensure React import exists if we introduced React.* references
233
308
  if (
234
309
  (result.includes("React.") || result.includes("React,")) &&
@@ -7,6 +7,46 @@ import type { TransformResult, SectionMeta } from "../types.ts";
7
7
  * These exports are read by generate-sections.ts in @decocms/start
8
8
  * to build the sections.gen.ts registry.
9
9
  */
10
+
11
+ const EAGER_SYNC_SECTIONS = new Set([
12
+ "UtilLinks",
13
+ "DepartamentList",
14
+ "ImageGallery",
15
+ "BannersGrid",
16
+ "Carousel",
17
+ "Tipbar",
18
+ "Live",
19
+ ]);
20
+
21
+ const SYNC_SECTIONS = new Set([
22
+ "ProductShelf",
23
+ "ProductShelfTabbed",
24
+ "ProductShelfGroup",
25
+ "ProductShelfTopSort",
26
+ "CouponList",
27
+ "NotFoundChallenge",
28
+ "MountedPDP",
29
+ "BackgroundWrapper",
30
+ "SearchResult",
31
+ "LpCartao",
32
+ ]);
33
+
34
+ const LISTING_CACHE_SECTIONS = new Set([
35
+ "ProductShelf",
36
+ "ProductShelfTabbed",
37
+ "ProductShelfGroup",
38
+ "ProductShelfTimedOffers",
39
+ ]);
40
+
41
+ const STATIC_CACHE_SECTIONS = new Set([
42
+ "InstagramPosts",
43
+ "Faq",
44
+ ]);
45
+
46
+ function getSectionBasename(filePath: string): string {
47
+ return filePath.split("/").pop()?.replace(/\.\w+$/, "") || "";
48
+ }
49
+
10
50
  export function transformSectionConventions(
11
51
  content: string,
12
52
  sectionMeta: SectionMeta | undefined,
@@ -18,6 +58,7 @@ export function transformSectionConventions(
18
58
  const notes: string[] = [];
19
59
  let result = content;
20
60
  let changed = false;
61
+ const basename = getSectionBasename(sectionMeta.path);
21
62
 
22
63
  // Header, footer, theme → eager + sync + layout
23
64
  if (sectionMeta.isHeader || sectionMeta.isFooter || sectionMeta.isTheme) {
@@ -31,14 +72,50 @@ export function transformSectionConventions(
31
72
  notes.push("Added: export const sync = true");
32
73
  changed = true;
33
74
  }
34
- if (!result.includes("export const layout")) {
75
+ // Header in golden does NOT have layout=true; only footer+theme do
76
+ if ((sectionMeta.isFooter || sectionMeta.isTheme) && !result.includes("export const layout")) {
35
77
  result += "export const layout = true;\n";
36
78
  notes.push("Added: export const layout = true");
37
79
  changed = true;
38
80
  }
39
81
  }
40
82
 
41
- // Listing sections → cache = "listing"
83
+ // Known eager+sync sections (non-layout)
84
+ if (EAGER_SYNC_SECTIONS.has(basename)) {
85
+ if (!result.includes("export const eager")) {
86
+ result += "\nexport const eager = true;\n";
87
+ notes.push(`Added: export const eager = true (${basename})`);
88
+ changed = true;
89
+ }
90
+ if (!result.includes("export const sync")) {
91
+ result += "export const sync = true;\n";
92
+ notes.push(`Added: export const sync = true (${basename})`);
93
+ changed = true;
94
+ }
95
+ }
96
+
97
+ // Known sync-only sections
98
+ if (SYNC_SECTIONS.has(basename) && !result.includes("export const sync")) {
99
+ result += "\nexport const sync = true;\n";
100
+ notes.push(`Added: export const sync = true (${basename})`);
101
+ changed = true;
102
+ }
103
+
104
+ // Listing cache sections
105
+ if (LISTING_CACHE_SECTIONS.has(basename) && !result.includes("export const cache")) {
106
+ result += '\nexport const cache = "listing";\n';
107
+ notes.push(`Added: export const cache = "listing" (${basename})`);
108
+ changed = true;
109
+ }
110
+
111
+ // Static cache sections
112
+ if (STATIC_CACHE_SECTIONS.has(basename) && !result.includes("export const cache")) {
113
+ result += '\nexport const cache = "static";\n';
114
+ notes.push(`Added: export const cache = "static" (${basename})`);
115
+ changed = true;
116
+ }
117
+
118
+ // Generic: listing sections not already matched above
42
119
  if (sectionMeta.isListing && !result.includes("export const cache")) {
43
120
  result += '\nexport const cache = "listing";\n';
44
121
  notes.push('Added: export const cache = "listing"');
@@ -52,9 +129,34 @@ export function transformSectionConventions(
52
129
  changed = true;
53
130
  }
54
131
 
132
+ // Sections that render nested Section children need sync so they're in
133
+ // the syncComponents registry (SectionRenderer resolves the string key).
134
+ const hasNestedSections =
135
+ /children:\s*Section\b/.test(result) || /fallback:\s*Section\b/.test(result);
136
+ if (hasNestedSections && !result.includes("export const sync")) {
137
+ result += "\nexport const sync = true;\n";
138
+ notes.push("Added: export const sync = true (renders nested Section children)");
139
+ changed = true;
140
+ }
141
+
142
+ // Re-export sections that wrap PDP/nested content need sync too.
143
+ // Detect: file is a re-export AND the target component renders nested Sections
144
+ const isReExport = /^export\s+\{[^}]*default[^}]*\}\s+from\s+/.test(result.trim());
145
+ if (isReExport && (basename === "MountedPDP" || basename === "NotFoundChallenge")) {
146
+ if (!result.includes("export const sync")) {
147
+ result += "\nexport const sync = true;\n";
148
+ notes.push(`Added: export const sync = true (re-export: ${basename})`);
149
+ changed = true;
150
+ }
151
+ }
152
+
153
+ // Don't add LoadingFallback re-exports to thin section files —
154
+ // we can't guarantee the target component exports it.
155
+ // Instead, if it's a listing section, a generic skeleton will be added below.
156
+
55
157
  // Generate a basic LoadingFallback if the section doesn't have one
56
158
  // and it's a listing section (visible skeleton improvement)
57
- if (sectionMeta.isListing && !sectionMeta.hasLoadingFallback) {
159
+ if (sectionMeta.isListing && !sectionMeta.hasLoadingFallback && !result.includes("LoadingFallback")) {
58
160
  result += `
59
161
  export function LoadingFallback() {
60
162
  return (
@@ -131,6 +131,8 @@ export interface MigrationContext {
131
131
  sourceDir: string;
132
132
  siteName: string;
133
133
  platform: Platform;
134
+ /** VTEX account name (e.g. "casaevideonewio") — extracted from source code */
135
+ vtexAccount: string | null;
134
136
  gtmId: string | null;
135
137
 
136
138
  /** deno.json import map entries */
@@ -151,6 +153,8 @@ export interface MigrationContext {
151
153
  sectionMetas: SectionMeta[];
152
154
  /** Island classifications */
153
155
  islandClassifications: IslandClassification[];
156
+ /** Map from island path → wrapped component import path (for wrapper islands) */
157
+ islandWrapperTargets: Map<string, string>;
154
158
  /** Loader inventory */
155
159
  loaderInventory: LoaderInfo[];
156
160
 
@@ -191,6 +195,7 @@ export function createContext(
191
195
  sourceDir,
192
196
  siteName: "",
193
197
  platform: "custom",
198
+ vtexAccount: null,
194
199
  gtmId: null,
195
200
  importMap: {},
196
201
  discoveredNpmDeps: {},
@@ -199,6 +204,7 @@ export function createContext(
199
204
  files: [],
200
205
  sectionMetas: [],
201
206
  islandClassifications: [],
207
+ islandWrapperTargets: new Map(),
202
208
  loaderInventory: [],
203
209
  scaffoldedFiles: [],
204
210
  transformedFiles: [],