| name | decocms-v6-to-v7-upgrade |
| description | Upgrades an already-TanStack site from the monolithic @decocms/start@6.x + @decocms/apps@5.x to the split 7.x packages (@decocms/blocks, @decocms/tanstack, @decocms/blocks-admin, @decocms/blocks-cli, @decocms/apps-*). Use when a TanStack Start site's package.json pins @decocms/start to 6.x and/or @decocms/apps to 5.x. Not for Fresh/Deno sites (use the Fresh→TanStack migrator) and not for Next.js sites (use deco-next-package-migration). |
@decocms 6.x → 7.x Split-Package Upgrade (TanStack sites)
Moves a TanStack Start site off the two monoliths onto the split packages:
@decocms/start@6.x → @decocms/blocks (framework core), @decocms/tanstack (TanStack binding), @decocms/blocks-admin (admin/preview), @decocms/blocks-cli (generators + migration tooling, devDep)
@decocms/apps@5.x → per-vendor @decocms/apps-* — add only the splits the site actually imports
Proven on three production storefronts: lebiscuit-tanstack (commits 73daa3b → b5fdf69 → 6d311d8, the canonical sequence), miess-01-tanstack (PR #86), granadobr-tanstack (PR #69). Target ^7.6.0 at minimum; ^7.7.0 removes two workarounds (noted below).
The commit sequence
Do this as three separate commits in this order — each leaves the repo in a reviewable, explainable state, and the typecheck-baseline diff (see Verification) is only meaningful per-step.
Commit 1 — dependency swap
Remove:
"@decocms/start": "6.x",
"@decocms/apps": "^5.x",
Add (@decocms/blocks-admin and @decocms/blocks-cli as devDependencies is fine; sites have shipped both ways — match how the site treats other build-time deps):
"@decocms/blocks": "^7.6.0",
"@decocms/tanstack": "^7.6.0",
"@decocms/blocks-admin": "^7.6.0",
"@decocms/blocks-cli": "^7.6.0",
plus only the @decocms/apps-* splits the site imports. Grep first:
grep -rhoE '@decocms/apps/[a-z-]+' src/ | sort -u
Mapping per commerce platform: @decocms/apps/vtex → @decocms/apps-vtex, and likewise apps-magento, apps-algolia, apps-salesforce, apps-shopify. Almost every site also needs @decocms/apps-commerce (shared types/sdk/utils) and @decocms/apps-website (Seo, analytics components). Real splits used: lebiscuit/miess = vtex + commerce + website; granadobr = vtex + magento + algolia + salesforce + commerce + website.
In the same commit, replace the site's @decocms/start/scripts/* generate:* chain with the ONE unified orchestrator (blocks-cli ships it as scripts/generate.ts; it runs blocks/manifest/sections/loaders/invoke/schema with an incremental content-hash cache, skipping generators whose inputs didn't change):
"generate": "tsx node_modules/@decocms/blocks-cli/scripts/generate.ts --site <site>",
Fold the site's per-generator flags into that single line — --exclude <keys> (loaders), --namespace/--skip-apps/--platform (schema), --registry (sections; auto-on for Next.js sites). Run --help (or --dry-run to see the per-site plan) for the full flag mapping. Update the build script to npm run generate && tsr generate && vite build.
The individual scripts (generate-blocks.ts, generate-sections.ts, generate-loaders.ts, generate-schema.ts, generate-invoke.ts, generate-blocks-manifest.ts) remain available and unchanged at node_modules/@decocms/blocks-cli/scripts/ (literal paths — they are no longer exports-map subpaths) for one-off runs or --out-file-style overrides the orchestrator doesn't re-expose.
Commit .deco/generate.digests.json alongside the regenerated artifacts — it records content hashes of each generator's inputs, so fresh clones (and CI) cache-hit instead of re-running every generator on first boot. .deco/.cache/ stays out of git (the orchestrator drops its own .gitignore there).
(generate:routes stays tsr generate — that's TanStack's, not ours.)
Also update vite.config.ts resolve.dedupe: replace ["@decocms/start", "@decocms/apps"] with the full list of split package names the site now depends on.
Delete any committed package-lock.json. These sites are bun-first (bun.lock is the source of truth), but some carry a stale package-lock.json from an earlier npm era pinning @decocms/start@6.x / @decocms/apps@5.x. If the deploy pipeline (or a teammate) runs an npm-based install, npm ci resolves the old monolith from that lockfile and the site silently deploys on 6.x — the upgrade looks merged but never took effect. git rm package-lock.json; verify only bun.lock remains tracked (Verification gate 1 catches the monolith entries, but only in the lockfile it inspects). storefront-tanstack hit this — the migrated PR still had a 6.x package-lock.json.
Commit 2 — mechanical import rewrite
Pure codemod, no behavior change. The complete old→new subpath table is in references/import-mapping.md. The shape of it:
@decocms/start/sdk/<x> → @decocms/blocks/sdk/<x> (same subpath, ~15 modules: invoke, logger, clx, useScript, cacheHeaders, requestContext, …)
@decocms/start/cms → @decocms/blocks/cms (server) — but the client-safe registry accessors (getSection, getSectionRegistry) move to @decocms/blocks/cms/client
- setup split:
createSiteSetup from @decocms/blocks/setup (sections, blocks, productionOrigins, initPlatform, onResolveError) + createAdminSetup from @decocms/blocks-admin/setup (meta, css, fonts, previewWrapper). Drop any customMatchers: [registerBuiltinMatchers] passthrough — createSiteSetup registers builtin matchers unconditionally now.
- hooks barrel split:
RenderSection (and the framework-generic hooks) → @decocms/blocks/hooks; the TanStack-bound components (DecoPageRenderer, DecoRootLayout, SectionRenderer, PreviewProviders) → @decocms/tanstack root
- routes/router/workerEntry →
@decocms/tanstack root: cmsRouteConfig, cmsHomeRouteConfig, loadCmsPage, loadCmsHomePage, loadDeferredSection, the decoMetaRouteConfig/decoRenderRouteConfig/decoInvokeRouteConfig admin-route factories (7.10.0+ — the only admin-route exports; the old decoMetaRoute/decoRenderRoute/decoInvokeRoute literals were removed), withSiteGlobals, createDecoRouter, createDecoWorkerEntry; @decocms/start/vite → @decocms/tanstack/vite
- Dev-HMR footgun — admin route configs: while rewriting
src/routes/deco/{meta,render,invoke.$}.ts, emit the factory form: createFileRoute("/deco/meta")(decoMetaRouteConfig()). Never pass a shared config object by reference (the old createFileRoute("/deco/meta")(decoMetaRoute) pattern) — router-core's mutates the options object it receives (injects /), so any dev-HMR re-execution throws and 500s every route until restart. Historical: on ≤7.9.0 only the literals exist — spread them (); from 7.10.0 only the factories exist.
Verify with the typecheck-baseline diff (below) before committing.
Commit 3 — generated artifacts to .deco/
blocks-cli 7.x defaults generator output to .deco/ instead of src/server/{cms,admin}/:
git mv src/server/cms/blocks.gen.json .deco/blocks.gen.json (likewise blocks.gen.ts, loaders.gen.ts, src/server/admin/meta.gen.json → .deco/meta.gen.json); regenerate sections.gen.ts at .deco/sections.gen.ts
- Repoint the imports in
src/setup.ts (and any commerce-loaders wiring) at ../.deco/*
- Delete the emptied
src/server/cms/ + src/server/admin/ directories, and drop any stale knip ignore entries for them
src/server/invoke.gen.ts STAYS in src/ — generate-invoke's output default is intentionally unchanged. Its placement is empirically load-bearing: moved under .deco/, TanStack Start's server-function compiler generates client stubs fine but the server half 500s on every /_serverFn/ call. Do not "tidy" it into .deco/.
Commit 4 (or folded into 3) — bump, regenerate, verify
Bump all @decocms/* to the final target range, bun install, run bun run generate (a version bump busts the orchestrator's cache, so everything regenerates), and confirm the tree is clean afterwards (regeneration must be idempotent). Then run the Verification gates below.
Edge cases (all hit in real migrations)
autoconfigApps / aggregate APP_REGISTRY (sites NOT on the explicit COMMERCE_LOADERS-map pattern): some sites wire apps via autoconfigApps(blocks, APP_REGISTRY) instead of a hand-written loaders map. In 6.x the monolith exported ONE aggregate registry — import { APP_REGISTRY } from "@decocms/apps/registry" — covering every bundled app. That aggregate is gone in 7.x: each @decocms/apps-<vendor> exports its own single entry from its ./registry subpath (SHOPIFY_REGISTRY_ENTRY, VTEX_REGISTRY_ENTRY, …). Rebuild the array from every vendor whose resolveTypes appear in the decofile, not just the commerce platform: const APP_REGISTRY: AppRegistry = [SHOPIFY_REGISTRY_ENTRY, /* … */] (import each from @decocms/apps-<vendor>/registry; autoconfigApps/AppRegistry come from @decocms/blocks-admin/apps). Miss a vendor and its non-well-known app loaders dangle → sections render blank. Note what still resolves without any registry entry (and can mask a partial one): the WELL_KNOWN_TYPES (Lazy, Deferred, website/functions/requestToParam.ts, commerce/loaders/product/extensions/{details,listing}Page.ts, multivariate, Page) are native to @decocms/blocks; builtin matchers are registered by createSiteSetup; secret decrypt is native (sdk/crypto). So a Shopify PDP can look fine (its chain is all well-known + the shopify loader) while a decofile-referenced commerce/loaders/navbar.ts or a website/loaders/* app loader silently dangles. Grep the decofile for the app namespaces it actually uses and confirm each has a registry entry: grep -rhoE '"(shopify|vtex|commerce|website|algolia|magento|salesforce)/[^"]+"' .deco/blocks/ | sort -u. (Storefront-tanstack uses this pattern; its aggregate APP_REGISTRY collapsed to shopify-only during the swap — combine with the .ts-suffix gotcha below, since autoconfig registers only the extension-less manifest keys.) Worse — autoconfigApps can fail only in the production build: each registry entry's is a dynamic , which resolves under but , and autoconfig's swallows the error — so the app is skipped, its loaders dangle, and there is . Symptom: the PDP works under but renders "Page not found" on / the deployed worker. — . This is a nasty one to diagnose because dev, and even a direct , both resolve the product — only the build's deferred flow returns null. See the Verification note below on why alone is not a pass.
Verification gates
Run all of these; "it typechecks" alone is not a pass.
- Clean install:
bun install succeeds and the lockfile contains zero @decocms/start / @decocms/apps (monolith) entries.
- Regeneration idempotent: run
bun run generate --force (and bun run build), then git status — the tree must be clean. Diffs mean a generator default moved or an artifact wasn't relocated.
- Typecheck diff-vs-baseline, not absolute zero: capture
tsc --noEmit output on the pre-migration commit, again after each migration commit, and diff the error sets. Sites have pre-existing errors (granadobr's baseline was 258); the gate is zero new errors, not zero errors.
- Build passes:
bun run build (this is what catches server-only imports leaking into client bundles).
- Dev smoke:
bun run dev, then / (page renders with sections), /live/_meta (200, schema JSON), /.decofile (200, decofile JSON).
bun run dev is NOT sufficient — validate the PRODUCTION build + a client-deferred page. Some failures reproduce ONLY in the prod bundle (e.g. the autoconfigApps dynamic-import failure above), and commerce pages (PDP/PLP) are usually Lazy/deferred — resolved client-side via a /_serverFn/ call that a curl of the SSR HTML never triggers, and that server-side resolveValue in dev also won't expose. Run bun run preview (the built worker in workerd) and load a real PDP + PLP in a headless browser (Playwright, or the deco-e2e-testing skill), asserting the product renders (price, add-to-cart, image) — not just HTTP 200. When debugging, capture the deferred /_serverFn/ request (pagePath, stripped rawProps) and response (props.page null vs populated), and check x-cache (MISS = genuinely computed null, not stale cache).
- Parity diff of preview vs current deployment: the
/live/_meta schema must be structurally identical to production's, and the /.decofile diff must contain only drift explained by newer .deco/ sync commits — anything else is a migration regression.
Reference material
- references/import-mapping.md — the complete old→new import specifier table
- Site evidence: lebiscuit-tanstack
73daa3b/b5fdf69/6d311d8 (canonical sequence), miess-01-tanstack PR #86 (388305d neverDefer patch), granadobr-tanstack PR #69 (a32320a apps-dir workaround, generate-site-globals.ts)