Skip to content

Commit 3c2a195

Browse files
authored
Also enforce experimental features when there's no next config file (#81679)
When running `next build --debug-prerender`, we also want to set the experimental features even if there is no next.config.js file present. This PR also fixes the `cloneObject` function to better handle nested optional object properties, and we're now also freezing and cloning the default config before applying changes to it. > [!NOTE] > This PR is best reviewed with hidden whitespace changes.
1 parent 1a0c4d0 commit 3c2a195

File tree

9 files changed

+341
-202
lines changed

9 files changed

+341
-202
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,7 +1283,7 @@ export interface NextConfig extends Record<string, any> {
12831283
htmlLimitedBots?: RegExp
12841284
}
12851285

1286-
export const defaultConfig = {
1286+
export const defaultConfig = Object.freeze({
12871287
env: {},
12881288
webpack: null,
12891289
eslint: {
@@ -1503,7 +1503,7 @@ export const defaultConfig = {
15031503
},
15041504
htmlLimitedBots: undefined,
15051505
bundlePagesRouterDependencies: false,
1506-
} satisfies NextConfig
1506+
} satisfies NextConfig)
15071507

15081508
export async function normalizeConfig(phase: string, config: any) {
15091509
if (typeof config === 'function') {

packages/next/src/server/config.ts

Lines changed: 130 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ export default async function loadConfig(
12271227
}
12281228

12291229
const path = await findUp(CONFIG_FILES, { cwd: dir })
1230+
const configuredExperimentalFeatures: ConfiguredExperimentalFeature[] = []
12301231

12311232
// If config file was found
12321233
if (path?.length) {
@@ -1280,8 +1281,6 @@ export default async function loadConfig(
12801281
)) as NextConfig
12811282
)
12821283

1283-
const configuredExperimentalFeatures: ConfiguredExperimentalFeature[] = []
1284-
12851284
if (reportExperimentalFeatures && loadedConfig.experimental) {
12861285
for (const name of Object.keys(
12871286
loadedConfig.experimental
@@ -1420,58 +1419,13 @@ export default async function loadConfig(
14201419
userConfig.htmlLimitedBots = userConfig.htmlLimitedBots.source
14211420
}
14221421

1423-
if (
1424-
userConfig.experimental &&
1425-
userConfig.experimental.enablePrerenderSourceMaps === undefined &&
1426-
userConfig.experimental.dynamicIO === true
1427-
) {
1428-
userConfig.experimental.enablePrerenderSourceMaps = true
1429-
addConfiguredExperimentalFeature(
1430-
configuredExperimentalFeatures,
1431-
'enablePrerenderSourceMaps',
1432-
true,
1433-
'enabled by `experimental.dynamicIO`'
1434-
)
1435-
}
1436-
1437-
if (
1438-
debugPrerender &&
1439-
(phase === PHASE_PRODUCTION_BUILD || phase === PHASE_EXPORT)
1440-
) {
1441-
userConfig.experimental ??= {}
1442-
1443-
setExperimentalFeatureForDebugPrerender(
1444-
userConfig.experimental,
1445-
'serverSourceMaps',
1446-
true,
1447-
reportExperimentalFeatures ? configuredExperimentalFeatures : undefined
1448-
)
1449-
1450-
setExperimentalFeatureForDebugPrerender(
1451-
userConfig.experimental,
1452-
process.env.TURBOPACK ? 'turbopackMinify' : 'serverMinification',
1453-
false,
1454-
reportExperimentalFeatures ? configuredExperimentalFeatures : undefined
1455-
)
1456-
1457-
setExperimentalFeatureForDebugPrerender(
1458-
userConfig.experimental,
1459-
'enablePrerenderSourceMaps',
1460-
true,
1461-
reportExperimentalFeatures ? configuredExperimentalFeatures : undefined
1462-
)
1463-
1464-
setExperimentalFeatureForDebugPrerender(
1465-
userConfig.experimental,
1466-
'prerenderEarlyExit',
1467-
false,
1468-
reportExperimentalFeatures ? configuredExperimentalFeatures : undefined
1469-
)
1470-
}
1471-
1472-
if (reportExperimentalFeatures) {
1473-
reportExperimentalFeatures(configuredExperimentalFeatures)
1474-
}
1422+
enforceExperimentalFeatures(userConfig, {
1423+
configuredExperimentalFeatures: reportExperimentalFeatures
1424+
? configuredExperimentalFeatures
1425+
: undefined,
1426+
debugPrerender,
1427+
phase,
1428+
})
14751429

14761430
const completeConfig = assignDefaults(
14771431
dir,
@@ -1483,6 +1437,11 @@ export default async function loadConfig(
14831437
},
14841438
silent
14851439
) as NextConfigComplete
1440+
1441+
if (reportExperimentalFeatures) {
1442+
reportExperimentalFeatures(configuredExperimentalFeatures)
1443+
}
1444+
14861445
return await applyModifyConfig(completeConfig, phase, silent)
14871446
} else {
14881447
const configBaseName = basename(CONFIG_FILES[0], extname(CONFIG_FILES[0]))
@@ -1506,14 +1465,30 @@ export default async function loadConfig(
15061465
}
15071466
}
15081467

1468+
const clonedDefaultConfig = cloneObject(defaultConfig) as NextConfig
1469+
1470+
enforceExperimentalFeatures(clonedDefaultConfig, {
1471+
configuredExperimentalFeatures: reportExperimentalFeatures
1472+
? configuredExperimentalFeatures
1473+
: undefined,
1474+
debugPrerender,
1475+
phase,
1476+
})
1477+
15091478
// always call assignDefaults to ensure settings like
15101479
// reactRoot can be updated correctly even with no next.config.js
15111480
const completeConfig = assignDefaults(
15121481
dir,
1513-
{ ...defaultConfig, configFileName },
1482+
{ ...clonedDefaultConfig, configFileName },
15141483
silent
15151484
) as NextConfigComplete
1485+
15161486
setHttpClientAndAgentOptions(completeConfig)
1487+
1488+
if (reportExperimentalFeatures) {
1489+
reportExperimentalFeatures(configuredExperimentalFeatures)
1490+
}
1491+
15171492
return await applyModifyConfig(completeConfig, phase, silent)
15181493
}
15191494

@@ -1523,7 +1498,70 @@ export type ConfiguredExperimentalFeature = {
15231498
reason?: string
15241499
}
15251500

1526-
export function addConfiguredExperimentalFeature<
1501+
function enforceExperimentalFeatures(
1502+
config: NextConfig,
1503+
options: {
1504+
configuredExperimentalFeatures: ConfiguredExperimentalFeature[] | undefined
1505+
debugPrerender: boolean | undefined
1506+
phase: string
1507+
}
1508+
) {
1509+
const { configuredExperimentalFeatures, debugPrerender, phase } = options
1510+
1511+
if (
1512+
config.experimental &&
1513+
config.experimental.enablePrerenderSourceMaps === undefined &&
1514+
config.experimental.dynamicIO === true
1515+
) {
1516+
config.experimental.enablePrerenderSourceMaps = true
1517+
1518+
if (configuredExperimentalFeatures) {
1519+
addConfiguredExperimentalFeature(
1520+
configuredExperimentalFeatures,
1521+
'enablePrerenderSourceMaps',
1522+
true,
1523+
'enabled by `experimental.dynamicIO`'
1524+
)
1525+
}
1526+
}
1527+
1528+
config.experimental ??= {}
1529+
1530+
if (
1531+
debugPrerender &&
1532+
(phase === PHASE_PRODUCTION_BUILD || phase === PHASE_EXPORT)
1533+
) {
1534+
setExperimentalFeatureForDebugPrerender(
1535+
config.experimental,
1536+
'serverSourceMaps',
1537+
true,
1538+
configuredExperimentalFeatures
1539+
)
1540+
1541+
setExperimentalFeatureForDebugPrerender(
1542+
config.experimental,
1543+
process.env.TURBOPACK ? 'turbopackMinify' : 'serverMinification',
1544+
false,
1545+
configuredExperimentalFeatures
1546+
)
1547+
1548+
setExperimentalFeatureForDebugPrerender(
1549+
config.experimental,
1550+
'enablePrerenderSourceMaps',
1551+
true,
1552+
configuredExperimentalFeatures
1553+
)
1554+
1555+
setExperimentalFeatureForDebugPrerender(
1556+
config.experimental,
1557+
'prerenderEarlyExit',
1558+
false,
1559+
configuredExperimentalFeatures
1560+
)
1561+
}
1562+
}
1563+
1564+
function addConfiguredExperimentalFeature<
15271565
KeyType extends keyof ExperimentalConfig,
15281566
>(
15291567
configuredExperimentalFeatures: ConfiguredExperimentalFeature[],
@@ -1564,20 +1602,50 @@ function setExperimentalFeatureForDebugPrerender<
15641602
}
15651603

15661604
function cloneObject(obj: any): any {
1605+
// Primitives & null
15671606
if (obj === null || typeof obj !== 'object') {
15681607
return obj
15691608
}
15701609

1610+
// RegExp → clone via constructor
1611+
if (obj instanceof RegExp) {
1612+
return new RegExp(obj.source, obj.flags)
1613+
}
1614+
1615+
// Function → just reuse the function reference
1616+
if (typeof obj === 'function') {
1617+
return obj
1618+
}
1619+
1620+
// Arrays → map each element
15711621
if (Array.isArray(obj)) {
15721622
return obj.map(cloneObject)
15731623
}
1574-
const keys = Object.keys(obj)
1575-
if (keys.length === 0) {
1624+
1625+
// Detect non‑plain objects (class instances)
1626+
const proto = Object.getPrototypeOf(obj)
1627+
const isPlainObject = proto === Object.prototype || proto === null
1628+
1629+
// If it's not a plain object, just return the original
1630+
if (!isPlainObject) {
15761631
return obj
15771632
}
15781633

1579-
return keys.reduce((acc, key) => {
1580-
;(acc as any)[key] = cloneObject(obj[key])
1581-
return acc
1582-
}, {})
1634+
// Plain object → create a new object with the same prototype
1635+
// and copy all properties, cloning data properties and keeping
1636+
// accessor properties (getters/setters) as‑is.
1637+
const result = Object.create(proto)
1638+
for (const key of Reflect.ownKeys(obj)) {
1639+
const descriptor = Object.getOwnPropertyDescriptor(obj, key)
1640+
1641+
if (descriptor && (descriptor.get || descriptor.set)) {
1642+
// Accessor property → copy descriptor as‑is (get/set functions)
1643+
Object.defineProperty(result, key, descriptor)
1644+
} else {
1645+
// Data property → clone the value
1646+
result[key] = cloneObject(obj[key])
1647+
}
1648+
}
1649+
1650+
return result
15831651
}

0 commit comments

Comments
 (0)