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

interception routes: re-implementation #48027

Merged
merged 6 commits into from Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/next/src/build/index.ts
Expand Up @@ -137,6 +137,7 @@ import { isAppRouteRoute } from '../lib/is-app-route-route'
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'

export type SsgRoute = {
initialRevalidateSeconds: number | false
Expand Down Expand Up @@ -519,6 +520,12 @@ export default async function build(
appPageKeys.push(normalizedAppPageKey)
}
}

// Interception routes are modelled as beforeFiles rewrites
rewrites.beforeFiles.unshift(
...generateInterceptionRoutesRewrites(appPageKeys)
)

const totalAppPagesCount = appPageKeys.length

const pageKeys = {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/client/app-index.tsx
Expand Up @@ -240,6 +240,7 @@ export function hydrate() {
apply: false,
hashFragment: null,
},
nextUrl: null,
}}
>
<HotReload
Expand Down
Expand Up @@ -3,6 +3,7 @@ export const ACTION = 'Next-Action' as const

export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
export const NEXT_URL = 'Next-Url' as const
export const FETCH_CACHE_HEADER = 'x-vercel-sc-headers' as const
export const RSC_CONTENT_TYPE_HEADER = 'text/x-component' as const
export const RSC_VARY_HEADER =
Expand Down
11 changes: 10 additions & 1 deletion packages/next/src/client/components/app-router.tsx
Expand Up @@ -110,7 +110,15 @@ function Router({
[children, initialCanonicalUrl, initialTree, initialHead]
)
const [
{ tree, cache, prefetchCache, pushRef, focusAndScrollRef, canonicalUrl },
{
tree,
cache,
prefetchCache,
pushRef,
focusAndScrollRef,
canonicalUrl,
nextUrl,
},
dispatch,
sync,
] = useReducerWithReduxDevtools(reducer, initialState)
Expand Down Expand Up @@ -370,6 +378,7 @@ function Router({
changeByServerResponse,
tree,
focusAndScrollRef,
nextUrl,
}}
>
<AppRouterContext.Provider value={appRouter}>
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/client/components/layout-router.tsx
Expand Up @@ -298,7 +298,11 @@ function InnerLayoutRouter({
*/
childNodes.set(cacheKey, {
status: CacheStates.DATA_FETCH,
data: fetchServerResponse(new URL(url, location.origin), refetchTree),
data: fetchServerResponse(
new URL(url, location.origin),
refetchTree,
context.nextUrl
),
subTreeData: null,
head:
childNode && childNode.status === CacheStates.LAZY_INITIALIZED
Expand Down
@@ -0,0 +1,96 @@
import { FlightRouterState, Segment } from '../../../server/app-render/types'
import { INTERCEPTION_ROUTE_MARKERS } from '../../../server/future/helpers/interception-routes'
import { matchSegment } from '../match-segments'

const segmentToPathname = (segment: Segment): string => {
if (typeof segment === 'string') {
return segment
}

return segment[1]
}

export function extractPathFromFlightRouterState(
flightRouterState: FlightRouterState
): string | undefined {
const segment = Array.isArray(flightRouterState[0])
? flightRouterState[0][1]
: flightRouterState[0]

if (
segment === '__DEFAULT__' ||
INTERCEPTION_ROUTE_MARKERS.some((m) => segment.startsWith(m))
)
return undefined

if (segment === '__PAGE__') return ''

const path = [segment]

const parallelRoutes = flightRouterState[1] ?? {}

const childrenPath = parallelRoutes.children
? extractPathFromFlightRouterState(parallelRoutes.children)
: undefined

if (childrenPath !== undefined) {
path.push(childrenPath)
} else {
for (const [key, value] of Object.entries(parallelRoutes)) {
if (key === 'children') continue

const childPath = extractPathFromFlightRouterState(value)

if (childPath !== undefined) {
path.push(childPath)
}
}
}

const finalPath = path.join('/')

// it'll end up including a trailing slash because of '__PAGE__'
return finalPath.endsWith('/') ? finalPath.slice(0, -1) : finalPath
}

export function computeChangedPath(
treeA: FlightRouterState,
treeB: FlightRouterState
): string | null {
const [segmentA, parallelRoutesA] = treeA
const [segmentB, parallelRoutesB] = treeB

const normalizedSegmentA = segmentToPathname(segmentA)
const normalizedSegmentB = segmentToPathname(segmentB)

if (
INTERCEPTION_ROUTE_MARKERS.some(
(m) =>
normalizedSegmentA.startsWith(m) || normalizedSegmentB.startsWith(m)
)
) {
return ''
}

if (!matchSegment(segmentA, segmentB)) {
// once we find where the tree changed, we compute the rest of the path by traversing the tree
return extractPathFromFlightRouterState(treeB) ?? ''
}

for (const parallelRouterKey in parallelRoutesA) {
if (parallelRoutesB[parallelRouterKey]) {
const changedPath = computeChangedPath(
parallelRoutesA[parallelRouterKey],
parallelRoutesB[parallelRouterKey]
)
if (changedPath !== null) {
if (changedPath === '') {
return segmentToPathname(segmentB)
}
return `${segmentToPathname(segmentB)}/${changedPath}`
}
}
}

return null
}
Expand Up @@ -95,6 +95,7 @@ describe('createInitialRouterState', () => {
pushRef: { pendingPush: false, mpaNavigation: false },
focusAndScrollRef: { apply: false, hashFragment: null },
cache: expectedCache,
nextUrl: '/linking',
}

expect(state).toMatchObject(expected)
Expand Down
Expand Up @@ -51,5 +51,6 @@ export function createInitialRouterState({
? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs.
createHrefFromUrl(location)
: initialCanonicalUrl,
nextUrl: location?.pathname ?? null,
}
}
Expand Up @@ -8,6 +8,7 @@ import type {
import {
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
NEXT_URL,
RSC,
RSC_CONTENT_TYPE_HEADER,
} from '../app-router-headers'
Expand All @@ -21,11 +22,13 @@ import { callServer } from '../../app-call-server'
export async function fetchServerResponse(
url: URL,
flightRouterState: FlightRouterState,
nextUrl: string | null,
prefetch?: true
): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> {
const headers: {
[RSC]: '1'
[NEXT_ROUTER_STATE_TREE]: string
[NEXT_URL]?: string
[NEXT_ROUTER_PREFETCH]?: '1'
} = {
// Enable flight response
Expand All @@ -38,6 +41,10 @@ export async function fetchServerResponse(
headers[NEXT_ROUTER_PREFETCH] = '1'
}

if (nextUrl) {
headers[NEXT_URL] = nextUrl
}

try {
let fetchUrl = url
if (process.env.NODE_ENV === 'production') {
Expand Down
@@ -1,3 +1,4 @@
import { computeChangedPath } from './compute-changed-path'
import {
Mutable,
ReadonlyReducerState,
Expand Down Expand Up @@ -50,5 +51,10 @@ export function handleMutable(
typeof mutable.patchedTree !== 'undefined'
? mutable.patchedTree
: state.tree,
nextUrl:
typeof mutable.patchedTree !== 'undefined'
? computeChangedPath(state.tree, mutable.patchedTree) ??
state.canonicalUrl
: state.nextUrl,
}
}
Expand Up @@ -32,12 +32,11 @@ function fastRefreshReducerImpl(
// TODO-APP: verify that `href` is not an external url.
// Fetch data from the root of the tree.
cache.data = createRecordFromThenable(
fetchServerResponse(new URL(href, origin), [
state.tree[0],
state.tree[1],
state.tree[2],
'refetch',
])
fetchServerResponse(
new URL(href, origin),
[state.tree[0], state.tree[1], state.tree[2], 'refetch'],
state.nextUrl
)
)
}
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data!)
Expand Down
Expand Up @@ -195,6 +195,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: '/linking/about',
nextUrl: '/linking/about',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -378,6 +379,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: '/linking/about',
nextUrl: '/linking/about',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -564,6 +566,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: 'https://example.vercel.sh/',
nextUrl: '/linking',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -721,6 +724,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: 'https://example.vercel.sh/',
nextUrl: '/linking',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -875,6 +879,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: '/linking/about',
nextUrl: '/linking/about',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -1098,6 +1103,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: '/linking/about',
nextUrl: '/linking/about',
cache: {
status: CacheStates.READY,
data: null,
Expand Down Expand Up @@ -1325,6 +1331,7 @@ describe('navigateReducer', () => {
hashFragment: null,
},
canonicalUrl: '/parallel-tab-bar/demographics',
nextUrl: '/parallel-tab-bar/demographics',
cache: {
status: CacheStates.READY,
data: null,
Expand Down
Expand Up @@ -169,7 +169,7 @@ export function navigateReducer(
state.cache,
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
segments.slice(1),
() => fetchServerResponse(url, optimisticTree)
() => fetchServerResponse(url, optimisticTree, state.nextUrl)
)

// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
Expand All @@ -190,7 +190,9 @@ export function navigateReducer(

// If no in-flight fetch at the top, start it.
if (!cache.data) {
cache.data = createRecordFromThenable(fetchServerResponse(url, state.tree))
cache.data = createRecordFromThenable(
fetchServerResponse(url, state.tree, state.nextUrl)
)
}

// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
Expand Down
Expand Up @@ -124,7 +124,12 @@ describe('prefetchReducer', () => {
})

const url = new URL('/linking/about', 'https://localhost')
const serverResponse = await fetchServerResponse(url, initialTree, true)
const serverResponse = await fetchServerResponse(
url,
initialTree,
null,
true
)
const action: PrefetchAction = {
type: ACTION_PREFETCH,
url,
Expand Down Expand Up @@ -195,6 +200,7 @@ describe('prefetchReducer', () => {
undefined,
true,
],
nextUrl: '/linking',
}

expect(newState).toMatchObject(expectedState)
Expand Down Expand Up @@ -262,7 +268,12 @@ describe('prefetchReducer', () => {
})

const url = new URL('/linking/about', 'https://localhost')
const serverResponse = await fetchServerResponse(url, initialTree, true)
const serverResponse = await fetchServerResponse(
url,
initialTree,
null,
true
)
const action: PrefetchAction = {
type: ACTION_PREFETCH,
url,
Expand Down Expand Up @@ -335,6 +346,7 @@ describe('prefetchReducer', () => {
undefined,
true,
],
nextUrl: '/linking',
}

expect(newState).toMatchObject(expectedState)
Expand Down
Expand Up @@ -29,6 +29,7 @@ export function prefetchReducer(
url,
// initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case.
state.tree,
state.nextUrl,
true
)
)
Expand Down