Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PPR] Enable incremental adoption #63847

Merged
18 changes: 18 additions & 0 deletions packages/next-swc/crates/next-core/src/app_segment_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub struct NextSegmentConfig {
pub fetch_cache: Option<NextSegmentFetchCache>,
pub runtime: Option<NextRuntime>,
pub preferred_region: Option<Vec<String>>,
pub experimental_ppr: Option<bool>,
}

#[turbo_tasks::value_impl]
Expand All @@ -93,13 +94,15 @@ impl NextSegmentConfig {
fetch_cache,
runtime,
preferred_region,
experimental_ppr,
} = self;
*dynamic = dynamic.or(parent.dynamic);
*dynamic_params = dynamic_params.or(parent.dynamic_params);
*revalidate = revalidate.or(parent.revalidate);
*fetch_cache = fetch_cache.or(parent.fetch_cache);
*runtime = runtime.or(parent.runtime);
*preferred_region = preferred_region.take().or(parent.preferred_region.clone());
*experimental_ppr = experimental_ppr.or(parent.experimental_ppr);
}

/// Applies a config from a paralllel route to this config, returning an
Expand Down Expand Up @@ -133,6 +136,7 @@ impl NextSegmentConfig {
fetch_cache,
runtime,
preferred_region,
experimental_ppr,
} = self;
merge_parallel(dynamic, &parallel_config.dynamic, "dynamic")?;
merge_parallel(
Expand All @@ -148,6 +152,11 @@ impl NextSegmentConfig {
&parallel_config.preferred_region,
"referredRegion",
)?;
merge_parallel(
experimental_ppr,
&parallel_config.experimental_ppr,
"experimental_ppr",
)?;
Ok(())
}
}
Expand Down Expand Up @@ -422,6 +431,15 @@ fn parse_config_value(

config.preferred_region = Some(preferred_region);
}
"experimental_ppr" => {
let value = eval_context.eval(init);
let Some(val) = value.as_bool() else {
invalid_config("`experimental_ppr` needs to be a static boolean", &value);
return;
};

config.experimental_ppr = Some(val);
}
_ => {}
}
}
Expand Down
78 changes: 76 additions & 2 deletions packages/next-swc/crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ pub struct ExperimentalConfig {
output_file_tracing_root: Option<String>,
/// Using this feature will enable the `react@experimental` for the `app`
/// directory.
ppr: Option<bool>,
ppr: Option<ExperimentalPartialPrerendering>,
taint: Option<bool>,
proxy_timeout: Option<f64>,
/// enables the minification of server code.
Expand All @@ -542,6 +542,49 @@ pub struct ExperimentalConfig {
worker_threads: Option<bool>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)]
#[serde(rename_all = "lowercase")]
pub enum ExperimentalPartialPrerenderingIncrementalValue {
Incremental,
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs)]
#[serde(untagged)]
pub enum ExperimentalPartialPrerendering {
Incremental(ExperimentalPartialPrerenderingIncrementalValue),
Boolean(bool),
}

