Update app cache handler loading (#46290

Follow-up to https://github.com/vercel/next.js/pull/46287 this updates
how we load the cache handler for the incremental cache so it's
compatible with edge and also adds regression testing with a custom
handler.
This commit is contained in:
JJ Kasper 2023-02-23 01:19:59 -08:00 committed by GitHub
parent 2882eb4ebd
commit 5792533781
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 175 additions and 82 deletions

View file

@ -225,6 +225,8 @@ export function getEdgeServerEntry(opts: {
pagesType: opts.pagesType,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
incrementalCacheHandlerPath:
opts.config.experimental.incrementalCacheHandlerPath,
}
return {

View file

@ -913,6 +913,13 @@ export default async function build(
experimental: {
...config.experimental,
trustHostHeader: ciEnvironment.hasNextSupport,
incrementalCacheHandlerPath: config.experimental
.incrementalCacheHandlerPath
? path.relative(
distDir,
config.experimental.incrementalCacheHandlerPath
)
: undefined,
},
},
appDir: dir,

View file

@ -15,6 +15,7 @@ export type EdgeSSRLoaderQuery = {
appDirLoader?: string
pagesType: 'app' | 'pages' | 'root'
sriEnabled: boolean
incrementalCacheHandlerPath?: string
}
/*
@ -44,6 +45,7 @@ export default async function edgeSSRLoader(this: any) {
appDirLoader: appDirLoaderBase64,
pagesType,
sriEnabled,
incrementalCacheHandlerPath,
} = this.getOptions()
const appDirLoader = Buffer.from(
@ -117,6 +119,12 @@ export default async function edgeSSRLoader(this: any) {
const appRenderToHTML = null
`
}
const incrementalCacheHandler = ${
incrementalCacheHandlerPath
? `require("${incrementalCacheHandlerPath}")`
: 'null'
}
const buildManifest = self.__BUILD_MANIFEST
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
@ -146,6 +154,7 @@ export default async function edgeSSRLoader(this: any) {
config: ${stringifiedConfig},
buildId: ${JSON.stringify(buildId)},
fontLoaderManifest,
incrementalCacheHandler,
})
export const ComponentMod = pageMod

View file

@ -31,6 +31,7 @@ export function getRender({
config,
buildId,
fontLoaderManifest,
incrementalCacheHandler,
}: {
pagesType: 'app' | 'pages' | 'root'
dev: boolean
@ -51,6 +52,7 @@ export function getRender({
config: NextConfigComplete
buildId: string
fontLoaderManifest: FontLoaderManifest
incrementalCacheHandler?: any
}) {
const isAppPath = pagesType === 'app'
const baseLoadComponentResult = {
@ -80,6 +82,7 @@ export function getRender({
},
appRenderToHTML,
pagesRenderToHTML,
incrementalCacheHandler,
loadComponent: async (pathname) => {
if (isAppPath) return null

View file

@ -653,6 +653,8 @@ export default async function exportApp(
debugOutput: options.debugOutput,
isrMemoryCacheSize: nextConfig.experimental.isrMemoryCacheSize,
fetchCache: nextConfig.experimental.appDir,
incrementalCacheHandlerPath:
nextConfig.experimental.incrementalCacheHandlerPath,
})
for (const validation of result.ampValidations || []) {

View file

@ -84,6 +84,7 @@ interface ExportPageInput {
debugOutput?: boolean
isrMemoryCacheSize?: NextConfigComplete['experimental']['isrMemoryCacheSize']
fetchCache?: boolean
incrementalCacheHandlerPath?: string
}
interface ExportPageResults {
@ -141,6 +142,7 @@ export default async function exportPage({
debugOutput,
isrMemoryCacheSize,
fetchCache,
incrementalCacheHandlerPath,
}: ExportPageInput): Promise<ExportPageResults> {
setHttpClientAndAgentOptions({
httpAgentOptions,
@ -329,6 +331,13 @@ export default async function exportPage({
// only fully static paths are fully generated here
if (isAppDir) {
if (fetchCache) {
let CacheHandler: any
if (incrementalCacheHandlerPath) {
CacheHandler = require(incrementalCacheHandlerPath)
CacheHandler = CacheHandler.default || CacheHandler
}
curRenderOpts.incrementalCache = new IncrementalCache({
dev: false,
requestHeaders: {},
@ -353,6 +362,7 @@ export default async function exportPage({
stat: (f) => fs.promises.stat(f),
},
serverDistDir: join(distDir, 'server'),
CurCacheHandler: CacheHandler,
})
}

View file

@ -20,6 +20,8 @@ export function createClientRouterFilter(
let subPath = ''
const pathParts = path.split('/')
// start at 1 since we split on '/' and the path starts
// with this so the first entry is an empty string
for (let i = 1; i < pathParts.length + 1; i++) {
const curPart = pathParts[i]

View file

@ -67,8 +67,8 @@ export class IncrementalCache {
requestHeaders,
maxMemoryCacheSize,
getPrerenderManifest,
incrementalCacheHandlerPath,
fetchCacheKeyPrefix,
CurCacheHandler,
}: {
fs?: CacheFs
dev: boolean
@ -79,23 +79,18 @@ export class IncrementalCache {
flushToDisk?: boolean
requestHeaders: IncrementalCache['requestHeaders']
maxMemoryCacheSize?: number
incrementalCacheHandlerPath?: string
getPrerenderManifest: () => PrerenderManifest
fetchCacheKeyPrefix?: string
CurCacheHandler?: typeof CacheHandler
}) {
let cacheHandlerMod: any
if (!CurCacheHandler) {
if (fs && serverDistDir) {
CurCacheHandler = FileSystemCache
}
if (fs && serverDistDir) {
cacheHandlerMod = FileSystemCache
}
if (process.env.NEXT_RUNTIME !== 'edge' && incrementalCacheHandlerPath) {
cacheHandlerMod = require(incrementalCacheHandlerPath)
cacheHandlerMod = cacheHandlerMod.default || cacheHandlerMod
}
if (!incrementalCacheHandlerPath && minimalMode && fetchCache) {
cacheHandlerMod = FetchCache
if (minimalMode && fetchCache) {
CurCacheHandler = FetchCache
}
}
if (process.env.__NEXT_TEST_MAX_ISR_CACHE) {
@ -107,8 +102,8 @@ export class IncrementalCache {
this.requestHeaders = requestHeaders
this.prerenderManifest = getPrerenderManifest()
if (cacheHandlerMod) {
this.cacheHandler = new (cacheHandlerMod as typeof CacheHandler)({
if (CurCacheHandler) {
this.cacheHandler = new CurCacheHandler({
dev,
fs,
flushToDisk,

View file

@ -277,6 +277,15 @@ export default class NextNodeServer extends BaseServer {
requestHeaders: IncrementalCache['requestHeaders']
}) {
const dev = !!this.renderOpts.dev
let CacheHandler: any
const { incrementalCacheHandlerPath } = this.nextConfig.experimental
if (incrementalCacheHandlerPath) {
CacheHandler = require(this.minimalMode
? join(this.distDir, incrementalCacheHandlerPath)
: incrementalCacheHandlerPath)
CacheHandler = CacheHandler.default || CacheHandler
}
// incremental-cache is request specific with a shared
// although can have shared caches in module scope
// per-cache handler
@ -292,8 +301,6 @@ export default class NextNodeServer extends BaseServer {
maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize,
flushToDisk:
!this.minimalMode && this.nextConfig.experimental.isrFlushToDisk,
incrementalCacheHandlerPath:
this.nextConfig.experimental?.incrementalCacheHandlerPath,
getPrerenderManifest: () => {
if (dev) {
return {
@ -307,6 +314,7 @@ export default class NextNodeServer extends BaseServer {
return this.getPrerenderManifest()
}
},
CurCacheHandler: CacheHandler,
})
}

View file

@ -38,6 +38,7 @@ interface WebServerOptions extends Options {
Pick<BaseServer['renderOpts'], 'buildId'>
pagesRenderToHTML?: typeof import('./render').renderToHTML
appRenderToHTML?: typeof import('./app-render').renderToHTMLOrFlight
incrementalCacheHandler?: any
}
}
@ -71,8 +72,8 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
fetchCacheKeyPrefix: this.nextConfig.experimental.fetchCacheKeyPrefix,
maxMemoryCacheSize: this.nextConfig.experimental.isrMemoryCacheSize,
flushToDisk: false,
incrementalCacheHandlerPath:
this.nextConfig.experimental?.incrementalCacheHandlerPath,
CurCacheHandler:
this.serverOptions.webServerConfig.incrementalCacheHandler,
getPrerenderManifest: () => {
if (dev) {
return {

View file

@ -0,0 +1,2 @@
process.env.CUSTOM_CACHE_HANDLER = '1'
require('./app-static.test')

View file

@ -14,9 +14,10 @@ createNextDescribe(
files: __dirname,
env: {
NEXT_DEBUG_BUILD: '1',
CUSTOM_CACHE_HANDLER: process.env.CUSTOM_CACHE_HANDLER,
},
},
({ next, isNextDev: isDev, isNextStart }) => {
({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => {
if (isNextStart) {
it('should output HTML/RSC files for static paths', async () => {
const files = (
@ -383,19 +384,23 @@ createNextDescribe(
const html = await res.text()
const $ = cheerio.load(html)
const layoutData = $('#layout-data').text()
const pageData = $('#page-data').text()
// the test cache handler is simple and doesn't share
// state across workers so not guaranteed to have cache hit
if (!(isNextDeploy && process.env.CUSTOM_CACHE_HANDLER)) {
const layoutData = $('#layout-data').text()
const pageData = $('#page-data').text()
const res2 = await fetchViaHTTP(
next.url,
'/variable-revalidate-edge/revalidate-3'
)
expect(res2.status).toBe(200)
const html2 = await res2.text()
const $2 = cheerio.load(html2)
const res2 = await fetchViaHTTP(
next.url,
'/variable-revalidate-edge/revalidate-3'
)
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)
expect($2('#layout-data').text()).toBe(layoutData)
expect($2('#page-data').text()).toBe(pageData)
}
return 'success'
}, 'success')
})
@ -584,32 +589,36 @@ createNextDescribe(
}
})
it('should handle dynamicParams: false correctly', async () => {
const validParams = ['tim', 'seb', 'styfle']
// since we aren't leveraging fs cache with custom handler
// then these will 404 as they are cache misses
if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) {
it('should handle dynamicParams: false correctly', async () => {
const validParams = ['tim', 'seb', 'styfle']
for (const param of validParams) {
const res = await next.fetch(`/blog/${param}`, {
redirect: 'manual',
})
expect(res.status).toBe(200)
const html = await res.text()
const $ = cheerio.load(html)
for (const param of validParams) {
const res = await next.fetch(`/blog/${param}`, {
redirect: 'manual',
})
expect(res.status).toBe(200)
const html = await res.text()
const $ = cheerio.load(html)
expect(JSON.parse($('#params').text())).toEqual({
author: param,
})
expect($('#page').text()).toBe('/blog/[author]')
}
const invalidParams = ['timm', 'non-existent']
expect(JSON.parse($('#params').text())).toEqual({
author: param,
})
expect($('#page').text()).toBe('/blog/[author]')
}
const invalidParams = ['timm', 'non-existent']
for (const param of invalidParams) {
const invalidRes = await next.fetch(`/blog/${param}`, {
redirect: 'manual',
})
expect(invalidRes.status).toBe(404)
expect(await invalidRes.text()).toContain('page could not be found')
}
})
for (const param of invalidParams) {
const invalidRes = await next.fetch(`/blog/${param}`, {
redirect: 'manual',
})
expect(invalidRes.status).toBe(404)
expect(await invalidRes.text()).toContain('page could not be found')
}
})
}
it('should work with forced dynamic path', async () => {
for (const slug of ['first', 'second']) {
@ -664,40 +673,50 @@ createNextDescribe(
}
})
it('should navigate to static path correctly', async () => {
const browser = await next.browser('/blog/tim')
await browser.eval('window.beforeNav = 1')
// since we aren't leveraging fs cache with custom handler
// then these will 404 as they are cache misses
if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) {
it('should navigate to static path correctly', async () => {
const browser = await next.browser('/blog/tim')
await browser.eval('window.beforeNav = 1')
expect(
await browser.eval('document.documentElement.innerHTML')
).toContain('/blog/[author]')
await browser.elementByCss('#author-2').click()
expect(
await browser.eval('document.documentElement.innerHTML')
).toContain('/blog/[author]')
await browser.elementByCss('#author-2').click()
await check(async () => {
const params = JSON.parse(await browser.elementByCss('#params').text())
return params.author === 'seb' ? 'found' : params
}, 'found')
await check(async () => {
const params = JSON.parse(
await browser.elementByCss('#params').text()
)
return params.author === 'seb' ? 'found' : params
}, 'found')
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.elementByCss('#author-1-post-1').click()
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.elementByCss('#author-1-post-1').click()
await check(async () => {
const params = JSON.parse(await browser.elementByCss('#params').text())
return params.author === 'tim' && params.slug === 'first-post'
? 'found'
: params
}, 'found')
await check(async () => {
const params = JSON.parse(
await browser.elementByCss('#params').text()
)
return params.author === 'tim' && params.slug === 'first-post'
? 'found'
: params
}, 'found')
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.back()
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.back()
await check(async () => {
const params = JSON.parse(await browser.elementByCss('#params').text())
return params.author === 'seb' ? 'found' : params
}, 'found')
await check(async () => {
const params = JSON.parse(
await browser.elementByCss('#params').text()
)
return params.author === 'seb' ? 'found' : params
}, 'found')
expect(await browser.eval('window.beforeNav')).toBe(1)
})
expect(await browser.eval('window.beforeNav')).toBe(1)
})
}
it('should ssr dynamically when detected automatically with fetch cache option', async () => {
const pathname = '/ssr-auto/cache-no-store'
@ -959,5 +978,13 @@ createNextDescribe(
await waitFor(1000)
checkUrl()
})
if (process.env.CUSTOM_CACHE_HANDLER && !isNextDeploy) {
it('should have logs from cache-handler', () => {
expect(next.cliOutput).toContain('initialized custom cache-handler')
expect(next.cliOutput).toContain('cache-handler get')
expect(next.cliOutput).toContain('cache-handler set')
})
}
}
)

View file

@ -0,0 +1,22 @@
const cache = new Map()
module.exports = class CacheHandler {
constructor(options) {
this.options = options
this.cache = {}
console.log('initialized custom cache-handler')
}
async get(key) {
console.log('cache-handler get', key)
return cache.get(key)
}
async set(key, data) {
console.log('cache-handler set', key)
cache.set(key, {
value: data,
lastModified: Date.now(),
})
}
}

View file

@ -1,6 +1,9 @@
module.exports = {
experimental: {
appDir: true,
incrementalCacheHandlerPath: process.env.CUSTOM_CACHE_HANDLER
? require.resolve('./cache-handler.js')
: undefined,
},
// assetPrefix: '/assets',
rewrites: async () => {