@opennextjs/aws 3.8.4 → 3.9.0

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.
@@ -102,6 +102,12 @@ export default class Cache {
102
102
  }
103
103
  if (cacheData?.type === "page" || cacheData?.type === "app") {
104
104
  if (globalThis.isNextAfter15 && cacheData?.type === "app") {
105
+ const segmentData = new Map();
106
+ if (cacheData.segmentData) {
107
+ for (const [segmentPath, segmentContent] of Object.entries(cacheData.segmentData ?? {})) {
108
+ segmentData.set(segmentPath, Buffer.from(segmentContent));
109
+ }
110
+ }
105
111
  return {
106
112
  lastModified: _lastModified,
107
113
  value: {
@@ -111,6 +117,7 @@ export default class Cache {
111
117
  status: meta?.status,
112
118
  headers: meta?.headers,
113
119
  postponed: meta?.postponed,
120
+ segmentData,
114
121
  },
115
122
  };
116
123
  }
@@ -5,7 +5,9 @@ import { debug } from "../../logger.js";
5
5
  export async function optimizeImage(headers, imageParams, nextConfig, handleRequest) {
6
6
  const { isAbsolute, href } = imageParams;
7
7
  const imageUpstream = isAbsolute
8
- ? await fetchExternalImage(href)
8
+ ? //@ts-expect-error - fetchExternalImage signature has changed in Next.js 16, it has an extra boolean parameter.
9
+ // https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711
10
+ await fetchExternalImage(href)
9
11
  : await fetchInternalImage(href,
10
12
  // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used.
11
13
  { headers }, {}, // res object is not necessary as it's not actually used.
@@ -174,6 +174,7 @@ File ${serverPath} does not exist
174
174
  });
175
175
  // Only files that are actually copied
176
176
  const tracedFiles = [];
177
+ const erroredFiles = [];
177
178
  //Actually copy the files
178
179
  filesToCopy.forEach((to, from) => {
179
180
  // We don't want to copy excluded packages (e.g. sharp)
@@ -202,7 +203,16 @@ File ${serverPath} does not exist
202
203
  }
203
204
  }
204
205
  else {
205
- copyFileSync(from, to);
206
+ // Adding this inside a try-catch to handle errors on Next 16+
207
+ // where some files listed in the .nft.json might not be present in the standalone folder
208
+ // TODO: investigate that further - is it expected?
209
+ try {
210
+ copyFileSync(from, to);
211
+ }
212
+ catch (e) {
213
+ logger.debug("Error copying file:", e);
214
+ erroredFiles.push(to);
215
+ }
206
216
  }
207
217
  });
208
218
  readdirSync(standaloneNextDir)
@@ -265,7 +275,7 @@ File ${serverPath} does not exist
265
275
  }
266
276
  logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms");
267
277
  return {
268
- tracedFiles,
278
+ tracedFiles: tracedFiles.filter((f) => !erroredFiles.includes(f)),
269
279
  nodePackages,
270
280
  manifests,
271
281
  };
@@ -72,6 +72,10 @@ export function createCacheAssets(options) {
72
72
  const htmlPages = buildHelper.getHtmlPages(dotNextPath);
73
73
  const isFileSkipped = (relativePath) => relativePath.endsWith(".js") ||
74
74
  relativePath.endsWith(".js.nft.json") ||
75
+ // We skip manifest files as well
76
+ relativePath.endsWith("-manifest.json") ||
77
+ // We skip the segment rsc files as they are treated in a different way
78
+ relativePath.endsWith(".segment.rsc") ||
75
79
  (relativePath.endsWith(".html") && htmlPages.has(relativePath));
76
80
  // Merge cache files into a single file
77
81
  const cacheFilesPath = {};
@@ -116,6 +120,14 @@ export function createCacheAssets(options) {
116
120
  logger.warn(`Skipping invalid cache file: ${cacheFilePath}`);
117
121
  return;
118
122
  }
123
+ // If we have a meta file, and it contains segmentPaths, we need to add them to the cache file
124
+ const segments = Array.isArray(cacheFileMeta?.segmentPaths)
125
+ ? Object.fromEntries(cacheFileMeta.segmentPaths.map((segmentPath) => {
126
+ const absoluteSegmentPath = path.join(files.meta.replace(/\.meta$/, ".segments"), `${segmentPath}.segment.rsc`);
127
+ const segmentContent = fs.readFileSync(absoluteSegmentPath, "utf8");
128
+ return [segmentPath, segmentContent];
129
+ }))
130
+ : {};
119
131
  const cacheFileContent = {
120
132
  type: files.body ? "route" : files.json ? "page" : "app",
121
133
  meta: cacheFileMeta,
@@ -129,6 +141,7 @@ export function createCacheAssets(options) {
129
141
  ? "base64"
130
142
  : "utf8")
131
143
  : undefined,
144
+ segmentData: Object.keys(segments).length > 0 ? segments : undefined,
132
145
  };
133
146
  // Ensure directory exists before writing
134
147
  fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true });
