mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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:
@@ -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
|
||||
@@ -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",
|
||||
];
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user