Skip to content

fix(app-router): use experimental React channel#2372

Closed
james-elicx wants to merge 16 commits into
mainfrom
codex/fix-react-experimental-parity
Closed

fix(app-router): use experimental React channel#2372
james-elicx wants to merge 16 commits into
mainfrom
codex/fix-react-experimental-parity

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • select Next.js's vendored experimental React channel when experimental.taint, experimental.transitionIndicator, or experimental.gestureTransition is enabled
  • align React, ReactDOM, and the Flight runtime across RSC, SSR, and client Vite environments
  • adapt Next's vendored Flight runtime to plugin-rsc's module loader without affecting other modules
  • add focused unit and production build/preview coverage ported from Next.js v16.2.6

Next.js parity

Matches test/e2e/app-dir/rsc-basic/rsc-basic-react-experimental.test.ts from Next.js v16.2.6.

Validation

  • vp check packages/vinext/src/config/experimental-react.ts packages/vinext/src/config/next-config.ts packages/vinext/src/index.ts tests/experimental-react.test.ts tests/app-router-experimental-react.test.ts tests/fixtures/app-experimental-react/app/client-react.tsx
  • VINEXT_TEST_NEXT_PACKAGE_DIR=... vp test run tests/experimental-react.test.ts tests/app-router-experimental-react.test.ts --maxWorkers=1 — 9/9 passed
  • targeted Next.js deploy suite, concurrency 1 — 1/1 passed
  • repeated independent review loop — final result: no findings

Overlap

PR #2167 accepts an explicit config option but intentionally does not implement runtime channel parity. This PR follows Next.js v16.2.6's actual automatic selection and runtime behavior.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2372
npm i https://pkg.pr.new/vinext@2372

commit: 85cdd07

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Solid, focused PR. I checked out the branch, ran the unit + production build/preview tests (tests/experimental-react.test.ts 8/8, tests/app-router-experimental-react.test.ts 1/1), and vp check on the touched files — all green. The design composes correctly with @vitejs/plugin-rsc's own rsc:react-server-dom-webpack-alias (both pre, and this.resolve(..., {skipSelf:true}) lets the experimental resolveId intercept the rewritten react-server-dom-webpack/* specifier). I verified the runtime details against the installed Next package:

  • __NEXT_EXPERIMENTAL_REACT is the correct env var Next uses to switch its server runtime (dist/esm/server/route-modules/app-*/module.compiled.js). Setting it via define is right.
  • Next's vendored Flight runtime uses globalThis.__next_require__ (not __webpack_require__), so plugin-rsc's __webpack_require__ → __vite_rsc_require__ transform wouldn't touch it. The added __next_require__ → __vite_rsc_require__ transform is necessary and the flight-dir prefix check correctly covers the cjs/ subdir where those tokens live.
  • All 28 specifiers resolve (non-null) for rsc/ssr/client against the current Next layout, so the production path is sound.

A few things worth addressing:

1. Dev mode: dep optimizer bypasses the experimental aliases (likely dual-React in dev)

