Skip to content

Commit

Permalink
feat: added support for prefetch segments when deployed
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Jan 22, 2025
1 parent 8e2bd16 commit 6d5a363
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 28 deletions.
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -624,5 +624,6 @@
"623": "Invalid quality prop (%s) on \\`next/image\\` does not match \\`images.qualities\\` configured in your \\`next.config.js\\`\\nSee more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities",
"624": "Internal Next.js Error: createMutableActionQueue was called more than once",
"625": "Server Actions are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features",
"626": "Intercepting routes are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features"
"626": "Intercepting routes are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features",
"627": "Dynamic route not found"
}
43 changes: 40 additions & 3 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER,
NEXT_CACHE_REVALIDATED_TAGS_HEADER,
MATCHED_PATH_HEADER,
RSC_SEGMENTS_DIR_SUFFIX,
RSC_SEGMENT_SUFFIX,
} from '../lib/constants'
import { FileType, fileExists } from '../lib/file-exists'
import { findPagesDir } from '../lib/find-pages-dir'
Expand Down Expand Up @@ -217,6 +219,10 @@ import {
} from '../server/lib/utils'
import { InvariantError } from '../shared/lib/invariant-error'
import { HTML_LIMITED_BOT_UA_RE_STRING } from '../shared/lib/router/utils/is-bot'
import {
buildPrefetchSegmentDataRoute,
type PrefetchSegmentDataRoute,
} from '../server/lib/router-utils/build-prefetch-segment-data-route'

type Fallback = null | boolean | string

Expand Down Expand Up @@ -360,9 +366,10 @@ export type ManifestRoute = ManifestBuiltRoute & {
page: string
namedRegex?: string
routeKeys?: { [key: string]: string }
prefetchSegmentDataRoutes?: PrefetchSegmentDataRoute[]
}

export type ManifestDataRoute = {
type ManifestDataRoute = {
page: string
routeKeys?: { [key: string]: string }
dataRouteRegex: string
Expand All @@ -385,6 +392,7 @@ export type RoutesManifest = {
staticRoutes: Array<ManifestRoute>
dynamicRoutes: Array<ManifestRoute>
dataRoutes: Array<ManifestDataRoute>
prefetchSegmentDataRoutes: Array<PrefetchSegmentDataRoute>
i18n?: {
domains?: ReadonlyArray<{
http?: true
Expand All @@ -404,6 +412,9 @@ export type RoutesManifest = {
prefetchHeader: typeof NEXT_ROUTER_PREFETCH_HEADER
suffix: typeof RSC_SUFFIX
prefetchSuffix: typeof RSC_PREFETCH_SUFFIX
prefetchSegmentHeader: typeof NEXT_ROUTER_SEGMENT_PREFETCH_HEADER
prefetchSegmentDirSuffix: typeof RSC_SEGMENTS_DIR_SUFFIX
prefetchSegmentSuffix: typeof RSC_SEGMENT_SUFFIX
}
rewriteHeaders: {
pathHeader: typeof NEXT_REWRITTEN_PATH_HEADER
Expand Down Expand Up @@ -1256,6 +1267,7 @@ export default async function build(
dynamicRoutes,
staticRoutes,
dataRoutes: [],
prefetchSegmentDataRoutes: [],
i18n: config.i18n || undefined,
rsc: {
header: RSC_HEADER,
Expand All @@ -1267,6 +1279,9 @@ export default async function build(
contentTypeHeader: RSC_CONTENT_TYPE_HEADER,
suffix: RSC_SUFFIX,
prefetchSuffix: RSC_PREFETCH_SUFFIX,
prefetchSegmentHeader: NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
prefetchSegmentSuffix: RSC_SEGMENT_SUFFIX,
prefetchSegmentDirSuffix: RSC_SEGMENTS_DIR_SUFFIX,
},
rewriteHeaders: {
pathHeader: NEXT_REWRITTEN_PATH_HEADER,
Expand Down Expand Up @@ -2620,8 +2635,6 @@ export default async function build(
]).map((page) => {
return buildDataRoute(page, buildId)
})

// await writeManifest(routesManifestPath, routesManifest)
}

// We need to write the manifest with rewrites before build
Expand Down Expand Up @@ -3182,6 +3195,24 @@ export default async function build(
)
}

if (!isAppRouteHandler && metadata?.segmentPaths) {
const dynamicRoute = routesManifest.dynamicRoutes.find(
(r) => r.page === page
)
if (!dynamicRoute) {
throw new Error('Dynamic route not found')
}

dynamicRoute.prefetchSegmentDataRoutes = []
for (const segmentPath of metadata.segmentPaths) {
const result = buildPrefetchSegmentDataRoute(
route.pathname,
segmentPath
)
dynamicRoute.prefetchSegmentDataRoutes.push(result)
}
}

pageInfos.set(route.pathname, {
...(pageInfos.get(route.pathname) as PageInfo),
isDynamicAppRoute: true,
Expand Down Expand Up @@ -3590,6 +3621,12 @@ export default async function build(
await fs.rm(outdir, { recursive: true, force: true })
await writeManifest(pagesManifestPath, pagesManifest)
})

// We need to write the manifest with rewrites after build as it might
// have been modified.
await nextBuildSpan
.traceChild('write-routes-manifest')
.traceAsyncFn(() => writeManifest(routesManifestPath, routesManifest))
}

const postBuildSpinner = createSpinner('Finalizing page optimization')
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/segment-cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ export async function fetchSegmentOnCacheMiss(
// It just needs to match the equivalent logic that happens when
// prerendering the responses. It should not leak outside of Next.js.
'/_index'
: '/' + segmentKeyPath,
: segmentKeyPath,
routeKey.nextUrl
)
if (
Expand Down
8 changes: 3 additions & 5 deletions packages/next/src/export/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
TurborepoAccessTraceResult,
} from '../build/turborepo-access-trace'
import type { FetchMetrics } from '../server/base-http'
import type { RouteMetadata } from './routes/types'

export interface AmpValidation {
page: string
Expand Down Expand Up @@ -67,10 +68,7 @@ export type ExportRouteResult =
| {
ampValidations?: AmpValidation[]
revalidate: Revalidate
metadata?: {
status?: number
headers?: OutgoingHttpHeaders
}
metadata?: Partial<RouteMetadata>
ssgNotFound?: boolean
hasEmptyPrelude?: boolean
hasPostponed?: boolean
Expand Down Expand Up @@ -135,7 +133,7 @@ export type ExportAppResult = {
/**
* The metadata for the page.
*/
metadata?: { status?: number; headers?: OutgoingHttpHeaders }
metadata?: Partial<RouteMetadata>
/**
* If the page has an empty prelude when using PPR.
*/
Expand Down
6 changes: 1 addition & 5 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2461,13 +2461,9 @@ type PrerenderToStreamResult = {
* Determines whether we should generate static flight data.
*/
function shouldGenerateStaticFlightData(workStore: WorkStore): boolean {
const { fallbackRouteParams, isStaticGeneration } = workStore
const { isStaticGeneration } = workStore
if (!isStaticGeneration) return false

if (fallbackRouteParams && fallbackRouteParams.size > 0) {
return false
}

return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ async function renderSegmentPrefetch(
if (key === ROOT_SEGMENT_KEY) {
return ['/_index', segmentBuffer]
} else {
return ['/' + key, segmentBuffer]
return [key, segmentBuffer]
}
}

Expand Down
80 changes: 76 additions & 4 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ import { FallbackMode, parseFallbackField } from '../lib/fallback'
import { toResponseCacheEntry } from './response-cache/utils'
import { scheduleOnNextTick } from '../lib/scheduler'
import { shouldServeStreamingMetadata } from './lib/streaming-metadata'
import { SegmentPrefixRSCPathnameNormalizer } from './normalizers/request/segment-prefix-rsc'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -450,10 +451,12 @@ export default abstract class Server<
protected readonly normalizers: {
readonly rsc: RSCPathnameNormalizer | undefined
readonly prefetchRSC: PrefetchRSCPathnameNormalizer | undefined
readonly segmentPrefetchRSC: SegmentPrefixRSCPathnameNormalizer | undefined
readonly data: NextDataPathnameNormalizer | undefined
}

private readonly isAppPPREnabled: boolean
private readonly isAppSegmentPrefetchEnabled: boolean

/**
* This is used to persist cache scopes across
Expand Down Expand Up @@ -529,6 +532,10 @@ export default abstract class Server<
this.enabledDirectories.app &&
checkIsAppPPREnabled(this.nextConfig.experimental.ppr)

this.isAppSegmentPrefetchEnabled =
this.enabledDirectories.app &&
this.nextConfig.experimental.clientSegmentCache === true

this.normalizers = {
// We should normalize the pathname from the RSC prefix only in minimal
// mode as otherwise that route is not exposed external to the server as
Expand All @@ -541,6 +548,10 @@ export default abstract class Server<
this.isAppPPREnabled && this.minimalMode
? new PrefetchRSCPathnameNormalizer()
: undefined,
segmentPrefetchRSC:
this.isAppSegmentPrefetchEnabled && this.minimalMode
? new SegmentPrefixRSCPathnameNormalizer()
: undefined,
data: this.enabledDirectories.pages
? new NextDataPathnameNormalizer(this.buildId)
: undefined,
Expand Down Expand Up @@ -637,7 +648,25 @@ export default abstract class Server<
) => {
if (!parsedUrl.pathname) return false

if (this.normalizers.prefetchRSC?.match(parsedUrl.pathname)) {
if (this.normalizers.segmentPrefetchRSC?.match(parsedUrl.pathname)) {
const result = this.normalizers.segmentPrefetchRSC.extract(
parsedUrl.pathname
)
if (!result) return false

const { originalPathname, segmentPath } = result
parsedUrl.pathname = originalPathname

// Mark the request as a router prefetch request.
req.headers[RSC_HEADER.toLowerCase()] = '1'
req.headers[NEXT_ROUTER_PREFETCH_HEADER.toLowerCase()] = '1'
req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()] =
segmentPath

addRequestMeta(req, 'isRSCRequest', true)
addRequestMeta(req, 'isPrefetchRSCRequest', true)
addRequestMeta(req, 'segmentPrefetchRSCRequest', segmentPath)
} else if (this.normalizers.prefetchRSC?.match(parsedUrl.pathname)) {
parsedUrl.pathname = this.normalizers.prefetchRSC.normalize(
parsedUrl.pathname,
true
Expand Down Expand Up @@ -671,6 +700,16 @@ export default abstract class Server<

if (req.headers[NEXT_ROUTER_PREFETCH_HEADER.toLowerCase()] === '1') {
addRequestMeta(req, 'isPrefetchRSCRequest', true)

const segmentPrefetchRSCRequest =
req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()]
if (typeof segmentPrefetchRSCRequest === 'string') {
addRequestMeta(
req,
'segmentPrefetchRSCRequest',
segmentPrefetchRSCRequest
)
}
}
} else {
// Otherwise just return without doing anything.
Expand Down Expand Up @@ -1272,6 +1311,31 @@ export default abstract class Server<
if (params) {
matchedPath = utils.interpolateDynamicPath(srcPathname, params)
req.url = utils.interpolateDynamicPath(req.url!, params)

// If the request is for a segment prefetch, we need to update the
// segment prefetch request path to include the interpolated
// params.
let segmentPrefetchRSCRequest = getRequestMeta(
req,
'segmentPrefetchRSCRequest'
)
if (
segmentPrefetchRSCRequest &&
isDynamicRoute(segmentPrefetchRSCRequest)
) {
segmentPrefetchRSCRequest = utils.interpolateDynamicPath(
segmentPrefetchRSCRequest,
params
)

req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()] =
segmentPrefetchRSCRequest
addRequestMeta(
req,
'segmentPrefetchRSCRequest',
segmentPrefetchRSCRequest
)
}
}
}

Expand Down Expand Up @@ -1525,6 +1589,12 @@ export default abstract class Server<
normalizers.push(this.normalizers.data)
}

// We have to put the segment prefetch normalizer before the RSC normalizer
// because the RSC normalizer will match the prefetch RSC routes too.
if (this.normalizers.segmentPrefetchRSC) {
normalizers.push(this.normalizers.segmentPrefetchRSC)
}

// We have to put the prefetch normalizer before the RSC normalizer
// because the RSC normalizer will match the prefetch RSC routes too.
if (this.normalizers.prefetchRSC) {
Expand Down Expand Up @@ -2123,8 +2193,10 @@ export default abstract class Server<
// need to transfer it to the request meta because it's only read
// within this function; the static segment data should have already been
// generated, so we will always either return a static response or a 404.
const segmentPrefetchHeader =
req.headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER.toLowerCase()]
const segmentPrefetchHeader = getRequestMeta(
req,
'segmentPrefetchRSCRequest'
)

// we need to ensure the status code if /404 is visited directly
if (is404Page && !isNextDataRequest && !isRSCRequest) {
Expand Down Expand Up @@ -3154,7 +3226,7 @@ export default abstract class Server<

const { value: cachedData } = cacheEntry

if (isRoutePPREnabled && typeof segmentPrefetchHeader === 'string') {
if (typeof segmentPrefetchHeader === 'string') {
// This is a prefetch request issued by the client Segment Cache. These
// should never reach the application layer (lambda). We should either
// respond from the cache (HIT) or respond with 204 No Content (MISS).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { buildPrefetchSegmentDataRoute } from './build-prefetch-segment-data-route'

describe('buildPrefetchSegmentDataRoute', () => {
it('should build a prefetch segment data route', () => {
const route = buildPrefetchSegmentDataRoute(
'/blog/[...slug]',
'/$c$slug$[slug]/__PAGE__'
)

expect(route).toMatchInlineSnapshot(`
{
"destination": "/blog/[...slug].segments/$c$slug$[slug]/__PAGE__.segment.rsc",
"source": "^/blog/(?<nxtPslug>.+?)\\.segments/\\$c\\$slug\\$\\k<nxtPslug>/__PAGE__\\.segment\\.rsc$",
}
`)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from '../../../shared/lib/isomorphic/path'
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
import { getNamedRouteRegex } from '../../../shared/lib/router/utils/route-regex'
import {
RSC_SEGMENT_SUFFIX,
RSC_SEGMENTS_DIR_SUFFIX,
} from '../../../lib/constants'

export const SEGMENT_PATH_KEY = 'nextSegmentPath'

export type PrefetchSegmentDataRoute = {
source: string
destination: string
}

export function buildPrefetchSegmentDataRoute(
page: string,
segmentPath: string
): PrefetchSegmentDataRoute {
const pagePath = normalizePagePath(page)

const destination = path.posix.join(
`${pagePath}${RSC_SEGMENTS_DIR_SUFFIX}`,
`${segmentPath}${RSC_SEGMENT_SUFFIX}`
)

const { namedRegex } = getNamedRouteRegex(destination, {
prefixRouteKeys: true,
includePrefix: true,
includeSuffix: true,
excludeOptionalTrailingSlash: true,
backreferenceDuplicateKeys: true,
})

return {
destination,
source: namedRegex,
}
}
Loading

0 comments on commit 6d5a363

Please sign in to comment.