fix: prevent URL mutation in router rewrites (#83963)

### What?

This PR fixes critical URL mutation issues in Next.js routing that were
causing state pollution between requests and breaking compatibility
between App Router and Pages Router during rewrite handling.

### Why?

Several interconnected problems were discovered in the routing system:

1. **URL Object Mutation**: The original `parsedUrl` objects were being
mutated during rewrite processing, causing state to leak between
requests and potentially affecting subsequent routing decisions.

2. **Router Type Confusion**: The routing system lacked explicit
tracking of whether a route belonged to the App Router or Pages Router,
leading to incorrect query parameter handling and normalization logic
being applied inconsistently.

3. **Query Parameter Pollution**: App Router RSC payloads were being
polluted with rewrite query parameters that should only be applied to
Pages Router routes, breaking the separation of concerns between the two
routing systems.

4. **Catch-all Route Normalization**: Pages Router catch-all routes
require specific array normalization (e.g., `["hello", "world"]`) but
this was being applied inconsistently or to the wrong router type.

5. **Middleware Filtering Bypass**: The x-matched-path header handling
was bypassing proper middleware query filtering in deployment scenarios.

These issues manifested as test failures where Pages Router catch-all
routes weren't receiving proper array values, App Router routes were
getting contaminated with rewrite parameters, and the routing behavior
was inconsistent between development and deployment environments.

### Dependant PR's

https://github.com/vercel/vercel/pull/13927

[NAR-398](https://linear.app/vercel/issue/NAR-398)
This commit is contained in:
Wyatt Johnson
2025-09-24 13:13:33 -06:00
committed by GitHub
parent 9bd5a76df7
commit 2921fb3350
13 changed files with 316 additions and 73 deletions
+34
View File
@@ -678,6 +678,39 @@ jobs:
secrets: inherit
test-new-tests-deploy-experimental:
name: Test new tests when deployed (experimental)
needs:
[
'optimize-ci',
'test-experimental-prod',
'test-new-tests-dev',
'test-new-tests-start',
]
# test-new-tests-if
if: ${{ needs.optimize-ci.outputs.skip == 'false' }}
# test-new-tests-end-if
strategy:
fail-fast: false
matrix:
group: [1/5, 2/5, 3/5, 4/5, 5/5]
uses: ./.github/workflows/build_reusable.yml
with:
afterBuild: |
export __NEXT_EXPERIMENTAL_PPR=true # for compatibility with the existing tests
export __NEXT_EXPERIMENTAL_CACHE_COMPONENTS=true
export NEXT_EXTERNAL_TESTS_FILTERS="test/experimental-tests-manifest.json"
export NEXT_E2E_TEST_TIMEOUT=240000
export GH_PR_NUMBER=${{ github.event.pull_request && github.event.pull_request.number || '' }}
node scripts/test-new-tests.mjs \
--mode deploy \
--group ${{ matrix.group }}
stepName: 'test-new-tests-deploy-experimental-${{matrix.group}}'
secrets: inherit
test-dev:
name: test dev
needs: ['optimize-ci', 'changes', 'build-native', 'build-next']
@@ -1043,6 +1076,7 @@ jobs:
'test-new-tests-dev',
'test-new-tests-start',
'test-new-tests-deploy',
'test-new-tests-deploy-experimental',
'test-turbopack-production',
'test-turbopack-production-integration',
'test-unit-windows',
+39 -23
View File
@@ -149,6 +149,7 @@ import { setCacheBustingSearchParamWithHash } from '../client/components/router-
import type { CacheControl } from './lib/cache-control'
import type { PrerenderedRoute } from '../build/static-paths/types'
import { createOpaqueFallbackRouteParams } from './request/fallback-params'
import { RouteKind } from './route-kind'
export type FindComponentsResult = {
components: LoadComponentsReturnType
@@ -1132,23 +1133,21 @@ export default abstract class Server<
hasValidParams: false,
}
if (!pageIsDynamic) {
const match = await this.matchers.match(srcPathname, {
i18n: localeAnalysisResult,
})
const match = await this.matchers.match(srcPathname, {
i18n: localeAnalysisResult,
})
if (!pageIsDynamic && match) {
// Update the source pathname to the matched page's pathname.
if (match) {
srcPathname = match.definition.pathname
srcPathname = match.definition.pathname
// The page is dynamic if the params are defined. We know at this
// stage that the matched path is not a static page if the params
// were parsed from the matched path header.
if (typeof match.params !== 'undefined') {
pageIsDynamic = true
paramsResult.params = match.params
paramsResult.hasValidParams = true
}
// The page is dynamic if the params are defined. We know at this
// stage that the matched path is not a static page if the params
// were parsed from the matched path header.
if (typeof match.params !== 'undefined') {
pageIsDynamic = true
paramsResult.params = match.params
paramsResult.hasValidParams = true
}
}
@@ -1183,18 +1182,21 @@ export default abstract class Server<
const originQueryParams = { ...parsedUrl.query }
const pathnameBeforeRewrite = parsedUrl.pathname
const rewriteParamKeys = Object.keys(
utils.handleRewrites(req, parsedUrl)
const { rewriteParams, rewrittenParsedUrl } = utils.handleRewrites(
req,
parsedUrl
)
const rewriteParamKeys = Object.keys(rewriteParams)
// Create a copy of the query params to avoid mutating the original
// object. This prevents any overlapping query params that have the
// same normalized key from causing issues.
const queryParams = { ...parsedUrl.query }
const didRewrite = pathnameBeforeRewrite !== parsedUrl.pathname
const rewrittenQueryParams = { ...rewrittenParsedUrl.query }
const didRewrite =
pathnameBeforeRewrite !== rewrittenParsedUrl.pathname
if (didRewrite && parsedUrl.pathname) {
addRequestMeta(req, 'rewroteURL', parsedUrl.pathname)
if (didRewrite && rewrittenParsedUrl.pathname) {
addRequestMeta(req, 'rewroteURL', rewrittenParsedUrl.pathname)
}
const routeParamKeys = new Set<string>()
@@ -1209,7 +1211,7 @@ export default abstract class Server<
if (typeof value === 'undefined') continue
queryParams[normalizedKey] = Array.isArray(value)
rewrittenQueryParams[normalizedKey] = Array.isArray(value)
? value.map((v) => decodeQueryPathParameter(v))
: decodeQueryPathParameter(value)
}
@@ -1222,7 +1224,7 @@ export default abstract class Server<
// the query params.
if (!paramsResult.hasValidParams) {
paramsResult = utils.normalizeDynamicRouteParams(
queryParams,
rewrittenQueryParams,
false
)
}
@@ -1302,7 +1304,7 @@ export default abstract class Server<
// from the route matches but ignore missing optional params.
if (!paramsResult.hasValidParams) {
paramsResult = utils.normalizeDynamicRouteParams(
queryParams,
rewrittenQueryParams,
true
)
@@ -1368,6 +1370,7 @@ export default abstract class Server<
...Object.keys(utils.defaultRouteRegex?.groups || {}),
])
}
// Remove the route `params` keys from `parsedUrl.query` if they are
// not in the original query params.
// If it's used in both route `params` and query `searchParams`, it should be kept.
@@ -1376,8 +1379,21 @@ export default abstract class Server<
delete parsedUrl.query[key]
}
}
parsedUrl.pathname = matchedPath
url.pathname = parsedUrl.pathname
// For Pages Router routes, use the normalized queryParams from
// handleRewrites to ensure catch-all routes get proper array values.
// App Router routes should not include rewrite query params as they
// affect RSC payload.
if (
match?.definition.kind === RouteKind.PAGES ||
match?.definition.kind === RouteKind.PAGES_API
) {
parsedUrl.query = rewrittenQueryParams
}
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
if (finished) return
} catch (err) {
@@ -71,13 +71,6 @@ export class AppPageRouteModule extends RouteModule<
AppPageRouteDefinition,
AppPageUserlandModule
> {
constructor(
options: RouteModuleOptions<AppPageRouteDefinition, AppPageUserlandModule>
) {
super(options)
this.isAppRouter = true
}
private matchers = new WeakMap<
DeepReadonly<PrerenderManifest>,
PrerenderManifestMatcher
@@ -229,7 +229,6 @@ export class AppRouteRouteModule extends RouteModule<
// Automatically implement some methods if they aren't implemented by the
// userland module.
this.methods = autoImplementMethods(userland)
this.isAppRouter = true
// Get the non-static methods for this route.
this.hasNonStaticMethods = hasNonStaticMethods(userland)
@@ -48,7 +48,7 @@ import { isStaticMetadataRoute } from '../../lib/metadata/is-metadata-route'
import { IncrementalCache } from '../lib/incremental-cache'
import { initializeCacheHandlers, setCacheHandler } from '../use-cache/handlers'
import { interopDefault } from '../app-render/interop-default'
import type { RouteKind } from '../route-kind'
import { RouteKind } from '../route-kind'
import type { BaseNextRequest } from '../base-http'
import type { I18NConfig, NextConfigComplete } from '../config-shared'
import ResponseCache, { type ResponseGenerator } from '../response-cache'
@@ -120,7 +120,6 @@ export abstract class RouteModule<
public isDev: boolean
public distDir: string
public isAppRouter?: boolean
public relativeProjectDir: string
public incrementCache?: IncrementalCache
public responseCache?: ResponseCache
@@ -245,6 +244,12 @@ export abstract class RouteModule<
require('../load-manifest.external') as typeof import('../load-manifest.external')
const normalizedPagePath = normalizePagePath(srcPage)
const router =
this.definition.kind === RouteKind.PAGES ||
this.definition.kind === RouteKind.PAGES_API
? 'pages'
: 'app'
const [
routesManifest,
prerenderManifest,
@@ -290,7 +295,7 @@ export abstract class RouteModule<
projectDir,
distDir: this.distDir,
manifest: process.env.TURBOPACK
? `server/${this.isAppRouter ? 'app' : 'pages'}${normalizedPagePath}/${REACT_LOADABLE_MANIFEST}`
? `server/${router === 'app' ? 'app' : 'pages'}${normalizedPagePath}/${REACT_LOADABLE_MANIFEST}`
: REACT_LOADABLE_MANIFEST,
handleMissing: true,
shouldCache: !this.isDev,
@@ -301,7 +306,7 @@ export abstract class RouteModule<
manifest: `server/${NEXT_FONT_MANIFEST}.json`,
shouldCache: !this.isDev,
}),
this.isAppRouter && !isStaticMetadataRoute(srcPage)
router === 'app' && !isStaticMetadataRoute(srcPage)
? loadManifestFromRelativePath({
distDir: this.distDir,
projectDir,
@@ -311,7 +316,7 @@ export abstract class RouteModule<
shouldCache: !this.isDev,
})
: undefined,
this.isAppRouter
router === 'app'
? loadManifestFromRelativePath<any>({
distDir: this.distDir,
projectDir,
@@ -636,11 +641,12 @@ export abstract class RouteModule<
// we apply rewrites against cloned URL so that we don't
// modify the original with the rewrite destination
const clonedParsedUrl = structuredClone(parsedUrl)
const rewriteParamKeys = Object.keys(
serverUtils.handleRewrites(req, clonedParsedUrl)
const { rewriteParams, rewrittenParsedUrl } = serverUtils.handleRewrites(
req,
parsedUrl
)
Object.assign(parsedUrl.query, clonedParsedUrl.query)
const rewriteParamKeys = Object.keys(rewriteParams)
Object.assign(parsedUrl.query, rewrittenParsedUrl.query)
// after processing rewrites we want to remove locale
// from parsedUrl pathname
@@ -650,8 +656,8 @@ export abstract class RouteModule<
i18n.locales
).pathname
clonedParsedUrl.pathname = normalizeLocalePath(
clonedParsedUrl.pathname || '/',
rewrittenParsedUrl.pathname = normalizeLocalePath(
rewrittenParsedUrl.pathname || '/',
i18n.locales
).pathname
}
@@ -663,7 +669,7 @@ export abstract class RouteModule<
if (!params && serverUtils.dynamicRouteMatcher) {
const paramsMatch = serverUtils.dynamicRouteMatcher(
normalizeDataPath(
clonedParsedUrl?.pathname || parsedUrl.pathname || '/'
rewrittenParsedUrl?.pathname || parsedUrl.pathname || '/'
)
)
const paramsResult = serverUtils.normalizeDynamicRouteParams(
@@ -692,11 +698,14 @@ export abstract class RouteModule<
const routeParamKeys = new Set<string>()
const combinedParamKeys = []
// we don't include rewriteParamKeys in the combinedParamKeys
// We don't include rewriteParamKeys in the combinedParamKeys
// for app router since the searchParams is populated from the
// URL so we don't want to strip the rewrite params from the URL
// so that searchParams can include them
if (!this.isAppRouter) {
// so that searchParams can include them.
if (
this.definition.kind === RouteKind.PAGES ||
this.definition.kind === RouteKind.PAGES_API
) {
for (const key of [
...rewriteParamKeys,
...Object.keys(serverUtils.defaultRouteMatches || {}),
@@ -816,9 +825,7 @@ export abstract class RouteModule<
const nextConfig =
routerServerContext?.nextConfig || serverFilesManifest.config
let resolvedPathname =
getRequestMeta(req, 'rewroteURL') || normalizedSrcPage
let resolvedPathname = normalizedSrcPage
if (isDynamicRoute(resolvedPathname) && params) {
resolvedPathname = serverUtils.interpolateDynamicPath(
resolvedPathname,
+21 -16
View File
@@ -220,10 +220,13 @@ export function getServerUtils({
function handleRewrites(
req: BaseNextRequest | IncomingMessage,
parsedUrl: UrlWithParsedQuery
parsedUrl: DeepReadonly<UrlWithParsedQuery>
) {
// Here we deep clone the parsedUrl to avoid mutating the original. We also
// cast this to a mutable type so we can mutate it within this scope.
const rewrittenParsedUrl = structuredClone(parsedUrl) as UrlWithParsedQuery
const rewriteParams: Record<string, string> = {}
let fsPathname = parsedUrl.pathname
let fsPathname = rewrittenParsedUrl.pathname
const matchesPage = () => {
const fsPathnameNoSlash = removeTrailingSlash(fsPathname || '')
@@ -243,14 +246,14 @@ export function getServerUtils({
}
)
if (!parsedUrl.pathname) return false
if (!rewrittenParsedUrl.pathname) return false
let params = matcher(parsedUrl.pathname)
let params = matcher(rewrittenParsedUrl.pathname)
if ((rewrite.has || rewrite.missing) && params) {
const hasParams = matchHas(
req,
parsedUrl.query,
rewrittenParsedUrl.query,
rewrite.has as Rewrite['has'],
rewrite.missing as Rewrite['missing']
)
@@ -288,7 +291,7 @@ export function getServerUtils({
appendParamsToQuery: true,
destination: rewrite.destination,
params: params,
query: parsedUrl.query,
query: rewrittenParsedUrl.query,
})
// if the rewrite destination is external break rewrite chain
@@ -297,26 +300,26 @@ export function getServerUtils({
}
Object.assign(rewriteParams, destQuery, params)
Object.assign(parsedUrl.query, parsedDestination.query)
Object.assign(rewrittenParsedUrl.query, parsedDestination.query)
delete (parsedDestination as any).query
// for each property in parsedUrl.query, if the value is parametrized (eg :foo), look up the value
// for each property in rewrittenParsedUrl.query, if the value is parametrized (eg :foo), look up the value
// in rewriteParams and replace the parametrized value with the actual value
// this is used when the rewrite destination does not contain the original source param
// and so the value is still parametrized and needs to be replaced with the actual rewrite param
Object.entries(parsedUrl.query).forEach(([key, value]) => {
Object.entries(rewrittenParsedUrl.query).forEach(([key, value]) => {
if (value && typeof value === 'string' && value.startsWith(':')) {
const paramName = value.slice(1)
const actualValue = rewriteParams[paramName]
if (actualValue) {
parsedUrl.query[key] = actualValue
rewrittenParsedUrl.query[key] = actualValue
}
}
})
Object.assign(parsedUrl, parsedDestination)
Object.assign(rewrittenParsedUrl, parsedDestination)
fsPathname = parsedUrl.pathname
fsPathname = rewrittenParsedUrl.pathname
if (!fsPathname) return false
if (basePath) {
@@ -326,7 +329,7 @@ export function getServerUtils({
if (i18n) {
const result = normalizeLocalePath(fsPathname, i18n.locales)
fsPathname = result.pathname
parsedUrl.query.nextInternalLocale =
rewrittenParsedUrl.query.nextInternalLocale =
result.detectedLocale || params.nextInternalLocale
}
@@ -337,14 +340,15 @@ export function getServerUtils({
if (pageIsDynamic && dynamicRouteMatcher) {
const dynamicParams = dynamicRouteMatcher(fsPathname)
if (dynamicParams) {
parsedUrl.query = {
...parsedUrl.query,
rewrittenParsedUrl.query = {
...rewrittenParsedUrl.query,
...dynamicParams,
}
return true
}
}
}
return false
}
@@ -367,7 +371,8 @@ export function getServerUtils({
}
}
}
return rewriteParams
return { rewriteParams, rewrittenParsedUrl }
}
function getParamsFromRouteMatches(routeMatchesHeader: string) {
+17 -2
View File
@@ -130,14 +130,29 @@ async function main() {
for (let i = 0; i < attempts; i++) {
console.log(`\n\nRun ${i + 1}/${attempts} for ${testMode} tests (Webpack)`)
// We apply the external tests filter before the process.env so that if
// it's defined in the environment, it overrides the default filter.
// This is required for supporting the experimental tests setup.
const NEXT_EXTERNAL_TESTS_FILTERS = process.env.NEXT_EXTERNAL_TESTS_FILTERS
? process.env.NEXT_EXTERNAL_TESTS_FILTERS
: testMode === 'deploy'
? 'test/deploy-tests-manifest.json'
: undefined
if (NEXT_EXTERNAL_TESTS_FILTERS) {
console.log(
`Applying external tests filter: ${NEXT_EXTERNAL_TESTS_FILTERS}`
)
}
await execa('node', [...RUN_TESTS_ARGS, ...currentTests], {
...EXECA_OPTS_STDIO,
env: {
...process.env,
NEXT_EXTERNAL_TESTS_FILTERS,
NEXT_TEST_MODE: testMode,
NEXT_TEST_VERSION: nextTestVersion,
NEXT_EXTERNAL_TESTS_FILTERS:
testMode === 'deploy' ? 'test/deploy-tests-manifest.json' : undefined,
},
})
}
@@ -0,0 +1,25 @@
async function getMetadata() {
'use cache'
return {
title: `Grid Page ${Math.random()}`,
}
}
export async function generateMetadata() {
return await getMetadata()
}
export default async function GridPage({
params,
}: {
params: Promise<{
first: string
}>
}) {
const { first } = await params
return (
<div data-slug={`${first}/~/overview/grid`}>
Page /{first}/~/overview/grid
</div>
)
}
@@ -0,0 +1,16 @@
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
// Parse the path to revalidate from the request query params.
const paths = request.nextUrl.searchParams.getAll('path')
// Revalidate the paths.
for (const path of paths) {
const isPage = path.includes('[') && path.includes(']')
revalidatePath(path, isPage ? 'page' : undefined)
}
return new Response('Revalidated paths')
}
@@ -1,6 +1,31 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
const nextConfig = {
experimental: {
useCache: true,
},
rewrites: async () => {
return {
beforeFiles: [
{
source: '/:first/~/overview/:path*',
destination: '/404',
},
{
source: '/:first',
has: [
{
type: 'cookie',
key: 'overview-param',
value: 'grid',
},
],
destination: '/:first/~/overview/grid',
},
],
}
},
}
module.exports = nextConfig
@@ -1,6 +1,7 @@
import { nextTestSetup } from 'e2e-utils'
import * as cheerio from 'cheerio'
import { retry } from 'next-test-utils'
import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param'
describe('middleware-static-rewrite', () => {
const { next, isNextDeploy, isNextDev } = nextTestSetup({
@@ -123,6 +124,91 @@ describe('middleware-static-rewrite', () => {
'runtime'
)
})
it('should revalidate the overview page without replacing it with a 404', async () => {
const url = new URL('/my-team', 'http://localhost')
const rsc = computeCacheBustingSearchParam(
'1',
'/_tree',
undefined,
undefined
)
url.searchParams.set('_rsc', rsc)
let res = await next.fetch(url.pathname + url.search, {
headers: {
Cookie: 'overview-param=grid',
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-Segment-Prefetch': '/_tree',
},
})
// A 404 here represents a routing issue that was resolved by an upstream
// PR in the builder: https://github.com/vercel/vercel/pull/13927
expect(res.status).toBe(200)
// Now, let's verify that we both got rewritten to the correct page, and
// that we're being served the prerender shell.
expect(res.headers.get('x-nextjs-rewritten-path')).toBe(
'/my-team/~/overview/grid'
)
expect(res.headers.get('x-nextjs-postponed')).toBe('2')
expect(res.headers.get('x-nextjs-prerender')).toBe('1')
// Grab the RSC content.
const rsc1 = await res.text()
// Grab the title which includes the random number.
const title1 = rsc1.match(/Grid Page (\d+\.\d+)/)?.[1]
expect(title1).toBeDefined()
// Now, let's trigger a revalidation for the page.
res = await next.fetch(
'/api?path=/my-team&path=/my-team/~/overview/grid&path=/[first]/~/overview/grid'
)
// Now, let's keep polling the prefetch until it's revalidated.
let rsc2: string
await retry(async () => {
res = await next.fetch(url.pathname + url.search, {
headers: {
Cookie: 'overview-param=grid',
RSC: '1',
'Next-Router-Prefetch': '1',
'Next-Router-Segment-Prefetch': '/_tree',
},
})
// A 404 here represents a routing issue that was resolved by an upstream
// PR in the builder: https://github.com/vercel/vercel/pull/13927
expect(res.status).toBe(200)
// We're expecting that the title has changed, so let's compare that the
// rsc payload is different.
rsc2 = await res.text()
expect(rsc1).not.toBe(rsc2)
})
// Now that the revalidation has been completed, let's also verify that
// it revalidated correctly.
expect(res.headers.get('x-nextjs-postponed')).toBe('2')
expect(res.headers.get('x-nextjs-rewritten-path')).toBe(
'/my-team/~/overview/grid'
)
// We expect that the only difference between the two RSC contents is the
// title.
const title2 = rsc2.match(/Grid Page (\d+\.\d+)/)?.[1]
expect(title2).toBeDefined()
expect(title2).not.toBe(title1)
// Let's compare the RSC contents, with the titles removed.
const cleaned1 = rsc1.replace(/Grid Page (\d+\.\d+)/, 'Grid Page')
const cleaned2 = rsc2.replace(/Grid Page (\d+\.\d+)/, 'Grid Page')
expect(cleaned1).toBe(cleaned2)
})
} else {
// Here we're validating that there is a static page generated for the
// rewritten path.
+8
View File
@@ -393,6 +393,14 @@ export class NextInstance {
if (process.env.NEXT_PRIVATE_TEST_MODE) {
process.env.__NEXT_TEST_MODE = process.env.NEXT_PRIVATE_TEST_MODE
}
// alias experimental feature flags for deployment compatibility
if (process.env.NEXT_PRIVATE_EXPERIMENTAL_PPR) {
process.env.__NEXT_EXPERIMENTAL_PPR = process.env.NEXT_PRIVATE_EXPERIMENTAL_PPR
}
if (process.env.NEXT_PRIVATE_EXPERIMENTAL_CACHE_COMPONENTS) {
process.env.__NEXT_EXPERIMENTAL_CACHE_COMPONENTS = process.env.NEXT_PRIVATE_EXPERIMENTAL_CACHE_COMPONENTS
}
`
)
+19 -5
View File
@@ -91,17 +91,26 @@ export class NextDeployInstance extends NextInstance {
const additionalEnv: string[] = []
for (const key of Object.keys(this.env || {})) {
additionalEnv.push('--build-env')
additionalEnv.push(`${key}=${this.env[key]}`)
additionalEnv.push('--env')
additionalEnv.push(`${key}=${this.env[key]}`)
}
additionalEnv.push('--build-env')
additionalEnv.push(
`VERCEL_CLI_VERSION=${process.env.VERCEL_CLI_VERSION || 'vercel@latest'}`
)
// Add experimental feature flags
if (process.env.__NEXT_EXPERIMENTAL_PPR) {
additionalEnv.push(
`NEXT_PRIVATE_EXPERIMENTAL_PPR=${process.env.__NEXT_EXPERIMENTAL_PPR}`
)
}
if (process.env.__NEXT_EXPERIMENTAL_CACHE_COMPONENTS) {
additionalEnv.push(
`NEXT_PRIVATE_EXPERIMENTAL_CACHE_COMPONENTS=${process.env.__NEXT_EXPERIMENTAL_CACHE_COMPONENTS}`
)
}
const deployRes = await execa(
'vercel',
[
@@ -112,7 +121,12 @@ export class NextDeployInstance extends NextInstance {
'NEXT_TELEMETRY_DISABLED=1',
'--build-env',
'VERCEL_NEXT_BUNDLED_SERVER=1',
...additionalEnv,
...additionalEnv.flatMap((pair) => [
'--env',
pair,
'--build-env',
pair,
]),
'--force',
...vercelFlags,
],