vinext 0.0.25 → 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.
Files changed (86) hide show
  1. package/README.md +6 -1
  2. package/dist/check.js +4 -4
  3. package/dist/check.js.map +1 -1
  4. package/dist/cli.js +32 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/entry.js.map +1 -1
  7. package/dist/client/vinext-next-data.d.ts +22 -0
  8. package/dist/client/vinext-next-data.d.ts.map +1 -0
  9. package/dist/client/vinext-next-data.js +2 -0
  10. package/dist/client/vinext-next-data.js.map +1 -0
  11. package/dist/config/config-matchers.d.ts.map +1 -1
  12. package/dist/config/config-matchers.js +6 -2
  13. package/dist/config/config-matchers.js.map +1 -1
  14. package/dist/config/next-config.d.ts +31 -4
  15. package/dist/config/next-config.d.ts.map +1 -1
  16. package/dist/config/next-config.js +151 -13
  17. package/dist/config/next-config.js.map +1 -1
  18. package/dist/deploy.d.ts +11 -0
  19. package/dist/deploy.d.ts.map +1 -1
  20. package/dist/deploy.js +42 -24
  21. package/dist/deploy.js.map +1 -1
  22. package/dist/entries/app-browser-entry.d.ts +9 -0
  23. package/dist/entries/app-browser-entry.d.ts.map +1 -0
  24. package/dist/entries/app-browser-entry.js +340 -0
  25. package/dist/entries/app-browser-entry.js.map +1 -0
  26. package/dist/{server/app-dev-server.d.ts → entries/app-rsc-entry.d.ts} +4 -17
  27. package/dist/entries/app-rsc-entry.d.ts.map +1 -0
  28. package/dist/{server/app-dev-server.js → entries/app-rsc-entry.js} +360 -1205
  29. package/dist/entries/app-rsc-entry.js.map +1 -0
  30. package/dist/entries/app-ssr-entry.d.ts +8 -0
  31. package/dist/entries/app-ssr-entry.d.ts.map +1 -0
  32. package/dist/entries/app-ssr-entry.js +449 -0
  33. package/dist/entries/app-ssr-entry.js.map +1 -0
  34. package/dist/entries/pages-client-entry.d.ts +4 -0
  35. package/dist/entries/pages-client-entry.d.ts.map +1 -0
  36. package/dist/entries/pages-client-entry.js +94 -0
  37. package/dist/entries/pages-client-entry.js.map +1 -0
  38. package/dist/entries/pages-entry-helpers.d.ts +7 -0
  39. package/dist/entries/pages-entry-helpers.d.ts.map +1 -0
  40. package/dist/entries/pages-entry-helpers.js +18 -0
  41. package/dist/entries/pages-entry-helpers.js.map +1 -0
  42. package/dist/entries/pages-server-entry.d.ts +8 -0
  43. package/dist/entries/pages-server-entry.d.ts.map +1 -0
  44. package/dist/entries/pages-server-entry.js +993 -0
  45. package/dist/entries/pages-server-entry.js.map +1 -0
  46. package/dist/index.d.ts +1 -25
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +206 -1242
  49. package/dist/index.js.map +1 -1
  50. package/dist/server/instrumentation.d.ts +1 -1
  51. package/dist/server/instrumentation.js +1 -1
  52. package/dist/server/instrumentation.js.map +1 -1
  53. package/dist/server/middleware-codegen.d.ts +1 -1
  54. package/dist/server/middleware-codegen.js +1 -1
  55. package/dist/server/middleware-codegen.js.map +1 -1
  56. package/dist/server/prod-server.d.ts.map +1 -1
  57. package/dist/server/prod-server.js +18 -3
  58. package/dist/server/prod-server.js.map +1 -1
  59. package/dist/server/request-pipeline.d.ts +92 -0
  60. package/dist/server/request-pipeline.d.ts.map +1 -0
  61. package/dist/server/request-pipeline.js +202 -0
  62. package/dist/server/request-pipeline.js.map +1 -0
  63. package/dist/shims/constants.d.ts +120 -3
  64. package/dist/shims/constants.d.ts.map +1 -1
  65. package/dist/shims/constants.js +170 -3
  66. package/dist/shims/constants.js.map +1 -1
  67. package/dist/shims/headers.d.ts.map +1 -1
  68. package/dist/shims/headers.js +1 -0
  69. package/dist/shims/headers.js.map +1 -1
  70. package/dist/shims/link.d.ts.map +1 -1
  71. package/dist/shims/link.js +2 -2
  72. package/dist/shims/link.js.map +1 -1
  73. package/dist/shims/metadata.d.ts +7 -1
  74. package/dist/shims/metadata.d.ts.map +1 -1
  75. package/dist/shims/metadata.js +9 -3
  76. package/dist/shims/metadata.js.map +1 -1
  77. package/dist/shims/og.d.ts +6 -6
  78. package/dist/shims/og.js +6 -6
  79. package/dist/shims/og.js.map +1 -1
  80. package/dist/utils/project.d.ts +15 -0
  81. package/dist/utils/project.d.ts.map +1 -1
  82. package/dist/utils/project.js +48 -0
  83. package/dist/utils/project.js.map +1 -1
  84. package/package.json +1 -1
  85. package/dist/server/app-dev-server.d.ts.map +0 -1
  86. package/dist/server/app-dev-server.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,23 +1,27 @@
1
1
  import { loadEnv, parseAst } from "vite";
2
- import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, patternToNextFormat as pagesPatternToNextFormat } from "./routing/pages-router.js";
2
+ import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute } from "./routing/pages-router.js";
3
+ import { generateServerEntry as _generateServerEntry } from "./entries/pages-server-entry.js";
4
+ import { generateClientEntry as _generateClientEntry } from "./entries/pages-client-entry.js";
3
5
  import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
4
6
  import { createValidFileMatcher } from "./routing/file-matcher.js";
5
7
  import { createSSRHandler } from "./server/dev-server.js";
6
8
  import { handleApiRoute } from "./server/api-handler.js";
7
9
  import { createDirectRunner } from "./server/dev-module-runner.js";
8
- import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.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";
9
13
  import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
10
- import { findMiddlewareFile, isProxyFile, runMiddleware } from "./server/middleware.js";
14
+ import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
11
15
  import { logRequest, now } from "./server/request-log.js";
12
- import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
13
16
  import { normalizePath } from "./server/normalize-path.js";
14
17
  import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
15
18
  import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js";
16
19
  import { validateDevRequest } from "./server/dev-origin-check.js";
17
- import { safeRegExp, isExternalUrl, proxyExternalRequest, parseCookies, matchHeaders, matchRedirect, matchRewrite, } from "./config/config-matchers.js";
20
+ import { isExternalUrl, proxyExternalRequest, matchHeaders, matchRedirect, matchRewrite, requestContextFromRequest, sanitizeDestination, } from "./config/config-matchers.js";
18
21
  import { scanMetadataFiles } from "./server/metadata-routes.js";
19
22
  import { detectPackageManager } from "./utils/project.js";
20
23
  import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js";
24
+ import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js";
21
25
  import tsconfigPaths from "vite-tsconfig-paths";
22
26
  import react from "@vitejs/plugin-react";
23
27
  import MagicString from "magic-string";
