Skip to content

chore(debug): classification reasons sidecar behind VINEXT_DEBUG_CLASSIFICATION [6/6]#843

Draft
NathanDrake2406 wants to merge 6 commits intocloudflare:mainfrom
NathanDrake2406:chore/pr-768-6-classification-debug-reasons
Draft

chore(debug): classification reasons sidecar behind VINEXT_DEBUG_CLASSIFICATION [6/6]#843
NathanDrake2406 wants to merge 6 commits intocloudflare:mainfrom
NathanDrake2406:chore/pr-768-6-classification-debug-reasons

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Summary

PR 6 of 6 — restack of #768. Stacked on #842.

Operators tracing why a layout was flagged static or dynamic can opt in with `VINEXT_DEBUG_CLASSIFICATION=1` at build time. When active, the plugin patches a second stub (`__VINEXT_CLASS_REASONS`) with a dispatch table that returns per-layout `ClassificationReason` structures. The runtime probe loop emits a debug line per layout when `debugClassification` is wired, and the hot path remains allocation-free when debug is off because the stub returns `null`.

The reasons machinery (types, `report.ts` tagged results, and `buildReasonsReplacement`) already shipped in #842; this PR just enables the emission path and wires the debug consumer into the runtime probe.

What changes

  • `index.ts` `generateBundle` hook now conditionally calls `buildReasonsReplacement` when `VINEXT_DEBUG_CLASSIFICATION` is set and patches the `__VINEXT_CLASS_REASONS` stub.
  • Generated RSC entry now emits the `__VINEXT_CLASS_REASONS` stub and a `__classDebug` gated logger, and passes `buildTimeReasons` + `debugClassification` through the runtime classification block.
  • `app-page-execution.ts` emits a debug line per layout classification decision when the debug hook is wired.
  • New tests in `app-page-execution.test.ts` and `build-time-classification-integration.test.ts` cover the gated-off and gated-on cases.

Stack

  1. [1/6] refactor(app-rsc-entry): centralize request-derived page inputs [1/6] #838 — centralize request-derived page inputs
  2. [2/6] feat(app-router): emit per-layout flags in the RSC payload [2/6] #839 — emit per-layout flags
  3. [3/6] feat(app-router): filter skipped layouts from RSC responses and cached reads [3/6] #840 — filter skipped layouts
  4. [4/6] feat(app-router): send X-Vinext-Router-Skip on navigation requests [4/6] #841 — send X-Vinext-Router-Skip
  5. [5/6] feat(build): wire build-time layout classification into the generated RSC entry [5/6] #842 — wire build-time layout classification
  6. [6/6] this PR — classification reasons sidecar

Test plan

  • `tests/build-report.test.ts`
  • `tests/layout-classification.test.ts`
  • `tests/route-classification-manifest.test.ts`
  • `tests/build-time-classification-integration.test.ts` (reasons cases enabled)
  • `tests/app-page-execution.test.ts` (debug-gated cases)
  • `tests/entry-templates.test.ts` (snapshot regenerated)

Wire runtime layout classification through renderAppPageLifecycle and
attach the resulting flags as __layoutFlags in the outgoing RSC payload
via buildOutgoingAppPayload. Payload-shape helpers (withLayoutFlags,
isAppElementsRecord, buildOutgoingAppPayload, AppOutgoingElements) live
alongside the existing readAppElementsMetadata so the write and read
boundaries sit next to each other.
…dant clone

- isAppElementsRecord narrowed to Record<string, ReactNode>, but the
  outgoing payload legitimately carries heterogeneous values (ReactNode
  for the rendered tree plus LayoutFlags under __layoutFlags). Widen the
  predicate to Readonly<Record<string, unknown>> so the return type
  matches what the runtime check actually proves.

- buildOutgoingAppPayload spread input.element into a new object before
  passing it to withLayoutFlags, which immediately spreads again. Drop
  the outer copy; withLayoutFlags already guarantees immutability.
…d reads

Introduce app-page-skip-filter.ts with the canonical-bytes guarantee:
the render path always produces the full RSC payload and writes it to
the cache; the egress branch applies a byte-level filter that omits
layouts the client asked to skip, but only if the server independently
classified them as static (computeSkipDecision).

Wire the filter into renderAppPageLifecycle and buildAppPageCachedResponse
so both fresh renders and cache hits honor the skip header. Parse the
incoming X-Vinext-Router-Skip header at the handler scope and thread
the resulting set through render and ISR.

Gate the filter behind supportsFilteredRscStream: false in the generated
entry so this PR is dormant at runtime until the canonical-stream story
is validated. Tests exercise the filter directly by injecting the skip
set into renderAppPageLifecycle options.
Introduce buildSkipHeaderValue and createRscNavigationRequestHeaders
in app-elements. The client-side browser entry now uses the centralized
request-header builder for navigation fetches and forwards the current
layout flags so subsequent navigations can signal which static layouts
the server may omit from the response body.
… RSC entry

Introduce a Rollup generateBundle hook that patches the __VINEXT_CLASS
stub in the generated RSC entry with a real dispatch table built from
Layer 1 segment-config analysis and Layer 2 module-graph classification.
The runtime probe loop in app-page-execution.ts consults this table and
skips the dynamic-isolation probe for layouts we proved static or
dynamic at build time.

Add route-classification-manifest.ts as the codegen glue between the
classifier and the entry template, and flow buildTimeClassifications
through renderAppPageLifecycle so the runtime probe can honor the
build-time decision. Fail loudly if generateBundle sees __VINEXT_CLASS
referenced without the recognized stub body, so generator and plugin
cannot silently drift.
…CLASSIFICATION

Operators tracing why a layout was flagged static or dynamic can opt in
with VINEXT_DEBUG_CLASSIFICATION=1 at build time. When active, the
plugin patches a second stub (__VINEXT_CLASS_REASONS) with a dispatch
table that returns per-layout ClassificationReason structures. The
runtime probe loop emits a debug line per layout when
debugClassification is wired, and the hot path remains allocation-free
when debug is off because the stub returns null.

The reasons machinery (types, report.ts tagged results, and
buildReasonsReplacement) already shipped in the previous PR; this PR
just enables the emission path and wires the debug consumer into the
runtime probe.
@NathanDrake2406 NathanDrake2406 force-pushed the chore/pr-768-6-classification-debug-reasons branch from 5355568 to 54296aa Compare April 15, 2026 03:48
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 15, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@843

commit: 54296aa

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