@@ -3,7 +3,7 @@ import { NextConfig, PrerenderManifest } from "../../adapters/config/index";
3
3
  import { emptyReadableStream, toReadableStream } from "../../utils/stream";
4
4
  import { isBinaryContentType } from "../../utils/binary";
5
5
  import { getTagsFromValue, hasBeenRevalidated } from "../../utils/cache";
6
- import { debug } from "../../adapters/logger";
6
+ import { debug, error } from "../../adapters/logger";
7
7
  import { localizePath } from "./i18n";
8
8
  import { generateMessageGroupId } from "./queue";
9
9
  const CACHE_ONE_YEAR = 60 * 60 * 24 * 365;
@@ -18,6 +18,9 @@ const CACHE_ONE_MONTH = 60 * 60 * 24 * 30;
18
18
  * Also see this PR: https://github.com/vercel/next.js/pull/79426
19
19
  */
20
20
  const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url";
21
+ const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
22
+ const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
23
+ const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
21
24
  async function computeCacheControl(path, body, host, revalidate, lastModified) {
22
25
  let finalRevalidate = CACHE_ONE_YEAR;
23
26
  const existingRoute = Object.entries(PrerenderManifest.routes).find((p) => p[0] === path)?.[1];
@@ -81,24 +84,54 @@ async function computeCacheControl(path, body, host, revalidate, lastModified) {
81
84
  etag,
82
85
  };
83
86
  }
87
+ function getBodyForAppRouter(event, cachedValue) {
88
+ if (cachedValue.type !== "app") {
89
+ throw new Error("getBodyForAppRouter called with non-app cache value");
90
+ }
91
+ try {
92
+ const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;
93
+ const isSegmentResponse = Boolean(segmentHeader) &&
94
+ segmentHeader in (cachedValue.segmentData || {});
95
+ const body = isSegmentResponse
96
+ ? cachedValue.segmentData[segmentHeader]
97
+ : cachedValue.rsc;
98
+ return {
99
+ body,
100
+ additionalHeaders: isSegmentResponse
101
+ ? { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" }
102
+ : {},
103
+ };
104
+ }
105
+ catch (e) {
106
+ error("Error while getting body for app router from cache:", e);
107
+ return { body: cachedValue.rsc, additionalHeaders: {} };
108
+ }
109
+ }
84
110
  async function generateResult(event, localizedPath, cachedValue, lastModified) {
85
111
  debug("Returning result from experimental cache");
86
112
  let body = "";
87
113
  let type = "application/octet-stream";
88
114
  let isDataRequest = false;
89
- switch (cachedValue.type) {
90
- case "app":
91
- isDataRequest = Boolean(event.headers.rsc);
92
- body = isDataRequest ? cachedValue.rsc : cachedValue.html;
93
- type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8";
94
- break;
95
- case "page":
96
- isDataRequest = Boolean(event.query.__nextDataReq);
97
- body = isDataRequest
98
- ? JSON.stringify(cachedValue.json)
99
- : cachedValue.html;
100
- type = isDataRequest ? "application/json" : "text/html; charset=utf-8";
101
- break;
115
+ let additionalHeaders = {};
116
+ if (cachedValue.type === "app") {
117
+ isDataRequest = Boolean(event.headers.rsc);
118
+ if (isDataRequest) {
119
+ const { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue);
120
+ body = appRouterBody;
121
+ additionalHeaders = appHeaders;
122
+ }
123
+ else {
124
+ body = cachedValue.html;
125
+ }
126
+ type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8";
127
+ }
128
+ else if (cachedValue.type === "page") {
129
+ isDataRequest = Boolean(event.query.__nextDataReq);
130
+ body = isDataRequest ? JSON.stringify(cachedValue.json) : cachedValue.html;
131
+ type = isDataRequest ? "application/json" : "text/html; charset=utf-8";
132
+ }
133
+ else {
134
+ throw new Error("generateResult called with unsupported cache value type, only 'app' and 'page' are supported");
102
135
  }
103
136
  const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified);
104
137
  return {
@@ -116,6 +149,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified) {
116
149
  "content-type": type,
117
150
  ...cachedValue.meta?.headers,
118
151
  vary: VARY_HEADER,
152
+ ...additionalHeaders,
119
153
  },
120
154
  };
121
155
  }
