feat(migrator): v7 helpers from @decocms/compat-v7; matchers, section loaders, views and image URLs wired

TL
3h ago
+2412-267

The v7-to-v8 migration script now uses @decocms/compat-v7. Before this change, its report told sites to copy v7's helpers into their own code; now the script rewrites the imports and generates the wiring. It's stacked on #705.

What the script does now

Imports onto @decocms/compat-v7 (scripts/compat.ts, merged into MOVES):

  • Commerce: @decocms/apps-commerce/<subpath> becomes @decocms/compat-v7/commerce/<subpath>: the same files, in any import form, including inside vendored loaders. A stray 6.x @decocms/apps/commerce/<subpath> gets the same rewrite. v7's app runtime (app-types, registry, resolve, manifest-utils) is reported, not moved.
  • Helpers: clx, useScript/useScriptAsDataURI/inlineScript, detectDevice and its siblings, SectionProps/Resolved/LoadingFallbackProps/Flag, the widget types, and the mixins (compose, withDevice, withMobile, withSearchParam, withSectionLoader) each move to their compat subpath. A declaration that mixes moved and unmoved names is split.
  • Device context: useDevice, DeviceProvider and DeviceContext move to /tanstack or /nextjs, by binding. The report says how to seed the provider with requestDevice().
  • Names with no port (AppContext, useSection, registerLayoutSections, …) are reported with what to do instead.

ctx.device: in a v7 loader (props, req, ctx) it becomes detectDevice(req.headers.get("user-agent") ?? ""). That is what v7's buildSectionLoaderContext computed. Any other ctx.* read is reported: it throws in v8, and its section keeps its saved props.

Section loaders (scripts/sectionLoaders.ts):

  • registerSectionLoaders({ … }) becomes export const sectionLoaders, and registerNonCriticalSections([…]) becomes export const nonCriticalSections.
  • .deco/index.ts exports loadSections(sections, request, url?) over runSectionLoaders. It uses nested: "direct" for sites on 7.77.12 or earlier, and the default deep walk otherwise.
  • v7 ran only the loaders a site registered. The block map now also runs every other section's own loader (PO decision 8, 2026-10-15): the registered map wins where it lists a section, loadSections exists even when the site registered nothing, and each loader v7 never ran is reported by name as an approved difference to list in the PR. A loader typed with v7's ctx is wrapped, since the runner calls (props, request).

Matchers (scripts/matchers.ts):

  • On a TanStack Start or Next.js site, each v7 matcher the content uses is registered under its v7 name from @decocms/compat-v7/matchers: device, random, cookie, host, pathname, queryString, location and multi.
  • The request comes from the binding's matcherContext(). On TanStack, the generated matchPage(href) tells the matchers which page is opening during a client-side navigation.
  • environment is reported.
  • Each saved random matcher gets its saved name as cohort (the key v7's sticky random used in deco_segment), only on sites where the matcher is registered, so deco check stays green elsewhere.
  • The report says to call commitSegmentCookie() and pass cohortSegment to the worker on TanStack, or pageUrlRequestHeaders and <SegmentCookie /> on Next.js.

Block names: each block is registered under its v7 names only, with no short name, so the site editor's pickers list each block once (PO decision 9).

Views: a descriptor's component is now the section's v7 type name (site/sections/….tsx), the key v7's loader map uses. src/views.ts maps each name to its module. An existing src/views.ts is kept.

Images: on TanStack, the script writes src/imageUrl.ts with v7's URLs. These come from the release's rules, or from the site's own copy of v7's image helper, plus the domain and quality it registers. The script also puts ImageUrlProvider into createDecoRouter's Wrap. It reports the cases it can't handle: a non-literal registered value, an unreadable copy, a Wrap it can't read, or a .ts router. src/imageUrl.ts never sets stripAssetHosts (decoCdn's "leading" default, PO decision 5); for a site whose v7 Image stripped the asset hosts anywhere, the file's comment names the approved difference and the report's "image URLs" line counts the URLs it changes.

A/B split: a v7 withABTesting import is reported with abTestingRoutes from @decocms/compat-v7/tanstack/worker. It isn't rewritten, because a wrapper becomes a routes handler.

From the lower branches: deco schema --check in the site's CI, and no dev/isNextDev (decisions 1 and 2, from #700).

Docs: SKILL.md, import-map.md, script-steps.md, manual-steps.md, framework-code.md and CLAUDE.md describe the new behaviour. knip.json lets the skill's tests import compat-v7, as it already does for the other packages.