@@ -512,979 +516,7 @@ export default function vinext(options = {}) {
512
516
  * This is the entry point for `vite build --ssr`.
513
517
  */
514
518
  async function generateServerEntry() {
515
- const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
516
- const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
517
- // Generate import statements using absolute paths since virtual
518
- // modules don't have a real file location for relative resolution.
519
- const pageImports = pageRoutes.map((r, i) => {
520
- const absPath = r.filePath.replace(/\\/g, "/");
521
- return `import * as page_${i} from ${JSON.stringify(absPath)};`;
522
- });
523
- const apiImports = apiRoutes.map((r, i) => {
524
- const absPath = r.filePath.replace(/\\/g, "/");
525
- return `import * as api_${i} from ${JSON.stringify(absPath)};`;
526
- });
527
- // Build the route table — include filePath for SSR manifest lookup
528
- const pageRouteEntries = pageRoutes.map((r, i) => {
529
- const absPath = r.filePath.replace(/\\/g, "/");
530
- return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`;
531
- });
532
- const apiRouteEntries = apiRoutes.map((r, i) => {
533
- return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
534
- });
535
- // Check for _app and _document
536
- const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
537
- const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
538
- const hasApp = appFilePath !== null;
539
- const hasDoc = docFilePath !== null;
540
- const appImportCode = hasApp
541
- ? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
542
- : `const AppComponent = null;`;
543
- const docImportCode = hasDoc
544
- ? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
545
- : `const DocumentComponent = null;`;
546
- // Serialize i18n config for embedding in the server entry
547
- const i18nConfigJson = nextConfig?.i18n
548
- ? JSON.stringify({
549
- locales: nextConfig.i18n.locales,
550
- defaultLocale: nextConfig.i18n.defaultLocale,
551
- localeDetection: nextConfig.i18n.localeDetection,
552
- })
553
- : "null";
554
- // Serialize the full resolved config for the production server.
555
- // This embeds redirects, rewrites, headers, basePath, trailingSlash
556
- // so prod-server.ts can apply them without loading next.config.js at runtime.
557
- const vinextConfigJson = JSON.stringify({
558
- basePath: nextConfig?.basePath ?? "",
559
- trailingSlash: nextConfig?.trailingSlash ?? false,
560
- redirects: nextConfig?.redirects ?? [],
561
- rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
562
- headers: nextConfig?.headers ?? [],
563
- i18n: nextConfig?.i18n ?? null,
564
- images: {
565
- deviceSizes: nextConfig?.images?.deviceSizes,
566
- imageSizes: nextConfig?.images?.imageSizes,
567
- dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
568
- contentDispositionType: nextConfig?.images?.contentDispositionType,
569
- contentSecurityPolicy: nextConfig?.images?.contentSecurityPolicy,
570
- },
571
- });
572
- // Generate instrumentation code if instrumentation.ts exists.
573
- // For production (Cloudflare Workers), instrumentation.ts is bundled into the
574
- // Worker and register() is called as a top-level await at module evaluation time —
575
- // before any request is handled. This mirrors App Router behavior (generateRscEntry)
576
- // and matches Next.js semantics: register() runs once on startup in the process
577
- // that handles requests.
578
- //
579
- // The onRequestError handler is stored on globalThis so it is visible across
580
- // all code within the Worker (same global scope).
581
- const instrumentationImportCode = instrumentationPath
582
- ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};`
583
- : "";
584
- const instrumentationInitCode = instrumentationPath
585
- ? `// Run instrumentation register() once at module evaluation time — before any
586
- // requests are handled. Matches Next.js semantics: register() is called once
587
- // on startup in the process that handles requests.
588
- if (typeof _instrumentation.register === "function") {
589
- await _instrumentation.register();
590
- }
591
- // Store the onRequestError handler on globalThis so it is visible to all
592
- // code within the Worker (same global scope).
593
- if (typeof _instrumentation.onRequestError === "function") {
594
- globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
595
- }`
596
- : "";
597
- // Generate middleware code if middleware.ts exists
598
- const middlewareImportCode = middlewarePath
599
- ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
600
- import { NextRequest, NextFetchEvent } from "next/server";`
601
- : "";
602
- // The matcher config is read from the middleware module at import time.
603
- // We inline the matching + execution logic so the prod server can call it.
604
- const middlewareExportCode = middlewarePath
605
- ? `
606
- // --- Middleware support (generated from middleware-codegen.ts) ---
607
- ${generateNormalizePathCode("es5")}
608
- ${generateSafeRegExpCode("es5")}
609
- ${generateMiddlewareMatcherCode("es5")}
610
-
611
- export async function runMiddleware(request, ctx) {
612
- var isProxy = ${middlewarePath ? JSON.stringify(isProxyFile(middlewarePath)) : "false"};
613
- var middlewareFn = isProxy
614
- ? (middlewareModule.proxy ?? middlewareModule.default)
615
- : (middlewareModule.middleware ?? middlewareModule.default);
616
- if (typeof middlewareFn !== "function") {
617
- var fileType = isProxy ? "Proxy" : "Middleware";
618
- var expectedExport = isProxy ? "proxy" : "middleware";
619
- throw new Error("The " + fileType + " file must export a function named \`" + expectedExport + "\` or a \`default\` function.");
620
- }
621
-
622
- var config = middlewareModule.config;
623
- var matcher = config && config.matcher;
624
- var url = new URL(request.url);
625
-
626
- // Normalize pathname before matching to prevent path-confusion bypasses
627
- // (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
628
- var decodedPathname;
629
- try { decodedPathname = decodeURIComponent(url.pathname); } catch (e) {
630
- return { continue: false, response: new Response("Bad Request", { status: 400 }) };
631
- }
632
- var normalizedPathname = __normalizePath(decodedPathname);
633
-
634
- if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
635
-
636
- // Construct a new Request with the decoded + normalized pathname so middleware
637
- // always sees the same canonical path that the router uses.
638
- var mwRequest = request;
639
- if (normalizedPathname !== url.pathname) {
640
- var mwUrl = new URL(url);
641
- mwUrl.pathname = normalizedPathname;
642
- mwRequest = new Request(mwUrl, request);
643
- }
644
- var nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
645
- var fetchEvent = new NextFetchEvent({ page: normalizedPathname });
646
- var response;
647
- try { response = await middlewareFn(nextRequest, fetchEvent); }
648
- catch (e) {
649
- console.error("[vinext] Middleware error:", e);
650
- return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
651
- }
652
- if (ctx && typeof ctx.waitUntil === "function") { ctx.waitUntil(fetchEvent.drainWaitUntil()); } else { fetchEvent.drainWaitUntil(); }
653
-
654
- if (!response) return { continue: true };
655
-
656
- if (response.headers.get("x-middleware-next") === "1") {
657
- var rHeaders = new Headers();
658
- for (var [key, value] of response.headers) {
659
- // Keep x-middleware-request-* headers so the production server can
660
- // apply middleware-request header overrides before stripping internals
661
- // from the final client response.
662
- if (
663
- !key.startsWith("x-middleware-") ||
664
- key.startsWith("x-middleware-request-")
665
- ) rHeaders.append(key, value);
666
- }
667
- return { continue: true, responseHeaders: rHeaders };
668
- }
669
-
670
- if (response.status >= 300 && response.status < 400) {
671
- var location = response.headers.get("Location") || response.headers.get("location");
672
- if (location) return { continue: false, redirectUrl: location, redirectStatus: response.status };
673
- }
674
-
675
- var rewriteUrl = response.headers.get("x-middleware-rewrite");
676
- if (rewriteUrl) {
677
- var rwHeaders = new Headers();
678
- for (var [k, v] of response.headers) {
679
- if (!k.startsWith("x-middleware-") || k.startsWith("x-middleware-request-")) rwHeaders.append(k, v);
680
- }
681
- var rewritePath;
682
- try { var parsed = new URL(rewriteUrl, request.url); rewritePath = parsed.pathname + parsed.search; }
683
- catch { rewritePath = rewriteUrl; }
684
- return { continue: true, rewriteUrl: rewritePath, rewriteStatus: response.status !== 200 ? response.status : undefined, responseHeaders: rwHeaders };
685
- }
686
-
687
- return { continue: false, response: response };
688
- }
689
- `
690
- : `
691
- export async function runMiddleware() { return { continue: true }; }
692
- `;
693
- // The server entry is a self-contained module that uses Web-standard APIs
694
- // (Request/Response, renderToReadableStream) so it runs on Cloudflare Workers.
695
- return `
696
- import React from "react";
697
- import { renderToReadableStream } from "react-dom/server.edge";
698
- import { resetSSRHead, getSSRHeadHTML } from "next/head";
699
- import { flushPreloads } from "next/dynamic";
700
- import { setSSRContext, wrapWithRouterContext } from "next/router";
701
- import { getCacheHandler } from "next/cache";
702
- import { runWithFetchCache } from "vinext/fetch-cache";
703
- import { _runWithCacheState } from "next/cache";
704
- import { runWithPrivateCache } from "vinext/cache-runtime";
705
- import { runWithRouterState } from "vinext/router-state";
706
- import { runWithHeadState } from "vinext/head-state";
707
- import { safeJsonStringify } from "vinext/html";
708
- import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
709
- import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
710
- ${instrumentationImportCode}
711
- ${middlewareImportCode}
712
-
713
- ${instrumentationInitCode}
714
-
715
- // i18n config (embedded at build time)
716
- const i18nConfig = ${i18nConfigJson};
717
-
718
- // Full resolved config for production server (embedded at build time)
719
- export const vinextConfig = ${vinextConfigJson};
720
-
721
- // ISR cache helpers (inlined for the server entry)
722
- async function isrGet(key) {
723
- const handler = getCacheHandler();
724
- const result = await handler.get(key);
725
- if (!result || !result.value) return null;
726
- return { value: result, isStale: result.cacheState === "stale" };
727
- }
728
- async function isrSet(key, data, revalidateSeconds, tags) {
729
- const handler = getCacheHandler();
730
- await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
731
- }
732
- const pendingRegenerations = new Map();
733
- function triggerBackgroundRegeneration(key, renderFn) {
734
- if (pendingRegenerations.has(key)) return;
735
- const promise = renderFn()
736
- .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
737
- .finally(() => pendingRegenerations.delete(key));
738
- pendingRegenerations.set(key, promise);
739
- }
740
-
741
- async function renderToStringAsync(element) {
742
- const stream = await renderToReadableStream(element);
743
- await stream.allReady;
744
- return new Response(stream).text();
745
- }
746
-
747
- ${pageImports.join("\n")}
748
- ${apiImports.join("\n")}
749
-
750
- ${appImportCode}
751
- ${docImportCode}
752
-
753
- const pageRoutes = [
754
- ${pageRouteEntries.join(",\n")}
755
- ];
756
-
757
- const apiRoutes = [
758
- ${apiRouteEntries.join(",\n")}
759
- ];
760
-
761
- function matchRoute(url, routes) {
762
- const pathname = url.split("?")[0];
763
- let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
764
- // NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
765
- // the entry point. Decoding again would create a double-decode vector.
766
- for (const route of routes) {
767
- const params = matchPattern(normalizedUrl, route.pattern);
768
- if (params !== null) return { route, params };
769
- }
770
- return null;
771
- }
772
-
773
- function matchPattern(url, pattern) {
774
- const urlParts = url.split("/").filter(Boolean);
775
- const patternParts = pattern.split("/").filter(Boolean);
776
- const params = Object.create(null);
777
- for (let i = 0; i < patternParts.length; i++) {
778
- const pp = patternParts[i];
779
- if (pp.endsWith("+")) {
780
- const paramName = pp.slice(1, -1);
781
- const remaining = urlParts.slice(i);
782
- if (remaining.length === 0) return null;
783
- params[paramName] = remaining;
784
- return params;
785
- }
786
- if (pp.endsWith("*")) {
787
- const paramName = pp.slice(1, -1);
788
- params[paramName] = urlParts.slice(i);
789
- return params;
790
- }
791
- if (pp.startsWith(":")) {
792
- if (i >= urlParts.length) return null;
793
- params[pp.slice(1)] = urlParts[i];
794
- continue;
795
- }
796
- if (i >= urlParts.length || urlParts[i] !== pp) return null;
797
- }
798
- if (urlParts.length !== patternParts.length) return null;
799
- return params;
800
- }
801
-
802
- function parseQuery(url) {
803
- const qs = url.split("?")[1];
804
- if (!qs) return {};
805
- const p = new URLSearchParams(qs);
806
- const q = {};
807
- for (const [k, v] of p) {
808
- if (k in q) {
809
- q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
810
- } else {
811
- q[k] = v;
812
- }
813
- }
814
- return q;
815
- }
816
-
817
- function patternToNextFormat(pattern) {
818
- return pattern
819
- .replace(/:([\\w]+)\\*/g, "[[...$1]]")
820
- .replace(/:([\\w]+)\\+/g, "[...$1]")
821
- .replace(/:([\\w]+)/g, "[$1]");
822
- }
823
-
824
- function collectAssetTags(manifest, moduleIds) {
825
- // Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
826
- const m = (manifest && Object.keys(manifest).length > 0)
827
- ? manifest
828
- : (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
829
- const tags = [];
830
- const seen = new Set();
831
-
832
- // Load the set of lazy chunk filenames (only reachable via dynamic imports).
833
- // These should NOT get <link rel="modulepreload"> or <script type="module">
834
- // tags — they are fetched on demand when the dynamic import() executes (e.g.
835
- // chunks behind React.lazy() or next/dynamic boundaries).
836
- var lazyChunks = (typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__) || null;
837
- var lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
838
-
839
- // Inject the client entry script if embedded by vinext:cloudflare-build
840
- if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
841
- const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
842
- seen.add(entry);
843
- tags.push('<link rel="modulepreload" href="/' + entry + '" />');
844
- tags.push('<script type="module" src="/' + entry + '" crossorigin></script>');
845
- }
846
- if (m) {
847
- // Always inject shared chunks (framework, vinext runtime, entry) and
848
- // page-specific chunks. The manifest maps module file paths to their
849
- // associated JS/CSS assets.
850
- //
851
- // For page-specific injection, the module IDs may be absolute paths
852
- // while the manifest uses relative paths. Try both the original ID
853
- // and a suffix match to find the correct manifest entry.
854
- var allFiles = [];
855
-
856
- if (moduleIds && moduleIds.length > 0) {
857
- // Collect assets for the requested page modules
858
- for (var mi = 0; mi < moduleIds.length; mi++) {
859
- var id = moduleIds[mi];
860
- var files = m[id];
861
- if (!files) {
862
- // Absolute path didn't match — try matching by suffix.
863
- // Manifest keys are relative (e.g. "pages/about.tsx") while
864
- // moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
865
- for (var mk in m) {
866
- if (id.endsWith("/" + mk) || id === mk) {
867
- files = m[mk];
868
- break;
869
- }
870
- }
871
- }
872
- if (files) {
873
- for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
874
- }
875
- }
876
-
877
- // Also inject shared chunks that every page needs: framework,
878
- // vinext runtime, and the entry bootstrap. These are identified
879
- // by scanning all manifest values for chunk filenames containing
880
- // known prefixes.
881
- for (var key in m) {
882
- var vals = m[key];
883
- if (!vals) continue;
884
- for (var vi = 0; vi < vals.length; vi++) {
885
- var file = vals[vi];
886
- var basename = file.split("/").pop() || "";
887
- if (
888
- basename.startsWith("framework-") ||
889
- basename.startsWith("vinext-") ||
890
- basename.includes("vinext-client-entry") ||
891
- basename.includes("vinext-app-browser-entry")
892
- ) {
893
- allFiles.push(file);
894
- }
895
- }
896
- }
897
- } else {
898
- // No specific modules — include all assets from manifest
899
- for (var akey in m) {
900
- var avals = m[akey];
901
- if (avals) {
902
- for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
903
- }
904
- }
905
- }
906
-
907
- for (var ti = 0; ti < allFiles.length; ti++) {
908
- var tf = allFiles[ti];
909
- // Normalize: Vite's SSR manifest values include a leading '/'
910
- // (from base path), but we prepend '/' ourselves when building
911
- // href/src attributes. Strip any existing leading slash to avoid
912
- // producing protocol-relative URLs like "//assets/chunk.js".
913
- // This also ensures consistent keys for the seen-set dedup and
914
- // lazySet.has() checks (which use values without leading slash).
915
- if (tf.charAt(0) === '/') tf = tf.slice(1);
916
- if (seen.has(tf)) continue;
917
- seen.add(tf);
918
- if (tf.endsWith(".css")) {
919
- tags.push('<link rel="stylesheet" href="/' + tf + '" />');
920
- } else if (tf.endsWith(".js")) {
921
- // Skip lazy chunks — they are behind dynamic import() boundaries
922
- // (React.lazy, next/dynamic) and should only be fetched on demand.
923
- if (lazySet && lazySet.has(tf)) continue;
924
- tags.push('<link rel="modulepreload" href="/' + tf + '" />');
925
- tags.push('<script type="module" src="/' + tf + '" crossorigin></script>');
926
- }
927
- }
928
- }
929
- return tags.join("\\n ");
930
- }
931
-
932
- // i18n helpers
933
- function extractLocale(url) {
934
- if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
935
- const pathname = url.split("?")[0];
936
- const parts = pathname.split("/").filter(Boolean);
937
- const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
938
- if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
939
- const locale = parts[0];
940
- const rest = "/" + parts.slice(1).join("/");
941
- return { locale, url: (rest || "/") + query, hadPrefix: true };
942
- }
943
- return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
944
- }
945
-
946
- function detectLocaleFromHeaders(headers) {
947
- if (!i18nConfig) return null;
948
- const acceptLang = headers.get("accept-language");
949
- if (!acceptLang) return null;
950
- const langs = acceptLang.split(",").map(function(part) {
951
- const pieces = part.trim().split(";");
952
- const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
953
- return { lang: pieces[0].trim().toLowerCase(), q: q };
954
- }).sort(function(a, b) { return b.q - a.q; });
955
- for (let k = 0; k < langs.length; k++) {
956
- const lang = langs[k].lang;
957
- for (let j = 0; j < i18nConfig.locales.length; j++) {
958
- if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
959
- }
960
- const prefix = lang.split("-")[0];
961
- for (let j = 0; j < i18nConfig.locales.length; j++) {
962
- const loc = i18nConfig.locales[j].toLowerCase();
963
- if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
964
- }
965
- }
966
- return null;
967
- }
968
-
969
- function parseCookieLocaleFromHeader(cookieHeader) {
970
- if (!i18nConfig || !cookieHeader) return null;
971
- const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
972
- if (!match) return null;
973
- var value;
974
- try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
975
- if (i18nConfig.locales.indexOf(value) !== -1) return value;
976
- return null;
977
- }
978
-
979
- function parseCookies(cookieHeader) {
980
- const cookies = {};
981
- if (!cookieHeader) return cookies;
982
- for (const part of cookieHeader.split(";")) {
983
- const [key, ...rest] = part.split("=");
984
- if (key) cookies[key.trim()] = rest.join("=").trim();
985
- }
986
- return cookies;
987
- }
988
-
989
- // Lightweight req/res facade for getServerSideProps and API routes.
990
- // Next.js pages expect ctx.req/ctx.res with Node-like shapes.
991
- function createReqRes(request, url, query, body) {
992
- const headersObj = {};
993
- for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
994
-
995
- const req = {
996
- method: request.method,
997
- url: url,
998
- headers: headersObj,
999
- query: query,
1000
- body: body,
1001
- cookies: parseCookies(request.headers.get("cookie")),
1002
- };
1003
-
1004
- let resStatusCode = 200;
1005
- const resHeaders = {};
1006
- // set-cookie needs array support (multiple Set-Cookie headers are common)
1007
- const setCookieHeaders = [];
1008
- let resBody = null;
1009
- let ended = false;
1010
- let resolveResponse;
1011
- const responsePromise = new Promise(function(r) { resolveResponse = r; });
1012
-
1013
- const res = {
1014
- get statusCode() { return resStatusCode; },
1015
- set statusCode(code) { resStatusCode = code; },
1016
- writeHead: function(code, headers) {
1017
- resStatusCode = code;
1018
- if (headers) {
1019
- for (const [k, v] of Object.entries(headers)) {
1020
- if (k.toLowerCase() === "set-cookie") {
1021
- if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
1022
- else { setCookieHeaders.push(v); }
1023
- } else {
1024
- resHeaders[k] = v;
1025
- }
1026
- }
1027
- }
1028
- return res;
1029
- },
1030
- setHeader: function(name, value) {
1031
- if (name.toLowerCase() === "set-cookie") {
1032
- if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
1033
- else { setCookieHeaders.push(value); }
1034
- } else {
1035
- resHeaders[name.toLowerCase()] = value;
1036
- }
1037
- return res;
1038
- },
1039
- getHeader: function(name) {
1040
- if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
1041
- return resHeaders[name.toLowerCase()];
1042
- },
1043
- end: function(data) {
1044
- if (ended) return;
1045
- ended = true;
1046
- if (data !== undefined && data !== null) resBody = data;
1047
- const h = new Headers(resHeaders);
1048
- for (const c of setCookieHeaders) h.append("set-cookie", c);
1049
- resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
1050
- },
1051
- status: function(code) { resStatusCode = code; return res; },
1052
- json: function(data) {
1053
- resHeaders["content-type"] = "application/json";
1054
- res.end(JSON.stringify(data));
1055
- },
1056
- send: function(data) {
1057
- if (typeof data === "object" && data !== null) { res.json(data); }
1058
- else { if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain"; res.end(String(data)); }
1059
- },
1060
- redirect: function(statusOrUrl, url2) {
1061
- if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
1062
- else { res.writeHead(statusOrUrl, { Location: url2 }); }
1063
- res.end();
1064
- },
1065
- getHeaders: function() {
1066
- var h = Object.assign({}, resHeaders);
1067
- if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
1068
- return h;
1069
- },
1070
- get headersSent() { return ended; },
1071
- };
1072
-
1073
- return { req, res, responsePromise };
1074
- }
1075
-
1076
- /**
1077
- * Read request body as text with a size limit.
1078
- * Throws if the body exceeds maxBytes. This prevents DoS via chunked
1079
- * transfer encoding where Content-Length is absent or spoofed.
1080
- */
1081
- async function readBodyWithLimit(request, maxBytes) {
1082
- if (!request.body) return "";
1083
- var reader = request.body.getReader();
1084
- var decoder = new TextDecoder();
1085
- var chunks = [];
1086
- var totalSize = 0;
1087
- for (;;) {
1088
- var result = await reader.read();
1089
- if (result.done) break;
1090
- totalSize += result.value.byteLength;
1091
- if (totalSize > maxBytes) {
1092
- reader.cancel();
1093
- throw new Error("Request body too large");
1094
- }
1095
- chunks.push(decoder.decode(result.value, { stream: true }));
1096
- }
1097
- chunks.push(decoder.decode());
1098
- return chunks.join("");
1099
- }
1100
-
1101
- export async function renderPage(request, url, manifest) {
1102
- const localeInfo = extractLocale(url);
1103
- const locale = localeInfo.locale;
1104
- const routeUrl = localeInfo.url;
1105
- const cookieHeader = request.headers.get("cookie") || "";
1106
-
1107
- // i18n redirect: check NEXT_LOCALE cookie first, then Accept-Language
1108
- if (i18nConfig && !localeInfo.hadPrefix) {
1109
- const cookieLocale = parseCookieLocaleFromHeader(cookieHeader);
1110
- if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
1111
- return new Response(null, { status: 307, headers: { Location: "/" + cookieLocale + routeUrl } });
1112
- }
1113
- if (!cookieLocale && i18nConfig.localeDetection !== false) {
1114
- const detected = detectLocaleFromHeaders(request.headers);
1115
- if (detected && detected !== i18nConfig.defaultLocale) {
1116
- return new Response(null, { status: 307, headers: { Location: "/" + detected + routeUrl } });
1117
- }
1118
- }
1119
- }
1120
-
1121
- const match = matchRoute(routeUrl, pageRoutes);
1122
- if (!match) {
1123
- return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
1124
- { status: 404, headers: { "Content-Type": "text/html" } });
1125
- }
1126
-
1127
- const { route, params } = match;
1128
- return runWithRouterState(() =>
1129
- runWithHeadState(() =>
1130
- _runWithCacheState(() =>
1131
- runWithPrivateCache(() =>
1132
- runWithFetchCache(async () => {
1133
- try {
1134
- if (typeof setSSRContext === "function") {
1135
- setSSRContext({
1136
- pathname: routeUrl.split("?")[0],
1137
- query: { ...params, ...parseQuery(routeUrl) },
1138
- asPath: routeUrl,
1139
- locale: locale,
1140
- locales: i18nConfig ? i18nConfig.locales : undefined,
1141
- defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1142
- });
1143
- }
1144
-
1145
- if (i18nConfig) {
1146
- globalThis.__VINEXT_LOCALE__ = locale;
1147
- globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
1148
- globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
1149
- }
1150
-
1151
- const pageModule = route.module;
1152
- const PageComponent = pageModule.default;
1153
- if (!PageComponent) {
1154
- return new Response("Page has no default export", { status: 500 });
1155
- }
1156
-
1157
- // Handle getStaticPaths for dynamic routes
1158
- if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
1159
- const pathsResult = await pageModule.getStaticPaths({
1160
- locales: i18nConfig ? i18nConfig.locales : [],
1161
- defaultLocale: i18nConfig ? i18nConfig.defaultLocale : "",
1162
- });
1163
- const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
1164
-
1165
- if (fallback === false) {
1166
- const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
1167
- const isValidPath = paths.some(function(p) {
1168
- return Object.entries(p.params).every(function(entry) {
1169
- var key = entry[0], val = entry[1];
1170
- var actual = params[key];
1171
- if (Array.isArray(val)) {
1172
- return Array.isArray(actual) && val.join("/") === actual.join("/");
1173
- }
1174
- return String(val) === String(actual);
1175
- });
1176
- });
1177
- if (!isValidPath) {
1178
- return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
1179
- { status: 404, headers: { "Content-Type": "text/html" } });
1180
- }
1181
- }
1182
- }
1183
-
1184
- let pageProps = {};
1185
- var gsspRes = null;
1186
- if (typeof pageModule.getServerSideProps === "function") {
1187
- const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
1188
- const ctx = {
1189
- params, req, res,
1190
- query: parseQuery(routeUrl),
1191
- resolvedUrl: routeUrl,
1192
- locale: locale,
1193
- locales: i18nConfig ? i18nConfig.locales : undefined,
1194
- defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1195
- };
1196
- const result = await pageModule.getServerSideProps(ctx);
1197
- // If gSSP called res.end() directly (short-circuit), return that response.
1198
- if (res.headersSent) {
1199
- return await responsePromise;
1200
- }
1201
- if (result && result.props) pageProps = result.props;
1202
- if (result && result.redirect) {
1203
- var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1204
- return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1205
- }
1206
- if (result && result.notFound) {
1207
- return new Response("404", { status: 404 });
1208
- }
1209
- // Preserve the res object so headers/status/cookies set by gSSP
1210
- // can be merged into the final HTML response.
1211
- gsspRes = res;
1212
- }
1213
- // Build font Link header early so it's available for ISR cached responses too.
1214
- // Font preloads are module-level state populated at import time and persist across requests.
1215
- var _fontLinkHeader = "";
1216
- var _allFp = [];
1217
- try {
1218
- var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
1219
- var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
1220
- _allFp = _fpGoogle.concat(_fpLocal);
1221
- if (_allFp.length > 0) {
1222
- _fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
1223
- }
1224
- } catch (e) { /* font preloads not available */ }
1225
-
1226
- let isrRevalidateSeconds = null;
1227
- if (typeof pageModule.getStaticProps === "function") {
1228
- const pathname = routeUrl.split("?")[0];
1229
- const cacheKey = "pages:" + (pathname === "/" ? "/" : pathname.replace(/\\/$/, ""));
1230
- const cached = await isrGet(cacheKey);
1231
-
1232
- if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
1233
- var _hitHeaders = {
1234
- "Content-Type": "text/html", "X-Vinext-Cache": "HIT",
1235
- "Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
1236
- };
1237
- if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
1238
- return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
1239
- }
1240
-
1241
- if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
1242
- triggerBackgroundRegeneration(cacheKey, async function() {
1243
- const freshResult = await pageModule.getStaticProps({ params });
1244
- if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
1245
- await isrSet(cacheKey, { kind: "PAGES", html: cached.value.value.html, pageData: freshResult.props, headers: undefined, status: undefined }, freshResult.revalidate);
1246
- }
1247
- });
1248
- var _staleHeaders = {
1249
- "Content-Type": "text/html", "X-Vinext-Cache": "STALE",
1250
- "Cache-Control": "s-maxage=0, stale-while-revalidate",
1251
- };
1252
- if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
1253
- return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
1254
- }
1255
-
1256
- const ctx = {
1257
- params,
1258
- locale: locale,
1259
- locales: i18nConfig ? i18nConfig.locales : undefined,
1260
- defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1261
- };
1262
- const result = await pageModule.getStaticProps(ctx);
1263
- if (result && result.props) pageProps = result.props;
1264
- if (result && result.redirect) {
1265
- var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1266
- return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1267
- }
1268
- if (result && result.notFound) {
1269
- return new Response("404", { status: 404 });
1270
- }
1271
- if (typeof result.revalidate === "number" && result.revalidate > 0) {
1272
- isrRevalidateSeconds = result.revalidate;
1273
- }
1274
- }
1275
-
1276
- let element;
1277
- if (AppComponent) {
1278
- element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1279
- } else {
1280
- element = React.createElement(PageComponent, pageProps);
1281
- }
1282
- element = wrapWithRouterContext(element);
1283
-
1284
- if (typeof resetSSRHead === "function") resetSSRHead();
1285
- if (typeof flushPreloads === "function") await flushPreloads();
1286
-
1287
- const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
1288
-
1289
- // Collect SSR font data (Google Font links, font preloads, font-face styles)
1290
- var fontHeadHTML = "";
1291
- function _escAttr(s) { return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
1292
- try {
1293
- var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
1294
- for (var fl of fontLinks) { fontHeadHTML += '<link rel="stylesheet" href="' + _escAttr(fl) + '" />\\n '; }
1295
- } catch (e) { /* next/font/google not used */ }
1296
- // Emit <link rel="preload"> for all font files (reuse _allFp collected earlier for Link header)
1297
- for (var fp of _allFp) { fontHeadHTML += '<link rel="preload" href="' + _escAttr(fp.href) + '" as="font" type="' + _escAttr(fp.type) + '" crossorigin />\\n '; }
1298
- try {
1299
- var allFontStyles = [];
1300
- if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
1301
- if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
1302
- if (allFontStyles.length > 0) { fontHeadHTML += '<style data-vinext-fonts>' + allFontStyles.join("\\n") + '</style>\\n '; }
1303
- } catch (e) { /* font styles not available */ }
1304
-
1305
- const pageModuleIds = route.filePath ? [route.filePath] : [];
1306
- const assetTags = collectAssetTags(manifest, pageModuleIds);
1307
- const nextDataPayload = {
1308
- props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, isFallback: false,
1309
- };
1310
- if (i18nConfig) {
1311
- nextDataPayload.locale = locale;
1312
- nextDataPayload.locales = i18nConfig.locales;
1313
- nextDataPayload.defaultLocale = i18nConfig.defaultLocale;
1314
- }
1315
- const localeGlobals = i18nConfig
1316
- ? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
1317
- ";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
1318
- ";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(i18nConfig.defaultLocale)
1319
- : "";
1320
- const nextDataScript = "<script>window.__NEXT_DATA__ = " + safeJsonStringify(nextDataPayload) + localeGlobals + "</script>";
1321
-
1322
- // Build the document shell with a placeholder for the streamed body
1323
- var BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
1324
- var shellHtml;
1325
- if (DocumentComponent) {
1326
- const docElement = React.createElement(DocumentComponent);
1327
- shellHtml = await renderToStringAsync(docElement);
1328
- shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
1329
- if (ssrHeadHTML || assetTags || fontHeadHTML) {
1330
- shellHtml = shellHtml.replace("</head>", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>");
1331
- }
1332
- shellHtml = shellHtml.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
1333
- if (!shellHtml.includes("__NEXT_DATA__")) {
1334
- shellHtml = shellHtml.replace("</body>", " " + nextDataScript + "\\n</body>");
1335
- }
1336
- } else {
1337
- shellHtml = "<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"utf-8\\" />\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />\\n " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>\\n<body>\\n <div id=\\"__next\\">" + BODY_MARKER + "</div>\\n " + nextDataScript + "\\n</body>\\n</html>";
1338
- }
1339
-
1340
- if (typeof setSSRContext === "function") setSSRContext(null);
1341
-
1342
- // Split the shell at the body marker
1343
- var markerIdx = shellHtml.indexOf(BODY_MARKER);
1344
- var shellPrefix = shellHtml.slice(0, markerIdx);
1345
- var shellSuffix = shellHtml.slice(markerIdx + BODY_MARKER.length);
1346
-
1347
- // Start the React body stream — progressive SSR (no allReady wait)
1348
- var bodyStream = await renderToReadableStream(element);
1349
- var encoder = new TextEncoder();
1350
-
1351
- // Create a composite stream: prefix + body + suffix
1352
- var compositeStream = new ReadableStream({
1353
- async start(controller) {
1354
- controller.enqueue(encoder.encode(shellPrefix));
1355
- var reader = bodyStream.getReader();
1356
- try {
1357
- for (;;) {
1358
- var chunk = await reader.read();
1359
- if (chunk.done) break;
1360
- controller.enqueue(chunk.value);
1361
- }
1362
- } finally {
1363
- reader.releaseLock();
1364
- }
1365
- controller.enqueue(encoder.encode(shellSuffix));
1366
- controller.close();
1367
- }
1368
- });
1369
-
1370
- // Cache the rendered HTML for ISR (needs the full string — re-render synchronously)
1371
- if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
1372
- // Tee the stream so we can cache and respond simultaneously would be ideal,
1373
- // but ISR responses are rare on first hit. Re-render to get complete HTML for cache.
1374
- var isrElement;
1375
- if (AppComponent) {
1376
- isrElement = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1377
- } else {
1378
- isrElement = React.createElement(PageComponent, pageProps);
1379
- }
1380
- isrElement = wrapWithRouterContext(isrElement);
1381
- var isrHtml = await renderToStringAsync(isrElement);
1382
- var fullHtml = shellPrefix + isrHtml + shellSuffix;
1383
- var isrPathname = url.split("?")[0];
1384
- var isrCacheKey = "pages:" + (isrPathname === "/" ? "/" : isrPathname.replace(/\\/$/, ""));
1385
- await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
1386
- }
1387
-
1388
- // Merge headers/status/cookies set by getServerSideProps on the res object.
1389
- // gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
1390
- var finalStatus = 200;
1391
- const responseHeaders = new Headers({ "Content-Type": "text/html" });
1392
- if (gsspRes) {
1393
- finalStatus = gsspRes.statusCode;
1394
- var gsspHeaders = gsspRes.getHeaders();
1395
- for (var hk of Object.keys(gsspHeaders)) {
1396
- var hv = gsspHeaders[hk];
1397
- if (hk === "set-cookie" && Array.isArray(hv)) {
1398
- for (var sc of hv) responseHeaders.append("set-cookie", sc);
1399
- } else if (hv != null) {
1400
- responseHeaders.set(hk, String(hv));
1401
- }
1402
- }
1403
- // Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
1404
- responseHeaders.set("Content-Type", "text/html");
1405
- }
1406
- if (isrRevalidateSeconds) {
1407
- responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
1408
- responseHeaders.set("X-Vinext-Cache", "MISS");
1409
- }
1410
- // Set HTTP Link header for font preloading
1411
- if (_fontLinkHeader) {
1412
- responseHeaders.set("Link", _fontLinkHeader);
1413
- }
1414
- return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
1415
- } catch (e) {
1416
- console.error("[vinext] SSR error:", e);
1417
- return new Response("Internal Server Error", { status: 500 });
1418
- }
1419
- }) // end runWithFetchCache
1420
- ) // end runWithPrivateCache
1421
- ) // end _runWithCacheState
1422
- ) // end runWithHeadState
1423
- ); // end runWithRouterState
1424
- }
1425
-
1426
- export async function handleApiRoute(request, url) {
1427
- const match = matchRoute(url, apiRoutes);
1428
- if (!match) {
1429
- return new Response("404 - API route not found", { status: 404 });
1430
- }
1431
-
1432
- const { route, params } = match;
1433
- const handler = route.module.default;
1434
- if (typeof handler !== "function") {
1435
- return new Response("API route does not export a default function", { status: 500 });
1436
- }
1437
-
1438
- const query = { ...params };
1439
- const qs = url.split("?")[1];
1440
- if (qs) {
1441
- for (const [k, v] of new URLSearchParams(qs)) {
1442
- if (k in query) {
1443
- // Multi-value: promote to array (Next.js returns string[] for duplicate keys)
1444
- query[k] = Array.isArray(query[k]) ? query[k].concat(v) : [query[k], v];
1445
- } else {
1446
- query[k] = v;
1447
- }
1448
- }
1449
- }
1450
-
1451
- // Parse request body (enforce 1MB limit to prevent memory exhaustion,
1452
- // matching Next.js default bodyParser sizeLimit).
1453
- // Check Content-Length first as a fast path, then enforce on the actual
1454
- // stream to prevent bypasses via chunked transfer encoding.
1455
- const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
1456
- if (contentLength > 1 * 1024 * 1024) {
1457
- return new Response("Request body too large", { status: 413 });
1458
- }
1459
- let body;
1460
- const ct = request.headers.get("content-type") || "";
1461
- let rawBody;
1462
- try { rawBody = await readBodyWithLimit(request, 1 * 1024 * 1024); }
1463
- catch { return new Response("Request body too large", { status: 413 }); }
1464
- if (!rawBody) {
1465
- body = undefined;
1466
- } else if (ct.includes("application/json")) {
1467
- try { body = JSON.parse(rawBody); } catch { body = rawBody; }
1468
- } else {
1469
- body = rawBody;
1470
- }
1471
-
1472
- const { req, res, responsePromise } = createReqRes(request, url, query, body);
1473
-
1474
- try {
1475
- await handler(req, res);
1476
- // If handler didn't call res.end(), end it now.
1477
- // The end() method is idempotent — safe to call twice.
1478
- res.end();
1479
- return await responsePromise;
1480
- } catch (e) {
1481
- console.error("[vinext] API error:", e);
1482
- return new Response("Internal Server Error", { status: 500 });
1483
- }
1484
- }
1485
-
1486
- ${middlewareExportCode}
1487
- `;
519
+ return _generateServerEntry(pagesDir, nextConfig, fileMatcher, middlewarePath, instrumentationPath);
1488
520
  }
1489
521
  /**
1490
522
  * Generate the virtual client hydration entry module.
@@ -1495,84 +527,7 @@ ${middlewareExportCode}
1495
527
  * __NEXT_DATA__ to determine which page to hydrate.
1496
528
  */
1497
529
  async function generateClientEntry() {
1498
- const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
1499
- const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
1500
- const hasApp = appFilePath !== null;
1501
- // Build a map of route pattern -> dynamic import.
1502
- // Keys must use Next.js bracket format (e.g. "/user/[id]") to match
1503
- // __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
1504
- const loaderEntries = pageRoutes.map((r) => {
1505
- const absPath = r.filePath.replace(/\\/g, "/");
1506
- const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
1507
- // JSON.stringify safely escapes quotes, backslashes, and special chars in
1508
- // both the route pattern and the absolute file path.
1509
- // lgtm[js/bad-code-sanitization]
1510
- return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
1511
- });
1512
- const appFileBase = appFilePath?.replace(/\\/g, "/");
1513
- return `
1514
- import React from "react";
1515
- import { hydrateRoot } from "react-dom/client";
1516
- // Eagerly import the router shim so its module-level popstate listener is
1517
- // registered. Without this, browser back/forward buttons do nothing because
1518
- // navigateClient() is never invoked on history changes.
1519
- import "next/router";
1520
-
1521
- const pageLoaders = {
1522
- ${loaderEntries.join(",\n")}
1523
- };
1524
-
1525
- async function hydrate() {
1526
- const nextData = window.__NEXT_DATA__;
1527
- if (!nextData) {
1528
- console.error("[vinext] No __NEXT_DATA__ found");
1529
- return;
1530
- }
1531
-
1532
- const { pageProps } = nextData.props;
1533
- const loader = pageLoaders[nextData.page];
1534
- if (!loader) {
1535
- console.error("[vinext] No page loader for route:", nextData.page);
1536
- return;
1537
- }
1538
-
1539
- const pageModule = await loader();
1540
- const PageComponent = pageModule.default;
1541
- if (!PageComponent) {
1542
- console.error("[vinext] Page module has no default export");
1543
- return;
1544
- }
1545
-
1546
- let element;
1547
- ${hasApp ? `
1548
- try {
1549
- const appModule = await import(${JSON.stringify(appFileBase)});
1550
- const AppComponent = appModule.default;
1551
- window.__VINEXT_APP__ = AppComponent;
1552
- element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1553
- } catch {
1554
- element = React.createElement(PageComponent, pageProps);
1555
- }
1556
- ` : `
1557
- element = React.createElement(PageComponent, pageProps);
1558
- `}
1559
-
1560
- // Wrap with RouterContext.Provider so next/compat/router works during hydration
1561
- const { wrapWithRouterContext } = await import("next/router");
1562
- element = wrapWithRouterContext(element);
1563
-
1564
- const container = document.getElementById("__next");
1565
- if (!container) {
1566
- console.error("[vinext] No #__next element found");
1567
- return;
1568
- }
1569
-
1570
- const root = hydrateRoot(container, element);
1571
- window.__VINEXT_ROOT__ = root;
1572
- }
1573
-
1574
- hydrate();
1575
- `;
530
+ return _generateClientEntry(pagesDir, nextConfig, fileMatcher);
1576
531
  }
1577
532
  // Auto-register @vitejs/plugin-rsc when App Router is detected.
1578
533
  // Check eagerly at call time using the same heuristic as config().
@@ -1961,8 +916,11 @@ hydrate();
1961
916
  // Exclude vinext from dependency optimization so esbuild doesn't
1962
917
  // scan dist files containing virtual module imports (virtual:vinext-*)
1963
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.
1964
922
  optimizeDeps: {
1965
- exclude: ["vinext"],
923
+ exclude: ["vinext", "@vercel/og"],
1966
924
  },
1967
925
  // Enable JSX in .tsx/.jsx files
1968
926
  // Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc`
