@decocms/start 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) 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/.releaserc.json +1 -0
  7. package/package.json +1 -1
  8. package/scripts/generate-blocks.ts +8 -5
  9. package/scripts/generate-loaders.ts +79 -12
  10. package/scripts/migrate/analyzers/island-classifier.ts +23 -0
  11. package/scripts/migrate/analyzers/section-metadata.ts +63 -7
  12. package/scripts/migrate/phase-analyze.ts +190 -11
  13. package/scripts/migrate/phase-cleanup.ts +1162 -7
  14. package/scripts/migrate/phase-scaffold.ts +294 -5
  15. package/scripts/migrate/phase-transform.ts +56 -3
  16. package/scripts/migrate/templates/app-css.ts +149 -2
  17. package/scripts/migrate/templates/commerce-loaders.ts +174 -69
  18. package/scripts/migrate/templates/lib-utils.ts +255 -0
  19. package/scripts/migrate/templates/package-json.ts +30 -22
  20. package/scripts/migrate/templates/routes.ts +81 -11
  21. package/scripts/migrate/templates/section-loaders.ts +369 -33
  22. package/scripts/migrate/templates/server-entry.ts +350 -80
  23. package/scripts/migrate/templates/setup.ts +78 -8
  24. package/scripts/migrate/templates/types-gen.ts +58 -0
  25. package/scripts/migrate/templates/ui-components.ts +47 -16
  26. package/scripts/migrate/templates/vite-config.ts +17 -6
  27. package/scripts/migrate/templates/wrangler.ts +3 -1
  28. package/scripts/migrate/transforms/dead-code.ts +330 -4
  29. package/scripts/migrate/transforms/deno-isms.ts +19 -0
  30. package/scripts/migrate/transforms/imports.ts +93 -30
  31. package/scripts/migrate/transforms/jsx.ts +79 -4
  32. package/scripts/migrate/transforms/section-conventions.ts +105 -3
  33. package/scripts/migrate/types.ts +9 -1
  34. package/src/cms/resolve.ts +12 -1
  35. package/src/sdk/useScript.ts +27 -6
  36. package/src/sdk/workerEntry.ts +11 -2
  37. package/src/setup.ts +1 -1
@@ -1,37 +1,91 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
1
3
  import type { MigrationContext } from "../types.ts";
2
4
 
