Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f2732e8
Use serialized next config for prod server
devjiwonchoi Sep 19, 2025
a3ae0bc
test: add test
devjiwonchoi Sep 19, 2025
14d14f1
match the manifest format
devjiwonchoi Sep 19, 2025
ee41b8c
update comment
devjiwonchoi Sep 19, 2025
5c3b362
errors.json
devjiwonchoi Sep 19, 2025
330b4f6
test: update to ensure it loads from serialized
devjiwonchoi Sep 19, 2025
1899b72
fall back to loading the config
devjiwonchoi Sep 19, 2025
3a40f56
test: update tests
devjiwonchoi Sep 19, 2025
5431ec6
test: update test/integration/config-experimental-warning/test/index.…
devjiwonchoi Sep 19, 2025
c6e5c03
test: fix lint
devjiwonchoi Sep 19, 2025
e632b94
test: fix lint
devjiwonchoi Sep 19, 2025
ac81ea2
test: i hate semi false
devjiwonchoi Sep 19, 2025
6a6ff0d
add serializeNextConfigForProduction flag
devjiwonchoi Sep 19, 2025
15b5f8e
test: disable serializeNextConfigForProduction for nx test
devjiwonchoi Sep 19, 2025
a782d26
update comment
devjiwonchoi Sep 19, 2025
9b98fb5
test: update comment
devjiwonchoi Sep 19, 2025
21d753b
config.experimental?.serializeNextConfigForProduction when write next…
devjiwonchoi Sep 19, 2025
b20019d
set serializeNextConfigForProduction to true on default config
devjiwonchoi Sep 19, 2025
7a63bfb
fix lint
devjiwonchoi Sep 19, 2025
bd9356a
add to schema
devjiwonchoi Sep 19, 2025
ca0b9bd
add __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION flag
devjiwonchoi Sep 20, 2025
a27a151
ci: enable __NEXT_EXPERIMENTAL_SERIALIZE_NEXT_CONFIG_FOR_PRODUCTION flag
devjiwonchoi Sep 20, 2025
0a25ac8
flatten logic
devjiwonchoi Sep 20, 2025
7a94269
test: enable serializeNextConfigForProduction
devjiwonchoi Sep 20, 2025
89edac0
use nullish coalescing
devjiwonchoi Sep 20, 2025
3830f49
update error json
devjiwonchoi Sep 20, 2025
d54197a
test: update tests
devjiwonchoi Sep 20, 2025
e999ffb
test: remove unnecessary addition
devjiwonchoi Sep 20, 2025
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
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -808,5 +808,6 @@
"807": "Expected a %s response header.",
"808": "Invalid binary HMR message: insufficient data (expected %s bytes, got %s)",
"809": "Invalid binary HMR message of type %s",
"810": "React debug channel stream error"
"810": "React debug channel stream error",
"811": "Failed to load the serialized config file \"%s\" in \"%s\"."
}
26 changes: 26 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
PRERENDER_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
SERIALIZED_CONFIG_FILE,
SERVER_DIRECTORY,
SERVER_FILES_MANIFEST,
STATIC_STATUS_PAGES,
Expand Down Expand Up @@ -2723,6 +2724,31 @@ export default async function build(
requiredServerFilesManifest
)

// The required-server-files manifest contains serialized config, which can
// be loaded by the prod server. However, when a custom distDir is set,
// the prod server will not know the distDir until loading the config.
// Therefore we write the serialized config to the same directory as the
// original config file.
if (
config.output !== 'standalone' &&
config.distDir !== '.next' &&
config.experimental?.serializeNextConfigForProduction
) {
const serializedConfigPath = path.join(
// Write to the same directory as the original config file.
config.configFile ? path.dirname(config.configFile) : dir,
SERIALIZED_CONFIG_FILE
)
await fs.writeFile(
serializedConfigPath,
JSON.stringify({
// To match the format of required server files manifest.
version: 1,
config: requiredServerFilesManifest.config,
})
)
}

// we don't need to inline for turbopack build as
// it will handle it's own caching separate of compile
if (isGenerateMode && !isTurbopack) {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
])
.optional(),
optimizeRouterScrolling: z.boolean().optional(),
serializeNextConfigForProduction: z.boolean().optional(),
})
.optional(),
exportPathMap: z
Expand Down
13 changes: 13 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,18 @@ export interface ExperimentalConfig {
* instead of `{distDir}`.
*/
isolatedDevBuild?: boolean

/**
* When enabled, the produciton server will use the serialized config file
Comment thread
devjiwonchoi marked this conversation as resolved.
Outdated
* instead of the original config file. This can save the time of loading the
* config file, especially when you are using `next.config.ts`. When the `distDir`
* is set, the serialized config `next-config-serialized.json` will be written to
* the same directory as the original config file. This is because Next.js doesn't
* know the `distDir` until loading the config.
*
* @default true
*/
serializeNextConfigForProduction?: boolean
Comment thread
devjiwonchoi marked this conversation as resolved.
}

