vinext 0.1.3 → 0.1.4

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 (82) hide show
  1. package/dist/build/client-build-config.d.ts +11 -2
  2. package/dist/build/client-build-config.js +17 -6
  3. package/dist/build/prerender.js +1 -0
  4. package/dist/client/pages-router-link-navigation.d.ts +33 -7
  5. package/dist/client/pages-router-link-navigation.js +32 -2
  6. package/dist/client/vinext-next-data.js +2 -0
  7. package/dist/config/config-matchers.d.ts +11 -1
  8. package/dist/config/config-matchers.js +14 -2
  9. package/dist/config/tsconfig-paths.js +14 -1
  10. package/dist/deploy.js +20 -13
  11. package/dist/entries/app-rsc-entry.js +3 -2
  12. package/dist/entries/pages-client-entry.js +14 -13
  13. package/dist/entries/pages-server-entry.js +6 -26
  14. package/dist/index.js +217 -40
  15. package/dist/plugins/dynamic-preload-metadata.js +2 -4
  16. package/dist/plugins/fonts.js +5 -4
  17. package/dist/plugins/strip-server-exports.d.ts +9 -7
  18. package/dist/plugins/strip-server-exports.js +493 -46
  19. package/dist/routing/app-route-graph.js +2 -2
  20. package/dist/server/app-browser-action-result.js +1 -1
  21. package/dist/server/app-browser-entry.js +8 -1
  22. package/dist/server/app-browser-navigation-controller.d.ts +1 -1
  23. package/dist/server/app-browser-state.d.ts +1 -1
  24. package/dist/server/app-browser-state.js +19 -11
  25. package/dist/server/app-browser-visible-commit.d.ts +1 -1
  26. package/dist/server/app-pages-bridge.d.ts +5 -1
  27. package/dist/server/app-pages-bridge.js +5 -13
  28. package/dist/server/app-rsc-handler.d.ts +3 -0
  29. package/dist/server/app-rsc-handler.js +51 -15
  30. package/dist/server/app-rsc-route-matching.js +6 -2
  31. package/dist/server/app-server-action-execution.js +5 -2
  32. package/dist/server/app-ssr-entry.js +1 -29
  33. package/dist/server/before-interactive-head.d.ts +17 -0
  34. package/dist/server/before-interactive-head.js +35 -0
  35. package/dist/server/csp.js +1 -4
  36. package/dist/server/dev-server.js +81 -36
  37. package/dist/server/middleware-matcher.js +12 -3
  38. package/dist/server/middleware-runtime.d.ts +3 -4
  39. package/dist/server/middleware-runtime.js +2 -0
  40. package/dist/server/navigation-planner.d.ts +3 -12
  41. package/dist/server/navigation-planner.js +24 -0
  42. package/dist/server/navigation-trace.d.ts +2 -1
  43. package/dist/server/navigation-trace.js +1 -0
  44. package/dist/server/operation-token.d.ts +40 -0
  45. package/dist/server/operation-token.js +85 -0
  46. package/dist/server/pages-data-route.d.ts +1 -1
  47. package/dist/server/pages-data-route.js +7 -4
  48. package/dist/server/pages-dev-module-url.d.ts +4 -0
  49. package/dist/server/pages-dev-module-url.js +15 -0
  50. package/dist/server/pages-document-initial-props.d.ts +4 -15
  51. package/dist/server/pages-document-initial-props.js +27 -56
  52. package/dist/server/pages-i18n.js +2 -2
  53. package/dist/server/pages-page-data.js +3 -1
  54. package/dist/server/pages-page-handler.js +3 -1
  55. package/dist/server/pages-page-response.d.ts +2 -0
  56. package/dist/server/pages-page-response.js +4 -4
  57. package/dist/server/pages-readiness.js +1 -1
  58. package/dist/server/pages-request-pipeline.d.ts +7 -7
  59. package/dist/server/pages-request-pipeline.js +63 -21
  60. package/dist/server/prod-server.d.ts +3 -1
  61. package/dist/server/prod-server.js +41 -10
  62. package/dist/server/static-file-cache.js +16 -4
  63. package/dist/shims/before-interactive-context.d.ts +14 -3
  64. package/dist/shims/document.d.ts +15 -20
  65. package/dist/shims/document.js +5 -8
  66. package/dist/shims/image.js +9 -2
  67. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +6 -7
  68. package/dist/shims/internal/pages-data-fetch-dedup.js +67 -14
  69. package/dist/shims/internal/pages-data-target.js +1 -1
  70. package/dist/shims/link.js +37 -16
  71. package/dist/shims/metadata.js +4 -4
  72. package/dist/shims/navigation.js +2 -0
  73. package/dist/shims/router.d.ts +6 -2
  74. package/dist/shims/router.js +99 -20
  75. package/dist/shims/script.js +8 -4
  76. package/dist/utils/has-trailing-comma.d.ts +24 -0
  77. package/dist/utils/has-trailing-comma.js +62 -0
  78. package/dist/utils/text-stream.d.ts +1 -1
  79. package/dist/utils/text-stream.js +2 -2
  80. package/dist/utils/vite-version.d.ts +12 -1
  81. package/dist/utils/vite-version.js +9 -1
  82. package/package.json +1 -1
@@ -26,6 +26,7 @@ import { detectLocaleFromAcceptLanguage, extractLocaleFromUrl as extractLocaleFr
26
26
  import { buildDefaultPagesNotFoundResponse } from "./pages-default-404.js";
27
27
  import { buildPagesReadinessNextData } from "./pages-readiness.js";
28
28
  import { resolvePagesPageMethodResponse } from "./pages-page-method.js";
29
+ import { createPagesDevModuleUrl } from "./pages-dev-module-url.js";
29
30
  import { isSerializableProps } from "./pages-serializable-props.js";
30
31
  import { loadUserDocumentInitialProps, runDocumentRenderPage } from "./pages-document-initial-props.js";
31
32
  import { callDocumentGetInitialProps } from "./document-initial-head.js";
@@ -100,7 +101,7 @@ const STREAM_BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
100
101
  * shell sooner).
