@opennextjs/aws 3.5.4 → 3.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/build/createServerBundle.js +3 -2
  2. package/dist/build/patch/patches/index.d.ts +4 -0
  3. package/dist/build/patch/patches/index.js +4 -0
  4. package/dist/build/patch/patches/patchEnvVar.d.ts +10 -0
  5. package/dist/build/patch/patches/patchEnvVar.js +52 -0
  6. package/dist/build/patch/{patchFetchCacheISR.d.ts → patches/patchFetchCacheISR.d.ts} +1 -1
  7. package/dist/build/patch/{patchFetchCacheISR.js → patches/patchFetchCacheISR.js} +2 -2
  8. package/dist/build/patch/{patchFetchCacheWaitUntil.d.ts → patches/patchFetchCacheWaitUntil.d.ts} +1 -1
  9. package/dist/build/patch/{patchFetchCacheWaitUntil.js → patches/patchFetchCacheWaitUntil.js} +2 -2
  10. package/dist/build/patch/patches/patchNextServer.d.ts +5 -0
  11. package/dist/build/patch/patches/patchNextServer.js +91 -0
  12. package/dist/build/validateConfig.js +1 -0
  13. package/dist/core/createMainHandler.js +2 -0
  14. package/dist/core/requestHandler.js +3 -7
  15. package/dist/core/routing/matcher.js +4 -10
  16. package/dist/core/routing/middleware.d.ts +8 -1
  17. package/dist/core/routing/middleware.js +10 -17
  18. package/dist/core/routing/util.d.ts +5 -2
  19. package/dist/core/routing/util.js +18 -18
  20. package/dist/core/routingHandler.js +144 -112
  21. package/dist/core/util.js +41 -1
  22. package/dist/http/util.d.ts +7 -0
  23. package/dist/http/util.js +23 -0
  24. package/dist/overrides/converters/edge.js +2 -14
  25. package/dist/overrides/converters/node.js +2 -2
  26. package/dist/overrides/converters/utils.d.ts +7 -0
  27. package/dist/overrides/converters/utils.js +10 -0
  28. package/dist/overrides/wrappers/aws-lambda-compressed.d.ts +7 -0
  29. package/dist/overrides/wrappers/aws-lambda-compressed.js +96 -0
  30. package/dist/overrides/wrappers/aws-lambda-streaming.js +1 -0
  31. package/dist/overrides/wrappers/aws-lambda.d.ts +2 -0
  32. package/dist/overrides/wrappers/aws-lambda.js +1 -1
  33. package/dist/types/aws-lambda.d.ts +4 -1
  34. package/dist/types/global.d.ts +6 -0
  35. package/dist/types/open-next.d.ts +12 -1
  36. package/package.json +1 -1
@@ -14,8 +14,7 @@ import { copyMiddlewareResources, generateEdgeBundle, } from "./edge/createEdgeB
14
14
  import * as buildHelper from "./helper.js";
15
15
  import { installDependencies } from "./installDeps.js";
16
16
  import { applyCodePatches } from "./patch/codePatcher.js";
17
- import { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patch/patchFetchCacheISR.js";
18
- import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js";
17
+ import { patchEnvVars, patchFetchCacheForISR, patchFetchCacheSetMissingWaitUntil, patchNextServer, patchUnstableCacheForISR, } from "./patch/patches/index.js";
19
18
  export async function createServerBundle(options, codeCustomization) {
20
19
  const { config } = options;
21
20
  const foundRoutes = new Set();
@@ -120,6 +119,8 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
120
119
  patchFetchCacheSetMissingWaitUntil,
121
120
  patchFetchCacheForISR,
122
121
  patchUnstableCacheForISR,
122
+ patchNextServer,
123
+ patchEnvVars,
123
124
  ...additionalCodePatches,
124
125
  ]);
125
126
  // Build Lambda code
@@ -0,0 +1,4 @@
1
+ export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
+ export { patchNextServer } from "./patchNextServer.js";
3
+ export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
4
+ export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
@@ -0,0 +1,4 @@
1
+ export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2
+ export { patchNextServer } from "./patchNextServer.js";
3
+ export { patchFetchCacheForISR, patchUnstableCacheForISR, } from "./patchFetchCacheISR.js";
4
+ export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
@@ -0,0 +1,10 @@
1
+ import type { CodePatcher } from "../codePatcher";
2
+ /**
3
+ * Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements
4
+ * This is used to avoid loading unnecessary deps at runtime
5
+ * @param envVar The env var that we want to replace
6
+ * @param value The value that we want to replace it with
7
+ * @returns
8
+ */
9
+ export declare const envVarRuleCreator: (envVar: string, value: string) => string;
10
+ export declare const patchEnvVars: CodePatcher;
@@ -0,0 +1,52 @@
1
+ import { createPatchCode } from "../astCodePatcher.js";
2
+ /**
3
+ * Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements
4
+ * This is used to avoid loading unnecessary deps at runtime
5
+ * @param envVar The env var that we want to replace
6
+ * @param value The value that we want to replace it with
7
+ * @returns
8
+ */
9
+ export const envVarRuleCreator = (envVar, value) => `
10
+ rule:
11
+ kind: member_expression
12
+ pattern: process.env.${envVar}
13
+ inside:
14
+ kind: parenthesized_expression
15
+ stopBy: end
16
+ inside:
17
+ kind: if_statement
18
+ fix:
19
+ '${value}'
20
+ `;
21
+ export const patchEnvVars = {
22
+ name: "patch-env-vars",
23
+ patches: [
24
+ // This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime
25
+ {
26
+ versions: ">=15.0.0",
27
+ field: {
28
+ pathFilter: /module\.compiled\.js$/,
29
+ contentFilter: /process\.env\.NEXT_RUNTIME/,
30
+ patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')),
31
+ },
32
+ },
33
+ // This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime
34
+ {
35
+ versions: ">=15.0.0",
36
+ field: {
37
+ pathFilter: /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/,
38
+ contentFilter: /process\.env\.NODE_ENV/,
39
+ patchCode: createPatchCode(envVarRuleCreator("NODE_ENV", '"production"')),
40
+ },
41
+ },
42
+ // This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime
43
+ {
44
+ versions: ">=15.0.0",
45
+ field: {
46
+ pathFilter: /module\.compiled\.js$/,
47
+ contentFilter: /process\.env\.TURBOPACK/,
48
+ patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")),
49
+ },
50
+ },
51
+ ],
52
+ };
@@ -1,4 +1,4 @@
1
- import type { CodePatcher } from "./codePatcher";
1
+ import type { CodePatcher } from "../codePatcher.js";
2
2
  export declare const fetchRule = "\nrule:\n kind: member_expression\n pattern: $WORK_STORE.isOnDemandRevalidate\n inside:\n kind: ternary_expression\n all:\n - has: {kind: 'null'}\n - has: \n kind: await_expression\n has:\n kind: call_expression\n all:\n - has:\n kind: member_expression\n has:\n kind: property_identifier\n field: property\n regex: get\n - has:\n kind: arguments\n has:\n kind: object\n has:\n kind: pair\n all:\n - has:\n kind: property_identifier\n field: key\n regex: softTags\n inside:\n kind: variable_declarator\n\nfix:\n ($WORK_STORE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)\n";
