@opennextjs/aws 4.1.12 → 4.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cache.d.ts +21 -1
- package/dist/adapters/cache.js +138 -30
- package/dist/adapters/config/index.js +14 -4
- package/dist/adapters/config/util.d.ts +19 -0
- package/dist/adapters/config/util.js +21 -0
- package/dist/adapters/image-optimization-adapter.js +8 -2
- package/dist/build/createAssets.d.ts +3 -0
- package/dist/build/createAssets.js +11 -8
- package/dist/build/createImageOptimizationBundle.js +0 -1
- package/dist/build/helper.js +1 -1
- package/dist/build/installDeps.js +6 -7
- package/dist/build/patch/patches/patchNextServer.d.ts +25 -0
- package/dist/build/patch/patches/patchNextServer.js +48 -0
- package/dist/build/patch/testUtils.d.ts +49 -0
- package/dist/build/patch/testUtils.js +78 -0
- package/dist/core/requestHandler.js +5 -0
- package/dist/core/resolve.d.ts +8 -1
- package/dist/core/revalidate.d.ts +68 -0
- package/dist/core/revalidate.js +143 -0
- package/dist/core/util.js +5 -0
- package/dist/overrides/cdnInvalidation/cloudfront.js +2 -1
- package/dist/overrides/wrappers/aws-lambda-compressed.js +2 -4
- package/dist/overrides/wrappers/aws-lambda-streaming.js +4 -0
- package/dist/plugins/edge.js +3 -2
- package/dist/types/cache.d.ts +12 -4
- package/dist/types/global.d.ts +7 -0
- package/dist/types/open-next.d.ts +4 -1
- package/dist/types/overrides.d.ts +47 -0
- package/dist/utils/cache.js +1 -1
- package/dist/utils/compression.d.ts +8 -0
- package/dist/utils/compression.js +16 -0
- package/package.json +8 -1
|
@@ -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
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type CodePatcher } from "./codePatcher.js";
|
|
2
|
+
/**
|
|
3
|
+
* A Next.js package installed on disk.
|
|
4
|
+
*/
|
|
5
|
+
export interface NextPackage {
|
|
6
|
+
/** Directory of the package, i.e. the directory of its `package.json`. */
|
|
7
|
+
dir: string;
|
|
8
|
+
/** Version of the package. */
|
|
9
|
+
version: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Compute the diff resulting of applying the `rule` to `src`.
|
|
13
|
+
*
|
|
14
|
+
* `diff` is an optional peer dependency of `@opennextjs/aws`: it must be installed to call this function.
|
|
15
|
+
*
|
|
16
|
+
* @param filename Filename used in the patch output
|
|
17
|
+
* @param src Content of the source code
|
|
18
|
+
* @param rule ASTgrep rule
|
|
19
|
+
* @returns diff in unified diff format
|
|
20
|
+
* @throws When the `diff` package is not installed
|
|
21
|
+
*/
|
|
22
|
+
export declare function computePatchDiff(filename: string, src: string, rule: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Get the Next.js packages installed for some applications.
|
|
25
|
+
*
|
|
26
|
+
* Packages are deduplicated by version: a package manager can install the same version in multiple
|
|
27
|
+
* directories (i.e. pnpm when the peer dependencies differ).
|
|
28
|
+
*
|
|
29
|
+
* @param appDirs Directories of the applications (absolute or relative to the current working directory),
|
|
30
|
+
* `next` is resolved from their `package.json`
|
|
31
|
+
* @returns the path and version of the Next.js packages
|
|
32
|
+
* @throws When `next` can not be resolved from one of the applications
|
|
33
|
+
*/
|
|
34
|
+
export declare function getInstalledNextPackages(appDirs: string[]): NextPackage[];
|
|
35
|
+
/**
|
|
36
|
+
* Get the files of a Next.js package that a code patcher applies to.
|
|
37
|
+
*
|
|
38
|
+
* It selects the files the same way `applyCodePatches` does at build time,
|
|
39
|
+
* i.e. using the `versions`, the `pathFilter` and the `contentFilter` of the patches.
|
|
40
|
+
*
|
|
41
|
+
* @param next A Next.js package, see `getInstalledNextPackages`
|
|
42
|
+
* @param codePatcher The code patcher
|
|
43
|
+
* @returns the files (path relative to the package) with their content and the patches to apply
|
|
44
|
+
*/
|
|
45
|
+
export declare function getNextFilesToPatch(next: NextPackage, codePatcher: CodePatcher): {
|
|
46
|
+
name: string;
|
|
47
|
+
code: string;
|
|
48
|
+
patches: CodePatcher["patches"];
|
|
49
|
+
}[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities to test code patches.
|
|
3
|
+
*
|
|
4
|
+
* They are used by the OpenNext adapters to test their patches against frozen snippets and against
|
|
5
|
+
* the files of the Next.js versions they are installed with.
|
|
6
|
+
* This module is not used at build time or at runtime.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { patchCode } from "./astCodePatcher.js";
|
|
12
|
+
import { isVersionInRange } from "./codePatcher.js";
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
/**
|
|
15
|
+
* Compute the diff resulting of applying the `rule` to `src`.
|
|
16
|
+
*
|
|
17
|
+
* `diff` is an optional peer dependency of `@opennextjs/aws`: it must be installed to call this function.
|
|
18
|
+
*
|
|
19
|
+
* @param filename Filename used in the patch output
|
|
20
|
+
* @param src Content of the source code
|
|
21
|
+
* @param rule ASTgrep rule
|
|
22
|
+
* @returns diff in unified diff format
|
|
23
|
+
* @throws When the `diff` package is not installed
|
|
24
|
+
*/
|
|
25
|
+
export function computePatchDiff(filename, src, rule) {
|
|
26
|
+
// Loaded lazily so that the other utilities can be used without `diff`.
|
|
27
|
+
const { createPatch } = require("diff");
|
|
28
|
+
const dst = patchCode(src, rule);
|
|
29
|
+
return createPatch(filename, src, dst);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the Next.js packages installed for some applications.
|
|
33
|
+
*
|
|
34
|
+
* Packages are deduplicated by version: a package manager can install the same version in multiple
|
|
35
|
+
* directories (i.e. pnpm when the peer dependencies differ).
|
|
36
|
+
*
|
|
37
|
+
* @param appDirs Directories of the applications (absolute or relative to the current working directory),
|
|
38
|
+
* `next` is resolved from their `package.json`
|
|
39
|
+
* @returns the path and version of the Next.js packages
|
|
40
|
+
* @throws When `next` can not be resolved from one of the applications
|
|
41
|
+
*/
|
|
42
|
+
export function getInstalledNextPackages(appDirs) {
|
|
43
|
+
const packages = new Map();
|
|
44
|
+
for (const appDir of appDirs) {
|
|
45
|
+
const appRequire = createRequire(path.join(path.resolve(appDir), "package.json"));
|
|
46
|
+
const dir = path.dirname(appRequire.resolve("next/package.json"));
|
|
47
|
+
const { version } = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf-8"));
|
|
48
|
+
if (!packages.has(version)) {
|
|
49
|
+
packages.set(version, dir);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...packages].map(([version, dir]) => ({ dir, version }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the files of a Next.js package that a code patcher applies to.
|
|
56
|
+
*
|
|
57
|
+
* It selects the files the same way `applyCodePatches` does at build time,
|
|
58
|
+
* i.e. using the `versions`, the `pathFilter` and the `contentFilter` of the patches.
|
|
59
|
+
*
|
|
60
|
+
* @param next A Next.js package, see `getInstalledNextPackages`
|
|
61
|
+
* @param codePatcher The code patcher
|
|
62
|
+
* @returns the files (path relative to the package) with their content and the patches to apply
|
|
63
|
+
*/
|
|
64
|
+
export function getNextFilesToPatch(next, codePatcher) {
|
|
65
|
+
const patches = codePatcher.patches.filter(({ versions }) => isVersionInRange(next.version, versions));
|
|
66
|
+
return readdirSync(next.dir, { recursive: true }).flatMap((name) => {
|
|
67
|
+
const filePath = path.join(next.dir, name);
|
|
68
|
+
const matchingPath = patches.filter(({ pathFilter }) => filePath.match(pathFilter));
|
|
69
|
+
if (matchingPath.length === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const code = readFileSync(filePath, "utf-8");
|
|
73
|
+
const matchingContent = matchingPath.filter(({ contentFilter }) => !contentFilter || code.match(contentFilter));
|
|
74
|
+
return matchingContent.length === 0
|
|
75
|
+
? []
|
|
76
|
+
: [{ name, code, patches: matchingContent }];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -33,6 +33,11 @@ export async function openNextHandler(internalEvent, options) {
|
|
|
33
33
|
if (initialHeaders["x-forwarded-host"]) {
|
|
34
34
|
initialHeaders.host = initialHeaders["x-forwarded-host"];
|
|
35
35
|
}
|
|
36
|
+
const requestStore = globalThis.__openNextAls.getStore();
|
|
37
|
+
if (requestStore) {
|
|
38
|
+
// Used by `res.revalidate()`, see `core/revalidate.ts`
|
|
39
|
+
requestStore.host = initialHeaders.host;
|
|
40
|
+
}
|
|
36
41
|
debug("internalEvent", internalEvent);
|
|
37
42
|
// These 3 will get overwritten by the routing handler if not using an external middleware
|
|
38
43
|
const internalHeaders = {
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -51,5 +51,12 @@ export declare function resolveProxyRequest(proxyRequest: OverrideOptions["proxy
|
|
|
51
51
|
/**
|
|
52
52
|
* @__PURE__
|
|
53
53
|
*/
|
|
54
|
-
export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<
|
|
54
|
+
export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<{
|
|
55
|
+
name: string;
|
|
56
|
+
invalidatePaths: (_: {
|
|
57
|
+
initialPath: string;
|
|
58
|
+
rawPath: string;
|
|
59
|
+
resolvedRoutes: import("../types/open-next.js").ResolvedRoute[];
|
|
60
|
+
}[]) => Promise<void>;
|
|
61
|
+
}>;
|
|
55
62
|
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ResolvedRoute } from "../types/open-next.js";
|
|
2
|
+
/**
|
|
3
|
+
* Arguments passed by Next to the router server `revalidate` function.
|
|
4
|
+
*
|
|
5
|
+
* Next renamed `revalidateHeaders` to `headers` in 16.x.
|
|
6
|
+
*/
|
|
7
|
+
export interface RevalidateArgs {
|
|
8
|
+
urlPath: string;
|
|
9
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
10
|
+
revalidateHeaders?: Record<string, string | string[] | undefined>;
|
|
11
|
+
opts: {
|
|
12
|
+
unstable_onlyGenerated?: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type RevalidateFn = (args: RevalidateArgs) => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the route of a path.
|
|
18
|
+
*
|
|
19
|
+
* Static routes take precedence over dynamic routes, as in Next.
|
|
20
|
+
*
|
|
21
|
+
* @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
|
|
22
|
+
* @returns The resolved route or `undefined` when no route matches
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveRoute(urlPath: string): ResolvedRoute | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
|
|
27
|
+
*
|
|
28
|
+
* i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
|
|
29
|
+
*
|
|
30
|
+
* @param route The resolved route
|
|
31
|
+
* @returns The candidate route templates (app routes may have several because of route groups)
|
|
32
|
+
*/
|
|
33
|
+
export declare function getRouteTemplates(route: ResolvedRoute): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Returns whether the route is served by the current function.
|
|
36
|
+
*
|
|
37
|
+
* Routes that are not listed in any split function are served by the default function.
|
|
38
|
+
* Paths that do not match any route are considered local so that Next handles them.
|
|
39
|
+
*
|
|
40
|
+
* @param route The resolved route
|
|
41
|
+
* @returns `true` when the route is served by the current function
|
|
42
|
+
*/
|
|
43
|
+
export declare function isServedByCurrentFunction(route: ResolvedRoute | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Revalidate a page served by another function by sending a `HEAD` request to the app.
|
|
46
|
+
*
|
|
47
|
+
* Only the revalidate headers provided by Next are forwarded (no cookies).
|
|
48
|
+
* The function serving the page takes care of the CDN invalidation.
|
|
49
|
+
*
|
|
50
|
+
* TODO: the host comes from the request headers, use a trusted origin instead.
|
|
51
|
+
*
|
|
52
|
+
* @param args The arguments passed by Next to `revalidate`
|
|
53
|
+
* @throws When the host is unknown or the page has not been revalidated
|
|
54
|
+
*/
|
|
55
|
+
export declare function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }: RevalidateArgs): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
|
|
58
|
+
*
|
|
59
|
+
* - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
|
|
60
|
+
* - Pages served by another split function are revalidated with a `HEAD` request.
|
|
61
|
+
*
|
|
62
|
+
* `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
|
|
63
|
+
* see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
|
|
64
|
+
*
|
|
65
|
+
* @param revalidateInProcess The original `NextNodeServer#revalidate`
|
|
66
|
+
* @returns The wrapped revalidate function
|
|
67
|
+
*/
|
|
68
|
+
export declare function createRevalidate(revalidateInProcess: RevalidateFn): RevalidateFn;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppPathRoutesManifest } from "../adapters/config/index.js";
|
|
2
|
+
import { NEXTJS_CACHE_HEADER } from "../utils/cacheHeaders.js";
|
|
3
|
+
import { RequestCache } from "../utils/requestCache.js";
|
|
4
|
+
import { debug, error } from "../adapters/logger";
|
|
5
|
+
import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
|
|
6
|
+
/**
|
|
7
|
+
* Timeout of the request revalidating a page served by another function.
|
|
8
|
+
*/
|
|
9
|
+
const REMOTE_REVALIDATE_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the route of a path.
|
|
12
|
+
*
|
|
13
|
+
* Static routes take precedence over dynamic routes, as in Next.
|
|
14
|
+
*
|
|
15
|
+
* @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
|
|
16
|
+
* @returns The resolved route or `undefined` when no route matches
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRoute(urlPath) {
|
|
19
|
+
const pathname = urlPath.split(/[?#]/)[0];
|
|
20
|
+
return staticRouteMatcher(pathname)[0] ?? dynamicRouteMatcher(pathname)[0];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
|
|
24
|
+
*
|
|
25
|
+
* i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
|
|
26
|
+
*
|
|
27
|
+
* @param route The resolved route
|
|
28
|
+
* @returns The candidate route templates (app routes may have several because of route groups)
|
|
29
|
+
*/
|
|
30
|
+
export function getRouteTemplates(route) {
|
|
31
|
+
if (route.type === "page") {
|
|
32
|
+
return [`pages${route.route === "/" ? "/index" : route.route}`];
|
|
33
|
+
}
|
|
34
|
+
return Object.entries(AppPathRoutesManifest)
|
|
35
|
+
.filter(([, path]) => path === route.route)
|
|
36
|
+
.map(([appPath]) => `app${appPath}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns whether the route is served by the current function.
|
|
40
|
+
*
|
|
41
|
+
* Routes that are not listed in any split function are served by the default function.
|
|
42
|
+
* Paths that do not match any route are considered local so that Next handles them.
|
|
43
|
+
*
|
|
44
|
+
* @param route The resolved route
|
|
45
|
+
* @returns `true` when the route is served by the current function
|
|
46
|
+
*/
|
|
47
|
+
export function isServedByCurrentFunction(route) {
|
|
48
|
+
const functions = globalThis.openNextConfig?.functions ?? {};
|
|
49
|
+
if (!route || Object.keys(functions).length === 0) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const templates = getRouteTemplates(route);
|
|
53
|
+
const owner = Object.entries(functions).find(([, fn]) => fn.routes?.some((r) => templates.includes(r)))?.[0];
|
|
54
|
+
// `globalThis.fnName` is `undefined` in the default function
|
|
55
|
+
return owner === (globalThis.fnName || undefined);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Revalidate a page served by another function by sending a `HEAD` request to the app.
|
|
59
|
+
*
|
|
60
|
+
* Only the revalidate headers provided by Next are forwarded (no cookies).
|
|
61
|
+
* The function serving the page takes care of the CDN invalidation.
|
|
62
|
+
*
|
|
63
|
+
* TODO: the host comes from the request headers, use a trusted origin instead.
|
|
64
|
+
*
|
|
65
|
+
* @param args The arguments passed by Next to `revalidate`
|
|
66
|
+
* @throws When the host is unknown or the page has not been revalidated
|
|
67
|
+
*/
|
|
68
|
+
export async function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }) {
|
|
69
|
+
const host = globalThis.__openNextAls.getStore()?.host;
|
|
70
|
+
if (!host) {
|
|
71
|
+
throw new Error(`Unable to revalidate ${urlPath}: unknown host`);
|
|
72
|
+
}
|
|
73
|
+
const requestHeaders = new Headers();
|
|
74
|
+
for (const [key, value] of Object.entries(headers ?? revalidateHeaders ?? {})) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
78
|
+
requestHeaders.append(key, v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const url = `https://${host}${urlPath}`;
|
|
82
|
+
debug("Revalidating page served by another function", { url });
|
|
83
|
+
const res = await globalThis.internalFetch(url, {
|
|
84
|
+
method: "HEAD",
|
|
85
|
+
headers: requestHeaders,
|
|
86
|
+
redirect: "manual",
|
|
87
|
+
signal: AbortSignal.timeout(REMOTE_REVALIDATE_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
// Same check as Next, see `NextNodeServer#revalidate`
|
|
90
|
+
if (res.headers.get(NEXTJS_CACHE_HEADER)?.toUpperCase() !== "REVALIDATED" &&
|
|
91
|
+
res.status !== 200 &&
|
|
92
|
+
!(res.status === 404 && opts.unstable_onlyGenerated)) {
|
|
93
|
+
throw new Error(`Invalid response ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
|
|
98
|
+
*
|
|
99
|
+
* - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
|
|
100
|
+
* - Pages served by another split function are revalidated with a `HEAD` request.
|
|
101
|
+
*
|
|
102
|
+
* `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
|
|
103
|
+
* see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
|
|
104
|
+
*
|
|
105
|
+
* @param revalidateInProcess The original `NextNodeServer#revalidate`
|
|
106
|
+
* @returns The wrapped revalidate function
|
|
107
|
+
*/
|
|
108
|
+
export function createRevalidate(revalidateInProcess) {
|
|
109
|
+
return async (args) => {
|
|
110
|
+
const route = resolveRoute(args.urlPath);
|
|
111
|
+
if (!isServedByCurrentFunction(route)) {
|
|
112
|
+
return revalidateRemote(args);
|
|
113
|
+
}
|
|
114
|
+
// Render the page in a context isolated from the API route request
|
|
115
|
+
// so that per-request state (i.e. `lastModified`, request cache) does not leak.
|
|
116
|
+
// The promise runner and `waitUntil` are shared to keep background work alive.
|
|
117
|
+
const store = globalThis.__openNextAls.getStore();
|
|
118
|
+
const renderInProcess = () => revalidateInProcess(args);
|
|
119
|
+
// Throws when the page has not been revalidated
|
|
120
|
+
await (store
|
|
121
|
+
? globalThis.__openNextAls.run({
|
|
122
|
+
...store,
|
|
123
|
+
lastModified: undefined,
|
|
124
|
+
writtenTags: new Set(),
|
|
125
|
+
requestCache: new RequestCache(),
|
|
126
|
+
}, renderInProcess)
|
|
127
|
+
: renderInProcess());
|
|
128
|
+
const path = args.urlPath.split(/[?#]/)[0];
|
|
129
|
+
try {
|
|
130
|
+
await globalThis.cdnInvalidationHandler?.invalidatePaths([
|
|
131
|
+
{
|
|
132
|
+
initialPath: path,
|
|
133
|
+
rawPath: path,
|
|
134
|
+
resolvedRoutes: route ? [route] : [{ route: path, type: "page" }],
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
// The page has been revalidated, only the CDN may serve stale content
|
|
140
|
+
error(`Failed to invalidate ${path} in the CDN`, e);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
package/dist/core/util.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AppPathsManifestKeys, NextConfig, RoutesManifest, } from "../adapters/c
|
|
|
3
3
|
import NextServer from "next/dist/server/next-server.js";
|
|
4
4
|
import { debug, error } from "../adapters/logger.js";
|
|
5
5
|
import { applyOverride as applyNextjsRequireHooksOverride, overrideHooks as overrideNextjsRequireHooks, } from "./require-hooks.js";
|
|
6
|
+
import { createRevalidate } from "./revalidate.js";
|
|
6
7
|
// WORKAROUND: Set `__NEXT_PRIVATE_PREBUNDLED_REACT` to use prebundled Reac
|
|
7
8
|
// See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-__next_private_prebundled_react-to-use-prebundled-react
|
|
8
9
|
// Step 1: Need to override the require hooks for React before Next.js server
|
|
@@ -67,6 +68,10 @@ const nextServer = new NextServer.default({
|
|
|
67
68
|
dev: false,
|
|
68
69
|
dir: __dirname,
|
|
69
70
|
});
|
|
71
|
+
// `res.revalidate()` in Pages Router API routes (Next 15.4+)
|
|
72
|
+
if (typeof nextServer.revalidate === "function") {
|
|
73
|
+
nextServer.revalidate = createRevalidate(nextServer.revalidate.bind(nextServer));
|
|
74
|
+
}
|
|
70
75
|
let routesLoaded = false;
|
|
71
76
|
globalThis.__next_route_preloader = async (stage) => {
|
|
72
77
|
if (routesLoaded) {
|
|
@@ -11,7 +11,8 @@ export default {
|
|
|
11
11
|
? [`/${path}`, `/${path}?_rsc=*`]
|
|
12
12
|
: [
|
|
13
13
|
`/${path}`,
|
|
14
|
-
|
|
14
|
+
// Data URLs are generated by Next and use the Next build id, not the Open Next build id
|
|
15
|
+
`/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
|
|
15
16
|
];
|
|
16
17
|
});
|
|
17
18
|
await cloudfront.send(new CreateInvalidationCommand({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable, Writable } from "node:stream";
|
|
2
2
|
import zlib from "node:zlib";
|
|
3
3
|
import { error } from "../../adapters/logger";
|
|
4
|
+
import { getBrotliQuality } from "../../utils/compression";
|
|
4
5
|
import { formatWarmerResponse } from "./aws-lambda";
|
|
5
6
|
const handler = async (handler, converter) => async (event) => {
|
|
6
7
|
// Handle warmer event
|
|
@@ -70,10 +71,7 @@ function compressBody(body, encoding) {
|
|
|
70
71
|
case "br":
|
|
71
72
|
transform = zlib.createBrotliCompress({
|
|
72
73
|
params: {
|
|
73
|
-
|
|
74
|
-
// The default one will most likely timeout an AWS Lambda with default configuration on large bodies (>6mb).
|
|
75
|
-
// Therefore we set it to 6, which is a good compromise.
|
|
76
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]: Number(process.env.BROTLI_QUALITY) ?? 6,
|
|
74
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: getBrotliQuality(),
|
|
77
75
|
},
|
|
78
76
|
});
|
|
79
77
|
break;
|
|
@@ -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 { getBrotliQuality } from "../../utils/compression";
|
|
4
5
|
import { DetachedPromiseRunner } from "../../utils/promise";
|
|
5
6
|
function formatWarmerResponse(event) {
|
|
6
7
|
const result = new Promise((resolve) => {
|
|
@@ -34,6 +35,9 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
34
35
|
compressedStream = zlib.createBrotliCompress({
|
|
35
36
|
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
|
36
37
|
finishFlush: zlib.constants.BROTLI_OPERATION_FINISH,
|
|
38
|
+
params: {
|
|
39
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: getBrotliQuality(),
|
|
40
|
+
},
|
|
37
41
|
});
|
|
38
42
|
compressedStream.pipe(responseStream);
|
|
39
43
|
}
|
package/dist/plugins/edge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
|
|
4
|
+
import { getOpenNextBuildId, loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "../adapters/config/util.js";
|
|
5
5
|
import logger from "../logger.js";
|
|
6
6
|
import { normalizePath } from "../utils/normalize-path.js";
|
|
7
7
|
import { getCrossPlatformPathRegex } from "../utils/regex.js";
|
|
@@ -109,6 +109,7 @@ ${contents}
|
|
|
109
109
|
build.onLoad({ filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => {
|
|
110
110
|
const NextConfig = loadConfig(nextDir);
|
|
111
111
|
const BuildId = loadBuildId(nextDir);
|
|
112
|
+
const OpenNextBuildId = getOpenNextBuildId(NextConfig, BuildId);
|
|
112
113
|
const HtmlPages = loadHtmlPages(nextDir);
|
|
113
114
|
const RoutesManifest = loadRoutesManifest(nextDir);
|
|
114
115
|
const ConfigHeaders = loadConfigHeaders(nextDir);
|
|
@@ -145,7 +146,7 @@ ${contents}
|
|
|
145
146
|
export const PagesManifest = ${JSON.stringify(PagesManifest)};
|
|
146
147
|
|
|
147
148
|
process.env.NEXT_BUILD_ID = BuildId;
|
|
148
|
-
process.env.OPEN_NEXT_BUILD_ID =
|
|
149
|
+
process.env.OPEN_NEXT_BUILD_ID = ${JSON.stringify(OpenNextBuildId)};
|
|
149
150
|
process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId;
|
|
150
151
|
`;
|
|
151
152
|
return { contents };
|
package/dist/types/cache.d.ts
CHANGED
|
@@ -57,11 +57,22 @@ export interface CacheHandlerContext {
|
|
|
57
57
|
_requestHeaders: never;
|
|
58
58
|
fetchCacheKeyPrefix?: string;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The lifetime of a cache entry, as Next.js passes it to `set()` in `ctx.cacheControl`.
|
|
62
|
+
*/
|
|
63
|
+
export interface CacheControl {
|
|
64
|
+
revalidate: number | false;
|
|
65
|
+
expire?: number;
|
|
66
|
+
}
|
|
60
67
|
export interface CacheHandlerValue {
|
|
61
68
|
lastModified?: number;
|
|
62
69
|
age?: number;
|
|
63
70
|
cacheState?: string;
|
|
64
71
|
value: IncrementalCacheValue | null;
|
|
72
|
+
/**
|
|
73
|
+
* The lifetime of the entry, only read by Next.js 16.4+.
|
|
74
|
+
*/
|
|
75
|
+
cacheControl?: CacheControl;
|
|
65
76
|
}
|
|
66
77
|
export type Extension = "cache" | "fetch" | "composable";
|
|
67
78
|
type MetaHeaders = {
|
|
@@ -104,10 +115,7 @@ interface SetIncrementalFetchCacheContext {
|
|
|
104
115
|
}
|
|
105
116
|
interface SetIncrementalResponseCacheContext {
|
|
106
117
|
fetchCache?: false;
|
|
107
|
-
cacheControl?:
|
|
108
|
-
revalidate: number | false;
|
|
109
|
-
expire?: number;
|
|
110
|
-
};
|
|
118
|
+
cacheControl?: CacheControl;
|
|
111
119
|
/**
|
|
112
120
|
* True if the route is enabled for PPR.
|
|
113
121
|
*/
|
package/dist/types/global.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ interface OpenNextRequestContext {
|
|
|
58
58
|
writtenTags: Set<string>;
|
|
59
59
|
/** Per-request in-memory cache. Overrides can use this to store data scoped to the current request. */
|
|
60
60
|
requestCache: RequestCache;
|
|
61
|
+
/**
|
|
62
|
+
* Host of the request (`x-forwarded-host` or `host` header).
|
|
63
|
+
*
|
|
64
|
+
* Used by `res.revalidate()` to revalidate pages served by another split function.
|
|
65
|
+
* Only available in main functions.
|
|
66
|
+
*/
|
|
67
|
+
host?: string;
|
|
61
68
|
}
|
|
62
69
|
declare global {
|
|
63
70
|
/**
|
|
@@ -192,7 +192,10 @@ export interface InstallOptions {
|
|
|
192
192
|
packages: string[];
|
|
193
193
|
/** @default undefined */
|
|
194
194
|
arch?: "x64" | "arm64";
|
|
195
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* @default undefined
|
|
197
|
+
* @deprecated npm no longer supports the `--target` flag, so this option is ignored.
|
|
198
|
+
*/
|
|
196
199
|
nodeVersion?: string;
|
|
197
200
|
/** @default undefined */
|
|
198
201
|
libc?: "glibc" | "musl";
|
|
@@ -98,6 +98,11 @@ export type CacheValue<CacheType extends CacheEntryType> = (CacheType extends "f
|
|
|
98
98
|
* This is available for page cache entry, but only at runtime.
|
|
99
99
|
*/
|
|
100
100
|
revalidate?: number | false;
|
|
101
|
+
/**
|
|
102
|
+
* This is available for page cache entry, but only at runtime.
|
|
103
|
+
* The cache interceptor does not use it.
|
|
104
|
+
*/
|
|
105
|
+
expire?: number;
|
|
101
106
|
};
|
|
102
107
|
export type IncrementalCache = {
|
|
103
108
|
get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
|
|
@@ -240,12 +245,54 @@ export type OriginResolver = BaseOverride & {
|
|
|
240
245
|
export type ProxyExternalRequest = BaseOverride & {
|
|
241
246
|
proxy: (event: InternalEvent) => Promise<InternalResult>;
|
|
242
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* Describes a path whose CDN entries must be invalidated.
|
|
250
|
+
*
|
|
251
|
+
* Includes the path before and after routing as well as the routes that matched
|
|
252
|
+
* while resolving the request.
|
|
253
|
+
*/
|
|
243
254
|
type CDNPath = {
|
|
255
|
+
/** The original request pathname before route resolution. */
|
|
244
256
|
initialPath: string;
|
|
257
|
+
/** The effective request pathname after route resolution. */
|
|
245
258
|
rawPath: string;
|
|
259
|
+
/** The routes that matched while resolving the request. */
|
|
246
260
|
resolvedRoutes: ResolvedRoute[];
|
|
247
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* Invalidates CDN entries after on-demand revalidation.
|
|
264
|
+
*
|
|
265
|
+
* Implementations must support path invalidation and may additionally support
|
|
266
|
+
* native cache-tag invalidation.
|
|
267
|
+
*/
|
|
248
268
|
export type CDNInvalidationHandler = BaseOverride & {
|
|
269
|
+
/**
|
|
270
|
+
* Invalidates CDN entries associated with resolved paths.
|
|
271
|
+
*
|
|
272
|
+
* @param paths Paths and routing metadata for the entries to invalidate.
|
|
273
|
+
* @returns A promise that resolves when invalidation is complete.
|
|
274
|
+
*/
|
|
249
275
|
invalidatePaths: (paths: CDNPath[]) => Promise<void>;
|
|
276
|
+
/**
|
|
277
|
+
* Invalidates CDN entries associated with cache tags.
|
|
278
|
+
*
|
|
279
|
+
* Called once per `revalidateTag` call, after the tag cache has been updated,
|
|
280
|
+
* with the tags passed to Next.js as is: hard tags from `revalidateTag()` and
|
|
281
|
+
* soft tags (prefixed with `_N_T_/`) from `revalidatePath()`. Tags are not
|
|
282
|
+
* expanded, i.e. the hard tags of the paths matching a soft tag are not added.
|
|
283
|
+
*
|
|
284
|
+
* When provided, `invalidatePaths` is no longer called for tag revalidation.
|
|
285
|
+
* It is still used for `res.revalidate()` in the Pages Router.
|
|
286
|
+
* When omitted, OpenNext resolves the tags to paths and calls `invalidatePaths`.
|
|
287
|
+
*
|
|
288
|
+
* The CDN integration must associate cached entries with the same tags, both
|
|
289
|
+
* hard and soft, for invalidation to have an effect. For response-based CDN
|
|
290
|
+
* caches, the wrapper or converter must propagate the request's cache tags to
|
|
291
|
+
* the provider's response metadata.
|
|
292
|
+
*
|
|
293
|
+
* @param tags Cache tags associated with the entries to invalidate.
|
|
294
|
+
* @returns A promise that resolves when invalidation is complete.
|
|
295
|
+
*/
|
|
296
|
+
invalidateTags?: (tags: string[]) => Promise<void>;
|
|
250
297
|
};
|
|
251
298
|
export {};
|
package/dist/utils/cache.js
CHANGED
|
@@ -45,7 +45,7 @@ export function getStaleLastModified(revalidate) {
|
|
|
45
45
|
return 1;
|
|
46
46
|
}
|
|
47
47
|
if (typeof revalidate !== "number") {
|
|
48
|
-
//
|
|
48
|
+
// The cache adapter then reports `{ revalidate: 1 }` without `expire` (see `getCacheControl`),
|
|
49
49
|
// so the historical sentinel is still safe here.
|
|
50
50
|
return 1;
|
|
51
51
|
}
|