fix: deliver streamed deferred content as declarative templates, not inline scripts - #51
Conversation
…inline scripts Cold-load streaming previously delivered deferred loading.tsx content as an inline <script> that pasted it into the skeleton slot — invisible to no-JS user agents and blocked by any strict CSP (no nonce support). The mid-stream redirect instruction for fragments was also dead: it wrote window.__brandyRedirect from a script nothing read, injected in a way that never executes. Document-mode streaming now emits the same inert declarative templates the fragment path already uses (streamSwap + metadataSwap), plus a new streamRedirect template for deferred loader redirects in both modes. The client runtime applies them in one boot sweep at module evaluation — module scripts execute after the document (and therefore the stream) finishes parsing — and reconcileStream now honors the redirect template via location.replace. All inline-script emission is deleted; streamed responses carry no executable content, so strict CSP needs no nonces. The documented, scoped exception to the no-JS principle (PRD §6/§12): a no-JS cold load of a streaming route keeps the skeleton until reload. Fixes #6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughStreaming deferred content and redirects now render as declarative ChangesDeclarative streaming templates and boot-sweep reconciliation
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant Server
participant ClientRuntime
Browser->>Server: Request route with loading.tsx (cold load)
Server-->>Browser: Stream skeleton HTML
Server-->>Browser: Stream deferred template (data-brandy-stream-target / data-brandy-head)
alt Redirect occurs during render
Server-->>Browser: Stream redirect template (data-brandy-stream-redirect)
end
Browser->>ClientRuntime: DOMContentLoaded / module boot
ClientRuntime->>ClientRuntime: reconcileStream(document) boot sweep
alt Redirect template present
ClientRuntime->>Browser: location.replace(destination)
else No redirect
ClientRuntime->>Browser: Swap stream target and head templates into DOM
end
Related Issues: Suggested labels: streaming, no-js, csp Suggested reviewers: thinkter-maintainers 🐰 A skeleton waits, then templates arrive, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fixes #6.
Implements the decision from the design discussion: runtime-applied declarative swaps. Cold-load streaming no longer emits inline
<script>s; deferred content rides the same inert<template>wire format the fragment path already uses, and Brandy's always-shipped client runtime applies it.Server (
src/core/render.ts):streamSwap(deferred content) +metadataSwap(head metadata) — identical to fragment mode — followed by the held-back document closing tags.streamRedirect(location)template for deferred loader redirects, used by both modes. This also fixes a latent bug: the fragment-mode redirect wrotewindow.__brandyRedirectfrom an inline script that (a) nothing ever read and (b) was injected viareplaceChildren, where scripts never execute — it was dead code.inlineScript/inlineSwap/inlineMetaSwapare deleted. Streamed responses now contain zero executable content, so they work under a strictContent-Security-Policywith no nonce plumbing at all.Client (
src/client/runtime.ts):reconcileStreamhandlestemplate[data-brandy-stream-redirect]first (vialocation.replace), then head + slot templates as before.reconcileStream(document)at module evaluation. The runtime is atype="module"script, so it executes after the document — and therefore the stream — finishes parsing; every deferred template from a streamed cold load is already in the DOM and gets applied in one pass. Non-streamed documents make it a no-op.Docs (
docs/prd.md): the risk-register entry is resolved and principle 4 now carries the scoped, dated exception this issue asked for a decision on: a no-JS cold load of aloading.tsxroute keeps the skeleton until reload; every other route retains the full no-JS guarantee.Tests: streamed documents contain templates and no
<script; deferred head metadata arrives declaratively; deferred redirects emit the template in both modes; the compiled runtime understands the new wire format; and a newtests/client-runtime-boot.test.tscovers the boot sweep against pre-populated documents (content swap, head reconciliation, redirect vialocation.replace, and the non-streamed no-op) using cache-busted runtime imports.bun test: 166 pass, 0 fail.bun run typecheck: 27 errors, byte-identical to the pre-existing baseline on main (verified via stash) — zero new.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes