@opennextjs/aws 4.0.2 → 4.1.0
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/dist/adapters/plugins/image-optimization/image-optimization.replacement.js +51 -35
- package/dist/build/generateOutput.js +2 -1
- package/dist/build/validateConfig.js +1 -0
- package/dist/core/edgeFunctionHandler.js +16 -1
- package/dist/core/routing/cacheInterceptor.js +18 -14
- package/dist/core/routing/matcher.js +4 -0
- package/dist/core/routing/middleware.js +11 -1
- package/dist/overrides/wrappers/aws-apigw-streaming.d.ts +7 -0
- package/dist/overrides/wrappers/aws-apigw-streaming.js +74 -0
- package/dist/types/open-next.d.ts +1 -1
- package/package.json +2 -2
|
@@ -5,46 +5,62 @@ import { debug } from "../../logger.js";
|
|
|
5
5
|
//#override optimizeImage
|
|
6
6
|
export async function optimizeImage(headers, imageParams, nextConfig, handleRequest) {
|
|
7
7
|
const { isAbsolute, href } = imageParams;
|
|
8
|
-
// Signature for the fetch Image functions have changed across Next.js versions.
|
|
9
|
-
const isV15After15510 = compareSemver(globalThis.nextVersion, ">=", "15.5.10") &&
|
|
10
|
-
compareSemver(globalThis.nextVersion, "<", "16");
|
|
11
|
-
const isV15After15515 = compareSemver(globalThis.nextVersion, ">=", "15.5.16") &&
|
|
12
|
-
compareSemver(globalThis.nextVersion, "<", "16");
|
|
13
|
-
const isV16Plus = compareSemver(globalThis.nextVersion, ">=", "16");
|
|
14
|
-
const isAfter1615 = compareSemver(globalThis.nextVersion, ">=", "16.1.5");
|
|
15
|
-
const isAfter1625 = compareSemver(globalThis.nextVersion, ">=", "16.2.5");
|
|
16
|
-
// fetchInternalImage signature varies across Next.js versions:
|
|
17
|
-
// <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest)
|
|
18
|
-
// v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest)
|
|
19
|
-
const isNewArgsForInternalFetch = isAfter1625 || isV15After15515;
|
|
20
|
-
// fetchExternalImage signature varies across Next.js versions:
|
|
21
|
-
// <=v15.5.9: fetchExternalImage(href)
|
|
22
|
-
// v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody)
|
|
23
|
-
// v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP)
|
|
24
|
-
// v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody)
|
|
25
8
|
// The default value for maximumResponseBody is 50KB in Next code
|
|
26
9
|
// https://github.com/vercel/next.js/blob/0f38c522/packages/next/src/shared/lib/image-config.ts#L164
|
|
27
10
|
const maximumResponseBody = nextConfig.images?.maximumResponseBody || 50_000_000;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
11
|
+
/**
|
|
12
|
+
* fetchExternalImage signature varies across Next.js versions:
|
|
13
|
+
* <=v15.5.9: fetchExternalImage(href)
|
|
14
|
+
* v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody)
|
|
15
|
+
* v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP)
|
|
16
|
+
* v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody)
|
|
17
|
+
*
|
|
18
|
+
* https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711
|
|
19
|
+
*/
|
|
20
|
+
const callFetchExternalImage = () => {
|
|
21
|
+
const version = globalThis.nextVersion;
|
|
22
|
+
// v16.1.5+
|
|
23
|
+
if (compareSemver(version, ">=", "16.1.5")) {
|
|
24
|
+
return fetchExternalImage(href, false, maximumResponseBody);
|
|
25
|
+
}
|
|
26
|
+
// v16.0.0–v16.1.4
|
|
27
|
+
if (compareSemver(version, ">=", "16")) {
|
|
28
|
+
// @ts-expect-error - fetchExternalImage signature varies across Next.js versions
|
|
29
|
+
return fetchExternalImage(href, false);
|
|
30
|
+
}
|
|
31
|
+
// v15.5.10–v15.x
|
|
32
|
+
if (compareSemver(version, ">=", "15.5.10")) {
|
|
33
|
+
// @ts-expect-error - fetchExternalImage signature varies across Next.js versions
|
|
34
|
+
return fetchExternalImage(href, maximumResponseBody);
|
|
35
|
+
}
|
|
36
|
+
// <=v15.5.9
|
|
37
|
+
// @ts-expect-error - fetchExternalImage signature varies across Next.js versions
|
|
38
|
+
return fetchExternalImage(href);
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* fetchInternalImage signature varies across Next.js versions:
|
|
42
|
+
* <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest)
|
|
43
|
+
* v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest)
|
|
44
|
+
*/
|
|
45
|
+
const callFetchInternalImage = () => {
|
|
46
|
+
const version = globalThis.nextVersion;
|
|
47
|
+
const isV15WithNewArgs = compareSemver(version, ">=", "15.5.16") &&
|
|
48
|
+
compareSemver(version, "<", "16");
|
|
49
|
+
// v15.5.16–v15.x, v16.2.5+
|
|
50
|
+
if (isV15WithNewArgs || compareSemver(version, ">=", "16.2.5")) {
|
|
51
|
+
return fetchInternalImage(href,
|
|
42
52
|
// @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used.
|
|
43
53
|
{ headers }, {}, // res object is not necessary as it's not actually used.
|
|
44
|
-
maximumResponseBody, handleRequest)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
maximumResponseBody, handleRequest);
|
|
55
|
+
}
|
|
56
|
+
// <=v15.5.15, v16.0.0–v16.2.4
|
|
57
|
+
// @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5
|
|
58
|
+
return fetchInternalImage(href, { headers }, {}, // res object is not necessary as it's not actually used.
|
|
59
|
+
handleRequest);
|
|
60
|
+
};
|
|
61
|
+
const imageUpstream = isAbsolute
|
|
62
|
+
? await callFetchExternalImage()
|
|
63
|
+
: await callFetchInternalImage();
|
|
48
64
|
const result = await imageOptimizer(imageUpstream, imageParams,
|
|
49
65
|
// @ts-ignore
|
|
50
66
|
nextConfig, false);
|
|
@@ -8,7 +8,8 @@ async function canStream(opts) {
|
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
10
10
|
if (typeof opts.override.wrapper === "string") {
|
|
11
|
-
return opts.override.wrapper === "aws-lambda-streaming"
|
|
11
|
+
return (opts.override.wrapper === "aws-lambda-streaming" ||
|
|
12
|
+
opts.override.wrapper === "aws-apigw-streaming");
|
|
12
13
|
}
|
|
13
14
|
const wrapper = await opts.override.wrapper();
|
|
14
15
|
return wrapper.supportStreaming;
|
|
@@ -8,6 +8,7 @@ const compatibilityMatrix = {
|
|
|
8
8
|
],
|
|
9
9
|
"aws-lambda-compressed": ["aws-apigw-v2"],
|
|
10
10
|
"aws-lambda-streaming": ["aws-apigw-v2"],
|
|
11
|
+
"aws-apigw-streaming": ["aws-apigw-v1", "aws-apigw-v2"],
|
|
11
12
|
cloudflare: ["edge"],
|
|
12
13
|
"cloudflare-edge": ["edge"],
|
|
13
14
|
"cloudflare-node": ["edge"],
|
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
export default async function edgeFunctionHandler(request) {
|
|
3
3
|
const path = new URL(request.url).pathname;
|
|
4
4
|
const routes = globalThis._ROUTES;
|
|
5
|
-
|
|
5
|
+
let decodedPath;
|
|
6
|
+
try {
|
|
7
|
+
// Decode the complete pathname atomically so a malformed escape cannot be
|
|
8
|
+
// partially decoded.
|
|
9
|
+
decodedPath = decodeURIComponent(path);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// A malformed pathname cannot provide a decoded route to match.
|
|
13
|
+
}
|
|
14
|
+
// Also match against the decoded pathname, while keeping the encoded URL in
|
|
15
|
+
// the request passed to middleware.
|
|
16
|
+
const correspondingRoute = routes.find((route) => route.regex.some((r) => {
|
|
17
|
+
const regex = new RegExp(r);
|
|
18
|
+
return (regex.test(path) ||
|
|
19
|
+
(decodedPath !== undefined && regex.test(decodedPath)));
|
|
20
|
+
}));
|
|
6
21
|
if (!correspondingRoute) {
|
|
7
22
|
throw new Error(`No route found for ${request.url}`);
|
|
8
23
|
}
|
|
@@ -169,19 +169,13 @@ function escapePathDelimiters(segment, escapeEncoded) {
|
|
|
169
169
|
*
|
|
170
170
|
* SSG cache key needs to be decoded, but some characters needs to be properly escaped
|
|
171
171
|
* https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
|
|
172
|
+
* Decoding must be atomic, as in Next.js. Partially decoding a malformed path
|
|
173
|
+
* could select a different cache route than the one evaluated by middleware.
|
|
172
174
|
*/
|
|
173
175
|
function decodePathParams(pathname) {
|
|
174
176
|
return pathname
|
|
175
177
|
.split("/")
|
|
176
|
-
.map((segment) =>
|
|
177
|
-
try {
|
|
178
|
-
return escapePathDelimiters(decodeURIComponent(segment), true);
|
|
179
|
-
}
|
|
180
|
-
catch (e) {
|
|
181
|
-
// If decodeURIComponent fails, we return the original segment
|
|
182
|
-
return segment;
|
|
183
|
-
}
|
|
184
|
-
})
|
|
178
|
+
.map((segment) => escapePathDelimiters(decodeURIComponent(segment), true))
|
|
185
179
|
.join("/");
|
|
186
180
|
}
|
|
187
181
|
export async function cacheInterceptor(event) {
|
|
@@ -205,14 +199,24 @@ export async function cacheInterceptor(event) {
|
|
|
205
199
|
// We also need to remove trailing slash
|
|
206
200
|
localizedPath = localizedPath.replace(/\/$/, "");
|
|
207
201
|
// Then we decode the path params
|
|
208
|
-
|
|
202
|
+
try {
|
|
203
|
+
localizedPath = decodePathParams(localizedPath) || "/";
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Next.js rejects malformed path params. Do not let cache interception
|
|
207
|
+
// partially decode the path and select a different route identity.
|
|
208
|
+
return event;
|
|
209
|
+
}
|
|
210
|
+
// The route is keyed as `/` in the prerender manifest, but the generated
|
|
211
|
+
// cache asset for the app index route is uploaded as `/index`.
|
|
212
|
+
const cacheKey = localizedPath === "/" ? "/index" : localizedPath;
|
|
209
213
|
debug("Checking cache for", localizedPath, PrerenderManifest);
|
|
210
|
-
const isISR = Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath
|
|
214
|
+
const isISR = Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath) ||
|
|
211
215
|
Object.values(PrerenderManifest?.dynamicRoutes ?? {}).some((dr) => new RegExp(dr.routeRegex).test(localizedPath));
|
|
212
216
|
debug("isISR", isISR);
|
|
213
217
|
if (isISR) {
|
|
214
218
|
try {
|
|
215
|
-
const cachedData = await globalThis.incrementalCache.get(
|
|
219
|
+
const cachedData = await globalThis.incrementalCache.get(cacheKey);
|
|
216
220
|
debug("cached data in interceptor", cachedData);
|
|
217
221
|
if (!cachedData?.value) {
|
|
218
222
|
return event;
|
|
@@ -223,7 +227,7 @@ export async function cacheInterceptor(event) {
|
|
|
223
227
|
cachedData.value?.type === "route") {
|
|
224
228
|
const _hasBeenRevalidated = cachedData.shouldBypassTagCache
|
|
225
229
|
? false
|
|
226
|
-
: await hasBeenRevalidated(
|
|
230
|
+
: await hasBeenRevalidated(cacheKey, tags, cachedData);
|
|
227
231
|
if (_hasBeenRevalidated) {
|
|
228
232
|
return event;
|
|
229
233
|
}
|
|
@@ -231,7 +235,7 @@ export async function cacheInterceptor(event) {
|
|
|
231
235
|
// Check if the cache entry is stale (valid but needs background revalidation)
|
|
232
236
|
const _isStale = cachedData.shouldBypassTagCache
|
|
233
237
|
? false
|
|
234
|
-
: await isStale(
|
|
238
|
+
: await isStale(cacheKey, tags, cachedData.lastModified ?? Date.now());
|
|
235
239
|
const host = event.headers.host;
|
|
236
240
|
switch (cachedData?.value?.type) {
|
|
237
241
|
case "app":
|
|
@@ -287,6 +287,10 @@ export function fixDataPage(internalEvent, buildId) {
|
|
|
287
287
|
...internalEvent,
|
|
288
288
|
rawPath: newPath,
|
|
289
289
|
query,
|
|
290
|
+
headers: {
|
|
291
|
+
...internalEvent.headers,
|
|
292
|
+
"x-nextjs-data": "1",
|
|
293
|
+
},
|
|
290
294
|
url: new URL(`${newPath}${convertToQueryString(query)}`, internalEvent.url).href,
|
|
291
295
|
};
|
|
292
296
|
}
|
|
@@ -29,7 +29,17 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
|
|
|
29
29
|
return internalEvent;
|
|
30
30
|
// We only need the normalizedPath to check if the middleware should run
|
|
31
31
|
const normalizedPath = localizePath(internalEvent);
|
|
32
|
-
|
|
32
|
+
let decodedPath;
|
|
33
|
+
try {
|
|
34
|
+
decodedPath = decodeURIComponent(normalizedPath);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// An invalid encoding cannot provide a decoded pathname to match.
|
|
38
|
+
}
|
|
39
|
+
// Next.js also tries the decoded pathname for matching, while preserving the
|
|
40
|
+
// encoded pathname in the request passed to middleware.
|
|
41
|
+
const hasMatch = middleMatch.some((r) => r.test(normalizedPath) ||
|
|
42
|
+
(decodedPath !== undefined && r.test(decodedPath)));
|
|
33
43
|
if (!hasMatch)
|
|
34
44
|
return internalEvent;
|
|
35
45
|
const initialUrl = new URL(normalizedPath, internalEvent.url);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { debug } from "../../adapters/logger";
|
|
3
|
+
function formatWarmerResponse(event) {
|
|
4
|
+
const result = new Promise((resolve) => {
|
|
5
|
+
setTimeout(() => {
|
|
6
|
+
resolve({ serverId, type: "warmer" });
|
|
7
|
+
}, event.delay);
|
|
8
|
+
});
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Streaming wrapper for an API Gateway proxy integration in
|
|
13
|
+
* `ResponseTransferMode: STREAM`.
|
|
14
|
+
*
|
|
15
|
+
* Differs from `aws-lambda-streaming` (the Lambda **Function URL** wrapper) in
|
|
16
|
+
* two ways, both mandated by API Gateway response streaming
|
|
17
|
+
* (https://aws.amazon.com/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/):
|
|
18
|
+
*
|
|
19
|
+
* 1. It does NOT set the
|
|
20
|
+
* `application/vnd.awslambda.http-integration-response` content type. That
|
|
21
|
+
* marker is Function-URL-specific metadata; API Gateway forwards it onto
|
|
22
|
+
* the wire, corrupting HTTP framing.
|
|
23
|
+
* 2. It does NOT compress the response. API Gateway streaming explicitly does
|
|
24
|
+
* not support content encoding, so the body is always emitted as
|
|
25
|
+
* `identity`. Compression, if desired, is handled by the CDN in front
|
|
26
|
+
* (e.g. CloudFront).
|
|
27
|
+
*
|
|
28
|
+
* The response framing itself — a JSON metadata prelude followed by an
|
|
29
|
+
* 8-null-byte delimiter, then the payload — is identical to the Function URL
|
|
30
|
+
* format and is what API Gateway expects in STREAM mode.
|
|
31
|
+
*/
|
|
32
|
+
const handler = async (handler, converter) => awslambda.streamifyResponse(async (event, responseStream, context) => {
|
|
33
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
34
|
+
if ("type" in event) {
|
|
35
|
+
const result = await formatWarmerResponse(event);
|
|
36
|
+
responseStream.end(Buffer.from(JSON.stringify(result)), "utf-8");
|
|
37
|
+
await globalThis.__next_route_preloader("warmerEvent");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const internalEvent = await converter.convertFrom(event);
|
|
41
|
+
responseStream.on("error", (err) => {
|
|
42
|
+
debug(err);
|
|
43
|
+
responseStream.end();
|
|
44
|
+
});
|
|
45
|
+
const streamCreator = {
|
|
46
|
+
writeHeaders: (_prelude) => {
|
|
47
|
+
// No content-encoding header — API Gateway streaming does not
|
|
48
|
+
// support content encoding; the body is emitted as identity.
|
|
49
|
+
_prelude.headers["content-encoding"] = "identity";
|
|
50
|
+
const prelude = JSON.stringify(_prelude);
|
|
51
|
+
responseStream.write(prelude);
|
|
52
|
+
// 8-null-byte delimiter separating the JSON metadata prelude from
|
|
53
|
+
// the response payload. Same framing API Gateway STREAM expects.
|
|
54
|
+
responseStream.write(new Uint8Array(8));
|
|
55
|
+
return responseStream;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const response = await handler(internalEvent, { streamCreator });
|
|
59
|
+
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
|
|
60
|
+
if (isUsingEdge) {
|
|
61
|
+
debug("Headers has not been set, we must be in the edge runtime");
|
|
62
|
+
const stream = streamCreator.writeHeaders({
|
|
63
|
+
statusCode: response.statusCode,
|
|
64
|
+
headers: response.headers,
|
|
65
|
+
cookies: [],
|
|
66
|
+
});
|
|
67
|
+
Readable.fromWeb(response.body).pipe(stream);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
export default {
|
|
71
|
+
wrapper: handler,
|
|
72
|
+
name: "aws-apigw-streaming",
|
|
73
|
+
supportStreaming: true,
|
|
74
|
+
};
|
|
@@ -96,7 +96,7 @@ export interface Origin {
|
|
|
96
96
|
port?: number;
|
|
97
97
|
customHeaders?: Record<string, string>;
|
|
98
98
|
}
|
|
99
|
-
export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "aws-lambda-compressed" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "express-dev" | "dummy";
|
|
99
|
+
export type IncludedWrapper = "aws-lambda" | "aws-lambda-streaming" | "aws-apigw-streaming" | "aws-lambda-compressed" | "node" | "cloudflare" | "cloudflare-edge" | "cloudflare-node" | "express-dev" | "dummy";
|
|
100
100
|
export type IncludedConverter = "aws-apigw-v2" | "aws-apigw-v1" | "aws-cloudfront" | "edge" | "node" | "sqs-revalidate" | "dummy";
|
|
101
101
|
export type RouteType = "route" | "page" | "app";
|
|
102
102
|
export interface ResolvedRoute {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"access": "public"
|
|
4
4
|
},
|
|
5
5
|
"name": "@opennextjs/aws",
|
|
6
|
-
"version": "4.0
|
|
6
|
+
"version": "4.1.0",
|
|
7
7
|
"bin": {
|
|
8
8
|
"open-next": "./dist/index.js"
|
|
9
9
|
},
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"typescript": "5.9.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"next": ">=15.5.
|
|
63
|
+
"next": ">=15.5.21 <16 || >=16.2.11"
|
|
64
64
|
},
|
|
65
65
|
"bugs": {
|
|
66
66
|
"url": "https://github.com/opennextjs/opennextjs-aws/issues"
|