Skip to content

feat: the v7-to-v8 migrator and skill use @decocms/tanstack and @decocms/nextjs - #700

Closed
tlgimenes wants to merge 1 commit into
feat/release-8-3from
feat/migrator-uses-bindings
Closed

tlgimenes wants to merge 1 commit into
feat/release-8-3from
feat/migrator-uses-bindings

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #699 (P6). P7 of the v8 framework packages: the v7-to-v8 migration script and the deco-v7-to-v8-migration skill use @decocms/tanstack and @decocms/nextjs, so sites stop copying v7's worker, draft, Studio-bridge and image code into their own tree.

After the product owner's review (2026-10-14), this branch follows the stack below it: @decocms/blocks exports no draft names or Studio origins (they are in each binding's root), @decocms/blocks/image is platform-neutral, and VTEX's and Shopify's image URL helpers live in @decocms/apps-vtex/image and @decocms/apps-shopify/image.

@decocms/tanstack/react: Image, Picture, Source

  • They take v7's props and render v7's markup, so a call site only changes its import.
  • The URLs come from ImageUrlProvider (an ImageUrl the site builds with createImageUrl) instead of v7's registerImageCdnDomain/registerImageQuality module state. A site puts the provider around its pages with createDecoRouter's Wrap option.
  • No platform knowledge. @decocms/tanstack depends on no @decocms/apps-* package. A site that shows VTEX or Shopify images lists the platform's helper first:
    createImageUrl({ providers: [vtexCatalog(), shopifyCdn(), decoCdn({ sources: "all", ...byRelease })] })
    That gives v7's URLs byte for byte, except PO-approved fix 2 (feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694): an already-resized Deco CDN URL in content is re-resized from its src. byRelease depends on the @decocms/blocks 7.x release the site ran, because v7's Image changed across releases (e.g. stripAssetHosts: "leading" from 7.71.4); the skill has the table and the script reports the line for the site's version. Keep both helpers whatever the platform: v7 resized VTEX and Shopify images on every site. Without a provider, every source goes through the Deco image CDN (DECO_IMAGE_URL, which replaces the earlier V7_IMAGE_URL).
  • Golden markup: tests/tanstack-image-markup.golden.test.ts renders the 160 cases recorded from v7's own components (v7's defaults, and a site with another CDN host and quality) with the helpers supplied as a site would. Zero cases changed. It sits outside the package because the package can't depend on apps-*, not even for tests. With no provider, the non-VTEX/Shopify cases also match.
  • More golden markup (review pass): tests/__fixtures__/tanstack-v7-image-markup-nowidth-nuvemshop.jsonl, 90 more cases from v7's own components (7.x latest): Image/Source without a width (v7's width=NaN … NaNw candidates, which blocks' srcSet now reproduces, feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694) and Nuvemshop product images (v7 from 7.77.1 picks one of the CDN's fixed sizes). No @decocms/apps-* package covers Nuvemshop, so the test holds a store's own provider (the one framework-code.md gives) to v7's markup. Zero changed; dropping that provider, or the srcSet fix, makes it fail.
  • Differences from v7: no "Missing height" warning on every render, and PO-approved fix 2 (feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694, in decoCdn, not in the component): an already-resized Deco CDN URL that v7 turned into src=image is re-resized from its src. The component itself has no image-URL rule of its own.

