@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.
Files changed (41) hide show
  1. package/.agents/skills/deco-migrate-script/SKILL.md +434 -0
  2. package/.agents/skills/deco-to-tanstack-migration/SKILL.md +382 -0
  3. package/.agents/skills/deco-to-tanstack-migration/references/admin-cms.md +154 -0
  4. package/{.cursor/skills/deco-async-rendering-site-guide/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/async-rendering.md} +296 -31
  5. package/.agents/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  6. package/.agents/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  7. package/.agents/skills/deco-to-tanstack-migration/references/css-styling.md +156 -0
  8. package/.agents/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  9. package/.agents/skills/deco-to-tanstack-migration/references/gotchas.md +13 -0
  10. package/{.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/hydration-fixes.md} +139 -4
  11. package/.agents/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  12. package/{.cursor/skills/deco-islands-migration/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/islands.md} +0 -14
  13. package/.agents/skills/deco-to-tanstack-migration/references/jsx-migration.md +80 -0
  14. package/.agents/skills/deco-to-tanstack-migration/references/matchers.md +1064 -0
  15. package/{.cursor/skills/deco-tanstack-navigation/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/navigation.md} +1 -16
  16. package/.agents/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  17. package/.agents/skills/deco-to-tanstack-migration/references/react-hooks-patterns.md +142 -0
  18. package/.agents/skills/deco-to-tanstack-migration/references/react-signals-state.md +72 -0
  19. package/{.cursor/skills/deco-tanstack-search/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/search.md} +1 -13
  20. package/.agents/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  21. package/{.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md → .agents/skills/deco-to-tanstack-migration/references/storefront-patterns.md} +1 -137
  22. package/.agents/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  23. package/.agents/skills/deco-to-tanstack-migration/references/vtex-commerce.md +165 -0
  24. package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +209 -0
  25. package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  26. package/.agents/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  27. package/.agents/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  28. package/.agents/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  29. package/.agents/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  30. package/.agents/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  31. package/README.md +45 -0
  32. package/package.json +2 -1
  33. package/src/admin/index.ts +2 -0
  34. package/src/admin/invoke.ts +53 -5
  35. package/src/admin/setup.ts +7 -1
  36. package/src/apps/autoconfig.ts +50 -72
  37. package/src/sdk/invoke.ts +123 -12
  38. package/src/sdk/requestContext.ts +42 -0
  39. package/src/sdk/setupApps.ts +211 -0
  40. package/src/sdk/workerEntry.ts +6 -0
  41. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +0 -270
@@ -1,13 +1,9 @@
1
- ---
2
- name: deco-tanstack-storefront-patterns
3
- description: Runtime patterns, fixes, and learnings for Deco storefronts running on TanStack Start/React/Cloudflare Workers. Covers nested sections, dev cache control, workerEntry guards, section loaders, Fresh-to-React JSX porting, VTEX API resilience, node:async_hooks client bundle leak, SliderJS DOM timing, cart button loading state, server functions for VTEX actions, DOM manipulation conflicts with React, TypeScript patterns, loader cache/cacheKey module exports via createCachedLoaderFromModule, and the full multi-layer VTEX cache architecture. Also covers: setup.ts import order for server functions (client-nav 404 fix), globalThis backing for module-level state (split-module RPC problem), async rendering/deferred sections with IntersectionObserver, loadDeferredSection POST instead of GET (431 fix), section ordering with index stamping (mergeSections sort-based), layout section caching (5min TTL for Header/Footer), analytics hydration mismatch fix (useScript + suppressHydrationWarning), and vite.config.ts requirements for published @decocms/start packages (esbuildOptions jsx automatic + virtual module fallback + resolve.dedupe). Use when debugging or enhancing a deco-start storefront after the initial migration.
4
- ---
5
1
 
6
2
  # Deco TanStack Storefront Patterns
7
3
 
8
4
  Patterns and fixes discovered while porting and running `espacosmart-storefront` on the `@decocms/start` + TanStack Start stack. These apply to **any** Deco site after the initial migration.
9
5
 
10
- ## When to Use This Skill
6
+ ## When to Use This Reference
11
7
 
12
8
  - Debugging runtime errors in a deco-start storefront
13
9
  - Porting sections that use nested sections (`{ Component, props }`)
