@opennextjs/aws 3.10.1 → 3.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,5 +22,10 @@ export const AppPathRoutesManifest =
22
22
  /* @__PURE__ */ loadAppPathRoutesManifest(NEXT_DIR);
23
23
  export const FunctionsConfigManifest =
24
24
  /* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR);
25
+ /**
26
+ * @deprecated Do not use NEXT_BUILD_ID above Next 16.2, as it could be a fixed value if used in conjonction with deploymentId
27
+ * We keep it for backward compatibility
28
+ */
25
29
  process.env.NEXT_BUILD_ID = BuildId;
30
+ process.env.OPEN_NEXT_BUILD_ID = NextConfig.deploymentId ?? BuildId;
26
31
  process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId;
@@ -22,7 +22,6 @@ export function compileCache(options, format = "cjs") {
22
22
  js: [
23
23
  `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`,
24
24
  `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`,
25
- `globalThis.nextVersion = "${options.nextVersion}";`,
26
25
  ].join(""),
27
26
  },
28
27
  }, options);
@@ -40,7 +39,6 @@ export function compileCache(options, format = "cjs") {
40
39
  js: [
41
40
  `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`,
42
41
  `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`,
43
- `globalThis.nextVersion = "${options.nextVersion}";`,
44
42
  ].join(""),
45
43
  },
46
44
  }, options);
@@ -80,7 +80,7 @@ function findNextPackageJsonPath(appPath, root) {
80
80
  : path.join(root, "./package.json");
81
81
  }
82
82
  export function esbuildSync(esbuildOptions, options) {
83
- const { openNextVersion, debug, minify } = options;
83
+ const { openNextVersion, debug, minify, nextVersion } = options;
84
84
  const result = buildSync({
85
85
  target: "esnext",
86
86
  format: "esm",
@@ -98,6 +98,7 @@ export function esbuildSync(esbuildOptions, options) {
98
98
  esbuildOptions.banner?.js || "",
99
99
  `globalThis.openNextDebug = ${debug};`,
100
100
  `globalThis.openNextVersion = "${openNextVersion}";`,
101
+ `globalThis.nextVersion = "${nextVersion}";`,
101
102
  ].join(""),
102
103
  },
103
104
  });
@@ -107,7 +108,7 @@ export function esbuildSync(esbuildOptions, options) {
107
108
  }
108
109
  }
109
110
  export async function esbuildAsync(esbuildOptions, options) {
110
- const { openNextVersion, debug, minify } = options;
111
+ const { openNextVersion, debug, minify, nextVersion } = options;
111
112
  // Dump ESBuild build metadata to file in debug mode
112
113
  const metafile = debug && esbuildOptions.outfile !== undefined;
113
114
  const result = await buildAsync({
@@ -132,6 +133,7 @@ export async function esbuildAsync(esbuildOptions, options) {
132
133
  esbuildOptions.banner?.js || "",
133
134
  `globalThis.openNextDebug = ${debug};`,
134
135
  `globalThis.openNextVersion = "${openNextVersion}";`,
136
+ `globalThis.nextVersion = "${nextVersion}";`,
135
137
  ].join(""),
136
138
  },
137
139
  });
