Skip to content

Commit 0e973f7

Browse files
authored
Add experimental.runtimeServerDeploymentId (#86865)
If this is set, `process.env.NEXT_DEPLOYMENT_ID` is available at runtime, and we don't need to inline the build-time value into the build output. This is currently true for Node.js routes, and will also be the case for edge routes once #86769 is merged. `experimental.runtimeServerDeploymentId` defaults to true when deploying on Vercel and `NEXT_DEPLOYMENT_ID` is set. - [x] Perform `config.deploymentId = process.env.NEXT_DEPLOYMENT_ID` in the right places in the server This brings us to this state regarding the deployoment ids in the output (notice the exclusion field): <img width="628" height="323" alt="Bildschirmfoto 2025-12-05 um 21 17 53" src="https://github.com/user-attachments/assets/6e35997f-3f81-4da4-880e-d1d1984f7272" />
1 parent 3c2fc61 commit 0e973f7

File tree

25 files changed

+277
-42
lines changed

25 files changed

+277
-42
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -967,5 +967,7 @@
967967
"966": "Expected process.nextTick to reject invalid arguments",
968968
"967": "The key \"%s\" under \"env\" in %sconfig is not allowed. https://nextjs.org/docs/messages/env-key-not-allowed",
969969
"968": "Invariant: getNextConfigEdge must only be called in edge runtime",
970-
"969": "Invariant: nextConfig couldn't be loaded"
970+
"969": "Invariant: nextConfig couldn't be loaded",
971+
"970": "process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled",
972+
"971": "The NEXT_DEPLOYMENT_ID environment variable value \"%s\" does not match the provided deploymentId \"%s\" in the config."
971973
}