export type ExportPathMap = {
Expand Down Expand Up @@ -1495,6 +1507,7 @@ export const defaultConfig = Object.freeze({
browserDebugInfoInTerminal: false,
optimizeRouterScrolling: false,
isolatedDevBuild: false,
serializeNextConfigForProduction: true,
},
htmlLimitedBots: undefined,
bundlePagesRouterDependencies: false,
Expand Down
73 changes: 71 additions & 2 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { existsSync } from 'fs'
import { basename, extname, join, relative, isAbsolute, resolve } from 'path'
import { existsSync, readFileSync } from 'fs'
import {
basename,
extname,
join,
relative,
isAbsolute,
resolve,
dirname,
} from 'path'
import { pathToFileURL } from 'url'
import findUp from 'next/dist/compiled/find-up'
import * as Log from '../build/output/log'
Expand All @@ -9,6 +17,9 @@ import {
PHASE_DEVELOPMENT_SERVER,
PHASE_EXPORT,
PHASE_PRODUCTION_BUILD,
PHASE_PRODUCTION_SERVER,
SERVER_FILES_MANIFEST,
SERIALIZED_CONFIG_FILE,
type PHASE_TYPE,
} from '../shared/lib/constants'
import { defaultConfig, normalizeConfig } from './config-shared'
Expand Down Expand Up @@ -1344,6 +1355,64 @@ export default async function loadConfig(
return standaloneConfig
}

// During prod server, we can load from the serialized config.
if (phase === PHASE_PRODUCTION_SERVER) {
function tryLoadSerializedConfig(
configPath: string
): NextConfigComplete | null {
try {
if (!existsSync(configPath)) {
return null
}

const parsed = JSON.parse(readFileSync(configPath, 'utf8'))
const config = parsed.config

// Cache the config
configCache.set(cacheKey, {
config,
rawConfig: config,
configuredExperimentalFeatures: [],
})

return config
} catch {
// Continue to next option
Comment thread
devjiwonchoi marked this conversation as resolved.
Outdated
}
Comment thread
devjiwonchoi marked this conversation as resolved.
Outdated
return null
}

// Try load from ".next" dir since we don't know the
// distDir until loading the config.
const fromManifest = tryLoadSerializedConfig(
join(dir, '.next', SERVER_FILES_MANIFEST)
)
if (
fromManifest &&
// Don't return here and will eventually fall back to loading the config.
fromManifest.experimental?.serializeNextConfigForProduction
) {
return fromManifest
}

// If the custom distDir is set, we write the serialized config
// to the same directory as the original config file.
const configPath = await findUp(CONFIG_FILES, { cwd: dir })
const targetDir = configPath ? dirname(configPath) : dir

// Even though serializeNextConfigForProduction is enabled, we still need to check
// the existSync because there's no way to know if serializeNextConfigForProduction
// is enabled until we load the config.
const fromSerialized = tryLoadSerializedConfig(
join(targetDir, SERIALIZED_CONFIG_FILE)
)
if (fromSerialized) {
return fromSerialized
}

// Fall back to loading the config.
}
Comment thread
devjiwonchoi marked this conversation as resolved.

const curLog = silent
? {
warn: () => {},
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/shared/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const PRERENDER_MANIFEST = 'prerender-manifest.json'
export const ROUTES_MANIFEST = 'routes-manifest.json'
export const IMAGES_MANIFEST = 'images-manifest.json'
export const SERVER_FILES_MANIFEST = 'required-server-files.json'
export const SERIALIZED_CONFIG_FILE = 'next-config-serialized.json'
export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json'
export const MIDDLEWARE_MANIFEST = 'middleware-manifest.json'
export const TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST =
Expand Down
7 changes: 7 additions & 0 deletions test/e2e/app-dir/nx-handling/apps/next-nx-test/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const nextConfig = {
// Use this to set Nx-specific options
// See: https://nx.dev/recipes/next/next-config-setup
nx: {},
experimental: {
// Disable because nx tries to copy the config to the dist dir
// and expect to load the config inside the dist dir again.
// In this case, the `distDir` will be relative to the original config,
// not the config inside the dist dir, and cause distDir path mismatch.
serializeNextConfigForProduction: false,
},
}

const plugins = [
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/config-schema-check/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('next.config.js schema validating - defaultConfig', () => {
})

describe('next.config.js schema validating - invalid config', () => {
const { next, isNextStart, skipped } = nextTestSetup({
const { next, skipped } = nextTestSetup({
files: {
'pages/index.js': `
export default function Page() {
Expand All @@ -58,8 +58,8 @@ describe('next.config.js schema validating - invalid config', () => {

expect(output).toContain('Invalid next.config.js options detected')
expect(output).toContain('badKey')
// for next start and next build we both display the warnings
expect(warningTimes).toBe(isNextStart ? 2 : 1)
// With serialized config, warnings only appear during build, not during start
expect(warningTimes).toBe(1)

return 'success'
}, 'success')
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/next-phase/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe('next-phase', () => {
const phases = {
dev: 'phase-development-server',
build: 'phase-production-build',
start: 'phase-production-server',
// Serialized next config will use the config from build.
start: 'phase-production-build',
}
const currentPhase = isNextDev ? phases.dev : phases.build
const nonExistedPhase = isNextDev ? phases.build : phases.dev
Expand All @@ -39,8 +40,6 @@ describe('next-phase', () => {

if (isNextDev) {
expect(next.cliOutput).not.toContain(phases.start)
} else {
expect(next.cliOutput).toContain(phases.start)
}
})
})
Loading
Loading