5
+ function discoverFonts(ctx: MigrationContext): string[] {
6
+ const fontsDir = path.join(ctx.sourceDir, "public", "fonts");
7
+ if (!fs.existsSync(fontsDir)) {
8
+ const staticFonts = path.join(ctx.sourceDir, "static", "fonts");
9
+ if (!fs.existsSync(staticFonts)) return [];
10
+ return scanFontDir(staticFonts);
11
+ }
12
+ return scanFontDir(fontsDir);
13
+ }
14
+
15
+ function scanFontDir(dir: string): string[] {
16
+ try {
17
+ return fs.readdirSync(dir)
18
+ .filter((f) => /\.(woff2)$/i.test(f))
19
+ .map((f) => `/fonts/${f}`);
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
3
25
  export function generateRoutes(
4
26
  ctx: MigrationContext,
5
27
  ): Record<string, string> {
6
28
  const siteName = ctx.siteName;
7
29
  const siteTitle = siteName.charAt(0).toUpperCase() + siteName.slice(1);
30
+ const vtexAccount = ctx.vtexAccount || siteName;
8
31
 
9
32
  return {
10
- "src/routes/__root.tsx": generateRoot(ctx, siteTitle),
11
- "src/routes/index.tsx": generateIndex(siteTitle),
12
- "src/routes/$.tsx": generateCatchAll(siteTitle),
33
+ "src/routes/__root.tsx": generateRoot(ctx, siteTitle, vtexAccount),
34
+ "src/routes/index.tsx": generateIndex(ctx, siteTitle),
35
+ "src/routes/$.tsx": generateCatchAll(ctx, siteTitle),
13
36
  "src/routes/deco/meta.ts": generateDecoMeta(),
14
37
  "src/routes/deco/invoke.$.ts": generateDecoInvoke(),
15
38
  "src/routes/deco/render.ts": generateDecoRender(),
16
39
  };
17
40
  }
18
41
 
19
- function generateRoot(ctx: MigrationContext, siteTitle: string): string {
42
+ function generateRoot(ctx: MigrationContext, siteTitle: string, vtexAccount: string): string {
43
+ const fonts = discoverFonts(ctx);
44
+ const isVtex = ctx.platform === "vtex";
45
+ const deployedSiteName = `${ctx.siteName}-tanstack`;
46
+
47
+ // Build preconnect list based on platform
48
+ const preconnects: string[] = [];
49
+ preconnects.push(` { rel: "preconnect", href: "https://decoims.com", crossOrigin: "anonymous" as const },`);
50
+ if (isVtex) {
51
+ preconnects.push(` { rel: "preconnect", href: "https://${vtexAccount}.vtexassets.com", crossOrigin: "anonymous" as const },`);
52
+ }
53
+
54
+ // Font preloads
55
+ const fontPreloads = fonts.map((f) =>
56
+ ` { rel: "preload", href: "${f}", as: "font", type: "font/woff2", crossOrigin: "anonymous" as const },`
57
+ );
58
+
59
+ // DNS prefetch for common third-party services
60
+ const dnsPrefetch: string[] = [];
61
+ if (isVtex) {
62
+ dnsPrefetch.push(` { rel: "dns-prefetch", href: "https://sp.vtex.com" },`);
63
+ }
64
+
20
65
  return `import { createRootRoute } from "@tanstack/react-router";
21
66
  import { DecoRootLayout } from "@decocms/start/hooks";
22
67
  // @ts-ignore Vite ?url import
23
68
  import appCss from "../styles/app.css?url";
24
69
 
70
+ const DEFAULT_DESCRIPTION =
71
+ "${siteTitle} - Tudo para sua casa com os melhores preços.";
72
+
25
73
  export const Route = createRootRoute({
26
74
  head: () => ({
27
75
  meta: [
28
76
  { charSet: "utf-8" },
29
77
  { name: "viewport", content: "width=device-width, initial-scale=1" },
30
78
  { title: "${siteTitle}" },
79
+ { name: "description", content: DEFAULT_DESCRIPTION },
80
+ { property: "og:site_name", content: "${siteTitle}" },
81
+ { property: "og:locale", content: "pt_BR" },
31
82
  ],
32
83
  links: [
84
+ ${preconnects.join("\n")}
85
+ ${fontPreloads.join("\n")}
33
86
  { rel: "stylesheet", href: appCss },
34
87
  { rel: "icon", href: "/favicon.ico" },
88
+ ${dnsPrefetch.join("\n")}
35
89
  ],
36
90
  }),
37
91
  component: RootLayout,
@@ -41,21 +95,24 @@ function RootLayout() {
41
95
  return (
42
96
  <DecoRootLayout
43
97
  lang="pt-BR"
44
- siteName="${ctx.siteName}"
98
+ siteName="${deployedSiteName}"${isVtex ? `
99
+ account="${vtexAccount}"` : ""}
45
100
  />
46
101
  );
47
102
  }
48
103
  `;
49
104
  }
50
105
 
51
- function generateIndex(siteTitle: string): string {
106
+ function generateIndex(ctx: MigrationContext, siteTitle: string): string {
52
107
  return `import { createFileRoute } from "@tanstack/react-router";
53
108
  import { cmsHomeRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
54
109
  import { DecoPageRenderer } from "@decocms/start/hooks";
55
110
 
56
111
  export const Route = createFileRoute("/")({
57
112
  ...cmsHomeRouteConfig({
58
- defaultTitle: "${siteTitle}",
113
+ defaultTitle: "${siteTitle} - Tudo para sua casa",
114
+ defaultDescription:
115
+ "${siteTitle} - Tudo para sua casa com os melhores preços.",
59
116
  siteName: "${siteTitle}",
60
117
  }),
61
118
  component: HomePage,
@@ -69,7 +126,8 @@ function HomePage() {
69
126
  <div className="min-h-screen flex items-center justify-center">
70
127
  <div className="text-center">
71
128
  <h1 className="text-4xl font-bold mb-4">${siteTitle}</h1>
72
- <p className="text-sm text-base-content/40 mt-2">No CMS page found for /</p>
129
+ <p className="text-lg text-base-content/60">Tudo para sua casa</p>
130
+ <p className="text-sm text-base-content/40 mt-2">Nenhuma página CMS encontrada para /</p>
73
131
  </div>
74
132
  </div>
75
133
  );
@@ -89,14 +147,16 @@ function HomePage() {
89
147
  `;
90
148
  }
91
149
 
92
- function generateCatchAll(siteTitle: string): string {
150
+ function generateCatchAll(ctx: MigrationContext, siteTitle: string): string {
93
151
  return `import { createFileRoute } from "@tanstack/react-router";
94
152
  import { cmsRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
95
153
  import { DecoPageRenderer } from "@decocms/start/hooks";
96
154
 
97
155
  const routeConfig = cmsRouteConfig({
98
156
  siteName: "${siteTitle}",
99
- defaultTitle: "${siteTitle}",
157
+ defaultTitle: "${siteTitle} - Tudo para sua casa",
158
+ defaultDescription:
159
+ "${siteTitle} - Tudo para sua casa com os melhores preços.",
100
160
  ignoreSearchParams: ["skuId"],
101
161
  });
102
162
 
@@ -122,6 +182,14 @@ function CmsPage() {
122
182
  );
123
183
  }
124
184
 
185
+ function PendingPage() {
186
+ return (
187
+ <div className="min-h-screen flex items-center justify-center">
188
+ <span className="loading loading-ring loading-xl" />
189
+ </div>
190
+ );
191
+ }
192
+
125
193
  function NotFoundPage() {
126
194
  return (
127
195
  <div className="min-h-screen flex items-center justify-center">
@@ -129,7 +197,9 @@ function NotFoundPage() {
129
197
  <h1 className="text-6xl font-bold text-base-content/20 mb-4">404</h1>
130
198
  <h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
131
199
  <p className="text-base-content/60 mb-6">No CMS page block matches this URL.</p>
132
- <a href="/" className="btn btn-primary">Go Home</a>
200
+ <a href="/" className="btn btn-primary">
201
+ Go Home
202
+ </a>
133
203
  </div>
134
204
  </div>
135
205
  );
@@ -1,15 +1,21 @@
1
1
  import type { MigrationContext, SectionMeta } from "../types.ts";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
 
3
5
  const ACCOUNT_LOADER_MAP: Record<string, string> = {
4
6
  personaldata: "personalData",
5
7
  myorders: "orders",
8
+ myordersdata: "orders",
6
9
  orders: "orders",
7
10
  cards: "cards",
8
11
  payments: "cards",
9
12
  addresses: "addresses",
10
13
  auth: "authentication",
11
14
  authentication: "authentication",
15
+ authenticationpage: "authentication",
12
16
  login: "authentication",
17
+ myinsurances: "loggedIn",
18
+ privacypolice: "loggedIn",
13
19
  };
14
20
 
15
21
  function getAccountLoaderName(sectionBasename: string): string {
@@ -17,89 +23,419 @@ function getAccountLoaderName(sectionBasename: string): string {
17
23
  return ACCOUNT_LOADER_MAP[key] || "loggedIn";
18
24
  }
19
25
 
26
+ function sectionExists(ctx: MigrationContext, sectionPath: string): boolean {
27
+ const full = path.join(ctx.sourceDir, "src", sectionPath);
28
+ if (fs.existsSync(full)) return true;
29
+ const root = path.join(ctx.sourceDir, sectionPath);
30
+ return fs.existsSync(root);
31
+ }
32
+
33
+ function loaderExists(ctx: MigrationContext, loaderPath: string): boolean {
34
+ const full = path.join(ctx.sourceDir, "src", loaderPath);
35
+ if (fs.existsSync(full)) return true;
36
+ const root = path.join(ctx.sourceDir, loaderPath);
37
+ return fs.existsSync(root);
38
+ }
39
+
20
40
  export function generateSectionLoaders(ctx: MigrationContext): string {
21
41
  const lines: string[] = [];
22
42
  const isVtex = ctx.platform === "vtex";
23
- const hasAccountSections = isVtex && ctx.sectionMetas.some((m) => m.isAccountSection && m.hasLoader);
43
+ const hasAccountSections = isVtex && ctx.sectionMetas.some((m) => m.isAccountSection);
44
+ const hasWishlistSection = sectionExists(ctx, "sections/Wishlist.tsx");
45
+ const hasInstagramSection = sectionExists(ctx, "sections/Social/InstagramPosts.tsx");
46
+ const hasCategoryBanner = sectionExists(ctx, "sections/Category/CategoryBanner.tsx");
47
+ const hasBackgroundWrapper = sectionExists(ctx, "sections/LpContent/BackgroundWrapper.tsx");
48
+ const hasProductReviews = sectionExists(ctx, "sections/Product/ProductReviews.tsx");
49
+ const hasProductDescription = sectionExists(ctx, "sections/Product/ProductDescription.tsx");
50
+ const hasProductFaq = sectionExists(ctx, "sections/Product/ProductFaq.tsx");
51
+ const hasSearchResult = sectionExists(ctx, "sections/Product/SearchResult.tsx");
52
+ const hasPrivacyPolice = sectionExists(ctx, "sections/Account/PrivacyPolice.tsx");
53
+ const hasSEOPDP = sectionExists(ctx, "sections/SEOPDP.tsx");
54
+ const hasCallCenter = sectionExists(ctx, "sections/CallCenter.tsx");
55
+ const hasIsEvents = sectionExists(ctx, "sections/Analytics/IsEvents.tsx");
56
+ const hasWishlistLoaders = loaderExists(ctx, "loaders/Wishlist/get-wishlist-list.ts");
57
+ const hasProductReviewsLoader = loaderExists(ctx, "loaders/reviews/productReviews.ts");
24
58
 
25
59
  lines.push(`/**`);
26
60
  lines.push(` * Section Loaders — server-side prop enrichment for CMS sections.`);
27
61
  lines.push(` *`);
62
+ lines.push(` * Each entry receives CMS-resolved props + Request, returns enriched props.`);
28
63
  lines.push(` * Simple patterns (device, mobile) use framework mixins.`);
29
- lines.push(` * Complex loaders delegate to the section's own loader export.`);
30
- lines.push(` * Account sections use vtexAccountLoaders from @decocms/apps.`);
64
+ lines.push(` * Complex logic (SearchResult, PDP fallback, Wishlist) is inline.`);
31
65
  lines.push(` */`);
32
66
  lines.push(`import {`);
33
67
  lines.push(` registerSectionLoaders,`);
68
+ if (hasBackgroundWrapper) lines.push(` runSingleSectionLoader,`);
34
69
  lines.push(` withDevice,`);
35
70
  lines.push(` withMobile,`);
36
71
  lines.push(` withSearchParam,`);
37
72
  lines.push(` compose,`);
38
73
  lines.push(`} from "@decocms/start/cms";`);
39
74
 
75
+ if (hasSearchResult) {
76
+ lines.push(`import { detectDevice } from "@decocms/start/sdk/useDevice";`);
77
+ }
78
+
79
+ if (isVtex) {
80
+ lines.push(`import { getVtexConfig } from "@decocms/apps/vtex";`);
81
+ if (hasWishlistSection && hasWishlistLoaders) {
82
+ lines.push(`import { getUser } from "@decocms/apps/vtex/loaders/user";`);
83
+ lines.push(`import { getVtexCookies } from "@decocms/apps/vtex/utils/cookies";`);
84
+ }
85
+ }
86
+
40
87
  if (hasAccountSections) {
41
88
  lines.push(`import { vtexAccountLoaders } from "@decocms/apps/vtex/utils/accountLoaders";`);
42
89
  }
43
90
 
91
+ if (hasProductReviewsLoader && (hasProductReviews || hasSearchResult)) {
92
+ lines.push(`import productReviewsLoader from "../loaders/reviews/productReviews";`);
93
+ }
94
+
95
+ if (hasWishlistLoaders && hasWishlistSection) {
96
+ lines.push(`import getWishlistList from "../loaders/Wishlist/get-wishlist-list";`);
97
+ lines.push(`import getWishlistItems from "../loaders/Wishlist/get-wishlist-items";`);
98
+ }
99
+
100
+ // Check for secrets file
101
+ const hasSecrets = loaderExists(ctx, "utils/secrets.ts") || loaderExists(ctx, "src/utils/secrets.ts");
102
+ if (hasSecrets && (hasProductReviews || hasProductReviewsLoader)) {
103
+ lines.push(`import { secrets } from "../utils/secrets";`);
104
+ }
105
+
106
+ // Import cachedPDP for PDP fallbacks
107
+ const needsCachedPDP = hasProductDescription || hasProductFaq || hasProductReviews;
108
+ if (isVtex && needsCachedPDP) {
109
+ lines.push(`import { cachedPDP } from "./commerce-loaders";`);
110
+ }
111
+
44
112
  lines.push(``);
45
113
 
46
114
  const entries: string[] = [];
47
115
 
116
+ // ---------- Headers ----------
48
117
  for (const meta of ctx.sectionMetas) {
49
- if (!meta.hasLoader) continue;
118
+ if (!meta.isHeader || !meta.hasLoader) continue;
119
+ const sectionKey = `site/${meta.path}`;
120
+ entries.push(` // Header: device + search param`);
121
+ entries.push(` "${sectionKey}": async (props, req) => ({`);
122
+ entries.push(` ...(await compose(withDevice(), withSearchParam())(props, req)),`);
123
+ entries.push(` userName: "",`);
124
+ entries.push(` }),`);
125
+ }
50
126
 
127
+ // ---------- Device/mobile sections ----------
128
+ for (const meta of ctx.sectionMetas) {
129
+ if (meta.isHeader || meta.isAccountSection || meta.isStatusOnly) continue;
130
+ // Skip sections with no loader AND no device needs
131
+ if (!meta.hasLoader && !meta.loaderUsesDevice) continue;
51
132
  const sectionKey = `site/${meta.path}`;
52
133
  const basename = meta.path.split("/").pop()?.replace(/\.\w+$/, "") || "";
53
134
 
54
- // Skip status-only loaders (they just set ctx.response.status — handled at route level)
55
- if (meta.isStatusOnly) {
56
- entries.push(` // ${meta.path}: status-only loader — handled at route/worker level, no section loader needed`);
57
- continue;
58
- }
59
-
60
- // Account sections -> vtexAccountLoaders
61
- if (isVtex && meta.isAccountSection) {
62
- const loaderName = getAccountLoaderName(basename);
63
- entries.push(` // Account: ${basename}`);
64
- entries.push(` "${sectionKey}": vtexAccountLoaders.${loaderName}(),`);
65
- continue;
66
- }
67
-
68
- // Header: compose device + search param
69
- if (meta.isHeader) {
70
- entries.push(` // Header: device + search param`);
71
- entries.push(` "${sectionKey}": compose(withDevice(), withSearchParam()),`);
72
- continue;
73
- }
135
+ // Skip sections handled specially below
136
+ const specialSections = [
137
+ "BackgroundWrapper", "CategoryBanner", "SearchResult",
138
+ "ProductDescription", "ProductFaq", "ProductReviews",
139
+ "SEOPDP", "CallCenter", "IsEvents", "Wishlist", "PrivacyPolice",
140
+ "InstagramPosts",
141
+ ];
142
+ if (specialSections.includes(basename)) continue;
74
143
 
75
- // Simple mixins
76
144
  if (meta.loaderUsesDevice && meta.loaderUsesUrl) {
77
145
  const deviceMixin = meta.usesMobileBoolean ? "withMobile()" : "withDevice()";
78
- entries.push(` // ${meta.path}: ${meta.usesMobileBoolean ? "mobile" : "device"} + URL`);
79
146
  entries.push(` "${sectionKey}": compose(${deviceMixin}, withSearchParam()),`);
80
147
  } else if (meta.loaderUsesDevice) {
81
148
  if (meta.usesMobileBoolean) {
82
- entries.push(` // ${meta.path}: mobile detection`);
83
149
  entries.push(` "${sectionKey}": withMobile(),`);
84
150
  } else {
85
- entries.push(` // ${meta.path}: device detection`);
86
151
  entries.push(` "${sectionKey}": withDevice(),`);
87
152
  }
88
153
  } else if (meta.loaderUsesUrl) {
89
- entries.push(` // ${meta.path}: URL/search params`);
90
154
  entries.push(` "${sectionKey}": withSearchParam(),`);
91
- } else {
92
- // Complex loader — delegate to the section's own loader export
155
+ } else if (meta.hasLoader) {
93
156
  const importPath = `~/` + meta.path.replace(/\.tsx?$/, "");
94
- entries.push(` // ${meta.path}: complex loader — delegated to section's loader export`);
95
157
  entries.push(` "${sectionKey}": async (props: any, req: Request) => {`);
96
158
  entries.push(` const mod = await import("${importPath}");`);
97
- entries.push(` if (typeof mod.loader === "function") return mod.loader(props, req);`);
159
+ // Cast to any: legacy Fresh/Deno section loaders are typed `(props, req, ctx)`.
160
+ // We invoke with 2 args; any ctx-dependent code path inside the loader will throw
161
+ // at runtime and must be refactored — the migration phase-transform flags these.
162
+ entries.push(` if (typeof mod.loader === "function") return (mod.loader as any)(props, req);`);
98
163
  entries.push(` return props;`);
99
164
  entries.push(` },`);
100
165
  }
101
166
  }
102
167
 
168
+ // ---------- BackgroundWrapper: nested section resolution ----------
169
+ if (hasBackgroundWrapper) {
170
+ entries.push(``);
171
+ entries.push(` // BackgroundWrapper: resolve nested sections`);
172
+ entries.push(` "site/sections/LpContent/BackgroundWrapper.tsx": async (props, req) => {`);
173
+ entries.push(` const sections = (props as any).sections ?? [];`);
174
+ entries.push(` const enrichedSections = await Promise.all(`);
175
+ entries.push(` sections.map(async (s: any) => {`);
176
+ entries.push(` const component = s.Component ?? s.component;`);
177
+ entries.push(` if (!component) return s;`);
178
+ entries.push(` const result = await runSingleSectionLoader({ component, props: s.props ?? {}, key: component, originalIndex: 0 } as any, req);`);
179
+ entries.push(` return { ...s, props: result.props };`);
180
+ entries.push(` }),`);
181
+ entries.push(` );`);
182
+ entries.push(` return { ...withMobile()(props, req), sections: enrichedSections };`);
183
+ entries.push(` },`);
184
+ }
185
+
186
+ // ---------- CategoryBanner: URLPattern matcher ----------
187
+ if (hasCategoryBanner) {
188
+ entries.push(``);
189
+ entries.push(` // CategoryBanner: match URL against banner patterns`);
190
+ entries.push(` "site/sections/Category/CategoryBanner.tsx": (props, req) => {`);
191
+ entries.push(` const banners = (props as any).banners ?? [];`);
192
+ entries.push(` const banner = banners.find(({ matcher }: { matcher: string }) => {`);
193
+ entries.push(` try {`);
194
+ entries.push(` return new URLPattern({ pathname: matcher }).test(req.url);`);
195
+ entries.push(` } catch {`);
196
+ entries.push(` return false;`);
197
+ entries.push(` }`);
198
+ entries.push(` });`);
199
+ entries.push(` return { ...props, banner };`);
200
+ entries.push(` },`);
201
+ }
202
+
203
+ // ---------- PDP fallbacks ----------
204
+ if (isVtex && needsCachedPDP) {
205
+ entries.push(``);
206
+ entries.push(` // PDP fallbacks — when CMS resolver fails to resolve nested __resolveType chain`);
207
+
208
+ if (hasProductDescription) {
209
+ entries.push(` "site/sections/Product/ProductDescription.tsx": async (props: any, req) => {`);
210
+ entries.push(` if (props.page?.product) return props;`);
211
+ entries.push(` const url = new URL(req.url);`);
212
+ entries.push(` const page = await cachedPDP({ __pagePath: url.pathname });`);
213
+ entries.push(` return { ...props, page };`);
214
+ entries.push(` },`);
215
+ }
216
+
217
+ if (hasProductFaq) {
218
+ entries.push(` "site/sections/Product/ProductFaq.tsx": async (props: any, req) => {`);
219
+ entries.push(` if (props.page?.product) return props;`);
220
+ entries.push(` const url = new URL(req.url);`);
221
+ entries.push(` const page = await cachedPDP({ __pagePath: url.pathname });`);
222
+ entries.push(` return { ...props, page };`);
223
+ entries.push(` },`);
224
+ }
225
+
226
+ if (hasProductReviews && hasProductReviewsLoader) {
227
+ entries.push(` "site/sections/Product/ProductReviews.tsx": async (props: any, req) => {`);
228
+ entries.push(` if (props.page?.reviews) return props;`);
229
+ entries.push(` const url = new URL(req.url);`);
230
+ entries.push(` const pdpPage = await cachedPDP({ __pagePath: url.pathname });`);
231
+ entries.push(` if (!pdpPage) return props;`);
232
+ entries.push(` const { account } = getVtexConfig();`);
233
+ entries.push(` const result = await productReviewsLoader(`);
234
+ entries.push(` { product: pdpPage },`);
235
+ entries.push(` null as any,`);
236
+ entries.push(` { account${hasSecrets ? ", ...secrets" : ""} } as any,`);
237
+ entries.push(` );`);
238
+ entries.push(` if (!result) return props;`);
239
+ entries.push(` const { getProductReview: _r, reviewLikeAction: _l, reviewVote: _v, getProductsListReviews: _p, ...serializable } = result as any;`);
240
+ entries.push(` return { ...props, page: serializable };`);
241
+ entries.push(` },`);
242
+ }
243
+ }
244
+
245
+ // ---------- SearchResult ----------
246
+ if (hasSearchResult) {
247
+ entries.push(``);
248
+ entries.push(` // SearchResult: URL parsing + device + SEO text + sponsored info`);
249
+ entries.push(` "site/sections/Product/SearchResult.tsx": (props: any, req) => {`);
250
+ entries.push(` const url = new URL(req.url);`);
251
+ entries.push(` const currentSearchTerm = url.searchParams.get("q") || null;`);
252
+ entries.push(` const pathname = url.pathname;`);
253
+ entries.push(` const page = props?.page;`);
254
+ entries.push(` const device = detectDevice(req.headers.get("user-agent") ?? "");`);
255
+ entries.push(``);
256
+ entries.push(` const seoTexts = [...(props.seoTexts || []), ...(page?.seoTexts || [])];`);
257
+ entries.push(` const sortedSeoTexts = seoTexts?.sort(`);
258
+ entries.push(` (a: any, b: any) => (b.route?.split("/")?.length ?? 0) - (a.route?.split("/")?.length ?? 0),`);
259
+ entries.push(` );`);
260
+ entries.push(` const seoText = sortedSeoTexts?.find(`);
261
+ entries.push(` (st: any) =>`);
262
+ entries.push(` pathname === st.route?.split("?")[0] ||`);
263
+ entries.push(` pathname === st.route?.split("?")[0]?.replace(",", "-"),`);
264
+ entries.push(` ) || { title: "", text: "", route: "", bottomText: "" };`);
265
+ entries.push(``);
266
+ entries.push(` let sponsoredInfo: {`);
267
+ entries.push(` pageType: string | undefined;`);
268
+ entries.push(` category: string | null;`);
269
+ entries.push(` query: string | null;`);
270
+ entries.push(` device: string;`);
271
+ entries.push(` } | null = null;`);
272
+ entries.push(` if (props.enableSponsoredBanner) {`);
273
+ entries.push(` try {`);
274
+ entries.push(` const pageType = page?.pageInfo?.pageTypes?.[0];`);
275
+ entries.push(` let category: string | null = null;`);
276
+ entries.push(` let query: string | null = null;`);
277
+ entries.push(` if (pageType === "Department") {`);
278
+ entries.push(` category = page?.breadcrumb?.itemListElement?.[0]?.name || null;`);
279
+ entries.push(` } else if (pageType === "Search") {`);
280
+ entries.push(` query = currentSearchTerm;`);
281
+ entries.push(` }`);
282
+ entries.push(` if (category || query) {`);
283
+ entries.push(` sponsoredInfo = { pageType, category, query, device };`);
284
+ entries.push(` }`);
285
+ entries.push(` } catch {`);
286
+ entries.push(` sponsoredInfo = null;`);
287
+ entries.push(` }`);
288
+ entries.push(` }`);
289
+ entries.push(``);
290
+ entries.push(` return {`);
291
+ entries.push(` ...props,`);
292
+ entries.push(` sponsoredInfo,`);
293
+ entries.push(` seoText,`);
294
+ entries.push(` device,`);
295
+ entries.push(` page: page ? { ...page, products: page.products ?? [] } : page,`);
296
+ entries.push(` currentSearchTerm,`);
297
+ entries.push(` ...(!page?.products?.length && { notFoundPage: props.notFoundPage }),`);
298
+ entries.push(` };`);
299
+ entries.push(` },`);
300
+ }
301
+
302
+ // ---------- SEO + analytics delegation ----------
303
+ if (hasSEOPDP) {
304
+ entries.push(``);
305
+ entries.push(` "site/sections/SEOPDP.tsx": async (props: any, _req) => {`);
306
+ entries.push(` const mod = await import("../sections/SEOPDP");`);
307
+ entries.push(` const result = mod.loader(props, _req, { seo: {} } as any);`);
308
+ entries.push(` return result ?? props;`);
309
+ entries.push(` },`);
310
+ }
311
+
312
+ if (hasCallCenter) {
313
+ entries.push(` "site/sections/CallCenter.tsx": (props: any, req) => {`);
314
+ entries.push(` const url = new URL(req.url);`);
315
+ entries.push(` return { ...props, pathname: url.pathname };`);
316
+ entries.push(` },`);
317
+ }
318
+
319
+ if (hasIsEvents) {
320
+ entries.push(` "site/sections/Analytics/IsEvents.tsx": async (props: any, req) => {`);
321
+ entries.push(` const mod = await import("../sections/Analytics/IsEvents");`);
322
+ entries.push(` return mod.loader(props, req) as unknown as Record<string, unknown>;`);
323
+ entries.push(` },`);
324
+ }
325
+
326
+ // ---------- Account sections ----------
327
+ if (isVtex && hasAccountSections) {
328
+ entries.push(``);
329
+ entries.push(` // Account sections — via @decocms/apps factory`);
330
+
331
+ for (const meta of ctx.sectionMetas) {
332
+ if (!meta.isAccountSection) continue;
333
+ const sectionKey = `site/${meta.path}`;
334
+ const basename = meta.path.split("/").pop()?.replace(/\.\w+$/, "") || "";
335
+ const loaderName = getAccountLoaderName(basename);
336
+
337
+ if (basename === "PersonalData") {
338
+ entries.push(` "${sectionKey}": vtexAccountLoaders.personalData({`);
339
+ entries.push(` extraProfileFields: ["isNewsletterOptIn", "cartAbandoned"],`);
340
+ entries.push(` mapProfile: (p) => ({`);
341
+ entries.push(` "@id": p.userId ?? p.id,`);
342
+ entries.push(` email: p.email,`);
343
+ entries.push(` givenName: p.firstName ?? null,`);
344
+ entries.push(` familyName: p.lastName ?? null,`);
345
+ entries.push(` taxID: p.document,`);
346
+ entries.push(` gender: p.gender,`);
347
+ entries.push(` telephone: p.homePhone,`);
348
+ entries.push(` birthDate: p.birthDate,`);
349
+ entries.push(` corporateName: p.corporateName,`);
350
+ entries.push(` tradeName: p.tradeName,`);
351
+ entries.push(` corporateDocument: p.corporateDocument,`);
352
+ entries.push(` businessPhone: p.businessPhone,`);
353
+ entries.push(` stateRegistration: p.stateRegistration,`);
354
+ entries.push(` isCorporate: p.isCorporate,`);
355
+ entries.push(` customFields: p.customFields,`);
356
+ entries.push(` }),`);
357
+ entries.push(` }),`);
358
+ } else {
359
+ entries.push(` "${sectionKey}": vtexAccountLoaders.${loaderName}(),`);
360
+ }
361
+ }
362
+ }
363
+
364
+ // ---------- Wishlist ----------
365
+ if (isVtex && hasWishlistSection && hasWishlistLoaders) {
366
+ entries.push(``);
367
+ entries.push(` // Wishlist`);
368
+ entries.push(` "site/sections/Wishlist.tsx": async (props: any, req) => {`);
369
+ entries.push(` const cookie = getVtexCookies(req);`);
370
+ entries.push(` try {`);
371
+ entries.push(` const userData = await getUser(cookie);`);
372
+ entries.push(` const userId = userData?.email ? (userData.email as string) : "";`);
373
+ entries.push(` if (!userId) return { ...props, wishlist: null };`);
374
+ entries.push(``);
375
+ entries.push(` const listResponse = await getWishlistList({ userId });`);
376
+ entries.push(` if (listResponse && typeof listResponse === "object" && "data" in (listResponse as any)) {`);
377
+ entries.push(` const { data } = listResponse as { data: { id: string; title: string }[] };`);
378
+ entries.push(` if (data?.length > 0) {`);
379
+ entries.push(` const firstList = data[0];`);
380
+ entries.push(` const itemsResponse = await getWishlistItems({ listId: firstList.id, userId });`);
381
+ entries.push(` if (itemsResponse && typeof itemsResponse === "object" && "data" in (itemsResponse as any)) {`);
382
+ entries.push(` const { data: itemsData } = itemsResponse as any;`);
383
+ entries.push(` return {`);
384
+ entries.push(` ...props,`);
385
+ entries.push(` wishlist: {`);
386
+ entries.push(` title: itemsData.title as string,`);
387
+ entries.push(` products: itemsData.products ?? [],`);
388
+ entries.push(` id: firstList.id,`);
389
+ entries.push(` userId,`);
390
+ entries.push(` },`);
391
+ entries.push(` };`);
392
+ entries.push(` }`);
393
+ entries.push(` return { ...props, wishlist: { title: firstList.title, products: [], id: firstList.id, userId } };`);
394
+ entries.push(` }`);
395
+ entries.push(` }`);
396
+ entries.push(` return { ...props, wishlist: null };`);
397
+ entries.push(` } catch (err) {`);
398
+ entries.push(` console.error("[Wishlist SectionLoader] Error:", err);`);
399
+ entries.push(` return { ...props, wishlist: null };`);
400
+ entries.push(` }`);
401
+ entries.push(` },`);
402
+ }
403
+
404
+ // ---------- Privacy cookie check ----------
405
+ if (isVtex && hasPrivacyPolice) {
406
+ entries.push(``);
407
+ const vtexAccount = ctx.vtexAccount || "casaevideonewio";
408
+ entries.push(` "site/sections/Account/PrivacyPolice.tsx": (props: any, req) => {`);
409
+ entries.push(` const cookies = req.headers.get("cookie") ?? "";`);
410
+ entries.push(` const logged = cookies.includes("VtexIdclientAutCookie_${vtexAccount}");`);
411
+ entries.push(` return { ...props, logged };`);
412
+ entries.push(` },`);
413
+ }
414
+
415
+ // ---------- Instagram ----------
416
+ if (hasInstagramSection) {
417
+ entries.push(``);
418
+ entries.push(` // Social`);
419
+ entries.push(` "site/sections/Social/InstagramPosts.tsx": async (props: any, _req) => {`);
420
+ entries.push(` const { facebookToken, layout, title, description } = props;`);
421
+ entries.push(` if (!facebookToken) return props;`);
422
+ entries.push(` try {`);
423
+ entries.push(` const fields = "media_url,media_type,permalink";`);
424
+ entries.push(" const apiUrl = `https://graph.instagram.com/me/media?access_token=${facebookToken}&fields=${fields}`;");
425
+ entries.push(` const { data } = (await fetch(apiUrl).then((r) => r.json())) as { data: any[] };`);
426
+ entries.push(` return {`);
427
+ entries.push(` data: (data || []).slice(0, layout?.numberOfPosts ?? 12),`);
428
+ entries.push(` title,`);
429
+ entries.push(` description,`);
430
+ entries.push(` layout,`);
431
+ entries.push(` };`);
432
+ entries.push(` } catch (err) {`);
433
+ entries.push(` console.error("[InstagramPosts] loader error:", err);`);
434
+ entries.push(` return props;`);
435
+ entries.push(` }`);
436
+ entries.push(` },`);
437
+ }
438
+
103
439
  lines.push(`registerSectionLoaders({`);
104
440
  lines.push(entries.join("\n"));
105
441
  lines.push(`});`);