diff --git a/.changeset/slow-dolphins-sparkle.md b/.changeset/slow-dolphins-sparkle.md new file mode 100644 index 00000000..1cc5c28f --- /dev/null +++ b/.changeset/slow-dolphins-sparkle.md @@ -0,0 +1,7 @@ +--- +"@opennextjs/cloudflare": patch +--- + +refactor: retrieve cache handler kv instance inside constructor + +The cache handler was retrieving it's KV instance as a static property on the class that was defined at some point during the execution of the Next.js server. This moves the retrieval of the KV instance to happen inside the constructor for the class, so that it is retrieved during instantiation instead. diff --git a/packages/cloudflare/env.d.ts b/packages/cloudflare/env.d.ts index bea42567..b5e1c844 100644 --- a/packages/cloudflare/env.d.ts +++ b/packages/cloudflare/env.d.ts @@ -5,6 +5,7 @@ declare global { __NEXT_PRIVATE_STANDALONE_CONFIG?: string; SKIP_NEXT_APP_BUILD?: string; NEXT_PRIVATE_DEBUG_CACHE?: string; + __OPENNEXT_KV_BINDING_NAME: string; [key: string]: string | Fetcher; } } diff --git a/packages/cloudflare/src/cli/build/build-worker.ts b/packages/cloudflare/src/cli/build/build-worker.ts index becb7ba6..81c0dbb2 100644 --- a/packages/cloudflare/src/cli/build/build-worker.ts +++ b/packages/cloudflare/src/cli/build/build-worker.ts @@ -51,9 +51,7 @@ export async function buildWorker(config: Config): Promise { copyPackageCliFiles(packageDistDir, config); - const templateDir = path.join(config.paths.internalPackage, "cli", "templates"); - - const workerEntrypoint = path.join(templateDir, "worker.ts"); + const workerEntrypoint = path.join(config.paths.internalTemplates, "worker.ts"); const workerOutputFile = path.join(config.paths.builderOutput, "index.mjs"); const nextConfigStr = @@ -73,20 +71,20 @@ export async function buildWorker(config: Config): Promise { format: "esm", target: "esnext", minify: false, - plugins: [createFixRequiresESBuildPlugin(templateDir)], + plugins: [createFixRequiresESBuildPlugin(config)], alias: { // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s: // eval("require")("bufferutil"); // eval("require")("utf-8-validate"); - "next/dist/compiled/ws": path.join(templateDir, "shims", "empty.ts"), + "next/dist/compiled/ws": path.join(config.paths.internalTemplates, "shims", "empty.ts"), // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`: // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext)); // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63 // QUESTION: Why did I encountered this but mhart didn't? - "next/dist/compiled/edge-runtime": path.join(templateDir, "shims", "empty.ts"), + "next/dist/compiled/edge-runtime": path.join(config.paths.internalTemplates, "shims", "empty.ts"), // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env - "@next/env": path.join(templateDir, "shims", "env.ts"), + "@next/env": path.join(config.paths.internalTemplates, "shims", "env.ts"), }, define: { // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139 @@ -167,23 +165,23 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config) patchedCode = inlineNextRequire(patchedCode, config); patchedCode = patchFindDir(patchedCode, config); patchedCode = inlineEvalManifest(patchedCode, config); - patchedCode = patchCache(patchedCode, config); + patchedCode = await patchCache(patchedCode, config); patchedCode = inlineMiddlewareManifestRequire(patchedCode, config); patchedCode = patchExceptionBubbling(patchedCode); await writeFile(workerOutputFile, patchedCode); } -function createFixRequiresESBuildPlugin(templateDir: string): Plugin { +function createFixRequiresESBuildPlugin(config: Config): Plugin { return { name: "replaceRelative", setup(build) { // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({ - path: path.join(templateDir, "shims", "empty.ts"), + path: path.join(config.paths.internalTemplates, "shims", "empty.ts"), })); build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({ - path: path.join(templateDir, "shims", "empty.ts"), + path: path.join(config.paths.internalTemplates, "shims", "empty.ts"), })); }, }; diff --git a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts index 9b5ef064..1f5145d9 100644 --- a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts +++ b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts @@ -1,24 +1,38 @@ import { Config } from "../../../config"; -import path from "node:path"; +import { build } from "esbuild"; +import { join } from "node:path"; /** * Install the cloudflare KV cache handler */ -export function patchCache(code: string, config: Config): string { +export async function patchCache(code: string, config: Config): Promise { console.log("# patchCache"); - const cacheHandler = path.join(config.paths.internalPackage, "cli", "cache-handler", "index.mjs"); + const cacheHandlerFileName = "cache-handler.mjs"; + const cacheHandlerEntrypoint = join(config.paths.internalTemplates, "cache-handler", "index.ts"); + const cacheHandlerOutputFile = join(config.paths.builderOutput, cacheHandlerFileName); + + await build({ + entryPoints: [cacheHandlerEntrypoint], + bundle: true, + outfile: cacheHandlerOutputFile, + format: "esm", + target: "esnext", + minify: true, + define: { + "process.env.__OPENNEXT_KV_BINDING_NAME": `"${config.cache.kvBindingName}"`, + }, + }); const patchedCode = code.replace( "const { cacheHandler } = this.nextConfig;", `const cacheHandler = null; -CacheHandler = (await import('${cacheHandler}')).OpenNextCacheHandler; -CacheHandler.maybeKVNamespace = process.env["${config.cache.kvBindingName}"]; +CacheHandler = (await import('./${cacheHandlerFileName}')).OpenNextCacheHandler; ` ); if (patchedCode === code) { - throw new Error("Cache patch not applied"); + throw new Error("Patch `patchCache` not applied"); } return patchedCode; diff --git a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts index 96577394..e2cecb6f 100644 --- a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts +++ b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts @@ -1,4 +1,4 @@ -import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../cache-handler"; +import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../constants/incremental-cache"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { Config } from "../../config"; diff --git a/packages/cloudflare/src/cli/config.ts b/packages/cloudflare/src/cli/config.ts index 05d2b6fc..b79d356d 100644 --- a/packages/cloudflare/src/cli/config.ts +++ b/packages/cloudflare/src/cli/config.ts @@ -31,6 +31,8 @@ export type Config = { standaloneAppServer: string; // Package in the standalone node_modules internalPackage: string; + // Templates in the package in the standalone node_modules + internalTemplates: string; }; cache: { @@ -59,6 +61,7 @@ export function getConfig(appDir: string, outputDir: string): Config { const nodeModules = path.join(standaloneApp, "node_modules"); const internalPackage = path.join(nodeModules, ...PACKAGE_NAME.split("/")); + const internalTemplates = path.join(internalPackage, "cli", "templates"); return { buildTimestamp: Date.now(), @@ -72,6 +75,7 @@ export function getConfig(appDir: string, outputDir: string): Config { standaloneAppDotNext, standaloneAppServer, internalPackage, + internalTemplates, }, cache: { diff --git a/packages/cloudflare/src/cli/cache-handler/constants.ts b/packages/cloudflare/src/cli/constants/incremental-cache.ts similarity index 100% rename from packages/cloudflare/src/cli/cache-handler/constants.ts rename to packages/cloudflare/src/cli/constants/incremental-cache.ts diff --git a/packages/cloudflare/src/cli/cache-handler/index.ts b/packages/cloudflare/src/cli/templates/cache-handler/index.ts similarity index 59% rename from packages/cloudflare/src/cli/cache-handler/index.ts rename to packages/cloudflare/src/cli/templates/cache-handler/index.ts index 5d23b49d..46fc26df 100644 --- a/packages/cloudflare/src/cli/cache-handler/index.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/index.ts @@ -1,2 +1 @@ -export * from "./constants"; export * from "./open-next-cache-handler"; diff --git a/packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts similarity index 87% rename from packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts rename to packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts index 46a664a8..abfbe882 100644 --- a/packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts @@ -10,10 +10,10 @@ import { RSC_PREFETCH_SUFFIX, RSC_SUFFIX, SEED_DATA_DIR, -} from "./constants"; +} from "../../constants/incremental-cache"; import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; import type { IncrementalCacheValue } from "next/dist/server/response-cache"; -import { KVNamespace } from "@cloudflare/workers-types"; +import type { KVNamespace } from "@cloudflare/workers-types"; type CacheEntry = { lastModified: number; @@ -21,11 +21,13 @@ type CacheEntry = { }; export class OpenNextCacheHandler implements CacheHandler { - static maybeKVNamespace: KVNamespace | undefined = undefined; + protected kv: KVNamespace | undefined; protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; - constructor(protected ctx: CacheHandlerContext) {} + constructor(protected ctx: CacheHandlerContext) { + this.kv = process.env[process.env.__OPENNEXT_KV_BINDING_NAME] as KVNamespace | undefined; + } async get(...args: Parameters): Promise { const [key, _ctx] = args; @@ -33,9 +35,9 @@ export class OpenNextCacheHandler implements CacheHandler { if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - if (OpenNextCacheHandler.maybeKVNamespace !== undefined) { + if (this.kv !== undefined) { try { - const value = await OpenNextCacheHandler.maybeKVNamespace.get(key, "json"); + const value = await this.kv.get(key, "json"); if (value) return value; } catch (e) { console.error(`Failed to get value for key = ${key}: ${e}`); @@ -115,7 +117,7 @@ export class OpenNextCacheHandler implements CacheHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [key, entry, _ctx] = args; - if (OpenNextCacheHandler.maybeKVNamespace === undefined) { + if (this.kv === undefined) { return; } @@ -127,7 +129,7 @@ export class OpenNextCacheHandler implements CacheHandler { }; try { - await OpenNextCacheHandler.maybeKVNamespace.put(key, JSON.stringify(data)); + await this.kv.put(key, JSON.stringify(data)); } catch (e) { console.error(`Failed to set value for key = ${key}: ${e}`); } @@ -135,7 +137,7 @@ export class OpenNextCacheHandler implements CacheHandler { async revalidateTag(...args: Parameters) { const [tags] = args; - if (OpenNextCacheHandler.maybeKVNamespace === undefined) { + if (this.kv === undefined) { return; } diff --git a/packages/cloudflare/src/cli/cache-handler/utils.ts b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts similarity index 88% rename from packages/cloudflare/src/cli/cache-handler/utils.ts rename to packages/cloudflare/src/cli/templates/cache-handler/utils.ts index 2241eecc..b1e7d08a 100644 --- a/packages/cloudflare/src/cli/cache-handler/utils.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts @@ -1,5 +1,5 @@ -import { IncrementalCache } from "next/dist/server/lib/incremental-cache"; -import { NEXT_META_SUFFIX } from "./constants"; +import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; +import { NEXT_META_SUFFIX } from "../../constants/incremental-cache"; type PrerenderedRouteMeta = { lastModified: number; diff --git a/packages/cloudflare/tsup.config.ts b/packages/cloudflare/tsup.config.ts index ade3b9fa..42713089 100644 --- a/packages/cloudflare/tsup.config.ts +++ b/packages/cloudflare/tsup.config.ts @@ -2,7 +2,7 @@ import { cp } from "node:fs/promises"; import { defineConfig } from "tsup"; const cliConfig = defineConfig({ - entry: ["src/cli/index.ts", "src/cli/cache-handler/index.ts"], + entry: ["src/cli/index.ts"], outDir: "dist/cli", dts: false, format: ["esm"], @@ -10,6 +10,9 @@ const cliConfig = defineConfig({ external: ["esbuild"], clean: true, onSuccess: async () => { + await cp(`${__dirname}/src/cli/constants`, `${__dirname}/dist/cli/constants`, { + recursive: true, + }); await cp(`${__dirname}/src/cli/templates`, `${__dirname}/dist/cli/templates`, { recursive: true, });