Skip to content

Commit 33c561b

Browse files
authored
Consolidate experimental React opt-in & add ppr flag (#55560)
This consolidates how we're evaluating when to opt into `react@experimental` since it's sprinkled in a lot of spots. Also adds a new flag to opt into the experimental channel Closes NEXT-1632
1 parent f630cb8 commit 33c561b

File tree

12 files changed

+99
-36
lines changed

12 files changed

+99
-36
lines changed

packages/next/src/build/index.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ import { createClientRouterFilter } from '../lib/create-client-router-filter'
145145
import { createValidFileMatcher } from '../server/lib/find-page-file'
146146
import { startTypeChecking } from './type-check'
147147
import { generateInterceptionRoutesRewrites } from '../lib/generate-interception-routes-rewrites'
148+
import { needsExperimentalReact } from '../lib/needs-experimental-react'
148149

149150
import { buildDataRoute } from '../server/lib/router-utils/build-data-route'
150151
import { defaultOverrides } from '../server/require-hook'
@@ -1255,11 +1256,9 @@ export default async function build(
12551256
__NEXT_INCREMENTAL_CACHE_IPC_PORT: incrementalCacheIpcPort + '',
12561257
__NEXT_INCREMENTAL_CACHE_IPC_KEY:
12571258
incrementalCacheIpcValidationKey,
1258-
__NEXT_PRIVATE_PREBUNDLED_REACT: hasAppDir
1259-
? config.experimental.serverActions
1260-
? 'experimental'
1261-
: 'next'
1262-
: '',
1259+
__NEXT_PRIVATE_PREBUNDLED_REACT: needsExperimentalReact(config)
1260+
? 'experimental'
1261+
: 'next',
12631262
},
12641263
},
12651264
enableWorkerThreads: config.experimental.workerThreads,
@@ -2438,8 +2437,7 @@ export default async function build(
24382437
outputFileTracingRoot,
24392438
requiredServerFiles.config,
24402439
middlewareManifest,
2441-
hasInstrumentationHook,
2442-
hasAppDir
2440+
hasInstrumentationHook
24432441
)
24442442
})
24452443
}

