@opennextjs/aws 3.6.0 → 3.6.2

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.
@@ -83,5 +83,9 @@ export function compileOpenNextConfigEdge(sourcePath, outputDir, externals) {
83
83
  conditions: ["worker", "browser"],
84
84
  platform: "browser",
85
85
  external: externals,
86
+ define: {
87
+ // with the default esbuild config, the NODE_ENV will be set to "development", we don't want that
88
+ "process.env.NODE_ENV": '"production"',
89
+ },
86
90
  });
87
91
  }
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loadFunctionsConfigManifest, loadMiddlewareManifest, } from "../adapters/config/util.js";
4
4
  import logger from "../logger.js";
5
- import { buildEdgeBundle } from "./edge/createEdgeBundle.js";
5
+ import { buildEdgeBundle, copyMiddlewareResources, } from "./edge/createEdgeBundle.js";
6
6
  import * as buildHelper from "./helper.js";
7
7
  import { installDependencies } from "./installDeps.js";
8
8
  import { buildBundledNodeMiddleware, buildExternalNodeMiddleware, } from "./middleware/buildNodeMiddleware.js";
@@ -31,6 +31,7 @@ export async function createMiddleware(options, { forceOnlyBuildOnce = false } =
31
31
  }
32
32
  if (config.middleware?.external) {
33
33
  const outputPath = path.join(outputDir, "middleware");
34
+ copyMiddlewareResources(options, edgeMiddlewareInfo, outputPath);
34
35
  fs.mkdirSync(outputPath, { recursive: true });
35
36
  // Copy open-next.config.mjs
36
37
  buildHelper.copyOpenNextConfig(options.buildDir, outputPath, await buildHelper.isEdgeRuntime(config.middleware.override));
@@ -51,7 +51,11 @@ export async function openNextHandler(internalEvent, options) {
51
51
  continue;
52
52
  }
53
53
  const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN);
54
- overwrittenResponseHeaders[key] = value;
54
+ // We skip this header here since it is used by Next internally and we don't want it on the response headers.
55
+ // This header needs to be present in the request headers for processRequest, so cookies().get() from Next will work on initial render.
56
+ if (key !== "x-middleware-set-cookie") {
57
+ overwrittenResponseHeaders[key] = value;
58
+ }
55
59
  headers[key] = value;
56
60
  delete headers[rawKey];
57
61
  }
@@ -151,44 +155,68 @@ async function processRequest(req, res, routingResult) {
151
155
  // Next.js doesn't parse body if the property exists
152
156
  // https://github.com/dougmoscrop/serverless-http/issues/227
153
157
  delete req.body;
158
+ // Here we try to apply as much request metadata as possible
159
+ // We apply every metadata from `resolve-routes` https://github.com/vercel/next.js/blob/916f105b97211de50f8580f0b39c9e7c60de4886/packages/next/src/server/lib/router-utils/resolve-routes.ts
160
+ // and `router-server` https://github.com/vercel/next.js/blob/916f105b97211de50f8580f0b39c9e7c60de4886/packages/next/src/server/lib/router-server.ts
161
+ const initialURL = new URL(routingResult.initialURL);
162
+ let invokeStatus;
163
+ if (routingResult.internalEvent.rawPath === "/500") {
164
+ invokeStatus = 500;
165
+ }
166
+ else if (routingResult.internalEvent.rawPath === "/404") {
167
+ invokeStatus = 404;
168
+ }
169
+ const requestMetadata = {
170
+ isNextDataReq: routingResult.internalEvent.query.__nextDataReq === "1",
171
+ initURL: routingResult.initialURL,
172
+ initQuery: convertToQuery(initialURL.search),
173
+ initProtocol: initialURL.protocol,
174
+ defaultLocale: NextConfig.i18n?.defaultLocale,
175
+ locale: routingResult.locale,
176
+ middlewareInvoke: false,
177
+ // By setting invokePath and invokeQuery we can bypass some of the routing logic in Next.js
178
+ invokePath: routingResult.internalEvent.rawPath,
179
+ invokeQuery: routingResult.internalEvent.query,
180
+ // invokeStatus is only used for error pages
181
+ invokeStatus,
182
+ };
154
183
  try {
155
184
  //#override applyNextjsPrebundledReact
156
185
  setNextjsPrebundledReact(routingResult.internalEvent.rawPath);
157
186
  //#endOverride
158
- // Here we try to apply as much request metadata as possible
159
- // We apply every metadata from `resolve-routes` https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/resolve-routes.ts
160
- // and `router-server` https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-server.ts
161
- const initialURL = new URL(routingResult.initialURL);
162
- let invokeStatus;
163
- if (routingResult.internalEvent.rawPath === "/500") {
164
- invokeStatus = 500;
165
- }
166
- else if (routingResult.internalEvent.rawPath === "/404") {
167
- invokeStatus = 404;
168
- }
169
- const requestMetadata = {
170
- isNextDataReq: routingResult.internalEvent.query.__nextDataReq === "1",
171
- initURL: routingResult.initialURL,
172
- initQuery: convertToQuery(initialURL.search),
173
- initProtocol: initialURL.protocol,
174
- defaultLocale: NextConfig.i18n?.defaultLocale,
175
- locale: routingResult.locale,
176
- middlewareInvoke: false,
177
- // By setting invokePath and invokeQuery we can bypass some of the routing logic in Next.js
178
- invokePath: routingResult.internalEvent.rawPath,
179
- invokeQuery: routingResult.internalEvent.query,
180
- // invokeStatus is only used for error pages
181
- invokeStatus,
182
- };
183
187
  // Next Server
184
188
  await requestHandler(requestMetadata)(req, res);
185
189
  }
186
190
  catch (e) {
187
191
  // This might fail when using bundled next, importing won't do the trick either
188
192
  if (e.constructor.name === "NoFallbackError") {
189
- // Do we need to handle _not-found
190
- // Ideally this should never get triggered and be intercepted by the routing handler
191
- await tryRenderError("404", res, routingResult.internalEvent);
193
+ await handleNoFallbackError(req, res, routingResult, requestMetadata);
194
+ }
195
+ else {
196
+ error("NextJS request failed.", e);
197
+ await tryRenderError("500", res, routingResult.internalEvent);
198
+ }
199
+ }
200
+ }
201
+ async function handleNoFallbackError(req, res, routingResult, metadata, index = 1) {
202
+ if (index >= 5) {
203
+ await tryRenderError("500", res, routingResult.internalEvent);
204
+ return;
205
+ }
206
+ if (index >= routingResult.resolvedRoutes.length) {
207
+ await tryRenderError("404", res, routingResult.internalEvent);
208
+ return;
209
+ }
210
+ try {
211
+ await requestHandler({
212
+ ...routingResult,
213
+ invokeOutput: routingResult.resolvedRoutes[index].route,
214
+ ...metadata,
215
+ })(req, res);
216
+ }
217
+ catch (e) {
218
+ if (e.constructor.name === "NoFallbackError") {
219
+ await handleNoFallbackError(req, res, routingResult, metadata, index + 1);
192
220
  }
193
221
  else {
194
222
  error("NextJS request failed.", e);
@@ -102,6 +102,32 @@ async function generateResult(event, localizedPath, cachedValue, lastModified) {
102
102
  },
103
103
  };
104
104
  }
105
+ /**
106
+ *
107
+ * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10
108
+ */
109
+ function escapePathDelimiters(segment, escapeEncoded) {
110
+ return segment.replace(new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), (char) => encodeURIComponent(char));
111
+ }
112
+ /**
113
+ *
114
+ * SSG cache key needs to be decoded, but some characters needs to be properly escaped
115
+ * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
116
+ */
117
+ function decodePathParams(pathname) {
118
+ return pathname
119
+ .split("/")
120
+ .map((segment) => {
121
+ try {
122
+ return escapePathDelimiters(decodeURIComponent(segment), true);
123
+ }
124
+ catch (e) {
125
+ // If decodeURIComponent fails, we return the original segment
126
+ return segment;
127
+ }
128
+ })
129
+ .join("/");
130
+ }
105
131
  export async function cacheInterceptor(event) {
106
132
  if (Boolean(event.headers["next-action"]) ||
107
133
  Boolean(event.headers["x-prerender-revalidate"]))
@@ -114,17 +140,15 @@ export async function cacheInterceptor(event) {
114
140
  }
115
141
  // We also need to remove trailing slash
116
142
  localizedPath = localizedPath.replace(/\/$/, "");
117
- // If empty path, it means we want index
118
- if (localizedPath === "") {
119
- localizedPath = "index";
120
- }
143
+ // Then we decode the path params
144
+ localizedPath = decodePathParams(localizedPath);
121
145
  debug("Checking cache for", localizedPath, PrerenderManifest);
122
- const isISR = Object.keys(PrerenderManifest.routes).includes(localizedPath) ||
146
+ const isISR = Object.keys(PrerenderManifest.routes).includes(localizedPath ?? "/") ||
123
147
  Object.values(PrerenderManifest.dynamicRoutes).some((dr) => new RegExp(dr.routeRegex).test(localizedPath));
124
148
  debug("isISR", isISR);
125
149
  if (isISR) {
126
150
  try {
127
- const cachedData = await globalThis.incrementalCache.get(localizedPath);
151
+ const cachedData = await globalThis.incrementalCache.get(localizedPath ?? "/index");
128
152
  debug("cached data in interceptor", cachedData);
129
153
  if (!cachedData?.value) {
130
154
  return event;
@@ -1,8 +1,10 @@
1
1
  import { NextConfig } from "../../adapters/config/index";
2
2
  import { compile, match } from "path-to-regexp";
3
+ import { normalizeRepeatedSlashes } from "../../utils/normalize-path";
3
4
  import { emptyReadableStream, toReadableStream } from "../../utils/stream";
4
5
  import { debug } from "../../adapters/logger";
5
6
  import { handleLocaleRedirect, localizePath } from "./i18n";
7
+ import { dynamicRouteMatcher, staticRouteMatcher } from "./routeMatcher";
6
8
  import { constructNextUrl, convertFromQueryString, convertToQueryString, escapeRegex, getUrlParts, isExternal, unescapeRegex, } from "./util";
7
9
  const routeHasMatcher = (headers, cookies, query) => (redirect) => {
8
10
  switch (redirect.type) {
@@ -168,6 +170,25 @@ export function handleRewrites(event, rewrites) {
168
170
  isExternalRewrite,
169
171
  };
170
172
  }
173
+ // Normalizes repeated slashes in the path e.g. hello//world -> hello/world
174
+ // or backslashes to forward slashes. This prevents requests such as //domain
175
+ // from invoking the middleware with `request.url === "domain"`.
176
+ // See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/server/base-server.ts#L1016-L1020
177
+ function handleRepeatedSlashRedirect(event) {
178
+ // Redirect `https://example.com//foo` to `https://example.com/foo`.
179
+ if (event.rawPath.match(/(\\|\/\/)/)) {
180
+ return {
181
+ type: event.type,
182
+ statusCode: 308,
183
+ headers: {
184
+ Location: normalizeRepeatedSlashes(new URL(event.url)),
185
+ },
186
+ body: emptyReadableStream(),
187
+ isBase64Encoded: false,
188
+ };
189
+ }
190
+ return false;
191
+ }
171
192
  function handleTrailingSlashRedirect(event) {
172
193
  // When rawPath is `//domain`, `url.host` would be `domain`.
173
194
  // https://github.com/opennextjs/opennextjs-aws/issues/355
@@ -214,6 +235,9 @@ function handleTrailingSlashRedirect(event) {
214
235
  return false;
215
236
  }
216
237
  export function handleRedirects(event, redirects) {
238
+ const repeatedSlashRedirect = handleRepeatedSlashRedirect(event);
239
+ if (repeatedSlashRedirect)
240
+ return repeatedSlashRedirect;
217
241
  const trailingSlashRedirect = handleTrailingSlashRedirect(event);
218
242
  if (trailingSlashRedirect)
219
243
  return trailingSlashRedirect;
@@ -265,9 +289,8 @@ export function fixDataPage(internalEvent, buildId) {
265
289
  export function handleFallbackFalse(internalEvent, prerenderManifest) {
266
290
  const { rawPath } = internalEvent;
267
291
  const { dynamicRoutes, routes } = prerenderManifest;
268
- const routeFallback = Object.entries(dynamicRoutes)
269
- .filter(([, { fallback }]) => fallback === false)
270
- .some(([, { routeRegex }]) => {
292
+ const prerenderedFallbackRoutes = Object.entries(dynamicRoutes).filter(([, { fallback }]) => fallback === false);
293
+ const routeFallback = prerenderedFallbackRoutes.some(([, { routeRegex }]) => {
271
294
  const routeRegexExp = new RegExp(routeRegex);
272
295
  return routeRegexExp.test(rawPath);
273
296
  });
@@ -275,11 +298,21 @@ export function handleFallbackFalse(internalEvent, prerenderManifest) {
275
298
  const routesAlreadyHaveLocale = locales?.includes(rawPath.split("/")[1]) ||
276
299
  // If we don't use locales, we don't need to add the default locale
277
300
  locales === undefined;
278
- const localizedPath = routesAlreadyHaveLocale
301
+ let localizedPath = routesAlreadyHaveLocale
279
302
  ? rawPath
280
303
  : `/${NextConfig.i18n?.defaultLocale}${rawPath}`;
304
+ // We need to remove the trailing slash if it exists
305
+ if (NextConfig.trailingSlash && localizedPath.endsWith("/")) {
306
+ localizedPath = localizedPath.slice(0, -1);
307
+ }
308
+ const matchedStaticRoute = staticRouteMatcher(localizedPath);
309
+ const prerenderedFallbackRoutesName = prerenderedFallbackRoutes.map(([name]) => name);
310
+ const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter(({ route }) => !prerenderedFallbackRoutesName.includes(route));
281
311
  const isPregenerated = Object.keys(routes).includes(localizedPath);
282
- if (routeFallback && !isPregenerated) {
312
+ if (routeFallback &&
313
+ !isPregenerated &&
314
+ matchedStaticRoute.length === 0 &&
315
+ matchedDynamicRoute.length === 0) {
283
316
  return {
284
317
  event: {
285
318
  ...internalEvent,
@@ -75,7 +75,6 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
75
75
  // These are internal headers used by Next.js, we don't want to expose them to the client
76
76
  const filteredHeaders = [
77
77
  "x-middleware-override-headers",
78
- "x-middleware-set-cookie",
79
78
  "x-middleware-next",
80
79
  "x-middleware-rewrite",
81
80
  // We need to drop `content-encoding` because it will be decoded
@@ -1,10 +1,4 @@
1
- import type { RouteType } from "../../types/open-next";
1
+ import type { ResolvedRoute } from "../../types/open-next";
2
2
  export declare const apiPrefix: string;
3
- export declare const staticRouteMatcher: (path: string) => {
4
- route: string;
5
- type: RouteType;
6
- }[];
7
- export declare const dynamicRouteMatcher: (path: string) => {
8
- route: string;
9
- type: RouteType;
10
- }[];
3
+ export declare const staticRouteMatcher: (path: string) => ResolvedRoute[];
4
+ export declare const dynamicRouteMatcher: (path: string) => ResolvedRoute[];
@@ -55,7 +55,23 @@ const converter = {
55
55
  }
56
56
  const headers = new Headers();
57
57
  for (const [key, value] of Object.entries(result.headers)) {
58
- headers.set(key, Array.isArray(value) ? value.join(",") : value);
58
+ if (key === "set-cookie" && typeof value === "string") {
59
+ // If the value is a string, we need to parse it into an array
60
+ // This is the case for middleware direct result
61
+ const cookies = parseCookies(value);
62
+ for (const cookie of cookies) {
63
+ headers.append(key, cookie);
64
+ }
65
+ continue;
66
+ }
67
+ if (Array.isArray(value)) {
68
+ for (const v of value) {
69
+ headers.append(key, v);
70
+ }
71
+ }
72
+ else {
73
+ headers.set(key, value);
74
+ }
59
75
  }
60
76
  return new Response(result.body, {
61
77
  status: result.statusCode,
@@ -17,7 +17,8 @@ const converter = {
17
17
  Array.isArray(value) ? value.join(",") : value,
18
18
  ])
19
19
  .filter(([key]) => key));
20
- const url = new URL(req.url, `http://${extractHostFromHeaders(headers)}`);
20
+ // https://nodejs.org/api/http.html#messageurl
21
+ const url = new URL(`${req.protocol ? req.protocol : "http"}://${extractHostFromHeaders(headers)}${req.url}`);
21
22
  const query = getQueryFromSearchParams(url.searchParams);
22
23
  return {
23
24
  type: "core",
@@ -1,9 +1,11 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getMonorepoRelativePath } from "../../utils/normalize-path";
2
4
  export default {
3
5
  name: "fs-dev",
4
6
  load: async (url) => {
5
- const basePath = "../../assets";
6
- const body = fs.createReadStream(`${basePath}/${url}`);
7
+ const imagePath = path.join(getMonorepoRelativePath(), "assets", url);
8
+ const body = fs.createReadStream(imagePath);
7
9
  const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg";
8
10
  return {
9
11
  body,
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { getMonorepoRelativePath } from "../../utils/normalize-path";
3
4
  const buildId = process.env.NEXT_BUILD_ID;
4
- const basePath = path.resolve(process.cwd(), `../../cache/${buildId}`);
5
+ const basePath = path.join(getMonorepoRelativePath(), `cache/${buildId}`);
5
6
  const getCacheKey = (key) => {
6
7
  return path.join(basePath, `${key}.cache`);
7
8
  };
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
- // TODO: fix this for monorepo
3
- const tagFile = "../../dynamodb-provider/dynamodb-cache.json";
2
+ import path from "node:path";
3
+ import { getMonorepoRelativePath } from "../../utils/normalize-path";
4
+ const tagFile = path.join(getMonorepoRelativePath(), "dynamodb-provider/dynamodb-cache.json");
4
5
  const tagContent = fs.readFileSync(tagFile, "utf-8");
5
6
  let tags = JSON.parse(tagContent);
6
7
  const tagCache = {
@@ -1,9 +1,11 @@
1
+ import path from "node:path";
1
2
  import express from "express";
3
+ import { getMonorepoRelativePath } from "../../utils/normalize-path";
2
4
  const wrapper = async (handler, converter) => {
3
5
  const app = express();
4
6
  // To serve static assets
5
- app.use(express.static("../../assets"));
6
- const imageHandlerPath = "../../image-optimization-function/index.mjs";
7
+ app.use(express.static(path.join(getMonorepoRelativePath(), "assets")));
8
+ const imageHandlerPath = path.join(getMonorepoRelativePath(), "image-optimization-function/index.mjs");
7
9
  const imageHandler = await import(imageHandlerPath).then((m) => m.handler);
8
10
  app.all("/_next/image", async (req, res) => {
9
11
  const internalEvent = await converter.convertFrom(req);
@@ -27,7 +27,7 @@ export function openNextEdgePlugins({ nextDir, middlewareInfo, isInCloudflare, }
27
27
  name: "opennext-edge",
28
28
  setup(build) {
29
29
  logger.debug(chalk.blue("OpenNext Edge plugin"));
30
- build.onResolve({ filter: /\.(mjs|wasm)$/g }, () => {
30
+ build.onResolve({ filter: /\.(mjs|wasm)$/ }, () => {
31
31
  return {
32
32
  external: true,
33
33
  };
@@ -185,5 +185,10 @@ declare global {
185
185
  * Only available in main functions.
186
186
  */
187
187
  var __next_route_preloader: (stage: "waitUntil" | "start" | "warmerEvent" | "onDemand") => Promise<void>;
188
+ /**
189
+ * This is the relative package path of the monorepo. It will be an empty string "" in normal repos.
190
+ * ex. `packages/web`
191
+ */
192
+ var monorepoPackagePath: string;
188
193
  }
189
194
  export {};
@@ -1 +1,3 @@
1
1
  export declare function normalizePath(path: string): string;
2
+ export declare function normalizeRepeatedSlashes(url: URL): string;
3
+ export declare function getMonorepoRelativePath(relativePath?: string): string;
@@ -1,3 +1,18 @@
1
+ import path from "node:path";
1
2
  export function normalizePath(path) {
2
3
  return path.replace(/\\/g, "/");
3
4
  }
5
+ // See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/shared/lib/utils.ts#L348
6
+ export function normalizeRepeatedSlashes(url) {
7
+ const urlNoQuery = url.host + url.pathname;
8
+ return `${url.protocol}//${urlNoQuery
9
+ .replace(/\\/g, "/")
10
+ .replace(/\/\/+/g, "/")}${url.search}`;
11
+ }
12
+ export function getMonorepoRelativePath(relativePath = "../..") {
13
+ return path.join(globalThis.monorepoPackagePath
14
+ .split("/")
15
+ .filter(Boolean)
16
+ .map(() => "..")
17
+ .join("/"), relativePath);
18
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.6.0",
6
+ "version": "3.6.2",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },
@@ -14,8 +14,8 @@
14
14
  "main": "./dist/index.js",
15
15
  "exports": {
16
16
  "./*": {
17
- "default": "./dist/*",
18
- "types": "./dist/*.d.ts"
17
+ "types": "./dist/*.d.ts",
18
+ "default": "./dist/*"
19
19
  }
20
20
  },
21
21
  "typesVersions": {
@@ -44,7 +44,7 @@
44
44
  "@tsconfig/node18": "^1.0.1",
45
45
  "aws4fetch": "^1.0.18",
46
46
  "chalk": "^5.3.0",
47
- "esbuild": "0.19.2",
47
+ "esbuild": "0.25.4",
48
48
  "express": "5.0.1",
49
49
  "path-to-regexp": "^6.3.0",
50
50
  "urlpattern-polyfill": "^10.0.0",