@decocms/start 0.21.0 → 0.21.1

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.
@@ -15,6 +15,46 @@ Phase-based playbook for converting `deco-sites/*` storefronts from Fresh/Preact
15
15
  | **@decocms/apps** | `@decocms/apps` | VTEX/Shopify loaders, commerce types, commerce sdk (useOffer, formatPrice, analytics) | Passthrough HTML components, Preact/Fresh refs |
16
16
  | **Site repo** | (not published) | All UI: components, hooks, types, routes, styles | No compat/ layer, no aliases beyond `~` |
17
17
 
18
+ ### What Belongs Where
19
+
20
+ ```
21
+ @decocms/start (framework)
22
+ ├── src/cms/ # Block loading, page resolution, section registry
23
+ │ └── loader.ts # loadBlocks, setBlocks, AsyncLocalStorage for per-request overrides
24
+ ├── src/admin/ # Admin protocol: meta, decofile, invoke, render, schema composition
25
+ │ ├── meta.ts # setMetaData() calls composeMeta() at startup; /deco/meta handler
26
+ │ ├── schema.ts # MetaResponse type, composeMeta(), framework block schemas (pages)
27
+ │ ├── render.ts # /live/previews/* for section + page preview (HTML shell)
28
+ │ ├── setup.ts # Client-safe setup (setMetaData, setInvokeLoaders, setRenderShell)
29
+ │ ├── decofile.ts # /.decofile read/reload
30
+ │ ├── invoke.ts # /deco/invoke for loader/action calls
31
+ │ ├── cors.ts # CORS + admin origin validation
32
+ │ └── liveControls.ts # Admin iframe bridge postMessage script
33
+ ├── src/sdk/
34
+ │ ├── workerEntry.ts # createDecoWorkerEntry: outermost Cloudflare Worker wrapper
35
+ │ ├── useScript.ts, signal.ts, clx.ts, cachedLoader.ts, instrumentedFetch.ts
36
+ │ └── ...
37
+ ├── src/hooks/ # DecoPageRenderer (uses registry, NOT hardcoded map), LiveControls
38
+ ├── src/types/ # FnContext, App, AppContext, Section, SectionProps, Resolved
39
+ └── scripts/ # generate-blocks.ts, generate-schema.ts
40
+
41
+ @decocms/apps (commerce)
42
+ ├── commerce/types/ # Product, AnalyticsItem, BreadcrumbList, Filter, etc.
43
+ ├── commerce/utils/ # mapProductToAnalyticsItem, parseRange, formatRange
44
+ ├── commerce/sdk/ # useOffer, useVariantPossibilities, formatPrice, relative, analytics
45
+ ├── vtex/ # Client, loaders (actual VTEX API calls)
46
+ └── shopify/ # Client, loaders (actual Shopify API calls)
47
+
48
+ site repo (UI + business logic)
49
+ ├── src/components/ # All UI components (Image, Picture, Seo, Theme, etc.)
50
+ ├── src/hooks/ # useCart (real VTEX implementation), useUser, useWishlist
51
+ ├── src/types/ # widgets.ts (string aliases), vtex.ts (OrderFormItem, etc.)
52
+ ├── src/sdk/ # Site-specific contexts, usePlatform, useUI, useSuggestions
53
+ ├── src/sections/ # All CMS-renderable sections
54
+ ├── src/routes/ # TanStack Router routes
55
+ └── src/server/ # Server functions (invoke.ts — createServerFn wrappers for @decocms/apps)
56
+ ```
57
+
18
58
  ### Architecture Map
19
59
 
20
60
  | Old Stack | New Stack |
@@ -278,6 +318,24 @@ npm run dev
278
318
  # 8. Admin — /deco/meta returns JSON, /live/previews works
