Imported from decocms/blocks (
.agents/skills/deco-to-tanstack-migration/SKILL.md). Install upstream withnpx skills add decocms/blocks --skill deco-to-tanstack-migration. Copyright stays with the author.
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— seetemplates/package-json.md - Create
vite.config.ts— seetemplates/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→ReactNodeclass=→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.tsimports (asset()→ identity,IS_BROWSER→typeof window !== "undefined") from "deco-sites/SITENAME/"→from "~/"from "$store/"→from "~/"from "site/"→from "~/"SectionProps→ inline typeuseScript→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 localsrc/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 aroundcreateUseCartfrom@decocms/apps/vtex/hooks/createUseCartsrc/hooks/useUser.ts— 5-line shim aroundcreateUseUsersrc/hooks/useWishlist.ts— 5-line shim aroundcreateUseWishlist- 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/
- Wrapper: just re-exports from
- Update all imports pointing to
islands/to point tocomponents/ - 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()withalwaysEagerfor 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.tsxwith scroll restoration - Create
src/routes/__root.tsxwith QueryClient, LiveControls, NavigationProgress, analytics - Create
src/routes/index.tsxusingcmsHomeRouteConfig() - Create
src/routes/$.tsxusingcmsRouteConfig()
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.tsviaregisterMatcher() - 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
loaderDepsfor 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 --
Producttype comes from@decocms/apps/commerce/types, but the<Image>component is site-local - One Vite alias maximum --
"~"->"src/"is the only acceptable alias tsconfig.jsonmirrorsvite.config.ts-- only"~/*": ["./src/*"]in paths- Signals don't auto-subscribe in React -- reading
signal.valuein render creates NO subscription; useuseStore(signal.store)from@tanstack/react-store - Commerce loaders need request context --
resolve.tsmust pass URL/path to PLP/PDP loaders wrangler.jsoncmain must be a custom worker-entry -- TanStack Start ignoresexport defaultinserver.ts. Themainfield lives in the site's per-sitewrangler.jsonc.- Copy components faithfully, never rewrite --
cpthe 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 buildpasses even with missing modules import "./setup"first — in bothserver.tsandworker-entry.ts- globalThis for split modules — Vite server function split modules need
globalThis.__decoto 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:
./setupMUST be imported first- Admin handlers passed as options, NOT imported inside
createDecoWorkerEntry /live/and/.decofileare inDEFAULT_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 |
| Staging learnings for an epic-scale migration (index-per-target-file, from day one) | references/migration-learnings-staging.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 |