#[test]
fn test_parse_experimental_partial_prerendering() {
let json = serde_json::json!({
"ppr": "incremental"
});
let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
assert_eq!(
config.ppr,
Some(ExperimentalPartialPrerendering::Incremental(
ExperimentalPartialPrerenderingIncrementalValue::Incremental
))
);

let json = serde_json::json!({
"ppr": true
});
let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
assert_eq!(
config.ppr,
Some(ExperimentalPartialPrerendering::Boolean(true))
);

// Expect if we provide a random string, it will fail.
let json = serde_json::json!({
"ppr": "random"
});
let config = serde_json::from_value::<ExperimentalConfig>(json);
assert!(config.is_err());
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)]
#[serde(rename_all = "camelCase")]
pub struct SubResourceIntegrity {
Expand Down Expand Up @@ -572,6 +615,25 @@ pub enum EsmExternals {
Bool(bool),
}

// Test for esm externals deserialization.
#[test]
fn test_esm_externals_deserialization() {
let json = serde_json::json!({
"esmExternals": true
});
let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
assert_eq!(config.esm_externals, Some(EsmExternals::Bool(true)));

let json = serde_json::json!({
"esmExternals": "loose"
});
let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
assert_eq!(
config.esm_externals,
Some(EsmExternals::Loose(EsmExternalsValue::Loose))
);
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, TraceRawVcs)]
#[serde(rename_all = "camelCase")]
pub struct ServerActions {
Expand Down Expand Up @@ -934,7 +996,19 @@ impl NextConfig {

#[turbo_tasks::function]
pub async fn enable_ppr(self: Vc<Self>) -> Result<Vc<bool>> {
Ok(Vc::cell(self.await?.experimental.ppr.unwrap_or(false)))
Ok(Vc::cell(
self.await?
.experimental
.ppr
.as_ref()
.map(|ppr| match ppr {
ExperimentalPartialPrerendering::Incremental(
ExperimentalPartialPrerenderingIncrementalValue::Incremental,
) => true,
ExperimentalPartialPrerendering::Boolean(b) => *b,
})
.unwrap_or(false),
))
}

#[turbo_tasks::function]
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) {

export function getMiddlewareMatchers(
matcherOrMatchers: unknown,
nextConfig: NextConfig
nextConfig: Pick<NextConfig, 'basePath' | 'i18n'>
): MiddlewareMatcher[] {
let matchers: unknown[] = []
if (Array.isArray(matcherOrMatchers)) {
Expand Down
66 changes: 24 additions & 42 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ import { traceMemoryUsage } from '../lib/memory/trace'
import { generateEncryptionKeyBase64 } from '../server/app-render/encryption-utils'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import uploadTrace from '../trace/upload-trace'
import {
checkIsAppPPREnabled,
checkIsRoutePPREnabled,
} from '../server/lib/experimental/ppr'

interface ExperimentalBypassForInfo {
experimentalBypassFor?: RouteHas[]
Expand Down Expand Up @@ -1740,7 +1744,6 @@ export default async function build(
const additionalSsgPaths = new Map<string, Array<string>>()
const additionalSsgPathsEncoded = new Map<string, Array<string>>()
const appStaticPaths = new Map<string, Array<string>>()
const appPrefetchPaths = new Map<string, string>()
const appStaticPathsEncoded = new Map<string, Array<string>>()
const appNormalizedPaths = new Map<string, string>()
const appDynamicParamPaths = new Set<string>()
Expand Down Expand Up @@ -1808,7 +1811,7 @@ export default async function build(
minimalMode: ciEnvironment.hasNextSupport,
allowedRevalidateHeaderKeys:
config.experimental.allowedRevalidateHeaderKeys,
experimental: { ppr: config.experimental.ppr === true },
isAppPPREnabled: checkIsAppPPREnabled(config.experimental.ppr),
})

incrementalCacheIpcPort = cacheInitialization.ipcPort
Expand Down Expand Up @@ -1886,7 +1889,7 @@ export default async function build(
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
nextConfigOutput: config.output,
ppr: config.experimental.ppr === true,
pprConfig: config.experimental.ppr,
})
)

Expand Down Expand Up @@ -1984,7 +1987,7 @@ export default async function build(
computedManifestData
)

let isPPR = false
let isRoutePPREnabled = false
let isSSG = false
let isStatic = false
let isServerComponent = false
Expand Down Expand Up @@ -2099,7 +2102,7 @@ export default async function build(
: config.experimental.isrFlushToDisk,
maxMemoryCacheSize: config.cacheMaxMemorySize,
nextConfigOutput: config.output,
ppr: config.experimental.ppr === true,
pprConfig: config.experimental.ppr,
})
}
)
Expand All @@ -2118,8 +2121,8 @@ export default async function build(
// If this route can be partially pre-rendered, then
// mark it as such and mark that it can be
// generated server-side.
if (workerResult.isPPR) {
isPPR = workerResult.isPPR
if (workerResult.isRoutePPREnabled) {
isRoutePPREnabled = workerResult.isRoutePPREnabled
isSSG = true
isStatic = true

Expand Down Expand Up @@ -2174,15 +2177,14 @@ export default async function build(
])
isStatic = true
} else if (
isDynamic &&
wyattjoh marked this conversation as resolved.
Show resolved Hide resolved
!hasGenerateStaticParams &&
(appConfig.dynamic === 'error' ||
appConfig.dynamic === 'force-static')
) {
appStaticPaths.set(originalAppPath, [])
appStaticPathsEncoded.set(originalAppPath, [])
isStatic = true
isPPR = false
isRoutePPREnabled = false
}
}
}
Expand All @@ -2193,18 +2195,6 @@ export default async function build(
appDynamicParamPaths.add(originalAppPath)
}
appDefaultConfigs.set(originalAppPath, appConfig)

// Only generate the app prefetch rsc if the route is
// an app page.
if (
!isStatic &&
!isAppRouteRoute(originalAppPath) &&
!isDynamicRoute(originalAppPath) &&
!isPPR &&
!isInterceptionRoute
) {
appPrefetchPaths.set(originalAppPath, page)
}
}
} else {
if (isEdgeRuntime(pageRuntime)) {
Expand Down Expand Up @@ -2328,7 +2318,7 @@ export default async function build(
totalSize,
isStatic,
isSSG,
isPPR,
isRoutePPREnabled,
isHybridAmp,
ssgPageRoutes,
initialRevalidateSeconds: false,
Expand Down Expand Up @@ -2623,34 +2613,24 @@ export default async function build(
// revalidate periods and dynamicParams settings
appStaticPaths.forEach((routes, originalAppPath) => {
const encodedRoutes = appStaticPathsEncoded.get(originalAppPath)
const appConfig = appDefaultConfigs.get(originalAppPath) || {}
const appConfig = appDefaultConfigs.get(originalAppPath)

routes.forEach((route, routeIdx) => {
defaultMap[route] = {
page: originalAppPath,
query: { __nextSsgPath: encodedRoutes?.[routeIdx] },
_isDynamicError: appConfig.dynamic === 'error',
_isDynamicError: appConfig?.dynamic === 'error',
_isAppDir: true,
_isRoutePPREnabled: appConfig
? checkIsRoutePPREnabled(
config.experimental.ppr,
appConfig
)
: undefined,
}
})
})

// Ensure we don't generate explicit app prefetches while in PPR.
if (config.experimental.ppr && appPrefetchPaths.size > 0) {
wyattjoh marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(
"Invariant: explicit app prefetches shouldn't generated with PPR"
)
}

for (const [originalAppPath, page] of appPrefetchPaths) {
defaultMap[page] = {
page: originalAppPath,
query: {},
_isAppDir: true,
_isAppPrefetch: true,
}
}

if (i18n) {
for (const page of [
...staticPages,
Expand Down Expand Up @@ -2682,6 +2662,7 @@ export default async function build(
}
}
}

return defaultMap
},
}
Expand Down Expand Up @@ -2752,8 +2733,9 @@ export default async function build(

// When this is an app page and PPR is enabled, the route supports
// partial pre-rendering.
const experimentalPPR =
!isRouteHandler && config.experimental.ppr === true
const experimentalPPR: true | undefined =
wyattjoh marked this conversation as resolved.
Show resolved Hide resolved
!isRouteHandler &&
checkIsRoutePPREnabled(config.experimental.ppr, appConfig)
? true
: undefined

Expand Down
Loading
Loading