vinext 0.0.25 → 0.0.27
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 +95 -86
- package/dist/build/static-export.d.ts.map +1 -1
- package/dist/build/static-export.js +3 -8
- package/dist/build/static-export.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +154 -50
- package/dist/check.js.map +1 -1
- package/dist/cli.js +42 -12
- package/dist/cli.js.map +1 -1
- package/dist/client/entry.js.map +1 -1
- package/dist/client/vinext-next-data.d.ts +22 -0
- package/dist/client/vinext-next-data.d.ts.map +1 -0
- package/dist/client/vinext-next-data.js +2 -0
- package/dist/client/vinext-next-data.js.map +1 -0
- package/dist/cloudflare/kv-cache-handler.d.ts +32 -1
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
- package/dist/cloudflare/kv-cache-handler.js +47 -21
- package/dist/cloudflare/kv-cache-handler.js.map +1 -1
- package/dist/cloudflare/tpr.d.ts.map +1 -1
- package/dist/cloudflare/tpr.js +15 -4
- package/dist/cloudflare/tpr.js.map +1 -1
- package/dist/config/config-matchers.d.ts +27 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +312 -62
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/dotenv.d.ts.map +1 -1
- package/dist/config/dotenv.js +1 -6
- package/dist/config/dotenv.js.map +1 -1
- package/dist/config/next-config.d.ts +38 -4
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +194 -31
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts +11 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +74 -39
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-browser-entry.d.ts +9 -0
- package/dist/entries/app-browser-entry.d.ts.map +1 -0
- package/dist/entries/app-browser-entry.js +340 -0
- package/dist/entries/app-browser-entry.js.map +1 -0
- package/dist/{server/app-dev-server.d.ts → entries/app-rsc-entry.d.ts} +4 -17
- package/dist/entries/app-rsc-entry.d.ts.map +1 -0
- package/dist/{server/app-dev-server.js → entries/app-rsc-entry.js} +438 -1232
- package/dist/entries/app-rsc-entry.js.map +1 -0
- package/dist/entries/app-ssr-entry.d.ts +8 -0
- package/dist/entries/app-ssr-entry.d.ts.map +1 -0
- package/dist/entries/app-ssr-entry.js +449 -0
- package/dist/entries/app-ssr-entry.js.map +1 -0
- package/dist/entries/pages-client-entry.d.ts +4 -0
- package/dist/entries/pages-client-entry.d.ts.map +1 -0
- package/dist/entries/pages-client-entry.js +96 -0
- package/dist/entries/pages-client-entry.js.map +1 -0
- package/dist/entries/pages-entry-helpers.d.ts +7 -0
- package/dist/entries/pages-entry-helpers.d.ts.map +1 -0
- package/dist/entries/pages-entry-helpers.js +18 -0
- package/dist/entries/pages-entry-helpers.js.map +1 -0
- package/dist/entries/pages-server-entry.d.ts +8 -0
- package/dist/entries/pages-server-entry.d.ts.map +1 -0
- package/dist/entries/pages-server-entry.js +1015 -0
- package/dist/entries/pages-server-entry.js.map +1 -0
- package/dist/index.d.ts +2 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -1357
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +6 -5
- package/dist/init.js.map +1 -1
- package/dist/routing/app-router.d.ts +2 -0
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +10 -18
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts.map +1 -1
- package/dist/routing/file-matcher.js.map +1 -1
- package/dist/routing/pages-router.d.ts +2 -0
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +8 -5
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +7 -2
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-router-entry.d.ts +3 -2
- package/dist/server/app-router-entry.d.ts.map +1 -1
- package/dist/server/app-router-entry.js +8 -4
- package/dist/server/app-router-entry.js.map +1 -1
- package/dist/server/dev-module-runner.d.ts.map +1 -1
- package/dist/server/dev-module-runner.js +1 -1
- package/dist/server/dev-module-runner.js.map +1 -1
- package/dist/server/dev-origin-check.d.ts.map +1 -1
- package/dist/server/dev-origin-check.js.map +1 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +30 -18
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/image-optimization.d.ts.map +1 -1
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/instrumentation.d.ts +1 -1
- package/dist/server/instrumentation.js +2 -2
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/isr-cache.d.ts +13 -1
- package/dist/server/isr-cache.d.ts.map +1 -1
- package/dist/server/isr-cache.js +10 -1
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/metadata-routes.js +6 -18
- package/dist/server/metadata-routes.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +1 -1
- package/dist/server/middleware-codegen.d.ts.map +1 -1
- package/dist/server/middleware-codegen.js +13 -11
- package/dist/server/middleware-codegen.js.map +1 -1
- package/dist/server/middleware-request-headers.d.ts +9 -0
- package/dist/server/middleware-request-headers.d.ts.map +1 -0
- package/dist/server/middleware-request-headers.js +77 -0
- package/dist/server/middleware-request-headers.js.map +1 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +38 -19
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/normalize-path.js.map +1 -1
- package/dist/server/prod-server.d.ts +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +71 -41
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +93 -0
- package/dist/server/request-pipeline.d.ts.map +1 -0
- package/dist/server/request-pipeline.js +200 -0
- package/dist/server/request-pipeline.js.map +1 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +21 -16
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +18 -17
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/constants.d.ts +120 -3
- package/dist/shims/constants.d.ts.map +1 -1
- package/dist/shims/constants.js +165 -3
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/dynamic.d.ts.map +1 -1
- package/dist/shims/dynamic.js +1 -1
- package/dist/shims/dynamic.js.map +1 -1
- package/dist/shims/error-boundary.d.ts.map +1 -1
- package/dist/shims/error-boundary.js +2 -3
- package/dist/shims/error-boundary.js.map +1 -1
- package/dist/shims/error.d.ts.map +1 -1
- package/dist/shims/error.js +1 -3
- package/dist/shims/error.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +53 -29
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/font-google-base.d.ts.map +1 -1
- package/dist/shims/font-google-base.js +16 -4
- package/dist/shims/font-google-base.js.map +1 -1
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/font-google.generated.d.ts.map +1 -1
- package/dist/shims/font-google.generated.js +412 -206
- package/dist/shims/font-google.generated.js.map +1 -1
- package/dist/shims/font-google.js +1 -1
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.d.ts.map +1 -1
- package/dist/shims/font-local.js +13 -3
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +2 -2
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/head.d.ts.map +1 -1
- package/dist/shims/head.js +10 -8
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +23 -5
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +98 -37
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts.map +1 -1
- package/dist/shims/image.js +35 -8
- package/dist/shims/image.js.map +1 -1
- package/dist/shims/legacy-image.d.ts.map +1 -1
- package/dist/shims/legacy-image.js +1 -1
- package/dist/shims/legacy-image.js.map +1 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +31 -17
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +19 -3
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +19 -11
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation-state.d.ts.map +1 -1
- package/dist/shims/navigation-state.js +3 -2
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +26 -19
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/og.d.ts +6 -6
- package/dist/shims/og.js +6 -6
- package/dist/shims/og.js.map +1 -1
- package/dist/shims/request-context.d.ts +50 -0
- package/dist/shims/request-context.d.ts.map +1 -0
- package/dist/shims/request-context.js +59 -0
- package/dist/shims/request-context.js.map +1 -0
- package/dist/shims/router-state.d.ts.map +1 -1
- package/dist/shims/router-state.js +2 -1
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +18 -25
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/script.d.ts.map +1 -1
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.d.ts +13 -0
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +100 -34
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts.map +1 -1
- package/dist/shims/url-utils.js +1 -3
- package/dist/shims/url-utils.js.map +1 -1
- package/dist/utils/base-path.d.ts +17 -0
- package/dist/utils/base-path.d.ts.map +1 -0
- package/dist/utils/base-path.js +25 -0
- package/dist/utils/base-path.js.map +1 -0
- package/dist/utils/project.d.ts +15 -0
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/project.js +50 -4
- package/dist/utils/project.js.map +1 -1
- package/dist/utils/query.d.ts.map +1 -1
- package/dist/utils/query.js +3 -1
- package/dist/utils/query.js.map +1 -1
- package/package.json +47 -33
- package/dist/server/app-dev-server.d.ts.map +0 -1
- package/dist/server/app-dev-server.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { loadEnv, parseAst } from "vite";
|
|
2
|
-
import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute,
|
|
2
|
+
import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, } from "./routing/pages-router.js";
|
|
3
|
+
import { generateServerEntry as _generateServerEntry } from "./entries/pages-server-entry.js";
|
|
4
|
+
import { generateClientEntry as _generateClientEntry } from "./entries/pages-client-entry.js";
|
|
3
5
|
import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
|
|
4
6
|
import { createValidFileMatcher } from "./routing/file-matcher.js";
|
|
5
7
|
import { createSSRHandler } from "./server/dev-server.js";
|
|
6
8
|
import { handleApiRoute } from "./server/api-handler.js";
|
|
7
9
|
import { createDirectRunner } from "./server/dev-module-runner.js";
|
|
8
|
-
import { generateRscEntry
|
|
10
|
+
import { generateRscEntry } from "./entries/app-rsc-entry.js";
|
|
11
|
+
import { generateSsrEntry } from "./entries/app-ssr-entry.js";
|
|
12
|
+
import { generateBrowserEntry } from "./entries/app-browser-entry.js";
|
|
9
13
|
import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
|
|
10
|
-
import { findMiddlewareFile,
|
|
14
|
+
import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
|
|
11
15
|
import { logRequest, now } from "./server/request-log.js";
|
|
12
|
-
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
|
|
13
16
|
import { normalizePath } from "./server/normalize-path.js";
|
|
14
17
|
import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
|
|
15
18
|
import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js";
|
|
16
19
|
import { validateDevRequest } from "./server/dev-origin-check.js";
|
|
17
|
-
import {
|
|
20
|
+
import { isExternalUrl, proxyExternalRequest, matchHeaders, matchRedirect, matchRewrite, requestContextFromRequest, sanitizeDestination, } from "./config/config-matchers.js";
|
|
18
21
|
import { scanMetadataFiles } from "./server/metadata-routes.js";
|
|
22
|
+
import { buildRequestHeadersFromMiddlewareResponse } from "./server/middleware-request-headers.js";
|
|
19
23
|
import { detectPackageManager } from "./utils/project.js";
|
|
24
|
+
import { hasBasePath } from "./utils/base-path.js";
|
|
20
25
|
import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js";
|
|
26
|
+
import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js";
|
|
21
27
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
22
28
|
import react from "@vitejs/plugin-react";
|
|
23
29
|
import MagicString from "magic-string";
|
|
@@ -62,7 +68,11 @@ async function fetchAndCacheFont(cssUrl, family, cacheDir) {
|
|
|
62
68
|
while ((urlMatch = urlRe.exec(css)) !== null) {
|
|
63
69
|
const fontUrl = urlMatch[1];
|
|
64
70
|
if (!urls.has(fontUrl)) {
|
|
65
|
-
const ext = fontUrl.includes(".woff2")
|
|
71
|
+
const ext = fontUrl.includes(".woff2")
|
|
72
|
+
? ".woff2"
|
|
73
|
+
: fontUrl.includes(".woff")
|
|
74
|
+
? ".woff"
|
|
75
|
+
: ".ttf";
|
|
66
76
|
const fileHash = createHash("md5").update(fontUrl).digest("hex").slice(0, 8);
|
|
67
77
|
urls.set(fontUrl, `${family.toLowerCase().replace(/\s+/g, "-")}-${fileHash}${ext}`);
|
|
68
78
|
}
|
|
@@ -129,7 +139,9 @@ function extractStaticValue(node) {
|
|
|
129
139
|
return node.value;
|
|
130
140
|
case "UnaryExpression":
|
|
131
141
|
// Handle negative numbers: -1, -3.14
|
|
132
|
-
if (node.operator === "-" &&
|
|
142
|
+
if (node.operator === "-" &&
|
|
143
|
+
node.argument?.type === "Literal" &&
|
|
144
|
+
typeof node.argument.value === "number") {
|
|
133
145
|
return -node.argument.value;
|
|
134
146
|
}
|
|
135
147
|
return undefined;
|
|
@@ -240,7 +252,9 @@ async function resolvePostcssStringPlugins(projectRoot) {
|
|
|
240
252
|
// Load the config file
|
|
241
253
|
let config;
|
|
242
254
|
try {
|
|
243
|
-
if (configPath.endsWith(".json") ||
|
|
255
|
+
if (configPath.endsWith(".json") ||
|
|
256
|
+
configPath.endsWith(".yaml") ||
|
|
257
|
+
configPath.endsWith(".yml")) {
|
|
244
258
|
// JSON/YAML configs use object form — postcss-load-config handles these fine
|
|
245
259
|
return undefined;
|
|
246
260
|
}
|
|
@@ -263,8 +277,7 @@ async function resolvePostcssStringPlugins(projectRoot) {
|
|
|
263
277
|
// (either bare strings or tuple form ["plugin-name", { options }])
|
|
264
278
|
if (!config || !Array.isArray(config.plugins))
|
|
265
279
|
return undefined;
|
|
266
|
-
const hasStringPlugins = config.plugins.some((p) => typeof p === "string" ||
|
|
267
|
-
(Array.isArray(p) && typeof p[0] === "string"));
|
|
280
|
+
const hasStringPlugins = config.plugins.some((p) => typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"));
|
|
268
281
|
if (!hasStringPlugins)
|
|
269
282
|
return undefined;
|
|
270
283
|
// Resolve string plugin names to actual plugin functions
|
|
@@ -362,9 +375,7 @@ function clientManualChunks(id) {
|
|
|
362
375
|
const pkg = getPackageName(id);
|
|
363
376
|
if (!pkg)
|
|
364
377
|
return undefined;
|
|
365
|
-
if (pkg === "react" ||
|
|
366
|
-
pkg === "react-dom" ||
|
|
367
|
-
pkg === "scheduler") {
|
|
378
|
+
if (pkg === "react" || pkg === "react-dom" || pkg === "scheduler") {
|
|
368
379
|
return "framework";
|
|
369
380
|
}
|
|
370
381
|
// Let Rollup handle all other vendor code via its default
|
|
@@ -512,979 +523,7 @@ export default function vinext(options = {}) {
|
|
|
512
523
|
* This is the entry point for `vite build --ssr`.
|
|
513
524
|
*/
|
|
514
525
|
async function generateServerEntry() {
|
|
515
|
-
|
|
516
|
-
const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
517
|
-
// Generate import statements using absolute paths since virtual
|
|
518
|
-
// modules don't have a real file location for relative resolution.
|
|
519
|
-
const pageImports = pageRoutes.map((r, i) => {
|
|
520
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
521
|
-
return `import * as page_${i} from ${JSON.stringify(absPath)};`;
|
|
522
|
-
});
|
|
523
|
-
const apiImports = apiRoutes.map((r, i) => {
|
|
524
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
525
|
-
return `import * as api_${i} from ${JSON.stringify(absPath)};`;
|
|
526
|
-
});
|
|
527
|
-
// Build the route table — include filePath for SSR manifest lookup
|
|
528
|
-
const pageRouteEntries = pageRoutes.map((r, i) => {
|
|
529
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
530
|
-
return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`;
|
|
531
|
-
});
|
|
532
|
-
const apiRouteEntries = apiRoutes.map((r, i) => {
|
|
533
|
-
return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
|
|
534
|
-
});
|
|
535
|
-
// Check for _app and _document
|
|
536
|
-
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
537
|
-
const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
|
|
538
|
-
const hasApp = appFilePath !== null;
|
|
539
|
-
const hasDoc = docFilePath !== null;
|
|
540
|
-
const appImportCode = hasApp
|
|
541
|
-
? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
|
|
542
|
-
: `const AppComponent = null;`;
|
|
543
|
-
const docImportCode = hasDoc
|
|
544
|
-
? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
|
|
545
|
-
: `const DocumentComponent = null;`;
|
|
546
|
-
// Serialize i18n config for embedding in the server entry
|
|
547
|
-
const i18nConfigJson = nextConfig?.i18n
|
|
548
|
-
? JSON.stringify({
|
|
549
|
-
locales: nextConfig.i18n.locales,
|
|
550
|
-
defaultLocale: nextConfig.i18n.defaultLocale,
|
|
551
|
-
localeDetection: nextConfig.i18n.localeDetection,
|
|
552
|
-
})
|
|
553
|
-
: "null";
|
|
554
|
-
// Serialize the full resolved config for the production server.
|
|
555
|
-
// This embeds redirects, rewrites, headers, basePath, trailingSlash
|
|
556
|
-
// so prod-server.ts can apply them without loading next.config.js at runtime.
|
|
557
|
-
const vinextConfigJson = JSON.stringify({
|
|
558
|
-
basePath: nextConfig?.basePath ?? "",
|
|
559
|
-
trailingSlash: nextConfig?.trailingSlash ?? false,
|
|
560
|
-
redirects: nextConfig?.redirects ?? [],
|
|
561
|
-
rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
|
|
562
|
-
headers: nextConfig?.headers ?? [],
|
|
563
|
-
i18n: nextConfig?.i18n ?? null,
|
|
564
|
-
images: {
|
|
565
|
-
deviceSizes: nextConfig?.images?.deviceSizes,
|
|
566
|
-
imageSizes: nextConfig?.images?.imageSizes,
|
|
567
|
-
dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
|
|
568
|
-
contentDispositionType: nextConfig?.images?.contentDispositionType,
|
|
569
|
-
contentSecurityPolicy: nextConfig?.images?.contentSecurityPolicy,
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
// Generate instrumentation code if instrumentation.ts exists.
|
|
573
|
-
// For production (Cloudflare Workers), instrumentation.ts is bundled into the
|
|
574
|
-
// Worker and register() is called as a top-level await at module evaluation time —
|
|
575
|
-
// before any request is handled. This mirrors App Router behavior (generateRscEntry)
|
|
576
|
-
// and matches Next.js semantics: register() runs once on startup in the process
|
|
577
|
-
// that handles requests.
|
|
578
|
-
//
|
|
579
|
-
// The onRequestError handler is stored on globalThis so it is visible across
|
|
580
|
-
// all code within the Worker (same global scope).
|
|
581
|
-
const instrumentationImportCode = instrumentationPath
|
|
582
|
-
? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};`
|
|
583
|
-
: "";
|
|
584
|
-
const instrumentationInitCode = instrumentationPath
|
|
585
|
-
? `// Run instrumentation register() once at module evaluation time — before any
|
|
586
|
-
// requests are handled. Matches Next.js semantics: register() is called once
|
|
587
|
-
// on startup in the process that handles requests.
|
|
588
|
-
if (typeof _instrumentation.register === "function") {
|
|
589
|
-
await _instrumentation.register();
|
|
590
|
-
}
|
|
591
|
-
// Store the onRequestError handler on globalThis so it is visible to all
|
|
592
|
-
// code within the Worker (same global scope).
|
|
593
|
-
if (typeof _instrumentation.onRequestError === "function") {
|
|
594
|
-
globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
|
|
595
|
-
}`
|
|
596
|
-
: "";
|
|
597
|
-
// Generate middleware code if middleware.ts exists
|
|
598
|
-
const middlewareImportCode = middlewarePath
|
|
599
|
-
? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
|
|
600
|
-
import { NextRequest, NextFetchEvent } from "next/server";`
|
|
601
|
-
: "";
|
|
602
|
-
// The matcher config is read from the middleware module at import time.
|
|
603
|
-
// We inline the matching + execution logic so the prod server can call it.
|
|
604
|
-
const middlewareExportCode = middlewarePath
|
|
605
|
-
? `
|
|
606
|
-
// --- Middleware support (generated from middleware-codegen.ts) ---
|
|
607
|
-
${generateNormalizePathCode("es5")}
|
|
608
|
-
${generateSafeRegExpCode("es5")}
|
|
609
|
-
${generateMiddlewareMatcherCode("es5")}
|
|
610
|
-
|
|
611
|
-
export async function runMiddleware(request, ctx) {
|
|
612
|
-
var isProxy = ${middlewarePath ? JSON.stringify(isProxyFile(middlewarePath)) : "false"};
|
|
613
|
-
var middlewareFn = isProxy
|
|
614
|
-
? (middlewareModule.proxy ?? middlewareModule.default)
|
|
615
|
-
: (middlewareModule.middleware ?? middlewareModule.default);
|
|
616
|
-
if (typeof middlewareFn !== "function") {
|
|
617
|
-
var fileType = isProxy ? "Proxy" : "Middleware";
|
|
618
|
-
var expectedExport = isProxy ? "proxy" : "middleware";
|
|
619
|
-
throw new Error("The " + fileType + " file must export a function named \`" + expectedExport + "\` or a \`default\` function.");
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
var config = middlewareModule.config;
|
|
623
|
-
var matcher = config && config.matcher;
|
|
624
|
-
var url = new URL(request.url);
|
|
625
|
-
|
|
626
|
-
// Normalize pathname before matching to prevent path-confusion bypasses
|
|
627
|
-
// (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
|
|
628
|
-
var decodedPathname;
|
|
629
|
-
try { decodedPathname = decodeURIComponent(url.pathname); } catch (e) {
|
|
630
|
-
return { continue: false, response: new Response("Bad Request", { status: 400 }) };
|
|
631
|
-
}
|
|
632
|
-
var normalizedPathname = __normalizePath(decodedPathname);
|
|
633
|
-
|
|
634
|
-
if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
|
|
635
|
-
|
|
636
|
-
// Construct a new Request with the decoded + normalized pathname so middleware
|
|
637
|
-
// always sees the same canonical path that the router uses.
|
|
638
|
-
var mwRequest = request;
|
|
639
|
-
if (normalizedPathname !== url.pathname) {
|
|
640
|
-
var mwUrl = new URL(url);
|
|
641
|
-
mwUrl.pathname = normalizedPathname;
|
|
642
|
-
mwRequest = new Request(mwUrl, request);
|
|
643
|
-
}
|
|
644
|
-
var nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
|
|
645
|
-
var fetchEvent = new NextFetchEvent({ page: normalizedPathname });
|
|
646
|
-
var response;
|
|
647
|
-
try { response = await middlewareFn(nextRequest, fetchEvent); }
|
|
648
|
-
catch (e) {
|
|
649
|
-
console.error("[vinext] Middleware error:", e);
|
|
650
|
-
return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
|
|
651
|
-
}
|
|
652
|
-
if (ctx && typeof ctx.waitUntil === "function") { ctx.waitUntil(fetchEvent.drainWaitUntil()); } else { fetchEvent.drainWaitUntil(); }
|
|
653
|
-
|
|
654
|
-
if (!response) return { continue: true };
|
|
655
|
-
|
|
656
|
-
if (response.headers.get("x-middleware-next") === "1") {
|
|
657
|
-
var rHeaders = new Headers();
|
|
658
|
-
for (var [key, value] of response.headers) {
|
|
659
|
-
// Keep x-middleware-request-* headers so the production server can
|
|
660
|
-
// apply middleware-request header overrides before stripping internals
|
|
661
|
-
// from the final client response.
|
|
662
|
-
if (
|
|
663
|
-
!key.startsWith("x-middleware-") ||
|
|
664
|
-
key.startsWith("x-middleware-request-")
|
|
665
|
-
) rHeaders.append(key, value);
|
|
666
|
-
}
|
|
667
|
-
return { continue: true, responseHeaders: rHeaders };
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (response.status >= 300 && response.status < 400) {
|
|
671
|
-
var location = response.headers.get("Location") || response.headers.get("location");
|
|
672
|
-
if (location) return { continue: false, redirectUrl: location, redirectStatus: response.status };
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
var rewriteUrl = response.headers.get("x-middleware-rewrite");
|
|
676
|
-
if (rewriteUrl) {
|
|
677
|
-
var rwHeaders = new Headers();
|
|
678
|
-
for (var [k, v] of response.headers) {
|
|
679
|
-
if (!k.startsWith("x-middleware-") || k.startsWith("x-middleware-request-")) rwHeaders.append(k, v);
|
|
680
|
-
}
|
|
681
|
-
var rewritePath;
|
|
682
|
-
try { var parsed = new URL(rewriteUrl, request.url); rewritePath = parsed.pathname + parsed.search; }
|
|
683
|
-
catch { rewritePath = rewriteUrl; }
|
|
684
|
-
return { continue: true, rewriteUrl: rewritePath, rewriteStatus: response.status !== 200 ? response.status : undefined, responseHeaders: rwHeaders };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return { continue: false, response: response };
|
|
688
|
-
}
|
|
689
|
-
`
|
|
690
|
-
: `
|
|
691
|
-
export async function runMiddleware() { return { continue: true }; }
|
|
692
|
-
`;
|
|
693
|
-
// The server entry is a self-contained module that uses Web-standard APIs
|
|
694
|
-
// (Request/Response, renderToReadableStream) so it runs on Cloudflare Workers.
|
|
695
|
-
return `
|
|
696
|
-
import React from "react";
|
|
697
|
-
import { renderToReadableStream } from "react-dom/server.edge";
|
|
698
|
-
import { resetSSRHead, getSSRHeadHTML } from "next/head";
|
|
699
|
-
import { flushPreloads } from "next/dynamic";
|
|
700
|
-
import { setSSRContext, wrapWithRouterContext } from "next/router";
|
|
701
|
-
import { getCacheHandler } from "next/cache";
|
|
702
|
-
import { runWithFetchCache } from "vinext/fetch-cache";
|
|
703
|
-
import { _runWithCacheState } from "next/cache";
|
|
704
|
-
import { runWithPrivateCache } from "vinext/cache-runtime";
|
|
705
|
-
import { runWithRouterState } from "vinext/router-state";
|
|
706
|
-
import { runWithHeadState } from "vinext/head-state";
|
|
707
|
-
import { safeJsonStringify } from "vinext/html";
|
|
708
|
-
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
|
|
709
|
-
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
|
|
710
|
-
${instrumentationImportCode}
|
|
711
|
-
${middlewareImportCode}
|
|
712
|
-
|
|
713
|
-
${instrumentationInitCode}
|
|
714
|
-
|
|
715
|
-
// i18n config (embedded at build time)
|
|
716
|
-
const i18nConfig = ${i18nConfigJson};
|
|
717
|
-
|
|
718
|
-
// Full resolved config for production server (embedded at build time)
|
|
719
|
-
export const vinextConfig = ${vinextConfigJson};
|
|
720
|
-
|
|
721
|
-
// ISR cache helpers (inlined for the server entry)
|
|
722
|
-
async function isrGet(key) {
|
|
723
|
-
const handler = getCacheHandler();
|
|
724
|
-
const result = await handler.get(key);
|
|
725
|
-
if (!result || !result.value) return null;
|
|
726
|
-
return { value: result, isStale: result.cacheState === "stale" };
|
|
727
|
-
}
|
|
728
|
-
async function isrSet(key, data, revalidateSeconds, tags) {
|
|
729
|
-
const handler = getCacheHandler();
|
|
730
|
-
await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
|
|
731
|
-
}
|
|
732
|
-
const pendingRegenerations = new Map();
|
|
733
|
-
function triggerBackgroundRegeneration(key, renderFn) {
|
|
734
|
-
if (pendingRegenerations.has(key)) return;
|
|
735
|
-
const promise = renderFn()
|
|
736
|
-
.catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
|
|
737
|
-
.finally(() => pendingRegenerations.delete(key));
|
|
738
|
-
pendingRegenerations.set(key, promise);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
async function renderToStringAsync(element) {
|
|
742
|
-
const stream = await renderToReadableStream(element);
|
|
743
|
-
await stream.allReady;
|
|
744
|
-
return new Response(stream).text();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
${pageImports.join("\n")}
|
|
748
|
-
${apiImports.join("\n")}
|
|
749
|
-
|
|
750
|
-
${appImportCode}
|
|
751
|
-
${docImportCode}
|
|
752
|
-
|
|
753
|
-
const pageRoutes = [
|
|
754
|
-
${pageRouteEntries.join(",\n")}
|
|
755
|
-
];
|
|
756
|
-
|
|
757
|
-
const apiRoutes = [
|
|
758
|
-
${apiRouteEntries.join(",\n")}
|
|
759
|
-
];
|
|
760
|
-
|
|
761
|
-
function matchRoute(url, routes) {
|
|
762
|
-
const pathname = url.split("?")[0];
|
|
763
|
-
let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
|
|
764
|
-
// NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
|
|
765
|
-
// the entry point. Decoding again would create a double-decode vector.
|
|
766
|
-
for (const route of routes) {
|
|
767
|
-
const params = matchPattern(normalizedUrl, route.pattern);
|
|
768
|
-
if (params !== null) return { route, params };
|
|
769
|
-
}
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function matchPattern(url, pattern) {
|
|
774
|
-
const urlParts = url.split("/").filter(Boolean);
|
|
775
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
776
|
-
const params = Object.create(null);
|
|
777
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
778
|
-
const pp = patternParts[i];
|
|
779
|
-
if (pp.endsWith("+")) {
|
|
780
|
-
const paramName = pp.slice(1, -1);
|
|
781
|
-
const remaining = urlParts.slice(i);
|
|
782
|
-
if (remaining.length === 0) return null;
|
|
783
|
-
params[paramName] = remaining;
|
|
784
|
-
return params;
|
|
785
|
-
}
|
|
786
|
-
if (pp.endsWith("*")) {
|
|
787
|
-
const paramName = pp.slice(1, -1);
|
|
788
|
-
params[paramName] = urlParts.slice(i);
|
|
789
|
-
return params;
|
|
790
|
-
}
|
|
791
|
-
if (pp.startsWith(":")) {
|
|
792
|
-
if (i >= urlParts.length) return null;
|
|
793
|
-
params[pp.slice(1)] = urlParts[i];
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
if (i >= urlParts.length || urlParts[i] !== pp) return null;
|
|
797
|
-
}
|
|
798
|
-
if (urlParts.length !== patternParts.length) return null;
|
|
799
|
-
return params;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function parseQuery(url) {
|
|
803
|
-
const qs = url.split("?")[1];
|
|
804
|
-
if (!qs) return {};
|
|
805
|
-
const p = new URLSearchParams(qs);
|
|
806
|
-
const q = {};
|
|
807
|
-
for (const [k, v] of p) {
|
|
808
|
-
if (k in q) {
|
|
809
|
-
q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
|
|
810
|
-
} else {
|
|
811
|
-
q[k] = v;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return q;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function patternToNextFormat(pattern) {
|
|
818
|
-
return pattern
|
|
819
|
-
.replace(/:([\\w]+)\\*/g, "[[...$1]]")
|
|
820
|
-
.replace(/:([\\w]+)\\+/g, "[...$1]")
|
|
821
|
-
.replace(/:([\\w]+)/g, "[$1]");
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function collectAssetTags(manifest, moduleIds) {
|
|
825
|
-
// Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
|
|
826
|
-
const m = (manifest && Object.keys(manifest).length > 0)
|
|
827
|
-
? manifest
|
|
828
|
-
: (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
|
|
829
|
-
const tags = [];
|
|
830
|
-
const seen = new Set();
|
|
831
|
-
|
|
832
|
-
// Load the set of lazy chunk filenames (only reachable via dynamic imports).
|
|
833
|
-
// These should NOT get <link rel="modulepreload"> or <script type="module">
|
|
834
|
-
// tags — they are fetched on demand when the dynamic import() executes (e.g.
|
|
835
|
-
// chunks behind React.lazy() or next/dynamic boundaries).
|
|
836
|
-
var lazyChunks = (typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__) || null;
|
|
837
|
-
var lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
|
|
838
|
-
|
|
839
|
-
// Inject the client entry script if embedded by vinext:cloudflare-build
|
|
840
|
-
if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
|
|
841
|
-
const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
|
|
842
|
-
seen.add(entry);
|
|
843
|
-
tags.push('<link rel="modulepreload" href="/' + entry + '" />');
|
|
844
|
-
tags.push('<script type="module" src="/' + entry + '" crossorigin></script>');
|
|
845
|
-
}
|
|
846
|
-
if (m) {
|
|
847
|
-
// Always inject shared chunks (framework, vinext runtime, entry) and
|
|
848
|
-
// page-specific chunks. The manifest maps module file paths to their
|
|
849
|
-
// associated JS/CSS assets.
|
|
850
|
-
//
|
|
851
|
-
// For page-specific injection, the module IDs may be absolute paths
|
|
852
|
-
// while the manifest uses relative paths. Try both the original ID
|
|
853
|
-
// and a suffix match to find the correct manifest entry.
|
|
854
|
-
var allFiles = [];
|
|
855
|
-
|
|
856
|
-
if (moduleIds && moduleIds.length > 0) {
|
|
857
|
-
// Collect assets for the requested page modules
|
|
858
|
-
for (var mi = 0; mi < moduleIds.length; mi++) {
|
|
859
|
-
var id = moduleIds[mi];
|
|
860
|
-
var files = m[id];
|
|
861
|
-
if (!files) {
|
|
862
|
-
// Absolute path didn't match — try matching by suffix.
|
|
863
|
-
// Manifest keys are relative (e.g. "pages/about.tsx") while
|
|
864
|
-
// moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
|
|
865
|
-
for (var mk in m) {
|
|
866
|
-
if (id.endsWith("/" + mk) || id === mk) {
|
|
867
|
-
files = m[mk];
|
|
868
|
-
break;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
if (files) {
|
|
873
|
-
for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// Also inject shared chunks that every page needs: framework,
|
|
878
|
-
// vinext runtime, and the entry bootstrap. These are identified
|
|
879
|
-
// by scanning all manifest values for chunk filenames containing
|
|
880
|
-
// known prefixes.
|
|
881
|
-
for (var key in m) {
|
|
882
|
-
var vals = m[key];
|
|
883
|
-
if (!vals) continue;
|
|
884
|
-
for (var vi = 0; vi < vals.length; vi++) {
|
|
885
|
-
var file = vals[vi];
|
|
886
|
-
var basename = file.split("/").pop() || "";
|
|
887
|
-
if (
|
|
888
|
-
basename.startsWith("framework-") ||
|
|
889
|
-
basename.startsWith("vinext-") ||
|
|
890
|
-
basename.includes("vinext-client-entry") ||
|
|
891
|
-
basename.includes("vinext-app-browser-entry")
|
|
892
|
-
) {
|
|
893
|
-
allFiles.push(file);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
} else {
|
|
898
|
-
// No specific modules — include all assets from manifest
|
|
899
|
-
for (var akey in m) {
|
|
900
|
-
var avals = m[akey];
|
|
901
|
-
if (avals) {
|
|
902
|
-
for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
for (var ti = 0; ti < allFiles.length; ti++) {
|
|
908
|
-
var tf = allFiles[ti];
|
|
909
|
-
// Normalize: Vite's SSR manifest values include a leading '/'
|
|
910
|
-
// (from base path), but we prepend '/' ourselves when building
|
|
911
|
-
// href/src attributes. Strip any existing leading slash to avoid
|
|
912
|
-
// producing protocol-relative URLs like "//assets/chunk.js".
|
|
913
|
-
// This also ensures consistent keys for the seen-set dedup and
|
|
914
|
-
// lazySet.has() checks (which use values without leading slash).
|
|
915
|
-
if (tf.charAt(0) === '/') tf = tf.slice(1);
|
|
916
|
-
if (seen.has(tf)) continue;
|
|
917
|
-
seen.add(tf);
|
|
918
|
-
if (tf.endsWith(".css")) {
|
|
919
|
-
tags.push('<link rel="stylesheet" href="/' + tf + '" />');
|
|
920
|
-
} else if (tf.endsWith(".js")) {
|
|
921
|
-
// Skip lazy chunks — they are behind dynamic import() boundaries
|
|
922
|
-
// (React.lazy, next/dynamic) and should only be fetched on demand.
|
|
923
|
-
if (lazySet && lazySet.has(tf)) continue;
|
|
924
|
-
tags.push('<link rel="modulepreload" href="/' + tf + '" />');
|
|
925
|
-
tags.push('<script type="module" src="/' + tf + '" crossorigin></script>');
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
return tags.join("\\n ");
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// i18n helpers
|
|
933
|
-
function extractLocale(url) {
|
|
934
|
-
if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
|
|
935
|
-
const pathname = url.split("?")[0];
|
|
936
|
-
const parts = pathname.split("/").filter(Boolean);
|
|
937
|
-
const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
938
|
-
if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
|
|
939
|
-
const locale = parts[0];
|
|
940
|
-
const rest = "/" + parts.slice(1).join("/");
|
|
941
|
-
return { locale, url: (rest || "/") + query, hadPrefix: true };
|
|
942
|
-
}
|
|
943
|
-
return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function detectLocaleFromHeaders(headers) {
|
|
947
|
-
if (!i18nConfig) return null;
|
|
948
|
-
const acceptLang = headers.get("accept-language");
|
|
949
|
-
if (!acceptLang) return null;
|
|
950
|
-
const langs = acceptLang.split(",").map(function(part) {
|
|
951
|
-
const pieces = part.trim().split(";");
|
|
952
|
-
const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
|
|
953
|
-
return { lang: pieces[0].trim().toLowerCase(), q: q };
|
|
954
|
-
}).sort(function(a, b) { return b.q - a.q; });
|
|
955
|
-
for (let k = 0; k < langs.length; k++) {
|
|
956
|
-
const lang = langs[k].lang;
|
|
957
|
-
for (let j = 0; j < i18nConfig.locales.length; j++) {
|
|
958
|
-
if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
|
|
959
|
-
}
|
|
960
|
-
const prefix = lang.split("-")[0];
|
|
961
|
-
for (let j = 0; j < i18nConfig.locales.length; j++) {
|
|
962
|
-
const loc = i18nConfig.locales[j].toLowerCase();
|
|
963
|
-
if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
return null;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function parseCookieLocaleFromHeader(cookieHeader) {
|
|
970
|
-
if (!i18nConfig || !cookieHeader) return null;
|
|
971
|
-
const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
|
|
972
|
-
if (!match) return null;
|
|
973
|
-
var value;
|
|
974
|
-
try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
|
|
975
|
-
if (i18nConfig.locales.indexOf(value) !== -1) return value;
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function parseCookies(cookieHeader) {
|
|
980
|
-
const cookies = {};
|
|
981
|
-
if (!cookieHeader) return cookies;
|
|
982
|
-
for (const part of cookieHeader.split(";")) {
|
|
983
|
-
const [key, ...rest] = part.split("=");
|
|
984
|
-
if (key) cookies[key.trim()] = rest.join("=").trim();
|
|
985
|
-
}
|
|
986
|
-
return cookies;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Lightweight req/res facade for getServerSideProps and API routes.
|
|
990
|
-
// Next.js pages expect ctx.req/ctx.res with Node-like shapes.
|
|
991
|
-
function createReqRes(request, url, query, body) {
|
|
992
|
-
const headersObj = {};
|
|
993
|
-
for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
|
|
994
|
-
|
|
995
|
-
const req = {
|
|
996
|
-
method: request.method,
|
|
997
|
-
url: url,
|
|
998
|
-
headers: headersObj,
|
|
999
|
-
query: query,
|
|
1000
|
-
body: body,
|
|
1001
|
-
cookies: parseCookies(request.headers.get("cookie")),
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
let resStatusCode = 200;
|
|
1005
|
-
const resHeaders = {};
|
|
1006
|
-
// set-cookie needs array support (multiple Set-Cookie headers are common)
|
|
1007
|
-
const setCookieHeaders = [];
|
|
1008
|
-
let resBody = null;
|
|
1009
|
-
let ended = false;
|
|
1010
|
-
let resolveResponse;
|
|
1011
|
-
const responsePromise = new Promise(function(r) { resolveResponse = r; });
|
|
1012
|
-
|
|
1013
|
-
const res = {
|
|
1014
|
-
get statusCode() { return resStatusCode; },
|
|
1015
|
-
set statusCode(code) { resStatusCode = code; },
|
|
1016
|
-
writeHead: function(code, headers) {
|
|
1017
|
-
resStatusCode = code;
|
|
1018
|
-
if (headers) {
|
|
1019
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
1020
|
-
if (k.toLowerCase() === "set-cookie") {
|
|
1021
|
-
if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
|
|
1022
|
-
else { setCookieHeaders.push(v); }
|
|
1023
|
-
} else {
|
|
1024
|
-
resHeaders[k] = v;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
return res;
|
|
1029
|
-
},
|
|
1030
|
-
setHeader: function(name, value) {
|
|
1031
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
1032
|
-
if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
|
|
1033
|
-
else { setCookieHeaders.push(value); }
|
|
1034
|
-
} else {
|
|
1035
|
-
resHeaders[name.toLowerCase()] = value;
|
|
1036
|
-
}
|
|
1037
|
-
return res;
|
|
1038
|
-
},
|
|
1039
|
-
getHeader: function(name) {
|
|
1040
|
-
if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
|
|
1041
|
-
return resHeaders[name.toLowerCase()];
|
|
1042
|
-
},
|
|
1043
|
-
end: function(data) {
|
|
1044
|
-
if (ended) return;
|
|
1045
|
-
ended = true;
|
|
1046
|
-
if (data !== undefined && data !== null) resBody = data;
|
|
1047
|
-
const h = new Headers(resHeaders);
|
|
1048
|
-
for (const c of setCookieHeaders) h.append("set-cookie", c);
|
|
1049
|
-
resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
|
|
1050
|
-
},
|
|
1051
|
-
status: function(code) { resStatusCode = code; return res; },
|
|
1052
|
-
json: function(data) {
|
|
1053
|
-
resHeaders["content-type"] = "application/json";
|
|
1054
|
-
res.end(JSON.stringify(data));
|
|
1055
|
-
},
|
|
1056
|
-
send: function(data) {
|
|
1057
|
-
if (typeof data === "object" && data !== null) { res.json(data); }
|
|
1058
|
-
else { if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain"; res.end(String(data)); }
|
|
1059
|
-
},
|
|
1060
|
-
redirect: function(statusOrUrl, url2) {
|
|
1061
|
-
if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
|
|
1062
|
-
else { res.writeHead(statusOrUrl, { Location: url2 }); }
|
|
1063
|
-
res.end();
|
|
1064
|
-
},
|
|
1065
|
-
getHeaders: function() {
|
|
1066
|
-
var h = Object.assign({}, resHeaders);
|
|
1067
|
-
if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
|
|
1068
|
-
return h;
|
|
1069
|
-
},
|
|
1070
|
-
get headersSent() { return ended; },
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
return { req, res, responsePromise };
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
/**
|
|
1077
|
-
* Read request body as text with a size limit.
|
|
1078
|
-
* Throws if the body exceeds maxBytes. This prevents DoS via chunked
|
|
1079
|
-
* transfer encoding where Content-Length is absent or spoofed.
|
|
1080
|
-
*/
|
|
1081
|
-
async function readBodyWithLimit(request, maxBytes) {
|
|
1082
|
-
if (!request.body) return "";
|
|
1083
|
-
var reader = request.body.getReader();
|
|
1084
|
-
var decoder = new TextDecoder();
|
|
1085
|
-
var chunks = [];
|
|
1086
|
-
var totalSize = 0;
|
|
1087
|
-
for (;;) {
|
|
1088
|
-
var result = await reader.read();
|
|
1089
|
-
if (result.done) break;
|
|
1090
|
-
totalSize += result.value.byteLength;
|
|
1091
|
-
if (totalSize > maxBytes) {
|
|
1092
|
-
reader.cancel();
|
|
1093
|
-
throw new Error("Request body too large");
|
|
1094
|
-
}
|
|
1095
|
-
chunks.push(decoder.decode(result.value, { stream: true }));
|
|
1096
|
-
}
|
|
1097
|
-
chunks.push(decoder.decode());
|
|
1098
|
-
return chunks.join("");
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
export async function renderPage(request, url, manifest) {
|
|
1102
|
-
const localeInfo = extractLocale(url);
|
|
1103
|
-
const locale = localeInfo.locale;
|
|
1104
|
-
const routeUrl = localeInfo.url;
|
|
1105
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1106
|
-
|
|
1107
|
-
// i18n redirect: check NEXT_LOCALE cookie first, then Accept-Language
|
|
1108
|
-
if (i18nConfig && !localeInfo.hadPrefix) {
|
|
1109
|
-
const cookieLocale = parseCookieLocaleFromHeader(cookieHeader);
|
|
1110
|
-
if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
|
|
1111
|
-
return new Response(null, { status: 307, headers: { Location: "/" + cookieLocale + routeUrl } });
|
|
1112
|
-
}
|
|
1113
|
-
if (!cookieLocale && i18nConfig.localeDetection !== false) {
|
|
1114
|
-
const detected = detectLocaleFromHeaders(request.headers);
|
|
1115
|
-
if (detected && detected !== i18nConfig.defaultLocale) {
|
|
1116
|
-
return new Response(null, { status: 307, headers: { Location: "/" + detected + routeUrl } });
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const match = matchRoute(routeUrl, pageRoutes);
|
|
1122
|
-
if (!match) {
|
|
1123
|
-
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
|
|
1124
|
-
{ status: 404, headers: { "Content-Type": "text/html" } });
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const { route, params } = match;
|
|
1128
|
-
return runWithRouterState(() =>
|
|
1129
|
-
runWithHeadState(() =>
|
|
1130
|
-
_runWithCacheState(() =>
|
|
1131
|
-
runWithPrivateCache(() =>
|
|
1132
|
-
runWithFetchCache(async () => {
|
|
1133
|
-
try {
|
|
1134
|
-
if (typeof setSSRContext === "function") {
|
|
1135
|
-
setSSRContext({
|
|
1136
|
-
pathname: routeUrl.split("?")[0],
|
|
1137
|
-
query: { ...params, ...parseQuery(routeUrl) },
|
|
1138
|
-
asPath: routeUrl,
|
|
1139
|
-
locale: locale,
|
|
1140
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1141
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
if (i18nConfig) {
|
|
1146
|
-
globalThis.__VINEXT_LOCALE__ = locale;
|
|
1147
|
-
globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
|
|
1148
|
-
globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
const pageModule = route.module;
|
|
1152
|
-
const PageComponent = pageModule.default;
|
|
1153
|
-
if (!PageComponent) {
|
|
1154
|
-
return new Response("Page has no default export", { status: 500 });
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Handle getStaticPaths for dynamic routes
|
|
1158
|
-
if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
|
|
1159
|
-
const pathsResult = await pageModule.getStaticPaths({
|
|
1160
|
-
locales: i18nConfig ? i18nConfig.locales : [],
|
|
1161
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : "",
|
|
1162
|
-
});
|
|
1163
|
-
const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
|
|
1164
|
-
|
|
1165
|
-
if (fallback === false) {
|
|
1166
|
-
const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
|
|
1167
|
-
const isValidPath = paths.some(function(p) {
|
|
1168
|
-
return Object.entries(p.params).every(function(entry) {
|
|
1169
|
-
var key = entry[0], val = entry[1];
|
|
1170
|
-
var actual = params[key];
|
|
1171
|
-
if (Array.isArray(val)) {
|
|
1172
|
-
return Array.isArray(actual) && val.join("/") === actual.join("/");
|
|
1173
|
-
}
|
|
1174
|
-
return String(val) === String(actual);
|
|
1175
|
-
});
|
|
1176
|
-
});
|
|
1177
|
-
if (!isValidPath) {
|
|
1178
|
-
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
|
|
1179
|
-
{ status: 404, headers: { "Content-Type": "text/html" } });
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
let pageProps = {};
|
|
1185
|
-
var gsspRes = null;
|
|
1186
|
-
if (typeof pageModule.getServerSideProps === "function") {
|
|
1187
|
-
const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1188
|
-
const ctx = {
|
|
1189
|
-
params, req, res,
|
|
1190
|
-
query: parseQuery(routeUrl),
|
|
1191
|
-
resolvedUrl: routeUrl,
|
|
1192
|
-
locale: locale,
|
|
1193
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1194
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1195
|
-
};
|
|
1196
|
-
const result = await pageModule.getServerSideProps(ctx);
|
|
1197
|
-
// If gSSP called res.end() directly (short-circuit), return that response.
|
|
1198
|
-
if (res.headersSent) {
|
|
1199
|
-
return await responsePromise;
|
|
1200
|
-
}
|
|
1201
|
-
if (result && result.props) pageProps = result.props;
|
|
1202
|
-
if (result && result.redirect) {
|
|
1203
|
-
var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1204
|
-
return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1205
|
-
}
|
|
1206
|
-
if (result && result.notFound) {
|
|
1207
|
-
return new Response("404", { status: 404 });
|
|
1208
|
-
}
|
|
1209
|
-
// Preserve the res object so headers/status/cookies set by gSSP
|
|
1210
|
-
// can be merged into the final HTML response.
|
|
1211
|
-
gsspRes = res;
|
|
1212
|
-
}
|
|
1213
|
-
// Build font Link header early so it's available for ISR cached responses too.
|
|
1214
|
-
// Font preloads are module-level state populated at import time and persist across requests.
|
|
1215
|
-
var _fontLinkHeader = "";
|
|
1216
|
-
var _allFp = [];
|
|
1217
|
-
try {
|
|
1218
|
-
var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
|
|
1219
|
-
var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
|
|
1220
|
-
_allFp = _fpGoogle.concat(_fpLocal);
|
|
1221
|
-
if (_allFp.length > 0) {
|
|
1222
|
-
_fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
|
|
1223
|
-
}
|
|
1224
|
-
} catch (e) { /* font preloads not available */ }
|
|
1225
|
-
|
|
1226
|
-
let isrRevalidateSeconds = null;
|
|
1227
|
-
if (typeof pageModule.getStaticProps === "function") {
|
|
1228
|
-
const pathname = routeUrl.split("?")[0];
|
|
1229
|
-
const cacheKey = "pages:" + (pathname === "/" ? "/" : pathname.replace(/\\/$/, ""));
|
|
1230
|
-
const cached = await isrGet(cacheKey);
|
|
1231
|
-
|
|
1232
|
-
if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
|
|
1233
|
-
var _hitHeaders = {
|
|
1234
|
-
"Content-Type": "text/html", "X-Vinext-Cache": "HIT",
|
|
1235
|
-
"Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
|
|
1236
|
-
};
|
|
1237
|
-
if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
|
|
1238
|
-
return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
|
|
1242
|
-
triggerBackgroundRegeneration(cacheKey, async function() {
|
|
1243
|
-
const freshResult = await pageModule.getStaticProps({ params });
|
|
1244
|
-
if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
|
|
1245
|
-
await isrSet(cacheKey, { kind: "PAGES", html: cached.value.value.html, pageData: freshResult.props, headers: undefined, status: undefined }, freshResult.revalidate);
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
var _staleHeaders = {
|
|
1249
|
-
"Content-Type": "text/html", "X-Vinext-Cache": "STALE",
|
|
1250
|
-
"Cache-Control": "s-maxage=0, stale-while-revalidate",
|
|
1251
|
-
};
|
|
1252
|
-
if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
|
|
1253
|
-
return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
const ctx = {
|
|
1257
|
-
params,
|
|
1258
|
-
locale: locale,
|
|
1259
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1260
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1261
|
-
};
|
|
1262
|
-
const result = await pageModule.getStaticProps(ctx);
|
|
1263
|
-
if (result && result.props) pageProps = result.props;
|
|
1264
|
-
if (result && result.redirect) {
|
|
1265
|
-
var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1266
|
-
return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1267
|
-
}
|
|
1268
|
-
if (result && result.notFound) {
|
|
1269
|
-
return new Response("404", { status: 404 });
|
|
1270
|
-
}
|
|
1271
|
-
if (typeof result.revalidate === "number" && result.revalidate > 0) {
|
|
1272
|
-
isrRevalidateSeconds = result.revalidate;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
let element;
|
|
1277
|
-
if (AppComponent) {
|
|
1278
|
-
element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1279
|
-
} else {
|
|
1280
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1281
|
-
}
|
|
1282
|
-
element = wrapWithRouterContext(element);
|
|
1283
|
-
|
|
1284
|
-
if (typeof resetSSRHead === "function") resetSSRHead();
|
|
1285
|
-
if (typeof flushPreloads === "function") await flushPreloads();
|
|
1286
|
-
|
|
1287
|
-
const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
|
|
1288
|
-
|
|
1289
|
-
// Collect SSR font data (Google Font links, font preloads, font-face styles)
|
|
1290
|
-
var fontHeadHTML = "";
|
|
1291
|
-
function _escAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """); }
|
|
1292
|
-
try {
|
|
1293
|
-
var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
|
|
1294
|
-
for (var fl of fontLinks) { fontHeadHTML += '<link rel="stylesheet" href="' + _escAttr(fl) + '" />\\n '; }
|
|
1295
|
-
} catch (e) { /* next/font/google not used */ }
|
|
1296
|
-
// Emit <link rel="preload"> for all font files (reuse _allFp collected earlier for Link header)
|
|
1297
|
-
for (var fp of _allFp) { fontHeadHTML += '<link rel="preload" href="' + _escAttr(fp.href) + '" as="font" type="' + _escAttr(fp.type) + '" crossorigin />\\n '; }
|
|
1298
|
-
try {
|
|
1299
|
-
var allFontStyles = [];
|
|
1300
|
-
if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
|
|
1301
|
-
if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
|
|
1302
|
-
if (allFontStyles.length > 0) { fontHeadHTML += '<style data-vinext-fonts>' + allFontStyles.join("\\n") + '</style>\\n '; }
|
|
1303
|
-
} catch (e) { /* font styles not available */ }
|
|
1304
|
-
|
|
1305
|
-
const pageModuleIds = route.filePath ? [route.filePath] : [];
|
|
1306
|
-
const assetTags = collectAssetTags(manifest, pageModuleIds);
|
|
1307
|
-
const nextDataPayload = {
|
|
1308
|
-
props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, isFallback: false,
|
|
1309
|
-
};
|
|
1310
|
-
if (i18nConfig) {
|
|
1311
|
-
nextDataPayload.locale = locale;
|
|
1312
|
-
nextDataPayload.locales = i18nConfig.locales;
|
|
1313
|
-
nextDataPayload.defaultLocale = i18nConfig.defaultLocale;
|
|
1314
|
-
}
|
|
1315
|
-
const localeGlobals = i18nConfig
|
|
1316
|
-
? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
|
|
1317
|
-
";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
|
|
1318
|
-
";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(i18nConfig.defaultLocale)
|
|
1319
|
-
: "";
|
|
1320
|
-
const nextDataScript = "<script>window.__NEXT_DATA__ = " + safeJsonStringify(nextDataPayload) + localeGlobals + "</script>";
|
|
1321
|
-
|
|
1322
|
-
// Build the document shell with a placeholder for the streamed body
|
|
1323
|
-
var BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
|
|
1324
|
-
var shellHtml;
|
|
1325
|
-
if (DocumentComponent) {
|
|
1326
|
-
const docElement = React.createElement(DocumentComponent);
|
|
1327
|
-
shellHtml = await renderToStringAsync(docElement);
|
|
1328
|
-
shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
|
|
1329
|
-
if (ssrHeadHTML || assetTags || fontHeadHTML) {
|
|
1330
|
-
shellHtml = shellHtml.replace("</head>", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>");
|
|
1331
|
-
}
|
|
1332
|
-
shellHtml = shellHtml.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
|
|
1333
|
-
if (!shellHtml.includes("__NEXT_DATA__")) {
|
|
1334
|
-
shellHtml = shellHtml.replace("</body>", " " + nextDataScript + "\\n</body>");
|
|
1335
|
-
}
|
|
1336
|
-
} else {
|
|
1337
|
-
shellHtml = "<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"utf-8\\" />\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />\\n " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>\\n<body>\\n <div id=\\"__next\\">" + BODY_MARKER + "</div>\\n " + nextDataScript + "\\n</body>\\n</html>";
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (typeof setSSRContext === "function") setSSRContext(null);
|
|
1341
|
-
|
|
1342
|
-
// Split the shell at the body marker
|
|
1343
|
-
var markerIdx = shellHtml.indexOf(BODY_MARKER);
|
|
1344
|
-
var shellPrefix = shellHtml.slice(0, markerIdx);
|
|
1345
|
-
var shellSuffix = shellHtml.slice(markerIdx + BODY_MARKER.length);
|
|
1346
|
-
|
|
1347
|
-
// Start the React body stream — progressive SSR (no allReady wait)
|
|
1348
|
-
var bodyStream = await renderToReadableStream(element);
|
|
1349
|
-
var encoder = new TextEncoder();
|
|
1350
|
-
|
|
1351
|
-
// Create a composite stream: prefix + body + suffix
|
|
1352
|
-
var compositeStream = new ReadableStream({
|
|
1353
|
-
async start(controller) {
|
|
1354
|
-
controller.enqueue(encoder.encode(shellPrefix));
|
|
1355
|
-
var reader = bodyStream.getReader();
|
|
1356
|
-
try {
|
|
1357
|
-
for (;;) {
|
|
1358
|
-
var chunk = await reader.read();
|
|
1359
|
-
if (chunk.done) break;
|
|
1360
|
-
controller.enqueue(chunk.value);
|
|
1361
|
-
}
|
|
1362
|
-
} finally {
|
|
1363
|
-
reader.releaseLock();
|
|
1364
|
-
}
|
|
1365
|
-
controller.enqueue(encoder.encode(shellSuffix));
|
|
1366
|
-
controller.close();
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
// Cache the rendered HTML for ISR (needs the full string — re-render synchronously)
|
|
1371
|
-
if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
|
|
1372
|
-
// Tee the stream so we can cache and respond simultaneously would be ideal,
|
|
1373
|
-
// but ISR responses are rare on first hit. Re-render to get complete HTML for cache.
|
|
1374
|
-
var isrElement;
|
|
1375
|
-
if (AppComponent) {
|
|
1376
|
-
isrElement = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1377
|
-
} else {
|
|
1378
|
-
isrElement = React.createElement(PageComponent, pageProps);
|
|
1379
|
-
}
|
|
1380
|
-
isrElement = wrapWithRouterContext(isrElement);
|
|
1381
|
-
var isrHtml = await renderToStringAsync(isrElement);
|
|
1382
|
-
var fullHtml = shellPrefix + isrHtml + shellSuffix;
|
|
1383
|
-
var isrPathname = url.split("?")[0];
|
|
1384
|
-
var isrCacheKey = "pages:" + (isrPathname === "/" ? "/" : isrPathname.replace(/\\/$/, ""));
|
|
1385
|
-
await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// Merge headers/status/cookies set by getServerSideProps on the res object.
|
|
1389
|
-
// gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
|
|
1390
|
-
var finalStatus = 200;
|
|
1391
|
-
const responseHeaders = new Headers({ "Content-Type": "text/html" });
|
|
1392
|
-
if (gsspRes) {
|
|
1393
|
-
finalStatus = gsspRes.statusCode;
|
|
1394
|
-
var gsspHeaders = gsspRes.getHeaders();
|
|
1395
|
-
for (var hk of Object.keys(gsspHeaders)) {
|
|
1396
|
-
var hv = gsspHeaders[hk];
|
|
1397
|
-
if (hk === "set-cookie" && Array.isArray(hv)) {
|
|
1398
|
-
for (var sc of hv) responseHeaders.append("set-cookie", sc);
|
|
1399
|
-
} else if (hv != null) {
|
|
1400
|
-
responseHeaders.set(hk, String(hv));
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
// Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
|
|
1404
|
-
responseHeaders.set("Content-Type", "text/html");
|
|
1405
|
-
}
|
|
1406
|
-
if (isrRevalidateSeconds) {
|
|
1407
|
-
responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
|
|
1408
|
-
responseHeaders.set("X-Vinext-Cache", "MISS");
|
|
1409
|
-
}
|
|
1410
|
-
// Set HTTP Link header for font preloading
|
|
1411
|
-
if (_fontLinkHeader) {
|
|
1412
|
-
responseHeaders.set("Link", _fontLinkHeader);
|
|
1413
|
-
}
|
|
1414
|
-
return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
|
|
1415
|
-
} catch (e) {
|
|
1416
|
-
console.error("[vinext] SSR error:", e);
|
|
1417
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
1418
|
-
}
|
|
1419
|
-
}) // end runWithFetchCache
|
|
1420
|
-
) // end runWithPrivateCache
|
|
1421
|
-
) // end _runWithCacheState
|
|
1422
|
-
) // end runWithHeadState
|
|
1423
|
-
); // end runWithRouterState
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
export async function handleApiRoute(request, url) {
|
|
1427
|
-
const match = matchRoute(url, apiRoutes);
|
|
1428
|
-
if (!match) {
|
|
1429
|
-
return new Response("404 - API route not found", { status: 404 });
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
const { route, params } = match;
|
|
1433
|
-
const handler = route.module.default;
|
|
1434
|
-
if (typeof handler !== "function") {
|
|
1435
|
-
return new Response("API route does not export a default function", { status: 500 });
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
const query = { ...params };
|
|
1439
|
-
const qs = url.split("?")[1];
|
|
1440
|
-
if (qs) {
|
|
1441
|
-
for (const [k, v] of new URLSearchParams(qs)) {
|
|
1442
|
-
if (k in query) {
|
|
1443
|
-
// Multi-value: promote to array (Next.js returns string[] for duplicate keys)
|
|
1444
|
-
query[k] = Array.isArray(query[k]) ? query[k].concat(v) : [query[k], v];
|
|
1445
|
-
} else {
|
|
1446
|
-
query[k] = v;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
// Parse request body (enforce 1MB limit to prevent memory exhaustion,
|
|
1452
|
-
// matching Next.js default bodyParser sizeLimit).
|
|
1453
|
-
// Check Content-Length first as a fast path, then enforce on the actual
|
|
1454
|
-
// stream to prevent bypasses via chunked transfer encoding.
|
|
1455
|
-
const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
|
|
1456
|
-
if (contentLength > 1 * 1024 * 1024) {
|
|
1457
|
-
return new Response("Request body too large", { status: 413 });
|
|
1458
|
-
}
|
|
1459
|
-
let body;
|
|
1460
|
-
const ct = request.headers.get("content-type") || "";
|
|
1461
|
-
let rawBody;
|
|
1462
|
-
try { rawBody = await readBodyWithLimit(request, 1 * 1024 * 1024); }
|
|
1463
|
-
catch { return new Response("Request body too large", { status: 413 }); }
|
|
1464
|
-
if (!rawBody) {
|
|
1465
|
-
body = undefined;
|
|
1466
|
-
} else if (ct.includes("application/json")) {
|
|
1467
|
-
try { body = JSON.parse(rawBody); } catch { body = rawBody; }
|
|
1468
|
-
} else {
|
|
1469
|
-
body = rawBody;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const { req, res, responsePromise } = createReqRes(request, url, query, body);
|
|
1473
|
-
|
|
1474
|
-
try {
|
|
1475
|
-
await handler(req, res);
|
|
1476
|
-
// If handler didn't call res.end(), end it now.
|
|
1477
|
-
// The end() method is idempotent — safe to call twice.
|
|
1478
|
-
res.end();
|
|
1479
|
-
return await responsePromise;
|
|
1480
|
-
} catch (e) {
|
|
1481
|
-
console.error("[vinext] API error:", e);
|
|
1482
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
${middlewareExportCode}
|
|
1487
|
-
`;
|
|
526
|
+
return _generateServerEntry(pagesDir, nextConfig, fileMatcher, middlewarePath, instrumentationPath);
|
|
1488
527
|
}
|
|
1489
528
|
/**
|
|
1490
529
|
* Generate the virtual client hydration entry module.
|
|
@@ -1495,84 +534,7 @@ ${middlewareExportCode}
|
|
|
1495
534
|
* __NEXT_DATA__ to determine which page to hydrate.
|
|
1496
535
|
*/
|
|
1497
536
|
async function generateClientEntry() {
|
|
1498
|
-
|
|
1499
|
-
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
1500
|
-
const hasApp = appFilePath !== null;
|
|
1501
|
-
// Build a map of route pattern -> dynamic import.
|
|
1502
|
-
// Keys must use Next.js bracket format (e.g. "/user/[id]") to match
|
|
1503
|
-
// __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
|
|
1504
|
-
const loaderEntries = pageRoutes.map((r) => {
|
|
1505
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
1506
|
-
const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
|
|
1507
|
-
// JSON.stringify safely escapes quotes, backslashes, and special chars in
|
|
1508
|
-
// both the route pattern and the absolute file path.
|
|
1509
|
-
// lgtm[js/bad-code-sanitization]
|
|
1510
|
-
return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
|
|
1511
|
-
});
|
|
1512
|
-
const appFileBase = appFilePath?.replace(/\\/g, "/");
|
|
1513
|
-
return `
|
|
1514
|
-
import React from "react";
|
|
1515
|
-
import { hydrateRoot } from "react-dom/client";
|
|
1516
|
-
// Eagerly import the router shim so its module-level popstate listener is
|
|
1517
|
-
// registered. Without this, browser back/forward buttons do nothing because
|
|
1518
|
-
// navigateClient() is never invoked on history changes.
|
|
1519
|
-
import "next/router";
|
|
1520
|
-
|
|
1521
|
-
const pageLoaders = {
|
|
1522
|
-
${loaderEntries.join(",\n")}
|
|
1523
|
-
};
|
|
1524
|
-
|
|
1525
|
-
async function hydrate() {
|
|
1526
|
-
const nextData = window.__NEXT_DATA__;
|
|
1527
|
-
if (!nextData) {
|
|
1528
|
-
console.error("[vinext] No __NEXT_DATA__ found");
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
const { pageProps } = nextData.props;
|
|
1533
|
-
const loader = pageLoaders[nextData.page];
|
|
1534
|
-
if (!loader) {
|
|
1535
|
-
console.error("[vinext] No page loader for route:", nextData.page);
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
const pageModule = await loader();
|
|
1540
|
-
const PageComponent = pageModule.default;
|
|
1541
|
-
if (!PageComponent) {
|
|
1542
|
-
console.error("[vinext] Page module has no default export");
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
let element;
|
|
1547
|
-
${hasApp ? `
|
|
1548
|
-
try {
|
|
1549
|
-
const appModule = await import(${JSON.stringify(appFileBase)});
|
|
1550
|
-
const AppComponent = appModule.default;
|
|
1551
|
-
window.__VINEXT_APP__ = AppComponent;
|
|
1552
|
-
element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1553
|
-
} catch {
|
|
1554
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1555
|
-
}
|
|
1556
|
-
` : `
|
|
1557
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1558
|
-
`}
|
|
1559
|
-
|
|
1560
|
-
// Wrap with RouterContext.Provider so next/compat/router works during hydration
|
|
1561
|
-
const { wrapWithRouterContext } = await import("next/router");
|
|
1562
|
-
element = wrapWithRouterContext(element);
|
|
1563
|
-
|
|
1564
|
-
const container = document.getElementById("__next");
|
|
1565
|
-
if (!container) {
|
|
1566
|
-
console.error("[vinext] No #__next element found");
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const root = hydrateRoot(container, element);
|
|
1571
|
-
window.__VINEXT_ROOT__ = root;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
hydrate();
|
|
1575
|
-
`;
|
|
537
|
+
return _generateClientEntry(pagesDir, nextConfig, fileMatcher);
|
|
1576
538
|
}
|
|
1577
539
|
// Auto-register @vitejs/plugin-rsc when App Router is detected.
|
|
1578
540
|
// Check eagerly at call time using the same heuristic as config().
|
|
@@ -1611,11 +573,12 @@ hydrate();
|
|
|
1611
573
|
if (earlyAppDirExists && autoRsc) {
|
|
1612
574
|
if (!resolvedRscPath) {
|
|
1613
575
|
throw new Error("vinext: App Router detected but @vitejs/plugin-rsc is not installed.\n" +
|
|
1614
|
-
"Run: " +
|
|
576
|
+
"Run: " +
|
|
577
|
+
detectPackageManager(process.cwd()) +
|
|
578
|
+
" @vitejs/plugin-rsc");
|
|
1615
579
|
}
|
|
1616
580
|
const rscImport = import(pathToFileURL(resolvedRscPath).href);
|
|
1617
|
-
rscPluginPromise = rscImport
|
|
1618
|
-
.then((mod) => {
|
|
581
|
+
rscPluginPromise = rscImport.then((mod) => {
|
|
1619
582
|
const rsc = mod.default;
|
|
1620
583
|
return rsc({
|
|
1621
584
|
entries: {
|
|
@@ -1630,9 +593,7 @@ hydrate();
|
|
|
1630
593
|
// Shared state for the MDX proxy plugin. Populated during config() if MDX
|
|
1631
594
|
// files are detected and @mdx-js/rollup is installed.
|
|
1632
595
|
let mdxDelegate = null;
|
|
1633
|
-
const reactPlugin = options.react === false
|
|
1634
|
-
? false
|
|
1635
|
-
: react(options.react === true ? undefined : options.react);
|
|
596
|
+
const reactPlugin = options.react === false ? false : react(options.react === true ? undefined : options.react);
|
|
1636
597
|
const plugins = [
|
|
1637
598
|
// Resolve tsconfig paths/baseUrl aliases so real-world Next.js repos
|
|
1638
599
|
// that use @/*, #/*, or baseUrl imports work out of the box.
|
|
@@ -1734,7 +695,9 @@ hydrate();
|
|
|
1734
695
|
// server-side validation. Matches Next.js behavior: only configured
|
|
1735
696
|
// sizes are accepted by the image optimization endpoint.
|
|
1736
697
|
{
|
|
1737
|
-
const deviceSizes = nextConfig.images?.deviceSizes ?? [
|
|
698
|
+
const deviceSizes = nextConfig.images?.deviceSizes ?? [
|
|
699
|
+
640, 750, 828, 1080, 1200, 1920, 2048, 3840,
|
|
700
|
+
];
|
|
1738
701
|
const imageSizes = nextConfig.images?.imageSizes ?? [16, 32, 48, 64, 96, 128, 256, 384];
|
|
1739
702
|
defines["process.env.__VINEXT_IMAGE_DEVICE_SIZES"] = JSON.stringify(JSON.stringify(deviceSizes));
|
|
1740
703
|
defines["process.env.__VINEXT_IMAGE_SIZES"] = JSON.stringify(JSON.stringify(imageSizes));
|
|
@@ -1746,6 +709,9 @@ hydrate();
|
|
|
1746
709
|
// __prerender_bypass cookie is consistent across all server
|
|
1747
710
|
// instances (e.g. multiple Cloudflare Workers isolates).
|
|
1748
711
|
defines["process.env.__VINEXT_DRAFT_SECRET"] = JSON.stringify(crypto.randomUUID());
|
|
712
|
+
// Build ID — resolved from next.config generateBuildId() or random UUID.
|
|
713
|
+
// Exposed so server entries and the next/server shim can inject it.
|
|
714
|
+
defines["process.env.__VINEXT_BUILD_ID"] = JSON.stringify(nextConfig.buildId);
|
|
1749
715
|
// Build the shim alias map — used by both resolve.alias and resolveId
|
|
1750
716
|
// (resolveId handles .js extension variants for libraries like nuqs)
|
|
1751
717
|
nextShimMap = {
|
|
@@ -1815,8 +781,14 @@ hydrate();
|
|
|
1815
781
|
}
|
|
1816
782
|
}
|
|
1817
783
|
flattenPlugins(config.plugins ?? []);
|
|
1818
|
-
hasCloudflarePlugin = pluginsFlat.some((p) => p &&
|
|
1819
|
-
|
|
784
|
+
hasCloudflarePlugin = pluginsFlat.some((p) => p &&
|
|
785
|
+
typeof p === "object" &&
|
|
786
|
+
typeof p.name === "string" &&
|
|
787
|
+
(p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")));
|
|
788
|
+
hasNitroPlugin = pluginsFlat.some((p) => p &&
|
|
789
|
+
typeof p === "object" &&
|
|
790
|
+
typeof p.name === "string" &&
|
|
791
|
+
(p.name === "nitro" || p.name.startsWith("nitro:")));
|
|
1820
792
|
// Resolve PostCSS string plugin names that Vite can't handle.
|
|
1821
793
|
// Next.js projects commonly use array-form plugins like
|
|
1822
794
|
// `plugins: ["@tailwindcss/postcss"]` which postcss-load-config
|
|
@@ -1830,9 +802,12 @@ hydrate();
|
|
|
1830
802
|
}
|
|
1831
803
|
// Auto-inject @mdx-js/rollup when MDX files exist and no MDX plugin is
|
|
1832
804
|
// already configured. Applies remark/rehype plugins from next.config.
|
|
1833
|
-
const hasMdxPlugin = pluginsFlat.some((p) => p &&
|
|
805
|
+
const hasMdxPlugin = pluginsFlat.some((p) => p &&
|
|
806
|
+
typeof p === "object" &&
|
|
807
|
+
typeof p.name === "string" &&
|
|
1834
808
|
(p.name === "@mdx-js/rollup" || p.name === "mdx"));
|
|
1835
|
-
if (!hasMdxPlugin &&
|
|
809
|
+
if (!hasMdxPlugin &&
|
|
810
|
+
hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null)) {
|
|
1836
811
|
try {
|
|
1837
812
|
const mdxRollup = await import("@mdx-js/rollup");
|
|
1838
813
|
const mdxFactory = mdxRollup.default ?? mdxRollup;
|
|
@@ -1856,7 +831,9 @@ hydrate();
|
|
|
1856
831
|
catch {
|
|
1857
832
|
// @mdx-js/rollup not installed — warn but don't fail
|
|
1858
833
|
console.warn("[vinext] MDX files detected but @mdx-js/rollup is not installed. " +
|
|
1859
|
-
"Install it with: " +
|
|
834
|
+
"Install it with: " +
|
|
835
|
+
detectPackageManager(process.cwd()) +
|
|
836
|
+
" @mdx-js/rollup");
|
|
1860
837
|
}
|
|
1861
838
|
}
|
|
1862
839
|
// Detect if this is a standalone SSR build (set by `vite build --ssr`
|
|
@@ -1938,12 +915,14 @@ hydrate();
|
|
|
1938
915
|
// Skip when targeting bundled runtimes (Cloudflare/Nitro bundle everything).
|
|
1939
916
|
// This also resolves extensionless-import issues in packages like
|
|
1940
917
|
// `validator` (see #189) by routing them through Vite's resolver.
|
|
1941
|
-
...(hasCloudflarePlugin || hasNitroPlugin
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
918
|
+
...(hasCloudflarePlugin || hasNitroPlugin
|
|
919
|
+
? {}
|
|
920
|
+
: {
|
|
921
|
+
ssr: {
|
|
922
|
+
external: ["react", "react-dom", "react-dom/server"],
|
|
923
|
+
noExternal: true,
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
1947
926
|
resolve: {
|
|
1948
927
|
alias: nextShimMap,
|
|
1949
928
|
// Dedupe React packages to prevent dual-instance errors.
|
|
@@ -1951,18 +930,16 @@ hydrate();
|
|
|
1951
930
|
// brings its own React copy, multiple React instances can load,
|
|
1952
931
|
// causing cryptic "Invalid hook call" errors. This is a no-op
|
|
1953
932
|
// when only one copy exists.
|
|
1954
|
-
dedupe: [
|
|
1955
|
-
"react",
|
|
1956
|
-
"react-dom",
|
|
1957
|
-
"react/jsx-runtime",
|
|
1958
|
-
"react/jsx-dev-runtime",
|
|
1959
|
-
],
|
|
933
|
+
dedupe: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
|
|
1960
934
|
},
|
|
1961
935
|
// Exclude vinext from dependency optimization so esbuild doesn't
|
|
1962
936
|
// scan dist files containing virtual module imports (virtual:vinext-*)
|
|
1963
937
|
// that only resolve at Vite plugin time, not during pre-bundling.
|
|
938
|
+
// Exclude @vercel/og so Vite's esbuild pre-bundler doesn't cache it
|
|
939
|
+
// before our vinext:og-font-patch transform can inline the font and
|
|
940
|
+
// patch the yoga WASM instantiation for workerd compatibility.
|
|
1964
941
|
optimizeDeps: {
|
|
1965
|
-
exclude: ["vinext"],
|
|
942
|
+
exclude: ["vinext", "@vercel/og"],
|
|
1966
943
|
},
|
|
1967
944
|
// Enable JSX in .tsx/.jsx files
|
|
1968
945
|
// Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc`
|
|
@@ -1996,36 +973,33 @@ hydrate();
|
|
|
1996
973
|
// "Invalid hook call" from duplicate React instances).
|
|
1997
974
|
// The entries must be relative to the project root.
|
|
1998
975
|
const relAppDir = path.relative(root, appDir);
|
|
1999
|
-
const appEntries = [
|
|
2000
|
-
`${relAppDir}/**/*.{tsx,ts,jsx,js}`,
|
|
2001
|
-
];
|
|
976
|
+
const appEntries = [`${relAppDir}/**/*.{tsx,ts,jsx,js}`];
|
|
2002
977
|
viteConfig.environments = {
|
|
2003
978
|
rsc: {
|
|
2004
|
-
...(hasCloudflarePlugin || hasNitroPlugin
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
},
|
|
2026
|
-
}),
|
|
979
|
+
...(hasCloudflarePlugin || hasNitroPlugin
|
|
980
|
+
? {}
|
|
981
|
+
: {
|
|
982
|
+
resolve: {
|
|
983
|
+
// Externalize native/heavy packages so the RSC environment
|
|
984
|
+
// loads them natively via Node rather than through Vite's
|
|
985
|
+
// ESM module evaluator (which can't handle native addons).
|
|
986
|
+
// Note: Do NOT externalize react/react-dom here — they must
|
|
987
|
+
// be bundled with the "react-server" condition for RSC.
|
|
988
|
+
// Skip when targeting bundled runtimes (Cloudflare/Nitro).
|
|
989
|
+
external: userSsrExternal === true
|
|
990
|
+
? true
|
|
991
|
+
: ["satori", "@resvg/resvg-js", "yoga-wasm-web", ...userSsrExternal],
|
|
992
|
+
// Force all node_modules through Vite's transform pipeline
|
|
993
|
+
// so non-JS imports (CSS, images) don't hit Node's native
|
|
994
|
+
// ESM loader. Matches Next.js behavior of bundling everything.
|
|
995
|
+
// Packages in `external` above take precedence per Vite rules.
|
|
996
|
+
// When user sets `ssr.external: true`, skip noExternal since
|
|
997
|
+
// everything is already externalized.
|
|
998
|
+
...(userSsrExternal === true ? {} : { noExternal: true }),
|
|
999
|
+
},
|
|
1000
|
+
}),
|
|
2027
1001
|
optimizeDeps: {
|
|
2028
|
-
exclude: ["vinext"],
|
|
1002
|
+
exclude: ["vinext", "@vercel/og"],
|
|
2029
1003
|
entries: appEntries,
|
|
2030
1004
|
},
|
|
2031
1005
|
build: {
|
|
@@ -2036,19 +1010,21 @@ hydrate();
|
|
|
2036
1010
|
},
|
|
2037
1011
|
},
|
|
2038
1012
|
ssr: {
|
|
2039
|
-
...(hasCloudflarePlugin || hasNitroPlugin
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
1013
|
+
...(hasCloudflarePlugin || hasNitroPlugin
|
|
1014
|
+
? {}
|
|
1015
|
+
: {
|
|
1016
|
+
resolve: {
|
|
1017
|
+
external: userSsrExternal === true ? true : [...userSsrExternal],
|
|
1018
|
+
// Force all node_modules through Vite's transform pipeline
|
|
1019
|
+
// so non-JS imports (CSS, images) don't hit Node's native
|
|
1020
|
+
// ESM loader. Matches Next.js behavior of bundling everything.
|
|
1021
|
+
// When user sets `ssr.external: true`, skip noExternal since
|
|
1022
|
+
// everything is already externalized.
|
|
1023
|
+
...(userSsrExternal === true ? {} : { noExternal: true }),
|
|
1024
|
+
},
|
|
1025
|
+
}),
|
|
2050
1026
|
optimizeDeps: {
|
|
2051
|
-
exclude: ["vinext"],
|
|
1027
|
+
exclude: ["vinext", "@vercel/og"],
|
|
2052
1028
|
entries: appEntries,
|
|
2053
1029
|
},
|
|
2054
1030
|
build: {
|
|
@@ -2136,6 +1112,18 @@ hydrate();
|
|
|
2136
1112
|
" Or: pass rsc: false to vinext() if you want to configure rsc() yourself.");
|
|
2137
1113
|
}
|
|
2138
1114
|
}
|
|
1115
|
+
// Fail the build when targeting Cloudflare Workers without the
|
|
1116
|
+
// cloudflare() plugin. Without it, wrangler's esbuild can't resolve
|
|
1117
|
+
// virtual:vinext-rsc-entry and produces a cryptic error. (#325)
|
|
1118
|
+
if (config.command === "build" &&
|
|
1119
|
+
!hasCloudflarePlugin &&
|
|
1120
|
+
!hasNitroPlugin &&
|
|
1121
|
+
hasWranglerConfig(root)) {
|
|
1122
|
+
throw new Error(formatMissingCloudflarePluginError({
|
|
1123
|
+
isAppRouter: hasAppDir,
|
|
1124
|
+
configFile: config.configFile,
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
2139
1127
|
},
|
|
2140
1128
|
resolveId: {
|
|
2141
1129
|
// Hook filter: only invoke JS for next/* imports and virtual:vinext-* modules.
|
|
@@ -2167,10 +1155,12 @@ hydrate();
|
|
|
2167
1155
|
return RESOLVED_SERVER_ENTRY;
|
|
2168
1156
|
if (cleanId === VIRTUAL_CLIENT_ENTRY)
|
|
2169
1157
|
return RESOLVED_CLIENT_ENTRY;
|
|
2170
|
-
if (cleanId.endsWith("/" + VIRTUAL_SERVER_ENTRY) ||
|
|
1158
|
+
if (cleanId.endsWith("/" + VIRTUAL_SERVER_ENTRY) ||
|
|
1159
|
+
cleanId.endsWith("\\" + VIRTUAL_SERVER_ENTRY)) {
|
|
2171
1160
|
return RESOLVED_SERVER_ENTRY;
|
|
2172
1161
|
}
|
|
2173
|
-
if (cleanId.endsWith("/" + VIRTUAL_CLIENT_ENTRY) ||
|
|
1162
|
+
if (cleanId.endsWith("/" + VIRTUAL_CLIENT_ENTRY) ||
|
|
1163
|
+
cleanId.endsWith("\\" + VIRTUAL_CLIENT_ENTRY)) {
|
|
2174
1164
|
return RESOLVED_CLIENT_ENTRY;
|
|
2175
1165
|
}
|
|
2176
1166
|
// App Router virtual modules
|
|
@@ -2180,13 +1170,16 @@ hydrate();
|
|
|
2180
1170
|
return RESOLVED_APP_SSR_ENTRY;
|
|
2181
1171
|
if (cleanId === VIRTUAL_APP_BROWSER_ENTRY)
|
|
2182
1172
|
return RESOLVED_APP_BROWSER_ENTRY;
|
|
2183
|
-
if (cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) ||
|
|
1173
|
+
if (cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) ||
|
|
1174
|
+
cleanId.endsWith("\\" + VIRTUAL_RSC_ENTRY)) {
|
|
2184
1175
|
return RESOLVED_RSC_ENTRY;
|
|
2185
1176
|
}
|
|
2186
|
-
if (cleanId.endsWith("/" + VIRTUAL_APP_SSR_ENTRY) ||
|
|
1177
|
+
if (cleanId.endsWith("/" + VIRTUAL_APP_SSR_ENTRY) ||
|
|
1178
|
+
cleanId.endsWith("\\" + VIRTUAL_APP_SSR_ENTRY)) {
|
|
2187
1179
|
return RESOLVED_APP_SSR_ENTRY;
|
|
2188
1180
|
}
|
|
2189
|
-
if (cleanId.endsWith("/" + VIRTUAL_APP_BROWSER_ENTRY) ||
|
|
1181
|
+
if (cleanId.endsWith("/" + VIRTUAL_APP_BROWSER_ENTRY) ||
|
|
1182
|
+
cleanId.endsWith("\\" + VIRTUAL_APP_BROWSER_ENTRY)) {
|
|
2190
1183
|
return RESOLVED_APP_BROWSER_ENTRY;
|
|
2191
1184
|
}
|
|
2192
1185
|
},
|
|
@@ -2211,6 +1204,7 @@ hydrate();
|
|
|
2211
1204
|
headers: nextConfig?.headers,
|
|
2212
1205
|
allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
|
|
2213
1206
|
allowedDevOrigins: nextConfig?.allowedDevOrigins,
|
|
1207
|
+
bodySizeLimit: nextConfig?.serverActionsBodySizeLimit,
|
|
2214
1208
|
}, instrumentationPath);
|
|
2215
1209
|
}
|
|
2216
1210
|
if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
|
|
@@ -2241,6 +1235,10 @@ hydrate();
|
|
|
2241
1235
|
return fn.call(this, config, env);
|
|
2242
1236
|
},
|
|
2243
1237
|
transform(code, id, options) {
|
|
1238
|
+
// Skip ?raw and other query imports — @mdx-js/rollup ignores the query
|
|
1239
|
+
// and would compile the file as MDX instead of returning raw text.
|
|
1240
|
+
if (id.includes("?"))
|
|
1241
|
+
return;
|
|
2244
1242
|
if (!mdxDelegate?.transform)
|
|
2245
1243
|
return;
|
|
2246
1244
|
const hook = mdxDelegate.transform;
|
|
@@ -2473,9 +1471,14 @@ hydrate();
|
|
|
2473
1471
|
// loads the module. The gap between them is exactly the Vite
|
|
2474
1472
|
// compile/transform cost.
|
|
2475
1473
|
function _parseTiming(raw) {
|
|
2476
|
-
const [handlerStart, inHandlerCompileMs, renderMs] = String(raw)
|
|
2477
|
-
|
|
2478
|
-
|
|
1474
|
+
const [handlerStart, inHandlerCompileMs, renderMs] = String(raw)
|
|
1475
|
+
.split(",")
|
|
1476
|
+
.map((v) => Number(v));
|
|
1477
|
+
if (!Number.isNaN(handlerStart) &&
|
|
1478
|
+
!Number.isNaN(inHandlerCompileMs) &&
|
|
1479
|
+
inHandlerCompileMs !== -1) {
|
|
1480
|
+
_compileMs =
|
|
1481
|
+
Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs;
|
|
2479
1482
|
}
|
|
2480
1483
|
if (!Number.isNaN(renderMs) && renderMs !== -1) {
|
|
2481
1484
|
_renderMs = renderMs;
|
|
@@ -2521,7 +1524,9 @@ hydrate();
|
|
|
2521
1524
|
// matching what Next.js shows for soft navigations.
|
|
2522
1525
|
const resolvedRenderMs = _renderMs !== undefined
|
|
2523
1526
|
? _renderMs
|
|
2524
|
-
:
|
|
1527
|
+
: _compileMs !== undefined
|
|
1528
|
+
? Math.max(0, Math.round(totalMs - _compileMs))
|
|
1529
|
+
: undefined;
|
|
2525
1530
|
logRequest({
|
|
2526
1531
|
method: req.method ?? "GET",
|
|
2527
1532
|
url: logUrl,
|
|
@@ -2658,7 +1663,10 @@ hydrate();
|
|
|
2658
1663
|
}
|
|
2659
1664
|
// Normalize trailing slash based on next.config.js trailingSlash setting.
|
|
2660
1665
|
// Redirect to the canonical form if needed.
|
|
2661
|
-
if (nextConfig &&
|
|
1666
|
+
if (nextConfig &&
|
|
1667
|
+
pathname !== "/" &&
|
|
1668
|
+
pathname !== "/api" &&
|
|
1669
|
+
!pathname.startsWith("/api/")) {
|
|
2662
1670
|
const hasTrailing = pathname.endsWith("/");
|
|
2663
1671
|
if (nextConfig.trailingSlash && !hasTrailing) {
|
|
2664
1672
|
// trailingSlash: true — redirect /about → /about/
|
|
@@ -2677,12 +1685,24 @@ hydrate();
|
|
|
2677
1685
|
return;
|
|
2678
1686
|
}
|
|
2679
1687
|
}
|
|
1688
|
+
const applyRequestHeadersToNodeRequest = (nextRequestHeaders) => {
|
|
1689
|
+
for (const key of Object.keys(req.headers)) {
|
|
1690
|
+
delete req.headers[key];
|
|
1691
|
+
}
|
|
1692
|
+
for (const [key, value] of nextRequestHeaders) {
|
|
1693
|
+
req.headers[key] = value;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
let middlewareRequestHeaders = null;
|
|
2680
1697
|
// Run middleware.ts if present
|
|
2681
1698
|
if (middlewarePath) {
|
|
2682
1699
|
// Only trust X-Forwarded-Proto when behind a trusted proxy
|
|
2683
|
-
const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" ||
|
|
1700
|
+
const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" ||
|
|
1701
|
+
(process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some((h) => h.trim());
|
|
2684
1702
|
const rawProto = devTrustProxy
|
|
2685
|
-
? String(req.headers["x-forwarded-proto"] || "")
|
|
1703
|
+
? String(req.headers["x-forwarded-proto"] || "")
|
|
1704
|
+
.split(",")[0]
|
|
1705
|
+
.trim()
|
|
2686
1706
|
: "";
|
|
2687
1707
|
const mwProto = rawProto === "https" || rawProto === "http" ? rawProto : "http";
|
|
2688
1708
|
const origin = `${mwProto}://${req.headers.host || "localhost"}`;
|
|
@@ -2695,7 +1715,9 @@ hydrate();
|
|
|
2695
1715
|
const result = await runMiddleware(getPagesRunner(), middlewarePath, middlewareRequest);
|
|
2696
1716
|
if (!result.continue) {
|
|
2697
1717
|
if (result.redirectUrl) {
|
|
2698
|
-
const redirectHeaders = {
|
|
1718
|
+
const redirectHeaders = {
|
|
1719
|
+
Location: result.redirectUrl,
|
|
1720
|
+
};
|
|
2699
1721
|
if (result.responseHeaders) {
|
|
2700
1722
|
for (const [key, value] of result.responseHeaders) {
|
|
2701
1723
|
const existing = redirectHeaders[key];
|
|
@@ -2729,13 +1751,21 @@ hydrate();
|
|
|
2729
1751
|
// config has/missing conditions and downstream handlers
|
|
2730
1752
|
// see middleware-modified cookies and headers.
|
|
2731
1753
|
if (result.responseHeaders) {
|
|
2732
|
-
const
|
|
2733
|
-
for (const [key, value] of
|
|
2734
|
-
if (
|
|
2735
|
-
|
|
2736
|
-
|
|
1754
|
+
const currentRequestHeaders = new Headers();
|
|
1755
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1756
|
+
if (Array.isArray(value)) {
|
|
1757
|
+
currentRequestHeaders.set(key, value.join(", "));
|
|
1758
|
+
}
|
|
1759
|
+
else if (value !== undefined) {
|
|
1760
|
+
currentRequestHeaders.set(key, value);
|
|
2737
1761
|
}
|
|
2738
|
-
|
|
1762
|
+
}
|
|
1763
|
+
middlewareRequestHeaders = buildRequestHeadersFromMiddlewareResponse(currentRequestHeaders, result.responseHeaders);
|
|
1764
|
+
if (middlewareRequestHeaders && !hasAppDir) {
|
|
1765
|
+
applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
|
|
1766
|
+
}
|
|
1767
|
+
for (const [key, value] of result.responseHeaders) {
|
|
1768
|
+
if (!key.startsWith("x-middleware-")) {
|
|
2739
1769
|
res.appendHeader(key, value);
|
|
2740
1770
|
}
|
|
2741
1771
|
}
|
|
@@ -2780,23 +1810,21 @@ hydrate();
|
|
|
2780
1810
|
return next();
|
|
2781
1811
|
// Build request context once for has/missing condition checks
|
|
2782
1812
|
// across headers, redirects, and rewrites.
|
|
1813
|
+
// Convert Node.js IncomingMessage headers to a Web Request for
|
|
1814
|
+
// requestContextFromRequest(), which uses the standard Web API.
|
|
2783
1815
|
const reqUrl = new URL(url, `http://${req.headers.host || "localhost"}`);
|
|
2784
|
-
const reqCtxHeaders =
|
|
2785
|
-
.
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
cookies: parseCookies(reqCtxHeaders.get("cookie")),
|
|
2790
|
-
query: reqUrl.searchParams,
|
|
2791
|
-
host: reqCtxHeaders.get("host") ?? reqUrl.host,
|
|
2792
|
-
};
|
|
1816
|
+
const reqCtxHeaders = middlewareRequestHeaders ??
|
|
1817
|
+
new Headers(Object.fromEntries(Object.entries(req.headers)
|
|
1818
|
+
.filter(([, v]) => v !== undefined)
|
|
1819
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
|
|
1820
|
+
const reqCtx = requestContextFromRequest(new Request(reqUrl, { headers: reqCtxHeaders }));
|
|
2793
1821
|
// Apply custom headers from next.config.js
|
|
2794
1822
|
if (nextConfig?.headers.length) {
|
|
2795
1823
|
applyHeaders(pathname, res, nextConfig.headers, reqCtx);
|
|
2796
1824
|
}
|
|
2797
1825
|
// Apply redirects from next.config.js
|
|
2798
1826
|
if (nextConfig?.redirects.length) {
|
|
2799
|
-
const redirected = applyRedirects(pathname, res, nextConfig.redirects, reqCtx);
|
|
1827
|
+
const redirected = applyRedirects(pathname, res, nextConfig.redirects, reqCtx, nextConfig.basePath ?? "");
|
|
2800
1828
|
if (redirected)
|
|
2801
1829
|
return;
|
|
2802
1830
|
}
|
|
@@ -2804,8 +1832,7 @@ hydrate();
|
|
|
2804
1832
|
let resolvedUrl = url;
|
|
2805
1833
|
if (nextConfig?.rewrites.beforeFiles.length) {
|
|
2806
1834
|
resolvedUrl =
|
|
2807
|
-
applyRewrites(pathname, nextConfig.rewrites.beforeFiles, reqCtx) ??
|
|
2808
|
-
url;
|
|
1835
|
+
applyRewrites(pathname, nextConfig.rewrites.beforeFiles, reqCtx) ?? url;
|
|
2809
1836
|
}
|
|
2810
1837
|
// External rewrite from beforeFiles — proxy to external URL
|
|
2811
1838
|
if (isExternalUrl(resolvedUrl)) {
|
|
@@ -2814,9 +1841,12 @@ hydrate();
|
|
|
2814
1841
|
}
|
|
2815
1842
|
// Handle API routes first (pages/api/*)
|
|
2816
1843
|
const resolvedPathname = resolvedUrl.split("?")[0];
|
|
2817
|
-
if (resolvedPathname.startsWith("/api/") ||
|
|
2818
|
-
resolvedPathname === "/api") {
|
|
1844
|
+
if (resolvedPathname.startsWith("/api/") || resolvedPathname === "/api") {
|
|
2819
1845
|
const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
1846
|
+
const apiMatch = matchRoute(resolvedUrl, apiRoutes);
|
|
1847
|
+
if (apiMatch && middlewareRequestHeaders) {
|
|
1848
|
+
applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
|
|
1849
|
+
}
|
|
2820
1850
|
const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
|
|
2821
1851
|
if (handled)
|
|
2822
1852
|
return;
|
|
@@ -2848,6 +1878,9 @@ hydrate();
|
|
|
2848
1878
|
// Try rendering the resolved URL
|
|
2849
1879
|
const match = matchRoute(resolvedUrl.split("?")[0], routes);
|
|
2850
1880
|
if (match) {
|
|
1881
|
+
if (middlewareRequestHeaders) {
|
|
1882
|
+
applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
|
|
1883
|
+
}
|
|
2851
1884
|
await handler(req, res, resolvedUrl, mwStatus);
|
|
2852
1885
|
return;
|
|
2853
1886
|
}
|
|
@@ -2860,6 +1893,13 @@ hydrate();
|
|
|
2860
1893
|
await proxyExternalRewriteNode(req, res, fallbackRewrite);
|
|
2861
1894
|
return;
|
|
2862
1895
|
}
|
|
1896
|
+
const fallbackMatch = matchRoute(fallbackRewrite.split("?")[0], routes);
|
|
1897
|
+
if (!fallbackMatch && hasAppDir) {
|
|
1898
|
+
return next();
|
|
1899
|
+
}
|
|
1900
|
+
if (middlewareRequestHeaders) {
|
|
1901
|
+
applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
|
|
1902
|
+
}
|
|
2863
1903
|
await handler(req, res, fallbackRewrite, mwStatus);
|
|
2864
1904
|
return;
|
|
2865
1905
|
}
|
|
@@ -3060,7 +2100,10 @@ hydrate();
|
|
|
3060
2100
|
const importMatch = code.match(importRe);
|
|
3061
2101
|
if (!importMatch)
|
|
3062
2102
|
return null;
|
|
3063
|
-
const importedNames = new Set(importMatch[1]
|
|
2103
|
+
const importedNames = new Set(importMatch[1]
|
|
2104
|
+
.split(",")
|
|
2105
|
+
.map((s) => s.trim())
|
|
2106
|
+
.filter(Boolean));
|
|
3064
2107
|
const s = new MagicString(code);
|
|
3065
2108
|
let hasChanges = false;
|
|
3066
2109
|
const cacheDir = this._cacheDir;
|
|
@@ -3086,10 +2129,14 @@ hydrate();
|
|
|
3086
2129
|
}
|
|
3087
2130
|
// Build the Google Fonts CSS URL
|
|
3088
2131
|
const weights = options.weight
|
|
3089
|
-
? Array.isArray(options.weight)
|
|
2132
|
+
? Array.isArray(options.weight)
|
|
2133
|
+
? options.weight
|
|
2134
|
+
: [options.weight]
|
|
3090
2135
|
: [];
|
|
3091
2136
|
const styles = options.style
|
|
3092
|
-
? Array.isArray(options.style)
|
|
2137
|
+
? Array.isArray(options.style)
|
|
2138
|
+
? options.style
|
|
2139
|
+
: [options.style]
|
|
3093
2140
|
: [];
|
|
3094
2141
|
const display = options.display ?? "swap";
|
|
3095
2142
|
let spec = family.replace(/\s+/g, "+");
|
|
@@ -3245,7 +2292,9 @@ hydrate();
|
|
|
3245
2292
|
return null;
|
|
3246
2293
|
if (!resolvedRscTransformsPath) {
|
|
3247
2294
|
throw new Error("vinext: 'use cache' requires @vitejs/plugin-rsc to be installed.\n" +
|
|
3248
|
-
"Run: " +
|
|
2295
|
+
"Run: " +
|
|
2296
|
+
detectPackageManager(process.cwd()) +
|
|
2297
|
+
" @vitejs/plugin-rsc");
|
|
3249
2298
|
}
|
|
3250
2299
|
const { transformWrapExport, transformHoistInlineDirective } = await import(pathToFileURL(resolvedRscTransformsPath).href);
|
|
3251
2300
|
const ast = parseAst(code);
|
|
@@ -3260,7 +2309,9 @@ hydrate();
|
|
|
3260
2309
|
// (they're leaf components). Layout/template defaults are excluded
|
|
3261
2310
|
// because they receive {children} from the framework.
|
|
3262
2311
|
const directiveValue = cacheDirective.expression.value;
|
|
3263
|
-
const variant = directiveValue === "use cache"
|
|
2312
|
+
const variant = directiveValue === "use cache"
|
|
2313
|
+
? ""
|
|
2314
|
+
: directiveValue.replace("use cache:", "").replace("use cache: ", "").trim();
|
|
3264
2315
|
// Only skip default export wrapping for layouts and templates —
|
|
3265
2316
|
// they receive {children} from the framework which requires
|
|
3266
2317
|
// temporary reference handling that registerCachedFunction doesn't
|
|
@@ -3312,7 +2363,9 @@ hydrate();
|
|
|
3312
2363
|
directive: /^use cache(:\s*\w+)?$/,
|
|
3313
2364
|
runtime: (value, name, meta) => {
|
|
3314
2365
|
const directiveMatch = meta.directiveMatch[0];
|
|
3315
|
-
const variant = directiveMatch === "use cache"
|
|
2366
|
+
const variant = directiveMatch === "use cache"
|
|
2367
|
+
? ""
|
|
2368
|
+
: directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim();
|
|
3316
2369
|
return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`;
|
|
3317
2370
|
},
|
|
3318
2371
|
rejectNonAsyncFunction: false,
|
|
@@ -3332,11 +2385,110 @@ hydrate();
|
|
|
3332
2385
|
},
|
|
3333
2386
|
},
|
|
3334
2387
|
},
|
|
3335
|
-
//
|
|
3336
|
-
//
|
|
3337
|
-
//
|
|
3338
|
-
//
|
|
3339
|
-
//
|
|
2388
|
+
// Inline binary assets fetched via `fetch(new URL("./asset", import.meta.url))`.
|
|
2389
|
+
//
|
|
2390
|
+
// Some bundled libraries (notably @vercel/og) load assets at module init time
|
|
2391
|
+
// with the pattern:
|
|
2392
|
+
//
|
|
2393
|
+
// fetch(new URL("./some-font.ttf", import.meta.url)).then(res => res.arrayBuffer())
|
|
2394
|
+
//
|
|
2395
|
+
// This works in browser and standard Node.js because import.meta.url is a real
|
|
2396
|
+
// file:// URL. In Cloudflare Workers (both wrangler dev and production), however,
|
|
2397
|
+
// import.meta.url is the string "worker" — not a URL — so new URL(...) throws
|
|
2398
|
+
// "TypeError: Invalid URL string" and the Worker fails to start.
|
|
2399
|
+
//
|
|
2400
|
+
// Fix: at Vite transform time, find every such pattern, resolve the referenced
|
|
2401
|
+
// file relative to the module's actual path on disk (available as `id`), read it,
|
|
2402
|
+
// and replace the entire fetch(new URL(...)) expression with an inline base64 IIFE
|
|
2403
|
+
// that resolves synchronously. This eliminates the runtime fetch entirely and works
|
|
2404
|
+
// in all environments (workerd, Node.js, browser).
|
|
2405
|
+
//
|
|
2406
|
+
// Note: WASM files imported via `import ... from "./foo.wasm?module"` are handled
|
|
2407
|
+
// by the bundler/Vite directly and do not need this treatment. Only assets that
|
|
2408
|
+
// are runtime-fetched (not statically imported) need to be inlined here.
|
|
2409
|
+
{
|
|
2410
|
+
name: "vinext:og-inline-fetch-assets",
|
|
2411
|
+
enforce: "pre",
|
|
2412
|
+
transform(code, id) {
|
|
2413
|
+
// Quick bail-out: only process modules that use new URL(..., import.meta.url)
|
|
2414
|
+
if (!code.includes("import.meta.url")) {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
const moduleDir = path.dirname(id);
|
|
2418
|
+
let newCode = code;
|
|
2419
|
+
let didReplace = false;
|
|
2420
|
+
// Pattern 1 — edge build: fetch(new URL("./file", import.meta.url)).then((res) => res.arrayBuffer())
|
|
2421
|
+
// Replace with an inline IIFE that decodes the asset as base64 and returns Promise<ArrayBuffer>.
|
|
2422
|
+
if (code.includes("fetch(")) {
|
|
2423
|
+
const fetchPattern = /fetch\(\s*new URL\(\s*(["'])(\.\/[^"']+)\1\s*,\s*import\.meta\.url\s*\)\s*\)(?:\.then\(\s*(?:function\s*\([^)]*\)|\([^)]*\)\s*=>)\s*\{?\s*return\s+[^.]+\.arrayBuffer\(\)\s*\}?\s*\)|\.then\(\s*\([^)]*\)\s*=>\s*[^.]+\.arrayBuffer\(\)\s*\))/g;
|
|
2424
|
+
for (const match of code.matchAll(fetchPattern)) {
|
|
2425
|
+
const fullMatch = match[0];
|
|
2426
|
+
const relPath = match[2]; // e.g. "./noto-sans-v27-latin-regular.ttf"
|
|
2427
|
+
const absPath = path.resolve(moduleDir, relPath);
|
|
2428
|
+
let fileBase64;
|
|
2429
|
+
try {
|
|
2430
|
+
fileBase64 = fs.readFileSync(absPath).toString("base64");
|
|
2431
|
+
}
|
|
2432
|
+
catch {
|
|
2433
|
+
// File not found on disk — skip (may be a runtime-only asset)
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
// Replace fetch(...).then(...) with an inline IIFE that returns Promise<ArrayBuffer>.
|
|
2437
|
+
const inlined = [
|
|
2438
|
+
`(function(){`,
|
|
2439
|
+
`var b=${JSON.stringify(fileBase64)};`,
|
|
2440
|
+
`var r=atob(b);`,
|
|
2441
|
+
`var a=new Uint8Array(r.length);`,
|
|
2442
|
+
`for(var i=0;i<r.length;i++)a[i]=r.charCodeAt(i);`,
|
|
2443
|
+
`return Promise.resolve(a.buffer);`,
|
|
2444
|
+
`})()`,
|
|
2445
|
+
].join("");
|
|
2446
|
+
newCode = newCode.replaceAll(fullMatch, inlined);
|
|
2447
|
+
didReplace = true;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
// Pattern 2 — node build: readFileSync(fileURLToPath(new URL("./file", import.meta.url)))
|
|
2451
|
+
// Replace with Buffer.from("<base64>", "base64"), which returns a Buffer (compatible with
|
|
2452
|
+
// both font data passed to satori and WASM bytes passed to initWasm).
|
|
2453
|
+
if (code.includes("readFileSync(")) {
|
|
2454
|
+
const readFilePattern = /[a-zA-Z_$][a-zA-Z0-9_$]*\.readFileSync\(\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)?fileURLToPath\(\s*new URL\(\s*(["'])(\.\/[^"']+)\1\s*,\s*import\.meta\.url\s*\)\s*\)\s*\)/g;
|
|
2455
|
+
for (const match of newCode.matchAll(readFilePattern)) {
|
|
2456
|
+
const fullMatch = match[0];
|
|
2457
|
+
const relPath = match[2]; // e.g. "./noto-sans-v27-latin-regular.ttf"
|
|
2458
|
+
const absPath = path.resolve(moduleDir, relPath);
|
|
2459
|
+
let fileBase64;
|
|
2460
|
+
try {
|
|
2461
|
+
fileBase64 = fs.readFileSync(absPath).toString("base64");
|
|
2462
|
+
}
|
|
2463
|
+
catch {
|
|
2464
|
+
// File not found on disk — skip
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
// Replace readFileSync(...) with Buffer.from("<base64>", "base64").
|
|
2468
|
+
// Buffer is always available in Node.js and in the vinext SSR/RSC environments.
|
|
2469
|
+
const inlined = `Buffer.from(${JSON.stringify(fileBase64)},"base64")`;
|
|
2470
|
+
newCode = newCode.replaceAll(fullMatch, inlined);
|
|
2471
|
+
didReplace = true;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (!didReplace)
|
|
2475
|
+
return null;
|
|
2476
|
+
return { code: newCode, map: null };
|
|
2477
|
+
},
|
|
2478
|
+
},
|
|
2479
|
+
// Copy @vercel/og binary assets to the RSC output directory for production builds.
|
|
2480
|
+
//
|
|
2481
|
+
// The edge build (dist/index.edge.js) uses:
|
|
2482
|
+
// - fetch(new URL("./noto-sans...", import.meta.url)) → inlined by og-inline-fetch-assets
|
|
2483
|
+
// - import resvg_wasm from "./resvg.wasm?module" → static Vite import, emitted by Rollup
|
|
2484
|
+
//
|
|
2485
|
+
// The node build (dist/index.node.js) uses:
|
|
2486
|
+
// - fs.readFileSync(fileURLToPath(new URL("./noto-sans...", import.meta.url))) → inlined
|
|
2487
|
+
// - fs.readFileSync(fileURLToPath(new URL("./resvg.wasm", import.meta.url))) → inlined
|
|
2488
|
+
//
|
|
2489
|
+
// Both builds' font + WASM assets are inlined as base64 by vinext:og-inline-fetch-assets,
|
|
2490
|
+
// so no file copy is strictly needed. This plugin is kept as a safety net for any edge-build
|
|
2491
|
+
// ?module WASM imports that Rollup/Vite might not emit correctly in the RSC environment.
|
|
3340
2492
|
{
|
|
3341
2493
|
name: "vinext:og-assets",
|
|
3342
2494
|
apply: "build",
|
|
@@ -3356,12 +2508,11 @@ hydrate();
|
|
|
3356
2508
|
if (!fs.existsSync(indexPath))
|
|
3357
2509
|
return;
|
|
3358
2510
|
const content = fs.readFileSync(indexPath, "utf-8");
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
];
|
|
2511
|
+
// The font is inlined as base64 by vinext:og-inline-fetch-assets, so only
|
|
2512
|
+
// the WASM needs to be present as a file alongside the bundle.
|
|
2513
|
+
const ogAssets = ["resvg.wasm"];
|
|
3363
2514
|
// Only copy if the bundle actually references these files
|
|
3364
|
-
const referencedAssets = ogAssets.filter(asset => content.includes(asset));
|
|
2515
|
+
const referencedAssets = ogAssets.filter((asset) => content.includes(asset));
|
|
3365
2516
|
if (referencedAssets.length === 0)
|
|
3366
2517
|
return;
|
|
3367
2518
|
// Find @vercel/og in node_modules
|
|
@@ -3461,7 +2612,9 @@ hydrate();
|
|
|
3461
2612
|
if (lazy.length > 0)
|
|
3462
2613
|
lazyChunksData = lazy;
|
|
3463
2614
|
}
|
|
3464
|
-
catch {
|
|
2615
|
+
catch {
|
|
2616
|
+
/* ignore parse errors */
|
|
2617
|
+
}
|
|
3465
2618
|
}
|
|
3466
2619
|
// Read SSR manifest for per-page CSS/JS injection
|
|
3467
2620
|
let ssrManifestData = null;
|
|
@@ -3470,7 +2623,9 @@ hydrate();
|
|
|
3470
2623
|
try {
|
|
3471
2624
|
ssrManifestData = JSON.parse(fs.readFileSync(ssrManifestPath, "utf-8"));
|
|
3472
2625
|
}
|
|
3473
|
-
catch {
|
|
2626
|
+
catch {
|
|
2627
|
+
/* ignore parse errors */
|
|
2628
|
+
}
|
|
3474
2629
|
}
|
|
3475
2630
|
if (hasAppDir) {
|
|
3476
2631
|
// App Router: the RSC plugin handles __VINEXT_CLIENT_ENTRY__
|
|
@@ -3517,7 +2672,8 @@ hydrate();
|
|
|
3517
2672
|
const assetsDir = path.join(clientDir, "assets");
|
|
3518
2673
|
if (fs.existsSync(assetsDir)) {
|
|
3519
2674
|
const files = fs.readdirSync(assetsDir);
|
|
3520
|
-
const entry = files.find((f) => (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) &&
|
|
2675
|
+
const entry = files.find((f) => (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) &&
|
|
2676
|
+
f.endsWith(".js"));
|
|
3521
2677
|
if (entry)
|
|
3522
2678
|
clientEntryFile = "assets/" + entry;
|
|
3523
2679
|
}
|
|
@@ -3560,6 +2716,58 @@ hydrate();
|
|
|
3560
2716
|
},
|
|
3561
2717
|
},
|
|
3562
2718
|
},
|
|
2719
|
+
{
|
|
2720
|
+
// @vercel/og patch for workerd (cloudflare-dev + cloudflare-workers)
|
|
2721
|
+
//
|
|
2722
|
+
// @vercel/og/dist/index.edge.js has one remaining workerd issue after the
|
|
2723
|
+
// generic vinext:og-inline-fetch-assets plugin runs (which already handles
|
|
2724
|
+
// the font fetch pattern):
|
|
2725
|
+
//
|
|
2726
|
+
// YOGA WASM: yoga-layout embeds its WASM as a base64 data URL and instantiates
|
|
2727
|
+
// it via WebAssembly.instantiate(bytes) at runtime.
|
|
2728
|
+
// workerd forbids dynamic WASM compilation from bytes — WASM must be loaded
|
|
2729
|
+
// through the module system as a pre-compiled WebAssembly.Module.
|
|
2730
|
+
// Fix: extract the yoga WASM bytes at Vite transform time (Node.js), write
|
|
2731
|
+
// yoga.wasm to @vercel/og/dist/, import it via `?module` so @cloudflare/vite-plugin
|
|
2732
|
+
// can serve it through the module system, and inject h2.instantiateWasm to
|
|
2733
|
+
// use the pre-compiled module instead of bytes.
|
|
2734
|
+
name: "vinext:og-font-patch",
|
|
2735
|
+
enforce: "pre",
|
|
2736
|
+
transform(code, id) {
|
|
2737
|
+
if (!id.includes("@vercel/og") || !id.includes("index.edge.js"))
|
|
2738
|
+
return null;
|
|
2739
|
+
let result = code;
|
|
2740
|
+
// ── Extract yoga WASM and import via ?module ──────────────────────────────────
|
|
2741
|
+
// yoga-layout's emscripten bundle sets H to a data URL containing the yoga WASM,
|
|
2742
|
+
// then later calls WebAssembly.instantiate(bytes, imports), which workerd rejects.
|
|
2743
|
+
// Emscripten supports a custom h2.instantiateWasm(imports, callback) escape hatch
|
|
2744
|
+
// that we inject to use a pre-compiled WebAssembly.Module loaded via ?module.
|
|
2745
|
+
const YOGA_DATA_URL_RE = /H = "data:application\/octet-stream;base64,([A-Za-z0-9+/]+=*)";/;
|
|
2746
|
+
const yogaMatch = YOGA_DATA_URL_RE.exec(result);
|
|
2747
|
+
if (yogaMatch) {
|
|
2748
|
+
const yogaBase64 = yogaMatch[1];
|
|
2749
|
+
const distDir = path.dirname(id);
|
|
2750
|
+
const yogaWasmPath = path.join(distDir, "yoga.wasm");
|
|
2751
|
+
// Write yoga.wasm to disk idempotently at transform time (Node.js side)
|
|
2752
|
+
if (!fs.existsSync(yogaWasmPath)) {
|
|
2753
|
+
fs.writeFileSync(yogaWasmPath, Buffer.from(yogaBase64, "base64"));
|
|
2754
|
+
}
|
|
2755
|
+
// Disable the data-URL branch so emscripten doesn't try to instantiate from bytes
|
|
2756
|
+
result = result.replace(yogaMatch[0], `H = "";`);
|
|
2757
|
+
// Patch the loadYoga call site to inject instantiateWasm using the ?module import
|
|
2758
|
+
const YOGA_CALL = `yoga_wasm_base64_esm_default()`;
|
|
2759
|
+
const YOGA_CALL_PATCHED = `yoga_wasm_base64_esm_default({ instantiateWasm: function(imports, callback) {` +
|
|
2760
|
+
` WebAssembly.instantiate(yoga_wasm_module, imports).then(function(inst) { callback(inst); });` +
|
|
2761
|
+
` return {}; } })`;
|
|
2762
|
+
result = result.replace(YOGA_CALL, YOGA_CALL_PATCHED);
|
|
2763
|
+
// Prepend the yoga wasm ?module import so @cloudflare/vite-plugin handles it
|
|
2764
|
+
result = `import yoga_wasm_module from "./yoga.wasm?module";\n` + result;
|
|
2765
|
+
}
|
|
2766
|
+
if (result === code)
|
|
2767
|
+
return null;
|
|
2768
|
+
return { code: result, map: null };
|
|
2769
|
+
},
|
|
2770
|
+
},
|
|
3563
2771
|
];
|
|
3564
2772
|
// Append auto-injected RSC plugins if applicable
|
|
3565
2773
|
if (rscPluginPromise) {
|
|
@@ -3580,53 +2788,13 @@ function getNextPublicEnvDefines() {
|
|
|
3580
2788
|
}
|
|
3581
2789
|
return defines;
|
|
3582
2790
|
}
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
return null;
|
|
3591
|
-
const start = re.lastIndex + 1;
|
|
3592
|
-
let depth = 1;
|
|
3593
|
-
let i = start;
|
|
3594
|
-
while (i < str.length && depth > 0) {
|
|
3595
|
-
if (str[i] === "(")
|
|
3596
|
-
depth++;
|
|
3597
|
-
else if (str[i] === ")")
|
|
3598
|
-
depth--;
|
|
3599
|
-
i++;
|
|
3600
|
-
}
|
|
3601
|
-
if (depth !== 0)
|
|
3602
|
-
return null;
|
|
3603
|
-
re.lastIndex = i;
|
|
3604
|
-
return str.slice(start, i - 1);
|
|
3605
|
-
}
|
|
3606
|
-
/**
|
|
3607
|
-
* Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
|
|
3608
|
-
* Returns matched params or null.
|
|
3609
|
-
*
|
|
3610
|
-
* Supports:
|
|
3611
|
-
* :param — matches a single path segment
|
|
3612
|
-
* :param* — matches zero or more segments (catch-all)
|
|
3613
|
-
* :param+ — matches one or more segments
|
|
3614
|
-
* (regex) — inline regex patterns in the source
|
|
3615
|
-
*/
|
|
3616
|
-
/**
|
|
3617
|
-
* Strip server-only data-fetching exports from a page module's source code.
|
|
3618
|
-
* Returns the transformed code, or null if no changes were made.
|
|
3619
|
-
*
|
|
3620
|
-
* Handles:
|
|
3621
|
-
* - export (async) function getServerSideProps(...) { ... }
|
|
3622
|
-
* - export const getStaticProps = async (...) => { ... }
|
|
3623
|
-
* - export const getServerSideProps = someHelper;
|
|
3624
|
-
*/
|
|
3625
|
-
/**
|
|
3626
|
-
* Skip past balanced brackets/parens/braces starting at `pos` (which should
|
|
3627
|
-
* point to the opening bracket). Returns the position AFTER the closing bracket.
|
|
3628
|
-
* Handles nested brackets, string literals, and comments.
|
|
3629
|
-
*/
|
|
2791
|
+
// matchConfigPattern is imported from config-matchers.ts and re-exported
|
|
2792
|
+
// for tests and other consumers that import it from vinext's main entry.
|
|
2793
|
+
// The duplicate local implementation and its extractConstraint helper
|
|
2794
|
+
// have been removed in favor of the canonical config-matchers.ts version
|
|
2795
|
+
// which uses a single-pass tokenizer (fixing the chained .replace()
|
|
2796
|
+
// divergence that CodeQL flagged as incomplete sanitization).
|
|
2797
|
+
export { matchConfigPattern } from "./config/config-matchers.js";
|
|
3630
2798
|
/**
|
|
3631
2799
|
* Strip server-only data-fetching exports (getServerSideProps,
|
|
3632
2800
|
* getStaticProps, getStaticPaths) from page modules for the client
|
|
@@ -3638,7 +2806,7 @@ function extractConstraint(str, re) {
|
|
|
3638
2806
|
*/
|
|
3639
2807
|
function stripServerExports(code) {
|
|
3640
2808
|
const SERVER_EXPORTS = new Set(["getServerSideProps", "getStaticProps", "getStaticPaths"]);
|
|
3641
|
-
if (![...SERVER_EXPORTS].some(name => code.includes(name)))
|
|
2809
|
+
if (![...SERVER_EXPORTS].some((name) => code.includes(name)))
|
|
3642
2810
|
return null;
|
|
3643
2811
|
let ast;
|
|
3644
2812
|
try {
|
|
@@ -3689,11 +2857,13 @@ function stripServerExports(code) {
|
|
|
3689
2857
|
// Build replacement: keep non-server specifiers, add stubs for stripped ones
|
|
3690
2858
|
const parts = [];
|
|
3691
2859
|
if (kept.length > 0) {
|
|
3692
|
-
const keptStr = kept
|
|
2860
|
+
const keptStr = kept
|
|
2861
|
+
.map((sp) => {
|
|
3693
2862
|
const local = sp.local.name;
|
|
3694
2863
|
const exported = sp.exported?.name ?? sp.exported?.value;
|
|
3695
2864
|
return local === exported ? local : `${local} as ${exported}`;
|
|
3696
|
-
})
|
|
2865
|
+
})
|
|
2866
|
+
.join(", ");
|
|
3697
2867
|
parts.push(`export { ${keptStr} };`);
|
|
3698
2868
|
}
|
|
3699
2869
|
for (const name of stripped) {
|
|
@@ -3708,142 +2878,24 @@ function stripServerExports(code) {
|
|
|
3708
2878
|
return null;
|
|
3709
2879
|
return s.toString();
|
|
3710
2880
|
}
|
|
3711
|
-
export function matchConfigPattern(pathname, pattern) {
|
|
3712
|
-
// If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
|
|
3713
|
-
// Also enter this branch when a catch-all parameter (:param* or :param+) is
|
|
3714
|
-
// followed by a literal suffix (e.g. "/:path*.md"). Without this, the suffix
|
|
3715
|
-
// pattern falls through to the simple segment matcher which incorrectly treats
|
|
3716
|
-
// the whole segment (":path*.md") as a named parameter and matches everything.
|
|
3717
|
-
if (pattern.includes("(") ||
|
|
3718
|
-
pattern.includes("\\") ||
|
|
3719
|
-
/:[\w-]+[*+][^/]/.test(pattern) ||
|
|
3720
|
-
/:[\w-]+\./.test(pattern)) {
|
|
3721
|
-
try {
|
|
3722
|
-
// Extract named params and their constraints from the pattern.
|
|
3723
|
-
// :param(constraint) -> use constraint as the regex group
|
|
3724
|
-
// :param -> ([^/]+)
|
|
3725
|
-
// :param* -> (.*)
|
|
3726
|
-
// :param+ -> (.+)
|
|
3727
|
-
// Param names may contain hyphens (e.g. :auth-method, :sign-in).
|
|
3728
|
-
const paramNames = [];
|
|
3729
|
-
// Single-pass conversion with procedural suffix handling. The tokenizer
|
|
3730
|
-
// matches only simple, non-overlapping tokens; quantifier/constraint
|
|
3731
|
-
// suffixes after :param are consumed procedurally to avoid polynomial
|
|
3732
|
-
// backtracking in the regex engine.
|
|
3733
|
-
let regexStr = "";
|
|
3734
|
-
const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
|
|
3735
|
-
let tok;
|
|
3736
|
-
while ((tok = tokenRe.exec(pattern)) !== null) {
|
|
3737
|
-
if (tok[1] !== undefined) {
|
|
3738
|
-
const name = tok[1];
|
|
3739
|
-
const rest = pattern.slice(tokenRe.lastIndex);
|
|
3740
|
-
// Check for quantifier (* or +) with optional constraint
|
|
3741
|
-
if (rest.startsWith("*") || rest.startsWith("+")) {
|
|
3742
|
-
const quantifier = rest[0];
|
|
3743
|
-
tokenRe.lastIndex += 1;
|
|
3744
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
3745
|
-
paramNames.push(name);
|
|
3746
|
-
if (constraint !== null) {
|
|
3747
|
-
regexStr += `(${constraint})`;
|
|
3748
|
-
}
|
|
3749
|
-
else {
|
|
3750
|
-
regexStr += quantifier === "*" ? "(.*)" : "(.+)";
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
else {
|
|
3754
|
-
// Check for inline constraint without quantifier
|
|
3755
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
3756
|
-
paramNames.push(name);
|
|
3757
|
-
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
|
-
else if (tok[0] === ".") {
|
|
3761
|
-
regexStr += "\\.";
|
|
3762
|
-
}
|
|
3763
|
-
else {
|
|
3764
|
-
regexStr += tok[0];
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
const re = safeRegExp("^" + regexStr + "$");
|
|
3768
|
-
if (!re)
|
|
3769
|
-
return null;
|
|
3770
|
-
const match = re.exec(pathname);
|
|
3771
|
-
if (!match)
|
|
3772
|
-
return null;
|
|
3773
|
-
const params = {};
|
|
3774
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
3775
|
-
params[paramNames[i]] = match[i + 1] ?? "";
|
|
3776
|
-
}
|
|
3777
|
-
return params;
|
|
3778
|
-
}
|
|
3779
|
-
catch {
|
|
3780
|
-
// Fall through to segment-based matching
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
// Check for catch-all patterns (:param* or :param+) without regex groups
|
|
3784
|
-
// Param names may contain hyphens (e.g. :sign-in*, :sign-up+).
|
|
3785
|
-
const catchAllMatch = pattern.match(/:([\w-]+)(\*|\+)$/);
|
|
3786
|
-
if (catchAllMatch) {
|
|
3787
|
-
const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
|
|
3788
|
-
const paramName = catchAllMatch[1];
|
|
3789
|
-
const isPlus = catchAllMatch[2] === "+";
|
|
3790
|
-
if (!pathname.startsWith(prefix.replace(/\/$/, "")))
|
|
3791
|
-
return null;
|
|
3792
|
-
const rest = pathname.slice(prefix.replace(/\/$/, "").length);
|
|
3793
|
-
// For :path+ we need at least one segment (non-empty after the prefix)
|
|
3794
|
-
if (isPlus && (!rest || rest === "/"))
|
|
3795
|
-
return null;
|
|
3796
|
-
// For :path* zero segments is fine
|
|
3797
|
-
let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
|
|
3798
|
-
// NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
|
|
3799
|
-
// the entry point. Decoding again would create a double-decode vector.
|
|
3800
|
-
return { [paramName]: restValue };
|
|
3801
|
-
}
|
|
3802
|
-
// Simple segment-based matching for exact patterns and :param
|
|
3803
|
-
const parts = pattern.split("/");
|
|
3804
|
-
const pathParts = pathname.split("/");
|
|
3805
|
-
if (parts.length !== pathParts.length)
|
|
3806
|
-
return null;
|
|
3807
|
-
const params = {};
|
|
3808
|
-
for (let i = 0; i < parts.length; i++) {
|
|
3809
|
-
if (parts[i].startsWith(":")) {
|
|
3810
|
-
params[parts[i].slice(1)] = pathParts[i];
|
|
3811
|
-
}
|
|
3812
|
-
else if (parts[i] !== pathParts[i]) {
|
|
3813
|
-
return null;
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
return params;
|
|
3817
|
-
}
|
|
3818
|
-
/**
|
|
3819
|
-
* Sanitize a redirect/rewrite destination by collapsing leading slashes and
|
|
3820
|
-
* backslashes to a single "/" for non-external URLs. Browsers interpret "\"
|
|
3821
|
-
* as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
|
|
3822
|
-
*/
|
|
3823
|
-
function sanitizeDestinationLocal(dest) {
|
|
3824
|
-
if (dest.startsWith("http://") || dest.startsWith("https://"))
|
|
3825
|
-
return dest;
|
|
3826
|
-
dest = dest.replace(/^[\\/]+/, "/");
|
|
3827
|
-
return dest;
|
|
3828
|
-
}
|
|
3829
2881
|
/**
|
|
3830
2882
|
* Apply redirect rules from next.config.js.
|
|
3831
2883
|
* Returns true if a redirect was applied.
|
|
3832
2884
|
*/
|
|
3833
|
-
function applyRedirects(pathname, res, redirects, ctx) {
|
|
2885
|
+
function applyRedirects(pathname, res, redirects, ctx, basePath = "") {
|
|
3834
2886
|
const result = matchRedirect(pathname, redirects, ctx);
|
|
3835
2887
|
if (result) {
|
|
3836
2888
|
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3837
|
-
const dest =
|
|
2889
|
+
const dest = sanitizeDestination(basePath && !isExternalUrl(result.destination) && !hasBasePath(result.destination, basePath)
|
|
2890
|
+
? basePath + result.destination
|
|
2891
|
+
: result.destination);
|
|
3838
2892
|
res.writeHead(result.permanent ? 308 : 307, { Location: dest });
|
|
3839
2893
|
res.end();
|
|
3840
2894
|
return true;
|
|
3841
2895
|
}
|
|
3842
2896
|
return false;
|
|
3843
2897
|
}
|
|
3844
|
-
|
|
3845
|
-
* Proxy an external rewrite in the Node.js dev server context.
|
|
3846
|
-
*
|
|
2898
|
+
/*
|
|
3847
2899
|
* Converts the Node.js IncomingMessage into a Web Request, calls
|
|
3848
2900
|
* proxyExternalRequest(), and pipes the response back to the Node.js
|
|
3849
2901
|
* ServerResponse.
|
|
@@ -3874,9 +2926,7 @@ async function proxyExternalRewriteNode(req, res, externalUrl) {
|
|
|
3874
2926
|
proxyResponse.headers.forEach((value, key) => {
|
|
3875
2927
|
const existing = nodeHeaders[key];
|
|
3876
2928
|
if (existing !== undefined) {
|
|
3877
|
-
nodeHeaders[key] = Array.isArray(existing)
|
|
3878
|
-
? [...existing, value]
|
|
3879
|
-
: [existing, value];
|
|
2929
|
+
nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
3880
2930
|
}
|
|
3881
2931
|
else {
|
|
3882
2932
|
nodeHeaders[key] = value;
|
|
@@ -3908,7 +2958,7 @@ function applyRewrites(pathname, rewrites, ctx) {
|
|
|
3908
2958
|
const dest = matchRewrite(pathname, rewrites, ctx);
|
|
3909
2959
|
if (dest) {
|
|
3910
2960
|
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3911
|
-
return
|
|
2961
|
+
return sanitizeDestination(dest);
|
|
3912
2962
|
}
|
|
3913
2963
|
return null;
|
|
3914
2964
|
}
|