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
This commit is contained in:
Zack Tanner 2023-09-19 03:45:25 -07:00 committed by GitHub
parent f630cb8e56
commit 33c561b21d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 99 additions and 36 deletions

View file

@ -145,6 +145,7 @@ import { createClientRouterFilter } from '../lib/create-client-router-filter'
import { createValidFileMatcher } from '../server/lib/find-page-file'
import { startTypeChecking } from './type-check'
import { generateInterceptionRoutesRewrites } from '../lib/generate-interception-routes-rewrites'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
import { buildDataRoute } from '../server/lib/router-utils/build-data-route'
import { defaultOverrides } from '../server/require-hook'
@ -1255,11 +1256,9 @@ export default async function build(
__NEXT_INCREMENTAL_CACHE_IPC_PORT: incrementalCacheIpcPort + '',
__NEXT_INCREMENTAL_CACHE_IPC_KEY:
incrementalCacheIpcValidationKey,
__NEXT_PRIVATE_PREBUNDLED_REACT: hasAppDir
? config.experimental.serverActions
? 'experimental'
: 'next'
: '',
__NEXT_PRIVATE_PREBUNDLED_REACT: needsExperimentalReact(config)
? 'experimental'
: 'next',
},
},
enableWorkerThreads: config.experimental.workerThreads,
@ -2438,8 +2437,7 @@ export default async function build(
outputFileTracingRoot,
requiredServerFiles.config,
middlewareManifest,
hasInstrumentationHook,
hasAppDir
hasInstrumentationHook
)
})
}

View file