The migration script

  • Imports (imports.ts):

    • It moves the v7 names the bindings keep: createDecoRouter and the search functions go to /router, NavigationProgress/StableOutlet to /react, decoServerFnFetch to /start, and on a TanStack site Image/Picture/Source to /react.
    • DRAFT_PARAM, DRAFT_COOKIE, DRAFT_HEADER, draftResponseHeaders, STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS and isStudioOrigin imported from @decocms/blocks move to the site's binding root (@decocms/tanstack or @decocms/nextjs). A site with neither binding gets a report line instead.
    • An import that mixes moved and other names is split.
    • Every other binding import is reported name by name with its replacement.
    • The image hints (registerImageCdnDomain, getOptimizedMediaUrl, …) and the Image note spell out v7's URLs as v8 code: createImageUrl and decoCdn from @decocms/blocks/image, vtexCatalog from @decocms/apps-vtex/image, shopifyCdn from @decocms/apps-shopify/image, with the decoCdn options of the site's 7.x release (siteBlocksVersion: the installed copy, else the package.json range). Rows: before 7.71.0 legacyHosts: [] with decoims.com as the default domain; 7.71.0 to 7.71.3 legacyHosts: []; 7.71.4 legacyHosts: ["decoims.com"]; 7.71.5 on nothing more, plus a Nuvemshop provider from 7.77.1. stripAssetHosts is left at decoCdn's default, "leading", on every row (see the PO decisions below). Each row was checked against that release's hooks/Image.tsx over every golden source, six widths, three heights, two fits and three CDN configs (Cloudflare, decoims.com + quality, an Azion host): no changed case.
    • Left alone as v8: the platform-neutral names of @decocms/blocks/image (now a list, not *, so a stale name is reported), @decocms/apps-vtex/image, @decocms/apps-shopify/image, the @decocms/tanstack root, and the @decocms/nextjs root with its draft names and Studio origins.
  • Settings (siteSettings.ts):

    • <site>.deco.site and <site>.deco-cx.workers.dev are no longer written into CMS.json, because the bindings allow them from their site option.
    • The report says which site to pass.
    • DECO_ALLOWED_PREVIEW_HOSTS maps to previewHosts.
  • Dependencies note: @decocms/blocks ^8.3.0 plus the matching binding ^8.3.0.

  • A site's own copy of v7's image helper (f80cf1a). A site that kept a copy of hooks/Image.tsx under src/ (a file that defines getOptimizedMediaUrl) serves the copy's URLs, not its release's. One storefront's copy was 7.20.7, edited to 7.77.1's CDN domain and decoims.com rule. The 7.20.7 row would have given it legacyHosts: [] on decoims.com. The script (scripts/imageCopy.ts) now reads these rules from the copy:

    • the initial domain and quality;
    • asset-host stripping (anywhere or leading);
    • the decoims.com rule (none, Cloudflare-only or always);
    • the VTEX, Shopify and Nuvemshop rules.

    It also reads the site's registerImageCdnDomain/registerImageQuality literals through the TypeScript AST, so a comment doesn't count. The copy gets its own report line with the createImageUrl call, and the Image note uses that line too. A copy whose rules the script can't read is reported for comparison by hand. Fuzz check: the options read from each 7.x release's Image.tsx (7.2.0, 7.51.0, 7.71.0, 7.71.4, 7.71.5, 7.77.1) and from both storefronts' pre-migration copies, each with four registered domains, give 0 differing URLs (1,944 cases each) besides fix 2's. On those two storefronts' trees, the script prints exactly the imageUrl they ship. The demo storefront's copy (7.64.3) and its release row agree.

The skill

  • framework-code.md covers the TanStack image setup with the platform helpers. It now says that options come from a site's own copy when it has one, and names fix 2 (an already-resized Deco CDN URL is re-resized from its src). For next/image it names fix 1 (via: "thumbor"). script-steps.md and import-map.md say the same.
  • framework-code.md and nextjs.md cover the Next.js loader: vtexCatalogWebp() + vtexFileManager({ via }) + decoCdn({ quality: "high", fit: "contain", layout: "quality-last" }). The earlier line about file-manager images moving to Thumbor is gone: URLs match the site's current loader.
  • framework-code.md also gives the draft names and Studio origins per binding.
  • import-map.md: moved-name rows, the left-alone list, and the image row.
  • Also updated: manual-steps.md, script-steps.md and SKILL.md (10,236 bytes, under the 10 KB budget).
  • Outside the skill: packages/tanstack/README.md, CLAUDE.md and knip.json.

