Skip to content

Commit c422755

Browse files
authored
feat: added partial shell generation using root params (#73816)
This enables the generation of partial shells when using with partial prerendering, aided by the new `rootParams()` API. Essentially, when your application only returns partial routes (where not all the route parameters are known), Next.js will now build route shells for these pages. We call these shells, fallback shells. They represent the partial state of the page that once served to the user, will provide a more complete loading experience as fast as possible. We'll also take every permutation of the provided root params and generate a shell just for them to ensure that any future calls to those routes will be able to use the more specific fallback shell that's generated rather than having to rely on a blank shell by default.
1 parent 6ed03f4 commit c422755

File tree

15 files changed

+549
-210
lines changed

15 files changed

+549
-210
lines changed

packages/next/errors.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,5 +613,10 @@
613613
"612": "ServerPrerenderStreamResult cannot be consumed as a stream because it is not yet complete. status: %s",
614614
"613": "Expected the input to be `string | string[]`",
615615
"614": "Route %s used \"unstable_rootParams\" inside \"use cache\". This is not currently supported.",
616-
"615": "Missing workStore in unstable_rootParams"
616+
"615": "Missing workStore in unstable_rootParams",
617+
"616": "App config not found",
618+
"617": "A required parameter (%s) was not provided as a string received %s in generateStaticParams for %s",
619+
"618": "A required parameter (%s) was not provided as an array received %s in generateStaticParams for %s",
620+
"619": "Page not found",
621+
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s"
617622
}

packages/next/src/build/index.ts

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import {
134134
collectMeta,
135135
} from './utils'
136136
import type { PageInfo, PageInfos } from './utils'
137+
import type { PrerenderedRoute } from './static-paths/types'
137138
import type { AppSegmentConfig } from './segment-config/app/app-segment-config'
138139
import { writeBuildId } from './write-build-id'
139140
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
@@ -212,7 +213,7 @@ import {
212213
formatNodeOptions,
213214
getParsedNodeOptionsWithoutInspect,
214215
} from '../server/lib/utils'
215-
import type { PrerenderedRoute } from './static-paths/types'
216+
import { InvariantError } from '../shared/lib/invariant-error'
216217

217218
type Fallback = null | boolean | string
218219