279
319
  ```
280
320
 
321
+ ## Quick Start (Full Migration)
322
+
323
+ 1. **Scaffold**: `npm create @tanstack/app@latest -- --template cloudflare-workers`
324
+ 2. **Copy source**: Move `src/` from deco-site
325
+ 3. **Configure Vite**: See `references/vite-config/`
326
+ 4. **Install deps**: `npm install @decocms/start @decocms/apps @tanstack/store @tanstack/react-store`
327
+ 5. **Link local libs** (if not published): `cd apps-start && npm link && cd ../deco-start && npm link && cd ../my-store && npm link @decocms/apps @decocms/start`
328
+ 6. **Rewrite imports** (parallelizable):
329
+ - Preact -> React: `references/imports/`
330
+ - Signals -> TanStack Store: `references/signals/`
331
+ - Deco framework -> inline: `references/deco-framework/`
332
+ - Commerce/widget types -> @decocms/apps + local: `references/commerce/`
333
+ - SDK utilities -> packages: `~/sdk/useOffer` -> `@decocms/apps/commerce/sdk/useOffer`, `~/sdk/useScript` -> `@decocms/start/sdk/useScript`, etc.
334
+ 7. **Create local UI components**: Image.tsx, Picture.tsx, Seo.tsx, Theme.tsx in `~/components/ui/`
335
+ 8. **Implement platform hooks**: `references/platform-hooks/`
336
+ 9. **Build & verify**: `npm run build`
337
+ 10. **Final audit**: Zero `from "apps/"`, `from "$store/"`, `from "preact"`, `from "@preact"`, `from "@deco/deco"`, `from "~/sdk/useOffer"`, `from "~/sdk/format"` etc. -- all sdk utilities should come from packages
338
+
281
339
  ## Key Principles
282
340
 
283
341
  1. **No compat layer anywhere** -- not in `@decocms/start`, not in `@decocms/apps`, not in the site repo
@@ -492,6 +550,7 @@ The conductor approach that worked (836 errors → 0 across 213 files) treated e
492
550
  | Vite configuration | `references/vite-config/` |
493
551
  | Automation commands | `references/codemod-commands.md` |
494
552
  | Admin schema composition | `src/admin/schema.ts` in `@decocms/start` |
553
+ | Server functions (`createServerFn`) | `references/server-functions/` |
495
554
  | Common gotchas (45 items) | `references/gotchas.md` |
496
555
  | setup.ts template | `templates/setup-ts.md` |
497
556
  | vite.config.ts template | `templates/vite-config.md` |
@@ -126,3 +126,49 @@ grep -r '@deco/deco' src/ --include='*.ts' --include='*.tsx'
126
126
  grep -r '\$fresh/' src/ --include='*.ts' --include='*.tsx'
127
127
  # Both should return ZERO matches
128
128
  ```
129
+
130
+ ## Deferred Sections and CLS Prevention
131
+
132
+ When a CMS page wraps a section in `website/sections/Rendering/Lazy.tsx`, it becomes a `DeferredSection`. The `DeferredSectionWrapper` renders a skeleton via `getSectionOptions(key).loadingFallback`.
133
+
134
+ **Problem**: `getSectionOptions` returns `undefined` if the section module hasn't loaded yet. The module is loaded async in a `useEffect` (`preloadSectionModule`), so on first paint `skeleton = null` — the space is blank and the footer jumps down as sections load (CLS).
135
+
136
+ **Fix**: Use `registerSection` with `loadingFallback` pre-populated in `setup.ts` instead of including the section in the bulk `registerSections` call:
137
+
138
+ ```typescript
139
+ import { registerSection, registerSections } from "@decocms/start/cms";
140
+ import PDPSkeleton from "./components/product/PDPSkeleton";
141
+
142
+ registerSections({
143
+ // ... all other sections (NOT the lazy-wrapped one)
144
+ });
145
+
146
+ // Pre-populate sectionOptions synchronously so DeferredSectionWrapper
147
+ // has loadingFallback on the very first render — no null flash.
148
+ registerSection(
149
+ "site/sections/Product/NotFoundChallenge.tsx",
150
+ () => import("./sections/Product/NotFoundChallenge") as any,
151
+ { loadingFallback: PDPSkeleton },
152
+ );
153
+ ```
154
+
155
+ **Why `as any`**: `registerSection` expects `Promise<SectionModule>` but the actual module export is wider (includes `LoadingFallback`, `loader`, etc.). The cast is safe.
156
+
157
+ **Composite skeleton**: Create a `PDPSkeleton` that combines all `LoadingFallback` exports from the section's sub-components:
158
+
159
+ ```tsx
160
+ // src/components/product/PDPSkeleton.tsx
161
+ import { LoadingFallback as MountedPDPSkeleton } from "~/components/product/MountedPDP";
162
+ import { LoadingFallback as ProductDescriptionSkeleton } from "~/components/product/MountedPDP/ProductDescription";
163
+ import { LoadingFallback as ProductFAQSkeleton } from "~/components/product/MountedPDP/ProductFAQ";
164
+
165
+ export default function PDPSkeleton() {
166
+ return (
167
+ <div>
168
+ <MountedPDPSkeleton />
169
+ <ProductDescriptionSkeleton />
170
+ <ProductFAQSkeleton />
171
+ </div>
172
+ );
173
+ }
174
+ ```
@@ -1,6 +1,6 @@
1
1
  # Migration Gotchas
