Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fa74e70
refactor: remove createServerBundle.ts and integrate its functionalit…
conico974 Jun 27, 2026
33a6eff
feat: implement default overrides for bundle configurations and add t…
conico974 Jun 27, 2026
fe002e1
format
conico974 Jun 27, 2026
9ee9515
fix ts issue
conico974 Jun 27, 2026
6fad0c6
feat: add default overrides for AWS adapter and integrate Cloudflare …
conico974 Jun 27, 2026
84397ab
Revert "feat: add default overrides for AWS adapter and integrate Clo…
conico974 Jun 27, 2026
f22d37f
feat: enhance AWS adapter with default overrides and improve middlewa…
conico974 Jun 27, 2026
a3165f0
refactor(core): return ValidateConfigResult from validateConfig inste…
conico974 Jun 28, 2026
6296e94
refactor(core): extract buildOpenNextOutput, export OpenNextOutput type
conico974 Jun 28, 2026
4e82821
refactor(core): branch on ValidateConfigResult in compileOpenNextConfig
conico974 Jun 28, 2026
0af2e73
feat(core): overridable validateConfig and generic generateOutput on …
conico974 Jun 28, 2026
bab47f1
fix(core): use require.resolve for full-path overrides in resolve plugin
conico974 Jun 28, 2026
51b5e3b
format
conico974 Jun 28, 2026
a437de1
feat(core): enforce mandatory externals in serverBundle and update re…
conico974 Aug 1, 2026
cd8f83e
refactor(core): enhance resolve plugin to support dynamic override re…
conico974 Aug 1, 2026
c200d7a
feat(core): run the cache in a dedicated handler function
conico974 Aug 16, 2026
8b63860
feat(cloudflare): add the OpenNextCache entrypoint
conico974 Aug 16, 2026
dc444fc
refactor(core)!: route all caching through the cache override
conico974 Aug 16, 2026
3ec214c
Merge origin/main into vicb/review-pr-45
vicb Sep 22, 2026
01543dd
fix(cache): preserve fetch tags on writes
vicb Sep 22, 2026
c89f3e8
fix(cache): preserve Pages Router tags
vicb Sep 22, 2026
f291f43
fix(cache): restore fetch path revalidation
vicb Sep 22, 2026
142c431
fix(core): report cache providers on cache function
vicb Sep 22, 2026
729090e
test(core): update cache bundle provider expectations
vicb Sep 22, 2026
ed18497
test(cache): cover internal tag header stripping
vicb Sep 22, 2026
2c3999a
fix(aws): wire dedicated cache defaults
vicb Sep 22, 2026
e618535
fix(aws): use local cache transport by default
vicb Sep 23, 2026
4be77cd
fix(cache): check untagged entries for revalidation
vicb Sep 23, 2026
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
32 changes: 32 additions & 0 deletions .changeset/cache-consolidate-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@opennextjs/core": major
"@opennextjs/cloudflare": minor
---

Route all caching through the `cache` override

The incremental cache and the tag cache no longer run inside the server function. They run in the
cache handler function, which the server, the middleware and the composable cache reach through the
`cache` override. Tag revalidation - `hasBeenRevalidated`, `writeTags` and CDN invalidation - moves
with them, so `get`, `set` and `revalidateTags` now handle tags transparently.

`incrementalCache` and `tagCache` are removed from `default.override` and from the middleware
override. Configurations that are not created by `defineCloudflareConfig` should move them to the
top level `cacheHandler` option and set `default.override.cache`:

```diff
default: {
override: {
- incrementalCache: "s3",
- tagCache: "dynamodb",
+ cache: "local",
},
},
+ cacheHandler: {
+ incrementalCache: "s3",
+ tagCache: "dynamodb",
+ },
```

`defineCloudflareConfig` is unchanged: it now wires the cache to the `OpenNextCache` entrypoint on
its own.
11 changes: 9 additions & 2 deletions examples/app-pages-router/open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import type { OpenNextConfig, OverrideOptions } from "@opennextjs/core/types/ope
const devOverride = {
wrapper: "express-dev",
converter: "node",
incrementalCache: "fs-dev",
cache: "local",
queue: "direct",
tagCache: "fs-dev-nextMode",
} satisfies OverrideOptions;

export default {
Expand All @@ -26,6 +25,14 @@ export default {
},
loader: "fs-dev",
},
cacheHandler: {
override: {
wrapper: "dummy",
converter: "dummy",
},
incrementalCache: "fs-dev",
tagCache: "fs-dev-nextMode",
},
// You can override the build command here so that you don't have to rebuild next every time you make a change
// buildCommand: "echo 'No build command'",
} satisfies OpenNextConfig;
12 changes: 10 additions & 2 deletions examples/app-router/open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ export default {
override: {
wrapper: "express-dev",
converter: "node",
incrementalCache: "fs-dev",
cache: "local",
queue: "direct",
tagCache: "fs-dev-nextMode",
},
},

Expand All @@ -19,6 +18,15 @@ export default {
loader: "fs-dev",
},

