@opennextjs/aws 4.1.12 → 4.1.14

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.
@@ -12,9 +12,29 @@ export default class Cache {
12
12
  getIncrementalCache(key: string): Promise<CacheHandlerValue | null>;
13
13
  set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise<void>;
14
14
  private writeCache;
15
+ /**
16
+ * Revalidates cache entries associated with one or more tags.
17
+ *
18
+ * The tag cache is updated before the CDN is invalidated. CDN handlers that
19
+ * support tag invalidation receive the original tags; other handlers retain
20
+ * the existing path-based invalidation behavior.
21
+ *
22
+ * @param tags Tags whose cache entries should be revalidated.
23
+ * @param durations Optional stale and expiry durations.
24
+ * @returns A promise that resolves when revalidation is complete.
25
+ */
15
26
  revalidateTag(tags: string | string[], durations?: {
16
27
  expire?: number;
17
28
  }): Promise<void>;
18
29
  private updateTagsOnSet;
19
- private extractRevalidateForSet;
30
+ /**
31
+ * Returns the lifetime Next.js passed to `set()`.
32
+ *
33
+ * It is stored with the entry so that `get()` can report it. Only the newest context
34
+ * shape carries an `expire`.
35
+ *
36
+ * @param ctx The context passed to `set()`
37
+ * @returns The lifetime, empty when there is none
38
+ */
39
+ private extractCacheControlForSet;
20
40
  }
@@ -1,7 +1,7 @@
1
1
  import { getStaleLastModified, getTagsFromValue, hasBeenRevalidated, isStale, writeTags, } from "../utils/cache.js";
2
2
  import { isBinaryContentType } from "../utils/binary";
3
3
  import { CACHE_TAGS_HEADER } from "../utils/cacheHeaders";
4
- import { getPathFromRouteCacheKey } from "../utils/routeCacheKey";
4
+ import { getPathFromRouteCacheKey, useRouteCacheKeys, } from "../utils/routeCacheKey";
5
5
  import { compareSemver } from "../utils/semver";
6
6
  import { debug, error, warn } from "./logger";
7
7
  export const SOFT_TAG_PREFIX = "_N_T_/";
@@ -16,6 +16,78 @@ function isFetchCache(options) {
16
16
  }
17
17
  return false;
18
18
  }
19
+ /**
20
+ * Returns the lifetime to report to Next.js for a cache entry.
21
+ *
22
+ * Next.js takes it from a per-route record that only the instance which rendered the page
23
+ * has filled, with the prerender manifest as a fallback. A page rendered on demand by a
24
+ * dynamic route is in neither, and Next.js then falls back to `revalidate: 1`. The
25
+ * lifetime stored with the entry fills that gap.
26
+ *
27
+ * A stale entry is signalled by offsetting `lastModified` by `revalidate` (see
28
+ * `getStaleLastModified`), which needs a numeric `revalidate`. Without one, Next.js would use
29
+ * its own lifetime: `revalidate: false` serves the entry as a hit, and the `expire` of the
30
+ * prerender manifest turns it into a blocking revalidation. `{ revalidate: 1 }` is reported
31
+ * instead, Next.js records the actual lifetime again when the entry is re-rendered.
32
+ *
33
+ * @param value The cache entry
34
+ * @param isEntryStale Whether the tag cache reported the entry as stale
35
+ * @returns The lifetime to report, or `undefined` to leave it to Next.js
36
+ */
37
+ function getCacheControl({ revalidate, expire }, isEntryStale) {
38
+ if (isEntryStale && typeof revalidate !== "number") {
39
+ return { revalidate: 1 };
40
+ }
41
+ // A build time entry has no stored lifetime, the prerender manifest has it.
42
+ if (revalidate === undefined) {
43
+ return undefined;
44
+ }
45
+ return { revalidate, expire };
46
+ }
47
+ // `null` until the module is loaded, `undefined` when it can not be loaded.
48
+ let sharedCacheControls = null;
49
+ /**
50
+ * Returns the map Next.js keeps the lifetime of routes in.
51
+ *
52
+ * The module is loaded lazily, and only once: a static import would pull `next` into the
53
+ * external middleware bundle. `next` is external to the compiled cache handler, so this is
54
+ * the map the server reads.
55
+ *
56
+ * @returns The map, or `undefined` when the module can not be loaded
57
+ */
58
+ async function getSharedCacheControls() {
59
+ if (sharedCacheControls === null) {
60
+ try {
61
+ const { SharedCacheControls } = await import("next/dist/server/lib/incremental-cache/shared-cache-controls.external.js");
62
+ // `cacheControls` is only private to TypeScript.
63
+ sharedCacheControls = SharedCacheControls.cacheControls;
64
+ }
65
+ catch (e) {
66
+ debug("Failed to load the Next.js cache controls", e);
67
+ sharedCacheControls = undefined;
68
+ }
69
+ }
70
+ return sharedCacheControls;
71
+ }
72
+ /**
73
+ * Records the lifetime of a cache entry where Next.js < 16.4 reads it.
74
+ *
75
+ * Next.js 16.4+ reads the `cacheControl` returned by `get()` instead. The map is keyed by
76
+ * route cache key, which is the key passed to the cache handler. Versions without route
77
+ * cache keys are skipped. Like 16.4, the lifetime of the entry overwrites the recorded one.
78
+ *
79
+ * @param key The key of the cache entry
80
+ * @param cacheControl The lifetime to record
81
+ */
82
+ async function recordCacheControl(key, cacheControl) {
83
+ const nextVersion = globalThis.nextVersion;
84
+ if (!cacheControl ||
85
+ compareSemver(nextVersion, ">=", "16.4.0") ||
86
+ !useRouteCacheKeys(nextVersion)) {
87
+ return;
88
+ }
89
+ (await getSharedCacheControls())?.set(key, cacheControl);
90
+ }
19
91
  // We need to use globalThis client here as this class can be defined at load time in next 12 but client is not available at load time
