Skip to content

Commit aa7d4de

Browse files
authored
Enable ppr when dynamicIO is enabled (#79302)
With this PR, we're automatically enabling `ppr` when `dynamicIO` is enabled, and forbid using `ppr: 'incremental'` or `ppr: false` together with `dynamicIO: true`. While implementing the config validation, I noticed that the `userConfig`, `config`, and `result` objects in `assignDefaults` were untyped. This is fixed now.
1 parent 5393395 commit aa7d4de

File tree

5 files changed

+87
-16
lines changed

5 files changed

+87
-16
lines changed

.changeset/dry-roses-nail.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"next": patch
3+
---
4+
5+
Enable `ppr` when `dynamicIO` is enabled

packages/next/errors.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -679,5 +679,6 @@
679679
"678": "CacheSignal got more endRead() calls than beginRead() calls",
680680
"679": "A CacheSignal cannot subscribe to itself",
681681
"680": "CacheSignal cannot be used in the edge runtime, because `dynamicIO` does not support it.",
682-
"681": "Dynamic imports should not be instrumented in the edge runtime, because `dynamicIO` doesn't support it"
682+
"681": "Dynamic imports should not be instrumented in the edge runtime, because `dynamicIO` doesn't support it",
683+
"682": "\\`experimental.ppr\\` can not be \\`%s\\` when \\`experimental.dynamicIO\\` is \\`true\\`. PPR is implicitly enabled when Dynamic IO is enabled."
683684
}

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,10 @@ export interface ExperimentalConfig {
637637
serverComponentsHmrCache?: boolean
638638

639639
/**
640-
* When enabled will cause IO in App Router to be excluded from prerenders
641-
* unless explicitly cached.
640+
* When enabled, will cause IO in App Router to be excluded from prerenders,
641+
* unless explicitly cached. This also enables the experimental Partial
642+
* Prerendering feature of Next.js, and it enables `react@experimental` being
643+
* used for the `app` directory.
642644
*/
643645
dynamicIO?: boolean
644646

@@ -1198,7 +1200,7 @@ export interface NextConfig extends Record<string, any> {
11981200
htmlLimitedBots?: RegExp
11991201
}
12001202

1201-
export const defaultConfig: NextConfig = {
1203+
export const defaultConfig = {
12021204
env: {},
12031205
webpack: null,
12041206
eslint: {
@@ -1391,7 +1393,7 @@ export const defaultConfig: NextConfig = {
13911393
},
13921394
htmlLimitedBots: undefined,
13931395
bundlePagesRouterDependencies: false,
1394-
}
1396+
} satisfies NextConfig
13951397

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

packages/next/src/server/config.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,44 @@ describe('loadConfig', () => {
136136
)
137137
})
138138
})
139+
140+
describe('with a canary version', () => {
141+
beforeAll(() => {
142+
process.env.__NEXT_VERSION = '15.4.0-canary.35'
143+
})
144+
145+
afterAll(() => {
146+
delete process.env.__NEXT_VERSION
147+
})
148+
149+
it('errors when dynamicIO is enabled but PPR is disabled', async () => {
150+
await expect(
151+
loadConfig('', __dirname, {
152+
customConfig: {
153+
experimental: {
154+
dynamicIO: true,
155+
ppr: false,
156+
},
157+
},
158+
})
159+
).rejects.toThrow(
160+
'`experimental.ppr` can not be `false` when `experimental.dynamicIO` is `true`. PPR is implicitly enabled when Dynamic IO is enabled.'
161+
)
162+
})
163+
164+
it('errors when dynamicIO is enabled but PPR set to "incremental"', async () => {
165+
await expect(
166+
loadConfig('', __dirname, {
167+
customConfig: {
168+
experimental: {
169+
dynamicIO: true,
170+
ppr: 'incremental',
171+
},
172+
},
173+
})
174+
).rejects.toThrow(
175+
'`experimental.ppr` can not be `"incremental"` when `experimental.dynamicIO` is `true`. PPR is implicitly enabled when Dynamic IO is enabled.'
176+
)
177+
})
178+
})
139179
})

packages/next/src/server/config.ts

