@decocms/start 1.2.6 → 1.2.8

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 (32) hide show
  1. package/package.json +1 -1
  2. package/scripts/deco-migrate-cli.ts +444 -0
  3. package/scripts/migrate/analyzers/island-classifier.ts +73 -0
  4. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  5. package/scripts/migrate/analyzers/section-metadata.ts +91 -0
  6. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  7. package/scripts/migrate/phase-analyze.ts +147 -17
  8. package/scripts/migrate/phase-cleanup.ts +124 -2
  9. package/scripts/migrate/phase-report.ts +44 -16
  10. package/scripts/migrate/phase-scaffold.ts +38 -132
  11. package/scripts/migrate/phase-transform.ts +28 -3
  12. package/scripts/migrate/phase-verify.ts +127 -5
  13. package/scripts/migrate/templates/app-css.ts +204 -0
  14. package/scripts/migrate/templates/cache-config.ts +26 -0
  15. package/scripts/migrate/templates/commerce-loaders.ts +124 -0
  16. package/scripts/migrate/templates/hooks.ts +358 -0
  17. package/scripts/migrate/templates/package-json.ts +29 -6
  18. package/scripts/migrate/templates/routes.ts +41 -136
  19. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  20. package/scripts/migrate/templates/section-loaders.ts +108 -0
  21. package/scripts/migrate/templates/server-entry.ts +174 -67
  22. package/scripts/migrate/templates/setup.ts +64 -55
  23. package/scripts/migrate/templates/types-gen.ts +119 -0
  24. package/scripts/migrate/templates/ui-components.ts +113 -0
  25. package/scripts/migrate/templates/vite-config.ts +18 -1
  26. package/scripts/migrate/templates/wrangler.ts +4 -1
  27. package/scripts/migrate/transforms/dead-code.ts +23 -2
  28. package/scripts/migrate/transforms/imports.ts +40 -10
  29. package/scripts/migrate/transforms/jsx.ts +9 -0
  30. package/scripts/migrate/transforms/section-conventions.ts +83 -0
  31. package/scripts/migrate/types.ts +74 -0
  32. package/src/routes/cmsRoute.ts +26 -0
@@ -17,99 +17,11 @@ export function generateRoutes(
17
17
  }
18
18
 
