@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 TanStack storefronts (one of them supplied the canonical three-commit sequence). 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: two of the reference sites = vtex + commerce + website; a third = 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 update() mutates the options object it receives (injects id/path), so any dev-HMR re-execution throws Route cannot have both an 'id' and a 'path' option and 500s every route until restart. Historical: on ≤7.9.0 only the literals exist — spread them (({ ...decoMetaRoute })); from 7.10.0 only the factories exist.
@decocms/apps/<vendor>/<x> → @decocms/apps-<vendor>/<x> (identical subpaths), except @decocms/apps/commerce/components/{Image,Picture} → @decocms/blocks/hooks
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 module is a dynamic () => import("./mod"), which resolves under vite dev but does not resolve in the vite/workerd production bundle, and autoconfig's catch {} swallows the error — so the app is skipped, all its loaders dangle, and there is no log. Symptom: the PDP works under bun run dev but renders "Page not found" on bun run preview / the deployed worker. Fix: pass the module statically — import * as shopifyMod from "@decocms/apps-shopify/mod"; const APP_REGISTRY = [{ ...SHOPIFY_REGISTRY_ENTRY, module: async () => shopifyMod }]. This is a nasty one to diagnose because dev, and even a direct resolveValue(loaderBlock, routeParams, ctx), both resolve the product — only the production build's deferred flow returns null. See the Verification note below on why bun run dev alone is not a pass.
useHydrated: @decocms/start/sdk/useHydrated was a one-line re-export. Import useHydrated from @tanstack/react-router directly — the split packages deliberately don't expose it.
cookiePassthrough: publicly exported since 7.6.0 at @decocms/tanstack/sdk/cookiePassthrough (getRequestCookieHeader, forwardResponseCookies) — use it, no shim. If you're studying a pre-7.6 migration for reference, you'll see local shims of it; beware the failure mode that pattern caused on one reference site: the shim imports @tanstack/react-start/server at module scope, and any client-bundled file importing it (section components, registry loaders) fails vite's import-protection at production build. Server-only helpers must only be imported from server-only modules; components should read request.headers.get("cookie") from the request their loader already receives.
deferredSectionLoader: publicly exported since 7.7.0 at @decocms/tanstack/sdk/deferredSectionLoader — pass it straight to <DecoPageRenderer loadDeferredSectionFn={...} />. On 7.6.x it was unreachable from any public subpath and every site carried a byte-identical local shim (src/sdk/deco/deferredSectionLoader.ts) wrapping the public loadDeferredSection; delete that shim when on ^7.7.0.
generate:invoke apps-dir (pre-7.7 only): the published @decocms/apps-vtex tarball nests sources under src/, and 7.6.x generate-invoke only probed the package root — so the default resolution failed on any npm-installed site and needed --apps-dir node_modules/@decocms/apps-vtex/src (hit on one reference site). Fixed in blocks-cli 7.7.0 (probes <pkg>/invoke.ts then <pkg>/src/invoke.ts); on ^7.7.0 drop the flag. Either way, consider keeping generate:invoke OUT of the build chain — invoke.gen.ts is committed, and regeneration is an explicit dev action.
neverDefer hand-patch (pre-7.7 only): 7.6.x generate-sections emitted neverDefer: true entries without declaring the field on the emitted SectionMetaEntry interface, so the generated file failed typecheck for any site with a export const neverDefer = true section (e.g. a Product/SearchResult.tsx) and needed a hand-patch that every regeneration wiped. Fixed in blocks-cli 7.7.0 — regenerate on ^7.7.0 and drop the patch.
- Site-local generators (the
scripts/generate-site-globals.ts pattern seen on one reference site — client-side site-globals snapshot to survive the vite plugin stubbing blocks.gen.ts to {} in the client bundle): repoint their INPUT at .deco/blocks.gen.json, relocate their OUTPUT under .deco/ (e.g. .deco/site-globals.gen.ts), and verify the framework vite plugin's client-stub suffix matching doesn't catch the new output name — the plugin stubs any id ending in blocks.gen.ts or meta.gen.{json,ts} out of the client bundle, so never name a site generator's output *blocks.gen.ts (site-globals.gen.ts is safe; the generated globals file must reach the client bundle intact, that's its entire purpose).
- Legacy
.ts/.tsx resolveType suffix on commerce loaders (PDP/PLP "finds no product"): Fresh/Deno-era decofiles reference app loaders WITH the file extension (.deco/blocks/*.json carrying "__resolveType": "shopify/loaders/ProductDetailsPage.ts"), but the split-package app manifests register them WITHOUT it (shopify/loaders/ProductDetailsPage). The resolver did an exact-key lookup, so the inner loader silently failed → the loader returns null → the PDP/PLP renders "no product" even though the identical content works on 6.x. The outer wrappers hide it: commerce/loaders/product/extensions/detailsPage.ts and website/functions/requestToParam.ts are WELL_KNOWN_TYPES (resolved natively, WITH the .ts), so only the innermost app-loader link breaks. Fixed in @decocms/blocks 7.11.2 — getCommerceLoader falls back to the extension-stripped key (an exact .ts registration still wins, so a site's wrapped-loader override is preserved). On <7.11.2, register the affected loaders under BOTH key forms in setup.ts ([KEY] + [`${KEY}.ts`] — the same double-registration a site's PLP wrapper already uses). Sites on the autoconfigApps(blocks, APP_REGISTRY) pattern are the most exposed — autoconfig registers only the extension-less manifest keys, so every .ts-suffixed decofile loader ref breaks (PDP, shelves, proxy), not just the one loader the site hand-wrapped. (This skill's reference sites use the explicit COMMERCE_LOADERS-map wiring + a 7.x-synced decofile, so they never surfaced it.) Diagnose by grepping the decofile: grep -rhoE '"(shopify|vtex|commerce|website)/[^"]+\.ts"' .deco/blocks/.
- Double-encoded page filenames in
.deco/blocks/: 6.x-era sync produced some filenames percent-encoded twice (multiple writers, different encodings). 7.x generate-blocks single-decodes the winning file's stem — matching the runtime's parseBlockId (one decodeURIComponent) — so keys self-heal on regeneration. If the generator warns about key collisions, delete the stale duplicate files it lists under ignore:.
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 (one reference site'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: validated across three production TanStack storefronts — the canonical three-commit sequence, the
neverDefer patch, the apps-dir workaround, and the generate-site-globals.ts pattern each originated in that work
1---2name: decocms-v6-to-v7-upgrade3description: 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).4---56# @decocms 6.x → 7.x Split-Package Upgrade (TanStack sites)78Moves a TanStack Start site off the two monoliths onto the split packages:910- `@decocms/start@6.x` → `@decocms/blocks` (framework core), `@decocms/tanstack` (TanStack binding), `@decocms/blocks-admin` (admin/preview), `@decocms/blocks-cli` (generators + migration tooling, devDep)11- `@decocms/apps@5.x` → per-vendor `@decocms/apps-*` — add **only** the splits the site actually imports1213Proven on three production TanStack storefronts (one of them supplied the canonical three-commit sequence). Target `^7.6.0` at minimum; `^7.7.0` removes two workarounds (noted below).1415## The commit sequence1617Do 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.1819### Commit 1 — dependency swap2021Remove:2223```24"@decocms/start": "6.x",25"@decocms/apps": "^5.x",26```2728Add (`@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):2930```31"@decocms/blocks": "^7.6.0",32"@decocms/tanstack": "^7.6.0",33"@decocms/blocks-admin": "^7.6.0",34"@decocms/blocks-cli": "^7.6.0",35```3637plus only the `@decocms/apps-*` splits the site imports. Grep first:3839```bash40grep -rhoE '@decocms/apps/[a-z-]+' src/ | sort -u41```4243Mapping 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: two of the reference sites = vtex + commerce + website; a third = vtex + magento + algolia + salesforce + commerce + website.4445In 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):4647```48"generate": "tsx node_modules/@decocms/blocks-cli/scripts/generate.ts --site <site>",49```5051Fold 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`.5253The 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.5455**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).5657(`generate:routes` stays `tsr generate` — that's TanStack's, not ours.)5859Also update `vite.config.ts` `resolve.dedupe`: replace `["@decocms/start", "@decocms/apps"]` with the full list of split package names the site now depends on.6061**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`.6263### Commit 2 — mechanical import rewrite6465Pure codemod, no behavior change. The complete old→new subpath table is in [references/import-mapping.md](references/import-mapping.md). The shape of it:6667- `@decocms/start/sdk/<x>` → `@decocms/blocks/sdk/<x>` (same subpath, ~15 modules: invoke, logger, clx, useScript, cacheHeaders, requestContext, …)68- `@decocms/start/cms` → `@decocms/blocks/cms` (server) — but the client-safe registry accessors (`getSection`, `getSectionRegistry`) move to `@decocms/blocks/cms/client`69- **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.70- **hooks barrel split**: `RenderSection` (and the framework-generic hooks) → `@decocms/blocks/hooks`; the TanStack-bound components (`DecoPageRenderer`, `DecoRootLayout`, `SectionRenderer`, `PreviewProviders`) → `@decocms/tanstack` root71- **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`72 - **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 `update()` mutates the options object it receives (injects `id`/`path`), so any dev-HMR re-execution throws `Route cannot have both an 'id' and a 'path' option` and 500s every route until restart. Historical: on ≤7.9.0 only the literals exist — spread them (`({ ...decoMetaRoute })`); from 7.10.0 only the factories exist.73- `@decocms/apps/<vendor>/<x>` → `@decocms/apps-<vendor>/<x>` (identical subpaths), except `@decocms/apps/commerce/components/{Image,Picture}` → `@decocms/blocks/hooks`7475Verify with the typecheck-baseline diff (below) before committing.7677### Commit 3 — generated artifacts to `.deco/`7879blocks-cli 7.x defaults generator output to `.deco/` instead of `src/server/{cms,admin}/`:8081- `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`82- Repoint the imports in `src/setup.ts` (and any commerce-loaders wiring) at `../.deco/*`83- Delete the emptied `src/server/cms/` + `src/server/admin/` directories, and drop any stale knip ignore entries for them84- **`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/`.8586### Commit 4 (or folded into 3) — bump, regenerate, verify8788Bump 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.8990## Edge cases (all hit in real migrations)9192- **`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 `module` is a dynamic `() => import("./mod")`, which resolves under `vite dev` but **does not resolve in the vite/workerd production bundle**, and autoconfig's `catch {}` swallows the error — so the app is skipped, *all* its loaders dangle, and there is **no log**. Symptom: the PDP works under `bun run dev` but renders "Page not found" on `bun run preview` / the deployed worker. **Fix: pass the module statically** — `import * as shopifyMod from "@decocms/apps-shopify/mod"; const APP_REGISTRY = [{ ...SHOPIFY_REGISTRY_ENTRY, module: async () => shopifyMod }]`. This is a nasty one to diagnose because dev, and even a direct `resolveValue(loaderBlock, routeParams, ctx)`, both resolve the product — only the *production* build's deferred flow returns null. See the Verification note below on why `bun run dev` alone is not a pass.93- **`useHydrated`**: `@decocms/start/sdk/useHydrated` was a one-line re-export. Import `useHydrated` from `@tanstack/react-router` directly — the split packages deliberately don't expose it.94- **`cookiePassthrough`**: publicly exported since 7.6.0 at `@decocms/tanstack/sdk/cookiePassthrough` (`getRequestCookieHeader`, `forwardResponseCookies`) — use it, no shim. If you're studying a pre-7.6 migration for reference, you'll see local shims of it; beware the failure mode that pattern caused on one reference site: the shim imports `@tanstack/react-start/server` at module scope, and any *client-bundled* file importing it (section components, registry loaders) fails vite's import-protection at production build. Server-only helpers must only be imported from server-only modules; components should read `request.headers.get("cookie")` from the request their loader already receives.95- **`deferredSectionLoader`**: publicly exported since 7.7.0 at `@decocms/tanstack/sdk/deferredSectionLoader` — pass it straight to `<DecoPageRenderer loadDeferredSectionFn={...} />`. On 7.6.x it was unreachable from any public subpath and every site carried a byte-identical local shim (`src/sdk/deco/deferredSectionLoader.ts`) wrapping the public `loadDeferredSection`; delete that shim when on ^7.7.0.96- **`generate:invoke` apps-dir (pre-7.7 only)**: the published `@decocms/apps-vtex` tarball nests sources under `src/`, and 7.6.x generate-invoke only probed the package root — so the default resolution failed on any npm-installed site and needed `--apps-dir node_modules/@decocms/apps-vtex/src` (hit on one reference site). Fixed in blocks-cli 7.7.0 (probes `<pkg>/invoke.ts` then `<pkg>/src/invoke.ts`); on ^7.7.0 drop the flag. Either way, consider keeping `generate:invoke` OUT of the build chain — `invoke.gen.ts` is committed, and regeneration is an explicit dev action.97- **`neverDefer` hand-patch (pre-7.7 only)**: 7.6.x generate-sections emitted `neverDefer: true` entries without declaring the field on the emitted `SectionMetaEntry` interface, so the generated file failed typecheck for any site with a `export const neverDefer = true` section (e.g. a `Product/SearchResult.tsx`) and needed a hand-patch that every regeneration wiped. Fixed in blocks-cli 7.7.0 — regenerate on ^7.7.0 and drop the patch.98- **Site-local generators** (the `scripts/generate-site-globals.ts` pattern seen on one reference site — client-side site-globals snapshot to survive the vite plugin stubbing `blocks.gen.ts` to `{}` in the client bundle): repoint their INPUT at `.deco/blocks.gen.json`, relocate their OUTPUT under `.deco/` (e.g. `.deco/site-globals.gen.ts`), and verify the framework vite plugin's client-stub suffix matching doesn't catch the new output name — the plugin stubs any id ending in `blocks.gen.ts` or `meta.gen.{json,ts}` out of the client bundle, so never name a site generator's output `*blocks.gen.ts` (`site-globals.gen.ts` is safe; the generated globals file must reach the client bundle intact, that's its entire purpose).99- **Legacy `.ts`/`.tsx` resolveType suffix on commerce loaders (PDP/PLP "finds no product")**: Fresh/Deno-era decofiles reference app loaders WITH the file extension (`.deco/blocks/*.json` carrying `"__resolveType": "shopify/loaders/ProductDetailsPage.ts"`), but the split-package app manifests register them WITHOUT it (`shopify/loaders/ProductDetailsPage`). The resolver did an exact-key lookup, so the inner loader silently failed → the loader returns `null` → the PDP/PLP renders "no product" even though the identical content works on 6.x. The outer wrappers hide it: `commerce/loaders/product/extensions/detailsPage.ts` and `website/functions/requestToParam.ts` are `WELL_KNOWN_TYPES` (resolved natively, WITH the `.ts`), so only the innermost app-loader link breaks. **Fixed in `@decocms/blocks` 7.11.2** — `getCommerceLoader` falls back to the extension-stripped key (an exact `.ts` registration still wins, so a site's wrapped-loader override is preserved). On `<7.11.2`, register the affected loaders under BOTH key forms in `setup.ts` (`[KEY]` + `` [`${KEY}.ts`] `` — the same double-registration a site's PLP wrapper already uses). **Sites on the `autoconfigApps(blocks, APP_REGISTRY)` pattern are the most exposed** — autoconfig registers only the extension-less manifest keys, so *every* `.ts`-suffixed decofile loader ref breaks (PDP, shelves, proxy), not just the one loader the site hand-wrapped. (This skill's reference sites use the explicit `COMMERCE_LOADERS`-map wiring + a 7.x-synced decofile, so they never surfaced it.) Diagnose by grepping the decofile: `grep -rhoE '"(shopify|vtex|commerce|website)/[^"]+\.ts"' .deco/blocks/`.100- **Double-encoded page filenames** in `.deco/blocks/`: 6.x-era sync produced some filenames percent-encoded twice (multiple writers, different encodings). 7.x generate-blocks single-decodes the winning file's stem — matching the runtime's `parseBlockId` (one `decodeURIComponent`) — so keys self-heal on regeneration. If the generator warns about key collisions, delete the stale duplicate files it lists under `ignore:`.101102## Verification gates103104Run all of these; "it typechecks" alone is not a pass.1051061. **Clean install**: `bun install` succeeds and the lockfile contains zero `@decocms/start` / `@decocms/apps` (monolith) entries.1072. **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.1083. **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 (one reference site's baseline was 258); the gate is **zero new errors**, not zero errors.1094. **Build passes**: `bun run build` (this is what catches server-only imports leaking into client bundles).1105. **Dev smoke**: `bun run dev`, then `/` (page renders with sections), `/live/_meta` (200, schema JSON), `/.decofile` (200, decofile JSON).1116. **`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).1127. **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.113114## Reference material115116- [references/import-mapping.md](references/import-mapping.md) — the complete old→new import specifier table117- Site evidence: validated across three production TanStack storefronts — the canonical three-commit sequence, the `neverDefer` patch, the apps-dir workaround, and the `generate-site-globals.ts` pattern each originated in that work