packages/next/src/build/utils.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NextConfigComplete } from '../server/config-shared'
1+
import type { NextConfig, NextConfigComplete } from '../server/config-shared'
22
import type { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
33
import type { AssetBinding } from './webpack/loaders/get-module-build-info'
44
import type { GetStaticPaths, PageConfig, ServerRuntime } from 'next/types'
@@ -67,7 +67,8 @@ import { nodeFs } from '../server/lib/node-fs-methods'
6767
import * as ciEnvironment from '../telemetry/ci-info'
6868
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
6969
import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path'
70-
// import { AppRouteRouteModule } from '../server/future/route-modules/app-route/module'
70+
import { needsExperimentalReact } from '../lib/needs-experimental-react'
71+
7172
const { AppRouteRouteModule } =
7273
require('../server/future/route-modules/app-route/module.compiled') as typeof import('../server/future/route-modules/app-route/module')
7374

@@ -1826,13 +1827,17 @@ export async function copyTracedFiles(
18261827
pageKeys: readonly string[],
18271828
appPageKeys: readonly string[] | undefined,
18281829
tracingRoot: string,
1829-
serverConfig: { [key: string]: any },
1830+
serverConfig: NextConfig,
18301831
middlewareManifest: MiddlewareManifest,
1831-
hasInstrumentationHook: boolean,
1832-
hasAppDir: boolean
1832+
hasInstrumentationHook: boolean
18331833
) {
18341834
const outputPath = path.join(distDir, 'standalone')
18351835
let moduleType = false
1836+
const nextConfig = {
1837+
...serverConfig,
1838+
distDir: `./${path.relative(dir, distDir)}`,
1839+
}
1840+
const hasExperimentalReact = needsExperimentalReact(nextConfig)
18361841
try {
18371842
const packageJsonPath = path.join(distDir, '../package.json')
18381843
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
@@ -1956,6 +1961,7 @@ export async function copyTracedFiles(
19561961
'server.js'
19571962
)
19581963
await fs.mkdir(path.dirname(serverOutputPath), { recursive: true })
1964+
19591965
await fs.writeFile(
19601966
serverOutputPath,
19611967
`${
@@ -1985,17 +1991,12 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
19851991
const hostname = process.env.HOSTNAME || '0.0.0.0'
19861992
19871993
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
1988-
const nextConfig = ${JSON.stringify({
1989-
...serverConfig,
1990-
distDir: `./${path.relative(dir, distDir)}`,
1991-
})}
1994+
const nextConfig = ${JSON.stringify(nextConfig)}
19921995
19931996
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
1994-
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = ${hasAppDir}
1995-
? nextConfig.experimental && nextConfig.experimental.serverActions
1996-
? 'experimental'
1997-
: 'next'
1998-
: '';
1997+
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = ${hasExperimentalReact}
1998+
? 'experimental'
1999+
: 'next'
19992000
20002001
require('next')
20012002
const { startServer } = require('next/dist/server/lib/start-server')

packages/next/src/build/webpack-config.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { getSupportedBrowsers } from './utils'
7171
import { MemoryWithGcCachePlugin } from './webpack/plugins/memory-with-gc-cache-plugin'
7272
import { getBabelConfigFile } from './get-babel-config-file'
7373
import { defaultOverrides } from '../server/require-hook'
74+
import { needsExperimentalReact } from '../lib/needs-experimental-react'
7475

7576
type ExcludesFalse = <T>(x: T | false) => x is T
7677
type ClientEntries = {
@@ -191,7 +192,6 @@ export function getDefineEnv({
191192
isNodeServer,
192193
middlewareMatchers,
193194
previewModeId,
194-
useServerActions,
195195
}: {
196196
allowedRevalidateHeaderKeys: string[] | undefined
197197
clientRouterFilters: Parameters<
@@ -208,7 +208,6 @@ export function getDefineEnv({
208208
isNodeServer: boolean
209209
middlewareMatchers: MiddlewareMatcher[] | undefined
210210
previewModeId: string | undefined
211-
useServerActions: boolean
212211
}) {
213212
return {
214213
// internal field to identify the plugin config
@@ -373,8 +372,9 @@ export function getDefineEnv({
373372
'process.env.TURBOPACK': JSON.stringify(false),
374373
...(isNodeServer
375374
? {
376-
'process.env.__NEXT_EXPERIMENTAL_REACT':
377-
JSON.stringify(useServerActions),
375+
'process.env.__NEXT_EXPERIMENTAL_REACT': JSON.stringify(
376+
needsExperimentalReact(config)
377+
),
378378
}
379379
: undefined),
380380
}
@@ -808,7 +808,9 @@ export default async function getBaseWebpackConfig(
808808
const disableOptimizedLoading = true
809809
const enableTypedRoutes = !!config.experimental.typedRoutes && hasAppDir
810810
const useServerActions = !!config.experimental.serverActions && hasAppDir
811-
const bundledReactChannel = useServerActions ? '-experimental' : ''
811+
const bundledReactChannel = needsExperimentalReact(config)
812+
? '-experimental'
813+
: ''
812814

813815
if (isClient) {
814816
if (
@@ -2543,7 +2545,6 @@ export default async function getBaseWebpackConfig(
25432545
isNodeServer,
25442546
middlewareMatchers,
25452547
previewModeId,
2546-
useServerActions,
25472548
})
25482549
),
25492550
isClient &&

packages/next/src/cli/next-dev.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getReservedPortExplanation,
2727
isPortIsReserved,
2828
} from '../lib/helpers/get-reserved-port'
29+
import { needsExperimentalReact } from '../lib/needs-experimental-react'
2930

3031
let dir: string
3132
let child: undefined | ReturnType<typeof fork>
@@ -198,8 +199,7 @@ const nextDev: CliCommand = async (args) => {
198199
},
199200
})
200201

201-
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = config.experimental
202-
.serverActions
202+
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = needsExperimentalReact(config)
203203
? 'experimental'
204204
: 'next'
205205

packages/next/src/export/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
5050
import { isAppRouteRoute } from '../lib/is-app-route-route'
5151
import { isAppPageRoute } from '../lib/is-app-page-route'
5252
import isError from '../lib/is-error'
53+
import { needsExperimentalReact } from '../lib/needs-experimental-react'
5354

5455
const exists = promisify(existsOrig)
5556

@@ -730,7 +731,7 @@ export default async function exportApp(
730731
fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
731732
incrementalCacheHandlerPath:
732733
nextConfig.experimental.incrementalCacheHandlerPath,
733-
serverActions: nextConfig.experimental.serverActions,
734+
enableExperimentalReact: needsExperimentalReact(nextConfig),
734735
})
735736

736737
for (const validation of result.ampValidations || []) {

packages/next/src/export/worker.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ interface ExportPageInput {
9797
incrementalCacheHandlerPath?: string
9898
fetchCacheKeyPrefix?: string
9999
nextConfigOutput?: NextConfigComplete['output']
100-
serverActions?: boolean
100+
enableExperimentalReact?: boolean
101101
}
102102

103103
interface ExportPageResults {
@@ -154,7 +154,7 @@ export default async function exportPage({
154154
fetchCache,
155155
fetchCacheKeyPrefix,
156156
incrementalCacheHandlerPath,
157-
serverActions,
157+
enableExperimentalReact,
158158
}: ExportPageInput): Promise<ExportPageResults> {
159159
setHttpClientAndAgentOptions({
160160
httpAgentOptions,
@@ -171,7 +171,7 @@ export default async function exportPage({
171171
if (renderOpts.deploymentId) {
172172
process.env.NEXT_DEPLOYMENT_ID = renderOpts.deploymentId
173173
}
174-
if (serverActions) {
174+
if (enableExperimentalReact) {
175175
process.env.__NEXT_EXPERIMENTAL_REACT = 'true'
176176
}
177177
const { query: originalQuery = {} } = pathMap
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NextConfig } from '../server/config-shared'
2+
3+
export function needsExperimentalReact(config: NextConfig) {
4+
return Boolean(config.experimental?.serverActions || config.experimental?.ppr)
5+
}

packages/next/src/server/config-schema.ts

+3
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ const configSchema = {
384384
outputFileTracingIncludes: {
385385
type: 'object',
386386
},
387+
ppr: {
388+
type: 'boolean',
389+
},
387390
proxyTimeout: {
388391
minimum: 0,
389392
type: 'number',

packages/next/src/server/config-shared.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,16 @@ export interface ExperimentalConfig {
302302
instrumentationHook?: boolean
303303

304304
/**
305-
* Enable `react@experimental` channel for the `app` directory.
305+
* Enables server actions. Using this feature will enable the `react@experimental` for the `app` directory.
306+
* @see https://nextjs.org/docs/app/api-reference/functions/server-actions
306307
*/
307308
serverActions?: boolean
308309

310+
/**
311+
* Using this feature will enable the `react@experimental` for the `app` directory.
312+
*/
313+
ppr?: boolean
314+
309315
/**
310316
* Allows adjusting body parser size limit for server actions.
311317
*/

packages/next/src/server/lib/router-utils/setup-dev.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1733,7 +1733,6 @@ async function startWatcher(opts: SetupOpts) {
17331733
isNodeServer,
17341734
middlewareMatchers: undefined,
17351735
previewModeId: undefined,
1736-
useServerActions: !!nextConfig.experimental.serverActions,
17371736
})
17381737

17391738
Object.keys(plugin.definitions).forEach((key) => {

scripts/minimal-server.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ console.time('next-wall-time')
44

55
process.env.NODE_ENV = 'production'
66

7-
// Change this to 'experimental' for server actions
7+
// Change this to 'experimental' to opt into the React experimental channel (needed for server actions, ppr)
88
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = 'next'
99

1010
if (process.env.LOG_REQUIRE) {

test/e2e/app-dir/rsc-basic/rsc-basic.test.ts

+49
Original file line numberDiff line numberDiff line change
@@ -592,5 +592,54 @@ createNextDescribe(
592592
await Promise.all(promises)
593593
})
594594
}
595+
596+
describe('react@experimental', () => {
597+
it.each([{ flag: 'ppr' }, { flag: 'serverActions' }])(
598+
'should opt into the react@experimental when enabling $flag',
599+
async ({ flag }) => {
600+
await next.stop()
601+
await next.patchFile(
602+
'next.config.js',
603+
`
604+
module.exports = {
605+
experimental: {
606+
${flag}: true
607+
}
608+
}
609+
`
610+
)
611+
612+
await next.start()
613+
const resPages$ = await next.render$('/app-react')
614+
const ssrPagesReactVersions = [
615+
await resPages$('#react').text(),
616+
await resPages$('#react-dom').text(),
617+
await resPages$('#react-dom-server').text(),
618+
await resPages$('#client-react').text(),
619+
await resPages$('#client-react-dom').text(),
620+
await resPages$('#client-react-dom-server').text(),
621+
]
622+
623+
ssrPagesReactVersions.forEach((version) => {
624+
expect(version).toMatch('-experimental-')
625+
})
626+
627+
const browser = await next.browser('/app-react')
628+
const browserAppReactVersions = await browser.eval(`
629+
[
630+
document.querySelector('#react').innerText,
631+
document.querySelector('#react-dom').innerText,
632+
document.querySelector('#react-dom-server').innerText,
633+
document.querySelector('#client-react').innerText,
634+
document.querySelector('#client-react-dom').innerText,
635+
document.querySelector('#client-react-dom-server').innerText,
636+
]
637+
`)
638+
browserAppReactVersions.forEach((version) =>
639+
expect(version).toMatch('-experimental-')
640+
)
641+
}
642+
)
643+
})
595644
}
596645
)

0 commit comments

Comments
 (0)