Collect modules with browser variants statically (#95200)

Instead of hardcoding the list, we now automatically detect any
`.browser` sibling and create a compiler alias for that module.

Together with a linting rule, we'll ensure no future regressions are
handled automatically (detection of conditional require and missing
.browser sibling).

This isn't the smoothest DX yet. Ideally we'd generate the list at
Next.js compile time (i.e. before publishing) to avoid having to rerun a
script (I'll follow-up). Generating the list during build time (next dev
or next build) is just another tiny cost users would have to pay.
This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-07-03 08:45:23 +02:00
committed by GitHub
parent 7e2efbc85f
commit d3169de7e5
29 changed files with 440 additions and 41 deletions
@@ -0,0 +1,74 @@
id: no-typeof-window-require
snapshots:
'const x = typeof window !== ''undefined'' ? require(''./browser'') : null':
labels:
- source: require('./browser')
style: primary
start: 42
end: 62
- source: typeof window
style: secondary
start: 10
end: 23
- source: typeof window
style: secondary
start: 10
end: 23
- source: 'typeof window !== ''undefined'' ? require(''./browser'') : null'
style: secondary
start: 10
end: 69
'const x = typeof window === ''undefined'' && FLAG ? require(''./server'').x : {}':
labels:
- source: require('./server')
style: primary
start: 50
end: 69
- source: typeof window
style: secondary
start: 10
end: 23
- source: typeof window
style: secondary
start: 10
end: 23
- source: 'typeof window === ''undefined'' && FLAG ? require(''./server'').x : {}'
style: secondary
start: 10
end: 76
'const x = typeof window === ''undefined'' ? require(''./server'').x : undefined':
labels:
- source: require('./server')
style: primary
start: 42
end: 61
- source: typeof window
style: secondary
start: 10
end: 23
- source: typeof window
style: secondary
start: 10
end: 23
- source: 'typeof window === ''undefined'' ? require(''./server'').x : undefined'
style: secondary
start: 10
end: 75
if (typeof window === 'undefined') { const s = require('./server') }:
labels:
- source: require('./server')
style: primary
start: 47
end: 66
- source: typeof window
style: secondary
start: 4
end: 17
- source: typeof window
style: secondary
start: 4
end: 17
- source: if (typeof window === 'undefined') { const s = require('./server') }
style: secondary
start: 0
end: 68
@@ -0,0 +1,56 @@
id: no-typeof-window-require-tsx
snapshots:
'const x = typeof window === ''undefined'' && FLAG ? require(''./server'').x : {}':
labels:
- source: require('./server')
style: primary
start: 50
end: 69
- source: typeof window
style: secondary
start: 10
end: 23
- source: typeof window
style: secondary
start: 10
end: 23
- source: 'typeof window === ''undefined'' && FLAG ? require(''./server'').x : {}'
style: secondary
start: 10
end: 76
'const x = typeof window === ''undefined'' ? require(''./server'').x : undefined':
labels:
- source: require('./server')
style: primary
start: 42
end: 61
- source: typeof window
style: secondary
start: 10
end: 23
- source: typeof window
style: secondary
start: 10
end: 23
- source: 'typeof window === ''undefined'' ? require(''./server'').x : undefined'
style: secondary
start: 10
end: 75
if (typeof window === 'undefined') { const s = require('./server') }:
labels:
- source: require('./server')
style: primary
start: 47
end: 66
- source: typeof window
style: secondary
start: 4
end: 17
- source: typeof window
style: secondary
start: 4
end: 17
- source: if (typeof window === 'undefined') { const s = require('./server') }
style: secondary
start: 0
end: 68
@@ -0,0 +1,11 @@
id: no-typeof-window-require
valid:
- "const mod = require('./mod')"
- "const mod = cond ? require('./a') : require('./b')"
- "if (enabled) { doThing(require('./a')) }"
- "const x = typeof window === 'undefined' ? serverValue : browserValue"
invalid:
- "const x = typeof window === 'undefined' ? require('./server').x : undefined"
- "const x = typeof window !== 'undefined' ? require('./browser') : null"
- "if (typeof window === 'undefined') { const s = require('./server') }"
- "const x = typeof window === 'undefined' && FLAG ? require('./server').x : {}"
@@ -0,0 +1,9 @@
id: no-typeof-window-require-tsx
valid:
- "const mod = require('./mod')"
- "const mod = cond ? require('./a') : require('./b')"
- "const el = typeof window === 'undefined' ? <Server /> : <Browser />"
invalid:
- "const x = typeof window === 'undefined' ? require('./server').x : undefined"
- "if (typeof window === 'undefined') { const s = require('./server') }"
- "const x = typeof window === 'undefined' && FLAG ? require('./server').x : {}"
@@ -0,0 +1,33 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/ast-grep/ast-grep/main/schemas/rule.json
# Tsx counterpart of no-typeof-window-require.yml (ast-grep rules are
# single-language; keep the two `rule` bodies in sync).
id: no-typeof-window-require-tsx
message: Don't gate a `require()` on `typeof window`.
note: |
Gating a `require()` on `typeof window` bundles the server branch into the
browser bundle. Split the module into `<name>.tsx` (the default, used on the
server and any unbundled runtime) and `<name>.browser.tsx` (the browser
variant). The browser bundle is aliased to the `.browser` sibling
automatically — see scripts/generate-browser-variant-aliases.mjs.
severity: error
language: Tsx
files:
- packages/next/src/**
rule:
pattern: require($$$ARGS)
inside:
stopBy: end
any:
- kind: ternary_expression
has:
field: condition
has:
stopBy: end
pattern: typeof window
- kind: if_statement
has:
field: condition
has:
stopBy: end
pattern: typeof window
@@ -0,0 +1,33 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/ast-grep/ast-grep/main/schemas/rule.json
# Also enforced for .tsx files by no-typeof-window-require-tsx.yml (ast-grep
# rules are single-language; keep the two `rule` bodies in sync).
id: no-typeof-window-require
message: Don't gate a `require()` on `typeof window`.
note: |
Gating a `require()` on `typeof window` bundles the server branch into the
browser bundle. Split the module into `<name>.ts` (the default, used on the
server and any unbundled runtime) and `<name>.browser.ts` (the browser
variant). The browser bundle is aliased to the `.browser` sibling
automatically — see scripts/generate-browser-variant-aliases.mjs.
severity: error
language: TypeScript
files:
- packages/next/src/**
rule:
pattern: require($$$ARGS)
inside:
stopBy: end
any:
- kind: ternary_expression
has:
field: condition
has:
stopBy: end
pattern: typeof window
- kind: if_statement
has:
field: condition
has:
stopBy: end
pattern: typeof window
+5
View File
@@ -230,6 +230,11 @@ jobs:
pnpm lint-no-typescript
pnpm check-examples
pnpm validate-externals-doc
# Regenerate the browser-variant lists and fail if they changed, i.e. a
# `*.browser.{ts,tsx}` sibling was added/removed without running
# `pnpm generate-browser-variant-aliases` and committing the result.
pnpm generate-browser-variant-aliases
git diff --exit-code
stepName: 'lint'
secrets: inherit
@@ -0,0 +1,14 @@
//! This file is @generated by `pnpm generate-browser-variant-aliases`.
//! Do not edit by hand.
//!
//! Modules with a `.browser` sibling. The default module is aliased to its
//! `.browser` variant for the browser bundle in
//! `get_next_client_resolved_map` (see `next_import_map.rs`). Paths are
//! relative to `next/dist` (extension omitted).
// rustfmt::skip keeps this list one-per-line (and matching the generator
// output) instead of collapsing short lists onto a single line.
#[rustfmt::skip]
pub static BROWSER_VARIANT_MODULES: &[&str] = &[
"client/components/unstable-rethrow",
];
+1
View File
@@ -6,6 +6,7 @@ mod app_page_loader_tree;
pub mod app_structure;
mod base_loader_tree;
mod bootstrap;
mod browser_variant_modules;
mod embed_js;
mod emit;
pub mod hmr_entry;
+27 -21
View File
@@ -31,6 +31,7 @@ use turbopack_node::execution_context::ExecutionContext;
use crate::{
app_structure::CollectedRootParams,
browser_variant_modules::BROWSER_VARIANT_MODULES,
embed_js::{VIRTUAL_PACKAGE_NAME, next_js_fs},
mode::NextMode,
next_client::context::ClientContextType,
@@ -568,27 +569,32 @@ pub async fn get_next_client_resolved_map(
root: FileSystemPath,
_mode: NextMode,
) -> Result<Vc<ResolvedMap>> {
// In the browser bundle, swap the default `unstable-rethrow` (which holds the full
// server logic) for its `.browser` sibling. The server-only checks can never occur in
// the browser, and bundling the default would drag server-only modules into the client
// bundle. This is the Turbopack analog of the webpack alias in `create-compiler-aliases.ts`
// and is client-only because `get_next_client_resolved_map` is used only by the client
// context. Matching is on the resolved file path, so it intercepts the relative import
// regardless of which module pulls it in. Anchored at the filesystem root so it matches
// wherever `next` resolves from (node_modules, pnpm store, or monorepo `packages/next`).
let glob_mappings = vec![(
root.root().owned().await?,
Glob::new(
rcstr!("**/next/dist/client/components/unstable-rethrow.js"),
GlobOptions::default(),
)
.to_resolved()
.await?,
request_to_import_mapping(
context_path,
rcstr!("next/dist/client/components/unstable-rethrow.browser"),
),
)];
// In the browser bundle, swap every module that has a `.browser` sibling (see
// BROWSER_VARIANT_MODULES, generated from the filesystem) for that sibling. The default
// module holds the full server logic, and bundling it would drag server-only modules
// into the client bundle. This is the Turbopack analog of the webpack alias in
// `create-compiler-aliases.ts` and is client-only because `get_next_client_resolved_map`
// is used only by the client context. Matching is on the resolved file path, so it
// intercepts the relative import regardless of which module pulls it in. Anchored at the
// filesystem root so it matches wherever `next` resolves from (node_modules, pnpm store,
// or monorepo `packages/next`).
let fs_root = root.root().owned().await?;
let mut glob_mappings = Vec::with_capacity(BROWSER_VARIANT_MODULES.len());
for module in BROWSER_VARIANT_MODULES {
glob_mappings.push((
fs_root.clone(),
Glob::new(
format!("**/next/dist/{module}.js").into(),
GlobOptions::default(),
)
.to_resolved()
.await?,
request_to_import_mapping(
context_path.clone(),
format!("next/dist/{module}.browser").into(),
),
));
}
Ok(ResolvedMap {
by_glob: glob_mappings,
}
+1
View File
@@ -67,6 +67,7 @@
"check-compiler-fixtures": "find crates/next-custom-transforms/tests/fixture -type f -name 'tsconfig.json' -print0 | xargs --null -I'{}' pnpm tsc --noEmit --project '{}'",
"validate-externals-doc": "node ./scripts/validate-externals-doc.js",
"check-unused-turbo-tasks": "node scripts/check-unused-turbo-tasks.mjs",
"generate-browser-variant-aliases": "node scripts/generate-browser-variant-aliases.mjs",
"lint": "run-p test-types lint-typescript prettier-check \"lint-eslint .\" lint-ast-grep lint-language check-unused-turbo-tasks",
"lint-fix": "pnpm prettier-fix && pnpm lint-eslint --fix .",
"lint-language": "alex . --quiet",
@@ -0,0 +1,10 @@
// This file is @generated by `pnpm generate-browser-variant-aliases`.
// Do not edit by hand.
//
// Modules with a `.browser` sibling. The default module is aliased to its
// `.browser` variant for the browser bundle in
// `packages/next/src/build/create-compiler-aliases.ts`. Paths are relative to
// `packages/next/dist` (extension omitted).
export const browserVariantModules = [
'client/components/unstable-rethrow',
] as const
@@ -16,10 +16,8 @@ import {
import type { NextConfigComplete } from '../server/config-shared'
import { defaultOverrides } from '../server/require-hook'
import { hasExternalOtelApiPackage } from './webpack-config'
import {
NEXT_PROJECT_ROOT,
NEXT_PROJECT_ROOT_DIST_CLIENT,
} from './next-dir-paths'
import { NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_DIST } from './next-dir-paths'
import { browserVariantModules } from './browser-variant-modules'
import { shouldUseReactServerCondition } from './utils'
interface CompilerAliases {
@@ -170,15 +168,18 @@ export function createWebpackAliases({
// disable typechecker, webpack5 allows aliases to be set to false to create a no-op module
'private-next-empty-module': false as any,
// In the browser bundle, swap the default `unstable-rethrow` (which holds the
// full server logic) for its `.browser` sibling. The server checks can never
// occur in the browser, and bundling the default would drag server-only modules
// into the client bundle. Server/edge compilers are not aliased and keep the
// default. The trailing `$` is an exact match so it cannot catch `.browser.js`.
[path.join(
NEXT_PROJECT_ROOT_DIST_CLIENT,
'components/unstable-rethrow.js'
) + '$']: 'next/dist/client/components/unstable-rethrow.browser',
// In the browser bundle, swap every module that has a `.browser` sibling
// (see browser-variant-modules.ts, generated from the filesystem) for that
// sibling. The default module holds the full server logic; bundling it would
// drag server-only modules into the client bundle. Server/edge compilers are
// not aliased and keep the default. The trailing `$` is an exact match so it
// cannot catch the `.browser.js` file itself.
...Object.fromEntries(
browserVariantModules.map((moduleId) => [
path.join(NEXT_PROJECT_ROOT_DIST, `${moduleId}.js`) + '$',
`next/dist/${moduleId}.browser`,
])
),
}
: {}),
@@ -302,6 +302,8 @@ function linkClicked(
}
const { dispatchNavigateAction } =
// 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('../components/app-router-instance') as typeof import('../components/app-router-instance')
React.startTransition(() => {
+2
View File
@@ -11,6 +11,8 @@ if (process.env.__NEXT_DEV_SERVER) {
// Browser-only.
if (process.env.__NEXT_EXPOSE_TESTING_API && typeof window !== 'undefined') {
const { startListeningForInstantNavigationCookie } =
// 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('./components/segment-cache/navigation-testing-lock') as typeof import('./components/segment-cache/navigation-testing-lock')
startListeningForInstantNavigationCookie()
}
+2
View File
@@ -217,6 +217,8 @@ if (
typeof window !== 'undefined'
) {
const { createDebugChannel } =
// 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('./dev/debug-channel') as typeof import('./dev/debug-channel')
debugChannel = createDebugChannel(undefined)
@@ -504,6 +504,8 @@ function Router({
// - catch runtime errors and display global-error when necessary
if (typeof window !== 'undefined') {
const { DevRootHTTPAccessFallbackBoundary } =
// 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('./dev-root-http-access-fallback-boundary') as typeof import('./dev-root-http-access-fallback-boundary')
content = (
<DevRootHTTPAccessFallbackBoundary>
@@ -51,19 +51,27 @@ export function ClientPageRoot({
let clientParams: Promise<Params>
const { createSearchParamsFromClient } =
// 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/request/search-params') as typeof import('../../server/request/search-params')
clientSearchParams = createSearchParamsFromClient(searchParams)
const { createParamsFromClient } =
// 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/request/params') as typeof import('../../server/request/params')
clientParams = createParamsFromClient(params)
return <Component params={clientParams} searchParams={clientSearchParams} />
} else {
const { createRenderSearchParamsFromClient } =
// 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('../request/search-params.browser') as typeof import('../request/search-params.browser')
const clientSearchParams = createRenderSearchParamsFromClient(searchParams)
const { createRenderParamsFromClient } =
// 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('../request/params.browser') as typeof import('../request/params.browser')
const clientParams = createRenderParamsFromClient(params)
@@ -37,12 +37,16 @@ export function ClientSegmentRoot({
if (typeof window === 'undefined') {
const { createParamsFromClient } =
// 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/request/params') as typeof import('../../server/request/params')
const clientParams: Promise<Params> = createParamsFromClient(params)
return <Component {...slots} params={clientParams} />
} else {
const { createRenderParamsFromClient } =
// 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('../request/params.browser') as typeof import('../request/params.browser')
const clientParams = createRenderParamsFromClient(params)
return <Component {...slots} params={clientParams} />
@@ -1,6 +1,9 @@
const workAsyncStorage =
typeof window === 'undefined'
? (
? // prettier-ignore
(
// 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/work-async-storage.external') as typeof import('../../server/app-render/work-async-storage.external')
).workAsyncStorage
: undefined
@@ -16,7 +16,9 @@ const {
SlotMarker,
} =
typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS
? (require('../../../server/app-render/instant-validation/boundary-impl') as typeof import('../../../server/app-render/instant-validation/boundary-impl'))
? // 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'))
export {
@@ -671,6 +671,8 @@ 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)
}
@@ -811,6 +813,8 @@ export default function OuterLayoutRouter({
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}>
@@ -143,6 +143,8 @@ function observeVisibility(element: Element, instance: PrefetchableInstance) {
function coercePrefetchableUrl(href: string): URL | null {
if (typeof window !== 'undefined') {
const { createPrefetchURL } =
// 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('./app-router-utils') as typeof import('./app-router-utils')
try {
@@ -330,6 +332,8 @@ function rescheduleLinkPrefetch(
}
const { getCurrentAppRouterState } =
// 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('./app-router-instance') as typeof import('./app-router-instance')
const appRouterState = getCurrentAppRouterState()
@@ -12,6 +12,8 @@ function hasFallbackRouteParams(): boolean {
if (typeof window === 'undefined') {
// AsyncLocalStorage should not be included in the client bundle.
const { workUnitAsyncStorage } =
// 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/work-unit-async-storage.external') as typeof import('../../server/app-render/work-unit-async-storage.external')
const workUnitStore = workUnitAsyncStorage.getStore()
@@ -20,14 +20,20 @@ import {
const useDynamicRouteParams =
typeof window === 'undefined'
? (
? // prettier-ignore
(
// 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/dynamic-rendering') as typeof import('../../server/app-render/dynamic-rendering')
).useDynamicRouteParams
: undefined
const useDynamicSearchParams =
typeof window === 'undefined'
? (
? // prettier-ignore
(
// 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/dynamic-rendering') as typeof import('../../server/app-render/dynamic-rendering')
).useDynamicSearchParams
: undefined
@@ -38,7 +44,9 @@ const {
expectCompleteParamsInClientValidation,
} =
typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS
? (require('../../server/app-render/instant-validation/instant-samples-client') as typeof import('../../server/app-render/instant-validation/instant-samples-client'))
? // 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'))
: {}
/**
@@ -8,7 +8,10 @@ import {
const actionAsyncStorage =
typeof window === 'undefined'
? (
? // prettier-ignore
(
// 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/action-async-storage.external') as typeof import('../../server/app-render/action-async-storage.external')
).actionAsyncStorage
: undefined
+2
View File
@@ -30,6 +30,8 @@ function _getInitialProps({
hostname = window.location.hostname
} else if (req) {
const { getRequestMeta } =
// 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/request-meta') as typeof import('../server/request-meta')
const initUrl = getRequestMeta(req, 'initURL')
@@ -0,0 +1,97 @@
// @ts-check
/**
* Generates the list of Next.js modules that have a browser-specific variant,
* i.e. a `<module>.ts` (the default, used on the server and any unbundled
* runtime) paired with a sibling `<module>.browser.ts`. The browser bundle is
* aliased to the `.browser` variant by both bundlers:
*
* - webpack: packages/next/src/build/create-compiler-aliases.ts
* - Turbopack: crates/next-core/src/next_import_map.rs
*
* Both consumers read the generated lists produced here so the set stays in
* sync with the filesystem. CI runs this script followed by `git diff
* --exit-code` to ensure the committed lists are up to date.
*/
import { execFileSync } from 'child_process'
import { writeFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '..')
const srcRel = 'packages/next/src'
const tsOut = join(repoRoot, srcRel, 'build/browser-variant-modules.ts')
const rsOut = join(repoRoot, 'crates/next-core/src/browser_variant_modules.rs')
/** Git-tracked files (posix paths relative to the repo root) under a pathspec. */
function trackedFiles(pathspec) {
return execFileSync('git', ['ls-files', '--', pathspec], {
cwd: repoRoot,
encoding: 'utf8',
})
.split('\n')
.filter(Boolean)
}
/**
* Find `<module>.browser.{ts,tsx}` files that have a base `<module>.{ts,tsx}`
* sibling, returning posix module paths relative to `packages/next/src` without
* an extension (e.g. `client/components/unstable-rethrow`). Enumerated from
* git so generated output, ignored files, and build artifacts never leak in.
*/
function collectModules() {
const tracked = trackedFiles(srcRel)
const trackedSet = new Set(tracked)
/** @type {string[]} */
const modules = []
for (const file of tracked) {
const match = file.match(/^(.*)\.browser\.tsx?$/)
if (!match) continue
const stem = match[1]
if (!trackedSet.has(`${stem}.ts`) && !trackedSet.has(`${stem}.tsx`))
continue
modules.push(stem.slice(srcRel.length + 1))
}
return modules.sort()
}
function renderTs(modules) {
const entries = modules.map((m) => ` '${m}',`).join('\n')
return `// This file is @generated by \`pnpm generate-browser-variant-aliases\`.
// Do not edit by hand.
//
// Modules with a \`.browser\` sibling. The default module is aliased to its
// \`.browser\` variant for the browser bundle in
// \`packages/next/src/build/create-compiler-aliases.ts\`. Paths are relative to
// \`packages/next/dist\` (extension omitted).
export const browserVariantModules = [
${entries}
] as const
`
}
function renderRust(modules) {
const entries = modules.map((m) => ` "${m}",`).join('\n')
return `//! This file is @generated by \`pnpm generate-browser-variant-aliases\`.
//! Do not edit by hand.
//!
//! Modules with a \`.browser\` sibling. The default module is aliased to its
//! \`.browser\` variant for the browser bundle in
//! \`get_next_client_resolved_map\` (see \`next_import_map.rs\`). Paths are
//! relative to \`next/dist\` (extension omitted).
// rustfmt::skip keeps this list one-per-line (and matching the generator
// output) instead of collapsing short lists onto a single line.
#[rustfmt::skip]
pub static BROWSER_VARIANT_MODULES: &[&str] = &[
${entries}
];
`
}
const modules = collectModules()
writeFileSync(tsOut, renderTs(modules))
writeFileSync(rsOut, renderRust(modules))
console.log(
`Generated ${modules.length} browser-variant module(s):\n ${modules.join('\n ')}`
)
@@ -299,7 +299,7 @@ describe('build-output-prerender', () => {
- [measure] If the value is for telemetry, use a timing API such as \`performance.now()\`
https://nextjs.org/docs/messages/blocking-prerender-current-time-client#for-telemetry-use-a-timing-api
at Page (webpack:///app/client/page.tsx:4:28)
at ClientPageRoot (webpack:///src/client/components/client-page.tsx:61:12)
at ClientPageRoot (webpack:///src/client/components/client-page.tsx:65:12)
2 |
3 | export default function Page() {
> 4 | return <p>Current time: {new Date().toISOString()}</p>