Skip to content

Commit

Permalink
refactor: fallback mode
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Aug 13, 2024
1 parent 57ccc37 commit a45f508
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 101 deletions.
11 changes: 9 additions & 2 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ import {
import { storeShuttle } from './flying-shuttle/store-shuttle'
import { stitchBuilds } from './flying-shuttle/stitch-builds'
import { inlineStaticEnv } from './flying-shuttle/inline-static-env'
import { FallbackMode } from '../lib/fallback'

interface ExperimentalBypassForInfo {
experimentalBypassFor?: RouteHas[]
Expand Down Expand Up @@ -2219,9 +2220,15 @@ export default async function build(
)
}

if (workerResult.prerenderFallback === 'blocking') {
if (
workerResult.prerenderFallback ===
FallbackMode.BLOCKING_RENDER
) {
ssgBlockingFallbackPages.add(page)
} else if (workerResult.prerenderFallback === true) {
} else if (
workerResult.prerenderFallback ===
FallbackMode.SERVE_PRERENDER
) {
ssgStaticFallbackPages.add(page)
}
} else if (workerResult.hasServerProps) {
Expand Down
150 changes: 91 additions & 59 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ import { formatDynamicImportPath } from '../lib/format-dynamic-import-path'
import { isInterceptionRouteAppPath } from '../server/lib/interception-routes'
import { checkIsRoutePPREnabled } from '../server/lib/experimental/ppr'
import type { Params } from '../client/components/params'
import { FallbackMode } from '../lib/fallback'
import {
fallbackToStaticPathsResult,
parseFallbackAppConfig,
parseFallbackStaticPathsResult,
} from '../lib/fallback'

export type ROUTER_TYPE = 'pages' | 'app'

Expand Down Expand Up @@ -945,7 +951,7 @@ export type PrerenderedRoute = {
}

export type StaticPathsResult = {
fallback: boolean | 'blocking'
fallback: FallbackMode
prerenderedRoutes: PrerenderedRoute[]
}

Expand Down Expand Up @@ -1172,7 +1178,7 @@ export async function buildStaticPaths({
const seen = new Set<string>()

return {
fallback: staticPathsResult.fallback,
fallback: parseFallbackStaticPathsResult(staticPathsResult.fallback),
prerenderedRoutes: prerenderRoutes.filter((route) => {
if (seen.has(route.path)) return false

Expand Down Expand Up @@ -1345,6 +1351,7 @@ export async function buildAppStaticPaths({
requestHeaders,
maxMemoryCacheSize,
fetchCacheKeyPrefix,
nextConfigOutput,
ComponentMod,
}: {
dir: string
Expand All @@ -1357,6 +1364,7 @@ export async function buildAppStaticPaths({
cacheHandler?: string
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
nextConfigOutput: 'standalone' | 'export' | undefined
ComponentMod: AppPageModule
}): Promise<PartialStaticPathsResult> {
ComponentMod.patchFetch()
Expand Down Expand Up @@ -1478,20 +1486,19 @@ export async function buildAppStaticPaths({
return newParams
}

const appConfig = reduceAppConfig(generateParams, nextConfigOutput)

const builtParams = await buildParams()
const fallback = parseFallbackAppConfig(appConfig)

// We should return `fallback: 'blocking'` when either `dynamicParams`
// is not set or it's only set to `true`. When it's instead set to
// `false`, we need to set `fallback: false` to prevent additional pages
// from being rendered.
const fallback: false | 'blocking' =
!generateParams.some(
// TODO: dynamic params should be allowed
// to be granular per segment but we need
// additional information stored/leveraged in
// the prerender-manifest to allow this behavior
(generate) => generate.config?.dynamicParams === false
) && 'blocking'
if (
fallback === FallbackMode.BLOCKING_RENDER &&
nextConfigOutput === 'export'
) {
throw new Error(
'"dynamicParams: true" cannot be used with "output: export". See more info here: https://nextjs.org/docs/app/building-your-application/deploying/static-exports'
)
}

if (!hadAllParamsGenerated) {
return {
Expand All @@ -1502,7 +1509,7 @@ export async function buildAppStaticPaths({

return buildStaticPaths({
staticPathsResult: {
fallback,
fallback: fallbackToStaticPathsResult(fallback),
paths: builtParams.map((params) => ({ params })),
},
page,
Expand All @@ -1522,7 +1529,7 @@ type PageIsStaticResult = {
hasServerProps?: boolean
hasStaticProps?: boolean
prerenderedRoutes: PrerenderedRoute[] | undefined
prerenderFallback: boolean | 'blocking' | undefined
prerenderFallback: FallbackMode | undefined
isNextImageImported?: boolean
traceIncludes?: string[]
traceExcludes?: string[]
Expand All @@ -1545,6 +1552,7 @@ export async function isPageStatic({
originalAppPath,
isrFlushToDisk,
maxMemoryCacheSize,
nextConfigOutput,
cacheHandler,
pprConfig,
}: {
Expand All @@ -1564,7 +1572,7 @@ export async function isPageStatic({
isrFlushToDisk?: boolean
maxMemoryCacheSize?: number
cacheHandler?: string
nextConfigOutput: 'standalone' | 'export'
nextConfigOutput: 'standalone' | 'export' | undefined
pprConfig: ExperimentalPPRConfig | undefined
}): Promise<PageIsStaticResult> {
const isPageStaticSpan = trace('is-page-static-utils', parentId)
Expand All @@ -1579,7 +1587,7 @@ export async function isPageStatic({

let componentsResult: LoadComponentsReturnType
let prerenderedRoutes: PrerenderedRoute[] | undefined
let prerenderFallback: boolean | 'blocking' | undefined
let prerenderFallback: FallbackMode | undefined
let appConfig: AppConfig = {}
let isClientComponent: boolean = false
const pathIsEdgeRuntime = isEdgeRuntime(pageRuntime)
Expand Down Expand Up @@ -1652,7 +1660,7 @@ export async function isPageStatic({
]
: await collectGenerateParams(tree)

appConfig = reduceAppConfig(generateParams)
appConfig = reduceAppConfig(generateParams, nextConfigOutput)

if (appConfig.dynamic === 'force-static' && pathIsEdgeRuntime) {
Log.warn(
Expand Down Expand Up @@ -1688,6 +1696,7 @@ export async function isPageStatic({
maxMemoryCacheSize,
cacheHandler,
ComponentMod,
nextConfigOutput,
}))
}
} else {
Expand Down Expand Up @@ -1781,50 +1790,73 @@ export async function isPageStatic({
})
}

function reduceAppConfig(generateParams: GenerateParamsResults): AppConfig {
return generateParams.reduce(
(builtConfig: AppConfig, curGenParams): AppConfig => {
const {
dynamic,
fetchCache,
preferredRegion,
revalidate: curRevalidate,
experimental_ppr,
} = curGenParams?.config || {}

// TODO: should conflicting configs here throw an error
// e.g. if layout defines one region but page defines another
if (typeof builtConfig.preferredRegion === 'undefined') {
builtConfig.preferredRegion = preferredRegion
}
if (typeof builtConfig.dynamic === 'undefined') {
builtConfig.dynamic = dynamic
}
if (typeof builtConfig.fetchCache === 'undefined') {
builtConfig.fetchCache = fetchCache
}
// If partial prerendering has been set, only override it if the current value is
// provided as it's resolved from root layout to leaf page.
if (typeof experimental_ppr !== 'undefined') {
builtConfig.experimental_ppr = experimental_ppr
}
function reduceAppConfig(
generateParams: GenerateParamsResults,
nextConfigOutput: 'standalone' | 'export' | undefined
): AppConfig {
return generateParams.reduce<AppConfig>((builtConfig, curGenParams) => {
const {
dynamic,
fetchCache,
preferredRegion,
revalidate: curRevalidate,
experimental_ppr,
dynamicParams,
} = curGenParams?.config || {}

// TODO: should conflicting configs here throw an error
// e.g. if layout defines one region but page defines another
if (typeof builtConfig.preferredRegion === 'undefined') {
builtConfig.preferredRegion = preferredRegion
}
if (typeof builtConfig.dynamic === 'undefined') {
builtConfig.dynamic = dynamic
}
if (typeof builtConfig.fetchCache === 'undefined') {
builtConfig.fetchCache = fetchCache
}
// If partial prerendering has been set, only override it if the current value is
// provided as it's resolved from root layout to leaf page.
if (typeof experimental_ppr !== 'undefined') {
builtConfig.experimental_ppr = experimental_ppr
}

// any revalidate number overrides false
// shorter revalidate overrides longer (initially)
if (typeof builtConfig.revalidate === 'undefined') {
builtConfig.revalidate = curRevalidate
}
// any revalidate number overrides false
// shorter revalidate overrides longer (initially)
if (typeof builtConfig.revalidate === 'undefined') {
builtConfig.revalidate = curRevalidate
}

if (
typeof curRevalidate === 'number' &&
(typeof builtConfig.revalidate !== 'number' ||
curRevalidate < builtConfig.revalidate)
) {
builtConfig.revalidate = curRevalidate
}

// The default for the dynamicParams will be based on the dynamic config
// and the next config output option. If any of these are set or the
// output config is set to export, set the dynamicParams to false,
// otherwise the default is true.
if (typeof builtConfig.dynamicParams === 'undefined') {
if (
typeof curRevalidate === 'number' &&
(typeof builtConfig.revalidate !== 'number' ||
curRevalidate < builtConfig.revalidate)
builtConfig.dynamic === 'error' ||
builtConfig.dynamic === 'force-static' ||
nextConfigOutput === 'export'
) {
builtConfig.revalidate = curRevalidate
builtConfig.dynamicParams = false
} else {
builtConfig.dynamicParams = true
}
return builtConfig
},
{}
)
}

if (typeof dynamicParams === 'boolean') {
builtConfig.dynamicParams = dynamicParams
}

return builtConfig
}, {})
}

export async function hasCustomGetInitialProps({
Expand Down
101 changes: 101 additions & 0 deletions packages/next/src/lib/fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { AppConfig } from '../build/utils'

/**
* Describes the different fallback modes that a given page can have.
*/
export const enum FallbackMode {
/**
* A BLOCKING_RENDER fallback will block the request until the page is\
* generated. No fallback page will be rendered, and users will have to wait
* to render the page.
*/
BLOCKING_RENDER = 'BLOCKING_RENDER',

/**
* When set to NOT_FOUND, pages that are not already prerendered will result
* in a not found response.
*/
NOT_FOUND = 'NOT_FOUND',

/**
* When set to PRERENDER, a fallback page will be sent to users in place of
* forcing them to wait for the page to be generated. This allows the user to
* see a rendered page earlier.
*/
SERVE_PRERENDER = 'SERVE_PRERENDER',
}

/**
* The fallback value returned from the `getStaticPaths` function.
*/
export type GetStaticPathsFallback = boolean | 'blocking'

/**
* Parses the fallback field from the prerender manifest.
*
* @param fallbackField The fallback field from the prerender manifest.
* @returns The fallback mode.
*/
export function parseFallbackField(
fallbackField: string | false | null
): FallbackMode {
if (typeof fallbackField === 'string') {
return FallbackMode.SERVE_PRERENDER
} else if (fallbackField === null) {
return FallbackMode.BLOCKING_RENDER
} else {
return FallbackMode.NOT_FOUND
}
}

/**
* Parses the fallback from the static paths result.
*
* @param result The result from the static paths function.
* @returns The fallback mode.
*/
export function parseFallbackStaticPathsResult(
result: GetStaticPathsFallback
): FallbackMode {
if (result === true) {
return FallbackMode.SERVE_PRERENDER
} else if (result === 'blocking') {
return FallbackMode.BLOCKING_RENDER
} else {
return FallbackMode.NOT_FOUND
}
}

/**
* Converts the fallback mode to a static paths result.
*
* @param fallback The fallback mode.
* @returns The static paths fallback result.
*/
export function fallbackToStaticPathsResult(
fallback: FallbackMode
): GetStaticPathsFallback {
switch (fallback) {
case FallbackMode.SERVE_PRERENDER:
return true
case FallbackMode.BLOCKING_RENDER:
return 'blocking'
case FallbackMode.NOT_FOUND:
default:
return false
}
}

/**
* Parses the fallback field from the app config.
*
* @param appConfig The app config.
* @returns The fallback mode.
*/
export function parseFallbackAppConfig(appConfig: AppConfig): FallbackMode {
if (appConfig.dynamicParams === false) {
return FallbackMode.NOT_FOUND
} else {
return FallbackMode.BLOCKING_RENDER
}
}
Loading

0 comments on commit a45f508

Please sign in to comment.