@opennextjs/aws 3.8.1 → 3.8.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
@@ -98,7 +98,28 @@ To run `OpenNext` locally:
98
98
 
99
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
100
 
101
- ### Coldstart
101
+ ## Testing
102
+
103
+ You can run unit tests with
104
+
105
+ ```bash
106
+ pnpm test
107
+ ```
108
+
109
+ You can tun e2e locally with:
110
+
111
+ ```bash
112
+ pnpm -r openbuild:local
113
+ pnpm -r openbuild:local:start
114
+ ```
115
+
116
+ And in a different shell:
117
+
118
+ ```bash
119
+ pnpm e2e:test
120
+ ```
121
+
122
+ ## Coldstart
102
123
 
103
124
  OpenNext provide you with a warmer function that can be used to reduce cold start.
104
125
 
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import logger from "../logger.js";
5
6
  import { openNextReplacementPlugin } from "../plugins/replacement.js";
@@ -74,10 +75,14 @@ export async function createImageOptimizationBundle(options) {
74
75
  // Target should be same as used by Lambda, see https://github.com/sst/sst/blob/ca6f763fdfddd099ce2260202d0ce48c72e211ea/packages/sst/src/constructs/NextjsSite.ts#L114
75
76
  // For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda
76
77
  const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6";
78
+ // In development, we want to use the local machine settings
79
+ const isDev = config.imageOptimization?.loader === "fs-dev";
77
80
  installDependencies(outputPath, config.imageOptimization?.install ?? {
78
81
  packages: [`sharp@${sharpVersion}`],
79
- // By not specifying an arch, `npm install` will choose one for us (i.e. our system one)
80
- arch: config.imageOptimization?.loader === "fs-dev" ? undefined : "arm64",
82
+ // By not specifying an arch in dev, `npm install` will choose one for us (i.e. our system one)
83
+ arch: isDev ? undefined : "arm64",
84
+ // Use the local platform in dev
85
+ os: isDev ? os.platform() : "linux",
81
86
  nodeVersion: "18",
82
87
  libc: "glibc",
83
88
  });
@@ -24,8 +24,9 @@ export function installDependencies(outputDir, installOptions) {
24
24
  const libcOption = installOptions.libc
25
25
  ? `--libc=${installOptions.libc}`
26
26
  : "";
27
+ const osOption = `--os=${installOptions.os ?? "linux"}`;
27
28
  const additionalArgs = installOptions.additionalArgs ?? "";
28
- const installCommand = `npm install --platform=linux ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
29
+ const installCommand = `npm install ${osOption} ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
29
30
  execSync(installCommand, {
30
31
  stdio: "pipe",
31
32
  cwd: tempInstallDir,
@@ -63,8 +63,8 @@ function getStaticAPIRoutes() {
63
63
  .map(createRouteDefinition);
64
64
  // We filter out both static API and page routes from the app paths manifest
65
65
  const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest)
66
- .filter((route) => route.startsWith("/api/") ||
67
- (route === "/api" && !dynamicRoutePages.has(route)))
66
+ .filter((route) => (route.startsWith("/api/") || route === "/api") &&
67
+ !dynamicRoutePages.has(route))
68
68
  .map(createRouteDefinition);
69
69
  return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes];
70
70
  }
@@ -126,6 +126,8 @@ export function convertToQueryString(query) {
126
126
  * @__PURE__
127
127
  */
128
128
  export function convertToQuery(querystring) {
129
+ if (!querystring)
130
+ return {};
129
131
  const query = new URLSearchParams(querystring);
130
132
  const queryObject = {};
131
133
  for (const key of query.keys()) {
@@ -177,13 +179,12 @@ export function convertBodyToReadableStream(method, body) {
177
179
  return undefined;
178
180
  if (!body)
179
181
  return undefined;
180
- const readable = new ReadableStream({
182
+ return new ReadableStream({
181
183
  start(controller) {
182
184
  controller.enqueue(body);
183
185
  controller.close();
184
186
  },
185
187
  });
186
- return readable;
187
188
  }
188
189
  var CommonHeaders;
189
190
  (function (CommonHeaders) {
@@ -47,8 +47,10 @@ function normalizeAPIGatewayProxyEventV2Headers(event) {
47
47
  if (Array.isArray(cookies)) {
48
48
  headers.cookie = cookies.join("; ");
49
49
  }
50
- for (const [key, value] of Object.entries(rawHeaders || {})) {
51
- headers[key.toLowerCase()] = value;
50
+ if (rawHeaders) {
51
+ for (const [key, value] of Object.entries(rawHeaders)) {
52
+ headers[key.toLowerCase()] = value;
53
+ }
52
54
  }
53
55
  return headers;
54
56
  }
@@ -48,9 +48,10 @@ const cloudfrontReadOnlyHeaders = [
48
48
  function normalizeCloudFrontRequestEventHeaders(rawHeaders) {
49
49
  const headers = {};
50
50
  for (const [key, values] of Object.entries(rawHeaders)) {
51
+ const lowerKey = key.toLowerCase();
51
52
  for (const { value } of values) {
52
53
  if (value) {
53
- headers[key.toLowerCase()] = value;
54
+ headers[lowerKey] = value;
54
55
  }
55
56
  }
56
57
  }
@@ -9,8 +9,6 @@ const converter = {
9
9
  const url = new URL(event.url);
10
10
  const searchParams = url.searchParams;
11
11
  const query = getQueryFromSearchParams(searchParams);
12
- // Transform body into Buffer
13
- const body = await event.arrayBuffer();
14
12
  const headers = {};
15
13
  event.headers.forEach((value, key) => {
16
14
  headers[key] = value;
@@ -18,6 +16,10 @@ const converter = {
18
16
  const rawPath = url.pathname;
19
17
  const method = event.method;
20
18
  const shouldHaveBody = method !== "GET" && method !== "HEAD";
19
+ // Only read body for methods that should have one
20
+ const body = shouldHaveBody
21
+ ? Buffer.from(await event.arrayBuffer())
22
+ : undefined;
21
23
  const cookieHeader = event.headers.get("cookie");
22
24
  const cookies = cookieHeader
23
25
  ? cookieParser.parse(cookieHeader)
@@ -27,7 +29,7 @@ const converter = {
27
29
  method,
28
30
  rawPath,
29
31
  url: event.url,
30
- body: shouldHaveBody ? Buffer.from(body) : undefined,
32
+ body,
31
33
  headers,
32
34
  remoteAddress: event.headers.get("x-forwarded-for") ?? "::1",
33
35
  query,
@@ -1,31 +1,61 @@
1
1
  import { debug, error } from "../../adapters/logger";
2
+ // Cache parsed origins and compiled patterns at module level
3
+ let cachedOrigins;
4
+ const cachedPatterns = [];
5
+ let initialized = false;
6
+ /**
7
+ * Initializes the cached values on the first execution
8
+ */
9
+ function initializeOnce() {
10
+ if (initialized)
11
+ return;
12
+ // Parse origin JSON once
13
+ cachedOrigins = JSON.parse(process.env.OPEN_NEXT_ORIGIN ?? "{}");
14
+ // Pre-compile all regex patterns
15
+ const functions = globalThis.openNextConfig.functions ?? {};
16
+ for (const key in functions) {
17
+ if (key !== "default") {
18
+ const value = functions[key];
19
+ const regexes = [];
20
+ for (const pattern of value.patterns) {
21
+ // Convert cloudfront pattern to regex
22
+ const regexPattern = `/${pattern
23
+ .replace(/\*\*/g, "(.*)")
24
+ .replace(/\*/g, "([^/]*)")
25
+ .replace(/\//g, "\\/")
26
+ .replace(/\?/g, ".")}`;
27
+ regexes.push(new RegExp(regexPattern));
28
+ }
29
+ cachedPatterns.push({
30
+ key,
31
+ patterns: value.patterns,
32
+ regexes,
33
+ });
34
+ }
35
+ }
36
+ initialized = true;
37
+ }
2
38
  const envLoader = {
3
39
  name: "env",
4
40
  resolve: async (_path) => {
5
41
  try {
6
- const origin = JSON.parse(process.env.OPEN_NEXT_ORIGIN ?? "{}");
7
- for (const [key, value] of Object.entries(globalThis.openNextConfig.functions ?? {}).filter(([key]) => key !== "default")) {
8
- if (value.patterns.some((pattern) => {
9
- // Convert cloudfront pattern to regex
10
- return new RegExp(
11
- // transform glob pattern to regex
12
- `/${pattern
13
- .replace(/\*\*/g, "(.*)")
14
- .replace(/\*/g, "([^/]*)")
15
- .replace(/\//g, "\\/")
16
- .replace(/\?/g, ".")}`).test(_path);
17
- })) {
18
- debug("Using origin", key, value.patterns);
19
- return origin[key];
42
+ initializeOnce();
43
+ // Test against pre-compiled patterns
44
+ for (const { key, patterns, regexes } of cachedPatterns) {
45
+ for (const regex of regexes) {
46
+ if (regex.test(_path)) {
47
+ debug("Using origin", key, patterns);
48
+ return cachedOrigins[key];
49
+ }
20
50
  }
21
51
  }
22
- if (_path.startsWith("/_next/image") && origin.imageOptimizer) {
52
+ if (_path.startsWith("/_next/image") && cachedOrigins.imageOptimizer) {
23
53
  debug("Using origin", "imageOptimizer", _path);
24
- return origin.imageOptimizer;
54
+ return cachedOrigins.imageOptimizer;
25
55
  }
26
- if (origin.default) {
27
- debug("Using default origin", origin.default, _path);
28
- return origin.default;
56
+ if (cachedOrigins.default) {
57
+ debug("Using default origin", cachedOrigins.default, _path);
58
+ return cachedOrigins.default;
29
59
  }
30
60
  return false;
31
61
  }
@@ -25,18 +25,50 @@ const handler = async (handler, converter) => async (request, env, ctx, abortSig
25
25
  if (url.hostname === "localhost") {
26
26
  responseHeaders.set("Content-Encoding", "identity");
27
27
  }
28
- const { readable, writable } = new TransformStream({
29
- transform(chunk, controller) {
30
- controller.enqueue(Uint8Array.from(chunk.chunk ?? chunk));
28
+ // Optimize: skip ReadableStream creation for null body statuses
29
+ if (NULL_BODY_STATUSES.has(statusCode)) {
30
+ const response = new Response(null, {
31
+ status: statusCode,
32
+ headers: responseHeaders,
33
+ });
34
+ resolveResponse(response);
35
+ // Return a no-op Writable that discards all data
36
+ return new Writable({
37
+ write(chunk, encoding, callback) {
38
+ callback();
39
+ },
40
+ });
41
+ }
42
+ let controller;
43
+ const readable = new ReadableStream({
44
+ start(c) {
45
+ controller = c;
31
46
  },
32
47
  });
33
- const body = NULL_BODY_STATUSES.has(statusCode) ? null : readable;
34
- const response = new Response(body, {
48
+ const response = new Response(readable, {
35
49
  status: statusCode,
36
50
  headers: responseHeaders,
37
51
  });
38
52
  resolveResponse(response);
39
- return Writable.fromWeb(writable);
53
+ return new Writable({
54
+ write(chunk, encoding, callback) {
55
+ controller.enqueue(chunk);
56
+ callback();
57
+ },
58
+ final(callback) {
59
+ controller.close();
60
+ callback();
61
+ },
62
+ destroy(error, callback) {
63
+ if (error) {
64
+ controller.error(error);
65
+ }
66
+ else {
67
+ controller.close();
68
+ }
69
+ callback(error);
70
+ },
71
+ });
40
72
  },
41
73
  // This is for passing along the original abort signal from the initial Request you retrieve in your worker
42
74
  // Ensures that the response we pass to NextServer is aborted if the request is aborted
@@ -190,18 +190,14 @@ export interface InstallOptions {
190
190
  * ```
191
191
  */
192
192
  packages: string[];
193
- /**
194
- * @default undefined
195
- */
193
+ /** @default undefined */
196
194
  arch?: "x64" | "arm64";
197
- /**
198
- * @default undefined
199
- */
195
+ /** @default undefined */
200
196
  nodeVersion?: string;
201
- /**
202
- * @default undefined
203
- */
197
+ /** @default undefined */
204
198
  libc?: "glibc" | "musl";
199
+ /** @default "linux" */
200
+ os?: string;
205
201
  /**
206
202
  * @default undefined
207
203
  * Additional arguments to pass to the install command (i.e. npm install)
@@ -61,6 +61,6 @@ const commonBinaryMimeTypes = new Set([
61
61
  export function isBinaryContentType(contentType) {
62
62
  if (!contentType)
63
63
  return false;
64
- const value = contentType?.split(";")[0] ?? "";
64
+ const value = contentType.split(";")[0];
65
65
  return commonBinaryMimeTypes.has(value);
66
66
  }
@@ -17,7 +17,6 @@ export function customFetchClient(client) {
17
17
  * This is necessary otherwise we get some error : SocketError: other side closed
18
18
  * https://github.com/nodejs/undici/issues/583#issuecomment-855384858
19
19
  */
20
- const clonedResponse = response.clone();
21
- return clonedResponse;
20
+ return response.clone();
22
21
  };
23
22
  }
@@ -1,4 +1,4 @@
1
- import type { ReadableStream } from "node:stream/web";
1
+ import { ReadableStream } from "node:stream/web";
2
2
  export declare function fromReadableStream(stream: ReadableStream<Uint8Array>, base64?: boolean): Promise<string>;
3
3
  export declare function toReadableStream(value: string, isBase64?: boolean): ReadableStream;
4
4
  export declare function emptyReadableStream(): ReadableStream;
@@ -1,30 +1,49 @@
1
- import { Readable } from "node:stream";
2
- export function fromReadableStream(stream, base64) {
3
- const reader = stream.getReader();
1
+ import { ReadableStream } from "node:stream/web";
2
+ export async function fromReadableStream(stream, base64) {
4
3
  const chunks = [];
5
- return new Promise((resolve, reject) => {
6
- function pump() {
7
- reader
8
- .read()
9
- .then(({ done, value }) => {
10
- if (done) {
11
- resolve(Buffer.concat(chunks).toString(base64 ? "base64" : "utf8"));
12
- return;
13
- }
14
- chunks.push(value);
15
- pump();
16
- })
17
- .catch(reject);
18
- }
19
- pump();
20
- });
4
+ let totalLength = 0;
5
+ for await (const chunk of stream) {
6
+ chunks.push(chunk);
7
+ totalLength += chunk.length;
8
+ }
9
+ if (chunks.length === 0) {
10
+ return "";
11
+ }
12
+ if (chunks.length === 1) {
13
+ return Buffer.from(chunks[0]).toString(base64 ? "base64" : "utf8");
14
+ }
15
+ // Pre-allocate buffer with exact size to avoid reallocation
16
+ const buffer = Buffer.alloc(totalLength);
17
+ let offset = 0;
18
+ for (const chunk of chunks) {
19
+ buffer.set(chunk, offset);
20
+ offset += chunk.length;
21
+ }
22
+ return buffer.toString(base64 ? "base64" : "utf8");
21
23
  }
22
24
  export function toReadableStream(value, isBase64) {
23
- return Readable.toWeb(Readable.from(Buffer.from(value, isBase64 ? "base64" : "utf8")));
25
+ return new ReadableStream({
26
+ pull(controller) {
27
+ // Defer the Buffer.from conversion to when the stream is actually read.
28
+ controller.enqueue(Buffer.from(value, isBase64 ? "base64" : "utf8"));
29
+ controller.close();
30
+ },
31
+ }, { highWaterMark: 0 });
24
32
  }
33
+ let maybeSomethingBuffer;
25
34
  export function emptyReadableStream() {
26
35
  if (process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true") {
27
- return Readable.toWeb(Readable.from([Buffer.from("SOMETHING")]));
36
+ return new ReadableStream({
37
+ pull(controller) {
38
+ maybeSomethingBuffer ??= Buffer.from("SOMETHING");
39
+ controller.enqueue(maybeSomethingBuffer);
40
+ controller.close();
41
+ },
42
+ }, { highWaterMark: 0 });
28
43
  }
29
- return Readable.toWeb(Readable.from([]));
44
+ return new ReadableStream({
45
+ start(controller) {
46
+ controller.close();
47
+ },
48
+ });
30
49
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.8.1",
6
+ "version": "3.8.3",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },