Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 3 additions & 2 deletions examples/vercel-blog-starter/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
import cache from "@opennextjs/cloudflare/kvCache";

const config: OpenNextConfig = {
default: {
override: {
wrapper: "cloudflare-node",
converter: "edge",
// Unused implementation
incrementalCache: "dummy",
incrementalCache: async () => cache,
// Unused implementations
tagCache: "dummy",
queue: "dummy",
},
Expand Down
1 change: 0 additions & 1 deletion packages/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export default config;

## Known issues

- Next cache is not supported in the experimental branch yet
- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored
- Maybe more, still experimental...

Expand Down
5 changes: 2 additions & 3 deletions packages/cloudflare/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
ASSETS: Fetcher;
__NEXT_PRIVATE_STANDALONE_CONFIG?: string;
SKIP_NEXT_APP_BUILD?: string;
NEXT_PRIVATE_DEBUG_CACHE?: string;
__OPENNEXT_KV_BINDING_NAME: string;
OPEN_NEXT_ORIGIN: string;
NODE_ENV?: string;
__OPENNEXT_PROCESSED_ENV?: string;
// Whether process.env has been populated (on first request).
__PROCESS_ENV_POPULATED?: string;
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
".": {
"import": "./dist/api/index.js",
"types": "./dist/api/index.d.ts"
},
"./*": {
"import": "./dist/api/*.js",
"types": "./dist/api/*.d.ts"
}
},
"files": [
Expand Down Expand Up @@ -65,7 +69,7 @@
"@types/mock-fs": "catalog:"
},
"dependencies": {
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@683",
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@684",
"ts-morph": "catalog:",
"@dotenvx/dotenvx": "catalog:"
},
Expand Down
8 changes: 4 additions & 4 deletions packages/cloudflare/src/api/get-cloudflare-context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import "server-only";

declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface CloudflareEnv {}
interface CloudflareEnv {
NEXT_CACHE_WORKERS_KV?: KVNamespace;
ASSETS?: Fetcher;
}
}

export type CloudflareContext<
Expand Down
150 changes: 150 additions & 0 deletions packages/cloudflare/src/api/kvCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { KVNamespace } from "@cloudflare/workers-types";
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";

import { getCloudflareContext } from "./get-cloudflare-context.js";

export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";

export const STATUS_DELETED = 1;