Tests

  • compat.test.ts (new): 21 tests, at least one per codemod.
    • Commerce mapping, with the entry list pinned to compat-v7's exports.
    • Name moves and splits, and the device context by binding.
    • ctx.device: the rewrite, aliasing, what is left alone and what is reported. A check runs the generated expression against detectDevice, as v7 did.
    • The section-loader conversion; an unregistered loader wired, listed, and overridden by the registered map (run end to end); loadSections with no registration.
    • Matcher registration on TanStack, and what a site without a binding gets.
    • cohort, views.ts, and Wrap (inline, added, unreadable, .ts).
  • bindings.test.ts: migrates a v7 TanStack Start fixture and a v7 Next.js one (an A/B test, a device rule, every helper, a registered withSectionLoader). Then it type-checks every src/ file plus .deco/index.ts and src/views.ts against the repo's packages.
  • migrate.test.ts: imports the migrated block map and runs loadSections on the migrated page. The result: v7's compose(withDevice(), withSearchParam()) output, and a ctx.device loader through withSectionLoader.
  • imageCopy.test.ts: for each 7.x Image excerpt, releaseRules(version) gives the same decoCdn options as the excerpt's own code, under five registered domain/quality pairs. src/imageUrl.ts is checked from a copy and from a release.
  • Dry runs: on throwaway copies of three v7 storefronts, with nothing committed. The script ran cleanly on each. On the demo, the generated src/imageUrl.ts equals the hand-migrated site's.

Gates

GateResult
bun run checkpass
bun run buildpass
Skill tests151 pass
bun run test (2 GB cap, at #707's head)2,723 passed, 2 failed, 95 skipped. DS-2 also fails on main (fix in its own PR); the schema.test.ts timeout passes alone (68/69 with DS-2)
skills:check and skills:readme:checkpass
Plain-Node import of every built export64/64 exports (compat-v7 27/27); deco --help lists --check

Not in this PR

  • A scaffold step (the page loader that calls matchPage, loadSections, commitSegmentCookie): the report says what to call where.
  • An invoke codemod.
  • v7's onBeforeResolveProps section convention.
  • The docs site's /next pages still say to copy these helpers into the site. A docs PR should follow.

Main refresh (2026-10-17, ceba1386)

  • Merged C4 18e9a8bb into this branch (normal merge, no conflicts), then merged up the stack to M2 #707.
  • Gates at the top of the stack (M2 5131a3c1): bun run check, bun run build, skills:check, skills:readme:check pass. bun run test (2 GB memory cap): 122 files pass, 1 skipped; 2,745 tests pass, 95 skipped, 0 fail. DS-2 now passes (fixed on main by #709); no timeouts this run.
  • CI on this PR's new head: all checks pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu


Summary by cubic

The v7-to-v8 migration script now rewrites v7 helpers, fixtures, and wiring onto @decocms/compat-v7 instead of telling sites to copy them in.

  • Rewrites commerce and helper imports (clx, useScript, detectDevice, widget types, section-loader mixins) to @decocms/compat-v7; names with no port are reported instead.
  • Rewrites a v7 loader's ctx.device to detectDevice(req.headers.get("user-agent") ?? ""); any other ctx.* read is reported since it throws in v8.
  • Converts registerSectionLoaders and registerNonCriticalSections into exports that .deco/index.ts runs via loadSections, along with every section's own loader (v7 only ran registered ones). The registered map wins where it lists a section; each loader v7 never ran is reported as an approved difference.
  • Registers the v7 matchers the content uses on TanStack and Next.js sites, keeping each saved random matcher's cohort so deco_segment groups carry over.
  • Registers every block under its v7 names only, no short names, so the editor lists each once; each section's descriptor component is its v7 type name and src/views.ts maps them (an existing one is kept).
  • On TanStack, writes src/imageUrl.ts with v7's URLs and puts ImageUrlProvider into createDecoRouter's Wrap; unwritable cases are reported. src/imageUrl.ts never sets stripAssetHosts; sites whose v7 Image stripped asset hosts get a comment naming the difference and a report line counting the URLs changed.
  • Types the page's seo as the block map's PageSeo, v7's shape, so existing SEO blocks register and pass deco check; the SEO hint references that same type.
  • Reports a v7 withABTesting import with abTestingRoutes (@decocms/compat-v7/tanstack/worker) instead of rewriting it, since a wrapper becomes a routes handler.
  • A scaffold step, an invoke codemod, and v7's onBeforeResolveProps convention are still left to the report.

New compat.test.ts covers each codemod, with the entry list pinned to compat-v7's exports. bindings.test.ts migrates and type-checks both a TanStack and a Next.js fixture; migrate.test.ts runs the migrated loadSections end to end. The image release rules for src/imageUrl.ts are checked against each 7.x Image excerpt in imageCopy.test.ts.