cacheHandler: {
override: {
wrapper: "dummy",
converter: "dummy",
},
incrementalCache: "fs-dev",
tagCache: "fs-dev-nextMode",
},

// You can override the build command here so that you don't have to rebuild next every time you make a change
//buildCommand: "echo 'No build command'",
} satisfies OpenNextConfig;
12 changes: 10 additions & 2 deletions examples/experimental/open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ export default {
override: {
wrapper: "express-dev",
converter: "node",
incrementalCache: "fs-dev",
queue: "direct",
tagCache: "fs-dev-nextMode",
cache: "local",
},
},

Expand All @@ -19,6 +18,15 @@ export default {
loader: "fs-dev",
},

cacheHandler: {
override: {
wrapper: "dummy",
converter: "dummy",
},
incrementalCache: "fs-dev",
tagCache: "fs-dev-nextMode",
},

// You can override the build command here so that you don't have to rebuild next every time you make a change
//buildCommand: "echo 'No build command'",
} satisfies OpenNextConfig;
12 changes: 10 additions & 2 deletions examples/pages-router/open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ export default {
override: {
wrapper: "express-dev",
converter: "node",
incrementalCache: "fs-dev",
queue: "direct",
tagCache: "dummy",
cache: "local",
},
},

Expand All @@ -17,6 +16,15 @@ export default {
loader: "fs-dev",
},

cacheHandler: {
override: {
wrapper: "dummy",
converter: "dummy",
},
incrementalCache: "fs-dev",
tagCache: "dummy",
},

