feat(nextjs,tanstack): v8-only StudioBridge and DecoDocument, Cypress-safe "." shortcut

Merged
TL
10h ago
+565-87

Not merged — waiting for review.

Summary

@decocms/nextjs's StudioBridge and @decocms/tanstack's DecoDocument / DecoPreview become v8 only:

  • v7's framed handler (scrollToComponent, editor::rerender, the core STUDIO_BRIDGE_SCRIPT branch guarded by window.self!==window.top) is no longer rendered. Studio's v8 editor reaches the page through the Studio channel only.
  • Both render StudioChannel from @decocms/blocks/preview with the exact studioOrigins() (option, else DECO_STUDIO_ORIGIN, else studio.decocms.com + studio-stg.decocms.com), as before.
  • v7's "." edit shortcut stays (Studio still serves /choose-editor), as a separate inline script, studioShortcut.ts. It is off in a frame. It reads the frame state as var w=window,p=w.parent; if(p!==w)return, which none of Cypress's frame-busting regexes match. It keeps the same data-deco-studio-bridge / data-site / data-shortcut / data-path-template / data-page-id attributes and the StudioPage marker lookup.

Why: Cypress's modifyObstructiveCode rewrites window.self!==window.top everywhere in proxied HTML. That includes the RSC payload, where a Server Component's inline script travels as a length-prefixed text row. The row comes out one byte longer than its prefix, React's Flight client misreads the stream, and every page throws an uncaught Connection closed.. This broke the osklen Cypress suite on deco-sites/osklenbr-faststore#1388, which is why that site rendered StudioChannel by hand.

Decisions taken

  • No props change and no type break. StudioBridgeProps (nextjs) keeps site, editShortcut, page, nonce and studioOrigins, and all of them still do something, so nothing needed a @deprecated no-op. DecoDocumentProps / DecoPreviewProps are unchanged. StudioBridgeProps is now its own interface with the same fields, no longer Omit<core StudioBridgeProps, "trustedOrigins">.
  • @decocms/blocks/preview is untouched. Its StudioBridge (v7 handler) stays exported for anyone who renders it directly. The shortcut script lives in each binding (packages/nextjs/src/studioShortcut.ts and packages/tanstack/src/studioShortcut.ts), following the existing per-binding studio.ts copies. tests/studio-shortcut.test.ts asserts the two files are byte-identical.
  • The new shortcut already reads the field from e.composedPath()[0], the shadow-root fix of #784, so the binding path gets it whether or not #784 lands first. #784 still matters for the core StudioBridge.
  • The shortcut uses its own global guard (__decoStudioShortcut).
  • Conventional type feat: (minor).

How to spike it

bun install && bun x vitest run tests/studio-shortcut.test.ts packages/nextjs packages/tanstack/src/react
cd examples/nextjs-binding-smoke && bun run build && bun run smoke   # "the bridge is v8 only" check

On a Next.js site: render <StudioBridge site="…" /> and curl a page. It should contain data-deco-studio-channel and data-shortcut, and no scrollToComponent and no window.self!==window.top. Load the page through Cypress (or the regex rewrite) and confirm there is no "Connection closed.". Pressing "." outside a frame goes to studio.decocms.com/choose-editor?.... In Studio's frame the editor still gets deco:ready and loads its modules.

Evidence

  • tests/studio-shortcut.test.ts (16 tests) applies Cypress's own regexes from packages/rewriter/lib/regex-rewriter.ts, in both modes (first-party, and modifyObstructiveThirdPartyCode's expanded ones), to the nextjs StudioBridge markup and to the tanstack shortcut + channel markup. The output is byte-identical. As a control, the same rewrite does change the core v7 STUDIO_BRIDGE_SCRIPT.
  • The same file runs the shortcut in jsdom for both bindings. It checks: Ctrl+. opens choose-editor with site/domain/path/pathTemplate/pageId; a plain "." and Ctrl+Shift+E are handled; the shortcut is inactive in a frame; editShortcut={false} turns it off; typing in an input, and in an input inside an open shadow root, is left alone; and a StudioPage marker wins at keypress time.
  • react.test.tsx checks that DecoDocument's Studio scripts are exactly the shortcut and the channel. components.test.tsx checks the same for StudioBridge, with no data-origins and no v7 handler.
  • Run locally on Linux: bun run typecheck, bun run lint, bun run lint:unused, skills:check, skills:readme:check, bun run build and the nextjs-binding-smoke build + smoke all pass. 233 tests pass across packages/nextjs, packages/tanstack/src/react, packages/blocks/src/v8/preview and tests/studio-shortcut.test.ts.
  • osklen spike. deco-sites/osklenbr-faststore feat/studio-bridge-bump was built with next build + next start on packed 8.15.0 tarballs of this branch. Home, /about-us and a PLP served markup that all 9 Cypress rewriter regexes leave alone. The Cypress-rewrite Playwright harness showed no page errors (no "Connection closed."). A fake Studio parent at studio.decocms.com got deco:ready + deco:loaded x2, and the wireframe selected and outlined each section and ran html-swap. "." unframed went to choose-editor?site=osklenbr-faststore-studio&…. "." in a field typed a ".". In a frame, "." did nothing.

Known limitations

  • A v7-era Studio that still sends scrollToComponent / editor::rerender to a v8 site gets no answer. v8 Studio doesn't send them: it loads its wireframe and html-swap modules through the channel.
  • Other sites that carry their own copy of the v7 bridge script are not touched by this PR.

šŸ¤– Generated with Claude Code


Summary by cubic

Makes @decocms/nextjs's StudioBridge and @decocms/tanstack's DecoDocument/DecoPreview v8-only: v7's framed handler is gone, and the "." edit shortcut is now a script of its own that never compares the window with top or parent. Cypress's modifyObstructiveCode rewrites such comparisons in proxied HTML; when the RSC payload carries the script, the rewrite changes its length and every page fails with an uncaught "Connection closed.".

Details

  • Renders the Studio channel plus the "." shortcut; props, defaults, and outside-a-frame behavior are unchanged.
  • @decocms/blocks/preview is untouched and still exports its v7 StudioBridge.
  • A v7-era Studio that still sends scrollToComponent or editor::rerender gets no answer; v8 Studio reaches the page through the channel only.