diff --git a/packages/next/src/bin/next.ts b/packages/next/src/bin/next.ts index 811eb34b92c2..68b8f6237bf3 100755 --- a/packages/next/src/bin/next.ts +++ b/packages/next/src/bin/next.ts @@ -124,7 +124,10 @@ program )}` ) .option('-d, --debug', 'Enables a more verbose build output.') - + .option( + '--debug-prerender', + 'Enables debug mode for prerendering. Not for production use!' + ) .option('--no-lint', 'Disables linting.') .option('--no-mangling', 'Disables mangling.') .option('--profile', 'Enables production profiling for React.') diff --git a/packages/next/src/build/build-context.ts b/packages/next/src/build/build-context.ts index d2e2774e1bdc..e0be8fc1f896 100644 --- a/packages/next/src/build/build-context.ts +++ b/packages/next/src/build/build-context.ts @@ -94,4 +94,5 @@ export const NextBuildContext: Partial<{ fetchCacheKeyPrefix?: string allowedRevalidateHeaderKeys?: string[] isCompileMode?: boolean + debugPrerender: boolean }> = {} diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 3423b0e7b8f6..82356d5f0a82 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -807,6 +807,7 @@ export default async function build( dir: string, reactProductionProfiling = false, debugOutput = false, + debugPrerender = false, runLint = true, noMangling = false, appDirOnly = false, @@ -832,6 +833,7 @@ export default async function build( NextBuildContext.appDirOnly = appDirOnly NextBuildContext.reactProductionProfiling = reactProductionProfiling NextBuildContext.noMangling = noMangling + NextBuildContext.debugPrerender = debugPrerender await nextBuildSpan.traceAsyncFn(async () => { // attempt to load global env values so they are available in next.config.js @@ -850,6 +852,7 @@ export default async function build( // Log for next.config loading process silent: false, reactProductionProfiling, + debugPrerender, }), turborepoAccessTraceResult ) @@ -970,10 +973,12 @@ export default async function build( ) // Always log next version first then start rest jobs - const { envInfo, experimentalFeatures } = await getStartServerInfo( + const { envInfo, experimentalFeatures } = await getStartServerInfo({ dir, - false - ) + dev: false, + debugPrerender, + }) + logStartInfo({ networkUrl: null, appUrl: null, @@ -2738,6 +2743,7 @@ export default async function build( silent: true, buildExport: true, debugOutput, + debugPrerender, pages: combinedPages, outdir, statusMessage: 'Generating static pages', diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts index 7a5243433276..17e202ac6651 100644 --- a/packages/next/src/build/turbopack-build/impl.ts +++ b/packages/next/src/build/turbopack-build/impl.ts @@ -249,7 +249,8 @@ export async function workerMain(workerData: { /// load the config because it's not serializable NextBuildContext.config = await loadConfig( PHASE_PRODUCTION_BUILD, - NextBuildContext.dir! + NextBuildContext.dir!, + { debugPrerender: NextBuildContext.debugPrerender } ) // Matches handling in build/index.ts diff --git a/packages/next/src/build/webpack-build/impl.ts b/packages/next/src/build/webpack-build/impl.ts index 50864661dc16..402e4b630ea5 100644 --- a/packages/next/src/build/webpack-build/impl.ts +++ b/packages/next/src/build/webpack-build/impl.ts @@ -385,7 +385,8 @@ export async function workerMain(workerData: { /// load the config because it's not serializable NextBuildContext.config = await loadConfig( PHASE_PRODUCTION_BUILD, - NextBuildContext.dir! + NextBuildContext.dir!, + { debugPrerender: NextBuildContext.debugPrerender } ) NextBuildContext.nextBuildSpan = trace( `worker-main-${workerData.compilerName}` diff --git a/packages/next/src/cli/next-build.ts b/packages/next/src/cli/next-build.ts index a8a1cfd061ae..a2df58fdc413 100755 --- a/packages/next/src/cli/next-build.ts +++ b/packages/next/src/cli/next-build.ts @@ -13,6 +13,7 @@ import { disableMemoryDebuggingMode } from '../lib/memory/shutdown' export type NextBuildOptions = { debug?: boolean + debugPrerender?: boolean profile?: boolean lint: boolean mangling: boolean @@ -31,6 +32,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { const { debug, + debugPrerender, experimentalDebugMemoryUsage, profile, lint, @@ -51,7 +53,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { if (!mangling) { warn( - 'Mangling is disabled. Note: This may affect performance and should only be used for debugging purposes.' + `Mangling is disabled. ${italic('Note: This may affect performance and should only be used for debugging purposes.')}` ) } @@ -61,6 +63,14 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { ) } + if (debugPrerender) { + warn( + `Prerendering is running in debug mode. ${italic( + 'Note: This may affect performance and should not be used for production.' + )}` + ) + } + if (experimentalDebugMemoryUsage) { process.env.EXPERIMENTAL_DEBUG_MEMORY_USAGE = '1' enableMemoryDebuggingMode() @@ -83,6 +93,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => { dir, profile, debug || Boolean(process.env.NEXT_DEBUG_BUILD), + debugPrerender, lint, !mangling, experimentalAppOnly, diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 58b7ddb79584..81368828a858 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -89,9 +89,11 @@ async function exportAppImpl( const nextConfig = options.nextConfig || - (await span - .traceChild('load-next-config') - .traceAsyncFn(() => loadConfig(PHASE_EXPORT, dir))) + (await span.traceChild('load-next-config').traceAsyncFn(() => + loadConfig(PHASE_EXPORT, dir, { + debugPrerender: options.debugPrerender, + }) + )) const distDir = join(dir, nextConfig.distDir) const telemetry = options.buildExport ? null : new Telemetry({ distDir }) diff --git a/packages/next/src/export/types.ts b/packages/next/src/export/types.ts index 6fcea46d1cbf..a8bd4c38fb83 100644 --- a/packages/next/src/export/types.ts +++ b/packages/next/src/export/types.ts @@ -105,6 +105,7 @@ export interface ExportAppOptions { enabledDirectories: NextEnabledDirectories silent?: boolean debugOutput?: boolean + debugPrerender?: boolean pages?: string[] buildExport: boolean statusMessage?: string diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c0ed0c6398bd..6ea826ff282a 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -7,6 +7,7 @@ import * as ciEnvironment from '../server/ci-info' import { CONFIG_FILES, PHASE_DEVELOPMENT_SERVER, + PHASE_EXPORT, PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER, } from '../shared/lib/constants' @@ -1156,14 +1157,18 @@ export default async function loadConfig( customConfig, rawConfig, silent = true, - onLoadUserConfig, + reportExperimentalFeatures, reactProductionProfiling, + debugPrerender, }: { customConfig?: object | null rawConfig?: boolean silent?: boolean - onLoadUserConfig?: (conf: NextConfig) => void + reportExperimentalFeatures?: ( + configuredExperimentalFeatures: ConfiguredExperimentalFeature[] + ) => void reactProductionProfiling?: boolean + debugPrerender?: boolean } = {} ): Promise { if (!process.env.__NEXT_PRIVATE_RENDER_WORKER) { @@ -1259,11 +1264,35 @@ export default async function loadConfig( throw err } + const loadedConfig = Object.freeze( + (await normalizeConfig( + phase, + interopDefault(userConfigModule) + )) as NextConfig + ) + + const configuredExperimentalFeatures: ConfiguredExperimentalFeature[] = [] + + if (reportExperimentalFeatures && loadedConfig.experimental) { + for (const name of Object.keys( + loadedConfig.experimental + ) as (keyof ExperimentalConfig)[]) { + const value = loadedConfig.experimental[name] + + if (name === 'turbo' && !process.env.TURBOPACK) { + // Ignore any Turbopack config if Turbopack is not enabled + continue + } + + addConfiguredExperimentalFeature( + configuredExperimentalFeatures, + name, + value + ) + } + } + // Clone a new userConfig each time to avoid mutating the original - const loadedConfig = (await normalizeConfig( - phase, - interopDefault(userConfigModule) - )) as NextConfig const userConfig = cloneObject(loadedConfig) as NextConfig if (!process.env.NEXT_MINIMAL) { @@ -1382,7 +1411,45 @@ export default async function loadConfig( userConfig.htmlLimitedBots = userConfig.htmlLimitedBots.source } - onLoadUserConfig?.(Object.freeze(loadedConfig)) + if ( + debugPrerender && + (phase === PHASE_PRODUCTION_BUILD || phase === PHASE_EXPORT) + ) { + userConfig.experimental ??= {} + + setExperimentalFeatureForDebugPrerender( + userConfig.experimental, + 'serverSourceMaps', + true, + reportExperimentalFeatures ? configuredExperimentalFeatures : undefined + ) + + setExperimentalFeatureForDebugPrerender( + userConfig.experimental, + process.env.TURBOPACK ? 'turbopackMinify' : 'serverMinification', + false, + reportExperimentalFeatures ? configuredExperimentalFeatures : undefined + ) + + setExperimentalFeatureForDebugPrerender( + userConfig.experimental, + 'enablePrerenderSourceMaps', + true, + reportExperimentalFeatures ? configuredExperimentalFeatures : undefined + ) + + setExperimentalFeatureForDebugPrerender( + userConfig.experimental, + 'prerenderEarlyExit', + false, + reportExperimentalFeatures ? configuredExperimentalFeatures : undefined + ) + } + + if (reportExperimentalFeatures) { + reportExperimentalFeatures(configuredExperimentalFeatures) + } + const completeConfig = assignDefaults( dir, { @@ -1427,48 +1494,50 @@ export default async function loadConfig( return await applyModifyConfig(completeConfig, phase, silent) } -export type ConfiguredExperimentalFeature = - | { name: keyof ExperimentalConfig; type: 'boolean'; value: boolean } - | { name: keyof ExperimentalConfig; type: 'number'; value: number } - | { name: keyof ExperimentalConfig; type: 'other' } +export type ConfiguredExperimentalFeature = { + key: keyof ExperimentalConfig + value: ExperimentalConfig[keyof ExperimentalConfig] + reason?: string +} -export function getConfiguredExperimentalFeatures( - userNextConfigExperimental: NextConfig['experimental'] +export function addConfiguredExperimentalFeature< + KeyType extends keyof ExperimentalConfig, +>( + configuredExperimentalFeatures: ConfiguredExperimentalFeature[], + key: KeyType, + value: ExperimentalConfig[KeyType], + reason?: string ) { - const configuredExperimentalFeatures: ConfiguredExperimentalFeature[] = [] - - if (!userNextConfigExperimental) { - return configuredExperimentalFeatures + if (value !== (defaultConfig.experimental as Record)[key]) { + configuredExperimentalFeatures.push({ key, value, reason }) } +} - // defaultConfig.experimental is predefined and will never be undefined - // This is only a type guard for the typescript - if (defaultConfig.experimental) { - for (const name of Object.keys( - userNextConfigExperimental - ) as (keyof ExperimentalConfig)[]) { - const value = userNextConfigExperimental[name] - - if (name === 'turbo' && !process.env.TURBOPACK) { - // Ignore any Turbopack config if Turbopack is not enabled - continue - } +function setExperimentalFeatureForDebugPrerender< + KeyType extends keyof ExperimentalConfig, +>( + experimentalConfig: ExperimentalConfig, + key: KeyType, + value: ExperimentalConfig[KeyType], + configuredExperimentalFeatures: ConfiguredExperimentalFeature[] | undefined +) { + if (experimentalConfig[key] !== value) { + experimentalConfig[key] = value - if ( - name in defaultConfig.experimental && - value !== (defaultConfig.experimental as Record)[name] - ) { - configuredExperimentalFeatures.push( - typeof value === 'boolean' - ? { name, type: 'boolean', value } - : typeof value === 'number' - ? { name, type: 'number', value } - : { name, type: 'other' } - ) - } + if (configuredExperimentalFeatures) { + const action = + value === true ? 'enabled' : value === false ? 'disabled' : 'set' + + const reason = `${action} by \`--debug-prerender\`` + + addConfiguredExperimentalFeature( + configuredExperimentalFeatures, + key, + value, + reason + ) } } - return configuredExperimentalFeatures } function cloneObject(obj: any): any { diff --git a/packages/next/src/server/lib/app-info-log.ts b/packages/next/src/server/lib/app-info-log.ts index 70fafaa1eaf1..fda653fa9a39 100644 --- a/packages/next/src/server/lib/app-info-log.ts +++ b/packages/next/src/server/lib/app-info-log.ts @@ -5,10 +5,7 @@ import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD, } from '../../shared/lib/constants' -import loadConfig, { - getConfiguredExperimentalFeatures, - type ConfiguredExperimentalFeature, -} from '../config' +import loadConfig, { type ConfiguredExperimentalFeature } from '../config' export function logStartInfo({ networkUrl, @@ -50,15 +47,16 @@ export function logStartInfo({ // only show a maximum number of flags for (const exp of experimentalFeatures.slice(0, maxExperimentalFeatures)) { const symbol = - exp.type === 'boolean' + typeof exp.value === 'boolean' ? exp.value === true ? bold('✓') : bold('⨯') : '·' - const suffix = exp.type === 'number' ? `: ${exp.value}` : '' + const suffix = typeof exp.value === 'number' ? `: ${exp.value}` : '' + const reason = exp.reason ? ` (${exp.reason})` : '' - Log.bootstrap(` ${symbol} ${exp.name}${suffix}`) + Log.bootstrap(` ${symbol} ${exp.key}${suffix}${reason}`) } /* indicate if there are more than the maximum shown no. flags */ if (experimentalFeatures.length > maxExperimentalFeatures) { @@ -70,10 +68,15 @@ export function logStartInfo({ Log.info('') } -export async function getStartServerInfo( - dir: string, +export async function getStartServerInfo({ + dir, + dev, + debugPrerender, +}: { + dir: string dev: boolean -): Promise<{ + debugPrerender?: boolean +}): Promise<{ envInfo?: string[] experimentalFeatures?: ConfiguredExperimentalFeature[] }> { @@ -82,14 +85,12 @@ export async function getStartServerInfo( dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_BUILD, dir, { - onLoadUserConfig(userConfig) { - const configuredExperimentalFeatures = - getConfiguredExperimentalFeatures(userConfig.experimental) - - experimentalFeatures = configuredExperimentalFeatures.sort( - ({ name: a }, { name: b }) => a.length - b.length + reportExperimentalFeatures(features) { + experimentalFeatures = features.sort( + ({ key: a }, { key: b }) => a.length - b.length ) }, + debugPrerender, } ) diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 35f1e5af30d3..e1de5d852350 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -278,7 +278,7 @@ export async function startServer( let envInfo: string[] | undefined let experimentalFeatures: ConfiguredExperimentalFeature[] | undefined if (isDev) { - const startServerInfo = await getStartServerInfo(dir, isDev) + const startServerInfo = await getStartServerInfo({ dir, dev: isDev }) envInfo = startServerInfo.envInfo experimentalFeatures = startServerInfo.experimentalFeatures } diff --git a/test/production/app-dir/build-output-prerender/app/client/page.tsx b/test/production/app-dir/build-output-prerender/app/client/page.tsx new file mode 100644 index 000000000000..215f0bea40bf --- /dev/null +++ b/test/production/app-dir/build-output-prerender/app/client/page.tsx @@ -0,0 +1,5 @@ +'use client' + +export default function Page() { + return

Current time: {new Date().toISOString()}

+} diff --git a/test/production/app-dir/build-output-prerender/app/layout.tsx b/test/production/app-dir/build-output-prerender/app/layout.tsx new file mode 100644 index 000000000000..888614deda3b --- /dev/null +++ b/test/production/app-dir/build-output-prerender/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/build-output-prerender/app/server/page.tsx b/test/production/app-dir/build-output-prerender/app/server/page.tsx new file mode 100644 index 000000000000..7659e2bdad2c --- /dev/null +++ b/test/production/app-dir/build-output-prerender/app/server/page.tsx @@ -0,0 +1,14 @@ +import { setTimeout } from 'timers/promises' + +async function cachedDelay() { + 'use cache' + await setTimeout(3000) +} + +export default async function Page() { + // Defer rendering to ensure the prerender order is deterministic, i.e. the + // client page will be finished prerendering first. + await cachedDelay() + + return

Random: {Math.random()}

+} diff --git a/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts b/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts new file mode 100644 index 000000000000..c43ac86c7f78 --- /dev/null +++ b/test/production/app-dir/build-output-prerender/build-output-prerender.test.ts @@ -0,0 +1,181 @@ +import { nextTestSetup } from 'e2e-utils' +const { version: nextVersion } = require('next/package.json') + +describe('build-output-prerender', () => { + describe('without --debug-prerender', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + beforeAll(() => next.build()) + + it('prints only the user-selected experimental flags', async () => { + if (isTurbopack) { + expect(getPreambleOutput(next.cliOutput)).toMatchInlineSnapshot(` + "▲ Next.js x.y.z (Turbopack) + - Experiments (use with caution): + ✓ dynamicIO" + `) + } else { + expect(getPreambleOutput(next.cliOutput)).toMatchInlineSnapshot(` + "▲ Next.js x.y.z + - Experiments (use with caution): + ✓ dynamicIO" + `) + } + }) + + it('shows only a single prerender error with a mangled stack', async () => { + if (isTurbopack) { + expect(getPrerenderOutput(next.cliOutput)).toMatchInlineSnapshot(` + "Error: Route "/client" used \`new Date()\` inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client + at x () + Error occurred prerendering page "/client". Read more: https://nextjs.org/docs/messages/prerender-error + Export encountered an error on /client/page: /client, exiting the build." + `) + } else { + expect(getPrerenderOutput(next.cliOutput)).toMatchInlineSnapshot(` + "Error: Route "/client" used \`new Date()\` inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client + at x () + Error occurred prerendering page "/client". Read more: https://nextjs.org/docs/messages/prerender-error + Export encountered an error on /client/page: /client, exiting the build." + `) + } + }) + }) + + describe('with --debug-prerender', () => { + const { next, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + buildOptions: ['--debug-prerender'], + }) + + beforeAll(() => next.build()) + + it('prints a warning and the customized experimental flags', async () => { + if (isTurbopack) { + expect(getPreambleOutput(next.cliOutput)).toMatchInlineSnapshot(` + "⚠ Prerendering is running in debug mode. Note: This may affect performance and should not be used for production. + ▲ Next.js x.y.z (Turbopack) + - Experiments (use with caution): + ✓ dynamicIO + ⨯ turbopackMinify (disabled by \`--debug-prerender\`) + ✓ serverSourceMaps (enabled by \`--debug-prerender\`) + ⨯ prerenderEarlyExit (disabled by \`--debug-prerender\`) + ✓ enablePrerenderSourceMaps (enabled by \`--debug-prerender\`)" + `) + } else { + expect(getPreambleOutput(next.cliOutput)).toMatchInlineSnapshot(` + "⚠ Prerendering is running in debug mode. Note: This may affect performance and should not be used for production. + ▲ Next.js x.y.z + - Experiments (use with caution): + ✓ dynamicIO + ✓ serverSourceMaps (enabled by \`--debug-prerender\`) + ⨯ serverMinification (disabled by \`--debug-prerender\`) + ⨯ prerenderEarlyExit (disabled by \`--debug-prerender\`) + ✓ enablePrerenderSourceMaps (enabled by \`--debug-prerender\`)" + `) + } + }) + + it('shows all prerender errors with readable stacks and code frames', async () => { + if (isTurbopack) { + expect(getPrerenderOutput(next.cliOutput)).toMatchInlineSnapshot(` + "Error: Route "/client" used \`new Date()\` inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client + at Page (turbopack:///[project]/app/client/page.tsx:4:27) + 2 | + 3 | export default function Page() { + > 4 | return

Current time: {new Date().toISOString()}

+ | ^ + 5 | } + 6 | + Error occurred prerendering page "/client". Read more: https://nextjs.org/docs/messages/prerender-error + Error: Route "/server" used \`Math.random()\` outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-random + at Page (turbopack:///[project]/app/server/page.tsx:13:26) + 11 | await cachedDelay() + 12 | + > 13 | return

Random: {Math.random()}

+ | ^ + 14 | } + 15 | + Error occurred prerendering page "/server". Read more: https://nextjs.org/docs/messages/prerender-error + + > Export encountered errors on following paths: + /client/page: /client + /server/page: /server" + `) + } else { + expect(getPrerenderOutput(next.cliOutput)).toMatchInlineSnapshot(` + "Error: Route "/client" used \`new Date()\` inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client + at Page (webpack:///app/client/page.tsx:4:27) + 2 | + 3 | export default function Page() { + > 4 | return

Current time: {new Date().toISOString()}

+ | ^ + 5 | } + 6 | + Error occurred prerendering page "/client". Read more: https://nextjs.org/docs/messages/prerender-error + Error: Route "/server" used \`Math.random()\` outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-random + at Page (webpack:///app/server/page.tsx:13:26) + 11 | await cachedDelay() + 12 | + > 13 | return

Random: {Math.random()}

+ | ^ + 14 | } + 15 | + Error occurred prerendering page "/server". Read more: https://nextjs.org/docs/messages/prerender-error + + > Export encountered errors on following paths: + /client/page: /client + /server/page: /server" + `) + } + }) + }) +}) + +function getPreambleOutput(cliOutput: string): string { + const lines: string[] = [] + + for (const line of cliOutput.split('\n')) { + if (line.includes('Creating an optimized production build')) { + break + } + + // Ignore the test-only warning that `experimental.ppr` has been defaulted + // to `true` when `__NEXT_EXPERIMENTAL_PPR` is set to `true`. + if (line.includes('__NEXT_EXPERIMENTAL_PPR')) { + continue + } + + lines.push(line.replace(nextVersion, 'x.y.z')) + } + + return lines.join('\n').trim() +} + +function getPrerenderOutput(cliOutput: string): string { + let foundPrerenderingLine = false + const lines: string[] = [] + + for (const line of cliOutput.split('\n')) { + if (line.includes('Collecting page data')) { + foundPrerenderingLine = true + continue + } + + if (line.includes('Next.js build worker exited')) { + break + } + + if (foundPrerenderingLine && !line.includes('Generating static pages')) { + lines.push( + line.replace(/at \w+ \(.next[^)]+\)/, 'at x ()') + ) + } + } + + return lines.join('\n').trim() +} diff --git a/test/production/app-dir/build-output-prerender/next.config.js b/test/production/app-dir/build-output-prerender/next.config.js new file mode 100644 index 000000000000..ac4afcf43219 --- /dev/null +++ b/test/production/app-dir/build-output-prerender/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + dynamicIO: true, + }, +} + +module.exports = nextConfig