Split remaining "client-node"-only modules into .browser variants (#95366)

Closes https://linear.app/vercel/issue/NAR-860/

Resolves all recent regressions of introducing `next/src/server` into
client-browser bundles.
This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-07-08 09:14:47 +02:00
committed by GitHub
parent 4173ca260e
commit ea16ee224c
10 changed files with 35 additions and 86 deletions
@@ -11,6 +11,8 @@
#[rustfmt::skip]
pub static BROWSER_VARIANT_MODULES: &[&str] = &[
"client/components/client-boundary-params",
"client/components/instant-samples",
"client/components/instant-validation/impl",
"client/components/navigation-dynamic-rendering",
"client/components/server-async-storage",
"client/components/unstable-rethrow",
@@ -7,6 +7,8 @@
// `packages/next/dist` (extension omitted).
export const browserVariantModules = [
'client/components/client-boundary-params',
'client/components/instant-samples',
'client/components/instant-validation/impl',
'client/components/navigation-dynamic-rendering',
'client/components/server-async-storage',
'client/components/unstable-rethrow',
@@ -0,0 +1,3 @@
export const instrumentParamsForClientValidation = undefined
export const expectCompleteParamsInClientValidation = undefined
export const instrumentSearchParamsForClientValidation = undefined
@@ -1,13 +1,13 @@
import type { Params } from '../../request/params'
import type { ReadonlyURLSearchParams } from '../../../client/components/readonly-url-search-params'
import { workUnitAsyncStorage } from '../work-unit-async-storage.external'
import { workAsyncStorage } from '../work-async-storage.external'
import type { Params } from '../../server/request/params'
import type { ReadonlyURLSearchParams } from './readonly-url-search-params'
import { workUnitAsyncStorage } from '../../server/app-render/work-unit-async-storage.external'
import { workAsyncStorage } from '../../server/app-render/work-async-storage.external'
import {
createExhaustiveParamsProxy,
createExhaustiveURLSearchParamsProxy,
trackMissingSampleErrorAndThrow,
} from './instant-samples'
import { InstantValidationError } from './instant-validation-error'
} from '../../server/app-render/instant-validation/instant-samples'
import { InstantValidationError } from '../../server/app-render/instant-validation/instant-validation-error'
export function instrumentParamsForClientValidation<TPArams extends Params>(
underlyingParams: TPArams
@@ -1,29 +1,9 @@
'use client'
// This facade ensures that the boundary code is DCE'd in browser bundles.
//
// It also exists to satisfy `browser-chunks.test.ts`, which looks for
// references to code in `packages/next/src/server` in browser bundles and errors if it finds any.
// A "use client" module seems to always have always have an entry in the browser bundle,
// so this module cannot be colocated with the rest of the instant validation code,
// because it ends up looking like it's importing server code in the browser
// even though all the server code inside is actually DCE'd.
const {
InstantValidationBoundaryContext,
PlaceValidationBoundaryBelowThisLevel,
RenderValidationBoundaryAtThisLevel,
SlotMarker,
} =
typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS
? // TODO(browser-variant): migrate to a .ts/.browser.ts split so the browser bundle drops the server branch; see scripts/generate-browser-variant-aliases.mjs
// ast-grep-ignore: no-typeof-window-require-tsx
(require('../../../server/app-render/instant-validation/boundary-impl') as typeof import('../../../server/app-render/instant-validation/boundary-impl'))
: ({} as typeof import('../../../server/app-render/instant-validation/boundary-impl'))
// We can't fork a `use client` boundary based on node-client vs browser-client.
// We need to fork one level deeper.
export {
InstantValidationBoundaryContext,
PlaceValidationBoundaryBelowThisLevel,
RenderValidationBoundaryAtThisLevel,
SlotMarker,
}
} from './impl'
@@ -0,0 +1,4 @@
export const InstantValidationBoundaryContext = null
export const PlaceValidationBoundaryBelowThisLevel = null
export const RenderValidationBoundaryAtThisLevel = null
export const SlotMarker = null
@@ -0,0 +1,6 @@
export {
InstantValidationBoundaryContext,
PlaceValidationBoundaryBelowThisLevel,
RenderValidationBoundaryAtThisLevel,
SlotMarker,
} from '../../../server/app-render/instant-validation/boundary-impl'
@@ -33,6 +33,10 @@ import { ErrorBoundary } from './error-boundary'
import { disableSmoothScrollDuringRouteTransition } from '../../shared/lib/router/utils/disable-smooth-scroll'
import { RedirectBoundary } from './redirect-boundary'
import { HTTPAccessFallbackBoundary } from './http-access-fallback/error-boundary'
import {
InstantValidationBoundaryContext,
RenderValidationBoundaryAtThisLevel,
} from './instant-validation/boundary'
import { createRouterCacheKey } from './router-reducer/create-router-cache-key'
import {
useRouterBFCache,
@@ -670,10 +674,6 @@ export default function OuterLayoutRouter({
let maybeValidationBoundaryId: string | null = null
if (typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS) {
const { InstantValidationBoundaryContext } =
// TODO(browser-variant): migrate to a .ts/.browser.ts split so the browser bundle drops the server branch; see scripts/generate-browser-variant-aliases.mjs
// ast-grep-ignore: no-typeof-window-require-tsx
require('./instant-validation/boundary') as typeof import('./instant-validation/boundary')
maybeValidationBoundaryId = use(InstantValidationBoundaryContext)
}
@@ -812,10 +812,6 @@ export default function OuterLayoutRouter({
process.env.__NEXT_CACHE_COMPONENTS &&
typeof maybeValidationBoundaryId === 'string'
) {
const { RenderValidationBoundaryAtThisLevel } =
// TODO(browser-variant): migrate to a .ts/.browser.ts split so the browser bundle drops the server branch; see scripts/generate-browser-variant-aliases.mjs
// ast-grep-ignore: no-typeof-window-require-tsx
require('./instant-validation/boundary') as typeof import('./instant-validation/boundary')
templateValue = (
<RenderValidationBoundaryAtThisLevel id={maybeValidationBoundaryId}>
{templateValue}
@@ -27,12 +27,9 @@ const {
instrumentParamsForClientValidation,
instrumentSearchParamsForClientValidation,
expectCompleteParamsInClientValidation,
} =
typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS
? // TODO(browser-variant): migrate to a .ts/.browser.ts split so the browser bundle drops the server branch; see scripts/generate-browser-variant-aliases.mjs
// ast-grep-ignore: no-typeof-window-require
(require('../../server/app-render/instant-validation/instant-samples-client') as typeof import('../../server/app-render/instant-validation/instant-samples-client'))
: {}
} = process.env.__NEXT_CACHE_COMPONENTS
? (require('./instant-samples') as typeof import('./instant-samples'))
: {}
/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
@@ -45,13 +45,11 @@ describe('browser-chunks', () => {
)
})
// These snapshots document which matching modules currently reach browser
// chunks. Some of these we don't intend to act on yet, so we snapshot the
// normalized paths (rather than hard-fail) to surface regressions on review.
it('must not bundle any server modules into browser chunks', () => {
const serverSources = Array.from(
new Set(
sources
// normalizing in case we regress and want to keep track of the regression
.map(normalizeSource)
.filter(
(source) =>
@@ -63,46 +61,7 @@ describe('browser-chunks', () => {
)
).sort()
// This set varies along two axes, so snapshot each combination separately
// rather than forcing them to agree:
// - bundler: webpack's browser chunks contain none of these; Turbopack
// still pulls in a set we haven't acted on yet.
// - cache components: enabling it pulls additional server modules
// (instant-validation, async storage, dynamic rendering) into the
// client render path. CI runs this suite both with and without it
// (see test/cache-components-tests-manifest.json).
const cacheComponents = process.env.__NEXT_CACHE_COMPONENTS === 'true'
if (process.env.IS_TURBOPACK_TEST) {
if (cacheComponents) {
expect(serverSources).toMatchInlineSnapshot(`
[
"src/server/app-render/async-local-storage.ts",
"src/server/app-render/instant-validation/boundary-constants.ts",
"src/server/app-render/instant-validation/boundary-impl.tsx",
"src/server/app-render/instant-validation/instant-samples-client.ts",
"src/server/app-render/instant-validation/instant-samples.ts",
"src/server/app-render/instant-validation/instant-validation-error.ts",
"src/server/app-render/staged-rendering.ts",
"src/server/app-render/work-async-storage-instance.ts",
"src/server/app-render/work-async-storage.external.ts",
"src/server/app-render/work-unit-async-storage-instance.ts",
"src/server/app-render/work-unit-async-storage.external.ts",
"src/server/web/spec-extension/adapters/headers.ts",
"src/server/web/spec-extension/adapters/reflect.ts",
"src/server/web/spec-extension/adapters/request-cookies.ts",
"src/server/web/spec-extension/cookies.ts",
]
`)
} else {
expect(serverSources).toMatchInlineSnapshot(`[]`)
}
} else {
if (cacheComponents) {
expect(serverSources).toMatchInlineSnapshot(`[]`)
} else {
expect(serverSources).toMatchInlineSnapshot(`[]`)
}
}
expect(serverSources).toEqual([])
})
it('must not bundle any dev overlay into browser chunks', () => {