@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.
Files changed (45) hide show
  1. package/dist/adapters/cache.d.ts +21 -1
  2. package/dist/adapters/cache.js +138 -30
  3. package/dist/adapters/composable-cache.d.ts +20 -0
  4. package/dist/adapters/composable-cache.js +33 -8
  5. package/dist/adapters/config/index.js +14 -4
  6. package/dist/adapters/config/util.d.ts +19 -0
  7. package/dist/adapters/config/util.js +21 -0
  8. package/dist/adapters/edge-adapter.js +2 -11
  9. package/dist/adapters/image-optimization-adapter.js +8 -2
  10. package/dist/build/createAssets.d.ts +3 -0
  11. package/dist/build/createAssets.js +11 -8
  12. package/dist/build/createImageOptimizationBundle.js +0 -1
  13. package/dist/build/helper.js +1 -1
  14. package/dist/build/installDeps.js +6 -7
  15. package/dist/build/patch/patches/patchNextServer.d.ts +25 -0
  16. package/dist/build/patch/patches/patchNextServer.js +48 -0
  17. package/dist/core/requestHandler.js +5 -0
  18. package/dist/core/resolve.d.ts +8 -1
  19. package/dist/core/revalidate.d.ts +68 -0
  20. package/dist/core/revalidate.js +143 -0
  21. package/dist/core/routing/cacheInterceptor.js +27 -4
  22. package/dist/core/routing/matcher.js +16 -4
  23. package/dist/core/routing/middleware.d.ts +4 -0
  24. package/dist/core/routing/middleware.js +19 -0
  25. package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
  26. package/dist/core/routing/routeParamPlaceholder.js +42 -0
  27. package/dist/core/routingHandler.js +8 -2
  28. package/dist/core/util.js +5 -0
  29. package/dist/http/framing.d.ts +35 -0
  30. package/dist/http/framing.js +181 -0
  31. package/dist/http/util.d.ts +11 -0
  32. package/dist/http/util.js +26 -0
  33. package/dist/overrides/cdnInvalidation/cloudfront.js +2 -1
  34. package/dist/overrides/proxyExternalRequest/fetch.js +5 -1
  35. package/dist/overrides/proxyExternalRequest/node.js +5 -0
  36. package/dist/overrides/wrappers/express-dev.js +3 -5
  37. package/dist/overrides/wrappers/node.js +2 -3
  38. package/dist/plugins/edge.js +3 -2
  39. package/dist/types/cache.d.ts +12 -4
  40. package/dist/types/global.d.ts +7 -0
  41. package/dist/types/open-next.d.ts +4 -1
  42. package/dist/types/overrides.d.ts +47 -0
  43. package/dist/utils/cache.d.ts +14 -0
  44. package/dist/utils/cache.js +21 -1
  45. package/package.json +3 -3