// You can override the build command here so that you don't have to rebuild next every time you make a change
//buildCommand: "echo 'No build command'",
};
10 changes: 7 additions & 3 deletions packages/aws/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export default buildAdapter((_config, buildOpts: BuildOptions) => ({
server: {
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda-streaming.js",
converter: "@opennextjs/aws/overrides/converters/aws-streaming.js",
cache: "@opennextjs/core/overrides/cache/local.js",
queue: "@opennextjs/aws/overrides/queue/sqs.js",
},
cache: {
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda.js",
converter: "@opennextjs/aws/overrides/converters/aws-apigw-v2.js",
incrementalCache: "@opennextjs/aws/overrides/incrementalCache/s3.js",
tagCache: "@opennextjs/aws/overrides/tagCache/dynamodb.js",
queue: "@opennextjs/aws/overrides/queue/sqs.js",
},
revalidation: {
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda.js",
Expand All @@ -31,8 +36,7 @@ export default buildAdapter((_config, buildOpts: BuildOptions) => ({
middleware: {
wrapper: "@opennextjs/aws/overrides/wrappers/aws-lambda.js",
converter: "@opennextjs/aws/overrides/converters/aws-cloudfront.js",
incrementalCache: "@opennextjs/aws/overrides/incrementalCache/s3-lite.js",
tagCache: "@opennextjs/aws/overrides/tagCache/dynamodb-lite.js",
cache: "@opennextjs/core/overrides/cache/local.js",
queue: "@opennextjs/aws/overrides/queue/sqs-lite.js",
},
},
Expand Down
3 changes: 1 addition & 2 deletions packages/cloudflare/src/api/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ describe("defineCloudflareConfig", () => {
wrapper: "node",
converter: "node",
generateDockerfile: true,
incrementalCache: "dummy",
tagCache: "dummy",
cache: "dummy",
queue: "dummy",
});
expect(config.middleware).toMatchObject({
Expand Down
16 changes: 10 additions & 6 deletions packages/cloudflare/src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from "@opennextjs/core/types/overrides.js";

import assetResolver from "./overrides/asset-resolver/index.js";
import serviceCache from "./overrides/cache/service-cache.js";

export type Override<T extends BaseOverride> = "dummy" | T | LazyLoadedOverride<T>;

Expand Down Expand Up @@ -89,16 +90,14 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
wrapper: "node" as const,
converter: "node" as const,
generateDockerfile: true,
incrementalCache: "dummy" as const,
tagCache: "dummy" as const,
cache: "dummy" as const,
queue: "dummy" as const,
}
: {
wrapper: "cloudflare-node" as const,
converter: "edge" as const,
proxyExternalRequest: "fetch" as const,
incrementalCache: resolveIncrementalCache(incrementalCache),
tagCache: resolveTagCache(tagCache),
cache: () => serviceCache,
queue: resolveQueue(queue),
cdnInvalidation: resolveCdnInvalidation(cachePurge),
};
Expand All @@ -108,6 +107,12 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
override: defaultOverride,
routePreloadingBehavior,
},
// The cache runs in the same worker, behind the `OpenNextCache` named entrypoint.
cacheHandler: {
incrementalCache: resolveIncrementalCache(incrementalCache),
tagCache: resolveTagCache(tagCache),
cdnInvalidation: resolveCdnInvalidation(cachePurge),
},
// node:crypto is used to compute cache keys
edgeExternals: ["node:crypto"],
cloudflare: {
Expand All @@ -120,8 +125,7 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
wrapper: "cloudflare-edge",
converter: "edge",
proxyExternalRequest: "fetch",
incrementalCache: resolveIncrementalCache(incrementalCache),
tagCache: resolveTagCache(tagCache),
cache: () => serviceCache,
queue: resolveQueue(queue),
},
assetResolver: () => assetResolver,
Expand Down
19 changes: 15 additions & 4 deletions packages/cloudflare/src/api/overrides/cache/service-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ describe("serviceCache", () => {
});

describe("get", () => {
it("requests the key and the cache type", async () => {
await serviceCache.get("key/with/slashes", "fetch");
it("requests the key, the cache type and the additional tags", async () => {
await serviceCache.get("key/with/slashes", "fetch", ["tag1", "tag2"]);

const { url, method } = lastRequest();
expect(method).toBe("GET");
expect(url.pathname).toBe(`/cache/${encodeURIComponent("key/with/slashes")}`);
expect(url.searchParams.get("type")).toBe("fetch");
expect(url.searchParams.get("tags")).toBe("tag1,tag2");
});

it("omits the tags when there is none", async () => {
await serviceCache.get("key", "cache", []);

expect(lastRequest().url.searchParams.has("tags")).toBe(false);
});

it("returns null on a cache miss", async () => {
Expand Down Expand Up @@ -63,13 +70,17 @@ describe("serviceCache", () => {
describe("set", () => {
// The cache type is part of the key for the incremental caches, it has to be forwarded
// or entries would be written where they are not read from.
it("sends the value and the cache type", async () => {
await serviceCache.set("key", { kind: "FETCH", data: { headers: {}, body: "b", url: "u" } }, "fetch");
it("sends the value, cache type, and additional tags", async () => {
await serviceCache.set("key", { kind: "FETCH", data: { headers: {}, body: "b", url: "u" } }, "fetch", [
"tag1",
"tag2",
]);

const { url, method, body } = lastRequest();
expect(method).toBe("PUT");
expect(url.pathname).toBe("/cache/key");
expect(url.searchParams.get("type")).toBe("fetch");
expect(url.searchParams.get("tags")).toBe("tag1,tag2");
expect(JSON.parse(body as string)).toEqual({
value: { kind: "FETCH", data: { headers: {}, body: "b", url: "u" } },
});
Expand Down
14 changes: 9 additions & 5 deletions packages/cloudflare/src/api/overrides/cache/service-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ function getCacheService(): Service {
return service;
}

function getCacheUrl(key: string, cacheType?: CacheEntryType) {
function getCacheUrl(key: string, cacheType?: CacheEntryType, additionalTags?: string[]) {
const url = new URL(`/cache/${encodeURIComponent(key)}`, CACHE_ORIGIN);

if (cacheType) {
url.searchParams.set("type", cacheType);
}
if (additionalTags && additionalTags.length > 0) {
url.searchParams.set("tags", additionalTags.join(","));
}

return url.href;
}

Expand All @@ -67,8 +71,8 @@ function ensureResponseOk(response: Pick<Response, "ok" | "status">, operation:
const serviceCache = {
name: NAME,

get: async (key, cacheType) => {
const response = await getCacheService().fetch(getCacheUrl(key, cacheType));
get: async (key, cacheType, additionalTags) => {
const response = await getCacheService().fetch(getCacheUrl(key, cacheType, additionalTags));

const body = await response.text();
const headers: Record<string, string> = {};
Expand All @@ -80,8 +84,8 @@ const serviceCache = {
return parseCacheGetResponse(headers, body) as any;
},

set: async (key, value, cacheType) => {
const response = await getCacheService().fetch(getCacheUrl(key, cacheType), {
set: async (key, value, cacheType, additionalTags) => {
const response = await getCacheService().fetch(getCacheUrl(key, cacheType, additionalTags), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ value }),
Expand Down
5 changes: 4 additions & 1 deletion packages/cloudflare/src/api/overrides/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export function computeCacheKey(key: string, options: KeyOptions) {

export function isPurgeCacheEnabled(): boolean {
// The `?` is required at `openNextConfig?` or the Open Next build fails because of a type error
const cdnInvalidation = globalThis.openNextConfig?.default?.override?.cdnInvalidation;
// The cache handler function only has `cacheHandler` populated, the other functions only have `default`.
const cdnInvalidation =
globalThis.openNextConfig?.cacheHandler?.cdnInvalidation ??
globalThis.openNextConfig?.default?.override?.cdnInvalidation;

return cdnInvalidation !== undefined && cdnInvalidation !== "dummy";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("ensureCloudflareConfig", () => {

test("rejects binding-backed caches in container mode", () => {
const config = defineCloudflareConfig({ container: true });
config.default.override!.incrementalCache = () => ({ name: "unsupported" }) as never;
config.default.override!.cache = () => ({ name: "unsupported" }) as never;

expect(() => ensureCloudflareConfig(config)).toThrow("Cloudflare Containers");
});
Expand Down
Loading
Loading