feat(migrator): v7 helpers from @decocms/compat-v7; matchers, section loaders, views and image URLs wired
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,detectDeviceand 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,DeviceProviderandDeviceContextmove to/tanstackor/nextjs, by binding. The report says how to seed the provider withrequestDevice(). - 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({ … })becomesexport const sectionLoaders, andregisterNonCriticalSections([…])becomesexport const nonCriticalSections..deco/index.tsexportsloadSections(sections, request, url?)overrunSectionLoaders. It usesnested: "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,loadSectionsexists 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'sctxis 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 generatedmatchPage(href)tells the matchers which page is opening during a client-side navigation. environmentis reported.- Each saved random matcher gets its saved name as
cohort(the key v7's sticky random used indeco_segment), only on sites where the matcher is registered, sodeco checkstays green elsewhere. - The report says to call
commitSegmentCookie()and passcohortSegmentto the worker on TanStack, orpageUrlRequestHeadersand<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 againstdetectDevice, as v7 did.- The section-loader conversion; an unregistered loader wired, listed, and overridden by the registered map (run end to end);
loadSectionswith no registration. - Matcher registration on TanStack, and what a site without a binding gets.
cohort,views.ts, andWrap(inline, added, unreadable,.ts).
- Commerce mapping, with the entry list pinned to compat-v7's
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 registeredwithSectionLoader). Then it type-checks everysrc/file plus.deco/index.tsandsrc/views.tsagainst the repo's packages.migrate.test.ts: imports the migrated block map and runsloadSectionson the migrated page. The result: v7'scompose(withDevice(), withSearchParam())output, and actx.deviceloader throughwithSectionLoader.imageCopy.test.ts: for each 7.xImageexcerpt,releaseRules(version)gives the samedecoCdnoptions as the excerpt's own code, under five registered domain/quality pairs.src/imageUrl.tsis 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.tsequals the hand-migrated site's.
Gates
| Gate | Result |
|---|---|
bun run check | pass |
bun run build | pass |
| Skill tests | 151 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:check | pass |
| Plain-Node import of every built export | 64/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
onBeforeResolvePropssection convention. - The docs site's
/nextpages still say to copy these helpers into the site. A docs PR should follow.
Main refresh (2026-10-17, ceba1386)
- Merged C4
18e9a8bbinto 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:checkpass.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.devicetodetectDevice(req.headers.get("user-agent") ?? ""); any otherctx.*read is reported since it throws in v8. - Converts
registerSectionLoadersandregisterNonCriticalSectionsinto exports that.deco/index.tsruns vialoadSections, along with every section's ownloader(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
cohortsodeco_segmentgroups 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.tsmaps them (an existing one is kept). - On TanStack, writes
src/imageUrl.tswith v7's URLs and putsImageUrlProviderintocreateDecoRouter'sWrap; unwritable cases are reported.src/imageUrl.tsnever setsstripAssetHosts; sites whose v7Imagestripped asset hosts get a comment naming the difference and a report line counting the URLs changed. - Types the page's
seoas the block map'sPageSeo, v7's shape, so existing SEO blocks register and passdeco check; the SEO hint references that same type. - Reports a v7
withABTestingimport withabTestingRoutes(@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
onBeforeResolvePropsconvention 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.