@opennextjs/aws 3.8.0 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/build/createImageOptimizationBundle.js +7 -2
- package/dist/build/installDeps.js +2 -1
- package/dist/core/routing/util.d.ts +1 -0
- package/dist/core/routing/util.js +11 -6
- package/dist/http/openNextResponse.d.ts +3 -2
- package/dist/http/openNextResponse.js +16 -9
- package/dist/http/util.js +21 -1
- package/dist/overrides/wrappers/cloudflare-node.js +2 -0
- package/dist/types/open-next.d.ts +17 -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,
|
|
@@ -3,6 +3,7 @@ import type { IncomingMessage } from "../../http/index.js";
|
|
|
3
3
|
import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
|
|
4
4
|
import type { FunctionsConfigManifest, MiddlewareManifest } from "../../types/next-types";
|
|
5
5
|
import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "../../types/open-next.js";
|
|
6
|
+
import { ReadableStream } from "node:stream/web";
|
|
6
7
|
/**
|
|
7
8
|
*
|
|
8
9
|
* @__PURE__
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { parse as parseQs, stringify as stringifyQs } from "node:querystring";
|
|
3
|
-
import { Readable } from "node:stream";
|
|
4
3
|
import { BuildId, HtmlPages, NextConfig } from "../../adapters/config/index.js";
|
|
5
4
|
import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
|
|
6
5
|
import { getQueryFromIterator, parseHeaders } from "../../http/util.js";
|
|
6
|
+
import { ReadableStream } from "node:stream/web";
|
|
7
7
|
import { debug, error } from "../../adapters/logger.js";
|
|
8
8
|
import { isBinaryContentType } from "../../utils/binary.js";
|
|
9
9
|
import { localizePath } from "./i18n/index.js";
|
|
@@ -84,9 +84,15 @@ export function convertRes(res) {
|
|
|
84
84
|
const headers = parseHeaders(res.getFixedHeaders());
|
|
85
85
|
const isBase64Encoded = isBinaryContentType(headers["content-type"]) ||
|
|
86
86
|
!!headers["content-encoding"];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const body = new ReadableStream({
|
|
88
|
+
pull(controller) {
|
|
89
|
+
if (!res._chunks || res._chunks.length === 0) {
|
|
90
|
+
controller.close();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
controller.enqueue(res._chunks.shift());
|
|
94
|
+
},
|
|
95
|
+
});
|
|
90
96
|
return {
|
|
91
97
|
type: "core",
|
|
92
98
|
statusCode,
|
|
@@ -171,13 +177,12 @@ export function convertBodyToReadableStream(method, body) {
|
|
|
171
177
|
return undefined;
|
|
172
178
|
if (!body)
|
|
173
179
|
return undefined;
|
|
174
|
-
|
|
180
|
+
return new ReadableStream({
|
|
175
181
|
start(controller) {
|
|
176
182
|
controller.enqueue(body);
|
|
177
183
|
controller.close();
|
|
178
184
|
},
|
|
179
185
|
});
|
|
180
|
-
return readable;
|
|
181
186
|
}
|
|
182
187
|
var CommonHeaders;
|
|
183
188
|
(function (CommonHeaders) {
|
|
@@ -11,10 +11,11 @@ export declare class OpenNextNodeResponse extends Transform implements ServerRes
|
|
|
11
11
|
statusCode: number;
|
|
12
12
|
statusMessage: string;
|
|
13
13
|
headers: OutgoingHttpHeaders;
|
|
14
|
-
private _cookies;
|
|
15
|
-
private responseStream?;
|
|
16
14
|
headersSent: boolean;
|
|
17
15
|
_chunks: Buffer[];
|
|
16
|
+
private _cookies;
|
|
17
|
+
private responseStream?;
|
|
18
|
+
private bodyLength;
|
|
18
19
|
strictContentLength: boolean;
|
|
19
20
|
assignSocket(_socket: Socket): void;
|
|
20
21
|
detachSocket(_socket: Socket): void;
|
|
@@ -12,10 +12,11 @@ export class OpenNextNodeResponse extends Transform {
|
|
|
12
12
|
statusCode;
|
|
13
13
|
statusMessage = "";
|
|
14
14
|
headers = {};
|
|
15
|
-
_cookies = [];
|
|
16
|
-
responseStream;
|
|
17
15
|
headersSent = false;
|
|
18
16
|
_chunks = [];
|
|
17
|
+
_cookies = [];
|
|
18
|
+
responseStream;
|
|
19
|
+
bodyLength = 0;
|
|
19
20
|
// To comply with the ServerResponse interface :
|
|
20
21
|
strictContentLength = false;
|
|
21
22
|
assignSocket(_socket) {
|
|
@@ -221,7 +222,12 @@ export class OpenNextNodeResponse extends Transform {
|
|
|
221
222
|
return Buffer.concat(this._chunks);
|
|
222
223
|
}
|
|
223
224
|
_internalWrite(chunk, encoding) {
|
|
224
|
-
|
|
225
|
+
const buffer = Buffer.from(chunk, encoding);
|
|
226
|
+
this.bodyLength += buffer.length;
|
|
227
|
+
if (this.streamCreator?.retainChunks !== false) {
|
|
228
|
+
// Avoid keeping chunks around when the `StreamCreator` supports it to save memory
|
|
229
|
+
this._chunks.push(buffer);
|
|
230
|
+
}
|
|
225
231
|
this.push(chunk, encoding);
|
|
226
232
|
this.streamCreator?.onWrite?.();
|
|
227
233
|
}
|
|
@@ -237,19 +243,18 @@ export class OpenNextNodeResponse extends Transform {
|
|
|
237
243
|
this.flushHeaders();
|
|
238
244
|
}
|
|
239
245
|
// In some cases we might not have a store i.e. for example in the image optimization function
|
|
240
|
-
// We may want to reconsider this in the future, it might be
|
|
246
|
+
// We may want to reconsider this in the future, it might be interesting to have access to this store everywhere
|
|
241
247
|
globalThis.__openNextAls
|
|
242
248
|
?.getStore()
|
|
243
249
|
?.pendingPromiseRunner.add(this.onEnd(this.headers));
|
|
244
|
-
|
|
245
|
-
this.streamCreator?.onFinish?.(bodyLength);
|
|
250
|
+
this.streamCreator?.onFinish?.(this.bodyLength);
|
|
246
251
|
//This is only here because of aws broken streaming implementation.
|
|
247
252
|
//Hopefully one day they will be able to give us a working streaming implementation in lambda for everyone
|
|
248
253
|
//If you're lucky you have a working streaming implementation in your aws account and don't need this
|
|
249
254
|
//If not you can set the OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE env variable to true
|
|
250
255
|
//BE CAREFUL: Aws keeps rolling out broken streaming implementations even on accounts that had working ones before
|
|
251
256
|
//This is not dependent on the node runtime used
|
|
252
|
-
if (bodyLength === 0 &&
|
|
257
|
+
if (this.bodyLength === 0 &&
|
|
253
258
|
// We use an env variable here because not all aws account have the same behavior
|
|
254
259
|
// On some aws accounts the response will hang if the body is empty
|
|
255
260
|
// We are modifying the response body here, this is not a good practice
|
|
@@ -283,8 +288,10 @@ export class OpenNextNodeResponse extends Transform {
|
|
|
283
288
|
return (Array.isArray(values) ? values : [values]).map((value) => value.toString());
|
|
284
289
|
}
|
|
285
290
|
send() {
|
|
286
|
-
const
|
|
287
|
-
|
|
291
|
+
for (const chunk of this._chunks) {
|
|
292
|
+
this.write(chunk);
|
|
293
|
+
}
|
|
294
|
+
this.end();
|
|
288
295
|
}
|
|
289
296
|
body(value) {
|
|
290
297
|
this.write(value);
|
package/dist/http/util.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logger from "../logger";
|
|
1
2
|
export const parseHeaders = (headers) => {
|
|
2
3
|
const result = {};
|
|
3
4
|
if (!headers) {
|
|
@@ -7,7 +8,26 @@ export const parseHeaders = (headers) => {
|
|
|
7
8
|
if (value === undefined) {
|
|
8
9
|
continue;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
const keyLower = key.toLowerCase();
|
|
12
|
+
/**
|
|
13
|
+
* Next can return an Array for the Location header when you return null from a get in the cacheHandler on a page that has a redirect()
|
|
14
|
+
* We dont want to merge that into a comma-separated string
|
|
15
|
+
* If they are the same just return one of them
|
|
16
|
+
* Otherwise return the last one
|
|
17
|
+
* See: https://github.com/opennextjs/opennextjs-cloudflare/issues/875#issuecomment-3258248276
|
|
18
|
+
* and https://github.com/opennextjs/opennextjs-aws/pull/977#issuecomment-3261763114
|
|
19
|
+
*/
|
|
20
|
+
if (keyLower === "location" && Array.isArray(value)) {
|
|
21
|
+
if (value[0] === value[1]) {
|
|
22
|
+
result[keyLower] = value[0];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger.warn("Multiple different values for Location header found. Using the last one");
|
|
26
|
+
result[keyLower] = value[value.length - 1];
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
result[keyLower] = convertHeader(value);
|
|
11
31
|
}
|
|
12
32
|
return result;
|
|
13
33
|
};
|
|
@@ -42,6 +42,8 @@ const handler = async (handler, converter) => async (request, env, ctx, abortSig
|
|
|
42
42
|
// Ensures that the response we pass to NextServer is aborted if the request is aborted
|
|
43
43
|
// By doing this `request.signal.onabort` will work in route handlers
|
|
44
44
|
abortSignal: abortSignal,
|
|
45
|
+
// There is no need to retain the chunks that were pushed to the response stream.
|
|
46
|
+
retainChunks: false,
|
|
45
47
|
};
|
|
46
48
|
ctx.waitUntil(handler(internalEvent, {
|
|
47
49
|
streamCreator,
|
|
@@ -36,6 +36,18 @@ export interface StreamCreator {
|
|
|
36
36
|
onWrite?: () => void;
|
|
37
37
|
onFinish?: (length: number) => void;
|
|
38
38
|
abortSignal?: AbortSignal;
|
|
39
|
+
/**
|
|
40
|
+
* Normally there is no need to retain the chunks that have been pushed to the response stream.
|
|
41
|
+
*
|
|
42
|
+
* However some implementations use a fake `StreamCreator` and expect the chunks to be retained.
|
|
43
|
+
* When your stream controller implementation doesn't need to retain the chunk, you can set this
|
|
44
|
+
* to `false` to reduce memory usage.
|
|
45
|
+
*
|
|
46
|
+
* @see https://github.com/opennextjs/opennextjs-aws/blob/main/packages/open-next/src/overrides/wrappers/aws-lambda.ts
|
|
47
|
+
*
|
|
48
|
+
* @default true for backward compatibility.
|
|
49
|
+
*/
|
|
50
|
+
retainChunks?: boolean;
|
|
39
51
|
}
|
|
40
52
|
export type WaitUntil = (promise: Promise<void>) => void;
|
|
41
53
|
export interface DangerousOptions {
|
|
@@ -178,18 +190,14 @@ export interface InstallOptions {
|
|
|
178
190
|
* ```
|
|
179
191
|
*/
|
|
180
192
|
packages: string[];
|
|
181
|
-
/**
|
|
182
|
-
* @default undefined
|
|
183
|
-
*/
|
|
193
|
+
/** @default undefined */
|
|
184
194
|
arch?: "x64" | "arm64";
|
|
185
|
-
/**
|
|
186
|
-
* @default undefined
|
|
187
|
-
*/
|
|
195
|
+
/** @default undefined */
|
|
188
196
|
nodeVersion?: string;
|
|
189
|
-
/**
|
|
190
|
-
* @default undefined
|
|
191
|
-
*/
|
|
197
|
+
/** @default undefined */
|
|
192
198
|
libc?: "glibc" | "musl";
|
|
199
|
+
/** @default "linux" */
|
|
200
|
+
os?: string;
|
|
193
201
|
/**
|
|
194
202
|
* @default undefined
|
|
195
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
|
}
|