The new aliasing lives only in the main plugin's resolveId. But the dev dep optimizer uses its own Rolldown pipeline that does not see the main plugin hooks — that's exactly why depOptimizeAliasPlugin exists (index.ts:2358, see the comment at index.ts:2354-2357 and #834). The experimental aliases are not registered there, and the RSC env still force-includes react-server-dom-webpack/static.edge in optimizeDeps.include (index.ts:2456). In vp dev with experimental.taint enabled, the prebundled deps would resolve stable React while the source graph resolves experimental React → version mismatch / "Invalid hook call" risk in dev.

The PR only adds a production createBuilder + preview test, so this gap is untested. Either (a) add the experimental aliases to depOptimizeAliasPlugin (and ensure the prebundled static.edge resolves to the experimental copy), or (b) add a dev-mode test for the fixture to confirm dev works. At minimum, please confirm dev behavior — most users hit vp dev first.

2. Non-null assertions will throw an opaque error if Next changes its exports

createExperimentalReactEnvironmentAliases ends with three resolveExperimentalReactSpecifier(...)! (and the hardcoded index.js for the flight runtime). Today every specifier resolves, but if a future Next vendored package drops/renames a condition, these ! turn into a cryptic Cannot read properties of null (reading 'replace') deep in Vite. Given resolveExperimentalReactAliases already throws nice [vinext] ... errors when a package is missing, consider a matching explicit guard here instead of !.

3. react-server-dom-webpack-experimental alias hardcodes index.js

The other two compiled aliases go through resolveExperimentalReactSpecifier (respecting export conditions), but the flight one is hardcoded to path.join(packages["react-server-dom-webpack"], "index.js"). That happens to match the package's "." : "./index.js", but it's an inconsistency that will silently break if the . export ever becomes conditional. Routing it through resolveExperimentalReactSpecifier(packages, "react-server-dom-webpack", environment) would be more robust and consistent.

4. Minor

  • The transform handler returns a bare string (no sourcemap); plugin-rsc's equivalent returns { code, map: null }. Harmless but worth matching for consistency / to avoid Vite's missing-sourcemap heuristics.
  • needsExperimentalReact is correctly ignoring the legacy useExperimentalReact flag from #2167 (the test [{ useExperimentalReact: false, taint: true }, true] documents that). Good — just flagging it's intentional divergence from that PR's config-only knob, consistent with the PR description.

Nothing here blocks the production path, which matches Next v16.2.6 behavior. Item #1 (dev parity) is the one I'd want resolved or explicitly confirmed before merge, per the repo's dev/prod parity guidance in AGENTS.md.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

To https://github.com/cloudflare/vinext.git
! [rejected] codex/fix-react-experimental-parity -> codex/fix-react-experimental-parity (fetch first)
error: failed to push some refs to 'https://github.com/cloudflare/vinext.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

github run

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 85cdd07 against base de014d4 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 1 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 117.1 KB 117.1 KB ⚫ +0.0%
Client entry size (gzip) vinext 112.5 KB 112.5 KB ⚫ +0.0%
Dev server cold start vinext 2.47 s 2.49 s ⚫ +0.9%
Production build time vinext 2.97 s 3.02 s 🔴 +1.9%
RSC entry closure size (gzip) vinext 93.0 KB 93.0 KB ⚫ +0.0%
Server bundle size (gzip) vinext 156.9 KB 157.0 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed after the three follow-up commits (align experimental React in dev, resolve experimental React for fixtures, cover experimental React edge cases). I checked out the branch, ran the suites against the installed Next (16.2.7), and verified runtime details against the vendored packages.

Verification

  • vp test run tests/experimental-react.test.ts — 10/10 ✅
  • vp test run tests/app-router-experimental-react.test.ts — 4/4 ✅ (build+preview, dev optimizer, and both ssr.external rejection cases)
  • vp check on all touched files — clean ✅
  • Confirmed __NEXT_EXPERIMENTAL_REACT is the correct switch in dist/esm/server/route-modules/app-*/module.compiled.js.
  • Confirmed the vendored Flight runtime uses globalThis.__next_require__ only under the cjs/ subdir, so the prefix-scoped transform is correctly targeted, and __vite_rsc_require__ is a plugin-rsc global defined in all environments (plugin-rsc's own __webpack_require__ rewrite has no env filter), so the new __next_require__ rewrite is consistent and safe across rsc/ssr/client.

The four items from the prior review are all resolved:

  1. Dev parity — addressed via the vinext:experimental-react-dev-optimizer plugin that rewrites optimizeDeps.include, registers Rolldown + esbuild resolvers, strips React from resolve.external, and disables externalizeSsrReactInDev. Backed by the new dev-mode test (uses the experimental React channel in the dev dependency optimizer).
  2. Non-null assertions — replaced with requireExperimentalReactSpecifier, which throws an explicit [vinext] ... error per environment.
  3. Hardcoded index.js — the Flight alias now routes through requireExperimentalReactSpecifier(packages, "react-server-dom-webpack", environment), respecting export conditions.
  4. Sourcemap — the transform now returns { code, map: null }, matching plugin-rsc.

Remaining observations (non-blocking):

  • selectPackageExport short-circuits on the first matching condition without falling back. In experimental-react.ts:193-195, once a condition key matches, it recurses and returns even if the nested lookup yields null, instead of continuing to the next condition (e.g. default). This is harmless against the current Next.js layout (all conditional exports are a single flat level), but it diverges from Node's conditional-exports algorithm and would silently mis-resolve if Next ever nests conditions under react-server. Worth a follow-up or a comment noting the assumption.

  • optimizeDeps.include rewriting depends on plugin-rsc's hasReactServerDomWebpack branch. The dev optimizer maps @vitejs/plugin-rsc/vendor/react-server-dom/*react-server-dom-webpack/* before alias matching. Today vinext forces react-server-dom-webpack into ssr.noExternal, so plugin-rsc emits bare react-server-dom-webpack/* specifiers and the replace is a no-op that still matches. The logic correctly covers both forms, but it's an implicit coupling to plugin-rsc internals — fine for now, just fragile if either side changes the vendor naming.

  • Replacing optimizeDeps.include specifiers with absolute file paths works (the dev test proves it), but is an unusual use of include. No action needed; flagging for awareness.

Nothing blocks merge. Production path matches Next v16.2.6 behavior, and the dev gap raised in the prior review is now closed and tested.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372. Summary of findings:

Review outcome: no blocking issues

The PR is solid. I re-reviewed after the three follow-up commits and confirmed all four items from the prior ask-bonk review are resolved:

  1. Dev parity — fixed via a new vinext:experimental-react-dev-optimizer plugin (rewrites optimizeDeps.include, registers Rolldown + esbuild resolvers, strips React from resolve.external, disables externalizeSsrReactInDev), now covered by a dev-mode test.
  2. Non-null assertions — replaced with requireExperimentalReactSpecifier that throws explicit [vinext] errors.
  3. Hardcoded index.js — Flight alias now routes through condition-aware resolution.
  4. Missing sourcemap — transform now returns { code, map: null }.

Verification: all tests pass (10/10 + 4/4), vp check clean, and I validated the env var name, the __next_require__ transform targeting, and the cross-environment availability of __vite_rsc_require__ against the installed Next package.

Non-blocking observations flagged:

  • selectPackageExport short-circuits on the first matching condition without falling back to default — harmless against current flat Next.js exports, but diverges from Node's resolution algorithm if conditions ever nest.
  • The dev optimizeDeps.include rewrite implicitly couples to plugin-rsc's hasReactServerDomWebpack vendor-naming branch (handles both forms correctly today).
  • Replacing include specifiers with absolute file paths is unusual but proven to work by the dev test.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed after the two commits pushed since the last ask-bonk review (72a6b86 reject external experimental React packages, 577264a preserve stable React dev renderer). The earlier review's "10/10 + 4/4" predates these commits. Running the suite on the current tip surfaces a regression.

🔴 Blocking: tests/app-router-experimental-react.test.ts fails on this branch

Commit 72a6b86 widened the runtime error message in index.ts:1471-1472:

"[vinext] Externalizing React through `ssr.external` or `serverExternalPackages` is incompatible …"

but left the existing assertion in expectExternalReactRejection unchanged (tests/app-router-experimental-react.test.ts:120):

).rejects.toThrow("Externalizing React through `ssr.external` is incompatible");

The old substring no longer matches — `ssr.external` is now followed by or `serverExternalPackages`, not is incompatible. Both tests that call this helper now fail:

FAIL  tests/app-router-experimental-react.test.ts > rejects ssr.external true …
FAIL  tests/app-router-experimental-react.test.ts > rejects React entries in ssr.external
Expected: "Externalizing React through `ssr.external` is incompatible"
Received: "[vinext] Externalizing React through `ssr.external` or `serverExternalPackages` is incompatible …"

Test Files  1 failed (1)
      Tests  2 failed | 3 passed (5)

These two cases are not gated behind VINEXT_TEST_NEXT_PACKAGE_DIR (they call createServer directly and reject before any Next resolution), so they run — and fail — in the standard Vitest CI job. This will block the required Vitest check.

Fix: update the assertion at line 120 to a substring that survives the new wording, e.g. "Externalizing React through `ssr.external`" (drop the trailing is incompatible), matching the style of the new serverExternalPackages test at line 145 ("serverExternalPackages is incompatible").

Other observations (non-blocking)

  1. No test for the new Pages Router renderer change (577264a). createPagesRenderToReadableStream (dev-server.ts:92) now selects a statically-imported nativeRenderToReadableStream for the stable path and only routes through the dev runner when useExperimentalReact is true. The rationale is sound (stable dev keeps both the renderer and page components on Node-resolved React via externalizeSsrReactInDev), but there is no test exercising either branch. Given AGENTS.md's dev/prod parity emphasis and that this touches every Pages Router dev render + error page, a focused test (stable + experimental) would be worth adding.

  2. useExperimentalReact is computed for Pages-only apps too, but aliases are App-Router-gated. nextConfig.useExperimentalReact is set unconditionally from experimental (next-config.ts:1666), while experimentalReactAliases is only built if (hasAppDir && …) (index.ts:1461). For a Pages-only app with experimental.taint, createSSRHandler receives useExperimentalReact = true and loads react-dom/server.edge via the runner, but no aliasing is active so it resolves stable React — which happens to be consistent (because externalizeSsrReactInDev is also true when experimentalReactAliases is null). It works today, but the coupling is implicit; a comment or a Pages-aware gate would make the invariant explicit.

  3. Duplicated specifier-matching after the isExperimentalReactSpecifier extraction. Commit 72a6b86 extracted the EXPERIMENTAL_REACT_SPECIFIERS.some(...) pattern into isExperimentalReactSpecifier, but the dev optimizer's resolve.external filter at index.ts:5793-5795 still inlines the same logic. Reusing the helper there would keep the two in sync.

  4. selectPackageExport first-match-wins (experimental-react.ts:199-202) returns once a condition key is present even if the nested lookup is null, rather than continuing to default. Confirmed harmless against the current vendored layout (all conditional exports — e.g. react-dom/server.edge{ "react-server", "default" } — are a single flat level), but it diverges from Node's resolution and would mis-resolve under nested conditions. Worth a comment noting the assumption.

The production design remains sound and matches Next v16.2.6. The single blocking item is the stale error-message assertion — once line 120 is updated, the suite is green.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372.

Summary of findings

🔴 One blocking issue — a test regression that will fail CI's Vitest check:

Commit 72a6b86 widened the runtime error message in index.ts:1471 to "... \ssr.external` or `serverExternalPackages` is incompatible ...", but left the existing assertion at tests/app-router-experimental-react.test.ts:120asserting the old substring"Externalizing React through `ssr.external` is incompatible"`. The substring no longer matches, so two ungated tests fail:

Tests  2 failed | 3 passed (5)

I reproduced this by running the suite on the current branch tip. The prior ask-bonk reviews reported green because they predate the two most recent commits (72a6b86, 577264a).

Fix: trim the assertion to "Externalizing React through \ssr.external`"`.

Non-blocking observations:

  1. The new Pages Router renderer change (577264a) has no test coverage for either branch.
  2. useExperimentalReact is computed for Pages-only apps but aliasing is App-Router-gated — works today via an implicit coupling, but the invariant is undocumented.
  3. Duplicated specifier-matching at index.ts:5793 could reuse the newly-extracted isExperimentalReactSpecifier.
  4. selectPackageExport first-match-wins diverges from Node resolution (harmless against the current flat exports).

The production design is sound and matches Next v16.2.6; the suite goes green once line 120 is updated.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed at branch tip (85932ec), which adds two commits since the last ask-bonk review: fbbdb9a match experimental React external error and 85932ec gate experimental renderer on aliases. I checked out the branch, ran the affected suites against the installed Next (16.2.7), type-checked the package, and traced the dev renderer / aliasing invariants.

✅ The prior blocking issue is resolved

The stale assertion at tests/app-router-experimental-react.test.ts:120 is fixed (commit fbbdb9a). It now asserts the substring "Externalizing React through `ssr.external` or `serverExternalPackages`", which matches the widened runtime error at index.ts:1471-1473. Both previously-failing ungated rejection cases now pass.

Verification (all green):

  • tests/app-router-experimental-react.test.ts5/5 (build+preview, dev optimizer, both ssr.external rejection cases, serverExternalPackages rejection)
  • tests/experimental-react.test.ts + tests/next-config.test.ts219/219
  • tests/pages-router.test.ts318/318 (stable Pages renderer path intact)
  • tests/app-router-dev-server.test.ts optimizeDeps test — pass
  • tsc --noEmit on the vinext package — clean

✅ Prior observation #2 (Pages renderer coupling) is now addressed

Commit 85932ec changes the dev Pages SSR renderer gate at index.ts:4468 from nextConfig?.useExperimentalReact to experimentalReactAliases !== null. This is the correct fix and makes the previously-implicit invariant explicit:

  • experimentalReactAliases !== null (App Router + experimental flag): createPagesRenderToReadableStream returns the runner-loaded react-dom/server.edge (aliased → experimental), and externalizeSsrReactInDev is false (index.ts:2358-2362), so page components also resolve aliased experimental React. Consistent.
  • experimentalReactAliases === null (everything else, incl. a Pages-only app with experimental.taint): renderer returns the statically-imported nativeRenderToReadableStream (Node-stable), and externalizeSsrReactInDev is true, so page components are Node-externalized stable React. Consistent.

Both the renderer and the page tree now key off the same condition, so the Pages-only-app divergence flagged previously can no longer occur. createSSRHandler is dev-only (single call site at index.ts:4453); the production Pages path lives in prod-server.ts/worker-entry.ts and bakes the aliases at build time, so there's no parity gap to mirror.

Non-blocking observations (carried forward, unchanged)

  1. Duplicated specifier-matching. The dev optimizer's resolve.external filter still inlines EXPERIMENTAL_REACT_SPECIFIERS.some(...) at index.ts:5793-5795 instead of reusing the extracted isExperimentalReactSpecifier helper. Minor; reusing it keeps the two in sync.

  2. selectPackageExport first-match-wins (experimental-react.ts:199-202) returns once a condition key is present even if the nested lookup resolves to null, rather than continuing to default. Harmless against the current flat vendored exports, but diverges from Node's conditional-exports algorithm and would mis-resolve under nested conditions. Worth a comment noting the assumption.

  3. optimizeDeps.include rewrite still couples implicitly to plugin-rsc's hasReactServerDomWebpack vendor naming (handles both @vitejs/plugin-rsc/vendor/react-server-dom/* and bare react-server-dom-webpack/*). Fine today; fragile if either side changes the naming.

Outcome: no blocking issues

The blocking test regression from the last review is fixed and the suite is green. The production design matches Next v16.2.6, and the dev renderer is now gated on the actual aliasing state. The remaining items are all non-blocking follow-ups.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372.

Summary

I re-reviewed PR #2372 at the branch tip (85932ec), which has two new commits since the last ask-bonk review.

Outcome: no blocking issues.

The blocking test regression flagged in the prior review is resolved:

  • Commit fbbdb9a updated the stale assertion at tests/app-router-experimental-react.test.ts:120 to match the widened error message in index.ts:1471. I confirmed all 5 tests in that file pass (including the two ungated ssr.external rejection cases that were failing).

The prior non-blocking observation #2 (implicit Pages-router coupling) is also addressed:

  • Commit 85932ec changed the dev Pages SSR renderer gate from nextConfig?.useExperimentalReact to experimentalReactAliases !== null (index.ts:4468). I verified this aligns the renderer with externalizeSsrReactInDev (index.ts:2358-2362), so the renderer and page-component React channel always match.

Verification (all green): experimental-react (5/5), unit + next-config (219/219), pages-router (318/318), the dev-server optimizeDeps test, and a clean tsc --noEmit.

Remaining items are non-blocking follow-ups: duplicated specifier-matching at index.ts:5793, selectPackageExport first-match-wins, and the plugin-rsc vendor-naming coupling in the optimizer rewrite.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed at branch tip (737aaec), which adds three commits since the last ask-bonk review: dd68992 interop experimental React in dev, 03d23d5 allow React dev eval in CSP fixture, 737aaec align dynamic CSP expectation. The prior "no blocking issues" review predates all three. Running the affected suite on the current tip surfaces a regression.

🔴 Blocking: tests/nextjs-compat/use-client-page-pathname.test.ts fails on this branch

Commit 03d23d5 widened the CSP header that app-basic's shared middleware sets for the /use-client-page-pathname?csp-nonce branch (tests/fixtures/app-basic/middleware.ts:293-296):

r.headers.set(
  "content-security-policy",
  "script-src 'nonce-vinext-test-nonce' 'strict-dynamic' 'unsafe-eval';",
);

The commit updated the two assertions that exercise this fixture in tests/e2e/.../csp.spec.ts and tests/nextjs-compat/dynamic.test.ts, but missed the assertion at tests/nextjs-compat/use-client-page-pathname.test.ts:107-108, which still expects the pre-change strict value:

const res = await fetch(`${_baseUrl}${ROUTE}?csp-nonce=1`);   // ROUTE = "/use-client-page-pathname"
expect(res.headers.get("content-security-policy")).toBe(
  "script-src 'nonce-vinext-test-nonce' 'strict-dynamic';",   // <- no 'unsafe-eval'
);

Since the middleware now emits 'unsafe-eval' for exactly this path+query, the assertion no longer matches. I reproduced it on the current tip:

FAIL  tests/nextjs-compat/use-client-page-pathname.test.ts > adds CSP nonce to inline hydration and bootstrap scripts
Expected: "script-src 'nonce-vinext-test-nonce' 'strict-dynamic';"
Received: "script-src 'nonce-vinext-test-nonce' 'strict-dynamic' 'unsafe-eval';"

Test Files  1 failed (1)
      Tests  1 failed | 10 skipped (11)

This test is not gated behind VINEXT_TEST_NEXT_PACKAGE_DIR — it hits the app-basic dev fixture directly and runs in the standard Vitest CI job. This will block the required Vitest check.

Fix: update line 108 to include 'unsafe-eval', matching the sibling assertions updated in dynamic.test.ts:147 and csp.spec.ts:

"script-src 'nonce-vinext-test-nonce' 'strict-dynamic' 'unsafe-eval';",

The other tests that hit this fixture without ?csp-nonce (use-client-page-pathname.test.ts:138/:149, script-nonce.test.ts) are unaffected and pass — confirmed locally.

Worth flagging: why the dev CSP now needs 'unsafe-eval'

The root cause of the CSP loosening is that app-basic's next.config.ts has experimental.gestureTransition: true, so needsExperimentalReact() is true and the whole fixture now runs on the experimental React channel. In vp dev, commit dd68992 resolves the client React alias to the vendored *.development.js CJS build and runs it through a second commonjs() pass; serving that under Vite dev requires 'unsafe-eval', hence the fixture/CSP changes.

This is a real, user-visible behavioral change: any app using the experimental React channel (taint / transitionIndicator / gestureTransition) will need 'unsafe-eval' in its dev CSP. Two things to confirm before merge:

  1. This is dev-only. The production CSP assertions in tests/app-router-production-server.test.ts are unchanged and still strict — good — but please confirm the production experimental build does not pull in an eval-using dev bundle.
  2. Whether this matches Next.js dev behavior. Next.js dev also injects an 'unsafe-eval' allowance for React Refresh, so this is likely parity-correct, but it's worth a one-line note in the PR since it changes the dev security posture for experimental-channel apps.

Non-blocking observations (carried forward)

  1. resolveExperimentalReactDevelopmentEntry regex parsing is fragile (experimental-react.ts:99-105). It scrapes module.exports = require('./cjs/*.development.js') out of the NODE_ENV wrapper. It correctly falls back to the original entry on no-match (so non-wrapper exports are safe), but it silently depends on the exact if (production) … else { module.exports = require(...) } shape of Next's vendored wrapper. A comment noting the assumption (and that the fallback is intentional) would help.
  2. selectPackageExport first-match-wins (experimental-react.ts:207-216) — unchanged from prior reviews; harmless against the current flat vendored exports but diverges from Node's conditional-exports resolution.
  3. Duplicated specifier-matching at index.ts:5793 still inlines EXPERIMENTAL_REACT_SPECIFIERS.some(...) instead of reusing isExperimentalReactSpecifier.

The production design remains sound and matches Next v16.2.6. The single blocking item is the stale CSP assertion at use-client-page-pathname.test.ts:108 — once that line is updated the suite goes green.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372.

Summary

I re-reviewed PR #2372 at the branch tip (737aaec), which has three new commits since the last ask-bonk review (dd68992, 03d23d5, 737aaec). The prior "no blocking issues" verdict predates all of them.

🔴 One blocking issue — a test regression that will fail CI's Vitest check:

Commit 03d23d5 added 'unsafe-eval' to the CSP that app-basic's shared middleware sets for the /use-client-page-pathname?csp-nonce path (tests/fixtures/app-basic/middleware.ts:293-296). It updated the matching assertions in dynamic.test.ts and csp.spec.ts, but missed tests/nextjs-compat/use-client-page-pathname.test.ts:107-108, which still asserts the old strict value. I reproduced the failure on the current tip:

Expected: "script-src 'nonce-vinext-test-nonce' 'strict-dynamic';"
Received: "script-src 'nonce-vinext-test-nonce' 'strict-dynamic' 'unsafe-eval';"

This test is ungated (runs against the app-basic dev fixture in the standard Vitest job), so it blocks the required Vitest check. Fix: add 'unsafe-eval' to line 108.

Also flagged (worth confirming, not strictly blocking): the root cause is that app-basic has experimental.gestureTransition: true, so the whole fixture now uses the experimental React channel, and the dev interop (dd68992) loads the eval-using *.development.js CJS bundle — meaning any experimental-channel app now needs 'unsafe-eval' in its dev CSP. Asked the author to confirm this is dev-only (prod assertions remain strict) and matches Next.js dev behavior.

Non-blocking: fragile regex in resolveExperimentalReactDevelopmentEntry, selectPackageExport first-match-wins, and duplicated specifier-matching at index.ts:5793.

The production design is sound and matches Next v16.2.6; the suite goes green once line 108 is updated.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed at branch tip (d39f2bc), which adds two commits since the last ask-bonk review: 4537fd4 align navigation CSP expectation and d39f2bc scope unsafe eval CSP to dev. The prior review's blocking item (stale CSP assertion at use-client-page-pathname.test.ts:108) is now resolved — and the fix is better than the one I previously suggested.

✅ The prior blocking issue is resolved (and improved)

Rather than just appending 'unsafe-eval' to the test assertion unconditionally, the author scoped the fixture middleware's 'unsafe-eval' to dev only (d39f2bc, tests/fixtures/app-basic/middleware.ts:293,331):

const devEvalSource = process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : "";
r.headers.set(
  "content-security-policy",
  `script-src 'nonce-vinext-test-nonce' 'strict-dynamic'${devEvalSource};`,
);

This keeps the production CSP strict while allowing the eval-using dev bundle, which is the correct behavioral truth: experimental React's dev CJS bundle needs 'unsafe-eval', but the production build does not. The matching dev assertion at use-client-page-pathname.test.ts:108 (4537fd4) now expects the dev value.

Verification (all green)

  • tests/nextjs-compat/use-client-page-pathname.test.ts11/11 (the previously-failing csp-nonce case now passes; dev server sets NODE_ENV=development)
  • tests/nextjs-compat/dynamic.test.ts12/12 (dev, expects 'unsafe-eval')
  • tests/experimental-react.test.ts11/11
  • tests/app-router-experimental-react.test.ts5/5 (build+preview, dev optimizer, both rejection cases)
  • tests/app-router-production-server.test.ts CSP-nonce cases — 6/6, all strict ('nonce-… 'strict-dynamic'; with NO 'unsafe-eval'), against a real production build of the app-basic fixture (which has experimental.gestureTransition: true, so the whole fixture runs on the experimental channel)
  • vp check on all touched files — clean

This directly answers the two confirmation requests from the prior review:

  1. The change is dev-only. Production CSP assertions remain strict and pass against the experimental-channel production build — confirming the production build does not pull in an eval-using bundle. ✅
  2. The DEV E2E project (csp.spec.ts) runs vp dev (documented at csp.spec.ts:13), so its 'unsafe-eval' assertions are correct; the production nonce-on-preload assertions live in the prod-server Vitest suite, which stays strict. ✅

Worth flagging (non-blocking)

  • The fixture middleware now keys CSP on process.env.NODE_ENV. This works because Vite's createServer sets NODE_ENV=development (dev tests), while the prod-server build/Vitest worker does not. It's empirically validated by both suites passing, but it's an implicit coupling to Vite's env mutation. It's test-fixture code, not production logic, so this is fine — just noting the mechanism.
  • User-visible dev behavior change: any app using the experimental React channel (taint / transitionIndicator / gestureTransition) will need 'unsafe-eval' in its dev CSP. This matches Next.js dev (which already allows 'unsafe-eval' for React Refresh). A one-line note in the PR description would help future readers.

Non-blocking observations (carried forward, unchanged)

  1. Duplicated specifier-matching at index.ts:5809-5811 still inlines EXPERIMENTAL_REACT_SPECIFIERS.some(...) instead of reusing the extracted isExperimentalReactSpecifier helper. Minor; reusing it keeps the two in sync.
  2. selectPackageExport first-match-wins (experimental-react.ts:212-215): once a condition key is present it recurses and returns even if the nested lookup is null, rather than continuing to default. Harmless against the current flat vendored exports, but diverges from Node's conditional-exports resolution and would mis-resolve under nested conditions. Worth a comment noting the assumption.
  3. resolveExperimentalReactDevelopmentEntry regex (experimental-react.ts:101-104) scrapes the NODE_ENV wrapper; it safely falls back to the original entry on no-match but silently depends on Next's vendored wrapper shape. A comment noting the intentional fallback would help.

Outcome: no blocking issues

The blocking CSP-assertion regression from the last review is fixed, and the fix correctly scopes 'unsafe-eval' to dev while keeping production strict. The full affected suite is green, the production design matches Next v16.2.6, and dev/prod parity is preserved. The remaining items are non-blocking follow-ups.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372.

Summary

I re-reviewed PR #2372 at branch tip (d39f2bc), which has two new commits since the last ask-bonk review (4537fd4, d39f2bc).

Outcome: no blocking issues.

The prior blocking issue — the stale CSP assertion at use-client-page-pathname.test.ts:108 — is resolved, and the fix is better than what was previously suggested. Instead of unconditionally appending 'unsafe-eval' to the assertion, the author scoped the fixture middleware's 'unsafe-eval' to dev only (process.env.NODE_ENV === "development"), keeping production CSP strict.

Verification (all green, run locally):

  • use-client-page-pathname.test.ts — 11/11
  • dynamic.test.ts — 12/12
  • experimental-react.test.ts — 11/11
  • app-router-experimental-react.test.ts — 5/5
  • app-router-production-server.test.ts CSP cases — 6/6, all strict (no 'unsafe-eval'), against a production build of the experimental-channel app-basic fixture
  • vp check — clean

This confirms the two open questions from the prior review: the change is dev-only, and the production experimental build does not pull in an eval-using bundle.

Remaining items are non-blocking follow-ups (duplicated specifier-matching at index.ts:5809, selectPackageExport first-match-wins, and the dev-entry regex coupling). Production design matches Next v16.2.6 and dev/prod parity is preserved.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(app-router): use experimental React channel

Re-reviewed at branch tip (85cdd07), which adds one commit since the last ask-bonk review: 85cdd07 fix(pages-router): preserve experimental React channel. The prior "no blocking issues" verdict predates it.

Outcome: no blocking issues

The new commit is a correctness fix, and it closes the implicit-coupling gap flagged as non-blocking observation #2 in several prior reviews.

What changed: the experimental-React alias setup at index.ts:1477 is no longer gated on hasAppDir — it now fires whenever nextConfig.useExperimentalReact is true:

- if (hasAppDir && nextConfig.useExperimentalReact) {
+ if (nextConfig.useExperimentalReact) {

Why this is correct (and a real fix): before this commit, a Pages-only app (or a hybrid app with disableAppRouter: true) that set experimental.taint / transitionIndicator / gestureTransition would compute useExperimentalReact = true, but experimentalReactAliases stayed null. Two consequences followed silently:

  • The process.env.__NEXT_EXPERIMENTAL_REACT define is gated on experimentalReactAliases (index.ts:1554), so it was never set — the Next server runtime stayed on the stable channel.
  • No React aliasing was active, so page components resolved stable React despite the user opting into the experimental channel.

That is the exact divergence prior reviews called out. This commit makes the Pages SSR build pick up the experimental channel, and I confirmed the rest of the machinery is already environment-gated rather than hasAppDir-gated, so nothing else needs to change for parity:

  • vinext:experimental-react-channel resolveId/transform gate only on experimentalReactAliases + environment name (index.ts:1205-1237).
  • vinext:experimental-react-dev-optimizer configEnvironment gates on experimentalReactAliases + rsc/ssr/client (index.ts:5795-5798).
  • externalizeSsrReactInDev is !experimentalReactAliases (index.ts:2374-2378), and the dev Pages renderer gate is experimentalReactAliases !== null (index.ts:4484). Both now flip together for a Pages-only experimental app, so renderer and page-tree React channels stay consistent — the same invariant 85932ec established for the App Router case.

The Pages production SSR build runs in the default ssr environment, which the channel resolveId matches, so the production Pages path is aliased too. The always-created rsc aliases (index.ts:1493) are inert for a Pages-only app (no rsc environment), so no harm.

New test is adequate: keeps the experimental React channel in router-disabled hybrid Pages builds resolves config with disableAppRouter: true and asserts both process.env.__NEXT_EXPERIMENTAL_REACT === "true" and that the ssr-environment react specifier resolves into /next/dist/compiled/react-experimental/. That directly covers the previously-broken path.

Verification (all green, run locally against installed Next 16.2.7)

  • tests/app-router-experimental-react.test.ts6/6 (build+preview, dev optimizer, both ssr.external rejection cases, serverExternalPackages rejection, and the new router-disabled hybrid case)
  • tests/experimental-react.test.ts11/11
  • tests/pages-router.test.ts318/318 (stable Pages renderer path intact)
  • tests/nextjs-compat/use-client-page-pathname.test.ts11/11
  • tests/nextjs-compat/dynamic.test.ts12/12
  • tests/app-router-dev-server.test.ts + tests/next-config.test.ts — pass
  • vp check on index.ts, experimental-react.ts, and the test — clean

Non-blocking observations (carried forward, unchanged)

  1. Duplicated specifier-matching at index.ts:5809-5811 still inlines EXPERIMENTAL_REACT_SPECIFIERS.some(...) instead of reusing the extracted isExperimentalReactSpecifier helper. Minor; reusing it keeps the two in sync.
  2. selectPackageExport first-match-wins (experimental-react.ts:212-215): once a condition key is present it recurses and returns even if the nested lookup is null, rather than continuing to default. Harmless against the current flat vendored exports, but diverges from Node's conditional-exports resolution and would mis-resolve under nested conditions. Worth a comment noting the assumption.
  3. resolveExperimentalReactDevelopmentEntry regex (experimental-react.ts:101-104) scrapes the NODE_ENV wrapper; it safely falls back to the original entry on no-match but silently depends on Next's vendored wrapper shape. A comment noting the intentional fallback would help.
  4. Dev 'unsafe-eval' CSP for experimental-channel apps (from earlier commits) is now also reachable by Pages-only experimental apps in dev, since the experimental dev CJS bundle is loaded there too. Production stays strict. Matches Next.js dev (React Refresh already allows 'unsafe-eval'); a one-line PR note would help future readers.

Conclusion

No blocking issues. The latest commit is a sound correctness fix that aligns Pages-only / router-disabled experimental apps with the App Router behavior, and it's covered by a focused test. Production design matches Next v16.2.6 and dev/prod parity is preserved.

@ask-bonk

ask-bonk Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2372.

Summary

I re-reviewed PR #2372 at branch tip (85cdd07), which adds one new commit since the last ask-bonk review: 85cdd07 fix(pages-router): preserve experimental React channel.

Outcome: no blocking issues.

The new commit removes the hasAppDir gate from the experimental-React alias setup (index.ts:1477), so it now fires whenever nextConfig.useExperimentalReact is true. This is a correctness fix that closes the implicit-coupling gap flagged as non-blocking observation #2 in earlier reviews:

  • Before: a Pages-only (or disableAppRouter: true) app with experimental.taint/transitionIndicator/gestureTransition computed useExperimentalReact = true but left experimentalReactAliases = null, so __NEXT_EXPERIMENTAL_REACT was never defined and page components silently resolved stable React.
  • After: the Pages SSR build picks up the experimental channel. I verified the rest of the machinery (resolveId/transform, dev optimizer, externalizeSsrReactInDev, the dev Pages renderer gate) is already environment-gated rather than hasAppDir-gated, so renderer and page-tree React channels stay consistent.

Verification (all green, against installed Next 16.2.7):

  • app-router-experimental-react.test.ts — 6/6 (incl. the new router-disabled hybrid case)
  • experimental-react.test.ts — 11/11
  • pages-router.test.ts — 318/318
  • use-client-page-pathname.test.ts — 11/11, dynamic.test.ts — 12/12
  • app-router-dev-server.test.ts + next-config.test.ts — pass
  • vp check — clean

Remaining items are all non-blocking follow-ups (duplicated specifier-matching, selectPackageExport first-match-wins, the dev-entry regex coupling, and the dev 'unsafe-eval' CSP now reachable by Pages-only experimental apps). Production design matches Next v16.2.6 and dev/prod parity is preserved.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Closing this attempt after reverting it to a zero-diff branch.

The fixture-wide experimental.gestureTransition flag correctly selects Next.js's experimental React channel, but enabling the experimental client runtime caused repeated deterministic App Router history-state regressions across exact-head CI: navigation-regressions, useParams back/forward, useRouter().bfcacheId, intercepted-route restoration, and scroll restoration. The failures recurred across multiple heads with no server exception, while the reverted latest-main baseline passes the targeted navigation suite 12/12.

A safe follow-up needs to make vinext's client navigation/history runtime compatible with the experimental React channel before enabling it globally; keeping a mixed stable-client/experimental-server channel would create hydration/runtime mismatches, so that workaround is not acceptable.

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