3
3
  export declare const unstable_cacheRule = "\nrule:\n kind: member_expression\n pattern: $STORE_OR_CACHE.isOnDemandRevalidate\n inside:\n kind: if_statement\n stopBy: end\n has:\n kind: statement_block\n has:\n kind: variable_declarator\n has: \n kind: await_expression\n has:\n kind: call_expression\n all:\n - has:\n kind: member_expression\n has:\n kind: property_identifier\n field: property\n regex: get\n - has:\n kind: arguments\n has:\n kind: object\n has:\n kind: pair\n all:\n - has:\n kind: property_identifier\n field: key\n regex: softTags\n stopBy: end\nfix:\n ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)\n";
4
4
  export declare const patchFetchCacheForISR: CodePatcher;
@@ -1,6 +1,6 @@
1
1
  import { Lang } from "@ast-grep/napi";
2
- import { getCrossPlatformPathRegex } from "../../utils/regex.js";
3
- import { createPatchCode } from "./astCodePatcher.js";
2
+ import { getCrossPlatformPathRegex } from "../../../utils/regex.js";
3
+ import { createPatchCode } from "../astCodePatcher.js";
4
4
  export const fetchRule = `
5
5
  rule:
6
6
  kind: member_expression
@@ -1,3 +1,3 @@
1
- import type { CodePatcher } from "./codePatcher";
1
+ import type { CodePatcher } from "../codePatcher.js";
2
2
  export declare const rule = "\nrule:\n kind: call_expression\n pattern: $PROMISE\n all:\n - has: { pattern: $_.arrayBuffer().then, stopBy: end }\n - has: { pattern: \"Buffer.from\", stopBy: end }\n - any:\n - inside:\n kind: sequence_expression\n inside:\n kind: return_statement\n - inside:\n kind: expression_statement\n precedes:\n kind: return_statement\n - has: { pattern: $_.FETCH, stopBy: end }\n\nfix: |\n globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE)\n";
3
3
  export declare const patchFetchCacheSetMissingWaitUntil: CodePatcher;
@@ -1,5 +1,5 @@
1
- import { getCrossPlatformPathRegex } from "../../utils/regex.js";
2
- import { createPatchCode } from "./astCodePatcher.js";
1
+ import { getCrossPlatformPathRegex } from "../../../utils/regex.js";
2
+ import { createPatchCode } from "../astCodePatcher.js";
3
3
  export const rule = `
4
4
  rule:
5
5
  kind: call_expression
@@ -0,0 +1,5 @@
1
+ import type { CodePatcher } from "../codePatcher.js";
2
+ export declare const minimalRule = "\nrule:\n kind: member_expression\n pattern: process.env.NEXT_MINIMAL\n any:\n - inside:\n kind: parenthesized_expression\n stopBy: end\n inside:\n kind: if_statement\n any:\n - inside:\n kind: statement_block\n inside:\n kind: method_definition\n any:\n - has: {kind: property_identifier, field: name, regex: runEdgeFunction}\n - has: {kind: property_identifier, field: name, regex: runMiddleware}\n - has: {kind: property_identifier, field: name, regex: imageOptimizer}\n - has:\n kind: statement_block\n has:\n kind: expression_statement\n pattern: res.statusCode = 400;\nfix:\n 'true'\n";
3
+ export declare const disablePreloadingRule = "\nrule:\n kind: statement_block\n inside:\n kind: if_statement\n any: \n - has: \n kind: member_expression\n pattern: this.nextConfig.experimental.preloadEntriesOnStart\n stopBy: end\n - has: \n kind: binary_expression\n pattern: appDocumentPreloading === true\n stopBy: end\nfix:\n '{}'\n";
4
+ export declare const removeMiddlewareManifestRule = "\nrule:\n kind: statement_block\n inside:\n kind: method_definition\n has:\n kind: property_identifier\n regex: getMiddlewareManifest\nfix:\n '{return null;}'\n";
5
+ export declare const patchNextServer: CodePatcher;
@@ -0,0 +1,91 @@
1
+ import { createPatchCode } from "../astCodePatcher.js";
2
+ // This rule will replace the `NEXT_MINIMAL` env variable with true in multiple places to avoid executing unwanted path (i.e. next middleware, edge functions and image optimization)
3
+ export const minimalRule = `
4
+ rule:
5
+ kind: member_expression
6
+ pattern: process.env.NEXT_MINIMAL
7
+ any:
8
+ - inside:
9
+ kind: parenthesized_expression
10
+ stopBy: end
11
+ inside:
12
+ kind: if_statement
13
+ any:
14
+ - inside:
15
+ kind: statement_block
16
+ inside:
17
+ kind: method_definition
18
+ any:
19
+ - has: {kind: property_identifier, field: name, regex: runEdgeFunction}
20
+ - has: {kind: property_identifier, field: name, regex: runMiddleware}
21
+ - has: {kind: property_identifier, field: name, regex: imageOptimizer}
22
+ - has:
23
+ kind: statement_block
24
+ has:
25
+ kind: expression_statement
26
+ pattern: res.statusCode = 400;
27
+ fix:
28
+ 'true'
29
+ `;
30
+ // This rule will disable the background preloading of route done by NextServer by default during the creation of NextServer
31
+ export const disablePreloadingRule = `
32
+ rule:
33
+ kind: statement_block
34
+ inside:
35
+ kind: if_statement
36
+ any:
37
+ - has:
38
+ kind: member_expression
39
+ pattern: this.nextConfig.experimental.preloadEntriesOnStart
40
+ stopBy: end
41
+ - has:
42
+ kind: binary_expression
43
+ pattern: appDocumentPreloading === true
44
+ stopBy: end
45
+ fix:
46
+ '{}'
47
+ `;
48
+ // This rule is mostly for splitted edge functions so that we don't try to match them on the other non edge functions
49
+ export const removeMiddlewareManifestRule = `
50
+ rule:
51
+ kind: statement_block
52
+ inside:
53
+ kind: method_definition
54
+ has:
55
+ kind: property_identifier
56
+ regex: getMiddlewareManifest
57
+ fix:
58
+ '{return null;}'
59
+ `;
60
+ export const patchNextServer = {
61
+ name: "patch-next-server",
62
+ patches: [
63
+ // Skip executing next middleware, edge functions and image optimization inside NextServer
64
+ {
65
+ versions: ">=15.0.0",
66
+ field: {
67
+ pathFilter: /next-server\.(js)$/,
68
+ contentFilter: /process\.env\.NEXT_MINIMAL/,
69
+ patchCode: createPatchCode(minimalRule),
70
+ },
71
+ },
72
+ // Disable Next background preloading done at creation of `NetxServer`
73
+ {
74
+ versions: ">=15.0.0",
75
+ field: {
76
+ pathFilter: /next-server\.(js)$/,
77
+ contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/,
78
+ patchCode: createPatchCode(disablePreloadingRule),
79
+ },
80
+ },
81
+ // Don't match edge functions in `NextServer`
82
+ {
83
+ versions: ">=15.0.0",
84
+ field: {
85
+ pathFilter: /next-server\.(js)$/,
86
+ contentFilter: /getMiddlewareManifest/,
87
+ patchCode: createPatchCode(removeMiddlewareManifestRule),
88
+ },
89
+ },
90
+ ],
91
+ };
@@ -6,6 +6,7 @@ const compatibilityMatrix = {
6
6
  "aws-cloudfront",
7
7
  "sqs-revalidate",
8
8
  ],
