@opennextjs/aws 3.6.1 → 3.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,6 +96,8 @@ To run `OpenNext` locally:
96
96
  path/to/opennextjs-aws/packages/open-next/dist/index.js build
97
97
  ```
98
98
 
99
+ There is also a way to run OpenNext locally. You can read a guide about it [here](https://opennext.js.org/aws/contribute/local_run). Its mostly used for development/debugging purposes.
100
+
99
101
  ### Coldstart
100
102
 
101
103
  OpenNext provide you with a warmer function that can be used to reduce cold start.
@@ -23,6 +23,7 @@ export declare const RoutesManifest: {
23
23
  };
24
24
  export declare const ConfigHeaders: import("../../types/next-types").Header[] | undefined;
25
25
  export declare const PrerenderManifest: import("../../types/next-types").PrerenderManifest;
26
+ export declare const PagesManifest: Record<string, string>;
26
27
  export declare const AppPathsManifestKeys: string[];
27
28
  export declare const MiddlewareManifest: import("../../types/next-types").MiddlewareManifest;
28
29
  export declare const AppPathsManifest: Record<string, string>;
@@ -1,10 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { debug } from "../logger";
3
- import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPrerenderManifest, loadRoutesManifest, } from "./util.js";
3
+ import { loadAppPathRoutesManifest, loadAppPathsManifest, loadAppPathsManifestKeys, loadBuildId, loadConfig, loadConfigHeaders, loadFunctionsConfigManifest, loadHtmlPages, loadMiddlewareManifest, loadPagesManifest, loadPrerenderManifest, loadRoutesManifest, } from "./util.js";
4
4
  export const NEXT_DIR = path.join(__dirname, ".next");
5
5
  export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next");
6
6
  debug({ NEXT_DIR, OPEN_NEXT_DIR });
7
- //TODO: inject these values at build time
8
7
  export const NextConfig = /* @__PURE__ */ loadConfig(NEXT_DIR);
9
8
  export const BuildId = /* @__PURE__ */ loadBuildId(NEXT_DIR);
10
9
  export const HtmlPages = /* @__PURE__ */ loadHtmlPages(NEXT_DIR);
@@ -13,6 +12,7 @@ export const RoutesManifest = /* @__PURE__ */ loadRoutesManifest(NEXT_DIR);
13
12
  export const ConfigHeaders = /* @__PURE__ */ loadConfigHeaders(NEXT_DIR);
14
13
  export const PrerenderManifest =
15
14
  /* @__PURE__ */ loadPrerenderManifest(NEXT_DIR);
15
+ export const PagesManifest = /* @__PURE__ */ loadPagesManifest(NEXT_DIR);
16
16
  export const AppPathsManifestKeys =
17
17
  /* @__PURE__ */ loadAppPathsManifestKeys(NEXT_DIR);
18
18
  export const MiddlewareManifest =
@@ -2,7 +2,7 @@ import type { FunctionsConfigManifest, MiddlewareManifest, NextConfig, Prerender
2
2
  import type { PublicFiles } from "../../build";
3
3
  export declare function loadConfig(nextDir: string): NextConfig;
4
4
  export declare function loadBuildId(nextDir: string): string;
5
- export declare function loadPagesManifest(nextDir: string): any;
5
+ export declare function loadPagesManifest(nextDir: string): Record<string, string>;
6
6
  export declare function loadHtmlPages(nextDir: string): string[];
7
7
  export declare function loadPublicAssets(openNextDir: string): PublicFiles;
8
8
  export declare function loadRoutesManifest(nextDir: string): {
@@ -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
  }
@@ -10,7 +10,7 @@ export declare function getManifests(nextDir: string): {
10
10
  buildId: string;
11
11
  config: import("../types/next-types.js").NextConfig;
12
12
  prerenderManifest: import("../types/next-types.js").PrerenderManifest;
13
- pagesManifest: any;
13
+ pagesManifest: Record<string, string>;
14
14
  appPathsManifest: Record<string, string>;
15
15
  middlewareManifest: import("../types/next-types.js").MiddlewareManifest;
16
16
  functionsConfigManifest: import("../types/next-types.js").FunctionsConfigManifest;
@@ -22,7 +22,7 @@ export declare function copyTracedFiles({ buildOutputPath, packagePath, outputDi
22
22
  buildId: string;
23
23
  config: import("../types/next-types.js").NextConfig;
24
24
  prerenderManifest: import("../types/next-types.js").PrerenderManifest;
25
- pagesManifest: any;
25
+ pagesManifest: Record<string, string>;
26
26
  appPathsManifest: Record<string, string>;
27
27
  middlewareManifest: import("../types/next-types.js").MiddlewareManifest;
28
28
  functionsConfigManifest: import("../types/next-types.js").FunctionsConfigManifest;
@@ -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));
@@ -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,6 +140,8 @@ export async function cacheInterceptor(event) {
114
140
  }
115
141
  // We also need to remove trailing slash
116
142
  localizedPath = localizedPath.replace(/\/$/, "");
143
+ // Then we decode the path params
144
+ localizedPath = decodePathParams(localizedPath);
117
145
  debug("Checking cache for", localizedPath, PrerenderManifest);
118
146
  const isISR = Object.keys(PrerenderManifest.routes).includes(localizedPath ?? "/") ||
119
147
  Object.values(PrerenderManifest.dynamicRoutes).some((dr) => new RegExp(dr.routeRegex).test(localizedPath));
@@ -1,5 +1,6 @@
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";
@@ -169,6 +170,25 @@ export function handleRewrites(event, rewrites) {
169
170
  isExternalRewrite,
170
171
  };
171
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
+ }
172
192
  function handleTrailingSlashRedirect(event) {
173
193
  // When rawPath is `//domain`, `url.host` would be `domain`.