@@ -295,6 +295,13 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
295
295
  * @__PURE__
296
296
  */
297
297
  export function fixISRHeaders(headers) {
298
+ const sMaxAgeRegex = /s-maxage=(\d+)/;
299
+ const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex);
300
+ const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
301
+ // We only apply the fix if the cache-control header contains s-maxage
302
+ if (!sMaxAge) {
303
+ return;
304
+ }
298
305
  if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
299
306
  headers[CommonHeaders.CACHE_CONTROL] =
300
307
  "private, no-cache, no-store, max-age=0, must-revalidate";
@@ -302,18 +309,11 @@ export function fixISRHeaders(headers) {
302
309
  }
303
310
  const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
304
311
  if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) {
305
- // calculate age
306
- const age = Math.round((Date.now() - _lastModified) / 1000);
307
- // extract s-maxage from cache-control
308
- const regex = /s-maxage=(\d+)/;
309
- const cacheControl = headers[CommonHeaders.CACHE_CONTROL];
310
- debug("cache-control", cacheControl, _lastModified, Date.now());
311
- if (typeof cacheControl !== "string")
312
- return;
313
- const match = cacheControl.match(regex);
314
- const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
312
+ debug("cache-control", headers[CommonHeaders.CACHE_CONTROL], _lastModified, Date.now());
315
313
  // 31536000 is the default s-maxage value for SSG pages
316
314
  if (sMaxAge && sMaxAge !== 31536000) {
315
+ // calculate age
316
+ const age = Math.round((Date.now() - _lastModified) / 1000);
317
317
  const remainingTtl = Math.max(sMaxAge - age, 1);
318
318
  headers[CommonHeaders.CACHE_CONTROL] =
319
319
  `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
@@ -4,7 +4,7 @@ import type { TransformCallback } from "node:stream";
4
4
  import { Transform } from "node:stream";
5
5
  import type { StreamCreator } from "../types/open-next";
6
6
  export declare class OpenNextNodeResponse extends Transform implements ServerResponse {
7
- private fixHeaders;
7
+ private fixHeadersFn;
8
8
  private onEnd;
9
9
  private streamCreator?;
10
10
  private initialHeaders?;
@@ -13,6 +13,7 @@ export declare class OpenNextNodeResponse extends Transform implements ServerRes
13
13
  headers: OutgoingHttpHeaders;
14
14
  headersSent: boolean;
15
15
  _chunks: Buffer[];
16
+ headersAlreadyFixed: boolean;
16
17
  private _cookies;
17
18
  private responseStream?;
18
19
  private bodyLength;
@@ -35,7 +36,7 @@ export declare class OpenNextNodeResponse extends Transform implements ServerRes
35
36
  socket: Socket | null;
36
37
  setTimeout(_msecs: number, _callback?: (() => void) | undefined): this;
37
38
  addTrailers(_headers: OutgoingHttpHeaders | readonly [string, string][]): void;
38
- constructor(fixHeaders: (headers: OutgoingHttpHeaders) => void, onEnd: (headers: OutgoingHttpHeaders) => Promise<void>, streamCreator?: StreamCreator | undefined, initialHeaders?: OutgoingHttpHeaders | undefined, statusCode?: number);
39
+ constructor(fixHeadersFn: (headers: OutgoingHttpHeaders) => void, onEnd: (headers: OutgoingHttpHeaders) => Promise<void>, streamCreator?: StreamCreator | undefined, initialHeaders?: OutgoingHttpHeaders | undefined, statusCode?: number);
39
40
  get originalResponse(): this;
40
41
  get finished(): boolean;
41
42
  setHeader(name: string, value: string | string[]): this;
@@ -51,8 +52,9 @@ export declare class OpenNextNodeResponse extends Transform implements ServerRes
51
52
  /**
52
53
  * OpenNext specific method
53
54
  */
55
+ fixHeaders(headers: OutgoingHttpHeaders): void;
54
56
  getFixedHeaders(): OutgoingHttpHeaders;
55
- getBody(): Buffer;
57
+ getBody(): Buffer<ArrayBuffer>;
56
58
  private _internalWrite;
57
59
  _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void;
58
60
  _flush(callback: TransformCallback): void;
@@ -5,7 +5,7 @@ const SET_COOKIE_HEADER = "set-cookie";
5
5
  const CANNOT_BE_USED = "This cannot be used in OpenNext";
6
6
  // We only need to implement the methods that are used by next.js
7
7
  export class OpenNextNodeResponse extends Transform {
8
- fixHeaders;
8
+ fixHeadersFn;
9
9
  onEnd;
10
10
  streamCreator;
11
11
  initialHeaders;
@@ -14,6 +14,7 @@ export class OpenNextNodeResponse extends Transform {
14
14
  headers = {};
15
15
  headersSent = false;
16
16
  _chunks = [];
17
+ headersAlreadyFixed = false;
17
18
  _cookies = [];
18
19
  responseStream;
19
20
  bodyLength = 0;
@@ -52,9 +53,9 @@ export class OpenNextNodeResponse extends Transform {
52
53
  addTrailers(_headers) {
53
54
  throw new Error(CANNOT_BE_USED);
54
55
  }
55
- constructor(fixHeaders, onEnd, streamCreator, initialHeaders, statusCode) {
56
+ constructor(fixHeadersFn, onEnd, streamCreator, initialHeaders, statusCode) {
56
57
  super();
57
- this.fixHeaders = fixHeaders;
58
+ this.fixHeadersFn = fixHeadersFn;
58
59
  this.onEnd = onEnd;
59
60
  this.streamCreator = streamCreator;
60
61
  this.initialHeaders = initialHeaders;
@@ -210,6 +211,13 @@ export class OpenNextNodeResponse extends Transform {
210
211
  /**
211
212
  * OpenNext specific method
212
213
  */
214
+ fixHeaders(headers) {
215
+ if (this.headersAlreadyFixed) {
216
+ return;
217
+ }
218
+ this.fixHeadersFn(headers);
219
+ this.headersAlreadyFixed = true;
220
+ }
213
221
  getFixedHeaders() {
214
222
  // Do we want to apply this on writeHead?
215
223
  this.fixHeaders(this.headers);
@@ -222,13 +230,18 @@ export class OpenNextNodeResponse extends Transform {
222
230
  return Buffer.concat(this._chunks);
223
231
  }
224
232
  _internalWrite(chunk, encoding) {
225
- const buffer = Buffer.from(chunk, encoding);
233
+ // When encoding === 'buffer', chunk is already a Buffer
234
+ // and does not need to be converted again.
235
+ // @ts-expect-error TS2367 'encoding' can be 'buffer', but it's not in the
236
+ // official type definition
237
+ const buffer = encoding === "buffer" ? chunk : Buffer.from(chunk, encoding);
226
238
  this.bodyLength += buffer.length;
227
239
  if (this.streamCreator?.retainChunks !== false) {
228
240
  // Avoid keeping chunks around when the `StreamCreator` supports it to save memory
229
241
  this._chunks.push(buffer);
230
242
  }
231
- this.push(chunk, encoding);
243
+ // No need to pass the encoding for buffers
244
+ this.push(buffer);
232
245
  this.streamCreator?.onWrite?.();
233
246
  }
234
247
  _transform(chunk, encoding, callback) {
package/dist/http/util.js CHANGED
@@ -1,4 +1,4 @@
1
- import logger from "../logger";
1
+ import { warn } from "../adapters/logger";
2
2
  export const parseHeaders = (headers) => {
3
3
  const result = {};
4
4
  if (!headers) {
@@ -18,11 +18,11 @@ export const parseHeaders = (headers) => {
18
18
  * and https://github.com/opennextjs/opennextjs-aws/pull/977#issuecomment-3261763114
19
19
  */
20
20
  if (keyLower === "location" && Array.isArray(value)) {
21
- if (value[0] === value[1]) {
21
+ if (value.length === 1 || value[0] === value[1]) {
22
22
  result[keyLower] = value[0];
23
23
  }
24
24
  else {
25
- logger.warn("Multiple different values for Location header found. Using the last one");
25
+ warn("Multiple different values for Location header found. Using the last one");
26
26
  result[keyLower] = value[value.length - 1];
27
27
  }
28
28
  continue;
@@ -8,7 +8,7 @@ const fetchProxy = {
8
8
  const response = await fetch(url, {
9
9
  method,
10
10
  headers,
11
- body,
11
+ body: body,
12
12
  });
13
13
  const responseHeaders = {};
14
14
  response.headers.forEach((value, key) => {
@@ -52,7 +52,12 @@ const handler = async (handler, converter) => async (request, env, ctx, abortSig
52
52
  resolveResponse(response);
53
53
  return new Writable({
54
54
  write(chunk, encoding, callback) {
55
- controller.enqueue(chunk);
55
+ try {
56
+ controller.enqueue(chunk);
57
+ }
58
+ catch (e) {
59
+ return callback(e);
60
+ }
56
61
  callback();
57
62
  },
58
63
  final(callback) {
@@ -6,16 +6,9 @@ export interface ResponseStream extends Writable {
6
6
  setContentType(contentType: string): void;
7
7
  }
8
8
  type Handler = (event: APIGatewayProxyEventV2, responseStream: ResponseStream, context: Context) => Promise<any>;
9
- interface Metadata {
10
- statusCode: number;
11
- headers: Record<string, string>;
12
- }
13
9
  declare global {
14
10
  namespace awslambda {
15
11
  function streamifyResponse(handler: Handler): Handler;
16
- namespace HttpResponseStream {
17
- function from(res: Writable, metadata: Metadata): ResponseStream;
18
- }
19
12
  }
20
13
  }
21
14
  export type AwsLambdaEvent = APIGatewayProxyEventV2 | CloudFrontRequestEvent | APIGatewayProxyEvent | WarmerEvent;
@@ -42,6 +42,7 @@ export type CachedFile = {
42
42
  html: string;
43
43
  rsc: string;
44
44
  meta?: Meta;
45
+ segmentData?: Record<string, string>;
45
46
  } | {
46
47
  type: "route";
47
48
  body: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.8.4",
6
+ "version": "3.9.0",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@ast-grep/napi": "^0.35.0",
36
+ "@ast-grep/napi": "^0.40.0",
37
37
  "@aws-sdk/client-cloudfront": "3.398.0",
38
38
  "@aws-sdk/client-dynamodb": "^3.398.0",
39
39
  "@aws-sdk/client-lambda": "^3.398.0",
@@ -41,22 +41,23 @@
41
41
  "@aws-sdk/client-sqs": "^3.398.0",
42
42
  "@node-minify/core": "^8.0.6",
43
43
  "@node-minify/terser": "^8.0.6",
44
- "@tsconfig/node18": "^1.0.1",
45
- "aws4fetch": "^1.0.18",
46
- "chalk": "^5.3.0",
44
+ "@tsconfig/node18": "^1.0.3",
45
+ "aws4fetch": "^1.0.20",
46
+ "chalk": "^5.6.2",
47
47
  "cookie": "^1.0.2",
48
48
  "esbuild": "0.25.4",
49
- "express": "5.0.1",
49
+ "express": "^5.1.0",
50
50
  "path-to-regexp": "^6.3.0",
51
- "urlpattern-polyfill": "^10.0.0",
52
- "yaml": "^2.7.0"
51
+ "urlpattern-polyfill": "^10.1.0",
52
+ "yaml": "^2.8.1"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/aws-lambda": "^8.10.109",
55
+ "@types/aws-lambda": "^8.10.158",
56
56
  "@types/express": "5.0.0",
57
57
  "@types/node": "20.17.6",
58
- "tsc-alias": "^1.8.8",
59
- "typescript": "5.6.3"
58
+ "concurrently": "^9.2.1",
59
+ "tsc-alias": "^1.8.16",
60
+ "typescript": "5.9.3"
60
61
  },
61
62
  "bugs": {
62
63
  "url": "https://github.com/opennextjs/opennextjs-aws/issues"
@@ -68,6 +69,6 @@
68
69
  },
69
70
  "scripts": {
70
71
  "build": "tsc && tsc-alias",
71
- "dev": "tsc -w"
72
+ "dev": "concurrently \"tsc -w\" \"tsc-alias -w\""
72
73
  }
73
74
  }