/**
* Open Next cache based on cloudflare KV and Assets.
*
* Note: The class is instantiated outside of the request context.
* The cloudflare context and process.env are not initialzed yet
* when the constructor is called.
*/
class Cache implements IncrementalCache {
readonly name = "cloudflare-kv";
protected initialized = false;
protected kv: KVNamespace | undefined;
protected assets: Fetcher | undefined;

async get<IsFetch extends boolean = false>(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>>> {
if (!this.initialized) {
await this.init();
}

if (!(this.kv || this.assets)) {
throw new IgnorableError(`No KVNamespace nor Fetcher`);
}

this.debug(`Get ${key}`);

try {
let entry: {
value?: CacheValue<IsFetch>;
lastModified?: number;
status?: number;
} | null = null;

if (this.kv) {
this.debug(`- From KV`);
const kvKey = this.getKVKey(key, isFetch);
entry = await this.kv.get(kvKey, "json");
if (entry?.status === STATUS_DELETED) {
return {};
}
}

if (!entry && this.assets) {
this.debug(`- From Assets`);
const url = this.getAssetUrl(key, isFetch);
const response = await this.assets.fetch(url);
if (response.ok) {
// TODO: consider populating KV with the asset value if faster.
// This could be optional as KV writes are $$.
// See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
entry = {
value: await response.json(),
// __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
lastModified: (globalThis as { __BUILD_TIMESTAMP_MS__?: number }).__BUILD_TIMESTAMP_MS__,
};
}
}
this.debug(entry ? `-> hit` : `-> miss`);
return { value: entry?.value, lastModified: entry?.lastModified };
} catch {
throw new RecoverableError(`Failed to get cache [${key}]`);
}
}

async set<IsFetch extends boolean = false>(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
): Promise<void> {
if (!this.initialized) {
await this.init();
}
if (!this.kv) {
throw new IgnorableError(`No KVNamespace`);
}
this.debug(`Set ${key}`);
try {
const kvKey = this.getKVKey(key, isFetch);
// Note: We can not set a TTL as we might fallback to assets,
// still removing old data (old BUILD_ID) could help avoiding
// the cache growing too big.
await this.kv.put(
kvKey,
JSON.stringify({
value,
lastModified: Date.now(),
})
);
} catch {
throw new RecoverableError(`Failed to set cache [${key}]`);
}
}

async delete(key: string): Promise<void> {
if (!this.initialized) {
await this.init();
}
if (!this.kv) {
throw new IgnorableError(`No KVNamespace`);
}
this.debug(`Delete ${key}`);
try {
const kvKey = this.getKVKey(key, /* isFetch= */ false);
// Do not delete the key as we would then fallback to the assets.
await this.kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
} catch {
throw new RecoverableError(`Failed to delete cache [${key}]`);
}
}

protected getKVKey(key: string, isFetch?: boolean): string {
return `${this.getBuildId()}/${key}.${isFetch ? "fetch" : "cache"}`;
}

protected getAssetUrl(key: string, isFetch?: boolean): string {
return isFetch
? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}`
: `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`;
}

protected debug(...args: unknown[]) {
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
console.log(`[Cache ${this.name}] `, ...args);
}
}

protected getBuildId() {
return process.env.NEXT_BUILD_ID ?? "no-build-id";
}

protected async init() {
const env = (await getCloudflareContext()).env;
this.kv = env.NEXT_CACHE_WORKERS_KV;
this.assets = env.ASSETS;
this.initialized = true;
}
}

export default new Cache();
5 changes: 1 addition & 4 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { build, Plugin } from "esbuild";

import { Config } from "../config.js";
import * as patches from "./patches/index.js";
import { copyPrerenderedRoutes } from "./utils/index.js";

/** The dist directory of the Cloudflare adapter package */
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
Expand All @@ -17,9 +16,6 @@ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "
* Bundle the Open Next server.
*/
export async function bundleServer(config: Config, openNextOptions: BuildOptions): Promise<void> {
// Copy over prerendered assets (e.g. SSG routes)
copyPrerenderedRoutes(config);

patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);

const nextConfigStr =
Expand Down Expand Up @@ -113,6 +109,7 @@ globalThis.Request = CustomRequest;
Request = globalThis.Request;
// Makes the edge converter returns either a Response or a Request.
globalThis.__dangerous_ON_edge_converter_returns_request = true;
globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
`,
},
});
Expand Down
19 changes: 13 additions & 6 deletions packages/cloudflare/src/cli/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
import { createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
import * as buildHelper from "@opennextjs/aws/build/helper.js";
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
Expand All @@ -16,6 +16,7 @@ import type { ProjectOptions } from "../config.js";
import { containsDotNextDir, getConfig } from "../config.js";
import { bundleServer } from "./bundle-server.js";
import { compileEnvFiles } from "./open-next/compile-env-files.js";
import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
import { createServerBundle } from "./open-next/createServerBundle.js";

/**
Expand Down Expand Up @@ -80,6 +81,11 @@ export async function build(projectOpts: ProjectOptions): Promise<void> {

createStaticAssets(options);

if (config.dangerous?.disableIncrementalCache !== true) {
createCacheAssets(options);
copyCacheAssets(options);
}

await createServerBundle(options);

// TODO: drop this copy.
Expand All @@ -103,10 +109,11 @@ function ensureCloudflareConfig(config: OpenNextConfig) {
const requirements = {
dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
dftUseEdgeConverter: config.default?.override?.converter === "edge",
dftUseDummyCache:
config.default?.override?.incrementalCache === "dummy" &&
config.default?.override?.tagCache === "dummy" &&
config.default?.override?.queue === "dummy",
dftMaybeUseCache:
config.default?.override?.incrementalCache === "dummy" ||
typeof config.default?.override?.incrementalCache === "function",
dftUseDummyTagCacheAndQueue:
config.default?.override?.tagCache === "dummy" && config.default?.override?.queue === "dummy",
disableCacheInterception: config.dangerous?.enableCacheInterception !== true,
mwIsMiddlewareExternal: config.middleware?.external == true,
mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
Expand All @@ -121,7 +128,7 @@ function ensureCloudflareConfig(config: OpenNextConfig) {
override: {
wrapper: "cloudflare-node",
converter: "edge",
incrementalCache: "dummy",
incrementalCache: "dummy" | function,
tagCache: "dummy",
queue: "dummy",
},
Expand Down
14 changes: 14 additions & 0 deletions packages/cloudflare/src/cli/build/open-next/copyCacheAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { cpSync, mkdirSync } from "node:fs";
import { join } from "node:path";

import * as buildHelper from "@opennextjs/aws/build/helper.js";

import { CACHE_ASSET_DIR } from "../../../api/kvCache.js";

export function copyCacheAssets(options: buildHelper.BuildOptions) {
const { outputDir } = options;
const srcPath = join(outputDir, "cache");
const dstPath = join(outputDir, "assets", CACHE_ASSET_DIR);
mkdirSync(dstPath, { recursive: true });
cpSync(srcPath, dstPath, { recursive: true });
}
48 changes: 0 additions & 48 deletions packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/cloudflare/src/cli/build/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./copy-prerendered-routes.js";
export * from "./extract-project-env-vars.js";
export * from "./normalize-path.js";
export * from "./ts-parse-file.js";
Loading
Loading