| name | deco-to-tanstack-migration |
| description | Consolidated migration skill for Deco storefronts. Phase-based playbook for Fresh/Preact/Deno to TanStack Start/React/Cloudflare Workers. Covers all phases from scaffold to async rendering, plus post-migration patterns, hydration fixes, navigation, search, matchers, and islands elimination. Single entry point — all deep-dive content in references/. |
Deco-to-TanStack-Start Migration Playbook
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, VTEX, async rendering).
Architecture Boundaries
| Layer | npm Package | Purpose | Must NOT Contain |
|---|
| @decocms/start | @decocms/start | CMS resolution, DecoPageRenderer, worker entry, sdk (useScript, signal, clx) | Preact shims, widget types, site-specific maps |
| @decocms/apps | @decocms/apps | VTEX/Shopify loaders, commerce types, commerce sdk (useOffer, formatPrice, analytics) | Passthrough HTML components, Preact/Fresh refs |
| Site repo | (not published) | All UI: components, hooks, types, routes, styles | No compat/ layer, no aliases beyond ~ |
Architecture Map
| Old Stack | New Stack |
|---|
| Deno + Fresh | Node + TanStack Start |
| Preact + Islands | React 19 + React Compiler |
| @preact/signals | @tanstack/store + @tanstack/react-store |
| Deco CMS runtime | Static JSON blocks via @decocms/start |
| $fresh/runtime.ts | Inlined (asset() removed, IS_BROWSER inlined) |
| @deco/deco/* | @decocms/start/sdk/* or inline stubs |
| apps/commerce/types | @decocms/apps/commerce/types |
| apps/website/components/* | ~/components/ui/* (local React) |
| apps/{platform}/hooks/* | ~/hooks/useCart (real implementation) |
| ~/sdk/useOffer | @decocms/apps/commerce/sdk/useOffer |
| ~/sdk/useScript | @decocms/start/sdk/useScript |
| ~/sdk/signal | @decocms/start/sdk/signal |
| ~/sdk/useSuggestions (hand-rolled) | @decocms/start/sdk/useSuggestions → createUseSuggestions<T>() factory |
Migration Phases
Each phase has entry/exit criteria. Follow in order. Automation % indicates how much can be done with bulk sed/grep.
| Phase | Name | Automation | Reference |
|---|
| 0 | Scaffold & Copy | 100% | templates/ |
| 1 | Import Rewrites | ~90% | references/imports/ |
| 2 | Signals & State | ~50% | references/signals/ |
| 3 | Deco Framework Elimination | ~80% | references/deco-framework/ |
| 4 | Commerce Types & UI | ~70% | references/commerce/ |
| 5 | Platform Hooks (factories, W12+) | template | references/platform-hooks-factories.md |
| 6 | Islands Elimination | ~60% | references/islands.md |
| 7 | Section Registry & Setup | 0% | references/async-rendering.md |
| 8 | Routes & CMS | template | references/navigation.md |
| 9 | Worker Entry & Server | template | references/worker-cloudflare.md |
| 10 | Matchers | ~40% | references/matchers.md |
| 11 | Async Rendering & Polish | 0% | references/async-rendering.md |
| 12 | Search | 0% | references/search.md |
Phase 0 — Scaffold
Entry: Source site accessible, @decocms/start + @decocms/apps published
Actions:
- Create TanStack Start project
- Copy
src/components/, src/sections/, src/islands/, src/hooks/, src/sdk/, src/loaders/ from source
- Copy
.deco/blocks/ (CMS content)
- Copy
static/ assets
- Create
package.json — see templates/package-json.md
- Create
vite.config.ts — see templates/vite-config.md
npm install
Exit: Empty project builds with npm run build
Phase 1 — Imports & JSX
Entry: Source files copied to src/
Actions (bulk sed — see references/codemod-commands.md):
- Preact → React:
from "preact/hooks" → from "react", etc.
ComponentChildren → ReactNode
class= → className= in JSX
- SVG attrs:
stroke-width → strokeWidth, fill-rule → fillRule, etc.
- HTML attrs:
for= → htmlFor=, fetchpriority → fetchPriority
- Remove
/** @jsxRuntime automatic */ pragma comments
Verification: grep -r 'from "preact' src/ | wc -l → 0
Exit: Zero preact imports, zero class= in JSX
See: references/imports/README.md, references/jsx-migration.md
Phase 2 — Signals & State
Entry: Phase 1 complete
Actions:
- Bulk:
from "@preact/signals" → from "@decocms/start/sdk/signal" (module-level signals)
- Manual:
useSignal(val) → useState(val) (component hooks)
- Manual:
useComputed(() => expr) → useMemo(() => expr, [deps]) (component hooks)
- For global reactive state: use
signal() from @decocms/start/sdk/signal + useStore() from @tanstack/react-store
Verification: grep -r '@preact/signals' src/ | wc -l → 0
Exit: Zero @preact/signals imports
See: references/signals/README.md, references/react-signals-state.md
Phase 3 — Deco Framework
Entry: Phase 2 complete
Actions (mostly bulk sed):
- Remove
$fresh/runtime.ts imports (asset() → identity, IS_BROWSER → typeof window !== "undefined")
from "deco-sites/SITENAME/" → from "~/"
from "$store/" → from "~/"
from "site/" → from "~/"
SectionProps → inline type
useScript → from "@decocms/start/sdk/useScript"
clx → from "@decocms/start/sdk/clx"
Verification: grep -rE 'from "(@deco/deco|\$fresh|deco-sites/)' src/ | wc -l → 0
Exit: Zero @deco/deco, $fresh, deco-sites/ imports
See: references/deco-framework/README.md
Phase 4 — Commerce & Types
Entry: Phase 3 complete
Actions:
from "apps/commerce/types.ts" → from "@decocms/apps/commerce/types"
from "apps/admin/widgets.ts" → from "@decocms/start/types/widgets" (framework owns the string aliases — do not create a local src/types/widgets.ts)
from "apps/website/components/Image.tsx" → from "~/components/ui/Image" (create local)
- SDK utilities:
~/sdk/useOffer → @decocms/apps/commerce/sdk/useOffer, etc.
Verification: grep -r 'from "apps/' src/ | wc -l → 0
Exit: Zero apps/ imports
See: references/commerce/README.md, references/vtex-commerce.md
Phase 5 — Platform Hooks
Entry: Phase 4 complete
Actions (Wave 12+ factory-based — current):
src/hooks/useCart.ts — 5-line shim around createUseCart from @decocms/apps/vtex/hooks/createUseCart
src/hooks/useUser.ts — 5-line shim around createUseUser
src/hooks/useWishlist.ts — 5-line shim around createUseWishlist
- The migration template (
scripts/migrate/templates/hooks.ts) emits all three for VTEX sites automatically.
For non-VTEX platforms, scaffold no-op stubs using @decocms/start/sdk/signal (see factories doc § "Non-VTEX platforms").
Exit: Cart add/remove works, no apps/{platform}/hooks imports
See: references/platform-hooks-factories.md (canonical, Wave 12+).
Pre-W12 manual approach is preserved at references/platform-hooks/README.md for sites that haven't migrated to factories yet.
Phase 6 — Islands Elimination
Entry: Phase 5 complete
Actions:
- Audit
src/islands/ — categorize each file:
- Wrapper: just re-exports from
components/ → delete, repoint imports
- Standalone: has real logic → move to
src/components/
- Update all imports pointing to
islands/ to point to components/
- Delete
src/islands/ directory
Verification: ls src/islands/ 2>/dev/null → directory not found
Exit: No islands/ directory
See: references/islands.md
Phase 7 — Section Registry
Entry: Phase 6 complete
Actions (critical — build src/setup.ts):
- Register all sections via
registerSections() with dynamic imports
- Register critical sections (Header, Footer) via
registerSectionsSync() + setResolvedComponent()
- Register section loaders via
registerSectionLoaders()
- Register layout sections via
registerLayoutSections()
- Register commerce loaders via
registerCommerceLoaders() with SWR caching
- Wire
onBeforeResolve() → initVtexFromBlocks() for VTEX config
- Configure
setAsyncRenderingConfig() with alwaysEager for critical sections
- Configure admin:
setMetaData(), setRenderShell(), setInvokeLoaders()
Template: templates/setup-ts.md
Exit: setup.ts compiles, all sections registered
See: references/async-rendering.md (Part 2: Site Implementation)
Phase 8 — Routes & CMS
Entry: Phase 7 complete
Actions:
- Create
src/router.tsx with scroll restoration
- Create
src/routes/__root.tsx with QueryClient, LiveControls, NavigationProgress, analytics
- Create
src/routes/index.tsx using cmsHomeRouteConfig()
- Create
src/routes/$.tsx using cmsRouteConfig()
Templates: templates/root-route.md, templates/router.md
Exit: Routes compile, CMS pages resolve
See: references/navigation.md
Phase 9 — Worker Entry
Entry: Phase 8 complete
Actions:
- Create
src/server.ts — CRITICAL: import "./setup" MUST be the first line
- Create
src/worker-entry.ts — same: import "./setup" first
- Wire admin handlers (handleMeta, handleDecofileRead, handleRender)
- Wire VTEX proxy if needed
Template: templates/worker-entry.md
CRITICAL: Without import "./setup" as the first import, server functions in Vite split modules will have empty state. This causes 404 on client-side navigation.
Exit: npm run dev serves pages, admin endpoints work
See: references/worker-cloudflare.md
Phase 10 — Matchers
Entry: Phase 9 complete
Actions:
- Audit existing matchers (check
src/matchers/, src/sdk/matcher*)
- Migrate MatchContext → MatcherContext (different shape)
- Register matchers in
setup.ts via registerMatcher()
- Wire CF geo cookie injection if using location matchers
Exit: All matchers registered, flags/variants work
See: references/matchers.md
Phase 11 — Async Rendering
Entry: Phase 10 complete (site builds and serves pages)
Actions:
- Identify lazy sections from CMS Lazy wrappers
- Add
export function LoadingFallback() to lazy sections
- Configure
registerCacheableSections() for SWR on heavy sections
- Test deferred section loading on scroll
Exit: Above-the-fold renders instantly, below-fold loads on scroll
See: references/async-rendering.md
Phase 12 — Search
Entry: Phase 11 complete
Actions:
- Wire search route with
loaderDeps for URL params (q, sort, page, filters)
- Configure VTEX Intelligent Search loader
- Wire SearchBar autocomplete via server function
- Test filter toggling, pagination, sort
Exit: Search page works end-to-end
See: references/search.md
Post-Migration
| Problem | Reference |
|---|
| Hydration mismatches, flash-of-white, CLS | references/hydration-fixes.md |
| Runtime bugs, nested sections, VTEX resilience | references/storefront-patterns.md |
| CSS / Tailwind / DaisyUI issues | references/css-styling.md |
| Admin / CMS integration issues | references/admin-cms.md |
| React hooks patterns | references/react-hooks-patterns.md |
| All indexed gotchas | references/gotchas.md |
Key Principles
- No compat layer anywhere -- not in
@decocms/start, not in @decocms/apps, not in the site repo
- Replace, don't wrap -- change the import to the real thing, don't create a pass-through
- Types from the library, UI from the site --
Product type comes from @decocms/apps/commerce/types, but the <Image> component is site-local
- One Vite alias maximum --
"~" -> "src/" is the only acceptable alias
tsconfig.json mirrors vite.config.ts -- only "~/*": ["./src/*"] in paths
- Signals don't auto-subscribe in React -- reading
signal.value in render creates NO subscription; use useStore(signal.store) from @tanstack/react-store
- Commerce loaders need request context --
resolve.ts must pass URL/path to PLP/PDP loaders
wrangler.jsonc main must be a custom worker-entry -- TanStack Start ignores export default in server.ts. The main field lives in the site's per-site wrangler.jsonc.
- Copy components faithfully, never rewrite --
cp the original, then only change mechanical things (class→className, imports). NEVER regenerate or "improve" — AI-rewritten components are the #1 source of visual regressions
- Tailwind v4 logical property hazard -- mixed
px-* + pl-*/pr-* on the same element breaks the cascade
- oklch CSS variables need triplets, not hex --
oklch(var(--x)) must store variables as oklch triplets
- Verify ALL imports resolve at runtime, not just build -- Vite tree-shakes dead imports, so
npm run build passes even with missing modules
import "./setup" first — in both server.ts and worker-entry.ts
- globalThis for split modules — Vite server function split modules need
globalThis.__deco to share state
Worker Entry Architecture
Admin routes MUST be handled in createDecoWorkerEntry (the outermost wrapper), NOT inside TanStack's createServerEntry. Vite strips custom logic from createServerEntry in production.
Request
└─> createDecoWorkerEntry(serverEntry, { admin: { ... } })
├─> tryAdminRoute() ← FIRST: /live/_meta, /.decofile, /live/previews/*
├─> cache purge check ← __deco_purge_cache
├─> static asset bypass ← /assets/*, favicon, sprites
├─> Cloudflare cache (caches.open)
└─> serverEntry.fetch() ← TanStack Start handles everything else
Key rules:
./setup MUST be imported first
- Admin handlers passed as options, NOT imported inside
createDecoWorkerEntry
/live/ and /.decofile are in DEFAULT_BYPASS_PATHS -- never cached
Conductor / AI Bulk Migration Workflow
For sites with 100+ sections:
- Scaffold + Copy (human): scaffold project,
cp -r src/, set up config files
- Mechanical Rewrites (AI/conductor): bulk import rewrites, JSX attr rewrites, type rewrites, signal-to-state — see
references/codemod-commands.md
- Verify (human + AI):
npx tsc --noEmit, npm run build, npm run dev + browser test, visual comparison
- Fix Runtime Issues (human-guided): gotchas and architectural differences
Key Insight: The approach that worked (836 errors → 0 across 213 files) treated every file as: copy the original, apply mechanical changes only. Never "rewrite in React".
Reference Index
| Topic | Path |
|---|
| Preact → React imports | references/imports/ |
| Signals → TanStack Store | references/signals/ |
| Deco framework elimination | references/deco-framework/ |
| Commerce & widget types | references/commerce/ |
| Platform hooks (VTEX, factories — Wave 12+) | references/platform-hooks-factories.md |
| Platform hooks (manual, legacy pre-W12) | references/platform-hooks/README.md |
| Vite configuration | references/vite-config/ |
| Automation commands | references/codemod-commands.md |
| Islands elimination | references/islands.md |
| Navigation & routing | references/navigation.md |
| Search implementation | references/search.md |
| Matchers (architecture + migration) | references/matchers.md |
| Async rendering (architecture + site guide) | references/async-rendering.md |
| Hydration fixes | references/hydration-fixes.md |
| Runtime storefront patterns | references/storefront-patterns.md |
| Admin / CMS integration | references/admin-cms.md |
| Gotchas index | references/gotchas.md |
| Post-migration cleanup checklist | references/post-migration-cleanup.md |
| HTMX → React rewrite recipes | references/htmx-rewrite.md |
| React hooks patterns | references/react-hooks-patterns.md |
| React signals & state | references/react-signals-state.md |
| JSX migration differences | references/jsx-migration.md |
| VTEX commerce gotchas | references/vtex-commerce.md |
| Worker / Cloudflare / build | references/worker-cloudflare.md |
| CSS / Tailwind / DaisyUI | references/css-styling.md |
| setup.ts template | templates/setup-ts.md |
| vite.config.ts template | templates/vite-config.md |
| worker-entry template | templates/worker-entry.md |
| __root.tsx template | templates/root-route.md |
| router.tsx template | templates/router.md |
| package.json template | templates/package-json.md |