Skip to content

Commit 2810004

Browse files
authored
feat: support 'use cache' (#2862)
* test: add integration test cases for use-cache * feat: add support for default use-cache handler * test: cleanup after use-cache tests * test: fix typo in test use cache test helper * chore: remove unused PrivateCacheEntry properties and add more information in module preamble comment * chore: remove unneed return to clean up PR diff
1 parent f568502 commit 2810004

File tree

29 files changed

+1936
-319
lines changed

29 files changed

+1936
-319
lines changed

package-lock.json

+547-295
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"msw": "^2.0.7",
7878
"netlify-cli": "^20.1.1",
7979
"next": "^15.0.0-canary.28",
80+
"next-with-cache-handler-v2": "npm:[email protected]",
8081
"os": "^0.1.2",
8182
"outdent": "^0.8.0",
8283
"p-limit": "^5.0.0",
@@ -89,13 +90,18 @@
8990
"uuid": "^10.0.0",
9091
"vitest": "^3.0.0"
9192
},
93+
"overrides": {
94+
"react": "19.0.0-rc.0",
95+
"react-dom": "19.0.0-rc.0"
96+
},
9297
"clean-package": {
9398
"indent": 2,
9499
"remove": [
95100
"clean-package",
96101
"dependencies",
97102
"devDependencies",
98-
"scripts"
103+
"scripts",
104+
"overrides"
99105
]
100106
}
101107
}

src/build/content/server.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import { join as posixJoin, sep as posixSep } from 'node:path/posix'
1616
import { trace } from '@opentelemetry/api'
1717
import { wrapTracer } from '@opentelemetry/api/experimental'
1818
import glob from 'fast-glob'
19-
import { prerelease, lt as semverLowerThan, lte as semverLowerThanOrEqual } from 'semver'
19+
import { prerelease, satisfies, lt as semverLowerThan, lte as semverLowerThanOrEqual } from 'semver'
2020

21-
import { RUN_CONFIG } from '../../run/constants.js'
22-
import { PluginContext } from '../plugin-context.js'
21+
import type { RunConfig } from '../../run/config.js'
22+
import { RUN_CONFIG_FILE } from '../../run/constants.js'
23+
import type { PluginContext, RequiredServerFilesManifest } from '../plugin-context.js'
2324

2425
const tracer = wrapTracer(trace.getTracer('Next runtime'))
2526

@@ -54,7 +55,9 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
5455
throw error
5556
}
5657
}
57-
const reqServerFiles = JSON.parse(await readFile(reqServerFilesPath, 'utf-8'))
58+
const reqServerFiles = JSON.parse(
59+
await readFile(reqServerFilesPath, 'utf-8'),
60+
) as RequiredServerFilesManifest
5861

5962
// if the resolved dist folder does not match the distDir of the required-server-files.json
6063
// this means the path got altered by a plugin like nx and contained ../../ parts so we have to reset it
@@ -73,8 +76,17 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
7376
// write our run-config.json to the root dir so that we can easily get the runtime config of the required-server-files.json
7477
// without the need to know about the monorepo or distDir configuration upfront.
7578
await writeFile(
76-
join(ctx.serverHandlerDir, RUN_CONFIG),
77-
JSON.stringify(reqServerFiles.config),
79+
join(ctx.serverHandlerDir, RUN_CONFIG_FILE),
80+
JSON.stringify({
81+
nextConfig: reqServerFiles.config,
82+
// only enable setting up 'use cache' handler when Next.js supports CacheHandlerV2 as we don't have V1 compatible implementation
83+
// see https://github.com/vercel/next.js/pull/76687 first released in v15.3.0-canary.13
84+
enableUseCacheHandler: ctx.nextVersion
85+
? satisfies(ctx.nextVersion, '>=15.3.0-canary.13', {
86+
includePrerelease: true,
87+
})
88+
: false,
89+
} satisfies RunConfig),
7890
'utf-8',
7991
)
8092

@@ -336,9 +348,11 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) =
336348
}
337349