2
2
 
3
- 45 pitfalls discovered during real migrations (espacosmart-storefront, osklen).
3
+ 46 pitfalls discovered during real migrations (espacosmart-storefront, osklen).
4
4
 
5
5
  ## 1. Section Loaders Don't Execute
6
6
 
@@ -717,3 +717,20 @@ Or in project `.npmrc` with an env var (for CI):
717
717
  ```
718
718
 
719
719
  **Tradeoff with `github:` syntax**: No semver resolution — `npm update` is meaningless. Pin to a tag for stability: `github:decocms/deco-start#v0.14.2`. Without a tag, you get HEAD of the default branch.
720
+
721
+ ## 46. `export type { X } from "..."` Does Not Scope `X` for Parameter Annotations
722
+
723
+ Re-exporting a type makes it available to importers but does NOT bring it into scope in the same file:
724
+
725
+ ```typescript
726
+ // BROKEN — Props is not in scope
727
+ export type { Props } from "~/components/ui/Foo";
728
+ export default function Foo({ bar }: Props) { } // Error: Cannot find name 'Props'
729
+
730
+ // CORRECT — import separately, then re-export
731
+ import type { Props } from "~/components/ui/Foo";
732
+ export type { Props };
733
+ export default function Foo({ bar }: Props) { }
734
+ ```
735
+
736
+ This is a common pattern in section wrapper files (e.g. `src/sections/CountDownApp/CountDownApp.tsx`).
@@ -18,60 +18,27 @@ The storefront domain (e.g., `my-store.deco.site`) differs from the VTEX checkou
18
18
 
19
19
  Use TanStack Start `createServerFn` to create server-side proxy functions that the client hook calls transparently.
20
20
 
21
- ### Server Functions (~/lib/vtex-cart-server.ts)
21
+ > See `references/server-functions/README.md` for the full pattern and all TypeScript pitfalls.
22
22
 
