@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.
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
- package/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +136 -11
- package/scripts/migrate/phase-cleanup.ts +1057 -6
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +14 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +173 -68
- package/scripts/migrate/templates/lib-utils.ts +255 -0
- package/scripts/migrate/templates/package-json.ts +30 -22
- package/scripts/migrate/templates/routes.ts +81 -11
- package/scripts/migrate/templates/section-loaders.ts +365 -32
- package/scripts/migrate/templates/server-entry.ts +350 -80
- package/scripts/migrate/templates/setup.ts +78 -8
- package/scripts/migrate/templates/types-gen.ts +58 -0
- package/scripts/migrate/templates/ui-components.ts +47 -16
- package/scripts/migrate/templates/vite-config.ts +17 -6
- package/scripts/migrate/templates/wrangler.ts +3 -1
- package/scripts/migrate/transforms/dead-code.ts +330 -4
- package/scripts/migrate/transforms/deno-isms.ts +19 -0
- package/scripts/migrate/transforms/imports.ts +93 -30
- package/scripts/migrate/transforms/jsx.ts +79 -4
- package/scripts/migrate/transforms/section-conventions.ts +105 -3
- package/scripts/migrate/types.ts +6 -0
|
@@ -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="${
|
|
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-
|
|
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">
|
|
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,81 +23,137 @@ 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
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
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
159
|
entries.push(` if (typeof mod.loader === "function") return mod.loader(props, req);`);
|
|
@@ -100,6 +162,277 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
100
162
|
}
|
|
101
163
|
}
|
|
102
164
|
|
|
165
|
+
// ---------- BackgroundWrapper: nested section resolution ----------
|
|
166
|
+
if (hasBackgroundWrapper) {
|
|
167
|
+
entries.push(``);
|
|
168
|
+
entries.push(` // BackgroundWrapper: resolve nested sections`);
|
|
169
|
+
entries.push(` "site/sections/LpContent/BackgroundWrapper.tsx": async (props, req) => {`);
|
|
170
|
+
entries.push(` const sections = (props as any).sections ?? [];`);
|
|
171
|
+
entries.push(` const enrichedSections = await Promise.all(`);
|
|
172
|
+
entries.push(` sections.map(async (s: any) => {`);
|
|
173
|
+
entries.push(` const component = s.Component ?? s.component;`);
|
|
174
|
+
entries.push(` if (!component) return s;`);
|
|
175
|
+
entries.push(` const result = await runSingleSectionLoader({ component, props: s.props ?? {}, key: component, originalIndex: 0 } as any, req);`);
|
|
176
|
+
entries.push(` return { ...s, props: result.props };`);
|
|
177
|
+
entries.push(` }),`);
|
|
178
|
+
entries.push(` );`);
|
|
179
|
+
entries.push(` return { ...withMobile()(props, req), sections: enrichedSections };`);
|
|
180
|
+
entries.push(` },`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------- CategoryBanner: URLPattern matcher ----------
|
|
184
|
+
if (hasCategoryBanner) {
|
|
185
|
+
entries.push(``);
|
|
186
|
+
entries.push(` // CategoryBanner: match URL against banner patterns`);
|
|
187
|
+
entries.push(` "site/sections/Category/CategoryBanner.tsx": (props, req) => {`);
|
|
188
|
+
entries.push(` const banners = (props as any).banners ?? [];`);
|
|
189
|
+
entries.push(` const banner = banners.find(({ matcher }: { matcher: string }) => {`);
|
|
190
|
+
entries.push(` try {`);
|
|
191
|
+
entries.push(` return new URLPattern({ pathname: matcher }).test(req.url);`);
|
|
192
|
+
entries.push(` } catch {`);
|
|
193
|
+
entries.push(` return false;`);
|
|
194
|
+
entries.push(` }`);
|
|
195
|
+
entries.push(` });`);
|
|
196
|
+
entries.push(` return { ...props, banner };`);
|
|
197
|
+
entries.push(` },`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- PDP fallbacks ----------
|
|
201
|
+
if (isVtex && needsCachedPDP) {
|
|
202
|
+
entries.push(``);
|
|
203
|
+
entries.push(` // PDP fallbacks — when CMS resolver fails to resolve nested __resolveType chain`);
|
|
204
|
+
|
|
205
|
+
if (hasProductDescription) {
|
|
206
|
+
entries.push(` "site/sections/Product/ProductDescription.tsx": async (props: any, req) => {`);
|
|
207
|
+
entries.push(` if (props.page?.product) return props;`);
|
|
208
|
+
entries.push(` const url = new URL(req.url);`);
|
|
209
|
+
entries.push(` const page = await cachedPDP({ __pagePath: url.pathname });`);
|
|
210
|
+
entries.push(` return { ...props, page };`);
|
|
211
|
+
entries.push(` },`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (hasProductFaq) {
|
|
215
|
+
entries.push(` "site/sections/Product/ProductFaq.tsx": async (props: any, req) => {`);
|
|
216
|
+
entries.push(` if (props.page?.product) return props;`);
|
|
217
|
+
entries.push(` const url = new URL(req.url);`);
|
|
218
|
+
entries.push(` const page = await cachedPDP({ __pagePath: url.pathname });`);
|
|
219
|
+
entries.push(` return { ...props, page };`);
|
|
220
|
+
entries.push(` },`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (hasProductReviews && hasProductReviewsLoader) {
|
|
224
|
+
entries.push(` "site/sections/Product/ProductReviews.tsx": async (props: any, req) => {`);
|
|
225
|
+
entries.push(` if (props.page?.reviews) return props;`);
|
|
226
|
+
entries.push(` const url = new URL(req.url);`);
|
|
227
|
+
entries.push(` const pdpPage = await cachedPDP({ __pagePath: url.pathname });`);
|
|
228
|
+
entries.push(` if (!pdpPage) return props;`);
|
|
229
|
+
entries.push(` const { account } = getVtexConfig();`);
|
|
230
|
+
entries.push(` const result = await productReviewsLoader(`);
|
|
231
|
+
entries.push(` { product: pdpPage },`);
|
|
232
|
+
entries.push(` null as any,`);
|
|
233
|
+
entries.push(` { account${hasSecrets ? ", ...secrets" : ""} } as any,`);
|
|
234
|
+
entries.push(` );`);
|
|
235
|
+
entries.push(` if (!result) return props;`);
|
|
236
|
+
entries.push(` const { getProductReview: _r, reviewLikeAction: _l, reviewVote: _v, getProductsListReviews: _p, ...serializable } = result as any;`);
|
|
237
|
+
entries.push(` return { ...props, page: serializable };`);
|
|
238
|
+
entries.push(` },`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------- SearchResult ----------
|
|
243
|
+
if (hasSearchResult) {
|
|
244
|
+
entries.push(``);
|
|
245
|
+
entries.push(` // SearchResult: URL parsing + device + SEO text + sponsored info`);
|
|
246
|
+
entries.push(` "site/sections/Product/SearchResult.tsx": (props: any, req) => {`);
|
|
247
|
+
entries.push(` const url = new URL(req.url);`);
|
|
248
|
+
entries.push(` const currentSearchTerm = url.searchParams.get("q") || null;`);
|
|
249
|
+
entries.push(` const pathname = url.pathname;`);
|
|
250
|
+
entries.push(` const page = props?.page;`);
|
|
251
|
+
entries.push(` const device = detectDevice(req.headers.get("user-agent") ?? "");`);
|
|
252
|
+
entries.push(``);
|
|
253
|
+
entries.push(` const seoTexts = [...(props.seoTexts || []), ...(page?.seoTexts || [])];`);
|
|
254
|
+
entries.push(` const sortedSeoTexts = seoTexts?.sort(`);
|
|
255
|
+
entries.push(` (a: any, b: any) => (b.route?.split("/")?.length ?? 0) - (a.route?.split("/")?.length ?? 0),`);
|
|
256
|
+
entries.push(` );`);
|
|
257
|
+
entries.push(` const seoText = sortedSeoTexts?.find(`);
|
|
258
|
+
entries.push(` (st: any) =>`);
|
|
259
|
+
entries.push(` pathname === st.route?.split("?")[0] ||`);
|
|
260
|
+
entries.push(` pathname === st.route?.split("?")[0]?.replace(",", "-"),`);
|
|
261
|
+
entries.push(` ) || { title: "", text: "", route: "", bottomText: "" };`);
|
|
262
|
+
entries.push(``);
|
|
263
|
+
entries.push(` let sponsoredInfo: {`);
|
|
264
|
+
entries.push(` pageType: string | undefined;`);
|
|
265
|
+
entries.push(` category: string | null;`);
|
|
266
|
+
entries.push(` query: string | null;`);
|
|
267
|
+
entries.push(` device: string;`);
|
|
268
|
+
entries.push(` } | null = null;`);
|
|
269
|
+
entries.push(` if (props.enableSponsoredBanner) {`);
|
|
270
|
+
entries.push(` try {`);
|
|
271
|
+
entries.push(` const pageType = page?.pageInfo?.pageTypes?.[0];`);
|
|
272
|
+
entries.push(` let category: string | null = null;`);
|
|
273
|
+
entries.push(` let query: string | null = null;`);
|
|
274
|
+
entries.push(` if (pageType === "Department") {`);
|
|
275
|
+
entries.push(` category = page?.breadcrumb?.itemListElement?.[0]?.name || null;`);
|
|
276
|
+
entries.push(` } else if (pageType === "Search") {`);
|
|
277
|
+
entries.push(` query = currentSearchTerm;`);
|
|
278
|
+
entries.push(` }`);
|
|
279
|
+
entries.push(` if (category || query) {`);
|
|
280
|
+
entries.push(` sponsoredInfo = { pageType, category, query, device };`);
|
|
281
|
+
entries.push(` }`);
|
|
282
|
+
entries.push(` } catch {`);
|
|
283
|
+
entries.push(` sponsoredInfo = null;`);
|
|
284
|
+
entries.push(` }`);
|
|
285
|
+
entries.push(` }`);
|
|
286
|
+
entries.push(``);
|
|
287
|
+
entries.push(` return {`);
|
|
288
|
+
entries.push(` ...props,`);
|
|
289
|
+
entries.push(` sponsoredInfo,`);
|
|
290
|
+
entries.push(` seoText,`);
|
|
291
|
+
entries.push(` device,`);
|
|
292
|
+
entries.push(` page: page ? { ...page, products: page.products ?? [] } : page,`);
|
|
293
|
+
entries.push(` currentSearchTerm,`);
|
|
294
|
+
entries.push(` ...(!page?.products?.length && { notFoundPage: props.notFoundPage }),`);
|
|
295
|
+
entries.push(` };`);
|
|
296
|
+
entries.push(` },`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------- SEO + analytics delegation ----------
|
|
300
|
+
if (hasSEOPDP) {
|
|
301
|
+
entries.push(``);
|
|
302
|
+
entries.push(` "site/sections/SEOPDP.tsx": async (props: any, _req) => {`);
|
|
303
|
+
entries.push(` const mod = await import("../sections/SEOPDP");`);
|
|
304
|
+
entries.push(` const result = mod.loader(props, _req, { seo: {} } as any);`);
|
|
305
|
+
entries.push(` return result ?? props;`);
|
|
306
|
+
entries.push(` },`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (hasCallCenter) {
|
|
310
|
+
entries.push(` "site/sections/CallCenter.tsx": (props: any, req) => {`);
|
|
311
|
+
entries.push(` const url = new URL(req.url);`);
|
|
312
|
+
entries.push(` return { ...props, pathname: url.pathname };`);
|
|
313
|
+
entries.push(` },`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (hasIsEvents) {
|
|
317
|
+
entries.push(` "site/sections/Analytics/IsEvents.tsx": async (props: any, req) => {`);
|
|
318
|
+
entries.push(` const mod = await import("../sections/Analytics/IsEvents");`);
|
|
319
|
+
entries.push(` return mod.loader(props, req) as unknown as Record<string, unknown>;`);
|
|
320
|
+
entries.push(` },`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------- Account sections ----------
|
|
324
|
+
if (isVtex && hasAccountSections) {
|
|
325
|
+
entries.push(``);
|
|
326
|
+
entries.push(` // Account sections — via @decocms/apps factory`);
|
|
327
|
+
|
|
328
|
+
for (const meta of ctx.sectionMetas) {
|
|
329
|
+
if (!meta.isAccountSection) continue;
|
|
330
|
+
const sectionKey = `site/${meta.path}`;
|
|
331
|
+
const basename = meta.path.split("/").pop()?.replace(/\.\w+$/, "") || "";
|
|
332
|
+
const loaderName = getAccountLoaderName(basename);
|
|
333
|
+
|
|
334
|
+
if (basename === "PersonalData") {
|
|
335
|
+
entries.push(` "${sectionKey}": vtexAccountLoaders.personalData({`);
|
|
336
|
+
entries.push(` extraProfileFields: ["isNewsletterOptIn", "cartAbandoned"],`);
|
|
337
|
+
entries.push(` mapProfile: (p) => ({`);
|
|
338
|
+
entries.push(` "@id": p.userId ?? p.id,`);
|
|
339
|
+
entries.push(` email: p.email,`);
|
|
340
|
+
entries.push(` givenName: p.firstName ?? null,`);
|
|
341
|
+
entries.push(` familyName: p.lastName ?? null,`);
|
|
342
|
+
entries.push(` taxID: p.document,`);
|
|
343
|
+
entries.push(` gender: p.gender,`);
|
|
344
|
+
entries.push(` telephone: p.homePhone,`);
|
|
345
|
+
entries.push(` birthDate: p.birthDate,`);
|
|
346
|
+
entries.push(` corporateName: p.corporateName,`);
|
|
347
|
+
entries.push(` tradeName: p.tradeName,`);
|
|
348
|
+
entries.push(` corporateDocument: p.corporateDocument,`);
|
|
349
|
+
entries.push(` businessPhone: p.businessPhone,`);
|
|
350
|
+
entries.push(` stateRegistration: p.stateRegistration,`);
|
|
351
|
+
entries.push(` isCorporate: p.isCorporate,`);
|
|
352
|
+
entries.push(` customFields: p.customFields,`);
|
|
353
|
+
entries.push(` }),`);
|
|
354
|
+
entries.push(` }),`);
|
|
355
|
+
} else {
|
|
356
|
+
entries.push(` "${sectionKey}": vtexAccountLoaders.${loaderName}(),`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------- Wishlist ----------
|
|
362
|
+
if (isVtex && hasWishlistSection && hasWishlistLoaders) {
|
|
363
|
+
entries.push(``);
|
|
364
|
+
entries.push(` // Wishlist`);
|
|
365
|
+
entries.push(` "site/sections/Wishlist.tsx": async (props: any, req) => {`);
|
|
366
|
+
entries.push(` const cookie = getVtexCookies(req);`);
|
|
367
|
+
entries.push(` try {`);
|
|
368
|
+
entries.push(` const userData = await getUser(cookie);`);
|
|
369
|
+
entries.push(` const userId = userData?.email ? (userData.email as string) : "";`);
|
|
370
|
+
entries.push(` if (!userId) return { ...props, wishlist: null };`);
|
|
371
|
+
entries.push(``);
|
|
372
|
+
entries.push(` const listResponse = await getWishlistList({ userId });`);
|
|
373
|
+
entries.push(` if (listResponse && typeof listResponse === "object" && "data" in (listResponse as any)) {`);
|
|
374
|
+
entries.push(` const { data } = listResponse as { data: { id: string; title: string }[] };`);
|
|
375
|
+
entries.push(` if (data?.length > 0) {`);
|
|
376
|
+
entries.push(` const firstList = data[0];`);
|
|
377
|
+
entries.push(` const itemsResponse = await getWishlistItems({ listId: firstList.id, userId });`);
|
|
378
|
+
entries.push(` if (itemsResponse && typeof itemsResponse === "object" && "data" in (itemsResponse as any)) {`);
|
|
379
|
+
entries.push(` const { data: itemsData } = itemsResponse as any;`);
|
|
380
|
+
entries.push(` return {`);
|
|
381
|
+
entries.push(` ...props,`);
|
|
382
|
+
entries.push(` wishlist: {`);
|
|
383
|
+
entries.push(` title: itemsData.title as string,`);
|
|
384
|
+
entries.push(` products: itemsData.products ?? [],`);
|
|
385
|
+
entries.push(` id: firstList.id,`);
|
|
386
|
+
entries.push(` userId,`);
|
|
387
|
+
entries.push(` },`);
|
|
388
|
+
entries.push(` };`);
|
|
389
|
+
entries.push(` }`);
|
|
390
|
+
entries.push(` return { ...props, wishlist: { title: firstList.title, products: [], id: firstList.id, userId } };`);
|
|
391
|
+
entries.push(` }`);
|
|
392
|
+
entries.push(` }`);
|
|
393
|
+
entries.push(` return { ...props, wishlist: null };`);
|
|
394
|
+
entries.push(` } catch (err) {`);
|
|
395
|
+
entries.push(` console.error("[Wishlist SectionLoader] Error:", err);`);
|
|
396
|
+
entries.push(` return { ...props, wishlist: null };`);
|
|
397
|
+
entries.push(` }`);
|
|
398
|
+
entries.push(` },`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- Privacy cookie check ----------
|
|
402
|
+
if (isVtex && hasPrivacyPolice) {
|
|
403
|
+
entries.push(``);
|
|
404
|
+
const vtexAccount = ctx.vtexAccount || "casaevideonewio";
|
|
405
|
+
entries.push(` "site/sections/Account/PrivacyPolice.tsx": (props: any, req) => {`);
|
|
406
|
+
entries.push(` const cookies = req.headers.get("cookie") ?? "";`);
|
|
407
|
+
entries.push(` const logged = cookies.includes("VtexIdclientAutCookie_${vtexAccount}");`);
|
|
408
|
+
entries.push(` return { ...props, logged };`);
|
|
409
|
+
entries.push(` },`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------- Instagram ----------
|
|
413
|
+
if (hasInstagramSection) {
|
|
414
|
+
entries.push(``);
|
|
415
|
+
entries.push(` // Social`);
|
|
416
|
+
entries.push(` "site/sections/Social/InstagramPosts.tsx": async (props: any, _req) => {`);
|
|
417
|
+
entries.push(` const { facebookToken, layout, title, description } = props;`);
|
|
418
|
+
entries.push(` if (!facebookToken) return props;`);
|
|
419
|
+
entries.push(` try {`);
|
|
420
|
+
entries.push(` const fields = "media_url,media_type,permalink";`);
|
|
421
|
+
entries.push(" const apiUrl = `https://graph.instagram.com/me/media?access_token=${facebookToken}&fields=${fields}`;");
|
|
422
|
+
entries.push(` const { data } = (await fetch(apiUrl).then((r) => r.json())) as { data: any[] };`);
|
|
423
|
+
entries.push(` return {`);
|
|
424
|
+
entries.push(` data: (data || []).slice(0, layout?.numberOfPosts ?? 12),`);
|
|
425
|
+
entries.push(` title,`);
|
|
426
|
+
entries.push(` description,`);
|
|
427
|
+
entries.push(` layout,`);
|
|
428
|
+
entries.push(` };`);
|
|
429
|
+
entries.push(` } catch (err) {`);
|
|
430
|
+
entries.push(` console.error("[InstagramPosts] loader error:", err);`);
|
|
431
|
+
entries.push(` return props;`);
|
|
432
|
+
entries.push(` }`);
|
|
433
|
+
entries.push(` },`);
|
|
434
|
+
}
|
|
435
|
+
|
|
103
436
|
lines.push(`registerSectionLoaders({`);
|
|
104
437
|
lines.push(entries.join("\n"));
|
|
105
438
|
lines.push(`});`);
|