Update app dir cache heuristics (#46287)
x-ref: https://github.com/vercel/next.js/pull/46271 x-ref: https://github.com/vercel/next.js/pull/46081
This commit is contained in:
parent
1149cccd94
commit
6d25125ff1
17 changed files with 275 additions and 49 deletions
|
@ -1880,7 +1880,13 @@ export default async function build(
|
|||
'cache/next-server.js.nft.json'
|
||||
)
|
||||
|
||||
if (lockFiles.length > 0) {
|
||||
if (
|
||||
lockFiles.length > 0 &&
|
||||
// we can't leverage trace cache if this is configured
|
||||
// currently unless we break this to a separate trace
|
||||
// file
|
||||
!config.experimental.incrementalCacheHandlerPath
|
||||
) {
|
||||
const cacheHash = (
|
||||
require('crypto') as typeof import('crypto')
|
||||
).createHash('sha256')
|
||||
|
|
|
@ -652,7 +652,7 @@ export default async function exportApp(
|
|||
enableUndici: nextConfig.experimental.enableUndici,
|
||||
debugOutput: options.debugOutput,
|
||||
isrMemoryCacheSize: nextConfig.experimental.isrMemoryCacheSize,
|
||||
fetchCache: nextConfig.experimental.fetchCache,
|
||||
fetchCache: nextConfig.experimental.appDir,
|
||||
})
|
||||
|
||||
for (const validation of result.ampValidations || []) {
|
||||
|
|
|
@ -78,7 +78,6 @@ import {
|
|||
RSC,
|
||||
RSC_VARY_HEADER,
|
||||
FLIGHT_PARAMETERS,
|
||||
FETCH_CACHE_HEADER,
|
||||
} from '../client/components/app-router-headers'
|
||||
import {
|
||||
MatchOptions,
|
||||
|
@ -1432,9 +1431,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
requestHeaders: Object.assign({}, req.headers),
|
||||
})
|
||||
|
||||
if (this.nextConfig.experimental.fetchCache) {
|
||||
delete req.headers[FETCH_CACHE_HEADER]
|
||||
}
|
||||
let isRevalidate = false
|
||||
|
||||
const doRender: () => Promise<ResponseCacheEntry | null> = async () => {
|
||||
|
@ -1510,7 +1506,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
const renderOpts: RenderOpts = {
|
||||
...components,
|
||||
...opts,
|
||||
...(isAppPath && this.nextConfig.experimental.fetchCache
|
||||
...(isAppPath && this.nextConfig.experimental.appDir
|
||||
? {
|
||||
incrementalCache,
|
||||
isRevalidate: this.minimalMode || isRevalidate,
|
||||
|
|
|
@ -287,8 +287,8 @@ const configSchema = {
|
|||
fallbackNodePolyfills: {
|
||||
type: 'boolean',
|
||||
},
|
||||
fetchCache: {
|
||||
type: 'boolean',
|
||||
fetchCacheKeyPrefix: {
|
||||
type: 'string',
|
||||
},
|
||||
forceSwcTransforms: {
|
||||
type: 'boolean',
|
||||
|
|
|
@ -117,7 +117,7 @@ export interface ExperimentalConfig {
|
|||
externalMiddlewareRewritesResolve?: boolean
|
||||
extensionAlias?: Record<string, any>
|
||||
allowedRevalidateHeaderKeys?: string[]
|
||||
fetchCache?: boolean
|
||||
fetchCacheKeyPrefix?: string
|
||||
optimisticClientCache?: boolean
|
||||
middlewarePrefetch?: 'strict' | 'flexible'
|
||||
preCompiledNextServer?: boolean
|
||||
|
@ -622,7 +622,7 @@ export const defaultConfig: NextConfig = {
|
|||
experimental: {
|
||||
clientRouterFilter: false,
|
||||
preCompiledNextServer: false,
|
||||
fetchCache: false,
|
||||
fetchCacheKeyPrefix: '',
|
||||
middlewarePrefetch: 'flexible',
|
||||
optimisticClientCache: true,
|
||||
runtime: undefined,
|
||||
|
|
|
@ -43,6 +43,7 @@ export default class FetchCache implements CacheHandler {
|
|||
for (const k in newHeaders) {
|
||||
this.headers[k] = newHeaders[k]
|
||||
}
|
||||
delete ctx._requestHeaders[FETCH_CACHE_HEADER]
|
||||
}
|
||||
if (ctx._requestHeaders['x-vercel-sc-host']) {
|
||||
this.cacheEndpoint = `https://${ctx._requestHeaders['x-vercel-sc-host']}${
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface CacheHandlerContext {
|
|||
maxMemoryCacheSize?: number
|
||||
_appDir: boolean
|
||||
_requestHeaders: IncrementalCache['requestHeaders']
|
||||
fetchCacheKeyPrefix?: string
|
||||
}
|
||||
|
||||
export interface CacheHandlerValue {
|
||||
|
@ -67,6 +68,7 @@ export class IncrementalCache {
|
|||
maxMemoryCacheSize,
|
||||
getPrerenderManifest,
|
||||
incrementalCacheHandlerPath,
|
||||
fetchCacheKeyPrefix,
|
||||
}: {
|
||||
fs?: CacheFs
|
||||
dev: boolean
|
||||
|
@ -79,6 +81,7 @@ export class IncrementalCache {
|
|||
maxMemoryCacheSize?: number
|
||||
incrementalCacheHandlerPath?: string
|
||||
getPrerenderManifest: () => PrerenderManifest
|
||||
fetchCacheKeyPrefix?: string
|
||||
}) {
|
||||
let cacheHandlerMod: any
|
||||
|
||||
|
@ -108,11 +111,12 @@ export class IncrementalCache {
|
|||
this.cacheHandler = new (cacheHandlerMod as typeof CacheHandler)({
|
||||
dev,
|
||||
fs,
|
||||
flushToDisk: flushToDisk && !dev,
|
||||
flushToDisk,
|
||||
serverDistDir,
|
||||
maxMemoryCacheSize,
|
||||
_appDir: !!appDir,
|
||||
_requestHeaders: requestHeaders,
|
||||
fetchCacheKeyPrefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +152,7 @@ export class IncrementalCache {
|
|||
// x-ref: https://github.com/facebook/react/blob/2655c9354d8e1c54ba888444220f63e836925caa/packages/react/src/ReactFetch.js#L23
|
||||
async fetchCacheKey(url: string, init: RequestInit = {}): Promise<string> {
|
||||
const cacheString = JSON.stringify([
|
||||
this.fetchCacheKey || '',
|
||||
url,
|
||||
init.method,
|
||||
init.headers,
|
||||
|
|
|
@ -30,25 +30,53 @@ export function patchFetch({
|
|||
async (input: RequestInfo | URL, init: RequestInit | undefined) => {
|
||||
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
||||
|
||||
// If the staticGenerationStore is not available, we can't do any special
|
||||
// treatment of fetch, therefore fallback to the original fetch
|
||||
// implementation.
|
||||
// If the staticGenerationStore is not available, we can't do any
|
||||
// special treatment of fetch, therefore fallback to the original
|
||||
// fetch implementation.
|
||||
if (!staticGenerationStore) {
|
||||
return originFetch(input, init)
|
||||
}
|
||||
|
||||
let revalidate: number | undefined | boolean
|
||||
let revalidate: number | undefined | false = undefined
|
||||
let curRevalidate = init?.next?.revalidate
|
||||
|
||||
if (typeof init?.next?.revalidate === 'number') {
|
||||
revalidate = init.next.revalidate
|
||||
if (init?.cache === 'force-cache') {
|
||||
curRevalidate = false
|
||||
}
|
||||
if (['no-cache', 'no-store'].includes(init?.cache || '')) {
|
||||
curRevalidate = 0
|
||||
}
|
||||
if (typeof curRevalidate === 'number') {
|
||||
revalidate = curRevalidate
|
||||
}
|
||||
|
||||
if (init?.next?.revalidate === false) {
|
||||
if (curRevalidate === false) {
|
||||
revalidate = CACHE_ONE_YEAR
|
||||
}
|
||||
const initHeaders: Headers =
|
||||
typeof (init?.headers as Headers)?.get === 'function'
|
||||
? (init?.headers as Headers)
|
||||
: new Headers(init?.headers || {})
|
||||
|
||||
const hasUnCacheableHeader =
|
||||
initHeaders.get('authorization') || initHeaders.get('cookie')
|
||||
|
||||
if (typeof revalidate === 'undefined') {
|
||||
// if there are uncacheable headers and the cache value
|
||||
// wasn't overridden then we must bail static generation
|
||||
if (hasUnCacheableHeader) {
|
||||
revalidate = 0
|
||||
} else {
|
||||
revalidate =
|
||||
typeof staticGenerationStore.revalidate === 'boolean' ||
|
||||
typeof staticGenerationStore.revalidate === 'undefined'
|
||||
? CACHE_ONE_YEAR
|
||||
: staticGenerationStore.revalidate
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!staticGenerationStore.revalidate ||
|
||||
typeof staticGenerationStore.revalidate === 'undefined' ||
|
||||
(typeof revalidate === 'number' &&
|
||||
revalidate < staticGenerationStore.revalidate)
|
||||
) {
|
||||
|
|
|
@ -287,7 +287,8 @@ export default class NextNodeServer extends BaseServer {
|
|||
appDir: this.hasAppDir,
|
||||
minimalMode: this.minimalMode,
|
||||
serverDistDir: this.serverDistDir,
|
||||
fetchCache: this.nextConfig.experimental.fetchCache,
|
||||
fetchCache: this.nextConfig.experimental.appDir,
|
||||
fetchCacheKeyPrefix: this.nextConfig.experimental.fetchCacheKeyPrefix,
|
||||
maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize,
|
||||
flushToDisk:
|
||||
!this.minimalMode && this.nextConfig.experimental.isrFlushToDisk,
|
||||
|
|
|
@ -67,7 +67,8 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
|
|||
requestHeaders,
|
||||
appDir: this.hasAppDir,
|
||||
minimalMode: this.minimalMode,
|
||||
fetchCache: this.nextConfig.experimental.fetchCache,
|
||||
fetchCache: this.nextConfig.experimental.appDir,
|
||||
fetchCacheKeyPrefix: this.nextConfig.experimental.fetchCacheKeyPrefix,
|
||||
maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize,
|
||||
flushToDisk: false,
|
||||
incrementalCacheHandlerPath:
|
||||
|
|
|
@ -1078,7 +1078,6 @@ export default class Router implements BaseRouter {
|
|||
// it should be hard navigated
|
||||
for (let i = 0; i < asNoSlashParts.length + 1; i++) {
|
||||
const currentPart = asNoSlashParts.slice(0, i).join('/')
|
||||
console.log('checking for', currentPart)
|
||||
if (this._bfl_d?.has(currentPart)) {
|
||||
matchesBflDynamic = true
|
||||
break
|
||||
|
|
|
@ -79,6 +79,14 @@ createNextDescribe(
|
|||
'static-to-dynamic-error/[id]/page.js',
|
||||
'variable-revalidate-edge/no-store/page.js',
|
||||
'variable-revalidate-edge/revalidate-3/page.js',
|
||||
'variable-revalidate/authorization-cached.html',
|
||||
'variable-revalidate/authorization-cached.rsc',
|
||||
'variable-revalidate/authorization-cached/page.js',
|
||||
'variable-revalidate/authorization/page.js',
|
||||
'variable-revalidate/cookie-cached.html',
|
||||
'variable-revalidate/cookie-cached.rsc',
|
||||
'variable-revalidate/cookie-cached/page.js',
|
||||
'variable-revalidate/cookie/page.js',
|
||||
'variable-revalidate/no-store/page.js',
|
||||
'variable-revalidate/revalidate-3.html',
|
||||
'variable-revalidate/revalidate-3.rsc',
|
||||
|
@ -184,6 +192,16 @@ createNextDescribe(
|
|||
initialRevalidateSeconds: false,
|
||||
srcRoute: '/ssg-preview/[[...route]]',
|
||||
},
|
||||
'/variable-revalidate/authorization-cached': {
|
||||
dataRoute: '/variable-revalidate/authorization-cached.rsc',
|
||||
initialRevalidateSeconds: 3,
|
||||
srcRoute: '/variable-revalidate/authorization-cached',
|
||||
},
|
||||
'/variable-revalidate/cookie-cached': {
|
||||
dataRoute: '/variable-revalidate/cookie-cached.rsc',
|
||||
initialRevalidateSeconds: 3,
|
||||
srcRoute: '/variable-revalidate/cookie-cached',
|
||||
},
|
||||
'/variable-revalidate/revalidate-3': {
|
||||
dataRoute: '/variable-revalidate/revalidate-3.rsc',
|
||||
initialRevalidateSeconds: 3,
|
||||
|
@ -329,20 +347,18 @@ createNextDescribe(
|
|||
}
|
||||
|
||||
it('should honor fetch cache correctly', async () => {
|
||||
await fetchViaHTTP(next.url, '/variable-revalidate/revalidate-3')
|
||||
await check(async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/revalidate-3'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/revalidate-3'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/revalidate-3'
|
||||
|
@ -353,24 +369,23 @@ createNextDescribe(
|
|||
|
||||
expect($2('#layout-data').text()).toBe(layoutData)
|
||||
expect($2('#page-data').text()).toBe(pageData)
|
||||
}
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should honor fetch cache correctly (edge)', async () => {
|
||||
await fetchViaHTTP(next.url, '/variable-revalidate-edge/revalidate-3')
|
||||
await check(async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate-edge/revalidate-3'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate-edge/revalidate-3'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate-edge/revalidate-3'
|
||||
|
@ -381,9 +396,106 @@ createNextDescribe(
|
|||
|
||||
expect($2('#layout-data').text()).toBe(layoutData)
|
||||
expect($2('#page-data').text()).toBe(pageData)
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should not cache correctly with authorization header', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/authorization'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/authorization'
|
||||
)
|
||||
expect(res2.status).toBe(200)
|
||||
const html2 = await res2.text()
|
||||
const $2 = cheerio.load(html2)
|
||||
|
||||
expect($2('#page-data').text()).not.toBe(pageData)
|
||||
}
|
||||
})
|
||||
|
||||
it('should cache correctly with authorization header and revalidate', async () => {
|
||||
await check(async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/authorization-cached'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/authorization-cached'
|
||||
)
|
||||
expect(res2.status).toBe(200)
|
||||
const html2 = await res2.text()
|
||||
const $2 = cheerio.load(html2)
|
||||
|
||||
expect($2('#layout-data').text()).toBe(layoutData)
|
||||
expect($2('#page-data').text()).toBe(pageData)
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('should not cache correctly with cookie header', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/variable-revalidate/cookie')
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res2 = await fetchViaHTTP(next.url, '/variable-revalidate/cookie')
|
||||
expect(res2.status).toBe(200)
|
||||
const html2 = await res2.text()
|
||||
const $2 = cheerio.load(html2)
|
||||
|
||||
expect($2('#page-data').text()).not.toBe(pageData)
|
||||
}
|
||||
})
|
||||
|
||||
it('should cache correctly with cookie header and revalidate', async () => {
|
||||
await check(async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/cookie-cached'
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const layoutData = $('#layout-data').text()
|
||||
const pageData = $('#page-data').text()
|
||||
|
||||
const res2 = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/variable-revalidate/cookie-cached'
|
||||
)
|
||||
expect(res2.status).toBe(200)
|
||||
const html2 = await res2.text()
|
||||
const $2 = cheerio.load(html2)
|
||||
|
||||
expect($2('#layout-data').text()).toBe(layoutData)
|
||||
expect($2('#page-data').text()).toBe(pageData)
|
||||
return 'success'
|
||||
}, 'success')
|
||||
})
|
||||
|
||||
it('Should not throw Dynamic Server Usage error when using generateStaticParams with previewData', async () => {
|
||||
const browserOnIndexPage = await next.browser('/ssg-preview')
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/variable-revalidate/authorization-cached</p>
|
||||
<p id="page-data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/variable-revalidate/authorization</p>
|
||||
<p id="page-data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
// revalidate 3 is overridden by cookie header here
|
||||
cookie: 'authorized=true',
|
||||
},
|
||||
next: {
|
||||
revalidate: 3,
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/variable-revalidate/cookie-cached</p>
|
||||
<p id="page-data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
export const revalidate = 3
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
// revalidate 3 is overridden by cookie header here
|
||||
cookie: 'authorized=true',
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/variable-revalidate/cookie</p>
|
||||
<p id="page-data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
fetchCache: true,
|
||||
},
|
||||
// assetPrefix: '/assets',
|
||||
rewrites: async () => {
|
||||
|
|
Loading…
Reference in a new issue