Skip to content

Commit 2557fba

Browse files
committed
[fix] clone the config module to avoid mutation (#80573)
1 parent 616f6fa commit 2557fba

File tree

3 files changed

+34
-10
lines changed

3 files changed

+34
-10
lines changed

packages/next/src/server/config.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { normalizeZodErrors } from '../shared/lib/zod'
3030
import { HTML_LIMITED_BOT_UA_RE_STRING } from '../shared/lib/router/utils/is-bot'
3131
import { findDir } from '../lib/find-pages-dir'
3232
import { CanaryOnlyError, isStableBuild } from '../shared/lib/canary-only'
33+
import { interopDefault } from '../lib/interop-default'
3334

3435
export { normalizeConfig } from './config-shared'
3536
export type { DomainLocale, NextConfig } from './config-shared'
@@ -1192,10 +1193,12 @@ export default async function loadConfig(
11921193
throw err
11931194
}
11941195

1195-
const userConfig = (await normalizeConfig(
1196+
// Clone a new userConfig each time to avoid mutating the original
1197+
const loadedConfig = (await normalizeConfig(
11961198
phase,
1197-
userConfigModule.default || userConfigModule
1199+
interopDefault(userConfigModule)
11981200
)) as NextConfig
1201+
const userConfig = cloneObject(loadedConfig) as NextConfig
11991202

12001203
if (!process.env.NEXT_MINIMAL) {
12011204
// We only validate the config against schema in non minimal mode
@@ -1312,7 +1315,7 @@ export default async function loadConfig(
13121315
userConfig.htmlLimitedBots = userConfig.htmlLimitedBots.source
13131316
}
13141317

1315-
onLoadUserConfig?.(userConfig)
1318+
onLoadUserConfig?.(Object.freeze(loadedConfig))
13161319
const completeConfig = assignDefaults(
13171320
dir,
13181321
{
@@ -1401,3 +1404,22 @@ export function getConfiguredExperimentalFeatures(
14011404
}
14021405
return configuredExperimentalFeatures
14031406
}
1407+
1408+
function cloneObject(obj: any): any {
1409+
if (obj === null || typeof obj !== 'object') {
1410+
return obj
1411+
}
1412+
1413+
if (Array.isArray(obj)) {
1414+
return obj.map(cloneObject)
1415+
}
1416+
const keys = Object.keys(obj)
1417+
if (keys.length === 0) {
1418+
return obj
1419+
}
1420+
1421+
return keys.reduce((acc, key) => {
1422+
;(acc as any)[key] = cloneObject(obj[key])
1423+
return acc
1424+
}, {})
1425+
}

test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { nextTestSetup } from 'e2e-utils'
22

33
describe('app-dir - metadata-streaming-customized-rule', () => {
4-
const { next } = nextTestSetup({
4+
const { next, isNextDev } = nextTestSetup({
55
files: __dirname,
66
overrideFiles: {
77
'next.config.js': `
@@ -39,4 +39,12 @@ describe('app-dir - metadata-streaming-customized-rule', () => {
3939
expect(await $('head title').length).toBe(0)
4040
expect(await $('body title').length).toBe(1)
4141
})
42+
43+
if (isNextDev) {
44+
it('should not have schema issue', () => {
45+
expect(next.cliOutput).not.toContain(
46+
'Invalid next.config.js options detected'
47+
)
48+
})
49+
}
4250
})

test/integration/config-experimental-warning/next.config.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)