Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/next/src/build/define-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ export function getDefineEnv({
}),
'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH':
config.experimental.manualClientBasePath ?? false,
'process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED':
config.experimental.runtimeBasePath ?? false,
'process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME': JSON.stringify(
isNaN(Number(config.experimental.staleTimes?.dynamic))
? 0
Expand Down
7 changes: 6 additions & 1 deletion packages/next/src/client/add-base-path.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { addPathPrefix } from '../shared/lib/router/utils/add-path-prefix'
import { getRuntimeBasePath } from '../shared/lib/router/utils/runtime-base-path'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'

const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
const compileTimeBasePath =
(process.env.__NEXT_ROUTER_BASEPATH as string) || ''

export function addBasePath(path: string, required?: boolean): string {
const basePath = process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED
? getRuntimeBasePath()
: compileTimeBasePath
return normalizePathTrailingSlash(
process.env.__NEXT_MANUAL_CLIENT_BASE_PATH && !required
? path
Expand Down
7 changes: 6 additions & 1 deletion packages/next/src/client/has-base-path.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { pathHasPrefix } from '../shared/lib/router/utils/path-has-prefix'
import { getRuntimeBasePath } from '../shared/lib/router/utils/runtime-base-path'

const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
const compileTimeBasePath =
(process.env.__NEXT_ROUTER_BASEPATH as string) || ''

export function hasBasePath(path: string): boolean {
const basePath = process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED
? getRuntimeBasePath()
: compileTimeBasePath
return pathHasPrefix(path, basePath)
}
5 changes: 5 additions & 0 deletions packages/next/src/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { ImageConfigContext } from '../shared/lib/image-config-context.shared-ru
import type { ImageConfigComplete } from '../shared/lib/image-config'
import { removeBasePath } from './remove-base-path'
import { hasBasePath } from './has-base-path'
import { setClientRuntimeBasePath } from '../shared/lib/router/utils/runtime-base-path'
import { AppRouterContext } from '../shared/lib/app-router-context.shared-runtime'
import {
adaptForAppRouterInstance,
Expand Down Expand Up @@ -205,6 +206,10 @@ export async function initialize(opts: { devClient?: any } = {}): Promise<{
)
window.__NEXT_DATA__ = initialData

if (process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED) {
setClientRuntimeBasePath(initialData.basePath || '')
}

defaultLocale = initialData.defaultLocale
const prefix: string = initialData.assetPrefix || ''
// With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time
Expand Down
8 changes: 7 additions & 1 deletion packages/next/src/client/remove-base-path.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getRuntimeBasePath } from '../shared/lib/router/utils/runtime-base-path'
import { hasBasePath } from './has-base-path'

const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
const compileTimeBasePath =
(process.env.__NEXT_ROUTER_BASEPATH as string) || ''

export function removeBasePath(path: string): string {
if (process.env.__NEXT_MANUAL_CLIENT_BASE_PATH) {
Expand All @@ -9,6 +11,10 @@ export function removeBasePath(path: string): string {
}
}

const basePath = process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED
? getRuntimeBasePath()
: compileTimeBasePath

// Can't trim the basePath if it has zero length!
if (basePath.length === 0) return path

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export const experimentalSchema = {
largePageDataBytes: z.number().optional(),
linkNoTouchStart: z.boolean().optional(),
manualClientBasePath: z.boolean().optional(),
runtimeBasePath: z.boolean().optional(),
middlewarePrefetch: z.enum(['strict', 'flexible']).optional(),
proxyPrefetch: z.enum(['strict', 'flexible']).optional(),
middlewareClientMaxBodySize: zSizeLimit.optional(),
Expand Down
18 changes: 18 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,23 @@ export interface ExperimentalConfig {
middlewarePrefetch?: 'strict' | 'flexible'
proxyPrefetch?: 'strict' | 'flexible'
manualClientBasePath?: boolean
/**
* Read the basePath from the `x-base-path` request header at runtime instead
* of taking it from `next.config.js` at build time.
*
* When enabled:
* - The server reads `x-base-path` from the incoming request, uses it for
* the duration of the render, and writes it into `__NEXT_DATA__.basePath`.
* - The client picks the value up from `__NEXT_DATA__.basePath` and uses
* it for every `addBasePath` / `removeBasePath` call (Link, router.push,
* `_next/data` URLs, page chunk URLs).
*
* Intended for multi-tenant apps where a single Next.js deployment serves
* several sites that live behind different URL prefixes on the same host
* (e.g. `/site-a/*`, `/site-b/*`), with an edge function rewriting the URI
* and injecting `x-base-path` per request.
*/
runtimeBasePath?: boolean
/**
* CSS Chunking strategy. Defaults to `true` ("loose" mode), which guesses dependencies
* between CSS files to keep ordering of them.
Expand Down Expand Up @@ -1933,6 +1950,7 @@ export const defaultConfig = Object.freeze({
proxyPrefetch: 'flexible',
optimisticClientCache: true,
manualClientBasePath: false,
runtimeBasePath: false,
cpus: Math.max(
1,
(Number(process.env.CIRCLE_NODE_TOTAL) ||
Expand Down
47 changes: 37 additions & 10 deletions packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ import { extractNextErrorCode } from '../lib/error-telemetry-utils'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import type { PagesDevOverlayBridgeType } from '../next-devtools/userspace/pages/pages-dev-overlay-setup'
import { getScriptNonceFromHeader } from './app-render/get-script-nonce-from-header'
import {
getRuntimeBasePath,
runWithRuntimeBasePath,
} from '../shared/lib/router/utils/runtime-base-path'

const RUNTIME_BASE_PATH_HEADER = 'x-base-path'

function readRuntimeBasePathHeader(req: IncomingMessage): string {
const raw = req.headers[RUNTIME_BASE_PATH_HEADER]
const value = Array.isArray(raw) ? raw[0] : raw
if (!value) return ''
const trimmed = value.trim()
if (!trimmed || trimmed === '/') return ''
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
return withLeadingSlash.endsWith('/')
? withLeadingSlash.slice(0, -1)
: withLeadingSlash
}

let tryGetPreviewData: typeof import('./api-utils/node/try-get-preview-data').tryGetPreviewData
let warn: typeof import('../build/output/log').warn
Expand Down Expand Up @@ -1520,6 +1538,9 @@ export async function renderToHTMLImpl(
notFoundSrcPage && process.env.__NEXT_DEV_SERVER
? notFoundSrcPage
: undefined,
basePath: process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED
? getRuntimeBasePath() || undefined
: undefined,
},
nonce,
buildManifest: filteredBuildManifest,
Expand Down Expand Up @@ -1630,14 +1651,20 @@ export const renderToHTML: PagesRender = (
sharedContext,
renderContext
) => {
return renderToHTMLImpl(
req,
res,
pathname,
query,
renderOpts,
renderOpts,
sharedContext,
renderContext
)
const invoke = () =>
renderToHTMLImpl(
req,
res,
pathname,
query,
renderOpts,
renderOpts,
sharedContext,
renderContext
)

if (process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED) {
return runWithRuntimeBasePath(readRuntimeBasePathHeader(req), invoke)
}
return invoke()
}
6 changes: 5 additions & 1 deletion packages/next/src/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,11 @@ export default class Router implements BaseRouter {
const autoExportDynamic =
isDynamicRoute(pathname) && self.__NEXT_DATA__.autoExport

this.basePath = process.env.__NEXT_ROUTER_BASEPATH || ''
this.basePath = process.env.__NEXT_RUNTIME_BASE_PATH_ENABLED
? self.__NEXT_DATA__.basePath ||
process.env.__NEXT_ROUTER_BASEPATH ||
''
: process.env.__NEXT_ROUTER_BASEPATH || ''
this.sub = subscription
this.clc = null
this._wrapApp = wrapApp
Expand Down
47 changes: 47 additions & 0 deletions packages/next/src/shared/lib/router/utils/runtime-base-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createAsyncLocalStorage } from '../../../../server/app-render/async-local-storage'

// Client-side: a single module-scoped value, hydrated from
// `__NEXT_DATA__.basePath` during client bootstrap. The browser only ever
// renders one document at a time, so a single mutable value is enough.
let clientBasePath = ''

// Server-side: a per-request value held in AsyncLocalStorage so concurrent
// renders don't see each other's basePath. `createAsyncLocalStorage()`
// returns a no-op implementation in environments without `AsyncLocalStorage`
// (e.g. browsers), and `getStore()` returns `undefined` there — the client
// falls through to `clientBasePath`.
const runtimeBasePathStorage = createAsyncLocalStorage<{ basePath: string }>()

/**
* Returns the basePath that should be applied to URLs at this moment.
*
* On the server, this is the value the current request was rendered with
* (set via `runWithRuntimeBasePath`). On the client, this is the value
* hydrated from `__NEXT_DATA__.basePath`.
*
* Only meaningful when `experimental.runtimeBasePath` is enabled.
*/
export function getRuntimeBasePath(): string {
const store = runtimeBasePathStorage.getStore()
if (store) return store.basePath
return clientBasePath
}

/**
* Sets the client-side runtime basePath. Should be called exactly once
* during client bootstrap, before the router is created.
*/
export function setClientRuntimeBasePath(basePath: string): void {
clientBasePath = basePath
}

/**
* Runs `fn` with the given basePath as the active runtime basePath.
* Used on the server to scope a render to a per-request basePath.
*/
export function runWithRuntimeBasePath<R>(
basePath: string,
fn: () => R
): R {
return runtimeBasePathStorage.run({ basePath }, fn)
}
6 changes: 6 additions & 0 deletions packages/next/src/shared/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export type NEXT_DATA = {
scriptLoader?: any[]
isPreview?: boolean
notFoundSrcPage?: string
/**
* Per-request basePath when `experimental.runtimeBasePath` is enabled.
* The client reads this on hydration and uses it for all subsequent
* URL building (Link, router navigation, `_next/data` fetches).
*/
basePath?: string
}

/**
Expand Down
Loading