Skip to content

Commit

Permalink
fix: update cache handler to accommodate changes in next@canary (#2572)
Browse files Browse the repository at this point in the history
* update react version needed by next@canary

* tmp: just checking if canary tests will be happier

* what's up with NODE_ENV?

* missing PAGES

* chore: use npm info to figure out react version needed for canary instead of hardcoding it

* chore: drop package lock file when preparing fixtures

* test: unset more things related to next's fetch patching

* use correct cache kind for initial cache seeding depending on next version

* small cleanup

* typehell

* just checking

* proper NODE_ENV setting with explanation

* any is bad

* Update src/shared/cache-types.cts

Co-authored-by: Philippe Serhal <[email protected]>

* add note about deleting next-patch symbol

---------

Co-authored-by: Philippe Serhal <[email protected]>
  • Loading branch information
pieh and serhalp authored Aug 29, 2024
1 parent 4a53512 commit 83f685e
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 60 deletions.
31 changes: 24 additions & 7 deletions src/build/content/prerendered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { satisfies } from 'semver'

import { encodeBlobKey } from '../../shared/blobkey.js'
import type {
CachedFetchValue,
CachedFetchValueForMultipleVersions,
NetlifyCachedAppPageValue,
NetlifyCachedPageValue,
NetlifyCachedRouteValue,
Expand Down Expand Up @@ -45,8 +45,11 @@ const writeCacheEntry = async (
*/
const routeToFilePath = (path: string) => (path === '/' ? '/index' : path)

const buildPagesCacheValue = async (path: string): Promise<NetlifyCachedPageValue> => ({
kind: 'PAGE',
const buildPagesCacheValue = async (
path: string,
shouldUseEnumKind: boolean,
): Promise<NetlifyCachedPageValue> => ({
kind: shouldUseEnumKind ? 'PAGES' : 'PAGE',
html: await readFile(`${path}.html`, 'utf-8'),
pageData: JSON.parse(await readFile(`${path}.json`, 'utf-8')),
headers: undefined,
Expand Down Expand Up @@ -96,14 +99,17 @@ const buildAppCacheValue = async (
const buildRouteCacheValue = async (
path: string,
initialRevalidateSeconds: number | false,
shouldUseEnumKind: boolean,
): Promise<NetlifyCachedRouteValue> => ({
kind: 'ROUTE',
kind: shouldUseEnumKind ? 'APP_ROUTE' : 'ROUTE',
body: await readFile(`${path}.body`, 'base64'),
...JSON.parse(await readFile(`${path}.meta`, 'utf-8')),
revalidate: initialRevalidateSeconds,
})

const buildFetchCacheValue = async (path: string): Promise<CachedFetchValue> => ({
const buildFetchCacheValue = async (
path: string,
): Promise<CachedFetchValueForMultipleVersions> => ({
kind: 'FETCH',
...JSON.parse(await readFile(path, 'utf-8')),
})
Expand Down Expand Up @@ -133,6 +139,13 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
})
: false

// https://github.com/vercel/next.js/pull/68602 changed the cache kind for Pages router pages from `PAGE` to `PAGES` and from `ROUTE` to `APP_ROUTE`.
const shouldUseEnumKind = ctx.nextVersion
? satisfies(ctx.nextVersion, '>=15.0.0-canary.114 <15.0.0-d || >15.0.0-rc.0', {
includePrerelease: true,
})
: false

await Promise.all(
Object.entries(manifest.routes).map(
([route, meta]): Promise<void> =>
Expand All @@ -152,7 +165,10 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
// if pages router returns 'notFound: true', build won't produce html and json files
return
}
value = await buildPagesCacheValue(join(ctx.publishDir, 'server/pages', key))
value = await buildPagesCacheValue(
join(ctx.publishDir, 'server/pages', key),
shouldUseEnumKind,
)
break
case meta.dataRoute?.endsWith('.rsc'):
value = await buildAppCacheValue(
Expand All @@ -164,14 +180,15 @@ export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void>
value = await buildRouteCacheValue(
join(ctx.publishDir, 'server/app', key),
meta.initialRevalidateSeconds,
shouldUseEnumKind,
)
break
default:
throw new Error(`Unrecognized content: ${route}`)
}

// Netlify Forms are not support and require a workaround
if (value.kind === 'PAGE' || value.kind === 'APP_PAGE') {
if (value.kind === 'PAGE' || value.kind === 'PAGES' || value.kind === 'APP_PAGE') {
verifyNetlifyForms(ctx, value.html)
}

Expand Down
67 changes: 43 additions & 24 deletions src/run/handlers/cache.cts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { type Span } from '@opentelemetry/api'
import type { PrerenderManifest } from 'next/dist/build/index.js'
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'

import type {
CacheHandler,
CacheHandlerContext,
IncrementalCache,
NetlifyCachedPageValue,
NetlifyCachedRouteValue,
NetlifyCacheHandlerValue,
NetlifyIncrementalCacheValue,
import {
type CacheHandlerContext,
type CacheHandlerForMultipleVersions,
isCachedPageValue,
isCachedRouteValue,
type NetlifyCachedPageValue,
type NetlifyCachedRouteValue,
type NetlifyCacheHandlerValue,
type NetlifyIncrementalCacheValue,
} from '../../shared/cache-types.cjs'
import { getRegionalBlobStore } from '../regional-blob-store.cjs'

Expand All @@ -29,7 +30,7 @@ type TagManifest = { revalidatedAt: number }

type TagManifestBlobCache = Record<string, Promise<TagManifest>>

export class NetlifyCacheHandler implements CacheHandler {
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
options: CacheHandlerContext
revalidatedTags: string[]
blobStore: Store
Expand Down Expand Up @@ -132,13 +133,18 @@ export class NetlifyCacheHandler implements CacheHandler {

if (
cacheValue.kind === 'PAGE' ||
cacheValue.kind === 'PAGES' ||
cacheValue.kind === 'APP_PAGE' ||
cacheValue.kind === 'ROUTE'
cacheValue.kind === 'ROUTE' ||
cacheValue.kind === 'APP_ROUTE'
) {
if (cacheValue.headers?.[NEXT_CACHE_TAGS_HEADER]) {
const cacheTags = (cacheValue.headers[NEXT_CACHE_TAGS_HEADER] as string).split(',')
requestContext.responseCacheTags = cacheTags
} else if (cacheValue.kind === 'PAGE' && typeof cacheValue.pageData === 'object') {
} else if (
(cacheValue.kind === 'PAGE' || cacheValue.kind === 'PAGES') &&
typeof cacheValue.pageData === 'object'
) {
// pages router doesn't have cache tags headers in PAGE cache value
// so we need to generate appropriate cache tags for it
const cacheTags = [`_N_T_${key === '/index' ? '/' : key}`]
Expand Down Expand Up @@ -185,7 +191,9 @@ export class NetlifyCacheHandler implements CacheHandler {
}
}

async get(...args: Parameters<CacheHandler['get']>): ReturnType<CacheHandler['get']> {
async get(
...args: Parameters<CacheHandlerForMultipleVersions['get']>
): ReturnType<CacheHandlerForMultipleVersions['get']> {
return this.tracer.withActiveSpan('get cache key', async (span) => {
const [key, ctx = {}] = args
getLogger().debug(`[NetlifyCacheHandler.get]: ${key}`)
Expand Down Expand Up @@ -224,8 +232,12 @@ export class NetlifyCacheHandler implements CacheHandler {
value: blob.value,
}

case 'ROUTE': {
span.addEvent('ROUTE', { lastModified: blob.lastModified, status: blob.value.status })
case 'ROUTE':
case 'APP_ROUTE': {
span.addEvent(blob.value?.kind, {
lastModified: blob.lastModified,
status: blob.value.status,
})

const valueWithoutRevalidate = this.captureRouteRevalidateAndRemoveFromObject(blob.value)

Expand All @@ -237,8 +249,9 @@ export class NetlifyCacheHandler implements CacheHandler {
},
}
}
case 'PAGE': {
span.addEvent('PAGE', { lastModified: blob.lastModified })
case 'PAGE':
case 'PAGES': {
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified })

const { revalidate, ...restOfPageValue } = blob.value

Expand All @@ -250,7 +263,7 @@ export class NetlifyCacheHandler implements CacheHandler {
}
}
case 'APP_PAGE': {
span.addEvent('APP_PAGE', { lastModified: blob.lastModified })
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified })

const { revalidate, rscData, ...restOfPageValue } = blob.value

Expand All @@ -272,18 +285,22 @@ export class NetlifyCacheHandler implements CacheHandler {
}

private transformToStorableObject(
data: Parameters<IncrementalCache['set']>[1],
context: Parameters<IncrementalCache['set']>[2],
data: Parameters<CacheHandlerForMultipleVersions['set']>[1],
context: Parameters<CacheHandlerForMultipleVersions['set']>[2],
): NetlifyIncrementalCacheValue | null {
if (data?.kind === 'ROUTE') {
if (!data) {
return null
}

if (isCachedRouteValue(data)) {
return {
...data,
revalidate: context.revalidate,
body: data.body.toString('base64'),
}
}

if (data?.kind === 'PAGE') {
if (isCachedPageValue(data)) {
return {
...data,
revalidate: context.revalidate,
Expand All @@ -301,7 +318,7 @@ export class NetlifyCacheHandler implements CacheHandler {
return data
}

async set(...args: Parameters<IncrementalCache['set']>) {
async set(...args: Parameters<CacheHandlerForMultipleVersions['set']>) {
return this.tracer.withActiveSpan('set cache key', async (span) => {
const [key, data, context] = args
const blobKey = await this.encodeBlobKey(key)
Expand All @@ -321,7 +338,7 @@ export class NetlifyCacheHandler implements CacheHandler {
value,
})

if (data?.kind === 'PAGE') {
if (data?.kind === 'PAGE' || data?.kind === 'PAGES') {
const requestContext = getRequestContext()
if (requestContext?.didPagesRouterOnDemandRevalidate) {
const tag = `_N_T_${key === '/index' ? '/' : key}`
Expand Down Expand Up @@ -397,8 +414,10 @@ export class NetlifyCacheHandler implements CacheHandler {
cacheTags = [...tags, ...softTags]
} else if (
cacheEntry.value?.kind === 'PAGE' ||
cacheEntry.value?.kind === 'PAGES' ||
cacheEntry.value?.kind === 'APP_PAGE' ||
cacheEntry.value?.kind === 'ROUTE'
cacheEntry.value?.kind === 'ROUTE' ||
cacheEntry.value?.kind === 'APP_ROUTE'
) {
cacheTags = (cacheEntry.value.headers?.[NEXT_CACHE_TAGS_HEADER] as string)?.split(',') || []
} else {
Expand Down
9 changes: 9 additions & 0 deletions src/run/next.cts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import { getRequestContext } from './handlers/request-context.cjs'
import { getTracer } from './handlers/tracer.cjs'
import { getRegionalBlobStore } from './regional-blob-store.cjs'

// https://github.com/vercel/next.js/pull/68193/files#diff-37243d614f1f5d3f7ea50bbf2af263f6b1a9a4f70e84427977781e07b02f57f1R49
// This import resulted in importing unbundled React which depending if NODE_ENV is `production` or not would use
// either development or production version of React. When not set to `production` it would use development version
// which later cause mismatching problems when both development and production versions of React were loaded causing
// react errors.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ignoring readonly NODE_ENV
process.env.NODE_ENV = 'production'

console.time('import next server')

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down
Loading

0 comments on commit 83f685e

Please sign in to comment.