PO decisions of 2026-10-15 (merged up from #693–#699, then here)

  • Image rule (decision 5). decoCdn strips only a leading asset host by default now (feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694), so the script writes no stripAssetHosts for any site. A site whose v7 code stripped the asset hosts anywhere (7.x before 7.71.4, or a copy of v7's image helper that did) moves to "leading" too, as approved, and never gets stripAssetHosts: "anywhere". The report's new "image URLs" line counts and lists the URLs in .deco/blocks and src/ whose image URL changes: a URL that merely contains an asset URL (https://other.host/https://assets.decocache.com/…). changedByLeadingStrip is tested against decoCdn itself (both settings, eight sources). On a fresh copy of the demo storefront (7.64.3), the line says no URL changes.
  • Dev mode (decision 1). The skill no longer tells Next.js sites to pass dev: isNextDev(...): createCMS reads NODE_ENV, and isNextDev is gone from @decocms/nextjs (feat: @decocms/nextjs for v8: draft proxy and pages helpers, its own draft names and Studio origins, withDeco() and a platform-neutral image loader #697). The script never wrote a dev flag. telemetry.md (merge conflict resolved) says to leave dev out.
  • Schema check in CI (decision 2). New scripts/ciSchemaCheck.ts, run as part of the scripts step. It adds deco schema --check right after the install step of the site's pull-request workflows, or of every workflow when none runs on pull requests, and never to v7's regen-blocks. It uses the lockfile's runner (bunx, pnpm exec, yarn, npx) and the install's working directory (the site's folder in a monorepo). Comments and layout are kept, a second run changes nothing, and a workflow it can't edit (none, or a multi-line install) gets a report line. On the demo storefront, whose only workflow posts preview links, the script reports that line.
  • Tests: ciSchemaCheck.test.ts (5) and 4 new imageCopy.test.ts tests. Mutations that widen the install match, drop the working directory, skip the report, misread 7.71.0–7.71.3 or stop comparing against the leading strip each fail them. The skill's scripts pass 90/90.
  • Gates at aedc30b: bun run check, bun run build, skills:check and skills:readme:check pass. Plain Node imports all 38 built exports, @decocms/nextjs has no isNextDev, and deco --help lists --check. bun run test (2 GB): 2,482 passed, 4 failed, 95 skipped. The failures are DS-2 (known; the fix is on its own PR against main) and three timeouts under full-suite load (two in schema.test.ts, plus tr-13). Those three files run alone: 102 passed, 1 failed (DS-2).

Tests

  • steps.test.ts:

    • The draft names move per binding and are reported without one.
    • The image hint names the apps-*/image helpers.
    • Every move target exports the name.
    • The runtime exports of the @decocms/tanstack root, the @decocms/nextjs root, /preview and @decocms/blocks/image are all listed.
    • @decocms/blocks exports none of the moved names.
    • @decocms/blocks/image's list names no platform.
  • bindings.test.ts: a v7 TanStack Start fixture goes through migrate, then the rewritten files type-check against this repo's sources. That includes a prerelease DRAFT_COOKIE/STUDIO_ORIGINS import (moved to @decocms/tanstack) and the imageUrl + ImageUrlProvider module the report asks for, built on @decocms/apps-vtex/image and @decocms/apps-shopify/image. Feeding it an old helper signature makes it fail.

  • steps.test.ts also checks the per-release table and how the version is read.

  • imageCopy.test.ts (7 tests) uses verbatim excerpts of each release form and of an edited copy. It covers:

    • telling a copy apart from code that only uses the helper;
    • the options for each release form, the registered domain and quality, and an edited copy's own rules;
    • an unreadable copy;
    • the report line taking priority over the release's.

    A mutation that ignores the copy makes it fail.

  • Results (adversarial pass, 2026-10-14, at db44e19): fix 2 narrowed in feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694 (6b7e700: decoCdn's default match is back to the pre-fix sources, so a Deco CDN URL that used to pass through, e.g. deco-assets.edgedeco.com under sources: "deco", is unchanged again) and merged up. bun run check, bun run build, skills:check and skills:readme:check pass. bun run test: 2,455 passed, 3 failed under full-suite load: DS-2 (known), a schema.test.ts timeout and a syntax.cli.test.ts timeout (saved-01); those three files alone: 89 passed, 1 failed (DS-2). Copy-reading fuzz rerun with a strict fix-2 rule (a difference counts as fix 2 only when v7 wrote src=image or src=<cdn>/image): 7 releases' Image.tsx + 2 storefront copies x 4 registrations, 1,944 cases each, 0 other differences.

  • Results (fix 2 + copy reading, 2026-10-14, at f80cf1a):

    • bun run check, bun run build, skills:check and skills:readme:check pass.
    • bun run test: 2,453 passed, 4 failed under full-suite load: DS-2 (known), two schema.test.ts timeouts and an HRI-3 timing assertion. Run alone, schema.test.ts passes 58/58 and delivery.test.ts passes 94/94 (twice); delivery.cli.test.ts fails only DS-2.
    • The fix-2 golden cases pass: blocks 160 and next/image 60, all marked approved. The TanStack markup goldens (250 cases) have no already-resized CDN source, and all of them still match.
  • Results (review pass, 2026-10-14, at 73c0a22):

    • bun run check, bun run build, skills:check and skills:readme:check pass; plain Node imports all 14 built @decocms/blocks exports and none exports a draft name, a Studio origin or a platform name.
    • bun run test (at 2755ef9): 2,434 passed, 8 failed: DS-2 (known) plus load timeouts in schema, serve, cli (cli-01, cli-02) and migrate. Those five files run alone: 274 passed, 1 failed (DS-2). The skill's scripts pass 74/74 at 73c0a22.
  • Earlier results:

    • bun run check, bun run build, skills:check and skills:readme:check pass.
    • bun run test: 2,433 passed, 7 failed. The failures are DS-2 (known) and six tests that timed out under full-suite load, in check, schema, cli cp-51, HRI-3 and HRI-4. Those five files pass when run alone (338/339, the only failure being DS-2).

For the PO

  • The image components are in @decocms/tanstack, not in blocks, and know no platform. PageBlocks and pageRoute from the plan are still not shipped.
  • blocks-cli templates live only on 7.x, so nothing changed for them here.
  • On Next.js, passing site lets <site>.deco.site preview, which v7's Next binding didn't allow. The report and nextjs.md say so, and say to leave site out to keep v7's hosts.
  • No release or npm action in this PR.
  • Decided (PO, 2026-10-14): Nuvemshop's image rule stays in the store's own code. There is no @decocms/apps-nuvemshop. A Nuvemshop store keeps v7's URLs with a provider in its own imageUrl.ts; the skill gives that provider, and the golden test holds it to v7.
  • Approved (PO, 2026-10-14): fixes 1 and 2 from feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694. Fix 2 is merged here and documented in the skill. Fix 1 is a site change.

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig

Review round (2026-10-15)

  • a771f6cf: migrate() itself is now tested to add deco schema --check to the site's pull-request CI. Before, only addSchemaCheck was tested, so dropping its call from migrate() failed nothing; now it fails the new test.

Studio's "." and outlines in the skill (2026-10-16, 19393a99)

  • framework-code.md and the import map: a TanStack page loader returns pathTemplate; a site with its own document renders DecoPreview (bridge, badge and draft) rather than StudioBridge alone; a Next.js page renders StudioPage; page sections keep v7's <section id data-manifest-key> wrapper, which Studio's editor outlines, selects and scrolls to. A Next site whose own section list drops it (v7 Next storefronts did) makes that wrapper a <section>.
  • The import check accepts StudioPage and StudioPageInfo; the import test covers them and DecoPreview.

Main refresh (2026-10-17, ced92e19)

🤖 Generated with Claude Code

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

@tlgimenes
tlgimenes marked this pull request as ready for review October 18, 2026 13:19
@tlgimenes
tlgimenes added this pull request to stack #721 October 18, 2026 18:51
tlgimenes added a commit that referenced this pull request Oct 18, 2026
… stops at the first match (#693)

## What this PR does

Three product-owner decisions (2026-10-14/15) in `@decocms/blocks`. The
rest of the stack (#694 and up) merges this branch.

### 1. `createCMS` reads `NODE_ENV` again to decide dev mode (d03448c)

- With no `dev` option, a `createCMS` with `site` stays on local files
when `process.env.NODE_ENV === "development"`. Hosted releases are never
swapped in, and drafts still load.
- The read is guarded (`typeof process !== "undefined" && process.env !=
null && …`), so a Worker or browser without `process` counts as
production. It is spelled `process.env.NODE_ENV` so a bundler's `define`
can replace it.
- The explicit `dev` option, which shipped in 8.0–8.2, stays as an
override in both directions (`dev: false` forces swaps even when
`NODE_ENV=development`).
- This is the SDK's only environment read. `noEnv.test.ts` allows
exactly that one line. `remoteLoader` itself still reads no environment.
- Conformance: four new H-5 tests (default, override, production or
unset, no `process`). Each was mutation-checked.
- Removing `isNextDev` from `@decocms/nextjs`, the migrator, the skill
and the smoke apps happens in #697 and #700, not here.

### 2. `deco schema --check` (fa83481)

- For CI: generates the schema in memory, writes nothing, and exits 1
when `.deco/schema.gen.json` is missing or differs from what `deco
schema` would write. The message says what moved:
  - blocks that are new in code or gone,
  - the number of changed definitions,
  - the generating version,
  - or formatting only.

  It ends with: Run `deco schema` and commit .deco/schema.gen.json.
- Schema errors exit 1 with the errors. `--check` and `--watch` can't be
combined, and there is no dev watch.
- Usage line: `deco schema [--root <dir>] [--watch | --check]`. Seven
tests in `cli/run.test.ts`, including one through the bin under plain
Node. `dd-06` follows the new usage line.
- The migrator adds it to each site's CI workflow in #700.

### 3. `multivariate` stops at the first matching rule, as v7 did
(ac23a70)

- Before this change, the resolver resolved every variant's rule before
the built-in `multivariate` ran. A matcher after the winner still ran,
so a random rule there drew a number and recorded a cohort.
- Now the built-in `multivariate` (marked the way `lazy` is) gets its
rules resolved in order, one at a time, up to the first `true` one.
Later variants are left out and their matchers are never called.
- A `multivariate` that the site declares in its block map replaces the
built-in and still gets every rule resolved, like any other block.
- Legacy multivariate types (`website/flags/multivariate.ts`, …) go
through the same path.
- `builtin-03/mv-02` now claims first-match order. That test changed
because the docs claim changes. New tests cover in-order evaluation, a
random rule after the winner never drawing, legacy types, and the
block-map override. Mutation-checked: 4 fail without the change.
- CLAUDE.md now says that `lazy` and the built-in `multivariate` are the
resolver's special cases.

### Docs site follow-up (deco-sites/docs-tanstack, `/next/*`)

- **api-reference / releases-and-drafts**: `dev` defaults to `NODE_ENV
=== "development"` (guarded; `false` without `process`). Pass `dev` to
override it, e.g. `dev: import.meta.env.DEV` on a Worker. Remove "the
SDK reads no environment variable" wherever it refers to `dev`.
- **cli / checking**: `deco schema --check` (usage line, exit codes, the
message) and a CI snippet that runs `deco schema --check`.
- **matchers-and-variants** line 75: replace "Rules aren't lazy: they're
cheap booleans and always run." with "Rules run in order and stop at the
first `true` one, as in v7: a later rule's matcher never runs, so a
random rule after the winner draws nothing and sets no cookie. A
`multivariate` you declare in your block map gets every rule."
- **how-resolution-works**: `lazy` is no longer the only special case.
Add the built-in `multivariate`'s in-order rules.

### Gates

- `bun run check` passes.
- `bun run build` passes.
- `skills:check` and `skills:readme:check` pass.
- A plain-Node import of every built export works (22), and so does
`deco --help`.
- `bun run test` (2 GB): 2092 pass, 3 fail, 95 skipped.
  - DS-2 is a known failure on `main` since #689.
- Two `schema.test.ts` tests hit the 5 s timeout under full-suite load.
They pass alone (116/116 with noEnv, guides.cli and telemetry).

## Draft shape and block JSON: unchanged (2026-10-16)

Two who-edited-what experiments came and went on this branch, and the
net change is none:

- A top-level `edits` field on the draft (`00125500`), reverted in
`a886d720`.
- A reserved `__meta` key on saved blocks that the resolver dropped
(`b1cd9a13`, `36fb75f1`), reverted in the commit on top. Both reverts
are merged up the stack.

PO: "only have metadata and changed by on v8 hosted". Studio keeps who
and when in the hosted draft object's storage metadata, so the draft
stays exactly `{ set, delete }` and blocks has no reserved metadata key.
The tree at M2 equals its state before these commits.

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

### Main refresh (2026-10-17, `63b9e6d2`)
- Merged main `8bd3d88c` (#709) 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.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tlgimenes added a commit that referenced this pull request Oct 18, 2026
…image providers in their packages (#694)

Stacked on #693 (P2 of the v8 framework-packages plan). Reworked after
the product owner's review: `@decocms/blocks` keeps framework code only,
and nothing that knows VTEX, Shopify or another commerce platform.

## What

**`@decocms/blocks/image`** (platform-neutral): pure functions, no
React, no module state.

- `decoCdn(options)`: the Deco image CDN
(`https://<domain>/image?fit=&width=…&src=`). Options: `domain`,
`quality` (tier), `fit` (default `cover`, as v7's `getSrcSet`/`Image`),
`legacyHosts` (default `["decoims.com"]` on the Cloudflare CDNs),
`sources` (`"deco"` default, `"all"` for v7's "everything else goes
through the CDN", or a predicate), `stripAssetHosts` (`"leading"`, the
default since the PO's 2026-10-15 decision: v7 from 7.71.4; or
`"anywhere"`: v7 up to 7.71.3 and two storefronts' vendored helpers),
`layout` (`"src-last"` or `"quality-last"`).
- `createImageUrl({ providers })`: one `ImageUrl` per site, with
`isOptimizable` and `hosts()`. Providers are tried in order; a
provider's `format` may return `null` to leave a source to the next one.
- `srcSet`, `parseSrcSet`, `preloadLink`, and the generic types
(`ImageProvider`, `ImageUrl`, `ResizeOptions`, `Quality`, `Fit`, …).
`srcSet` without a width writes v7's two `width=NaN … NaNw` candidates,
as v7's `getSrcSet` did for an `Image` or `Source` without a width (a
storefront's pages have 12 such `<source>` tags; before this, they lost
their srcset).

**`@decocms/apps-vtex/image`**: VTEX's URL formats as providers. They
are pure helpers over the blocks types: no fetch, no React, no env
reads, no runtime imports.

- `vtexCatalog()`: v7's `Image` rule. VTEX hosts, `{id}-{w}-{h ?? w}`,
URL re-serialised.
- `vtexCatalogFrame({ frame? })`: any host, a fixed frame or the call's
box, rounded.
- `vtexCatalogWebp({ quality? })`: the next/image loaders' rule.
`{id}-{w}-auto/{file}.webp?…&quality=` on VTEX's 1-10 scale.
- `vtexFileManager({ via: "query" | "thumbor", quality? })`.

**`@decocms/apps-shopify/image`**: `shopifyCdn()`, v7's rule (`width`,
`height` when given, `crop=center`).

A site lists the platform providers before `decoCdn`. The bindings never
depend on an `apps-*` package; sites wire the helper in. v7's
`registerImageCdnDomain`/`registerImageQuality` do not come back: they
were module state. Their values are `decoCdn`'s `domain` and `quality`.

## Golden URLs: only the approved changes

The fixtures were regenerated by running each v8 storefront's own
helpers, **unchanged**:

- the three vendored v7 `getOptimizedMediaUrl`/`getSrcSet` variants
- two next/image loaders
- a fixed-frame catalog loader

The corpus grew to cover edge cases: unencoded characters, an asset host
inside another URL, already-resized CDN URLs, uppercase hosts, odd id
suffixes, store-domain catalog URLs, and fractional widths. The fixtures
are split per package:

| Package | Cases | Sources |
|---|---|---|
| `@decocms/blocks` | 1,508 | Deco hosts, other hosts, paths, `data:`,
`""` |
| `@decocms/apps-vtex` | 2,625 | catalog + file manager, composed with
`decoCdn` |
| `@decocms/apps-shopify` | 144 | Shopify CDN, composed with `decoCdn` |

Every case reproduces today's URL byte for byte, except 192 approved
`@decocms/blocks` cases:

- 160 for approved change 2 below (the two already-resized CDN sources,
in every config);
- 32 for approved change 3 (the source
`https://images.example.com/https://assets.decocache.com/…`, on the two
configs that keep `decoCdn`'s default with `sources: "all"`).

Each of those carries `before` (today's URL) and `reason`. The test
asserts that no other case changed, and that each change-3 `before` is
exactly what `stripAssetHosts: "anywhere"` still writes. The `apps-vtex`
and `apps-shopify` fixtures have no changed case and still assert that
none carries a `before`. Where a helper throws today, the case is
recorded as `<throws>`, and the provider still throws (v7's VTEX rule on
a URL that only mentions a VTEX host in its query). The fixtures use no
site names. The generator stays out of the repo because it reads the
sites' paths.

## Approved changes (PO, 2026-10-14 and 2026-10-15)

The product owner approved 1 and 2 on 2026-10-14 ("the image fixes are
good") and 3 on 2026-10-15:

1. **File-manager images on the next/image loader that still uses VTEX's
`?width=&aspect=true` resizer.** That resizer has answered 500 for
raster files since 2026-10-07. That store switches to `vtexFileManager({
via: "thumbor" })`, which the other loader already uses. This is a site
change only; no package changes.
2. **An already-resized Deco CDN URL pasted into content**
(`https://assets.decocms.com/image?…`, or the `decoims.com`,
`deco-assets.edgedeco.com` and `deco-assets.decoazn.com` endpoints).
Today it becomes `src=image`, or the whole `/image` URL without its
query, and the CDN cannot serve either one. `decoCdn` now re-resizes it
from its `src` by default (c80dd33, narrowed in 6b7e700). That covers
an encoded `src`, a nested CDN URL, `http` and any host case. Resizing a
result again gives the same URL. It applies only to the URLs `decoCdn`
already resized (its `domain` and legacy hosts with the default
`sources: "deco"`; every endpoint with `sources: "all"` or a predicate
that takes it): a Deco CDN URL it passed through before, such as a
`deco-assets.edgedeco.com` one under `sources: "deco"`, still passes
through unchanged, because it was served fine. Other URLs are
unaffected, including a non-Deco `/image` path and an `/image` URL with
no `src`. A differential fuzz against the previous commit (9 configs, 10
hosts, 6 path shapes) finds 108 changed URLs, every one of them
`src=image` or `src=<cdn>/image` before.

3. **`stripAssetHosts` defaults to `"leading"`** (afe430c). An asset
host (`assets.decocache.com/`, the S3 bucket) is removed only where the
source starts with it, as v7's `Image` does since 7.71.4. Two
storefronts' vendored helpers (and v7 up to 7.71.3) removed it anywhere,
so a source that only contains an asset URL
(`https://other.host/https://assets.decocache.com/x.png`) used to lose
that part and now keeps it. Sources that start with an asset host, which
is every one the storefronts' content has in the corpus, give the same
URL either way. `"anywhere"` stays an option. Follow-up in the migrator
(#700): stop emitting `"anywhere"` and count and report the URLs this
changes per site.

## Proposals for the PO (not decided, not in the code)

4. **A mobile app's VTEX helper copies disagree with each other.** One
drops the source's query; the other leaves a pre-sized `-W-auto` id at
its baked size. The app does not use these packages, so it is out of the
golden suite. Proposal: point it at one helper.

Nuvemshop's image helper stays in the store's own code (PO, 2026-10-14).
There is no `apps-nuvemshop` package.

## Checks

Decision 3 (afe430c, on top of #693's decisions 1–3 merged in
d654d9a): `bun run check`, `bun run build`, `skills:check` and
`skills:readme:check` pass, and plain Node imports all 24 built exports
(`deco --help` runs). `bun run test` (2 GB): 2150 passed, 3 failed: DS-2
(fails on main too; fixed separately) and two `schema.test.ts` timeouts
under full-suite load (58/58 alone). Image tests 48/48 across the three
packages; reverting the default fails three of them.

Adversarial pass (6b7e700): c80dd33 had also widened `decoCdn`'s
default match to every Deco CDN endpoint, which changed 168 URLs that
used to pass through and were served fine; the match is back to the
pre-fix sources, and a new unit test pins those pass-throughs. `bun run
check` and `bun run build` pass; image tests 27/27; the golden fixtures
are unchanged (160 approved cases). Full gates at the top of the stack
(#700).

Fix 2 (c80dd33): `bun run check`, `bun run build`, `skills:check` and
`skills:readme:check` pass. `bun run test`: 2133 passed, 2 failed: DS-2
(the known flake) and a `schema.test.ts` timeout under full-suite load,
which passes 58/58 alone. The image tests pass 26/26, with 6 new fix-2
unit tests.

Review pass (2026-10-14): the `srcSet` fix above (ea0a6bf). Its golden
markup cases (v7's own components, no width) are in #700's
`tests/tanstack-image-markup.golden.test.ts`. Full gates at the top of
the stack (#700).


First round (before the PO review):

- `bun run check` passes. `bun run build` passes and emits
`dist/v8/image.*`, `apps-vtex/dist/image.*` and
`apps-shopify/dist/image.*`. Plain Node imports the built
`@decocms/apps-vtex/image`.
- `bun run test`: 2124 passed, 4 failed:
  - DS-2 in `delivery.cli.test.ts`, the known flake.
- Three CLI conformance timeouts under full-suite load (`cli-02`,
`qs-01/in-03`, `saved-03/saved-11`). Run alone, those files pass
179/179.
- `bun run skills:check` and `bun run skills:readme:check` pass.
- Bundle tests:
- `@decocms/blocks/image` bundles for a browser and for workerd with no
Node built-ins and no React (esbuild).
- New `tests/upstream-helpers.guardrail.test.ts`: every `apps-*` helper
subpath gets the same bundle check, plus a purity check (no network,
React, env reads or runtime imports). Its comment records that the PO
asked for helper subpaths in `apps-*`.
- `CLAUDE.md` now says where platform helpers live.

**For the PRs above (#696, #697, #700):** the API changed:

- `vtexCatalog`/`vtexFileManager`/`shopify` are no longer in
`@decocms/blocks/image`.
- `parse`/`ParsedImage` and `proxyOtherHosts` are gone (use `sources:
"all"`).
- `@decocms/tanstack`'s default `ImageUrl` cannot include VTEX/Shopify,
because it must not depend on `apps-*`.

### Main refresh (2026-10-17, `0103f85f`)
- Merged P1 `63b9e6d2` 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.com/claude-code)
https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cms/nextjs (#700)

Rebased onto main as one commit per PR (PO, 2026-10-18); the previous history is kept on backup/migrator-uses-bindings-2026-10-18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
@tlgimenes

Copy link
Copy Markdown
Contributor Author

Superseded by #722 so the stack lands as one commit and one 8.4.0 release (PO, 2026-10-18).

tlgimenes added a commit that referenced this pull request Oct 18, 2026
…cocms/compat-v7, /preview, lockstep release and the migrator (#722)

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)

```bash
# 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:

```bash
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.com/claude-code)

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu


<!-- This is an auto-generated description by cubic. -->
---
## 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).

<sup>Written for commit 6195fe5.
Summary will update on new commits.</sup>

<a href="https://cubic.dev/pr/decocms/blocks/pull/722?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/decocms/blocks/722?returnTo=https%3A%2F%2Fgithub.com%2Fdecocms%2Fblocks%2Fpull%2F722&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@tlgimenes

Copy link
Copy Markdown
Contributor Author

Landed in #722 (squashed as one commit and one 8.4.0 release).

@tlgimenes tlgimenes closed this Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant