@decocms/start 1.6.2 → 1.7.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/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
- package/.releaserc.json +1 -0
- package/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/generate-loaders.ts +79 -12
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +190 -11
- package/scripts/migrate/phase-cleanup.ts +1162 -7
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +56 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +174 -69
- package/scripts/migrate/templates/lib-utils.ts +255 -0
- package/scripts/migrate/templates/package-json.ts +30 -22
- package/scripts/migrate/templates/routes.ts +81 -11
- package/scripts/migrate/templates/section-loaders.ts +369 -33
- package/scripts/migrate/templates/server-entry.ts +350 -80
- package/scripts/migrate/templates/setup.ts +78 -8
- package/scripts/migrate/templates/types-gen.ts +58 -0
- package/scripts/migrate/templates/ui-components.ts +47 -16
- package/scripts/migrate/templates/vite-config.ts +17 -6
- package/scripts/migrate/templates/wrangler.ts +3 -1
- package/scripts/migrate/transforms/dead-code.ts +330 -4
- package/scripts/migrate/transforms/deno-isms.ts +19 -0
- package/scripts/migrate/transforms/imports.ts +93 -30
- package/scripts/migrate/transforms/jsx.ts +79 -4
- package/scripts/migrate/transforms/section-conventions.ts +105 -3
- package/scripts/migrate/types.ts +9 -1
- package/src/cms/resolve.ts +12 -1
- package/src/sdk/useScript.ts +27 -6
- package/src/sdk/workerEntry.ts +11 -2
- package/src/setup.ts +1 -1
|
@@ -5,7 +5,51 @@ description: Migrate Deco.cx storefronts from Fresh/Preact to TanStack Start/Rea
|
|
|
5
5
|
|
|
6
6
|
# Deco-to-TanStack-Start Migration Playbook
|
|
7
7
|
|
|
8
|
-
Phase-based playbook for converting `deco-sites/*` storefronts from Fresh/Preact/Deno to TanStack Start/React/Cloudflare Workers. Battle-tested on espacosmart-storefront (100+ sections
|
|
8
|
+
Phase-based playbook for converting `deco-sites/*` storefronts from Fresh/Preact/Deno to TanStack Start/React/Cloudflare Workers. Battle-tested on espacosmart-storefront (VTEX, 100+ sections) and storefront-tanstack (Shopify).
|
|
9
|
+
|
|
10
|
+
**Framework minimum**: `@decocms/start >= 1.6.2` (deferred sections with `:slug` route params require the `routeParams` propagation fix — see gotcha #47).
|
|
11
|
+
|
|
12
|
+
## One-Prompt Migration (copy/paste to an AI agent)
|
|
13
|
+
|
|
14
|
+
Paste the prompt below verbatim into a code-writing AI (Claude Code, Cursor, etc.) pointed at a fresh TanStack Start scaffold that already has the source `deco-sites/*` project copied to `.context/` for reference. The agent will execute phases 0–10 sequentially, gating on exit criteria, and stop to confirm when a phase needs a human judgment call.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
You are migrating a Deco/Fresh/Preact storefront to TanStack Start/React/Cloudflare Workers.
|
|
18
|
+
|
|
19
|
+
Source site reference: .context/<SITE_NAME>/ (read-only)
|
|
20
|
+
Target repo: this working directory
|
|
21
|
+
Framework docs: .cursor/skills/deco-to-tanstack-migration/SKILL.md + references/ + templates/
|
|
22
|
+
|
|
23
|
+
Execute the migration in phase order. For each phase:
|
|
24
|
+
1. Read the phase section in SKILL.md and the linked references/templates
|
|
25
|
+
2. Run the automation commands from references/codemod-commands.md where the phase is bulk-safe
|
|
26
|
+
3. Verify the phase's exit criteria before moving on
|
|
27
|
+
4. If a verification fails, fix root causes — do not skip
|
|
28
|
+
|
|
29
|
+
Critical invariants (violate none of these):
|
|
30
|
+
- Never add compat/ layers or Vite aliases beyond `~` → `src/`
|
|
31
|
+
- Copy components faithfully from source — do NOT rewrite JSX, class names, or grid/flex structure. Only apply mechanical migrations (class→className, for→htmlFor, preact→react imports). AI-rewritten components are the #1 regression source.
|
|
32
|
+
- `@decocms/start >= 1.6.2` in package.json (deferred-section routeParams fix)
|
|
33
|
+
- `import "./setup"` is the FIRST line in server.ts and worker-entry.ts
|
|
34
|
+
- Use `createSiteSetup()` + `applySectionConventions()` + `autoconfigApps(blocks, APP_REGISTRY)` — do NOT hand-wire section loaders, alwaysEager lists, or commerce loaders for apps the registry already covers (Phase 7 reference has the shape)
|
|
35
|
+
- Section metadata is declared in the section file (`export const eager = true`, `export const cache = "listing"`, etc.) — `generate-sections.ts` extracts it. Do not maintain duplicate arrays in setup.ts.
|
|
36
|
+
- After every phase, run: `npm run build && npm run typecheck`. Both must be green before proceeding.
|
|
37
|
+
|
|
38
|
+
Stop and ask the human when:
|
|
39
|
+
- A component's layout output differs visibly from the source reference
|
|
40
|
+
- A gotcha listed in references/gotchas.md applies but the fix is non-mechanical
|
|
41
|
+
- Phase 5 (commerce/platform hooks): cart/user/wishlist flows are platform-specific and require real credentials — confirm the hook implementation before writing
|
|
42
|
+
- Phase 8 (routes): SEO sections, SSR/SPA decisions, and device detection need human confirmation
|
|
43
|
+
|
|
44
|
+
Start at Phase 0. Report phase completion with a one-line summary + the verification command output.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The prompt assumes:
|
|
48
|
+
- The target repo has TanStack Start scaffolded (`npm create @tanstack/app@latest -- --template cloudflare-workers`)
|
|
49
|
+
- `@decocms/start` and `@decocms/apps` are published and installable (or locally linked)
|
|
50
|
+
- The source `src/` has been copied and `.deco/blocks/` is present
|
|
51
|
+
|
|
52
|
+
If either is missing, the agent will stop at Phase 0 and ask.
|
|
9
53
|
|
|
10
54
|
## Architecture Boundaries
|
|
11
55
|
|
|
@@ -227,21 +271,50 @@ See: skill `deco-islands-migration`
|
|
|
227
271
|
**Entry**: Phase 6 complete
|
|
228
272
|
|
|
229
273
|
**Actions** (critical — build `src/setup.ts`):
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
274
|
+
|
|
275
|
+
Modern site setup uses three framework composers + convention-driven section metadata. The old manual registration arrays (`registerSectionsSync`, `setResolvedComponent`, `registerSectionLoaders`, `registerLayoutSections`, `registerSeoSections`, `setAsyncRenderingConfig({ alwaysEager })`) are replaced by codegen reading `export const X = true` declarations from each section file.
|
|
276
|
+
|
|
277
|
+
1. **`createSiteSetup(options)`** from `@decocms/start/setup` — one-call framework bootstrap (sections glob, blocks, meta, CSS, fonts, production origins, preview wrapper, platform init).
|
|
278
|
+
|
|
279
|
+
2. **`applySectionConventions({ meta, syncComponents, loadingFallbacks, sectionGlob })`** from `@decocms/start/cms` — reads `src/server/cms/sections.gen.ts` (produced by `generate-sections.ts`) and registers:
|
|
280
|
+
- `eager` → `registerEagerSections`
|
|
281
|
+
- `layout` → `registerLayoutSections`
|
|
282
|
+
- `seo` → `registerSeoSections`
|
|
283
|
+
- `cache` → `registerCacheableSections`
|
|
284
|
+
- `sync` → `registerSectionsSync` + bundled sync import
|
|
285
|
+
- `hasLoadingFallback` → `registerSection` with fallback
|
|
286
|
+
- `clientOnly` → `registerSection` with `clientOnly: true`
|
|
287
|
+
|
|
288
|
+
3. **`autoconfigApps(blocks, APP_REGISTRY)`** from `@decocms/start/apps` — dual-registers every app's loaders + actions into the CMS resolve path AND the admin `/deco/invoke` path from a single source. `APP_REGISTRY` from `@decocms/apps/registry` maintains the list — adding a new app is one entry there, no site code change.
|
|
289
|
+
|
|
290
|
+
4. **`registerCommerceLoaders({ ... })`** — ONLY for site-local loaders that don't belong to an app (e.g., `site/loaders/minicart.ts`, `site/loaders/user.ts`). Do NOT register Shopify/VTEX loaders manually — autoconfig handles those.
|
|
291
|
+
|
|
292
|
+
5. **Section metadata lives in section files**, not in setup.ts:
|
|
293
|
+
```typescript
|
|
294
|
+
// src/sections/Header/Header.tsx
|
|
295
|
+
export default function Header(props) { /* ... */ }
|
|
296
|
+
export const eager = true; // include in alwaysEager
|
|
297
|
+
export const sync = true; // bundle sync (no Suspense boundary)
|
|
298
|
+
export const layout = true; // render even when CMS page wraps it in Lazy
|
|
299
|
+
```
|
|
300
|
+
Then `npm run generate:sections` emits `sections.gen.ts` with the extracted arrays.
|
|
239
301
|
|
|
240
302
|
**Template**: `templates/setup-ts.md`
|
|
241
303
|
|
|
242
|
-
**
|
|
304
|
+
**Build pipeline** (add to `package.json` scripts):
|
|
305
|
+
```
|
|
306
|
+
"generate:blocks": "tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
|
|
307
|
+
"generate:schema": "tsx node_modules/@decocms/start/scripts/generate-schema.ts --site <SITE>",
|
|
308
|
+
"generate:sections": "tsx node_modules/@decocms/start/scripts/generate-sections.ts",
|
|
309
|
+
"generate:loaders": "tsx node_modules/@decocms/start/scripts/generate-loaders.ts",
|
|
310
|
+
"generate:invoke": "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
|
|
311
|
+
"generate:routes": "tsr generate",
|
|
312
|
+
"build": "npm run generate:blocks && npm run generate:schema && npm run generate:sections && npm run generate:loaders && tsr generate && vite build"
|
|
313
|
+
```
|
|
243
314
|
|
|
244
|
-
|
|
315
|
+
**Exit**: `setup.ts` compiles; `npm run build` green; every section declared in decofile resolves on SSR.
|
|
316
|
+
|
|
317
|
+
See: skill `deco-async-rendering-site-guide`, reference `templates/setup-ts.md`
|
|
245
318
|
|
|
246
319
|
---
|
|
247
320
|
|
|
@@ -718,6 +718,104 @@ Or in project `.npmrc` with an env var (for CI):
|
|
|
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
720
|
|
|
721
|
+
## 47. Deferred Sections Lose routeParams on PDP (`@decocms/start < 1.6.2`)
|
|
722
|
+
|
|
723
|
+
**Severity**: CRITICAL — every deferred PDP section loads with `page: null`, product content never renders.
|
|
724
|
+
|
|
725
|
+
`resolveDeferredSection` (called by the client `_serverFn` that streams `LazySection` contents) builds its own `ResolveContext` without populating `routeParams`. When a PDP loader chain uses `website/functions/requestToParam.ts` to read `:slug` from the route, the resolver returns `null` because `routeParams` is empty. Shopify/VTEX `productDetailsPage` then receives `slug: null` and returns `null`.
|
|
726
|
+
|
|
727
|
+
**Symptom**: PDP HTTP returns 200, initial HTML renders the `<LazySection>` placeholder, but the deferred `_serverFn` POST returns `{ page: null }` (or a seroval null sentinel `{t:2,s:0}`). Product details section stays empty forever.
|
|
728
|
+
|
|
729
|
+
**Fix**: upgrade to `@decocms/start >= 1.6.2`. The fix:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
// src/cms/resolve.ts — resolveDeferredSection
|
|
733
|
+
const match = findPageByPath(pagePath);
|
|
734
|
+
const rctx: ResolveContext = {
|
|
735
|
+
routeParams: match?.params, // ← recovered from page match
|
|
736
|
+
matcherCtx: ctx,
|
|
737
|
+
memo: new Map(),
|
|
738
|
+
depth: 0,
|
|
739
|
+
};
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Verification**: POST directly to the _serverFn endpoint for the deferred section with a real PDP path — response body should contain `"@type":"ProductDetailsPage"`. If it contains `{t:2,s:0}` (seroval null), you're on the broken version.
|
|
743
|
+
|
|
744
|
+
**Detection grep**: search your site's node_modules for the fix marker:
|
|
745
|
+
```bash
|
|
746
|
+
grep -n "findPageByPath(pagePath)" node_modules/@decocms/start/src/cms/resolve.ts
|
|
747
|
+
```
|
|
748
|
+
No match → upgrade.
|
|
749
|
+
|
|
750
|
+
## 48. Section Metadata Must Live in the Section File (Convention-Driven)
|
|
751
|
+
|
|
752
|
+
**Severity**: MEDIUM — duplicate registration arrays drift out of sync; sections silently miss eager/layout flags.
|
|
753
|
+
|
|
754
|
+
Pre-1.6 migrations hand-maintained arrays in `setup.ts`: `alwaysEager: ["site/sections/Header/Header.tsx", ...]`, `layoutSections: [...]`, `seoSections: [...]`, `cacheableSections: {...}`. Every new section meant editing setup.ts. These lists drifted — renamed/deleted sections left dangling entries; new eager sections were forgotten.
|
|
755
|
+
|
|
756
|
+
The current pattern: declare behavior in the section file, `generate-sections.ts` extracts it into `sections.gen.ts`, `applySectionConventions()` registers it.
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
// src/sections/Header/Header.tsx
|
|
760
|
+
export default function Header(props) { /* ... */ }
|
|
761
|
+
export const eager = true; // alwaysEager
|
|
762
|
+
export const sync = true; // bundle sync (first paint)
|
|
763
|
+
export const layout = true; // layout section (never deferred)
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
// src/sections/Product/SearchResult.tsx
|
|
768
|
+
export const cache = "listing"; // SWR cache profile
|
|
769
|
+
export function LoadingFallback() { /* ... */ }
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
// src/sections/SEO/SeoPDP.tsx
|
|
774
|
+
export const seo = true;
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Fix during migration**: delete hand-maintained arrays from setup.ts. Add the right `export const` flags on each section. Run `npm run generate:sections`. The `applySectionConventions({ meta: sectionMeta, ... })` call wires everything.
|
|
778
|
+
|
|
779
|
+
**Regen cadence**: `generate:sections` runs automatically as part of `npm run build`. In dev, rerun it manually after changing a section's metadata exports.
|
|
780
|
+
|
|
781
|
+
## 49. `window.STOREFRONT` Stub for Phase 6 (Cart/User/Wishlist)
|
|
782
|
+
|
|
783
|
+
**Severity**: MEDIUM — pages crash during Phase 6 development without the stub; the stub itself is a placeholder, not a solution.
|
|
784
|
+
|
|
785
|
+
The legacy Deco cart pattern injects `window.STOREFRONT.CART.{getCart, addToCart, setQuantity, subscribe}` + matching `USER` and `WISHLIST` channels. Header badges, minicart, AddToCart button, and wishlist buttons all read from this global. Until Phase 6 (commerce wiring) ships a real TanStack Store-backed implementation, pages crash on every render where any of these are read.
|
|
786
|
+
|
|
787
|
+
**Interim stub** in `src/routes/__root.tsx` `head.scripts`:
|
|
788
|
+
|
|
789
|
+
```javascript
|
|
790
|
+
(function(){
|
|
791
|
+
if (window.STOREFRONT) return;
|
|
792
|
+
var noop = function(){};
|
|
793
|
+
var mkChan = function(emptyValue) {
|
|
794
|
+
return {
|
|
795
|
+
getCart: function(){ return emptyValue; },
|
|
796
|
+
getUser: function(){ return emptyValue; },
|
|
797
|
+
getWishlist: function(){ return emptyValue; },
|
|
798
|
+
getQuantity: function(){ return 0; },
|
|
799
|
+
inWishlist: function(){ return false; },
|
|
800
|
+
addToCart: noop, setQuantity: noop, dispatch: noop, toggle: noop,
|
|
801
|
+
subscribe: function(cb){ try { cb(this); } catch(e){} }
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
window.STOREFRONT = { CART: mkChan(null), USER: mkChan(null), WISHLIST: mkChan(null) };
|
|
805
|
+
})();
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Real Phase 6 replacement** (remove the stub, wire the real thing):
|
|
809
|
+
|
|
810
|
+
1. Create `src/sdk/cart.ts` with a `@tanstack/store` `Store<ShopifyCart>` (or VTEX `OrderForm`).
|
|
811
|
+
2. Create `src/hooks/useCart.ts` that returns `{ cart, addItem, updateItem, clear }`, backed by the store. Mutations call the app's invoke handler (`/deco/invoke/shopify/actions/cart/addItems`).
|
|
812
|
+
3. Replace `AddToCartButton.tsx` inline-script / HTMX pattern with a real React component using `onClick={() => addItem(...)}`. Delete `hx-on:click` + `useScript(onClick)`.
|
|
813
|
+
4. Replace `ProductVariantSelector.tsx` `hx-get` + `useSection()` with TanStack Router `<Link to={relativeLink}>` for client-side navigation.
|
|
814
|
+
5. Wire the minicart loader (`src/loaders/minicart.ts`) to call `invoke.shopify.loaders.cart` server-side for SSR hydration.
|
|
815
|
+
6. Either delete the STOREFRONT stub entirely, or keep a thin `STOREFRONT` bridge that delegates to the store (for any inline scripts still in Header/Minicart templates you haven't migrated).
|
|
816
|
+
|
|
817
|
+
**Gotcha within the gotcha**: the HTMX-era `AddToCartButton` uses an `<input type="checkbox" className="hidden peer">` + `peer-checked:hidden` CSS to swap between the "Add to Cart" button and a QuantitySelector once the item is in the cart. The real Phase 6 implementation should drive that swap from cart state (`const inCart = useCart().items.some(i => i.id === productID)`) and render one or the other. Don't carry the checkbox trick into the TanStack world.
|
|
818
|
+
|
|
721
819
|
## 46. `export type { X } from "..."` Does Not Scope `X` for Parameter Annotations
|
|
722
820
|
|
|
723
821
|
Re-exporting a type makes it available to importers but does NOT bring it into scope in the same file:
|
|
@@ -1,55 +1,75 @@
|
|
|
1
1
|
# package.json Template
|
|
2
2
|
|
|
3
|
+
Current as of `@decocms/start@1.6.2` and `@decocms/apps@1.4.1+`.
|
|
4
|
+
|
|
3
5
|
```json
|
|
4
6
|
{
|
|
5
7
|
"name": "my-tanstack-store",
|
|
6
8
|
"version": "0.1.0",
|
|
7
9
|
"type": "module",
|
|
10
|
+
"description": "storefront powered by TanStack Start",
|
|
8
11
|
"scripts": {
|
|
9
|
-
"dev": "
|
|
10
|
-
"
|
|
11
|
-
"generate":
|
|
12
|
-
"generate:
|
|
13
|
-
"generate:
|
|
14
|
-
"generate:
|
|
15
|
-
"
|
|
12
|
+
"dev": "vite dev",
|
|
13
|
+
"dev:clean": "rm -rf node_modules/.vite .wrangler/state .tanstack && vite dev",
|
|
14
|
+
"generate:blocks": "tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
|
|
15
|
+
"generate:routes": "tsr generate",
|
|
16
|
+
"generate:schema": "tsx node_modules/@decocms/start/scripts/generate-schema.ts --site <SITE>",
|
|
17
|
+
"generate:invoke": "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
|
|
18
|
+
"generate:sections": "tsx node_modules/@decocms/start/scripts/generate-sections.ts",
|
|
19
|
+
"generate:loaders": "tsx node_modules/@decocms/start/scripts/generate-loaders.ts --exclude shopify/loaders,shopify/actions",
|
|
20
|
+
"build": "npm run generate:blocks && npm run generate:schema && npm run generate:sections && npm run generate:loaders && tsr generate && vite build",
|
|
21
|
+
"preview": "vite preview",
|
|
22
|
+
"deploy": "npm run build && wrangler deploy",
|
|
23
|
+
"types": "wrangler types",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
26
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
27
|
+
"knip": "knip",
|
|
28
|
+
"tailwind:lint": "tsx scripts/tailwind-lint.ts",
|
|
29
|
+
"tailwind:fix": "tsx scripts/tailwind-lint.ts --fix"
|
|
16
30
|
},
|
|
17
31
|
"dependencies": {
|
|
18
|
-
"@decocms/apps": "^
|
|
19
|
-
"@decocms/start": "^
|
|
20
|
-
"@tanstack/react-query": "
|
|
21
|
-
"@tanstack/react-router": "
|
|
22
|
-
"@tanstack/react-
|
|
23
|
-
"@tanstack/react-
|
|
24
|
-
"@tanstack/
|
|
32
|
+
"@decocms/apps": "^1.4.1",
|
|
33
|
+
"@decocms/start": "^1.6.2",
|
|
34
|
+
"@tanstack/react-query": "5.90.21",
|
|
35
|
+
"@tanstack/react-router": "1.166.7",
|
|
36
|
+
"@tanstack/react-start": "1.166.8",
|
|
37
|
+
"@tanstack/react-store": "0.9.2",
|
|
38
|
+
"@tanstack/store": "0.9.2",
|
|
25
39
|
"react": "^19.2.4",
|
|
26
40
|
"react-dom": "^19.2.4"
|
|
27
41
|
},
|
|
28
42
|
"devDependencies": {
|
|
29
|
-
"@cloudflare/vite-plugin": "^1.
|
|
43
|
+
"@cloudflare/vite-plugin": "^1.27.0",
|
|
30
44
|
"@tailwindcss/vite": "^4.2.1",
|
|
31
|
-
"@tanstack/router-
|
|
32
|
-
"@types/react": "^19.
|
|
33
|
-
"@types/react-dom": "^19.
|
|
45
|
+
"@tanstack/router-cli": "1.166.7",
|
|
46
|
+
"@types/react": "^19.2.14",
|
|
47
|
+
"@types/react-dom": "^19.2.3",
|
|
48
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
34
49
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
35
50
|
"daisyui": "^5.5.19",
|
|
51
|
+
"knip": "^5.61.2",
|
|
52
|
+
"prettier": "^3.5.3",
|
|
36
53
|
"tailwindcss": "^4.2.1",
|
|
37
|
-
"
|
|
54
|
+
"ts-morph": "^27.0.2",
|
|
55
|
+
"tsx": "^4.19.4",
|
|
38
56
|
"typescript": "^5.9.3",
|
|
39
57
|
"vite": "^7.3.1",
|
|
40
|
-
"wrangler": "^4.
|
|
41
|
-
"@vitejs/plugin-react": "^4.5.2"
|
|
58
|
+
"wrangler": "^4.72.0"
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
```
|
|
45
62
|
|
|
46
63
|
## Notes
|
|
47
64
|
|
|
48
|
-
- `@decocms/start`
|
|
65
|
+
- **Minimum `@decocms/start` version is `1.6.2`** — earlier versions have a bug where deferred sections (`Lazy`-wrapped) lose `routeParams`, causing PDP loaders with `:slug` to return null. See gotcha #47.
|
|
66
|
+
- **`generate` scripts** run as part of `build`. In dev, Vite HMR picks up changes without re-running them — you only need to re-run `generate:blocks` / `generate:sections` after editing `.deco/blocks/` or a section's metadata exports.
|
|
67
|
+
- **`generate:loaders --exclude shopify/loaders,shopify/actions`** — the Shopify app ships its own loaders/actions, registered by `autoconfigApps`. Exclude to avoid double-registering site-local invoke entries for them.
|
|
68
|
+
- **`tsr generate`** produces `src/routeTree.gen.ts` from file-based routes.
|
|
69
|
+
- **GitHub Packages**: add an `.npmrc` in the repo root (git-ignore is optional):
|
|
49
70
|
```
|
|
50
71
|
@decocms:registry=https://npm.pkg.github.com
|
|
51
72
|
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
|
|
52
73
|
```
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
- `tsx` is needed for the generate scripts (TypeScript execution)
|
|
74
|
+
Then set `NODE_AUTH_TOKEN` in `.env` for local installs and as a CI secret. Alternatively, pin by Git tag via `github:` URL syntax — see gotcha #45.
|
|
75
|
+
- **React Compiler** is enabled via `babel-plugin-react-compiler` in `vite.config.ts`. Most sections benefit without annotation.
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# __root.tsx Template
|
|
2
2
|
|
|
3
|
+
Two options — pick the one that matches the site's needs.
|
|
4
|
+
|
|
5
|
+
## Option A — Minimal (recommended default)
|
|
6
|
+
|
|
7
|
+
`DecoRootLayout` from `@decocms/start/hooks` wraps the `<html>` shell with all the pieces the framework expects (DaisyUI theme, LiveControls, HeadContent, Scripts, analytics bootstrap). Use this for every new site unless you need custom providers at the root.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createRootRoute } from "@tanstack/react-router";
|
|
11
|
+
import { DecoRootLayout } from "@decocms/start/hooks";
|
|
12
|
+
// @ts-ignore Vite ?url import
|
|
13
|
+
import appCss from "../styles/app.css?url";
|
|
14
|
+
|
|
15
|
+
export const Route = createRootRoute({
|
|
16
|
+
head: () => ({
|
|
17
|
+
meta: [
|
|
18
|
+
{ charSet: "utf-8" },
|
|
19
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
20
|
+
{ title: "My Store" },
|
|
21
|
+
],
|
|
22
|
+
links: [
|
|
23
|
+
{ rel: "stylesheet", href: appCss },
|
|
24
|
+
{ rel: "icon", href: "/favicon.ico" },
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
component: RootLayout,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function RootLayout() {
|
|
31
|
+
return <DecoRootLayout lang="pt-BR" siteName="my-store" />;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Option B — Custom providers (cart/React Query at root)
|
|
36
|
+
|
|
37
|
+
Only reach for this when you need providers wrapping the CMS outlet — a root-mounted cart store that must hydrate before any section, global React Query client, etc. Everything else stays in sections.
|
|
38
|
+
|
|
3
39
|
```typescript
|
|
4
40
|
import { useState } from "react";
|
|
5
41
|
import {
|
|
@@ -12,27 +48,12 @@ import {
|
|
|
12
48
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
13
49
|
import { LiveControls } from "@decocms/start/hooks";
|
|
14
50
|
import { ANALYTICS_SCRIPT } from "@decocms/start/sdk/analytics";
|
|
51
|
+
// @ts-ignore Vite ?url import
|
|
15
52
|
import appCss from "../styles/app.css?url";
|
|
16
53
|
|
|
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
54
|
const PROGRESS_CSS = `
|
|
32
55
|
@keyframes decoProgress{0%{width:0}30%{width:50%}60%{width:80%}100%{width:98%}}
|
|
33
56
|
.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
57
|
`;
|
|
37
58
|
|
|
38
59
|
function NavigationProgress() {
|
|
@@ -42,7 +63,6 @@ function NavigationProgress() {
|
|
|
42
63
|
<>
|
|
43
64
|
<style dangerouslySetInnerHTML={{ __html: PROGRESS_CSS }} />
|
|
44
65
|
<div className="deco-nav-progress" />
|
|
45
|
-
<div className="deco-nav-overlay" />
|
|
46
66
|
</>
|
|
47
67
|
);
|
|
48
68
|
}
|
|
@@ -55,10 +75,6 @@ export const Route = createRootRoute({
|
|
|
55
75
|
{ title: "My Store" },
|
|
56
76
|
],
|
|
57
77
|
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
78
|
{ rel: "stylesheet", href: appCss },
|
|
63
79
|
{ rel: "icon", href: "/favicon.ico" },
|
|
64
80
|
],
|
|
@@ -68,22 +84,20 @@ export const Route = createRootRoute({
|
|
|
68
84
|
|
|
69
85
|
function RootLayout() {
|
|
70
86
|
const [queryClient] = useState(
|
|
71
|
-
() =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
refetchOnWindowFocus: import.meta.env.DEV,
|
|
78
|
-
},
|
|
87
|
+
() => new QueryClient({
|
|
88
|
+
defaultOptions: {
|
|
89
|
+
queries: {
|
|
90
|
+
staleTime: import.meta.env.DEV ? 0 : 30_000,
|
|
91
|
+
gcTime: import.meta.env.DEV ? 0 : 5 * 60_000,
|
|
92
|
+
refetchOnWindowFocus: import.meta.env.DEV,
|
|
79
93
|
},
|
|
80
|
-
}
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
81
96
|
);
|
|
82
97
|
|
|
83
98
|
return (
|
|
84
99
|
<html lang="pt-BR" data-theme="light" suppressHydrationWarning>
|
|
85
100
|
<head>
|
|
86
|
-
<script dangerouslySetInnerHTML={{ __html: DECO_EVENTS_BOOTSTRAP }} />
|
|
87
101
|
<HeadContent />
|
|
88
102
|
</head>
|
|
89
103
|
<body className="bg-base-100 text-base-content" suppressHydrationWarning>
|
|
@@ -91,7 +105,7 @@ function RootLayout() {
|
|
|
91
105
|
<QueryClientProvider client={queryClient}>
|
|
92
106
|
<Outlet />
|
|
93
107
|
</QueryClientProvider>
|
|
94
|
-
<LiveControls site=
|
|
108
|
+
<LiveControls site="my-store" />
|
|
95
109
|
<script type="module" dangerouslySetInnerHTML={{ __html: ANALYTICS_SCRIPT }} />
|
|
96
110
|
<Scripts />
|
|
97
111
|
</body>
|
|
@@ -100,11 +114,14 @@ function RootLayout() {
|
|
|
100
114
|
}
|
|
101
115
|
```
|
|
102
116
|
|
|
103
|
-
## Key
|
|
117
|
+
## Key points (applies to both)
|
|
118
|
+
|
|
119
|
+
1. **`data-theme="light"`** on `<html>` — required for DaisyUI v4/v5 CSS variables to activate in production AND in the admin preview shell. Option A sets this for you.
|
|
120
|
+
2. **`suppressHydrationWarning`** on `<html>` and `<body>` — browser extensions mutate these elements; React would warn on mismatch.
|
|
121
|
+
3. **`LiveControls site={...}`** — admin iframe bridge. `site` MUST match the CMS site name used in `@decocms/apps/registry` entries and `.deco/blocks/`.
|
|
122
|
+
4. **No `Device.Provider`** — do NOT hardcode `<Device.Provider value={{ isMobile: true }}>` in the root. Device detection belongs in each page route's `createServerFn` loader (see gotcha #29).
|
|
123
|
+
5. **Keep the root thin** — sections own their own data. Avoid cross-section state at the root unless it's truly global (cart, theme). Every provider at the root ships to every page, even ones that don't need it.
|
|
124
|
+
|
|
125
|
+
## Cart/platform state
|
|
104
126
|
|
|
105
|
-
|
|
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.
|
|
127
|
+
If you add a cart store at the root (Option B), wire it AFTER `QueryClientProvider` so section-local hooks can use both. The cart store itself should be a module-level `@tanstack/store` `Store`, and a `useCart()` hook reads via `useStore(cartStore)`. SSR hydration: the page loader fetches the initial cart (via the minicart loader or Shopify `getCart`) and the root passes the initial state to `cartStore.setState()` before hydration.
|