feat: v8 framework packages — @decocms/tanstack, @decocms/nextjs, @decocms/compat-v7, /preview, lockstep release and the migrator

Merged
TL
4h ago
+41651-369

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's LiveControls as one static inline script, so it runs before hydration and a Server Component can render it. trustedOrigins is required; it answers only the parent frame from those origins.
  • @decocms/blocks exports 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 __cseg server-function cache check, Set-Cookie dedup, immutable assets. onCache is 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, StudioBridge with Studio's origins), /config (withDeco), /image-loader (createDecoImageLoader, platform-neutral).
  • PO decision 1: isNextDev is gone; createCMS reads NODE_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.json publishes every package in packages/ at one version on latest from main, blocks first, then the bindings.
  • Every package that builds on blocks takes it as a caret peer (now ^8.4.0) with workspace:* only in devDependencies, so a site installs one blocks copy.
  • scripts/sync-versions.mjs writes the version and refuses a plain in-repo dependency or a peer floor above the release. tests/release.test.ts checks 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-migration skill move sites onto @decocms/tanstack / @decocms/nextjs instead of copying v7's worker, draft, bridge and image code.
  • @decocms/tanstack/react gets v7's Image, Picture, Source (same props and markup, URLs from ImageUrlProvider); 160 golden markup cases, zero changed.
  • PO decisions 1, 2 and 5: no dev flag; the script adds deco schema --check to the site's CI; decoCdn strips 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. random is latest 7.x's sticky random on the deco_segment cookie (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, or nested: "direct" for 7.77.12 and earlier), __pageUrl/__pagePath, and the degraded list. v7's mixins. Reading ctx throws with what to use instead.

#705 — @decocms/compat-v7 /commerce, /tanstack, /nextjs

  • /commerce/*: v7's @decocms/apps-commerce pure files under the same layout (AST-equal to v7, 5,000 generated inputs each).
  • /tanstack and /nextjs: matcherContext, commitSegmentCookie, cohortSegment, requestDevice, DeviceProvider.
  • /tanstack/worker (PO decision 6): v7's withABTesting legacy-origin split as a createDecoWorker routes handler, 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-commerce imports onto compat-v7, rewrites ctx.device, turns registerSectionLoaders into sectionLoaders and wires loadSections, registers the matchers the content uses and writes the image URLs module.
  • PO decision 8: every section's own loader runs, 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/invoke key becomes one server function (TanStack Start GET loaders marked private and POST actions; Next.js "use server"), imported at each call site.
  • PO decision 7: VTEX invoke-map keys need an explicit --vtex-invoke keep|v8 choice, no default.
  • deco-vtex/-shopify/-wake/-resend app blocks become src/clients/<app>.ts; src/migration-stubs.ts holds 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 latest for 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/blocks shows one 8.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), and bun run test under a 2 GB cap: 122 files pass, 1 skipped; 2,746 tests pass, 95 skipped, 0 fail.

Landing

  1. 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: one feat:, a minor release.
  2. The release publishes 8.4.0 on latest for all 11 packages.
  3. Before landing, check that npm Trusted Publishing for @decocms/tanstack, @decocms/nextjs and @decocms/compat-v7 accepts main'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, so main should be accepted, but if either is tied to a GitHub environment, add it for main first. compat-v7's trust config already exists and its 0.0.0 placeholder 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/invoke calls 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.0 caret peer, so a site installs one blocks copy.
  • Existing v7 sites stay on latest-7; next dist-tags and misplaced 8.0.0/prerelease deprecations must be cleaned up after release (commands in the PR body).