Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dry-roses-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"next": patch
---

Enable `ppr` when `dynamicIO` is enabled
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -679,5 +679,6 @@
"678": "CacheSignal got more endRead() calls than beginRead() calls",
"679": "A CacheSignal cannot subscribe to itself",
"680": "CacheSignal cannot be used in the edge runtime, because `dynamicIO` does not support it.",
"681": "Dynamic imports should not be instrumented in the edge runtime, because `dynamicIO` doesn't support it"
"681": "Dynamic imports should not be instrumented in the edge runtime, because `dynamicIO` doesn't support it",
"682": "\\`experimental.ppr\\` can not be \\`%s\\` when \\`experimental.dynamicIO\\` is \\`true\\`. PPR is implicitly enabled when Dynamic IO is enabled."
}
10 changes: 6 additions & 4 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,10 @@ export interface ExperimentalConfig {
serverComponentsHmrCache?: boolean

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

Expand Down Expand Up @@ -1198,7 +1200,7 @@ export interface NextConfig extends Record<string, any> {
htmlLimitedBots?: RegExp
}

export const defaultConfig: NextConfig = {
export const defaultConfig = {
env: {},
webpack: null,
eslint: {
Expand Down Expand Up @@ -1391,7 +1393,7 @@ export const defaultConfig: NextConfig = {
},
htmlLimitedBots: undefined,
bundlePagesRouterDependencies: false,
}
} satisfies NextConfig

export async function normalizeConfig(phase: string, config: any) {
if (typeof config === 'function') {
Expand Down
40 changes: 40 additions & 0 deletions packages/next/src/server/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,44 @@ describe('loadConfig', () => {
)
})
})

describe('with a canary version', () => {
beforeAll(() => {
process.env.__NEXT_VERSION = '15.4.0-canary.35'
})

afterAll(() => {
delete process.env.__NEXT_VERSION
})

it('errors when dynamicIO is enabled but PPR is disabled', async () => {
await expect(
loadConfig('', __dirname, {
customConfig: {
experimental: {
dynamicIO: true,
ppr: false,
},
},
})
).rejects.toThrow(
'`experimental.ppr` can not be `false` when `experimental.dynamicIO` is `true`. PPR is implicitly enabled when Dynamic IO is enabled.'
)
})

it('errors when dynamicIO is enabled but PPR set to "incremental"', async () => {
await expect(
loadConfig('', __dirname, {
customConfig: {
experimental: {
dynamicIO: true,
ppr: 'incremental',
},
},
})
).rejects.toThrow(
'`experimental.ppr` can not be `"incremental"` when `experimental.dynamicIO` is `true`. PPR is implicitly enabled when Dynamic IO is enabled.'
)
})
})
})
45 changes: 34 additions & 11 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ function warnCustomizedOption(

function assignDefaults(
dir: string,
userConfig: { [key: string]: any },
userConfig: NextConfig & { configFileName: string },
silent: boolean
) {
): NextConfigComplete {
const configFileName = userConfig.configFileName
if (typeof userConfig.exportTrailingSlash !== 'undefined') {
if (!silent) {
Expand Down Expand Up @@ -213,9 +213,15 @@ function assignDefaults(
})
}

if (!!value && value.constructor === Object) {
const defaultValue = (defaultConfig as Record<string, unknown>)[key]

if (
!!value &&
value.constructor === Object &&
typeof defaultValue === 'object'
) {
currentConfig[key] = {
...defaultConfig[key],
...defaultValue,
...Object.keys(value).reduce<any>((c, k) => {
const v = value[k]
if (v !== undefined && v !== null) {
Expand All @@ -231,7 +237,7 @@ function assignDefaults(
return currentConfig
},
{}
)
) as NextConfig & { configFileName: string }

// TODO: remove these once we've made PPR default
// If this was defaulted to true, it implies that the configuration was
Expand Down Expand Up @@ -532,7 +538,7 @@ function assignDefaults(
if (
hasWarnedBuildActivityPosition &&
result.devIndicators !== false &&
result.devIndicators?.buildActivityPosition &&
'buildActivityPosition' in result.devIndicators &&
result.devIndicators.buildActivityPosition !== result.devIndicators.position
) {
Log.warnOnce(
Expand Down Expand Up @@ -963,7 +969,11 @@ function assignDefaults(
reason: 'key must only use characters a-z and -',
})
} else {
const handlerPath = result.experimental.cacheHandlers[key]
const handlerPath = (
result.experimental.cacheHandlers as {
[handlerName: string]: string | undefined
}
)[key]

if (handlerPath && !existsSync(handlerPath)) {
invalidHandlerItems.push({
Expand Down Expand Up @@ -1095,7 +1105,21 @@ function assignDefaults(
result.experimental.useCache = result.experimental.dynamicIO
}

return result
// If dynamicIO is enabled, we also enable PPR.
if (result.experimental.dynamicIO) {
if (
userConfig.experimental?.ppr === false ||
userConfig.experimental?.ppr === 'incremental'
) {
throw new Error(
`\`experimental.ppr\` can not be \`${JSON.stringify(userConfig.experimental?.ppr)}\` when \`experimental.dynamicIO\` is \`true\`. PPR is implicitly enabled when Dynamic IO is enabled.`
)
}

result.experimental.ppr = true
}

return result as NextConfigComplete
}

async function applyModifyConfig(
Expand Down Expand Up @@ -1401,10 +1425,9 @@ export default async function loadConfig(
// reactRoot can be updated correctly even with no next.config.js
const completeConfig = assignDefaults(
dir,
defaultConfig,
{ ...defaultConfig, configFileName },
silent
) as NextConfigComplete
completeConfig.configFileName = configFileName
setHttpClientAndAgentOptions(completeConfig)
return await applyModifyConfig(completeConfig, phase, silent)
}
Expand Down Expand Up @@ -1438,7 +1461,7 @@ export function getConfiguredExperimentalFeatures(

if (
name in defaultConfig.experimental &&
value !== defaultConfig.experimental[name]
value !== (defaultConfig.experimental as Record<string, unknown>)[name]
) {
configuredExperimentalFeatures.push(
typeof value === 'boolean'
Expand Down