vinext 1.4.2 → 1.4.4
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 +4 -1
- package/dist/build/prerender-paths.d.ts +11 -1
- package/dist/build/prerender-paths.js +41 -11
- package/dist/cli.js +1 -1
- package/dist/client/dev-error-overlay.d.ts +2 -1
- package/dist/client/dev-error-overlay.js +8 -3
- package/dist/config/config-matchers.js +6 -2
- package/dist/config/next-config.d.ts +9 -0
- package/dist/entries/app-rsc-entry.js +24 -12
- package/dist/entries/pages-server-entry.js +17 -4
- package/dist/index.js +48 -39
- package/dist/init-platform.js +2 -2
- package/dist/init.js +42 -5
- package/dist/server/api-handler.js +8 -5
- package/dist/server/app-browser-entry.js +26 -47
- package/dist/server/app-browser-error.d.ts +1 -1
- package/dist/server/app-browser-error.js +13 -4
- package/dist/server/app-browser-navigation-controller.js +2 -2
- package/dist/server/app-browser-server-action-client.js +2 -2
- package/dist/server/app-browser-state.js +2 -2
- package/dist/server/app-browser-supplemental-refresh.d.ts +13 -0
- package/dist/server/app-browser-supplemental-refresh.js +36 -4
- package/dist/server/app-browser-visible-commit.js +1 -1
- package/dist/server/app-history-state.js +1 -1
- package/dist/server/app-middleware.d.ts +2 -0
- package/dist/server/app-middleware.js +1 -0
- package/dist/server/app-page-boundary-render.js +4 -2
- package/dist/server/app-page-dispatch.d.ts +1 -0
- package/dist/server/app-page-dispatch.js +5 -1
- package/dist/server/app-page-element-builder.d.ts +2 -0
- package/dist/server/app-page-element-builder.js +6 -5
- package/dist/server/app-page-render.js +1 -1
- package/dist/server/app-page-route-wiring.d.ts +7 -5
- package/dist/server/app-page-route-wiring.js +11 -9
- package/dist/server/app-pages-bridge.d.ts +6 -0
- package/dist/server/app-pages-bridge.js +8 -3
- package/dist/server/app-request-context.js +1 -1
- package/dist/server/app-request-stage-independent-entry.js +2 -3
- package/dist/server/app-route-handler-dispatch.js +1 -1
- package/dist/server/app-route-handler-execution.js +1 -1
- package/dist/server/app-route-handler-response.d.ts +1 -0
- package/dist/server/app-route-handler-response.js +27 -2
- package/dist/server/app-router-entry.js +2 -3
- package/dist/server/app-rsc-cache-busting.d.ts +8 -0
- package/dist/server/app-rsc-cache-busting.js +13 -1
- package/dist/server/app-rsc-combined-handler.js +2 -1
- package/dist/server/app-rsc-handler.d.ts +4 -0
- package/dist/server/app-rsc-handler.js +41 -24
- package/dist/server/app-rsc-response-finalizer.d.ts +15 -3
- package/dist/server/app-rsc-response-finalizer.js +51 -7
- package/dist/server/app-rsc-response-stage.js +1 -1
- package/dist/server/app-server-action-execution.js +1 -1
- package/dist/server/app-ssr-entry.js +18 -10
- package/dist/server/config-headers.d.ts +7 -0
- package/dist/server/config-headers.js +20 -1
- package/dist/server/edge-api-runtime.d.ts +6 -2
- package/dist/server/edge-api-runtime.js +21 -4
- package/dist/server/image-animation.d.ts +9 -0
- package/dist/server/image-animation.js +118 -0
- package/dist/server/image-optimization.d.ts +25 -5
- package/dist/server/image-optimization.js +305 -21
- package/dist/server/image-source-reader.d.ts +34 -0
- package/dist/server/image-source-reader.js +126 -0
- package/dist/server/metadata-route-response.d.ts +6 -1
- package/dist/server/metadata-route-response.js +24 -6
- package/dist/server/middleware-matcher.d.ts +1 -0
- package/dist/server/middleware-matcher.js +1 -1
- package/dist/server/middleware-runtime.d.ts +5 -0
- package/dist/server/middleware-runtime.js +48 -15
- package/dist/server/middleware.d.ts +1 -1
- package/dist/server/middleware.js +2 -1
- package/dist/server/navigation-planner.js +14 -6
- package/dist/server/nitro-request-target-plugin.d.ts +17 -0
- package/dist/server/nitro-request-target-plugin.js +22 -0
- package/dist/server/open-redirect.d.ts +2 -1
- package/dist/server/open-redirect.js +2 -1
- package/dist/server/pages-client-assets.d.ts +1 -0
- package/dist/server/pages-i18n.d.ts +7 -0
- package/dist/server/pages-i18n.js +14 -1
- package/dist/server/pages-node-compat.js +7 -3
- package/dist/server/pages-page-handler.js +4 -4
- package/dist/server/pages-request-pipeline.d.ts +9 -0
- package/dist/server/pages-request-pipeline.js +29 -10
- package/dist/server/pages-request-stage-entry.js +7 -2
- package/dist/server/pages-router-entry.js +3 -0
- package/dist/server/prod-server.d.ts +1 -1
- package/dist/server/prod-server.js +48 -54
- package/dist/server/remove-request-headers.d.ts +16 -0
- package/dist/server/remove-request-headers.js +23 -0
- package/dist/server/srvx-middleware-origin.d.ts +13 -0
- package/dist/server/srvx-middleware-origin.js +19 -0
- package/dist/server/srvx-request-target.d.ts +12 -0
- package/dist/server/srvx-request-target.js +20 -0
- package/dist/server/static-file-cache.js +10 -22
- package/dist/server/worker-image-optimization.d.ts +8 -1
- package/dist/server/worker-image-optimization.js +25 -6
- package/dist/server/worker-revalidation-context.js +1 -0
- package/dist/shims/cache-runtime.js +1 -1
- package/dist/shims/cache.js +6 -3
- package/dist/shims/client-page-root.js +2 -2
- package/dist/shims/error-boundary.d.ts +6 -2
- package/dist/shims/error-boundary.js +6 -4
- package/dist/shims/internal/navigation-untracked.js +1 -1
- package/dist/shims/metadata.js +1 -1
- package/dist/shims/navigation-server.js +1 -1
- package/dist/shims/navigation.js +4 -4
- package/dist/shims/navigation.react-server.js +3 -3
- package/dist/shims/request-context.d.ts +3 -1
- package/dist/shims/root-params.js +16 -2
- package/dist/shims/slot.js +3 -3
- package/dist/shims/url-utils.js +6 -1
- package/dist/utils/client-entry-manifest.d.ts +5 -0
- package/dist/utils/client-entry-manifest.js +1 -0
- package/dist/utils/client-runtime-metadata.d.ts +5 -0
- package/dist/utils/client-runtime-metadata.js +33 -7
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//#region src/server/image-animation.ts
|
|
2
|
+
const PNG_SIGNATURE = [
|
|
3
|
+
137,
|
|
4
|
+
80,
|
|
5
|
+
78,
|
|
6
|
+
71,
|
|
7
|
+
13,
|
|
8
|
+
10,
|
|
9
|
+
26,
|
|
10
|
+
10
|
|
11
|
+
];
|
|
12
|
+
const WEBP_FOURCC = [
|
|
13
|
+
87,
|
|
14
|
+
69,
|
|
15
|
+
66,
|
|
16
|
+
80
|
|
17
|
+
];
|
|
18
|
+
const WEBP_ANIM_CHUNK = [
|
|
19
|
+
65,
|
|
20
|
+
78,
|
|
21
|
+
73,
|
|
22
|
+
77
|
|
23
|
+
];
|
|
24
|
+
function bytesAt(bytes, offset, expected) {
|
|
25
|
+
return expected.every((byte, index) => bytes[offset + index] === byte);
|
|
26
|
+
}
|
|
27
|
+
function ascii(bytes, start, end) {
|
|
28
|
+
return String.fromCharCode(...bytes.subarray(start, end));
|
|
29
|
+
}
|
|
30
|
+
/** Length of a run of GIF data sub-blocks, including its zero terminator. */
|
|
31
|
+
async function gifDataBlocksLength(source, offset) {
|
|
32
|
+
let length = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const size = await source.byte(offset + length) ?? 0;
|
|
35
|
+
if (size === 0) return length + 1;
|
|
36
|
+
length += size + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function isAnimatedGif(source, head) {
|
|
40
|
+
let hasColorTable = head[10] & 128;
|
|
41
|
+
let colorTableSize = head[10] & 7;
|
|
42
|
+
let offset = 13 + (hasColorTable ? 3 * 2 ** (colorTableSize + 1) : 0);
|
|
43
|
+
let frames = 0;
|
|
44
|
+
while (frames < 2) switch (await source.byte(offset) ?? 59) {
|
|
45
|
+
case 44: {
|
|
46
|
+
frames += 1;
|
|
47
|
+
const flags = await source.byte(offset + 9) ?? 0;
|
|
48
|
+
hasColorTable = flags & 128;
|
|
49
|
+
colorTableSize = flags & 7;
|
|
50
|
+
offset += 10 + (hasColorTable ? 3 * 2 ** (colorTableSize + 1) : 0);
|
|
51
|
+
offset += await gifDataBlocksLength(source, offset + 1) + 1;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 33:
|
|
55
|
+
offset += 2;
|
|
56
|
+
offset += await gifDataBlocksLength(source, offset);
|
|
57
|
+
break;
|
|
58
|
+
default: return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** An APNG has an `acTL` chunk before its first `IDAT`, and `fdAT` frames after. */
|
|
63
|
+
async function isAnimatedPng(source) {
|
|
64
|
+
let hasAnimationControl = false;
|
|
65
|
+
let hasImageData = false;
|
|
66
|
+
let hasFrameData = false;
|
|
67
|
+
let previousChunk = null;
|
|
68
|
+
let offset = 8;
|
|
69
|
+
for (;;) {
|
|
70
|
+
const header = await source.bytes(offset, 8);
|
|
71
|
+
if (header.byteLength < 8) break;
|
|
72
|
+
const length = new DataView(header.buffer).getUint32(0);
|
|
73
|
+
const chunk = ascii(header, 4, 8);
|
|
74
|
+
switch (chunk) {
|
|
75
|
+
case "acTL":
|
|
76
|
+
hasAnimationControl = true;
|
|
77
|
+
break;
|
|
78
|
+
case "IDAT":
|
|
79
|
+
if (!hasAnimationControl) return false;
|
|
80
|
+
if (previousChunk !== "fcTL" && previousChunk !== "IDAT") return false;
|
|
81
|
+
hasImageData = true;
|
|
82
|
+
break;
|
|
83
|
+
case "fdAT":
|
|
84
|
+
if (!hasImageData) return false;
|
|
85
|
+
if (previousChunk !== "fcTL" && previousChunk !== "fdAT") return false;
|
|
86
|
+
hasFrameData = true;
|
|
87
|
+
}
|
|
88
|
+
previousChunk = chunk;
|
|
89
|
+
offset += 12 + length;
|
|
90
|
+
}
|
|
91
|
+
return hasAnimationControl && hasImageData && hasFrameData;
|
|
92
|
+
}
|
|
93
|
+
/** Bytes of a WebP source searched per read. */
|
|
94
|
+
const WEBP_SCAN_STEP = 65536;
|
|
95
|
+
/** Like is-animated, any `ANIM` in a WebP source marks it animated. */
|
|
96
|
+
async function isAnimatedWebp(source) {
|
|
97
|
+
const first = WEBP_ANIM_CHUNK[0];
|
|
98
|
+
const readLength = WEBP_SCAN_STEP + WEBP_ANIM_CHUNK.length - 1;
|
|
99
|
+
for (let start = 0;; start += WEBP_SCAN_STEP) {
|
|
100
|
+
const bytes = await source.bytes(start, readLength);
|
|
101
|
+
for (let offset = 0; offset < bytes.length; offset++) if (bytes[offset] === first && bytesAt(bytes, offset, WEBP_ANIM_CHUNK)) return true;
|
|
102
|
+
if (bytes.length < readLength) return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a source is an animated GIF, PNG or WebP, judged by its signature.
|
|
107
|
+
* The source is parsed as it streams in, so only the bytes a format's check
|
|
108
|
+
* needs are read, and none are kept beyond what the reader holds to replay.
|
|
109
|
+
*/
|
|
110
|
+
async function isAnimatedImage(source) {
|
|
111
|
+
const head = await source.bytes(0, 12);
|
|
112
|
+
if (ascii(head, 0, 3) === "GIF") return isAnimatedGif(source, head);
|
|
113
|
+
if (bytesAt(head, 0, PNG_SIGNATURE)) return isAnimatedPng(source);
|
|
114
|
+
if (bytesAt(head, 8, WEBP_FOURCC)) return isAnimatedWebp(source);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { isAnimatedImage };
|
|
@@ -64,6 +64,13 @@ export type ImageConfig = {
|
|
|
64
64
|
contentDispositionType?: "inline" | "attachment";
|
|
65
65
|
/** Content-Security-Policy header value. Default: "script-src 'none'; frame-src 'none'; sandbox;" */
|
|
66
66
|
contentSecurityPolicy?: string;
|
|
67
|
+
/** Least max-age, in seconds, of an image served as-is. Default: 14400. */
|
|
68
|
+
minimumCacheTTL?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Most bytes of a GIF, PNG or WebP source read for optimization before
|
|
71
|
+
* answering 413. Default: 50000000.
|
|
72
|
+
*/
|
|
73
|
+
maximumResponseBody?: number;
|
|
67
74
|
};
|
|
68
75
|
/**
|
|
69
76
|
* Next.js default device sizes and image sizes.
|
|
@@ -144,11 +151,6 @@ export declare function negotiateImageFormat(acceptHeader: string | null): strin
|
|
|
144
151
|
* Optimized images are immutable because the URL encodes the transform params.
|
|
145
152
|
*/
|
|
146
153
|
export declare const IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
147
|
-
/**
|
|
148
|
-
* Next.js's Cache-Control for an image whose source can change at its URL
|
|
149
|
-
* (anything but a hashed build asset), at the default `images.minimumCacheTTL`.
|
|
150
|
-
*/
|
|
151
|
-
export declare const MUTABLE_IMAGE_SOURCE_CACHE_CONTROL = "public, max-age=14400, must-revalidate";
|
|
152
154
|
/**
|
|
153
155
|
* Content-Security-Policy for image optimization responses.
|
|
154
156
|
* Blocks script execution and framing to prevent XSS via SVG or other
|
|
@@ -156,6 +158,10 @@ export declare const MUTABLE_IMAGE_SOURCE_CACHE_CONTROL = "public, max-age=14400
|
|
|
156
158
|
* Matches Next.js default: script-src 'none'; frame-src 'none'; sandbox;
|
|
157
159
|
*/
|
|
158
160
|
export declare const IMAGE_CONTENT_SECURITY_POLICY = "script-src 'none'; frame-src 'none'; sandbox;";
|
|
161
|
+
/** Next.js's 400 body for a source whose type it does not serve, such as SVG. */
|
|
162
|
+
export declare const IMAGE_TYPE_NOT_ALLOWED_MESSAGE = "\"url\" parameter is valid but image type is not allowed";
|
|
163
|
+
/** The image type Next.js detects from a source's first bytes, or null. */
|
|
164
|
+
export declare function sniffImageContentType(bytes: Uint8Array): string | null;
|
|
159
165
|
/**
|
|
160
166
|
* Check if a Content-Type header value is a safe image type.
|
|
161
167
|
* Returns false for SVG (unless dangerouslyAllowSVG is true), HTML, or any non-image type.
|
|
@@ -177,6 +183,20 @@ export declare function isSafeImageContentType(contentType: string | null, dange
|
|
|
177
183
|
* go out without it. Only full image responses carry either header here too.
|
|
178
184
|
*/
|
|
179
185
|
export declare const IMAGE_RESPONSE_CACHE_STATE = "MISS";
|
|
186
|
+
/**
|
|
187
|
+
* Mark a source the host fetched from its `_next/static` build output, which
|
|
188
|
+
* is served immutably like Next.js' `isStatic` sources. A public file is never
|
|
189
|
+
* marked, whatever its path.
|
|
190
|
+
*/
|
|
191
|
+
export declare function markImageBuildAsset(response: Response): Response;
|
|
192
|
+
/**
|
|
193
|
+
* Next.js's Cache-Control for a source it serves as-is: a build asset stays
|
|
194
|
+
* immutable, and any other source is revalidated after the larger of
|
|
195
|
+
* `images.minimumCacheTTL` and the source's own max-age. Next.js ignores the
|
|
196
|
+
* source's max-age when it falls back after a failed transform, so that
|
|
197
|
+
* fallback passes null.
|
|
198
|
+
*/
|
|
199
|
+
export declare function sourceImageCacheControl(isBuildAsset: boolean, upstreamCacheControl: string | null, config?: ImageConfig): string;
|
|
180
200
|
/**
|
|
181
201
|
* Handlers for image optimization I/O operations.
|
|
182
202
|
* Workers provide these callbacks to adapt their specific bindings.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { badRequestResponse } from "./http-error-responses.js";
|
|
2
2
|
import { setCacheStateHeaders } from "./cache-headers.js";
|
|
3
|
+
import { isAnimatedImage } from "./image-animation.js";
|
|
4
|
+
import { ImageSourceReader, ImageSourceTooLargeError } from "./image-source-reader.js";
|
|
3
5
|
import { hasLocalMatch } from "./image-local-patterns.js";
|
|
4
6
|
//#region src/server/image-optimization.ts
|
|
5
7
|
/**
|
|
@@ -19,6 +21,9 @@ import { hasLocalMatch } from "./image-local-patterns.js";
|
|
|
19
21
|
* spoofing. SVG content is blocked by default (following Next.js behavior).
|
|
20
22
|
* When `dangerouslyAllowSVG` is enabled in next.config.js, SVGs are served
|
|
21
23
|
* as-is (no transformation) with security headers applied.
|
|
24
|
+
*
|
|
25
|
+
* Like Next.js, formats it does not optimize (SVG, ICO, ICNS, BMP, JPEG XL,
|
|
26
|
+
* HEIC) and animated images are served as-is too.
|
|
22
27
|
*/
|
|
23
28
|
/** The pathname that triggers image optimization (matches Next.js). */
|
|
24
29
|
const IMAGE_OPTIMIZATION_PATH = "/_next/image";
|
|
@@ -238,11 +243,13 @@ function negotiateImageFormat(acceptHeader) {
|
|
|
238
243
|
* Optimized images are immutable because the URL encodes the transform params.
|
|
239
244
|
*/
|
|
240
245
|
const IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
246
|
+
/** Next.js's default `images.minimumCacheTTL`, in seconds. */
|
|
247
|
+
const DEFAULT_MINIMUM_CACHE_TTL = 14400;
|
|
241
248
|
/**
|
|
242
|
-
* Next.js's
|
|
243
|
-
*
|
|
249
|
+
* Next.js's default `images.maximumResponseBody`: the most it reads of a
|
|
250
|
+
* source before answering 413.
|
|
244
251
|
*/
|
|
245
|
-
const
|
|
252
|
+
const DEFAULT_MAXIMUM_RESPONSE_BODY = 5e7;
|
|
246
253
|
/**
|
|
247
254
|
* Content-Security-Policy for image optimization responses.
|
|
248
255
|
* Blocks script execution and framing to prevent XSS via SVG or other
|
|
@@ -263,16 +270,207 @@ const SAFE_IMAGE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
263
270
|
"image/avif",
|
|
264
271
|
"image/x-icon",
|
|
265
272
|
"image/vnd.microsoft.icon",
|
|
273
|
+
"image/x-icns",
|
|
266
274
|
"image/bmp",
|
|
267
|
-
"image/tiff"
|
|
275
|
+
"image/tiff",
|
|
276
|
+
"image/jxl",
|
|
277
|
+
"image/heic"
|
|
268
278
|
]);
|
|
269
279
|
/**
|
|
280
|
+
* Formats Next.js serves as-is instead of optimizing (`BYPASS_TYPES` in
|
|
281
|
+
* packages/next/src/server/image-optimizer.ts), with ICO's registered name.
|
|
282
|
+
*/
|
|
283
|
+
const BYPASS_IMAGE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
284
|
+
"image/svg+xml",
|
|
285
|
+
"image/x-icon",
|
|
286
|
+
"image/vnd.microsoft.icon",
|
|
287
|
+
"image/x-icns",
|
|
288
|
+
"image/bmp",
|
|
289
|
+
"image/jxl",
|
|
290
|
+
"image/heic"
|
|
291
|
+
]);
|
|
292
|
+
/** Formats Next.js serves as-is when animated (`ANIMATABLE_TYPES`). */
|
|
293
|
+
const ANIMATABLE_IMAGE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
294
|
+
"image/webp",
|
|
295
|
+
"image/png",
|
|
296
|
+
"image/gif"
|
|
297
|
+
]);
|
|
298
|
+
/** Next.js's 400 body for a source whose type it does not serve, such as SVG. */
|
|
299
|
+
const IMAGE_TYPE_NOT_ALLOWED_MESSAGE = "\"url\" parameter is valid but image type is not allowed";
|
|
300
|
+
function imageTypeNotAllowedResponse() {
|
|
301
|
+
return new Response(IMAGE_TYPE_NOT_ALLOWED_MESSAGE, { status: 400 });
|
|
302
|
+
}
|
|
303
|
+
function imageMediaType(contentType) {
|
|
304
|
+
return contentType?.split(";")[0].trim().toLowerCase() ?? "";
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Next.js's image signatures (`detectContentType` in
|
|
308
|
+
* packages/next/src/server/image-optimizer.ts); a null byte matches any value.
|
|
309
|
+
*/
|
|
310
|
+
const IMAGE_SIGNATURES = [
|
|
311
|
+
["image/jpeg", [
|
|
312
|
+
255,
|
|
313
|
+
216,
|
|
314
|
+
255
|
|
315
|
+
]],
|
|
316
|
+
["image/png", [
|
|
317
|
+
137,
|
|
318
|
+
80,
|
|
319
|
+
78,
|
|
320
|
+
71,
|
|
321
|
+
13,
|
|
322
|
+
10,
|
|
323
|
+
26,
|
|
324
|
+
10
|
|
325
|
+
]],
|
|
326
|
+
["image/gif", [
|
|
327
|
+
71,
|
|
328
|
+
73,
|
|
329
|
+
70,
|
|
330
|
+
56
|
|
331
|
+
]],
|
|
332
|
+
["image/webp", [
|
|
333
|
+
82,
|
|
334
|
+
73,
|
|
335
|
+
70,
|
|
336
|
+
70,
|
|
337
|
+
null,
|
|
338
|
+
null,
|
|
339
|
+
null,
|
|
340
|
+
null,
|
|
341
|
+
87,
|
|
342
|
+
69,
|
|
343
|
+
66,
|
|
344
|
+
80
|
|
345
|
+
]],
|
|
346
|
+
["image/svg+xml", [
|
|
347
|
+
60,
|
|
348
|
+
63,
|
|
349
|
+
120,
|
|
350
|
+
109,
|
|
351
|
+
108
|
|
352
|
+
]],
|
|
353
|
+
["image/svg+xml", [
|
|
354
|
+
60,
|
|
355
|
+
115,
|
|
356
|
+
118,
|
|
357
|
+
103
|
|
358
|
+
]],
|
|
359
|
+
["image/avif", [
|
|
360
|
+
null,
|
|
361
|
+
null,
|
|
362
|
+
null,
|
|
363
|
+
null,
|
|
364
|
+
102,
|
|
365
|
+
116,
|
|
366
|
+
121,
|
|
367
|
+
112,
|
|
368
|
+
97,
|
|
369
|
+
118,
|
|
370
|
+
105,
|
|
371
|
+
102
|
|
372
|
+
]],
|
|
373
|
+
["image/x-icon", [
|
|
374
|
+
0,
|
|
375
|
+
0,
|
|
376
|
+
1,
|
|
377
|
+
0
|
|
378
|
+
]],
|
|
379
|
+
["image/x-icns", [
|
|
380
|
+
105,
|
|
381
|
+
99,
|
|
382
|
+
110,
|
|
383
|
+
115
|
|
384
|
+
]],
|
|
385
|
+
["image/tiff", [
|
|
386
|
+
73,
|
|
387
|
+
73,
|
|
388
|
+
42,
|
|
389
|
+
0
|
|
390
|
+
]],
|
|
391
|
+
["image/bmp", [66, 77]],
|
|
392
|
+
["image/jxl", [255, 10]],
|
|
393
|
+
["image/jxl", [
|
|
394
|
+
0,
|
|
395
|
+
0,
|
|
396
|
+
0,
|
|
397
|
+
12,
|
|
398
|
+
74,
|
|
399
|
+
88,
|
|
400
|
+
76,
|
|
401
|
+
32,
|
|
402
|
+
13,
|
|
403
|
+
10,
|
|
404
|
+
135,
|
|
405
|
+
10
|
|
406
|
+
]],
|
|
407
|
+
["image/heic", [
|
|
408
|
+
null,
|
|
409
|
+
null,
|
|
410
|
+
null,
|
|
411
|
+
null,
|
|
412
|
+
102,
|
|
413
|
+
116,
|
|
414
|
+
121,
|
|
415
|
+
112,
|
|
416
|
+
104,
|
|
417
|
+
101,
|
|
418
|
+
105,
|
|
419
|
+
99
|
|
420
|
+
]]
|
|
421
|
+
];
|
|
422
|
+
const IMAGE_SIGNATURE_LENGTH = 12;
|
|
423
|
+
/** The image type Next.js detects from a source's first bytes, or null. */
|
|
424
|
+
function sniffImageContentType(bytes) {
|
|
425
|
+
for (const [type, signature] of IMAGE_SIGNATURES) if (signature.every((byte, i) => byte === null || bytes[i] === byte)) return type;
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Label a source that arrives without an image Content-Type by its first
|
|
430
|
+
* bytes, as Next.js types every source. Workers static assets have no type for
|
|
431
|
+
* some formats Next.js serves, such as ICNS.
|
|
432
|
+
*/
|
|
433
|
+
async function withSniffedImageContentType(source) {
|
|
434
|
+
if (!source.ok || !source.body || imageMediaType(source.headers.get("Content-Type")).startsWith("image/")) return source;
|
|
435
|
+
const reader = source.body.getReader();
|
|
436
|
+
const head = [];
|
|
437
|
+
const signature = new Uint8Array(IMAGE_SIGNATURE_LENGTH);
|
|
438
|
+
let length = 0;
|
|
439
|
+
while (length < IMAGE_SIGNATURE_LENGTH) {
|
|
440
|
+
const { done, value } = await reader.read();
|
|
441
|
+
if (done) break;
|
|
442
|
+
head.push(value);
|
|
443
|
+
signature.set(value.subarray(0, IMAGE_SIGNATURE_LENGTH - length), length);
|
|
444
|
+
length += Math.min(value.byteLength, IMAGE_SIGNATURE_LENGTH - length);
|
|
445
|
+
}
|
|
446
|
+
const body = new ReadableStream({
|
|
447
|
+
start(controller) {
|
|
448
|
+
for (const chunk of head) controller.enqueue(chunk);
|
|
449
|
+
},
|
|
450
|
+
async pull(controller) {
|
|
451
|
+
const { done, value } = await reader.read();
|
|
452
|
+
if (done) controller.close();
|
|
453
|
+
else controller.enqueue(value);
|
|
454
|
+
},
|
|
455
|
+
cancel(reason) {
|
|
456
|
+
return reader.cancel(reason);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const headers = new Headers(source.headers);
|
|
460
|
+
const type = sniffImageContentType(signature.subarray(0, length));
|
|
461
|
+
if (type !== null) headers.set("Content-Type", type);
|
|
462
|
+
return new Response(body, {
|
|
463
|
+
status: source.status,
|
|
464
|
+
statusText: source.statusText,
|
|
465
|
+
headers
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
270
469
|
* Check if a Content-Type header value is a safe image type.
|
|
271
470
|
* Returns false for SVG (unless dangerouslyAllowSVG is true), HTML, or any non-image type.
|
|
272
471
|
*/
|
|
273
472
|
function isSafeImageContentType(contentType, dangerouslyAllowSVG = false) {
|
|
274
|
-
|
|
275
|
-
const mediaType = contentType.split(";")[0].trim().toLowerCase();
|
|
473
|
+
const mediaType = imageMediaType(contentType);
|
|
276
474
|
if (SAFE_IMAGE_CONTENT_TYPES.has(mediaType)) return true;
|
|
277
475
|
if (dangerouslyAllowSVG && mediaType === "image/svg+xml") return true;
|
|
278
476
|
return false;
|
|
@@ -305,20 +503,65 @@ function setImageSecurityHeaders(headers, config) {
|
|
|
305
503
|
*/
|
|
306
504
|
const IMAGE_RESPONSE_CACHE_STATE = "MISS";
|
|
307
505
|
/** Headers shared by every successful image response, transformed or not. */
|
|
308
|
-
function setImageResponseHeaders(headers, config) {
|
|
309
|
-
headers.set("Cache-Control",
|
|
506
|
+
function setImageResponseHeaders(headers, cacheControl, config) {
|
|
507
|
+
headers.set("Cache-Control", cacheControl);
|
|
310
508
|
headers.set("Vary", "Accept");
|
|
311
509
|
setImageSecurityHeaders(headers, config);
|
|
312
510
|
setCacheStateHeaders(headers, IMAGE_RESPONSE_CACHE_STATE);
|
|
313
511
|
}
|
|
314
|
-
|
|
512
|
+
/** The `s-maxage` or `max-age` of a Cache-Control value, like Next.js' getMaxAge. */
|
|
513
|
+
function cacheControlMaxAge(cacheControl) {
|
|
514
|
+
const directives = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const directive of cacheControl?.split(",") ?? []) {
|
|
516
|
+
const [key, value] = directive.trim().split("=", 2);
|
|
517
|
+
directives.set(key.toLowerCase(), value?.toLowerCase());
|
|
518
|
+
}
|
|
519
|
+
let age = directives.get("s-maxage") || directives.get("max-age") || "";
|
|
520
|
+
if (age.startsWith("\"") && age.endsWith("\"")) age = age.slice(1, -1);
|
|
521
|
+
const seconds = Number.parseInt(age, 10);
|
|
522
|
+
return Number.isNaN(seconds) ? 0 : seconds;
|
|
523
|
+
}
|
|
524
|
+
const IMAGE_BUILD_ASSET = Symbol.for("vinext.imageBuildAsset");
|
|
525
|
+
/**
|
|
526
|
+
* Mark a source the host fetched from its `_next/static` build output, which
|
|
527
|
+
* is served immutably like Next.js' `isStatic` sources. A public file is never
|
|
528
|
+
* marked, whatever its path.
|
|
529
|
+
*/
|
|
530
|
+
function markImageBuildAsset(response) {
|
|
531
|
+
Reflect.set(response, IMAGE_BUILD_ASSET, true);
|
|
532
|
+
return response;
|
|
533
|
+
}
|
|
534
|
+
function isImageBuildAsset(response) {
|
|
535
|
+
return Reflect.get(response, IMAGE_BUILD_ASSET) === true;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Next.js's Cache-Control for a source it serves as-is: a build asset stays
|
|
539
|
+
* immutable, and any other source is revalidated after the larger of
|
|
540
|
+
* `images.minimumCacheTTL` and the source's own max-age. Next.js ignores the
|
|
541
|
+
* source's max-age when it falls back after a failed transform, so that
|
|
542
|
+
* fallback passes null.
|
|
543
|
+
*/
|
|
544
|
+
function sourceImageCacheControl(isBuildAsset, upstreamCacheControl, config) {
|
|
545
|
+
if (isBuildAsset) return IMAGE_CACHE_CONTROL;
|
|
546
|
+
return `public, max-age=${Math.max(config?.minimumCacheTTL ?? DEFAULT_MINIMUM_CACHE_TTL, cacheControlMaxAge(upstreamCacheControl))}, must-revalidate`;
|
|
547
|
+
}
|
|
548
|
+
function createPassthroughImageResponse(source, body, cacheControl, config, readLength = null) {
|
|
315
549
|
const headers = new Headers(source.headers);
|
|
316
|
-
setImageResponseHeaders(headers, config);
|
|
317
|
-
|
|
550
|
+
setImageResponseHeaders(headers, cacheControl, config);
|
|
551
|
+
if (readLength !== null) headers.set("Content-Length", String(readLength));
|
|
552
|
+
return new Response(body, {
|
|
318
553
|
status: 200,
|
|
319
554
|
headers
|
|
320
555
|
});
|
|
321
556
|
}
|
|
557
|
+
/** Whether a source's Content-Length is more than `limit` bytes. */
|
|
558
|
+
function declaresMoreThan(source, limit) {
|
|
559
|
+
return Number(source.headers.get("Content-Length") ?? NaN) > limit;
|
|
560
|
+
}
|
|
561
|
+
/** Next.js's 413 for a source larger than `images.maximumResponseBody`. */
|
|
562
|
+
function sourceTooLargeResponse() {
|
|
563
|
+
return new Response("\"url\" parameter is valid but internal response is invalid", { status: 413 });
|
|
564
|
+
}
|
|
322
565
|
/**
|
|
323
566
|
* Handle image optimization requests.
|
|
324
567
|
*
|
|
@@ -330,36 +573,77 @@ async function handleImageOptimization(request, handlers, allowedWidths, imageCo
|
|
|
330
573
|
const params = parseImageParams(new URL(request.url), allowedWidths, imageConfig?.qualities, { localPatterns: imageConfig?.localPatterns });
|
|
331
574
|
if (!params) return badRequestResponse();
|
|
332
575
|
const { imageUrl, width, quality } = params;
|
|
333
|
-
const
|
|
576
|
+
const fetchedSource = await handlers.fetchAsset(imageUrl, request);
|
|
577
|
+
const isBuildAsset = isImageBuildAsset(fetchedSource);
|
|
578
|
+
const source = await withSniffedImageContentType(fetchedSource);
|
|
334
579
|
if (!source.ok || !source.body) return invalidImageSourceResponse();
|
|
335
580
|
const format = negotiateImageFormat(request.headers.get("Accept"));
|
|
336
581
|
const sourceContentType = source.headers.get("Content-Type");
|
|
337
|
-
if (!isSafeImageContentType(sourceContentType, imageConfig?.dangerouslyAllowSVG)) return
|
|
338
|
-
|
|
582
|
+
if (!isSafeImageContentType(sourceContentType, imageConfig?.dangerouslyAllowSVG)) return imageTypeNotAllowedResponse();
|
|
583
|
+
const sourceMediaType = imageMediaType(sourceContentType);
|
|
584
|
+
const sourceCacheControl = sourceImageCacheControl(isBuildAsset, source.headers.get("Cache-Control"), imageConfig);
|
|
585
|
+
if (BYPASS_IMAGE_CONTENT_TYPES.has(sourceMediaType)) {
|
|
586
|
+
const response = createPassthroughImageResponse(source, source.body, sourceCacheControl, imageConfig);
|
|
587
|
+
if (sourceMediaType === "image/vnd.microsoft.icon") response.headers.set("Content-Type", "image/x-icon");
|
|
588
|
+
return response;
|
|
589
|
+
}
|
|
590
|
+
const limit = imageConfig?.maximumResponseBody ?? DEFAULT_MAXIMUM_RESPONSE_BODY;
|
|
591
|
+
let reader = null;
|
|
339
592
|
if (handlers.transformImage) try {
|
|
340
|
-
|
|
593
|
+
let body = source.body;
|
|
594
|
+
if (ANIMATABLE_IMAGE_CONTENT_TYPES.has(sourceMediaType)) {
|
|
595
|
+
if (declaresMoreThan(source, limit)) {
|
|
596
|
+
source.body.cancel().catch(() => {});
|
|
597
|
+
return sourceTooLargeResponse();
|
|
598
|
+
}
|
|
599
|
+
reader = new ImageSourceReader(source.body, limit);
|
|
600
|
+
const animated = await isAnimatedImage(reader);
|
|
601
|
+
let sourceBody = reader.body();
|
|
602
|
+
if (!sourceBody) {
|
|
603
|
+
reader.cancel();
|
|
604
|
+
const refetched = await handlers.fetchAsset(imageUrl, request);
|
|
605
|
+
if (!refetched.ok || !refetched.body) return invalidImageSourceResponse();
|
|
606
|
+
reader = new ImageSourceReader(refetched.body, limit);
|
|
607
|
+
sourceBody = reader.body();
|
|
608
|
+
}
|
|
609
|
+
if (animated) return createPassthroughImageResponse(source, sourceBody, sourceCacheControl, imageConfig, reader.completeLength);
|
|
610
|
+
body = sourceBody;
|
|
611
|
+
}
|
|
612
|
+
const transformed = await handlers.transformImage(body, {
|
|
341
613
|
width,
|
|
342
614
|
format,
|
|
343
615
|
quality
|
|
344
616
|
});
|
|
345
617
|
const headers = new Headers(transformed.headers);
|
|
346
|
-
setImageResponseHeaders(headers, imageConfig);
|
|
618
|
+
setImageResponseHeaders(headers, IMAGE_CACHE_CONTROL, imageConfig);
|
|
347
619
|
if (!isSafeImageContentType(headers.get("Content-Type"), imageConfig?.dangerouslyAllowSVG)) headers.set("Content-Type", format);
|
|
348
620
|
return new Response(transformed.body, {
|
|
349
621
|
status: 200,
|
|
350
622
|
headers
|
|
351
623
|
});
|
|
352
624
|
} catch (e) {
|
|
625
|
+
if (e instanceof ImageSourceTooLargeError || reader?.tooLarge) return sourceTooLargeResponse();
|
|
353
626
|
console.error("[vinext] Image optimization error:", e);
|
|
354
627
|
}
|
|
628
|
+
const fallbackCacheControl = handlers.transformImage ? sourceImageCacheControl(isBuildAsset, null, imageConfig) : sourceCacheControl;
|
|
629
|
+
const readLength = reader?.completeLength ?? null;
|
|
355
630
|
try {
|
|
356
|
-
return createPassthroughImageResponse(source, imageConfig);
|
|
631
|
+
return createPassthroughImageResponse(source, readLength !== null && reader?.body() || source.body, fallbackCacheControl, imageConfig, readLength);
|
|
357
632
|
} catch (e) {
|
|
358
633
|
console.error("[vinext] Image fallback error, refetching source image:", e);
|
|
359
|
-
|
|
634
|
+
reader?.cancel();
|
|
635
|
+
const refetchedSource = await withSniffedImageContentType(await handlers.fetchAsset(imageUrl, request));
|
|
360
636
|
if (!refetchedSource.ok || !refetchedSource.body) return invalidImageSourceResponse();
|
|
361
|
-
if (!isSafeImageContentType(refetchedSource.headers.get("Content-Type"), imageConfig?.dangerouslyAllowSVG)) return
|
|
362
|
-
|
|
637
|
+
if (!isSafeImageContentType(refetchedSource.headers.get("Content-Type"), imageConfig?.dangerouslyAllowSVG)) return imageTypeNotAllowedResponse();
|
|
638
|
+
let body = refetchedSource.body;
|
|
639
|
+
if (reader) {
|
|
640
|
+
if (declaresMoreThan(refetchedSource, limit)) {
|
|
641
|
+
body.cancel().catch(() => {});
|
|
642
|
+
return sourceTooLargeResponse();
|
|
643
|
+
}
|
|
644
|
+
body = new ImageSourceReader(body, limit).body();
|
|
645
|
+
}
|
|
646
|
+
return createPassthroughImageResponse(refetchedSource, body, fallbackCacheControl, imageConfig);
|
|
363
647
|
}
|
|
364
648
|
}
|
|
365
649
|
const _IMAGE_OPTIMIZER_KEY = Symbol.for("vinext.imageOptimizer");
|
|
@@ -412,4 +696,4 @@ function handleConfiguredImageOptimization(request, fetchAsset, allowedWidths, i
|
|
|
412
696
|
}, allowedWidths, imageConfig);
|
|
413
697
|
}
|
|
414
698
|
//#endregion
|
|
415
|
-
export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, DEFAULT_LOCAL_PATTERNS, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE,
|
|
699
|
+
export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, DEFAULT_LOCAL_PATTERNS, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE, IMAGE_TYPE_NOT_ALLOWED_MESSAGE, INVALID_IMAGE_SOURCE_MESSAGE, VINEXT_IMAGE_OPTIMIZATION_PATH, getImageOptimizer, handleConfiguredImageOptimization, handleImageOptimization, invalidImageSourceResponse, isImageOptimizationPath, isImageSourceBodyStatus, isSafeImageContentType, markImageBuildAsset, markImageOptimizationSignal, negotiateImageFormat, parseImageParams, readImageOptimizationSignal, resolveDevImageRedirect, restoreImageOptimizationSignalFromTransport, serializeImageOptimizationSignalForTransport, setImageOptimizer, sniffImageContentType, sourceImageCacheControl };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/server/image-source-reader.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Streamed reads of an image source for the image optimization endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Animation detection parses a GIF, PNG or WebP source as it streams in rather
|
|
6
|
+
* than buffering it, so a large source cannot exhaust a Worker isolate's
|
|
7
|
+
* memory. Only the source's first bytes are kept, to replay without a refetch.
|
|
8
|
+
*/
|
|
9
|
+
/** Thrown once a source grows past `images.maximumResponseBody`. */
|
|
10
|
+
export declare class ImageSourceTooLargeError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
export declare class ImageSourceReader {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(body: ReadableStream<Uint8Array>, limit: number);
|
|
16
|
+
/** Whether the source has grown past the limit. */
|
|
17
|
+
get tooLarge(): boolean;
|
|
18
|
+
/** The source's length once it has all been read and held, else null. */
|
|
19
|
+
get completeLength(): number | null;
|
|
20
|
+
/** Stop reading the source without waiting on its cancellation. */
|
|
21
|
+
cancel(): void;
|
|
22
|
+
/** Up to `length` bytes at `offset`, fewer where the source ends. */
|
|
23
|
+
bytes(offset: number, length: number): Promise<Uint8Array>;
|
|
24
|
+
/** The byte at `offset`, or undefined past the source's end. */
|
|
25
|
+
byte(offset: number): Promise<number | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* The whole source as a stream, replaying the bytes already read, or null
|
|
28
|
+
* once they are no longer all held. An incompletely read source replays
|
|
29
|
+
* once; the rest streams from the source, still within the limit, and is
|
|
30
|
+
* held too while the whole source fits.
|
|
31
|
+
*/
|
|
32
|
+
body(): ReadableStream<Uint8Array> | null;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|