Skip to content

Commit d80bd57

Browse files
authored
Indicate boolean value for configured experimental features on startup (vercel#74691)
When printing the configured experimental features of the Next.js config, we are currently not discriminating `true` from `false` values. This is especially confusing when disabling an experimental feature that is enabled by default. In this case it appears in the output as if the feature was enabled. By using `✓` and `⨯` for boolean feature flags (and `·` for others), users can now clearly see whether a configured feature is enabled or disabled. **Before:** <img width="377" alt="before" src="https://github.com/user-attachments/assets/9cb75c1a-910d-48d2-ba1e-048213523e5f" /> **After:** <img width="377" alt="after" src="https://github.com/user-attachments/assets/9975366d-b8fd-48ef-83e3-44fbad633f48" />
1 parent cbef647 commit d80bd57

File tree

7 files changed

+82
-39
lines changed

7 files changed

+82
-39
lines changed

packages/next/src/build/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -904,12 +904,15 @@ export default async function build(
904904
)
905905

906906
// Always log next version first then start rest jobs
907-
const { envInfo, expFeatureInfo } = await getStartServerInfo(dir, false)
907+
const { envInfo, experimentalFeatures } = await getStartServerInfo(
908+
dir,
909+
false
910+
)
908911
logStartInfo({
909912
networkUrl: null,
910913
appUrl: null,
911914
envInfo,
912-
expFeatureInfo,
915+
experimentalFeatures,
913916
})
914917

915918
const ignoreESLint = Boolean(config.eslint.ignoreDuringBuilds)

packages/next/src/server/config.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,29 +1268,40 @@ export default async function loadConfig(
12681268
return completeConfig
12691269
}
12701270

1271-
export function getEnabledExperimentalFeatures(
1271+
export type ConfiguredExperimentalFeature =
1272+
| { name: keyof ExperimentalConfig; type: 'boolean'; value: boolean }
1273+
| { name: keyof ExperimentalConfig; type: 'other' }
1274+
1275+
export function getConfiguredExperimentalFeatures(
12721276
userNextConfigExperimental: NextConfig['experimental']
12731277
) {
1274-
const enabledExperiments: (keyof ExperimentalConfig)[] = []
1278+
const configuredExperimentalFeatures: ConfiguredExperimentalFeature[] = []
12751279

1276-
if (!userNextConfigExperimental) return enabledExperiments
1280+
if (!userNextConfigExperimental) {
1281+
return configuredExperimentalFeatures
1282+
}
12771283

12781284
// defaultConfig.experimental is predefined and will never be undefined
12791285
// This is only a type guard for the typescript
12801286
if (defaultConfig.experimental) {
1281-
for (const featureName of Object.keys(
1287+
for (const name of Object.keys(
12821288
userNextConfigExperimental
12831289
) as (keyof ExperimentalConfig)[]) {
1290+
const value = userNextConfigExperimental[name]
1291+
12841292
if (
1285-
featureName in defaultConfig.experimental &&
1286-
userNextConfigExperimental[featureName] !==
1287-
defaultConfig.experimental[featureName]
1293+
name in defaultConfig.experimental &&
1294+
value !== defaultConfig.experimental[name]
12881295
) {
1289-
enabledExperiments.push(featureName)
1296+
configuredExperimentalFeatures.push(
1297+
typeof value === 'boolean'
1298+
? { name, type: 'boolean', value }
1299+
: { name, type: 'other' }
1300+
)
12901301
}
12911302
}
12921303
}
1293-
return enabledExperiments
1304+
return configuredExperimentalFeatures
12941305
}
12951306

12961307
class CanaryOnlyError extends Error {

packages/next/src/server/lib/app-info-log.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import {
55
PHASE_DEVELOPMENT_SERVER,
66
PHASE_PRODUCTION_BUILD,
77
} from '../../shared/lib/constants'
8-
import loadConfig, { getEnabledExperimentalFeatures } from '../config'
8+
import loadConfig, {
9+
getConfiguredExperimentalFeatures,
10+
type ConfiguredExperimentalFeature,
11+
} from '../config'
912

1013
export function logStartInfo({
1114
networkUrl,
1215
appUrl,
1316
envInfo,
14-
expFeatureInfo,
17+
experimentalFeatures,
1518
maxExperimentalFeatures = Infinity,
1619
}: {
1720
networkUrl: string | null
1821
appUrl: string | null
1922
envInfo?: string[]
20-
expFeatureInfo?: string[]
23+
experimentalFeatures?: ConfiguredExperimentalFeature[]
2124
maxExperimentalFeatures?: number
2225
}) {
2326
Log.bootstrap(
@@ -33,14 +36,21 @@ export function logStartInfo({
3336
}
3437
if (envInfo?.length) Log.bootstrap(`- Environments: ${envInfo.join(', ')}`)
3538

36-
if (expFeatureInfo?.length) {
39+
if (experimentalFeatures?.length) {
3740
Log.bootstrap(`- Experiments (use with caution):`)
3841
// only show a maximum number of flags
39-
for (const exp of expFeatureInfo.slice(0, maxExperimentalFeatures)) {
40-
Log.bootstrap(` · ${exp}`)
42+
for (const exp of experimentalFeatures.slice(0, maxExperimentalFeatures)) {
43+
const symbol =
44+
exp.type === 'boolean'
45+
? exp.value === true
46+
? bold('✓')
47+
: bold('⨯')
48+
: '·'
49+
50+
Log.bootstrap(` ${symbol} ${exp.name}`)
4151
}
4252
/* indicate if there are more than the maximum shown no. flags */
43-
if (expFeatureInfo.length > maxExperimentalFeatures) {
53+
if (experimentalFeatures.length > maxExperimentalFeatures) {
4454
Log.bootstrap(` · ...`)
4555
}
4656
}
@@ -54,19 +64,19 @@ export async function getStartServerInfo(
5464
dev: boolean
5565
): Promise<{
5666
envInfo?: string[]
57-
expFeatureInfo?: string[]
67+
experimentalFeatures?: ConfiguredExperimentalFeature[]
5868
}> {
59-
let expFeatureInfo: string[] = []
69+
let experimentalFeatures: ConfiguredExperimentalFeature[] = []
6070
await loadConfig(
6171
dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_BUILD,
6272
dir,
6373
{
6474
onLoadUserConfig(userConfig) {
65-
const userNextConfigExperimental = getEnabledExperimentalFeatures(
66-
userConfig.experimental
67-
)
68-
expFeatureInfo = userNextConfigExperimental.sort(
69-
(a, b) => a.length - b.length
75+
const configuredExperimentalFeatures =
76+
getConfiguredExperimentalFeatures(userConfig.experimental)
77+
78+
experimentalFeatures = configuredExperimentalFeatures.sort(
79+
({ name: a }, { name: b }) => a.length - b.length
7080
)
7181
},
7282
}
@@ -83,6 +93,6 @@ export async function getStartServerInfo(
8393

8494
return {
8595
envInfo,
86-
expFeatureInfo,
96+
experimentalFeatures,
8797
}
8898
}

packages/next/src/server/lib/start-server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { isPostpone } from './router-utils/is-postpone'
3434
import { isIPv6 } from './is-ipv6'
3535
import { AsyncCallbackSet } from './async-callback-set'
3636
import type { NextServer } from '../next'
37+
import type { ConfiguredExperimentalFeature } from '../config'
3738

3839
const debug = setupDebug('next:start-server')
3940
let startServerSpan: Span | undefined
@@ -270,17 +271,17 @@ export async function startServer(
270271

271272
// Only load env and config in dev to for logging purposes
272273
let envInfo: string[] | undefined
273-
let expFeatureInfo: string[] | undefined
274+
let experimentalFeatures: ConfiguredExperimentalFeature[] | undefined
274275
if (isDev) {
275276
const startServerInfo = await getStartServerInfo(dir, isDev)
276277
envInfo = startServerInfo.envInfo
277-
expFeatureInfo = startServerInfo.expFeatureInfo
278+
experimentalFeatures = startServerInfo.experimentalFeatures
278279
}
279280
logStartInfo({
280281
networkUrl,
281282
appUrl,
282283
envInfo,
283-
expFeatureInfo,
284+
experimentalFeatures,
284285
maxExperimentalFeatures: 3,
285286
})
286287

test/e2e/app-dir/ppr/ppr.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { nextTestSetup } from 'e2e-utils'
22
import { retry, findAllTelemetryEvents } from 'next-test-utils'
3+
import stripAnsi from 'strip-ansi'
34

45
describe('ppr', () => {
56
const { next, isNextDev, isNextStart } = nextTestSetup({
@@ -12,7 +13,7 @@ describe('ppr', () => {
1213
it('should indicate the feature is experimental', async () => {
1314
await retry(() => {
1415
expect(next.cliOutput).toContain('Experiments (use with caution)')
15-
expect(next.cliOutput).toContain('ppr')
16+
expect(stripAnsi(next.cliOutput)).toContain('ppr')
1617
})
1718
})
1819
if (isNextStart) {

test/e2e/react-compiler/react-compiler.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { nextTestSetup, FileRef } from 'e2e-utils'
22
import { retry } from 'next-test-utils'
33
import { join } from 'path'
4+
import stripAnsi from 'strip-ansi'
45

56
describe.each(
67
['default', process.env.TURBOPACK ? undefined : 'babelrc'].filter(Boolean)
@@ -22,7 +23,7 @@ describe.each(
2223
it('should show an experimental warning', async () => {
2324
await retry(() => {
2425
expect(next.cliOutput).toContain('Experiments (use with caution)')
25-
expect(next.cliOutput).toContain('reactCompiler')
26+
expect(stripAnsi(next.cliOutput)).toContain('reactCompiler')
2627
})
2728
})
2829

test/integration/config-experimental-warning/test/index.test.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('Config Experimental Warning', () => {
8484

8585
const stdout = await collectStdoutFromDev(appDir)
8686
expect(stdout).toMatch(experimentalHeader)
87-
expect(stdout).toMatch(' · workerThreads')
87+
expect(stdout).toMatch(' workerThreads')
8888
})
8989

9090
it('should show warning with config from function with experimental', async () => {
@@ -98,7 +98,7 @@ describe('Config Experimental Warning', () => {
9898

9999
const stdout = await collectStdoutFromDev(appDir)
100100
expect(stdout).toMatch(experimentalHeader)
101-
expect(stdout).toMatch(' · workerThreads')
101+
expect(stdout).toMatch(' workerThreads')
102102
})
103103

104104
it('should not show warning with default value', async () => {
@@ -112,7 +112,21 @@ describe('Config Experimental Warning', () => {
112112

113113
const stdout = await collectStdoutFromDev(appDir)
114114
expect(stdout).not.toContain(experimentalHeader)
115-
expect(stdout).not.toContain(' · workerThreads')
115+
expect(stdout).not.toContain('workerThreads')
116+
})
117+
118+
it('should show warning with a symbol indicating that a default `true` value is set to `false`', async () => {
119+
configFile.write(`
120+
module.exports = {
121+
experimental: {
122+
prerenderEarlyExit: false
123+
}
124+
}
125+
`)
126+
127+
const stdout = await collectStdoutFromDev(appDir)
128+
expect(stdout).toMatch(experimentalHeader)
129+
expect(stdout).toMatch(' ⨯ prerenderEarlyExit')
116130
})
117131

118132
it('should show warning with config from object with experimental and multiple keys', async () => {
@@ -127,8 +141,8 @@ describe('Config Experimental Warning', () => {
127141

128142
const stdout = await collectStdoutFromDev(appDir)
129143
expect(stdout).toContain(experimentalHeader)
130-
expect(stdout).toContain(' · workerThreads')
131-
expect(stdout).toContain(' · scrollRestoration')
144+
expect(stdout).toContain(' workerThreads')
145+
expect(stdout).toContain(' scrollRestoration')
132146
})
133147
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
134148
'production mode',
@@ -163,16 +177,18 @@ describe('Config Experimental Warning', () => {
163177
workerThreads: true,
164178
scrollRestoration: true,
165179
parallelServerCompiles: true,
180+
prerenderEarlyExit: false,
166181
cpus: 2,
167182
}
168183
}
169184
`)
170185
const stdout = await collectStdoutFromBuild(appDir)
171186
expect(stdout).toMatch(experimentalHeader)
172187
expect(stdout).toMatch(' · cpus')
173-
expect(stdout).toMatch(' · workerThreads')
174-
expect(stdout).toMatch(' · scrollRestoration')
175-
expect(stdout).toMatch(' · parallelServerCompiles')
188+
expect(stdout).toMatch(' ✓ workerThreads')
189+
expect(stdout).toMatch(' ✓ scrollRestoration')
190+
expect(stdout).toMatch(' ⨯ prerenderEarlyExit')
191+
expect(stdout).toMatch(' ✓ parallelServerCompiles')
176192
})
177193

178194
it('should show unrecognized experimental features in warning but not in start log experiments section', async () => {

0 commit comments

Comments
 (0)