vinext 0.0.24 → 0.0.26
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 +30 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +6 -5
- package/dist/check.js.map +1 -1
- package/dist/cli.js +32 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/entry.js +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/config/config-matchers.d.ts +21 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +52 -8
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/next-config.d.ts +39 -6
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +241 -48
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts +21 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +94 -41
- 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} +6 -19
- package/dist/entries/app-rsc-entry.d.ts.map +1 -0
- package/dist/{server/app-dev-server.js → entries/app-rsc-entry.js} +572 -1293
- 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 +94 -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 +993 -0
- package/dist/entries/pages-server-entry.js.map +1 -0
- package/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +345 -1227
- package/dist/index.js.map +1 -1
- package/dist/plugins/async-hooks-stub.d.ts +16 -0
- package/dist/plugins/async-hooks-stub.d.ts.map +1 -0
- package/dist/plugins/async-hooks-stub.js +45 -0
- package/dist/plugins/async-hooks-stub.js.map +1 -0
- package/dist/routing/app-router.d.ts +12 -6
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +19 -40
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +3 -9
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/utils.d.ts +9 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js +10 -0
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +6 -0
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/dev-module-runner.d.ts +84 -0
- package/dist/server/dev-module-runner.d.ts.map +1 -0
- package/dist/server/dev-module-runner.js +105 -0
- package/dist/server/dev-module-runner.js.map +1 -0
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/instrumentation.d.ts +52 -9
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +52 -15
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +1 -1
- package/dist/server/middleware-codegen.js +1 -1
- package/dist/server/middleware-codegen.js.map +1 -1
- package/dist/server/middleware.d.ts +7 -3
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +16 -6
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +33 -28
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +92 -0
- package/dist/server/request-pipeline.d.ts.map +1 -0
- package/dist/server/request-pipeline.js +202 -0
- package/dist/server/request-pipeline.js.map +1 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +14 -2
- 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 +170 -3
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +139 -29
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +2 -3
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +1 -0
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/layout-segment-context.d.ts +5 -4
- package/dist/shims/layout-segment-context.d.ts.map +1 -1
- package/dist/shims/layout-segment-context.js +6 -5
- package/dist/shims/layout-segment-context.js.map +1 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +33 -18
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +7 -1
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +9 -3
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation.d.ts +14 -11
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +122 -102
- 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/router.d.ts.map +1 -1
- package/dist/shims/router.js +37 -21
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/server.d.ts +2 -0
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +4 -0
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts +13 -0
- package/dist/shims/url-utils.d.ts.map +1 -0
- package/dist/shims/url-utils.js +28 -0
- package/dist/shims/url-utils.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 +48 -0
- package/dist/utils/project.js.map +1 -1
- package/package.json +1 -1
- 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,21 +1,27 @@
|
|
|
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
|
-
import {
|
|
9
|
+
import { createDirectRunner } from "./server/dev-module-runner.js";
|
|
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";
|
|
8
13
|
import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
|
|
9
|
-
import { findMiddlewareFile,
|
|
14
|
+
import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
|
|
10
15
|
import { logRequest, now } from "./server/request-log.js";
|
|
11
|
-
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
|
|
12
16
|
import { normalizePath } from "./server/normalize-path.js";
|
|
13
17
|
import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
|
|
14
18
|
import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js";
|
|
15
19
|
import { validateDevRequest } from "./server/dev-origin-check.js";
|
|
16
|
-
import {
|
|
20
|
+
import { isExternalUrl, proxyExternalRequest, matchHeaders, matchRedirect, matchRewrite, requestContextFromRequest, sanitizeDestination, } from "./config/config-matchers.js";
|
|
17
21
|
import { scanMetadataFiles } from "./server/metadata-routes.js";
|
|
18
22
|
import { detectPackageManager } from "./utils/project.js";
|
|
23
|
+
import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js";
|
|
24
|
+
import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js";
|
|
19
25
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
20
26
|
import react from "@vitejs/plugin-react";
|
|
21
27
|
import MagicString from "magic-string";
|
|
@@ -510,949 +516,7 @@ export default function vinext(options = {}) {
|
|
|
510
516
|
* This is the entry point for `vite build --ssr`.
|
|
511
517
|
*/
|
|
512
518
|
async function generateServerEntry() {
|
|
513
|
-
|
|
514
|
-
const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
515
|
-
// Generate import statements using absolute paths since virtual
|
|
516
|
-
// modules don't have a real file location for relative resolution.
|
|
517
|
-
const pageImports = pageRoutes.map((r, i) => {
|
|
518
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
519
|
-
return `import * as page_${i} from ${JSON.stringify(absPath)};`;
|
|
520
|
-
});
|
|
521
|
-
const apiImports = apiRoutes.map((r, i) => {
|
|
522
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
523
|
-
return `import * as api_${i} from ${JSON.stringify(absPath)};`;
|
|
524
|
-
});
|
|
525
|
-
// Build the route table — include filePath for SSR manifest lookup
|
|
526
|
-
const pageRouteEntries = pageRoutes.map((r, i) => {
|
|
527
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
528
|
-
return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`;
|
|
529
|
-
});
|
|
530
|
-
const apiRouteEntries = apiRoutes.map((r, i) => {
|
|
531
|
-
return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
|
|
532
|
-
});
|
|
533
|
-
// Check for _app and _document
|
|
534
|
-
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
535
|
-
const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
|
|
536
|
-
const hasApp = appFilePath !== null;
|
|
537
|
-
const hasDoc = docFilePath !== null;
|
|
538
|
-
const appImportCode = hasApp
|
|
539
|
-
? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
|
|
540
|
-
: `const AppComponent = null;`;
|
|
541
|
-
const docImportCode = hasDoc
|
|
542
|
-
? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
|
|
543
|
-
: `const DocumentComponent = null;`;
|
|
544
|
-
// Serialize i18n config for embedding in the server entry
|
|
545
|
-
const i18nConfigJson = nextConfig?.i18n
|
|
546
|
-
? JSON.stringify({
|
|
547
|
-
locales: nextConfig.i18n.locales,
|
|
548
|
-
defaultLocale: nextConfig.i18n.defaultLocale,
|
|
549
|
-
localeDetection: nextConfig.i18n.localeDetection,
|
|
550
|
-
})
|
|
551
|
-
: "null";
|
|
552
|
-
// Serialize the full resolved config for the production server.
|
|
553
|
-
// This embeds redirects, rewrites, headers, basePath, trailingSlash
|
|
554
|
-
// so prod-server.ts can apply them without loading next.config.js at runtime.
|
|
555
|
-
const vinextConfigJson = JSON.stringify({
|
|
556
|
-
basePath: nextConfig?.basePath ?? "",
|
|
557
|
-
trailingSlash: nextConfig?.trailingSlash ?? false,
|
|
558
|
-
redirects: nextConfig?.redirects ?? [],
|
|
559
|
-
rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
|
|
560
|
-
headers: nextConfig?.headers ?? [],
|
|
561
|
-
i18n: nextConfig?.i18n ?? null,
|
|
562
|
-
images: {
|
|
563
|
-
deviceSizes: nextConfig?.images?.deviceSizes,
|
|
564
|
-
imageSizes: nextConfig?.images?.imageSizes,
|
|
565
|
-
dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
|
|
566
|
-
contentDispositionType: nextConfig?.images?.contentDispositionType,
|
|
567
|
-
contentSecurityPolicy: nextConfig?.images?.contentSecurityPolicy,
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
// Generate middleware code if middleware.ts exists
|
|
571
|
-
const middlewareImportCode = middlewarePath
|
|
572
|
-
? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
|
|
573
|
-
import { NextRequest } from "next/server";`
|
|
574
|
-
: "";
|
|
575
|
-
// The matcher config is read from the middleware module at import time.
|
|
576
|
-
// We inline the matching + execution logic so the prod server can call it.
|
|
577
|
-
const middlewareExportCode = middlewarePath
|
|
578
|
-
? `
|
|
579
|
-
// --- Middleware support (generated from middleware-codegen.ts) ---
|
|
580
|
-
${generateNormalizePathCode("es5")}
|
|
581
|
-
${generateSafeRegExpCode("es5")}
|
|
582
|
-
${generateMiddlewareMatcherCode("es5")}
|
|
583
|
-
|
|
584
|
-
export async function runMiddleware(request) {
|
|
585
|
-
var isProxy = ${middlewarePath ? JSON.stringify(isProxyFile(middlewarePath)) : "false"};
|
|
586
|
-
var middlewareFn = isProxy
|
|
587
|
-
? (middlewareModule.proxy ?? middlewareModule.default)
|
|
588
|
-
: (middlewareModule.middleware ?? middlewareModule.default);
|
|
589
|
-
if (typeof middlewareFn !== "function") {
|
|
590
|
-
var fileType = isProxy ? "Proxy" : "Middleware";
|
|
591
|
-
var expectedExport = isProxy ? "proxy" : "middleware";
|
|
592
|
-
throw new Error("The " + fileType + " file must export a function named \`" + expectedExport + "\` or a \`default\` function.");
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
var config = middlewareModule.config;
|
|
596
|
-
var matcher = config && config.matcher;
|
|
597
|
-
var url = new URL(request.url);
|
|
598
|
-
|
|
599
|
-
// Normalize pathname before matching to prevent path-confusion bypasses
|
|
600
|
-
// (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
|
|
601
|
-
var decodedPathname;
|
|
602
|
-
try { decodedPathname = decodeURIComponent(url.pathname); } catch (e) {
|
|
603
|
-
return { continue: false, response: new Response("Bad Request", { status: 400 }) };
|
|
604
|
-
}
|
|
605
|
-
var normalizedPathname = __normalizePath(decodedPathname);
|
|
606
|
-
|
|
607
|
-
if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
|
|
608
|
-
|
|
609
|
-
// Construct a new Request with the decoded + normalized pathname so middleware
|
|
610
|
-
// always sees the same canonical path that the router uses.
|
|
611
|
-
var mwRequest = request;
|
|
612
|
-
if (normalizedPathname !== url.pathname) {
|
|
613
|
-
var mwUrl = new URL(url);
|
|
614
|
-
mwUrl.pathname = normalizedPathname;
|
|
615
|
-
mwRequest = new Request(mwUrl, request);
|
|
616
|
-
}
|
|
617
|
-
var nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
|
|
618
|
-
var response;
|
|
619
|
-
try { response = await middlewareFn(nextRequest); }
|
|
620
|
-
catch (e) {
|
|
621
|
-
console.error("[vinext] Middleware error:", e);
|
|
622
|
-
return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (!response) return { continue: true };
|
|
626
|
-
|
|
627
|
-
if (response.headers.get("x-middleware-next") === "1") {
|
|
628
|
-
var rHeaders = new Headers();
|
|
629
|
-
for (var [key, value] of response.headers) {
|
|
630
|
-
// Keep x-middleware-request-* headers so the production server can
|
|
631
|
-
// apply middleware-request header overrides before stripping internals
|
|
632
|
-
// from the final client response.
|
|
633
|
-
if (
|
|
634
|
-
!key.startsWith("x-middleware-") ||
|
|
635
|
-
key.startsWith("x-middleware-request-")
|
|
636
|
-
) rHeaders.append(key, value);
|
|
637
|
-
}
|
|
638
|
-
return { continue: true, responseHeaders: rHeaders };
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (response.status >= 300 && response.status < 400) {
|
|
642
|
-
var location = response.headers.get("Location") || response.headers.get("location");
|
|
643
|
-
if (location) return { continue: false, redirectUrl: location, redirectStatus: response.status };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
var rewriteUrl = response.headers.get("x-middleware-rewrite");
|
|
647
|
-
if (rewriteUrl) {
|
|
648
|
-
var rwHeaders = new Headers();
|
|
649
|
-
for (var [k, v] of response.headers) {
|
|
650
|
-
if (!k.startsWith("x-middleware-") || k.startsWith("x-middleware-request-")) rwHeaders.append(k, v);
|
|
651
|
-
}
|
|
652
|
-
var rewritePath;
|
|
653
|
-
try { var parsed = new URL(rewriteUrl, request.url); rewritePath = parsed.pathname + parsed.search; }
|
|
654
|
-
catch { rewritePath = rewriteUrl; }
|
|
655
|
-
return { continue: true, rewriteUrl: rewritePath, rewriteStatus: response.status !== 200 ? response.status : undefined, responseHeaders: rwHeaders };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return { continue: false, response: response };
|
|
659
|
-
}
|
|
660
|
-
`
|
|
661
|
-
: `
|
|
662
|
-
export async function runMiddleware() { return { continue: true }; }
|
|
663
|
-
`;
|
|
664
|
-
// The server entry is a self-contained module that uses Web-standard APIs
|
|
665
|
-
// (Request/Response, renderToReadableStream) so it runs on Cloudflare Workers.
|
|
666
|
-
return `
|
|
667
|
-
import React from "react";
|
|
668
|
-
import { renderToReadableStream } from "react-dom/server.edge";
|
|
669
|
-
import { resetSSRHead, getSSRHeadHTML } from "next/head";
|
|
670
|
-
import { flushPreloads } from "next/dynamic";
|
|
671
|
-
import { setSSRContext, wrapWithRouterContext } from "next/router";
|
|
672
|
-
import { getCacheHandler } from "next/cache";
|
|
673
|
-
import { runWithFetchCache } from "vinext/fetch-cache";
|
|
674
|
-
import { _runWithCacheState } from "next/cache";
|
|
675
|
-
import { runWithPrivateCache } from "vinext/cache-runtime";
|
|
676
|
-
import { runWithRouterState } from "vinext/router-state";
|
|
677
|
-
import { runWithHeadState } from "vinext/head-state";
|
|
678
|
-
import { safeJsonStringify } from "vinext/html";
|
|
679
|
-
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
|
|
680
|
-
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
|
|
681
|
-
${middlewareImportCode}
|
|
682
|
-
|
|
683
|
-
// i18n config (embedded at build time)
|
|
684
|
-
const i18nConfig = ${i18nConfigJson};
|
|
685
|
-
|
|
686
|
-
// Full resolved config for production server (embedded at build time)
|
|
687
|
-
export const vinextConfig = ${vinextConfigJson};
|
|
688
|
-
|
|
689
|
-
// ISR cache helpers (inlined for the server entry)
|
|
690
|
-
async function isrGet(key) {
|
|
691
|
-
const handler = getCacheHandler();
|
|
692
|
-
const result = await handler.get(key);
|
|
693
|
-
if (!result || !result.value) return null;
|
|
694
|
-
return { value: result, isStale: result.cacheState === "stale" };
|
|
695
|
-
}
|
|
696
|
-
async function isrSet(key, data, revalidateSeconds, tags) {
|
|
697
|
-
const handler = getCacheHandler();
|
|
698
|
-
await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
|
|
699
|
-
}
|
|
700
|
-
const pendingRegenerations = new Map();
|
|
701
|
-
function triggerBackgroundRegeneration(key, renderFn) {
|
|
702
|
-
if (pendingRegenerations.has(key)) return;
|
|
703
|
-
const promise = renderFn()
|
|
704
|
-
.catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
|
|
705
|
-
.finally(() => pendingRegenerations.delete(key));
|
|
706
|
-
pendingRegenerations.set(key, promise);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
async function renderToStringAsync(element) {
|
|
710
|
-
const stream = await renderToReadableStream(element);
|
|
711
|
-
await stream.allReady;
|
|
712
|
-
return new Response(stream).text();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
${pageImports.join("\n")}
|
|
716
|
-
${apiImports.join("\n")}
|
|
717
|
-
|
|
718
|
-
${appImportCode}
|
|
719
|
-
${docImportCode}
|
|
720
|
-
|
|
721
|
-
const pageRoutes = [
|
|
722
|
-
${pageRouteEntries.join(",\n")}
|
|
723
|
-
];
|
|
724
|
-
|
|
725
|
-
const apiRoutes = [
|
|
726
|
-
${apiRouteEntries.join(",\n")}
|
|
727
|
-
];
|
|
728
|
-
|
|
729
|
-
function matchRoute(url, routes) {
|
|
730
|
-
const pathname = url.split("?")[0];
|
|
731
|
-
let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
|
|
732
|
-
// NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
|
|
733
|
-
// the entry point. Decoding again would create a double-decode vector.
|
|
734
|
-
for (const route of routes) {
|
|
735
|
-
const params = matchPattern(normalizedUrl, route.pattern);
|
|
736
|
-
if (params !== null) return { route, params };
|
|
737
|
-
}
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function matchPattern(url, pattern) {
|
|
742
|
-
const urlParts = url.split("/").filter(Boolean);
|
|
743
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
744
|
-
const params = Object.create(null);
|
|
745
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
746
|
-
const pp = patternParts[i];
|
|
747
|
-
if (pp.endsWith("+")) {
|
|
748
|
-
const paramName = pp.slice(1, -1);
|
|
749
|
-
const remaining = urlParts.slice(i);
|
|
750
|
-
if (remaining.length === 0) return null;
|
|
751
|
-
params[paramName] = remaining;
|
|
752
|
-
return params;
|
|
753
|
-
}
|
|
754
|
-
if (pp.endsWith("*")) {
|
|
755
|
-
const paramName = pp.slice(1, -1);
|
|
756
|
-
params[paramName] = urlParts.slice(i);
|
|
757
|
-
return params;
|
|
758
|
-
}
|
|
759
|
-
if (pp.startsWith(":")) {
|
|
760
|
-
if (i >= urlParts.length) return null;
|
|
761
|
-
params[pp.slice(1)] = urlParts[i];
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (i >= urlParts.length || urlParts[i] !== pp) return null;
|
|
765
|
-
}
|
|
766
|
-
if (urlParts.length !== patternParts.length) return null;
|
|
767
|
-
return params;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function parseQuery(url) {
|
|
771
|
-
const qs = url.split("?")[1];
|
|
772
|
-
if (!qs) return {};
|
|
773
|
-
const p = new URLSearchParams(qs);
|
|
774
|
-
const q = {};
|
|
775
|
-
for (const [k, v] of p) {
|
|
776
|
-
if (k in q) {
|
|
777
|
-
q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
|
|
778
|
-
} else {
|
|
779
|
-
q[k] = v;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return q;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function patternToNextFormat(pattern) {
|
|
786
|
-
return pattern
|
|
787
|
-
.replace(/:([\\w]+)\\*/g, "[[...$1]]")
|
|
788
|
-
.replace(/:([\\w]+)\\+/g, "[...$1]")
|
|
789
|
-
.replace(/:([\\w]+)/g, "[$1]");
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function collectAssetTags(manifest, moduleIds) {
|
|
793
|
-
// Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
|
|
794
|
-
const m = (manifest && Object.keys(manifest).length > 0)
|
|
795
|
-
? manifest
|
|
796
|
-
: (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
|
|
797
|
-
const tags = [];
|
|
798
|
-
const seen = new Set();
|
|
799
|
-
|
|
800
|
-
// Load the set of lazy chunk filenames (only reachable via dynamic imports).
|
|
801
|
-
// These should NOT get <link rel="modulepreload"> or <script type="module">
|
|
802
|
-
// tags — they are fetched on demand when the dynamic import() executes (e.g.
|
|
803
|
-
// chunks behind React.lazy() or next/dynamic boundaries).
|
|
804
|
-
var lazyChunks = (typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__) || null;
|
|
805
|
-
var lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
|
|
806
|
-
|
|
807
|
-
// Inject the client entry script if embedded by vinext:cloudflare-build
|
|
808
|
-
if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
|
|
809
|
-
const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
|
|
810
|
-
seen.add(entry);
|
|
811
|
-
tags.push('<link rel="modulepreload" href="/' + entry + '" />');
|
|
812
|
-
tags.push('<script type="module" src="/' + entry + '" crossorigin></script>');
|
|
813
|
-
}
|
|
814
|
-
if (m) {
|
|
815
|
-
// Always inject shared chunks (framework, vinext runtime, entry) and
|
|
816
|
-
// page-specific chunks. The manifest maps module file paths to their
|
|
817
|
-
// associated JS/CSS assets.
|
|
818
|
-
//
|
|
819
|
-
// For page-specific injection, the module IDs may be absolute paths
|
|
820
|
-
// while the manifest uses relative paths. Try both the original ID
|
|
821
|
-
// and a suffix match to find the correct manifest entry.
|
|
822
|
-
var allFiles = [];
|
|
823
|
-
|
|
824
|
-
if (moduleIds && moduleIds.length > 0) {
|
|
825
|
-
// Collect assets for the requested page modules
|
|
826
|
-
for (var mi = 0; mi < moduleIds.length; mi++) {
|
|
827
|
-
var id = moduleIds[mi];
|
|
828
|
-
var files = m[id];
|
|
829
|
-
if (!files) {
|
|
830
|
-
// Absolute path didn't match — try matching by suffix.
|
|
831
|
-
// Manifest keys are relative (e.g. "pages/about.tsx") while
|
|
832
|
-
// moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
|
|
833
|
-
for (var mk in m) {
|
|
834
|
-
if (id.endsWith("/" + mk) || id === mk) {
|
|
835
|
-
files = m[mk];
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
if (files) {
|
|
841
|
-
for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Also inject shared chunks that every page needs: framework,
|
|
846
|
-
// vinext runtime, and the entry bootstrap. These are identified
|
|
847
|
-
// by scanning all manifest values for chunk filenames containing
|
|
848
|
-
// known prefixes.
|
|
849
|
-
for (var key in m) {
|
|
850
|
-
var vals = m[key];
|
|
851
|
-
if (!vals) continue;
|
|
852
|
-
for (var vi = 0; vi < vals.length; vi++) {
|
|
853
|
-
var file = vals[vi];
|
|
854
|
-
var basename = file.split("/").pop() || "";
|
|
855
|
-
if (
|
|
856
|
-
basename.startsWith("framework-") ||
|
|
857
|
-
basename.startsWith("vinext-") ||
|
|
858
|
-
basename.includes("vinext-client-entry") ||
|
|
859
|
-
basename.includes("vinext-app-browser-entry")
|
|
860
|
-
) {
|
|
861
|
-
allFiles.push(file);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
} else {
|
|
866
|
-
// No specific modules — include all assets from manifest
|
|
867
|
-
for (var akey in m) {
|
|
868
|
-
var avals = m[akey];
|
|
869
|
-
if (avals) {
|
|
870
|
-
for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
for (var ti = 0; ti < allFiles.length; ti++) {
|
|
876
|
-
var tf = allFiles[ti];
|
|
877
|
-
// Normalize: Vite's SSR manifest values include a leading '/'
|
|
878
|
-
// (from base path), but we prepend '/' ourselves when building
|
|
879
|
-
// href/src attributes. Strip any existing leading slash to avoid
|
|
880
|
-
// producing protocol-relative URLs like "//assets/chunk.js".
|
|
881
|
-
// This also ensures consistent keys for the seen-set dedup and
|
|
882
|
-
// lazySet.has() checks (which use values without leading slash).
|
|
883
|
-
if (tf.charAt(0) === '/') tf = tf.slice(1);
|
|
884
|
-
if (seen.has(tf)) continue;
|
|
885
|
-
seen.add(tf);
|
|
886
|
-
if (tf.endsWith(".css")) {
|
|
887
|
-
tags.push('<link rel="stylesheet" href="/' + tf + '" />');
|
|
888
|
-
} else if (tf.endsWith(".js")) {
|
|
889
|
-
// Skip lazy chunks — they are behind dynamic import() boundaries
|
|
890
|
-
// (React.lazy, next/dynamic) and should only be fetched on demand.
|
|
891
|
-
if (lazySet && lazySet.has(tf)) continue;
|
|
892
|
-
tags.push('<link rel="modulepreload" href="/' + tf + '" />');
|
|
893
|
-
tags.push('<script type="module" src="/' + tf + '" crossorigin></script>');
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
return tags.join("\\n ");
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// i18n helpers
|
|
901
|
-
function extractLocale(url) {
|
|
902
|
-
if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
|
|
903
|
-
const pathname = url.split("?")[0];
|
|
904
|
-
const parts = pathname.split("/").filter(Boolean);
|
|
905
|
-
const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
906
|
-
if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
|
|
907
|
-
const locale = parts[0];
|
|
908
|
-
const rest = "/" + parts.slice(1).join("/");
|
|
909
|
-
return { locale, url: (rest || "/") + query, hadPrefix: true };
|
|
910
|
-
}
|
|
911
|
-
return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function detectLocaleFromHeaders(headers) {
|
|
915
|
-
if (!i18nConfig) return null;
|
|
916
|
-
const acceptLang = headers.get("accept-language");
|
|
917
|
-
if (!acceptLang) return null;
|
|
918
|
-
const langs = acceptLang.split(",").map(function(part) {
|
|
919
|
-
const pieces = part.trim().split(";");
|
|
920
|
-
const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
|
|
921
|
-
return { lang: pieces[0].trim().toLowerCase(), q: q };
|
|
922
|
-
}).sort(function(a, b) { return b.q - a.q; });
|
|
923
|
-
for (let k = 0; k < langs.length; k++) {
|
|
924
|
-
const lang = langs[k].lang;
|
|
925
|
-
for (let j = 0; j < i18nConfig.locales.length; j++) {
|
|
926
|
-
if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
|
|
927
|
-
}
|
|
928
|
-
const prefix = lang.split("-")[0];
|
|
929
|
-
for (let j = 0; j < i18nConfig.locales.length; j++) {
|
|
930
|
-
const loc = i18nConfig.locales[j].toLowerCase();
|
|
931
|
-
if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
return null;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function parseCookieLocaleFromHeader(cookieHeader) {
|
|
938
|
-
if (!i18nConfig || !cookieHeader) return null;
|
|
939
|
-
const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
|
|
940
|
-
if (!match) return null;
|
|
941
|
-
var value;
|
|
942
|
-
try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
|
|
943
|
-
if (i18nConfig.locales.indexOf(value) !== -1) return value;
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function parseCookies(cookieHeader) {
|
|
948
|
-
const cookies = {};
|
|
949
|
-
if (!cookieHeader) return cookies;
|
|
950
|
-
for (const part of cookieHeader.split(";")) {
|
|
951
|
-
const [key, ...rest] = part.split("=");
|
|
952
|
-
if (key) cookies[key.trim()] = rest.join("=").trim();
|
|
953
|
-
}
|
|
954
|
-
return cookies;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Lightweight req/res facade for getServerSideProps and API routes.
|
|
958
|
-
// Next.js pages expect ctx.req/ctx.res with Node-like shapes.
|
|
959
|
-
function createReqRes(request, url, query, body) {
|
|
960
|
-
const headersObj = {};
|
|
961
|
-
for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
|
|
962
|
-
|
|
963
|
-
const req = {
|
|
964
|
-
method: request.method,
|
|
965
|
-
url: url,
|
|
966
|
-
headers: headersObj,
|
|
967
|
-
query: query,
|
|
968
|
-
body: body,
|
|
969
|
-
cookies: parseCookies(request.headers.get("cookie")),
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
let resStatusCode = 200;
|
|
973
|
-
const resHeaders = {};
|
|
974
|
-
// set-cookie needs array support (multiple Set-Cookie headers are common)
|
|
975
|
-
const setCookieHeaders = [];
|
|
976
|
-
let resBody = null;
|
|
977
|
-
let ended = false;
|
|
978
|
-
let resolveResponse;
|
|
979
|
-
const responsePromise = new Promise(function(r) { resolveResponse = r; });
|
|
980
|
-
|
|
981
|
-
const res = {
|
|
982
|
-
get statusCode() { return resStatusCode; },
|
|
983
|
-
set statusCode(code) { resStatusCode = code; },
|
|
984
|
-
writeHead: function(code, headers) {
|
|
985
|
-
resStatusCode = code;
|
|
986
|
-
if (headers) {
|
|
987
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
988
|
-
if (k.toLowerCase() === "set-cookie") {
|
|
989
|
-
if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
|
|
990
|
-
else { setCookieHeaders.push(v); }
|
|
991
|
-
} else {
|
|
992
|
-
resHeaders[k] = v;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return res;
|
|
997
|
-
},
|
|
998
|
-
setHeader: function(name, value) {
|
|
999
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
1000
|
-
if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
|
|
1001
|
-
else { setCookieHeaders.push(value); }
|
|
1002
|
-
} else {
|
|
1003
|
-
resHeaders[name.toLowerCase()] = value;
|
|
1004
|
-
}
|
|
1005
|
-
return res;
|
|
1006
|
-
},
|
|
1007
|
-
getHeader: function(name) {
|
|
1008
|
-
if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
|
|
1009
|
-
return resHeaders[name.toLowerCase()];
|
|
1010
|
-
},
|
|
1011
|
-
end: function(data) {
|
|
1012
|
-
if (ended) return;
|
|
1013
|
-
ended = true;
|
|
1014
|
-
if (data !== undefined && data !== null) resBody = data;
|
|
1015
|
-
const h = new Headers(resHeaders);
|
|
1016
|
-
for (const c of setCookieHeaders) h.append("set-cookie", c);
|
|
1017
|
-
resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
|
|
1018
|
-
},
|
|
1019
|
-
status: function(code) { resStatusCode = code; return res; },
|
|
1020
|
-
json: function(data) {
|
|
1021
|
-
resHeaders["content-type"] = "application/json";
|
|
1022
|
-
res.end(JSON.stringify(data));
|
|
1023
|
-
},
|
|
1024
|
-
send: function(data) {
|
|
1025
|
-
if (typeof data === "object" && data !== null) { res.json(data); }
|
|
1026
|
-
else { if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain"; res.end(String(data)); }
|
|
1027
|
-
},
|
|
1028
|
-
redirect: function(statusOrUrl, url2) {
|
|
1029
|
-
if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
|
|
1030
|
-
else { res.writeHead(statusOrUrl, { Location: url2 }); }
|
|
1031
|
-
res.end();
|
|
1032
|
-
},
|
|
1033
|
-
getHeaders: function() {
|
|
1034
|
-
var h = Object.assign({}, resHeaders);
|
|
1035
|
-
if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
|
|
1036
|
-
return h;
|
|
1037
|
-
},
|
|
1038
|
-
get headersSent() { return ended; },
|
|
1039
|
-
};
|
|
1040
|
-
|
|
1041
|
-
return { req, res, responsePromise };
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Read request body as text with a size limit.
|
|
1046
|
-
* Throws if the body exceeds maxBytes. This prevents DoS via chunked
|
|
1047
|
-
* transfer encoding where Content-Length is absent or spoofed.
|
|
1048
|
-
*/
|
|
1049
|
-
async function readBodyWithLimit(request, maxBytes) {
|
|
1050
|
-
if (!request.body) return "";
|
|
1051
|
-
var reader = request.body.getReader();
|
|
1052
|
-
var decoder = new TextDecoder();
|
|
1053
|
-
var chunks = [];
|
|
1054
|
-
var totalSize = 0;
|
|
1055
|
-
for (;;) {
|
|
1056
|
-
var result = await reader.read();
|
|
1057
|
-
if (result.done) break;
|
|
1058
|
-
totalSize += result.value.byteLength;
|
|
1059
|
-
if (totalSize > maxBytes) {
|
|
1060
|
-
reader.cancel();
|
|
1061
|
-
throw new Error("Request body too large");
|
|
1062
|
-
}
|
|
1063
|
-
chunks.push(decoder.decode(result.value, { stream: true }));
|
|
1064
|
-
}
|
|
1065
|
-
chunks.push(decoder.decode());
|
|
1066
|
-
return chunks.join("");
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
export async function renderPage(request, url, manifest) {
|
|
1070
|
-
const localeInfo = extractLocale(url);
|
|
1071
|
-
const locale = localeInfo.locale;
|
|
1072
|
-
const routeUrl = localeInfo.url;
|
|
1073
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1074
|
-
|
|
1075
|
-
// i18n redirect: check NEXT_LOCALE cookie first, then Accept-Language
|
|
1076
|
-
if (i18nConfig && !localeInfo.hadPrefix) {
|
|
1077
|
-
const cookieLocale = parseCookieLocaleFromHeader(cookieHeader);
|
|
1078
|
-
if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
|
|
1079
|
-
return new Response(null, { status: 307, headers: { Location: "/" + cookieLocale + routeUrl } });
|
|
1080
|
-
}
|
|
1081
|
-
if (!cookieLocale && i18nConfig.localeDetection !== false) {
|
|
1082
|
-
const detected = detectLocaleFromHeaders(request.headers);
|
|
1083
|
-
if (detected && detected !== i18nConfig.defaultLocale) {
|
|
1084
|
-
return new Response(null, { status: 307, headers: { Location: "/" + detected + routeUrl } });
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const match = matchRoute(routeUrl, pageRoutes);
|
|
1090
|
-
if (!match) {
|
|
1091
|
-
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
|
|
1092
|
-
{ status: 404, headers: { "Content-Type": "text/html" } });
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const { route, params } = match;
|
|
1096
|
-
return runWithRouterState(() =>
|
|
1097
|
-
runWithHeadState(() =>
|
|
1098
|
-
_runWithCacheState(() =>
|
|
1099
|
-
runWithPrivateCache(() =>
|
|
1100
|
-
runWithFetchCache(async () => {
|
|
1101
|
-
try {
|
|
1102
|
-
if (typeof setSSRContext === "function") {
|
|
1103
|
-
setSSRContext({
|
|
1104
|
-
pathname: routeUrl.split("?")[0],
|
|
1105
|
-
query: { ...params, ...parseQuery(routeUrl) },
|
|
1106
|
-
asPath: routeUrl,
|
|
1107
|
-
locale: locale,
|
|
1108
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1109
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (i18nConfig) {
|
|
1114
|
-
globalThis.__VINEXT_LOCALE__ = locale;
|
|
1115
|
-
globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
|
|
1116
|
-
globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const pageModule = route.module;
|
|
1120
|
-
const PageComponent = pageModule.default;
|
|
1121
|
-
if (!PageComponent) {
|
|
1122
|
-
return new Response("Page has no default export", { status: 500 });
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Handle getStaticPaths for dynamic routes
|
|
1126
|
-
if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
|
|
1127
|
-
const pathsResult = await pageModule.getStaticPaths({
|
|
1128
|
-
locales: i18nConfig ? i18nConfig.locales : [],
|
|
1129
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : "",
|
|
1130
|
-
});
|
|
1131
|
-
const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
|
|
1132
|
-
|
|
1133
|
-
if (fallback === false) {
|
|
1134
|
-
const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
|
|
1135
|
-
const isValidPath = paths.some(function(p) {
|
|
1136
|
-
return Object.entries(p.params).every(function(entry) {
|
|
1137
|
-
var key = entry[0], val = entry[1];
|
|
1138
|
-
var actual = params[key];
|
|
1139
|
-
if (Array.isArray(val)) {
|
|
1140
|
-
return Array.isArray(actual) && val.join("/") === actual.join("/");
|
|
1141
|
-
}
|
|
1142
|
-
return String(val) === String(actual);
|
|
1143
|
-
});
|
|
1144
|
-
});
|
|
1145
|
-
if (!isValidPath) {
|
|
1146
|
-
return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
|
|
1147
|
-
{ status: 404, headers: { "Content-Type": "text/html" } });
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
let pageProps = {};
|
|
1153
|
-
var gsspRes = null;
|
|
1154
|
-
if (typeof pageModule.getServerSideProps === "function") {
|
|
1155
|
-
const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1156
|
-
const ctx = {
|
|
1157
|
-
params, req, res,
|
|
1158
|
-
query: parseQuery(routeUrl),
|
|
1159
|
-
resolvedUrl: routeUrl,
|
|
1160
|
-
locale: locale,
|
|
1161
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1162
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1163
|
-
};
|
|
1164
|
-
const result = await pageModule.getServerSideProps(ctx);
|
|
1165
|
-
// If gSSP called res.end() directly (short-circuit), return that response.
|
|
1166
|
-
if (res.headersSent) {
|
|
1167
|
-
return await responsePromise;
|
|
1168
|
-
}
|
|
1169
|
-
if (result && result.props) pageProps = result.props;
|
|
1170
|
-
if (result && result.redirect) {
|
|
1171
|
-
var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1172
|
-
return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1173
|
-
}
|
|
1174
|
-
if (result && result.notFound) {
|
|
1175
|
-
return new Response("404", { status: 404 });
|
|
1176
|
-
}
|
|
1177
|
-
// Preserve the res object so headers/status/cookies set by gSSP
|
|
1178
|
-
// can be merged into the final HTML response.
|
|
1179
|
-
gsspRes = res;
|
|
1180
|
-
}
|
|
1181
|
-
// Build font Link header early so it's available for ISR cached responses too.
|
|
1182
|
-
// Font preloads are module-level state populated at import time and persist across requests.
|
|
1183
|
-
var _fontLinkHeader = "";
|
|
1184
|
-
var _allFp = [];
|
|
1185
|
-
try {
|
|
1186
|
-
var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
|
|
1187
|
-
var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
|
|
1188
|
-
_allFp = _fpGoogle.concat(_fpLocal);
|
|
1189
|
-
if (_allFp.length > 0) {
|
|
1190
|
-
_fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
|
|
1191
|
-
}
|
|
1192
|
-
} catch (e) { /* font preloads not available */ }
|
|
1193
|
-
|
|
1194
|
-
let isrRevalidateSeconds = null;
|
|
1195
|
-
if (typeof pageModule.getStaticProps === "function") {
|
|
1196
|
-
const pathname = routeUrl.split("?")[0];
|
|
1197
|
-
const cacheKey = "pages:" + (pathname === "/" ? "/" : pathname.replace(/\\/$/, ""));
|
|
1198
|
-
const cached = await isrGet(cacheKey);
|
|
1199
|
-
|
|
1200
|
-
if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
|
|
1201
|
-
var _hitHeaders = {
|
|
1202
|
-
"Content-Type": "text/html", "X-Vinext-Cache": "HIT",
|
|
1203
|
-
"Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
|
|
1204
|
-
};
|
|
1205
|
-
if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
|
|
1206
|
-
return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
|
|
1210
|
-
triggerBackgroundRegeneration(cacheKey, async function() {
|
|
1211
|
-
const freshResult = await pageModule.getStaticProps({ params });
|
|
1212
|
-
if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
|
|
1213
|
-
await isrSet(cacheKey, { kind: "PAGES", html: cached.value.value.html, pageData: freshResult.props, headers: undefined, status: undefined }, freshResult.revalidate);
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
var _staleHeaders = {
|
|
1217
|
-
"Content-Type": "text/html", "X-Vinext-Cache": "STALE",
|
|
1218
|
-
"Cache-Control": "s-maxage=0, stale-while-revalidate",
|
|
1219
|
-
};
|
|
1220
|
-
if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
|
|
1221
|
-
return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const ctx = {
|
|
1225
|
-
params,
|
|
1226
|
-
locale: locale,
|
|
1227
|
-
locales: i18nConfig ? i18nConfig.locales : undefined,
|
|
1228
|
-
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1229
|
-
};
|
|
1230
|
-
const result = await pageModule.getStaticProps(ctx);
|
|
1231
|
-
if (result && result.props) pageProps = result.props;
|
|
1232
|
-
if (result && result.redirect) {
|
|
1233
|
-
var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1234
|
-
return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1235
|
-
}
|
|
1236
|
-
if (result && result.notFound) {
|
|
1237
|
-
return new Response("404", { status: 404 });
|
|
1238
|
-
}
|
|
1239
|
-
if (typeof result.revalidate === "number" && result.revalidate > 0) {
|
|
1240
|
-
isrRevalidateSeconds = result.revalidate;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
let element;
|
|
1245
|
-
if (AppComponent) {
|
|
1246
|
-
element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1247
|
-
} else {
|
|
1248
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1249
|
-
}
|
|
1250
|
-
element = wrapWithRouterContext(element);
|
|
1251
|
-
|
|
1252
|
-
if (typeof resetSSRHead === "function") resetSSRHead();
|
|
1253
|
-
if (typeof flushPreloads === "function") await flushPreloads();
|
|
1254
|
-
|
|
1255
|
-
const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
|
|
1256
|
-
|
|
1257
|
-
// Collect SSR font data (Google Font links, font preloads, font-face styles)
|
|
1258
|
-
var fontHeadHTML = "";
|
|
1259
|
-
function _escAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """); }
|
|
1260
|
-
try {
|
|
1261
|
-
var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
|
|
1262
|
-
for (var fl of fontLinks) { fontHeadHTML += '<link rel="stylesheet" href="' + _escAttr(fl) + '" />\\n '; }
|
|
1263
|
-
} catch (e) { /* next/font/google not used */ }
|
|
1264
|
-
// Emit <link rel="preload"> for all font files (reuse _allFp collected earlier for Link header)
|
|
1265
|
-
for (var fp of _allFp) { fontHeadHTML += '<link rel="preload" href="' + _escAttr(fp.href) + '" as="font" type="' + _escAttr(fp.type) + '" crossorigin />\\n '; }
|
|
1266
|
-
try {
|
|
1267
|
-
var allFontStyles = [];
|
|
1268
|
-
if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
|
|
1269
|
-
if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
|
|
1270
|
-
if (allFontStyles.length > 0) { fontHeadHTML += '<style data-vinext-fonts>' + allFontStyles.join("\\n") + '</style>\\n '; }
|
|
1271
|
-
} catch (e) { /* font styles not available */ }
|
|
1272
|
-
|
|
1273
|
-
const pageModuleIds = route.filePath ? [route.filePath] : [];
|
|
1274
|
-
const assetTags = collectAssetTags(manifest, pageModuleIds);
|
|
1275
|
-
const nextDataPayload = {
|
|
1276
|
-
props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, isFallback: false,
|
|
1277
|
-
};
|
|
1278
|
-
if (i18nConfig) {
|
|
1279
|
-
nextDataPayload.locale = locale;
|
|
1280
|
-
nextDataPayload.locales = i18nConfig.locales;
|
|
1281
|
-
nextDataPayload.defaultLocale = i18nConfig.defaultLocale;
|
|
1282
|
-
}
|
|
1283
|
-
const localeGlobals = i18nConfig
|
|
1284
|
-
? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
|
|
1285
|
-
";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
|
|
1286
|
-
";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(i18nConfig.defaultLocale)
|
|
1287
|
-
: "";
|
|
1288
|
-
const nextDataScript = "<script>window.__NEXT_DATA__ = " + safeJsonStringify(nextDataPayload) + localeGlobals + "</script>";
|
|
1289
|
-
|
|
1290
|
-
// Build the document shell with a placeholder for the streamed body
|
|
1291
|
-
var BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
|
|
1292
|
-
var shellHtml;
|
|
1293
|
-
if (DocumentComponent) {
|
|
1294
|
-
const docElement = React.createElement(DocumentComponent);
|
|
1295
|
-
shellHtml = await renderToStringAsync(docElement);
|
|
1296
|
-
shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
|
|
1297
|
-
if (ssrHeadHTML || assetTags || fontHeadHTML) {
|
|
1298
|
-
shellHtml = shellHtml.replace("</head>", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>");
|
|
1299
|
-
}
|
|
1300
|
-
shellHtml = shellHtml.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
|
|
1301
|
-
if (!shellHtml.includes("__NEXT_DATA__")) {
|
|
1302
|
-
shellHtml = shellHtml.replace("</body>", " " + nextDataScript + "\\n</body>");
|
|
1303
|
-
}
|
|
1304
|
-
} else {
|
|
1305
|
-
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>";
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (typeof setSSRContext === "function") setSSRContext(null);
|
|
1309
|
-
|
|
1310
|
-
// Split the shell at the body marker
|
|
1311
|
-
var markerIdx = shellHtml.indexOf(BODY_MARKER);
|
|
1312
|
-
var shellPrefix = shellHtml.slice(0, markerIdx);
|
|
1313
|
-
var shellSuffix = shellHtml.slice(markerIdx + BODY_MARKER.length);
|
|
1314
|
-
|
|
1315
|
-
// Start the React body stream — progressive SSR (no allReady wait)
|
|
1316
|
-
var bodyStream = await renderToReadableStream(element);
|
|
1317
|
-
var encoder = new TextEncoder();
|
|
1318
|
-
|
|
1319
|
-
// Create a composite stream: prefix + body + suffix
|
|
1320
|
-
var compositeStream = new ReadableStream({
|
|
1321
|
-
async start(controller) {
|
|
1322
|
-
controller.enqueue(encoder.encode(shellPrefix));
|
|
1323
|
-
var reader = bodyStream.getReader();
|
|
1324
|
-
try {
|
|
1325
|
-
for (;;) {
|
|
1326
|
-
var chunk = await reader.read();
|
|
1327
|
-
if (chunk.done) break;
|
|
1328
|
-
controller.enqueue(chunk.value);
|
|
1329
|
-
}
|
|
1330
|
-
} finally {
|
|
1331
|
-
reader.releaseLock();
|
|
1332
|
-
}
|
|
1333
|
-
controller.enqueue(encoder.encode(shellSuffix));
|
|
1334
|
-
controller.close();
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
// Cache the rendered HTML for ISR (needs the full string — re-render synchronously)
|
|
1339
|
-
if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
|
|
1340
|
-
// Tee the stream so we can cache and respond simultaneously would be ideal,
|
|
1341
|
-
// but ISR responses are rare on first hit. Re-render to get complete HTML for cache.
|
|
1342
|
-
var isrElement;
|
|
1343
|
-
if (AppComponent) {
|
|
1344
|
-
isrElement = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1345
|
-
} else {
|
|
1346
|
-
isrElement = React.createElement(PageComponent, pageProps);
|
|
1347
|
-
}
|
|
1348
|
-
isrElement = wrapWithRouterContext(isrElement);
|
|
1349
|
-
var isrHtml = await renderToStringAsync(isrElement);
|
|
1350
|
-
var fullHtml = shellPrefix + isrHtml + shellSuffix;
|
|
1351
|
-
var isrPathname = url.split("?")[0];
|
|
1352
|
-
var isrCacheKey = "pages:" + (isrPathname === "/" ? "/" : isrPathname.replace(/\\/$/, ""));
|
|
1353
|
-
await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// Merge headers/status/cookies set by getServerSideProps on the res object.
|
|
1357
|
-
// gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
|
|
1358
|
-
var finalStatus = 200;
|
|
1359
|
-
const responseHeaders = new Headers({ "Content-Type": "text/html" });
|
|
1360
|
-
if (gsspRes) {
|
|
1361
|
-
finalStatus = gsspRes.statusCode;
|
|
1362
|
-
var gsspHeaders = gsspRes.getHeaders();
|
|
1363
|
-
for (var hk of Object.keys(gsspHeaders)) {
|
|
1364
|
-
var hv = gsspHeaders[hk];
|
|
1365
|
-
if (hk === "set-cookie" && Array.isArray(hv)) {
|
|
1366
|
-
for (var sc of hv) responseHeaders.append("set-cookie", sc);
|
|
1367
|
-
} else if (hv != null) {
|
|
1368
|
-
responseHeaders.set(hk, String(hv));
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
// Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
|
|
1372
|
-
responseHeaders.set("Content-Type", "text/html");
|
|
1373
|
-
}
|
|
1374
|
-
if (isrRevalidateSeconds) {
|
|
1375
|
-
responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
|
|
1376
|
-
responseHeaders.set("X-Vinext-Cache", "MISS");
|
|
1377
|
-
}
|
|
1378
|
-
// Set HTTP Link header for font preloading
|
|
1379
|
-
if (_fontLinkHeader) {
|
|
1380
|
-
responseHeaders.set("Link", _fontLinkHeader);
|
|
1381
|
-
}
|
|
1382
|
-
return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
|
|
1383
|
-
} catch (e) {
|
|
1384
|
-
console.error("[vinext] SSR error:", e);
|
|
1385
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
1386
|
-
}
|
|
1387
|
-
}) // end runWithFetchCache
|
|
1388
|
-
) // end runWithPrivateCache
|
|
1389
|
-
) // end _runWithCacheState
|
|
1390
|
-
) // end runWithHeadState
|
|
1391
|
-
); // end runWithRouterState
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
export async function handleApiRoute(request, url) {
|
|
1395
|
-
const match = matchRoute(url, apiRoutes);
|
|
1396
|
-
if (!match) {
|
|
1397
|
-
return new Response("404 - API route not found", { status: 404 });
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
const { route, params } = match;
|
|
1401
|
-
const handler = route.module.default;
|
|
1402
|
-
if (typeof handler !== "function") {
|
|
1403
|
-
return new Response("API route does not export a default function", { status: 500 });
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
const query = { ...params };
|
|
1407
|
-
const qs = url.split("?")[1];
|
|
1408
|
-
if (qs) {
|
|
1409
|
-
for (const [k, v] of new URLSearchParams(qs)) {
|
|
1410
|
-
if (k in query) {
|
|
1411
|
-
// Multi-value: promote to array (Next.js returns string[] for duplicate keys)
|
|
1412
|
-
query[k] = Array.isArray(query[k]) ? query[k].concat(v) : [query[k], v];
|
|
1413
|
-
} else {
|
|
1414
|
-
query[k] = v;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Parse request body (enforce 1MB limit to prevent memory exhaustion,
|
|
1420
|
-
// matching Next.js default bodyParser sizeLimit).
|
|
1421
|
-
// Check Content-Length first as a fast path, then enforce on the actual
|
|
1422
|
-
// stream to prevent bypasses via chunked transfer encoding.
|
|
1423
|
-
const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
|
|
1424
|
-
if (contentLength > 1 * 1024 * 1024) {
|
|
1425
|
-
return new Response("Request body too large", { status: 413 });
|
|
1426
|
-
}
|
|
1427
|
-
let body;
|
|
1428
|
-
const ct = request.headers.get("content-type") || "";
|
|
1429
|
-
let rawBody;
|
|
1430
|
-
try { rawBody = await readBodyWithLimit(request, 1 * 1024 * 1024); }
|
|
1431
|
-
catch { return new Response("Request body too large", { status: 413 }); }
|
|
1432
|
-
if (!rawBody) {
|
|
1433
|
-
body = undefined;
|
|
1434
|
-
} else if (ct.includes("application/json")) {
|
|
1435
|
-
try { body = JSON.parse(rawBody); } catch { body = rawBody; }
|
|
1436
|
-
} else {
|
|
1437
|
-
body = rawBody;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const { req, res, responsePromise } = createReqRes(request, url, query, body);
|
|
1441
|
-
|
|
1442
|
-
try {
|
|
1443
|
-
await handler(req, res);
|
|
1444
|
-
// If handler didn't call res.end(), end it now.
|
|
1445
|
-
// The end() method is idempotent — safe to call twice.
|
|
1446
|
-
res.end();
|
|
1447
|
-
return await responsePromise;
|
|
1448
|
-
} catch (e) {
|
|
1449
|
-
console.error("[vinext] API error:", e);
|
|
1450
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
${middlewareExportCode}
|
|
1455
|
-
`;
|
|
519
|
+
return _generateServerEntry(pagesDir, nextConfig, fileMatcher, middlewarePath, instrumentationPath);
|
|
1456
520
|
}
|
|
1457
521
|
/**
|
|
1458
522
|
* Generate the virtual client hydration entry module.
|
|
@@ -1463,84 +527,7 @@ ${middlewareExportCode}
|
|
|
1463
527
|
* __NEXT_DATA__ to determine which page to hydrate.
|
|
1464
528
|
*/
|
|
1465
529
|
async function generateClientEntry() {
|
|
1466
|
-
|
|
1467
|
-
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
1468
|
-
const hasApp = appFilePath !== null;
|
|
1469
|
-
// Build a map of route pattern -> dynamic import.
|
|
1470
|
-
// Keys must use Next.js bracket format (e.g. "/user/[id]") to match
|
|
1471
|
-
// __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
|
|
1472
|
-
const loaderEntries = pageRoutes.map((r) => {
|
|
1473
|
-
const absPath = r.filePath.replace(/\\/g, "/");
|
|
1474
|
-
const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
|
|
1475
|
-
// JSON.stringify safely escapes quotes, backslashes, and special chars in
|
|
1476
|
-
// both the route pattern and the absolute file path.
|
|
1477
|
-
// lgtm[js/bad-code-sanitization]
|
|
1478
|
-
return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
|
|
1479
|
-
});
|
|
1480
|
-
const appFileBase = appFilePath?.replace(/\\/g, "/");
|
|
1481
|
-
return `
|
|
1482
|
-
import React from "react";
|
|
1483
|
-
import { hydrateRoot } from "react-dom/client";
|
|
1484
|
-
// Eagerly import the router shim so its module-level popstate listener is
|
|
1485
|
-
// registered. Without this, browser back/forward buttons do nothing because
|
|
1486
|
-
// navigateClient() is never invoked on history changes.
|
|
1487
|
-
import "next/router";
|
|
1488
|
-
|
|
1489
|
-
const pageLoaders = {
|
|
1490
|
-
${loaderEntries.join(",\n")}
|
|
1491
|
-
};
|
|
1492
|
-
|
|
1493
|
-
async function hydrate() {
|
|
1494
|
-
const nextData = window.__NEXT_DATA__;
|
|
1495
|
-
if (!nextData) {
|
|
1496
|
-
console.error("[vinext] No __NEXT_DATA__ found");
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const { pageProps } = nextData.props;
|
|
1501
|
-
const loader = pageLoaders[nextData.page];
|
|
1502
|
-
if (!loader) {
|
|
1503
|
-
console.error("[vinext] No page loader for route:", nextData.page);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
const pageModule = await loader();
|
|
1508
|
-
const PageComponent = pageModule.default;
|
|
1509
|
-
if (!PageComponent) {
|
|
1510
|
-
console.error("[vinext] Page module has no default export");
|
|
1511
|
-
return;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
let element;
|
|
1515
|
-
${hasApp ? `
|
|
1516
|
-
try {
|
|
1517
|
-
const appModule = await import(${JSON.stringify(appFileBase)});
|
|
1518
|
-
const AppComponent = appModule.default;
|
|
1519
|
-
window.__VINEXT_APP__ = AppComponent;
|
|
1520
|
-
element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
|
|
1521
|
-
} catch {
|
|
1522
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1523
|
-
}
|
|
1524
|
-
` : `
|
|
1525
|
-
element = React.createElement(PageComponent, pageProps);
|
|
1526
|
-
`}
|
|
1527
|
-
|
|
1528
|
-
// Wrap with RouterContext.Provider so next/compat/router works during hydration
|
|
1529
|
-
const { wrapWithRouterContext } = await import("next/router");
|
|
1530
|
-
element = wrapWithRouterContext(element);
|
|
1531
|
-
|
|
1532
|
-
const container = document.getElementById("__next");
|
|
1533
|
-
if (!container) {
|
|
1534
|
-
console.error("[vinext] No #__next element found");
|
|
1535
|
-
return;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
const root = hydrateRoot(container, element);
|
|
1539
|
-
window.__VINEXT_ROOT__ = root;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
hydrate();
|
|
1543
|
-
`;
|
|
530
|
+
return _generateClientEntry(pagesDir, nextConfig, fileMatcher);
|
|
1544
531
|
}
|
|
1545
532
|
// Auto-register @vitejs/plugin-rsc when App Router is detected.
|
|
1546
533
|
// Check eagerly at call time using the same heuristic as config().
|
|
@@ -1677,7 +664,7 @@ hydrate();
|
|
|
1677
664
|
// Load next.config.js if present (always from project root, not src/)
|
|
1678
665
|
const phase = env?.command === "build" ? PHASE_PRODUCTION_BUILD : PHASE_DEVELOPMENT_SERVER;
|
|
1679
666
|
const rawConfig = await loadNextConfig(root, phase);
|
|
1680
|
-
nextConfig = await resolveNextConfig(rawConfig);
|
|
667
|
+
nextConfig = await resolveNextConfig(rawConfig, root);
|
|
1681
668
|
fileMatcher = createValidFileMatcher(nextConfig.pageExtensions);
|
|
1682
669
|
// Merge env from next.config.js with NEXT_PUBLIC_* env vars
|
|
1683
670
|
const defines = getNextPublicEnvDefines();
|
|
@@ -1717,6 +704,7 @@ hydrate();
|
|
|
1717
704
|
// Build the shim alias map — used by both resolve.alias and resolveId
|
|
1718
705
|
// (resolveId handles .js extension variants for libraries like nuqs)
|
|
1719
706
|
nextShimMap = {
|
|
707
|
+
...nextConfig.aliases,
|
|
1720
708
|
"next/link": path.join(shimsDir, "link"),
|
|
1721
709
|
"next/head": path.join(shimsDir, "head"),
|
|
1722
710
|
"next/router": path.join(shimsDir, "router"),
|
|
@@ -1894,12 +882,21 @@ hydrate();
|
|
|
1894
882
|
origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/,
|
|
1895
883
|
},
|
|
1896
884
|
},
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
885
|
+
// Configure SSR transform behaviour for Node targets.
|
|
886
|
+
// - `external`: React packages are loaded natively by Node (CJS)
|
|
887
|
+
// rather than through Vite's ESM evaluator.
|
|
888
|
+
// - `noExternal: true`: force everything else through Vite's
|
|
889
|
+
// transform pipeline so non-JS imports (CSS, images) from
|
|
890
|
+
// node_modules don't hit Node's native ESM loader.
|
|
891
|
+
// Any user-provided `ssr.noExternal` is intentionally superseded
|
|
892
|
+
// by this setting; only `ssr.external` entries escape Vite's transform.
|
|
1899
893
|
// Skip when targeting bundled runtimes (Cloudflare/Nitro bundle everything).
|
|
894
|
+
// This also resolves extensionless-import issues in packages like
|
|
895
|
+
// `validator` (see #189) by routing them through Vite's resolver.
|
|
1900
896
|
...(hasCloudflarePlugin || hasNitroPlugin ? {} : {
|
|
1901
897
|
ssr: {
|
|
1902
898
|
external: ["react", "react-dom", "react-dom/server"],
|
|
899
|
+
noExternal: true,
|
|
1903
900
|
},
|
|
1904
901
|
}),
|
|
1905
902
|
resolve: {
|
|
@@ -1919,8 +916,11 @@ hydrate();
|
|
|
1919
916
|
// Exclude vinext from dependency optimization so esbuild doesn't
|
|
1920
917
|
// scan dist files containing virtual module imports (virtual:vinext-*)
|
|
1921
918
|
// that only resolve at Vite plugin time, not during pre-bundling.
|
|
919
|
+
// Exclude @vercel/og so Vite's esbuild pre-bundler doesn't cache it
|
|
920
|
+
// before our vinext:og-font-patch transform can inline the font and
|
|
921
|
+
// patch the yoga WASM instantiation for workerd compatibility.
|
|
1922
922
|
optimizeDeps: {
|
|
1923
|
-
exclude: ["vinext"],
|
|
923
|
+
exclude: ["vinext", "@vercel/og"],
|
|
1924
924
|
},
|
|
1925
925
|
// Enable JSX in .tsx/.jsx files
|
|
1926
926
|
// Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc`
|
|
@@ -1934,6 +934,17 @@ hydrate();
|
|
|
1934
934
|
// Inject resolved PostCSS plugins if string names were found
|
|
1935
935
|
...(postcssOverride ? { css: { postcss: postcssOverride } } : {}),
|
|
1936
936
|
};
|
|
937
|
+
// Collect user-provided ssr.external so we can propagate it into
|
|
938
|
+
// both the RSC and SSR environment configs. Vite's `ssr.*` config
|
|
939
|
+
// only applies to the default `ssr` environment, not custom ones
|
|
940
|
+
// like `rsc`. Native addon packages (e.g. better-sqlite3) listed
|
|
941
|
+
// in ssr.external must be externalized from ALL server environments.
|
|
942
|
+
// Vite's SSROptions.external is `string[] | true`; handle both forms.
|
|
943
|
+
const userSsrExternal = Array.isArray(config.ssr?.external)
|
|
944
|
+
? config.ssr.external
|
|
945
|
+
: config.ssr?.external === true
|
|
946
|
+
? true
|
|
947
|
+
: [];
|
|
1937
948
|
// If app/ directory exists, configure RSC environments
|
|
1938
949
|
if (hasAppDir) {
|
|
1939
950
|
// Compute optimizeDeps.entries so Vite discovers server-side
|
|
@@ -1956,15 +967,23 @@ hydrate();
|
|
|
1956
967
|
// Note: Do NOT externalize react/react-dom here — they must
|
|
1957
968
|
// be bundled with the "react-server" condition for RSC.
|
|
1958
969
|
// Skip when targeting bundled runtimes (Cloudflare/Nitro).
|
|
1959
|
-
external: [
|
|
970
|
+
external: userSsrExternal === true ? true : [
|
|
1960
971
|
"satori",
|
|
1961
972
|
"@resvg/resvg-js",
|
|
1962
973
|
"yoga-wasm-web",
|
|
974
|
+
...userSsrExternal,
|
|
1963
975
|
],
|
|
976
|
+
// Force all node_modules through Vite's transform pipeline
|
|
977
|
+
// so non-JS imports (CSS, images) don't hit Node's native
|
|
978
|
+
// ESM loader. Matches Next.js behavior of bundling everything.
|
|
979
|
+
// Packages in `external` above take precedence per Vite rules.
|
|
980
|
+
// When user sets `ssr.external: true`, skip noExternal since
|
|
981
|
+
// everything is already externalized.
|
|
982
|
+
...(userSsrExternal === true ? {} : { noExternal: true }),
|
|
1964
983
|
},
|
|
1965
984
|
}),
|
|
1966
985
|
optimizeDeps: {
|
|
1967
|
-
exclude: ["vinext"],
|
|
986
|
+
exclude: ["vinext", "@vercel/og"],
|
|
1968
987
|
entries: appEntries,
|
|
1969
988
|
},
|
|
1970
989
|
build: {
|
|
@@ -1975,8 +994,19 @@ hydrate();
|
|
|
1975
994
|
},
|
|
1976
995
|
},
|
|
1977
996
|
ssr: {
|
|
997
|
+
...(hasCloudflarePlugin || hasNitroPlugin ? {} : {
|
|
998
|
+
resolve: {
|
|
999
|
+
external: userSsrExternal === true ? true : [...userSsrExternal],
|
|
1000
|
+
// Force all node_modules through Vite's transform pipeline
|
|
1001
|
+
// so non-JS imports (CSS, images) don't hit Node's native
|
|
1002
|
+
// ESM loader. Matches Next.js behavior of bundling everything.
|
|
1003
|
+
// When user sets `ssr.external: true`, skip noExternal since
|
|
1004
|
+
// everything is already externalized.
|
|
1005
|
+
...(userSsrExternal === true ? {} : { noExternal: true }),
|
|
1006
|
+
},
|
|
1007
|
+
}),
|
|
1978
1008
|
optimizeDeps: {
|
|
1979
|
-
exclude: ["vinext"],
|
|
1009
|
+
exclude: ["vinext", "@vercel/og"],
|
|
1980
1010
|
entries: appEntries,
|
|
1981
1011
|
},
|
|
1982
1012
|
build: {
|
|
@@ -2064,6 +1094,18 @@ hydrate();
|
|
|
2064
1094
|
" Or: pass rsc: false to vinext() if you want to configure rsc() yourself.");
|
|
2065
1095
|
}
|
|
2066
1096
|
}
|
|
1097
|
+
// Fail the build when targeting Cloudflare Workers without the
|
|
1098
|
+
// cloudflare() plugin. Without it, wrangler's esbuild can't resolve
|
|
1099
|
+
// virtual:vinext-rsc-entry and produces a cryptic error. (#325)
|
|
1100
|
+
if (config.command === "build" &&
|
|
1101
|
+
!hasCloudflarePlugin &&
|
|
1102
|
+
!hasNitroPlugin &&
|
|
1103
|
+
hasWranglerConfig(root)) {
|
|
1104
|
+
throw new Error(formatMissingCloudflarePluginError({
|
|
1105
|
+
isAppRouter: hasAppDir,
|
|
1106
|
+
configFile: config.configFile,
|
|
1107
|
+
}));
|
|
1108
|
+
}
|
|
2067
1109
|
},
|
|
2068
1110
|
resolveId: {
|
|
2069
1111
|
// Hook filter: only invoke JS for next/* imports and virtual:vinext-* modules.
|
|
@@ -2138,8 +1180,9 @@ hydrate();
|
|
|
2138
1180
|
rewrites: nextConfig?.rewrites,
|
|
2139
1181
|
headers: nextConfig?.headers,
|
|
2140
1182
|
allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
|
|
2141
|
-
allowedDevOrigins: nextConfig?.
|
|
2142
|
-
|
|
1183
|
+
allowedDevOrigins: nextConfig?.allowedDevOrigins,
|
|
1184
|
+
bodySizeLimit: nextConfig?.serverActionsBodySizeLimit,
|
|
1185
|
+
}, instrumentationPath);
|
|
2143
1186
|
}
|
|
2144
1187
|
if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
|
|
2145
1188
|
return generateSsrEntry();
|
|
@@ -2149,6 +1192,8 @@ hydrate();
|
|
|
2149
1192
|
}
|
|
2150
1193
|
},
|
|
2151
1194
|
},
|
|
1195
|
+
// Stub node:async_hooks in client builds — see src/plugins/async-hooks-stub.ts
|
|
1196
|
+
asyncHooksStubPlugin,
|
|
2152
1197
|
// Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily
|
|
2153
1198
|
// during vinext:config's config() (when MDX files are detected), but
|
|
2154
1199
|
// plugins returned from config() hooks run too late in the pipeline —
|
|
@@ -2167,6 +1212,10 @@ hydrate();
|
|
|
2167
1212
|
return fn.call(this, config, env);
|
|
2168
1213
|
},
|
|
2169
1214
|
transform(code, id, options) {
|
|
1215
|
+
// Skip ?raw and other query imports — @mdx-js/rollup ignores the query
|
|
1216
|
+
// and would compile the file as MDX instead of returning raw text.
|
|
1217
|
+
if (id.includes("?"))
|
|
1218
|
+
return;
|
|
2170
1219
|
if (!mdxDelegate?.transform)
|
|
2171
1220
|
return;
|
|
2172
1221
|
const hook = mdxDelegate.transform;
|
|
@@ -2242,6 +1291,32 @@ hydrate();
|
|
|
2242
1291
|
configureServer(server) {
|
|
2243
1292
|
// Watch pages directory for file additions/removals to invalidate route cache.
|
|
2244
1293
|
const pageExtensions = fileMatcher.extensionRegex;
|
|
1294
|
+
// Build a long-lived ModuleRunner for loading all Pages Router modules
|
|
1295
|
+
// (middleware, API routes, SSR page rendering) on every request.
|
|
1296
|
+
//
|
|
1297
|
+
// We must NOT use server.ssrLoadModule() here: when @cloudflare/vite-plugin
|
|
1298
|
+
// is present its environments replace the SSR transport, causing
|
|
1299
|
+
// SSRCompatModuleRunner to crash with:
|
|
1300
|
+
// TypeError: Cannot read properties of undefined (reading 'outsideEmitter')
|
|
1301
|
+
// on the very first request.
|
|
1302
|
+
//
|
|
1303
|
+
// createDirectRunner() builds a runner on environment.fetchModule() which
|
|
1304
|
+
// is a plain async method — safe with all plugin combinations, including
|
|
1305
|
+
// @cloudflare/vite-plugin.
|
|
1306
|
+
//
|
|
1307
|
+
// The runner is created lazily on first use so that all environments are
|
|
1308
|
+
// fully registered before we inspect them. We prefer "ssr", then any
|
|
1309
|
+
// non-"rsc" environment, then whatever is available.
|
|
1310
|
+
let pagesRunner = null;
|
|
1311
|
+
function getPagesRunner() {
|
|
1312
|
+
if (!pagesRunner) {
|
|
1313
|
+
const env = server.environments["ssr"] ??
|
|
1314
|
+
Object.values(server.environments).find((e) => e !== server.environments["rsc"]) ??
|
|
1315
|
+
Object.values(server.environments)[0];
|
|
1316
|
+
pagesRunner = createDirectRunner(env);
|
|
1317
|
+
}
|
|
1318
|
+
return pagesRunner;
|
|
1319
|
+
}
|
|
2245
1320
|
/**
|
|
2246
1321
|
* Invalidate the virtual RSC entry module in Vite's module graph.
|
|
2247
1322
|
*
|
|
@@ -2291,7 +1366,7 @@ hydrate();
|
|
|
2291
1366
|
"x-forwarded-host": req.headers["x-forwarded-host"],
|
|
2292
1367
|
"sec-fetch-site": req.headers["sec-fetch-site"],
|
|
2293
1368
|
"sec-fetch-mode": req.headers["sec-fetch-mode"],
|
|
2294
|
-
}, nextConfig?.
|
|
1369
|
+
}, nextConfig?.allowedDevOrigins);
|
|
2295
1370
|
if (blockReason) {
|
|
2296
1371
|
console.warn(`[vinext] Blocked dev request: ${blockReason} (${req.url})`);
|
|
2297
1372
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -2303,11 +1378,28 @@ hydrate();
|
|
|
2303
1378
|
// Return a function to register middleware AFTER Vite's built-in middleware
|
|
2304
1379
|
return () => {
|
|
2305
1380
|
// Run instrumentation.ts register() if present (once at server startup).
|
|
2306
|
-
// Must be inside the returned function
|
|
2307
|
-
//
|
|
2308
|
-
//
|
|
2309
|
-
|
|
2310
|
-
|
|
1381
|
+
// Must be inside the returned function so that all environments are
|
|
1382
|
+
// fully registered before getPagesRunner() inspects them.
|
|
1383
|
+
//
|
|
1384
|
+
// App Router: register() is baked into the generated RSC entry as a
|
|
1385
|
+
// top-level await, so it runs inside the Worker process (or RSC Vite
|
|
1386
|
+
// environment) — the same process as request handling. Calling
|
|
1387
|
+
// runInstrumentation() here too would run it a second time in the host
|
|
1388
|
+
// process, which is wrong when @cloudflare/vite-plugin is present.
|
|
1389
|
+
//
|
|
1390
|
+
// Pages Router prod: register() is baked into generateServerEntry() as
|
|
1391
|
+
// a top-level await, so it runs inside the Worker bundle — the same
|
|
1392
|
+
// process as request handling. configureServer() is never called during
|
|
1393
|
+
// a prod build, so there is no double-invocation risk there either.
|
|
1394
|
+
//
|
|
1395
|
+
// We pass getPagesRunner() (createDirectRunner) rather than server so
|
|
1396
|
+
// that this is safe when @cloudflare/vite-plugin is present. That
|
|
1397
|
+
// plugin replaces the SSR environment's hot channel, causing
|
|
1398
|
+
// server.ssrLoadModule() to crash with outsideEmitter. The runner
|
|
1399
|
+
// calls environment.fetchModule() directly and never touches the hot
|
|
1400
|
+
// channel, making it safe with all Vite plugin combinations.
|
|
1401
|
+
if (instrumentationPath && !hasAppDir) {
|
|
1402
|
+
runInstrumentation(getPagesRunner(), instrumentationPath).catch((err) => {
|
|
2311
1403
|
console.error("[vinext] Instrumentation error:", err);
|
|
2312
1404
|
});
|
|
2313
1405
|
}
|
|
@@ -2446,7 +1538,7 @@ hydrate();
|
|
|
2446
1538
|
"x-forwarded-host": req.headers["x-forwarded-host"],
|
|
2447
1539
|
"sec-fetch-site": req.headers["sec-fetch-site"],
|
|
2448
1540
|
"sec-fetch-mode": req.headers["sec-fetch-mode"],
|
|
2449
|
-
}, nextConfig?.
|
|
1541
|
+
}, nextConfig?.allowedDevOrigins);
|
|
2450
1542
|
if (blockReason) {
|
|
2451
1543
|
console.warn(`[vinext] Blocked dev request: ${blockReason} (${url})`);
|
|
2452
1544
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
@@ -2541,7 +1633,7 @@ hydrate();
|
|
|
2541
1633
|
}
|
|
2542
1634
|
// Normalize trailing slash based on next.config.js trailingSlash setting.
|
|
2543
1635
|
// Redirect to the canonical form if needed.
|
|
2544
|
-
if (nextConfig && pathname !== "/" && !pathname.startsWith("/api")) {
|
|
1636
|
+
if (nextConfig && pathname !== "/" && pathname !== "/api" && !pathname.startsWith("/api/")) {
|
|
2545
1637
|
const hasTrailing = pathname.endsWith("/");
|
|
2546
1638
|
if (nextConfig.trailingSlash && !hasTrailing) {
|
|
2547
1639
|
// trailingSlash: true — redirect /about → /about/
|
|
@@ -2575,7 +1667,7 @@ hydrate();
|
|
|
2575
1667
|
.filter(([, v]) => v !== undefined)
|
|
2576
1668
|
.map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])),
|
|
2577
1669
|
});
|
|
2578
|
-
const result = await runMiddleware(
|
|
1670
|
+
const result = await runMiddleware(getPagesRunner(), middlewarePath, middlewareRequest);
|
|
2579
1671
|
if (!result.continue) {
|
|
2580
1672
|
if (result.redirectUrl) {
|
|
2581
1673
|
const redirectHeaders = { Location: result.redirectUrl };
|
|
@@ -2626,23 +1718,50 @@ hydrate();
|
|
|
2626
1718
|
// Apply middleware rewrite (URL and optional status code)
|
|
2627
1719
|
if (result.rewriteUrl) {
|
|
2628
1720
|
url = result.rewriteUrl;
|
|
1721
|
+
// Write the rewritten URL back onto req.url so every subsequent
|
|
1722
|
+
// handler in the connect chain sees the correct path. The local
|
|
1723
|
+
// `url` variable is only visible within this handler — anything
|
|
1724
|
+
// further down the chain (Vite's built-in middleware, the
|
|
1725
|
+
// Cloudflare plugin's handler, or any other connect middleware)
|
|
1726
|
+
// reads req.url directly. Without this, a middleware rewrite
|
|
1727
|
+
// would be invisible to those handlers and the original URL
|
|
1728
|
+
// would be dispatched instead.
|
|
1729
|
+
req.url = url;
|
|
2629
1730
|
}
|
|
2630
1731
|
if (result.rewriteStatus) {
|
|
2631
1732
|
req.__vinextRewriteStatus = result.rewriteStatus;
|
|
2632
1733
|
}
|
|
2633
1734
|
}
|
|
1735
|
+
// ── Cloudflare Workers dev mode ────────────────────────────
|
|
1736
|
+
// When @cloudflare/vite-plugin is present, ALL rendering runs
|
|
1737
|
+
// inside the miniflare Worker subprocess — both App Router (via
|
|
1738
|
+
// virtual:vinext-rsc-entry) and Pages Router (via
|
|
1739
|
+
// virtual:vinext-server-entry → renderPage/handleApiRoute).
|
|
1740
|
+
//
|
|
1741
|
+
// The Worker entry already handles config redirects, rewrites,
|
|
1742
|
+
// headers, and all routing internally. Running them here too
|
|
1743
|
+
// would duplicate that logic and produce incorrect behaviour
|
|
1744
|
+
// (double redirects, headers set on the wrong object, etc.).
|
|
1745
|
+
//
|
|
1746
|
+
// Middleware.ts is the only thing that belongs in the host connect
|
|
1747
|
+
// handler — it has already run above. Any terminal middleware
|
|
1748
|
+
// result (redirect, block response) has already been sent.
|
|
1749
|
+
// Any rewrite has been written back to req.url above so the
|
|
1750
|
+
// Cloudflare plugin's handler sees the correct path.
|
|
1751
|
+
//
|
|
1752
|
+
// Call next() to hand off to the Cloudflare plugin's connect
|
|
1753
|
+
// handler, which dispatches the request to miniflare.
|
|
1754
|
+
if (hasCloudflarePlugin)
|
|
1755
|
+
return next();
|
|
2634
1756
|
// Build request context once for has/missing condition checks
|
|
2635
1757
|
// across headers, redirects, and rewrites.
|
|
1758
|
+
// Convert Node.js IncomingMessage headers to a Web Request for
|
|
1759
|
+
// requestContextFromRequest(), which uses the standard Web API.
|
|
2636
1760
|
const reqUrl = new URL(url, `http://${req.headers.host || "localhost"}`);
|
|
2637
1761
|
const reqCtxHeaders = new Headers(Object.fromEntries(Object.entries(req.headers)
|
|
2638
1762
|
.filter(([, v]) => v !== undefined)
|
|
2639
1763
|
.map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
|
|
2640
|
-
const reqCtx = {
|
|
2641
|
-
headers: reqCtxHeaders,
|
|
2642
|
-
cookies: parseCookies(reqCtxHeaders.get("cookie")),
|
|
2643
|
-
query: reqUrl.searchParams,
|
|
2644
|
-
host: reqCtxHeaders.get("host") ?? reqUrl.host,
|
|
2645
|
-
};
|
|
1764
|
+
const reqCtx = requestContextFromRequest(new Request(reqUrl, { headers: reqCtxHeaders }));
|
|
2646
1765
|
// Apply custom headers from next.config.js
|
|
2647
1766
|
if (nextConfig?.headers.length) {
|
|
2648
1767
|
applyHeaders(pathname, res, nextConfig.headers, reqCtx);
|
|
@@ -3185,11 +2304,110 @@ hydrate();
|
|
|
3185
2304
|
},
|
|
3186
2305
|
},
|
|
3187
2306
|
},
|
|
3188
|
-
//
|
|
3189
|
-
//
|
|
3190
|
-
//
|
|
3191
|
-
//
|
|
3192
|
-
//
|
|
2307
|
+
// Inline binary assets fetched via `fetch(new URL("./asset", import.meta.url))`.
|
|
2308
|
+
//
|
|
2309
|
+
// Some bundled libraries (notably @vercel/og) load assets at module init time
|
|
2310
|
+
// with the pattern:
|
|
2311
|
+
//
|
|
2312
|
+
// fetch(new URL("./some-font.ttf", import.meta.url)).then(res => res.arrayBuffer())
|
|
2313
|
+
//
|
|
2314
|
+
// This works in browser and standard Node.js because import.meta.url is a real
|
|
2315
|
+
// file:// URL. In Cloudflare Workers (both wrangler dev and production), however,
|
|
2316
|
+
// import.meta.url is the string "worker" — not a URL — so new URL(...) throws
|
|
2317
|
+
// "TypeError: Invalid URL string" and the Worker fails to start.
|
|
2318
|
+
//
|
|
2319
|
+
// Fix: at Vite transform time, find every such pattern, resolve the referenced
|
|
2320
|
+
// file relative to the module's actual path on disk (available as `id`), read it,
|
|
2321
|
+
// and replace the entire fetch(new URL(...)) expression with an inline base64 IIFE
|
|
2322
|
+
// that resolves synchronously. This eliminates the runtime fetch entirely and works
|
|
2323
|
+
// in all environments (workerd, Node.js, browser).
|
|
2324
|
+
//
|
|
2325
|
+
// Note: WASM files imported via `import ... from "./foo.wasm?module"` are handled
|
|
2326
|
+
// by the bundler/Vite directly and do not need this treatment. Only assets that
|
|
2327
|
+
// are runtime-fetched (not statically imported) need to be inlined here.
|
|
2328
|
+
{
|
|
2329
|
+
name: "vinext:og-inline-fetch-assets",
|
|
2330
|
+
enforce: "pre",
|
|
2331
|
+
transform(code, id) {
|
|
2332
|
+
// Quick bail-out: only process modules that use new URL(..., import.meta.url)
|
|
2333
|
+
if (!code.includes("import.meta.url")) {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
const moduleDir = path.dirname(id);
|
|
2337
|
+
let newCode = code;
|
|
2338
|
+
let didReplace = false;
|
|
2339
|
+
// Pattern 1 — edge build: fetch(new URL("./file", import.meta.url)).then((res) => res.arrayBuffer())
|
|
2340
|
+
// Replace with an inline IIFE that decodes the asset as base64 and returns Promise<ArrayBuffer>.
|
|
2341
|
+
if (code.includes("fetch(")) {
|
|
2342
|
+
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;
|
|
2343
|
+
for (const match of code.matchAll(fetchPattern)) {
|
|
2344
|
+
const fullMatch = match[0];
|
|
2345
|
+
const relPath = match[2]; // e.g. "./noto-sans-v27-latin-regular.ttf"
|
|
2346
|
+
const absPath = path.resolve(moduleDir, relPath);
|
|
2347
|
+
let fileBase64;
|
|
2348
|
+
try {
|
|
2349
|
+
fileBase64 = fs.readFileSync(absPath).toString("base64");
|
|
2350
|
+
}
|
|
2351
|
+
catch {
|
|
2352
|
+
// File not found on disk — skip (may be a runtime-only asset)
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
// Replace fetch(...).then(...) with an inline IIFE that returns Promise<ArrayBuffer>.
|
|
2356
|
+
const inlined = [
|
|
2357
|
+
`(function(){`,
|
|
2358
|
+
`var b=${JSON.stringify(fileBase64)};`,
|
|
2359
|
+
`var r=atob(b);`,
|
|
2360
|
+
`var a=new Uint8Array(r.length);`,
|
|
2361
|
+
`for(var i=0;i<r.length;i++)a[i]=r.charCodeAt(i);`,
|
|
2362
|
+
`return Promise.resolve(a.buffer);`,
|
|
2363
|
+
`})()`,
|
|
2364
|
+
].join("");
|
|
2365
|
+
newCode = newCode.replaceAll(fullMatch, inlined);
|
|
2366
|
+
didReplace = true;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
// Pattern 2 — node build: readFileSync(fileURLToPath(new URL("./file", import.meta.url)))
|
|
2370
|
+
// Replace with Buffer.from("<base64>", "base64"), which returns a Buffer (compatible with
|
|
2371
|
+
// both font data passed to satori and WASM bytes passed to initWasm).
|
|
2372
|
+
if (code.includes("readFileSync(")) {
|
|
2373
|
+
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;
|
|
2374
|
+
for (const match of newCode.matchAll(readFilePattern)) {
|
|
2375
|
+
const fullMatch = match[0];
|
|
2376
|
+
const relPath = match[2]; // e.g. "./noto-sans-v27-latin-regular.ttf"
|
|
2377
|
+
const absPath = path.resolve(moduleDir, relPath);
|
|
2378
|
+
let fileBase64;
|
|
2379
|
+
try {
|
|
2380
|
+
fileBase64 = fs.readFileSync(absPath).toString("base64");
|
|
2381
|
+
}
|
|
2382
|
+
catch {
|
|
2383
|
+
// File not found on disk — skip
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
// Replace readFileSync(...) with Buffer.from("<base64>", "base64").
|
|
2387
|
+
// Buffer is always available in Node.js and in the vinext SSR/RSC environments.
|
|
2388
|
+
const inlined = `Buffer.from(${JSON.stringify(fileBase64)},"base64")`;
|
|
2389
|
+
newCode = newCode.replaceAll(fullMatch, inlined);
|
|
2390
|
+
didReplace = true;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (!didReplace)
|
|
2394
|
+
return null;
|
|
2395
|
+
return { code: newCode, map: null };
|
|
2396
|
+
},
|
|
2397
|
+
},
|
|
2398
|
+
// Copy @vercel/og binary assets to the RSC output directory for production builds.
|
|
2399
|
+
//
|
|
2400
|
+
// The edge build (dist/index.edge.js) uses:
|
|
2401
|
+
// - fetch(new URL("./noto-sans...", import.meta.url)) → inlined by og-inline-fetch-assets
|
|
2402
|
+
// - import resvg_wasm from "./resvg.wasm?module" → static Vite import, emitted by Rollup
|
|
2403
|
+
//
|
|
2404
|
+
// The node build (dist/index.node.js) uses:
|
|
2405
|
+
// - fs.readFileSync(fileURLToPath(new URL("./noto-sans...", import.meta.url))) → inlined
|
|
2406
|
+
// - fs.readFileSync(fileURLToPath(new URL("./resvg.wasm", import.meta.url))) → inlined
|
|
2407
|
+
//
|
|
2408
|
+
// Both builds' font + WASM assets are inlined as base64 by vinext:og-inline-fetch-assets,
|
|
2409
|
+
// so no file copy is strictly needed. This plugin is kept as a safety net for any edge-build
|
|
2410
|
+
// ?module WASM imports that Rollup/Vite might not emit correctly in the RSC environment.
|
|
3193
2411
|
{
|
|
3194
2412
|
name: "vinext:og-assets",
|
|
3195
2413
|
apply: "build",
|
|
@@ -3209,8 +2427,9 @@ hydrate();
|
|
|
3209
2427
|
if (!fs.existsSync(indexPath))
|
|
3210
2428
|
return;
|
|
3211
2429
|
const content = fs.readFileSync(indexPath, "utf-8");
|
|
2430
|
+
// The font is inlined as base64 by vinext:og-inline-fetch-assets, so only
|
|
2431
|
+
// the WASM needs to be present as a file alongside the bundle.
|
|
3212
2432
|
const ogAssets = [
|
|
3213
|
-
"noto-sans-v27-latin-regular.ttf",
|
|
3214
2433
|
"resvg.wasm",
|
|
3215
2434
|
];
|
|
3216
2435
|
// Only copy if the bundle actually references these files
|
|
@@ -3413,6 +2632,58 @@ hydrate();
|
|
|
3413
2632
|
},
|
|
3414
2633
|
},
|
|
3415
2634
|
},
|
|
2635
|
+
{
|
|
2636
|
+
// @vercel/og patch for workerd (cloudflare-dev + cloudflare-workers)
|
|
2637
|
+
//
|
|
2638
|
+
// @vercel/og/dist/index.edge.js has one remaining workerd issue after the
|
|
2639
|
+
// generic vinext:og-inline-fetch-assets plugin runs (which already handles
|
|
2640
|
+
// the font fetch pattern):
|
|
2641
|
+
//
|
|
2642
|
+
// YOGA WASM: yoga-layout embeds its WASM as a base64 data URL and instantiates
|
|
2643
|
+
// it via WebAssembly.instantiate(bytes) at runtime.
|
|
2644
|
+
// workerd forbids dynamic WASM compilation from bytes — WASM must be loaded
|
|
2645
|
+
// through the module system as a pre-compiled WebAssembly.Module.
|
|
2646
|
+
// Fix: extract the yoga WASM bytes at Vite transform time (Node.js), write
|
|
2647
|
+
// yoga.wasm to @vercel/og/dist/, import it via `?module` so @cloudflare/vite-plugin
|
|
2648
|
+
// can serve it through the module system, and inject h2.instantiateWasm to
|
|
2649
|
+
// use the pre-compiled module instead of bytes.
|
|
2650
|
+
name: "vinext:og-font-patch",
|
|
2651
|
+
enforce: "pre",
|
|
2652
|
+
transform(code, id) {
|
|
2653
|
+
if (!id.includes("@vercel/og") || !id.includes("index.edge.js"))
|
|
2654
|
+
return null;
|
|
2655
|
+
let result = code;
|
|
2656
|
+
// ── Extract yoga WASM and import via ?module ──────────────────────────────────
|
|
2657
|
+
// yoga-layout's emscripten bundle sets H to a data URL containing the yoga WASM,
|
|
2658
|
+
// then later calls WebAssembly.instantiate(bytes, imports), which workerd rejects.
|
|
2659
|
+
// Emscripten supports a custom h2.instantiateWasm(imports, callback) escape hatch
|
|
2660
|
+
// that we inject to use a pre-compiled WebAssembly.Module loaded via ?module.
|
|
2661
|
+
const YOGA_DATA_URL_RE = /H = "data:application\/octet-stream;base64,([A-Za-z0-9+/]+=*)";/;
|
|
2662
|
+
const yogaMatch = YOGA_DATA_URL_RE.exec(result);
|
|
2663
|
+
if (yogaMatch) {
|
|
2664
|
+
const yogaBase64 = yogaMatch[1];
|
|
2665
|
+
const distDir = path.dirname(id);
|
|
2666
|
+
const yogaWasmPath = path.join(distDir, "yoga.wasm");
|
|
2667
|
+
// Write yoga.wasm to disk idempotently at transform time (Node.js side)
|
|
2668
|
+
if (!fs.existsSync(yogaWasmPath)) {
|
|
2669
|
+
fs.writeFileSync(yogaWasmPath, Buffer.from(yogaBase64, "base64"));
|
|
2670
|
+
}
|
|
2671
|
+
// Disable the data-URL branch so emscripten doesn't try to instantiate from bytes
|
|
2672
|
+
result = result.replace(yogaMatch[0], `H = "";`);
|
|
2673
|
+
// Patch the loadYoga call site to inject instantiateWasm using the ?module import
|
|
2674
|
+
const YOGA_CALL = `yoga_wasm_base64_esm_default()`;
|
|
2675
|
+
const YOGA_CALL_PATCHED = `yoga_wasm_base64_esm_default({ instantiateWasm: function(imports, callback) {` +
|
|
2676
|
+
` WebAssembly.instantiate(yoga_wasm_module, imports).then(function(inst) { callback(inst); });` +
|
|
2677
|
+
` return {}; } })`;
|
|
2678
|
+
result = result.replace(YOGA_CALL, YOGA_CALL_PATCHED);
|
|
2679
|
+
// Prepend the yoga wasm ?module import so @cloudflare/vite-plugin handles it
|
|
2680
|
+
result = `import yoga_wasm_module from "./yoga.wasm?module";\n` + result;
|
|
2681
|
+
}
|
|
2682
|
+
if (result === code)
|
|
2683
|
+
return null;
|
|
2684
|
+
return { code: result, map: null };
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
3416
2687
|
];
|
|
3417
2688
|
// Append auto-injected RSC plugins if applicable
|
|
3418
2689
|
if (rscPluginPromise) {
|
|
@@ -3433,53 +2704,13 @@ function getNextPublicEnvDefines() {
|
|
|
3433
2704
|
}
|
|
3434
2705
|
return defines;
|
|
3435
2706
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
return null;
|
|
3444
|
-
const start = re.lastIndex + 1;
|
|
3445
|
-
let depth = 1;
|
|
3446
|
-
let i = start;
|
|
3447
|
-
while (i < str.length && depth > 0) {
|
|
3448
|
-
if (str[i] === "(")
|
|
3449
|
-
depth++;
|
|
3450
|
-
else if (str[i] === ")")
|
|
3451
|
-
depth--;
|
|
3452
|
-
i++;
|
|
3453
|
-
}
|
|
3454
|
-
if (depth !== 0)
|
|
3455
|
-
return null;
|
|
3456
|
-
re.lastIndex = i;
|
|
3457
|
-
return str.slice(start, i - 1);
|
|
3458
|
-
}
|
|
3459
|
-
/**
|
|
3460
|
-
* Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
|
|
3461
|
-
* Returns matched params or null.
|
|
3462
|
-
*
|
|
3463
|
-
* Supports:
|
|
3464
|
-
* :param — matches a single path segment
|
|
3465
|
-
* :param* — matches zero or more segments (catch-all)
|
|
3466
|
-
* :param+ — matches one or more segments
|
|
3467
|
-
* (regex) — inline regex patterns in the source
|
|
3468
|
-
*/
|
|
3469
|
-
/**
|
|
3470
|
-
* Strip server-only data-fetching exports from a page module's source code.
|
|
3471
|
-
* Returns the transformed code, or null if no changes were made.
|
|
3472
|
-
*
|
|
3473
|
-
* Handles:
|
|
3474
|
-
* - export (async) function getServerSideProps(...) { ... }
|
|
3475
|
-
* - export const getStaticProps = async (...) => { ... }
|
|
3476
|
-
* - export const getServerSideProps = someHelper;
|
|
3477
|
-
*/
|
|
3478
|
-
/**
|
|
3479
|
-
* Skip past balanced brackets/parens/braces starting at `pos` (which should
|
|
3480
|
-
* point to the opening bracket). Returns the position AFTER the closing bracket.
|
|
3481
|
-
* Handles nested brackets, string literals, and comments.
|
|
3482
|
-
*/
|
|
2707
|
+
// matchConfigPattern is imported from config-matchers.ts and re-exported
|
|
2708
|
+
// for tests and other consumers that import it from vinext's main entry.
|
|
2709
|
+
// The duplicate local implementation and its extractConstraint helper
|
|
2710
|
+
// have been removed in favor of the canonical config-matchers.ts version
|
|
2711
|
+
// which uses a single-pass tokenizer (fixing the chained .replace()
|
|
2712
|
+
// divergence that CodeQL flagged as incomplete sanitization).
|
|
2713
|
+
export { matchConfigPattern } from "./config/config-matchers.js";
|
|
3483
2714
|
/**
|
|
3484
2715
|
* Strip server-only data-fetching exports (getServerSideProps,
|
|
3485
2716
|
* getStaticProps, getStaticPaths) from page modules for the client
|
|
@@ -3561,124 +2792,6 @@ function stripServerExports(code) {
|
|
|
3561
2792
|
return null;
|
|
3562
2793
|
return s.toString();
|
|
3563
2794
|
}
|
|
3564
|
-
export function matchConfigPattern(pathname, pattern) {
|
|
3565
|
-
// If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
|
|
3566
|
-
// Also enter this branch when a catch-all parameter (:param* or :param+) is
|
|
3567
|
-
// followed by a literal suffix (e.g. "/:path*.md"). Without this, the suffix
|
|
3568
|
-
// pattern falls through to the simple segment matcher which incorrectly treats
|
|
3569
|
-
// the whole segment (":path*.md") as a named parameter and matches everything.
|
|
3570
|
-
if (pattern.includes("(") ||
|
|
3571
|
-
pattern.includes("\\") ||
|
|
3572
|
-
/:[\w-]+[*+][^/]/.test(pattern) ||
|
|
3573
|
-
/:[\w-]+\./.test(pattern)) {
|
|
3574
|
-
try {
|
|
3575
|
-
// Extract named params and their constraints from the pattern.
|
|
3576
|
-
// :param(constraint) -> use constraint as the regex group
|
|
3577
|
-
// :param -> ([^/]+)
|
|
3578
|
-
// :param* -> (.*)
|
|
3579
|
-
// :param+ -> (.+)
|
|
3580
|
-
// Param names may contain hyphens (e.g. :auth-method, :sign-in).
|
|
3581
|
-
const paramNames = [];
|
|
3582
|
-
// Single-pass conversion with procedural suffix handling. The tokenizer
|
|
3583
|
-
// matches only simple, non-overlapping tokens; quantifier/constraint
|
|
3584
|
-
// suffixes after :param are consumed procedurally to avoid polynomial
|
|
3585
|
-
// backtracking in the regex engine.
|
|
3586
|
-
let regexStr = "";
|
|
3587
|
-
const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
|
|
3588
|
-
let tok;
|
|
3589
|
-
while ((tok = tokenRe.exec(pattern)) !== null) {
|
|
3590
|
-
if (tok[1] !== undefined) {
|
|
3591
|
-
const name = tok[1];
|
|
3592
|
-
const rest = pattern.slice(tokenRe.lastIndex);
|
|
3593
|
-
// Check for quantifier (* or +) with optional constraint
|
|
3594
|
-
if (rest.startsWith("*") || rest.startsWith("+")) {
|
|
3595
|
-
const quantifier = rest[0];
|
|
3596
|
-
tokenRe.lastIndex += 1;
|
|
3597
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
3598
|
-
paramNames.push(name);
|
|
3599
|
-
if (constraint !== null) {
|
|
3600
|
-
regexStr += `(${constraint})`;
|
|
3601
|
-
}
|
|
3602
|
-
else {
|
|
3603
|
-
regexStr += quantifier === "*" ? "(.*)" : "(.+)";
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
else {
|
|
3607
|
-
// Check for inline constraint without quantifier
|
|
3608
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
3609
|
-
paramNames.push(name);
|
|
3610
|
-
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
else if (tok[0] === ".") {
|
|
3614
|
-
regexStr += "\\.";
|
|
3615
|
-
}
|
|
3616
|
-
else {
|
|
3617
|
-
regexStr += tok[0];
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
const re = safeRegExp("^" + regexStr + "$");
|
|
3621
|
-
if (!re)
|
|
3622
|
-
return null;
|
|
3623
|
-
const match = re.exec(pathname);
|
|
3624
|
-
if (!match)
|
|
3625
|
-
return null;
|
|
3626
|
-
const params = {};
|
|
3627
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
3628
|
-
params[paramNames[i]] = match[i + 1] ?? "";
|
|
3629
|
-
}
|
|
3630
|
-
return params;
|
|
3631
|
-
}
|
|
3632
|
-
catch {
|
|
3633
|
-
// Fall through to segment-based matching
|
|
3634
|
-
}
|
|
3635
|
-
}
|
|
3636
|
-
// Check for catch-all patterns (:param* or :param+) without regex groups
|
|
3637
|
-
// Param names may contain hyphens (e.g. :sign-in*, :sign-up+).
|
|
3638
|
-
const catchAllMatch = pattern.match(/:([\w-]+)(\*|\+)$/);
|
|
3639
|
-
if (catchAllMatch) {
|
|
3640
|
-
const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
|
|
3641
|
-
const paramName = catchAllMatch[1];
|
|
3642
|
-
const isPlus = catchAllMatch[2] === "+";
|
|
3643
|
-
if (!pathname.startsWith(prefix.replace(/\/$/, "")))
|
|
3644
|
-
return null;
|
|
3645
|
-
const rest = pathname.slice(prefix.replace(/\/$/, "").length);
|
|
3646
|
-
// For :path+ we need at least one segment (non-empty after the prefix)
|
|
3647
|
-
if (isPlus && (!rest || rest === "/"))
|
|
3648
|
-
return null;
|
|
3649
|
-
// For :path* zero segments is fine
|
|
3650
|
-
let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
|
|
3651
|
-
// NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
|
|
3652
|
-
// the entry point. Decoding again would create a double-decode vector.
|
|
3653
|
-
return { [paramName]: restValue };
|
|
3654
|
-
}
|
|
3655
|
-
// Simple segment-based matching for exact patterns and :param
|
|
3656
|
-
const parts = pattern.split("/");
|
|
3657
|
-
const pathParts = pathname.split("/");
|
|
3658
|
-
if (parts.length !== pathParts.length)
|
|
3659
|
-
return null;
|
|
3660
|
-
const params = {};
|
|
3661
|
-
for (let i = 0; i < parts.length; i++) {
|
|
3662
|
-
if (parts[i].startsWith(":")) {
|
|
3663
|
-
params[parts[i].slice(1)] = pathParts[i];
|
|
3664
|
-
}
|
|
3665
|
-
else if (parts[i] !== pathParts[i]) {
|
|
3666
|
-
return null;
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
return params;
|
|
3670
|
-
}
|
|
3671
|
-
/**
|
|
3672
|
-
* Sanitize a redirect/rewrite destination by collapsing leading slashes and
|
|
3673
|
-
* backslashes to a single "/" for non-external URLs. Browsers interpret "\"
|
|
3674
|
-
* as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
|
|
3675
|
-
*/
|
|
3676
|
-
function sanitizeDestinationLocal(dest) {
|
|
3677
|
-
if (dest.startsWith("http://") || dest.startsWith("https://"))
|
|
3678
|
-
return dest;
|
|
3679
|
-
dest = dest.replace(/^[\\/]+/, "/");
|
|
3680
|
-
return dest;
|
|
3681
|
-
}
|
|
3682
2795
|
/**
|
|
3683
2796
|
* Apply redirect rules from next.config.js.
|
|
3684
2797
|
* Returns true if a redirect was applied.
|
|
@@ -3687,16 +2800,14 @@ function applyRedirects(pathname, res, redirects, ctx) {
|
|
|
3687
2800
|
const result = matchRedirect(pathname, redirects, ctx);
|
|
3688
2801
|
if (result) {
|
|
3689
2802
|
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3690
|
-
const dest =
|
|
2803
|
+
const dest = sanitizeDestination(result.destination);
|
|
3691
2804
|
res.writeHead(result.permanent ? 308 : 307, { Location: dest });
|
|
3692
2805
|
res.end();
|
|
3693
2806
|
return true;
|
|
3694
2807
|
}
|
|
3695
2808
|
return false;
|
|
3696
2809
|
}
|
|
3697
|
-
|
|
3698
|
-
* Proxy an external rewrite in the Node.js dev server context.
|
|
3699
|
-
*
|
|
2810
|
+
/*
|
|
3700
2811
|
* Converts the Node.js IncomingMessage into a Web Request, calls
|
|
3701
2812
|
* proxyExternalRequest(), and pipes the response back to the Node.js
|
|
3702
2813
|
* ServerResponse.
|
|
@@ -3761,12 +2872,14 @@ function applyRewrites(pathname, rewrites, ctx) {
|
|
|
3761
2872
|
const dest = matchRewrite(pathname, rewrites, ctx);
|
|
3762
2873
|
if (dest) {
|
|
3763
2874
|
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3764
|
-
return
|
|
2875
|
+
return sanitizeDestination(dest);
|
|
3765
2876
|
}
|
|
3766
2877
|
return null;
|
|
3767
2878
|
}
|
|
3768
2879
|
/**
|
|
3769
2880
|
* Apply custom header rules from next.config.js.
|
|
2881
|
+
* Middleware headers take precedence: if a header key was already set on the
|
|
2882
|
+
* response (by middleware), the config value is skipped for that key.
|
|
3770
2883
|
*/
|
|
3771
2884
|
function applyHeaders(pathname, res, headers, ctx) {
|
|
3772
2885
|
const matched = matchHeaders(pathname, headers, ctx);
|
|
@@ -3799,7 +2912,11 @@ function applyHeaders(pathname, res, headers, ctx) {
|
|
|
3799
2912
|
}
|
|
3800
2913
|
}
|
|
3801
2914
|
else {
|
|
3802
|
-
|
|
2915
|
+
// Middleware headers take precedence: skip config keys already set by
|
|
2916
|
+
// middleware so middleware always wins over next.config.js headers.
|
|
2917
|
+
if (!res.getHeader(lk)) {
|
|
2918
|
+
res.setHeader(header.key, header.value);
|
|
2919
|
+
}
|
|
3803
2920
|
}
|
|
3804
2921
|
}
|
|
3805
2922
|
}
|
|
@@ -3854,4 +2971,5 @@ export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeL
|
|
|
3854
2971
|
export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins };
|
|
3855
2972
|
export { parseStaticObjectLiteral as _parseStaticObjectLiteral };
|
|
3856
2973
|
export { stripServerExports as _stripServerExports };
|
|
2974
|
+
export { asyncHooksStubPlugin as _asyncHooksStubPlugin };
|
|
3857
2975
|
//# sourceMappingURL=index.js.map
|