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:
JJ Kasper 2023-02-22 23:59:38 -08:00 committed by GitHub
parent 1149cccd94
commit 6d25125ff1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 275 additions and 49 deletions

View file

@ -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')

View file

@ -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 || []) {

View file

@ -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,

View file

@ -287,8 +287,8 @@ const configSchema = {
fallbackNodePolyfills: {
type: 'boolean',
},
fetchCache: {
type: 'boolean',
fetchCacheKeyPrefix: {
type: 'string',
},
forceSwcTransforms: {
type: 'boolean',

View file

@ -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,

View file

@ -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']}${

View file

@ -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,

View file

@ -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)
) {

View file

@ -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,

View file

@ -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:

View file

@ -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

View file

@ -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')

View file

@ -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>
</>
)
}

View file

@ -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>
</>
)
}

View file

@ -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>
</>
)
}

View file

@ -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>
</>
)
}

View file

@ -1,7 +1,6 @@
module.exports = {
experimental: {
appDir: true,
fetchCache: true,
},
// assetPrefix: '/assets',
rewrites: async () => {