338350
export const verifyHandlerDirStructure = async (ctx: PluginContext) => {
339-
const runConfig = JSON.parse(await readFile(join(ctx.serverHandlerDir, RUN_CONFIG), 'utf-8'))
351+
const { nextConfig } = JSON.parse(
352+
await readFile(join(ctx.serverHandlerDir, RUN_CONFIG_FILE), 'utf-8'),
353+
) as RunConfig
340354

341-
const expectedBuildIDPath = join(ctx.serverHandlerDir, runConfig.distDir, 'BUILD_ID')
355+
const expectedBuildIDPath = join(ctx.serverHandlerDir, nextConfig.distDir, 'BUILD_ID')
342356
if (!existsSync(expectedBuildIDPath)) {
343357
ctx.failBuild(
344358
`Failed creating server handler. BUILD_ID file not found at expected location "${expectedBuildIDPath}".`,

src/run/config.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import { join, resolve } from 'node:path'
44

55
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
66

7-
import { PLUGIN_DIR, RUN_CONFIG } from './constants.js'
7+
import { PLUGIN_DIR, RUN_CONFIG_FILE } from './constants.js'
88
import { setInMemoryCacheMaxSizeFromNextConfig } from './storage/storage.cjs'
99

10+
export type RunConfig = {
11+
nextConfig: NextConfigComplete
12+
enableUseCacheHandler: boolean
13+
}
14+
1015
/**
1116
* Get Next.js config from the build output
1217
*/
1318
export const getRunConfig = async () => {
14-
return JSON.parse(await readFile(resolve(PLUGIN_DIR, RUN_CONFIG), 'utf-8'))
19+
return JSON.parse(await readFile(resolve(PLUGIN_DIR, RUN_CONFIG_FILE), 'utf-8')) as RunConfig
1520
}
1621

1722
type NextConfigForMultipleVersions = NextConfigComplete & {

src/run/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ import { fileURLToPath } from 'node:url'
44
export const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
55
export const PLUGIN_DIR = resolve(`${MODULE_DIR}../../..`)
66
// a file where we store the required-server-files config object in to access during runtime
7-
export const RUN_CONFIG = 'run-config.json'
7+
export const RUN_CONFIG_FILE = 'run-config.json'

src/run/handlers/server.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { OutgoingHttpHeaders } from 'http'
33
import { ComputeJsOutgoingMessage, toComputeResponse, toReqRes } from '@fastly/http-compute-js'
44
import type { Context } from '@netlify/functions'
55
import { Span } from '@opentelemetry/api'
6-
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
76
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
87

8+
import { getRunConfig, setRunConfig } from '../config.js'
99
import {
1010
adjustDateHeader,
1111
setCacheControlHeaders,
@@ -18,15 +18,22 @@ import { setFetchBeforeNextPatchedIt } from '../storage/storage.cjs'
1818

1919
import { getLogger, type RequestContext } from './request-context.cjs'
2020
import { getTracer, recordWarning } from './tracer.cjs'
21+
import { configureUseCacheHandlers } from './use-cache-handler.js'
2122
import { setupWaitUntil } from './wait-until.cjs'
22-
23+
// make use of global fetch before Next.js applies any patching
2324
setFetchBeforeNextPatchedIt(globalThis.fetch)
25+
// configure globals that Next.js make use of before we start importing any Next.js code
26+
// as some globals are consumed at import time
27+
const { nextConfig, enableUseCacheHandler } = await getRunConfig()
28+
if (enableUseCacheHandler) {
29+
configureUseCacheHandlers()
30+
}
31+
setRunConfig(nextConfig)
32+
setupWaitUntil()
2433

2534
const nextImportPromise = import('../next.cjs')
2635

27-
setupWaitUntil()
28-
29-
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
36+
let nextHandler: WorkerRequestHandler
3037

3138
/**
3239
* When Next.js proxies requests externally, it writes the response back as-is.
@@ -61,11 +68,6 @@ export default async (
6168

6269
if (!nextHandler) {
6370
await tracer.withActiveSpan('initialize next server', async () => {
64-
// set the server config
65-
const { getRunConfig, setRunConfig } = await import('../config.js')
66-
nextConfig = await getRunConfig()
67-
setRunConfig(nextConfig)
68-
6971
const { getMockedRequestHandler } = await nextImportPromise
7072
const url = new URL(request.url)
7173

src/run/handlers/tags-handler.cts

+21
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@ async function getTagRevalidatedAt(
2525
return tagManifest.revalidatedAt
2626
}
2727

28+
/**
29+
* Get the most recent revalidation timestamp for a list of tags
30+
*/
31+
export async function getMostRecentTagRevalidationTimestamp(tags: string[]) {
32+
if (tags.length === 0) {
33+
return 0
34+
}
35+
36+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
37+
38+
const timestampsOrNulls = await Promise.all(
39+
tags.map((tag) => getTagRevalidatedAt(tag, cacheStore)),
40+
)
41+
42+
const timestamps = timestampsOrNulls.filter((timestamp) => timestamp !== null)
43+
if (timestamps.length === 0) {
44+
return 0
45+
}
46+
return Math.max(...timestamps)
47+
}
48+
2849
/**
2950
* Check if any of the tags were invalidated since the given timestamp
3051
*/

0 commit comments

Comments
 (0)