101
102
  */
102
103
  async function streamPageToResponse(res, element, options) {
103
- const { url, server, fontHeadHTML, scripts, DocumentComponent, statusCode = 200, extraHeaders, getHeadHTML, enhancePageElement, scriptNonce, documentContext, setDocumentInitialHead } = options;
104
+ const { url, server, fontHeadHTML, scripts, DocumentComponent, statusCode = 200, extraHeaders, getHeadHTML, enhancePageElement, scriptNonce, documentContext, setDocumentInitialHead, bufferBodyBeforeHeaders = false } = options;
104
105
  const documentRenderPage = await runDocumentRenderPage({
105
106
  DocumentComponent,
106
107
  enhancePageElement,
@@ -144,6 +145,7 @@ async function streamPageToResponse(res, element, options) {
144
145
  const markerIdx = transformedShell.indexOf(STREAM_BODY_MARKER);
145
146
  const prefix = transformedShell.slice(0, markerIdx);
146
147
  const suffix = transformedShell.slice(markerIdx + 25);
148
+ const bufferedBody = bufferBodyBeforeHeaders ? await new Response(bodyStream).text() : null;
147
149
  const headers = {
148
150
  "Content-Type": "text/html",
149
151
  "Transfer-Encoding": "chunked"
@@ -152,6 +154,10 @@ async function streamPageToResponse(res, element, options) {
152
154
  else headers[key] = val;
153
155
  res.writeHead(statusCode, headers);
154
156
  res.write(prefix);
157
+ if (bufferedBody !== null) {
158
+ res.end(bufferedBody + suffix);
159
+ return;
160
+ }
155
161
  const reader = bodyStream.getReader();
156
162
  try {
157
163
  for (;;) {
@@ -622,9 +628,10 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
622
628
  }) : React.createElement(pageModule.default, freshPageProps);
623
629
  if (routerShim.wrapWithRouterContext) el = routerShim.wrapWithRouterContext(el);
624
630
  const freshBody = await renderIsrPassToStringAsync(withScriptNonce(el, scriptNonce));
625
- const viteRoot = server.config?.root;
626
- const regenPageUrl = viteRoot ? "/" + path.relative(viteRoot, route.filePath) : route.filePath;
627
- const regenAppUrl = RegenApp ? viteRoot ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app")) : path.join(pagesDir, "_app") : null;
631
+ const viteRoot = server.config.root;
632
+ const viteBase = server.config.base;
633
+ const regenPageUrl = createPagesDevModuleUrl(viteRoot, route.filePath, viteBase);
634
+ const regenAppUrl = RegenApp ? createPagesDevModuleUrl(viteRoot, path.join(pagesDir, "_app"), viteBase) : null;
628
635
  const freshPagesNextData = {
629
636
  ...pagesNextData,
630
637
  __vinext: {
@@ -634,7 +641,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
634
641
  hasMiddleware
635
642
  }
636
643
  };
637
- await isrSet(cacheKey, buildPagesCacheValue(`<!DOCTYPE html><html><head></head><body><div id="__next">${freshBody}</div>${`<script>window.__NEXT_DATA__ = ${safeJsonStringify({
644
+ await isrSet(cacheKey, buildPagesCacheValue(`<!DOCTYPE html><html><head></head><body><div id="__next">${freshBody}</div>${`<script id="__NEXT_DATA__" type="application/json">${safeJsonStringify({
638
645
  props: freshRenderProps,
639
646
  page: patternToNextFormat(route.pattern),
640
647
  query: params,
@@ -645,7 +652,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
645
652
  defaultLocale: currentDefaultLocale,
646
653
  domainLocales,
647
654
  ...freshPagesNextData
648
- })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? currentDefaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(currentDefaultLocale)}` : ""}<\/script>`}\n ${cachedHtml.match(/<script type="module">[\s\S]*?<\/script>/)?.[0] ?? ""}</body></html>`, freshRenderProps), revalidate);
655
+ })}<\/script>`}\n ${cachedHtml.match(/<script type="module">[\s\S]*?<\/script>/)?.[0] ?? ""}</body></html>`, freshRenderProps), revalidate);
649
656
  setRevalidateDuration(cacheKey, revalidate);
650
657
  }
651
658
  }
@@ -809,8 +816,11 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
809
816
  }
810
817
  if (allFontStyles.length > 0) fontHeadHTML += `<style data-vinext-fonts${nonceAttr}>${allFontStyles.join("\n")}</style>\n `;
811
818
  const viteRoot = server.config.root;
812
- const pageModuleUrl = "/" + path.relative(viteRoot, route.filePath);
813
- const appModuleUrl = AppComponent ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app")) : null;
819
+ const viteBase = server.config.base;
820
+ const pageModuleUrl = createPagesDevModuleUrl(viteRoot, route.filePath, viteBase);
821
+ const pageModuleSource = createPagesDevModuleUrl(viteRoot, route.filePath, "/");
822
+ const appModuleUrl = AppComponent ? createPagesDevModuleUrl(viteRoot, path.join(pagesDir, "_app"), viteBase) : null;
823
+ const appModuleSource = AppComponent ? createPagesDevModuleUrl(viteRoot, path.join(pagesDir, "_app"), "/") : null;
814
824
  const serializedPagesNextData = {
815
825
  ...pagesNextData,
816
826
  __vinext: {
@@ -825,14 +835,22 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
825
835
  import "vinext/instrumentation-client";
826
836
  import React from "react";
827
837
  import { hydrateRoot } from "react-dom/client";
828
- import Router, { wrapWithRouterContext } from "next/router";
838
+ import Router, { wrapWithRouterContext, _initializePagesRouterReadyFromNextData } from "next/router";
829
839
 
840
+ const nextDataElement = document.getElementById("__NEXT_DATA__");
841
+ if (nextDataElement?.textContent) {
842
+ window.__NEXT_DATA__ = JSON.parse(nextDataElement.textContent);
843
+ window.__VINEXT_LOCALE__ = window.__NEXT_DATA__.locale;
844
+ window.__VINEXT_LOCALES__ = window.__NEXT_DATA__.locales;
845
+ window.__VINEXT_DEFAULT_LOCALE__ = window.__NEXT_DATA__.defaultLocale;
846
+ }
830
847
  const nextData = window.__NEXT_DATA__;
848
+ _initializePagesRouterReadyFromNextData(nextData);
831
849
  const props = nextData.props && typeof nextData.props === "object" ? nextData.props : {};
832
850
  const rawPageProps = props.pageProps;
833
851
  const pageProps = rawPageProps && typeof rawPageProps === "object" ? rawPageProps : {};
834
- window.__VINEXT_PAGE_LOADERS__ = { [nextData.page]: () => import("${pageModuleUrl}") };
835
- window.__VINEXT_APP_LOADER__ = ${appModuleUrl ? `() => import("${appModuleUrl}")` : "undefined"};
852
+ window.__VINEXT_PAGE_LOADERS__ = { [nextData.page]: () => import("${pageModuleSource}") };
853
+ window.__VINEXT_APP_LOADER__ = ${appModuleSource ? `() => import("${appModuleSource}")` : "undefined"};
836
854
 
837
855
  async function hydrate() {
838
856
  let hydrateRootOptions;
@@ -847,11 +865,11 @@ async function hydrate() {
847
865
  };
848
866
  }
849
867
 
850
- const pageModule = await import("${pageModuleUrl}");
868
+ const pageModule = await import("${pageModuleSource}");
851
869
  const PageComponent = pageModule.default;
852
870
  let element;
853
- ${appModuleUrl ? `
854
- const appModule = await import("${appModuleUrl}");
871
+ ${appModuleSource ? `
872
+ const appModule = await import("${appModuleSource}");
855
873
  const AppComponent = appModule.default;
856
874
  window.__VINEXT_APP__ = AppComponent;
857
875
  element = React.createElement(AppComponent, {
@@ -880,7 +898,7 @@ async function hydrate() {
880
898
  }
881
899
  hydrate();
882
900
  <\/script>`;
883
- const nextDataScript = createInlineScriptTag(`window.__NEXT_DATA__ = ${safeJsonStringify({
901
+ const nextDataScript = `<script id="__NEXT_DATA__" type="application/json"${nonceAttr}>${safeJsonStringify({
884
902
  props: renderProps,
885
903
  page: patternToNextFormat(route.pattern),
886
904
  query: params,
@@ -891,7 +909,7 @@ hydrate();
891
909
  defaultLocale: currentDefaultLocale,
892
910
  domainLocales,
893
911
  ...serializedPagesNextData
894
- })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? currentDefaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(currentDefaultLocale)}` : ""}`, scriptNonce);
912
+ })}<\/script>`;
895
913
  const docPath = path.join(pagesDir, "_document");
896
914
  let DocumentComponent = null;
897
915
  if (findFileWithExtensions(docPath, matcher)) try {
@@ -939,7 +957,8 @@ hydrate();
939
957
  const traceHTML = getClientTraceMetadataHTML(clientTraceMetadata);
940
958
  return traceHTML ? `${headHTML}\n ${traceHTML}` : headHTML;
941
959
  },
942
- setDocumentInitialHead: typeof headShim.setDocumentInitialHead === "function" ? headShim.setDocumentInitialHead : void 0
960
+ setDocumentInitialHead: typeof headShim.setDocumentInitialHead === "function" ? headShim.setDocumentInitialHead : void 0,
961
+ bufferBodyBeforeHeaders: true
943
962
  });
944
963
  _renderEnd = now();
945
964
  if (typeof routerShim.setSSRContext === "function") routerShim.setSSRContext(null);
@@ -1015,41 +1034,67 @@ async function renderErrorPage(server, runner, req, res, url, pagesDir, statusCo
1015
1034
  if (!wrapFn) try {
1016
1035
  wrapFn = (await importModule(runner, "next/router")).wrapWithRouterContext;
1017
1036
  } catch {}
1018
- let element;
1019
- if (AppComponent) element = createElement(AppComponent, {
1020
- Component: ErrorComponent,
1021
- pageProps: errorProps
1022
- });
1023
- else element = createElement(ErrorComponent, errorProps);
1024
- if (wrapFn) element = wrapFn(element);
1025
- const bodyHtml = await renderToStringAsync(element);
1026
- let html;
1027
1037
  let DocumentComponent = null;
1028
1038
  const docPathErr = path.join(pagesDir, "_document");
1029
1039
  if (findFileWithExtensions(docPathErr, matcher)) try {
1030
1040
  DocumentComponent = (await importModule(runner, docPathErr)).default ?? null;
1031
1041
  } catch {}
1042
+ const createErrorElement = (FinalApp, FinalComponent) => {
1043
+ let errorElement = FinalApp ? createElement(FinalApp, {
1044
+ Component: FinalComponent,
1045
+ pageProps: errorProps
1046
+ }) : createElement(FinalComponent, errorProps);
1047
+ if (wrapFn) errorElement = wrapFn(errorElement);
1048
+ return errorElement;
1049
+ };
1050
+ const element = createErrorElement(AppComponent, ErrorComponent);
1051
+ const headShim = await importModule(runner, "next/head");
1052
+ if (typeof headShim.resetSSRHead === "function") headShim.resetSSRHead();
1032
1053
  if (DocumentComponent) {
1033
- const docProps = await loadUserDocumentInitialProps(DocumentComponent);
1034
- let docHtml = await renderToStringAsync(docProps ? createElement(DocumentComponent, docProps) : createElement(DocumentComponent));
1035
- docHtml = docHtml.replace("__NEXT_MAIN__", bodyHtml);
1036
- docHtml = docHtml.replace("<!-- __NEXT_SCRIPTS__ -->", "");
1037
- html = docHtml;
1038
- } else html = `<!DOCTYPE html>
1054
+ const errorPathname = candidate === "_error" ? "/_error" : `/${candidate}`;
1055
+ await streamPageToResponse(res, element, {
1056
+ url,
1057
+ server,
1058
+ fontHeadHTML: "",
1059
+ scripts: "",
1060
+ DocumentComponent,
1061
+ statusCode,
1062
+ documentContext: {
1063
+ err,
1064
+ pathname: errorPathname,
1065
+ query: parseQueryString(url),
1066
+ asPath: url,
1067
+ req,
1068
+ res
1069
+ },
1070
+ enhancePageElement: (renderPageOpts) => {
1071
+ let FinalApp = AppComponent;
1072
+ let FinalComponent = ErrorComponent;
1073
+ if (renderPageOpts.enhanceApp && FinalApp) FinalApp = renderPageOpts.enhanceApp(FinalApp);
1074
+ if (renderPageOpts.enhanceComponent) FinalComponent = renderPageOpts.enhanceComponent(FinalComponent);
1075
+ return createErrorElement(FinalApp, FinalComponent);
1076
+ },
1077
+ getHeadHTML: () => typeof headShim.getSSRHeadHTML === "function" ? headShim.getSSRHeadHTML() : "",
1078
+ setDocumentInitialHead: typeof headShim.setDocumentInitialHead === "function" ? headShim.setDocumentInitialHead : void 0
1079
+ });
1080
+ } else {
1081
+ const html = `<!DOCTYPE html>
1039
1082
  <html>
1040
1083
  <head>
1041
1084
  <meta charset="utf-8" />
1042
1085
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1043
1086
  </head>
1044
1087
  <body>
1045
- <div id="__next">${bodyHtml}</div>
1088
+ <div id="__next">${await renderToStringAsync(element)}</div>
1046
1089
  </body>
1047
1090
  </html>`;
1048
- const transformedHtml = await server.transformIndexHtml(url, html);
1049
- res.writeHead(statusCode, { "Content-Type": "text/html" });
1050
- res.end(transformedHtml);
1091
+ const transformedHtml = await server.transformIndexHtml(url, html);
1092
+ res.writeHead(statusCode, { "Content-Type": "text/html" });
1093
+ res.end(transformedHtml);
1094
+ }
1051
1095
  return;
1052
1096
  } catch {
1097
+ if (res.headersSent || res.writableEnded) return;
1053
1098
  continue;
1054
1099
  }
1055
1100
  if (statusCode === 404) {
@@ -1,5 +1,5 @@
1
1
  import { removeTrailingSlash } from "../utils/base-path.js";
2
- import { checkHasConditions, requestContextFromRequest, safeRegExp } from "../config/config-matchers.js";
2
+ import { checkHasConditions, isSafeRegex, requestContextFromRequest, safeRegExp } from "../config/config-matchers.js";
3
3
  //#region src/server/middleware-matcher.ts
4
4
  const EMPTY_MIDDLEWARE_REQUEST_CONTEXT = {
5
5
  headers: new Headers(),
@@ -7,6 +7,7 @@ const EMPTY_MIDDLEWARE_REQUEST_CONTEXT = {
7
7
  query: new URLSearchParams(),
8
8
  host: ""
9
9
  };
10
+ const UNSAFE_MATCHER_PATTERN = Symbol("unsafe matcher pattern");
10
11
  const _mwPatternCache = /* @__PURE__ */ new Map();
11
12
  function matchesMiddleware(pathname, matcher, request, i18nConfig) {
12
13
  if (!matcher) return true;
@@ -55,6 +56,7 @@ function matchPattern(pathname, pattern) {
55
56
  cached = compileMatcherPattern(pattern);
56
57
  _mwPatternCache.set(pattern, cached);
57
58
  }
59
+ if (cached === UNSAFE_MATCHER_PATTERN) return true;
58
60
  if (cached === null) return pathname === pattern;
59
61
  return cached.test(pathname);
60
62
  }
@@ -74,7 +76,7 @@ function extractConstraint(str, re) {
74
76
  }
75
77
  function compileMatcherPattern(pattern) {
76
78
  const hasConstraints = /:[\w-]+[*+]?\(/.test(pattern);
77
- if (!hasConstraints && (pattern.includes("(") || pattern.includes("\\"))) return safeRegExp("^" + pattern + "$");
79
+ if (!hasConstraints && (pattern.includes("(") || pattern.includes("\\"))) return compileMatcherRegExp("^" + pattern + "$", pattern);
78
80
  let regexStr = "";
79
81
  const tokenRe = /\/:([\w-]+)\*|\/:([\w-]+)\+|:([\w-]+)|[.]|[^/:.]+|./g;
80
82
  let tok;
@@ -94,7 +96,14 @@ function compileMatcherPattern(pattern) {
94
96
  else regexStr += group;
95
97
  } else if (tok[0] === ".") regexStr += "\\.";
96
98
  else regexStr += tok[0];
97
- return safeRegExp("^" + regexStr + "$");
99
+ return compileMatcherRegExp("^" + regexStr + "$", pattern);
100
+ }
101
+ function compileMatcherRegExp(regexPattern, sourcePattern) {
102
+ if (!isSafeRegex(regexPattern)) {
103
+ console.warn(`[vinext] Rejecting potentially unsafe middleware matcher (ReDoS risk): ${sourcePattern}\n Middleware will run for all paths to avoid bypassing request guards.\n Simplify the matcher to avoid nested repetition.`);
104
+ return UNSAFE_MATCHER_PATTERN;
105
+ }
106
+ return safeRegExp(regexPattern);
98
107
  }
99
108
  //#endregion
100
109
  export { matchPattern, matchesMiddleware };
@@ -35,10 +35,9 @@ type ExecuteMiddlewareOptions = {
35
35
  i18nConfig?: NextI18nConfig | null;
36
36
  includeErrorDetails?: boolean;
37
37
  /**
38
- * Whether the incoming request was a Next.js `_next/data` fetch (carried
39
- * `x-nextjs-data: 1`). The header itself is stripped by `filterInternalHeaders`
40
- * before the middleware request is constructed, so callers must capture this
41
- * flag from the raw incoming headers and forward it explicitly.
38
+ * Whether the incoming request was recognized as a Next.js `_next/data`
39
+ * fetch. Internal headers are stripped before middleware runs, so adapters
40
+ * must derive and forward this from trusted URL normalization.
42
41
  */
43
42
  isDataRequest?: boolean;
44
43
  isProxy: boolean;
@@ -142,6 +142,8 @@ async function executeMiddleware(options) {
142
142
  response: internalServerErrorResponse(options.includeErrorDetails ? "Middleware Error: " + (e instanceof Error ? e.message : String(e)) : "Internal Server Error"),
143
143
  waitUntilPromises
144
144
  };
145
+ } finally {
146
+ if (process.env.NODE_ENV !== "development" && nextRequest.body) nextRequest.body.cancel().catch(() => {});
145
147
  }
146
148
  const waitUntilPromises = drainFetchEvent(fetchEvent);
147
149
  if (!response) return {
@@ -2,18 +2,9 @@ import { RouteManifest } from "../routing/app-route-graph.js";
2
2
  import { CacheEntryReuseDecision, CacheEntryReuseProof } from "./cache-proof.js";
3
3
  import { AppElementsSlotBinding } from "./app-elements-wire.js";
4
4
  import { NavigationTrace, NavigationTraceFields } from "./navigation-trace.js";
5
+ import { OperationLane, OperationToken } from "./operation-token.js";
5
6
 
6
7
  //#region src/server/navigation-planner.d.ts
7
- type OperationLane = "hmr" | "navigation" | "prefetch" | "refresh" | "server-action" | "traverse";
8
- type OperationToken = {
9
- operationId: number;
10
- lane: OperationLane;
11
- baseVisibleCommitVersion: number;
12
- graphVersion: string | null;
13
- deploymentVersion: string | null;
14
- targetSnapshotFingerprint: string;
15
- cacheVariantFingerprint?: string;
16
- };
17
8
  type RouteSnapshot = {
18
9
  interception: InterceptionSnapshot | null;
19
10
  interceptionContext: string | null;
@@ -85,7 +76,7 @@ type CommitProposal = {
85
76
  targetSnapshot: RouteSnapshot;
86
77
  };
87
78
  type NoCommitReason = "prefetchOnly";
88
- type HardNavigationReason = "cacheProofRejected" | "interceptionProofRejected" | "rootBoundaryChanged";
79
+ type HardNavigationReason = "cacheProofRejected" | "cacheReuseTokenRejected" | "interceptionProofRejected" | "rootBoundaryChanged";
89
80
  type RootBoundaryTransition = "currentRootBoundary" | "rootBoundaryChanged" | "rootBoundaryUnknown";
90
81
  type NavigationDecision = {
91
82
  kind: "requestWork";
@@ -314,4 +305,4 @@ declare const navigationPlanner: {
314
305
  resolveSameLayoutAncestorPersistence: typeof resolveSameLayoutAncestorPersistence;
315
306
  };
316
307
  //#endregion
317
- export { EarlyNavigationIntentDecision, EarlyNavigationIntentFacts, FlightResult, InterceptionSnapshot, MountedParallelSlotSnapshot, NavigationDecision, NavigationEvent, NavigationPlannerInput, NavigationPlannerState, NavigationReuseDecision, NavigationReuseFacts, OperationLane, OperationToken, ParallelSlotBindingSnapshot, RefreshScope, RootBoundaryTransition, RouteSnapshot, RscFetchResultDecision, RscFetchResultFacts, RscNavigationErrorDecision, RscNavigationErrorFacts, ServerActionResultDecision, ServerActionResultFacts, TraverseDirection, VisitedResponseCacheCandidateFacts, navigationPlanner, resolveDefaultOrUnmatchedSlotPersistenceForLayouts };
308
+ export { EarlyNavigationIntentDecision, EarlyNavigationIntentFacts, FlightResult, InterceptionSnapshot, MountedParallelSlotSnapshot, NavigationDecision, NavigationEvent, NavigationPlannerInput, NavigationPlannerState, NavigationReuseDecision, NavigationReuseFacts, type OperationLane, type OperationToken, ParallelSlotBindingSnapshot, RefreshScope, RootBoundaryTransition, RouteSnapshot, RscFetchResultDecision, RscFetchResultFacts, RscNavigationErrorDecision, RscNavigationErrorFacts, ServerActionResultDecision, ServerActionResultFacts, TraverseDirection, VisitedResponseCacheCandidateFacts, navigationPlanner, resolveDefaultOrUnmatchedSlotPersistenceForLayouts };
@@ -6,6 +6,7 @@ import "./app-elements.js";
6
6
  import { resolveHardNavigationTargetFromRscResponse, resolveRscCompatibilityNavigationDecision } from "./app-rsc-cache-busting.js";
7
7
  import { resolveRscRedirectLifecycleHop, resolveStreamedRscRedirectLifecycleHop } from "./app-browser-rsc-redirect.js";
8
8
  import { NavigationTraceReasonCodes, createNavigationLifecycleTraceFields, createNavigationTrace } from "./navigation-trace.js";
9
+ import { verifyOperationTokenForCacheReuse } from "./operation-token.js";
9
10
  //#region src/server/navigation-planner.ts
10
11
  const ROUTE_INTERCEPTION_CONTEXT_SEPARATOR = "\0";
11
12
  const CACHE_ENTRY_PROOF_MISSING_CODE = "CP_CACHE_ENTRY_PROOF_MISSING";
@@ -531,6 +532,18 @@ function createCacheProofRejectedDecision(options) {
531
532
  url: options.event.result.href
532
533
  };
533
534
  }
535
+ function createCacheReuseTokenRejectedDecision(options) {
536
+ return {
537
+ kind: "hardNavigate",
538
+ reason: "cacheReuseTokenRejected",
539
+ token: options.event.token,
540
+ trace: createNavigationTrace(NavigationTraceReasonCodes.cacheReuseTokenRejected, {
541
+ ...options.traceFields,
542
+ cacheReuseTokenReason: options.reason
543
+ }),
544
+ url: options.event.result.href
545
+ };
546
+ }
534
547
  function createAcceptedCacheProofTraceFields(traceFields, decision) {
535
548
  if (decision === null) return traceFields;
536
549
  return {
@@ -619,6 +632,17 @@ function planFlightResponseArrived(options) {
619
632
  traceFields
620
633
  });
621
634
  const acceptedCacheEntryDecision = cacheEntryProofEvaluation.decision;
635
+ if (acceptedCacheEntryDecision !== null) {
636
+ const reuseVerdict = verifyOperationTokenForCacheReuse(options.event.token, {
637
+ graphVersion: options.routeManifest?.graphVersion ?? null,
638
+ installedCacheVariantFingerprint: null
639
+ });
640
+ if (!reuseVerdict.authorized) return createCacheReuseTokenRejectedDecision({
641
+ event: options.event,
642
+ reason: reuseVerdict.reason,
643
+ traceFields
644
+ });
645
+ }
622
646
  const commitTraceFields = createAcceptedCacheProofTraceFields(traceFields, acceptedCacheEntryDecision);
623
647
  const cacheEntryProposalFields = createCacheEntryProposalFields(acceptedCacheEntryDecision);
624
648
  if (targetSnapshot.interception !== null) {
@@ -3,6 +3,7 @@ declare const NAVIGATION_TRACE_SCHEMA_VERSION = 0;
3
3
  type NavigationTraceSchemaVersion = 0;
4
4
  declare const NavigationTraceReasonCodes: {
5
5
  cacheProofRejected: "NC_CACHE_REJECT";
6
+ cacheReuseTokenRejected: "NC_CACHE_TOKEN_REJECT";
6
7
  commitCurrent: "NC_COMMIT";
7
8
  crossDocumentFlight: "NC_CROSS_DOC_FLIGHT";
8
9
  fetchFresh: "NC_FETCH_FRESH";
@@ -42,7 +43,7 @@ declare const NavigationTraceTransactionCodes: {
42
43
  type NavigationTraceReasonCode = (typeof NavigationTraceReasonCodes)[keyof typeof NavigationTraceReasonCodes];
43
44
  type NavigationTraceTransactionCode = (typeof NavigationTraceTransactionCodes)[keyof typeof NavigationTraceTransactionCodes];
44
45
  type NavigationTraceCode = NavigationTraceReasonCode | NavigationTraceTransactionCode;
45
- type NavigationTraceFieldName = "activeNavigationId" | "cacheProofCode" | "cacheProofMode" | "cacheProofReuseClass" | "cacheProofScope" | "currentRootLayoutTreePath" | "currentVisibleCommitVersion" | "nextRootLayoutTreePath" | "eventKind" | "fetchResultSource" | "freshFetchReason" | "operationLane" | "pendingOperationId" | "redirectDepth" | "redirectSignal" | "startedVisibleCommitVersion" | "startedNavigationId" | "targetHref" | "traverseDirection";
46
+ type NavigationTraceFieldName = "activeNavigationId" | "cacheProofCode" | "cacheProofMode" | "cacheProofReuseClass" | "cacheProofScope" | "cacheReuseTokenReason" | "currentRootLayoutTreePath" | "currentVisibleCommitVersion" | "nextRootLayoutTreePath" | "eventKind" | "fetchResultSource" | "freshFetchReason" | "operationLane" | "pendingOperationId" | "redirectDepth" | "redirectSignal" | "startedVisibleCommitVersion" | "startedNavigationId" | "targetHref" | "traverseDirection";
46
47
  type NavigationTraceFieldValue = string | number | boolean | null;
47
48
  type NavigationTraceFields = Readonly<Partial<Record<NavigationTraceFieldName, NavigationTraceFieldValue>>>;
48
49
  type NavigationTraceEntry = Readonly<{
@@ -2,6 +2,7 @@
2
2
  const NAVIGATION_TRACE_SCHEMA_VERSION = 0;
3
3
  const NavigationTraceReasonCodes = {
4
4
  cacheProofRejected: "NC_CACHE_REJECT",
5
+ cacheReuseTokenRejected: "NC_CACHE_TOKEN_REJECT",
5
6
  commitCurrent: "NC_COMMIT",
6
7
  crossDocumentFlight: "NC_CROSS_DOC_FLIGHT",
7
8
  fetchFresh: "NC_FETCH_FRESH",
@@ -0,0 +1,40 @@
1
+ //#region src/server/operation-token.d.ts
2
+ type OperationLane = "hmr" | "navigation" | "prefetch" | "refresh" | "server-action" | "traverse";
3
+ type OperationToken = {
4
+ operationId: number;
5
+ lane: OperationLane;
6
+ navigationId: number;
7
+ baseVisibleCommitVersion: number;
8
+ graphVersion: string | null;
9
+ deploymentVersion: string | null;
10
+ targetSnapshotFingerprint: string;
11
+ cacheVariantFingerprint?: string;
12
+ };
13
+ declare const verifiedOperationTokenBrand: unique symbol;
14
+ type VerifiedOperationToken = OperationToken & {
15
+ readonly [verifiedOperationTokenBrand]: true;
16
+ };
17
+ type OperationTokenAuthority = {
18
+ activeNavigationId: number;
19
+ visibleCommitVersion: number;
20
+ graphVersion: string | null;
21
+ installedCacheVariantFingerprint: string | null;
22
+ };
23
+ type OperationTokenDimension = "navigation" | "visibleCommit" | "graphVersion" | "cacheVariant";
24
+ type OperationTokenRejectionReason = "staleNavigation" | "staleVisibleCommit" | "graphVersionMismatch" | "graphVersionMissing" | "cacheVariantMismatch" | "cacheVariantMissing";
25
+ type OperationTokenVerdict = {
26
+ readonly authorized: true;
27
+ readonly token: VerifiedOperationToken;
28
+ } | {
29
+ readonly authorized: false;
30
+ readonly reason: OperationTokenRejectionReason;
31
+ };
32
+ type OperationTokenVerificationPolicy = {
33
+ check: readonly OperationTokenDimension[];
34
+ require: readonly OperationTokenDimension[];
35
+ };
36
+ declare function verifyOperationToken(token: OperationToken, authority: OperationTokenAuthority, policy: OperationTokenVerificationPolicy): OperationTokenVerdict;
37
+ declare function verifyOperationTokenForCommit(token: OperationToken, authority: Pick<OperationTokenAuthority, "activeNavigationId" | "visibleCommitVersion">): OperationTokenVerdict;
38
+ declare function verifyOperationTokenForCacheReuse(token: OperationToken, authority: Pick<OperationTokenAuthority, "graphVersion" | "installedCacheVariantFingerprint">): OperationTokenVerdict;
39
+ //#endregion
40
+ export { OperationLane, OperationToken, OperationTokenAuthority, OperationTokenRejectionReason, OperationTokenVerdict, OperationTokenVerificationPolicy, VerifiedOperationToken, verifyOperationToken, verifyOperationTokenForCacheReuse, verifyOperationTokenForCommit };
@@ -0,0 +1,85 @@
1
+ //#region src/server/operation-token.ts
2
+ const DIMENSION_ORDER = [
3
+ "navigation",
4
+ "visibleCommit",
5
+ "graphVersion",
6
+ "cacheVariant"
7
+ ];
8
+ function evaluateDimension(dimension, token, authority) {
9
+ switch (dimension) {
10
+ case "navigation": return token.navigationId === authority.activeNavigationId ? { kind: "satisfied" } : {
11
+ kind: "mismatch",
12
+ reason: "staleNavigation"
13
+ };
14
+ case "visibleCommit": return token.baseVisibleCommitVersion === authority.visibleCommitVersion ? { kind: "satisfied" } : {
15
+ kind: "mismatch",
16
+ reason: "staleVisibleCommit"
17
+ };
18
+ case "graphVersion":
19
+ if (token.graphVersion === null || authority.graphVersion === null) return {
20
+ kind: "absent",
21
+ missingReason: "graphVersionMissing"
22
+ };
23
+ return token.graphVersion === authority.graphVersion ? { kind: "satisfied" } : {
24
+ kind: "mismatch",
25
+ reason: "graphVersionMismatch"
26
+ };
27
+ case "cacheVariant": {
28
+ const tokenVariant = token.cacheVariantFingerprint;
29
+ const installedVariant = authority.installedCacheVariantFingerprint;
30
+ if (tokenVariant === void 0 || installedVariant === null) return {
31
+ kind: "absent",
32
+ missingReason: "cacheVariantMissing"
33
+ };
34
+ return tokenVariant === installedVariant ? { kind: "satisfied" } : {
35
+ kind: "mismatch",
36
+ reason: "cacheVariantMismatch"
37
+ };
38
+ }
39
+ default: throw new Error("[vinext] Unknown operation-token dimension: " + String(dimension));
40
+ }
41
+ }
42
+ function verifyOperationToken(token, authority, policy) {
43
+ const required = new Set(policy.require);
44
+ const evaluated = new Set([...policy.check, ...policy.require]);
45
+ for (const dimension of DIMENSION_ORDER) {
46
+ if (!evaluated.has(dimension)) continue;
47
+ const status = evaluateDimension(dimension, token, authority);
48
+ if (status.kind === "mismatch") return {
49
+ authorized: false,
50
+ reason: status.reason
51
+ };
52
+ if (status.kind === "absent" && required.has(dimension)) return {
53
+ authorized: false,
54
+ reason: status.missingReason
55
+ };
56
+ }
57
+ return {
58
+ authorized: true,
59
+ token
60
+ };
61
+ }
62
+ function verifyOperationTokenForCommit(token, authority) {
63
+ return verifyOperationToken(token, {
64
+ activeNavigationId: authority.activeNavigationId,
65
+ visibleCommitVersion: authority.visibleCommitVersion,
66
+ graphVersion: token.graphVersion,
67
+ installedCacheVariantFingerprint: token.cacheVariantFingerprint ?? null
68
+ }, {
69
+ check: ["navigation", "visibleCommit"],
70
+ require: ["navigation", "visibleCommit"]
71
+ });
72
+ }
73
+ function verifyOperationTokenForCacheReuse(token, authority) {
74
+ return verifyOperationToken(token, {
75
+ activeNavigationId: token.navigationId,
76
+ visibleCommitVersion: token.baseVisibleCommitVersion,
77
+ graphVersion: authority.graphVersion,
78
+ installedCacheVariantFingerprint: authority.installedCacheVariantFingerprint
79
+ }, {
80
+ check: ["graphVersion", "cacheVariant"],
81
+ require: []
82
+ });
83
+ }
84
+ //#endregion
85
+ export { verifyOperationToken, verifyOperationTokenForCacheReuse, verifyOperationTokenForCommit };
@@ -116,6 +116,6 @@ type NormalizePagesDataRequestResult = {
116
116
  * Extracted from `entries/pages-server-entry.ts` so both `renderPage` and
117
117
  * `runMiddleware` share a single implementation.
118
118
  */
119
- declare function normalizePagesDataRequest(request: Request, buildId: string | null): NormalizePagesDataRequestResult;
119
+ declare function normalizePagesDataRequest(request: Request, buildId: string | null, basePath?: string): NormalizePagesDataRequestResult;
120
120
  //#endregion
121
121
  export { buildNextDataJsonResponse, buildNextDataNotFoundResponse, buildNextDataPropsJsonResponse, isNextDataPathname, normalizePagesDataRequest, parseNextDataPathname };
@@ -1,3 +1,4 @@
1
+ import { addBasePathToPathname, hasBasePath, stripBasePath } from "../utils/base-path.js";
1
2
  import { NEXTJS_DEPLOYMENT_ID_HEADER } from "./headers.js";
2
3
  //#region src/server/pages-data-route.ts
3
4
  /**
@@ -123,16 +124,18 @@ function buildNextDataNotFoundResponse() {
123
124
  * Extracted from `entries/pages-server-entry.ts` so both `renderPage` and
124
125
  * `runMiddleware` share a single implementation.
125
126
  */
126
- function normalizePagesDataRequest(request, buildId) {
127
+ function normalizePagesDataRequest(request, buildId, basePath = "") {
127
128
  const reqUrl = new URL(request.url);
128
- if (!isNextDataPathname(reqUrl.pathname)) return {
129
+ const hadBasePath = !!basePath && hasBasePath(reqUrl.pathname, basePath);
130
+ const dataPathname = basePath ? stripBasePath(reqUrl.pathname, basePath) : reqUrl.pathname;
131
+ if (!isNextDataPathname(dataPathname)) return {
129
132
  isDataReq: false,
130
133
  request,
131
134
  normalizedPathname: null,
132
135
  search: "",
133
136
  notFoundResponse: null
134
137
  };
135
- const dataMatch = buildId ? parseNextDataPathname(reqUrl.pathname, buildId) : null;
138
+ const dataMatch = buildId ? parseNextDataPathname(dataPathname, buildId) : null;
136
139
  if (!dataMatch) return {
137
140
  isDataReq: false,
138
141
  request,
@@ -141,7 +144,7 @@ function normalizePagesDataRequest(request, buildId) {
141
144
  notFoundResponse: buildNextDataNotFoundResponse()
142
145
  };
143
146
  const normalizedUrl = new URL(reqUrl);
144
- normalizedUrl.pathname = dataMatch.pagePathname;
147
+ normalizedUrl.pathname = hadBasePath ? addBasePathToPathname(dataMatch.pagePathname, basePath) : dataMatch.pagePathname;
145
148
  return {
146
149
  isDataReq: true,
147
150
  request: new Request(normalizedUrl, request),
@@ -0,0 +1,4 @@
1
+ //#region src/server/pages-dev-module-url.d.ts
2
+ declare function createPagesDevModuleUrl(viteRoot: string, moduleFilePath: string, viteBase: string): string;
3
+ //#endregion
4
+ export { createPagesDevModuleUrl };
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ //#region src/server/pages-dev-module-url.ts
3
+ function normalizeBase(base) {
4
+ if (!base || base === "/") return "/";
5
+ return `/${base.replace(/^\/+|\/+$/g, "")}/`;
6
+ }
7
+ function encodeModulePath(modulePath) {
8
+ return encodeURI(modulePath).replace(/%5B/gi, "[").replace(/%5D/gi, "]").replace(/\?/g, "%3F").replace(/#/g, "%23");
9
+ }
10
+ function createPagesDevModuleUrl(viteRoot, moduleFilePath, viteBase) {
11
+ const relativePath = (/^[A-Za-z]:[\\/]/.test(viteRoot) ? path.win32 : path).relative(viteRoot, moduleFilePath).replace(/\\/g, "/");
12
+ return normalizeBase(viteBase) + encodeModulePath(relativePath);
13
+ }
14
+ //#endregion
15
+ export { createPagesDevModuleUrl };
@@ -45,12 +45,6 @@ type DocumentRenderPageInput = {
45
45
  * the head nodes returned by `getInitialProps` (forward them to
46
46
  * `setDocumentInitialHead()` — do NOT call
47
47
  * `callDocumentGetInitialProps()` as well).
48
- * - `consumed` — `getInitialProps` WAS invoked but no body was produced
49
- * (it never called `renderPage`, returned no `{ html }`, or
50
- * threw). Callers must NOT re-invoke `getInitialProps` (that
51
- * would call it a second time) — render the streaming body,
52
- * spread `docProps` (possibly empty) onto `<Document>`, and
53
- * forward `head` to `setDocumentInitialHead()`.
54
48
  */
55
49
  type RunDocumentRenderPageResult = {
56
50
  status: "skipped";
@@ -60,10 +54,6 @@ type RunDocumentRenderPageResult = {
60
54
  stylesHTML: string;
61
55
  docProps: Record<string, unknown>;
62
56
  head: ReactNode[];
63
- } | {
64
- status: "consumed";
65
- docProps: Record<string, unknown>;
66
- head: ReactNode[];
67
57
  };
68
58
  /**
69
59
  * Run a user `_document.getInitialProps()` with a `ctx.renderPage()` that
@@ -75,11 +65,10 @@ type RunDocumentRenderPageResult = {
75
65
  * prod (`pages-page-response.ts`) and dev (`dev-server.ts`) SSR pipelines so
76
66
  * the `getInitialProps` + `renderPage` contract lives in one place.
77
67
  *
78
- * `getInitialProps` is invoked at most once here. When this returns `consumed`
79
- * or `rendered`, callers MUST treat that as the single invocation and must not
80
- * call `loadUserDocumentInitialProps` (which would invoke it again — and, for a
81
- * throwing override, surface the error as a 500 rather than the clean fallback
82
- * this contract guarantees).
68
+ * `getInitialProps` is invoked at most once here. When this returns `rendered`,
69
+ * callers MUST treat that as the single invocation and must not call
70
+ * `loadUserDocumentInitialProps` again. Errors intentionally propagate to the
71
+ * Pages Router's normal error-page pipeline, matching Next.js.
83
72
  *
84
73
  * @see .nextjs-ref/packages/next/src/server/render.tsx (search `renderPage`)
85
74
  */