19
19
  function generateRoot(ctx: MigrationContext, siteTitle: string): string {
20
- const gtmScript = ctx.gtmId
21
- ? `
22
- // Google Tag Manager
23
- useEffect(() => {
24
- if (typeof window === "undefined") return;
25
- const script = document.createElement("script");
26
- script.async = true;
27
- script.src = "https://www.googletagmanager.com/gtm.js?id=${ctx.gtmId}";
28
- document.head.appendChild(script);
29
- window.dataLayer = window.dataLayer || [];
30
- window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
31
- }, []);`
32
- : "";
33
-
34
- return `import { useState, useEffect, useRef } from "react";
35
- import {
36
- createRootRoute,
37
- HeadContent,
38
- Outlet,
39
- Scripts,
40
- useRouterState,
41
- } from "@tanstack/react-router";
42
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
43
- import { LiveControls } from "@decocms/start/hooks";
44
- import { ANALYTICS_SCRIPT } from "@decocms/start/sdk/analytics";
20
+ return `import { createRootRoute } from "@tanstack/react-router";
21
+ import { DecoRootLayout } from "@decocms/start/hooks";
45
22
  // @ts-ignore Vite ?url import
46
23
  import appCss from "../styles/app.css?url";
47
24
 
48
- declare global {
49
- interface Window {
50
- __deco_ready?: boolean;
51
- dataLayer: unknown[];
52
- }
53
- }
54
-
55
- const PROGRESS_CSS = \`
56
- @keyframes progressSlide { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
57
- .nav-progress-bar { animation: progressSlide 1s ease-in-out infinite; }
58
- \`;
59
-
60
- function NavigationProgress() {
61
- const isLoading = useRouterState({ select: (s) => s.isLoading });
62
- if (!isLoading) return null;
63
- return (
64
- <div className="fixed top-0 left-0 right-0 z-[9999] h-1 bg-primary/20 overflow-hidden">
65
- <style dangerouslySetInnerHTML={{ __html: PROGRESS_CSS }} />
66
- <div className="nav-progress-bar h-full w-1/3 bg-primary rounded-full" />
67
- </div>
68
- );
69
- }
70
-
71
- function StableOutlet() {
72
- const isLoading = useRouterState({ select: (s) => s.isLoading });
73
- const ref = useRef<HTMLDivElement>(null);
74
- const savedHeight = useRef<number | undefined>(undefined);
75
-
76
- useEffect(() => {
77
- if (isLoading && ref.current) {
78
- savedHeight.current = ref.current.offsetHeight;
79
- }
80
- if (!isLoading) {
81
- savedHeight.current = undefined;
82
- }
83
- }, [isLoading]);
84
-
85
- return (
86
- <div ref={ref} style={savedHeight.current ? { minHeight: savedHeight.current } : undefined}>
87
- <Outlet />
88
- </div>
89
- );
90
- }
91
-
92
- const DECO_EVENTS_BOOTSTRAP = \`
93
- window.DECO = window.DECO || {};
94
- window.DECO.events = window.DECO.events || {
95
- _q: [],
96
- _subs: [],
97
- dispatch: function(e) {
98
- this._q.push(e);
99
- for (var i = 0; i < this._subs.length; i++) {
100
- try { this._subs[i](e); } catch(err) { console.error('[DECO.events]', err); }
101
- }
102
- },
103
- subscribe: function(fn) {
104
- this._subs.push(fn);
105
- for (var i = 0; i < this._q.length; i++) {
106
- try { fn(this._q[i]); } catch(err) {}
107
- }
108
- }
109
- };
110
- window.dataLayer = window.dataLayer || [];
111
- \`;
112
-
113
25
  export const Route = createRootRoute({
114
26
  head: () => ({
115
27
  meta: [
@@ -126,45 +38,11 @@ export const Route = createRootRoute({
126
38
  });
127
39
 
128
40
  function RootLayout() {
129
- const [queryClient] = useState(
130
- () =>
131
- new QueryClient({
132
- defaultOptions: {
133
- queries: { staleTime: 30_000 },
134
- },
135
- }),
136
- );
137
- ${gtmScript}
138
-
139
- useEffect(() => {
140
- const id = setTimeout(() => {
141
- window.__deco_ready = true;
142
- document.dispatchEvent(new Event("deco:ready"));
143
- }, 500);
144
- return () => clearTimeout(id);
145
- }, []);
146
-
147
41
  return (
148
- <html lang="pt-BR" data-theme="light" suppressHydrationWarning>
149
- <head>
150
- <HeadContent />${ctx.fontFamily ? `
151
- <link rel="preconnect" href="https://fonts.googleapis.com" />
152
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
153
- <link href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(ctx.fontFamily)}:wght@400;500;600;700&display=swap" rel="stylesheet" />` : ""}
154
- </head>
155
- <body className="bg-base-200 text-base-content" suppressHydrationWarning>
156
- <script dangerouslySetInnerHTML={{ __html: DECO_EVENTS_BOOTSTRAP }} />
157
- <QueryClientProvider client={queryClient}>
158
- <NavigationProgress />
159
- <main>
160
- <StableOutlet />
161
- </main>
162
- </QueryClientProvider>
163
- <LiveControls site="${ctx.siteName}" />
164
- <script type="module" dangerouslySetInnerHTML={{ __html: ANALYTICS_SCRIPT }} />
165
- <Scripts />
166
- </body>
167
- </html>
42
+ <DecoRootLayout
43
+ lang="pt-BR"
44
+ siteName="${ctx.siteName}"
45
+ />
168
46
  );
169
47
  }
170
48
  `;
@@ -172,7 +50,8 @@ ${gtmScript}
172
50
 
173
51
  function generateIndex(siteTitle: string): string {
174
52
  return `import { createFileRoute } from "@tanstack/react-router";