23
- ```typescript
24
- import { createServerFn } from "@tanstack/react-start";
23
+ ### Server Functions (`src/server/invoke.ts`)
25
24
 
26
- const ACCOUNT = "myaccount";
27
- const API_KEY = process.env.VTEX_APP_KEY!;
28
- const API_TOKEN = process.env.VTEX_APP_TOKEN!;
29
-
30
- export const getOrCreateCart = createServerFn({ method: "GET" })
31
- .validator((orderFormId: string) => orderFormId)
32
- .handler(async ({ data: orderFormId }) => {
33
- const url = orderFormId
34
- ? `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${orderFormId}`
35
- : `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm`;
36
- const res = await fetch(url, {
37
- method: "POST",
38
- headers: {
39
- "Content-Type": "application/json",
40
- "X-VTEX-API-AppKey": API_KEY,
41
- "X-VTEX-API-AppToken": API_TOKEN,
42
- },
43
- body: JSON.stringify({ expectedOrderFormSections: ["items", "totalizers", "shippingData", "clientPreferencesData", "storePreferencesData", "marketingData"] }),
44
- });
45
- return res.json();
46
- });
25
+ Wrap `@decocms/apps` VTEX actions in `createServerFn`. Always use `.inputValidator()` (not `.validator()`) and return `Promise<any>` to bypass `ValidateSerializable`:
47
26
 
48
- export const addItemsToCart = createServerFn({ method: "POST" })
49
- .validator((data: { orderFormId: string; items: any[] }) => data)
50
- .handler(async ({ data }) => {
51
- const res = await fetch(
52
- `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${data.orderFormId}/items`,
53
- {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
56
- body: JSON.stringify({ orderItems: data.items }),
57
- },
58
- );
59
- return res.json();
27
+ ```typescript
28
+ import { createServerFn } from "@tanstack/react-start";
29
+ import { getOrCreateCart } from "@decocms/apps/vtex/actions/checkout";
30
+
31
+ // Preferred: wrap @decocms/apps actions (handles auth + API details internally)
32
+ const _getOrCreateCart = createServerFn({ method: "POST" })
33
+ .inputValidator((data: { orderFormId?: string }) => data)
34
+ .handler(async ({ data }): Promise<any> => {
35
+ const result = await getOrCreateCart(data.orderFormId);
36
+ return (result as any).data;
60
37
  });
61
38
 