@@ -2131,11 +2132,16 @@ export default async function build(
21312132
} else {
21322133
const isDynamic = isDynamicRoute(page)
21332134

2135+
if (
2136+
typeof workerResult.isRoutePPREnabled === 'boolean'
2137+
) {
2138+
isRoutePPREnabled = workerResult.isRoutePPREnabled
2139+
}
2140+
21342141
// If this route can be partially pre-rendered, then
21352142
// mark it as such and mark that it can be
21362143
// generated server-side.
21372144
if (workerResult.isRoutePPREnabled) {
2138-
isRoutePPREnabled = workerResult.isRoutePPREnabled
21392145
isSSG = true
21402146
isStatic = true
21412147

@@ -2160,7 +2166,7 @@ export default async function build(
21602166
workerResult.prerenderedRoutes
21612167
)
21622168
ssgPageRoutes = workerResult.prerenderedRoutes.map(
2163-
(route) => route.path
2169+
(route) => route.pathname
21642170
)
21652171
isSSG = true
21662172
}
@@ -2188,9 +2194,12 @@ export default async function build(
21882194
if (!isDynamic) {
21892195
staticPaths.set(originalAppPath, [
21902196
{
2191-
path: page,
2192-
encoded: page,
2197+
pathname: page,
2198+
encodedPathname: page,
21932199
fallbackRouteParams: undefined,
2200+
fallbackMode:
2201+
workerResult.prerenderFallbackMode,
2202+
fallbackRootParams: undefined,
21942203
},
21952204
])
21962205
isStatic = true
@@ -2256,7 +2265,7 @@ export default async function build(
22562265
workerResult.prerenderedRoutes
22572266
)
22582267
ssgPageRoutes = workerResult.prerenderedRoutes.map(
2259-
(route) => route.path
2268+
(route) => route.pathname
22602269
)
22612270
}
22622271

@@ -2692,7 +2701,7 @@ export default async function build(
26922701
new Map(
26932702
Array.from(additionalPaths.entries()).map(
26942703
([page, routes]): [string, string[]] => {
2695-
return [page, routes.map((route) => route.path)]
2704+
return [page, routes.map((route) => route.pathname)]
26962705
}
26972706
)
26982707
)
@@ -2743,9 +2752,9 @@ export default async function build(
27432752
// post slugs.
27442753
additionalPaths.forEach((routes, page) => {
27452754
routes.forEach((route) => {
2746-
defaultMap[route.path] = {
2755+
defaultMap[route.pathname] = {
27472756
page,
2748-
query: { __nextSsgPath: route.encoded },
2757+
query: { __nextSsgPath: route.encodedPathname },
27492758
}
27502759
})
27512760
})
@@ -2773,9 +2782,9 @@ export default async function build(
27732782
: undefined
27742783

27752784
routes.forEach((route) => {
2776-
defaultMap[route.path] = {
2785+
defaultMap[route.pathname] = {
27772786
page: originalAppPath,
2778-
query: { __nextSsgPath: route.encoded },
2787+
query: { __nextSsgPath: route.encodedPathname },
27792788
_fallbackRouteParams: route.fallbackRouteParams,
27802789
_isDynamicError: isDynamicError,
27812790
_isAppDir: true,
@@ -2885,8 +2894,11 @@ export default async function build(
28852894
}
28862895

28872896
staticPaths.forEach((prerenderedRoutes, originalAppPath) => {
2888-
const page = appNormalizedPaths.get(originalAppPath) || ''
2889-
const appConfig = appDefaultConfigs.get(originalAppPath) || {}
2897+
const page = appNormalizedPaths.get(originalAppPath)
2898+
if (!page) throw new InvariantError('Page not found')
2899+
2900+
const appConfig = appDefaultConfigs.get(originalAppPath)
2901+
if (!appConfig) throw new InvariantError('App config not found')
28902902

28912903
let hasRevalidateZero =
28922904
appConfig.revalidate === 0 ||
@@ -2928,8 +2940,8 @@ export default async function build(
29282940
// route), any routes that were generated with unknown route params
29292941
// should be collected and included in the dynamic routes part
29302942
// of the manifest instead.
2931-
const routes: string[] = []
2932-
const dynamicRoutes: string[] = []
2943+
const routes: PrerenderedRoute[] = []
2944+
const dynamicRoutes: PrerenderedRoute[] = []
29332945

29342946
// Sort the outputted routes to ensure consistent output. Any route
29352947
// though that has unknown route params will be pulled and sorted
@@ -2951,11 +2963,11 @@ export default async function build(
29512963

29522964
unknownPrerenderRoutes = getSortedRouteObjects(
29532965
unknownPrerenderRoutes,
2954-
(prerenderedRoute) => prerenderedRoute.path
2966+
(prerenderedRoute) => prerenderedRoute.pathname
29552967
)
29562968
knownPrerenderRoutes = getSortedRouteObjects(
29572969
knownPrerenderRoutes,
2958-
(prerenderedRoute) => prerenderedRoute.path
2970+
(prerenderedRoute) => prerenderedRoute.pathname
29592971
)
29602972

29612973
prerenderedRoutes = [
@@ -2966,7 +2978,7 @@ export default async function build(
29662978
for (const prerenderedRoute of prerenderedRoutes) {
29672979
// TODO: check if still needed?
29682980
// Exclude the /_not-found route.
2969-
if (prerenderedRoute.path === UNDERSCORE_NOT_FOUND_ROUTE) {
2981+
if (prerenderedRoute.pathname === UNDERSCORE_NOT_FOUND_ROUTE) {
29702982
continue
29712983
}
29722984

@@ -2977,28 +2989,28 @@ export default async function build(
29772989
) {
29782990
// If the route has unknown params, then we need to add it to
29792991
// the list of dynamic routes.
2980-
dynamicRoutes.push(prerenderedRoute.path)
2992+
dynamicRoutes.push(prerenderedRoute)
29812993
} else {
29822994
// If the route doesn't have unknown params, then we need to
29832995
// add it to the list of routes.
2984-
routes.push(prerenderedRoute.path)
2996+
routes.push(prerenderedRoute)
29852997
}
29862998
}
29872999

29883000
// Handle all the static routes.
29893001
for (const route of routes) {
2990-
if (isDynamicRoute(page) && route === page) continue
2991-
if (route === UNDERSCORE_NOT_FOUND_ROUTE) continue
3002+
if (isDynamicRoute(page) && route.pathname === page) continue
3003+
if (route.pathname === UNDERSCORE_NOT_FOUND_ROUTE) continue
29923004

29933005
const {
29943006
revalidate = appConfig.revalidate ?? false,
29953007
metadata = {},
29963008
hasEmptyPrelude,
29973009
hasPostponed,
2998-
} = exportResult.byPath.get(route) ?? {}
3010+
} = exportResult.byPath.get(route.pathname) ?? {}
29993011

3000-
pageInfos.set(route, {
3001-
...(pageInfos.get(route) as PageInfo),
3012+
pageInfos.set(route.pathname, {
3013+
...(pageInfos.get(route.pathname) as PageInfo),
30023014
hasPostponed,
30033015
hasEmptyPrelude,
30043016
})
@@ -3011,7 +3023,7 @@ export default async function build(
30113023
})
30123024

30133025
if (revalidate !== 0) {
3014-
const normalizedRoute = normalizePagePath(route)
3026+
const normalizedRoute = normalizePagePath(route.pathname)
30153027

30163028
let dataRoute: string | null
30173029
if (isAppRouteHandler) {
@@ -3033,7 +3045,7 @@ export default async function build(
30333045

30343046
const meta = collectMeta(metadata)
30353047

3036-
prerenderManifest.routes[route] = {
3048+
prerenderManifest.routes[route.pathname] = {
30373049
initialStatus: meta.status,
30383050
initialHeaders: meta.headers,
30393051
renderingMode: isAppPPREnabled
@@ -3053,8 +3065,8 @@ export default async function build(
30533065
hasRevalidateZero = true
30543066
// we might have determined during prerendering that this page
30553067
// used dynamic data
3056-
pageInfos.set(route, {
3057-
...(pageInfos.get(route) as PageInfo),
3068+
pageInfos.set(route.pathname, {
3069+
...(pageInfos.get(route.pathname) as PageInfo),
30583070
isSSG: false,
30593071
isStatic: false,
30603072
})
@@ -3066,14 +3078,22 @@ export default async function build(
30663078
// they are enabled, then it'll already be included in the
30673079
// prerendered routes.
30683080
if (!isRoutePPREnabled) {
3069-
dynamicRoutes.push(page)
3081+
dynamicRoutes.push({
3082+
pathname: page,
3083+
encodedPathname: page,
3084+
fallbackRouteParams: undefined,
3085+
fallbackMode:
3086+
fallbackModes.get(originalAppPath) ??
3087+
FallbackMode.NOT_FOUND,
3088+
fallbackRootParams: undefined,
3089+
})
30703090
}
30713091

30723092
for (const route of dynamicRoutes) {
3073-
const normalizedRoute = normalizePagePath(route)
3093+
const normalizedRoute = normalizePagePath(route.pathname)
30743094

30753095
const { metadata, revalidate } =
3076-
exportResult.byPath.get(route) ?? {}
3096+
exportResult.byPath.get(route.pathname) ?? {}
30773097

30783098
let dataRoute: string | null = null
30793099
if (!isAppRouteHandler) {
@@ -3087,16 +3107,16 @@ export default async function build(
30873107
)
30883108
}
30893109

3090-
pageInfos.set(route, {
3091-
...(pageInfos.get(route) as PageInfo),
3110+
pageInfos.set(route.pathname, {
3111+
...(pageInfos.get(route.pathname) as PageInfo),
30923112
isDynamicAppRoute: true,
30933113
// if PPR is turned on and the route contains a dynamic segment,
30943114
// we assume it'll be partially prerendered
30953115
hasPostponed: isRoutePPREnabled,
30963116
})
30973117

30983118
const fallbackMode =
3099-
fallbackModes.get(originalAppPath) ?? FallbackMode.NOT_FOUND
3119+
route.fallbackMode ?? FallbackMode.NOT_FOUND
31003120

31013121
// When we're configured to serve a prerender, we should use the
31023122
// fallback revalidate from the export result. If it can't be
@@ -3108,7 +3128,7 @@ export default async function build(
31083128

31093129
const fallback: Fallback = fallbackModeToFallbackField(
31103130
fallbackMode,
3111-
route
3131+
route.pathname
31123132
)
31133133

31143134
const meta =
@@ -3118,7 +3138,7 @@ export default async function build(
31183138
? collectMeta(metadata)
31193139
: {}
31203140

3121-
prerenderManifest.dynamicRoutes[route] = {
3141+
prerenderManifest.dynamicRoutes[route.pathname] = {
31223142
experimentalPPR: isRoutePPREnabled,
31233143
renderingMode: isAppPPREnabled
31243144
? isRoutePPREnabled
@@ -3127,7 +3147,7 @@ export default async function build(
31273147
: undefined,
31283148
experimentalBypassFor: bypassFor,
31293149
routeRegex: normalizeRouteRegex(
3130-
getNamedRouteRegex(route, false).re.source
3150+
getNamedRouteRegex(route.pathname, false).re.source
31313151
),
31323152
dataRoute,
31333153
fallback,
@@ -3417,18 +3437,18 @@ export default async function build(
34173437
// We must also copy specific versions of this page as defined by
34183438
// `getStaticPaths` (additionalSsgPaths).
34193439
for (const route of additionalPaths.get(page) ?? []) {
3420-
const pageFile = normalizePagePath(route.path)
3440+
const pageFile = normalizePagePath(route.pathname)
34213441
await moveExportedPage(
34223442
page,
3423-
route.path,
3443+
route.pathname,
34243444
pageFile,
34253445
isSsg,
34263446
'html',
34273447
true
34283448
)
34293449
await moveExportedPage(
34303450
page,
3431-
route.path,
3451+
route.pathname,
34323452
pageFile,
34333453
isSsg,
34343454
'json',
@@ -3456,21 +3476,21 @@ export default async function build(
34563476
}
34573477

34583478
const initialRevalidateSeconds =
3459-
exportResult.byPath.get(route.path)?.revalidate ?? false
3479+
exportResult.byPath.get(route.pathname)?.revalidate ?? false
34603480

34613481
if (typeof initialRevalidateSeconds === 'undefined') {
34623482
throw new Error("Invariant: page wasn't built")
34633483
}
34643484

3465-
prerenderManifest.routes[route.path] = {
3485+
prerenderManifest.routes[route.pathname] = {
34663486
initialRevalidateSeconds,
34673487
experimentalPPR: undefined,
34683488
renderingMode: undefined,
34693489
srcRoute: page,
34703490
dataRoute: path.posix.join(
34713491
'/_next/data',
34723492
buildId,
3473-
`${normalizePagePath(route.path)}.json`
3493+
`${normalizePagePath(route.pathname)}.json`
34743494
),
34753495
// Pages does not have a prefetch data route.
34763496
prefetchDataRoute: undefined,

packages/next/src/build/manifests/formatter/format-manifest.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,5 @@
44
* JSON string, otherwise it will return a minified JSON string.
55
*/
66
export function formatManifest<T extends object>(manifest: T): string {
7-
if (process.env.NODE_ENV === 'development') {
8-
return JSON.stringify(manifest, null, 2)
9-
}
10-
11-
return JSON.stringify(manifest)
7+
return JSON.stringify(manifest, null, 2)
128
}

packages/next/src/build/segment-config/app/app-segments.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ async function collectAppPageSegments(routeModule: AppPageRouteModule) {
9292
// Process current node
9393
const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree)
9494
const isClientComponent = userland && isClientReference(userland)
95-
const isDynamicSegment = /\[.*\]$/.test(name)
96-
const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined
95+
96+
const param = getSegmentParam(name)?.param
9797

9898
const segment: AppSegment = {
9999
name,
100100
param,
101101
filePath,
102102
config: undefined,
103-
isDynamicSegment,
103+
isDynamicSegment: !!param,
104104
generateStaticParams: undefined,
105105
}
106106

@@ -157,14 +157,13 @@ function collectAppRouteSegments(
157157

158158
// Generate all the segments.
159159
const segments: AppSegment[] = parts.map((name) => {
160-
const isDynamicSegment = /^\[.*\]$/.test(name)
161-
const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined
160+
const param = getSegmentParam(name)?.param
162161

163162
return {
164163
name,
165164
param,
166165
filePath: undefined,
167-
isDynamicSegment,
166+
isDynamicSegment: !!param,
168167
config: undefined,
169168
generateStaticParams: undefined,
170169
}

0 commit comments

Comments
 (0)