Skip to content

feat: @decocms/blocks/preview, the draft badge and the Studio bridge - #695

Closed
tlgimenes wants to merge 1 commit into
feat/blocks-imagefrom
feat/blocks-preview
Closed

tlgimenes wants to merge 1 commit into
feat/blocks-imagefrom
feat/blocks-preview

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #694 (P2, feat/blocks-image). This is P3 of the v8 framework packages plan.

What it adds

A new subpath, @decocms/blocks/preview, with two components. Neither has framework code, and neither holds a list of Studio's origins: @decocms/blocks exports no Studio origins and no draft names (P1 was reverted after the PO's review). The bindings own those. @decocms/tanstack's document mounts both components and passes Studio's origins in (P4). A Next.js site renders both in its root layout with the origins from @decocms/nextjs (P5).

DraftBadge({ pointer, exitHref? })

  • v7's DraftPreviewBadge, unchanged on screen: the "Preview mode" pill with "Exit preview" and "Share preview".
  • "use client". It renders nothing in three cases: when pointer is empty, on the server, and inside a frame (Studio's site editor already shows its own chrome).
  • exitHref overrides the default exit link, which is the current URL with ?__draft=off.
  • The badge gets the __draft parameter name from draft.ts. That module exports the name to the badge only; the package does not export it.
  • The inlined Deco mark is now a 36px PNG (about 2KB). v7 inlined the 720px original (about 22KB) into every client bundle that mounted the badge.

StudioBridge({ site, trustedOrigins, editShortcut?, nonce? })

  • v7's LiveControls as one static inline <script>. Its settings travel in data-* attributes.
  • No hooks, so a Next.js Server Component can render it.
  • Why a script and not an effect: Studio posts to the frame on its load event, which can come before React hydrates.
  • trustedOrigins is required, and it is the whole list the bridge answers. It uses the CSP source form: https://host, or https://*.host for any name under it, with the same scheme and port. The binding passes Studio's origins. An empty list answers nothing.
  • In a frame, it answers only messages from the parent window whose origin is in trustedOrigins. It does not trust the page's own origin: under a test runner that proxies the site, the runner's own messages would navigate the page.
  • Messages, checked against what Studio v8 sends today:
    • editor::inject { args: { script } } is what Studio's Fast Preview and Local frames post. The injected script then handles the cms-editor::* messages itself.
    • It also keeps v7's scrollToComponent and editor::rerender (same-origin URLs only).
  • v7's "." / Ctrl+Shift+E shortcut, which opens the page in Studio's /choose-editor on a page that isn't framed, is on by default (PO, 2026-10-15, 280f1aa), as in v7, on TanStack and Next alike. editShortcut={false} turns it off.

Tests

  • preview/preview.test.tsx (jsdom) covers the badge and the bridge.
    • Badge: its links, its SSR output, framed and unframed rendering, the menu, and copying the share link.
    • Bridge: its script runs as a browser runs it. The cases are:
      • editor::inject from Studio;
      • a table of origins checked against Studio's list as a binding passes it;
      • an empty list answering nothing, not even Studio or the page's own origin;
      • any given origin, exact and wildcard;
      • a message from a window other than the parent;
      • an unframed page;
      • malformed messages;
      • scrollToComponent;
      • running only once;
      • the shortcut: on by default, off with an explicit false (no data-shortcut, "." and Ctrl+Shift+E ignored), never inside Studio's frame, never while typing in a field.
  • preview/exports.test.ts checks four things: the exports entry, the exact runtime names, that the badge module starts with "use client", and that the bridge has no hooks.
  • browserBundle.test.ts bundles /preview with esbuild for a browser target and a workerd target.
    • Each bundle's inputs must be only preview/*, draft.ts and React. studio.ts is no longer allowed, and it no longer exists.
    • The bundles must contain no node: built-ins, createCMS or AsyncLocalStorage.
  • Shortcut default (280f1aa, on top of feat: @decocms/blocks/image (platform-neutral) with VTEX and Shopify image providers in their packages #694 merged in 16d0532): bun run check, bun run build, skills:check and skills:readme:check pass; plain Node imports all 25 built exports and deco --help runs. bun run test (2 GB): 2,180 passed, 3 failed: DS-2 (fails on main too; fixed separately) and two schema.test.ts timeouts under full-suite load (58/58 alone). Preview tests 29/29; reverting the default fails three of them.
  • Earlier round: bun run check, bun run build, skills:check and skills:readme:check pass.
  • bun run test: 2,153 passed and 4 failed.
    • One failure is DS-2 in delivery.cli.test.ts (known).
    • The other three are timeouts under full-suite load: se-03 in cli.test.ts and two in schema.test.ts. Run alone, cli.test.ts and schema.test.ts pass (182/182).

For the PO

"." opens the page on screen (2026-10-16, 3d32c406, 32bdd30e)

  • The shortcut sent pathTemplate=/* on every page, so Studio's /choose-editor could only guess the page. The bridge now sends the page's pathTemplate (and pageId when known, as v7 did), read at the keypress so a client-side navigation sends the page then on screen. It comes from the bridge's new page prop (a layout that re-renders per page: @decocms/tanstack's document) or, winning, from the last StudioPage the page rendered (a Next.js root layout doesn't re-render per page).
  • StudioPage({ pathTemplate, id? }) is new: a <meta name="deco:page"> that React places in the <head>, so it adds nothing to the page's own markup.
  • The shortcut already ran on a page that isn't framed. The module comment said the bridge "does nothing unless the page is framed", which was true only of Studio's messages; it now says so. In a frame, it still answers only trustedOrigins.
  • Signed-in check: v7's LiveControls had none on the store page, and neither does this. Studio's /choose-editor is auth-gated and sends a visitor who isn't signed in to its login and back.
  • Tests: a plain "." on the store page; the page's template and id from the prop; a StudioPage on the page winning, read at the keypress; attributes changed after the script ran; "." typed in an input, textarea, select and contenteditable ignored; StudioPage hoisted into the head. Each fails without its change.
  • Checked in a browser on five locally built sites (three TanStack, two Next): the exact choose-editor URL with each page's template, "." in a field, the badge (shown, kept across client-side navigation and reloads, hidden in a frame, exit clears the cookie, absent on a host that may not preview), and Studio's injected editor script outlining, selecting and scrolling to sections.

Main refresh (2026-10-17, 4e244e17)

🤖 Generated with Claude Code
https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

tlgimenes added a commit that referenced this pull request Oct 14, 2026
…draft names and Studio's origins

Brings in the PO review: @decocms/blocks no longer exports the draft names,
draftResponseHeaders or Studio's origins (#693), @decocms/blocks/image is
platform-neutral with the VTEX and Shopify helpers in @decocms/apps-vtex/image
and @decocms/apps-shopify/image (#694), StudioBridge takes its trusted origins
from the binding (#695), and @decocms/tanstack owns its own copy (#696).
CLAUDE.md conflict: tests/ (helper guardrails) and examples/ lines combined.

@decocms/nextjs, in the same commit so every package type-checks:
- src/draft.ts (DRAFT_PARAM/COOKIE/HEADER, draftResponseHeaders) and
  src/studio.ts (STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin): the
  package's own copy, same values as @decocms/tanstack's, never imported from
  it. The proxy, drafts.ts, /server and config.test.ts use it; the root
  exports it.
- StudioBridge (src/StudioBridge.ts): blocks' bridge with trustedOrigins set
  to STUDIO_ORIGINS, so the layout still passes only the site.
- /image-loader stays platform-neutral; its next/image golden comparison
  moves to tests/nextjs-image-loader.golden.test.ts, which composes
  @decocms/apps-vtex/image with decoCdn as a Next storefront does: 1,212
  cases from blocks' and apps-vtex's fixtures, none changed. exports.test.ts
  pins that the package depends on no @decocms/tanstack or @decocms/apps-*.
- nextjs-binding-smoke: decoCdn({ fit: "contain" }) (blocks' default fit is
  now cover) and a check of the origins the bridge answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
Brings in the PO review down the stack: blocks without the draft names or
Studio's origins (#693), the platform-neutral @decocms/blocks/image with the
@decocms/apps-vtex/image and @decocms/apps-shopify/image helper subpaths
(#694), StudioBridge's required trustedOrigins (#695), and each binding's own
copy of the draft names and Studio's origins (#696, #697).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@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
…695)

Rebased onto main as one commit per PR (PO, 2026-10-18); the previous history is kept on backup/blocks-preview-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