+34-11
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ function warnCustomizedOption(
143143

144144
function assignDefaults(
145145
dir: string,
146-
userConfig: { [key: string]: any },
146+
userConfig: NextConfig & { configFileName: string },
147147
silent: boolean
148-
) {
148+
): NextConfigComplete {
149149
const configFileName = userConfig.configFileName
150150
if (typeof userConfig.exportTrailingSlash !== 'undefined') {
151151
if (!silent) {
@@ -213,9 +213,15 @@ function assignDefaults(
213213
})
214214
}
215215

216-
if (!!value && value.constructor === Object) {
216+
const defaultValue = (defaultConfig as Record<string, unknown>)[key]
217+
218+
if (
219+
!!value &&
220+
value.constructor === Object &&
221+
typeof defaultValue === 'object'
222+
) {
217223
currentConfig[key] = {
218-
...defaultConfig[key],
224+
...defaultValue,
219225
...Object.keys(value).reduce<any>((c, k) => {
220226
const v = value[k]
221227
if (v !== undefined && v !== null) {
@@ -231,7 +237,7 @@ function assignDefaults(
231237
return currentConfig
232238
},
233239
{}
234-
)
240+
) as NextConfig & { configFileName: string }
235241

236242
// TODO: remove these once we've made PPR default
237243
// If this was defaulted to true, it implies that the configuration was
@@ -532,7 +538,7 @@ function assignDefaults(
532538
if (
533539
hasWarnedBuildActivityPosition &&
534540
result.devIndicators !== false &&
535-
result.devIndicators?.buildActivityPosition &&
541+
'buildActivityPosition' in result.devIndicators &&
536542
result.devIndicators.buildActivityPosition !== result.devIndicators.position
537543
) {
538544
Log.warnOnce(
@@ -963,7 +969,11 @@ function assignDefaults(
963969
reason: 'key must only use characters a-z and -',
964970
})
965971
} else {
966-
const handlerPath = result.experimental.cacheHandlers[key]
972+
const handlerPath = (
973+
result.experimental.cacheHandlers as {
974+
[handlerName: string]: string | undefined
975+
}
976+
)[key]
967977

968978
if (handlerPath && !existsSync(handlerPath)) {
969979
invalidHandlerItems.push({
@@ -1095,7 +1105,21 @@ function assignDefaults(
10951105
result.experimental.useCache = result.experimental.dynamicIO
10961106
}
10971107

1098-
return result
1108+
// If dynamicIO is enabled, we also enable PPR.
1109+
if (result.experimental.dynamicIO) {
1110+
if (
1111+
userConfig.experimental?.ppr === false ||
1112+
userConfig.experimental?.ppr === 'incremental'
1113+
) {
1114+
throw new Error(
1115+
`\`experimental.ppr\` can not be \`${JSON.stringify(userConfig.experimental?.ppr)}\` when \`experimental.dynamicIO\` is \`true\`. PPR is implicitly enabled when Dynamic IO is enabled.`
1116+
)
1117+
}
1118+
1119+
result.experimental.ppr = true
1120+
}
1121+
1122+
return result as NextConfigComplete
10991123
}
11001124

11011125
async function applyModifyConfig(
@@ -1401,10 +1425,9 @@ export default async function loadConfig(
14011425
// reactRoot can be updated correctly even with no next.config.js
14021426
const completeConfig = assignDefaults(
14031427
dir,
1404-
defaultConfig,
1428+
{ ...defaultConfig, configFileName },
14051429
silent
14061430
) as NextConfigComplete
1407-
completeConfig.configFileName = configFileName
14081431
setHttpClientAndAgentOptions(completeConfig)
14091432
return await applyModifyConfig(completeConfig, phase, silent)
14101433
}
@@ -1438,7 +1461,7 @@ export function getConfiguredExperimentalFeatures(
14381461

14391462
if (
14401463
name in defaultConfig.experimental &&
1441-
value !== defaultConfig.experimental[name]
1464+
value !== (defaultConfig.experimental as Record<string, unknown>)[name]
14421465
) {
14431466
configuredExperimentalFeatures.push(
14441467
typeof value === 'boolean'

0 commit comments

Comments
 (0)