fix(build): share one RSC compatibility ID across all plugin instances - #1814
Conversation
Follow-up to #1810. createRscCompatibilityId() falls back to a fresh randomUUID() per plugin instance when no deploymentId is pinned, so a hybrid app+pages build baked two different RSC-compat tokens — the App Router buildApp() instance and the separate Pages Router vite.build() instance each minted their own. The token gates client-side RSC payload acceptance (the X-Vinext-RSC-Compatibility-Id header), so cross-instance divergence is a latent deploy-skew hazard. Coordinate it the same way as the build ID: relocate createRscCompatibilityId to config/next-config.ts (single source of truth, beside resolveBuildId / resolveDeploymentId), have the CLI resolve it once and publish it via __VINEXT_SHARED_RSC_COMPATIBILITY_ID, and have the plugin always adopt it when set. Reuses deploymentId when configured (already stable). The env var is only ever set by the build CLI, so dev and standalone resolution are unchanged. Adds a coordination test asserting the shared token lands in both the server and client output.
|
/bigbonk review for issues |
1 similar comment
|
/bigbonk review for issues |
commit: |
|
There was a problem hiding this comment.
/bigbonk review
Reviewed the change. It correctly mirrors the build-ID coordination just landed in #1810, and is the right fix for the per-instance divergence the #1810 reviewer flagged. No blocking issues found.
Verified
- Single source of truth.
createRscCompatibilityIdmoves fromindex.tsintoconfig/next-config.tsbesideresolveBuildId/resolveDeploymentId, and is exported.randomUUIDis already imported there; inindex.tsrandomUUIDis still used fordraftModeSecret(line 782), so no orphaned import. - Type narrowing is sound. Signature changed from
ResolvedNextConfigtoPick<ResolvedNextConfig, "deploymentId">. The function only readsdeploymentId, so the narrower type is accurate and both call sites (CLI withresolvedNextConfig, plugin withnextConfig) satisfy it. - Resolve-once / always-adopt is preserved. The plugin keeps the original memoization placement (the
??=became an explicit=== undefinedguard outside theif (!nextConfig)block). So it still resolves once per plugin instance across the multi-environmentconfighook calls, and adopts the shared token across instances.length > 0guards an empty string, matching the build-ID adoption shape. - CLI placement matches build-ID exactly, and the "not cleaned up intentionally —
vinext buildruns once then exits" rationale (cli.ts:462-464) covers the new var too. - End-to-end flow is consistent: CLI sets
__VINEXT_SHARED_RSC_COMPATIBILITY_ID-> plugin adopts intorscCompatibilityId-> baked as the__VINEXT_RSC_COMPATIBILITY_IDdefine (index.ts:1339) -> read at runtime inapp-rsc-cache-busting.ts:77and echoed inX-Vinext-RSC-Compatibility-Id. Server and client bake the same value. - Test mirrors the existing build-ID test (distinct sentinel value, env save/restore in
finally, scans full server+client trees for the code-split chunk case). Ran it locally: passes (6.9s).vp checkclean on all 5 changed files. CI is green.
Non-blocking observations
-
No
deploymentIdtest for the compat ID. The build-ID suite has an "adopts shared ID even whengenerateBuildIdis set" test proving the shared value wins over a non-deterministic per-instance resolver. The compat ID has only one non-deterministic input (the missing-deploymentIdrandom-UUID fallback), which the new test covers. WhendeploymentIdis pinned, CLI and plugin both produce the same deterministic value, so they can't diverge — there's no equivalent "side-effecting resolver" hazard. So no extra test is strictly required, but a smalldeploymentId-pinned assertion (shared var ignored / value equalsdeploymentId) would document that branch symmetrically with the build-ID suite. Optional. -
Known parity caveat (not introduced here): this guarantees one consistent token per build, but the token is still non-deterministic across separate
vinext buildinvocations when nodeploymentIdis set — so the deploy-skew check only protects within a single build's output, not across rebuilds of the same commit. That's inherent to the random-UUID fallback and matches the documented build-ID behavior; pinningdeploymentIdis the escape hatch. Worth keeping in mind, nothing to change in this PR.
LGTM.
|
Review posted to PR #1814. SummaryThe change is correct and ready to merge. It cleanly mirrors the build-ID coordination from #1810 to fix the per-instance RSC-compat token divergence the #1810 reviewer flagged. What I verified:
Non-blocking notes (no changes required):
LGTM. |
Follow-up to #1810 (build-ID coordination), addressing the reviewer's note there:
Why it matters
The RSC compatibility token is baked into the client bundle and echoed by the server via the
X-Vinext-RSC-Compatibility-Idresponse header. Browser navigation rejects RSC payloads whose token differs (deploy-skew protection) without exposing the raw build ID.createRscCompatibilityId()falls back torandomUUID()per plugin instance when nodeploymentIdis pinned. A singlevinext buildcan instantiatevinext()more than once — the App RouterbuildApp()(RSC + SSR + client) and the separate Pages Routervite.build()for hybrid app+pages apps — so each instance minted its own token. For App Router today the client and server both come from thebuildApp()instance so they happen to agree, but the divergence with the Pages instance is a latent hazard and inconsistent with the build-ID coordination just landed.Fix
Mirror the build-ID approach exactly:
createRscCompatibilityIdfromindex.tsintoconfig/next-config.ts, besideresolveBuildId/resolveDeploymentId, and export it.deploymentIdwhen configured, else one random UUID) and publishes it via__VINEXT_SHARED_RSC_COMPATIBILITY_ID.createRscCompatibilityId()resolution are unchanged.Verified on the hybrid
app-router-cloudflareexample: the App Router server bundle and client bundle now carry the same RSC-compat token (distinct from the build ID and the draft-mode secret, as intended).Tests
Adds
adopts __VINEXT_SHARED_RSC_COMPATIBILITY_ID across the App Router buildtotests/app-router-production-build.test.ts— sets the shared token, runsbuildApp(), and asserts it lands in both the server and client output (the token can be code-split into a shared chunk, so the test scans the full output trees).