@opennextjs/aws 4.1.13 → 4.1.15
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/composable-cache.d.ts +20 -0
- package/dist/adapters/composable-cache.js +33 -8
- 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/edge-adapter.js +2 -11
- 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/routing/cacheInterceptor.js +27 -4
- package/dist/core/routing/matcher.js +16 -4
- package/dist/core/routing/middleware.d.ts +4 -0
- package/dist/core/routing/middleware.js +19 -0
- package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
- package/dist/core/routing/routeParamPlaceholder.js +42 -0
- package/dist/core/routingHandler.js +8 -2
- package/dist/core/util.js +5 -0
- package/dist/http/framing.d.ts +35 -0
- package/dist/http/framing.js +181 -0
- package/dist/http/util.d.ts +11 -0
- package/dist/http/util.js +26 -0
- package/dist/overrides/cdnInvalidation/cloudfront.js +2 -1
- package/dist/overrides/proxyExternalRequest/fetch.js +5 -1
- package/dist/overrides/proxyExternalRequest/node.js +5 -0
- package/dist/overrides/wrappers/express-dev.js +3 -5
- package/dist/overrides/wrappers/node.js +2 -3
- 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.d.ts +14 -0
- package/dist/utils/cache.js +21 -1
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -24,6 +24,30 @@ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Rou
|
|
|
24
24
|
const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
|
|
25
25
|
const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
|
|
26
26
|
const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
|
|
27
|
+
/**
|
|
28
|
+
* Internal header that can be stored in a cache entry but must not be sent to the client.
|
|
29
|
+
*
|
|
30
|
+
* It is set when a route handler calls `response.cookies` and contains the `HttpOnly` cookies.
|
|
31
|
+
* Next.js strips it in `sendResponse` when it serves a cached response.
|
|
32
|
+
*/
|
|
33
|
+
const MIDDLEWARE_SET_COOKIE_HEADER = "x-middleware-set-cookie";
|
|
34
|
+
/**
|
|
35
|
+
* Returns the headers stored in a cache entry, without the internal `x-middleware-set-cookie` header.
|
|
36
|
+
*
|
|
37
|
+
* @param headers The headers stored in the cache entry meta
|
|
38
|
+
* @returns A copy of the headers without the internal header
|
|
39
|
+
*/
|
|
40
|
+
function getCachedResponseHeaders(headers) {
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
43
|
+
if (value === undefined ||
|
|
44
|
+
key.toLowerCase() === MIDDLEWARE_SET_COOKIE_HEADER) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
result[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
27
51
|
async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false, revalidationPath = path, revalidationKey = path) {
|
|
28
52
|
let finalRevalidate = CACHE_ONE_YEAR;
|
|
29
53
|
const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1];
|
|
@@ -194,7 +218,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
|
|
|
194
218
|
const headers = {
|
|
195
219
|
...cacheControl,
|
|
196
220
|
"content-type": type,
|
|
197
|
-
...cachedValue.meta?.headers,
|
|
221
|
+
...getCachedResponseHeaders(cachedValue.meta?.headers),
|
|
198
222
|
vary: VARY_HEADER,
|
|
199
223
|
...additionalHeaders,
|
|
200
224
|
};
|
|
@@ -400,8 +424,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
|
|
|
400
424
|
statusCode: cachedData.value.meta?.status ?? 307,
|
|
401
425
|
body: emptyReadableStream(),
|
|
402
426
|
headers: {
|
|
403
|
-
...(cachedData.value.meta?.headers
|
|
404
|
-
{}),
|
|
427
|
+
...getCachedResponseHeaders(cachedData.value.meta?.headers),
|
|
405
428
|
...cacheControl,
|
|
406
429
|
},
|
|
407
430
|
isBase64Encoded: false,
|
|
@@ -413,7 +436,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
|
|
|
413
436
|
const statusCode = computeStatusCode(event.rewriteStatusCode, cachedData.value.meta?.status);
|
|
414
437
|
const headers = {
|
|
415
438
|
...cacheControl,
|
|
416
|
-
...cachedData.value.meta?.headers,
|
|
439
|
+
...getCachedResponseHeaders(cachedData.value.meta?.headers),
|
|
417
440
|
vary: VARY_HEADER,
|
|
418
441
|
};
|
|
419
442
|
// See the note in `generateResult`.
|
|
@@ -426,6 +426,17 @@ function handleRepeatedSlashRedirect(event) {
|
|
|
426
426
|
}
|
|
427
427
|
return false;
|
|
428
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Returns the internal trailing slash redirect for the request, if any.
|
|
431
|
+
*
|
|
432
|
+
* Mirrors the internal redirects that Next.js generates from `trailingSlash`. They apply to every
|
|
433
|
+
* path, including API routes, and run before the middleware so that it always sees the canonical
|
|
434
|
+
* pathname. Data requests (`/_next/data/<buildId>/*.json`) are not redirected.
|
|
435
|
+
* See https://github.com/vercel/next.js/blob/324b297/packages/next/src/lib/load-custom-routes.ts#L791-L849
|
|
436
|
+
*
|
|
437
|
+
* @param event The incoming request
|
|
438
|
+
* @returns The 308 redirect, or `false` when the path is already canonical
|
|
439
|
+
*/
|
|
429
440
|
function handleTrailingSlashRedirect(event) {
|
|
430
441
|
// When rawPath is `//domain`, `url.host` would be `domain`.
|
|
431
442
|
// https://github.com/opennextjs/opennextjs-aws/issues/355
|
|
@@ -433,14 +444,15 @@ function handleTrailingSlashRedirect(event) {
|
|
|
433
444
|
if (
|
|
434
445
|
// Someone is trying to redirect to a different origin, let's not do that
|
|
435
446
|
url.host !== "localhost" ||
|
|
436
|
-
NextConfig.skipTrailingSlashRedirect
|
|
437
|
-
// We should not apply trailing slash redirect to API routes
|
|
438
|
-
event.rawPath.startsWith("/api/")) {
|
|
447
|
+
NextConfig.skipTrailingSlashRedirect) {
|
|
439
448
|
return false;
|
|
440
449
|
}
|
|
441
450
|
const emptyBody = emptyReadableStream();
|
|
442
451
|
if (NextConfig.trailingSlash &&
|
|
443
|
-
|
|
452
|
+
// Data requests (`/_next/data/<buildId>/*.json`) are never redirected, as with Next.js.
|
|
453
|
+
// The header is set by `fixDataPage`, a client supplied value is removed by the routing handler.
|
|
454
|
+
// The `__nextDataReq` query parameter can not be used as it is set by the client.
|
|
455
|
+
event.headers["x-nextjs-data"] !== "1" &&
|
|
444
456
|
!event.rawPath.endsWith("/") &&
|
|
445
457
|
!event.rawPath.match(/[\w-]+\.[\w]+$/g)) {
|
|
446
458
|
const headersLocation = event.url.split("?");
|
|
@@ -8,6 +8,10 @@ type MiddlewareLoader = () => Promise<{
|
|
|
8
8
|
*
|
|
9
9
|
* Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
|
|
10
10
|
*
|
|
11
|
+
* Next.js internal headers are never exposed on the response.
|
|
12
|
+
* `x-middleware-set-cookie` is only forwarded on the request headers of the returned `MiddlewareEvent`,
|
|
13
|
+
* regardless of the allowlist.
|
|
14
|
+
*
|
|
11
15
|
* @param internalEvent The internal request event.
|
|
12
16
|
* @param initialSearch The initial query string received by the handler.
|
|
13
17
|
* @param middlewareLoader The middleware loader, overridden by unit tests.
|
|
@@ -9,6 +9,8 @@ const middlewareManifest = MiddlewareManifest;
|
|
|
9
9
|
const functionsConfigManifest = FunctionsConfigManifest;
|
|
10
10
|
const middleMatch = getMiddlewareMatch(middlewareManifest, functionsConfigManifest);
|
|
11
11
|
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
12
|
+
// Internal header set by `NextResponse` when `response.cookies` is modified.
|
|
13
|
+
const MIDDLEWARE_SET_COOKIE_HEADER = "x-middleware-set-cookie";
|
|
12
14
|
function defaultMiddlewareLoader() {
|
|
13
15
|
// @ts-expect-error - This is bundled
|
|
14
16
|
return import("./middleware.mjs");
|
|
@@ -18,6 +20,10 @@ function defaultMiddlewareLoader() {
|
|
|
18
20
|
*
|
|
19
21
|
* Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
|
|
20
22
|
*
|
|
23
|
+
* Next.js internal headers are never exposed on the response.
|
|
24
|
+
* `x-middleware-set-cookie` is only forwarded on the request headers of the returned `MiddlewareEvent`,
|
|
25
|
+
* regardless of the allowlist.
|
|
26
|
+
*
|
|
21
27
|
* @param internalEvent The internal request event.
|
|
22
28
|
* @param initialSearch The initial query string received by the handler.
|
|
23
29
|
* @param middlewareLoader The middleware loader, overridden by unit tests.
|
|
@@ -92,6 +98,7 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
|
|
|
92
98
|
const responseHeaders = result.headers;
|
|
93
99
|
const reqHeaders = {};
|
|
94
100
|
const resHeaders = {};
|
|
101
|
+
let middlewareSetCookie;
|
|
95
102
|
// These are internal headers used by Next.js, we don't want to expose them to the client
|
|
96
103
|
// NOTE: keys must be in lowercase to match the normalized header keys.
|
|
97
104
|
const filteredHeaders = [
|
|
@@ -112,6 +119,13 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
|
|
|
112
119
|
else {
|
|
113
120
|
if (filteredHeaders.includes(normalizedKey))
|
|
114
121
|
return;
|
|
122
|
+
// `x-middleware-set-cookie` holds a copy of the cookies set by the middleware,
|
|
123
|
+
// including the `HttpOnly` ones. It must never be sent to the client.
|
|
124
|
+
// It is only forwarded on the request (see below) so that `cookies().get()` sees them.
|
|
125
|
+
if (normalizedKey === MIDDLEWARE_SET_COOKIE_HEADER) {
|
|
126
|
+
middlewareSetCookie = value;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
115
129
|
// Headers.forEach folds same-name headers; use getSetCookie() below instead.
|
|
116
130
|
if (normalizedKey === "set-cookie")
|
|
117
131
|
return;
|
|
@@ -139,6 +153,11 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
|
|
|
139
153
|
const newHeaders = Object.fromEntries([...Object.entries(internalEvent.headers), ...Object.entries(reqHeaders)]
|
|
140
154
|
.map(([key, value]) => [key.toLowerCase(), value])
|
|
141
155
|
.filter(([key]) => !overriddenHeaders || overriddenHeaders.has(key)));
|
|
156
|
+
// Next.js forwards `x-middleware-set-cookie` after applying the override allowlist,
|
|
157
|
+
// so it is never filtered out (see `resolve-routes.ts`).
|
|
158
|
+
if (middlewareSetCookie !== undefined) {
|
|
159
|
+
newHeaders[MIDDLEWARE_SET_COOKIE_HEADER] = middlewareSetCookie;
|
|
160
|
+
}
|
|
142
161
|
// If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
|
|
143
162
|
// NOTE: the header was added to `req` from above
|
|
144
163
|
const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether a segment of the pathname is the placeholder of a matched dynamic App Router page.
|
|
3
|
+
*
|
|
4
|
+
* Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]` for `[slug]`) as an
|
|
5
|
+
* unresolved fallback param. Such values are not legitimate and must not reach Next.js.
|
|
6
|
+
*
|
|
7
|
+
* Segments are decoded like Next.js decodes param values (catch-all values are split on `/` first).
|
|
8
|
+
* Segments with a malformed percent encoding never match.
|
|
9
|
+
* Pathnames without an opening bracket return early, without matching the dynamic routes.
|
|
10
|
+
*
|
|
11
|
+
* @param pathname The internal pathname, which may include the basePath, a locale prefix and a trailing slash
|
|
12
|
+
* @returns Whether a segment of the pathname is a placeholder of a matched dynamic App Router page
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasRouteParamPlaceholder(pathname: string): boolean;
|