diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 3c624efc9642..de5150ca36c0 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1267,10 +1267,12 @@ export default async function loadConfig( throw err } - const userConfig = (await normalizeConfig( + // Clone a new userConfig each time to avoid mutating the original + const loadedConfig = (await normalizeConfig( phase, - userConfigModule.default || userConfigModule + interopDefault(userConfigModule) )) as NextConfig + const userConfig = cloneObject(loadedConfig) as NextConfig if (!process.env.NEXT_MINIMAL) { // We only validate the config against schema in non minimal mode @@ -1388,7 +1390,7 @@ export default async function loadConfig( userConfig.htmlLimitedBots = userConfig.htmlLimitedBots.source } - onLoadUserConfig?.(userConfig) + onLoadUserConfig?.(Object.freeze(loadedConfig)) const completeConfig = assignDefaults( dir, { @@ -1476,3 +1478,22 @@ export function getConfiguredExperimentalFeatures( } return configuredExperimentalFeatures } + +function cloneObject(obj: any): any { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (Array.isArray(obj)) { + return obj.map(cloneObject) + } + const keys = Object.keys(obj) + if (keys.length === 0) { + return obj + } + + return keys.reduce((acc, key) => { + ;(acc as any)[key] = cloneObject(obj[key]) + return acc + }, {}) +} diff --git a/test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts b/test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts index f8781d302519..88e2d7d48107 100644 --- a/test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts +++ b/test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' describe('app-dir - metadata-streaming-customized-rule', () => { - const { next } = nextTestSetup({ + const { next, isNextDev } = nextTestSetup({ files: __dirname, overrideFiles: { 'next.config.js': ` @@ -39,4 +39,12 @@ describe('app-dir - metadata-streaming-customized-rule', () => { expect(await $('head title').length).toBe(0) expect(await $('body title').length).toBe(1) }) + + if (isNextDev) { + it('should not have schema issue', () => { + expect(next.cliOutput).not.toContain( + 'Invalid next.config.js options detected' + ) + }) + } }) diff --git a/test/integration/config-experimental-warning/next.config.js b/test/integration/config-experimental-warning/next.config.js deleted file mode 100644 index ba08da856bf8..000000000000 --- a/test/integration/config-experimental-warning/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - experimental: { - staticGenerationRetryCount: 2, - staticGenerationMaxConcurrency: 4, - }, -}