@@ -994,135 +990,3 @@ export default defineConfig({
994
990
  - **Fix #1 (`esbuildOptions.jsx: "automatic"`)**: `@decocms/start` ships raw `.tsx` source. Vite's esbuild pre-bundler uses the classic transform by default. `jsx: "automatic"` makes it emit `import { jsx } from "react/jsx-runtime"` instead of `React.createElement`.
995
991
  - **Fix #2 (virtual module fallback)**: The import chain `cmsRoute.ts → @tanstack/react-start → @tanstack/start-server-core → router-manifest.js` reaches the client bundle. The `tanstack-start-injected-head-scripts:v` virtual is registered with `applyToEnvironment: server` only.
996
992
  - **Fix #3 (dedupe)**: Prevents duplicate TanStack instances when hoisting from peer deps.
997
-
998
- ---
999
-
1000
- ## 25. SEO Architecture — Three Layers
1001
-
1002
- ### Problem
1003
-
1004
- Sites migrated from Fresh/Deno often have broken SEO:
1005
- - `Seo.tsx` component returns `null` (dead stub from migration)
1006
- - Route `head()` functions only set `<title>`, missing description/canonical/OG
1007
- - No JSON-LD structured data on PDPs
1008
- - No Open Graph tags for social sharing
1009
-
1010
- ### Solution — Framework + Section + Component
1011
-
1012
- **Layer 1: Framework head()** (`cmsRouteConfig` / `cmsHomeRouteConfig`)
1013
- Automatically emits title, description, canonical, OG, Twitter Card, robots from `DecoPageResult.seo`. Requires `defaultDescription` option and `registerSeoSections()` in setup.
1014
-
1015
- **Layer 2: Section SEO** (`registerSeoSections` + `registerSectionLoaders`)
1016
- Sections like SEOPDP register as SEO contributors. Their section loader computes title/description/canonical from commerce data (product name, product description, breadcrumb canonical). The framework extracts these into `PageSeo`.
1017
-
1018
- **Layer 3: Structured data** (section component renders JSON-LD)
1019
- The `Seo.tsx` component renders `<script type="application/ld+json">` for Product, WebSite, BreadcrumbList schemas. Meta tags are NOT rendered here — the framework handles those via `head()`.
1020
-
1021
- ### Setup Checklist
1022
-
1023
- ```typescript
1024
- // setup.ts
1025
- import { registerSeoSections } from "@decocms/start/cms";
1026
-
1027
- registerSeoSections(["site/sections/SEOPDP.tsx"]);
1028
-
1029
- // Also register the SEOPDP section loader:
1030
- registerSectionLoaders({
1031
- "site/sections/SEOPDP.tsx": async (props, req) => {
1032
- const mod = await import("./sections/SEOPDP");
1033
- return mod.loader(props, req, { seo: {} } as any) ?? props;
1034
- },
1035
- });
1036
- ```
1037
-
1038
- ```typescript
1039
- // routes/$.tsx — spread full config, framework handles SEO
1040
- const routeConfig = cmsRouteConfig({
1041
- siteName: "My Store",
1042
- defaultTitle: "My Store - Default Title",
1043
- defaultDescription: "My Store — best products...",
1044
- ignoreSearchParams: ["skuId"],
1045
- });
1046
- export const Route = createFileRoute("/$")({ ...routeConfig, component: CmsPage });
1047
- ```
1048
-
1049
- ```typescript
1050
- // __root.tsx — fallback description and OG globals
1051
- head: () => ({
1052
- meta: [
1053
- { charSet: "utf-8" },
1054
- { name: "viewport", content: "width=device-width, initial-scale=1" },
1055
- { title: "My Store - Default Title" },
1056
- { name: "description", content: "My Store — default description." },
1057
- { property: "og:site_name", content: "My Store" },
1058
- { property: "og:locale", content: "pt_BR" },
1059
- ],
1060
- }),
1061
- ```
1062
-
1063
- ### Anti-Patterns
1064
-
1065
- - `Seo.tsx` returning `null` — must render JSON-LD at minimum
1066
- - Custom `head()` in routes that only sets title — use `cmsRouteConfig` which handles full SEO
1067
- - Hardcoded Device.Provider — use matchMedia for client, section loaders for server
1068
-
1069
- ---
1070
-
1071
- ## 26. Device Detection — No Hardcoded Provider
1072
-
1073
- ### Problem
1074
-
1075
- Sites often have `<Device.Provider value={{ isMobile: false }}>` in `__root.tsx`, making ALL client-side components think they're on desktop regardless of actual viewport.
1076
-
1077
- ### Solution — Two-Layer Detection
1078
-
1079
- **Server-side**: Section loaders via `registerSectionLoaders()` detect device from UA and inject `isMobile` / `device` as props. The page result also includes `device` for client use.
1080
-
1081
- **Client-side**: Use `useSyncExternalStore` + `window.matchMedia` instead of React context:
1082
-
1083
- ```typescript
1084
- // contexts/device.tsx
1085
- import { useSyncExternalStore } from "react";
1086
-
1087
- const MOBILE_QUERY = "(max-width: 767px)";
1088
-
1089
- function subscribe(cb: () => void) {
1090
- if (typeof window === "undefined") return () => {};
1091
- const mql = window.matchMedia(MOBILE_QUERY);
1092
- mql.addEventListener("change", cb);
1093
- return () => mql.removeEventListener("change", cb);
1094
- }
1095
-
1096
- function getSnapshot() { return window.matchMedia(MOBILE_QUERY).matches; }
1097
- function getServerSnapshot() { return false; }
1098
-
1099
- export const useDevice = () => {
1100
- const isMobile = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1101
- return { isMobile };
1102
- };
1103
- ```
1104
-
1105
- ### Why matchMedia > UA context
1106
-
1107
- - Reflects actual viewport, not a server guess
1108
- - Reactive — updates if user resizes browser
1109
- - No Provider needed — works anywhere in the component tree
1110
- - SSR defaults to desktop (false), hydrates to real value
1111
-
1112
- ---
1113
-
1114
- ## Related Skills
1115
-
1116
- | Skill | Purpose |
1117
- |-------|---------|
1118
- | `deco-to-tanstack-migration` | Initial migration playbook (imports, signals, architecture) |
1119
- | `deco-cms-route-config` | Route config, SEO architecture, device detection |
1120
- | `deco-tanstack-navigation` | SPA navigation patterns (`<Link>`, `useNavigate`, `loaderDeps`) |
1121
- | `deco-islands-migration` | Eliminating the `src/islands/` directory |
1122
- | `deco-edge-caching` | Edge caching with `createDecoWorkerEntry` |
1123
- | `deco-vtex-fetch-cache` | SWR fetch cache for VTEX APIs (`fetchWithCache`, `vtexCachedFetch`) |
1124
- | `deco-api-call-dedup` | API call dedup, batching, PLP filtering patterns |
1125
- | `deco-cms-layout-caching` | Layout section caching in CMS resolve |
1126
- | `deco-typescript-fixes` | TypeScript error patterns and fixes |
1127
- | `deco-storefront-test-checklist` | Context-aware QA checklist generation |
1128
- | `deco-startup-analysis` | Analyzing startup logs for issues |
@@ -0,0 +1,78 @@
1
+ # Vite Configuration
2
+
3
+ ## Final Config (Post-Migration)
4
+
5
+ After all imports are rewritten, the config should be minimal:
6
+
7
+ ```typescript
8
+ import { cloudflare } from "@cloudflare/vite-plugin";
9
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
10
+ import react from "@vitejs/plugin-react";
11
+ import tailwindcss from "@tailwindcss/vite";
12
+ import { defineConfig } from "vite";
13
+ import path from "path";
14
+
15
+ const srcDir = path.resolve(__dirname, "src");
16
+
17
+ export default defineConfig({
18
+ plugins: [
19
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
20
+ tanstackStart({ server: { entry: "server" } }),
21
+ react({
22
+ babel: {
23
+ plugins: [["babel-plugin-react-compiler", { target: "19" }]],
24
+ },
25
+ }),
26
+ tailwindcss(),
27
+ ],
28
+ resolve: {
29
+ alias: {
30
+ "~": srcDir,
31
+ },
32
+ },
33
+ });
34
+ ```
35
+
36
+ **One alias only**: `~` -> `src/`. Nothing else.
37
+
38
+ ## tsconfig.json
39
+
40
+ Must mirror the Vite alias:
41
+
42
+ ```json
43
+ {
44
+ "compilerOptions": {
45
+ "jsx": "react-jsx",
46
+ "moduleResolution": "bundler",
47
+ "module": "ESNext",
48
+ "target": "ES2022",
49
+ "skipLibCheck": true,
50
+ "strictNullChecks": true,
51
+ "baseUrl": ".",
52
+ "paths": {
53
+ "~/*": ["./src/*"]
54
+ }
55
+ },
56
+ "include": ["src/**/*", "vite.config.ts"]
57
+ }
58
+ ```
59
+
60
+ No `$store/*`, `site/*`, `apps/*`, `preact`, `@preact/signals`, `@deco/deco` paths. Those are all dead.
61
+
62
+ ## React Compiler
63
+
64
+ The `babel-plugin-react-compiler` with `target: "19"` enables automatic memoization. Requires `@vitejs/plugin-react` instead of the default SWC plugin.
65
+
66
+ Install: `npm install -D @vitejs/plugin-react babel-plugin-react-compiler`
67
+
68
+ ## Environment Variables
69
+
70
+ For VTEX API keys, use Cloudflare Workers secrets or `.dev.vars`:
71
+
72
+ ```
73
+ VTEX_ACCOUNT=mystore
74
+ VTEX_APP_KEY=...
75
+ VTEX_APP_TOKEN=...
76
+ ```
77
+
78
+ Accessed via `process.env.*` in `createServerFn` handlers.
@@ -0,0 +1,165 @@
1
+ # VTEX Commerce Gotchas
2
+
3
+ > Section loaders, cart CORS, price specs, facets, URL-blind loaders, cookie handling.
4
+
5
+
6
+ ## 1. Section Loaders Don't Execute
7
+
8
+ Deco sections have `export const loader = async (props, req, ctx) => { ... }` that runs server-side before the component renders. In TanStack Start, these don't execute automatically. Components typed as `SectionProps<typeof loader>` expect the augmented props, but only receive the raw CMS block props.
9
+
10
+ **Symptom**: Components crash on `.find()`, `.length`, or property access of loader-provided props that are `undefined`.
11
+
12
+ **Fix**: Register them via `registerSectionLoaders()` in `setup.ts`.
13
+
14
+ **Safe-default pattern** (most pragmatic for initial migration):
15
+
16
+ ```typescript
17
+ // Before: component expects loader-augmented props
18
+ function ProductMain({ page, productAdditional, showTogether, priceSimulation, isMobile }: SectionProps<typeof loader>) {
19
+
20
+ // After: destructure with safe defaults for all loader-only props
21
+ function ProductMain(rawProps: any) {
22
+ const {
23
+ page,
24
+ productAdditional = [], // from section loader
25
+ showTogether = [], // from section loader
26
+ showTogetherSimulation = [], // from section loader
27
+ priceSimulation = 0, // from section loader
28
+ noInterestInstallmentValue = null,
29
+ skuProductsKit = [], // from section loader
30
+ isMobile = false, // from section loader (device detection)
31
+ } = rawProps;
32
+ ```
33
+
34
+ This lets the core component render while gracefully degrading features that depend on loader data (cross-selling, price simulation, etc.).
35
+
36
+
37
+ ## 7. VTEX API Auth on Cloudflare Workers
38
+
39
+ Env vars must be set via `wrangler secret put` or `.dev.vars`, not `.env`.
40
+
41
+
42
+ ## 8. Cookie Handling
43
+
44
+ In TanStack Start, manage `checkout.vtex.com__orderFormId` cookies manually via `document.cookie`.
45
+
46
+
47
+ ## 32. Section Loader Logic Must Not Be Stripped
48
+
49
+ **Severity**: HIGH — sections render empty/broken
50
+
51
+ During migration, section loaders (e.g., `sections/Header/Header.tsx`) may have their async data-fetching logic removed. For example, the `ctx.invoke.vtex.loaders.categories.tree()` call that populates navigation menus. Without it, the header renders with no category links.
52
+
53
+ **Fix**: Keep all section loader logic intact. The loader signature `(props, req, ctx) => {...}` and the `ctx.invoke` calls should be preserved as-is.
54
+
55
+
56
+ ## 34. Commerce Loaders Are Blind to the URL
57
+
58
+ **Severity**: CRITICAL — search and category pages return wrong/no products
59
+
60
+ When `resolve.ts` processes CMS blocks, it passes only the static CMS block props to commerce loaders (PLP, PDP). The current URL, query string (`?q=`), path (`/drywall`), sort, pagination, and filter parameters are never forwarded.
61
+
62
+ **Symptom**: Search pages (`/s?q=parafuso`) return zero products. Category pages (`/drywall`) show random/no products. Sort and pagination controls do nothing.
63
+
64
+ **Root cause**: `resolveValue()` in `resolve.ts` calls commerce loaders with `resolvedProps` (CMS block config only). The `matcherCtx` (containing URL, path, user-agent) is used for matcher evaluation but never passed to commerce loaders.
65
+
66
+ **Fix**: Pass `matcherCtx` as a second argument to commerce loaders in `resolve.ts`. Then the PLP loader can extract `?q=` for search, path for categories, `?sort=` for sorting, `?page=` for pagination, and `?filter.X=Y` for facets.
67
+
68
+ This is a change in `@decocms/start` (resolve.ts). Until upstreamed, use patch-package or vendor the file.
69
+
70
+
71
+ ## 35. VTEX Product Loaders Ship with Empty priceSpecification
72
+
73
+ **Severity**: HIGH — no discount badges, no strikethrough prices, no installments
74
+
75
+ All three VTEX product loaders (`vtexProductList`, `productListingPage`, `productDetailsPage`) build offers with `priceSpecification: []`. The `useOffer()` hook depends on this array to extract `ListPrice` (for discount math + strikethrough), `SalePrice`, and `Installment` entries.
76
+
77
+ **Symptom**: Product cards show only one price (no strikethrough). No "X% OFF" discount badge. No "Ou em Nx de R$ X sem juros" installment text.
78
+
79
+ **Fix**: Add a `buildPriceSpecification()` helper to each loader that transforms the VTEX `commertialOffer` data:
80
+
81
+ ```typescript
82
+ function buildPriceSpecification(offer: any): any[] {
83
+ const specs: any[] = [];
84
+ if (offer.ListPrice != null) {
85
+ specs.push({ "@type": "UnitPriceSpecification", priceType: "https://schema.org/ListPrice", price: offer.ListPrice });
86
+ }
87
+ if (offer.Price != null) {
88
+ specs.push({ "@type": "UnitPriceSpecification", priceType: "https://schema.org/SalePrice", price: offer.Price });
89
+ }
90
+ // Find best no-interest installment
91
+ const noInterest = (offer.Installments ?? [])
92
+ .filter((i: any) => i.InterestRate === 0)
93
+ .sort((a: any, b: any) => b.NumberOfInstallments - a.NumberOfInstallments);
94
+ if (noInterest.length > 0) {
95
+ const best = noInterest[0];
96
+ specs.push({
97
+ "@type": "UnitPriceSpecification",
98
+ priceType: "https://schema.org/SalePrice",
99
+ priceComponentType: "https://schema.org/Installment",
100
+ billingDuration: best.NumberOfInstallments,
101
+ billingIncrement: best.Value,
102
+ price: best.TotalValuePlusInterestRate,
103
+ });
104
+ }
105
+ return specs;
106
+ }
107
+ ```
108
+
109
+ This is a change in `@decocms/apps`. Until upstreamed, patch or vendor the loader files.
110
+
111
+
112
+ ## 36. VTEX Facets API Response Structure Mismatch
113
+
114
+ The VTEX Intelligent Search facets endpoint returns `{ facets: ISFacetGroup[] }`, NOT a direct `ISFacetGroup[]` array. Accessing `response` directly as an array yields no filter data.
115
+
116
+ Additionally, `PRICERANGE` facets must be converted to `FilterToggle` format (with `value: "min:max"` strings) for the existing `Filters.tsx` component to render them. The component's `isToggle()` filter drops anything that isn't `FilterToggle`.
117
+
118
+ **Fix**: Unwrap with `const facetGroups = response.facets ?? [];` and convert price ranges:
119
+
120
+ ```typescript
121
+ if (group.type === "PRICERANGE") {
122
+ return { "@type": "FilterToggle" as const, key: "price", label: group.name, quantity: 0,
123
+ values: group.values.map((v) => ({
124
+ value: `${v.range.from}:${v.range.to}`, label: `R$ ${v.range.from} - R$ ${v.range.to}`,
125
+ quantity: v.quantity, selected: false, url: `?filter.price=${v.range.from}:${v.range.to}`,
126
+ })),
127
+ };
128
+ }
129
+ ```
130
+
131
+
132
+ ## 39. Cart Requires Server-Side Proxy for VTEX API (CORS)
133
+
134
+ **Severity**: HIGH — add-to-cart, minicart, and checkout flow completely broken
135
+
136
+ The storefront domain (e.g., `espacosmart-tanstack.deco.site`) differs from the VTEX checkout domain (`lojaespacosmart.vtexcommercestable.com.br`). Direct browser `fetch()` calls to VTEX are blocked by CORS. Additionally, the `checkout.vtex.com__orderFormId` cookie is scoped to the VTEX domain and inaccessible from the storefront.
137
+
138
+ **Fix**: Use TanStack Start `createServerFn` to create server-side proxy functions:
139
+
140
+ ```typescript
141
+ // src/lib/vtex-cart-server.ts
142
+ import { createServerFn } from "@tanstack/react-start";
143
+
144
+ export const getOrCreateCart = createServerFn({ method: "GET" })
145
+ .validator((orderFormId: string) => orderFormId)
146
+ .handler(async ({ data: orderFormId }) => {
147
+ const url = orderFormId
148
+ ? `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${orderFormId}`
149
+ : `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm`;
150
+ const res = await fetch(url, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
153
+ body: JSON.stringify({ expectedOrderFormSections: ["items", "totalizers", "shippingData", "clientPreferencesData", "storePreferencesData", "marketingData"] }),
154
+ });
155
+ return res.json();
156
+ });
157
+ ```
158
+
159
+ The `useCart` hook manages the `orderFormId` in a client-side cookie and calls these server functions.
160
+
161
+ **Checkout URL**: The minicart's "Finalizar Compra" link must append the `orderFormId` as a query parameter since the VTEX checkout domain can't read the storefront's cookies:
162
+
163
+ ```typescript
164
+ const checkoutUrl = `https://secure.${STORE_DOMAIN}/checkout/?orderFormId=${orderFormId}`;
165
+ ```
@@ -0,0 +1,209 @@
1
+ # Worker / Cloudflare / Build Gotchas
2
+
3
+ > TanStack worker entry stripping, setup ordering, AsyncLocalStorage, cache, npm.
4
+
5
+
6
+ ## 9. Build Succeeds but Runtime Fails
7
+
8
+ After import rewrites, always test: build → dev → visit pages → test interactive features.
9
+
10
+
11
+ ## 10. npm link for Local Dev
12
+
13
+ ```bash
14
+ cd apps-start && npm link
15
+ cd ../deco-start && npm link
16
+ cd ../my-store && npm link @decocms/apps @decocms/start
17
+ ```
18
+
19
+
20
+ ## 12. No Compat Layers
21
+
22
+ After migration: no `src/compat/`, only `~/*` alias, zero compat files in packages.
23
+
24
+
25
+ ## 13. AsyncLocalStorage in Client Bundles
26
+
27
+ Use namespace import + runtime conditional (or the `deco-server-only-stubs` Vite plugin).
28
+
29
+
30
+ ## 14. TanStack Start Ignores Custom Worker Entry Code
31
+
32
+ **Severity**: CRITICAL -- cache logic, admin routes, and any custom request interception will silently not work in production.
33
+
34
+ TanStack Start's Cloudflare adapter **completely ignores** the `export default` in `server.ts`. It generates its own Worker entry that calls `createStartHandler(defaultStreamHandler)` directly. Custom logic inside `createServerEntry({ async fetch(request) { ... } })` is also stripped by Vite/Rollup in production builds.
35
+
36
+ **Symptom**: Admin routes like `/live/_meta` return HTML instead of JSON. Edge caching (Cache API, X-Cache headers) doesn't work despite being implemented. Every request hits the origin at full SSR cost. The `Cache-Control` headers from route-level `headers()` functions appear correctly (because TanStack applies them), but the custom `X-Cache` header and cache storage never execute.
37
+
38
+ **Diagnosis**: Search the built `dist/server/worker-entry-*.js` bundle for your custom code (e.g., `X-Cache`, `caches.open`, `_cache/purge`). If absent, TanStack stripped it.
39
+
40
+ **Fix**: Create a **separate** `src/worker-entry.ts` file that wraps TanStack Start's built handler. Point `wrangler.jsonc` to this file instead of `@tanstack/react-start/server-entry`.
41
+
42
+ ```typescript
43
+ // src/worker-entry.ts
44
+ import "./setup";
45
+ import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
46
+ import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
47
+ import { handleMeta, handleDecofileRead, handleDecofileReload, handleRender, corsHeaders } from "@decocms/start/admin";
48
+
49
+ const serverEntry = createServerEntry({
50
+ async fetch(request) {
51
+ return await handler.fetch(request);
52
+ },
53
+ });
54
+
55
+ export default createDecoWorkerEntry(serverEntry, {
56
+ admin: { handleMeta, handleDecofileRead, handleDecofileReload, handleRender, corsHeaders },
57
+ });
58
+ ```
59
+
60
+ ```jsonc
61
+ // wrangler.jsonc -- MUST point to custom entry, NOT the default
62
+ {
63
+ "main": "./src/worker-entry.ts",
64
+ // NOT: "main": "@tanstack/react-start/server-entry"
65
+ }
66
+ ```
67
+
68
+ This ensures admin route interception AND edge caching survive the build because they're in the Worker's own fetch handler, outside of TanStack's build pipeline.
69
+
70
+
71
+ ## 19. `import "./setup"` Ordering (CRITICAL)
72
+
73
+ `import "./setup"` MUST be the first import in both `server.ts` and `worker-entry.ts`. Without it, server functions in Vite split modules execute before `setBlocks()` has been called, causing `resolveDecoPage` to return null → 404 on client-side navigation.
74
+
75
+ **Symptom**: SSR works fine (F5), but clicking links shows "No CMS page block matches this URL".
76
+
77
+
78
+ ## 20. loadDeferredSection Must Use POST
79
+
80
+ Without this, the admin shows "Incorrect type. Expected 'array'" for fields that contain loader references in the `.decofile`.
81
+
82
+
83
+ ## 24. new URL() with Relative Paths Fails in Workers
84
+
85
+ `new URL("/product/p")` works in browsers (uses `window.location` as base) but throws `Invalid URL` in Workers/Node because there's no implicit base.
86
+
87
+ **Fix**: Always provide a base URL:
88
+ ```typescript
89
+ const parsed = new URL(url, "https://localhost");
90
+ return parsed.pathname + parsed.search;
91
+ ```
92
+
93
+
94
+ ## 25. Global Variables Throw ReferenceError
95
+
96
+ Code that references undeclared globals (e.g., `userAddressData` injected by VTEX scripts) will throw `ReferenceError: X is not defined` in Workers where those scripts don't run.
97
+
98
+ **Fix**: Access via `globalThis`:
99
+ ```typescript
100
+ const data = (globalThis as any).userAddressData;
101
+ if (data && Array.isArray(data)) { /* use data */ }
102
+ ```
103
+
104
+
105
+ ## 26. Section-Type Props Use __resolveType Format
106
+
107
+ In the new `@decocms/start`, section-type props from the CMS arrive as `{ __resolveType: "site/sections/Foo.tsx", ...props }`, NOT the old `{ Component, props }` format. Components that render section props must handle this.
108
+
109
+ **Fix**: Create a `RenderSection` bridge component that:
110
+ 1. Checks for `section.Component` (old format) and renders directly
111
+ 2. Checks for `section.__resolveType` (new format), resolves via `getSection()` from `@decocms/start/cms`, and renders with `React.lazy` + `Suspense`
112
+
113
+
114
+ ## 27. jsdom Must Be Replaced in Workers
115
+
116
+ `jsdom` is a heavy Node.js dependency that cannot run in Cloudflare Workers. Components using it for HTML sanitization must use `dompurify` instead.
117
+
118
+ **Fix**: Replace `import { JSDOM } from "jsdom"` with:
119
+ ```typescript
120
+ import DOMPurify from "dompurify";
121
+ const clean = typeof document !== "undefined" ? DOMPurify.sanitize(html) : html;
122
+ ```
123
+
124
+
125
+ ## 28. Deno npm: Prefix Must Be Removed
126
+
127
+ Imports like `import Color from "npm:colorjs.io"` use the Deno-specific `npm:` prefix. Vite/Node don't understand it.
128
+
129
+ **Fix**: Remove the `npm:` prefix and install the package: `npm install colorjs.io`.
130
+
131
+
132
+ ## 30. Stale Edge Cache After Deploy Requires Explicit Purge
133
+
134
+ **Severity**: MEDIUM — causes "Failed to fetch dynamically imported module" errors
135
+
136
+ After deploying a new build to Cloudflare Workers, the edge cache may still serve old HTML that references previous JS bundle hashes. This causes module import failures.
137
+
138
+ **Fix**: After every deploy, purge the cache:
139
+ 1. Set a `PURGE_TOKEN` secret: `npx wrangler secret put PURGE_TOKEN`
140
+ 2. Call the purge endpoint: `POST /_cache/purge` with `Authorization: Bearer <token>` and body `{"paths":["/"]}`
141
+ 3. Automate this in CI/CD (see the deploy.yml workflow)
142
+
143
+
144
+ ## 44. Runtime Module Import Kills Lazy-Loaded Sections
145
+
146
+ **Severity**: HIGH — sections silently disappear, data appears in RSC streaming but component renders nothing
147
+
148
+ Vite tree-shakes unused imports in production builds, so a section file that imports a non-existent module may pass `npm run build` without errors. But at runtime, when the section is dynamically imported via `registerSections`'s lazy `() => import("./sections/X")`, ALL imports in the module execute. A missing file kills the entire section module.
149
+
150
+ **Symptom**: Product shelves or other sections disappear. HTML size drops significantly. Product data appears in React streaming data (`$R[...]` notation) but zero product cards render as actual HTML. No error in the build log.
151
+
152
+ **Example**:
153
+ ```typescript
154
+ // sections/Product/ProductShelf.tsx
155
+ import LoadingCard from "~/components/product/loadingCard"; // file doesn't exist!
156
+ export { default, loader } from "~/components/product/ProductShelf";
157
+
158
+ export function LoadingFallback() {
159
+ return <LoadingCard />; // only used here — tree-shaken in build
160
+ }
161
+ ```
162
+
163
+ Build passes because `LoadingFallback` is a named export that nothing imports. But at runtime, the dynamic `import("./sections/Product/ProductShelf")` executes the module, hits the missing `loadingCard` import, and the entire section fails to load.
164
+
165
+ **Fix**: Create the missing file, even if it's a minimal stub:
166
+ ```typescript
167
+ // components/product/loadingCard.tsx
168
+ export default function LoadingCard() {
169
+ return <div className="animate-pulse bg-base-200 h-[400px] w-[200px] rounded" />;
170
+ }
171
+ ```
172
+
173
+ **Prevention**: After copying files from the original repo, verify all imports resolve:
174
+ ```bash
175
+ npx tsc --noEmit # catches missing modules that Vite's tree-shaking hides
176
+ ```
177
+
178
+
179
+ ## 45. GitHub Packages npm Requires Auth Even for Public Packages
180
+
181
+ **Severity**: MEDIUM — blocks dependency installation for new contributors and CI
182
+
183
+ GitHub Packages' npm registry (`npm.pkg.github.com`) requires authentication even for public packages. This is a known limitation that GitHub has not resolved. Attempting to `npm install` a public `@decocms/*` package without a token returns `401 Unauthorized`.
184
+
185
+ **Workaround A (recommended for development)**: Use `github:` Git URL syntax instead of npm registry references. This bypasses the npm registry entirely and uses Git HTTPS (no auth needed for public repos):
186
+
187
+ ```json
188
+ {
189
+ "@decocms/apps": "github:decocms/apps-start",
190
+ "@decocms/start": "github:decocms/deco-start#main"
191
+ }
192
+ ```
193
+
194
+ **Important**: The repo name in the `github:` URL must match the actual GitHub repo name, not the npm package name. `@decocms/start` is published from repo `decocms/deco-start`, NOT `decocms/start`.
195
+
196
+ **Workaround B (recommended for production)**: Publish to npmjs.com instead. Only npm's public registry supports truly zero-auth public package installation.
197
+
198
+ **Workaround C (if you must use GitHub Packages)**: Generate a GitHub PAT with `read:packages` scope and configure:
199
+ ```bash
200
+ npm config set //npm.pkg.github.com/:_authToken <YOUR_TOKEN>
201
+ ```
202
+
203
+ Or in project `.npmrc` with an env var (for CI):
204
+ ```
205
+ @decocms:registry=https://npm.pkg.github.com
206
+ //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
207
+ ```
208
+
209
+ **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.
@@ -0,0 +1,55 @@
1
+ # package.json Template
2
+
3
+ ```json
4
+ {
5
+ "name": "my-tanstack-store",
6
+ "version": "0.1.0",
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "npm run generate && vite dev",
10
+ "build": "npm run generate && vite build",
11
+ "generate": "npm run generate:blocks && npm run generate:invoke && npm run generate:schema",
12
+ "generate:blocks": "tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
13
+ "generate:invoke": "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
14
+ "generate:schema": "tsx node_modules/@decocms/start/scripts/generate-schema.ts --site storefront",
15
+ "deploy": "wrangler deploy"
16
+ },
17
+ "dependencies": {
18
+ "@decocms/apps": "^0.20.1",
19
+ "@decocms/start": "^0.16.4",
20
+ "@tanstack/react-query": "^5.90.21",
21
+ "@tanstack/react-router": "^1.166.2",
22
+ "@tanstack/react-router-devtools": "^1.166.2",
23
+ "@tanstack/react-start": "^1.166.2",
24
+ "@tanstack/react-store": "^0.9.1",
25
+ "react": "^19.2.4",
26
+ "react-dom": "^19.2.4"
27
+ },
28
+ "devDependencies": {
29
+ "@cloudflare/vite-plugin": "^1.26.1",
30
+ "@tailwindcss/vite": "^4.2.1",
31
+ "@tanstack/router-generator": "^1.166.0",
32
+ "@types/react": "^19.0.0",
33
+ "@types/react-dom": "^19.0.0",
34
+ "babel-plugin-react-compiler": "^1.0.0",
35
+ "daisyui": "^5.5.19",
36
+ "tailwindcss": "^4.2.1",
37
+ "tsx": "^4.19.2",
38
+ "typescript": "^5.9.3",
39
+ "vite": "^7.3.1",
40
+ "wrangler": "^4.14.1",
41
+ "@vitejs/plugin-react": "^4.5.2"
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Notes
47
+
48
+ - `@decocms/start` and `@decocms/apps` come from GitHub Packages — needs `.npmrc`:
49
+ ```
50
+ @decocms:registry=https://npm.pkg.github.com
51
+ //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
52
+ ```
53
+ - Set `NODE_AUTH_TOKEN` in `.env` (add `.env` to `.gitignore`)
54
+ - `generate` scripts run before dev and build to produce `blocks.gen.ts`, `invoke.gen.ts`, `meta.gen.json`
55
+ - `tsx` is needed for the generate scripts (TypeScript execution)