@ -1,4 +1,4 @@
import type { NextConfigComplete } from '../server/config-shared'
import type { NextConfig, NextConfigComplete } from '../server/config-shared'
import type { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import type { AssetBinding } from './webpack/loaders/get-module-build-info'
import type { GetStaticPaths, PageConfig, ServerRuntime } from 'next/types'
@ -67,7 +67,8 @@ import { nodeFs } from '../server/lib/node-fs-methods'
import * as ciEnvironment from '../telemetry/ci-info'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path'
// import { AppRouteRouteModule } from '../server/future/route-modules/app-route/module'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
const { AppRouteRouteModule } =
require('../server/future/route-modules/app-route/module.compiled') as typeof import('../server/future/route-modules/app-route/module')
@ -1826,13 +1827,17 @@ export async function copyTracedFiles(
pageKeys: readonly string[],
appPageKeys: readonly string[] | undefined,
tracingRoot: string,
serverConfig: { [key: string]: any },
serverConfig: NextConfig,
middlewareManifest: MiddlewareManifest,
hasInstrumentationHook: boolean,
hasAppDir: boolean
hasInstrumentationHook: boolean
) {
const outputPath = path.join(distDir, 'standalone')
let moduleType = false
const nextConfig = {
...serverConfig,
distDir: `./${path.relative(dir, distDir)}`,
}
const hasExperimentalReact = needsExperimentalReact(nextConfig)
try {
const packageJsonPath = path.join(distDir, '../package.json')
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
@ -1956,6 +1961,7 @@ export async function copyTracedFiles(
'server.js'
)
await fs.mkdir(path.dirname(serverOutputPath), { recursive: true })
await fs.writeFile(
serverOutputPath,
`${
@ -1985,17 +1991,12 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || '0.0.0.0'
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = ${JSON.stringify({
...serverConfig,
distDir: `./${path.relative(dir, distDir)}`,
})}
const nextConfig = ${JSON.stringify(nextConfig)}
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = ${hasAppDir}
? nextConfig.experimental && nextConfig.experimental.serverActions
? 'experimental'
: 'next'
: '';
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = ${hasExperimentalReact}
? 'experimental'
: 'next'
require('next')
const { startServer } = require('next/dist/server/lib/start-server')

View file

@ -71,6 +71,7 @@ import { getSupportedBrowsers } from './utils'
import { MemoryWithGcCachePlugin } from './webpack/plugins/memory-with-gc-cache-plugin'
import { getBabelConfigFile } from './get-babel-config-file'
import { defaultOverrides } from '../server/require-hook'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
type ExcludesFalse = <T>(x: T | false) => x is T
type ClientEntries = {
@ -191,7 +192,6 @@ export function getDefineEnv({
isNodeServer,
middlewareMatchers,
previewModeId,
useServerActions,
}: {
allowedRevalidateHeaderKeys: string[] | undefined
clientRouterFilters: Parameters<
@ -208,7 +208,6 @@ export function getDefineEnv({
isNodeServer: boolean
middlewareMatchers: MiddlewareMatcher[] | undefined
previewModeId: string | undefined
useServerActions: boolean
}) {
return {
// internal field to identify the plugin config
@ -373,8 +372,9 @@ export function getDefineEnv({
'process.env.TURBOPACK': JSON.stringify(false),
...(isNodeServer
? {
'process.env.__NEXT_EXPERIMENTAL_REACT':
JSON.stringify(useServerActions),
'process.env.__NEXT_EXPERIMENTAL_REACT': JSON.stringify(
needsExperimentalReact(config)
),
}
: undefined),
}
@ -808,7 +808,9 @@ export default async function getBaseWebpackConfig(
const disableOptimizedLoading = true
const enableTypedRoutes = !!config.experimental.typedRoutes && hasAppDir
const useServerActions = !!config.experimental.serverActions && hasAppDir
const bundledReactChannel = useServerActions ? '-experimental' : ''
const bundledReactChannel = needsExperimentalReact(config)
? '-experimental'
: ''
if (isClient) {
if (
@ -2543,7 +2545,6 @@ export default async function getBaseWebpackConfig(
isNodeServer,
middlewareMatchers,
previewModeId,
useServerActions,
})
),
isClient &&

View file

@ -26,6 +26,7 @@ import {
getReservedPortExplanation,
isPortIsReserved,
} from '../lib/helpers/get-reserved-port'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
let dir: string
let child: undefined | ReturnType<typeof fork>
@ -198,8 +199,7 @@ const nextDev: CliCommand = async (args) => {
},
})
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = config.experimental
.serverActions
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = needsExperimentalReact(config)
? 'experimental'
: 'next'

View file

@ -50,6 +50,7 @@ import { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import { isAppRouteRoute } from '../lib/is-app-route-route'
import { isAppPageRoute } from '../lib/is-app-page-route'
import isError from '../lib/is-error'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
const exists = promisify(existsOrig)
@ -730,7 +731,7 @@ export default async function exportApp(
fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
incrementalCacheHandlerPath:
nextConfig.experimental.incrementalCacheHandlerPath,
serverActions: nextConfig.experimental.serverActions,
enableExperimentalReact: needsExperimentalReact(nextConfig),
})
for (const validation of result.ampValidations || []) {

View file

@ -97,7 +97,7 @@ interface ExportPageInput {
incrementalCacheHandlerPath?: string
fetchCacheKeyPrefix?: string
nextConfigOutput?: NextConfigComplete['output']
serverActions?: boolean
enableExperimentalReact?: boolean
}
interface ExportPageResults {
@ -154,7 +154,7 @@ export default async function exportPage({
fetchCache,
fetchCacheKeyPrefix,
incrementalCacheHandlerPath,
serverActions,
enableExperimentalReact,
}: ExportPageInput): Promise<ExportPageResults> {
setHttpClientAndAgentOptions({
httpAgentOptions,
@ -171,7 +171,7 @@ export default async function exportPage({
if (renderOpts.deploymentId) {
process.env.NEXT_DEPLOYMENT_ID = renderOpts.deploymentId
}
if (serverActions) {
if (enableExperimentalReact) {
process.env.__NEXT_EXPERIMENTAL_REACT = 'true'
}
const { query: originalQuery = {} } = pathMap

View file

@ -0,0 +1,5 @@
import type { NextConfig } from '../server/config-shared'
export function needsExperimentalReact(config: NextConfig) {
return Boolean(config.experimental?.serverActions || config.experimental?.ppr)
}

View file

@ -384,6 +384,9 @@ const configSchema = {
outputFileTracingIncludes: {
type: 'object',
},
ppr: {
type: 'boolean',
},
proxyTimeout: {
minimum: 0,
type: 'number',

View file

@ -302,10 +302,16 @@ export interface ExperimentalConfig {
instrumentationHook?: boolean
/**
* Enable `react@experimental` channel for the `app` directory.
* Enables server actions. Using this feature will enable the `react@experimental` for the `app` directory.
* @see https://nextjs.org/docs/app/api-reference/functions/server-actions
*/
serverActions?: boolean
/**
* Using this feature will enable the `react@experimental` for the `app` directory.
*/
ppr?: boolean
/**
* Allows adjusting body parser size limit for server actions.
*/

View file

@ -1733,7 +1733,6 @@ async function startWatcher(opts: SetupOpts) {
isNodeServer,
middlewareMatchers: undefined,
previewModeId: undefined,
useServerActions: !!nextConfig.experimental.serverActions,
})
Object.keys(plugin.definitions).forEach((key) => {

View file

@ -4,7 +4,7 @@ console.time('next-wall-time')
process.env.NODE_ENV = 'production'
// Change this to 'experimental' for server actions
// Change this to 'experimental' to opt into the React experimental channel (needed for server actions, ppr)
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = 'next'
if (process.env.LOG_REQUIRE) {

View file

@ -592,5 +592,54 @@ createNextDescribe(
await Promise.all(promises)
})
}
describe('react@experimental', () => {
it.each([{ flag: 'ppr' }, { flag: 'serverActions' }])(
'should opt into the react@experimental when enabling $flag',
async ({ flag }) => {
await next.stop()
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
${flag}: true
}
}
`
)
await next.start()
const resPages$ = await next.render$('/app-react')
const ssrPagesReactVersions = [
await resPages$('#react').text(),
await resPages$('#react-dom').text(),
await resPages$('#react-dom-server').text(),
await resPages$('#client-react').text(),
await resPages$('#client-react-dom').text(),
await resPages$('#client-react-dom-server').text(),
]
ssrPagesReactVersions.forEach((version) => {
expect(version).toMatch('-experimental-')
})
const browser = await next.browser('/app-react')
const browserAppReactVersions = await browser.eval(`
[
document.querySelector('#react').innerText,
document.querySelector('#react-dom').innerText,
document.querySelector('#react-dom-server').innerText,
document.querySelector('#client-react').innerText,
document.querySelector('#client-react-dom').innerText,
document.querySelector('#client-react-dom-server').innerText,
]
`)
browserAppReactVersions.forEach((version) =>
expect(version).toMatch('-experimental-')
)
}
)
})
}
)