vinext 1.4.1 → 1.4.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 +5 -2
- package/dist/build/action-owner-manifest.js +1 -0
- package/dist/build/prerender-paths.d.ts +11 -1
- package/dist/build/prerender-paths.js +36 -10
- package/dist/check.js +1 -1
- 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 +8 -4
- package/dist/config/destination-params.js +5 -2
- package/dist/config/next-config.d.ts +25 -1
- package/dist/config/next-config.js +31 -1
- package/dist/entries/app-rsc-entry.js +65 -30
- package/dist/entries/app-rsc-manifest.js +4 -0
- package/dist/entries/pages-server-entry.js +19 -4
- package/dist/index.js +37 -37
- package/dist/init-cloudflare.js +18 -2
- package/dist/init-platform.js +2 -2
- package/dist/init.js +42 -5
- package/dist/plugins/css-target.d.ts +18 -0
- package/dist/plugins/css-target.js +93 -0
- package/dist/routing/app-route-graph.d.ts +10 -0
- package/dist/routing/app-route-graph.js +15 -1
- package/dist/server/api-handler.d.ts +2 -0
- package/dist/server/api-handler.js +10 -8
- package/dist/server/app-action-forwarding.js +3 -1
- package/dist/server/app-browser-entry.js +37 -41
- package/dist/server/app-browser-error.d.ts +1 -1
- package/dist/server/app-browser-error.js +13 -4
- 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-elements.d.ts +3 -0
- package/dist/server/app-elements.js +14 -1
- package/dist/server/app-fallback-renderer.js +2 -2
- package/dist/server/app-middleware.d.ts +2 -0
- package/dist/server/app-middleware.js +1 -0
- package/dist/server/app-optimistic-routing.d.ts +12 -1
- package/dist/server/app-optimistic-routing.js +38 -16
- package/dist/server/app-page-boundary-render.js +4 -2
- package/dist/server/app-page-cache-render.js +1 -1
- package/dist/server/app-page-dispatch.d.ts +11 -1
- package/dist/server/app-page-dispatch.js +78 -31
- package/dist/server/app-page-element-builder.d.ts +11 -20
- package/dist/server/app-page-element-builder.js +39 -50
- package/dist/server/app-page-intercept-segment-config.d.ts +56 -0
- package/dist/server/app-page-intercept-segment-config.js +90 -0
- package/dist/server/app-page-navigation-params.d.ts +23 -0
- package/dist/server/app-page-navigation-params.js +49 -0
- package/dist/server/app-page-render.d.ts +5 -2
- package/dist/server/app-page-render.js +17 -3
- package/dist/server/app-page-request.d.ts +24 -2
- package/dist/server/app-page-request.js +10 -10
- package/dist/server/app-page-route-wiring.d.ts +17 -5
- package/dist/server/app-page-route-wiring.js +19 -13
- package/dist/server/app-page-segment-plan.js +9 -1
- package/dist/server/app-page-segment-state.d.ts +9 -0
- package/dist/server/app-page-segment-state.js +54 -1
- package/dist/server/app-pages-bridge.d.ts +6 -0
- package/dist/server/app-pages-bridge.js +8 -3
- package/dist/server/app-request-stage-independent-entry.js +2 -3
- package/dist/server/app-response-stage-entry.js +1 -0
- package/dist/server/app-route-module-loader.d.ts +2 -0
- package/dist/server/app-route-module-loader.js +1 -0
- 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 +14 -1
- package/dist/server/app-rsc-handler.js +111 -62
- package/dist/server/app-rsc-response-finalizer.d.ts +18 -3
- package/dist/server/app-rsc-response-finalizer.js +61 -7
- package/dist/server/app-rsc-route-matching.d.ts +14 -0
- package/dist/server/app-rsc-route-matching.js +42 -18
- package/dist/server/app-server-action-execution.js +16 -10
- package/dist/server/app-ssr-entry.js +8 -0
- package/dist/server/cache-control.d.ts +2 -0
- package/dist/server/cache-control.js +3 -1
- package/dist/server/config-headers.d.ts +7 -0
- package/dist/server/config-headers.js +20 -1
- package/dist/server/image-animation.d.ts +9 -0
- package/dist/server/image-animation.js +118 -0
- package/dist/server/image-local-patterns.d.ts +21 -0
- package/dist/server/image-local-patterns.js +59 -0
- package/dist/server/image-optimization.d.ts +48 -27
- package/dist/server/image-optimization.js +316 -22
- 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 +46 -10
- 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/nitro-public-files-plugin.js +20 -8
- 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-data-route.d.ts +5 -1
- package/dist/server/pages-data-route.js +14 -6
- 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 +35 -15
- package/dist/server/pages-request-stage-entry.js +8 -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 +64 -62
- package/dist/server/request-pipeline.d.ts +5 -0
- package/dist/server/request-pipeline.js +11 -1
- package/dist/server/response-stage-cacheability.d.ts +7 -0
- package/dist/server/response-stage-cacheability.js +6 -5
- 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 +10 -3
- package/dist/server/worker-image-optimization.js +43 -9
- package/dist/server/worker-revalidation-context.js +1 -0
- package/dist/server/worker-utils.d.ts +8 -0
- package/dist/server/worker-utils.js +19 -2
- package/dist/shims/error-boundary.d.ts +6 -2
- package/dist/shims/error-boundary.js +4 -2
- package/dist/shims/image-props.d.ts +6 -0
- package/dist/shims/image-props.js +39 -3
- package/dist/shims/image.js +3 -3
- package/dist/shims/request-context.d.ts +3 -1
- package/dist/shims/slot.d.ts +3 -0
- package/dist/shims/slot.js +67 -9
- package/dist/shims/url-utils.js +6 -1
- package/dist/utils/base-path.d.ts +7 -0
- package/dist/utils/base-path.js +11 -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,126 @@
|
|
|
1
|
+
//#region src/server/image-source-reader.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
|
+
/** Most bytes of a source kept to replay it after animation detection. */
|
|
10
|
+
const HELD_SOURCE_LIMIT = 1048576;
|
|
11
|
+
/** Thrown once a source grows past `images.maximumResponseBody`. */
|
|
12
|
+
var ImageSourceTooLargeError = class extends Error {
|
|
13
|
+
constructor() {
|
|
14
|
+
super("Image source exceeds images.maximumResponseBody");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ImageSourceReader = class {
|
|
18
|
+
#reader;
|
|
19
|
+
#limit;
|
|
20
|
+
#read = 0;
|
|
21
|
+
#done = false;
|
|
22
|
+
#tooLarge = false;
|
|
23
|
+
#window = [];
|
|
24
|
+
#windowStart = 0;
|
|
25
|
+
#windowEnd = 0;
|
|
26
|
+
#held = [];
|
|
27
|
+
#replayed = false;
|
|
28
|
+
constructor(body, limit) {
|
|
29
|
+
this.#reader = body.getReader();
|
|
30
|
+
this.#limit = limit;
|
|
31
|
+
}
|
|
32
|
+
/** Whether the source has grown past the limit. */
|
|
33
|
+
get tooLarge() {
|
|
34
|
+
return this.#tooLarge;
|
|
35
|
+
}
|
|
36
|
+
/** The source's length once it has all been read and held, else null. */
|
|
37
|
+
get completeLength() {
|
|
38
|
+
return this.#done && this.#held ? this.#read : null;
|
|
39
|
+
}
|
|
40
|
+
/** Stop reading the source without waiting on its cancellation. */
|
|
41
|
+
cancel() {
|
|
42
|
+
this.#reader.cancel().catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
/** Up to `length` bytes at `offset`, fewer where the source ends. */
|
|
45
|
+
async bytes(offset, length) {
|
|
46
|
+
const end = offset + length;
|
|
47
|
+
await this.#reach(offset, end);
|
|
48
|
+
const available = Math.min(end, this.#windowEnd);
|
|
49
|
+
const bytes = new Uint8Array(Math.max(0, available - offset));
|
|
50
|
+
let start = this.#windowStart;
|
|
51
|
+
for (const chunk of this.#window) {
|
|
52
|
+
if (start >= available) break;
|
|
53
|
+
const from = Math.max(offset - start, 0);
|
|
54
|
+
const to = Math.min(available - start, chunk.byteLength);
|
|
55
|
+
if (from < to) bytes.set(chunk.subarray(from, to), start + from - offset);
|
|
56
|
+
start += chunk.byteLength;
|
|
57
|
+
}
|
|
58
|
+
return bytes;
|
|
59
|
+
}
|
|
60
|
+
/** The byte at `offset`, or undefined past the source's end. */
|
|
61
|
+
async byte(offset) {
|
|
62
|
+
return (await this.bytes(offset, 1))[0];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The whole source as a stream, replaying the bytes already read, or null
|
|
66
|
+
* once they are no longer all held. An incompletely read source replays
|
|
67
|
+
* once; the rest streams from the source, still within the limit, and is
|
|
68
|
+
* held too while the whole source fits.
|
|
69
|
+
*/
|
|
70
|
+
body() {
|
|
71
|
+
const held = this.#held;
|
|
72
|
+
if (held === null || this.#replayed && !this.#done) return null;
|
|
73
|
+
this.#replayed = true;
|
|
74
|
+
this.#window = [];
|
|
75
|
+
let index = 0;
|
|
76
|
+
return new ReadableStream({
|
|
77
|
+
pull: async (controller) => {
|
|
78
|
+
if (index < held.length) {
|
|
79
|
+
controller.enqueue(held[index++]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const chunk = await this.#next();
|
|
83
|
+
if (!chunk) return controller.close();
|
|
84
|
+
this.#hold(chunk);
|
|
85
|
+
index = held.length;
|
|
86
|
+
controller.enqueue(chunk);
|
|
87
|
+
},
|
|
88
|
+
cancel: (reason) => this.#reader.cancel(reason)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async #next() {
|
|
92
|
+
if (this.#done) return void 0;
|
|
93
|
+
const { done, value } = await this.#reader.read();
|
|
94
|
+
if (done) {
|
|
95
|
+
this.#done = true;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.#read += value.byteLength;
|
|
99
|
+
if (this.#read > this.#limit) {
|
|
100
|
+
this.#tooLarge = true;
|
|
101
|
+
this.cancel();
|
|
102
|
+
throw new ImageSourceTooLargeError();
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
#hold(chunk) {
|
|
107
|
+
if (!this.#held) return;
|
|
108
|
+
if (this.#read <= HELD_SOURCE_LIMIT) this.#held.push(chunk);
|
|
109
|
+
else this.#held = null;
|
|
110
|
+
}
|
|
111
|
+
/** Read until `end` is in the window, dropping chunks wholly before `offset`. */
|
|
112
|
+
async #reach(offset, end) {
|
|
113
|
+
if (offset < this.#windowStart) throw new Error("Image source reads must not move backwards");
|
|
114
|
+
for (;;) {
|
|
115
|
+
while (this.#window.length > 0 && this.#windowStart + this.#window[0].byteLength <= offset) this.#windowStart += this.#window.shift().byteLength;
|
|
116
|
+
if (this.#windowEnd >= end) return;
|
|
117
|
+
const chunk = await this.#next();
|
|
118
|
+
if (!chunk) return;
|
|
119
|
+
this.#hold(chunk);
|
|
120
|
+
this.#window.push(chunk);
|
|
121
|
+
this.#windowEnd += chunk.byteLength;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
export { ImageSourceReader, ImageSourceTooLargeError };
|
|
@@ -21,6 +21,11 @@ type MetadataRouteRequestOptions = {
|
|
|
21
21
|
isrRouteKey?: (pathname: string) => string;
|
|
22
22
|
isrSet?: MetadataRouteCacheSetter;
|
|
23
23
|
makeThenableParams: MetadataRouteMakeThenableParams;
|
|
24
|
+
/**
|
|
25
|
+
* The pathname params are read from. A direct request passes its raw path
|
|
26
|
+
* here and its decoded path as `cleanPathname`; a rewrite passes its
|
|
27
|
+
* destination, still percent-encoded, as both.
|
|
28
|
+
*/
|
|
24
29
|
routePathname?: string;
|
|
25
30
|
scheduleBackgroundRegeneration?: MetadataRouteBackgroundRegenerator;
|
|
26
31
|
};
|
|
@@ -36,6 +41,6 @@ export type PrerenderableMetadataRoute = {
|
|
|
36
41
|
*/
|
|
37
42
|
export declare function getPrerenderableMetadataRoutePaths(metadataRoutes: readonly MetadataRuntimeRoute[]): Promise<PrerenderableMetadataRoute[]>;
|
|
38
43
|
export declare function isMetadataRouteRequestPath(metadataRoutes: readonly MetadataRuntimeRoute[], cleanPathname: string): boolean;
|
|
39
|
-
export declare function handleMetadataRouteRequest(
|
|
44
|
+
export declare function handleMetadataRouteRequest(requestOptions: MetadataRouteRequestOptions): Promise<Response | null>;
|
|
40
45
|
//#endregion
|
|
41
46
|
export {};
|
|
@@ -2,7 +2,7 @@ import { markFullyBufferedBody } from "./fully-buffered-response.js";
|
|
|
2
2
|
import { closeAfterResponse, createRequestContext, runWithRequestContext } from "../shims/unified-request-context.js";
|
|
3
3
|
import { VINEXT_METADATA_ROUTE_CACHE_HEADER } from "./headers.js";
|
|
4
4
|
import { notFoundResponse } from "./http-error-responses.js";
|
|
5
|
-
import { decodeMatchedParams } from "../routing/utils.js";
|
|
5
|
+
import { decodeMatchedParams, normalizePathnameForRouteMatch } from "../routing/utils.js";
|
|
6
6
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
7
7
|
import { runInWorkStoreScope } from "../shims/internal/work-store-scope.js";
|
|
8
8
|
import { beginRouteCacheability, markRouteCacheabilityExplicitResponsePolicy, markRouteCacheabilityResponseBodyComplete, recordRouteCacheability } from "../shims/cacheability-classification.js";
|
|
@@ -89,6 +89,20 @@ function getMetadataRouteFunctions(route) {
|
|
|
89
89
|
routeFunctionCache.set(route, functions);
|
|
90
90
|
return functions;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Next.js compiles a metadata route into a Route Handler, and under a dynamic
|
|
94
|
+
* segment its build stores it only when the route's own module generates its
|
|
95
|
+
* params or is `force-static`/`error` (collectAppRouteSegments reads no parent
|
|
96
|
+
* layout). Its metadata loader adds generateStaticParams for
|
|
97
|
+
* generateImageMetadata. Probed on next@16.2.7: a parent layout's
|
|
98
|
+
* generateStaticParams, `revalidate` or `dynamicParams` leave the route
|
|
99
|
+
* dynamic. Generated sitemaps are only served below a static prefix.
|
|
100
|
+
*/
|
|
101
|
+
function isMetadataRouteStaticGenEnabled(route) {
|
|
102
|
+
if (!route.isDynamic || !route.patternParts) return true;
|
|
103
|
+
const functions = getMetadataRouteFunctions(route);
|
|
104
|
+
return route.module?.dynamic === "force-static" || route.module?.dynamic === "error" || functions.generateStaticParams !== null || functions.generateImageMetadata !== null;
|
|
105
|
+
}
|
|
92
106
|
function isUseCacheFunction(value) {
|
|
93
107
|
return value !== null && Reflect.get(value, USE_CACHE_FUNCTION_SYMBOL) === true;
|
|
94
108
|
}
|
|
@@ -165,7 +179,7 @@ async function captureRenderedMetadataRoute(response, route, cleanPathname) {
|
|
|
165
179
|
...observedCacheLife,
|
|
166
180
|
revalidate: frameworkRevalidate
|
|
167
181
|
};
|
|
168
|
-
const cacheable = (_isOnDemandRevalidate() || response.status < 400 || response.status === 404) && completed && !isRenderDynamicLatched() && !getActiveDraftModeState() && !hasDraftModeCookieHeader() && route.module?.dynamic !== "force-dynamic" && frameworkRevalidate > 0 && !response.headers.has("set-cookie");
|
|
182
|
+
const cacheable = (_isOnDemandRevalidate() || response.status < 400 || response.status === 404) && completed && !isRenderDynamicLatched() && !getActiveDraftModeState() && !hasDraftModeCookieHeader() && route.module?.dynamic !== "force-dynamic" && isMetadataRouteStaticGenEnabled(route) && frameworkRevalidate > 0 && !response.headers.has("set-cookie");
|
|
169
183
|
const cacheControl = cacheable ? buildAppRouteMissIsrCacheControl(frameworkRevalidate, cacheLife?.expire) : NEVER_CACHE_CONTROL;
|
|
170
184
|
if (isUserResponse && !hasUserPolicy) {
|
|
171
185
|
response = new Response(response.body, {
|
|
@@ -233,7 +247,7 @@ async function runMetadataRouteRegeneration(options, route, render, routeKey, pr
|
|
|
233
247
|
}
|
|
234
248
|
/** The outer ISR key for a matched metadata route, or null if it's never cached. */
|
|
235
249
|
function getOuterMetadataCacheKey(options, route) {
|
|
236
|
-
if (!isOuterMetadataCacheEnabled() || !options.isrRouteKey || getActiveDraftModeState() || hasDraftModeCookieHeader() || route.module?.dynamic === "force-dynamic" || route.module?.revalidate === 0) return null;
|
|
250
|
+
if (!isOuterMetadataCacheEnabled() || !options.isrRouteKey || getActiveDraftModeState() || hasDraftModeCookieHeader() || route.module?.dynamic === "force-dynamic" || route.module?.revalidate === 0 || !isMetadataRouteStaticGenEnabled(route)) return null;
|
|
237
251
|
return options.isrRouteKey(options.cleanPathname);
|
|
238
252
|
}
|
|
239
253
|
/**
|
|
@@ -501,23 +515,41 @@ async function writeMetadataRouteMiss(options, route, rendered, cachedEntry) {
|
|
|
501
515
|
console.error("[vinext] Metadata route ISR cache write error:", error);
|
|
502
516
|
}
|
|
503
517
|
}
|
|
504
|
-
async function handleMetadataRouteRequest(
|
|
518
|
+
async function handleMetadataRouteRequest(requestOptions) {
|
|
519
|
+
const options = requestOptions.routePathname === requestOptions.cleanPathname ? {
|
|
520
|
+
...requestOptions,
|
|
521
|
+
cleanPathname: normalizePathnameForRouteMatch(requestOptions.cleanPathname)
|
|
522
|
+
} : requestOptions;
|
|
505
523
|
let urlParts;
|
|
506
524
|
const getUrlParts = () => urlParts ??= options.cleanPathname.split("/").filter(Boolean);
|
|
525
|
+
const routePathname = options.routePathname ?? options.cleanPathname;
|
|
526
|
+
let routeUrlParts;
|
|
527
|
+
const getRouteUrlParts = () => routeUrlParts ??= routePathname.split("/").filter(Boolean);
|
|
528
|
+
let cacheOptions = options;
|
|
529
|
+
const skipDecodedOwner = () => {
|
|
530
|
+
cacheOptions = {
|
|
531
|
+
...options,
|
|
532
|
+
isrGet: void 0,
|
|
533
|
+
isrRouteKey: void 0,
|
|
534
|
+
isrSet: void 0
|
|
535
|
+
};
|
|
536
|
+
};
|
|
507
537
|
for (const route of options.metadataRoutes) {
|
|
508
538
|
const functions = getMetadataRouteFunctions(route);
|
|
509
539
|
if (route.type === "sitemap" && route.isDynamic) {
|
|
510
540
|
if (functions.generateSitemaps) {
|
|
511
|
-
if (isGeneratedSitemapPath(route, options.cleanPathname))
|
|
541
|
+
if (!isGeneratedSitemapPath(route, options.cleanPathname)) continue;
|
|
542
|
+
if (!isGeneratedSitemapPath(route, routePathname)) skipDecodedOwner();
|
|
543
|
+
else {
|
|
512
544
|
beginMetadataRouteCacheability(route);
|
|
513
545
|
const render = async () => {
|
|
514
546
|
setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
|
|
515
547
|
const response = await handleGeneratedSitemap(route, options.cleanPathname, functions);
|
|
516
548
|
return response ? captureRenderedMetadataRoute(response, route, options.cleanPathname) : null;
|
|
517
549
|
};
|
|
518
|
-
const cached = await readMatchedPrerenderedMetadataRouteResponse(
|
|
550
|
+
const cached = await readMatchedPrerenderedMetadataRouteResponse(cacheOptions, route, render);
|
|
519
551
|
if (cached.response) return cached.response;
|
|
520
|
-
const response = await renderMetadataRouteMiss(
|
|
552
|
+
const response = await renderMetadataRouteMiss(cacheOptions, route, render, cached.cachedEntry);
|
|
521
553
|
if (response) return response;
|
|
522
554
|
}
|
|
523
555
|
continue;
|
|
@@ -525,8 +557,12 @@ async function handleMetadataRouteRequest(options) {
|
|
|
525
557
|
}
|
|
526
558
|
const match = matchMetadataRoute(route, options.cleanPathname, functions, getUrlParts);
|
|
527
559
|
if (!match) continue;
|
|
560
|
+
if (!matchMetadataRoute(route, routePathname, functions, getRouteUrlParts)) {
|
|
561
|
+
skipDecodedOwner();
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
528
564
|
beginMetadataRouteCacheability(route);
|
|
529
|
-
const rawParams = route.patternParts ? metadataRouteRawParams(route, match,
|
|
565
|
+
const rawParams = route.patternParts ? metadataRouteRawParams(route, match, routePathname) : Object.create(null);
|
|
530
566
|
if (route.patternParts) {
|
|
531
567
|
const runtimeParams = { ...rawParams };
|
|
532
568
|
decodeMatchedParams(runtimeParams);
|
|
@@ -536,7 +572,7 @@ async function handleMetadataRouteRequest(options) {
|
|
|
536
572
|
setCurrentFetchSoftTags(buildMetadataRouteTags(route, options.cleanPathname, []));
|
|
537
573
|
return captureRenderedMetadataRoute(route.isDynamic ? await callDynamicMetadataRoute(route, match, options.makeThenableParams, functions) : serveStaticMetadataRoute(route), route, options.cleanPathname);
|
|
538
574
|
};
|
|
539
|
-
const cached = await readMatchedPrerenderedMetadataRouteResponse(
|
|
575
|
+
const cached = await readMatchedPrerenderedMetadataRouteResponse(cacheOptions, route, render);
|
|
540
576
|
if (cached.response) return cached.response;
|
|
541
577
|
if (route.isDynamic && isImageMetadataRoute(route) && functions.dynamicParams === false) {
|
|
542
578
|
const validationParams = { ...rawParams };
|
|
@@ -552,7 +588,7 @@ async function handleMetadataRouteRequest(options) {
|
|
|
552
588
|
});
|
|
553
589
|
if (dynamicParamsResponse) return dynamicParamsResponse;
|
|
554
590
|
}
|
|
555
|
-
return renderMetadataRouteMiss(
|
|
591
|
+
return renderMetadataRouteMiss(cacheOptions, route, render, cached.cachedEntry);
|
|
556
592
|
}
|
|
557
593
|
return null;
|
|
558
594
|
}
|
|
@@ -60,7 +60,7 @@ function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
|
|
|
60
60
|
function matchObjectMatcher(pathname, matcher, i18nConfig, localeContext) {
|
|
61
61
|
if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, matcher.source, i18nConfig, matcher.locale !== false);
|
|
62
62
|
if (matcher.locale !== false) return matchMatcherPattern(pathname, matcher.source, i18nConfig, localeContext);
|
|
63
|
-
return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname
|
|
63
|
+
return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname !== "/" ? pathname : localeContext.restoreRootTrailingSlash ? "/" : ""}` : pathname, matcher.source);
|
|
64
64
|
}
|
|
65
65
|
const APP_ROUTE_TRANSPORT_SUFFIX = "\\.rsc|\\.segments/.+\\.segment\\.rsc";
|
|
66
66
|
const APP_ROUTE_TRANSPORT_SUFFIX_RE = /(?:\.rsc|\.segments\/.+\.segment\.rsc)$/;
|
|
@@ -61,6 +61,11 @@ type ExecuteMiddlewareOptions = {
|
|
|
61
61
|
* second tee whose preserved side would never be consumed.
|
|
62
62
|
*/
|
|
63
63
|
requestBodyAlreadyIsolated?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* `shouldRestoreLocaleRootTrailingSlash` for the raw request pathname. Next.js
|
|
66
|
+
* then tests the as-requested root as `/en/` after default-locale insertion.
|
|
67
|
+
*/
|
|
68
|
+
restoreLocaleRootTrailingSlash?: boolean;
|
|
64
69
|
request: Request;
|
|
65
70
|
/**
|
|
66
71
|
* The user's `trailingSlash` config. Plumbed into the NextRequest's NextURL
|
|
@@ -69,9 +69,26 @@ function middlewareMatcher(mod) {
|
|
|
69
69
|
if (!isMiddlewareConfigExport(config)) return void 0;
|
|
70
70
|
return config.matcher;
|
|
71
71
|
}
|
|
72
|
+
const MIDDLEWARE_MESSAGE_HEADER_NAMES = [
|
|
73
|
+
"accept-encoding",
|
|
74
|
+
"connection",
|
|
75
|
+
"content-encoding",
|
|
76
|
+
"content-length",
|
|
77
|
+
"expect",
|
|
78
|
+
"keep-alive",
|
|
79
|
+
"keepalive",
|
|
80
|
+
"transfer-encoding"
|
|
81
|
+
];
|
|
82
|
+
function deleteMiddlewareMessageHeaders(headers) {
|
|
83
|
+
for (const name of MIDDLEWARE_MESSAGE_HEADER_NAMES) headers.delete(name);
|
|
84
|
+
}
|
|
85
|
+
function processMiddlewareResponseHeaders(headers) {
|
|
86
|
+
processMiddlewareHeaders(headers);
|
|
87
|
+
deleteMiddlewareMessageHeaders(headers);
|
|
88
|
+
}
|
|
72
89
|
function stripMiddlewareHeadersFromResponse(response) {
|
|
73
90
|
const headers = new Headers(response.headers);
|
|
74
|
-
|
|
91
|
+
processMiddlewareResponseHeaders(headers);
|
|
75
92
|
return new Response(response.body, {
|
|
76
93
|
status: response.status,
|
|
77
94
|
statusText: response.statusText,
|
|
@@ -108,7 +125,7 @@ function relativizeLocation(location, requestUrl) {
|
|
|
108
125
|
*/
|
|
109
126
|
function dataRedirectResponse(target, originalResponse) {
|
|
110
127
|
const headers = new Headers(originalResponse.headers);
|
|
111
|
-
|
|
128
|
+
processMiddlewareResponseHeaders(headers);
|
|
112
129
|
headers.delete("Location");
|
|
113
130
|
headers.set("x-nextjs-redirect", target);
|
|
114
131
|
return new Response(null, {
|
|
@@ -120,6 +137,7 @@ function collectMiddlewareHeaders(response) {
|
|
|
120
137
|
const responseHeaders = new Headers();
|
|
121
138
|
for (const [key, value] of response.headers) if (!key.startsWith("x-middleware-") || shouldKeepMiddlewareHeader(key)) responseHeaders.append(key, value);
|
|
122
139
|
dropEmptyMiddlewareSetCookie(responseHeaders);
|
|
140
|
+
deleteMiddlewareMessageHeaders(responseHeaders);
|
|
123
141
|
return responseHeaders;
|
|
124
142
|
}
|
|
125
143
|
function drainFetchEvent(fetchEvent) {
|
|
@@ -209,12 +227,19 @@ async function executeMiddlewareModule(options) {
|
|
|
209
227
|
};
|
|
210
228
|
const isStaleDataRequest = options.isDataRequest === true && options.isStaleDataRequest === true;
|
|
211
229
|
const toPageMatcherPathname = (candidate) => candidate !== null && options.isDataRequest && !isStaleDataRequest ? nextDataMiddlewareMatchPathname(candidate) ?? candidate : candidate;
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
const toMatchPathnames = (candidate) => {
|
|
231
|
+
const encoded = toPageMatcherPathname(prepareMatcherPathname(candidate));
|
|
232
|
+
let decoded = encoded;
|
|
233
|
+
try {
|
|
234
|
+
if (encoded !== null) decoded = decodeURIComponent(encoded);
|
|
235
|
+
else if (!options.i18nConfig) decoded = toPageMatcherPathname(prepareMatcherPathname(decodeURIComponent(candidate)));
|
|
236
|
+
} catch {}
|
|
237
|
+
return {
|
|
238
|
+
decoded,
|
|
239
|
+
encoded
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const { decoded: decodedMatchPathname, encoded: encodedMatchPathname } = toMatchPathnames(encodedRequestPathname);
|
|
218
243
|
let localeContext;
|
|
219
244
|
if (isStaleDataRequest) localeContext = { kind: "next-data" };
|
|
220
245
|
else if (options.i18nConfig && encodedMatchPathname !== null) {
|
|
@@ -226,12 +251,20 @@ async function executeMiddlewareModule(options) {
|
|
|
226
251
|
kind: "defaulted"
|
|
227
252
|
};
|
|
228
253
|
}
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
254
|
+
const slashedRequestPathname = normalizePath(requestPathname);
|
|
255
|
+
const requestedMatchPathnames = slashedRequestPathname !== encodedRequestPathname ? toMatchPathnames(slashedRequestPathname) : {
|
|
256
|
+
decoded: decodedMatchPathname,
|
|
257
|
+
encoded: encodedMatchPathname
|
|
258
|
+
};
|
|
259
|
+
const candidatePathnames = [];
|
|
260
|
+
if (requestedMatchPathnames.encoded !== null) candidatePathnames.push(requestedMatchPathnames.encoded);
|
|
261
|
+
if (requestedMatchPathnames.decoded !== null && requestedMatchPathnames.decoded !== requestedMatchPathnames.encoded) candidatePathnames.push(requestedMatchPathnames.decoded);
|
|
262
|
+
const requestedLocaleContext = options.restoreLocaleRootTrailingSlash && localeContext?.kind === "defaulted" ? {
|
|
263
|
+
...localeContext,
|
|
264
|
+
restoreRootTrailingSlash: true
|
|
265
|
+
} : localeContext;
|
|
266
|
+
if (candidatePathnames.some((candidate) => matchesMiddlewarePathname(candidate, matcher, options.i18nConfig, requestedLocaleContext))) options.onPathMatch?.();
|
|
267
|
+
if (!candidatePathnames.some((candidate) => matchesMiddleware(candidate, matcher, options.request, options.i18nConfig, requestedLocaleContext))) return { continue: true };
|
|
235
268
|
options.onMatch?.();
|
|
236
269
|
const middlewareUrl = withOrigin(requestUrl, getRequestExecutionContext()?.middlewareOrigin ?? requestUrl.origin);
|
|
237
270
|
const nextRequest = createNextRequest(options.request, middlewareUrl, options.i18nConfig, options.basePath, options.trailingSlash, hadBasePath, options.requestBodyAlreadyIsolated, options.skipTrailingSlashRedirect);
|
|
@@ -305,7 +338,7 @@ async function executeMiddlewareModule(options) {
|
|
|
305
338
|
}
|
|
306
339
|
const responseHeaders = new Headers(response.headers);
|
|
307
340
|
responseHeaders.delete("location");
|
|
308
|
-
|
|
341
|
+
processMiddlewareResponseHeaders(responseHeaders);
|
|
309
342
|
const relativizedResponseHeaders = new Headers(response.headers);
|
|
310
343
|
relativizedResponseHeaders.set("Location", normalizedLocation);
|
|
311
344
|
const relativizedResponse = new Response(response.body, {
|
|
@@ -44,6 +44,6 @@ export declare function findMiddlewareFile(root: string, fileMatcher: ValidFileM
|
|
|
44
44
|
* @param request - The incoming Request object
|
|
45
45
|
* @returns Middleware result describing what action to take
|
|
46
46
|
*/
|
|
47
|
-
export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean): Promise<MiddlewareResult>;
|
|
47
|
+
export declare function runMiddleware(runner: ModuleRunner, middlewarePath: string, request: Request, i18nConfig?: NextI18nConfig | null, basePath?: string, trailingSlash?: boolean, isDataRequest?: boolean, normalizedPathname?: string, isStaleDataRequest?: boolean, skipTrailingSlashRedirect?: boolean, restoreLocaleRootTrailingSlash?: boolean): Promise<MiddlewareResult>;
|
|
48
48
|
//#endregion
|
|
49
49
|
export { matchPattern, matchesMiddleware };
|
|
@@ -77,7 +77,7 @@ function isMiddlewareModule(value) {
|
|
|
77
77
|
* @param request - The incoming Request object
|
|
78
78
|
* @returns Middleware result describing what action to take
|
|
79
79
|
*/
|
|
80
|
-
async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect) {
|
|
80
|
+
async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePath, trailingSlash, isDataRequest, normalizedPathname, isStaleDataRequest, skipTrailingSlashRedirect, restoreLocaleRootTrailingSlash) {
|
|
81
81
|
const mod = await runner.import(middlewarePath);
|
|
82
82
|
if (!isMiddlewareModule(mod)) throw new Error(`Middleware module "${middlewarePath}" did not evaluate to an object.`);
|
|
83
83
|
return runGeneratedMiddleware({
|
|
@@ -92,6 +92,7 @@ async function runMiddleware(runner, middlewarePath, request, i18nConfig, basePa
|
|
|
92
92
|
module: mod,
|
|
93
93
|
normalizedPathname,
|
|
94
94
|
request,
|
|
95
|
+
restoreLocaleRootTrailingSlash,
|
|
95
96
|
skipTrailingSlashRedirect,
|
|
96
97
|
trailingSlash
|
|
97
98
|
});
|
|
@@ -53,17 +53,26 @@ function toH3Pathname(pathname) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
+
* The key Nitro's static handler looks a pathname up under, following
|
|
57
|
+
* `runtime/internal/static.mjs`: without one trailing slash and decoded
|
|
58
|
+
* except for `%2F`.
|
|
59
|
+
*/
|
|
60
|
+
function toAssetId(pathname) {
|
|
61
|
+
const id = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
62
|
+
try {
|
|
63
|
+
return decodeURIComponent(id.replace(/%2f/gi, "%252F"));
|
|
64
|
+
} catch {
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
56
69
|
* Whether Nitro's static handler would answer a request for a pathname with
|
|
57
|
-
* a public file
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* extension of a compressed copy for every encoding the request accepts.
|
|
70
|
+
* a public file. It looks h3's pathname up under its key, and then as a
|
|
71
|
+
* directory with an `index.html`, each first with the extension of a
|
|
72
|
+
* compressed copy for every encoding the request accepts.
|
|
61
73
|
*/
|
|
62
74
|
function isPublicFilePathname(pathname, extensions, publicAssets) {
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
id = decodeURIComponent(id.replace(/%2f/gi, "%252F"));
|
|
66
|
-
} catch {}
|
|
75
|
+
const id = toAssetId(pathname);
|
|
67
76
|
const index = id.endsWith("/") ? `${id}index.html` : `${id}/index.html`;
|
|
68
77
|
return extensions.some((extension) => publicAssets.has(id + extension) || publicAssets.has(index + extension));
|
|
69
78
|
}
|
|
@@ -86,6 +95,9 @@ function createNitroPublicFilesPlugin(options) {
|
|
|
86
95
|
const token = crypto.randomUUID();
|
|
87
96
|
Reflect.set(globalThis, Symbol.for(NITRO_ASSETS_KEY), {
|
|
88
97
|
async fetch(request) {
|
|
98
|
+
const { pathname } = new URL(request.url);
|
|
99
|
+
const h3Pathname = toH3Pathname(pathname);
|
|
100
|
+
if (h3Pathname === void 0 || toAssetId(h3Pathname) !== toAssetId(pathname)) return new Response(null, { status: 404 });
|
|
89
101
|
const headers = new Headers(request.headers);
|
|
90
102
|
headers.set(ASSET_REQUEST_HEADER, token);
|
|
91
103
|
return appFetch(new Request(request, { headers }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/server/nitro-request-target-plugin.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Nitro runtime plugin that records each request's raw target before h3 sees
|
|
4
|
+
* it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
|
|
5
|
+
* Node request's `url`, dropping the authority of an absolute-form target,
|
|
6
|
+
* so vinext's request-based entries read the target this plugin saved.
|
|
7
|
+
*
|
|
8
|
+
* This module runs in Nitro's server bundle, separate from vinext's request
|
|
9
|
+
* handlers, so it imports nothing and shares state through `Symbol.for`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
|
|
12
|
+
type NitroApp = {
|
|
13
|
+
fetch: (request: Request) => Response | Promise<Response>;
|
|
14
|
+
};
|
|
15
|
+
export default function nitroRequestTargetPlugin(nitroApp: NitroApp): void;
|
|
16
|
+
//#endregion
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/server/nitro-request-target-plugin.ts
|
|
2
|
+
/**
|
|
3
|
+
* Nitro runtime plugin that records each request's raw target before h3 sees
|
|
4
|
+
* it. srvx 0.11 writes the pathname h3 decodes (one with a `%`) back into the
|
|
5
|
+
* Node request's `url`, dropping the authority of an absolute-form target,
|
|
6
|
+
* so vinext's request-based entries read the target this plugin saved.
|
|
7
|
+
*
|
|
8
|
+
* This module runs in Nitro's server bundle, separate from vinext's request
|
|
9
|
+
* handlers, so it imports nothing and shares state through `Symbol.for`.
|
|
10
|
+
*/
|
|
11
|
+
const NITRO_REQUEST_TARGET_KEY = "vinext.nitro.requestTarget";
|
|
12
|
+
function nitroRequestTargetPlugin(nitroApp) {
|
|
13
|
+
const appFetch = nitroApp.fetch;
|
|
14
|
+
const key = Symbol.for(NITRO_REQUEST_TARGET_KEY);
|
|
15
|
+
nitroApp.fetch = (request) => {
|
|
16
|
+
const req = request.runtime?.node?.req;
|
|
17
|
+
if (req && !Reflect.has(req, key)) Reflect.set(req, key, req.url);
|
|
18
|
+
return appFetch(request);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { NITRO_REQUEST_TARGET_KEY, nitroRequestTargetPlugin as default };
|
|
@@ -43,7 +43,8 @@ export type RepeatedSlashRedirect = {
|
|
|
43
43
|
* `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
|
|
44
44
|
* them would let the request line choose the origin of `request.url` that
|
|
45
45
|
* middleware and handlers see, bypassing the Host trust policy. Request-based
|
|
46
|
-
* entries
|
|
46
|
+
* entries pass a pathname. Where srvx builds `request.url` from the raw target
|
|
47
|
+
* (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
|
|
47
48
|
*
|
|
48
49
|
* @param rawUrl - The raw request target: pathname plus optional `?query`
|
|
49
50
|
*/
|
|
@@ -44,7 +44,8 @@ const REDIRECT_LOCATION_BASE = "http://vinext.invalid";
|
|
|
44
44
|
* `//` after the scheme trips the same check) and 500s `OPTIONS *`. Routing
|
|
45
45
|
* them would let the request line choose the origin of `request.url` that
|
|
46
46
|
* middleware and handlers see, bypassing the Host trust policy. Request-based
|
|
47
|
-
* entries
|
|
47
|
+
* entries pass a pathname. Where srvx builds `request.url` from the raw target
|
|
48
|
+
* (Nitro's Node server), `isSrvxAbsoluteFormTarget` 404s these instead.
|
|
48
49
|
*
|
|
49
50
|
* @param rawUrl - The raw request target: pathname plus optional `?query`
|
|
50
51
|
*/
|
|
@@ -4,6 +4,7 @@ export type AssetCrossOrigin = "" | "anonymous" | "use-credentials";
|
|
|
4
4
|
export type PagesClientAssets = {
|
|
5
5
|
clientEntry?: string;
|
|
6
6
|
appBootstrapPreinitModules?: string[];
|
|
7
|
+
appBootstrapPreloadModules?: string[];
|
|
7
8
|
appRouteManifestModule?: string;
|
|
8
9
|
/** What each App Router client reference loads in the browser, by URL. */
|
|
9
10
|
appClientReferenceChunks?: ClientReferenceChunks;
|
|
@@ -59,8 +59,12 @@ export declare function encodeUrlParserIgnoredCharacters(pathname: string): stri
|
|
|
59
59
|
* `/_next/data/<buildId>/en.json` → `/en`
|
|
60
60
|
* `/_next/data/<wrong-id>/about.json` → null
|
|
61
61
|
* `/_next/data/<buildId>/about` → null (missing .json suffix)
|
|
62
|
+
* `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
|
|
63
|
+
*
|
|
64
|
+
* `decoded` marks a percent-decoded copy of a pathname whose raw form already
|
|
65
|
+
* parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
|
|
62
66
|
*/
|
|
63
|
-
export declare function parseNextDataPathname(pathname: string, buildId: string): NextDataMatch | null;
|
|
67
|
+
export declare function parseNextDataPathname(pathname: string, buildId: string, decoded?: boolean): NextDataMatch | null;
|
|
64
68
|
/**
|
|
65
69
|
* The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
|
|
66
70
|
* compiles every middleware matcher with an optional `/_next/data/<buildId>`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MIDDLEWARE_SKIP_HEADER } from "../utils/protocol-headers.js";
|
|
2
|
-
import {
|
|
2
|
+
import { hasBasePath, restoreBasePath, stripBasePath } from "../utils/base-path.js";
|
|
3
3
|
import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
|
|
4
|
-
import { cloneRequestWithUrl } from "./request-pipeline.js";
|
|
4
|
+
import { cloneRequestWithUrl, hasUrlParserDotSegment } from "./request-pipeline.js";
|
|
5
5
|
//#region src/server/pages-data-route.ts
|
|
6
6
|
/**
|
|
7
7
|
* Helpers for the Pages Router `/_next/data/{buildId}/{...page}.json` endpoint.
|
|
@@ -62,13 +62,19 @@ function encodeUrlParserIgnoredCharacters(pathname) {
|
|
|
62
62
|
* `/_next/data/<buildId>/en.json` → `/en`
|
|
63
63
|
* `/_next/data/<wrong-id>/about.json` → null
|
|
64
64
|
* `/_next/data/<buildId>/about` → null (missing .json suffix)
|
|
65
|
+
* `/_next/data/<buildId>/blog/...json` → null (strips to a dot segment)
|
|
66
|
+
*
|
|
67
|
+
* `decoded` marks a percent-decoded copy of a pathname whose raw form already
|
|
68
|
+
* parsed: a `%2e` left in it was `%252e` on the wire, an ordinary character.
|
|
65
69
|
*/
|
|
66
|
-
function parseNextDataPathname(pathname, buildId) {
|
|
70
|
+
function parseNextDataPathname(pathname, buildId, decoded = false) {
|
|
67
71
|
if (!buildId) return null;
|
|
68
72
|
if (!isNextDataPathname(pathname)) return null;
|
|
69
73
|
const expectedPrefix = `${NEXT_DATA_PREFIX}${buildId}/`;
|
|
70
74
|
if (!pathname.startsWith(expectedPrefix)) return null;
|
|
71
|
-
|
|
75
|
+
const rest = pathname.slice(expectedPrefix.length, -5);
|
|
76
|
+
if (!decoded && hasUrlParserDotSegment(rest)) return null;
|
|
77
|
+
return pageFromNextDataRest(rest);
|
|
72
78
|
}
|
|
73
79
|
/**
|
|
74
80
|
* The page pathname a `_next/data` URL addresses, whatever its buildId. Next.js
|
|
@@ -83,7 +89,9 @@ function nextDataMiddlewareMatchPathname(pathname) {
|
|
|
83
89
|
if (!isNextDataPathname(pathname)) return null;
|
|
84
90
|
const buildIdEnd = pathname.indexOf("/", 12);
|
|
85
91
|
if (buildIdEnd <= 12) return null;
|
|
86
|
-
|
|
92
|
+
const rest = pathname.slice(buildIdEnd + 1, -5);
|
|
93
|
+
if (hasUrlParserDotSegment(rest)) return null;
|
|
94
|
+
return pageFromNextDataRest(rest)?.pagePathname ?? null;
|
|
87
95
|
}
|
|
88
96
|
function pageFromNextDataRest(rest) {
|
|
89
97
|
if (rest.length === 0) return null;
|
|
@@ -189,7 +197,7 @@ function normalizePagesDataRequest(request, buildId, basePath = "", trailingSlas
|
|
|
189
197
|
};
|
|
190
198
|
const pagePathname = normalizeNextDataPagePathname(dataMatch.pagePathname, trailingSlash);
|
|
191
199
|
const normalizedUrl = new URL(reqUrl);
|
|
192
|
-
normalizedUrl.pathname = hadBasePath ?
|
|
200
|
+
normalizedUrl.pathname = hadBasePath ? restoreBasePath(pagePathname, basePath) : pagePathname;
|
|
193
201
|
return {
|
|
194
202
|
isDataReq: true,
|
|
195
203
|
request: cloneRequestWithUrl(request, normalizedUrl.href),
|
|
@@ -24,6 +24,13 @@ type PagesI18nRequestInfo = {
|
|
|
24
24
|
domainLocale?: DomainLocale;
|
|
25
25
|
redirectUrl?: string;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Mark a locale-detection redirect. Next.js answers it before next.config
|
|
29
|
+
* headers are staged, so it goes out without them. The global symbol crosses
|
|
30
|
+
* the App Router / Pages Router module graph boundary.
|
|
31
|
+
*/
|
|
32
|
+
export declare function markLocaleRedirect(response: Response): Response;
|
|
33
|
+
export declare function isLocaleRedirect(response: Response): boolean;
|
|
27
34
|
/**
|
|
28
35
|
* Return the configured locale that the first pathname segment names, compared
|
|
29
36
|
* case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
|