@@ -2025,7 +983,7 @@ hydrate();
2025
983
  },
2026
984
  }),
2027
985
  optimizeDeps: {
2028
- exclude: ["vinext"],
986
+ exclude: ["vinext", "@vercel/og"],
2029
987
  entries: appEntries,
2030
988
  },
2031
989
  build: {
@@ -2048,7 +1006,7 @@ hydrate();
2048
1006
  },
2049
1007
  }),
2050
1008
  optimizeDeps: {
2051
- exclude: ["vinext"],
1009
+ exclude: ["vinext", "@vercel/og"],
2052
1010
  entries: appEntries,
2053
1011
  },
2054
1012
  build: {
@@ -2136,6 +1094,18 @@ hydrate();
2136
1094
  " Or: pass rsc: false to vinext() if you want to configure rsc() yourself.");
2137
1095
  }
2138
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
+ }
2139
1109
  },
2140
1110
  resolveId: {
2141
1111
  // Hook filter: only invoke JS for next/* imports and virtual:vinext-* modules.
@@ -2211,6 +1181,7 @@ hydrate();
2211
1181
  headers: nextConfig?.headers,
2212
1182
  allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
2213
1183
  allowedDevOrigins: nextConfig?.allowedDevOrigins,
1184
+ bodySizeLimit: nextConfig?.serverActionsBodySizeLimit,
2214
1185
  }, instrumentationPath);