62
- export const updateCartItems = createServerFn({ method: "POST" })
63
- .validator((data: { orderFormId: string; items: any[] }) => data)
64
- .handler(async ({ data }) => {
65
- const res = await fetch(
66
- `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${data.orderFormId}/items/update`,
67
- {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
70
- body: JSON.stringify({ orderItems: data.items }),
71
- },
72
- );
73
- return res.json();
74
- });
39
+ export const invoke = {
40
+ vtex: { actions: { getOrCreateCart: _getOrCreateCart } },
41
+ } as const;
75
42
  ```
76
43
 
77
44
  ### Hook (~/hooks/useCart.ts)
@@ -0,0 +1,120 @@
1
+ # Server Functions (`createServerFn`)
2
+
3
+ All VTEX API calls that need credentials (cart, MasterData, session, newsletter, shipping simulation) must run on the Worker — not on the client — to avoid CORS errors and keep secrets server-side.
4
+
5
+ ## Pattern: `src/server/invoke.ts`
6
+
7
+ Create a single file that wraps `@decocms/apps` invoke functions in `createServerFn`. This keeps VTEX credentials inside the Worker and gives type-safe RPC from the browser.
8
+
9
+ ```typescript
10
+ import { createServerFn } from "@tanstack/react-start";
11
+ import {
12
+ getOrCreateCart,
13
+ addItemsToCart,
14
+ updateCartItems,
15
+ addCouponToCart,
16
+ updateOrderFormAttachment,
17
+ simulateCart,
18
+ } from "@decocms/apps/vtex/actions/checkout";
19
+
20
+ // CRITICAL: always chain .inputValidator() before .handler()
21
+ // Without it, ctx.data is typed as `undefined` and every access fails.
22
+ const _getOrCreateCart = createServerFn({ method: "POST" })
23
+ .inputValidator((data: { orderFormId?: string }) => data)
24
+ .handler(async ({ data }): Promise<any> => {
25
+ // Promise<any> bypasses ValidateSerializable on OrderForm.storeId: unknown
26
+ const result = await getOrCreateCart(data.orderFormId);
27
+ return (result as any).data;
28
+ });
29
+
30
+ const _addItemsToCart = createServerFn({ method: "POST" })
31
+ .inputValidator((data: {
32
+ orderFormId: string;
33
+ orderItems: Array<{ id: string; seller: string; quantity: number }>;
34
+ }) => data)
35
+ .handler(async ({ data }): Promise<any> => {
36
+ const result = await addItemsToCart(data.orderFormId, data.orderItems);
37
+ return (result as any).data;
38
+ });
39
+
40
+ // ... repeat for updateCartItems, addCouponToCart, simulateCart, etc.
41
+
42
+ export const invoke = {
43
+ vtex: {
44
+ actions: {
45
+ getOrCreateCart: _getOrCreateCart,
46
+ addItemsToCart: _addItemsToCart,
47
+ // ...
48
+ },
49
+ },
50
+ } as const;
51
+ ```
52
+
53
+ ## Required: `.inputValidator()`
54
+
55
+ Without `.inputValidator()`, TanStack Start types `ctx.data` as `undefined`:
56
+
57
+ ```typescript
58
+ // BROKEN — ctx.data is typed as undefined
59
+ createServerFn({ method: "POST" })
60
+ .handler(async (ctx) => {
61
+ ctx.data.orderFormId; // TS Error: ctx.data is undefined
62
+ });
63
+
64
+ // CORRECT
65
+ createServerFn({ method: "POST" })
66
+ .inputValidator((data: { orderFormId: string }) => data)
67
+ .handler(async ({ data }) => {
68
+ data.orderFormId; // typed correctly
69
+ });
70
+ ```
71
+
72
+ ## Required: `Promise<any>` Return Type
73
+
74
+ TanStack Start validates that return types are serializable via `ValidateSerializable`. Types with `unknown` fields (e.g. `OrderForm.storeId: unknown`) fail this check at compile time:
75
+
76
+ ```
77
+ Type 'OrderForm' does not satisfy the constraint 'Serializable'.
78
+ Types of property 'storeId' are incompatible.
79
+ Type 'unknown' is not assignable to type 'Serializable'
80
+ ```
81
+
82
+ **Fix**: Annotate the handler return as `Promise<any>`:
83
+
84
+ ```typescript
85
+ .handler(async ({ data }): Promise<any> => {
86
+ return await doSomething(data);
87
+ });
88
+ ```
89
+
90
+ ## Stripping Non-Serializable Properties
91
+
92
+ Loader results that include function properties must have those functions removed before being returned — Seroval (TanStack's hydration serializer) cannot serialize functions.
93
+
94
+ ```typescript
95
+ .handler(async ({ data }): Promise<any> => {
96
+ const result = await productReviewsLoader(data);
97
+ // Strip functions before serialization
98
+ const { getProductReview: _r, reviewLikeAction: _l, ...serializable } = result as any;
99
+ return serializable;
100
+ });
101
+ ```
102
+
103
+ ## Invoke from `useCart`
104
+
105
+ ```typescript
106
+ // ~/hooks/useCart.ts
107
+ import { invoke } from "~/server/invoke";
108
+
109
+ const updated = await invoke.vtex.actions.addItemsToCart({
110
+ data: { orderFormId, orderItems },
111
+ });
112
+ ```
113
+
114
+ ## Verification
115
+
116
+ ```bash
117
+ # No direct VTEX API calls from client components
118
+ rg 'vtexcommercestable\.com\.br' src/ --include='*.tsx'
119
+ # Should return ZERO (all calls go through invoke)
120
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
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",
@@ -160,7 +160,24 @@ export type SyncSectionEntry =
160
160
  */
161
161
  export function registerSectionsSync(sections: Record<string, SyncSectionEntry>): void {
162
162
  for (const [key, entry] of Object.entries(sections)) {
163
- const component = typeof entry === "function" ? entry : entry.default;
163
+ const raw = typeof entry === "function" ? entry : (entry as any).default;
164
+ // Accept functions and React wrapper objects (React.memo, forwardRef, lazy)
165
+ const REACT_WRAPPERS = [
166
+ Symbol.for("react.memo"),
167
+ Symbol.for("react.forward_ref"),
168
+ Symbol.for("react.lazy"),
169
+ ];
170
+ const component =
171
+ typeof raw === "function" ||
172
+ (raw != null &&
173
+ typeof raw === "object" &&
174
+ REACT_WRAPPERS.includes((raw as any).$$typeof))
175
+ ? raw
176
+ : undefined;
177
+ if (!component) {
178
+ console.warn(`[registerSectionsSync] "${key}" has no callable default export — skipping`);
179
+ continue;
180
+ }
164
181
  syncComponents[key] = component;
165
182
  resolvedComponents[key] = component;
166
183