Skip to content

Commit 4c7f00a

Browse files
authored
Merge branch 'main' into type
2 parents 112ca67 + a3a76a6 commit 4c7f00a

18 files changed

Lines changed: 1216 additions & 828 deletions

.changeset/drop-unused-vercel-og.md

Lines changed: 0 additions & 9 deletions
This file was deleted.

examples/e2e/app-router/e2e/revalidateTag.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ test("Revalidate tag - stale data served first", async ({ page, request }) => {
105105
const staleResponse = await responsePromise;
106106
const staleHeaders = staleResponse.headers();
107107
const staleCache = staleHeaders["x-nextjs-cache"] ?? staleHeaders["x-opennext-cache"];
108-
expect(staleCache).toMatch(/^(STALE|HIT)$/);
108+
expect(staleCache).toMatch(/^(STALE)$/);
109109

110110
const staleTime = await page.getByTestId("cached-time").textContent();
111111
// Stale content must match the pre-revalidation value
@@ -129,6 +129,19 @@ test("Revalidate tag - stale data served first", async ({ page, request }) => {
129129
// After background regen the cached value must have been updated
130130
expect(freshTime).not.toBeNull();
131131
expect(freshTime).not.toEqual(originalTime);
132+
133+
// Now we want to verfiy that the next entries stays fresh (HIT) after the first stale entry
134+
responsePromise = page.waitForResponse(
135+
(response) => response.url().includes("/revalidate-tag/stale") && response.status() === 200
136+
);
137+
await page.goto("/revalidate-tag/stale");
138+
const finalResponse = await responsePromise;
139+
const finalHeaders = finalResponse.headers();
140+
const finalCache = finalHeaders["x-nextjs-cache"] ?? finalHeaders["x-opennext-cache"];
141+
expect(finalCache).toEqual("HIT");
142+
143+
const finalTime = await page.getByTestId("cached-time").textContent();
144+
expect(finalTime).toEqual(freshTime);
132145
});
133146

134147
test("Revalidate path", async ({ page, request }) => {

packages/cloudflare/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# @opennextjs/cloudflare
22

3+
## 1.19.4
4+
5+
### Patch Changes
6+
7+
- [#1221](https://github.com/opennextjs/opennextjs-cloudflare/pull/1221) [`a2679bf`](https://github.com/opennextjs/opennextjs-cloudflare/commit/a2679bf9549f620e1ab0e1900dcc7a6b6ac03e0a) Thanks [@mushan0x0](https://github.com/mushan0x0)! - Stop bundling `@vercel/og` (and its ~1.4 MiB `resvg.wasm`) when the app does not use it.
8+
9+
Next.js's `externalImport` helper keeps a dynamic `import("next/dist/compiled/@vercel/og/index.edge.js")` in the emitted handler even for apps that never use `ImageResponse` / `opengraph-image`. Previously this module was marked as `external` when `useOg` was `false`, which left Wrangler to resolve and bundle it — pulling in ~800 KiB of JS plus `resvg.wasm` and pushing many Workers over the Cloudflare free-tier 3 MiB gzip limit.
10+
11+
When `useOg` is `false`, the edge entry is now aliased to the existing `throw.js` shim, so the unreachable dynamic import resolves to a tiny module and the real `@vercel/og` library is no longer pulled into the Worker bundle.
12+
13+
- [#1208](https://github.com/opennextjs/opennextjs-cloudflare/pull/1208) [`2c5b472`](https://github.com/opennextjs/opennextjs-cloudflare/commit/2c5b4729b6a48560b550af820c46c2350e149fa6) Thanks [@edmundhung](https://github.com/edmundhung)! - Use `OPEN_NEXT_BUILD_ID` instead of `NEXT_BUILD_ID` in the cache keys.
14+
15+
As of Next 16.2 `NEXT_BUILD_ID` is a fixed value when deploymentId is set explicitly.
16+
17+
See <https://github.com/opennextjs/opennextjs-aws/pull/1144>
18+
19+
- [#1193](https://github.com/opennextjs/opennextjs-cloudflare/pull/1193) [`1e8d232`](https://github.com/opennextjs/opennextjs-cloudflare/commit/1e8d232672353920a8e05e468cf3a5890b82b0f6) Thanks [@conico974](https://github.com/conico974)! - Fix tag cache stale logic
20+
321
## 1.19.3
422

523
### Patch Changes

packages/cloudflare/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@opennextjs/cloudflare",
33
"description": "Cloudflare builder for next apps",
4-
"version": "1.19.3",
4+
"version": "1.19.4",
55
"type": "module",
66
"scripts": {
77
"clean": "rimraf dist",
@@ -54,7 +54,7 @@
5454
"dependencies": {
5555
"@ast-grep/napi": "^0.40.5",
5656
"@dotenvx/dotenvx": "catalog:",
57-
"@opennextjs/aws": "3.10.2",
57+
"@opennextjs/aws": "3.10.4",
5858
"ci-info": "^4.2.0",
5959
"cloudflare": "^4.4.1",
6060
"comment-json": "^4.5.1",

packages/cloudflare/src/api/durable-objects/queue.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class DOQueueHandler extends DurableObject<CloudflareEnv> {
165165
"INSERT OR REPLACE INTO sync (id, lastSuccess, buildId) VALUES (?, unixepoch(), ?)",
166166
// We cannot use the deduplication id because it's not unique per route - every time a route is revalidated, the deduplication id is different.
167167
`${host}${url}`,
168-
process.env.__NEXT_BUILD_ID
168+
process.env.__OPEN_NEXT_BUILD_ID
169169
);
170170
}
171171
// If everything went well, we can remove the route from the failed state
@@ -238,7 +238,7 @@ export class DOQueueHandler extends DurableObject<CloudflareEnv> {
238238
"INSERT OR REPLACE INTO failed_state (id, data, buildId) VALUES (?, ?, ?)",
239239
msg.MessageDeduplicationId,
240240
JSON.stringify(updatedFailedState),
241-
process.env.__NEXT_BUILD_ID
241+
process.env.__OPEN_NEXT_BUILD_ID
242242
);
243243
}
244244
// We probably want to do something if routeInFailedState is becoming too big, at least log it
@@ -273,8 +273,8 @@ export class DOQueueHandler extends DurableObject<CloudflareEnv> {
273273

274274
// Before doing anything else, we clear the DB for any potential old data
275275
// TODO: extract this to a function so that it could be called by the user at another time than init
276-
this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
277-
this.sql.exec("DELETE FROM sync WHERE buildId != ?", process.env.__NEXT_BUILD_ID);
276+
this.sql.exec("DELETE FROM failed_state WHERE buildId != ?", process.env.__OPEN_NEXT_BUILD_ID);
277+
this.sql.exec("DELETE FROM sync WHERE buildId != ?", process.env.__OPEN_NEXT_BUILD_ID);
278278

279279
const failedStateCursor = this.sql.exec<{ id: string; data: string }>("SELECT * FROM failed_state");
280280
for (const row of failedStateCursor) {

packages/cloudflare/src/api/overrides/incremental-cache/kv-incremental-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class KVIncrementalCache implements IncrementalCache {
102102
protected getKVKey(key: string, cacheType?: CacheEntryType): string {
103103
return computeCacheKey(key, {
104104
prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
105-
buildId: process.env.NEXT_BUILD_ID,
105+
buildId: process.env.OPEN_NEXT_BUILD_ID,
106106
cacheType,
107107
});
108108
}

packages/cloudflare/src/api/overrides/incremental-cache/r2-incremental-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class R2IncrementalCache implements IncrementalCache {
8282
protected getR2Key(key: string, cacheType?: CacheEntryType): string {
8383
return computeCacheKey(key, {
8484
prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
85-
buildId: process.env.NEXT_BUILD_ID,
85+
buildId: process.env.OPEN_NEXT_BUILD_ID,
8686
cacheType,
8787
});
8888
}

packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
IncrementalCache,
66
WithLastModified,
77
} from "@opennextjs/aws/types/overrides.js";
8+
import { compareSemver } from "@opennextjs/aws/utils/semver.js";
89

910
import { getCloudflareContext } from "../../cloudflare-context.js";
1011
import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry, isPurgeCacheEnabled } from "../internal.js";
@@ -31,21 +32,33 @@ type Options = {
3132
defaultLongLivedTtlSec?: number;
3233

3334
/**
34-
* Whether the regional cache entry should be updated in the background or not when it experiences
35-
* a cache hit.
35+
* Whether the regional cache entry should be updated in the background on regional cache hits.
3636
*
37-
* @default `true` in `long-lived` mode when cache purge is not used, `false` otherwise.
37+
* NOTE: Use the default value unless you know what you are doing. It is set to:
38+
* - Next < 16:
39+
* `true` in `long-lived` mode when cache purge is not used, `false` otherwise.
40+
* - Next >= 16:
41+
* `!bypassTagCacheOnCacheHit`
3842
*/
3943
shouldLazilyUpdateOnCacheHit?: boolean;
4044

4145
/**
42-
* Whether on cache hits the tagCache should be skipped or not. Skipping the tagCache allows requests to be
43-
* handled faster,
46+
* Whether the tagCache should be skipped on regional cache hits.
4447
*
45-
* Note: When this is enabled, make sure that the cache gets purged
46-
* either by enabling the auto cache purging feature or manually.
48+
* Note:
49+
* - Skipping the tagCache allows requests to be handled faster
50+
* - When `true`, make sure the cache gets purged
51+
* either by enabling the auto cache purging feature or manually
4752
*
48-
* @default `true` if the auto cache purging is enabled, `false` otherwise.
53+
* `true` is not compatible with SWR types of revalidateTag
54+
* i.e. on Next 16+, anything different than `revalidateTag("tag", { expire: 0 })`.
55+
* That's why the default is `false` for Next 16+ which uses SWR by default.
56+
*
57+
* NOTE: Use the default value unless you know what you are doing. It is set to:
58+
* - Next <16:
59+
* `true` if the auto cache purging is enabled, `false` otherwise.
60+
* - Next >= 16:
61+
* `false`
4962
*/
5063
bypassTagCacheOnCacheHit?: boolean;
5164
};
@@ -78,17 +91,33 @@ class RegionalCache implements IncrementalCache {
7891
private opts: Options
7992
) {
8093
this.name = this.store.name;
81-
// `shouldLazilyUpdateOnCacheHit` is not needed when cache purge is enabled.
82-
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
83-
}
8494

85-
get #bypassTagCacheOnCacheHit(): boolean {
86-
if (this.opts.bypassTagCacheOnCacheHit !== undefined) {
87-
return this.opts.bypassTagCacheOnCacheHit;
95+
// `globalThis.nextVersion` is only defined at runtime but not when the Open Next build runs.
96+
// The options do no matter at build time so we can skip setting them.
97+
const { nextVersion } = globalThis;
98+
if (nextVersion) {
99+
if (compareSemver(nextVersion, "<", "16")) {
100+
// Next < 16
101+
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived" && !isPurgeCacheEnabled();
102+
this.opts.bypassTagCacheOnCacheHit ??= isPurgeCacheEnabled();
103+
} else {
104+
// Next >= 16
105+
this.opts.bypassTagCacheOnCacheHit ??= false;
106+
if (this.opts.bypassTagCacheOnCacheHit) {
107+
debugCache(
108+
"RegionalCache",
109+
`bypassTagCacheOnCacheHit is not recommended for Next 16+ as it is not compatible with SWR tags. Make sure to always use \`revalidateTag\` with \`{ expire: 0 }\` if you want to bypass the tag cache.`
110+
);
111+
}
112+
this.opts.shouldLazilyUpdateOnCacheHit ??= !this.opts.bypassTagCacheOnCacheHit;
113+
if (this.opts.shouldLazilyUpdateOnCacheHit !== this.opts.bypassTagCacheOnCacheHit) {
114+
debugCache(
115+
"RegionalCache",
116+
`\`shouldLazilyUpdateOnCacheHit\` and \`bypassTagCacheOnCacheHit\` are mutually exclusive for Next 16+.`
117+
);
118+
}
119+
}
88120
}
89-
90-
// When `bypassTagCacheOnCacheHit` is not set, we default to whether the automatic cache purging is enabled or not
91-
return isPurgeCacheEnabled();
92121
}
93122

94123
async get<CacheType extends CacheEntryType = "cache">(
@@ -123,7 +152,7 @@ class RegionalCache implements IncrementalCache {
123152

124153
return {
125154
...responseJson,
126-
shouldBypassTagCache: this.#bypassTagCacheOnCacheHit,
155+
shouldBypassTagCache: this.opts.bypassTagCacheOnCacheHit,
127156
};
128157
}
129158

@@ -190,7 +219,7 @@ class RegionalCache implements IncrementalCache {
190219
}
191220

192221
protected getCacheUrlKey(key: string, cacheType?: CacheEntryType): string {
193-
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
222+
const buildId = process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
194223
return "http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${cacheType ?? "cache"}`;
195224
}
196225

packages/cloudflare/src/api/overrides/incremental-cache/static-assets-incremental-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class StaticAssetsIncrementalCache implements IncrementalCache {
6565
if (cacheType === "composable") {
6666
throw new Error("Composable cache is not supported in static assets incremental cache");
6767
}
68-
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
68+
const buildId = process.env.OPEN_NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
6969
const name = (
7070
cacheType === "fetch"
7171
? `${CACHE_DIR}/__fetch/${buildId}/${key}`

packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,10 @@ describe("D1NextModeTagCache", () => {
143143
expect(error).toHaveBeenCalledWith(mockError);
144144
});
145145

146-
it("should use custom build ID when NEXT_BUILD_ID is set", async () => {
146+
it("should prefer OPEN_NEXT_BUILD_ID when it is set", async () => {
147147
const customBuildId = "custom-build-id";
148-
vi.stubEnv("NEXT_BUILD_ID", customBuildId);
148+
vi.stubEnv("NEXT_BUILD_ID", "legacy-build-id");
149+
vi.stubEnv("OPEN_NEXT_BUILD_ID", customBuildId);
149150

150151
mockRaw.mockResolvedValue([[`${customBuildId}/tag1`, 123, 123, null]]);
151152

@@ -395,6 +396,17 @@ describe("D1NextModeTagCache", () => {
395396
expect(result).toBe(false);
396397
});
397398

399+
it("should return false when revalidatedAt <= lastModified even if stale > lastModified", async () => {
400+
const now = 2000;
401+
vi.spyOn(Date, "now").mockReturnValue(now);
402+
// revalidatedAt=500 <= lastModified=1000, so the stale window is from a previous ISR cycle
403+
mockRaw.mockResolvedValue([[`${FALLBACK_BUILD_ID}/tag1`, 500, 1500, null]]);
404+
405+
const result = await tagCache.isStale(["tag1"], 1000);
406+
407+
expect(result).toBe(false);
408+
});
409+
398410
it("should return false when expire <= now (tag expired)", async () => {
399411
const now = 2000;
400412
vi.spyOn(Date, "now").mockReturnValue(now);
@@ -534,9 +546,9 @@ describe("D1NextModeTagCache", () => {
534546
expect(cacheKey).toBe(`${FALLBACK_BUILD_ID}/${key}`);
535547
});
536548

537-
it("should use custom build ID when NEXT_BUILD_ID is set", () => {
549+
it("should use custom build ID when OPEN_NEXT_BUILD_ID is set", () => {
538550
const customBuildId = "custom-build-id";
539-
vi.stubEnv("NEXT_BUILD_ID", customBuildId);
551+
vi.stubEnv("OPEN_NEXT_BUILD_ID", customBuildId);
540552

541553
const key = "test-tag";
542554
const cacheKey = (tagCache as unknown as { getCacheKey: (key: string) => string }).getCacheKey(key);
@@ -545,7 +557,7 @@ describe("D1NextModeTagCache", () => {
545557
});
546558

547559
it("should handle double slashes by replacing them with single slash", () => {
548-
vi.stubEnv("NEXT_BUILD_ID", "build//id");
560+
vi.stubEnv("OPEN_NEXT_BUILD_ID", "build//id");
549561

550562
const key = "test-tag";
551563
const cacheKey = (tagCache as unknown as { getCacheKey: (key: string) => string }).getCacheKey(key);
@@ -555,16 +567,16 @@ describe("D1NextModeTagCache", () => {
555567
});
556568

557569
describe("getBuildId", () => {
558-
it("should return NEXT_BUILD_ID when set", () => {
570+
it("should return OPEN_NEXT_BUILD_ID when set", () => {
559571
const customBuildId = "custom-build-id";
560-
vi.stubEnv("NEXT_BUILD_ID", customBuildId);
572+
vi.stubEnv("OPEN_NEXT_BUILD_ID", customBuildId);
561573

562574
const buildId = (tagCache as unknown as { getBuildId: () => string }).getBuildId();
563575

564576
expect(buildId).toBe(customBuildId);
565577
});
566578

567-
it("should return fallback build ID when NEXT_BUILD_ID is not set", () => {
579+
it("should return fallback build ID when no build ID env vars are set", () => {
568580
const buildId = (tagCache as unknown as { getBuildId: () => string }).getBuildId();
569581

570582
expect(buildId).toBe(FALLBACK_BUILD_ID);

0 commit comments

Comments
 (0)