20
92
  export default class Cache {
21
93
  async get(key,
@@ -101,9 +173,12 @@ export default class Cache {
101
173
  if (_isStale) {
102
174
  _lastModified = getStaleLastModified(cacheData.revalidate);
103
175
  }
176
+ const _cacheControl = getCacheControl(cacheData, _isStale);
177
+ await recordCacheControl(key, _cacheControl);
104
178
  if (cacheData?.type === "route") {
105
179
  return {
106
180
  lastModified: _lastModified,
181
+ cacheControl: _cacheControl,
107
182
  value: {
108
183
  kind: compareSemver(globalThis.nextVersion, ">=", "15.0.0")
109
184
  ? "APP_ROUTE"
@@ -137,6 +212,7 @@ export default class Cache {
137
212
  }
138
213
  return {
139
214
  lastModified: _lastModified,
215
+ cacheControl: _cacheControl,
140
216
  value: {
141
217
  kind: "APP_PAGE",
142
218
  html: cacheData.html,
@@ -163,6 +239,7 @@ export default class Cache {
163
239
  }
164
240
  return {
165
241
  lastModified: _lastModified,
242
+ cacheControl: _cacheControl,
166
243
  value: {
167
244
  kind: compareSemver(globalThis.nextVersion, ">=", "15.0.0")
168
245
  ? "PAGES"
@@ -177,6 +254,7 @@ export default class Cache {
177
254
  if (cacheData?.type === "redirect") {
178
255
  return {
179
256
  lastModified: _lastModified,
257
+ cacheControl: _cacheControl,
180
258
  value: {
181
259
  kind: "REDIRECT",
182
260
  props: cacheData.props,
@@ -214,7 +292,7 @@ export default class Cache {
214
292
  await globalThis.incrementalCache.delete(key);
215
293
  }
216
294
  else {
217
- const revalidate = this.extractRevalidateForSet(ctx);
295
+ const cacheControl = this.extractCacheControlForSet(ctx);
218
296
  switch (data.kind) {
219
297
  case "ROUTE":
220
298
  case "APP_ROUTE": {
@@ -228,7 +306,7 @@ export default class Cache {
228
306
  status,
229
307
  headers,
230
308
  },
231
- revalidate,
309
+ ...cacheControl,
232
310
  }, "cache");
233
311
  break;
234
312
  }
@@ -245,7 +323,7 @@ export default class Cache {
245
323
  status,
246
324
  headers,
247
325
  },
248
- revalidate,
326
+ ...cacheControl,
249
327
  }, "cache");
250
328
  }
251
329
  else {
@@ -253,7 +331,7 @@ export default class Cache {
253
331
  type: "page",
254
332
  html,
255
333
  json: pageData,
256
- revalidate,
334
+ ...cacheControl,
257
335
  }, "cache");
258
336
  }
259
337
  break;
@@ -275,7 +353,7 @@ export default class Cache {
275
353
  headers,
276
354
  postponed,
277
355
  },
278
- revalidate,
356
+ ...cacheControl,
279
357
  segmentData: segmentData ? segmentToWrite : undefined,
280
358
  }, "cache");
281
359
  break;
@@ -287,7 +365,7 @@ export default class Cache {
287
365
  await globalThis.incrementalCache.set(key, {
288
366
  type: "redirect",
289
367
  props: data.props,
290
- revalidate,
368
+ ...cacheControl,
291
369
  }, "cache");
292
370
  break;
293
371
  case "IMAGE":
@@ -302,6 +380,17 @@ export default class Cache {
302
380
  error("Failed to set cache", e);
303
381
  }
304
382
  }
383
+ /**
384
+ * Revalidates cache entries associated with one or more tags.
385
+ *
386
+ * The tag cache is updated before the CDN is invalidated. CDN handlers that
387
+ * support tag invalidation receive the original tags; other handlers retain
388
+ * the existing path-based invalidation behavior.
389
+ *
390
+ * @param tags Tags whose cache entries should be revalidated.
391
+ * @param durations Optional stale and expiry durations.
392
+ * @returns A promise that resolves when revalidation is complete.
393
+ */
305
394
  async revalidateTag(tags, durations) {
306
395
  const config = globalThis.openNextConfig.dangerous;
307
396
  if (config?.disableTagCache || config?.disableIncrementalCache) {
@@ -312,8 +401,8 @@ export default class Cache {
312
401
  return;
313
402
  }
314
403
  try {
404
+ const cdnInvalidationHandler = globalThis.cdnInvalidationHandler;
315
405
  if (globalThis.tagCache.mode === "nextMode") {
316
- const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
317
406
  const now = Date.now();
318
407
  const tagsToWrite = _tags.map((tag) => {
319
408
  if (durations) {
@@ -333,21 +422,25 @@ export default class Cache {
333
422
  };
334
423
  });
335
424
  await writeTags(tagsToWrite);
336
- if (paths.length > 0) {
337
- // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
338
- // It also means that we'll need to provide the tags used in every request to the wrapper or converter.
339
- // Fixed Next.js releases store route-scoped cache keys in the tag cache.
340
- await globalThis.cdnInvalidationHandler.invalidatePaths(paths.map(getPathFromRouteCacheKey).map((path) => ({
341
- initialPath: path,
342
- rawPath: path,
343
- resolvedRoutes: [
344
- {
345
- route: path,
346
- // TODO: ideally here we should check if it's an app router page or route
347
- type: "app",
348
- },
349
- ],
350
- })));
425
+ if (cdnInvalidationHandler.invalidateTags) {
426
+ await cdnInvalidationHandler.invalidateTags(_tags);
427
+ }
428
+ else {
429
+ const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
430
+ if (paths.length > 0) {
431
+ // Fixed Next.js releases store route-scoped cache keys in the tag cache.
432
+ await cdnInvalidationHandler.invalidatePaths(paths.map(getPathFromRouteCacheKey).map((path) => ({
433
+ initialPath: path,
434
+ rawPath: path,
435
+ resolvedRoutes: [
436
+ {
437
+ route: path,
438
+ // TODO: ideally here we should check if it's an app router page or route
439
+ type: "app",
440
+ },
441
+ ],
442
+ })));
443
+ }
351
444
  }
352
445
  return;
353
446
  }
@@ -406,6 +499,9 @@ export default class Cache {
406
499
  }
407
500
  // Update all keys with the given tag with revalidatedAt set to now
408
501
  await writeTags(toInsert);
502
+ if (cdnInvalidationHandler.invalidateTags) {
503
+ continue;
504
+ }
409
505
  // We can now invalidate all paths in the CDN
410
506
  // This only applies to `revalidateTag`, not to `res.revalidate()`
411
507
  const uniquePaths = Array.from(new Set(toInsert
@@ -414,7 +510,7 @@ export default class Cache {
414
510
  // Fixed Next.js releases store route-scoped cache keys in the tag cache.
415
511
  .map((t) => getPathFromRouteCacheKey(`/${t.path}`))));
416
512
  if (uniquePaths.length > 0) {
417
- await globalThis.cdnInvalidationHandler.invalidatePaths(uniquePaths.map((path) => ({
513
+ await cdnInvalidationHandler.invalidatePaths(uniquePaths.map((path) => ({
418
514
  initialPath: path,
419
515
  rawPath: path,
420
516
  resolvedRoutes: [
@@ -427,6 +523,9 @@ export default class Cache {
427
523
  })));
428
524
  }
429
525
  }
526
+ if (cdnInvalidationHandler.invalidateTags) {
527
+ await cdnInvalidationHandler.invalidateTags(_tags);
528
+ }
430
529
  }
431
530
  catch (e) {
432
531
  error("Failed to revalidate tag", e);
@@ -471,19 +570,28 @@ export default class Cache {
471
570
  })));
472
571
  }
473
572
  }
474
- extractRevalidateForSet(ctx) {
573
+ /**
574
+ * Returns the lifetime Next.js passed to `set()`.
575
+ *
576
+ * It is stored with the entry so that `get()` can report it. Only the newest context
577
+ * shape carries an `expire`.
578
+ *
579
+ * @param ctx The context passed to `set()`
580
+ * @returns The lifetime, empty when there is none
581
+ */
582
+ extractCacheControlForSet(ctx) {
475
583
  if (ctx === undefined) {
476
- return undefined;
584
+ return {};
477
585
  }
478
586
  if (typeof ctx === "number" || ctx === false) {
479
- return ctx;
587
+ return { revalidate: ctx };
480
588
  }
481
589
  if ("revalidate" in ctx) {
482
- return ctx.revalidate;
590
+ return { revalidate: ctx.revalidate };
483
591
  }
484
592
  if ("cacheControl" in ctx) {
485
- return ctx.cacheControl?.revalidate;
593
+ return { ...ctx.cacheControl };
486
594
  }
487
- return undefined;
595
+ return {};
488
596
  }
489
597
  }
@@ -1,10 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { debug } from "../logger";
3
- import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "./util.js";
3
+ import { getOpenNextBuildId, loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "./util.js";
4
4
  export const NEXT_DIR = path.join(__dirname, ".next");
5
5
  export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next");
6
6
  debug({ NEXT_DIR, OPEN_NEXT_DIR });
7
7
  export const NextConfig = /* @__PURE__ */ loadConfig(NEXT_DIR);
8
+ // Next.js build id, only use it for URLs (see `getOpenNextBuildId`)
8
9
  export const BuildId = /* @__PURE__ */ loadBuildId(NEXT_DIR);
9
10
  export const HtmlPages = /* @__PURE__ */ loadHtmlPages(NEXT_DIR);
10
11
  // export const PublicAssets = loadPublicAssets(OPEN_NEXT_DIR);
@@ -23,9 +24,18 @@ export const AppPathRoutesManifest =
23
24
  export const FunctionsConfigManifest =
24
25
  /* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR);
25
26
  /**
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
27
+ * Next.js build id (`.next/BUILD_ID`).
28
+ *
29
+ * Only use it for URLs generated by Next (i.e. `/_next/data/<BUILD_ID>/...`).
30
+ * Do not use it to scope storage: since Next 16.2 it is a constant when `deploymentId` is set.
31
+ * Use `OPEN_NEXT_BUILD_ID` instead.
28
32
  */
29
33
  process.env.NEXT_BUILD_ID = BuildId;
30
- process.env.OPEN_NEXT_BUILD_ID = NextConfig.deploymentId ?? BuildId;
34
+ /**
35
+ * Open Next build id, unique per deployment.
36
+ *
37
+ * Use it to prefix the cache keys (incremental cache, tag cache, ...).
38
+ * See `getOpenNextBuildId` for details.
39
+ */
40
+ process.env.OPEN_NEXT_BUILD_ID = getOpenNextBuildId(NextConfig, BuildId);
31
41
  process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId;
@@ -2,6 +2,25 @@ import type { FunctionsConfigManifest, MiddlewareManifest, NextConfig, Prerender
2
2
  import type { PublicFiles } from "../../build";
3
3
  export declare function loadConfig(nextDir: string): NextConfig;
4
4
  export declare function loadBuildId(nextDir: string): string;
5
+ /**
6
+ * Returns the Open Next build id, used to scope data to a deployment.
7
+ *
8
+ * Next.js uses two different ids:
9
+ * - The build id (`.next/BUILD_ID`) is part of the URLs generated by Next (i.e. `/_next/data/<BUILD_ID>/...`).
10
+ * Since Next 16.2 it is a constant (`build-TfctsWXpff2fKS`) when `deploymentId` is set without `generateBuildId`.
11
+ * - The deployment id (`deploymentId`) is unique per deployment when set.
12
+ *
13
+ * The Open Next build id is the deployment id when set, the build id otherwise.
14
+ * It must be used to prefix anything stored across deployments (incremental cache, tag cache, ...),
15
+ * both at build time (seeded data) and at runtime (via `process.env.OPEN_NEXT_BUILD_ID`).
16
+ *
17
+ * Use the Next build id for URLs, and the Open Next build id for storage keys.
18
+ *
19
+ * @param nextConfig The Next.js config (from `required-server-files.json`).
20
+ * @param buildId The Next.js build id (from `.next/BUILD_ID`).
21
+ * @returns The Open Next build id.
22
+ */
23
+ export declare function getOpenNextBuildId(nextConfig: Pick<NextConfig, "deploymentId">, buildId: string): string;
5
24
  export declare function loadPagesManifest(nextDir: string): Record<string, string>;
6
25
  export declare function loadHtmlPages(nextDir: string): string[];
7
26
  export declare function loadPublicAssets(openNextDir: string): PublicFiles;
@@ -10,6 +10,27 @@ export function loadBuildId(nextDir) {
10
10
  const filePath = path.join(nextDir, "BUILD_ID");
11
11
  return fs.readFileSync(filePath, "utf-8").trim();
12
12
  }
13
+ /**
14
+ * Returns the Open Next build id, used to scope data to a deployment.
15
+ *
16
+ * Next.js uses two different ids:
17
+ * - The build id (`.next/BUILD_ID`) is part of the URLs generated by Next (i.e. `/_next/data/<BUILD_ID>/...`).
18
+ * Since Next 16.2 it is a constant (`build-TfctsWXpff2fKS`) when `deploymentId` is set without `generateBuildId`.
19
+ * - The deployment id (`deploymentId`) is unique per deployment when set.
20
+ *
21
+ * The Open Next build id is the deployment id when set, the build id otherwise.
22
+ * It must be used to prefix anything stored across deployments (incremental cache, tag cache, ...),
23
+ * both at build time (seeded data) and at runtime (via `process.env.OPEN_NEXT_BUILD_ID`).
24
+ *
25
+ * Use the Next build id for URLs, and the Open Next build id for storage keys.
26
+ *
27
+ * @param nextConfig The Next.js config (from `required-server-files.json`).
28
+ * @param buildId The Next.js build id (from `.next/BUILD_ID`).
29
+ * @returns The Open Next build id.
30
+ */
31
+ export function getOpenNextBuildId(nextConfig, buildId) {
32
+ return nextConfig.deploymentId || buildId;
33
+ }
13
34
  export function loadPagesManifest(nextDir) {
14
35
  const filePath = path.join(nextDir, "server/pages-manifest.json");
15
36
  const json = fs.readFileSync(filePath, "utf-8");
@@ -1,13 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import https from "node:https";
3
3
  import path from "node:path";
4
- import { loadBuildId, loadConfig } from "./config/util.js";
4
+ import { getOpenNextBuildId, loadBuildId, loadConfig } from "./config/util.js";
5
5
  import { OpenNextNodeResponse } from "../http/openNextResponse.js";
6
6
  // @ts-ignore
7
7
  import { defaultConfig } from "next/dist/server/config-shared";
8
8
  import { ImageOptimizerCache,
9
9
  // @ts-ignore
10
10
  } from "next/dist/server/image-optimizer";
11
+ import { compareSemver } from "../utils/semver.js";
11
12
  import { emptyReadableStream, toReadableStream } from "../utils/stream.js";
12
13
  import { createGenericHandler } from "../core/createGenericHandler.js";
13
14
  import { resolveImageLoader } from "../core/resolve.js";
@@ -18,6 +19,11 @@ setNodeEnv();
18
19
  const nextDir = path.join(__dirname, ".next");
19
20
  const config = loadConfig(nextDir);
20
21
  const buildId = loadBuildId(nextDir);
22
+ // Used to compute the static ETag, it must change on every deployment.
23
+ // Since Next 16.2, the build id is a constant when `deploymentId` is set.
24
+ const etagBuildId = compareSemver(globalThis.nextVersion, ">=", "16.2.0")
25
+ ? getOpenNextBuildId(config, buildId)
26
+ : buildId;
21
27
  const nextConfig = {
22
28
  ...defaultConfig,
23
29
  images: {
@@ -96,7 +102,7 @@ function computeEtag(imageParams) {
96
102
  href: imageParams.href,
97
103
  width: imageParams.width,
98
104
  quality: imageParams.quality,
99
- buildId,
105
+ buildId: etagBuildId,
100
106
  }))
101
107
  .digest("base64");
102
108
  }
@@ -14,6 +14,9 @@ export declare function createStaticAssets(options: buildHelper.BuildOptions, {
14
14
  /**
15
15
  * Create the cache assets.
16
16
  *
17
+ * The cache entries, the fetch cache entries and the tag cache seed are prefixed with the Open Next build id
18
+ * so that they match the keys used at runtime.
19
+ *
17
20
  * @param options Build options.
18
21
  * @returns Whether the tag cache is used, and the meta files collected.
19
22
  */
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { loadConfig } from "../adapters/config/util.js";
3
+ import { getOpenNextBuildId, loadConfig } from "../adapters/config/util.js";
4
4
  import { safeParseJsonFile } from "../utils/safe-json-parse.js";
5
5
  import logger from "../logger.js";
6
6
  import { isBinaryContentType } from "../utils/binary.js";
@@ -59,6 +59,9 @@ export function createStaticAssets(options, { useBasePath = false } = {}) {
59
59
  /**
60
60
  * Create the cache assets.
61
61
  *
62
+ * The cache entries, the fetch cache entries and the tag cache seed are prefixed with the Open Next build id
63
+ * so that they match the keys used at runtime.
64
+ *
62
65
  * @param options Build options.
63
66
  * @returns Whether the tag cache is used, and the meta files collected.
64
67
  */
@@ -66,9 +69,9 @@ export function createCacheAssets(options) {
66
69
  logger.info("Bundling cache assets...");
67
70
  const { appBuildOutputPath, outputDir } = options;
68
71
  const packagePath = buildHelper.getPackagePath(options);
69
- const buildId = buildHelper.getBuildId(options);
70
72
  const nextConfig = loadConfig(path.join(appBuildOutputPath, ".next"));
71
- const openNextBuildId = nextConfig.deploymentId ?? buildId;
73
+ // Must match `process.env.OPEN_NEXT_BUILD_ID` used at runtime to read the cache entries and the tags
74
+ const openNextBuildId = getOpenNextBuildId(nextConfig, buildHelper.getBuildId(options));
72
75
  let useTagCache = false;
73
76
  const dotNextPath = path.join(appBuildOutputPath, ".next/standalone", packagePath);
74
77
  const outputCachePath = path.join(outputDir, "cache", openNextBuildId);
@@ -180,8 +183,8 @@ export function createCacheAssets(options) {
180
183
  .split(",")
181
184
  .forEach((tag) => {
182
185
  metaFiles.push({
183
- tag: { S: path.posix.join(buildId, tag.trim()) },
184
- path: { S: path.posix.join(buildId, cacheKey) },
186
+ tag: { S: path.posix.join(openNextBuildId, tag.trim()) },
187
+ path: { S: path.posix.join(openNextBuildId, cacheKey) },
185
188
  revalidatedAt: { N: "1" },
186
189
  });
187
190
  });
@@ -190,7 +193,7 @@ export function createCacheAssets(options) {
190
193
  // Copy fetch-cache to cache folder
191
194
  const fetchCachePath = path.join(appBuildOutputPath, ".next/cache/fetch-cache");
192
195
  if (fs.existsSync(fetchCachePath)) {
193
- const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId);
196
+ const fetchOutputPath = path.join(outputDir, "cache", "__fetch", openNextBuildId);
194
197
  fs.mkdirSync(fetchOutputPath, { recursive: true });
195
198
  fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true });
196
199
  buildHelper.traverseFiles(fetchCachePath, () => true, ({ absolutePath, relativePath }) => {
@@ -198,9 +201,9 @@ export function createCacheAssets(options) {
198
201
  const fileData = safeParseJsonFile(fileContent, absolutePath);
199
202
  fileData?.tags?.forEach((tag) => {
200
203
  metaFiles.push({
201
- tag: { S: path.posix.join(buildId, tag) },
204
+ tag: { S: path.posix.join(openNextBuildId, tag) },
202
205
  path: {
203
- S: path.posix.join(buildId, relativePath),
206
+ S: path.posix.join(openNextBuildId, relativePath),
204
207
  },
205
208
  revalidatedAt: { N: "1" },
206
209
  });
@@ -83,7 +83,6 @@ export async function createImageOptimizationBundle(options) {
83
83
  arch: isDev ? undefined : "arm64",
84
84
  // Use the local platform in dev
85
85
  os: isDev ? os.platform() : "linux",
86
- nodeVersion: "18",
87
86
  libc: "glibc",
88
87
  });
89
88
  }
@@ -381,7 +381,7 @@ export function getPackagePath(options) {
381
381
  * @returns the Next.js runtime used: "webpack" or "turbopack"
382
382
  */
383
383
  export function getBundlerRuntime(options) {
384
- const dotNextServerPath = path.join(options.appPath, ".next/server");
384
+ const dotNextServerPath = path.join(options.appBuildOutputPath, ".next/server");
385
385
  if (fs.existsSync(path.join(dotNextServerPath, "webpack-runtime.js"))) {
386
386
  return "webpack";
387
387
  }
@@ -12,20 +12,19 @@ export function installDependencies(outputDir, installOptions) {
12
12
  // First we create a tempDir
13
13
  const tempInstallDir = fs.mkdtempSync(path.join(os.tmpdir(), `open-next-install-${name}`));
14
14
  logger.info(`Installing dependencies for ${name}...`);
15
+ if (installOptions.nodeVersion) {
16
+ logger.warn("install.nodeVersion is deprecated and ignored (npm no longer supports --target)");
17
+ }
15
18
  // We then need to run install in the tempDir
16
19
  // We don't install in the output dir directly because it could contain a package.json, and npm would then try to reinstall not complete deps from tracing the files
17
- const archOption = installOptions.arch
18
- ? `--arch=${installOptions.arch}`
19
- : "";
20
- const targetOption = installOptions.nodeVersion
21
- ? `--target=${installOptions.nodeVersion}`
22
- : "";
20
+ // npm filters platform-specific optional dependencies (e.g. sharp's @img/* packages) by --cpu; npm 12 rejects (npm 11 warns) both --arch and --target
21
+ const cpuOption = installOptions.arch ? `--cpu=${installOptions.arch}` : "";
23
22
  const libcOption = installOptions.libc
24
23
  ? `--libc=${installOptions.libc}`
25
24
  : "";
26
25
  const osOption = `--os=${installOptions.os ?? "linux"}`;
27
26
  const additionalArgs = installOptions.additionalArgs ?? "";
28
- const installCommand = `npm install ${osOption} ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
27
+ const installCommand = `npm install ${osOption} ${cpuOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
29
28
  execSync(installCommand, {
30
29
  stdio: "pipe",
31
30
  cwd: tempInstallDir,
@@ -3,6 +3,31 @@ export declare const disablePreloadingRule = "\nrule:\n kind: statement_block\n
3
3
  export declare const removeMiddlewareManifestRule = "\nrule:\n kind: statement_block\n inside:\n kind: method_definition\n has:\n kind: property_identifier\n regex: ^getMiddlewareManifest$\nfix:\n '{return null;}'\n";
4
4
  export declare const emptyHandleNextImageRequestRule = "\nrule:\n kind: assignment_expression\n pattern: this.handleNextImageRequest = $VALUE\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: ^constructor$\n inside:\n kind: class_body\nfix:\n this.handleNextImageRequest = async (req, res, parsedUrl) => false\n";
5
5
  export declare const provideInternalWaitUntil = "\nrule:\n kind: return_statement\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: getInternalWaitUntil\n\nfix:\n return globalThis.__openNextAls.getStore()?.waitUntil;\n";
6
+ /**
7
+ * Exposes `NextNodeServer#revalidate` to `res.revalidate()` of the Pages Router API routes.
8
+ *
9
+ * Starting with Next 15.4, `res.revalidate()` uses the `revalidate` function of the router server
10
+ * context (`globalThis[Symbol.for("@next/router-server-methods")]`) to revalidate in-process.
11
+ * The context is populated by the router server which is not used by OpenNext.
12
+ * Without it, `res.revalidate()` throws
13
+ * `Invariant: missing internal router-server-methods this is an internal bug`.
14
+ *
15
+ * `NextNodeServer#handleCatchallRenderRequest` creates the context for environments without
16
+ * router server, this rule adds `revalidate` to it:
17
+ *
18
+ * ```js
19
+ * routerServerGlobal[RouterServerContextSymbol][relativeProjectDir] = {
20
+ * render404: this.render404.bind(this),
21
+ * // added by the rule
22
+ * revalidate: this.revalidate.bind(this),
23
+ * };
24
+ * ```
25
+ *
26
+ * OpenNext wraps `NextNodeServer#revalidate` at runtime, see `core/revalidate.ts`.
27
+ *
28
+ * https://github.com/vercel/next.js/blob/4e6c27b/packages/next/src/server/api-utils/node/api-resolver.ts
29
+ */
30
+ export declare const exposeRevalidateRule = "\nrule:\n pattern:\n context: \"({ render404: this.render404.bind(this) })\"\n selector: pair\n inside:\n kind: object\n not:\n has:\n kind: pair\n has:\n field: key\n regex: ^revalidate$\nfix: |-\n render404: this.render404.bind(this),\n revalidate: this.revalidate.bind(this)\n";
6
31
  /**
7
32
  * Swaps the body for a throwing implementation
8
33
  *