Skip to content

fix: deliver streamed deferred content as declarative templates, not inline scripts - #51

Merged
thinkter merged 1 commit into
mainfrom
fix/issue-6-declarative-streaming
Jul 9, 2026
Merged

fix: deliver streamed deferred content as declarative templates, not inline scripts#51
thinkter merged 1 commit into
mainfrom
fix/issue-6-declarative-streaming

Conversation

@thinkter

@thinkter thinkter commented Jul 7, 2026

Copy link
Copy Markdown
Owner

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):

  • Document-mode streaming tails now emit streamSwap (deferred content) + metadataSwap (head metadata) — identical to fragment mode — followed by the held-back document closing tags.
  • New streamRedirect(location) template for deferred loader redirects, used by both modes. This also fixes a latent bug: the fragment-mode redirect wrote window.__brandyRedirect from an inline script that (a) nothing ever read and (b) was injected via replaceChildren, where scripts never execute — it was dead code.
  • inlineScript/inlineSwap/inlineMetaSwap are deleted. Streamed responses now contain zero executable content, so they work under a strict Content-Security-Policy with no nonce plumbing at all.

Client (src/client/runtime.ts):

  • reconcileStream handles template[data-brandy-stream-redirect] first (via location.replace), then head + slot templates as before.
  • A boot sweep runs reconcileStream(document) at module evaluation. The runtime is a type="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 a loading.tsx route 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 new tests/client-runtime-boot.test.ts covers the boot sweep against pre-populated documents (content swap, head reconciliation, redirect via location.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

    • Streamed pages now deliver deferred content, head updates, and redirects as declarative templates, improving compatibility with stricter security policies.
    • Cold-load streaming now has a documented no-JS fallback for routes using loading states.
  • Bug Fixes

    • Mid-stream redirects now resolve correctly without applying stale page content.
    • Streaming behavior on cold loads and soft navigations is more consistent, with deferred content and metadata applied reliably.

…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>
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
brandy Ready Ready Preview, Comment Jul 7, 2026 12:57pm

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d648f483-f336-4995-9c86-a2bee634a2a0

📥 Commits

Reviewing files that changed from the base of the PR and between a568db7 and 0a0d35a.

📒 Files selected for processing (5)
  • docs/prd.md
  • src/client/runtime.ts
  • src/core/render.ts
  • tests/client-runtime-boot.test.ts
  • tests/streaming.test.ts

📝 Walkthrough

Walkthrough

Streaming deferred content and redirects now render as declarative <template> markers instead of inline <script> code. The client runtime reconciles these templates via a new boot sweep after page load and during soft navigation. Tests and PRD documentation are updated to reflect the scoped no-JS exception.

Changes

Declarative streaming templates and boot-sweep reconciliation

Layer / File(s) Summary
Server-side declarative template rendering
src/core/render.ts
Adds exported streamRedirect(location) emitting a <template data-brandy-stream-redirect> marker; deferred RedirectError handling and successful deferred rendering now emit streamSwap/metadataSwap templates instead of inline location.replace/swap scripts.
Client runtime boot sweep and redirect handling
src/client/runtime.ts
reconcileStream() handles redirect templates via location.replace() before other reconciliation; adds a one-time boot-time reconcileStream(document) call after parsing to apply deferred templates on cold loads.
Tests validating template-based streaming and boot sweep
tests/client-runtime-boot.test.ts, tests/streaming.test.ts
New boot-sweep test suite verifies template swap, head metadata reconciliation, redirect handling, and no-op on non-streamed documents; streaming tests assert declarative markers, absence of <script, and redirect template output.
PRD documentation of scoped no-JS exception
docs/prd.md
Documents the scoped exception for streamed cold loads under loading.tsx and converts the prior open streaming risk into a resolved decision for issue #6.

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
Loading

Related Issues: #6

Suggested labels: streaming, no-js, csp

Suggested reviewers: thinkter-maintainers

🐰 A skeleton waits, then templates arrive,
No scripts to run, just markup alive,
A boot-time sweep, redirects declared,
No-JS users, now finally squared. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: streamed deferred content now uses declarative templates instead of inline scripts.
Linked Issues check ✅ Passed The PR addresses #6 by removing inline streaming scripts, adding declarative redirect/template handling, and documenting the scoped no-JS exception.
Out of Scope Changes check ✅ Passed All changes map to the streaming, runtime, PRD, and test updates needed for the scoped fix; no unrelated functionality appears added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-6-declarative-streaming

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thinkter
thinkter merged commit abf4e87 into main Jul 9, 2026
3 of 5 checks passed
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.

Streaming cold loads violate progressive-enhancement (no-JS users see skeleton forever); inline script has no nonce

1 participant