2215
1186
  }
2216
1187
  if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
@@ -2241,6 +1212,10 @@ hydrate();
2241
1212
  return fn.call(this, config, env);
2242
1213
  },
2243
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;
2244
1219
  if (!mdxDelegate?.transform)
2245
1220
  return;
2246
1221
  const hook = mdxDelegate.transform;
@@ -2658,7 +1633,7 @@ hydrate();
2658
1633
  }
2659
1634
  // Normalize trailing slash based on next.config.js trailingSlash setting.
2660
1635
  // Redirect to the canonical form if needed.
2661
- if (nextConfig && pathname !== "/" && !pathname.startsWith("/api")) {
1636
+ if (nextConfig && pathname !== "/" && pathname !== "/api" && !pathname.startsWith("/api/")) {
2662
1637
  const hasTrailing = pathname.endsWith("/");
2663
1638
  if (nextConfig.trailingSlash && !hasTrailing) {
2664
1639
  // trailingSlash: true — redirect /about → /about/
@@ -2780,16 +1755,13 @@ hydrate();
2780
1755
  return next();
2781
1756
  // Build request context once for has/missing condition checks
2782
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.
2783
1760
  const reqUrl = new URL(url, `http://${req.headers.host || "localhost"}`);
2784
1761
  const reqCtxHeaders = new Headers(Object.fromEntries(Object.entries(req.headers)
2785
1762
  .filter(([, v]) => v !== undefined)
2786
1763
  .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
2787
- const reqCtx = {
2788
- headers: reqCtxHeaders,
2789
- cookies: parseCookies(reqCtxHeaders.get("cookie")),
2790
- query: reqUrl.searchParams,
2791
- host: reqCtxHeaders.get("host") ?? reqUrl.host,
2792
- };
1764
+ const reqCtx = requestContextFromRequest(new Request(reqUrl, { headers: reqCtxHeaders }));
2793
1765
  // Apply custom headers from next.config.js
2794
1766
  if (nextConfig?.headers.length) {
2795
1767
  applyHeaders(pathname, res, nextConfig.headers, reqCtx);
@@ -3332,11 +2304,110 @@ hydrate();
3332
2304
  },
3333
2305
  },
3334
2306
  },
3335
- // Copy @vercel/og assets (font, WASM) to the RSC output directory.
3336
- // @vercel/og uses readFileSync(new URL("./font.ttf", import.meta.url)) which
3337
- // breaks when the module is bundled because Vite doesn't process
3338
- // new URL(..., import.meta.url) for server-side (SSR/RSC) builds.
3339
- // This plugin copies the required assets so they exist alongside the bundle.
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.
3340
2411
  {
3341
2412
  name: "vinext:og-assets",
3342
2413
  apply: "build",
@@ -3356,8 +2427,9 @@ hydrate();
3356
2427
  if (!fs.existsSync(indexPath))
3357
2428
  return;
3358
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.
3359
2432
  const ogAssets = [
3360
- "noto-sans-v27-latin-regular.ttf",
3361
2433
  "resvg.wasm",
3362
2434
  ];
3363
2435
  // Only copy if the bundle actually references these files
@@ -3560,6 +2632,58 @@ hydrate();
3560
2632
  },
3561
2633
  },
3562
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
+ },
3563
2687
  ];
