@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 +22 -1
- package/dist/build/createImageOptimizationBundle.js +7 -2
- package/dist/build/installDeps.js +2 -1
- package/dist/core/routing/routeMatcher.js +2 -2
- package/dist/core/routing/util.js +3 -2
- package/dist/overrides/converters/aws-apigw-v2.js +4 -2
- package/dist/overrides/converters/aws-cloudfront.js +2 -1
- package/dist/overrides/converters/edge.js +5 -3
- package/dist/overrides/originResolver/pattern-env.js +49 -19
- package/dist/overrides/wrappers/cloudflare-node.js +38 -6
- package/dist/types/open-next.d.ts +5 -9
- package/dist/utils/binary.js +1 -1
- package/dist/utils/fetch.js +1 -2
- package/dist/utils/stream.d.ts +1 -1
- package/dist/utils/stream.js +41 -22
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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[
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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") &&
|
|
52
|
+
if (_path.startsWith("/_next/image") && cachedOrigins.imageOptimizer) {
|
|
23
53
|
debug("Using origin", "imageOptimizer", _path);
|
|
24
|
-
return
|
|
54
|
+
return cachedOrigins.imageOptimizer;
|
|
25
55
|
}
|
|
26
|
-
if (
|
|
27
|
-
debug("Using default origin",
|
|
28
|
-
return
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
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)
|
package/dist/utils/binary.js
CHANGED
|
@@ -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
|
|
64
|
+
const value = contentType.split(";")[0];
|
|
65
65
|
return commonBinaryMimeTypes.has(value);
|
|
66
66
|
}
|
package/dist/utils/fetch.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
return clonedResponse;
|
|
20
|
+
return response.clone();
|
|
22
21
|
};
|
|
23
22
|
}
|
package/dist/utils/stream.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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;
|
package/dist/utils/stream.js
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
44
|
+
return new ReadableStream({
|
|
45
|
+
start(controller) {
|
|
46
|
+
controller.close();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
30
49
|
}
|