@opennextjs/aws 4.1.5 → 4.1.7
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 +1 -0
- package/dist/adapters/cache.js +17 -11
- package/dist/build/createAssets.js +37 -35
- package/dist/build/createServerBundle.js +1 -0
- package/dist/build/patch/patches/patchBackgroundRevalidation.d.ts +1 -1
- package/dist/build/patch/patches/patchBackgroundRevalidation.js +1 -1
- package/dist/core/routing/cacheInterceptor.d.ts +2 -2
- package/dist/core/routing/cacheInterceptor.js +131 -36
- package/dist/core/routing/matcher.js +2 -1
- package/dist/core/routingHandler.js +8 -5
- package/dist/core/util.js +13 -0
- package/dist/overrides/incrementalCache/fs-dev.js +33 -8
- package/dist/overrides/queue/sqs-lite.js +25 -13
- package/dist/overrides/queue/sqs.js +14 -2
- package/dist/overrides/wrappers/aws-apigw-streaming.js +16 -1
- package/dist/overrides/wrappers/aws-lambda-streaming.js +16 -1
- package/dist/types/next-types.d.ts +7 -1
- package/dist/utils/promise.js +10 -6
- package/dist/utils/routeCacheKey.d.ts +124 -0
- package/dist/utils/routeCacheKey.js +210 -0
- package/package.json +3 -3
package/dist/adapters/cache.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export default class Cache {
|
|
|
11
11
|
getFetchCache(key: string, softTags?: string[], tags?: string[]): Promise<CacheHandlerValue | null>;
|
|
12
12
|
getIncrementalCache(key: string): Promise<CacheHandlerValue | null>;
|
|
13
13
|
set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise<void>;
|
|
14
|
+
private writeCache;
|
|
14
15
|
revalidateTag(tags: string | string[], durations?: {
|
|
15
16
|
expire?: number;
|
|
16
17
|
}): Promise<void>;
|
package/dist/adapters/cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getStaleLastModified, getTagsFromValue, hasBeenRevalidated, isStale, writeTags, } from "../utils/cache";
|
|
2
2
|
import { isBinaryContentType } from "../utils/binary";
|
|
3
3
|
import { CACHE_TAGS_HEADER } from "../utils/cacheHeaders";
|
|
4
|
+
import { getPathFromRouteCacheKey } from "../utils/routeCacheKey";
|
|
4
5
|
import { compareSemver } from "../utils/semver";
|
|
5
6
|
import { debug, error, warn } from "./logger";
|
|
6
7
|
export const SOFT_TAG_PREFIX = "_N_T_/";
|
|
@@ -195,11 +196,18 @@ export default class Cache {
|
|
|
195
196
|
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
const store = globalThis.__openNextAls.getStore();
|
|
200
|
+
const writePromise = this.writeCache(key, data, ctx);
|
|
201
|
+
// Next.js ignores the value returned by `set`, so the write is registered on the request's
|
|
202
|
+
// pending promise runner instead of being awaited. `FETCH` writes are awaited: Next.js runs
|
|
203
|
+
// them in a detached chain of its own that only covers the write while `set` waits for it.
|
|
204
|
+
if (data?.kind === "FETCH" || store === undefined) {
|
|
205
|
+
await writePromise;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
store.pendingPromiseRunner.add(writePromise);
|
|
209
|
+
}
|
|
210
|
+
async writeCache(key, data, ctx) {
|
|
203
211
|
try {
|
|
204
212
|
if (data === null || data === undefined) {
|
|
205
213
|
await globalThis.incrementalCache.delete(key);
|
|
@@ -292,10 +300,6 @@ export default class Cache {
|
|
|
292
300
|
catch (e) {
|
|
293
301
|
error("Failed to set cache", e);
|
|
294
302
|
}
|
|
295
|
-
finally {
|
|
296
|
-
// We need to resolve the promise even if there was an error
|
|
297
|
-
detachedPromise?.resolve();
|
|
298
|
-
}
|
|
299
303
|
}
|
|
300
304
|
async revalidateTag(tags, durations) {
|
|
301
305
|
const config = globalThis.openNextConfig.dangerous;
|
|
@@ -331,7 +335,8 @@ export default class Cache {
|
|
|
331
335
|
if (paths.length > 0) {
|
|
332
336
|
// TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
|
|
333
337
|
// It also means that we'll need to provide the tags used in every request to the wrapper or converter.
|
|
334
|
-
|
|
338
|
+
// Fixed Next.js releases store route-scoped cache keys in the tag cache.
|
|
339
|
+
await globalThis.cdnInvalidationHandler.invalidatePaths(paths.map(getPathFromRouteCacheKey).map((path) => ({
|
|
335
340
|
initialPath: path,
|
|
336
341
|
rawPath: path,
|
|
337
342
|
resolvedRoutes: [
|
|
@@ -405,7 +410,8 @@ export default class Cache {
|
|
|
405
410
|
const uniquePaths = Array.from(new Set(toInsert
|
|
406
411
|
// We need to filter fetch cache key as they are not in the CDN
|
|
407
412
|
.filter((t) => t.tag.startsWith(SOFT_TAG_PREFIX))
|
|
408
|
-
.
|
|
413
|
+
// Fixed Next.js releases store route-scoped cache keys in the tag cache.
|
|
414
|
+
.map((t) => getPathFromRouteCacheKey(`/${t.path}`))));
|
|
409
415
|
if (uniquePaths.length > 0) {
|
|
410
416
|
await globalThis.cdnInvalidationHandler.invalidatePaths(uniquePaths.map((path) => ({
|
|
411
417
|
initialPath: path,
|
|
@@ -5,6 +5,7 @@ import { safeParseJsonFile } from "../utils/safe-json-parse.js";
|
|
|
5
5
|
import logger from "../logger.js";
|
|
6
6
|
import { isBinaryContentType } from "../utils/binary.js";
|
|
7
7
|
import { CACHE_TAGS_HEADER } from "../utils/cacheHeaders.js";
|
|
8
|
+
import { useRouteCacheKeys } from "../utils/routeCacheKey.js";
|
|
8
9
|
import * as buildHelper from "./helper.js";
|
|
9
10
|
/**
|
|
10
11
|
* Copy the static assets to the output folder
|
|
@@ -84,7 +85,7 @@ export function createCacheAssets(options) {
|
|
|
84
85
|
relativePath.endsWith(".segment.rsc") ||
|
|
85
86
|
(relativePath.endsWith(".html") && htmlPages.has(relativePath));
|
|
86
87
|
// Merge cache files into a single file
|
|
87
|
-
const
|
|
88
|
+
const cacheArtifacts = {};
|
|
88
89
|
// Process each source directory
|
|
89
90
|
sourceDirs.forEach((sourceDir) => {
|
|
90
91
|
buildHelper.traverseFiles(sourceDir, ({ relativePath }) => !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
|
|
@@ -102,14 +103,18 @@ export function createCacheAssets(options) {
|
|
|
102
103
|
//
|
|
103
104
|
// Next 16.1 removed `.prefetch.rsc`, so the strip is a no-op on Next 16 where
|
|
104
105
|
// the `rsc` field always holds a full payload.
|
|
105
|
-
const
|
|
106
|
-
.
|
|
107
|
-
.substring(0, path.join(outputCachePath, relativePath).length - ext.length)
|
|
106
|
+
const legacyKey = relativePath
|
|
107
|
+
.slice(0, -ext.length)
|
|
108
108
|
.replace(/\.prefetch$/, "")
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
.split(path.sep)
|
|
110
|
+
.join(path.posix.sep);
|
|
111
|
+
const artifactId = `${sourceDir}:${legacyKey}`;
|
|
112
|
+
const existingArtifact = cacheArtifacts[artifactId];
|
|
113
|
+
const fileType = ext.slice(1);
|
|
114
|
+
cacheArtifacts[artifactId] = {
|
|
115
|
+
...existingArtifact,
|
|
116
|
+
legacyKey,
|
|
117
|
+
[fileType]: existingArtifact?.[fileType] ?? absolutePath,
|
|
113
118
|
};
|
|
114
119
|
break;
|
|
115
120
|
}
|
|
@@ -121,11 +126,21 @@ export function createCacheAssets(options) {
|
|
|
121
126
|
}
|
|
122
127
|
});
|
|
123
128
|
});
|
|
129
|
+
const metaFiles = [];
|
|
124
130
|
// Generate cache file
|
|
125
|
-
Object.
|
|
131
|
+
Object.values(cacheArtifacts).forEach(({ legacyKey, ...files }) => {
|
|
126
132
|
const cacheFileMeta = files.meta
|
|
127
|
-
? safeParseJsonFile(fs.readFileSync(files.meta, "utf8"),
|
|
133
|
+
? safeParseJsonFile(fs.readFileSync(files.meta, "utf8"), files.meta)
|
|
128
134
|
: undefined;
|
|
135
|
+
const cacheKey = useRouteCacheKeys(options.nextVersion) && cacheFileMeta?.routeCache?.key
|
|
136
|
+
? cacheFileMeta.routeCache.key.replace(/^[/\\]+/, "")
|
|
137
|
+
: legacyKey;
|
|
138
|
+
const outputRoot = path.resolve(outputCachePath);
|
|
139
|
+
const cacheFilePath = path.resolve(outputRoot, `${cacheKey}.cache`);
|
|
140
|
+
if (!cacheFilePath.startsWith(`${outputRoot}${path.sep}`)) {
|
|
141
|
+
logger.warn(`Cache key resolves outside the cache output: ${cacheKey}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
129
144
|
const cacheJson = files.json
|
|
130
145
|
? safeParseJsonFile(fs.readFileSync(files.json, "utf8"), cacheFilePath)
|
|
131
146
|
: undefined;
|
|
@@ -159,9 +174,19 @@ export function createCacheAssets(options) {
|
|
|
159
174
|
// Ensure directory exists before writing
|
|
160
175
|
fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
|
|
161
176
|
fs.writeFileSync(cacheFilePath, JSON.stringify(cacheFileContent));
|
|
177
|
+
if (!options.config.dangerous?.disableTagCache &&
|
|
178
|
+
cacheFileMeta?.headers?.[CACHE_TAGS_HEADER]) {
|
|
179
|
+
cacheFileMeta.headers[CACHE_TAGS_HEADER]
|
|
180
|
+
.split(",")
|
|
181
|
+
.forEach((tag) => {
|
|
182
|
+
metaFiles.push({
|
|
183
|
+
tag: { S: path.posix.join(buildId, tag.trim()) },
|
|
184
|
+
path: { S: path.posix.join(buildId, cacheKey) },
|
|
185
|
+
revalidatedAt: { N: "1" },
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
162
189
|
});
|
|
163
|
-
// We need to traverse the cache to find every .meta file
|
|
164
|
-
const metaFiles = [];
|
|
165
190
|
// Copy fetch-cache to cache folder
|
|
166
191
|
const fetchCachePath = path.join(appBuildOutputPath, ".next/cache/fetch-cache");
|
|
167
192
|
if (fs.existsSync(fetchCachePath)) {
|
|
@@ -183,29 +208,6 @@ export function createCacheAssets(options) {
|
|
|
183
208
|
});
|
|
184
209
|
}
|
|
185
210
|
if (!options.config.dangerous?.disableTagCache) {
|
|
186
|
-
// Compute dynamodb cache data
|
|
187
|
-
// Traverse files inside cache to find all meta files and cache tags associated with them
|
|
188
|
-
sourceDirs.forEach((sourceDir) => {
|
|
189
|
-
buildHelper.traverseFiles(sourceDir, ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
|
|
190
|
-
const fileContent = fs.readFileSync(absolutePath, "utf8");
|
|
191
|
-
const fileData = safeParseJsonFile(fileContent, absolutePath);
|
|
192
|
-
if (fileData?.headers?.[CACHE_TAGS_HEADER]) {
|
|
193
|
-
fileData.headers[CACHE_TAGS_HEADER]
|
|
194
|
-
.split(",")
|
|
195
|
-
.forEach((tag) => {
|
|
196
|
-
// TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation
|
|
197
|
-
metaFiles.push({
|
|
198
|
-
tag: { S: path.posix.join(buildId, tag.trim()) },
|
|
199
|
-
path: {
|
|
200
|
-
S: path.posix.join(buildId, relativePath.replace(".meta", "")),
|
|
201
|
-
},
|
|
202
|
-
// We don't care about the revalidation time here, we just need to make sure it's there
|
|
203
|
-
revalidatedAt: { N: "1" },
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
211
|
if (metaFiles.length > 0) {
|
|
210
212
|
useTagCache = true;
|
|
211
213
|
const providerPath = path.join(outputDir, "dynamodb-provider");
|
|
@@ -168,6 +168,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
|
|
|
168
168
|
? ["experimentalIncrementalCacheHandler"]
|
|
169
169
|
: ["stableIncrementalCache"]),
|
|
170
170
|
...(isAfter152 ? [] : ["composableCache"]),
|
|
171
|
+
...(isAfter152 ? [] : ["topLevelComposableCache"]),
|
|
171
172
|
],
|
|
172
173
|
}),
|
|
173
174
|
openNextResolvePlugin({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const rule = "\nrule:\n kind: binary_expression\n all:\n - has:\n kind: unary_expression\n
|
|
1
|
+
export declare const rule = "\nrule:\n kind: binary_expression\n all:\n - has:\n kind: unary_expression\n pattern: \"!$ENTRY.isStale\"\n - has:\n kind: member_expression\n regex: \"context.isPrefetch\"\nfix:\n 'true'";
|
|
2
2
|
export declare const patchBackgroundRevalidation: {
|
|
3
3
|
name: string;
|
|
4
4
|
patches: {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { InternalEvent, InternalResult, MiddlewareEvent } from "../../types/open-next";
|
|
2
|
-
export declare function cacheInterceptor(event: MiddlewareEvent): Promise<InternalEvent | InternalResult>;
|
|
1
|
+
import type { InternalEvent, InternalResult, MiddlewareEvent, ResolvedRoute } from "../../types/open-next";
|
|
2
|
+
export declare function cacheInterceptor(event: MiddlewareEvent, resolvedRoutes?: ResolvedRoute[]): Promise<InternalEvent | InternalResult>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { NextConfig, PrerenderManifest } from "../../adapters/config/index";
|
|
2
|
+
import { AppPathsManifest, NextConfig, PagesManifest, PrerenderManifest, } from "../../adapters/config/index";
|
|
3
3
|
import { emptyReadableStream, toReadableStream } from "../../utils/stream";
|
|
4
4
|
import { isBinaryContentType } from "../../utils/binary";
|
|
5
5
|
import { getTagsFromValue, hasBeenRevalidated, isStale } from "../../utils/cache";
|
|
6
6
|
import { CACHE_CONTROL_HEADER, NO_STORE_CACHE_CONTROL, OPEN_NEXT_CACHE_HEADER, PRERENDER_REVALIDATE_HEADER, fixCacheControlForError, } from "../../utils/cacheHeaders";
|
|
7
|
+
import { getDynamicRouteCachePathname, getPrerenderRouteCacheKey, getRouteCacheKey, getRouteCacheOwner, useRouteCacheKeys, } from "../../utils/routeCacheKey";
|
|
7
8
|
import { debug } from "../../adapters/logger";
|
|
8
9
|
import { localizePath } from "./i18n";
|
|
9
10
|
import { generateMessageGroupId } from "./queue";
|
|
11
|
+
import { dynamicRouteMatcher } from "./routeMatcher";
|
|
10
12
|
const CACHE_ONE_YEAR = 60 * 60 * 24 * 365;
|
|
11
13
|
const CACHE_ONE_MONTH = 60 * 60 * 24 * 30;
|
|
12
14
|
/*
|
|
@@ -22,7 +24,7 @@ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Rou
|
|
|
22
24
|
const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
|
|
23
25
|
const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
|
|
24
26
|
const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
|
|
25
|
-
async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false) {
|
|
27
|
+
async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false, revalidationPath = path, revalidationKey = path) {
|
|
26
28
|
let finalRevalidate = CACHE_ONE_YEAR;
|
|
27
29
|
const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1];
|
|
28
30
|
if (revalidate === undefined && existingRoute) {
|
|
@@ -64,7 +66,9 @@ async function computeCacheControl(path, body, host, revalidate, lastModified, i
|
|
|
64
66
|
isStaleFromTagCache,
|
|
65
67
|
});
|
|
66
68
|
if (isStale) {
|
|
67
|
-
let url = NextConfig.trailingSlash
|
|
69
|
+
let url = NextConfig.trailingSlash && revalidationPath !== "/"
|
|
70
|
+
? `${revalidationPath}/`
|
|
71
|
+
: revalidationPath;
|
|
68
72
|
if (NextConfig.basePath) {
|
|
69
73
|
url = `${NextConfig.basePath}${url}`;
|
|
70
74
|
}
|
|
@@ -75,8 +79,8 @@ async function computeCacheControl(path, body, host, revalidate, lastModified, i
|
|
|
75
79
|
eTag: etag,
|
|
76
80
|
lastModified: lastModified ?? Date.now(),
|
|
77
81
|
},
|
|
78
|
-
MessageDeduplicationId: hash(`${
|
|
79
|
-
MessageGroupId: generateMessageGroupId(
|
|
82
|
+
MessageDeduplicationId: hash(`${revalidationKey}-${lastModified}-${etag}`),
|
|
83
|
+
MessageGroupId: generateMessageGroupId(revalidationKey),
|
|
80
84
|
});
|
|
81
85
|
}
|
|
82
86
|
return {
|
|
@@ -104,18 +108,33 @@ function getBodyForAppRouter(event, cachedValue) {
|
|
|
104
108
|
if (cachedValue.type !== "app") {
|
|
105
109
|
throw new Error("getBodyForAppRouter called with non-app cache value");
|
|
106
110
|
}
|
|
107
|
-
const segmentHeader =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
const segmentHeader = event.headers[NEXT_SEGMENT_PREFETCH_HEADER];
|
|
112
|
+
// A request from the client Segment Cache has to be answered with the segment it asked
|
|
113
|
+
// for. The full page payload is not a different-but-valid answer: the router never
|
|
114
|
+
// records the prefetch as satisfied and re-requests it forever.
|
|
115
|
+
// This mirrors how Next serves a cached app page - see the `segmentPrefetchHeader`
|
|
116
|
+
// branch of `packages/next/src/build/templates/app-page.ts`, which responds with the
|
|
117
|
+
// matching segment or an empty 404, and never with the full page.
|
|
118
|
+
//
|
|
119
|
+
// `experimental.prefetchInlining` is deliberately not consulted. Next normalizes every
|
|
120
|
+
// truthy value - including the 16.2+ default - into `{ maxSize, maxBundleSize }`, so it
|
|
121
|
+
// must never be read as a flag. More importantly it has no say here: inlining only
|
|
122
|
+
// changes *which* segments the build emits (an emitted segment then being a bundle that
|
|
123
|
+
// already holds its inlined ancestors), and `segmentData` holds exactly the segments
|
|
124
|
+
// that were emitted, which is exactly the set the router asks for.
|
|
125
|
+
if (typeof segmentHeader === "string" && cachedValue.segmentData) {
|
|
126
|
+
if (Object.hasOwn(cachedValue.segmentData, segmentHeader)) {
|
|
127
|
+
return {
|
|
128
|
+
body: cachedValue.segmentData[segmentHeader],
|
|
129
|
+
additionalHeaders: {
|
|
130
|
+
[NEXT_PRERENDER_HEADER]: "1",
|
|
131
|
+
[NEXT_POSTPONED_HEADER]: "2",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// The entry holds segments, but not this one. Next answers with an empty 404 here -
|
|
136
|
+
// let the server do that rather than serving a payload of a different shape.
|
|
137
|
+
return undefined;
|
|
119
138
|
}
|
|
120
139
|
// `rsc` is absent when the build collected neither a `.rsc` nor a `.prefetch.rsc` file for
|
|
121
140
|
// this entry - fallback shells, and postponed PPR routes on Next 16.2+, see `CachedFile`.
|
|
@@ -138,7 +157,7 @@ function getBodyForAppRouter(event, cachedValue) {
|
|
|
138
157
|
* request - the caller should then fallback to the server.
|
|
139
158
|
* @throws When `cachedValue` is neither of type `app` nor `page`
|
|
140
159
|
*/
|
|
141
|
-
async function generateResult(event, localizedPath, cachedValue, lastModified, isStaleFromTagCache = false) {
|
|
160
|
+
async function generateResult(event, localizedPath, cachedValue, lastModified, isStaleFromTagCache = false, cacheKey = localizedPath, revalidationPath = localizedPath) {
|
|
142
161
|
debug("Returning result from experimental cache");
|
|
143
162
|
let body;
|
|
144
163
|
let type = "application/octet-stream";
|
|
@@ -170,7 +189,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
|
|
|
170
189
|
debug("Missing body in the cache entry, falling back to the server");
|
|
171
190
|
return undefined;
|
|
172
191
|
}
|
|
173
|
-
const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified, isStaleFromTagCache);
|
|
192
|
+
const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified, isStaleFromTagCache, revalidationPath, cacheKey);
|
|
174
193
|
const statusCode = computeStatusCode(event.rewriteStatusCode, cachedValue.meta?.status);
|
|
175
194
|
const headers = {
|
|
176
195
|
...cacheControl,
|
|
@@ -232,10 +251,83 @@ function decodePathParams(pathname) {
|
|
|
232
251
|
.map((segment) => escapePathDelimiters(decodeURIComponent(segment), true))
|
|
233
252
|
.join("/");
|
|
234
253
|
}
|
|
235
|
-
|
|
254
|
+
let routeCacheManifests;
|
|
255
|
+
function getRouteCacheManifests() {
|
|
256
|
+
routeCacheManifests ??= {
|
|
257
|
+
prerenderManifest: {
|
|
258
|
+
routes: PrerenderManifest?.routes ?? {},
|
|
259
|
+
dynamicRoutes: PrerenderManifest?.dynamicRoutes ?? {},
|
|
260
|
+
},
|
|
261
|
+
appPaths: Object.keys(AppPathsManifest ?? {}),
|
|
262
|
+
pagesManifest: PagesManifest ?? {},
|
|
263
|
+
locales: NextConfig.i18n?.locales,
|
|
264
|
+
};
|
|
265
|
+
return routeCacheManifests;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Computes the key of the cache entry serving the request.
|
|
269
|
+
*
|
|
270
|
+
* Next.js 15.5.27 and 16.3.8 scope the key by the route owning the entry. It is computed from:
|
|
271
|
+
* - the prerender manifest for prerendered routes,
|
|
272
|
+
* - the dynamic route Next.js would render for entries cached at runtime.
|
|
273
|
+
*
|
|
274
|
+
* @param event The incoming event
|
|
275
|
+
* @param localizedPath The localized and decoded path, without basePath nor trailing slash
|
|
276
|
+
* @param resolvedRoutes Ordered route matches for the final internal pathname
|
|
277
|
+
* @returns The cache key, or `undefined` when the path is not served by an ISR route
|
|
278
|
+
*/
|
|
279
|
+
function getCacheKey(event, localizedPath, resolvedRoutes) {
|
|
280
|
+
if (!useRouteCacheKeys(globalThis.nextVersion)) {
|
|
281
|
+
const isISR = Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath) ||
|
|
282
|
+
Object.values(PrerenderManifest?.dynamicRoutes ?? {}).some((dr) => new RegExp(dr.routeRegex).test(localizedPath));
|
|
283
|
+
// The route is keyed as `/` in the prerender manifest, but the generated
|
|
284
|
+
// cache asset for the index route is uploaded as `/index`.
|
|
285
|
+
if (!isISR)
|
|
286
|
+
return undefined;
|
|
287
|
+
return localizedPath === "/" ? "/index" : localizedPath;
|
|
288
|
+
}
|
|
289
|
+
const manifests = getRouteCacheManifests();
|
|
290
|
+
if (Object.hasOwn(manifests.prerenderManifest.routes, localizedPath)) {
|
|
291
|
+
const manifestKey = getPrerenderRouteCacheKey(localizedPath, manifests);
|
|
292
|
+
const selectedRoute = resolvedRoutes?.[0];
|
|
293
|
+
// The routing handler always supplies matches. An empty list there means
|
|
294
|
+
// ownership could not be proven and scoped interception must fail closed.
|
|
295
|
+
if (resolvedRoutes && !selectedRoute)
|
|
296
|
+
return undefined;
|
|
297
|
+
if (!selectedRoute)
|
|
298
|
+
return manifestKey;
|
|
299
|
+
const selectedOwner = getRouteCacheOwner(selectedRoute.route, manifests);
|
|
300
|
+
if (!selectedOwner)
|
|
301
|
+
return undefined;
|
|
302
|
+
const selectedKey = getRouteCacheKey(localizedPath, selectedOwner);
|
|
303
|
+
return selectedKey === manifestKey ? manifestKey : undefined;
|
|
304
|
+
}
|
|
305
|
+
// Not prerendered, the entry might have been cached at runtime by a dynamic route.
|
|
306
|
+
// Dynamic routes are sorted by specificity, the first match is the one Next.js renders.
|
|
307
|
+
const dynamicRoute = resolvedRoutes
|
|
308
|
+
? resolvedRoutes[0]
|
|
309
|
+
: dynamicRouteMatcher(event.rawPath)[0];
|
|
310
|
+
const dynamicPrerender = dynamicRoute
|
|
311
|
+
? manifests.prerenderManifest.dynamicRoutes[dynamicRoute.route]
|
|
312
|
+
: undefined;
|
|
313
|
+
if (!dynamicRoute ||
|
|
314
|
+
!dynamicPrerender ||
|
|
315
|
+
dynamicPrerender.fallback !== null) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const owner = getRouteCacheOwner(dynamicRoute.route, manifests);
|
|
319
|
+
return owner
|
|
320
|
+
? getRouteCacheKey(getDynamicRouteCachePathname(localizedPath, manifests.locales), owner)
|
|
321
|
+
: undefined;
|
|
322
|
+
}
|
|
323
|
+
export async function cacheInterceptor(event, resolvedRoutes) {
|
|
236
324
|
if (Boolean(event.headers["next-action"]) ||
|
|
237
325
|
Boolean(event.headers[PRERENDER_REVALIDATE_HEADER]))
|
|
238
326
|
return event;
|
|
327
|
+
// Only GET and HEAD can be answered from the cache. A form posted without
|
|
328
|
+
// JavaScript carries its server action id in the body, not in the next-action header.
|
|
329
|
+
if (event.method !== "GET" && event.method !== "HEAD")
|
|
330
|
+
return event;
|
|
239
331
|
// Check for Next.js preview mode cookies
|
|
240
332
|
const cookies = event.headers.cookie || "";
|
|
241
333
|
const hasPreviewData = cookies.includes("__prerender_bypass") ||
|
|
@@ -244,14 +336,21 @@ export async function cacheInterceptor(event) {
|
|
|
244
336
|
debug("Preview mode detected, passing through to handler");
|
|
245
337
|
return event;
|
|
246
338
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
339
|
+
const basePath = NextConfig.basePath;
|
|
340
|
+
const pathWithoutBasePath = !basePath
|
|
341
|
+
? event.rawPath
|
|
342
|
+
: event.rawPath === basePath
|
|
343
|
+
? "/"
|
|
344
|
+
: event.rawPath.startsWith(`${basePath}/`)
|
|
345
|
+
? event.rawPath.slice(basePath.length)
|
|
346
|
+
: event.rawPath;
|
|
347
|
+
// Locale detection must inspect the first segment after basePath.
|
|
348
|
+
let localizedPath = localizePath({ ...event, rawPath: pathWithoutBasePath });
|
|
253
349
|
// We also need to remove trailing slash
|
|
254
350
|
localizedPath = localizedPath.replace(/\/$/, "");
|
|
351
|
+
// Revalidation must request the original encoded spelling so route selection
|
|
352
|
+
// cannot switch from an encoded catch-all to a more specific canonical route.
|
|
353
|
+
const revalidationPath = localizedPath || "/";
|
|
255
354
|
// Then we decode the path params
|
|
256
355
|
try {
|
|
257
356
|
localizedPath = decodePathParams(localizedPath) || "/";
|
|
@@ -261,14 +360,10 @@ export async function cacheInterceptor(event) {
|
|
|
261
360
|
// partially decode the path and select a different route identity.
|
|
262
361
|
return event;
|
|
263
362
|
}
|
|
264
|
-
// The route is keyed as `/` in the prerender manifest, but the generated
|
|
265
|
-
// cache asset for the app index route is uploaded as `/index`.
|
|
266
|
-
const cacheKey = localizedPath === "/" ? "/index" : localizedPath;
|
|
267
363
|
debug("Checking cache for", localizedPath, PrerenderManifest);
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (isISR) {
|
|
364
|
+
const cacheKey = getCacheKey(event, localizedPath, resolvedRoutes);
|
|
365
|
+
debug("cacheKey", cacheKey);
|
|
366
|
+
if (cacheKey) {
|
|
272
367
|
try {
|
|
273
368
|
const cachedData = await globalThis.incrementalCache.get(cacheKey);
|
|
274
369
|
debug("cached data in interceptor", cachedData);
|
|
@@ -294,12 +389,12 @@ export async function cacheInterceptor(event) {
|
|
|
294
389
|
switch (cachedData?.value?.type) {
|
|
295
390
|
case "app":
|
|
296
391
|
case "page": {
|
|
297
|
-
const result = await generateResult(event, localizedPath, cachedData.value, cachedData.lastModified, _isStale);
|
|
392
|
+
const result = await generateResult(event, localizedPath, cachedData.value, cachedData.lastModified, _isStale, cacheKey, revalidationPath);
|
|
298
393
|
// The cache entry can not serve this request, fallback to the server.
|
|
299
394
|
return result ?? event;
|
|
300
395
|
}
|
|
301
396
|
case "redirect": {
|
|
302
|
-
const cacheControl = await computeCacheControl(localizedPath, "", host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
|
|
397
|
+
const cacheControl = await computeCacheControl(localizedPath, "", host, cachedData.value.revalidate, cachedData.lastModified, _isStale, revalidationPath, cacheKey);
|
|
303
398
|
return {
|
|
304
399
|
type: "core",
|
|
305
400
|
statusCode: cachedData.value.meta?.status ?? 307,
|
|
@@ -313,7 +408,7 @@ export async function cacheInterceptor(event) {
|
|
|
313
408
|
};
|
|
314
409
|
}
|
|
315
410
|
case "route": {
|
|
316
|
-
const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
|
|
411
|
+
const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified, _isStale, revalidationPath, cacheKey);
|
|
317
412
|
const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"]));
|
|
318
413
|
const statusCode = computeStatusCode(event.rewriteStatusCode, cachedData.value.meta?.status);
|
|
319
414
|
const headers = {
|
|
@@ -123,7 +123,8 @@ export function handleRewrites(event, rewrites) {
|
|
|
123
123
|
const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath;
|
|
124
124
|
debug("urlParts", { pathname, protocol, hostname, queryString });
|
|
125
125
|
const toDestinationPath = compile(escapeRegex(pathname, { isPath: true }));
|
|
126
|
-
|
|
126
|
+
// A literal numeric port is URL syntax, not a path-to-regexp parameter.
|
|
127
|
+
const toDestinationHost = compile(escapeRegex(hostname).replace(/:(\d+)$/, "\\:$1"));
|
|
127
128
|
const toDestinationQuery = compile(escapeRegex(queryString));
|
|
128
129
|
const params = {
|
|
129
130
|
// params for the source
|
|
@@ -157,6 +157,12 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
157
157
|
}
|
|
158
158
|
const isNextImageRoute = eventOrResult.rawPath.startsWith("/_next/image");
|
|
159
159
|
const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
|
|
160
|
+
// Resolve against the final internal pathname after every rewrite phase.
|
|
161
|
+
// These ordered matches are also the authoritative cache owner candidates.
|
|
162
|
+
const resolvedRoutes = [
|
|
163
|
+
...staticRouteMatcher(eventOrResult.rawPath),
|
|
164
|
+
...dynamicRouteMatcher(eventOrResult.rawPath),
|
|
165
|
+
];
|
|
160
166
|
// If we still haven't found a route, we show the 404 page
|
|
161
167
|
// We need to ensure that rewrites are applied before showing the 404 page
|
|
162
168
|
if (!(isRouteFoundBeforeAllRewrites ||
|
|
@@ -175,9 +181,10 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
if (globalThis.openNextConfig.dangerous?.enableCacheInterception &&
|
|
184
|
+
!isExternalRewrite &&
|
|
178
185
|
!isInternalResult(eventOrResult)) {
|
|
179
186
|
debug("Cache interception enabled");
|
|
180
|
-
eventOrResult = await cacheInterceptor(eventOrResult);
|
|
187
|
+
eventOrResult = await cacheInterceptor(eventOrResult, resolvedRoutes);
|
|
181
188
|
if (isInternalResult(eventOrResult)) {
|
|
182
189
|
applyMiddlewareHeaders(eventOrResult, headers);
|
|
183
190
|
return eventOrResult;
|
|
@@ -185,10 +192,6 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
185
192
|
}
|
|
186
193
|
// We apply the headers from the middleware response last
|
|
187
194
|
applyMiddlewareHeaders(eventOrResult, headers);
|
|
188
|
-
const resolvedRoutes = [
|
|
189
|
-
...foundStaticRoute,
|
|
190
|
-
...foundDynamicRoute,
|
|
191
|
-
];
|
|
192
195
|
debug("resolvedRoutes", resolvedRoutes);
|
|
193
196
|
return {
|
|
194
197
|
internalEvent: eventOrResult,
|
package/dist/core/util.js
CHANGED
|
@@ -33,6 +33,19 @@ const nextServer = new NextServer.default({
|
|
|
33
33
|
cacheHandler: cacheHandlerPath,
|
|
34
34
|
cacheMaxMemorySize: 0, // We need to disable memory cache
|
|
35
35
|
//#endOverride
|
|
36
|
+
// Next.js 16 promoted `cacheHandlers` out of `experimental` to a top-level
|
|
37
|
+
// config key. At runtime `route-module.js#loadCustomCacheHandlers` reads
|
|
38
|
+
// `nextConfig.cacheHandlers` (top-level) — the nested `experimental` copy
|
|
39
|
+
// below is ignored on Next >= 16, so the composable ("use cache") handler
|
|
40
|
+
// must also be registered here or Next falls back to its in-memory default
|
|
41
|
+
// and composable entries never reach the incremental cache override.
|
|
42
|
+
// Older Next versions ignore this unknown top-level key and read the
|
|
43
|
+
// `experimental` copy instead; setting both is safe across versions.
|
|
44
|
+
//#override topLevelComposableCache
|
|
45
|
+
cacheHandlers: {
|
|
46
|
+
default: composableCacheHandlerPath,
|
|
47
|
+
},
|
|
48
|
+
//#endOverride
|
|
36
49
|
experimental: {
|
|
37
50
|
...NextConfig.experimental,
|
|
38
51
|
// This uses the request.headers.host as the URL
|
|
@@ -2,24 +2,49 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getMonorepoRelativePath } from "../../utils/normalize-path";
|
|
4
4
|
const buildId = process.env.OPEN_NEXT_BUILD_ID;
|
|
5
|
-
const basePath = path.join(getMonorepoRelativePath(),
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const basePath = path.join(getMonorepoRelativePath(), "cache");
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a cache entry using the same layout as the build's cache assets.
|
|
8
|
+
*
|
|
9
|
+
* @param key The cache entry key
|
|
10
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
11
|
+
* @returns The file path for the entry
|
|
12
|
+
*/
|
|
13
|
+
const getCacheKey = (key, cacheType = "cache") => {
|
|
14
|
+
return path.join(basePath, cacheType === "fetch" ? "__fetch" : "", `${buildId}`, cacheType === "fetch" ? key : `${key}.${cacheType}`);
|
|
8
15
|
};
|
|
9
16
|
const cache = {
|
|
10
17
|
name: "fs-dev",
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Reads an entry from the selected cache namespace.
|
|
20
|
+
*
|
|
21
|
+
* @param key The cache entry key
|
|
22
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
23
|
+
* @returns The cached value and its last modification time
|
|
24
|
+
* @throws When the entry cannot be read or contains invalid JSON
|
|
25
|
+
*/
|
|
26
|
+
get: async (key, cacheType) => {
|
|
27
|
+
const cacheKey = getCacheKey(key, cacheType);
|
|
28
|
+
const fileData = await fs.readFile(cacheKey, "utf-8");
|
|
13
29
|
const data = JSON.parse(fileData);
|
|
14
|
-
const { mtime } = await fs.stat(
|
|
30
|
+
const { mtime } = await fs.stat(cacheKey);
|
|
15
31
|
return {
|
|
16
32
|
value: data,
|
|
17
33
|
lastModified: mtime.getTime(),
|
|
18
34
|
};
|
|
19
35
|
},
|
|
20
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Writes an entry to the selected cache namespace.
|
|
38
|
+
*
|
|
39
|
+
* @param key The cache entry key
|
|
40
|
+
* @param value The value to persist
|
|
41
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
42
|
+
* @returns A promise that resolves when the entry has been written
|
|
43
|
+
* @throws When serialization or a file system operation fails
|
|
44
|
+
*/
|
|
45
|
+
set: async (key, value, cacheType) => {
|
|
21
46
|
const data = JSON.stringify(value);
|
|
22
|
-
const cacheKey = getCacheKey(key);
|
|
47
|
+
const cacheKey = getCacheKey(key, cacheType);
|
|
23
48
|
// We need to create the directory before writing the file
|
|
24
49
|
await fs.mkdir(path.dirname(cacheKey), { recursive: true });
|
|
25
50
|
await fs.writeFile(cacheKey, data);
|
|
@@ -29,21 +29,33 @@ const awsFetch = (body) => {
|
|
|
29
29
|
};
|
|
30
30
|
const queue = {
|
|
31
31
|
send: async ({ MessageBody, MessageDeduplicationId, MessageGroupId }) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const send = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const { REVALIDATION_QUEUE_URL } = process.env;
|
|
35
|
+
const result = await awsFetch(JSON.stringify({
|
|
36
|
+
QueueUrl: REVALIDATION_QUEUE_URL,
|
|
37
|
+
MessageBody: JSON.stringify(MessageBody),
|
|
38
|
+
MessageDeduplicationId,
|
|
39
|
+
MessageGroupId,
|
|
40
|
+
}));
|
|
41
|
+
if (result.status !== 200) {
|
|
42
|
+
throw new RecoverableError(`Failed to send message: ${result.status}`);
|
|
43
|
+
}
|
|
42
44
|
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
error(e);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
// Enqueuing revalidation should not block the response. When the wrapper
|
|
50
|
+
// provides a `waitUntil`, hand the send to it and return immediately so the
|
|
51
|
+
// SQS round trip runs after the body is flushed; otherwise (edge, or a
|
|
52
|
+
// wrapper without `waitUntil`) fall back to awaiting it inline.
|
|
53
|
+
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
|
|
54
|
+
if (waitUntil) {
|
|
55
|
+
waitUntil(send);
|
|
56
|
+
return;
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
error(e);
|
|
46
|
-
}
|
|
58
|
+
await send;
|
|
47
59
|
},
|
|
48
60
|
name: "sqs",
|
|
49
61
|
};
|
|
@@ -8,12 +8,24 @@ const sqsClient = new SQSClient({
|
|
|
8
8
|
});
|
|
9
9
|
const queue = {
|
|
10
10
|
send: async ({ MessageBody, MessageDeduplicationId, MessageGroupId }) => {
|
|
11
|
-
|
|
11
|
+
const send = sqsClient
|
|
12
|
+
.send(new SendMessageCommand({
|
|
12
13
|
QueueUrl: REVALIDATION_QUEUE_URL,
|
|
13
14
|
MessageBody: JSON.stringify(MessageBody),
|
|
14
15
|
MessageDeduplicationId,
|
|
15
16
|
MessageGroupId,
|
|
16
|
-
}))
|
|
17
|
+
}))
|
|
18
|
+
.then(() => undefined);
|
|
19
|
+
// Enqueuing revalidation should not block the response. When the wrapper
|
|
20
|
+
// provides a `waitUntil`, hand the send to it and return immediately so the
|
|
21
|
+
// SQS round trip runs after the body is flushed; otherwise (edge, or a
|
|
22
|
+
// wrapper without `waitUntil`) fall back to awaiting it inline.
|
|
23
|
+
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
|
|
24
|
+
if (waitUntil) {
|
|
25
|
+
waitUntil(send);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await send;
|
|
17
29
|
},
|
|
18
30
|
name: "sqs",
|
|
19
31
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { debug } from "../../adapters/logger";
|
|
3
|
+
import { DetachedPromiseRunner } from "../../utils/promise";
|
|
3
4
|
function formatWarmerResponse(event) {
|
|
4
5
|
const result = new Promise((resolve) => {
|
|
5
6
|
setTimeout(() => {
|
|
@@ -42,6 +43,15 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
42
43
|
debug(err);
|
|
43
44
|
responseStream.end();
|
|
44
45
|
});
|
|
46
|
+
// AWS provides no native `waitUntil`, so back it with the shared
|
|
47
|
+
// `DetachedPromiseRunner`, which tracks the deferred promises and awaits
|
|
48
|
+
// (and logs the failures of) them before the stream closes. This keeps
|
|
49
|
+
// the Lambda alive for deferred work (background revalidation, cache
|
|
50
|
+
// write-through) that starts after the response body is sent — matching
|
|
51
|
+
// the `waitUntil` the Cloudflare wrappers pass through, and enabling the
|
|
52
|
+
// `withWaitUntil` route preloading that otherwise falls back to `none`.
|
|
53
|
+
const promiseRunner = new DetachedPromiseRunner();
|
|
54
|
+
const waitUntil = (promise) => promiseRunner.add(promise);
|
|
45
55
|
const streamCreator = {
|
|
46
56
|
writeHeaders: (_prelude) => {
|
|
47
57
|
// No content-encoding header — API Gateway streaming does not
|
|
@@ -55,7 +65,10 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
55
65
|
return responseStream;
|
|
56
66
|
},
|
|
57
67
|
};
|
|
58
|
-
const response = await handler(internalEvent, {
|
|
68
|
+
const response = await handler(internalEvent, {
|
|
69
|
+
streamCreator,
|
|
70
|
+
waitUntil,
|
|
71
|
+
});
|
|
59
72
|
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
|
|
60
73
|
if (isUsingEdge) {
|
|
61
74
|
debug("Headers has not been set, we must be in the edge runtime");
|
|
@@ -66,6 +79,8 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
66
79
|
});
|
|
67
80
|
Readable.fromWeb(response.body).pipe(stream);
|
|
68
81
|
}
|
|
82
|
+
// Keep the invocation alive until deferred `waitUntil` work settles.
|
|
83
|
+
await promiseRunner.await();
|
|
69
84
|
});
|
|
70
85
|
export default {
|
|
71
86
|
wrapper: handler,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import zlib from "node:zlib";
|
|
3
3
|
import { debug, error } from "../../adapters/logger";
|
|
4
|
+
import { DetachedPromiseRunner } from "../../utils/promise";
|
|
4
5
|
function formatWarmerResponse(event) {
|
|
5
6
|
const result = new Promise((resolve) => {
|
|
6
7
|
setTimeout(() => {
|
|
@@ -64,7 +65,19 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
64
65
|
return compressedStream ?? responseStream;
|
|
65
66
|
},
|
|
66
67
|
};
|
|
67
|
-
|
|
68
|
+
// AWS provides no native `waitUntil`, so back it with the shared
|
|
69
|
+
// `DetachedPromiseRunner`, which tracks the deferred promises and awaits
|
|
70
|
+
// (and logs the failures of) them before the stream closes. This keeps
|
|
71
|
+
// the Lambda alive for deferred work (background revalidation, cache
|
|
72
|
+
// write-through) that starts after the response body is sent — matching
|
|
73
|
+
// the `waitUntil` the Cloudflare wrappers pass through, and enabling the
|
|
74
|
+
// `withWaitUntil` route preloading that otherwise falls back to `none`.
|
|
75
|
+
const promiseRunner = new DetachedPromiseRunner();
|
|
76
|
+
const waitUntil = (promise) => promiseRunner.add(promise);
|
|
77
|
+
const response = await handler(internalEvent, {
|
|
78
|
+
streamCreator,
|
|
79
|
+
waitUntil,
|
|
80
|
+
});
|
|
68
81
|
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
|
|
69
82
|
if (isUsingEdge) {
|
|
70
83
|
debug("Headers has not been set, we must be in the edge runtime");
|
|
@@ -75,6 +88,8 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
75
88
|
});
|
|
76
89
|
Readable.fromWeb(response.body).pipe(stream);
|
|
77
90
|
}
|
|
91
|
+
// Keep the invocation alive until deferred `waitUntil` work settles.
|
|
92
|
+
await promiseRunner.await();
|
|
78
93
|
// return converter.convertTo(response);
|
|
79
94
|
});
|
|
80
95
|
export default {
|
|
@@ -73,7 +73,10 @@ export interface NextConfig {
|
|
|
73
73
|
serverActions?: boolean;
|
|
74
74
|
appDir?: boolean;
|
|
75
75
|
optimizeCss?: boolean;
|
|
76
|
-
prefetchInlining?: boolean
|
|
76
|
+
prefetchInlining?: boolean | {
|
|
77
|
+
maxSize: number;
|
|
78
|
+
maxBundleSize: number;
|
|
79
|
+
};
|
|
77
80
|
};
|
|
78
81
|
images: ImageConfig;
|
|
79
82
|
poweredByHeader?: boolean;
|
|
@@ -148,11 +151,14 @@ export interface MiddlewareManifest {
|
|
|
148
151
|
export interface PrerenderManifest {
|
|
149
152
|
routes: Record<string, {
|
|
150
153
|
initialRevalidateSeconds: number | false;
|
|
154
|
+
srcRoute?: string | null;
|
|
155
|
+
dataRoute?: string | null;
|
|
151
156
|
}>;
|
|
152
157
|
dynamicRoutes: {
|
|
153
158
|
[route: string]: {
|
|
154
159
|
routeRegex: string;
|
|
155
160
|
fallback: string | false | null;
|
|
161
|
+
fallbackSourceRoute?: string;
|
|
156
162
|
dataRouteRegex: string;
|
|
157
163
|
};
|
|
158
164
|
};
|
package/dist/utils/promise.js
CHANGED
|
@@ -61,13 +61,17 @@ function provideNextAfterProvider() {
|
|
|
61
61
|
// This is needed by some lib that relies on the vercel request context to properly await stuff.
|
|
62
62
|
// Remove this when vercel builder is updated to provide '@next/request-context'.
|
|
63
63
|
const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// `get` resolves the store on every call rather than capturing it here: the
|
|
65
|
+
// object is published on `globalThis`, which every request in the isolate
|
|
66
|
+
// shares, so a captured store could belong to a different request.
|
|
67
67
|
const nextAfterContext = {
|
|
68
|
-
get: () =>
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
get: () => {
|
|
69
|
+
const store = globalThis.__openNextAls.getStore();
|
|
70
|
+
return {
|
|
71
|
+
waitUntil: store?.waitUntil ??
|
|
72
|
+
((promise) => store?.pendingPromiseRunner.add(promise)),
|
|
73
|
+
};
|
|
74
|
+
},
|
|
71
75
|
};
|
|
72
76
|
//@ts-expect-error
|
|
73
77
|
globalThis[NEXT_REQUEST_CONTEXT_SYMBOL] = nextAfterContext;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { PrerenderManifest } from "../types/next-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Next.js 15.5.27 and 16.3.8 introduced response cache keys scoped by the route
|
|
4
|
+
* owning each pages, app page, or app route entry:
|
|
5
|
+
* `/route-cache/<kind>/<sha256(sourceRoute)>/$<normalizedPathname>`.
|
|
6
|
+
*
|
|
7
|
+
* The helpers below mirror Next.js so that the build time cache population and the cache
|
|
8
|
+
* interceptor compute the exact same key Next.js uses when reading and writing the cache.
|
|
9
|
+
* https://github.com/vercel/next.js/blob/v16.3.8/packages/next/src/server/lib/route-cache-key.ts
|
|
10
|
+
*/
|
|
11
|
+
export type RouteCacheKind = "PAGES" | "APP_PAGE" | "APP_ROUTE";
|
|
12
|
+
export interface RouteCacheOwner {
|
|
13
|
+
kind: RouteCacheKind;
|
|
14
|
+
/**
|
|
15
|
+
* The page pathname for the pages router (i.e. `/blog/[slug]`).
|
|
16
|
+
* The app entry for the app router (i.e. `/(group)/blog/[slug]/page`).
|
|
17
|
+
*/
|
|
18
|
+
sourceRoute: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RouteCacheManifests {
|
|
21
|
+
prerenderManifest: Pick<PrerenderManifest, "routes" | "dynamicRoutes">;
|
|
22
|
+
/** The keys of the app paths manifest */
|
|
23
|
+
appPaths: string[];
|
|
24
|
+
pagesManifest: Record<string, string>;
|
|
25
|
+
/** The i18n locales */
|
|
26
|
+
locales?: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether the Next.js version uses scoped route cache keys.
|
|
30
|
+
*
|
|
31
|
+
* @param nextVersion The Next.js version
|
|
32
|
+
*/
|
|
33
|
+
export declare function useRouteCacheKeys(nextVersion: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Mirrors `normalizePagePath` from Next.js.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* normalizePagePath("/") === "/index"
|
|
39
|
+
* normalizePagePath("/index/foo") === "/index/index/foo"
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizePagePath(page: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Mirrors `denormalizePagePath` from Next.js, the inverse of `normalizePagePath`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* denormalizePagePath("/index") === "/"
|
|
47
|
+
* denormalizePagePath("/index/index/foo") === "/index/foo"
|
|
48
|
+
*/
|
|
49
|
+
export declare function denormalizePagePath(page: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Mirrors `normalizeAppPath` from Next.js: removes groups, parallel slots and the leaf
|
|
52
|
+
* `page`/`route` segment from an app entry.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* normalizeAppPath("/(group)/@modal/blog/[slug]/page") === "/blog/[slug]"
|
|
56
|
+
*/
|
|
57
|
+
export declare function normalizeAppPath(entry: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Mirrors `selectAppPageEntry` from Next.js: selects the app entry rendering `route`,
|
|
60
|
+
* a route can have multiple entries when it uses parallel routes.
|
|
61
|
+
*
|
|
62
|
+
* @param route The route, i.e. `/blog/[slug]`
|
|
63
|
+
* @param appPaths The keys of the app paths manifest
|
|
64
|
+
* @returns The app entry, or `undefined` when no entry renders the route
|
|
65
|
+
*/
|
|
66
|
+
export declare function selectAppPageEntry(route: string, appPaths: string[]): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Computes the owner of the cache entries for a route.
|
|
69
|
+
*
|
|
70
|
+
* @param sourceRoute The route owning the entry, i.e. `/blog/[slug]`
|
|
71
|
+
* @param manifests.appPaths The keys of the app paths manifest
|
|
72
|
+
* @param manifests.pagesManifest The pages manifest
|
|
73
|
+
* @returns The owner, or `undefined` when the route is not handled by an app entry nor a page
|
|
74
|
+
*/
|
|
75
|
+
export declare function getRouteCacheOwner(sourceRoute: string, { appPaths, pagesManifest, }: {
|
|
76
|
+
appPaths: string[];
|
|
77
|
+
pagesManifest: Record<string, string>;
|
|
78
|
+
}): RouteCacheOwner | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Mirrors `normalizeLocalePath` from Next.js: removes the locale prefix from a pathname.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* normalizeLocalePath("/en/isr", ["en", "fr"]) === "/isr"
|
|
84
|
+
* normalizeLocalePath("/en", ["en", "fr"]) === "/"
|
|
85
|
+
*/
|
|
86
|
+
export declare function normalizeLocalePath(pathname: string, locales?: string[]): string;
|
|
87
|
+
/**
|
|
88
|
+
* Mirrors how Next.js resolves the cache pathname of a dynamic route: a route resolving to
|
|
89
|
+
* `/index` (i.e. a root catch-all rendering `/index`) is cached as `/`, or as `/<locale>` with i18n.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* getDynamicRouteCachePathname("/index") === "/"
|
|
93
|
+
* getDynamicRouteCachePathname("/en/index", ["en", "fr"]) === "/en"
|
|
94
|
+
*
|
|
95
|
+
* @param pathname The localized pathname, i.e. `/en/blog/hello` with i18n
|
|
96
|
+
* @param locales The i18n locales
|
|
97
|
+
*/
|
|
98
|
+
export declare function getDynamicRouteCachePathname(pathname: string, locales?: string[]): string;
|
|
99
|
+
/**
|
|
100
|
+
* Computes the cache key of an entry from the prerender manifest, i.e. a prerendered
|
|
101
|
+
* route (`routes`) or a fallback shell (`dynamicRoutes`).
|
|
102
|
+
*
|
|
103
|
+
* @param pathname The pathname of the entry, as keyed in the prerender manifest
|
|
104
|
+
* @param manifests The manifests used to compute the owner
|
|
105
|
+
* @returns The cache key, or `undefined` when the pathname is not in the prerender manifest
|
|
106
|
+
* or when its owner could not be found.
|
|
107
|
+
*/
|
|
108
|
+
export declare function getPrerenderRouteCacheKey(pathname: string, manifests: RouteCacheManifests): string | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Mirrors `getRouteCacheKey` from Next.js.
|
|
111
|
+
*
|
|
112
|
+
* @param pathname The pathname of the entry, i.e. `/blog/hello` (`/en/blog/hello` with i18n)
|
|
113
|
+
* @param owner The owner of the entry
|
|
114
|
+
* @returns The cache key, i.e. `/route-cache/APP_PAGE/<sha256>/$/blog/hello`
|
|
115
|
+
*/
|
|
116
|
+
export declare function getRouteCacheKey(pathname: string, owner: RouteCacheOwner): string;
|
|
117
|
+
/**
|
|
118
|
+
* Returns the normalized pathname of a route cache key, i.e. `/blog/hello` for
|
|
119
|
+
* `/route-cache/APP_PAGE/<sha256>/$/blog/hello`.
|
|
120
|
+
* Keys that are not route cache keys are returned unchanged.
|
|
121
|
+
*
|
|
122
|
+
* @param key The cache key, with or without the leading `/`
|
|
123
|
+
*/
|
|
124
|
+
export declare function getPathFromRouteCacheKey(key: string): string;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { compareSemver } from "./semver.js";
|
|
3
|
+
const ROUTE_CACHE_DIRECTORY = "route-cache";
|
|
4
|
+
const ROUTE_CACHE_KEY_PREFIX = /^\/?route-cache\/(?:PAGES|APP_PAGE|APP_ROUTE)\/[0-9a-f]{64}\/\$(?=\/)/;
|
|
5
|
+
const DYNAMIC_ROUTE_REGEX = /\/\[[^/]+?\](?=\/|$)/;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the Next.js version uses scoped route cache keys.
|
|
8
|
+
*
|
|
9
|
+
* @param nextVersion The Next.js version
|
|
10
|
+
*/
|
|
11
|
+
export function useRouteCacheKeys(nextVersion) {
|
|
12
|
+
return ((compareSemver(nextVersion, ">=", "15.5.27") &&
|
|
13
|
+
compareSemver(nextVersion, "<", "16.0.0")) ||
|
|
14
|
+
compareSemver(nextVersion, ">=", "16.3.8"));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mirrors `normalizePagePath` from Next.js.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* normalizePagePath("/") === "/index"
|
|
21
|
+
* normalizePagePath("/index/foo") === "/index/index/foo"
|
|
22
|
+
*/
|
|
23
|
+
export function normalizePagePath(page) {
|
|
24
|
+
if (/^\/index(\/|$)/.test(page) && !DYNAMIC_ROUTE_REGEX.test(page)) {
|
|
25
|
+
return `/index${page}`;
|
|
26
|
+
}
|
|
27
|
+
if (page === "/") {
|
|
28
|
+
return "/index";
|
|
29
|
+
}
|
|
30
|
+
return page.startsWith("/") ? page : `/${page}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mirrors `denormalizePagePath` from Next.js, the inverse of `normalizePagePath`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* denormalizePagePath("/index") === "/"
|
|
37
|
+
* denormalizePagePath("/index/index/foo") === "/index/foo"
|
|
38
|
+
*/
|
|
39
|
+
export function denormalizePagePath(page) {
|
|
40
|
+
if (page.startsWith("/index/") && !DYNAMIC_ROUTE_REGEX.test(page)) {
|
|
41
|
+
return page.slice(6);
|
|
42
|
+
}
|
|
43
|
+
return page === "/index" ? "/" : page;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mirrors `normalizeAppPath` from Next.js: removes groups, parallel slots and the leaf
|
|
47
|
+
* `page`/`route` segment from an app entry.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* normalizeAppPath("/(group)/@modal/blog/[slug]/page") === "/blog/[slug]"
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeAppPath(entry) {
|
|
53
|
+
const pathname = entry.split("/").reduce((acc, segment, index, segments) => {
|
|
54
|
+
if (!segment)
|
|
55
|
+
return acc;
|
|
56
|
+
// Groups are ignored.
|
|
57
|
+
if (segment.startsWith("(") && segment.endsWith(")"))
|
|
58
|
+
return acc;
|
|
59
|
+
// Parallel segments are ignored.
|
|
60
|
+
if (segment.startsWith("@"))
|
|
61
|
+
return acc;
|
|
62
|
+
// The last segment (if it's a leaf) is ignored.
|
|
63
|
+
if ((segment === "page" || segment === "route") &&
|
|
64
|
+
index === segments.length - 1) {
|
|
65
|
+
return acc;
|
|
66
|
+
}
|
|
67
|
+
return `${acc}/${segment}`;
|
|
68
|
+
}, "");
|
|
69
|
+
return pathname === "" ? "/" : pathname;
|
|
70
|
+
}
|
|
71
|
+
function compareAppPaths(a, b) {
|
|
72
|
+
const aHasSlot = a.includes("/@");
|
|
73
|
+
const bHasSlot = b.includes("/@");
|
|
74
|
+
if (aHasSlot && !bHasSlot)
|
|
75
|
+
return -1;
|
|
76
|
+
if (!aHasSlot && bHasSlot)
|
|
77
|
+
return 1;
|
|
78
|
+
return a.localeCompare(b);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Mirrors `selectAppPageEntry` from Next.js: selects the app entry rendering `route`,
|
|
82
|
+
* a route can have multiple entries when it uses parallel routes.
|
|
83
|
+
*
|
|
84
|
+
* @param route The route, i.e. `/blog/[slug]`
|
|
85
|
+
* @param appPaths The keys of the app paths manifest
|
|
86
|
+
* @returns The app entry, or `undefined` when no entry renders the route
|
|
87
|
+
*/
|
|
88
|
+
export function selectAppPageEntry(route, appPaths) {
|
|
89
|
+
let entry;
|
|
90
|
+
for (const appPath of appPaths) {
|
|
91
|
+
if (normalizeAppPath(appPath).replace(/%5F/g, "_") !== route)
|
|
92
|
+
continue;
|
|
93
|
+
if (entry === undefined || compareAppPaths(entry, appPath) < 0) {
|
|
94
|
+
entry = appPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return entry;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Computes the owner of the cache entries for a route.
|
|
101
|
+
*
|
|
102
|
+
* @param sourceRoute The route owning the entry, i.e. `/blog/[slug]`
|
|
103
|
+
* @param manifests.appPaths The keys of the app paths manifest
|
|
104
|
+
* @param manifests.pagesManifest The pages manifest
|
|
105
|
+
* @returns The owner, or `undefined` when the route is not handled by an app entry nor a page
|
|
106
|
+
*/
|
|
107
|
+
export function getRouteCacheOwner(sourceRoute, { appPaths, pagesManifest, }) {
|
|
108
|
+
const appEntry = selectAppPageEntry(sourceRoute, appPaths);
|
|
109
|
+
if (appEntry) {
|
|
110
|
+
return {
|
|
111
|
+
kind: appEntry.endsWith("/route") ? "APP_ROUTE" : "APP_PAGE",
|
|
112
|
+
sourceRoute: appEntry,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (Object.hasOwn(pagesManifest, sourceRoute)) {
|
|
116
|
+
return { kind: "PAGES", sourceRoute };
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Mirrors `normalizeLocalePath` from Next.js: removes the locale prefix from a pathname.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* normalizeLocalePath("/en/isr", ["en", "fr"]) === "/isr"
|
|
125
|
+
* normalizeLocalePath("/en", ["en", "fr"]) === "/"
|
|
126
|
+
*/
|
|
127
|
+
export function normalizeLocalePath(pathname, locales) {
|
|
128
|
+
const segment = pathname.split("/", 2)[1]?.toLowerCase();
|
|
129
|
+
const locale = locales?.find((l) => l.toLowerCase() === segment);
|
|
130
|
+
if (!locale) {
|
|
131
|
+
return pathname;
|
|
132
|
+
}
|
|
133
|
+
return pathname.slice(locale.length + 1) || "/";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Mirrors how Next.js resolves the cache pathname of a dynamic route: a route resolving to
|
|
137
|
+
* `/index` (i.e. a root catch-all rendering `/index`) is cached as `/`, or as `/<locale>` with i18n.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* getDynamicRouteCachePathname("/index") === "/"
|
|
141
|
+
* getDynamicRouteCachePathname("/en/index", ["en", "fr"]) === "/en"
|
|
142
|
+
*
|
|
143
|
+
* @param pathname The localized pathname, i.e. `/en/blog/hello` with i18n
|
|
144
|
+
* @param locales The i18n locales
|
|
145
|
+
*/
|
|
146
|
+
export function getDynamicRouteCachePathname(pathname, locales) {
|
|
147
|
+
if (normalizeLocalePath(pathname, locales) !== "/index") {
|
|
148
|
+
return pathname;
|
|
149
|
+
}
|
|
150
|
+
return pathname.slice(0, -"/index".length) || "/";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Computes the cache key of an entry from the prerender manifest, i.e. a prerendered
|
|
154
|
+
* route (`routes`) or a fallback shell (`dynamicRoutes`).
|
|
155
|
+
*
|
|
156
|
+
* @param pathname The pathname of the entry, as keyed in the prerender manifest
|
|
157
|
+
* @param manifests The manifests used to compute the owner
|
|
158
|
+
* @returns The cache key, or `undefined` when the pathname is not in the prerender manifest
|
|
159
|
+
* or when its owner could not be found.
|
|
160
|
+
*/
|
|
161
|
+
export function getPrerenderRouteCacheKey(pathname, manifests) {
|
|
162
|
+
const { routes, dynamicRoutes } = manifests.prerenderManifest;
|
|
163
|
+
const prerender = Object.hasOwn(routes, pathname)
|
|
164
|
+
? routes[pathname]
|
|
165
|
+
: undefined;
|
|
166
|
+
const fallback = !prerender && Object.hasOwn(dynamicRoutes, pathname)
|
|
167
|
+
? dynamicRoutes[pathname]
|
|
168
|
+
: undefined;
|
|
169
|
+
if (!prerender && !fallback) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const sourceRoute = prerender
|
|
173
|
+
? prerender.srcRoute
|
|
174
|
+
: (fallback?.fallbackSourceRoute ?? pathname);
|
|
175
|
+
let owner;
|
|
176
|
+
if (sourceRoute) {
|
|
177
|
+
owner = getRouteCacheOwner(sourceRoute, manifests);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Static routes have no `srcRoute`, the pathname is the route.
|
|
181
|
+
// Pages routes are localized while the page itself is not.
|
|
182
|
+
owner = getRouteCacheOwner(pathname, manifests);
|
|
183
|
+
if (!owner && manifests.locales?.length) {
|
|
184
|
+
const pagesOwner = getRouteCacheOwner(normalizeLocalePath(pathname, manifests.locales), manifests);
|
|
185
|
+
owner = pagesOwner?.kind === "PAGES" ? pagesOwner : undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return owner ? getRouteCacheKey(pathname, owner) : undefined;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Mirrors `getRouteCacheKey` from Next.js.
|
|
192
|
+
*
|
|
193
|
+
* @param pathname The pathname of the entry, i.e. `/blog/hello` (`/en/blog/hello` with i18n)
|
|
194
|
+
* @param owner The owner of the entry
|
|
195
|
+
* @returns The cache key, i.e. `/route-cache/APP_PAGE/<sha256>/$/blog/hello`
|
|
196
|
+
*/
|
|
197
|
+
export function getRouteCacheKey(pathname, owner) {
|
|
198
|
+
const source = createHash("sha256").update(owner.sourceRoute).digest("hex");
|
|
199
|
+
return `/${ROUTE_CACHE_DIRECTORY}/${owner.kind}/${source}/$${normalizePagePath(pathname)}`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Returns the normalized pathname of a route cache key, i.e. `/blog/hello` for
|
|
203
|
+
* `/route-cache/APP_PAGE/<sha256>/$/blog/hello`.
|
|
204
|
+
* Keys that are not route cache keys are returned unchanged.
|
|
205
|
+
*
|
|
206
|
+
* @param key The cache key, with or without the leading `/`
|
|
207
|
+
*/
|
|
208
|
+
export function getPathFromRouteCacheKey(key) {
|
|
209
|
+
return key.replace(ROUTE_CACHE_KEY_PREFIX, "");
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"access": "public"
|
|
4
4
|
},
|
|
5
5
|
"name": "@opennextjs/aws",
|
|
6
|
-
"version": "4.1.
|
|
6
|
+
"version": "4.1.7",
|
|
7
7
|
"bin": {
|
|
8
8
|
"open-next": "./dist/index.js"
|
|
9
9
|
},
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"@types/express": "5.0.0",
|
|
57
57
|
"@types/node": "20.17.6",
|
|
58
58
|
"concurrently": "^9.2.1",
|
|
59
|
-
"next": "16.3.
|
|
59
|
+
"next": "16.3.8",
|
|
60
60
|
"tsc-alias": "^1.8.16",
|
|
61
61
|
"typescript": "5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"next": ">=15.5.
|
|
64
|
+
"next": ">=15.5.27 <16 || >=16.3.8"
|
|
65
65
|
},
|
|
66
66
|
"bugs": {
|
|
67
67
|
"url": "https://github.com/opennextjs/opennextjs-aws/issues"
|