packages/next/src/build/define-env.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ interface SerializedDefineEnv {
6161
* Serializes the DefineEnv config so that it can be inserted into the code by Webpack/Turbopack, JSON stringifies each value.
6262
*/
6363
function serializeDefineEnv(defineEnv: DefineEnv): SerializedDefineEnv {
64-
const defineEnvStringified: SerializedDefineEnv = {}
65-
for (const key in defineEnv) {
66-
const value = defineEnv[key]
67-
defineEnvStringified[key] = JSON.stringify(value)
68-
}
69-
64+
const defineEnvStringified: SerializedDefineEnv = Object.fromEntries(
65+
Object.entries(defineEnv).map(([key, value]) => [
66+
key,
67+
JSON.stringify(value),
68+
])
69+
)
7070
return defineEnvStringified
7171
}
7272

@@ -167,9 +167,24 @@ export function getDefineEnv({
167167
'process.env.__NEXT_CACHE_COMPONENTS': isCacheComponentsEnabled,
168168
'process.env.__NEXT_USE_CACHE': isUseCacheEnabled,
169169

170-
'process.env.NEXT_DEPLOYMENT_ID': config.experimental?.useSkewCookie
171-
? false
172-
: config.deploymentId || false,
170+
...(isClient
171+
? {
172+
// TODO use `globalThis.NEXT_DEPLOYMENT_ID` on client to still support accessing
173+
// process.env.NEXT_DEPLOYMENT_ID in userland
174+
'process.env.NEXT_DEPLOYMENT_ID': config.experimental?.useSkewCookie
175+
? false
176+
: config.deploymentId || false,
177+
}
178+
: config.experimental?.runtimeServerDeploymentId
179+
? {
180+
// Don't inline at all, keep process.env.NEXT_DEPLOYMENT_ID as is
181+
}
182+
: {
183+
'process.env.NEXT_DEPLOYMENT_ID': config.experimental?.useSkewCookie
184+
? false
185+
: config.deploymentId || false,
186+
}),
187+
173188
// Propagates the `__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING` environment
174189
// variable to the client.
175190
'process.env.__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING':
@@ -372,8 +387,10 @@ export function getDefineEnv({
372387
for (const key in nextConfigEnv) {
373388
serializedDefineEnv[key] = safeKey(key)
374389
}
375-
for (const key of ['process.env.NEXT_DEPLOYMENT_ID']) {
376-
serializedDefineEnv[key] = safeKey(key)
390+
if (!config.experimental.runtimeServerDeploymentId) {
391+
for (const key of ['process.env.NEXT_DEPLOYMENT_ID']) {
392+
serializedDefineEnv[key] = safeKey(key)
393+
}
377394
}
378395
}
379396

packages/next/src/build/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ export default async function build(
989989
// when using compile mode static env isn't inlined so we
990990
// need to populate in normal runtime env
991991
if (isCompileMode || isGenerateMode) {
992-
populateStaticEnv(config)
992+
populateStaticEnv(config, config.deploymentId)
993993
}
994994

995995
const customRoutes: CustomRoutes = await nextBuildSpan

packages/next/src/build/templates/app-page.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export async function handler(
170170
nextConfig,
171171
parsedUrl,
172172
interceptionRoutePatterns,
173+
deploymentId,
173174
} = prepareResult
174175

175176
const normalizedSrcPage = normalizeAppPath(srcPage)
@@ -559,7 +560,7 @@ export async function handler(
559560
trailingSlash: nextConfig.trailingSlash,
560561
images: nextConfig.images,
561562
previewProps: prerenderManifest.preview,
562-
deploymentId: nextConfig.deploymentId,
563+
deploymentId: deploymentId,
563564
enableTainting: nextConfig.experimental.taint,
564565
htmlLimitedBots: nextConfig.htmlLimitedBots,
565566
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,

packages/next/src/build/templates/edge-ssr-app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ async function requestHandler(
8181
resolvedPathname,
8282
interceptionRoutePatterns,
8383
routerServerContext,
84+
deploymentId,
8485
} = prepareResult
8586

8687
// Initialize the cache handlers interface.
@@ -137,7 +138,7 @@ async function requestHandler(
137138
trailingSlash: nextConfig.trailingSlash,
138139
images: nextConfig.images,
139140
previewProps: prerenderManifest.preview,
140-
deploymentId: nextConfig.deploymentId,
141+
deploymentId,
141142
enableTainting: nextConfig.experimental.taint,
142143
htmlLimitedBots: nextConfig.htmlLimitedBots,
143144
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,

packages/next/src/lib/inline-static-env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export async function inlineStaticEnv({
1717
config: NextConfigComplete
1818
}) {
1919
const nextConfigEnv = getNextConfigEnv(config)
20-
const staticEnv = getStaticEnv(config)
20+
const staticEnv = getStaticEnv(config, config.deploymentId)
2121

2222
const serverDir = path.join(distDir, 'server')
2323
const serverChunks = await glob('**/*.{js,json,js.map}', {

packages/next/src/lib/static-env.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,26 @@ export function getNextConfigEnv(
5353
return defineEnv
5454
}
5555

56-
export function getStaticEnv(config: NextConfigComplete | NextConfigRuntime) {
56+
export function getStaticEnv(
57+
config: NextConfigComplete | NextConfigRuntime,
58+
deploymentId: string
59+
) {
5760
const staticEnv: Record<string, string | undefined> = {
5861
...getNextPublicEnvironmentVariables(),
5962
...getNextConfigEnv(config),
60-
'process.env.NEXT_DEPLOYMENT_ID': config.deploymentId || '',
63+
'process.env.NEXT_DEPLOYMENT_ID': deploymentId,
6164
}
6265
return staticEnv
6366
}
6467

6568
export function populateStaticEnv(
66-
config: NextConfigComplete | NextConfigRuntime
69+
config: NextConfigComplete | NextConfigRuntime,
70+
deploymentId: string
6771
) {
6872
// since inlining comes after static generation we need
6973
// to ensure this value is assigned to process env so it
7074
// can still be accessed
71-
const staticEnv = getStaticEnv(config)
75+
const staticEnv = getStaticEnv(config, deploymentId)
7276
for (const key in staticEnv) {
7377
const innerKey = key.split('.').pop() || ''
7478
if (!process.env[innerKey]) {

packages/next/src/server/base-server.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,24 @@ export default abstract class Server<
447447
// TODO: should conf be normalized to prevent missing
448448
// values from causing issues as this can be user provided
449449
this.nextConfig = conf as NextConfigRuntime
450+
451+
let deploymentId
452+
if (this.nextConfig.experimental.runtimeServerDeploymentId) {
453+
if (!process.env.NEXT_DEPLOYMENT_ID) {
454+
throw new Error(
455+
'process.env.NEXT_DEPLOYMENT_ID is missing but runtimeServerDeploymentId is enabled'
456+
)
457+
}
458+
deploymentId = process.env.NEXT_DEPLOYMENT_ID
459+
} else {
460+
let id = this.nextConfig.experimental.useSkewCookie
461+
? ''
462+
: this.nextConfig.deploymentId || ''
463+
464+
deploymentId = id
465+
process.env.NEXT_DEPLOYMENT_ID = id
466+
}
467+
450468
this.hostname = hostname
451469
if (this.hostname) {
452470
// we format the hostname so that it can be fetched
@@ -501,13 +519,12 @@ export default abstract class Server<
501519
}
502520

503521
this.nextFontManifest = this.getNextFontManifest()
504-
process.env.NEXT_DEPLOYMENT_ID = this.nextConfig.deploymentId || ''
505522

506523
this.renderOpts = {
507524
dir: this.dir,
508525
supportsDynamicResponse: true,
509526
trailingSlash: this.nextConfig.trailingSlash,
510-
deploymentId: this.nextConfig.deploymentId,
527+
deploymentId: deploymentId,
511528
poweredByHeader: this.nextConfig.poweredByHeader,
512529
generateEtags,
513530
previewProps: this.getPrerenderManifest().preview,

packages/next/src/server/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ export const experimentalSchema = {
347347
.optional(),
348348
lockDistDir: z.boolean().optional(),
349349
hideLogsAfterAbort: z.boolean().optional(),
350+
runtimeServerDeploymentId: z.boolean().optional(),
350351
}
351352

352353
export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>

packages/next/src/server/config-shared.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,14 @@ export interface ExperimentalConfig {
836836
* @default false
837837
*/
838838
hideLogsAfterAbort?: boolean
839+
840+
/**
841+
* Whether `process.env.NEXT_DEPLOYMENT_ID` is available at runtime in the server (and `next
842+
* build` doesn't need to embed the deployment ID value into the build output).
843+
*
844+
* @default false
845+
*/
846+
runtimeServerDeploymentId?: boolean
839847
}
840848

841849
export type ExportPathMap = {
@@ -1586,8 +1594,8 @@ export async function normalizeConfig(phase: string, config: any) {
15861594
// };
15871595
// };
15881596
export interface NextConfigRuntime {
1589-
// TODO remove in some cases
1590-
deploymentId: NextConfigComplete['deploymentId']
1597+
// Can be undefined, particularly when experimental.runtimeServerDeploymentId is true
1598+
deploymentId?: NextConfigComplete['deploymentId']
15911599

15921600
configFileName?: string
15931601
// Should only be included when using isExperimentalCompile
@@ -1652,6 +1660,7 @@ export interface NextConfigRuntime {
16521660
| 'proxyClientMaxBodySize'
16531661
| 'proxyTimeout'
16541662
| 'testProxy'
1663+
| 'runtimeServerDeploymentId'
16551664
> & {
16561665
// Pick on @internal fields generates invalid .d.ts files
16571666
/** @internal */
@@ -1706,14 +1715,17 @@ export function getNextConfigRuntime(
17061715
proxyClientMaxBodySize: ex.proxyClientMaxBodySize,
17071716
proxyTimeout: ex.proxyTimeout,
17081717
testProxy: ex.testProxy,
1718+
runtimeServerDeploymentId: ex.runtimeServerDeploymentId,
17091719

17101720
trustHostHeader: ex.trustHostHeader,
17111721
isExperimentalCompile: ex.isExperimentalCompile,
17121722
} satisfies Requiredish<NextConfigRuntime['experimental']>)
17131723
: {}
17141724

17151725
let runtimeConfig: Requiredish<NextConfigRuntime> = {
1716-
deploymentId: config.deploymentId,
1726+
deploymentId: config.experimental.runtimeServerDeploymentId
1727+
? ''
1728+
: config.deploymentId,
17171729

17181730
configFileName: undefined,
17191731
env: undefined,

0 commit comments

Comments
 (0)