@@ -0,0 +1,42 @@
1
+ import { dynamicRouteMatcher } from "./routeMatcher";
2
+ // The placeholders of a route pattern: `[slug]`, `[...slug]` or `[[...slug]]`
3
+ const PLACEHOLDER_REGEX = /\[\[\.\.\.[^\]]+\]\]|\[(?:\.\.\.)?[^\]]+\]/g;
4
+ // A placeholder starts with `[`, which is either literal or percent encoded in the pathname
5
+ const OPENING_BRACKET_REGEX = /\[|%5b/i;
6
+ /**
7
+ * Returns whether a segment of the pathname is the placeholder of a matched dynamic App Router page.
8
+ *
9
+ * Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]` for `[slug]`) as an
10
+ * unresolved fallback param. Such values are not legitimate and must not reach Next.js.
11
+ *
12
+ * Segments are decoded like Next.js decodes param values (catch-all values are split on `/` first).
13
+ * Segments with a malformed percent encoding never match.
14
+ * Pathnames without an opening bracket return early, without matching the dynamic routes.
15
+ *
16
+ * @param pathname The internal pathname, which may include the basePath, a locale prefix and a trailing slash
17
+ * @returns Whether a segment of the pathname is a placeholder of a matched dynamic App Router page
18
+ */
19
+ export function hasRouteParamPlaceholder(pathname) {
20
+ if (!OPENING_BRACKET_REGEX.test(pathname)) {
21
+ return false;
22
+ }
23
+ const placeholders = new Set(dynamicRouteMatcher(pathname)
24
+ .filter(({ type }) => type === "app")
25
+ .flatMap(({ route }) => route.match(PLACEHOLDER_REGEX) ?? []));
26
+ return (placeholders.size > 0 &&
27
+ pathname.split("/").some((segment) => placeholders.has(safeDecode(segment))));
28
+ }
29
+ /**
30
+ * Decodes a URI component without throwing.
31
+ *
32
+ * @param value The encoded value
33
+ * @returns The decoded value, or an empty string when the encoding is malformed
34
+ */
35
+ function safeDecode(value) {
36
+ try {
37
+ return decodeURIComponent(value);
38
+ }
39
+ catch {
40
+ return "";
41
+ }
42
+ }
@@ -6,6 +6,7 @@ import { detectLocale } from "./routing/i18n";
6
6
  import { fixDataPage, getNextConfigHeaders, handleBeforeFilesRewrites, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
7
7
  import { handleMiddleware } from "./routing/middleware";
8
8
  import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
9
+ import { hasRouteParamPlaceholder } from "./routing/routeParamPlaceholder";
9
10
  import { constructNextUrl, normalizeLocationHeader } from "./routing/util";
10
11
  export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
11
12
  export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
@@ -159,7 +160,7 @@ export default async function routingHandler(event, { assetResolver }) {
159
160
  const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
160
161
  // Resolve against the final internal pathname after every rewrite phase.
161
162
  // These ordered matches are also the authoritative cache owner candidates.
162
- const resolvedRoutes = [
163
+ let resolvedRoutes = [
163
164
  ...staticRouteMatcher(eventOrResult.rawPath),
164
165
  ...dynamicRouteMatcher(eventOrResult.rawPath),
165
166
  ];
@@ -169,7 +170,12 @@ export default async function routingHandler(event, { assetResolver }) {
169
170
  isNextImageRoute ||
170
171
  // We need to check again once all rewrites have been applied
171
172
  staticRouteMatcher(eventOrResult.rawPath).length > 0 ||
172
- dynamicRouteMatcher(eventOrResult.rawPath).length > 0)) {
173
+ dynamicRouteMatcher(eventOrResult.rawPath).length > 0) ||
174
+ // Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]`) as unresolved.
175
+ // Next.js receives both the rawPath and the URL pathname, which is normalized (e.g. dot segments).
176
+ (!isExternalRewrite &&
177
+ [eventOrResult.rawPath, new URL(eventOrResult.url).pathname].some(hasRouteParamPlaceholder))) {
178
+ resolvedRoutes = [];
173
179
  eventOrResult = {
174
180
  ...eventOrResult,
175
181
  rawPath: "/404",
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) {
@@ -0,0 +1,35 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import { type Writable } from "node:stream";
3
+ /**
4
+ * Largest `content-length` that is verified by buffering the body.
5
+ *
6
+ * It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
7
+ * Larger responses are streamed without `content-length`.
8
+ */
9
+ export declare const MAX_VERIFIED_CONTENT_LENGTH = 10000000;
10
+ /**
11
+ * Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
12
+ *
13
+ * The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
14
+ * body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
15
+ * the keep-alive connection and be read as the next response.
16
+ *
17
+ * - `transfer-encoding` is always removed, Node.js picks the transfer coding.
18
+ * - A `content-length` that is not a valid length is removed.
19
+ * - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
20
+ * - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
21
+ * until this is known, and the head is written then, so the first byte is only sent once the whole body is
22
+ * received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
23
+ * removed and Node.js frames the body itself (chunked on HTTP/1.1).
24
+ *
25
+ * The status code and the headers are always validated synchronously, even when the head is written later.
26
+ *
27
+ * @param res The Node.js response.
28
+ * @param method The request method.
29
+ * @param statusCode The response status code.
30
+ * @param headers The response headers, keys are matched case-insensitively.
31
+ * @returns The stream to write the body to.
32
+ * @throws When the status code or a header is invalid, or the headers were already sent (same errors as
33
+ * `res.writeHead()`).
34
+ */
35
+ export declare function writeResponseHead(res: ServerResponse, method: string | undefined, statusCode: number, headers: Record<string, string>): Writable;
@@ -0,0 +1,181 @@
1
+ import { Transform } from "node:stream";
2
+ import { error } from "../adapters/logger";
3
+ /**
4
+ * Largest `content-length` that is verified by buffering the body.
5
+ *
6
+ * It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
7
+ * Larger responses are streamed without `content-length`.
8
+ */
9
+ export const MAX_VERIFIED_CONTENT_LENGTH = 10_000_000;
10
+ /**
11
+ * Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
12
+ *
13
+ * The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
14
+ * body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
15
+ * the keep-alive connection and be read as the next response.
16
+ *
17
+ * - `transfer-encoding` is always removed, Node.js picks the transfer coding.
18
+ * - A `content-length` that is not a valid length is removed.
19
+ * - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
20
+ * - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
21
+ * until this is known, and the head is written then, so the first byte is only sent once the whole body is
22
+ * received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
23
+ * removed and Node.js frames the body itself (chunked on HTTP/1.1).
24
+ *
25
+ * The status code and the headers are always validated synchronously, even when the head is written later.
26
+ *
27
+ * @param res The Node.js response.
28
+ * @param method The request method.
29
+ * @param statusCode The response status code.
30
+ * @param headers The response headers, keys are matched case-insensitively.
31
+ * @returns The stream to write the body to.
32
+ * @throws When the status code or a header is invalid, or the headers were already sent (same errors as
33
+ * `res.writeHead()`).
34
+ */
35
+ export function writeResponseHead(res, method, statusCode, headers) {
36
+ validateHead(res, statusCode);
37
+ let contentLength;
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ const lowerKey = key.toLowerCase();
40
+ if (lowerKey === "content-length") {
41
+ contentLength = value;
42
+ }
43
+ else if (lowerKey !== "transfer-encoding") {
44
+ // `setHeader()` validates the header like `writeHead()` does
45
+ res.setHeader(key, value);
46
+ }
47
+ }
48
+ const expectedLength = parseContentLength(contentLength);
49
+ if (!Number.isNaN(expectedLength) &&
50
+ (method?.toUpperCase() === "HEAD" || statusCode === 304)) {
51
+ res.setHeader("content-length", String(expectedLength));
52
+ res.writeHead(statusCode);
53
+ res.flushHeaders();
54
+ return res;
55
+ }
56
+ if (statusCode === 204 || !(expectedLength <= MAX_VERIFIED_CONTENT_LENGTH)) {
57
+ res.writeHead(statusCode);
58
+ res.flushHeaders();
59
+ return res;
60
+ }
61
+ const body = new ContentLengthVerifier(res, statusCode, expectedLength);
62
+ body.pipe(res);
63
+ return body;
64
+ }
65
+ /**
66
+ * Checks that the head can be written, the way `res.writeHead()` does.
67
+ *
68
+ * The headers are validated by `res.setHeader()`.
69
+ *
70
+ * @param res The Node.js response.
71
+ * @param statusCode The response status code.
72
+ * @throws {Error} When the headers were already sent (code `ERR_HTTP_HEADERS_SENT`).
73
+ * @throws {RangeError} When the status code is not in 100-999 (code `ERR_HTTP_INVALID_STATUS_CODE`).
74
+ */
75
+ function validateHead(res, statusCode) {
76
+ if (res.headersSent) {
77
+ throw Object.assign(new Error("Cannot write headers after they are sent to the client"), { code: "ERR_HTTP_HEADERS_SENT" });
78
+ }
79
+ const code = statusCode | 0;
80
+ if (code < 100 || code > 999) {
81
+ throw Object.assign(new RangeError(`Invalid status code: ${statusCode}`), {
82
+ code: "ERR_HTTP_INVALID_STATUS_CODE",
83
+ });
84
+ }
85
+ }
86
+ /**
87
+ * Parses a `content-length` header value.
88
+ *
89
+ * @param contentLength The header value.
90
+ * @returns The length, or `NaN` when the value is missing or not a valid length.
91
+ */
92
+ function parseContentLength(contentLength) {
93
+ const value = contentLength?.trim() ?? "";
94
+ const length = /^\d+$/.test(value) ? Number(value) : Number.NaN;
95
+ return Number.isSafeInteger(length) ? length : Number.NaN;
96
+ }
97
+ /**
98
+ * Buffers the body until it is known whether it has the declared length, then writes the head and the body.
99
+ *
100
+ * The headers, other than `content-length`, are already set on the response.
101
+ */
102
+ class ContentLengthVerifier extends Transform {
103
+ res;
104
+ statusCode;
105
+ expectedLength;
106
+ chunks = [];
107
+ length = 0;
108
+ headWritten = false;
109
+ /**
110
+ * @param res The Node.js response.
111
+ * @param statusCode The response status code, already validated.
112
+ * @param expectedLength The declared `content-length`.
113
+ */
114
+ constructor(res, statusCode, expectedLength) {
115
+ super();
116
+ this.res = res;
117
+ this.statusCode = statusCode;
118
+ this.expectedLength = expectedLength;
119
+ }
120
+ /**
121
+ * Buffers the chunk until the head is written, then passes it through.
122
+ *
123
+ * The head is written without `content-length` as soon as the body is longer than declared.
124
+ *
125
+ * @param chunk The body chunk.
126
+ * @param _encoding Unused, chunks are buffers.
127
+ * @param callback Called once the chunk is processed.
128
+ */
129
+ _transform(chunk, _encoding, callback) {
130
+ if (this.headWritten) {
131
+ callback(null, chunk);
132
+ return;
133
+ }
134
+ this.chunks.push(chunk);
135
+ this.length += chunk.length;
136
+ if (this.length > this.expectedLength) {
137
+ this.writeHead(false);
138
+ }
139
+ callback();
140
+ }
141
+ /**
142
+ * Writes the head, when not already written, once the whole body is received.
143
+ *
144
+ * @param callback Called once the head and the buffered chunks are written.
145
+ */
146
+ _flush(callback) {
147
+ if (!this.headWritten) {
148
+ this.writeHead(this.length === this.expectedLength);
149
+ }
150
+ callback();
151
+ }
152
+ /**
153
+ * Writes the head and the buffered chunks.
154
+ *
155
+ * This runs from the stream callbacks, where a thrown error would be uncaught and crash the process. The
156
+ * head is validated synchronously in `writeResponseHead()`, if writing it still fails, the response is destroyed.
157
+ *
158
+ * @param keepContentLength Whether the body has the declared length.
159
+ */
160
+ writeHead(keepContentLength) {
161
+ this.headWritten = true;
162
+ const chunks = this.chunks;
163
+ this.chunks = [];
164
+ try {
165
+ if (keepContentLength) {
166
+ this.res.setHeader("content-length", String(this.expectedLength));
167
+ }
168
+ this.res.writeHead(this.statusCode);
169
+ }
170
+ catch (e) {
171
+ error("Failed to write the response head", e);
172
+ this.res.destroy();
173
+ // Without an error, no `error` event is emitted
174
+ this.destroy();
175
+ return;
176
+ }
177
+ for (const chunk of chunks) {
178
+ this.push(chunk);
179
+ }
180
+ }
181
+ }
@@ -8,6 +8,17 @@ export declare const convertHeader: (header: http.OutgoingHttpHeader) => string;
8
8
  * @returns A list of Set-Cookie header
9
9
  */
10
10
  export declare function parseSetCookieHeader(cookies: string | string[] | null | undefined): string[];
11
+ /**
12
+ * Converts the headers of a response returned by an edge function.
13
+ *
14
+ * `Set-Cookie` headers are returned as a list.
15
+ * The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
16
+ * `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
17
+ *
18
+ * @param headers The headers of the response
19
+ * @returns The response headers to send to the client
20
+ */
21
+ export declare function convertEdgeResponseHeaders(headers: Pick<Headers, "forEach" | "getSetCookie">): Record<string, string | string[]>;
11
22
  /**
12
23
  *
13
24
  * Get the query object from an iterable of [key, value] pairs
package/dist/http/util.js CHANGED
@@ -58,6 +58,32 @@ export function parseSetCookieHeader(cookies) {
58
58
  }
59
59
  return cookies;
60
60
  }
61
+ /**
62
+ * Converts the headers of a response returned by an edge function.
63
+ *
64
+ * `Set-Cookie` headers are returned as a list.
65
+ * The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
66
+ * `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
67
+ *
68
+ * @param headers The headers of the response
69
+ * @returns The response headers to send to the client
70
+ */
71
+ export function convertEdgeResponseHeaders(headers) {
72
+ const responseHeaders = {};
73
+ headers.forEach((value, key) => {
74
+ const lowerKey = key.toLowerCase();
75
+ // Headers.forEach folds same-name headers; use getSetCookie() below instead.
76
+ if (lowerKey === "set-cookie" || lowerKey === "x-middleware-set-cookie") {
77
+ return;
78
+ }
79
+ responseHeaders[key] = value;
80
+ });
81
+ const setCookies = headers.getSetCookie();
82
+ if (setCookies.length > 0) {
83
+ responseHeaders["set-cookie"] = setCookies;
84
+ }
85
+ return responseHeaders;
86
+ }
61
87
  /**
62
88
  *
63
89
  * Get the query object from an iterable of [key, value] pairs
@@ -11,7 +11,8 @@ export default {
11
11
  ? [`/${path}`, `/${path}?_rsc=*`]
12
12
  : [
13
13
  `/${path}`,
14
- `/_next/data/${process.env.OPEN_NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
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({
@@ -4,7 +4,11 @@ const fetchProxy = {
4
4
  // @ts-ignore
5
5
  proxy: async (internalEvent) => {
6
6
  const { url, headers: eventHeaders, method, body } = internalEvent;
7
- const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) => key.toLowerCase() !== "cf-connecting-ip"));
7
+ const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) => !["cf-connecting-ip", "x-forwarded-host"].includes(key.toLowerCase())));
8
+ // Like Next.js, do not relay the `x-forwarded-host` sent by the client
9
+ if (eventHeaders.host) {
10
+ headers["x-forwarded-host"] = eventHeaders.host;
11
+ }
8
12
  const response = await fetch(url, {
9
13
  method,
10
14
  headers,
@@ -6,6 +6,7 @@ function filterHeadersForProxy(headers) {
6
6
  const filteredHeaders = {};
7
7
  const disallowedHeaders = [
8
8
  "host",
9
+ "x-forwarded-host",
9
10
  "connection",
10
11
  "via",
11
12
  "x-cache",
@@ -30,6 +31,10 @@ const nodeProxy = {
30
31
  debug("proxyRequest", url);
31
32
  return new Promise((resolve, reject) => {
32
33
  const filteredHeaders = filterHeadersForProxy(headers);
34
+ // Like Next.js, do not relay the `x-forwarded-host` sent by the client
35
+ if (headers.host) {
36
+ filteredHeaders["x-forwarded-host"] = headers.host;
37
+ }
33
38
  debug("filteredHeaders", filteredHeaders);
34
39
  const req = request(url, {
35
40
  headers: filteredHeaders,
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import express from "express";
3
3
  import { NextConfig } from "../../adapters/config/index.js";
4
+ import { writeResponseHead } from "../../http/framing.js";
4
5
  import { getMonorepoRelativePath } from "../../utils/normalize-path.js";
5
6
  const wrapper = async (handler, converter) => {
6
7
  const app = express();
@@ -16,8 +17,7 @@ const wrapper = async (handler, converter) => {
16
17
  const internalEvent = await converter.convertFrom(req);
17
18
  const streamCreator = {
18
19
  writeHeaders: (prelude) => {
19
- res.writeHead(prelude.statusCode, prelude.headers);
20
- return res;
20
+ return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
21
21
  },
22
22
  };
23
23
  await imageHandler(internalEvent, { streamCreator });
@@ -34,9 +34,7 @@ const wrapper = async (handler, converter) => {
34
34
  const streamCreator = {
35
35
  writeHeaders: (prelude) => {
36
36
  res.setHeader("Set-Cookie", prelude.cookies);
37
- res.writeHead(prelude.statusCode, prelude.headers);
38
- res.flushHeaders();
39
- return res;
37
+ return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
40
38
  },
41
39
  onFinish: () => { },
42
40
  abortSignal: abortController.signal,
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:http";
2
+ import { writeResponseHead } from "../../http/framing.js";
2
3
  import { debug, error } from "../../adapters/logger";
3
4
  const wrapper = async (handler, converter) => {
4
5
  const server = createServer(async (req, res) => {
@@ -7,9 +8,7 @@ const wrapper = async (handler, converter) => {
7
8
  const streamCreator = {
8
9
  writeHeaders: (prelude) => {
9
10
  res.setHeader("Set-Cookie", prelude.cookies);
10
- res.writeHead(prelude.statusCode, prelude.headers);
11
- res.flushHeaders();
12
- return res;
11
+ return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
13
12
  },
14
13
  abortSignal: abortController.signal,
15
14
  };
@@ -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 = NextConfig.deploymentId ?? BuildId;
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 };
@@ -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
  */
@@ -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
- /** @default undefined */
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 {};
@@ -1,4 +1,18 @@
1
1
  import type { CacheValue, NextModeTagCacheWriteInput, OriginalTagCacheWriteInput, WithLastModified } from "../types/overrides.js";
2
+ /**
3
+ * Computes the key used to store a composable (`"use cache"`) entry.
4
+ *
5
+ * The cache key provided by Next.js embeds the serialized arguments of the cached function and may
6
+ * contain any UTF-16 string. Storage backends transform keys (path normalization, URL parsing,
7
+ * UTF-8 encoding, ...) which could map distinct Next.js keys to the same stored entry.
8
+ *
9
+ * The storage key is the sha256 of the exact UTF-16 code units of the cache key, as a 64 characters
10
+ * lowercase hex string that every backend can store as is.
11
+ *
12
+ * @param cacheKey The cache key provided by Next.js
13
+ * @returns The key to use for the incremental cache and the tag cache
14
+ */
15
+ export declare function getComposableCacheStorageKey(cacheKey: string): string;
2
16
  /**
3
17
  *
4
18
  * @param key The key for that specific cache entry