feat: v8 framework packages — @decocms/tanstack, @decocms/nextjs, @decocms/compat-v7, /preview, lockstep release and the migrator
This PR replaces the 11 stacked PRs #695 … #707 so the rest of the v8 framework packages land on main as one commit and one release, 8.4.0, publishing all 11 packages together: @decocms/blocks, @decocms/tanstack, @decocms/nextjs, @decocms/compat-v7 and the seven @decocms/apps-*. Its branch is #707's (feat/migrator-invoke-scaffold), already rebased onto main with one commit per former PR and their trees unchanged, plus one commit that raises every @decocms/blocks peer floor to ^8.4.0.
#693 and #694 are already on main and shipped in 8.3.0 (NODE_ENV dev default, deco schema --check, first-match multivariate, the platform-neutral /image with @decocms/apps-vtex/image and @decocms/apps-shopify/image).
What lands
#695 — @decocms/blocks/preview: the draft badge and the Studio bridge
DraftBadge({ pointer, exitHref? }): v7's "Preview mode" pill, unchanged on screen. Renders nothing without a pointer, on the server and inside a frame. The inlined Deco mark is now a 36px PNG (~2KB, v7 inlined ~22KB).StudioBridge({ site, trustedOrigins, editShortcut?, nonce? }): v7'sLiveControlsas one static inline script, so it runs before hydration and a Server Component can render it.trustedOriginsis required; it answers only the parent frame from those origins.@decocms/blocksexports no Studio origins and no draft names: the bindings own them.- PO decision 4: the "." / Ctrl+Shift+E edit shortcut is on by default, as in v7;
editShortcut={false}turns it off.
#696 — @decocms/tanstack for v8
- For TanStack Start on Cloudflare Workers, depending only on
@decocms/blocks. Subpaths: root (draft names,draftResponseHeaders,STUDIO_ORIGINS),/worker(createDecoWorker),/server,/start,/router,/react(DecoDocument,NavigationProgress,StableOutlet),/vite(decoTanstack),/scope. createDecoWorker: Studio framing, v7's draft host allowlist (D1), CMS redirects, the edge cache (build + revision + device + segment key, SWR/SIE, never storing private, signed-in or degraded responses), the__csegserver-function cache check, Set-Cookie dedup, immutable assets.onCacheis the only metrics hook (D3); build hash from CI's sha (D4).- Owns the draft names, headers and Studio origins that #693 had put in blocks (PO review of #693); values unchanged.
#697 — @decocms/nextjs for v8
- Helpers for any Next.js App Router app, no hosting assumptions, depending only on
@decocms/blocks:/proxy(decideDraft,draftProxy, …),/server(bindDraft,contentClient), root (DraftPreviewIndicator,DraftBadge,StudioBridgewith Studio's origins),/config(withDeco),/image-loader(createDecoImageLoader, platform-neutral). - PO decision 1:
isNextDevis gone;createCMSreadsNODE_ENV. PO decision 4: the edit shortcut is on by default. Its own copy of the draft names and Studio origins (PO review of #693). Image golden test: 1,212 cases, only the 60 of PO-approved fix 2 (#694) change.
#699 — lockstep release
.releaserc.jsonpublishes every package inpackages/at one version onlatestfrommain, blocks first, then the bindings.- Every package that builds on blocks takes it as a caret peer (now
^8.4.0) withworkspace:*only indevDependencies, so a site installs one blocks copy. scripts/sync-versions.mjswrites the version and refuses a plain in-repo dependency or a peer floor above the release.tests/release.test.tschecks the allowlist, the ranges, every export's shape and what each tarball ships; CI runs it on the built dist.
#700 — the migrator and skill use the bindings
- The v7-to-v8 script and the
deco-v7-to-v8-migrationskill move sites onto@decocms/tanstack/@decocms/nextjsinstead of copying v7's worker, draft, bridge and image code. @decocms/tanstack/reactgets v7'sImage,Picture,Source(same props and markup, URLs fromImageUrlProvider); 160 golden markup cases, zero changed.- PO decisions 1, 2 and 5: no dev flag; the script adds
deco schema --checkto the site's CI;decoCdnstrips only a leading asset host and the report lists every URL that changes.
#702 — @decocms/compat-v7: types, device, scripts, clx
- The PO-approved home (2026-10-14) for v7's pure helpers on v8, so migrated sites stop keeping copies. No v7 runtime (
ctx, invoke, request scope), no env reads, nothing added to blocks. /types(SectionProps= the loader's awaited return, per the PO),/device,/script,/clx. v7's own files are hash-pinned fixtures and every helper is compared against them over seeded inputs.
#703 — @decocms/compat-v7/matchers
- v7's website matchers with v7's results; each takes the request as its second argument.
randomis latest 7.x's sticky random on thedeco_segmentcookie (PO decision), keyed by the saved block's name (cohort).
#704 — @decocms/compat-v7/sections
runSectionLoaders/runSectionLoader: v7's(props, req)section loaders after a page resolves, with v7's nested walk (deep, ornested: "direct"for 7.77.12 and earlier),__pageUrl/__pagePath, and thedegradedlist. v7's mixins. Readingctxthrows with what to use instead.
#705 — @decocms/compat-v7 /commerce, /tanstack, /nextjs
/commerce/*: v7's@decocms/apps-commercepure files under the same layout (AST-equal to v7, 5,000 generated inputs each)./tanstackand/nextjs:matcherContext,commitSegmentCookie,cohortSegment,requestDevice,DeviceProvider./tanstack/worker(PO decision 6): v7'swithABTestinglegacy-origin split as acreateDecoWorkerrouteshandler, matched to v7 over 3,000 seeded requests. PO decision 3 checked against the compat random.
#706 — the migrator imports @decocms/compat-v7
- Rewrites v7 helper and
@decocms/apps-commerceimports onto compat-v7, rewritesctx.device, turnsregisterSectionLoadersintosectionLoadersand wiresloadSections, registers the matchers the content uses and writes the image URLs module. - PO decision 8: every section's own
loaderruns, and each one v7 never ran is reported. PO decision 9: blocks are registered under their v7 names only.
#707 — the migrator writes invoke, app clients, the page setup and stubs
- Each
/deco/invokekey becomes one server function (TanStack StartGETloaders marked private andPOSTactions; Next.js"use server"), imported at each call site. - PO decision 7: VTEX invoke-map keys need an explicit
--vtex-invoke keep|v8choice, no default. deco-vtex/-shopify/-wake/-resendapp blocks becomesrc/clients/<app>.ts;src/migration-stubs.tsholds deprecated stubs for v7 runtime with no port; the page setup (src/cms.ts,open-page, the catch-all route) is written per binding.
This PR's own commit — peer floors ^8.4.0
The bindings and compat-v7 use blocks APIs that 8.3.0 doesn't have (@decocms/blocks/preview and the rest of this stack), so every @decocms/blocks peer (and compat-v7's optional binding peers) is ^8.4.0. tests/release.test.ts syncs at 8.4.0 and 8.5.1 and refuses 8.3.x; the migrator writes ^8.4.0; CLAUDE.md, the README and the skill name 8.4 as the floor.
Checks
- Release dry run in a throwaway clone of a mirror with the tags (deleted afterwards; nothing pushed to GitHub or published):
main+ this branch squashed into one commit with this PR's title.semantic-release --dry-run: "Found git tag blocks-v8.3.0 … The next release version is 8.4.0". The prepare step (VERSION=8.4.0 node scripts/sync-versions.mjs && bun run build): 11 manifests at 8.4.0, every peer^8.4.0, all builds exit 0.npm publish --dry-run --tag latestfor all 11:@decocms/blocks@8.4.0…@decocms/apps-wake@8.4.0, each "with tag latest". The 11 tarballs installed together:npm ls @decocms/blocksshows one8.4.0, every other entry deduped; no required peer is unmet. - Gates at the top:
bun run check,bun run build,skills:check,skills:readme:check,bun test tests/release.test.ts(42 pass), the compat-v7 tests (13 files, 153 pass), andbun run testunder a 2 GB cap: 122 files pass, 1 skipped; 2,746 tests pass, 95 skipped, 0 fail.
Landing
- Approve, then squash with this PR's
feat:title. The squash message is the PR body (repo setting), so this body is what commit-analyzer reads: onefeat:, a minor release. - The release publishes 8.4.0 on
latestfor all 11 packages. - Before landing, check that npm Trusted Publishing for
@decocms/tanstack,@decocms/nextjsand@decocms/compat-v7acceptsmain's.github/workflows/release.yml. The bindings were last published from the 7.x branch: npm's trusted publisher names the repo and workflow file, not a branch, somainshould be accepted, but if either is tied to a GitHub environment, add it formainfirst. compat-v7's trust config already exists and its0.0.0placeholder is published.
Once this lands, the 11 stacked PRs can be closed.
npm commands for the PO after the release (not run)
# 1. Remove the stale `next` dist-tag (8.1.0) from blocks and every apps-* that has it (all seven do today).
npm dist-tag rm @decocms/blocks next
npm dist-tag rm @decocms/apps-algolia next
npm dist-tag rm @decocms/apps-magento next
npm dist-tag rm @decocms/apps-resend next
npm dist-tag rm @decocms/apps-sfmc-personalization next
npm dist-tag rm @decocms/apps-shopify next
npm dist-tag rm @decocms/apps-vtex next
npm dist-tag rm @decocms/apps-wake next
# 2. Rewrite the 8.0.0 deprecation (it says "use @latest (7.x)", wrong once latest is 8.4.0).
npm deprecate @decocms/tanstack@8.0.0 "Published by mistake. Use ^8.4.0 for v8, or 7.x for v7."
npm deprecate @decocms/nextjs@8.0.0 "Published by mistake. Use ^8.4.0 for v8, or 7.x for v7."
# 3. Deprecate the two prereleases (not deprecated today).
npm deprecate @decocms/tanstack@"8.0.0-next.0 || 8.1.0-next.0" "Prerelease. Use ^8.4.0 for v8, or 7.x for v7."
npm deprecate @decocms/nextjs@"8.0.0-next.0 || 8.1.0-next.0" "Prerelease. Use ^8.4.0 for v8, or 7.x for v7."
No latest-7 command is needed: #698 already moved latest-7 to 7.80.2 for both bindings. latest moves to 8.4.0 by itself when the release publishes.
Checks afterwards:
npm view @decocms/blocks dist-tags # { latest: '8.4.0', 'latest-7': '7.80.2' }, no next
npm view @decocms/tanstack dist-tags # { latest: '8.4.0', 'latest-7': '7.80.2' }
npm view @decocms/nextjs dist-tags # { latest: '8.4.0', 'latest-7': '7.80.2' }
npm view @decocms/compat-v7 dist-tags # { latest: '8.4.0' }
for p in algolia magento resend sfmc-personalization shopify vtex wake; do npm view @decocms/apps-$p dist-tags; done # latest: '8.4.0', no next
npm view @decocms/tanstack@8.0.0 deprecated
npm view @decocms/nextjs@8.1.0-next.0 deprecated
npm view @decocms/tanstack@8.4.0 peerDependencies # @decocms/blocks: '^8.4.0'
🤖 Generated with Claude Code
https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
Summary by cubic
Replaces PRs #695–#707 so the v8 framework packages land as one commit and one release: 8.4.0 publishes all 11 packages together (@decocms/blocks, @decocms/tanstack, @decocms/nextjs, @decocms/compat-v7, and the seven @decocms/apps-*). The v7-to-v8 migrator and its skill now move sites onto these bindings instead of copying v7's worker, draft, bridge, and image code into the site.
What ships
@decocms/blocks/preview: the draft badge and Studio bridge, as one static inline script that runs before hydration.@decocms/tanstack: the TanStack Start binding on Cloudflare Workers — drafts, edge cache, CMS redirects, draft names and Studio origins.@decocms/nextjs: the Next.js App Router binding — draft proxy, content client, image loader, Studio bridge, without hosting assumptions.@decocms/compat-v7: v7's pure helpers (types, device, scripts, clx, matchers, section loaders, commerce files) with v7's output, so migrated sites stop keeping their own copies.- The migrator rewrites
/deco/invokecalls into server functions, writes the page setup per binding, registers v7 matchers and section loaders, and reports everything left for a person.
Release and migration
- All packages release in lockstep at one version on
latest; every package that builds on blocks takes it as a^8.4.0caret peer, so a site installs one blocks copy. - Existing v7 sites stay on
latest-7;nextdist-tags and misplaced8.0.0/prerelease deprecations must be cleaned up after release (commands in the PR body).