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
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
152 changes: 152 additions & 0 deletions packages/cloudflare/src/api/kvCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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,
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
// See https://developers.cloudflare.com/workers/reference/security-model/
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