@@ -11,9 +11,11 @@ export type RuleConfig = NapiConfig & {
11
11
  /**
12
12
  * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
13
13
  *
14
- * The rule must have a `fix` to rewrite the matched node.
15
- *
16
- * Tip: use https://ast-grep.github.io/playground.html to create rules.
14
+ * Notes:
15
+ * - The rule must not contain a `transform` field
16
+ * - The rule may contain a `fix` field, which is used to generate the edits.
17
+ * If not provided, no edits will be generated and the matches will be returned as is.
18
+ * - You can use https://ast-grep.github.io/playground.html to create and verify rules.
17
19
  *
18
20
  * @param rule The rule. Either a yaml string or an instance of `RuleConfig`
19
21
  * @param root The root node
@@ -31,9 +33,17 @@ export declare function applyRule(rule: string | RuleConfig, root: SgNode, { onc
31
33
  *
32
34
  * @param path The file path
33
35
  * @param lang The language to parse. Defaults to TypeScript.
34
- * @returns The root for the file.
36
+ * @returns The root node for the file.
35
37
  */
36
38
  export declare function parseFile(path: string, lang?: Lang): SgNode;
39
+ /**
40
+ * Parse a code string and obtain its root.
41
+ *
42
+ * @param code The code string
43
+ * @param lang The language to parse. Defaults to TypeScript.
44
+ * @returns The root node for the code string.
45
+ */
46
+ export declare function parseCode(code: string, lang?: Lang): SgNode;
37
47
  /**
38
48
  * Patches the code from by applying the rule.
39
49
  *
@@ -5,9 +5,11 @@ import yaml from "yaml";
5
5
  /**
6
6
  * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
7
7
  *
8
- * The rule must have a `fix` to rewrite the matched node.
9
- *
10
- * Tip: use https://ast-grep.github.io/playground.html to create rules.
8
+ * Notes:
9
+ * - The rule must not contain a `transform` field
10
+ * - The rule may contain a `fix` field, which is used to generate the edits.
11
+ * If not provided, no edits will be generated and the matches will be returned as is.
12
+ * - You can use https://ast-grep.github.io/playground.html to create and verify rules.
11
13
  *
12
14
  * @param rule The rule. Either a yaml string or an instance of `RuleConfig`
13
15
  * @param root The root node
@@ -19,24 +21,23 @@ export function applyRule(rule, root, { once = false } = {}) {
19
21
  if (ruleConfig.transform) {
20
22
  throw new Error("transform is not supported");
21
23
  }
22
- if (!ruleConfig.fix) {
23
- throw new Error("no fix to apply");
24
- }
25
- const fix = ruleConfig.fix;
26
24
  const matches = once
27
25
  ? [root.find(ruleConfig)].filter((m) => m !== null)
28
26
  : root.findAll(ruleConfig);
29
27
  const edits = [];
30
- matches.forEach((match) => {
31
- edits.push(match.replace(
32
- // Replace known placeholders by their value
33
- fix
34
- .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => match
35
- .getMultipleMatches(name)
36
- .map((n) => n.text())
37
- .join(""))
38
- .replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m)));
39
- });
28
+ const fix = ruleConfig.fix;
29
+ if (fix !== undefined) {
30
+ matches.forEach((match) => {
31
+ edits.push(match.replace(
32
+ // Replace known placeholders by their value
33
+ fix
34
+ .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => match
35
+ .getMultipleMatches(name)
36
+ .map((n) => n.text())
37
+ .join(""))
38
+ .replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m)));
39
+ });
40
+ }
40
41
  return { edits, matches };
41
42
  }
42
43
  /**
@@ -44,10 +45,20 @@ export function applyRule(rule, root, { once = false } = {}) {
44
45
  *
45
46
  * @param path The file path
46
47
  * @param lang The language to parse. Defaults to TypeScript.
47
- * @returns The root for the file.
48
+ * @returns The root node for the file.
48
49
  */
49
50
  export function parseFile(path, lang = Lang.TypeScript) {
50
- return parse(lang, readFileSync(path, { encoding: "utf-8" })).root();
51
+ return parseCode(readFileSync(path, { encoding: "utf-8" }), lang);
52
+ }
53
+ /**
54
+ * Parse a code string and obtain its root.
55
+ *
56
+ * @param code The code string
57
+ * @param lang The language to parse. Defaults to TypeScript.
58
+ * @returns The root node for the code string.
59
+ */
60
+ export function parseCode(code, lang = Lang.TypeScript) {
61
+ return parse(lang, code).root();
51
62
  }
52
63
  /**
53
64
  * Patches the code from by applying the rule.
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { NextConfig, PrerenderManifest } from "../../adapters/config/index";
3
3
  import { emptyReadableStream, toReadableStream } from "../../utils/stream";
4
4
  import { isBinaryContentType } from "../../utils/binary";
5
- import { getTagsFromValue, hasBeenRevalidated } from "../../utils/cache";
5
+ import { getTagsFromValue, hasBeenRevalidated, isStale } from "../../utils/cache";
6
6
  import { debug, error } from "../../adapters/logger";
7
7
  import { localizePath } from "./i18n";
8
8
  import { generateMessageGroupId } from "./queue";
@@ -21,7 +21,7 @@ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Rou
21
21
  const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
22
22
  const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
23
23
  const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
24
- async function computeCacheControl(path, body, host, revalidate, lastModified) {
24
+ async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false) {
25
25
  let finalRevalidate = CACHE_ONE_YEAR;
26
26
  const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1];
27
27
  if (revalidate === undefined && existingRoute) {
@@ -46,19 +46,23 @@ async function computeCacheControl(path, body, host, revalidate, lastModified) {
46
46
  etag,
47
47
  };
48
48
  }
49
- if (finalRevalidate !== CACHE_ONE_YEAR) {
50
- const sMaxAge = Math.max(finalRevalidate - age, 1);
49
+ // SSG uses one year cache
50
+ const isSSG = finalRevalidate === CACHE_ONE_YEAR;
51
+ const remainingTtl = Math.max(finalRevalidate - age, 1);
52
+ const isStaleFromTime = !isSSG && remainingTtl === 1;
53
+ const isStale = isStaleFromTime || isStaleFromTagCache;
54
+ if (!isSSG || isStaleFromTagCache) {
55
+ const sMaxAge = isStaleFromTagCache ? 1 : remainingTtl;
51
56
  debug("sMaxAge", {
52
57
  finalRevalidate,
53
58
  age,
54
59
  lastModified,
55
60
  revalidate,
61
+ isStaleFromTagCache,
56
62
  });
57
- const isStale = sMaxAge === 1;
58
63
  if (isStale) {
59
64
  let url = NextConfig.trailingSlash ? `${path}/` : path;
60
65
  if (NextConfig.basePath) {
61
- // We need to add the basePath to the url
62
66
  url = `${NextConfig.basePath}${url}`;
63
67
  }
64
68
  await globalThis.queue.send({
@@ -108,7 +112,7 @@ function getBodyForAppRouter(event, cachedValue) {
108
112
  return { body: cachedValue.rsc, additionalHeaders: {} };
109
113
  }
110
114
  }
111
- async function generateResult(event, localizedPath, cachedValue, lastModified) {
115
+ async function generateResult(event, localizedPath, cachedValue, lastModified, isStaleFromTagCache = false) {
112
116
  debug("Returning result from experimental cache");
113
117
  let body = "";
114
118
  let type = "application/octet-stream";
@@ -134,7 +138,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified) {
134
138
  else {
135
139
  throw new Error("generateResult called with unsupported cache value type, only 'app' and 'page' are supported");
136
140
  }
137
- const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified);
141
+ const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified, isStaleFromTagCache);
138
142
  return {
139
143
  type: "core",
140
144
  // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
@@ -213,10 +217,10 @@ export async function cacheInterceptor(event) {
213
217
  if (!cachedData?.value) {
214
218
  return event;
215
219
  }
220
+ const tags = getTagsFromValue(cachedData.value);
216
221
  // We need to check the tag cache now
217
222
  if (cachedData.value?.type === "app" ||
218
223
  cachedData.value?.type === "route") {
219
- const tags = getTagsFromValue(cachedData.value);
220
224
  const _hasBeenRevalidated = cachedData.shouldBypassTagCache
221
225
  ? false
222
226
  : await hasBeenRevalidated(localizedPath, tags, cachedData);
@@ -224,13 +228,17 @@ export async function cacheInterceptor(event) {
224
228
  return event;
225
229
  }
226
230
  }
231
+ // Check if the cache entry is stale (valid but needs background revalidation)
232
+ const _isStale = cachedData.shouldBypassTagCache
233
+ ? false
234
+ : await isStale(localizedPath, tags, cachedData.lastModified ?? Date.now());
227
235
  const host = event.headers.host;
228
236
  switch (cachedData?.value?.type) {
229
237
  case "app":
230
238
  case "page":
231
- return generateResult(event, localizedPath, cachedData.value, cachedData.lastModified);
239
+ return generateResult(event, localizedPath, cachedData.value, cachedData.lastModified, _isStale);
232
240
  case "redirect": {
233
- const cacheControl = await computeCacheControl(localizedPath, "", host, cachedData.value.revalidate, cachedData.lastModified);
241
+ const cacheControl = await computeCacheControl(localizedPath, "", host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
234
242
  return {
235
243
  type: "core",
236
244
  statusCode: cachedData.value.meta?.status ?? 307,
@@ -244,7 +252,7 @@ export async function cacheInterceptor(event) {
244
252
  };
245
253
  }
246
254
  case "route": {
247
- const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified);
255
+ const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
248
256
  const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"]));
249
257
  return {
250
258
  type: "core",
@@ -11,7 +11,7 @@ export default {
11
11
  ? [`/${path}`, `/${path}?_rsc=*`]
12
12
  : [
13
13
  `/${path}`,
14
- `/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
14
+ `/_next/data/${process.env.OPEN_NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
15
15
  ];
16
16
  });
17
17
  await cloudfront.send(new CreateInvalidationCommand({
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { getMonorepoRelativePath } from "../../utils/normalize-path";
4
- const buildId = process.env.NEXT_BUILD_ID;
4
+ const buildId = process.env.OPEN_NEXT_BUILD_ID;
5
5
  const basePath = path.join(getMonorepoRelativePath(), `cache/${buildId}`);
6
6
  const getCacheKey = (key) => {
7
7
  return path.join(basePath, `${key}.cache`);
@@ -24,8 +24,8 @@ const awsFetch = (body, type = "get") => {
24
24
  });
25
25
  };
26
26
  const buildDynamoKey = (key) => {
27
- const { NEXT_BUILD_ID } = process.env;
28
- return `__meta_${NEXT_BUILD_ID}_${key}`;
27
+ const { OPEN_NEXT_BUILD_ID } = process.env;
28
+ return `__meta_${OPEN_NEXT_BUILD_ID}_${key}`;
29
29
  };
30
30
  /**
31
31
  * This cache implementation uses a multi-tier cache with a local cache, a DynamoDB metadata cache and an S3 cache.
@@ -26,8 +26,8 @@ const awsFetch = async (key, options) => {
26
26
  return customFetchClient(client)(url, options);
27
27
  };
28
28
  function buildS3Key(key, extension) {
29
- const { CACHE_BUCKET_KEY_PREFIX, NEXT_BUILD_ID } = process.env;
30
- return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
29
+ const { CACHE_BUCKET_KEY_PREFIX, OPEN_NEXT_BUILD_ID } = process.env;
30
+ return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", OPEN_NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
31
31
  }
32
32
  const incrementalCache = {
33
33
  async get(key, cacheType) {
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
3
  import { awsLogger } from "../../adapters/logger";
4
4
  import { parseNumberFromEnv } from "../../adapters/util";
5
- const { CACHE_BUCKET_REGION, CACHE_BUCKET_KEY_PREFIX, NEXT_BUILD_ID, CACHE_BUCKET_NAME, } = process.env;
5
+ const { CACHE_BUCKET_REGION, CACHE_BUCKET_KEY_PREFIX, OPEN_NEXT_BUILD_ID, CACHE_BUCKET_NAME, } = process.env;
6
6
  function parseS3ClientConfigFromEnv() {
7
7
  return {
8
8
  region: CACHE_BUCKET_REGION,
@@ -12,7 +12,7 @@ function parseS3ClientConfigFromEnv() {
12
12
  }
13
13
  const s3Client = new S3Client(parseS3ClientConfigFromEnv());
14
14
  function buildS3Key(key, extension) {
15
- return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
15
+ return path.posix.join(CACHE_BUCKET_KEY_PREFIX ?? "", extension === "fetch" ? "__fetch" : "", OPEN_NEXT_BUILD_ID ?? "", extension === "fetch" ? key : `${key}.${extension}`);
16
16
  }
17
17
  const incrementalCache = {
18
18
  async get(key, cacheType) {
@@ -34,10 +34,10 @@ const awsFetch = (body, type = "query") => {
34
34
  });
35
35
  };
36
36
  function buildDynamoKey(key) {
37
- const { NEXT_BUILD_ID } = process.env;
37
+ const { OPEN_NEXT_BUILD_ID } = process.env;
38
38
  // FIXME: We should probably use something else than path.join here
39
39
  // this could transform some fetch cache key into a valid path
40
- return path.posix.join(NEXT_BUILD_ID ?? "", key);
40
+ return path.posix.join(OPEN_NEXT_BUILD_ID ?? "", key);
41
41
  }
42
42
  function buildDynamoObject(path, tags, revalidatedAt, stale, expire) {
43
43
  const obj = {
@@ -56,7 +56,7 @@ const tagCache = {
56
56
  if (globalThis.openNextConfig.dangerous?.disableTagCache) {
57
57
  return [];
58
58
  }
59
- const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
59
+ const { CACHE_DYNAMO_TABLE, OPEN_NEXT_BUILD_ID } = process.env;
60
60
  const store = globalThis.__openNextAls.getStore();
61
61
  const cache = store?.requestCache.getOrCreate("dynamoDb:getByPath");
62
62
  if (cache?.has(path)) {
@@ -80,7 +80,7 @@ const tagCache = {
80
80
  const tags = Items?.map((item) => item.tag.S ?? "") ?? [];
81
81
  debug("tags for path", path, tags);
82
82
  // We need to remove the buildId from the path
83
- const resultTags = tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
83
+ const resultTags = tags.map((tag) => tag.replace(`${OPEN_NEXT_BUILD_ID}/`, ""));
84
84
  cache?.set(path, resultTags);
85
85
  return resultTags;
86
86
  }
@@ -94,7 +94,7 @@ const tagCache = {
94
94
  if (globalThis.openNextConfig.dangerous?.disableTagCache) {
95
95
  return [];
96
96
  }
97
- const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
97
+ const { CACHE_DYNAMO_TABLE, OPEN_NEXT_BUILD_ID } = process.env;
98
98
  const store = globalThis.__openNextAls.getStore();
99
99
  const cache = store?.requestCache.getOrCreate("dynamoDb:getByTag");
100
100
  if (cache?.has(tag)) {
@@ -115,7 +115,7 @@ const tagCache = {
115
115
  }
116
116
  const { Items } = (await result.json());
117
117
  // We need to remove the buildId from the path
118
- const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? [];
118
+ const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${OPEN_NEXT_BUILD_ID}/`, "") ?? "") ?? [];
119
119
  cache?.set(tag, paths);
120
120
  return paths;
121
121
  }
@@ -33,10 +33,10 @@ const awsFetch = (body, type = "query") => {
33
33
  });
34
34
  };
35
35
  function buildDynamoKey(key) {
36
- const { NEXT_BUILD_ID } = process.env;
36
+ const { OPEN_NEXT_BUILD_ID } = process.env;
37
37
  // FIXME: We should probably use something else than path.join here
38
38
  // this could transform some fetch cache key into a valid path
39
- return path.posix.join(NEXT_BUILD_ID ?? "", "_tag", key);
39
+ return path.posix.join(OPEN_NEXT_BUILD_ID ?? "", "_tag", key);
40
40
  }
41
41
  // We use the same key for both path and tag
42
42
  // That's mostly for compatibility reason so that it's easier to use this with existing infra
@@ -157,7 +157,12 @@ export default {
157
157
  const compute = (item) => {
158
158
  if (!item?.stale?.N)
159
159
  return false;
160
- return Number.parseInt(item.stale.N) > (lastModified ?? 0);
160
+ const revalidatedAt = Number.parseInt(item.revalidatedAt?.N ?? "0");
161
+ // A tag is stale when both its stale timestamp and its revalidatedAt are newer than the page.
162
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
163
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
164
+ return (revalidatedAt > (lastModified ?? 0) &&
165
+ Number.parseInt(item.stale.N) >= (lastModified ?? 0));
161
166
  };
162
167
  const { uncachedTags, hasMatch } = checkItemsCache(tags, itemsCache, compute);
163
168
  if (hasMatch)
@@ -3,7 +3,7 @@ import { BatchWriteItemCommand, DynamoDBClient, QueryCommand, } from "@aws-sdk/c
3
3
  import { awsLogger, debug, error } from "../../adapters/logger";
4
4
  import { chunk, parseNumberFromEnv } from "../../adapters/util";
5
5
  import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency, } from "./constants";
6
- const { CACHE_BUCKET_REGION, CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
6
+ const { CACHE_BUCKET_REGION, CACHE_DYNAMO_TABLE, OPEN_NEXT_BUILD_ID } = process.env;
7
7
  function parseDynamoClientConfigFromEnv() {
8
8
  return {
9
9
  region: CACHE_BUCKET_REGION,
@@ -15,7 +15,7 @@ const dynamoClient = new DynamoDBClient(parseDynamoClientConfigFromEnv());
15
15
  function buildDynamoKey(key) {
16
16
  // FIXME: We should probably use something else than path.join here
17
17
  // this could transform some fetch cache key into a valid path
18
- return path.posix.join(NEXT_BUILD_ID ?? "", key);
18
+ return path.posix.join(OPEN_NEXT_BUILD_ID ?? "", key);
19
19
  }
20
20
  function buildDynamoObject(path, tags, revalidatedAt, stale, expire) {
21
21
  const obj = {
@@ -53,7 +53,7 @@ const tagCache = {
53
53
  const tags = result.Items?.map((item) => item.tag.S ?? "") ?? [];
54
54
  debug("tags for path", path, tags);
55
55
  // We need to remove the buildId from the path
56
- const resultTags = tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
56
+ const resultTags = tags.map((tag) => tag.replace(`${OPEN_NEXT_BUILD_ID}/`, ""));
57
57
  cache?.set(path, resultTags);
58
58
  return resultTags;
59
59
  }
@@ -83,7 +83,7 @@ const tagCache = {
83
83
  },
84
84
  }));
85
85
  // We need to remove the buildId from the path
86
- const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? [];
86
+ const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${OPEN_NEXT_BUILD_ID}/`, "") ?? "") ?? [];
87
87
  cache?.set(tag, paths);
88
88
  return paths;
89
89
  }
@@ -47,7 +47,11 @@ export default {
47
47
  if (!tagData || typeof tagData.stale !== "number") {
48
48
  return false;
49
49
  }
50
- return tagData.stale > (lastModified ?? 0);
50
+ // A tag is stale when both its stale timestamp and its revalidatedAt are newer than the page.
51
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
52
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
53
+ return (tagData.revalidatedAt > (lastModified ?? 0) &&
54
+ tagData.stale >= (lastModified ?? 0));
51
55
  });
52
56
  debug("isStale result:", hasStaleTag);
53
57
  return hasStaleTag;
@@ -4,9 +4,9 @@ import { getMonorepoRelativePath } from "../../utils/normalize-path";
4
4
  const tagFile = path.join(getMonorepoRelativePath(), "dynamodb-provider/dynamodb-cache.json");
5
5
  const tagContent = fs.readFileSync(tagFile, "utf-8");
6
6
  let tags = JSON.parse(tagContent);
7
- const { NEXT_BUILD_ID } = process.env;
7
+ const { OPEN_NEXT_BUILD_ID } = process.env;
8
8
  function buildKey(key) {
9
- return path.posix.join(NEXT_BUILD_ID ?? "", key);
9
+ return path.posix.join(OPEN_NEXT_BUILD_ID ?? "", key);
10
10
  }
11
11
  const tagCache = {
12
12
  name: "fs-dev",
@@ -14,12 +14,12 @@ const tagCache = {
14
14
  getByPath: async (path) => {
15
15
  return tags
16
16
  .filter((tagPathMapping) => tagPathMapping.path.S === buildKey(path))
17
- .map((tag) => tag.tag.S.replace(`${NEXT_BUILD_ID}/`, ""));
17
+ .map((tag) => tag.tag.S.replace(`${OPEN_NEXT_BUILD_ID}/`, ""));
18
18
  },
19
19
  getByTag: async (tag) => {
20
20
  return tags
21
21
  .filter((tagPathMapping) => tagPathMapping.tag.S === buildKey(tag))
22
- .map((tagEntry) => tagEntry.path.S.replace(`${NEXT_BUILD_ID}/`, ""));
22
+ .map((tagEntry) => tagEntry.path.S.replace(`${OPEN_NEXT_BUILD_ID}/`, ""));
23
23
  },
24
24
  getLastModified: async (path, lastModified) => {
25
25
  // Check if any tag has expired
@@ -46,7 +46,11 @@ const tagCache = {
46
46
  return false;
47
47
  if (!entry.stale?.N)
48
48
  return false;
49
- return Number.parseInt(entry.stale.N) > (lastModified ?? 0);
49
+ // A tag is stale when both its stale timestamp and its revalidatedAt are newer than the page.
50
+ // revalidatedAt > lastModified ensures the revalidation that set this stale window happened
51
+ // after the page was generated, preventing a stale signal from a previous ISR cycle.
52
+ return (Number.parseInt(entry.revalidatedAt.N) > (lastModified ?? 0) &&
53
+ Number.parseInt(entry.stale.N) > (lastModified ?? 0));
50
54
  });
51
55
  },
52
56
  writeTags: async (newTags) => {
@@ -145,6 +145,7 @@ ${contents}
145
145
  export const PagesManifest = ${JSON.stringify(PagesManifest)};
146
146
 
147
147
  process.env.NEXT_BUILD_ID = BuildId;
148
+ process.env.OPEN_NEXT_BUILD_ID = NextConfig.deploymentId ?? BuildId;
148
149
  process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId;
149
150
  `;
150
151
  return { contents };
@@ -90,12 +90,6 @@ declare global {
90
90
  * Defined in esbuild banner for the cache adapter.
91
91
  */
92
92
  var disableIncrementalCache: boolean;
93
- /**
94
- * The Next.js version of the application.
95
- * Only available in the cache adapter.
96
- * Defined in the esbuild banner for the cache adapter.
97
- */
98
- var nextVersion: string;
99
93
  /**
100
94
  * A boolean that indicates if the runtime is Edge.
101
95
  * Only available in `edge` runtime functions (i.e. external middleware or function with edge runtime).
@@ -167,11 +161,12 @@ declare global {
167
161
  */
168
162
  var AsyncLocalStorage: any;
169
163
  /**
170
- * The version of the Open Next runtime.
164
+ * The versions of Open Next and Next.js.
171
165
  * Available everywhere.
172
166
  * Defined in the esbuild banner.
173
167
  */
174
168
  var openNextVersion: string;
169
+ var nextVersion: string;
175
170
  /**
176
171
  * The function that is used when resolving external rewrite requests.
177
172
  * Only available in main functions
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.10.1",
6
+ "version": "3.10.3",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -60,7 +60,7 @@
60
60
  "typescript": "5.9.3"
61
61
  },
62
62
  "peerDependencies": {
63
- "next": ">=15.5.15 || >=16.2.3"
63
+ "next": ">=15.5.15 <16 || >=16.2.3"
64
64
  },
65
65
  "bugs": {
66
66
  "url": "https://github.com/opennextjs/opennextjs-aws/issues"