Commit Graph

9 Commits

Author SHA1 Message Date
James Anderson 4ffd5bf225 fix(app-router): ensure streamed SSR body ends with </body></html> (#1532) (#1624)
* fix(app-router): ensure SSR stream ends with </body></html> suffix (#1532)

Trailing flight chunks and preinit scripts emitted by `rscEmbed.finalize()`
in the tick-buffered transform's `flush()` were being appended after React
Fizz's `</body></html>` closing tags, leaving the streamed document body
ending in `<script>...</script>` rather than a well-formed close.

Strip the first `</body></html>` we see in the buffered HTML chunks and
re-emit it at the very end of `flush()`, after the final RSC scripts.
Mirrors Next.js's `createMoveSuffixStream` behaviour
(packages/next/src/server/stream-utils/node-web-streams-helper.ts).

Ported from Next.js test:
test/e2e/app-dir/app/index.test.ts — "should ensure the </body></html>
suffix is at the end of the stream".

Fixes #1532.

* test(rsc-streaming): adjust progressive-delivery assertion for #1532

The 'delivers RSC chunks progressively' case still asserted the old order
where the navigation-runtime done signal trailed </html>. After #1532
suffix-moved </body></html> to the very end of the stream, the done
signal sits *between* the body content and the document close. Update
the assertion to match: done > last body content, and </html> > done.

* style: apply biome formatter to rsc-streaming.test.ts
2026-06-03 22:42:26 +01:00
Nathan Nguyen 7be428dbc3 feat(app-router): support inline CSS parity (#1595)
* feat(app-router): support inline CSS parity

App Router production builds ignored Next.js experimental.inlineCss, so HTML responses kept stylesheet links, next/font CSS lived in a second style tag, and Next-style RSC requests with RSC: 1 plus _rsc could return HTML.

The missing invariant was that inlineCss changes the production CSS delivery boundary: non-RSC HTML gets inlined style resources, RSC payloads keep CSS bodies out, and client navigations must not reintroduce duplicate stylesheet resources for CSS already present inline.

Resolve the config flag into the generated RSC entry, collect emitted CSS assets into an inline manifest for Node and Workers production, rewrite matching React stylesheet links into hoistable style tags, merge next/font CSS into the first inline style with a fallback path, and recognize Next-style RSC requests.

Regression coverage now ports the upstream app-inline-css behavior at production-browser level and adds focused tests for request normalization, stream rewriting, font merging, and fallback emission.

* feat(app-router): preserve inline CSS import ordering

Inline CSS font merging could prepend generated font rules before a stylesheet import. CSS imports must stay at the beginning of their stylesheet, so that ordering caused imported styles to be ignored when experimental.inlineCss and local font CSS were combined.

Keep import-sensitive stylesheet preambles untouched and let the existing font fallback style carry the font rules instead. Add stream-helper coverage for the import case.

* feat(app-router): harden inline CSS review edges

Inline CSS rewriting only saw complete link tags in a single decoded chunk and treated rel as an exact value. That left valid stylesheet links un-inlined when rel had multiple tokens or when streaming split the tag across chunks.

The production manifest injection also assumed the RSC worker bundle was always at dist/server/index.js, so custom rscOutDir builds missed the inline CSS manifest.

Buffer incomplete link tags across stream flushes, match stylesheet as a rel token, and inject the manifest into the configured RSC output entry. Add regressions for the streaming and custom-output cases.

* feat(app-router): match inline CSS cleanup semantics

Client navigation cleanup only pruned links whose rel was exactly stylesheet and whose precedence marker used data-precedence. That diverged from the server inline CSS rewrite, so tokenized rel values or legacy precedence links could survive after navigation as duplicate stylesheet resources.

The fix moves the cleanup into a testable helper, reuses shared HTML token-list matching for server and browser paths, accepts both precedence marker forms, and only buffers split link tags when an inline CSS manifest is present.

Regression coverage now covers browser cleanup and non-inlineCSS streaming chunk granularity.

* feat(app-router): harden inline CSS HTML rewriting

Inline CSS rewriting could prepend SSR font CSS ahead of namespace-bearing stylesheets and could treat stylesheet-like text inside script bodies as real link tags. That violates CSS ordering constraints and corrupts streamed script text when experimental.inlineCss is enabled.

The stream rewrite now treats @namespace as an unsafe preamble, skips link replacement inside raw-text and comment regions, and buffers incomplete rewrite boundaries across flushes. Regression coverage pins namespace fallback, complete script text, split script text, and unterminated script text.

* test(e2e): specify junction type for fs.symlink to support Windows

* refactor(app-router): drop write-only __PAGE__ wire marker

Every outgoing App Router payload carried `__PAGE__: true` in its wire
metadata, but nothing in the runtime ever read it. Its only consumer was
an inline-css E2E assertion checking that a dynamic navigation returned an
RSC payload rather than HTML, so the marker added permanent per-response
wire overhead with no behavioural effect.

The `__route` key is already present on every payload and proves the same
thing: the response carries page metadata. Remove the marker key, its type
field, and its re-export, and point the unit and E2E assertions at
`__route` instead.

* perf(app-router): skip inline-css manifest scan on the streaming hot path

The tick-buffered SSR transform recomputed `Object.keys(manifest).length`
on every flush through `rewriteInlineCssStylesheetLinks`, even when inline
CSS was disabled and the manifest empty. Streaming flushes are a hot path,
so this ran per chunk of every response for no benefit.

Compute the manifest-present flag once at transform creation and reuse it
to gate both the split-link boundary buffering and the link rewrite, so
non-inline-css responses skip the rewrite entirely. Also document the
shared-lastIndex constraint on the module-level rewrite regexes and the
startup/build-time population contract for `__VINEXT_INLINE_CSS__`.

* fix(app-router): forward SSR script nonce to inline-css <style> tags

Sites running `Content-Security-Policy: style-src 'nonce-…'` saw the
inlined `<style>` blocks blocked at parse time and rendered unstyled
when `experimental.inlineCss` was enabled. The `<link rel="stylesheet">`
tags the feature replaces aren't subject to inline-style CSP, but the
new inline blocks are — and React Fizz typically doesn't put a nonce on
the `<link>`, so the rewrite path had no nonce to carry over.

Thread the SSR-time `scriptNonce` through `createTickBufferedTransform`
into `rewriteInlineCssStylesheetLinks`, and prefer the link's own nonce
when present (otherwise fall back to the SSR nonce). Add unit coverage
for the SSR-only, link-only, and missing-nonce cases.

Also replace `html.toLowerCase().lastIndexOf("<link")` in
`splitTrailingIncompleteLinkTag` with a forward `g+i` regex scan so the
streaming hot path no longer allocates a lowercased copy of every flush
(can be tens of KB on large responses).

* chore(check): register experimental.inlineCss as supported

So `vp check` advertises App Router CSS inlining alongside the other
experimental flags. Notes that next/font CSS is folded into the first
inline `<style>` block.

---------

Co-authored-by: James <james@eli.cx>
2026-05-28 18:17:06 +00:00
Jim Geurts 3d5fc8dd84 fix(app-router): hoist inline beforeInteractive scripts above resource hints (#1558)
* fix(app-router): hoist inline beforeInteractive scripts above resource hints (#1557)

React Fizz puts every stylesheet, modulepreload, and preload link emitted via React Float above user-rendered <script> children of <head>, so an inline `<Script strategy="beforeInteractive">` placed first in the root layout lands near the bottom of head instead — well after styles begin to parse. This breaks the standard no-flash dark-mode pattern.

Capture inline beforeInteractive content via a new BeforeInteractiveContext that the App Router SSR pipeline wraps the rendered tree with. The Script shim registers the escaped innerHTML + nonce + attributes and returns null from render. The stream transform splices the captured tag immediately after `<head ...>` opens — before any chunk that contains React-emitted resource hints. On the client the shim suppresses the inline render when the App Router runtime is present (detected via the new hasAppNavigationRuntimeBootstrap helper) so hydration matches.

External-src and non-App-Router rendering paths are unchanged: external scripts still preload + render in source order, and Pages Router falls through to the existing inline render.

* fix: address PR feedback

- e2e spec now reads `self.__vinextThemeInitRan` (matches the fixture's actual marker; the prior `documentElement.dataset` assertion would always have been undefined)
- gate `renderBeforeInteractiveInlineScripts` on a `/^[a-zA-Z][\w.-]*$/` attribute-name regex so unknown developer-supplied prop keys can't break out of the unquoted name position
- drop unused `BeforeInteractiveProvider` export — the SSR entry uses `BeforeInteractiveContext.Provider` directly
- correct the fixture layout docstring to describe the actual `self.__vinextThemeInitRan` marker and explain why we avoid `<html>` dataset attributes here
- consolidate the duplicate CSP-nonce middleware blocks for `/revalidate-test` and `/beforeinteractive-head-ordering`

* docs/test: address PR nit feedback

- correct the createTickBufferedTransform jsdoc: only `injectHTML` falls back to end-of-stream; `injectAfterHeadOpenHTML` is silently dropped when `<head ...>` is never found in a discoverable chunk (covered by the existing unit test)
- drop the dead pre-request scaffolding from the CSP-nonce test; the meaningful assertions live in the second request, so the first one was just `void html` noise
2026-05-22 16:12:08 +01:00
Nathan Nguyen 0d8d3e3c60 refactor(router): quarantine navigation runtime topology (#1322)
* refactor(router): quarantine navigation runtime topology

* fix(router): restore manifest-backed interception runtime

App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload.

The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data.

Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice.

* fix(router): narrow prerender RSC script parsing

The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun.

Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix.

* fix(router): address navigation runtime review

* fix(router): resolve navigation runtime review threads
2026-05-20 10:29:13 +01:00
Nathan Nguyen 5d622369dc fix(rsc): preserve binary inlined Flight chunks (#1245)
Inlined App Router Flight data could only travel through string chunks. That corrupted arbitrary binary RSC chunks during SSR hydration and static prerender extraction because non-UTF-8 bytes were decoded to replacement characters before React read them.

Encode non-UTF-8 embedded chunks as base64 binary records, decode those records back to bytes in the browser, and write prerendered .rsc payloads as bytes instead of UTF-8 text.

Regression coverage exercises server embedding, browser reconstruction, and prerender extraction for binary RSC chunks.
2026-05-16 17:21:10 +01:00
Divanshu Chauhan (divkix) 66c8464903 fix: re-throw stream errors in pumpReader to prevent incomplete ISR cache data (#1003)
* fix: pumpReader now re-throws errors to prevent incomplete ISR cache data (#1002)

* test: add regression test for pumpReader error propagation (#1002)
2026-05-01 23:43:18 +01:00
Divanshu Chauhan (divkix) d45c5905f3 perf: fuse RSC stream tees reducing allocs from 3 to 2 (#984)
* perf: fuse RSC stream tees reducing allocs from 3 to 2

ISR cache path teed the RSC stream twice (once for capture,
once for embed). Fuse into single tee: sideStream serves both
embed transform and raw byte accumulation via createRscEmbedTransform.

Tee 1 (capture) and Tee 2 (embed) now share one tee. Tee 3 (HTML
cache) remains separate — it operates on post-SSR HTML with a
conditional write decision.

Fixes #981

* fix: address PR review feedback for fused RSC stream tees (#981)

- Fix AppPageSsrHandler type to include sideStream and capturedRscDataRef
- Clear rawChunks after getRawBuffer() to reduce peak memory usage
- Remove misleading no-op null assignment in handleSsr else branch
- Guard await null in ISR cache write to prevent undefined in cache

* fix: repair CSP nonce handling and update snapshots for #981

- Fix renderAppPageHtmlStream to always pass ssrOptions (including scriptNonce)
  to handleSsr, instead of conditionally when sideStream is defined. This fixes
  CSP nonce regressions in dev mode.
- Update 6 snapshot occurrences in entry-templates.test.ts.snap to match the
  updated __revalCapturedRscRef.value access pattern with null safety.

Fixes CI failures for the fuse RSC stream tees PR.
2026-05-01 08:49:58 +01:00
James Anderson 6aa460f9ad fix: HL flight hint regex not matching correct format (#631)
* fix: HL flight hint regex not matching correct format

* update test
2026-03-21 18:39:29 +00:00
Steve Faulkner a6d2c3a09c Refactor App Router virtual entries into typed runtime modules (#616)
* refactor: move app router SSR/browser runtime into typed modules

* style: format typed app runtime modules

* refactor: address runtime entry review follow-ups
2026-03-20 23:03:27 -05:00