diff --git a/.changeset/dry-roses-nail.md b/.changeset/dry-roses-nail.md new file mode 100644 index 000000000000..84b8b9737832 --- /dev/null +++ b/.changeset/dry-roses-nail.md @@ -0,0 +1,5 @@ +--- +"next": patch +--- + +Enable `ppr` when `dynamicIO` is enabled diff --git a/packages/next/errors.json b/packages/next/errors.json index 177868cb7cc2..222c81ff167d 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -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." } diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index d2504d2d8e14..0b5314a1e346 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -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 @@ -1198,7 +1200,7 @@ export interface NextConfig extends Record { htmlLimitedBots?: RegExp } -export const defaultConfig: NextConfig = { +export const defaultConfig = { env: {}, webpack: null, eslint: { @@ -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') { diff --git a/packages/next/src/server/config.test.ts b/packages/next/src/server/config.test.ts index 4ec04e82f896..700ac089f368 100644 --- a/packages/next/src/server/config.test.ts +++ b/packages/next/src/server/config.test.ts @@ -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.' + ) + }) + }) }) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index fd9ae3c18bb3..f53de12325b4 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -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) { @@ -213,9 +213,15 @@ function assignDefaults( }) } - if (!!value && value.constructor === Object) { + const defaultValue = (defaultConfig as Record)[key] + + if ( + !!value && + value.constructor === Object && + typeof defaultValue === 'object' + ) { currentConfig[key] = { - ...defaultConfig[key], + ...defaultValue, ...Object.keys(value).reduce((c, k) => { const v = value[k] if (v !== undefined && v !== null) { @@ -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 @@ -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( @@ -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({ @@ -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( @@ -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) } @@ -1438,7 +1461,7 @@ export function getConfiguredExperimentalFeatures( if ( name in defaultConfig.experimental && - value !== defaultConfig.experimental[name] + value !== (defaultConfig.experimental as Record)[name] ) { configuredExperimentalFeatures.push( typeof value === 'boolean'