174
194
  // https://github.com/opennextjs/opennextjs-aws/issues/355
@@ -215,6 +235,9 @@ function handleTrailingSlashRedirect(event) {
215
235
  return false;
216
236
  }
217
237
  export function handleRedirects(event, redirects) {
238
+ const repeatedSlashRedirect = handleRepeatedSlashRedirect(event);
239
+ if (repeatedSlashRedirect)
240
+ return repeatedSlashRedirect;
218
241
  const trailingSlashRedirect = handleTrailingSlashRedirect(event);
219
242
  if (trailingSlashRedirect)
220
243
  return trailingSlashRedirect;
@@ -279,7 +302,11 @@ export function handleFallbackFalse(internalEvent, prerenderManifest) {
279
302
  ? rawPath
280
303
  : `/${NextConfig.i18n?.defaultLocale}${rawPath}`;
281
304
  // We need to remove the trailing slash if it exists
282
- if (NextConfig.trailingSlash && localizedPath.endsWith("/")) {
305
+ if (
306
+ // Not if localizedPath is "/" tho, because that would not make it find `isPregenerated` below since it would be try to match an empty string.
307
+ localizedPath !== "/" &&
308
+ NextConfig.trailingSlash &&
309
+ localizedPath.endsWith("/")) {
283
310
  localizedPath = localizedPath.slice(0, -1);
284
311
  }
285
312
  const matchedStaticRoute = staticRouteMatcher(localizedPath);
@@ -1,4 +1,3 @@
1
1
  import type { ResolvedRoute } from "../../types/open-next";
2
- export declare const apiPrefix: string;
3
2
  export declare const staticRouteMatcher: (path: string) => ResolvedRoute[];
4
3
  export declare const dynamicRouteMatcher: (path: string) => ResolvedRoute[];
@@ -1,12 +1,10 @@
1
- import { AppPathRoutesManifest, RoutesManifest } from "../../adapters/config/index";
1
+ import { AppPathRoutesManifest, PagesManifest, RoutesManifest, } from "../../adapters/config/index";
2
2
  // Add the locale prefix to the regex so we correctly match the rawPath
3
3
  const optionalLocalePrefixRegex = `^/(?:${RoutesManifest.locales.map((locale) => `${locale}/?`).join("|")})?`;
4
4
  // Add the basepath prefix to the regex so we correctly match the rawPath
5
5
  const optionalBasepathPrefixRegex = RoutesManifest.basePath
6
6
  ? `^${RoutesManifest.basePath}/?`
7
7
  : "^/";
8
- // Add the basePath prefix to the api routes
9
- export const apiPrefix = `${RoutesManifest.basePath ?? ""}/api`;
10
8
  const optionalPrefix = optionalLocalePrefixRegex.replace("^/", optionalBasepathPrefixRegex);
11
9
  function routeMatcher(routeDefinitions) {
12
10
  const regexp = routeDefinitions.map((route) => ({
@@ -41,5 +39,32 @@ function routeMatcher(routeDefinitions) {
41
39
  });
42
40
  };
43
41
  }
44
- export const staticRouteMatcher = routeMatcher(RoutesManifest.routes.static);
42
+ export const staticRouteMatcher = routeMatcher([
43
+ ...RoutesManifest.routes.static,
44
+ ...getStaticAPIRoutes(),
45
+ ]);
45
46
  export const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic);
47
+ /**
48
+ * Returns static API routes for both app and pages router cause Next will filter them out in staticRoutes in `routes-manifest.json`.
49
+ * We also need to filter out page files that are under `app/api/*` as those would not be present in the routes manifest either.
50
+ * This line from Next.js skips it:
51
+ * https://github.com/vercel/next.js/blob/ded56f952154a40dcfe53bdb38c73174e9eca9e5/packages/next/src/build/index.ts#L1299
52
+ *
53
+ * Without it handleFallbackFalse will 404 on static API routes if there is a catch-all route on root level.
54
+ */
55
+ function getStaticAPIRoutes() {
56
+ const createRouteDefinition = (route) => ({
57
+ page: route,
58
+ regex: `^${route}(?:/)?$`,
59
+ });
60
+ const dynamicRoutePages = new Set(RoutesManifest.routes.dynamic.map(({ page }) => page));
61
+ const pagesStaticAPIRoutes = Object.keys(PagesManifest)
62
+ .filter((route) => route.startsWith("/api/") && !dynamicRoutePages.has(route))
63
+ .map(createRouteDefinition);
64
+ // We filter out both static API and page routes from the app paths manifest
65
+ const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest)
66
+ .filter((route) => route.startsWith("/api/") ||
67
+ (route === "/api" && !dynamicRoutePages.has(route)))
68
+ .map(createRouteDefinition);
69
+ return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes];
70
+ }
@@ -4,7 +4,7 @@ import { cacheInterceptor } from "./routing/cacheInterceptor";
4
4
  import { detectLocale } from "./routing/i18n";
5
5
  import { fixDataPage, getNextConfigHeaders, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
6
6
  import { handleMiddleware } from "./routing/middleware";
7
- import { apiPrefix, dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
7
+ import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
8
8
  import { constructNextUrl } from "./routing/util";
9
9
  export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
10
10
  export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
@@ -55,6 +55,9 @@ export default async function routingHandler(event) {
55
55
  }
56
56
  const redirect = handleRedirects(internalEvent, RoutesManifest.redirects);
57
57
  if (redirect) {
58
+ // We need to encode the value in the Location header to make sure it is valid according to RFC
59
+ // https://stackoverflow.com/a/7654605/16587222
60
+ redirect.headers.Location = new URL(redirect.headers.Location).href;
58
61
  debug("redirect", redirect);
59
62
  return redirect;
60
63
  }
@@ -85,9 +88,14 @@ export default async function routingHandler(event) {
85
88
  internalEvent = afterRewrites.internalEvent;
86
89
  isExternalRewrite = afterRewrites.isExternalRewrite;
87
90
  }
91
+ let isISR = false;
88
92
  // We want to run this just before the dynamic route check
89
- const { event: fallbackEvent, isISR } = handleFallbackFalse(internalEvent, PrerenderManifest);
90
- internalEvent = fallbackEvent;
93
+ // We can skip it if its an external rewrite
94
+ if (!isExternalRewrite) {
95
+ const fallbackResult = handleFallbackFalse(internalEvent, PrerenderManifest);
96
+ internalEvent = fallbackResult.event;
97
+ isISR = fallbackResult.isISR;
98
+ }
91
99
  const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
92
100
  const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
93
101
  if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
@@ -96,17 +104,11 @@ export default async function routingHandler(event) {
96
104
  internalEvent = fallbackRewrites.internalEvent;
97
105
  isExternalRewrite = fallbackRewrites.isExternalRewrite;
98
106
  }
99
- // Api routes are not present in the routes manifest except if they're not behind /api
100
- // /api even if it's a page route doesn't get generated in the manifest
101
- // Ideally we would need to properly check api routes here
102
- const isApiRoute = internalEvent.rawPath === apiPrefix ||
103
- internalEvent.rawPath.startsWith(`${apiPrefix}/`);
104
107
  const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");
105
108
  const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
106
109
  // If we still haven't found a route, we show the 404 page
107
110
  // We need to ensure that rewrites are applied before showing the 404 page
108
111
  if (!(isRouteFoundBeforeAllRewrites ||
109
- isApiRoute ||
110
112
  isNextImageRoute ||
111
113
  // We need to check again once all rewrites have been applied
112
114
  staticRouteMatcher(internalEvent.rawPath).length > 0 ||
@@ -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,10 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { NextConfig } from "../../adapters/config/index";
3
4
  import { getMonorepoRelativePath } from "../../utils/normalize-path";
4
5
  export default {
5
6
  name: "fs-dev",
6
7
  load: async (url) => {
7
- const imagePath = path.join(getMonorepoRelativePath(), "assets", url);
8
+ const urlWithoutBasePath = NextConfig.basePath
9
+ ? url.slice(NextConfig.basePath.length)
10
+ : url;
11
+ const imagePath = path.join(getMonorepoRelativePath(), "assets", urlWithoutBasePath);
8
12
  const body = fs.createReadStream(imagePath);
9
13
  const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg";
10
14
  return {
@@ -1,13 +1,15 @@
1
1
  import path from "node:path";
2
2
  import express from "express";
3
+ import { NextConfig } from "../../adapters/config/index";
3
4
  import { getMonorepoRelativePath } from "../../utils/normalize-path";
4
5
  const wrapper = async (handler, converter) => {
5
6
  const app = express();
6
7
  // To serve static assets
7
- app.use(express.static(path.join(getMonorepoRelativePath(), "assets")));
8
+ const basePath = NextConfig.basePath ?? "";
9
+ app.use(basePath, express.static(path.join(getMonorepoRelativePath(), "assets")));
8
10
  const imageHandlerPath = path.join(getMonorepoRelativePath(), "image-optimization-function/index.mjs");
9
11
  const imageHandler = await import(imageHandlerPath).then((m) => m.handler);
10
- app.all("/_next/image", async (req, res) => {
12
+ app.all(`${NextConfig.basePath ?? ""}/_next/image`, async (req, res) => {
11
13
  const internalEvent = await converter.convertFrom(req);
12
14
  const streamCreator = {
13
15
  writeHeaders: (prelude) => {
@@ -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
  };
@@ -1,2 +1,3 @@
1
1
  export declare function normalizePath(path: string): string;
2
+ export declare function normalizeRepeatedSlashes(url: URL): string;
2
3
  export declare function getMonorepoRelativePath(relativePath?: string): string;
@@ -2,6 +2,13 @@ import path from "node:path";
2
2
  export function normalizePath(path) {
3
3
  return path.replace(/\\/g, "/");
4
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
+ }
5
12
  export function getMonorepoRelativePath(relativePath = "../..") {
6
13
  return path.join(globalThis.monorepoPackagePath
7
14
  .split("/")
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.6.1",
6
+ "version": "3.6.3",
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",