@opennextjs/aws 4.1.15 → 4.1.16

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.
@@ -167,8 +167,9 @@ export default class Cache {
167
167
  const store = globalThis.__openNextAls.getStore();
168
168
  if (store) {
169
169
  // The sentinel is kept as is in the store so that the revalidation queue keeps
170
- // deduplicating every stale request for the same path.
170
+ // deduplicating every stale request for the same path. The queue gets the real time.
171
171
  store.lastModified = _isStale ? 1 : _lastModified;
172
+ store.entryLastModified = _lastModified;
172
173
  }
173
174
  if (_isStale) {
174
175
  _lastModified = getStaleLastModified(cacheData.revalidate);
@@ -2,6 +2,22 @@ import type { OpenNextNodeResponse } from "../http/index.js";
2
2
  import { IncomingMessage } from "../http/index.js";
3
3
  import type { InternalEvent, InternalResult, RoutingResult } from "../types/open-next.js";
4
4
  import type { OpenNextHandlerOptions } from "../types/overrides.js";
5
+ type HeaderRecord = InternalEvent["headers"];
6
+ /**
7
+ * Splits the headers of a routed event into the request and response headers.
8
+ *
9
+ * The routing adds the middleware and `next.config` response headers to the event with a prefix each (see
10
+ * `applyResponseHeaders`). As in Next.js, both go on the response but only the middleware headers go on the request:
11
+ * a `Content-Security-Policy` from `next.config` must not replace the one, holding the script nonce, that the
12
+ * middleware set on the request.
13
+ *
14
+ * @param eventHeaders The headers of the routed event, not modified
15
+ * @returns The headers to send to Next.js and the headers to set on the response
16
+ */
17
+ export declare function splitRoutingHeaders(eventHeaders: HeaderRecord): {
18
+ requestHeaders: HeaderRecord;
19
+ responseHeaders: HeaderRecord;
20
+ };
5
21
  export declare function openNextHandler(internalEvent: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
6
22
  /**
7
23
  * Renders the next resolved route after a `fallback: false` route threw a `NoFallbackError`.
@@ -26,3 +42,4 @@ export declare function openNextHandler(internalEvent: InternalEvent, options?:
26
42
  * @return A promise that resolves after rendering a route or an error response
27
43
  */
28
44
  export declare function handleNoFallbackError(req: IncomingMessage, res: OpenNextNodeResponse, routingResult: RoutingResult, metadata: Record<string, unknown>, index?: number): Promise<void>;
45
+ export {};
@@ -8,13 +8,45 @@ import { debug, error } from "../adapters/logger";
8
8
  import { patchAsyncStorage } from "./patchAsyncStorage";
9
9
  import { getRouteMatchMetadata } from "./routeMatchMetadata";
10
10
  import { constructNextUrl, convertRes, convertToQuery, createServerResponse, } from "./routing/util";
11
- import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_REWRITE_STATUS_CODE, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
11
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_REWRITE_STATUS_CODE, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, NEXT_CONFIG_HEADER_PREFIX, NEXT_CONFIG_HEADER_PREFIX_LEN, } from "./routingHandler";
12
12
  import { requestHandler, setNextjsPrebundledReact } from "./util";
13
13
  // This is used to identify requests in the cache
14
14
  globalThis.__openNextAls = new AsyncLocalStorage();
15
15
  //#override patchAsyncStorage
16
16
  patchAsyncStorage();
17
17
  //#endOverride
18
+ /**
19
+ * Splits the headers of a routed event into the request and response headers.
20
+ *
21
+ * The routing adds the middleware and `next.config` response headers to the event with a prefix each (see
22
+ * `applyResponseHeaders`). As in Next.js, both go on the response but only the middleware headers go on the request:
23
+ * a `Content-Security-Policy` from `next.config` must not replace the one, holding the script nonce, that the
24
+ * middleware set on the request.
25
+ *
26
+ * @param eventHeaders The headers of the routed event, not modified
27
+ * @returns The headers to send to Next.js and the headers to set on the response
28
+ */
29
+ export function splitRoutingHeaders(eventHeaders) {
30
+ const requestHeaders = {};
31
+ const middlewareHeaders = {};
32
+ const nextConfigHeaders = {};
33
+ for (const [key, value] of Object.entries(eventHeaders)) {
34
+ if (key.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
35
+ middlewareHeaders[key.slice(MIDDLEWARE_HEADER_PREFIX_LEN)] = value;
36
+ }
37
+ else if (key.startsWith(NEXT_CONFIG_HEADER_PREFIX)) {
38
+ nextConfigHeaders[key.slice(NEXT_CONFIG_HEADER_PREFIX_LEN)] = value;
39
+ }
40
+ else {
41
+ requestHeaders[key] = value;
42
+ }
43
+ }
44
+ return {
45
+ requestHeaders: { ...requestHeaders, ...middlewareHeaders },
46
+ // The routing already dropped the `next.config` headers that must not take precedence.
47
+ responseHeaders: { ...middlewareHeaders, ...nextConfigHeaders },
48
+ };
49
+ }
18
50
  export async function openNextHandler(internalEvent, options) {
19
51
  const initialHeaders = internalEvent.headers;
20
52
  // We only use the requestId header if we are using an external middleware
@@ -60,22 +92,20 @@ export async function openNextHandler(internalEvent, options) {
60
92
  assetResolver: globalThis.assetResolver,
61
93
  });
62
94
  //#endOverride
63
- const headers = "type" in routingResult
64
- ? routingResult.headers
65
- : routingResult.internalEvent.headers;
66
- const overwrittenResponseHeaders = {};
67
- for (const [rawKey, value] of Object.entries(headers)) {
68
- if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
69
- continue;
70
- }
71
- const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN);
72
- // We skip this header here since it is used by Next internally and we don't want it on the response headers.
73
- // This header needs to be present in the request headers for processRequest, so cookies().get() from Next will work on initial render.
74
- if (key !== "x-middleware-set-cookie") {
75
- overwrittenResponseHeaders[key] = value;
76
- }
77
- headers[key] = value;
78
- delete headers[rawKey];
95
+ // The headers of a result are not prefixed, they are the response headers.
96
+ let requestHeaders = {};
97
+ let overwrittenResponseHeaders = {};
98
+ if (!("type" in routingResult)) {
99
+ const split = splitRoutingHeaders(routingResult.internalEvent.headers);
100
+ requestHeaders = split.requestHeaders;
101
+ overwrittenResponseHeaders = split.responseHeaders;
102
+ routingResult = {
103
+ ...routingResult,
104
+ internalEvent: {
105
+ ...routingResult.internalEvent,
106
+ headers: requestHeaders,
107
+ },
108
+ };
79
109
  }
80
110
  if ("isExternalRewrite" in routingResult &&
81
111
  routingResult.isExternalRewrite === true) {
@@ -138,7 +168,7 @@ export async function openNextHandler(internalEvent, options) {
138
168
  // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh
139
169
  // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests)
140
170
  headers: {
141
- ...headers,
171
+ ...requestHeaders,
142
172
  //#override appendPrefetch
143
173
  purpose: "prefetch",
144
174
  //#endOverride
@@ -121,6 +121,7 @@ export function createRevalidate(revalidateInProcess) {
121
121
  ? globalThis.__openNextAls.run({
122
122
  ...store,
123
123
  lastModified: undefined,
124
+ entryLastModified: undefined,
124
125
  writtenTags: new Set(),
125
126
  requestCache: new RequestCache(),
126
127
  }, renderInProcess)
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AppPathsManifest, NextConfig, PagesManifest, PrerenderManifest, } from "../../adapters/config/index.js";
3
+ import { decodePathParams } from "../../utils/normalize-path.js";
3
4
  import { emptyReadableStream, toReadableStream } from "../../utils/stream.js";
4
5
  import { isBinaryContentType } from "../../utils/binary.js";
5
6
  import { getTagsFromValue, hasBeenRevalidated, isStale } from "../../utils/cache.js";
@@ -255,26 +256,6 @@ function computeStatusCode(rewriteStatusCode, cachedStatusCode) {
255
256
  }
256
257
  return rewriteStatusCode ?? cachedStatusCode ?? 200;
257
258
  }
258
- /**
259
- *
260
- * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10
261
- */
262
- function escapePathDelimiters(segment, escapeEncoded) {
263
- return segment.replace(new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), (char) => encodeURIComponent(char));
264
- }
265
- /**
266
- *
267
- * SSG cache key needs to be decoded, but some characters needs to be properly escaped
268
- * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
269
- * Decoding must be atomic, as in Next.js. Partially decoding a malformed path
270
- * could select a different cache route than the one evaluated by middleware.
271
- */
272
- function decodePathParams(pathname) {
273
- return pathname
274
- .split("/")
275
- .map((segment) => escapePathDelimiters(decodeURIComponent(segment), true))
276
- .join("/");
277
- }
278
259
  let routeCacheManifests;
279
260
  function getRouteCacheManifests() {
280
261
  routeCacheManifests ??= {
@@ -20,9 +20,12 @@ export declare function getCompiledRegExp(pattern: string, flags?: string): RegE
20
20
  * interpolation, with condition parameters taking precedence. See
21
21
  * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L400-L414
22
22
  *
23
+ * Header names are lowercased. As Next.js applies them with the case-insensitive `res.setHeader()`, the last matching
24
+ * route wins when several set the same header, whatever its case.
25
+ *
23
26
  * @param event The request used to match configured headers
24
27
  * @param configHeaders The configured header routes
25
- * @returns The response headers produced by every matching route
28
+ * @returns The response headers produced by every matching route, keyed by lowercase name
26
29
  * @throws {SyntaxError} When a configured route pattern is invalid
27
30
  */
28
31
  export declare function getNextConfigHeaders(event: InternalEvent, configHeaders?: Header[] | undefined): Record<string, string | undefined>;
@@ -78,6 +81,16 @@ export declare function handleRewrites<T extends RewriteDefinition>(event: Inter
78
81
  export declare function handleBeforeFilesRewrites<T extends RewriteDefinition>(event: InternalEvent, rewrites: T[]): ReturnType<typeof handleRewrites<T>>;
79
82
  export declare function handleRedirects(event: InternalEvent, redirects: RedirectDefinition[]): InternalResult | undefined;
80
83
  export declare function fixDataPage(internalEvent: InternalEvent, buildId: string): InternalEvent | InternalResult;
84
+ /**
85
+ * Rewrites the event to `/404` for paths that `fallback: false` routes do not pregenerate.
86
+ *
87
+ * A path is pregenerated when its decoded form is a key of the prerender manifest routes.
88
+ * The rewrite only applies when no other static or dynamic route can serve the path.
89
+ *
90
+ * @param internalEvent The incoming event
91
+ * @param prerenderManifest The prerender manifest
92
+ * @returns The (possibly rewritten) event and whether the path is pregenerated
93
+ */
81
94
  export declare function handleFallbackFalse(internalEvent: InternalEvent, prerenderManifest?: PrerenderManifest): {
82
95
  event: InternalEvent;
83
96
  isISR: boolean;
@@ -1,6 +1,6 @@
1
1
  import { NextConfig } from "../../adapters/config/index.js";
2
2
  import { compile, match, parse } from "path-to-regexp";
3
- import { normalizeRepeatedSlashes } from "../../utils/normalize-path.js";
3
+ import { decodePathParams, normalizeRepeatedSlashes, } from "../../utils/normalize-path.js";
4
4
  import { emptyReadableStream, toReadableStream } from "../../utils/stream.js";
5
5
  import { debug } from "../../adapters/logger";
6
6
  import { handleLocaleRedirect, localizePath } from "./i18n";
@@ -223,9 +223,12 @@ function compileQueryString(queryString, params) {
223
223
  * interpolation, with condition parameters taking precedence. See
224
224
  * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L400-L414
225
225
  *
226
+ * Header names are lowercased. As Next.js applies them with the case-insensitive `res.setHeader()`, the last matching
227
+ * route wins when several set the same header, whatever its case.
228
+ *
226
229
  * @param event The request used to match configured headers
227
230
  * @param configHeaders The configured header routes
228
- * @returns The response headers produced by every matching route
231
+ * @returns The response headers produced by every matching route, keyed by lowercase name
229
232
  * @throws {SyntaxError} When a configured route pattern is invalid
230
233
  */
231
234
  export function getNextConfigHeaders(event, configHeaders) {
@@ -254,11 +257,11 @@ export function getNextConfigHeaders(event, configHeaders) {
254
257
  try {
255
258
  const key = hasParams ? compileNonPath(h.key, params) : h.key;
256
259
  const value = hasParams ? compileNonPath(h.value, params) : h.value;
257
- requestHeaders[key] = value;
260
+ requestHeaders[key.toLowerCase()] = value;
258
261
  }
259
262
  catch {
260
263
  debug(`Error matching header ${h.key} with value ${h.value}`);
261
- requestHeaders[h.key] = h.value;
264
+ requestHeaders[h.key.toLowerCase()] = h.value;
262
265
  }
263
266
  });
264
267
  }
@@ -540,6 +543,16 @@ export function fixDataPage(internalEvent, buildId) {
540
543
  }
541
544
  return internalEvent;
542
545
  }
546
+ /**
547
+ * Rewrites the event to `/404` for paths that `fallback: false` routes do not pregenerate.
548
+ *
549
+ * A path is pregenerated when its decoded form is a key of the prerender manifest routes.
550
+ * The rewrite only applies when no other static or dynamic route can serve the path.
551
+ *
552
+ * @param internalEvent The incoming event
553
+ * @param prerenderManifest The prerender manifest
554
+ * @returns The (possibly rewritten) event and whether the path is pregenerated
555
+ */
543
556
  export function handleFallbackFalse(internalEvent, prerenderManifest) {
544
557
  const { rawPath } = internalEvent;
545
558
  const { dynamicRoutes = {}, routes = {} } = prerenderManifest ?? {};
@@ -563,7 +576,14 @@ export function handleFallbackFalse(internalEvent, prerenderManifest) {
563
576
  const matchedStaticRoute = staticRouteMatcher(localizedPath);
564
577
  const prerenderedFallbackRoutesName = prerenderedFallbackRoutes.map(([name]) => name);
565
578
  const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter(({ route }) => !prerenderedFallbackRoutesName.includes(route));
566
- const isPregenerated = Object.keys(routes).includes(localizedPath);
579
+ let isPregenerated = false;
580
+ try {
581
+ // The prerender manifest keys routes by their decoded path while rawPath is percent-encoded.
582
+ isPregenerated = Object.hasOwn(routes, decodePathParams(localizedPath));
583
+ }
584
+ catch {
585
+ // A malformed escape sequence cannot name a prerendered route.
586
+ }
567
587
  if (routeFallback &&
568
588
  !isPregenerated &&
569
589
  matchedStaticRoute.length === 0 &&
@@ -101,10 +101,13 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
101
101
  let middlewareSetCookie;
102
102
  // These are internal headers used by Next.js, we don't want to expose them to the client
103
103
  // NOTE: keys must be in lowercase to match the normalized header keys.
104
+ // See `resolve-routes.ts` in Next.js for the headers it drops from the middleware response.
104
105
  const filteredHeaders = [
105
106
  "x-middleware-override-headers",
106
107
  "x-middleware-next",
107
108
  "x-middleware-rewrite",
109
+ "x-middleware-redirect",
110
+ "x-middleware-refresh",
108
111
  // We need to drop `content-encoding` because it will be decoded
109
112
  "content-encoding",
110
113
  ];
@@ -303,12 +303,20 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
303
303
  // If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
304
304
  try {
305
305
  const hash = (str) => crypto.createHash("md5").update(str).digest("hex");
306
- const lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
306
+ const store = globalThis.__openNextAls.getStore();
307
+ // `1` when the entry is stale by tag, so that the deduplication id is the same for every copy of the entry
308
+ const lastModified = store?.lastModified ?? 0;
307
309
  // For some weird cases, lastModified is not set, haven't been able to figure out yet why
308
310
  // For those cases we add the etag to the deduplication id, it might help
309
311
  const eTag = `${headers.etag ?? headers.ETag ?? ""}`;
310
312
  await globalThis.queue.send({
311
- MessageBody: { host, url: revalidateUrl, eTag, lastModified },
313
+ MessageBody: {
314
+ host,
315
+ url: revalidateUrl,
316
+ eTag,
317
+ // The time of the entry, so that the queue can skip it when it was revalidated since
318
+ lastModified: store?.entryLastModified ?? lastModified,
319
+ },
312
320
  MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${eTag}`),
313
321
  MessageGroupId: generateMessageGroupId(rawPath),
314
322
  });
@@ -3,6 +3,8 @@ import type { AssetResolver } from "../types/overrides.js";
3
3
  export declare const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
4
4
  export declare const MIDDLEWARE_HEADER_PREFIX_LEN: number;
5
5
  export declare const INTERNAL_HEADER_PREFIX = "x-opennext-";
6
+ export declare const NEXT_CONFIG_HEADER_PREFIX = "x-opennext-next-config-response-";
7
+ export declare const NEXT_CONFIG_HEADER_PREFIX_LEN: number;
6
8
  export declare const INTERNAL_HEADER_INITIAL_URL = "x-opennext-initial-url";
7
9
  export declare const INTERNAL_HEADER_LOCALE = "x-opennext-locale";
8
10
  export declare const INTERNAL_HEADER_RESOLVED_ROUTES = "x-opennext-resolved-routes";
@@ -11,6 +11,8 @@ import { constructNextUrl, normalizeLocationHeader } from "./routing/util";
11
11
  export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
12
12
  export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
13
13
  export const INTERNAL_HEADER_PREFIX = "x-opennext-";
14
+ export const NEXT_CONFIG_HEADER_PREFIX = `${INTERNAL_HEADER_PREFIX}next-config-response-`;
15
+ export const NEXT_CONFIG_HEADER_PREFIX_LEN = NEXT_CONFIG_HEADER_PREFIX.length;
14
16
  export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`;
15
17
  export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`;
16
18
  export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`;
@@ -40,24 +42,34 @@ const NEXT_INTERNAL_HEADERS = [
40
42
  "x-matched-path",
41
43
  "x-nextjs-data",
42
44
  "x-next-resume-state-length",
45
+ "next-resume",
43
46
  ];
44
47
  /**
45
- * Adds the middleware headers to an event or result.
48
+ * Adds the `next.config` and middleware response headers to an event or result.
46
49
  *
47
- * @param eventOrResult
48
- * @param middlewareHeaders
50
+ * On an event, each kind gets its own prefix so that the request handler can tell them apart (see
51
+ * `splitRoutingHeaders`). On a result, the headers are the response and are not prefixed.
52
+ *
53
+ * `nextConfigHeaders` take precedence over `middlewareHeaders` on the response.
54
+ *
55
+ * @param eventOrResult The event or result to add the headers to, its headers are mutated
56
+ * @param nextConfigHeaders The response headers from the `next.config` `headers()`
57
+ * @param middlewareHeaders The response headers set by the middleware
49
58
  */
50
- function applyMiddlewareHeaders(eventOrResult, middlewareHeaders) {
51
- // Use the `MIDDLEWARE_HEADER_PREFIX` prefix for events, they will be processed by the request handler later.
52
- // Results do not go through the request handler and should not be prefixed.
59
+ function applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders) {
53
60
  const isResult = isInternalResult(eventOrResult);
54
61
  const headers = eventOrResult.headers;
55
- const keyPrefix = isResult ? "" : MIDDLEWARE_HEADER_PREFIX;
56
- Object.entries(middlewareHeaders).forEach(([key, value]) => {
57
- if (value) {
58
- headers[keyPrefix + key] = Array.isArray(value) ? value.join(",") : value;
62
+ const addHeaders = (source, keyPrefix) => {
63
+ for (const [key, value] of Object.entries(source)) {
64
+ if (value) {
65
+ headers[keyPrefix + key] = Array.isArray(value)
66
+ ? value.join(",")
67
+ : value;
68
+ }
59
69
  }
60
- });
70
+ };
71
+ addHeaders(middlewareHeaders, isResult ? "" : MIDDLEWARE_HEADER_PREFIX);
72
+ addHeaders(nextConfigHeaders, isResult ? "" : NEXT_CONFIG_HEADER_PREFIX);
61
73
  }
62
74
  export default async function routingHandler(event, { assetResolver }) {
63
75
  try {
@@ -78,8 +90,7 @@ export default async function routingHandler(event, { assetResolver }) {
78
90
  delete event.headers[key];
79
91
  }
80
92
  }
81
- // Headers from the Next config and middleware (the later are applied further down).
82
- let headers = getNextConfigHeaders(event, ConfigHeaders);
93
+ let nextConfigHeaders = getNextConfigHeaders(event, ConfigHeaders);
83
94
  let eventOrResult = fixDataPage(event, BuildId);
84
95
  if (isInternalResult(eventOrResult)) {
85
96
  return eventOrResult;
@@ -101,17 +112,10 @@ export default async function routingHandler(event, { assetResolver }) {
101
112
  }
102
113
  const middlewareHeadersPrioritized = globalThis.openNextConfig.dangerous
103
114
  ?.middlewareHeadersOverrideNextConfigHeaders ?? false;
115
+ const middlewareHeaders = middlewareEventOrResult.responseHeaders ?? {};
104
116
  if (middlewareHeadersPrioritized) {
105
- headers = {
106
- ...headers,
107
- ...middlewareEventOrResult.responseHeaders,
108
- };
109
- }
110
- else {
111
- headers = {
112
- ...middlewareEventOrResult.responseHeaders,
113
- ...headers,
114
- };
117
+ // Both are keyed by lowercase name.
118
+ nextConfigHeaders = Object.fromEntries(Object.entries(nextConfigHeaders).filter(([key]) => !Object.hasOwn(middlewareHeaders, key)));
115
119
  }
116
120
  let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false;
117
121
  eventOrResult = middlewareEventOrResult;
@@ -127,7 +131,7 @@ export default async function routingHandler(event, { assetResolver }) {
127
131
  if (!isExternalRewrite) {
128
132
  const assetResult = await assetResolver?.maybeGetAssetResult?.(eventOrResult);
129
133
  if (assetResult) {
130
- applyMiddlewareHeaders(assetResult, headers);
134
+ applyResponseHeaders(assetResult, nextConfigHeaders, middlewareHeaders);
131
135
  return assetResult;
132
136
  }
133
137
  }
@@ -192,12 +196,12 @@ export default async function routingHandler(event, { assetResolver }) {
192
196
  debug("Cache interception enabled");
193
197
  eventOrResult = await cacheInterceptor(eventOrResult, resolvedRoutes);
194
198
  if (isInternalResult(eventOrResult)) {
195
- applyMiddlewareHeaders(eventOrResult, headers);
199
+ applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders);
196
200
  return eventOrResult;
197
201
  }
198
202
  }
199
- // We apply the headers from the middleware response last
200
- applyMiddlewareHeaders(eventOrResult, headers);
203
+ // We apply the response headers last
204
+ applyResponseHeaders(eventOrResult, nextConfigHeaders, middlewareHeaders);
201
205
  debug("resolvedRoutes", resolvedRoutes);
202
206
  return {
203
207
  internalEvent: eventOrResult,
@@ -1 +1,10 @@
1
+ /**
2
+ * Minifies the JS (and optionally JSON) files of a directory in place.
3
+ *
4
+ * Calls can run concurrently. Failed files are logged and do not throw.
5
+ *
6
+ * @param dir Directory to walk recursively.
7
+ * @param opts Minification options.
8
+ * @return A promise that resolves once all the files have been processed.
9
+ */
1
10
  export declare function minifyAll(dir: any, opts: any): Promise<void>;
@@ -4,11 +4,8 @@
4
4
  // @ts-nocheck
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
- import minify from "@node-minify/core";
8
- import terser from "@node-minify/terser";
9
- const failed_files = [];
10
- let total_files = 0;
11
- const options = {};
7
+ import { minify } from "@node-minify/core";
8
+ import { terser } from "@node-minify/terser";
12
9
  const promiseSeries = async (tasks, initial) => {
13
10
  if (!Array.isArray(tasks)) {
14
11
  return Promise.reject(new TypeError("promise.series only accepts an array of functions"));
@@ -17,29 +14,28 @@ const promiseSeries = async (tasks, initial) => {
17
14
  return current.then(next);
18
15
  }, Promise.resolve(initial));
19
16
  };
20
- const minifyJS = async (file) => {
21
- total_files++;
17
+ const minifyJS = async (file, ctx) => {
22
18
  try {
23
19
  await minify({
24
20
  compressor: terser,
25
21
  input: file,
26
22
  output: file,
23
+ // node-minify 10 throws when the output is empty (i.e. empty or comment-only files)
24
+ allowEmptyOutput: true,
27
25
  options: {
28
- module: options.module,
29
- mangle: options.mangle,
26
+ module: ctx.options.module,
27
+ mangle: ctx.options.mangle,
30
28
  compress: { reduce_vars: false },
31
29
  },
32
30
  });
33
31
  }
34
32
  catch (e) {
35
- failed_files.push(file);
33
+ ctx.failed_files.push(file);
36
34
  }
37
- //process.stdout.write(".");
38
35
  };
39
- const minifyJSON = async (file) => {
36
+ const minifyJSON = async (file, options) => {
40
37
  try {
41
38
  if (options.compress_json || options.packagejson) {
42
- total_files++;
43
39
  const is_package_json = file.indexOf("package.json") > -1;
44
40
  const data = await fs.readFile(file, "utf8");
45
41
  const json = JSON.parse(data);
@@ -63,9 +59,8 @@ const minifyJSON = async (file) => {
63
59
  }
64
60
  }
65
61
  catch (e) { }
66
- //process.stdout.write(".");
67
62
  };
68
- const walk = async (currentDirPath) => {
63
+ const walk = async (currentDirPath, ctx) => {
69
64
  const js_files = [];
70
65
  const json_files = [];
71
66
  const dirs = [];
@@ -77,26 +72,32 @@ const walk = async (currentDirPath) => {
77
72
  if (stat.isFile()) {
78
73
  if (filePath.substr(-5) === ".json")
79
74
  json_files.push(filePath);
80
- else if (filePath.substr(-3) === ".js" || options.all_js)
75
+ else if (filePath.substr(-3) === ".js" || ctx.options.all_js)
81
76
  js_files.push(filePath);
82
77
  }
83
78
  else if (stat.isDirectory() && !is_bin.test(filePath)) {
84
79
  dirs.push(filePath);
85
80
  }
86
81
  }
87
- const js_promise = Promise.all(js_files.map((f) => minifyJS(f)));
88
- const json_promise = Promise.all(json_files.map((f) => minifyJSON(f)));
82
+ const js_promise = Promise.all(js_files.map((f) => minifyJS(f, ctx)));
83
+ const json_promise = Promise.all(json_files.map((f) => minifyJSON(f, ctx.options)));
89
84
  await Promise.all([js_promise, json_promise]);
90
- await promiseSeries(dirs.map((dir) => () => walk(dir)));
85
+ await promiseSeries(dirs.map((dir) => () => walk(dir, ctx)));
91
86
  };
87
+ /**
88
+ * Minifies the JS (and optionally JSON) files of a directory in place.
89
+ *
90
+ * Calls can run concurrently. Failed files are logged and do not throw.
91
+ *
92
+ * @param dir Directory to walk recursively.
93
+ * @param opts Minification options.
94
+ * @return A promise that resolves once all the files have been processed.
95
+ */
92
96
  export async function minifyAll(dir, opts) {
93
- Object.assign(options, opts || {});
94
- //console.log("minify-all-js options:\n", JSON.stringify(options, null, 2));
95
- await walk(dir);
96
- //process.stdout.write(".\n");
97
- //console.log("Total found files: " + total_files);
98
- if (failed_files.length) {
97
+ const ctx = { options: { ...opts }, failed_files: [] };
98
+ await walk(dir, ctx);
99
+ if (ctx.failed_files.length) {
99
100
  console.log("\n\nFailed to minify files:");
100
- failed_files.forEach((f) => console.log(`\t${f}`));
101
+ ctx.failed_files.forEach((f) => console.log(`\t${f}`));
101
102
  }
102
103
  }
@@ -52,7 +52,19 @@ interface OpenNextRequestContext {
52
52
  pendingPromiseRunner: DetachedPromiseRunner;
53
53
  isISRRevalidation?: boolean;
54
54
  mergeHeadersPriority?: "middleware" | "handler";
55
+ /**
56
+ * Last modified time of the page (used in main functions, only available for ISR/SSG).
57
+ *
58
+ * `1` when the entry is stale by tag, so that the revalidation queue deduplicates every stale
59
+ * request for the same path. See `entryLastModified` for the time of the entry.
60
+ */
55
61
  lastModified?: number;
62
+ /**
63
+ * Last modified time of the cache entry, also when it is stale by tag (`lastModified` is then `1`).
64
+ *
65
+ * Sent to the revalidation queue in `MessageBody.lastModified`.
66
+ */
67
+ entryLastModified?: number;
56
68
  waitUntil?: WaitUntil;
57
69
  /** We use this to deduplicate write of the tags*/
58
70
  writtenTags: Set<string>;
@@ -1,3 +1,18 @@
1
1
  export declare function normalizePath(path: string): string;
2
2
  export declare function normalizeRepeatedSlashes(url: URL): string;
3
+ /**
4
+ * Decodes a pathname the way Next.js does to key prerendered routes.
5
+ *
6
+ * Each segment is decoded while path delimiters (e.g. `%2F`) stay encoded,
7
+ * so that an encoded delimiter does not split a segment.
8
+ * Decoding must be atomic, as in Next.js: partially decoding a malformed path
9
+ * could select a different route than the one Next.js would select.
10
+ *
11
+ * See: https://github.com/vercel/next.js/blob/3403955/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
12
+ *
13
+ * @param pathname The percent-encoded pathname
14
+ * @returns The decoded pathname
15
+ * @throws {URIError} When the pathname contains a malformed escape sequence
16
+ */
17
+ export declare function decodePathParams(pathname: string): string;
3
18
  export declare function getMonorepoRelativePath(relativePath?: string): string;
@@ -9,6 +9,38 @@ export function normalizeRepeatedSlashes(url) {
9
9
  .replace(/\\/g, "/")
10
10
  .replace(/\/\/+/g, "/")}${url.search}`;
11
11
  }
12
+ /**
13
+ * Percent-encodes the path delimiters of a decoded path segment.
14
+ *
15
+ * See: https://github.com/vercel/next.js/blob/3403955/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10
16
+ *
17
+ * @param segment The decoded path segment
18
+ * @param escapeEncoded Whether to also escape already encoded delimiters (e.g. `%2F`)
19
+ * @returns The segment with its delimiters encoded
20
+ */
21
+ function escapePathDelimiters(segment, escapeEncoded) {
22
+ return segment.replace(new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), (char) => encodeURIComponent(char));
23
+ }
24
+ /**
25
+ * Decodes a pathname the way Next.js does to key prerendered routes.
26
+ *
27
+ * Each segment is decoded while path delimiters (e.g. `%2F`) stay encoded,
28
+ * so that an encoded delimiter does not split a segment.
29
+ * Decoding must be atomic, as in Next.js: partially decoding a malformed path
30
+ * could select a different route than the one Next.js would select.
31
+ *
32
+ * See: https://github.com/vercel/next.js/blob/3403955/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
33
+ *
34
+ * @param pathname The percent-encoded pathname
35
+ * @returns The decoded pathname
36
+ * @throws {URIError} When the pathname contains a malformed escape sequence
37
+ */
38
+ export function decodePathParams(pathname) {
39
+ return pathname
40
+ .split("/")
41
+ .map((segment) => escapePathDelimiters(decodeURIComponent(segment), true))
42
+ .join("/");
43
+ }
12
44
  export function getMonorepoRelativePath(relativePath = "../..") {
13
45
  return path.join(globalThis.monorepoPackagePath
14
46
  .split("/")
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "4.1.15",
6
+ "version": "4.1.16",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -39,8 +39,8 @@
39
39
  "@aws-sdk/client-lambda": "3.984.0",
40
40
  "@aws-sdk/client-s3": "3.984.0",
41
41
  "@aws-sdk/client-sqs": "3.984.0",
42
- "@node-minify/core": "^9.0.2",
43
- "@node-minify/terser": "^9.0.1",
42
+ "@node-minify/core": "^10.5.0",
43
+ "@node-minify/terser": "^10.5.0",
44
44
  "@tsconfig/node18": "^18.2.7",
45
45
  "aws4fetch": "^1.0.20",
46
46
  "chalk": "^5.6.2",