Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/slow-dolphins-sparkle.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions packages/cloudflare/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@james-elicx Is this documented already?
If not, would you mind sending a PR?
Also maybe add a default value.
Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was for this to just be used internally to inline a value into the cache handler, but yeah, it would make sense to let users use it as well

[key: string]: string | Fetcher;
}
}
Expand Down
20 changes: 9 additions & 11 deletions packages/cloudflare/src/cli/build/build-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ export async function buildWorker(config: Config): Promise<void> {

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 =
Expand All @@ -73,20 +71,20 @@ export async function buildWorker(config: Config): Promise<void> {
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
Expand Down Expand Up @@ -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"),
}));
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
4 changes: 4 additions & 0 deletions packages/cloudflare/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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(),
Expand All @@ -72,6 +75,7 @@ export function getConfig(appDir: string, outputDir: string): Config {
standaloneAppDotNext,
standaloneAppServer,
internalPackage,
internalTemplates,
},

cache: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./constants";
export * from "./open-next-cache-handler";
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,34 @@ 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;
value: IncrementalCacheValue | null;
};

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<CacheHandler["get"]>): Promise<CacheHandlerValue | null> {
const [key, _ctx] = args;
const ctx = parseCtx(_ctx);

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<CacheEntry>(key, "json");
const value = await this.kv.get<CacheEntry>(key, "json");
if (value) return value;
} catch (e) {
console.error(`Failed to get value for key = ${key}: ${e}`);
Expand Down Expand Up @@ -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;
}

Expand All @@ -127,15 +129,15 @@ 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}`);
}
}

async revalidateTag(...args: Parameters<CacheHandler["revalidateTag"]>) {
const [tags] = args;
if (OpenNextCacheHandler.maybeKVNamespace === undefined) {
if (this.kv === undefined) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 4 additions & 1 deletion packages/cloudflare/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ 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"],
platform: "node",
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,
});
Expand Down