Revert "app-router: new client-side cache semantics" (#48678)
Reverts vercel/next.js#48383 fix NEXT-1011 revert and re-land later
This commit is contained in:
parent
f779f10f38
commit
52fcc59717
27 changed files with 271 additions and 951 deletions
|
@ -25,7 +25,6 @@ import {
|
|||
ACTION_REFRESH,
|
||||
ACTION_RESTORE,
|
||||
ACTION_SERVER_PATCH,
|
||||
PrefetchKind,
|
||||
} from './router-reducer/router-reducer-types'
|
||||
import { createHrefFromUrl } from './router-reducer/create-href-from-url'
|
||||
import {
|
||||
|
@ -235,7 +234,7 @@ function Router({
|
|||
const routerInstance: AppRouterInstance = {
|
||||
back: () => window.history.back(),
|
||||
forward: () => window.history.forward(),
|
||||
prefetch: async (href, options) => {
|
||||
prefetch: async (href) => {
|
||||
// If prefetch has already been triggered, don't trigger it again.
|
||||
if (isBot(window.navigator.userAgent)) {
|
||||
return
|
||||
|
@ -245,12 +244,12 @@ function Router({
|
|||
if (isExternalURL(url)) {
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-ignore startTransition exists
|
||||
React.startTransition(() => {
|
||||
dispatch({
|
||||
type: ACTION_PREFETCH,
|
||||
url,
|
||||
kind: options?.kind ?? PrefetchKind.FULL,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
|
@ -277,7 +277,16 @@ function InnerLayoutRouter({
|
|||
// TODO-APP: verify if this can be null based on user code
|
||||
childProp.current !== null
|
||||
) {
|
||||
if (!childNode) {
|
||||
if (childNode) {
|
||||
if (childNode.status === CacheStates.LAZY_INITIALIZED) {
|
||||
// @ts-expect-error we're changing it's type!
|
||||
childNode.status = CacheStates.READY
|
||||
// @ts-expect-error
|
||||
childNode.subTreeData = childProp.current
|
||||
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
|
||||
childProp.current = null
|
||||
}
|
||||
} else {
|
||||
// Add the segment's subTreeData to the cache.
|
||||
// This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
|
||||
childNodes.set(cacheKey, {
|
||||
|
@ -286,15 +295,10 @@ function InnerLayoutRouter({
|
|||
subTreeData: childProp.current,
|
||||
parallelRoutes: new Map(),
|
||||
})
|
||||
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
|
||||
childProp.current = null
|
||||
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
|
||||
childNode = childNodes.get(cacheKey)
|
||||
} else {
|
||||
if (childNode.status === CacheStates.LAZY_INITIALIZED) {
|
||||
// @ts-expect-error we're changing it's type!
|
||||
childNode.status = CacheStates.READY
|
||||
// @ts-expect-error
|
||||
childNode.subTreeData = childProp.current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ export function applyFlightData(
|
|||
existingCache: CacheNode,
|
||||
cache: CacheNode,
|
||||
flightDataPath: FlightDataPath,
|
||||
wasPrefetched: boolean = false
|
||||
wasPrefetched?: boolean
|
||||
): boolean {
|
||||
// The one before last item is the router state tree patch
|
||||
const [treePatch, subTreeData, head] = flightDataPath.slice(-3)
|
||||
|
@ -33,12 +33,7 @@ export function applyFlightData(
|
|||
cache.subTreeData = existingCache.subTreeData
|
||||
cache.parallelRoutes = new Map(existingCache.parallelRoutes)
|
||||
// Create a copy of the existing cache with the subTreeData applied.
|
||||
fillCacheWithNewSubTreeData(
|
||||
cache,
|
||||
existingCache,
|
||||
flightDataPath,
|
||||
wasPrefetched
|
||||
)
|
||||
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '../app-router-headers'
|
||||
import { urlToUrlWithoutFlightMarker } from '../app-router'
|
||||
import { callServer } from '../../app-call-server'
|
||||
import { PrefetchKind } from './router-reducer-types'
|
||||
|
||||
/**
|
||||
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
|
||||
|
@ -24,7 +23,7 @@ export async function fetchServerResponse(
|
|||
url: URL,
|
||||
flightRouterState: FlightRouterState,
|
||||
nextUrl: string | null,
|
||||
prefetchKind?: PrefetchKind
|
||||
prefetch?: true
|
||||
): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> {
|
||||
const headers: {
|
||||
[RSC]: '1'
|
||||
|
@ -37,14 +36,8 @@ export async function fetchServerResponse(
|
|||
// Provide the current router state
|
||||
[NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState),
|
||||
}
|
||||
|
||||
/**
|
||||
* Three cases:
|
||||
* - `prefetchKind` is `undefined`, it means it's a normal navigation, so we want to prefetch the page data fully
|
||||
* - `prefetchKind` is `full` - we want to prefetch the whole page so same as above
|
||||
* - `prefetchKind` is `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully
|
||||
*/
|
||||
if (prefetchKind === PrefetchKind.AUTO) {
|
||||
if (prefetch) {
|
||||
// Enable prefetch response
|
||||
headers[NEXT_ROUTER_PREFETCH] = '1'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { FlightSegmentPath } from '../../../server/app-render/types'
|
||||
import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context'
|
||||
import { createRouterCacheKey } from './create-router-cache-key'
|
||||
import { fetchServerResponse } from './fetch-server-response'
|
||||
|
||||
/**
|
||||
|
@ -9,24 +7,19 @@ import { fetchServerResponse } from './fetch-server-response'
|
|||
export function fillCacheWithDataProperty(
|
||||
newCache: CacheNode,
|
||||
existingCache: CacheNode,
|
||||
flightSegmentPath: FlightSegmentPath,
|
||||
fetchResponse: () => ReturnType<typeof fetchServerResponse>,
|
||||
bailOnParallelRoutes: boolean = false
|
||||
segments: string[],
|
||||
fetchResponse: () => ReturnType<typeof fetchServerResponse>
|
||||
): { bailOptimistic: boolean } | undefined {
|
||||
const isLastEntry = flightSegmentPath.length <= 2
|
||||
const isLastEntry = segments.length === 1
|
||||
|
||||
const [parallelRouteKey, segment] = flightSegmentPath
|
||||
const cacheKey = createRouterCacheKey(segment)
|
||||
const parallelRouteKey = 'children'
|
||||
const [segment] = segments
|
||||
|
||||
const existingChildSegmentMap =
|
||||
existingCache.parallelRoutes.get(parallelRouteKey)
|
||||
|
||||
if (
|
||||
!existingChildSegmentMap ||
|
||||
(bailOnParallelRoutes && existingCache.parallelRoutes.size > 1)
|
||||
) {
|
||||
if (!existingChildSegmentMap) {
|
||||
// Bailout because the existing cache does not have the path to the leaf node
|
||||
// or the existing cache has multiple parallel routes
|
||||
// Will trigger lazy fetch in layout-router because of missing segment
|
||||
return { bailOptimistic: true }
|
||||
}
|
||||
|
@ -38,8 +31,8 @@ export function fillCacheWithDataProperty(
|
|||
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)
|
||||
}
|
||||
|
||||
const existingChildCacheNode = existingChildSegmentMap.get(cacheKey)
|
||||
let childCacheNode = childSegmentMap.get(cacheKey)
|
||||
const existingChildCacheNode = existingChildSegmentMap.get(segment)
|
||||
let childCacheNode = childSegmentMap.get(segment)
|
||||
|
||||
// In case of last segment start off the fetch at this level and don't copy further down.
|
||||
if (isLastEntry) {
|
||||
|
@ -48,7 +41,7 @@ export function fillCacheWithDataProperty(
|
|||
!childCacheNode.data ||
|
||||
childCacheNode === existingChildCacheNode
|
||||
) {
|
||||
childSegmentMap.set(cacheKey, {
|
||||
childSegmentMap.set(segment, {
|
||||
status: CacheStates.DATA_FETCH,
|
||||
data: fetchResponse(),
|
||||
subTreeData: null,
|
||||
|
@ -61,7 +54,7 @@ export function fillCacheWithDataProperty(
|
|||
if (!childCacheNode || !existingChildCacheNode) {
|
||||
// Start fetch in the place where the existing cache doesn't have the data yet.
|
||||
if (!childCacheNode) {
|
||||
childSegmentMap.set(cacheKey, {
|
||||
childSegmentMap.set(segment, {
|
||||
status: CacheStates.DATA_FETCH,
|
||||
data: fetchResponse(),
|
||||
subTreeData: null,
|
||||
|
@ -78,13 +71,13 @@ export function fillCacheWithDataProperty(
|
|||
subTreeData: childCacheNode.subTreeData,
|
||||
parallelRoutes: new Map(childCacheNode.parallelRoutes),
|
||||
} as CacheNode
|
||||
childSegmentMap.set(cacheKey, childCacheNode)
|
||||
childSegmentMap.set(segment, childCacheNode)
|
||||
}
|
||||
|
||||
return fillCacheWithDataProperty(
|
||||
childCacheNode,
|
||||
existingChildCacheNode,
|
||||
flightSegmentPath.slice(2),
|
||||
segments.slice(1),
|
||||
fetchResponse
|
||||
)
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ describe('fillCacheWithNewSubtreeData', () => {
|
|||
// Mirrors the way router-reducer values are passed in.
|
||||
const flightDataPath = flightData[0]
|
||||
|
||||
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath, false)
|
||||
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
|
||||
|
||||
const expectedCache: CacheNode = {
|
||||
data: null,
|
||||
|
|
|
@ -10,8 +10,7 @@ import { createRouterCacheKey } from './create-router-cache-key'
|
|||
export function fillCacheWithNewSubTreeData(
|
||||
newCache: CacheNode,
|
||||
existingCache: CacheNode,
|
||||
flightDataPath: FlightDataPath,
|
||||
wasPrefetched?: boolean
|
||||
flightDataPath: FlightDataPath
|
||||
): void {
|
||||
const isLastEntry = flightDataPath.length <= 5
|
||||
const [parallelRouteKey, segment] = flightDataPath
|
||||
|
@ -64,8 +63,7 @@ export function fillCacheWithNewSubTreeData(
|
|||
childCacheNode,
|
||||
existingChildCacheNode,
|
||||
flightDataPath[2],
|
||||
flightDataPath[4],
|
||||
wasPrefetched
|
||||
flightDataPath[4]
|
||||
)
|
||||
|
||||
childSegmentMap.set(cacheKey, childCacheNode)
|
||||
|
@ -92,7 +90,6 @@ export function fillCacheWithNewSubTreeData(
|
|||
fillCacheWithNewSubTreeData(
|
||||
childCacheNode,
|
||||
existingChildCacheNode,
|
||||
flightDataPath.slice(2),
|
||||
wasPrefetched
|
||||
flightDataPath.slice(2)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import { PrefetchCacheEntry } from './router-reducer-types'
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000
|
||||
const THIRTY_SECONDS = 30 * 1000
|
||||
|
||||
export enum PrefetchCacheEntryStatus {
|
||||
fresh = 'fresh',
|
||||
reusable = 'reusable',
|
||||
expired = 'expired',
|
||||
stale = 'stale',
|
||||
}
|
||||
|
||||
export function getPrefetchEntryCacheStatus({
|
||||
kind,
|
||||
prefetchTime,
|
||||
lastUsedTime,
|
||||
}: PrefetchCacheEntry): PrefetchCacheEntryStatus {
|
||||
// if the cache entry was prefetched or read less than 30s ago, then we want to re-use it
|
||||
if (Date.now() < (lastUsedTime ?? prefetchTime) + THIRTY_SECONDS) {
|
||||
return lastUsedTime
|
||||
? PrefetchCacheEntryStatus.reusable
|
||||
: PrefetchCacheEntryStatus.fresh
|
||||
}
|
||||
|
||||
// if the cache entry was prefetched less than 5 mins ago, then we want to re-use only the loading state
|
||||
if (kind === 'auto') {
|
||||
if (Date.now() < prefetchTime + FIVE_MINUTES) {
|
||||
return PrefetchCacheEntryStatus.stale
|
||||
}
|
||||
}
|
||||
|
||||
// if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it "full
|
||||
if (kind === 'full') {
|
||||
if (Date.now() < prefetchTime + FIVE_MINUTES) {
|
||||
return PrefetchCacheEntryStatus.reusable
|
||||
}
|
||||
}
|
||||
|
||||
return PrefetchCacheEntryStatus.expired
|
||||
}
|
|
@ -86,7 +86,7 @@ describe('invalidateCacheBelowFlightSegmentPath', () => {
|
|||
// @ts-expect-error TODO-APP: investigate why this is not a TS error in router-reducer.
|
||||
cache.subTreeData = existingCache.subTreeData
|
||||
// Create a copy of the existing cache with the subTreeData applied.
|
||||
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath, false)
|
||||
fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
|
||||
|
||||
// Invalidate the cache below the flight segment path. This should remove the 'about' node.
|
||||
invalidateCacheBelowFlightSegmentPath(
|
||||
|
|
|
@ -80,7 +80,6 @@ import {
|
|||
ACTION_NAVIGATE,
|
||||
ACTION_PREFETCH,
|
||||
PrefetchAction,
|
||||
PrefetchKind,
|
||||
} from '../router-reducer-types'
|
||||
import { navigateReducer } from './navigate-reducer'
|
||||
import { prefetchReducer } from './prefetch-reducer'
|
||||
|
@ -1006,7 +1005,6 @@ describe('navigateReducer', () => {
|
|||
const prefetchAction: PrefetchAction = {
|
||||
type: ACTION_PREFETCH,
|
||||
url,
|
||||
kind: PrefetchKind.AUTO,
|
||||
}
|
||||
|
||||
const state = createInitialRouterState({
|
||||
|
@ -1088,9 +1086,6 @@ describe('navigateReducer', () => {
|
|||
'/linking/about',
|
||||
{
|
||||
data: record,
|
||||
kind: PrefetchKind.AUTO,
|
||||
lastUsedTime: null,
|
||||
prefetchTime: expect.any(Number),
|
||||
treeAtTimeOfPrefetch: [
|
||||
'',
|
||||
{
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
CacheNode,
|
||||
CacheStates,
|
||||
} from '../../../../shared/lib/app-router-context'
|
||||
import { CacheStates } from '../../../../shared/lib/app-router-context'
|
||||
import type {
|
||||
FlightRouterState,
|
||||
FlightSegmentPath,
|
||||
|
@ -16,20 +13,14 @@ import { createOptimisticTree } from '../create-optimistic-tree'
|
|||
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
|
||||
import { shouldHardNavigate } from '../should-hard-navigate'
|
||||
import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'
|
||||
import {
|
||||
import type {
|
||||
Mutable,
|
||||
NavigateAction,
|
||||
PrefetchKind,
|
||||
ReadonlyReducerState,
|
||||
ReducerState,
|
||||
} from '../router-reducer-types'
|
||||
import { handleMutable } from '../handle-mutable'
|
||||
import { applyFlightData } from '../apply-flight-data'
|
||||
import {
|
||||
PrefetchCacheEntryStatus,
|
||||
getPrefetchEntryCacheStatus,
|
||||
} from '../get-prefetch-cache-entry-status'
|
||||
import { prunePrefetchCache } from './prune-prefetch-cache'
|
||||
|
||||
export function handleExternalUrl(
|
||||
state: ReadonlyReducerState,
|
||||
|
@ -72,37 +63,6 @@ function generateSegmentsFromPatch(
|
|||
return segments
|
||||
}
|
||||
|
||||
function addRefetchToLeafSegments(
|
||||
newCache: CacheNode,
|
||||
currentCache: CacheNode,
|
||||
flightSegmentPath: FlightSegmentPath,
|
||||
treePatch: FlightRouterState,
|
||||
data: () => ReturnType<typeof fetchServerResponse>
|
||||
) {
|
||||
let appliedPatch = false
|
||||
|
||||
newCache.status = CacheStates.READY
|
||||
newCache.subTreeData = currentCache.subTreeData
|
||||
newCache.parallelRoutes = new Map(currentCache.parallelRoutes)
|
||||
|
||||
const segmentPathsToFill = generateSegmentsFromPatch(treePatch).map(
|
||||
(segment) => [...flightSegmentPath, ...segment]
|
||||
)
|
||||
|
||||
for (const segmentPaths of segmentPathsToFill) {
|
||||
const res = fillCacheWithDataProperty(
|
||||
newCache,
|
||||
currentCache,
|
||||
segmentPaths,
|
||||
data
|
||||
)
|
||||
if (!res?.bailOptimistic) {
|
||||
appliedPatch = true
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatch
|
||||
}
|
||||
export function navigateReducer(
|
||||
state: ReadonlyReducerState,
|
||||
action: NavigateAction
|
||||
|
@ -118,8 +78,6 @@ export function navigateReducer(
|
|||
const { pathname, hash } = url
|
||||
const href = createHrefFromUrl(url)
|
||||
const pendingPush = navigateType === 'push'
|
||||
// we want to prune the prefetch cache on every navigation to avoid it growing too large
|
||||
prunePrefetchCache(state.prefetchCache)
|
||||
|
||||
const isForCurrentTree =
|
||||
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)
|
||||
|
@ -132,12 +90,116 @@ export function navigateReducer(
|
|||
return handleExternalUrl(state, mutable, url.toString(), pendingPush)
|
||||
}
|
||||
|
||||
let prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false))
|
||||
const prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false))
|
||||
if (prefetchValues) {
|
||||
// The one before last item is the router state tree patch
|
||||
const { treeAtTimeOfPrefetch, data } = prefetchValues
|
||||
|
||||
if (
|
||||
forceOptimisticNavigation &&
|
||||
prefetchValues?.kind !== PrefetchKind.TEMPORARY
|
||||
) {
|
||||
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
|
||||
const [flightData, canonicalUrlOverride] = readRecordValue(data!)
|
||||
|
||||
// Handle case when navigating to page in `pages` from `app`
|
||||
if (typeof flightData === 'string') {
|
||||
return handleExternalUrl(state, mutable, flightData, pendingPush)
|
||||
}
|
||||
|
||||
let currentTree = state.tree
|
||||
let currentCache = state.cache
|
||||
let scrollableSegments: FlightSegmentPath[] = []
|
||||
for (const flightDataPath of flightData) {
|
||||
const flightSegmentPath = flightDataPath.slice(
|
||||
0,
|
||||
-3
|
||||
) as unknown as FlightSegmentPath
|
||||
|
||||
// The one before last item is the router state tree patch
|
||||
const [treePatch] = flightDataPath.slice(-3) as [FlightRouterState]
|
||||
|
||||
// Create new tree based on the flightSegmentPath and router state patch
|
||||
let newTree = applyRouterStatePatchToTree(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
currentTree,
|
||||
treePatch
|
||||
)
|
||||
|
||||
// If the tree patch can't be applied to the current tree then we use the tree at time of prefetch
|
||||
// TODO-APP: This should instead fill in the missing pieces in `currentTree` with the data from `treeAtTimeOfPrefetch`, then apply the patch.
|
||||
if (newTree === null) {
|
||||
newTree = applyRouterStatePatchToTree(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
treeAtTimeOfPrefetch,
|
||||
treePatch
|
||||
)
|
||||
}
|
||||
|
||||
if (newTree !== null) {
|
||||
if (isNavigatingToNewRootLayout(currentTree, newTree)) {
|
||||
return handleExternalUrl(state, mutable, href, pendingPush)
|
||||
}
|
||||
|
||||
const applied = applyFlightData(
|
||||
currentCache,
|
||||
cache,
|
||||
flightDataPath,
|
||||
true
|
||||
)
|
||||
|
||||
const hardNavigate = shouldHardNavigate(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
currentTree
|
||||
)
|
||||
|
||||
if (hardNavigate) {
|
||||
cache.status = CacheStates.READY
|
||||
// Copy subTreeData for the root node of the cache.
|
||||
cache.subTreeData = currentCache.subTreeData
|
||||
|
||||
invalidateCacheBelowFlightSegmentPath(
|
||||
cache,
|
||||
currentCache,
|
||||
flightSegmentPath
|
||||
)
|
||||
// Ensure the existing cache value is used when the cache was not invalidated.
|
||||
mutable.cache = cache
|
||||
} else if (applied) {
|
||||
mutable.cache = cache
|
||||
}
|
||||
|
||||
currentCache = cache
|
||||
currentTree = newTree
|
||||
|
||||
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
|
||||
scrollableSegments.push(
|
||||
// the last segment is the same as the first segment in the patch
|
||||
[...flightSegmentPath.slice(0, -1), ...subSegment].filter(
|
||||
(segment) => segment !== '__PAGE__'
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutable.previousTree = state.tree
|
||||
mutable.patchedTree = currentTree
|
||||
mutable.scrollableSegments = scrollableSegments
|
||||
mutable.canonicalUrl = canonicalUrlOverride
|
||||
? createHrefFromUrl(canonicalUrlOverride)
|
||||
: href
|
||||
mutable.pendingPush = pendingPush
|
||||
mutable.hashFragment = hash
|
||||
|
||||
return handleMutable(state, mutable)
|
||||
}
|
||||
|
||||
// When doing a hard push there can be two cases: with optimistic tree and without
|
||||
// The with optimistic tree case only happens when the layouts have a loading state (loading.js)
|
||||
// The without optimistic tree case happens when there is no loading state, in that case we suspend in this reducer
|
||||
|
||||
// forceOptimisticNavigation is used for links that have `prefetch={false}`.
|
||||
if (forceOptimisticNavigation) {
|
||||
const segments = pathname.split('/')
|
||||
// TODO-APP: figure out something better for index pages
|
||||
segments.push('')
|
||||
|
@ -146,36 +208,18 @@ export function navigateReducer(
|
|||
// If the optimistic tree is deeper than the current state leave that deeper part out of the fetch
|
||||
const optimisticTree = createOptimisticTree(segments, state.tree, false)
|
||||
|
||||
// we need a copy of the cache in case we need to revert to it
|
||||
const temporaryCacheNode: CacheNode = {
|
||||
...cache,
|
||||
}
|
||||
|
||||
// Copy subTreeData for the root node of the cache.
|
||||
// Note: didn't do it above because typescript doesn't like it.
|
||||
temporaryCacheNode.status = CacheStates.READY
|
||||
temporaryCacheNode.subTreeData = state.cache.subTreeData
|
||||
temporaryCacheNode.parallelRoutes = new Map(state.cache.parallelRoutes)
|
||||
|
||||
const data = createRecordFromThenable(
|
||||
fetchServerResponse(url, optimisticTree, state.nextUrl)
|
||||
)
|
||||
|
||||
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
|
||||
// TODO-APP: re-evaluate if we need to strip the last segment
|
||||
const optimisticFlightSegmentPath = segments
|
||||
.slice(1)
|
||||
.map((segment) => ['children', segment === '' ? '__PAGE__' : segment])
|
||||
.flat()
|
||||
cache.status = CacheStates.READY
|
||||
cache.subTreeData = state.cache.subTreeData
|
||||
|
||||
// Copy existing cache nodes as far as possible and fill in `data` property with the started data fetch.
|
||||
// The `data` property is used to suspend in layout-router during render if it hasn't resolved yet by the time it renders.
|
||||
const res = fillCacheWithDataProperty(
|
||||
temporaryCacheNode,
|
||||
cache,
|
||||
state.cache,
|
||||
optimisticFlightSegmentPath,
|
||||
() => data,
|
||||
true
|
||||
// TODO-APP: segments.slice(1) strips '', we can get rid of '' altogether.
|
||||
segments.slice(1),
|
||||
() => fetchServerResponse(url, optimisticTree, state.nextUrl)
|
||||
)
|
||||
|
||||
// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
|
||||
|
@ -185,154 +229,83 @@ export function navigateReducer(
|
|||
mutable.pendingPush = pendingPush
|
||||
mutable.hashFragment = hash
|
||||
mutable.scrollableSegments = []
|
||||
mutable.cache = temporaryCacheNode
|
||||
mutable.cache = cache
|
||||
mutable.canonicalUrl = href
|
||||
|
||||
state.prefetchCache.set(createHrefFromUrl(url, false), {
|
||||
data: Promise.resolve(data),
|
||||
// this will make sure that the entry will be discarded after 30s
|
||||
kind: PrefetchKind.TEMPORARY,
|
||||
prefetchTime: Date.now(),
|
||||
treeAtTimeOfPrefetch: state.tree,
|
||||
lastUsedTime: Date.now(),
|
||||
})
|
||||
|
||||
return handleMutable(state, mutable)
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a prefetch value, we need to create one
|
||||
if (!prefetchValues) {
|
||||
const data = createRecordFromThenable(
|
||||
// Below is the not-optimistic case. Data is fetched at the root and suspended there without a suspense boundary.
|
||||
|
||||
// If no in-flight fetch at the top, start it.
|
||||
if (!cache.data) {
|
||||
cache.data = createRecordFromThenable(
|
||||
fetchServerResponse(url, state.tree, state.nextUrl)
|
||||
)
|
||||
|
||||
const newPrefetchValue = {
|
||||
data: Promise.resolve(data),
|
||||
// this will make sure that the entry will be discarded after 30s
|
||||
kind: PrefetchKind.TEMPORARY,
|
||||
prefetchTime: Date.now(),
|
||||
treeAtTimeOfPrefetch: state.tree,
|
||||
lastUsedTime: null,
|
||||
}
|
||||
|
||||
state.prefetchCache.set(createHrefFromUrl(url, false), newPrefetchValue)
|
||||
prefetchValues = newPrefetchValue
|
||||
}
|
||||
|
||||
const prefetchEntryCacheStatus = getPrefetchEntryCacheStatus(prefetchValues)
|
||||
|
||||
// The one before last item is the router state tree patch
|
||||
const { treeAtTimeOfPrefetch, data } = prefetchValues
|
||||
|
||||
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
|
||||
const [flightData, canonicalUrlOverride] = readRecordValue(data!)
|
||||
|
||||
// important: we should only mark the cache node as dirty after we unsuspend from the call above
|
||||
prefetchValues.lastUsedTime = Date.now()
|
||||
const [flightData, canonicalUrlOverride] = readRecordValue(cache.data!)
|
||||
|
||||
// Handle case when navigating to page in `pages` from `app`
|
||||
if (typeof flightData === 'string') {
|
||||
return handleExternalUrl(state, mutable, flightData, pendingPush)
|
||||
}
|
||||
|
||||
// Remove cache.data as it has been resolved at this point.
|
||||
cache.data = null
|
||||
|
||||
let currentTree = state.tree
|
||||
let currentCache = state.cache
|
||||
let scrollableSegments: FlightSegmentPath[] = []
|
||||
for (const flightDataPath of flightData) {
|
||||
const flightSegmentPath = flightDataPath.slice(
|
||||
0,
|
||||
-4
|
||||
) as unknown as FlightSegmentPath
|
||||
// The one before last item is the router state tree patch
|
||||
const [treePatch] = flightDataPath.slice(-3) as [FlightRouterState]
|
||||
const [treePatch] = flightDataPath.slice(-3, -2)
|
||||
|
||||
// Path without the last segment, router state, and the subTreeData
|
||||
const flightSegmentPath = flightDataPath.slice(0, -4)
|
||||
|
||||
// Create new tree based on the flightSegmentPath and router state patch
|
||||
let newTree = applyRouterStatePatchToTree(
|
||||
const newTree = applyRouterStatePatchToTree(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
currentTree,
|
||||
treePatch
|
||||
)
|
||||
|
||||
// If the tree patch can't be applied to the current tree then we use the tree at time of prefetch
|
||||
// TODO-APP: This should instead fill in the missing pieces in `currentTree` with the data from `treeAtTimeOfPrefetch`, then apply the patch.
|
||||
if (newTree === null) {
|
||||
newTree = applyRouterStatePatchToTree(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
treeAtTimeOfPrefetch,
|
||||
treePatch
|
||||
)
|
||||
throw new Error('SEGMENT MISMATCH')
|
||||
}
|
||||
|
||||
if (newTree !== null) {
|
||||
if (isNavigatingToNewRootLayout(currentTree, newTree)) {
|
||||
return handleExternalUrl(state, mutable, href, pendingPush)
|
||||
}
|
||||
if (isNavigatingToNewRootLayout(currentTree, newTree)) {
|
||||
return handleExternalUrl(state, mutable, href, pendingPush)
|
||||
}
|
||||
|
||||
let applied = applyFlightData(
|
||||
currentCache,
|
||||
cache,
|
||||
flightDataPath,
|
||||
prefetchValues.kind === 'auto' &&
|
||||
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.reusable
|
||||
)
|
||||
|
||||
if (
|
||||
!applied &&
|
||||
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.stale
|
||||
) {
|
||||
applied = addRefetchToLeafSegments(
|
||||
cache,
|
||||
currentCache,
|
||||
flightSegmentPath,
|
||||
treePatch,
|
||||
() => fetchServerResponse(url, newTree!, state.nextUrl)
|
||||
)
|
||||
}
|
||||
|
||||
const hardNavigate = shouldHardNavigate(
|
||||
// TODO-APP: remove ''
|
||||
['', ...flightSegmentPath],
|
||||
currentTree
|
||||
)
|
||||
|
||||
if (hardNavigate) {
|
||||
cache.status = CacheStates.READY
|
||||
// Copy subTreeData for the root node of the cache.
|
||||
cache.subTreeData = currentCache.subTreeData
|
||||
|
||||
invalidateCacheBelowFlightSegmentPath(
|
||||
cache,
|
||||
currentCache,
|
||||
flightSegmentPath
|
||||
)
|
||||
// Ensure the existing cache value is used when the cache was not invalidated.
|
||||
mutable.cache = cache
|
||||
} else if (applied) {
|
||||
mutable.cache = cache
|
||||
}
|
||||
mutable.canonicalUrl = canonicalUrlOverride
|
||||
? createHrefFromUrl(canonicalUrlOverride)
|
||||
: href
|
||||
|
||||
const applied = applyFlightData(currentCache, cache, flightDataPath)
|
||||
if (applied) {
|
||||
mutable.cache = cache
|
||||
currentCache = cache
|
||||
currentTree = newTree
|
||||
}
|
||||
|
||||
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
|
||||
scrollableSegments.push(
|
||||
[...flightSegmentPath, ...subSegment].filter(
|
||||
(segment) => segment !== '__PAGE__'
|
||||
)
|
||||
currentTree = newTree
|
||||
|
||||
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
|
||||
scrollableSegments.push(
|
||||
[...flightSegmentPath, ...subSegment].filter(
|
||||
(segment) => segment !== '__PAGE__'
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mutable.previousTree = state.tree
|
||||
mutable.patchedTree = currentTree
|
||||
mutable.scrollableSegments = scrollableSegments
|
||||
mutable.canonicalUrl = canonicalUrlOverride
|
||||
? createHrefFromUrl(canonicalUrlOverride)
|
||||
: href
|
||||
mutable.pendingPush = pendingPush
|
||||
mutable.hashFragment = hash
|
||||
|
||||
|
|
|
@ -38,11 +38,7 @@ import {
|
|||
CacheStates,
|
||||
} from '../../../../shared/lib/app-router-context'
|
||||
import { createInitialRouterState } from '../create-initial-router-state'
|
||||
import {
|
||||
PrefetchAction,
|
||||
ACTION_PREFETCH,
|
||||
PrefetchKind,
|
||||
} from '../router-reducer-types'
|
||||
import { PrefetchAction, ACTION_PREFETCH } from '../router-reducer-types'
|
||||
import { prefetchReducer } from './prefetch-reducer'
|
||||
import { fetchServerResponse } from '../fetch-server-response'
|
||||
import { createRecordFromThenable } from '../create-record-from-thenable'
|
||||
|
@ -132,12 +128,11 @@ describe('prefetchReducer', () => {
|
|||
url,
|
||||
initialTree,
|
||||
null,
|
||||
PrefetchKind.AUTO
|
||||
true
|
||||
)
|
||||
const action: PrefetchAction = {
|
||||
type: ACTION_PREFETCH,
|
||||
url,
|
||||
kind: PrefetchKind.AUTO,
|
||||
}
|
||||
|
||||
const newState = await runPromiseThrowChain(() =>
|
||||
|
@ -154,9 +149,6 @@ describe('prefetchReducer', () => {
|
|||
'/linking/about',
|
||||
{
|
||||
data: record,
|
||||
kind: PrefetchKind.AUTO,
|
||||
lastUsedTime: null,
|
||||
prefetchTime: expect.any(Number),
|
||||
treeAtTimeOfPrefetch: [
|
||||
'',
|
||||
{
|
||||
|
@ -281,12 +273,11 @@ describe('prefetchReducer', () => {
|
|||
url,
|
||||
initialTree,
|
||||
null,
|
||||
PrefetchKind.AUTO
|
||||
true
|
||||
)
|
||||
const action: PrefetchAction = {
|
||||
type: ACTION_PREFETCH,
|
||||
url,
|
||||
kind: PrefetchKind.AUTO,
|
||||
}
|
||||
|
||||
await runPromiseThrowChain(() => prefetchReducer(state, action))
|
||||
|
@ -305,9 +296,6 @@ describe('prefetchReducer', () => {
|
|||
'/linking/about',
|
||||
{
|
||||
data: record,
|
||||
prefetchTime: expect.any(Number),
|
||||
kind: PrefetchKind.AUTO,
|
||||
lastUsedTime: null,
|
||||
treeAtTimeOfPrefetch: [
|
||||
'',
|
||||
{
|
||||
|
|
|
@ -4,18 +4,13 @@ import {
|
|||
PrefetchAction,
|
||||
ReducerState,
|
||||
ReadonlyReducerState,
|
||||
PrefetchKind,
|
||||
} from '../router-reducer-types'
|
||||
import { createRecordFromThenable } from '../create-record-from-thenable'
|
||||
import { prunePrefetchCache } from './prune-prefetch-cache'
|
||||
|
||||
export function prefetchReducer(
|
||||
state: ReadonlyReducerState,
|
||||
action: PrefetchAction
|
||||
): ReducerState {
|
||||
// let's prune the prefetch cache before we do anything else
|
||||
prunePrefetchCache(state.prefetchCache)
|
||||
|
||||
const { url } = action
|
||||
const href = createHrefFromUrl(
|
||||
url,
|
||||
|
@ -23,32 +18,9 @@ export function prefetchReducer(
|
|||
false
|
||||
)
|
||||
|
||||
const cacheEntry = state.prefetchCache.get(href)
|
||||
if (cacheEntry) {
|
||||
/**
|
||||
* If the cache entry present was marked as temporary, it means that we prefetched it from the navigate reducer,
|
||||
* where we didn't have the prefetch intent. We want to update it to the new, more accurate, kind here.
|
||||
*/
|
||||
if (cacheEntry.kind === PrefetchKind.TEMPORARY) {
|
||||
console.log(href, action.kind, cacheEntry)
|
||||
state.prefetchCache.set(href, {
|
||||
...cacheEntry,
|
||||
kind: action.kind,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* if the prefetch action was a full prefetch and that the current cache entry wasn't one, we want to re-prefetch,
|
||||
* otherwise we can re-use the current cache entry
|
||||
**/
|
||||
if (
|
||||
!(
|
||||
cacheEntry.kind === PrefetchKind.AUTO &&
|
||||
action.kind === PrefetchKind.FULL
|
||||
)
|
||||
) {
|
||||
return state
|
||||
}
|
||||
// If the href was already prefetched it is not necessary to prefetch it again
|
||||
if (state.prefetchCache.has(href)) {
|
||||
return state
|
||||
}
|
||||
|
||||
// fetchServerResponse is intentionally not awaited so that it can be unwrapped in the navigate-reducer
|
||||
|
@ -58,7 +30,7 @@ export function prefetchReducer(
|
|||
// 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,
|
||||
action.kind
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -67,9 +39,6 @@ export function prefetchReducer(
|
|||
// Create new tree based on the flightSegmentPath and router state patch
|
||||
treeAtTimeOfPrefetch: state.tree,
|
||||
data: serverResponse,
|
||||
kind: action.kind,
|
||||
prefetchTime: Date.now(),
|
||||
lastUsedTime: null,
|
||||
})
|
||||
|
||||
return state
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import type { ReducerState } from '../router-reducer-types'
|
||||
import {
|
||||
PrefetchCacheEntryStatus,
|
||||
getPrefetchEntryCacheStatus,
|
||||
} from '../get-prefetch-cache-entry-status'
|
||||
|
||||
export function prunePrefetchCache(
|
||||
prefetchCache: ReducerState['prefetchCache']
|
||||
) {
|
||||
for (const [href, prefetchCacheEntry] of prefetchCache) {
|
||||
if (
|
||||
getPrefetchEntryCacheStatus(prefetchCacheEntry) ===
|
||||
PrefetchCacheEntryStatus.expired
|
||||
) {
|
||||
prefetchCache.delete(href)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -117,19 +117,6 @@ export interface ServerPatchAction {
|
|||
mutable: Mutable
|
||||
}
|
||||
|
||||
/**
|
||||
* PrefetchKind defines the type of prefetching that should be done.
|
||||
* - `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully.
|
||||
* - `full` - prefetch the page data fully.
|
||||
* - `temporary` - a temporary prefetch entry is added to the cache, this is used when prefetch={false} is used in next/link or when you push a route programmatically.
|
||||
*/
|
||||
|
||||
export enum PrefetchKind {
|
||||
AUTO = 'auto',
|
||||
FULL = 'full',
|
||||
TEMPORARY = 'temporary',
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch adds the provided FlightData to the prefetch cache
|
||||
* - Creates the router state tree based on the patch in FlightData
|
||||
|
@ -139,7 +126,6 @@ export enum PrefetchKind {
|
|||
export interface PrefetchAction {
|
||||
type: typeof ACTION_PREFETCH
|
||||
url: URL
|
||||
kind: PrefetchKind
|
||||
}
|
||||
|
||||
interface PushRef {
|
||||
|
@ -168,14 +154,6 @@ export type FocusAndScrollRef = {
|
|||
segmentPaths: FlightSegmentPath[]
|
||||
}
|
||||
|
||||
export type PrefetchCacheEntry = {
|
||||
treeAtTimeOfPrefetch: FlightRouterState
|
||||
data: ReturnType<typeof fetchServerResponse> | null
|
||||
kind: PrefetchKind
|
||||
prefetchTime: number
|
||||
lastUsedTime: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keeping the state of app-router.
|
||||
*/
|
||||
|
@ -195,7 +173,13 @@ export type AppRouterState = {
|
|||
/**
|
||||
* Cache that holds prefetched Flight responses keyed by url.
|
||||
*/
|
||||
prefetchCache: Map<string, PrefetchCacheEntry>
|
||||
prefetchCache: Map<
|
||||
string,
|
||||
{
|
||||
treeAtTimeOfPrefetch: FlightRouterState
|
||||
data: ReturnType<typeof fetchServerResponse> | null
|
||||
}
|
||||
>
|
||||
/**
|
||||
* Decides if the update should create a new history entry and if the navigation has to trigger a browser navigation.
|
||||
*/
|
||||
|
|
|
@ -16,12 +16,10 @@ import { RouterContext } from '../shared/lib/router-context'
|
|||
import {
|
||||
AppRouterContext,
|
||||
AppRouterInstance,
|
||||
PrefetchOptions as AppRouterPrefetchOptions,
|
||||
} from '../shared/lib/app-router-context'
|
||||
import { useIntersection } from './use-intersection'
|
||||
import { getDomainLocale } from './get-domain-locale'
|
||||
import { addBasePath } from './add-base-path'
|
||||
import { PrefetchKind } from './components/router-reducer/router-reducer-types'
|
||||
|
||||
type Url = string | UrlObject
|
||||
type RequiredKeys<T> = {
|
||||
|
@ -122,7 +120,6 @@ function prefetch(
|
|||
href: string,
|
||||
as: string,
|
||||
options: PrefetchOptions,
|
||||
appOptions: AppRouterPrefetchOptions,
|
||||
isAppRouter: boolean
|
||||
): void {
|
||||
if (typeof window === 'undefined') {
|
||||
|
@ -157,15 +154,11 @@ function prefetch(
|
|||
prefetched.add(prefetchedKey)
|
||||
}
|
||||
|
||||
const prefetchPromise = isAppRouter
|
||||
? (router as AppRouterInstance).prefetch(href, appOptions)
|
||||
: (router as NextRouter).prefetch(href, as, options)
|
||||
|
||||
// Prefetch the JSON page if asked (only in the client)
|
||||
// We need to handle a prefetch error here since we may be
|
||||
// loading with priority which can reject but we don't
|
||||
// want to force navigation since this is only a prefetch
|
||||
Promise.resolve(prefetchPromise).catch((err) => {
|
||||
Promise.resolve(router.prefetch(href, as, options)).catch((err) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// rethrow to show invalid URL errors
|
||||
throw err
|
||||
|
@ -255,51 +248,6 @@ function formatStringOrUrl(urlObjOrString: UrlObject | string): string {
|
|||
*/
|
||||
const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
||||
function LinkComponent(props, forwardedRef) {
|
||||
let children: React.ReactNode
|
||||
|
||||
const {
|
||||
href: hrefProp,
|
||||
as: asProp,
|
||||
children: childrenProp,
|
||||
prefetch: prefetchProp = null,
|
||||
passHref,
|
||||
replace,
|
||||
shallow,
|
||||
scroll,
|
||||
locale,
|
||||
onClick,
|
||||
onMouseEnter: onMouseEnterProp,
|
||||
onTouchStart: onTouchStartProp,
|
||||
// @ts-expect-error this is inlined as a literal boolean not a string
|
||||
legacyBehavior = process.env.__NEXT_NEW_LINK_BEHAVIOR === false,
|
||||
...restProps
|
||||
} = props
|
||||
|
||||
children = childrenProp
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
(typeof children === 'string' || typeof children === 'number')
|
||||
) {
|
||||
children = <a>{children}</a>
|
||||
}
|
||||
|
||||
const prefetchEnabled = prefetchProp !== false
|
||||
/**
|
||||
* The possible states for prefetch are:
|
||||
* - null: this is the default "auto" mode, where we will prefetch partially if the link is in the viewport
|
||||
* - true: we will prefetch if the link is visible and prefetch the full page, not just partially
|
||||
* - false: we will not prefetch if in the viewport at all
|
||||
*/
|
||||
const appPrefetchKind =
|
||||
prefetchProp === null ? PrefetchKind.AUTO : PrefetchKind.FULL
|
||||
|
||||
const pagesRouter = React.useContext(RouterContext)
|
||||
const appRouter = React.useContext(AppRouterContext)
|
||||
const router = pagesRouter ?? appRouter
|
||||
|
||||
// We're in the app directory if there is no pages router.
|
||||
const isAppRouter = !pagesRouter
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
function createPropError(args: {
|
||||
key: string
|
||||
|
@ -413,7 +361,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
// This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const hasWarned = React.useRef(false)
|
||||
if (props.prefetch && !hasWarned.current && !isAppRouter) {
|
||||
if (props.prefetch && !hasWarned.current) {
|
||||
hasWarned.current = true
|
||||
console.warn(
|
||||
'Next.js auto-prefetches automatically based on viewport. The prefetch attribute is no longer needed. More: https://nextjs.org/docs/messages/prefetch-true-deprecated'
|
||||
|
@ -421,6 +369,44 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
}
|
||||
}
|
||||
|
||||
let children: React.ReactNode
|
||||
|
||||
const {
|
||||
href: hrefProp,
|
||||
as: asProp,
|
||||
children: childrenProp,
|
||||
prefetch: prefetchProp,
|
||||
passHref,
|
||||
replace,
|
||||
shallow,
|
||||
scroll,
|
||||
locale,
|
||||
onClick,
|
||||
onMouseEnter: onMouseEnterProp,
|
||||
onTouchStart: onTouchStartProp,
|
||||
// @ts-expect-error this is inlined as a literal boolean not a string
|
||||
legacyBehavior = process.env.__NEXT_NEW_LINK_BEHAVIOR === false,
|
||||
...restProps
|
||||
} = props
|
||||
|
||||
children = childrenProp
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
(typeof children === 'string' || typeof children === 'number')
|
||||
) {
|
||||
children = <a>{children}</a>
|
||||
}
|
||||
|
||||
const prefetchEnabled = prefetchProp !== false
|
||||
|
||||
const pagesRouter = React.useContext(RouterContext)
|
||||
const appRouter = React.useContext(AppRouterContext)
|
||||
const router = pagesRouter ?? appRouter
|
||||
|
||||
// We're in the app directory if there is no pages router.
|
||||
const isAppRouter = !pagesRouter
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (isAppRouter && !asProp) {
|
||||
let href: string | undefined
|
||||
|
@ -560,16 +546,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
}
|
||||
|
||||
// Prefetch the URL.
|
||||
prefetch(
|
||||
router,
|
||||
href,
|
||||
as,
|
||||
{ locale },
|
||||
{
|
||||
kind: appPrefetchKind,
|
||||
},
|
||||
isAppRouter
|
||||
)
|
||||
prefetch(router, href, as, { locale }, isAppRouter)
|
||||
}, [
|
||||
as,
|
||||
href,
|
||||
|
@ -579,7 +556,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
pagesRouter?.locale,
|
||||
router,
|
||||
isAppRouter,
|
||||
appPrefetchKind,
|
||||
])
|
||||
|
||||
const childProps: {
|
||||
|
@ -663,9 +639,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
|
||||
bypassPrefetchedCheck: true,
|
||||
},
|
||||
{
|
||||
kind: appPrefetchKind,
|
||||
},
|
||||
isAppRouter
|
||||
)
|
||||
},
|
||||
|
@ -700,9 +673,6 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
|||
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
|
||||
bypassPrefetchedCheck: true,
|
||||
},
|
||||
{
|
||||
kind: appPrefetchKind,
|
||||
},
|
||||
isAppRouter
|
||||
)
|
||||
},
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
FocusAndScrollRef,
|
||||
PrefetchKind,
|
||||
} from '../../client/components/router-reducer/router-reducer-types'
|
||||
import { FocusAndScrollRef } from '../../client/components/router-reducer/router-reducer-types'
|
||||
import type { fetchServerResponse } from '../../client/components/router-reducer/fetch-server-response'
|
||||
import type {
|
||||
FlightRouterState,
|
||||
|
@ -71,10 +68,6 @@ export interface NavigateOptions {
|
|||
forceOptimisticNavigation?: boolean
|
||||
}
|
||||
|
||||
export interface PrefetchOptions {
|
||||
kind: PrefetchKind
|
||||
}
|
||||
|
||||
export interface AppRouterInstance {
|
||||
/**
|
||||
* Navigate to the previous history entry.
|
||||
|
@ -101,7 +94,7 @@ export interface AppRouterInstance {
|
|||
/**
|
||||
* Prefetch the provided href.
|
||||
*/
|
||||
prefetch(href: string, options?: PrefetchOptions): void
|
||||
prefetch(href: string): void
|
||||
}
|
||||
|
||||
export const AppRouterContext = React.createContext<AppRouterInstance | null>(
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
export default async function Page() {
|
||||
const randomNumber = Math.random()
|
||||
return (
|
||||
<div>
|
||||
<div>LOADING</div>
|
||||
<div id="loading">{randomNumber}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default async function Page({ searchParams: { timeout } }) {
|
||||
const randomNumber = await new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
resolve(Math.random())
|
||||
},
|
||||
timeout !== undefined ? Number.parseInt(timeout, 10) : 0
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Link href="/"> Back to Home </Link>
|
||||
</div>
|
||||
<div id="random-number">{randomNumber}</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export default function Root({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<head></head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import Link from 'next/link'
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Link href="/0?timeout=0" prefetch={true}>
|
||||
To Random Number - prefetch: true
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/1">To Random Number - prefetch: auto</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/2" prefetch={false}>
|
||||
To Random Number 2 - prefetch: false
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/1?timeout=1000">
|
||||
To Random Number - prefetch: auto, slow
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,386 +0,0 @@
|
|||
import { createNextDescribe } from 'e2e-utils'
|
||||
import { check } from 'next-test-utils'
|
||||
import { BrowserInterface } from 'test/lib/browsers/base'
|
||||
import { Request } from 'playwright-chromium'
|
||||
|
||||
const getPathname = (url: string) => {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.pathname
|
||||
}
|
||||
|
||||
const browserConfigWithFixedTime = {
|
||||
beforePageLoad: (page) => {
|
||||
page.addInitScript(() => {
|
||||
const startTime = new Date()
|
||||
const fixedTime = new Date('2023-04-17T00:00:00Z')
|
||||
|
||||
// Override the Date constructor
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-native-reassign
|
||||
Date = class extends Date {
|
||||
constructor() {
|
||||
super()
|
||||
// @ts-ignore
|
||||
return new startTime.constructor(fixedTime)
|
||||
}
|
||||
|
||||
static now() {
|
||||
return fixedTime.getTime()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const fastForwardTo = (ms) => {
|
||||
// Increment the fixed time by the specified duration
|
||||
const currentTime = new Date()
|
||||
currentTime.setTime(currentTime.getTime() + ms)
|
||||
|
||||
// Update the Date constructor to use the new fixed time
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-native-reassign
|
||||
Date = class extends Date {
|
||||
constructor() {
|
||||
super()
|
||||
// @ts-ignore
|
||||
return new currentTime.constructor(currentTime)
|
||||
}
|
||||
|
||||
static now() {
|
||||
return currentTime.getTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createRequestsListener = async (browser: BrowserInterface) => {
|
||||
// wait for network idle
|
||||
await browser.waitForIdleNetwork()
|
||||
|
||||
let requests = []
|
||||
|
||||
browser.on('request', (req: Request) => {
|
||||
requests.push([req.url(), !!req.headers()['next-router-prefetch']])
|
||||
})
|
||||
|
||||
await browser.refresh()
|
||||
|
||||
return {
|
||||
getRequests: () => requests,
|
||||
clearRequests: () => {
|
||||
requests = []
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
createNextDescribe(
|
||||
'app dir client cache semantics',
|
||||
{
|
||||
files: __dirname,
|
||||
},
|
||||
({ next, isNextDev }) => {
|
||||
if (isNextDev) {
|
||||
// since the router behavior is different in dev mode (no viewport prefetching + liberal revalidation)
|
||||
// we only check the production behavior
|
||||
it('should skip dev', () => {})
|
||||
} else {
|
||||
describe('prefetch={true}', () => {
|
||||
let browser: BrowserInterface
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = (await next.browser(
|
||||
'/',
|
||||
browserConfigWithFixedTime
|
||||
)) as BrowserInterface
|
||||
})
|
||||
|
||||
it('should prefetch the full page', async () => {
|
||||
const { getRequests, clearRequests } = await createRequestsListener(
|
||||
browser
|
||||
)
|
||||
await check(() => {
|
||||
return getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/0' && !didPartialPrefetch
|
||||
)
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
clearRequests()
|
||||
|
||||
await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
|
||||
expect(
|
||||
getRequests().every(([url]) => getPathname(url) !== '/0')
|
||||
).toEqual(true)
|
||||
})
|
||||
it('should re-use the cache for the full page, only for 5 mins', async () => {
|
||||
const randomNumber = await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const number = await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(number).toBe(randomNumber)
|
||||
|
||||
await browser.eval(fastForwardTo, 5 * 60 * 1000)
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const newNumber = await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(newNumber).not.toBe(randomNumber)
|
||||
})
|
||||
it('should prefetch again after 5 mins if the link is visible again', async () => {
|
||||
const { getRequests, clearRequests } = await createRequestsListener(
|
||||
browser
|
||||
)
|
||||
|
||||
await check(() => {
|
||||
return getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/0' && !didPartialPrefetch
|
||||
)
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
const randomNumber = await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.eval(fastForwardTo, 5 * 60 * 1000)
|
||||
clearRequests()
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
await check(() => {
|
||||
return getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/0' && !didPartialPrefetch
|
||||
)
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
const number = await browser
|
||||
.elementByCss('[href="/0?timeout=0"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(number).not.toBe(randomNumber)
|
||||
})
|
||||
})
|
||||
describe('prefetch={false}', () => {
|
||||
let browser: BrowserInterface
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = (await next.browser(
|
||||
'/',
|
||||
browserConfigWithFixedTime
|
||||
)) as BrowserInterface
|
||||
})
|
||||
it('should not prefetch the page at all', async () => {
|
||||
const { getRequests } = await createRequestsListener(browser)
|
||||
|
||||
await browser
|
||||
.elementByCss('[href="/2"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
|
||||
expect(
|
||||
getRequests().filter(([url]) => getPathname(url) === '/2')
|
||||
).toHaveLength(1)
|
||||
|
||||
expect(
|
||||
getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/2' && didPartialPrefetch
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
it('should re-use the cache only for 30 seconds', async () => {
|
||||
const randomNumber = await browser
|
||||
.elementByCss('[href="/2"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const number = await browser
|
||||
.elementByCss('[href="/2"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(number).toBe(randomNumber)
|
||||
|
||||
await browser.eval(fastForwardTo, 30 * 1000)
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const newNumber = await browser
|
||||
.elementByCss('[href="/2"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(newNumber).not.toBe(randomNumber)
|
||||
})
|
||||
})
|
||||
describe('prefetch={undefined} - default', () => {
|
||||
let browser: BrowserInterface
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = (await next.browser(
|
||||
'/',
|
||||
browserConfigWithFixedTime
|
||||
)) as BrowserInterface
|
||||
})
|
||||
|
||||
it('should prefetch partially a dynamic page', async () => {
|
||||
const { getRequests, clearRequests } = await createRequestsListener(
|
||||
browser
|
||||
)
|
||||
|
||||
await check(() => {
|
||||
return getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/1' && didPartialPrefetch
|
||||
)
|
||||
? 'success'
|
||||
: 'fail'
|
||||
}, 'success')
|
||||
|
||||
clearRequests()
|
||||
|
||||
await browser
|
||||
.elementByCss('[href="/1"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
|
||||
expect(
|
||||
getRequests().some(
|
||||
([url, didPartialPrefetch]) =>
|
||||
getPathname(url) === '/1' && !didPartialPrefetch
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
it('should re-use the full cache for only 30 seconds', async () => {
|
||||
const randomNumber = await browser
|
||||
.elementByCss('[href="/1"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const number = await browser
|
||||
.elementByCss('[href="/1"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(number).toBe(randomNumber)
|
||||
|
||||
await browser.eval(fastForwardTo, 30 * 1000)
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
const newNumber = await browser
|
||||
.elementByCss('[href="/1"]')
|
||||
.click()
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(newNumber).not.toBe(randomNumber)
|
||||
})
|
||||
it('should refetch below the fold after 30 seconds', async () => {
|
||||
const randomLoadingNumber = await browser
|
||||
.elementByCss('[href="/1?timeout=1000"]')
|
||||
.click()
|
||||
.waitForElementByCss('#loading')
|
||||
.text()
|
||||
|
||||
const randomNumber = await browser
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.elementByCss('[href="/"]').click()
|
||||
|
||||
await browser.eval(fastForwardTo, 30 * 1000)
|
||||
|
||||
const newLoadingNumber = await browser
|
||||
.elementByCss('[href="/1?timeout=1000"]')
|
||||
.click()
|
||||
.waitForElementByCss('#loading')
|
||||
.text()
|
||||
|
||||
const newNumber = await browser
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(newLoadingNumber).toBe(randomLoadingNumber)
|
||||
|
||||
expect(newNumber).not.toBe(randomNumber)
|
||||
})
|
||||
it('should refetch the full page after 5 mins', async () => {
|
||||
const randomLoadingNumber = await browser
|
||||
.elementByCss('[href="/1?timeout=1000"]')
|
||||
.click()
|
||||
.waitForElementByCss('#loading')
|
||||
.text()
|
||||
|
||||
const randomNumber = await browser
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
await browser.eval(fastForwardTo, 5 * 60 * 1000)
|
||||
|
||||
await browser
|
||||
.elementByCss('[href="/"]')
|
||||
.click()
|
||||
.waitForElementByCss('[href="/1?timeout=1000"]')
|
||||
|
||||
const newLoadingNumber = await browser
|
||||
.elementByCss('[href="/1?timeout=1000"]')
|
||||
.click()
|
||||
.waitForElementByCss('#loading')
|
||||
.text()
|
||||
|
||||
const newNumber = await browser
|
||||
.waitForElementByCss('#random-number')
|
||||
.text()
|
||||
|
||||
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
|
||||
|
||||
expect(newNumber).not.toBe(randomNumber)
|
||||
})
|
||||
})
|
||||
describe('router.push', () => {
|
||||
it('should re-use the cache for 30 seconds', async () => {})
|
||||
it('should fully refetch the page after 30 seconds', async () => {})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
}
|
|
@ -1,30 +1,6 @@
|
|||
import { createNextDescribe } from 'e2e-utils'
|
||||
import { check, waitFor } from 'next-test-utils'
|
||||
|
||||
const browserConfigWithFixedTime = {
|
||||
beforePageLoad: (page) => {
|
||||
page.addInitScript(() => {
|
||||
const startTime = new Date()
|
||||
const fixedTime = new Date('2023-04-17T00:00:00Z')
|
||||
|
||||
// Override the Date constructor
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-native-reassign
|
||||
Date = class extends Date {
|
||||
constructor() {
|
||||
super()
|
||||
// @ts-ignore
|
||||
return new startTime.constructor(fixedTime)
|
||||
}
|
||||
|
||||
static now() {
|
||||
return fixedTime.getTime()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
createNextDescribe(
|
||||
'app dir prefetching',
|
||||
{
|
||||
|
@ -39,7 +15,7 @@ createNextDescribe(
|
|||
}
|
||||
|
||||
it('should show layout eagerly when prefetched with loading one level down', async () => {
|
||||
const browser = await next.browser('/', browserConfigWithFixedTime)
|
||||
const browser = await next.browser('/')
|
||||
// Ensure the page is prefetched
|
||||
await waitFor(1000)
|
||||
|
||||
|
@ -74,7 +50,7 @@ createNextDescribe(
|
|||
})
|
||||
|
||||
it('should not fetch again when a static page was prefetched', async () => {
|
||||
const browser = await next.browser('/404', browserConfigWithFixedTime)
|
||||
const browser = await next.browser('/404')
|
||||
let requests: string[] = []
|
||||
|
||||
browser.on('request', (req) => {
|
||||
|
@ -102,7 +78,7 @@ createNextDescribe(
|
|||
})
|
||||
|
||||
it('should not fetch again when a static page was prefetched when navigating to it twice', async () => {
|
||||
const browser = await next.browser('/404', browserConfigWithFixedTime)
|
||||
const browser = await next.browser('/404')
|
||||
let requests: string[] = []
|
||||
|
||||
browser.on('request', (req) => {
|
||||
|
|
|
@ -522,10 +522,17 @@ createNextDescribe(
|
|||
await browser.waitForElementByCss('#render-id-456')
|
||||
expect(await browser.eval('window.history.length')).toBe(3)
|
||||
|
||||
// Get the id on the rendered page.
|
||||
const firstID = await browser.elementById('render-id-456').text()
|
||||
|
||||
// Go back, and redo the navigation by clicking the link.
|
||||
await browser.back()
|
||||
await browser.elementById('link').click()
|
||||
await browser.waitForElementByCss('#render-id-456')
|
||||
|
||||
// Get the id again, and compare, they should not be the same.
|
||||
const secondID = await browser.elementById('render-id-456').text()
|
||||
expect(secondID).not.toBe(firstID)
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
@ -542,6 +549,9 @@ createNextDescribe(
|
|||
await browser.waitForElementByCss('#render-id-456')
|
||||
expect(await browser.eval('window.history.length')).toBe(2)
|
||||
|
||||
// Get the date again, and compare, they should not be the same.
|
||||
const firstId = await browser.elementById('render-id-456').text()
|
||||
|
||||
// Navigate to the subpage, verify that the history entry was NOT added.
|
||||
await browser.elementById('link').click()
|
||||
await browser.waitForElementByCss('#render-id-123')
|
||||
|
@ -551,6 +561,10 @@ createNextDescribe(
|
|||
await browser.elementById('link').click()
|
||||
await browser.waitForElementByCss('#render-id-456')
|
||||
expect(await browser.eval('window.history.length')).toBe(2)
|
||||
|
||||
// Get the date again, and compare, they should not be the same.
|
||||
const secondId = await browser.elementById('render-id-456').text()
|
||||
expect(firstId).not.toBe(secondId)
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
|
|
@ -122,13 +122,10 @@ export class BrowserInterface implements PromiseLike<any> {
|
|||
async getAttribute<T = any>(name: string): Promise<T> {
|
||||
return
|
||||
}
|
||||
async eval<T = any>(snippet: string | Function, ...args: any[]): Promise<T> {
|
||||
async eval<T = any>(snippet: string | Function): Promise<T> {
|
||||
return
|
||||
}
|
||||
async evalAsync<T = any>(
|
||||
snippet: string | Function,
|
||||
...args: any[]
|
||||
): Promise<T> {
|
||||
async evalAsync<T = any>(snippet: string | Function): Promise<T> {
|
||||
return
|
||||
}
|
||||
async text(): Promise<string> {
|
||||
|
@ -151,6 +148,4 @@ export class BrowserInterface implements PromiseLike<any> {
|
|||
async url(): Promise<string> {
|
||||
return ''
|
||||
}
|
||||
|
||||
async waitForIdleNetwork(): Promise<void> {}
|
||||
}
|
||||
|
|
|
@ -350,10 +350,10 @@ export class Playwright extends BrowserInterface {
|
|||
})
|
||||
}
|
||||
|
||||
eval<T = any>(fn: any, ...args: any[]): Promise<T> {
|
||||
eval<T = any>(snippet): Promise<T> {
|
||||
return this.chainWithReturnValue(() =>
|
||||
page
|
||||
.evaluate(fn, ...args)
|
||||
.evaluate(snippet)
|
||||
.catch((err) => {
|
||||
console.error('eval error:', err)
|
||||
return null
|
||||
|
@ -365,15 +365,15 @@ export class Playwright extends BrowserInterface {
|
|||
)
|
||||
}
|
||||
|
||||
async evalAsync<T = any>(fn: any, ...args: any[]) {
|
||||
if (typeof fn === 'function') {
|
||||
fn = fn.toString()
|
||||
async evalAsync<T = any>(snippet) {
|
||||
if (typeof snippet === 'function') {
|
||||
snippet = snippet.toString()
|
||||
}
|
||||
|
||||
if (fn.includes(`var callback = arguments[arguments.length - 1]`)) {
|
||||
fn = `(function() {
|
||||
if (snippet.includes(`var callback = arguments[arguments.length - 1]`)) {
|
||||
snippet = `(function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const origFunc = ${fn}
|
||||
const origFunc = ${snippet}
|
||||
try {
|
||||
origFunc(resolve)
|
||||
} catch (err) {
|
||||
|
@ -383,7 +383,7 @@ export class Playwright extends BrowserInterface {
|
|||
})()`
|
||||
}
|
||||
|
||||
return page.evaluate<T>(fn).catch(() => null)
|
||||
return page.evaluate<T>(snippet).catch(() => null)
|
||||
}
|
||||
|
||||
async log() {
|
||||
|
@ -397,10 +397,4 @@ export class Playwright extends BrowserInterface {
|
|||
async url() {
|
||||
return this.chain(() => page.evaluate('window.location.href')) as any
|
||||
}
|
||||
|
||||
async waitForIdleNetwork(): Promise<void> {
|
||||
return this.chain(() => {
|
||||
return page.waitForLoadState('networkidle')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue