@opennextjs/aws 4.1.13 → 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.
- package/dist/adapters/cache.d.ts +21 -1
- package/dist/adapters/cache.js +138 -30
- package/dist/adapters/config/index.js +14 -4
- package/dist/adapters/config/util.d.ts +19 -0
- package/dist/adapters/config/util.js +21 -0
- package/dist/adapters/image-optimization-adapter.js +8 -2
- package/dist/build/createAssets.d.ts +3 -0
- package/dist/build/createAssets.js +11 -8
- package/dist/build/createImageOptimizationBundle.js +0 -1
- package/dist/build/helper.js +1 -1
- package/dist/build/installDeps.js +6 -7
- package/dist/build/patch/patches/patchNextServer.d.ts +25 -0
- package/dist/build/patch/patches/patchNextServer.js +48 -0
- package/dist/core/requestHandler.js +5 -0
- package/dist/core/resolve.d.ts +8 -1
- package/dist/core/revalidate.d.ts +68 -0
- package/dist/core/revalidate.js +143 -0
- package/dist/core/util.js +5 -0
- package/dist/overrides/cdnInvalidation/cloudfront.js +2 -1
- package/dist/plugins/edge.js +3 -2
- package/dist/types/cache.d.ts +12 -4
- package/dist/types/global.d.ts +7 -0
- package/dist/types/open-next.d.ts +4 -1
- package/dist/types/overrides.d.ts +47 -0
- package/dist/utils/cache.js +1 -1
- package/package.json +1 -1
package/dist/adapters/cache.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/adapters/cache.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await globalThis.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
593
|
+
return { ...ctx.cacheControl };
|
|
486
594
|
}
|
|
487
|
-
return
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
184
|
-
path: { S: path.posix.join(
|
|
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",
|
|
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(
|
|
204
|
+
tag: { S: path.posix.join(openNextBuildId, tag) },
|
|
202
205
|
path: {
|
|
203
|
-
S: path.posix.join(
|
|
206
|
+
S: path.posix.join(openNextBuildId, relativePath),
|
|
204
207
|
},
|
|
205
208
|
revalidatedAt: { N: "1" },
|
|
206
209
|
});
|
package/dist/build/helper.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
18
|
-
|
|
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} ${
|
|
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
|
*
|
|
@@ -60,6 +60,47 @@ rule:
|
|
|
60
60
|
fix:
|
|
61
61
|
return globalThis.__openNextAls.getStore()?.waitUntil;
|
|
62
62
|
`;
|
|
63
|
+
/**
|
|
64
|
+
* Exposes `NextNodeServer#revalidate` to `res.revalidate()` of the Pages Router API routes.
|
|
65
|
+
*
|
|
66
|
+
* Starting with Next 15.4, `res.revalidate()` uses the `revalidate` function of the router server
|
|
67
|
+
* context (`globalThis[Symbol.for("@next/router-server-methods")]`) to revalidate in-process.
|
|
68
|
+
* The context is populated by the router server which is not used by OpenNext.
|
|
69
|
+
* Without it, `res.revalidate()` throws
|
|
70
|
+
* `Invariant: missing internal router-server-methods this is an internal bug`.
|
|
71
|
+
*
|
|
72
|
+
* `NextNodeServer#handleCatchallRenderRequest` creates the context for environments without
|
|
73
|
+
* router server, this rule adds `revalidate` to it:
|
|
74
|
+
*
|
|
75
|
+
* ```js
|
|
76
|
+
* routerServerGlobal[RouterServerContextSymbol][relativeProjectDir] = {
|
|
77
|
+
* render404: this.render404.bind(this),
|
|
78
|
+
* // added by the rule
|
|
79
|
+
* revalidate: this.revalidate.bind(this),
|
|
80
|
+
* };
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* OpenNext wraps `NextNodeServer#revalidate` at runtime, see `core/revalidate.ts`.
|
|
84
|
+
*
|
|
85
|
+
* https://github.com/vercel/next.js/blob/4e6c27b/packages/next/src/server/api-utils/node/api-resolver.ts
|
|
86
|
+
*/
|
|
87
|
+
export const exposeRevalidateRule = `
|
|
88
|
+
rule:
|
|
89
|
+
pattern:
|
|
90
|
+
context: "({ render404: this.render404.bind(this) })"
|
|
91
|
+
selector: pair
|
|
92
|
+
inside:
|
|
93
|
+
kind: object
|
|
94
|
+
not:
|
|
95
|
+
has:
|
|
96
|
+
kind: pair
|
|
97
|
+
has:
|
|
98
|
+
field: key
|
|
99
|
+
regex: ^revalidate$
|
|
100
|
+
fix: |-
|
|
101
|
+
render404: this.render404.bind(this),
|
|
102
|
+
revalidate: this.revalidate.bind(this)
|
|
103
|
+
`;
|
|
63
104
|
/**
|
|
64
105
|
* Swaps the body for a throwing implementation
|
|
65
106
|
*
|
|
@@ -138,6 +179,13 @@ export const patchNextServer = {
|
|
|
138
179
|
contentFilter: /getInternalWaitUntil/,
|
|
139
180
|
patchCode: createPatchCode(provideInternalWaitUntil),
|
|
140
181
|
},
|
|
182
|
+
// Expose `NextNodeServer#revalidate` to `res.revalidate()`
|
|
183
|
+
{
|
|
184
|
+
versions: ">=15.4.0",
|
|
185
|
+
pathFilter,
|
|
186
|
+
contentFilter: /render404: this\.render404\.bind\(this\)/,
|
|
187
|
+
patchCode: createPatchCode(exposeRevalidateRule),
|
|
188
|
+
},
|
|
141
189
|
...babelPatches,
|
|
142
190
|
],
|
|
143
191
|
};
|
|
@@ -33,6 +33,11 @@ export async function openNextHandler(internalEvent, options) {
|
|
|
33
33
|
if (initialHeaders["x-forwarded-host"]) {
|
|
34
34
|
initialHeaders.host = initialHeaders["x-forwarded-host"];
|
|
35
35
|
}
|
|
36
|
+
const requestStore = globalThis.__openNextAls.getStore();
|
|
37
|
+
if (requestStore) {
|
|
38
|
+
// Used by `res.revalidate()`, see `core/revalidate.ts`
|
|
39
|
+
requestStore.host = initialHeaders.host;
|
|
40
|
+
}
|
|
36
41
|
debug("internalEvent", internalEvent);
|
|
37
42
|
// These 3 will get overwritten by the routing handler if not using an external middleware
|
|
38
43
|
const internalHeaders = {
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -51,5 +51,12 @@ export declare function resolveProxyRequest(proxyRequest: OverrideOptions["proxy
|
|
|
51
51
|
/**
|
|
52
52
|
* @__PURE__
|
|
53
53
|
*/
|
|
54
|
-
export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<
|
|
54
|
+
export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<{
|
|
55
|
+
name: string;
|
|
56
|
+
invalidatePaths: (_: {
|
|
57
|
+
initialPath: string;
|
|
58
|
+
rawPath: string;
|
|
59
|
+
resolvedRoutes: import("../types/open-next.js").ResolvedRoute[];
|
|
60
|
+
}[]) => Promise<void>;
|
|
61
|
+
}>;
|
|
55
62
|
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ResolvedRoute } from "../types/open-next.js";
|
|
2
|
+
/**
|
|
3
|
+
* Arguments passed by Next to the router server `revalidate` function.
|
|
4
|
+
*
|
|
5
|
+
* Next renamed `revalidateHeaders` to `headers` in 16.x.
|
|
6
|
+
*/
|
|
7
|
+
export interface RevalidateArgs {
|
|
8
|
+
urlPath: string;
|
|
9
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
10
|
+
revalidateHeaders?: Record<string, string | string[] | undefined>;
|
|
11
|
+
opts: {
|
|
12
|
+
unstable_onlyGenerated?: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type RevalidateFn = (args: RevalidateArgs) => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the route of a path.
|
|
18
|
+
*
|
|
19
|
+
* Static routes take precedence over dynamic routes, as in Next.
|
|
20
|
+
*
|
|
21
|
+
* @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
|
|
22
|
+
* @returns The resolved route or `undefined` when no route matches
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveRoute(urlPath: string): ResolvedRoute | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
|
|
27
|
+
*
|
|
28
|
+
* i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
|
|
29
|
+
*
|
|
30
|
+
* @param route The resolved route
|
|
31
|
+
* @returns The candidate route templates (app routes may have several because of route groups)
|
|
32
|
+
*/
|
|
33
|
+
export declare function getRouteTemplates(route: ResolvedRoute): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Returns whether the route is served by the current function.
|
|
36
|
+
*
|
|
37
|
+
* Routes that are not listed in any split function are served by the default function.
|
|
38
|
+
* Paths that do not match any route are considered local so that Next handles them.
|
|
39
|
+
*
|
|
40
|
+
* @param route The resolved route
|
|
41
|
+
* @returns `true` when the route is served by the current function
|
|
42
|
+
*/
|
|
43
|
+
export declare function isServedByCurrentFunction(route: ResolvedRoute | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Revalidate a page served by another function by sending a `HEAD` request to the app.
|
|
46
|
+
*
|
|
47
|
+
* Only the revalidate headers provided by Next are forwarded (no cookies).
|
|
48
|
+
* The function serving the page takes care of the CDN invalidation.
|
|
49
|
+
*
|
|
50
|
+
* TODO: the host comes from the request headers, use a trusted origin instead.
|
|
51
|
+
*
|
|
52
|
+
* @param args The arguments passed by Next to `revalidate`
|
|
53
|
+
* @throws When the host is unknown or the page has not been revalidated
|
|
54
|
+
*/
|
|
55
|
+
export declare function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }: RevalidateArgs): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
|
|
58
|
+
*
|
|
59
|
+
* - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
|
|
60
|
+
* - Pages served by another split function are revalidated with a `HEAD` request.
|
|
61
|
+
*
|
|
62
|
+
* `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
|
|
63
|
+
* see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
|
|
64
|
+
*
|
|
65
|
+
* @param revalidateInProcess The original `NextNodeServer#revalidate`
|
|
66
|
+
* @returns The wrapped revalidate function
|
|
67
|
+
*/
|
|
68
|
+
export declare function createRevalidate(revalidateInProcess: RevalidateFn): RevalidateFn;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppPathRoutesManifest } from "../adapters/config/index.js";
|
|
2
|
+
import { NEXTJS_CACHE_HEADER } from "../utils/cacheHeaders.js";
|
|
3
|
+
import { RequestCache } from "../utils/requestCache.js";
|
|
4
|
+
import { debug, error } from "../adapters/logger";
|
|
5
|
+
import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
|
|
6
|
+
/**
|
|
7
|
+
* Timeout of the request revalidating a page served by another function.
|
|
8
|
+
*/
|
|
9
|
+
const REMOTE_REVALIDATE_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the route of a path.
|
|
12
|
+
*
|
|
13
|
+
* Static routes take precedence over dynamic routes, as in Next.
|
|
14
|
+
*
|
|
15
|
+
* @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
|
|
16
|
+
* @returns The resolved route or `undefined` when no route matches
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRoute(urlPath) {
|
|
19
|
+
const pathname = urlPath.split(/[?#]/)[0];
|
|
20
|
+
return staticRouteMatcher(pathname)[0] ?? dynamicRouteMatcher(pathname)[0];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
|
|
24
|
+
*
|
|
25
|
+
* i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
|
|
26
|
+
*
|
|
27
|
+
* @param route The resolved route
|
|
28
|
+
* @returns The candidate route templates (app routes may have several because of route groups)
|
|
29
|
+
*/
|
|
30
|
+
export function getRouteTemplates(route) {
|
|
31
|
+
if (route.type === "page") {
|
|
32
|
+
return [`pages${route.route === "/" ? "/index" : route.route}`];
|
|
33
|
+
}
|
|
34
|
+
return Object.entries(AppPathRoutesManifest)
|
|
35
|
+
.filter(([, path]) => path === route.route)
|
|
36
|
+
.map(([appPath]) => `app${appPath}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns whether the route is served by the current function.
|
|
40
|
+
*
|
|
41
|
+
* Routes that are not listed in any split function are served by the default function.
|
|
42
|
+
* Paths that do not match any route are considered local so that Next handles them.
|
|
43
|
+
*
|
|
44
|
+
* @param route The resolved route
|
|
45
|
+
* @returns `true` when the route is served by the current function
|
|
46
|
+
*/
|
|
47
|
+
export function isServedByCurrentFunction(route) {
|
|
48
|
+
const functions = globalThis.openNextConfig?.functions ?? {};
|
|
49
|
+
if (!route || Object.keys(functions).length === 0) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const templates = getRouteTemplates(route);
|
|
53
|
+
const owner = Object.entries(functions).find(([, fn]) => fn.routes?.some((r) => templates.includes(r)))?.[0];
|
|
54
|
+
// `globalThis.fnName` is `undefined` in the default function
|
|
55
|
+
return owner === (globalThis.fnName || undefined);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Revalidate a page served by another function by sending a `HEAD` request to the app.
|
|
59
|
+
*
|
|
60
|
+
* Only the revalidate headers provided by Next are forwarded (no cookies).
|
|
61
|
+
* The function serving the page takes care of the CDN invalidation.
|
|
62
|
+
*
|
|
63
|
+
* TODO: the host comes from the request headers, use a trusted origin instead.
|
|
64
|
+
*
|
|
65
|
+
* @param args The arguments passed by Next to `revalidate`
|
|
66
|
+
* @throws When the host is unknown or the page has not been revalidated
|
|
67
|
+
*/
|
|
68
|
+
export async function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }) {
|
|
69
|
+
const host = globalThis.__openNextAls.getStore()?.host;
|
|
70
|
+
if (!host) {
|
|
71
|
+
throw new Error(`Unable to revalidate ${urlPath}: unknown host`);
|
|
72
|
+
}
|
|
73
|
+
const requestHeaders = new Headers();
|
|
74
|
+
for (const [key, value] of Object.entries(headers ?? revalidateHeaders ?? {})) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
78
|
+
requestHeaders.append(key, v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const url = `https://${host}${urlPath}`;
|
|
82
|
+
debug("Revalidating page served by another function", { url });
|
|
83
|
+
const res = await globalThis.internalFetch(url, {
|
|
84
|
+
method: "HEAD",
|
|
85
|
+
headers: requestHeaders,
|
|
86
|
+
redirect: "manual",
|
|
87
|
+
signal: AbortSignal.timeout(REMOTE_REVALIDATE_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
// Same check as Next, see `NextNodeServer#revalidate`
|
|
90
|
+
if (res.headers.get(NEXTJS_CACHE_HEADER)?.toUpperCase() !== "REVALIDATED" &&
|
|
91
|
+
res.status !== 200 &&
|
|
92
|
+
!(res.status === 404 && opts.unstable_onlyGenerated)) {
|
|
93
|
+
throw new Error(`Invalid response ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
|
|
98
|
+
*
|
|
99
|
+
* - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
|
|
100
|
+
* - Pages served by another split function are revalidated with a `HEAD` request.
|
|
101
|
+
*
|
|
102
|
+
* `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
|
|
103
|
+
* see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
|
|
104
|
+
*
|
|
105
|
+
* @param revalidateInProcess The original `NextNodeServer#revalidate`
|
|
106
|
+
* @returns The wrapped revalidate function
|
|
107
|
+
*/
|
|
108
|
+
export function createRevalidate(revalidateInProcess) {
|
|
109
|
+
return async (args) => {
|
|
110
|
+
const route = resolveRoute(args.urlPath);
|
|
111
|
+
if (!isServedByCurrentFunction(route)) {
|
|
112
|
+
return revalidateRemote(args);
|
|
113
|
+
}
|
|
114
|
+
// Render the page in a context isolated from the API route request
|
|
115
|
+
// so that per-request state (i.e. `lastModified`, request cache) does not leak.
|
|
116
|
+
// The promise runner and `waitUntil` are shared to keep background work alive.
|
|
117
|
+
const store = globalThis.__openNextAls.getStore();
|
|
118
|
+
const renderInProcess = () => revalidateInProcess(args);
|
|
119
|
+
// Throws when the page has not been revalidated
|
|
120
|
+
await (store
|
|
121
|
+
? globalThis.__openNextAls.run({
|
|
122
|
+
...store,
|
|
123
|
+
lastModified: undefined,
|
|
124
|
+
writtenTags: new Set(),
|
|
125
|
+
requestCache: new RequestCache(),
|
|
126
|
+
}, renderInProcess)
|
|
127
|
+
: renderInProcess());
|
|
128
|
+
const path = args.urlPath.split(/[?#]/)[0];
|
|
129
|
+
try {
|
|
130
|
+
await globalThis.cdnInvalidationHandler?.invalidatePaths([
|
|
131
|
+
{
|
|
132
|
+
initialPath: path,
|
|
133
|
+
rawPath: path,
|
|
134
|
+
resolvedRoutes: route ? [route] : [{ route: path, type: "page" }],
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
// The page has been revalidated, only the CDN may serve stale content
|
|
140
|
+
error(`Failed to invalidate ${path} in the CDN`, e);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
package/dist/core/util.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AppPathsManifestKeys, NextConfig, RoutesManifest, } from "../adapters/c
|
|
|
3
3
|
import NextServer from "next/dist/server/next-server.js";
|
|
4
4
|
import { debug, error } from "../adapters/logger.js";
|
|
5
5
|
import { applyOverride as applyNextjsRequireHooksOverride, overrideHooks as overrideNextjsRequireHooks, } from "./require-hooks.js";
|
|
6
|
+
import { createRevalidate } from "./revalidate.js";
|
|
6
7
|
// WORKAROUND: Set `__NEXT_PRIVATE_PREBUNDLED_REACT` to use prebundled Reac
|
|
7
8
|
// See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-__next_private_prebundled_react-to-use-prebundled-react
|
|
8
9
|
// Step 1: Need to override the require hooks for React before Next.js server
|
|
@@ -67,6 +68,10 @@ const nextServer = new NextServer.default({
|
|
|
67
68
|
dev: false,
|
|
68
69
|
dir: __dirname,
|
|
69
70
|
});
|
|
71
|
+
// `res.revalidate()` in Pages Router API routes (Next 15.4+)
|
|
72
|
+
if (typeof nextServer.revalidate === "function") {
|
|
73
|
+
nextServer.revalidate = createRevalidate(nextServer.revalidate.bind(nextServer));
|
|
74
|
+
}
|
|
70
75
|
let routesLoaded = false;
|
|
71
76
|
globalThis.__next_route_preloader = async (stage) => {
|
|
72
77
|
if (routesLoaded) {
|
|
@@ -11,7 +11,8 @@ export default {
|
|
|
11
11
|
? [`/${path}`, `/${path}?_rsc=*`]
|
|
12
12
|
: [
|
|
13
13
|
`/${path}`,
|
|
14
|
-
|
|
14
|
+
// Data URLs are generated by Next and use the Next build id, not the Open Next build id
|
|
15
|
+
`/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
|
|
15
16
|
];
|
|
16
17
|
});
|
|
17
18
|
await cloudfront.send(new CreateInvalidationCommand({
|
package/dist/plugins/edge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
|
|
4
|
+
import { getOpenNextBuildId, loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
|
|
5
5
|
import logger from "../logger.js";
|
|
6
6
|
import { normalizePath } from "../utils/normalize-path.js";
|
|
7
7
|
import { getCrossPlatformPathRegex } from "../utils/regex.js";
|
|
@@ -109,6 +109,7 @@ ${contents}
|
|
|
109
109
|
build.onLoad({ filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => {
|
|
110
110
|
const NextConfig = loadConfig(nextDir);
|
|
111
111
|
const BuildId = loadBuildId(nextDir);
|
|
112
|
+
const OpenNextBuildId = getOpenNextBuildId(NextConfig, BuildId);
|
|
112
113
|
const HtmlPages = loadHtmlPages(nextDir);
|
|
113
114
|
const RoutesManifest = loadRoutesManifest(nextDir);
|
|
114
115
|
const ConfigHeaders = loadConfigHeaders(nextDir);
|
|
@@ -145,7 +146,7 @@ ${contents}
|
|
|
145
146
|
export const PagesManifest = ${JSON.stringify(PagesManifest)};
|
|
146
147
|
|
|
147
148
|
process.env.NEXT_BUILD_ID = BuildId;
|
|
148
|
-
process.env.OPEN_NEXT_BUILD_ID =
|
|
149
|
+
process.env.OPEN_NEXT_BUILD_ID = ${JSON.stringify(OpenNextBuildId)};
|
|
149
150
|
process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId;
|
|
150
151
|
`;
|
|
151
152
|
return { contents };
|
package/dist/types/cache.d.ts
CHANGED
|
@@ -57,11 +57,22 @@ export interface CacheHandlerContext {
|
|
|
57
57
|
_requestHeaders: never;
|
|
58
58
|
fetchCacheKeyPrefix?: string;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The lifetime of a cache entry, as Next.js passes it to `set()` in `ctx.cacheControl`.
|
|
62
|
+
*/
|
|
63
|
+
export interface CacheControl {
|
|
64
|
+
revalidate: number | false;
|
|
65
|
+
expire?: number;
|
|
66
|
+
}
|
|
60
67
|
export interface CacheHandlerValue {
|
|
61
68
|
lastModified?: number;
|
|
62
69
|
age?: number;
|
|
63
70
|
cacheState?: string;
|
|
64
71
|
value: IncrementalCacheValue | null;
|
|
72
|
+
/**
|
|
73
|
+
* The lifetime of the entry, only read by Next.js 16.4+.
|
|
74
|
+
*/
|
|
75
|
+
cacheControl?: CacheControl;
|
|
65
76
|
}
|
|
66
77
|
export type Extension = "cache" | "fetch" | "composable";
|
|
67
78
|
type MetaHeaders = {
|
|
@@ -104,10 +115,7 @@ interface SetIncrementalFetchCacheContext {
|
|
|
104
115
|
}
|
|
105
116
|
interface SetIncrementalResponseCacheContext {
|
|
106
117
|
fetchCache?: false;
|
|
107
|
-
cacheControl?:
|
|
108
|
-
revalidate: number | false;
|
|
109
|
-
expire?: number;
|
|
110
|
-
};
|
|
118
|
+
cacheControl?: CacheControl;
|
|
111
119
|
/**
|
|
112
120
|
* True if the route is enabled for PPR.
|
|
113
121
|
*/
|
package/dist/types/global.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ interface OpenNextRequestContext {
|
|
|
58
58
|
writtenTags: Set<string>;
|
|
59
59
|
/** Per-request in-memory cache. Overrides can use this to store data scoped to the current request. */
|
|
60
60
|
requestCache: RequestCache;
|
|
61
|
+
/**
|
|
62
|
+
* Host of the request (`x-forwarded-host` or `host` header).
|
|
63
|
+
*
|
|
64
|
+
* Used by `res.revalidate()` to revalidate pages served by another split function.
|
|
65
|
+
* Only available in main functions.
|
|
66
|
+
*/
|
|
67
|
+
host?: string;
|
|
61
68
|
}
|
|
62
69
|
declare global {
|
|
63
70
|
/**
|
|
@@ -192,7 +192,10 @@ export interface InstallOptions {
|
|
|
192
192
|
packages: string[];
|
|
193
193
|
/** @default undefined */
|
|
194
194
|
arch?: "x64" | "arm64";
|
|
195
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* @default undefined
|
|
197
|
+
* @deprecated npm no longer supports the `--target` flag, so this option is ignored.
|
|
198
|
+
*/
|
|
196
199
|
nodeVersion?: string;
|
|
197
200
|
/** @default undefined */
|
|
198
201
|
libc?: "glibc" | "musl";
|
|
@@ -98,6 +98,11 @@ export type CacheValue<CacheType extends CacheEntryType> = (CacheType extends "f
|
|
|
98
98
|
* This is available for page cache entry, but only at runtime.
|
|
99
99
|
*/
|
|
100
100
|
revalidate?: number | false;
|
|
101
|
+
/**
|
|
102
|
+
* This is available for page cache entry, but only at runtime.
|
|
103
|
+
* The cache interceptor does not use it.
|
|
104
|
+
*/
|
|
105
|
+
expire?: number;
|
|
101
106
|
};
|
|
102
107
|
export type IncrementalCache = {
|
|
103
108
|
get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
|
|
@@ -240,12 +245,54 @@ export type OriginResolver = BaseOverride & {
|
|
|
240
245
|
export type ProxyExternalRequest = BaseOverride & {
|
|
241
246
|
proxy: (event: InternalEvent) => Promise<InternalResult>;
|
|
242
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* Describes a path whose CDN entries must be invalidated.
|
|
250
|
+
*
|
|
251
|
+
* Includes the path before and after routing as well as the routes that matched
|
|
252
|
+
* while resolving the request.
|
|
253
|
+
*/
|
|
243
254
|
type CDNPath = {
|
|
255
|
+
/** The original request pathname before route resolution. */
|
|
244
256
|
initialPath: string;
|
|
257
|
+
/** The effective request pathname after route resolution. */
|
|
245
258
|
rawPath: string;
|
|
259
|
+
/** The routes that matched while resolving the request. */
|
|
246
260
|
resolvedRoutes: ResolvedRoute[];
|
|
247
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* Invalidates CDN entries after on-demand revalidation.
|
|
264
|
+
*
|
|
265
|
+
* Implementations must support path invalidation and may additionally support
|
|
266
|
+
* native cache-tag invalidation.
|
|
267
|
+
*/
|
|
248
268
|
export type CDNInvalidationHandler = BaseOverride & {
|
|
269
|
+
/**
|
|
270
|
+
* Invalidates CDN entries associated with resolved paths.
|
|
271
|
+
*
|
|
272
|
+
* @param paths Paths and routing metadata for the entries to invalidate.
|
|
273
|
+
* @returns A promise that resolves when invalidation is complete.
|
|
274
|
+
*/
|
|
249
275
|
invalidatePaths: (paths: CDNPath[]) => Promise<void>;
|
|
276
|
+
/**
|
|
277
|
+
* Invalidates CDN entries associated with cache tags.
|
|
278
|
+
*
|
|
279
|
+
* Called once per `revalidateTag` call, after the tag cache has been updated,
|
|
280
|
+
* with the tags passed to Next.js as is: hard tags from `revalidateTag()` and
|
|
281
|
+
* soft tags (prefixed with `_N_T_/`) from `revalidatePath()`. Tags are not
|
|
282
|
+
* expanded, i.e. the hard tags of the paths matching a soft tag are not added.
|
|
283
|
+
*
|
|
284
|
+
* When provided, `invalidatePaths` is no longer called for tag revalidation.
|
|
285
|
+
* It is still used for `res.revalidate()` in the Pages Router.
|
|
286
|
+
* When omitted, OpenNext resolves the tags to paths and calls `invalidatePaths`.
|
|
287
|
+
*
|
|
288
|
+
* The CDN integration must associate cached entries with the same tags, both
|
|
289
|
+
* hard and soft, for invalidation to have an effect. For response-based CDN
|
|
290
|
+
* caches, the wrapper or converter must propagate the request's cache tags to
|
|
291
|
+
* the provider's response metadata.
|
|
292
|
+
*
|
|
293
|
+
* @param tags Cache tags associated with the entries to invalidate.
|
|
294
|
+
* @returns A promise that resolves when invalidation is complete.
|
|
295
|
+
*/
|
|
296
|
+
invalidateTags?: (tags: string[]) => Promise<void>;
|
|
250
297
|
};
|
|
251
298
|
export {};
|
package/dist/utils/cache.js
CHANGED
|
@@ -45,7 +45,7 @@ export function getStaleLastModified(revalidate) {
|
|
|
45
45
|
return 1;
|
|
46
46
|
}
|
|
47
47
|
if (typeof revalidate !== "number") {
|
|
48
|
-
//
|
|
48
|
+
// The cache adapter then reports `{ revalidate: 1 }` without `expire` (see `getCacheControl`),
|
|
49
49
|
// so the historical sentinel is still safe here.
|
|
50
50
|
return 1;
|
|
51
51
|
}
|