175
- import { cmsHomeRouteConfig, CmsPage, NotFoundPage } from "@decocms/start/routes";
53
+ import { cmsHomeRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
54
+ import { DecoPageRenderer } from "@decocms/start/hooks";
176
55
 
177
56
  export const Route = createFileRoute("/")({
178
57
  ...cmsHomeRouteConfig({
@@ -184,15 +63,26 @@ export const Route = createFileRoute("/")({
184
63
 
185
64
  function HomePage() {
186
65
  const data = Route.useLoaderData() as Record<string, any> | null;
187
- if (!data) return <NotFoundPage />;
66
+
67
+ if (!data) {
68
+ return (
69
+ <div className="min-h-screen flex items-center justify-center">
70
+ <div className="text-center">
71
+ <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>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
188
77
 
189
78
  return (
190
- <CmsPage
79
+ <DecoPageRenderer
191
80
  sections={data.resolvedSections ?? []}
192
81
  deferredSections={data.deferredSections ?? []}
193
82
  deferredPromises={data.deferredPromises}
194
83
  pagePath={data.pagePath}
195
84
  pageUrl={data.pageUrl}
85
+ loadDeferredSectionFn={deferredSectionLoader}
196
86
  />
197
87
  );
198
88
  }
@@ -201,34 +91,49 @@ function HomePage() {
201
91
 
202
92
  function generateCatchAll(siteTitle: string): string {
203
93
  return `import { createFileRoute } from "@tanstack/react-router";
204
- import { cmsRouteConfig, CmsPage, NotFoundPage } from "@decocms/start/routes";
94
+ import { cmsRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
95
+ import { DecoPageRenderer } from "@decocms/start/hooks";
205
96
 
206
97
  const routeConfig = cmsRouteConfig({
207
98
  siteName: "${siteTitle}",
208
99
  defaultTitle: "${siteTitle}",
100
+ ignoreSearchParams: ["skuId"],
209
101
  });
210
102
 
211
103
  export const Route = createFileRoute("/$")({
212
104
  ...routeConfig,
213
- component: CatchAllPage,
105
+ component: CmsPage,
214
106
  notFoundComponent: NotFoundPage,
215
- staleTime: 30_000,
216
107
  });
217
108
 
218
- function CatchAllPage() {
109
+ function CmsPage() {
219
110
  const data = Route.useLoaderData() as Record<string, any> | null;
220
111
  if (!data) return <NotFoundPage />;
221
112
 
222
113
  return (
223
- <CmsPage
114
+ <DecoPageRenderer
224
115
  sections={data.resolvedSections ?? []}
225
116
  deferredSections={data.deferredSections ?? []}
226
117
  deferredPromises={data.deferredPromises}
227
118
  pagePath={data.pagePath}
228
119
  pageUrl={data.pageUrl}
120
+ loadDeferredSectionFn={deferredSectionLoader}
229
121
  />
230
122
  );
231
123
  }
124
+
125
+ function NotFoundPage() {
126
+ return (
127
+ <div className="min-h-screen flex items-center justify-center">
128
+ <div className="text-center">
129
+ <h1 className="text-6xl font-bold text-base-content/20 mb-4">404</h1>
130
+ <h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
131
+ <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>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
232
137
  `;
233
138
  }
234
139
 
@@ -0,0 +1,59 @@
1
+ import type { MigrationContext } from "../types.ts";
2
+
3
+ export function generateSdkFiles(ctx: MigrationContext): Record<string, string> {
4
+ const files: Record<string, string> = {};
5
+
6
+ files["src/sdk/deviceServer.ts"] = generateDeviceServer();
7
+ files["src/sdk/logger.ts"] = generateLogger();
8
+
9
+ return files;
10
+ }
11
+
12
+ function generateDeviceServer(): string {
13
+ return `import { createServerFn } from "@tanstack/react-start";
14
+ import { getRequestHeader } from "@tanstack/react-start/server";
15
+ import { detectDevice } from "@decocms/start/sdk/useDevice";
16
+
17
+ export const getDeviceFromServer = createServerFn({ method: "GET" }).handler(
18
+ async () => {
19
+ const ua = getRequestHeader("user-agent") ?? "";
20
+ return detectDevice(ua);
21
+ },
22
+ );
23
+ `;
24
+ }
25
+
26
+ function generateLogger(): string {
27
+ return `type LogLevel = "debug" | "info" | "warn" | "error";
28
+
29
+ const LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
30
+ let minLevel: LogLevel = "info";
31
+
32
+ export function setLogLevel(level: LogLevel) {
33
+ minLevel = level;
34
+ }
35
+
36
+ function shouldLog(level: LogLevel): boolean {
37
+ return LEVELS[level] >= LEVELS[minLevel];
38
+ }
39
+
40
+ export function debug(...args: unknown[]) {
41
+ if (shouldLog("debug")) console.debug(...args);
42
+ }
43
+
44
+ export function info(...args: unknown[]) {
45
+ if (shouldLog("info")) console.info(...args);
46
+ }
47
+
48
+ export function warn(...args: unknown[]) {
49
+ if (shouldLog("warn")) console.warn(...args);
50
+ }
51
+
52
+ export function error(...args: unknown[]) {
53
+ if (shouldLog("error")) console.error(...args);
54
+ }
55
+
56
+ export const logger = { debug, info, warn, error, setLogLevel };
57
+ export default logger;
58
+ `;
59
+ }
@@ -0,0 +1,108 @@
1
+ import type { MigrationContext, SectionMeta } from "../types.ts";
2
+
3
+ const ACCOUNT_LOADER_MAP: Record<string, string> = {
4
+ personaldata: "personalData",
5
+ myorders: "orders",
6
+ orders: "orders",
7
+ cards: "cards",
8
+ payments: "cards",
9
+ addresses: "addresses",
10
+ auth: "authentication",
11
+ authentication: "authentication",
12
+ login: "authentication",
13
+ };
14
+
15
+ function getAccountLoaderName(sectionBasename: string): string {
16
+ const key = sectionBasename.toLowerCase().replace(/[^a-z]/g, "");
17
+ return ACCOUNT_LOADER_MAP[key] || "loggedIn";
18
+ }
19
+
20
+ export function generateSectionLoaders(ctx: MigrationContext): string {
21
+ const lines: string[] = [];
22
+ const isVtex = ctx.platform === "vtex";
23
+ const hasAccountSections = isVtex && ctx.sectionMetas.some((m) => m.isAccountSection && m.hasLoader);
24
+
25
+ lines.push(`/**`);
26
+ lines.push(` * Section Loaders — server-side prop enrichment for CMS sections.`);
27
+ lines.push(` *`);
28
+ 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.`);
31
+ lines.push(` */`);
32
+ lines.push(`import {`);
33
+ lines.push(` registerSectionLoaders,`);
34
+ lines.push(` withDevice,`);
35
+ lines.push(` withMobile,`);
36
+ lines.push(` withSearchParam,`);
37
+ lines.push(` compose,`);
38
+ lines.push(`} from "@decocms/start/cms";`);
39
+
40
+ if (hasAccountSections) {
41
+ lines.push(`import { vtexAccountLoaders } from "@decocms/apps/vtex/utils/accountLoaders";`);
42
+ }
43
+
44
+ lines.push(``);
45
+
46
+ const entries: string[] = [];
47
+
48
+ for (const meta of ctx.sectionMetas) {
49
+ if (!meta.hasLoader) continue;
50
+
51
+ const sectionKey = `site/${meta.path}`;
52
+ const basename = meta.path.split("/").pop()?.replace(/\.\w+$/, "") || "";
53
+
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
+ }
74
+
75
+ // Simple mixins
76
+ if (meta.loaderUsesDevice && meta.loaderUsesUrl) {
77
+ const deviceMixin = meta.usesMobileBoolean ? "withMobile()" : "withDevice()";
78
+ entries.push(` // ${meta.path}: ${meta.usesMobileBoolean ? "mobile" : "device"} + URL`);
79
+ entries.push(` "${sectionKey}": compose(${deviceMixin}, withSearchParam()),`);
80
+ } else if (meta.loaderUsesDevice) {
81
+ if (meta.usesMobileBoolean) {
82
+ entries.push(` // ${meta.path}: mobile detection`);
83
+ entries.push(` "${sectionKey}": withMobile(),`);
84
+ } else {
85
+ entries.push(` // ${meta.path}: device detection`);
86
+ entries.push(` "${sectionKey}": withDevice(),`);
87
+ }
88
+ } else if (meta.loaderUsesUrl) {
89
+ entries.push(` // ${meta.path}: URL/search params`);
90
+ entries.push(` "${sectionKey}": withSearchParam(),`);
91
+ } else {
92
+ // Complex loader — delegate to the section's own loader export
93
+ const importPath = `~/` + meta.path.replace(/\.tsx?$/, "");
94
+ entries.push(` // ${meta.path}: complex loader — delegated to section's loader export`);
95
+ entries.push(` "${sectionKey}": async (props: any, req: Request) => {`);
96
+ entries.push(` const mod = await import("${importPath}");`);
97
+ entries.push(` if (typeof mod.loader === "function") return mod.loader(props, req);`);
98
+ entries.push(` return props;`);
99
+ entries.push(` },`);
100
+ }
101
+ }
102
+
103
+ lines.push(`registerSectionLoaders({`);
104
+ lines.push(entries.join("\n"));
105
+ lines.push(`});`);
106
+
107
+ return lines.join("\n") + "\n";
108
+ }