vinext 1.4.2 → 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 +4 -1
- package/dist/build/prerender-paths.d.ts +11 -1
- package/dist/build/prerender-paths.js +36 -10
- 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 +22 -25
- package/dist/init-platform.js +2 -2
- package/dist/init.js +42 -5
- package/dist/server/api-handler.js +7 -3
- package/dist/server/app-browser-entry.js +16 -37
- 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-middleware.d.ts +2 -0
- package/dist/server/app-middleware.js +1 -0
- package/dist/server/app-page-boundary-render.js +2 -0
- 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 +2 -1
- package/dist/server/app-page-route-wiring.d.ts +7 -5
- package/dist/server/app-page-route-wiring.js +3 -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-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 +39 -22
- 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-ssr-entry.js +8 -0
- 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-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 +22 -4
- 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-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/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/error-boundary.d.ts +6 -2
- package/dist/shims/error-boundary.js +4 -2
- package/dist/shims/request-context.d.ts +3 -1
- 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
|
@@ -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,7 +515,11 @@ 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);
|
|
507
525
|
const routePathname = options.routePathname ?? options.cleanPathname;
|
|
@@ -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
|
});
|
|
@@ -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;
|
|
@@ -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.
|
|
@@ -8,6 +8,19 @@ function readHeader(headers, name) {
|
|
|
8
8
|
return direct;
|
|
9
9
|
}
|
|
10
10
|
const normalizeHostname = normalizeDomainHostname;
|
|
11
|
+
const LOCALE_REDIRECT = Symbol.for("vinext.pages-locale-redirect");
|
|
12
|
+
/**
|
|
13
|
+
* Mark a locale-detection redirect. Next.js answers it before next.config
|
|
14
|
+
* headers are staged, so it goes out without them. The global symbol crosses
|
|
15
|
+
* the App Router / Pages Router module graph boundary.
|
|
16
|
+
*/
|
|
17
|
+
function markLocaleRedirect(response) {
|
|
18
|
+
if (!isLocaleRedirect(response)) Object.defineProperty(response, LOCALE_REDIRECT, { value: true });
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
function isLocaleRedirect(response) {
|
|
22
|
+
return LOCALE_REDIRECT in response;
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* Return the configured locale that the first pathname segment names, compared
|
|
13
26
|
* case-insensitively (`/EN/about` -> `"en"`), or `undefined` when there is none.
|
|
@@ -259,4 +272,4 @@ function resolvePagesI18nRequest(url, i18nConfig, headers, hostname, basePath =
|
|
|
259
272
|
};
|
|
260
273
|
}
|
|
261
274
|
//#endregion
|
|
262
|
-
export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
|
|
275
|
+
export { detectDomainLocale, detectLocaleFromAcceptLanguage, detectPathnameLocale, extractLocaleFromUrl, getLocaleRedirect, isLocaleRedirect, markLocaleRedirect, normalizeDefaultLocalePathname, parseCookieLocaleFromHeader, resolvePagesI18nRequest, shouldRestoreLocaleRootTrailingSlash, stripI18nLocaleForApiRoute };
|
|
@@ -201,10 +201,14 @@ var PagesResponseStream = class extends Writable {
|
|
|
201
201
|
return this;
|
|
202
202
|
}
|
|
203
203
|
json(data) {
|
|
204
|
-
this.resHeaders["content-type"] = "application/json";
|
|
204
|
+
this.resHeaders["content-type"] = "application/json; charset=utf-8";
|
|
205
205
|
this.end(JSON.stringify(data));
|
|
206
206
|
}
|
|
207
207
|
send(data) {
|
|
208
|
+
if (data === null || data === void 0) {
|
|
209
|
+
this.end();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
208
212
|
if (NO_BODY_RESPONSE_STATUSES.has(this.resStatusCode)) {
|
|
209
213
|
this.end();
|
|
210
214
|
return;
|
|
@@ -220,8 +224,8 @@ var PagesResponseStream = class extends Writable {
|
|
|
220
224
|
this.end(data);
|
|
221
225
|
return;
|
|
222
226
|
}
|
|
223
|
-
if (typeof data === "object"
|
|
224
|
-
this.resHeaders["content-type"] = "application/json";
|
|
227
|
+
if (typeof data === "object" || typeof data === "number" || typeof data === "boolean") {
|
|
228
|
+
this.resHeaders["content-type"] = "application/json; charset=utf-8";
|
|
225
229
|
this.end(JSON.stringify(data));
|
|
226
230
|
return;
|
|
227
231
|
}
|
|
@@ -5,7 +5,7 @@ import { NEXTJS_CACHE_HEADER, NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_CACHE_HEADER,
|
|
|
5
5
|
import { normalizePathnameForRouteMatch } from "../routing/utils.js";
|
|
6
6
|
import { patternToNextFormat } from "../routing/route-validation.js";
|
|
7
7
|
import { getRequestExecutionContext } from "../shims/request-context.js";
|
|
8
|
-
import { extractLocaleFromUrl, resolvePagesI18nRequest } from "./pages-i18n.js";
|
|
8
|
+
import { extractLocaleFromUrl, markLocaleRedirect, resolvePagesI18nRequest } from "./pages-i18n.js";
|
|
9
9
|
import { buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, normalizePagesDataRequest, parseNextDataPathname } from "./pages-data-route.js";
|
|
10
10
|
import { reportRequestError } from "./instrumentation.js";
|
|
11
11
|
import { logRequestError } from "./request-error-log.js";
|
|
@@ -154,7 +154,7 @@ function createPagesPageHandler(opts) {
|
|
|
154
154
|
const originalRequestPathAndSearch = originalRequestUrl.pathname + originalRequestUrl.search;
|
|
155
155
|
let dataRequestPathname = null;
|
|
156
156
|
let dataRequestSearch = "";
|
|
157
|
-
const initialDataNorm = normalizePagesDataRequest(request, buildId,
|
|
157
|
+
const initialDataNorm = normalizePagesDataRequest(request, buildId, "", hasMiddleware && vinextConfig.trailingSlash);
|
|
158
158
|
if (!isDataReq) {
|
|
159
159
|
if (initialDataNorm.notFoundResponse) return initialDataNorm.notFoundResponse;
|
|
160
160
|
if (initialDataNorm.isDataReq) {
|
|
@@ -195,10 +195,10 @@ function createPagesPageHandler(opts) {
|
|
|
195
195
|
const currentDefaultLocale = i18nConfig ? localeInfo.domainLocale ? localeInfo.domainLocale.defaultLocale : i18nConfig.defaultLocale : void 0;
|
|
196
196
|
const domainLocales = i18nConfig ? i18nConfig.domains : void 0;
|
|
197
197
|
const pageIsrCacheKey = isrCacheKeyForRequest(i18nConfig ? localeInfo.domainLocale ? JSON.stringify([String(localeInfo.domainLocale.domain).toLowerCase(), locale]) : "locale:" + String(locale) : null);
|
|
198
|
-
if (localeInfo.redirectUrl) return new Response(null, {
|
|
198
|
+
if (localeInfo.redirectUrl) return markLocaleRedirect(new Response(null, {
|
|
199
199
|
status: 307,
|
|
200
200
|
headers: { Location: localeInfo.redirectUrl }
|
|
201
|
-
});
|
|
201
|
+
}));
|
|
202
202
|
let match = options && options.__forcedRoute ? {
|
|
203
203
|
route: options.__forcedRoute,
|
|
204
204
|
params: {}
|
|
@@ -15,6 +15,14 @@ type PageRouteMatch = {
|
|
|
15
15
|
dataKind?: PagesRouteDataKind;
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Whether a filesystem route may serve `requestPathname` from the client
|
|
20
|
+
* build directory. Like Next.js, only a public file or a build asset resolves
|
|
21
|
+
* there: the directory also holds build metadata and private artifacts. A
|
|
22
|
+
* direct GET/HEAD reaches this route only for a public file, since servers
|
|
23
|
+
* answer existing build assets before middleware.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isServableFilesystemRoute(requestPathname: string, phase: FilesystemRoutePhase, method: string, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): boolean;
|
|
18
26
|
export declare function fetchWorkerFilesystemRoute(request: Request, requestPathname: string, phase: FilesystemRoutePhase, fetchAsset: (request: Request) => Promise<Response>, publicFiles: ReadonlySet<string>, basePath?: string, assetPathPrefix?: string, pathnameHasBasePath?: boolean): Promise<Response | false>;
|
|
19
27
|
export type MiddlewareResult = {
|
|
20
28
|
continue: boolean;
|
|
@@ -77,6 +85,7 @@ export type PagesPipelineDeps = {
|
|
|
77
85
|
runMiddleware?: ((request: Request, ctx: unknown, opts: {
|
|
78
86
|
isDataRequest: boolean;
|
|
79
87
|
isStaleDataRequest?: boolean;
|
|
88
|
+
restoreLocaleRootTrailingSlash?: boolean;
|
|
80
89
|
}) => Promise<MiddlewareResult>) | null;
|
|
81
90
|
/** Original URL presented to middleware when URL normalization is disabled. */
|
|
82
91
|
middlewareRequest?: Request;
|
|
@@ -19,6 +19,7 @@ import { setFrameworkRequestRoute } from "./request-tracing.js";
|
|
|
19
19
|
import { withExternalRewriteResponseHeaders } from "./middleware-response-headers.js";
|
|
20
20
|
import { applyConfigHeadersToHeaderRecord } from "./config-headers.js";
|
|
21
21
|
import { finalizeMissingStaticAssetResponse, mergeHeaders, mergeStaticFileHeaders } from "./worker-utils.js";
|
|
22
|
+
import { publicFileRouteKey } from "../utils/public-file-path.js";
|
|
22
23
|
//#region src/server/pages-request-pipeline.ts
|
|
23
24
|
function ruleUsesUnkeyedRequestCondition(rule) {
|
|
24
25
|
return [...rule.has ?? [], ...rule.missing ?? []].some((condition) => condition.type === "header" || condition.type === "cookie" || condition.type === "host");
|
|
@@ -35,24 +36,41 @@ function headersFromRecord(record) {
|
|
|
35
36
|
else headers.set(name, value);
|
|
36
37
|
return headers;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Whether a filesystem route may serve `requestPathname` from the client
|
|
41
|
+
* build directory. Like Next.js, only a public file or a build asset resolves
|
|
42
|
+
* there: the directory also holds build metadata and private artifacts. A
|
|
43
|
+
* direct GET/HEAD reaches this route only for a public file, since servers
|
|
44
|
+
* answer existing build assets before middleware.
|
|
45
|
+
*/
|
|
46
|
+
function isServableFilesystemRoute(requestPathname, phase, method, publicFiles, basePath = "", assetPathPrefix = "", pathnameHasBasePath = true) {
|
|
47
|
+
const assetUrl = new URL("http://localhost");
|
|
41
48
|
assetUrl.pathname = requestPathname;
|
|
42
|
-
assetUrl.search = "";
|
|
43
49
|
const decodedAssetUrl = new URL(assetUrl);
|
|
44
50
|
let publicFileKey;
|
|
51
|
+
let servedPathname;
|
|
52
|
+
let servedFileKey;
|
|
45
53
|
try {
|
|
46
54
|
decodedAssetUrl.pathname = decodeURIComponent(assetUrl.pathname);
|
|
47
55
|
publicFileKey = normalizePathnameForRouteMatchStrict(assetUrl.pathname);
|
|
56
|
+
servedPathname = decodeURIComponent(requestPathname);
|
|
57
|
+
servedFileKey = publicFileRouteKey(servedPathname);
|
|
48
58
|
} catch {
|
|
49
59
|
return false;
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const decodedPathname = decodedAssetUrl.pathname;
|
|
62
|
+
if (!pathnameHasBasePath && !isAssetPrefixBuildAsset(decodedPathname, assetPathPrefix)) return false;
|
|
63
|
+
const isPublicFile = pathnameHasBasePath && publicFiles.has(publicFileKey) && publicFiles.has(servedFileKey);
|
|
53
64
|
if (!isPublicFile && (requestPathname === "/api" || requestPathname.startsWith("/api/"))) return false;
|
|
54
|
-
if (phase === "direct" &&
|
|
55
|
-
|
|
65
|
+
if (phase === "direct" && (method === "GET" || method === "HEAD") && !isPublicFile) return false;
|
|
66
|
+
return isPublicFile || isNextStaticPath(decodedPathname, basePath, assetPathPrefix) && isNextStaticPath(servedPathname, basePath, assetPathPrefix);
|
|
67
|
+
}
|
|
68
|
+
async function fetchWorkerFilesystemRoute(request, requestPathname, phase, fetchAsset, publicFiles, basePath = "", assetPathPrefix = "", pathnameHasBasePath = true) {
|
|
69
|
+
const isRetrievalMethod = request.method === "GET" || request.method === "HEAD";
|
|
70
|
+
const assetUrl = new URL(request.url);
|
|
71
|
+
assetUrl.pathname = requestPathname;
|
|
72
|
+
assetUrl.search = "";
|
|
73
|
+
if (!isServableFilesystemRoute(requestPathname, phase, request.method, publicFiles, basePath, assetPathPrefix, pathnameHasBasePath)) return false;
|
|
56
74
|
const response = await fetchAsset(isRetrievalMethod ? new Request(assetUrl, request) : new Request(assetUrl, {
|
|
57
75
|
method: "HEAD",
|
|
58
76
|
headers: request.headers
|
|
@@ -179,7 +197,8 @@ async function runPagesRequest(request, deps) {
|
|
|
179
197
|
if (!isOnDemandRevalidate && typeof deps.runMiddleware === "function") {
|
|
180
198
|
const result = await deps.runMiddleware(deps.middlewareRequest ?? request, deps.ctx ?? null, {
|
|
181
199
|
isDataRequest,
|
|
182
|
-
isStaleDataRequest: isDataRequest && !!deps.dataNotFoundResponse
|
|
200
|
+
isStaleDataRequest: isDataRequest && !!deps.dataNotFoundResponse,
|
|
201
|
+
restoreLocaleRootTrailingSlash: deps.restoreLocaleRootTrailingSlash
|
|
183
202
|
});
|
|
184
203
|
if (deps.recordCacheability !== false && result.pathnameEligible) markRouteCacheabilityDynamic("middleware can match this pathname");
|
|
185
204
|
if (result.waitUntilPromises && result.waitUntilPromises.length > 0) {
|
|
@@ -465,4 +484,4 @@ async function runPagesRequest(request, deps) {
|
|
|
465
484
|
};
|
|
466
485
|
}
|
|
467
486
|
//#endregion
|
|
468
|
-
export { fetchWorkerFilesystemRoute, runPagesRequest, wrapMiddlewareWithBasePath };
|
|
487
|
+
export { fetchWorkerFilesystemRoute, isServableFilesystemRoute, runPagesRequest, wrapMiddlewareWithBasePath };
|
|
@@ -73,7 +73,9 @@ const imageConfig = vinextConfig?.images ? {
|
|
|
73
73
|
dangerouslyAllowSVG: vinextConfig.images.dangerouslyAllowSVG,
|
|
74
74
|
dangerouslyAllowLocalIP: vinextConfig.images.dangerouslyAllowLocalIP,
|
|
75
75
|
contentDispositionType: vinextConfig.images.contentDispositionType,
|
|
76
|
-
contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy
|
|
76
|
+
contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy,
|
|
77
|
+
minimumCacheTTL: vinextConfig.images.minimumCacheTTL,
|
|
78
|
+
maximumResponseBody: vinextConfig.images.maximumResponseBody
|
|
77
79
|
} : void 0;
|
|
78
80
|
/**
|
|
79
81
|
* Whether the request stage serves this request as a client build file
|
|
@@ -384,7 +386,10 @@ async function handleRequestImpl(request, env, platformCtx, dispatchResponseStag
|
|
|
384
386
|
});
|
|
385
387
|
if (sourceResult.type !== "response") return null;
|
|
386
388
|
await sourceResult.response.body?.cancel().catch(() => {});
|
|
387
|
-
if (isImageSourceBodyStatus(sourceResult.response.status)
|
|
389
|
+
if (isImageSourceBodyStatus(sourceResult.response.status) && sourceAsset !== null) {
|
|
390
|
+
const { withStagedCacheControl } = await import("./worker-image-optimization.js");
|
|
391
|
+
return withStagedCacheControl(sourceAsset, sourceResult.response.headers.get("Cache-Control"));
|
|
392
|
+
}
|
|
388
393
|
await sourceAsset?.body?.cancel().catch(() => {});
|
|
389
394
|
return null;
|
|
390
395
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { VINEXT_CACHEABILITY_PROBE_HEADER, VINEXT_CACHEABILITY_PROBE_QUERY_PARAM } from "./headers.js";
|
|
2
|
+
import { notFoundResponse } from "./http-error-responses.js";
|
|
2
3
|
import { cloneRequestWithHeaders, cloneRequestWithUrl, stripRequestUrlFragment } from "./request-pipeline.js";
|
|
3
4
|
import { runWithExecutionContext } from "../shims/request-context.js";
|
|
4
5
|
import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
|
|
5
6
|
import { validateCdnRequest } from "./cache-control.js";
|
|
6
7
|
import { createWorkerRevalidationContext } from "./worker-revalidation-context.js";
|
|
7
8
|
import { createWorkerPrerenderDiscoveryContext, createWorkerPrerenderReadinessResponse } from "./worker-prerender-discovery.js";
|
|
9
|
+
import { isSrvxAbsoluteFormTarget } from "./srvx-request-target.js";
|
|
8
10
|
import { handleRequestStageLocally, isPagesClientBuildFileRequest, pagesRequestStageBuildId, pagesRequestStagePrerenderSecret } from "./pages-request-stage-entry.js";
|
|
9
11
|
import { renderPagesResponse } from "./pages-response-stage-entry.js";
|
|
10
12
|
import { registerConfiguredCacheAdapters } from "virtual:vinext-cache-adapters";
|
|
@@ -52,6 +54,7 @@ async function handleSingleStageRequest(request, env, platformCtx) {
|
|
|
52
54
|
return finalize ? finalize(response, ctx) : response;
|
|
53
55
|
}
|
|
54
56
|
var pages_router_entry_default = { fetch(request, env, ctx) {
|
|
57
|
+
if (isSrvxAbsoluteFormTarget(request)) return Promise.resolve(notFoundResponse());
|
|
55
58
|
const strippedRequest = stripRequestUrlFragment(request);
|
|
56
59
|
const handle = () => handleSingleStageRequest(strippedRequest, env, ctx);
|
|
57
60
|
return ctx && typeof ctx.waitUntil === "function" ? runWithExecutionContext(ctx, handle) : handle();
|
|
@@ -90,7 +90,7 @@ declare function mergeWebResponse(middlewareHeaders: Record<string, string | str
|
|
|
90
90
|
* Send a compressed response if the content type is compressible and the
|
|
91
91
|
* client supports compression. Otherwise send uncompressed.
|
|
92
92
|
*/
|
|
93
|
-
declare function sendCompressed(req: IncomingMessage, res: ServerResponse, body: string | Buffer, contentType: string, statusCode: number, extraHeaders?: Record<string, string | string[]>, compress?: boolean, statusText?: string): void;
|
|
93
|
+
declare function sendCompressed(req: IncomingMessage, res: ServerResponse, body: string | Buffer, contentType: string | undefined, statusCode: number, extraHeaders?: Record<string, string | string[]>, compress?: boolean, statusText?: string): void;
|
|
94
94
|
/**
|
|
95
95
|
* Try to serve a static file from the client build directory.
|
|
96
96
|
*
|