9
+ "aws-lambda-compressed": ["aws-apigw-v2"],
9
10
  "aws-lambda-streaming": ["aws-apigw-v2"],
10
11
  cloudflare: ["edge"],
11
12
  "cloudflare-edge": ["edge"],
@@ -10,6 +10,8 @@ export async function createMainHandler() {
10
10
  : config.default;
11
11
  globalThis.serverId = generateUniqueId();
12
12
  globalThis.openNextConfig = config;
13
+ // If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler.
14
+ await globalThis.__next_route_preloader("start");
13
15
  // Default queue
14
16
  globalThis.queue = await resolveQueue(thisFunction.override?.queue);
15
17
  globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache);
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { IncomingMessage } from "../http/index.js";
3
3
  import { runWithOpenNextRequestContext } from "../utils/promise";
4
4
  import { NextConfig } from "../adapters/config/index";
5
- import { debug, error, warn } from "../adapters/logger";
5
+ import { debug, error } from "../adapters/logger";
6
6
  import { patchAsyncStorage } from "./patchAsyncStorage";
7
7
  import { constructNextUrl, convertRes, convertToQuery, createServerResponse, } from "./routing/util";
8
8
  import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
@@ -19,6 +19,7 @@ export async function openNextHandler(internalEvent, options) {
19
19
  isISRRevalidation: initialHeaders["x-isr"] === "1",
20
20
  waitUntil: options?.waitUntil,
21
21
  }, async () => {
22
+ await globalThis.__next_route_preloader("waitUntil");
22
23
  if (initialHeaders["x-forwarded-host"]) {
23
24
  initialHeaders.host = initialHeaders["x-forwarded-host"];
24
25
  }
@@ -39,12 +40,7 @@ export async function openNextHandler(internalEvent, options) {
39
40
  ...internalHeaders,
40
41
  };
41
42
  //#override withRouting
42
- try {
43
- routingResult = await routingHandler(internalEvent);
44
- }
45
- catch (e) {
46
- warn("Routing failed.", e);
47
- }
43
+ routingResult = await routingHandler(internalEvent);
48
44
  //#endOverride
49
45
  const headers = "type" in routingResult
50
46
  ? routingResult.headers
@@ -114,19 +114,13 @@ export function handleRewrites(event, rewrites) {
114
114
  const { pathname, protocol, hostname, queryString } = getUrlParts(rewrite.destination, isExternalRewrite);
115
115
  // We need to use a localized path if the rewrite is not locale specific
116
116
  const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath;
117
- // We need to encode the "+" character with its UTF-8 equivalent "%20" to avoid 2 issues:
118
- // 1. The compile function from path-to-regexp will throw an error if it finds a "+" character.
119
- // https://github.com/pillarjs/path-to-regexp?tab=readme-ov-file#unexpected--or-
120
- // 2. The convertToQueryString function will replace the "+" character with %2B instead of %20.
121
- // %2B does not get interpreted as a space in the URL thus breaking the query string.
122
- const encodePlusQueryString = queryString.replaceAll("+", "%20");
123
117
  debug("urlParts", { pathname, protocol, hostname, queryString });
124
- const toDestinationPath = compile(escapeRegex(pathname));
118
+ const toDestinationPath = compile(escapeRegex(pathname, { isPath: true }));
125
119
  const toDestinationHost = compile(escapeRegex(hostname));
126
- const toDestinationQuery = compile(escapeRegex(encodePlusQueryString));
120
+ const toDestinationQuery = compile(escapeRegex(queryString));
127
121
  const params = {
128
122
  // params for the source
129
- ...getParamsFromSource(match(escapeRegex(rewrite.source)))(pathToUse),
123
+ ...getParamsFromSource(match(escapeRegex(rewrite.source, { isPath: true })))(pathToUse),
130
124
  // params for the has
131
125
  ...rewrite.has?.reduce((acc, cur) => {
132
126
  return Object.assign(acc, computeHas(cur));
@@ -137,7 +131,7 @@ export function handleRewrites(event, rewrites) {
137
131
  }, {}),
138
132
  };
139
133
  const isUsingParams = Object.keys(params).length > 0;
140
- let rewrittenQuery = encodePlusQueryString;
134
+ let rewrittenQuery = queryString;
141
135
  let rewrittenHost = hostname;
142
136
  let rewrittenPath = pathname;
143
137
  if (isUsingParams) {
@@ -7,5 +7,12 @@ type Middleware = (request: Request) => Response | Promise<Response>;
7
7
  type MiddlewareLoader = () => Promise<{
8
8
  default: Middleware;
9
9
  }>;
10
- export declare function handleMiddleware(internalEvent: InternalEvent, middlewareLoader?: MiddlewareLoader): Promise<MiddlewareEvent | InternalResult>;
10
+ /**
11
+ *
12
+ * @param internalEvent the internal event
13
+ * @param initialSearch the initial query string as it was received in the handler
14
+ * @param middlewareLoader Only used for unit test
15
+ * @returns `Promise<MiddlewareEvent | InternalResult>`
16
+ */
17
+ export declare function handleMiddleware(internalEvent: InternalEvent, initialSearch: string, middlewareLoader?: MiddlewareLoader): Promise<MiddlewareEvent | InternalResult>;
11
18
  export {};
@@ -1,14 +1,7 @@
1
1
  import { FunctionsConfigManifest, MiddlewareManifest, NextConfig, PrerenderManifest, } from "../../adapters/config/index.js";
2
2
  import { emptyReadableStream } from "../../utils/stream.js";
3
3
  import { localizePath } from "./i18n/index.js";
4
- //NOTE: we should try to avoid importing stuff from next as much as possible
5
- // every release of next could break this
6
- // const { run } = require("next/dist/server/web/sandbox");
7
- // const { getCloneableBody } = require("next/dist/server/body-streams");
8
- // const {
9
- // signalFromNodeResponse,
10
- // } = require("next/dist/server/web/spec-extension/adapters/next-request");
11
- import { convertBodyToReadableStream, convertToQueryString, getMiddlewareMatch, isExternal, } from "./util.js";
4
+ import { convertBodyToReadableStream, getMiddlewareMatch, isExternal, } from "./util.js";
12
5
  const middlewareManifest = MiddlewareManifest;
13
6
  const functionsConfigManifest = FunctionsConfigManifest;
14
7
  const middleMatch = getMiddlewareMatch(middlewareManifest, functionsConfigManifest);
@@ -16,12 +9,14 @@ function defaultMiddlewareLoader() {
16
9
  // @ts-expect-error - This is bundled
17
10
  return import("./middleware.mjs");
18
11
  }
19
- // NOTE: As of Nextjs 13.4.13+, the middleware is handled outside the next-server.
20
- // OpenNext will run the middleware in a sandbox and set the appropriate req headers
21
- // and res.body prior to processing the next-server.
22
- // @returns undefined | res.end()
23
- // if res.end() is return, the parent needs to return and not process next server
24
- export async function handleMiddleware(internalEvent, middlewareLoader = defaultMiddlewareLoader) {
12
+ /**
13
+ *
14
+ * @param internalEvent the internal event
15
+ * @param initialSearch the initial query string as it was received in the handler
16
+ * @param middlewareLoader Only used for unit test
17
+ * @returns `Promise<MiddlewareEvent | InternalResult>`
18
+ */
19
+ export async function handleMiddleware(internalEvent, initialSearch, middlewareLoader = defaultMiddlewareLoader) {
25
20
  const headers = internalEvent.headers;
26
21
  // We bypass the middleware if the request is internal
27
22
  // We should only do that if the request has the correct `x-prerender-revalidate` header
@@ -36,7 +31,7 @@ export async function handleMiddleware(internalEvent, middlewareLoader = default
36
31
  if (!hasMatch)
37
32
  return internalEvent;
38
33
  const initialUrl = new URL(normalizedPath, internalEvent.url);
39
- initialUrl.search = convertToQueryString(internalEvent.query);
34
+ initialUrl.search = initialSearch;
40
35
  const url = initialUrl.href;
41
36
  const middleware = await middlewareLoader();
42
37
  const result = await middleware.default({
@@ -117,13 +112,11 @@ export async function handleMiddleware(internalEvent, middlewareLoader = default
117
112
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
118
113
  // NOTE: the header was added to `req` from above
119
114
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
120
- let rewritten = false;
121
115
  let isExternalRewrite = false;
122
116
  let middlewareQueryString = internalEvent.query;
123
117
  let newUrl = internalEvent.url;
124
118
  if (rewriteUrl) {
125
119
  newUrl = rewriteUrl;
126
- rewritten = true;
127
120
  // If not a string, it should probably throw
128
121
  if (isExternal(newUrl, internalEvent.headers.host)) {
129
122
  isExternalRewrite = true;
@@ -8,7 +8,7 @@ import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from
8
8
  * @__PURE__
9
9
  */
10
10
  export declare function isExternal(url?: string, host?: string): boolean;
11
- export declare function convertFromQueryString(query: string): Record<string, string>;
11
+ export declare function convertFromQueryString(query: string): Record<string, string | string[]>;
12
12
  /**
13
13
  *
14
14
  * @__PURE__
@@ -38,6 +38,7 @@ export declare function convertRes(res: OpenNextNodeResponse): InternalResult;
38
38
  * Make sure that multi-value query parameters are transformed to
39
39
  * ?key=value1&key=value2&... so that Next converts those parameters
40
40
  * to an array when reading the query parameters
41
+ * query should be properly encoded before using this function
41
42
  * @__PURE__
42
43
  */
43
44
  export declare function convertToQueryString(query: Record<string, string | string[]>): string;
@@ -56,7 +57,9 @@ export declare function getMiddlewareMatch(middlewareManifest: MiddlewareManifes
56
57
  *
57
58
  * @__PURE__
58
59
  */
59
- export declare function escapeRegex(str: string): string;
60
+ export declare function escapeRegex(str: string, { isPath }?: {
61
+ isPath?: boolean;
62
+ }): string;
60
63
  /**
61
64
  *
62
65
  * @__PURE__
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import { Readable } from "node:stream";
3
3
  import { BuildId, HtmlPages, NextConfig } from "../../adapters/config/index.js";
4
4
  import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
5
- import { parseHeaders } from "../../http/util.js";
5
+ import { getQueryFromIterator, parseHeaders } from "../../http/util.js";
6
6
  import { debug, error } from "../../adapters/logger.js";
7
7
  import { isBinaryContentType } from "../../utils/binary.js";
8
8
  import { localizePath } from "./i18n/index.js";
@@ -24,11 +24,10 @@ export function convertFromQueryString(query) {
24
24
  if (query === "")
25
25
  return {};
26
26
  const queryParts = query.split("&");
27
- return queryParts.reduce((acc, part) => {
28
- const [key, value] = part.split("=");
29
- acc[key] = value;
30
- return acc;
31
- }, {});
27
+ return getQueryFromIterator(queryParts.map((p) => {
28
+ const [key, value] = p.split("=");
29
+ return [key, value];
30
+ }));
32
31
  }
33
32
  /**
34
33
  *
@@ -99,23 +98,20 @@ export function convertRes(res) {
99
98
  * Make sure that multi-value query parameters are transformed to
100
99
  * ?key=value1&key=value2&... so that Next converts those parameters
101
100
  * to an array when reading the query parameters
101
+ * query should be properly encoded before using this function
102
102
  * @__PURE__
103
103
  */
104
104
  export function convertToQueryString(query) {
105
- // URLSearchParams is a representation of the PARSED query.
106
- // So we must decode the value before appending it to the URLSearchParams.
107
- // https://stackoverflow.com/a/45516812
108
- const urlQuery = new URLSearchParams();
105
+ const queryStrings = [];
109
106
  Object.entries(query).forEach(([key, value]) => {
110
107
  if (Array.isArray(value)) {
111
- value.forEach((entry) => urlQuery.append(key, decodeURIComponent(entry)));
108
+ value.forEach((entry) => queryStrings.push(`${key}=${entry}`));
112
109
  }
113
110
  else {
114
- urlQuery.append(key, decodeURIComponent(value));
111
+ queryStrings.push(`${key}=${value}`);
115
112
  }
116
113
  });
117
- const queryString = urlQuery.toString();
118
- return queryString ? `?${queryString}` : "";
114
+ return queryStrings.length > 0 ? `?${queryStrings.join("&")}` : "";
119
115
  }
120
116
  /**
121
117
  * Given a raw query string, returns a record with key value-array pairs
@@ -148,11 +144,12 @@ export function getMiddlewareMatch(middlewareManifest, functionsManifest) {
148
144
  *
149
145
  * @__PURE__
150
146
  */
151
- export function escapeRegex(str) {
152
- return str
147
+ export function escapeRegex(str, { isPath } = {}) {
148
+ const result = str
153
149
  .replaceAll("(.)", "_µ1_")
154
150
  .replaceAll("(..)", "_µ2_")
155
151
  .replaceAll("(...)", "_µ3_");
152
+ return isPath ? result : result.replaceAll("+", "_µ4_");
156
153
  }
157
154
  /**
158
155
  *
@@ -162,7 +159,8 @@ export function unescapeRegex(str) {
162
159
  return str
163
160
  .replaceAll("_µ1_", "(.)")
164
161
  .replaceAll("_µ2_", "(..)")
165
- .replaceAll("_µ3_", "(...)");
162
+ .replaceAll("_µ3_", "(...)")
163
+ .replaceAll("_µ4_", "+");
166
164
  }
167
165
  /**
168
166
  * @__PURE__
@@ -202,7 +200,9 @@ export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
202
200
  const localizedPath = localizePath(internalEvent);
203
201
  // WORKAROUND: `NextServer` does not set cache headers for HTML pages
204
202
  // https://opennext.js.org/aws/v2/advanced/workaround#workaround-nextserver-does-not-set-cache-headers-for-html-pages
205
- if (HtmlPages.includes(localizedPath)) {
203
+ // Requests containing an `x-middleware-prefetch` header must not be cached
204
+ if (HtmlPages.includes(localizedPath) &&
205
+ !internalEvent.headers["x-middleware-prefetch"]) {
206
206
  headers[CommonHeaders.CACHE_CONTROL] =
207
207
  "public, max-age=0, s-maxage=31536000, must-revalidate";
208
208
  }
@@ -1,5 +1,5 @@
1
1
  import { BuildId, ConfigHeaders, NextConfig, PrerenderManifest, RoutesManifest, } from "../adapters/config/index";
2
- import { debug } from "../adapters/logger";
2
+ import { debug, error } from "../adapters/logger";
3
3
  import { cacheInterceptor } from "./routing/cacheInterceptor";
4
4
  import { detectLocale } from "./routing/i18n";
5
5
  import { fixDataPage, getNextConfigHeaders, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
@@ -32,121 +32,153 @@ function applyMiddlewareHeaders(eventHeaders, middlewareHeaders, setPrefix = tru
32
32
  });
33
33
  }
34
34
  export default async function routingHandler(event) {
35
- // Add Next geo headers
36
- for (const [openNextGeoName, nextGeoName] of Object.entries(geoHeaderToNextHeader)) {
37
- const value = event.headers[openNextGeoName];
38
- if (value) {
39
- event.headers[nextGeoName] = value;
35
+ try {
36
+ // Add Next geo headers
37
+ for (const [openNextGeoName, nextGeoName] of Object.entries(geoHeaderToNextHeader)) {
38
+ const value = event.headers[openNextGeoName];
39
+ if (value) {
40
+ event.headers[nextGeoName] = value;
41
+ }
40
42
  }
41
- }
42
- // First we remove internal headers
43
- // We don't want to allow users to set these headers
44
- for (const key of Object.keys(event.headers)) {
45
- if (key.startsWith(INTERNAL_HEADER_PREFIX) ||
46
- key.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
47
- delete event.headers[key];
43
+ // First we remove internal headers
44
+ // We don't want to allow users to set these headers
45
+ for (const key of Object.keys(event.headers)) {
46
+ if (key.startsWith(INTERNAL_HEADER_PREFIX) ||
47
+ key.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
48
+ delete event.headers[key];
49
+ }
48
50
  }
49
- }
50
- const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);
51
- let internalEvent = fixDataPage(event, BuildId);
52
- if ("statusCode" in internalEvent) {
53
- return internalEvent;
54
- }
55
- const redirect = handleRedirects(internalEvent, RoutesManifest.redirects);
56
- if (redirect) {
57
- debug("redirect", redirect);
58
- return redirect;
59
- }
60
- const eventOrResult = await handleMiddleware(internalEvent);
61
- const isResult = "statusCode" in eventOrResult;
62
- if (isResult) {
63
- return eventOrResult;
64
- }
65
- const middlewareResponseHeaders = eventOrResult.responseHeaders;
66
- let isExternalRewrite = eventOrResult.isExternalRewrite ?? false;
67
- // internalEvent is `InternalEvent | MiddlewareEvent`
68
- internalEvent = eventOrResult;
69
- if (!isExternalRewrite) {
70
- // First rewrite to be applied
71
- const beforeRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.beforeFiles);
72
- internalEvent = beforeRewrites.internalEvent;
73
- isExternalRewrite = beforeRewrites.isExternalRewrite;
74
- }
75
- const foundStaticRoute = staticRouteMatcher(internalEvent.rawPath);
76
- const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0;
77
- if (!(isStaticRoute || isExternalRewrite)) {
78
- // Second rewrite to be applied
79
- const afterRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.afterFiles);
80
- internalEvent = afterRewrites.internalEvent;
81
- isExternalRewrite = afterRewrites.isExternalRewrite;
82
- }
83
- // We want to run this just before the dynamic route check
84
- const { event: fallbackEvent, isISR } = handleFallbackFalse(internalEvent, PrerenderManifest);
85
- internalEvent = fallbackEvent;
86
- const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
87
- const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
88
- if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
89
- // Fallback rewrite to be applied
90
- const fallbackRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.fallback);
91
- internalEvent = fallbackRewrites.internalEvent;
92
- isExternalRewrite = fallbackRewrites.isExternalRewrite;
93
- }
94
- // Api routes are not present in the routes manifest except if they're not behind /api
95
- // /api even if it's a page route doesn't get generated in the manifest
96
- // Ideally we would need to properly check api routes here
97
- const isApiRoute = internalEvent.rawPath === apiPrefix ||
98
- internalEvent.rawPath.startsWith(`${apiPrefix}/`);
99
- const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");
100
- const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
101
- // If we still haven't found a route, we show the 404 page
102
- // We need to ensure that rewrites are applied before showing the 404 page
103
- if (!(isRouteFoundBeforeAllRewrites ||
104
- isApiRoute ||
105
- isNextImageRoute ||
106
- // We need to check again once all rewrites have been applied
107
- staticRouteMatcher(internalEvent.rawPath).length > 0 ||
108
- dynamicRouteMatcher(internalEvent.rawPath).length > 0)) {
109
- internalEvent = {
110
- ...internalEvent,
111
- rawPath: "/404",
112
- url: constructNextUrl(internalEvent.url, "/404"),
113
- headers: {
114
- ...internalEvent.headers,
115
- "x-middleware-response-cache-control": "private, no-cache, no-store, max-age=0, must-revalidate",
116
- },
117
- };
118
- }
119
- if (globalThis.openNextConfig.dangerous?.enableCacheInterception &&
120
- !("statusCode" in internalEvent)) {
121
- debug("Cache interception enabled");
122
- internalEvent = await cacheInterceptor(internalEvent);
51
+ const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);
52
+ let internalEvent = fixDataPage(event, BuildId);
123
53
  if ("statusCode" in internalEvent) {
124
- applyMiddlewareHeaders(internalEvent.headers, {
125
- ...middlewareResponseHeaders,
126
- ...nextHeaders,
127
- }, false);
128
54
  return internalEvent;
129
55
  }
56
+ const redirect = handleRedirects(internalEvent, RoutesManifest.redirects);
57
+ if (redirect) {
58
+ debug("redirect", redirect);
59
+ return redirect;
60
+ }
61
+ const eventOrResult = await handleMiddleware(internalEvent,
62
+ // We need to pass the initial search without any decoding
63
+ // TODO: we'd need to refactor InternalEvent to include the initial querystring directly
64
+ // Should be done in another PR because it is a breaking change
65
+ new URL(event.url).search);
66
+ const isResult = "statusCode" in eventOrResult;
67
+ if (isResult) {
68
+ return eventOrResult;
69
+ }
70
+ const middlewareResponseHeaders = eventOrResult.responseHeaders;
71
+ let isExternalRewrite = eventOrResult.isExternalRewrite ?? false;
72
+ // internalEvent is `InternalEvent | MiddlewareEvent`
73
+ internalEvent = eventOrResult;
74
+ if (!isExternalRewrite) {
75
+ // First rewrite to be applied
76
+ const beforeRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.beforeFiles);
77
+ internalEvent = beforeRewrites.internalEvent;
78
+ isExternalRewrite = beforeRewrites.isExternalRewrite;
79
+ }
80
+ const foundStaticRoute = staticRouteMatcher(internalEvent.rawPath);
81
+ const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0;
82
+ if (!(isStaticRoute || isExternalRewrite)) {
83
+ // Second rewrite to be applied
84
+ const afterRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.afterFiles);
85
+ internalEvent = afterRewrites.internalEvent;
86
+ isExternalRewrite = afterRewrites.isExternalRewrite;
87
+ }
88
+ // We want to run this just before the dynamic route check
89
+ const { event: fallbackEvent, isISR } = handleFallbackFalse(internalEvent, PrerenderManifest);
90
+ internalEvent = fallbackEvent;
91
+ const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
92
+ const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
93
+ if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
94
+ // Fallback rewrite to be applied
95
+ const fallbackRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.fallback);
96
+ internalEvent = fallbackRewrites.internalEvent;
97
+ isExternalRewrite = fallbackRewrites.isExternalRewrite;
98
+ }
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
+ const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");
105
+ const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
106
+ // If we still haven't found a route, we show the 404 page
107
+ // We need to ensure that rewrites are applied before showing the 404 page
108
+ if (!(isRouteFoundBeforeAllRewrites ||
109
+ isApiRoute ||
110
+ isNextImageRoute ||
111
+ // We need to check again once all rewrites have been applied
112
+ staticRouteMatcher(internalEvent.rawPath).length > 0 ||
113
+ dynamicRouteMatcher(internalEvent.rawPath).length > 0)) {
114
+ internalEvent = {
115
+ ...internalEvent,
116
+ rawPath: "/404",
117
+ url: constructNextUrl(internalEvent.url, "/404"),
118
+ headers: {
119
+ ...internalEvent.headers,
120
+ "x-middleware-response-cache-control": "private, no-cache, no-store, max-age=0, must-revalidate",
121
+ },
122
+ };
123
+ }
124
+ if (globalThis.openNextConfig.dangerous?.enableCacheInterception &&
125
+ !("statusCode" in internalEvent)) {
126
+ debug("Cache interception enabled");
127
+ internalEvent = await cacheInterceptor(internalEvent);
128
+ if ("statusCode" in internalEvent) {
129
+ applyMiddlewareHeaders(internalEvent.headers, {
130
+ ...middlewareResponseHeaders,
131
+ ...nextHeaders,
132
+ }, false);
133
+ return internalEvent;
134
+ }
135
+ }
136
+ // We apply the headers from the middleware response last
137
+ applyMiddlewareHeaders(internalEvent.headers, {
138
+ ...middlewareResponseHeaders,
139
+ ...nextHeaders,
140
+ });
141
+ const resolvedRoutes = [
142
+ ...foundStaticRoute,
143
+ ...foundDynamicRoute,
144
+ ];
145
+ debug("resolvedRoutes", resolvedRoutes);
146
+ return {
147
+ internalEvent,
148
+ isExternalRewrite,
149
+ origin: false,
150
+ isISR,
151
+ resolvedRoutes,
152
+ initialURL: event.url,
153
+ locale: NextConfig.i18n
154
+ ? detectLocale(internalEvent, NextConfig.i18n)
155
+ : undefined,
156
+ };
157
+ }
158
+ catch (e) {
159
+ error("Error in routingHandler", e);
160
+ // In case of an error, we want to return the 500 page from Next.js
161
+ return {
162
+ internalEvent: {
163
+ type: "core",
164
+ method: "GET",
165
+ rawPath: "/500",
166
+ url: constructNextUrl(event.url, "/500"),
167
+ headers: {
168
+ ...event.headers,
169
+ },
170
+ query: event.query,
171
+ cookies: event.cookies,
172
+ remoteAddress: event.remoteAddress,
173
+ },
174
+ isExternalRewrite: false,
175
+ origin: false,
176
+ isISR: false,
177
+ resolvedRoutes: [],
178
+ initialURL: event.url,
179
+ locale: NextConfig.i18n
180
+ ? detectLocale(event, NextConfig.i18n)
181
+ : undefined,
182
+ };
130
183
  }
131
- // We apply the headers from the middleware response last
132
- applyMiddlewareHeaders(internalEvent.headers, {
133
- ...middlewareResponseHeaders,
134
- ...nextHeaders,
135
- });
136
- const resolvedRoutes = [
137
- ...foundStaticRoute,
138
- ...foundDynamicRoute,
139
- ];
140
- debug("resolvedRoutes", resolvedRoutes);
141
- return {
142
- internalEvent,
143
- isExternalRewrite,
144
- origin: false,
145
- isISR,
146
- resolvedRoutes,
147
- initialURL: event.url,
148
- locale: NextConfig.i18n
149
- ? detectLocale(internalEvent, NextConfig.i18n)
150
- : undefined,
151
- };
152
184
  }
package/dist/core/util.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AppPathsManifestKeys, NextConfig, RoutesManifest, } from "../adapters/config/index.js";
2
2
  // @ts-ignore
3
3
  import NextServer from "next/dist/server/next-server.js";
4
- import { debug } from "../adapters/logger.js";
4
+ import { debug, error } from "../adapters/logger.js";
5
5
  import { applyOverride as applyNextjsRequireHooksOverride, overrideHooks as overrideNextjsRequireHooks, } from "./require-hooks.js";
6
6
  // WORKAROUND: Set `__NEXT_PRIVATE_PREBUNDLED_REACT` to use prebundled Reac
7
7
  // See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-__next_private_prebundled_react-to-use-prebundled-react
@@ -48,6 +48,46 @@ const nextServer = new NextServer.default({
48
48
  dev: false,
49
49
  dir: __dirname,
50
50
  });
51
+ let routesLoaded = false;
52
+ globalThis.__next_route_preloader = async (stage) => {
53
+ if (routesLoaded) {
54
+ return;
55
+ }
56
+ const thisFunction = globalThis.fnName
57
+ ? globalThis.openNextConfig.functions[globalThis.fnName]
58
+ : globalThis.openNextConfig.default;
59
+ const routePreloadingBehavior = thisFunction?.routePreloadingBehavior ?? "none";
60
+ if (routePreloadingBehavior === "none") {
61
+ routesLoaded = true;
62
+ return;
63
+ }
64
+ if (!("unstable_preloadEntries" in nextServer)) {
65
+ debug("The current version of Next.js does not support route preloading. Skipping route preloading.");
66
+ routesLoaded = true;
67
+ return;
68
+ }
69
+ if (stage === "waitUntil" && routePreloadingBehavior === "withWaitUntil") {
70
+ // We need to access the waitUntil
71
+ const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
72
+ if (!waitUntil) {
73
+ error("You've tried to use the 'withWaitUntil' route preloading behavior, but the 'waitUntil' function is not available.");
74
+ routesLoaded = true;
75
+ return;
76
+ }
77
+ debug("Preloading entries with waitUntil");
78
+ waitUntil?.(nextServer.unstable_preloadEntries());
79
+ routesLoaded = true;
80
+ }
81
+ else if ((stage === "start" && routePreloadingBehavior === "onStart") ||
82
+ (stage === "warmerEvent" && routePreloadingBehavior === "onWarmerEvent") ||
83
+ stage === "onDemand") {
84
+ const startTimestamp = Date.now();
85
+ debug("Preloading entries");
86
+ await nextServer.unstable_preloadEntries();
87
+ debug("Preloading entries took", Date.now() - startTimestamp, "ms");
88
+ routesLoaded = true;
89
+ }
90
+ };
51
91
  // `getRequestHandlerWithMetadata` is not available in older versions of Next.js
52
92
  // It is required to for next 15.2 to pass metadata for page router data route
53
93
  export const requestHandler = (metadata) => "getRequestHandlerWithMetadata" in nextServer
@@ -2,3 +2,10 @@ import type http from "node:http";
2
2
  export declare const parseHeaders: (headers?: http.OutgoingHttpHeader[] | http.OutgoingHttpHeaders) => Record<string, string>;
3
3
  export declare const convertHeader: (header: http.OutgoingHttpHeader) => string;
4
4
  export declare function parseCookies(cookies: string | string[] | null | undefined): string[];
5
+ /**
6
+ *
7
+ * Get the query object from an iterable of [key, value] pairs
8
+ * @param it - The iterable of [key, value] pairs
9
+ * @returns The query object
10
+ */
11
+ export declare function getQueryFromIterator(it: Iterable<[string, string]>): Record<string, string | string[]>;
package/dist/http/util.js CHANGED
@@ -28,3 +28,26 @@ export function parseCookies(cookies) {
28
28
  ? cookies.split(/(?<!Expires=\w+),/i).map((c) => c.trim())
29
29
  : cookies;
30
30
  }
31
+ /**
32
+ *
33
+ * Get the query object from an iterable of [key, value] pairs
34
+ * @param it - The iterable of [key, value] pairs
35
+ * @returns The query object
36
+ */
37
+ export function getQueryFromIterator(it) {
38
+ const query = {};
39
+ for (const [key, value] of it) {
40
+ if (key in query) {
41
+ if (Array.isArray(query[key])) {
42
+ query[key].push(value);
43
+ }
44
+ else {
45
+ query[key] = [query[key], value];
46
+ }
47
+ }
48
+ else {
49
+ query[key] = value;
50
+ }
51
+ }
52
+ return query;
53
+ }
@@ -1,23 +1,11 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { parseCookies } from "../../http/util";
3
+ import { getQueryFromSearchParams } from "./utils.js";
3
4
  const converter = {
4
5
  convertFrom: async (event) => {
5
6
  const url = new URL(event.url);
6
7
  const searchParams = url.searchParams;
7
- const query = {};
8
- for (const [key, value] of searchParams.entries()) {
9
- if (key in query) {
10
- if (Array.isArray(query[key])) {
11
- query[key].push(value);
12
- }
13
- else {
14
- query[key] = [query[key], value];
15
- }
16
- }
17
- else {
18
- query[key] = value;
19
- }
20
- }
8
+ const query = getQueryFromSearchParams(searchParams);
21
9
  // Transform body into Buffer
22
10
  const body = await event.arrayBuffer();
23
11
  const headers = {};
@@ -1,5 +1,5 @@
1
1
  import { parseCookies } from "../../http/util";
2
- import { extractHostFromHeaders } from "./utils";
2
+ import { extractHostFromHeaders, getQueryFromSearchParams } from "./utils.js";
3
3
  const converter = {
4
4
  convertFrom: async (req) => {
5
5
  const body = await new Promise((resolve) => {
@@ -18,7 +18,7 @@ const converter = {
18
18
  ])
19
19
  .filter(([key]) => key));
20
20
  const url = new URL(req.url, `http://${extractHostFromHeaders(headers)}`);
21
- const query = Object.fromEntries(url.searchParams.entries());
21
+ const query = getQueryFromSearchParams(url.searchParams);
22
22
  return {
23
23
  type: "core",
24
24
  method: req.method ?? "GET",
@@ -6,3 +6,10 @@ export declare function removeUndefinedFromQuery(query: Record<string, string |
6
6
  * @returns The host
7
7
  */
8
8
  export declare function extractHostFromHeaders(headers: Record<string, string>): string;
9
+ /**
10
+ * Get the query object from an URLSearchParams
11
+ *
12
+ * @param searchParams
13
+ * @returns
14
+ */
15
+ export declare function getQueryFromSearchParams(searchParams: URLSearchParams): Record<string, string | string[]>;
@@ -1,3 +1,4 @@
1
+ import { getQueryFromIterator } from "../../http/util.js";
1
2
  export function removeUndefinedFromQuery(query) {
2
3
  const newQuery = {};
3
4
  for (const [key, value] of Object.entries(query)) {
@@ -16,3 +17,12 @@ export function removeUndefinedFromQuery(query) {
16
17
  export function extractHostFromHeaders(headers) {
17
18
  return headers["x-forwarded-host"] ?? headers.host ?? "on";
18
19
  }
20
+ /**
21
+ * Get the query object from an URLSearchParams
22
+ *
23
+ * @param searchParams
24
+ * @returns
25
+ */
26
+ export function getQueryFromSearchParams(searchParams) {
27
+ return getQueryFromIterator(searchParams.entries());
28
+ }
@@ -0,0 +1,7 @@
1
+ import type { WrapperHandler } from "../../types/overrides";
2
+ declare const _default: {
3
+ wrapper: WrapperHandler;
4
+ name: string;
5
+ supportStreaming: boolean;
6
+ };
7
+ export default _default;
@@ -0,0 +1,96 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import zlib from "node:zlib";
3
+ import { error } from "../../adapters/logger";
4
+ import { formatWarmerResponse } from "./aws-lambda";
5
+ const handler = async (handler, converter) => async (event) => {
6
+ // Handle warmer event
7
+ if ("type" in event) {
8
+ return formatWarmerResponse(event);
9
+ }
10
+ const internalEvent = await converter.convertFrom(event);
11
+ // This is a workaround
12
+ // https://github.com/opennextjs/opennextjs-aws/blob/e9b37fd44eb856eb8ae73168bf455ff85dd8b285/packages/open-next/src/overrides/wrappers/aws-lambda.ts#L49-L53
13
+ const fakeStream = {
14
+ writeHeaders: () => {
15
+ return new Writable({
16
+ write: (_chunk, _encoding, callback) => {
17
+ callback();
18
+ },
19
+ });
20
+ },
21
+ };
22
+ const handlerResponse = await handler(internalEvent, {
23
+ streamCreator: fakeStream,
24
+ });
25
+ // Check if response is already compressed
26
+ // The handlers response headers are lowercase
27
+ const alreadyEncoded = handlerResponse.headers["content-encoding"] ?? "";
28
+ // Return early here if the response is already compressed
29
+ if (alreadyEncoded) {
30
+ return converter.convertTo(handlerResponse, event);
31
+ }
32
+ // We compress the body if the client accepts it
33
+ const acceptEncoding = internalEvent.headers["accept-encoding"] ??
34
+ internalEvent.headers["Accept-Encoding"] ??
35
+ "";
36
+ let contentEncoding = null;
37
+ if (acceptEncoding?.includes("br")) {
38
+ contentEncoding = "br";
39
+ }
40
+ else if (acceptEncoding?.includes("gzip")) {
41
+ contentEncoding = "gzip";
42
+ }
43
+ else if (acceptEncoding?.includes("deflate")) {
44
+ contentEncoding = "deflate";
45
+ }
46
+ const response = {
47
+ ...handlerResponse,
48
+ body: compressBody(handlerResponse.body, contentEncoding),
49
+ headers: {
50
+ ...handlerResponse.headers,
51
+ ...(contentEncoding ? { "content-encoding": contentEncoding } : {}),
52
+ },
53
+ isBase64Encoded: !!contentEncoding || handlerResponse.isBase64Encoded,
54
+ };
55
+ return converter.convertTo(response, event);
56
+ };
57
+ export default {
58
+ wrapper: handler,
59
+ name: "aws-lambda-compressed",
60
+ supportStreaming: false,
61
+ };
62
+ function compressBody(body, encoding) {
63
+ // If no encoding is specified, return original body
64
+ if (!encoding)
65
+ return body;
66
+ try {
67
+ const readable = Readable.fromWeb(body);
68
+ let transform;
69
+ switch (encoding) {
70
+ case "br":
71
+ transform = zlib.createBrotliCompress({
72
+ params: {
73
+ // This is a compromise between speed and compression ratio.
74
+ // The default one will most likely timeout an AWS Lambda with default configuration on large bodies (>6mb).
75
+ // Therefore we set it to 6, which is a good compromise.
76
+ [zlib.constants.BROTLI_PARAM_QUALITY]: Number(process.env.BROTLI_QUALITY) ?? 6,
77
+ },
78
+ });
79
+ break;
80
+ case "gzip":
81
+ transform = zlib.createGzip();
82
+ break;
83
+ case "deflate":
84
+ transform = zlib.createDeflate();
85
+ break;
86
+ default:
87
+ return body;
88
+ }
89
+ return Readable.toWeb(readable.pipe(transform));
90
+ }
91
+ catch (e) {
92
+ error("Error compressing body:", e);
93
+ // Fall back to no compression on error
94
+ return body;
95
+ }
96
+ }
@@ -14,6 +14,7 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
14
14
  if ("type" in event) {
15
15
  const result = await formatWarmerResponse(event);
16
16
  responseStream.end(Buffer.from(JSON.stringify(result)), "utf-8");
17
+ await globalThis.__next_route_preloader("warmerEvent");
17
18
  return;
18
19
  }
19
20
  const internalEvent = await converter.convertFrom(event);
@@ -1,4 +1,6 @@
1
1
  import type { WrapperHandler } from "../../types/overrides";
2
+ import type { WarmerEvent, WarmerResponse } from "../../adapters/warmer-function";
3
+ export declare function formatWarmerResponse(event: WarmerEvent): Promise<WarmerResponse>;
2
4
  declare const _default: {
3
5
  wrapper: WrapperHandler;
4
6
  name: string;
@@ -1,5 +1,5 @@
1
1
  import { Writable } from "node:stream";
2
- function formatWarmerResponse(event) {
2
+ export function formatWarmerResponse(event) {
3
3
  return new Promise((resolve) => {
4
4
  setTimeout(() => {
5
5
  resolve({ serverId, type: "warmer" });
@@ -1,5 +1,6 @@
1
1
  import type { Writable } from "node:stream";
2
- import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
2
+ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyResult, APIGatewayProxyResultV2, CloudFrontRequestEvent, CloudFrontRequestResult, Context } from "aws-lambda";
3
+ import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
3
4
  export interface ResponseStream extends Writable {
4
5
  getBufferedData(): Buffer;
5
6
  setContentType(contentType: string): void;
@@ -17,4 +18,6 @@ declare global {
17
18
  }
18
19
  }
19
20
  }
21
+ export type AwsLambdaEvent = APIGatewayProxyEventV2 | CloudFrontRequestEvent | APIGatewayProxyEvent | WarmerEvent;
22
+ export type AwsLambdaReturn = APIGatewayProxyResultV2 | APIGatewayProxyResult | CloudFrontRequestResult | WarmerResponse;
20
23
  export {};
@@ -179,5 +179,11 @@ declare global {
179
179
  * Defined in `createMainHandler`
180
180
  */
181
181
  var cdnInvalidationHandler: CDNInvalidationHandler;
182
+ /**
183
+ * A function to preload the routes.
184
+ * This needs to be defined on globalThis because it can be used by custom overrides.
185
+ * Only available in main functions.
186
+ */
187
+ var __next_route_preloader: (stage: "waitUntil" | "start" | "warmerEvent" | "onDemand") => Promise<void>;
182
188
  }
183
189
  export {};
@@ -67,13 +67,23 @@ export interface Origin {
67
67
  port?: number;
68
68
  customHeaders?: Record<string, string>;
69
69
  }
70
- export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "express-dev" | "dummy";
70
+ export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "aws-lambda-compressed" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "express-dev" | "dummy";
71
71
  export type IncludedConverter = "aws-apigw-v2" | "aws-apigw-v1" | "aws-cloudfront" | "edge" | "node" | "sqs-revalidate" | "dummy";
72
72
  export type RouteType = "route" | "page" | "app";
73
73
  export interface ResolvedRoute {
74
74
  route: string;
75
75
  type: RouteType;
76
76
  }
77
+ /**
78
+ * The route preloading behavior. Only supported in Next 15+.
79
+ * Default behavior of Next is disabled. You should do your own testing to choose which one suits you best
80
+ * - "none" - No preloading of the route at all. This is the default
81
+ * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none". At the moment only cloudflare wrappers provide a `waitUntil`
82
+ * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now
83
+ * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. Useful for long running server or in serverless with some careful testing
84
+ * @default "none"
85
+ */
86
+ export type RoutePreloadingBehavior = "none" | "withWaitUntil" | "onWarmerEvent" | "onStart";
77
87
  export interface RoutingResult {
78
88
  internalEvent: InternalEvent;
79
89
  isExternalRewrite: boolean;
@@ -212,6 +222,7 @@ export interface FunctionOptions extends DefaultFunctionOptions {
212
222
  * @deprecated This is not supported in 14.2+
213
223
  */
214
224
  experimentalBundledNextServer?: boolean;
225
+ routePreloadingBehavior?: RoutePreloadingBehavior;
215
226
  }
216
227
  export type RouteTemplate = `app/${string}/route` | `app/${string}/page` | `app/page` | `app/route` | `pages/${string}`;
217
228
  export interface SplittedFunctionOptions extends FunctionOptions {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.5.4",
6
+ "version": "3.5.6",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },