@decocms/start 0.38.0 → 0.40.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.
- package/.agents/skills/deco-migrate-script/SKILL.md +434 -0
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +382 -0
- package/.agents/skills/deco-to-tanstack-migration/references/admin-cms.md +154 -0
- package/{.cursor/skills/deco-async-rendering-site-guide/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/async-rendering.md} +296 -31
- package/.agents/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.agents/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.agents/skills/deco-to-tanstack-migration/references/css-styling.md +156 -0
- package/.agents/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.agents/skills/deco-to-tanstack-migration/references/gotchas.md +13 -0
- package/{.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/hydration-fixes.md} +139 -4
- package/.agents/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/{.cursor/skills/deco-islands-migration/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/islands.md} +0 -14
- package/.agents/skills/deco-to-tanstack-migration/references/jsx-migration.md +80 -0
- package/.agents/skills/deco-to-tanstack-migration/references/matchers.md +1064 -0
- package/{.cursor/skills/deco-tanstack-navigation/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/navigation.md} +1 -16
- package/.agents/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.agents/skills/deco-to-tanstack-migration/references/react-hooks-patterns.md +142 -0
- package/.agents/skills/deco-to-tanstack-migration/references/react-signals-state.md +72 -0
- package/{.cursor/skills/deco-tanstack-search/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/search.md} +1 -13
- package/.agents/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/{.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/storefront-patterns.md} +1 -137
- package/.agents/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +165 -0
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +209 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.agents/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/README.md +45 -0
- package/package.json +2 -1
- package/src/admin/index.ts +2 -0
- package/src/admin/invoke.ts +53 -5
- package/src/admin/setup.ts +7 -1
- package/src/apps/autoconfig.ts +50 -72
- package/src/sdk/invoke.ts +123 -12
- package/src/sdk/requestContext.ts +42 -0
- package/src/sdk/setupApps.ts +211 -0
- package/src/sdk/workerEntry.ts +6 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +0 -270
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# __root.tsx Template
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
createRootRoute,
|
|
7
|
+
HeadContent,
|
|
8
|
+
Outlet,
|
|
9
|
+
Scripts,
|
|
10
|
+
useRouterState,
|
|
11
|
+
} from "@tanstack/react-router";
|
|
12
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
13
|
+
import { LiveControls } from "@decocms/start/hooks";
|
|
14
|
+
import { ANALYTICS_SCRIPT } from "@decocms/start/sdk/analytics";
|
|
15
|
+
import appCss from "../styles/app.css?url";
|
|
16
|
+
|
|
17
|
+
// Deco analytics event dispatcher — must be in <head> before any section renders
|
|
18
|
+
const DECO_EVENTS_BOOTSTRAP = `
|
|
19
|
+
window.DECO = window.DECO || {};
|
|
20
|
+
window.DECO.events = window.DECO.events || {
|
|
21
|
+
_q: [],
|
|
22
|
+
dispatch: function(e) {
|
|
23
|
+
if (window.dataLayer) { window.dataLayer.push({ event: e.name, ecommerce: e.params }); }
|
|
24
|
+
this._q.push(e);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
window.dataLayer = window.dataLayer || [];
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
// Navigation progress bar CSS
|
|
31
|
+
const PROGRESS_CSS = `
|
|
32
|
+
@keyframes decoProgress{0%{width:0}30%{width:50%}60%{width:80%}100%{width:98%}}
|
|
33
|
+
.deco-nav-progress{position:fixed;top:0;left:0;height:3px;background:var(--color-primary,#e53e3e);z-index:9999;animation:decoProgress 4s cubic-bezier(.4,0,.2,1) forwards;pointer-events:none;box-shadow:0 0 8px var(--color-primary,#e53e3e)}
|
|
34
|
+
@keyframes decoFadeIn{from{opacity:0}to{opacity:1}}
|
|
35
|
+
.deco-nav-overlay{position:fixed;inset:0;z-index:9998;pointer-events:none;background:rgba(255,255,255,0.15);animation:decoFadeIn .2s ease-out}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
function NavigationProgress() {
|
|
39
|
+
const isLoading = useRouterState({ select: (s) => s.isLoading });
|
|
40
|
+
if (!isLoading) return null;
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<style dangerouslySetInnerHTML={{ __html: PROGRESS_CSS }} />
|
|
44
|
+
<div className="deco-nav-progress" />
|
|
45
|
+
<div className="deco-nav-overlay" />
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const Route = createRootRoute({
|
|
51
|
+
head: () => ({
|
|
52
|
+
meta: [
|
|
53
|
+
{ charSet: "utf-8" },
|
|
54
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
55
|
+
{ title: "My Store" },
|
|
56
|
+
],
|
|
57
|
+
links: [
|
|
58
|
+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
59
|
+
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
|
|
60
|
+
// Add your font stylesheet here:
|
|
61
|
+
// { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=..." },
|
|
62
|
+
{ rel: "stylesheet", href: appCss },
|
|
63
|
+
{ rel: "icon", href: "/favicon.ico" },
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
component: RootLayout,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function RootLayout() {
|
|
70
|
+
const [queryClient] = useState(
|
|
71
|
+
() =>
|
|
72
|
+
new QueryClient({
|
|
73
|
+
defaultOptions: {
|
|
74
|
+
queries: {
|
|
75
|
+
staleTime: import.meta.env.DEV ? 0 : 30_000,
|
|
76
|
+
gcTime: import.meta.env.DEV ? 0 : 5 * 60_000,
|
|
77
|
+
refetchOnWindowFocus: import.meta.env.DEV,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<html lang="pt-BR" data-theme="light" suppressHydrationWarning>
|
|
85
|
+
<head>
|
|
86
|
+
<script dangerouslySetInnerHTML={{ __html: DECO_EVENTS_BOOTSTRAP }} />
|
|
87
|
+
<HeadContent />
|
|
88
|
+
</head>
|
|
89
|
+
<body className="bg-base-100 text-base-content" suppressHydrationWarning>
|
|
90
|
+
<NavigationProgress />
|
|
91
|
+
<QueryClientProvider client={queryClient}>
|
|
92
|
+
<Outlet />
|
|
93
|
+
</QueryClientProvider>
|
|
94
|
+
<LiveControls site={process.env.DECO_SITE_NAME} />
|
|
95
|
+
<script type="module" dangerouslySetInnerHTML={{ __html: ANALYTICS_SCRIPT }} />
|
|
96
|
+
<Scripts />
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Key Points
|
|
104
|
+
|
|
105
|
+
1. **QueryClientProvider** — Required even if not using React Query directly. @decocms/apps hooks may use it.
|
|
106
|
+
2. **LiveControls** — Admin iframe bridge. `site` prop must match CMS site name.
|
|
107
|
+
3. **DECO_EVENTS_BOOTSTRAP** — Must be in `<head>` before sections. Sections dispatch analytics events on render.
|
|
108
|
+
4. **NavigationProgress** — Visual feedback during client-side navigation.
|
|
109
|
+
5. **suppressHydrationWarning** — On `<html>` and `<body>` to avoid mismatches from browser extensions.
|
|
110
|
+
6. **data-theme="light"** — Required for DaisyUI v4/v5 CSS variables to activate.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# router.tsx Template
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
5
|
+
import { routeTree } from "./routeTree.gen";
|
|
6
|
+
|
|
7
|
+
export function createRouter() {
|
|
8
|
+
const router = createTanStackRouter({
|
|
9
|
+
routeTree,
|
|
10
|
+
scrollRestoration: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Scroll to top on forward navigation (PUSH/REPLACE), skip on back/forward (GO)
|
|
14
|
+
router.subscribe("onResolved", (evt) => {
|
|
15
|
+
if (evt.type === "GO") return;
|
|
16
|
+
window.scrollTo({ top: 0 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return router;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "@tanstack/react-router" {
|
|
23
|
+
interface Register {
|
|
24
|
+
router: ReturnType<typeof createRouter>;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Route Files
|
|
30
|
+
|
|
31
|
+
### src/routes/index.tsx (Homepage)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
35
|
+
import { cmsHomeRouteConfig } from "@decocms/start/routes";
|
|
36
|
+
import { DecoPageRenderer } from "@decocms/start/hooks";
|
|
37
|
+
import { loadDeferredSection } from "@decocms/start/routes/cmsRoute";
|
|
38
|
+
|
|
39
|
+
const { loader, head } = cmsHomeRouteConfig({
|
|
40
|
+
defaultTitle: "My Store",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const Route = createFileRoute("/")({
|
|
44
|
+
loader,
|
|
45
|
+
head,
|
|
46
|
+
component: HomePage,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function HomePage() {
|
|
50
|
+
const { resolvedSections, deferredSections, pagePath } = Route.useLoaderData();
|
|
51
|
+
return (
|
|
52
|
+
<DecoPageRenderer
|
|
53
|
+
sections={resolvedSections}
|
|
54
|
+
deferredSections={deferredSections}
|
|
55
|
+
pagePath={pagePath}
|
|
56
|
+
loadDeferredSectionFn={loadDeferredSection}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### src/routes/$.tsx (Catch-All CMS Route)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
66
|
+
import { cmsRouteConfig } from "@decocms/start/routes";
|
|
67
|
+
import { DecoPageRenderer } from "@decocms/start/hooks";
|
|
68
|
+
import { loadDeferredSection } from "@decocms/start/routes/cmsRoute";
|
|
69
|
+
|
|
70
|
+
const { loader, head } = cmsRouteConfig({
|
|
71
|
+
siteName: "My Store",
|
|
72
|
+
ignoreSearchParams: ["skuId"],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const Route = createFileRoute("/$")({
|
|
76
|
+
loader: async (ctx) => {
|
|
77
|
+
const data = await loader(ctx);
|
|
78
|
+
if (!data) throw notFound();
|
|
79
|
+
return data;
|
|
80
|
+
},
|
|
81
|
+
head,
|
|
82
|
+
component: CmsPage,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function CmsPage() {
|
|
86
|
+
const { resolvedSections, deferredSections, pagePath } = Route.useLoaderData();
|
|
87
|
+
return (
|
|
88
|
+
<DecoPageRenderer
|
|
89
|
+
sections={resolvedSections}
|
|
90
|
+
deferredSections={deferredSections}
|
|
91
|
+
pagePath={pagePath}
|
|
92
|
+
loadDeferredSectionFn={loadDeferredSection}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# setup.ts Template
|
|
2
|
+
|
|
3
|
+
Annotated template based on espacosmart-storefront (100+ sections, VTEX, async rendering).
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// ==========================================================================
|
|
7
|
+
// 1. CMS BLOCKS & META
|
|
8
|
+
// ==========================================================================
|
|
9
|
+
|
|
10
|
+
import blocksJson from "./server/cms/blocks.gen.ts";
|
|
11
|
+
import metaData from "./server/admin/meta.gen.json";
|
|
12
|
+
import { setBlocks } from "@decocms/start/cms/loader";
|
|
13
|
+
import { setMetaData, setInvokeLoaders, setRenderShell } from "@decocms/start/admin";
|
|
14
|
+
import { registerSections, registerSectionsSync, setResolvedComponent } from "@decocms/start/cms/registry";
|
|
15
|
+
import { registerSectionLoaders, registerLayoutSections } from "@decocms/start/cms/sectionLoaders";
|
|
16
|
+
import { registerCommerceLoaders, setAsyncRenderingConfig, onBeforeResolve } from "@decocms/start/cms/resolve";
|
|
17
|
+
import { createCachedLoader } from "@decocms/start/sdk/cachedLoader";
|
|
18
|
+
import appCss from "./styles/app.css?url";
|
|
19
|
+
|
|
20
|
+
// Load CMS blocks (pages, sections, configs) from generated JSON
|
|
21
|
+
setBlocks(blocksJson);
|
|
22
|
+
|
|
23
|
+
// Set admin schema for /live/_meta endpoint
|
|
24
|
+
setMetaData(metaData);
|
|
25
|
+
|
|
26
|
+
// Configure admin preview HTML shell
|
|
27
|
+
setRenderShell({
|
|
28
|
+
css: appCss,
|
|
29
|
+
fonts: ["https://fonts.googleapis.com/css2?family=YourFont:wght@400;500;600;700&display=swap"],
|
|
30
|
+
theme: "light", // data-theme="light" on <html> for DaisyUI
|
|
31
|
+
bodyClass: "bg-base-100 text-base-content",
|
|
32
|
+
lang: "pt-BR",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ==========================================================================
|
|
36
|
+
// 2. SECTION REGISTRATION
|
|
37
|
+
// ==========================================================================
|
|
38
|
+
|
|
39
|
+
// Critical sections — above-the-fold, bundled synchronously
|
|
40
|
+
import HeaderSection from "./components/header/Header";
|
|
41
|
+
import FooterSection from "./sections/Footer/Footer";
|
|
42
|
+
|
|
43
|
+
const criticalSections: Record<string, any> = {
|
|
44
|
+
"site/sections/Header/Header.tsx": HeaderSection,
|
|
45
|
+
"site/sections/Footer/Footer.tsx": FooterSection,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Register sync components for instant SSR (no Suspense boundary)
|
|
49
|
+
for (const [key, mod] of Object.entries(criticalSections)) {
|
|
50
|
+
setResolvedComponent(key, mod.default || mod);
|
|
51
|
+
}
|
|
52
|
+
registerSectionsSync(criticalSections);
|
|
53
|
+
|
|
54
|
+
// All sections — lazy-loaded via dynamic import
|
|
55
|
+
registerSections({
|
|
56
|
+
"site/sections/Header/Header.tsx": () => import("./sections/Header/Header"),
|
|
57
|
+
"site/sections/Footer/Footer.tsx": () => import("./sections/Footer/Footer"),
|
|
58
|
+
"site/sections/Theme/Theme.tsx": () => import("./sections/Theme/Theme"),
|
|
59
|
+
// ... register ALL sections from src/sections/ here
|
|
60
|
+
// Pattern: "site/sections/Path/Name.tsx": () => import("./sections/Path/Name")
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ==========================================================================
|
|
64
|
+
// 3. LAYOUT SECTIONS
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
|
|
67
|
+
// Layout sections are always rendered eagerly (never lazy/deferred),
|
|
68
|
+
// even if wrapped in Lazy.tsx in the CMS.
|
|
69
|
+
registerLayoutSections([
|
|
70
|
+
"site/sections/Header/Header.tsx",
|
|
71
|
+
"site/sections/Footer/Footer.tsx",
|
|
72
|
+
"site/sections/Theme/Theme.tsx",
|
|
73
|
+
"site/sections/Social/WhatsApp.tsx",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// ==========================================================================
|
|
77
|
+
// 4. SECTION LOADERS
|
|
78
|
+
// ==========================================================================
|
|
79
|
+
|
|
80
|
+
// Section loaders enrich CMS props with server-side data (e.g., VTEX API calls).
|
|
81
|
+
// Only needed for sections that export `const loader`.
|
|
82
|
+
registerSectionLoaders({
|
|
83
|
+
"site/sections/Product/ProductShelf.tsx": (props: any, req: Request) =>
|
|
84
|
+
import("./components/product/ProductShelf").then((m) => m.loader(props, req)),
|
|
85
|
+
"site/sections/Product/SearchResult.tsx": (props: any, req: Request) =>
|
|
86
|
+
import("./components/search/SearchResult").then((m) => m.loader(props, req)),
|
|
87
|
+
// ... add for each section that has `export const loader`
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ==========================================================================
|
|
91
|
+
// 5. COMMERCE LOADERS (VTEX)
|
|
92
|
+
// ==========================================================================
|
|
93
|
+
|
|
94
|
+
import { vtexProductList } from "@decocms/apps/vtex/loaders/productList";
|
|
95
|
+
import { vtexProductDetailsPage } from "@decocms/apps/vtex/loaders/productDetailsPage";
|
|
96
|
+
import { vtexProductListingPage } from "@decocms/apps/vtex/loaders/productListingPage";
|
|
97
|
+
import { vtexSuggestions } from "@decocms/apps/vtex/loaders/suggestions";
|
|
98
|
+
import { initVtexFromBlocks } from "@decocms/apps/vtex/setup";
|
|
99
|
+
|
|
100
|
+
// SWR-cached commerce loaders — avoids re-fetching on every page navigation
|
|
101
|
+
const cachedProductList = createCachedLoader("vtex/productList", vtexProductList, {
|
|
102
|
+
policy: "stale-while-revalidate", maxAge: 60_000,
|
|
103
|
+
});
|
|
104
|
+
const cachedPDP = createCachedLoader("vtex/pdp", vtexProductDetailsPage, {
|
|
105
|
+
policy: "stale-while-revalidate", maxAge: 30_000,
|
|
106
|
+
});
|
|
107
|
+
const cachedPLP = createCachedLoader("vtex/plp", vtexProductListingPage, {
|
|
108
|
+
policy: "stale-while-revalidate", maxAge: 60_000,
|
|
109
|
+
});
|
|
110
|
+
const cachedSuggestions = createCachedLoader("vtex/suggestions", vtexSuggestions, {
|
|
111
|
+
policy: "stale-while-revalidate", maxAge: 120_000,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Map CMS __resolveType strings to actual loader functions
|
|
115
|
+
registerCommerceLoaders({
|
|
116
|
+
"vtex/loaders/intelligentSearch/productList.ts": cachedProductList,
|
|
117
|
+
"vtex/loaders/intelligentSearch/productListingPage.ts": cachedPLP,
|
|
118
|
+
"vtex/loaders/intelligentSearch/productDetailsPage.ts": cachedPDP,
|
|
119
|
+
"vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions,
|
|
120
|
+
// Add passthrough loaders for types that don't need caching:
|
|
121
|
+
// "vtex/loaders/config.ts": (props) => props,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ==========================================================================
|
|
125
|
+
// 6. VTEX INITIALIZATION
|
|
126
|
+
// ==========================================================================
|
|
127
|
+
|
|
128
|
+
// onBeforeResolve runs once before the first CMS page resolution.
|
|
129
|
+
// initVtexFromBlocks reads VTEX config (account, publicUrl) from CMS blocks.
|
|
130
|
+
onBeforeResolve(() => {
|
|
131
|
+
initVtexFromBlocks();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ==========================================================================
|
|
135
|
+
// 7. ASYNC RENDERING
|
|
136
|
+
// ==========================================================================
|
|
137
|
+
|
|
138
|
+
// Enable deferred section loading (scroll-triggered).
|
|
139
|
+
// Respects CMS Lazy wrappers. Layout sections and alwaysEager are never deferred.
|
|
140
|
+
setAsyncRenderingConfig({
|
|
141
|
+
alwaysEager: [
|
|
142
|
+
"site/sections/Header/Header.tsx",
|
|
143
|
+
"site/sections/Footer/Footer.tsx",
|
|
144
|
+
"site/sections/Theme/Theme.tsx",
|
|
145
|
+
"site/sections/Images/Carousel.tsx",
|
|
146
|
+
// Add above-the-fold sections here
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
// 8. INVOKE LOADERS (for /deco/invoke endpoint)
|
|
152
|
+
// ==========================================================================
|
|
153
|
+
|
|
154
|
+
setInvokeLoaders({
|
|
155
|
+
"vtex/loaders/intelligentSearch/productList.ts": cachedProductList,
|
|
156
|
+
"vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions,
|
|
157
|
+
// Used by the admin to preview loader results
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Key Patterns
|
|
162
|
+
|
|
163
|
+
1. **Order matters**: blocks → meta → sections → loaders → commerce → async config
|
|
164
|
+
2. **Critical sections**: Import synchronously for instant SSR, also register as lazy for client code-splitting
|
|
165
|
+
3. **SWR caching**: `createCachedLoader` wraps commerce loaders with stale-while-revalidate
|
|
166
|
+
4. **onBeforeResolve**: Deferred initialization — VTEX config is read from CMS blocks at first request
|
|
167
|
+
5. **alwaysEager**: Sections that must render on first paint (no deferred loading)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# vite.config.ts Template
|
|
2
|
+
|
|
3
|
+
Battle-tested configuration from espacosmart-storefront.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
7
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
8
|
+
import react from "@vitejs/plugin-react";
|
|
9
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
10
|
+
import { defineConfig } from "vite";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
const srcDir = path.resolve(__dirname, "src");
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [
|
|
17
|
+
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
|
18
|
+
tanstackStart({ server: { entry: "server" } }),
|
|
19
|
+
react({
|
|
20
|
+
babel: {
|
|
21
|
+
plugins: [
|
|
22
|
+
["babel-plugin-react-compiler", { target: "19" }],
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
tailwindcss(),
|
|
27
|
+
// CRITICAL: Stubs for client bundles that transitively import server modules.
|
|
28
|
+
// Without this, client build crashes on node:async_hooks, react-dom/server, etc.
|
|
29
|
+
{
|
|
30
|
+
name: "deco-server-only-stubs",
|
|
31
|
+
enforce: "pre" as const,
|
|
32
|
+
resolveId(id, _importer, options) {
|
|
33
|
+
if (options?.ssr) return undefined;
|
|
34
|
+
const CLIENT_STUBS: Record<string, string> = {
|
|
35
|
+
"react-dom/server": "\0stub:react-dom-server",
|
|
36
|
+
"react-dom/server.browser": "\0stub:react-dom-server",
|
|
37
|
+
"node:stream": "\0stub:node-stream",
|
|
38
|
+
"node:stream/web": "\0stub:node-stream-web",
|
|
39
|
+
"node:async_hooks": "\0stub:node-async-hooks",
|
|
40
|
+
"tanstack-start-injected-head-scripts:v": "\0stub:tanstack-head-scripts",
|
|
41
|
+
};
|
|
42
|
+
return CLIENT_STUBS[id];
|
|
43
|
+
},
|
|
44
|
+
configEnvironment(name: string, env: any) {
|
|
45
|
+
if (name === "ssr" || name === "client") {
|
|
46
|
+
env.optimizeDeps = env.optimizeDeps || {};
|
|
47
|
+
env.optimizeDeps.esbuildOptions = env.optimizeDeps.esbuildOptions || {};
|
|
48
|
+
env.optimizeDeps.esbuildOptions.jsx = "automatic";
|
|
49
|
+
env.optimizeDeps.esbuildOptions.jsxImportSource = "react";
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
load(id) {
|
|
53
|
+
if (id === "\0stub:react-dom-server") {
|
|
54
|
+
return [
|
|
55
|
+
"const noop = () => '';",
|
|
56
|
+
"export const renderToString = noop;",
|
|
57
|
+
"export const renderToStaticMarkup = noop;",
|
|
58
|
+
"export const renderToReadableStream = noop;",
|
|
59
|
+
"export const resume = noop;",
|
|
60
|
+
"export const version = '19.0.0';",
|
|
61
|
+
"export default { renderToString: noop, renderToStaticMarkup: noop, renderToReadableStream: noop, resume: noop, version: '19.0.0' };",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
if (id === "\0stub:node-stream") {
|
|
65
|
+
return "export class PassThrough {}; export class Readable {}; export class Writable {}; export default { PassThrough, Readable, Writable };";
|
|
66
|
+
}
|
|
67
|
+
if (id === "\0stub:node-stream-web") {
|
|
68
|
+
return "export const ReadableStream = globalThis.ReadableStream; export const WritableStream = globalThis.WritableStream; export const TransformStream = globalThis.TransformStream; export default { ReadableStream, WritableStream, TransformStream };";
|
|
69
|
+
}
|
|
70
|
+
if (id === "\0stub:node-async-hooks") {
|
|
71
|
+
return [
|
|
72
|
+
"class _ALS { getStore() { return undefined; } run(_store, fn, ...args) { return fn(...args); } enterWith() {} disable() {} }",
|
|
73
|
+
"export const AsyncLocalStorage = _ALS;",
|
|
74
|
+
"export const AsyncResource = class {};",
|
|
75
|
+
"export function executionAsyncId() { return 0; }",
|
|
76
|
+
"export function createHook() { return { enable() {}, disable() {} }; }",
|
|
77
|
+
"export default { AsyncLocalStorage: _ALS, AsyncResource, executionAsyncId, createHook };",
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
if (id === "\0stub:tanstack-head-scripts") {
|
|
81
|
+
return "export const injectedHeadScripts = undefined;";
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
// Inject site name at build time (not runtime)
|
|
87
|
+
define: {
|
|
88
|
+
"process.env.DECO_SITE_NAME": JSON.stringify(
|
|
89
|
+
process.env.DECO_SITE_NAME || "my-store"
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
esbuild: {
|
|
93
|
+
jsx: "automatic",
|
|
94
|
+
jsxImportSource: "react",
|
|
95
|
+
},
|
|
96
|
+
resolve: {
|
|
97
|
+
// CRITICAL: Without dedupe, multiple React/TanStack instances cause hook errors
|
|
98
|
+
dedupe: [
|
|
99
|
+
"@tanstack/react-start",
|
|
100
|
+
"@tanstack/react-router",
|
|
101
|
+
"@tanstack/react-start-server",
|
|
102
|
+
"@tanstack/start-server-core",
|
|
103
|
+
"@tanstack/start-client-core",
|
|
104
|
+
"@tanstack/start-plugin-core",
|
|
105
|
+
"@tanstack/start-storage-context",
|
|
106
|
+
"react",
|
|
107
|
+
"react-dom",
|
|
108
|
+
],
|
|
109
|
+
alias: {
|
|
110
|
+
"~": srcDir,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Key Points
|
|
117
|
+
|
|
118
|
+
1. **deco-server-only-stubs plugin** — Required. Client bundles transitively import `node:async_hooks`, `react-dom/server`, etc. Without stubs, build crashes.
|
|
119
|
+
2. **resolve.dedupe** — Required. Without it, multiple React instances cause "Invalid hook call" errors.
|
|
120
|
+
3. **process.env.DECO_SITE_NAME via define** — Must be injected at build time, not read at runtime.
|
|
121
|
+
4. **React Compiler** — `babel-plugin-react-compiler` with target 19 for automatic memoization.
|
|
122
|
+
5. **esbuild.jsx** — Must be `"automatic"` with `jsxImportSource: "react"` for proper JSX transform.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Worker Entry Templates
|
|
2
|
+
|
|
3
|
+
## src/server.ts
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// CRITICAL: import "./setup" MUST be the first import.
|
|
7
|
+
// Without it, server functions in Vite split modules have empty state
|
|
8
|
+
// (blocks, registry, commerce loaders) causing 404 on client-side navigation.
|
|
9
|
+
import "./setup";
|
|
10
|
+
import { createStartHandler, defaultStreamHandler } from "@tanstack/react-start/server";
|
|
11
|
+
|
|
12
|
+
export default createStartHandler(defaultStreamHandler);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## src/worker-entry.ts
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// CRITICAL: import "./setup" MUST be the first import.
|
|
19
|
+
import "./setup";
|
|
20
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
21
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
22
|
+
import {
|
|
23
|
+
handleMeta,
|
|
24
|
+
handleDecofileRead,
|
|
25
|
+
handleDecofileReload,
|
|
26
|
+
handleRender,
|
|
27
|
+
corsHeaders,
|
|
28
|
+
} from "@decocms/start/admin";
|
|
29
|
+
// Only if using VTEX:
|
|
30
|
+
import { shouldProxyToVtex, proxyToVtex } from "@decocms/apps/vtex/utils/proxy";
|
|
31
|
+
|
|
32
|
+
const serverEntry = createServerEntry({
|
|
33
|
+
async fetch(request) {
|
|
34
|
+
return await handler.fetch(request);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export default createDecoWorkerEntry(serverEntry, {
|
|
39
|
+
admin: {
|
|
40
|
+
handleMeta,
|
|
41
|
+
handleDecofileRead,
|
|
42
|
+
handleDecofileReload,
|
|
43
|
+
handleRender,
|
|
44
|
+
corsHeaders,
|
|
45
|
+
},
|
|
46
|
+
// VTEX proxy — routes like /api/*, /checkout/*, /arquivos/* go to VTEX
|
|
47
|
+
proxyHandler: (request, url) => {
|
|
48
|
+
if (shouldProxyToVtex(url.pathname)) {
|
|
49
|
+
return proxyToVtex(request);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Key Rules
|
|
57
|
+
|
|
58
|
+
1. **`import "./setup"` is ALWAYS the first line** — both files. This registers sections, loaders, blocks, and commerce config before any server function executes.
|
|
59
|
+
2. **Admin handlers go in `createDecoWorkerEntry`** — NOT inside `createServerEntry`. TanStack Start's build strips custom fetch logic from `createServerEntry` in production.
|
|
60
|
+
3. **Proxy handler** — Optional. Only needed for platforms (VTEX, Shopify) that require server-side proxying.
|
|
61
|
+
4. **Request flow**: `createDecoWorkerEntry` → admin routes (first) → cache check → proxy check → `serverEntry.fetch()` (TanStack Start).
|
|
62
|
+
|
|
63
|
+
## Why `import "./setup"` Must Be First
|
|
64
|
+
|
|
65
|
+
TanStack Start compiles `createServerFn()` calls into "split modules" — separate Vite module instances. Module-level state (blockData, commerceLoaders, sectionRegistry) initialized in `setup.ts` only exists in the original module instance. Without importing setup first, these split modules execute with empty state.
|
|
66
|
+
|
|
67
|
+
The fix in `@decocms/start` uses `globalThis.__deco` to share state across all module instances. But `setup.ts` must run BEFORE any server function is called — which means it must be imported before `createStartHandler` or `createServerEntry`.
|
package/README.md
CHANGED
|
@@ -62,6 +62,51 @@ Request → createDecoWorkerEntry()
|
|
|
62
62
|
| `/cart`, `/checkout` | private | none |
|
|
63
63
|
| Everything else | listing | 2 min |
|
|
64
64
|
|
|
65
|
+
## Migrating from Fresh/Preact/Deno
|
|
66
|
+
|
|
67
|
+
`@decocms/start` includes an Agent Skill that handles migration for you. It works with Claude Code, Cursor, Codex, and other AI coding tools. Install the skill, open your Fresh storefront, and tell the AI to migrate:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx skills add decocms/deco-start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then open your project in any supported tool and say:
|
|
74
|
+
|
|
75
|
+
> migrate this project to TanStack Start
|
|
76
|
+
|
|
77
|
+
The skill handles compatibility checking, import rewrites, config generation, section registry setup, and worker entry creation. It knows what `@decocms/start` supports and will flag anything that needs manual attention.
|
|
78
|
+
|
|
79
|
+
### Or run the script manually
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# From a new TanStack Start project with @decocms/start installed:
|
|
83
|
+
npx tsx node_modules/@decocms/start/scripts/migrate.ts --source /path/to/fresh-site
|
|
84
|
+
|
|
85
|
+
# Preview first without writing:
|
|
86
|
+
npx tsx node_modules/@decocms/start/scripts/migrate.ts --source /path/to/fresh-site --dry-run --verbose
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The script runs 7 phases automatically:
|
|
90
|
+
|
|
91
|
+
1. **Analyze** — scan source, detect Preact/Fresh/Deco patterns
|
|
92
|
+
2. **Scaffold** — generate `vite.config.ts`, `wrangler.jsonc`, routes, `setup.ts`, worker entry
|
|
93
|
+
3. **Transform** — rewrite imports (70+ rules), JSX attrs, Fresh APIs, Deno-isms, Tailwind v3→v4
|
|
94
|
+
4. **Cleanup** — delete `islands/`, old routes, `deno.json`, move `static/` → `public/`
|
|
95
|
+
5. **Report** — generate `MIGRATION_REPORT.md` with manual review items
|
|
96
|
+
6. **Verify** — 18+ smoke tests (zero old imports, scaffolded files exist)
|
|
97
|
+
7. **Bootstrap** — `npm install`, generate CMS blocks, generate routes
|
|
98
|
+
|
|
99
|
+
Your existing `src/sections/`, `src/components/`, and `.deco/blocks/` work as-is. The script gets you to "builds clean with zero old imports" — manual work starts at platform hooks (`useCart`) and runtime tuning.
|
|
100
|
+
|
|
101
|
+
### Agent Skills
|
|
102
|
+
|
|
103
|
+
Skills live in [`.agents/skills/`](.agents/skills/) and provide deep context to AI coding tools:
|
|
104
|
+
|
|
105
|
+
| Skill | What it covers |
|
|
106
|
+
|-------|---------------|
|
|
107
|
+
| `deco-to-tanstack-migration` | Full 12-phase migration playbook with 22 reference docs and 6 templates |
|
|
108
|
+
| `deco-migrate-script` | How the automated `scripts/migrate.ts` works, how to extend it |
|
|
109
|
+
|
|
65
110
|
## Peer Dependencies
|
|
66
111
|
|
|
67
112
|
- `@tanstack/react-start` >= 1.0.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"./sdk/createInvoke": "./src/sdk/createInvoke.ts",
|
|
43
43
|
"./matchers/posthog": "./src/matchers/posthog.ts",
|
|
44
44
|
"./apps/autoconfig": "./src/apps/autoconfig.ts",
|
|
45
|
+
"./sdk/setupApps": "./src/sdk/setupApps.ts",
|
|
45
46
|
"./matchers/builtins": "./src/matchers/builtins.ts",
|
|
46
47
|
"./types/widgets": "./src/types/widgets.ts",
|
|
47
48
|
"./routes": "./src/routes/index.ts",
|
package/src/admin/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { corsHeaders, isAdminOrLocalhost, registerAdminOrigin, registerAdminOrigins } from "./cors";
|
|
2
2
|
export { handleDecofileRead, handleDecofileReload } from "./decofile";
|
|
3
3
|
export {
|
|
4
|
+
clearInvokeHandlers,
|
|
4
5
|
handleInvoke,
|
|
5
6
|
type InvokeAction,
|
|
6
7
|
type InvokeLoader,
|
|
8
|
+
registerInvokeHandlers,
|
|
7
9
|
setInvokeActions,
|
|
8
10
|
setInvokeLoaders,
|
|
9
11
|
} from "./invoke";
|