3564
2688
  // Append auto-injected RSC plugins if applicable
3565
2689
  if (rscPluginPromise) {
@@ -3580,53 +2704,13 @@ function getNextPublicEnvDefines() {
3580
2704
  }
3581
2705
  return defines;
3582
2706
  }
3583
- /**
3584
- * If the current position in `str` starts with a parenthesized group, consume
3585
- * it and advance `re.lastIndex` past the closing `)`. Returns the group
3586
- * contents or null if no group is present.
3587
- */
3588
- function extractConstraint(str, re) {
3589
- if (str[re.lastIndex] !== "(")
3590
- return null;
3591
- const start = re.lastIndex + 1;
3592
- let depth = 1;
3593
- let i = start;
3594
- while (i < str.length && depth > 0) {
3595
- if (str[i] === "(")
3596
- depth++;
3597
- else if (str[i] === ")")
3598
- depth--;
3599
- i++;
3600
- }
3601
- if (depth !== 0)
3602
- return null;
3603
- re.lastIndex = i;
3604
- return str.slice(start, i - 1);
3605
- }
3606
- /**
3607
- * Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
3608
- * Returns matched params or null.
3609
- *
3610
- * Supports:
3611
- * :param — matches a single path segment
3612
- * :param* — matches zero or more segments (catch-all)
3613
- * :param+ — matches one or more segments
3614
- * (regex) — inline regex patterns in the source
3615
- */
3616
- /**
3617
- * Strip server-only data-fetching exports from a page module's source code.
3618
- * Returns the transformed code, or null if no changes were made.
3619
- *
3620
- * Handles:
3621
- * - export (async) function getServerSideProps(...) { ... }
3622
- * - export const getStaticProps = async (...) => { ... }
3623
- * - export const getServerSideProps = someHelper;
3624
- */
3625
- /**
3626
- * Skip past balanced brackets/parens/braces starting at `pos` (which should
3627
- * point to the opening bracket). Returns the position AFTER the closing bracket.
3628
- * Handles nested brackets, string literals, and comments.
3629
- */
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";
3630
2714
  /**
3631
2715
  * Strip server-only data-fetching exports (getServerSideProps,
3632
2716
  * getStaticProps, getStaticPaths) from page modules for the client
@@ -3708,124 +2792,6 @@ function stripServerExports(code) {
3708
2792
  return null;
3709
2793
  return s.toString();
3710
2794
  }
3711
- export function matchConfigPattern(pathname, pattern) {
3712
- // If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
3713
- // Also enter this branch when a catch-all parameter (:param* or :param+) is
3714
- // followed by a literal suffix (e.g. "/:path*.md"). Without this, the suffix
3715
- // pattern falls through to the simple segment matcher which incorrectly treats
3716
- // the whole segment (":path*.md") as a named parameter and matches everything.
3717
- if (pattern.includes("(") ||
3718
- pattern.includes("\\") ||
3719
- /:[\w-]+[*+][^/]/.test(pattern) ||
3720
- /:[\w-]+\./.test(pattern)) {
3721
- try {
3722
- // Extract named params and their constraints from the pattern.
3723
- // :param(constraint) -> use constraint as the regex group
3724
- // :param -> ([^/]+)
3725
- // :param* -> (.*)
3726
- // :param+ -> (.+)
3727
- // Param names may contain hyphens (e.g. :auth-method, :sign-in).
3728
- const paramNames = [];
3729
- // Single-pass conversion with procedural suffix handling. The tokenizer
3730
- // matches only simple, non-overlapping tokens; quantifier/constraint
3731
- // suffixes after :param are consumed procedurally to avoid polynomial
3732
- // backtracking in the regex engine.
3733
- let regexStr = "";
3734
- const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
3735
- let tok;
3736
- while ((tok = tokenRe.exec(pattern)) !== null) {
3737
- if (tok[1] !== undefined) {
3738
- const name = tok[1];
3739
- const rest = pattern.slice(tokenRe.lastIndex);
3740
- // Check for quantifier (* or +) with optional constraint
3741
- if (rest.startsWith("*") || rest.startsWith("+")) {
3742
- const quantifier = rest[0];
3743
- tokenRe.lastIndex += 1;
3744
- const constraint = extractConstraint(pattern, tokenRe);
3745
- paramNames.push(name);
3746
- if (constraint !== null) {
3747
- regexStr += `(${constraint})`;
3748
- }
3749
- else {
3750
- regexStr += quantifier === "*" ? "(.*)" : "(.+)";
3751
- }
3752
- }
3753
- else {
3754
- // Check for inline constraint without quantifier
3755
- const constraint = extractConstraint(pattern, tokenRe);
3756
- paramNames.push(name);
3757
- regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
3758
- }
3759
- }
3760
- else if (tok[0] === ".") {
3761
- regexStr += "\\.";
3762
- }
3763
- else {
3764
- regexStr += tok[0];
3765
- }
3766
- }
3767
- const re = safeRegExp("^" + regexStr + "$");
3768
- if (!re)
3769
- return null;
3770
- const match = re.exec(pathname);
3771
- if (!match)
3772
- return null;
3773
- const params = {};
3774
- for (let i = 0; i < paramNames.length; i++) {
3775
- params[paramNames[i]] = match[i + 1] ?? "";
3776
- }
3777
- return params;
3778
- }
3779
- catch {
3780
- // Fall through to segment-based matching
3781
- }
3782
- }
3783
- // Check for catch-all patterns (:param* or :param+) without regex groups
3784
- // Param names may contain hyphens (e.g. :sign-in*, :sign-up+).
3785
- const catchAllMatch = pattern.match(/:([\w-]+)(\*|\+)$/);
3786
- if (catchAllMatch) {
3787
- const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
3788
- const paramName = catchAllMatch[1];
3789
- const isPlus = catchAllMatch[2] === "+";
3790
- if (!pathname.startsWith(prefix.replace(/\/$/, "")))
3791
- return null;
3792
- const rest = pathname.slice(prefix.replace(/\/$/, "").length);
3793
- // For :path+ we need at least one segment (non-empty after the prefix)
3794
- if (isPlus && (!rest || rest === "/"))
3795
- return null;
3796
- // For :path* zero segments is fine
3797
- let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
3798
- // NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
3799
- // the entry point. Decoding again would create a double-decode vector.
3800
- return { [paramName]: restValue };
3801
- }
3802
- // Simple segment-based matching for exact patterns and :param
3803
- const parts = pattern.split("/");
3804
- const pathParts = pathname.split("/");
3805
- if (parts.length !== pathParts.length)
3806
- return null;
3807
- const params = {};
3808
- for (let i = 0; i < parts.length; i++) {
3809
- if (parts[i].startsWith(":")) {
3810
- params[parts[i].slice(1)] = pathParts[i];
3811
- }
3812
- else if (parts[i] !== pathParts[i]) {
3813
- return null;
3814
- }
3815
- }
3816
- return params;
3817
- }
3818
- /**
3819
- * Sanitize a redirect/rewrite destination by collapsing leading slashes and
3820
- * backslashes to a single "/" for non-external URLs. Browsers interpret "\"
3821
- * as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
3822
- */
3823
- function sanitizeDestinationLocal(dest) {
3824
- if (dest.startsWith("http://") || dest.startsWith("https://"))
3825
- return dest;
3826
- dest = dest.replace(/^[\\/]+/, "/");
3827
- return dest;
3828
- }
3829
2795
  /**
3830
2796
  * Apply redirect rules from next.config.js.
3831
2797
  * Returns true if a redirect was applied.
@@ -3834,16 +2800,14 @@ function applyRedirects(pathname, res, redirects, ctx) {
3834
2800
  const result = matchRedirect(pathname, redirects, ctx);
3835
2801
  if (result) {
3836
2802
  // Sanitize to prevent open redirect via protocol-relative URLs
3837
- const dest = sanitizeDestinationLocal(result.destination);
2803
+ const dest = sanitizeDestination(result.destination);
3838
2804
  res.writeHead(result.permanent ? 308 : 307, { Location: dest });
3839
2805
  res.end();
3840
2806
  return true;
3841
2807
  }
3842
2808
  return false;
3843
2809
  }
3844
- /**
3845
- * Proxy an external rewrite in the Node.js dev server context.
3846
- *
2810
+ /*
3847
2811
  * Converts the Node.js IncomingMessage into a Web Request, calls
3848
2812
  * proxyExternalRequest(), and pipes the response back to the Node.js
3849
2813
  * ServerResponse.
@@ -3908,7 +2872,7 @@ function applyRewrites(pathname, rewrites, ctx) {
3908
2872
  const dest = matchRewrite(pathname, rewrites, ctx);
3909
2873
  if (dest) {
3910
2874
  // Sanitize to prevent open redirect via protocol-relative URLs
3911
- return sanitizeDestinationLocal(dest);
2875
+ return sanitizeDestination(dest);
3912
2876
  }
3913
2877
  return null;
3914
2878
  }