vinext 0.1.0 → 0.1.1

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 (119) hide show
  1. package/dist/build/assets-ignore.d.ts +32 -0
  2. package/dist/build/assets-ignore.js +48 -0
  3. package/dist/build/client-build-config.d.ts +27 -1
  4. package/dist/build/client-build-config.js +58 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/client/navigation-runtime.d.ts +8 -0
  7. package/dist/client/navigation-runtime.js +1 -1
  8. package/dist/client/vinext-next-data.d.ts +2 -1
  9. package/dist/config/config-matchers.d.ts +20 -1
  10. package/dist/config/config-matchers.js +35 -1
  11. package/dist/config/next-config.d.ts +16 -3
  12. package/dist/config/next-config.js +30 -2
  13. package/dist/deploy.js +40 -304
  14. package/dist/entries/app-rsc-entry.d.ts +8 -2
  15. package/dist/entries/app-rsc-entry.js +54 -4
  16. package/dist/entries/app-rsc-manifest.js +20 -2
  17. package/dist/entries/pages-server-entry.js +9 -1
  18. package/dist/index.js +162 -217
  19. package/dist/plugins/postcss.js +18 -14
  20. package/dist/plugins/require-context.d.ts +6 -0
  21. package/dist/plugins/require-context.js +184 -0
  22. package/dist/routing/app-route-graph.d.ts +12 -1
  23. package/dist/routing/app-route-graph.js +137 -5
  24. package/dist/routing/route-pattern.d.ts +2 -1
  25. package/dist/routing/route-pattern.js +16 -1
  26. package/dist/server/api-handler.js +4 -0
  27. package/dist/server/app-browser-entry.js +84 -39
  28. package/dist/server/app-browser-interception-context.d.ts +2 -1
  29. package/dist/server/app-browser-interception-context.js +15 -2
  30. package/dist/server/app-browser-navigation-controller.d.ts +11 -1
  31. package/dist/server/app-browser-navigation-controller.js +77 -1
  32. package/dist/server/app-browser-popstate.d.ts +12 -3
  33. package/dist/server/app-browser-popstate.js +19 -4
  34. package/dist/server/app-browser-state.d.ts +3 -0
  35. package/dist/server/app-browser-state.js +6 -3
  36. package/dist/server/app-browser-visible-commit.js +9 -7
  37. package/dist/server/app-history-state.d.ts +45 -1
  38. package/dist/server/app-history-state.js +109 -1
  39. package/dist/server/app-page-boundary-render.js +41 -19
  40. package/dist/server/app-page-dispatch.d.ts +6 -0
  41. package/dist/server/app-page-dispatch.js +3 -1
  42. package/dist/server/app-page-element-builder.d.ts +1 -0
  43. package/dist/server/app-page-element-builder.js +22 -10
  44. package/dist/server/app-page-render.d.ts +6 -0
  45. package/dist/server/app-page-render.js +5 -3
  46. package/dist/server/app-page-request.d.ts +8 -6
  47. package/dist/server/app-page-request.js +12 -9
  48. package/dist/server/app-page-response.d.ts +2 -2
  49. package/dist/server/app-page-response.js +1 -1
  50. package/dist/server/app-page-route-wiring.js +2 -1
  51. package/dist/server/app-page-stream.d.ts +37 -2
  52. package/dist/server/app-page-stream.js +36 -3
  53. package/dist/server/app-pages-bridge.d.ts +16 -0
  54. package/dist/server/app-pages-bridge.js +23 -3
  55. package/dist/server/app-route-handler-cache.d.ts +1 -0
  56. package/dist/server/app-route-handler-cache.js +1 -0
  57. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  58. package/dist/server/app-route-handler-dispatch.js +2 -0
  59. package/dist/server/app-route-handler-execution.d.ts +1 -0
  60. package/dist/server/app-route-handler-execution.js +1 -0
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  62. package/dist/server/app-route-handler-runtime.js +3 -2
  63. package/dist/server/app-rsc-handler.d.ts +1 -0
  64. package/dist/server/app-rsc-handler.js +4 -3
  65. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  66. package/dist/server/app-rsc-route-matching.js +29 -4
  67. package/dist/server/app-server-action-execution.d.ts +11 -1
  68. package/dist/server/app-server-action-execution.js +68 -10
  69. package/dist/server/app-ssr-entry.d.ts +6 -0
  70. package/dist/server/app-ssr-entry.js +17 -1
  71. package/dist/server/dev-server.d.ts +1 -1
  72. package/dist/server/dev-server.js +54 -31
  73. package/dist/server/isr-cache.d.ts +37 -1
  74. package/dist/server/isr-cache.js +85 -1
  75. package/dist/server/navigation-planner.js +5 -3
  76. package/dist/server/navigation-trace.d.ts +1 -1
  77. package/dist/server/pages-node-compat.d.ts +2 -0
  78. package/dist/server/pages-node-compat.js +4 -0
  79. package/dist/server/pages-page-data.d.ts +10 -7
  80. package/dist/server/pages-page-data.js +4 -2
  81. package/dist/server/pages-page-handler.d.ts +9 -2
  82. package/dist/server/pages-page-handler.js +29 -16
  83. package/dist/server/pages-page-response.d.ts +11 -2
  84. package/dist/server/pages-page-response.js +8 -1
  85. package/dist/server/pages-readiness.d.ts +36 -0
  86. package/dist/server/pages-readiness.js +21 -0
  87. package/dist/server/pages-request-pipeline.d.ts +99 -0
  88. package/dist/server/pages-request-pipeline.js +209 -0
  89. package/dist/server/pages-revalidate.d.ts +15 -0
  90. package/dist/server/pages-revalidate.js +19 -0
  91. package/dist/server/prod-server.d.ts +6 -2
  92. package/dist/server/prod-server.js +101 -217
  93. package/dist/server/socket-error-backstop.d.ts +19 -1
  94. package/dist/server/socket-error-backstop.js +77 -4
  95. package/dist/shims/app-router-scroll.js +22 -4
  96. package/dist/shims/cache-runtime.js +31 -1
  97. package/dist/shims/error-boundary.d.ts +21 -11
  98. package/dist/shims/error-boundary.js +8 -1
  99. package/dist/shims/fetch-cache.d.ts +14 -1
  100. package/dist/shims/fetch-cache.js +18 -1
  101. package/dist/shims/hash-scroll.d.ts +1 -0
  102. package/dist/shims/hash-scroll.js +3 -1
  103. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  104. package/dist/shims/internal/link-status-registry.js +42 -0
  105. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  106. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  107. package/dist/shims/internal/utils.d.ts +1 -0
  108. package/dist/shims/link.js +20 -6
  109. package/dist/shims/navigation.d.ts +2 -2
  110. package/dist/shims/navigation.js +63 -7
  111. package/dist/shims/router-state.d.ts +1 -0
  112. package/dist/shims/router-state.js +2 -0
  113. package/dist/shims/router.d.ts +6 -3
  114. package/dist/shims/router.js +128 -21
  115. package/dist/utils/client-build-manifest.d.ts +8 -1
  116. package/dist/utils/client-build-manifest.js +30 -5
  117. package/dist/utils/client-entry-manifest.d.ts +11 -0
  118. package/dist/utils/client-entry-manifest.js +29 -0
  119. package/package.json +5 -1
@@ -2,11 +2,9 @@ import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
2
2
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
3
3
  import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_STATIC_FILE_HEADER } from "./headers.js";
4
4
  import { normalizePath } from "./normalize-path.js";
5
- import { applyMiddlewareRequestHeaders, isExternalUrl, matchRedirect, matchRewrite, proxyExternalRequest, requestContextFromRequest, sanitizeDestination } from "../config/config-matchers.js";
6
5
  import { notFoundResponse } from "./http-error-responses.js";
7
- import { applyConfigHeadersToHeaderRecord, filterInternalHeaders, isOpenRedirectShaped, normalizeTrailingSlash } from "./request-pipeline.js";
8
- import { mergeRewriteQuery } from "../utils/query.js";
9
- import { normalizeDefaultLocalePathname, stripI18nLocaleForApiRoute } from "./pages-i18n.js";
6
+ import { filterInternalHeaders, isOpenRedirectShaped } from "./request-pipeline.js";
7
+ import { isUnknownRecord } from "../utils/record.js";
10
8
  import { resolveRequestHost, resolveRequestProtocol, trustProxy, trustedHosts } from "./proxy-trust.js";
11
9
  import { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, isImageOptimizationPath, isSafeImageContentType, parseImageParams } from "./image-optimization.js";
12
10
  import { installSocketErrorBackstop } from "./socket-error-backstop.js";
@@ -14,10 +12,13 @@ import { ASSET_PREFIX_URL_DIR, assetPrefixPathname, isAbsoluteAssetPrefix, resol
14
12
  import { CONTENT_TYPES, StaticFileCache, etagFromFilenameHash } from "./static-file-cache.js";
15
13
  import { buildNextDataNotFoundResponse, isNextDataPathname, parseNextDataPathname } from "./pages-data-route.js";
16
14
  import { collectInlineCssManifest } from "../build/inline-css.js";
15
+ import { mergeHeaders } from "./worker-utils.js";
16
+ import { runPagesRequest } from "./pages-request-pipeline.js";
17
17
  import { manifestFileWithBase } from "../utils/manifest-paths.js";
18
18
  import { readTrustedPrerenderRouteParamsFromHeaders, serializePrerenderRouteParamsHeader } from "./prerender-route-params.js";
19
19
  import { computeLazyChunks } from "../utils/lazy-chunks.js";
20
- import { findClientEntryFile, readClientBuildManifest } from "../utils/client-build-manifest.js";
20
+ import { findClientEntryFile, findPagesClientEntryFile, readClientBuildManifest } from "../utils/client-build-manifest.js";
21
+ import { findClientEntryFileFromVinextManifest, findPagesClientEntryFileFromVinextManifest, readClientEntryManifest } from "../utils/client-entry-manifest.js";
21
22
  import { readPrerenderSecret } from "../build/server-manifest.js";
22
23
  import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
23
24
  import fs from "node:fs";
@@ -157,10 +158,6 @@ const NO_BODY_RESPONSE_STATUSES = new Set([
157
158
  205,
158
159
  304
159
160
  ]);
160
- function hasHeader(headersRecord, name) {
161
- const target = name.toLowerCase();
162
- return Object.keys(headersRecord).some((key) => key.toLowerCase() === target);
163
- }
164
161
  function omitHeadersCaseInsensitive(headersRecord, names) {
165
162
  const targets = new Set(names.map((name) => name.toLowerCase()));
166
163
  const filtered = {};
@@ -175,10 +172,6 @@ function matchesIfNoneMatchHeader(ifNoneMatch, etag) {
175
172
  if (ifNoneMatch === "*") return true;
176
173
  return ifNoneMatch.split(",").map((value) => value.trim()).some((value) => value === etag);
177
174
  }
178
- function stripHeaders(headersRecord, names) {
179
- const targets = new Set(names.map((name) => name.toLowerCase()));
180
- for (const key of Object.keys(headersRecord)) if (targets.has(key.toLowerCase())) delete headersRecord[key];
181
- }
182
175
  function isNoBodyResponseStatus(status) {
183
176
  return NO_BODY_RESPONSE_STATUSES.has(status);
184
177
  }
@@ -201,36 +194,15 @@ function logProdServerStarted(host, port, purpose) {
201
194
  /**
202
195
  * Merge middleware/config headers and an optional status override into a new
203
196
  * Web Response while preserving the original body stream when allowed.
204
- * Keep this in sync with server/worker-utils.ts and the generated copy in
205
- * deploy.ts.
197
+ *
198
+ * This is the canonical {@link mergeHeaders} (server/worker-utils.ts) with the
199
+ * arguments in (headers, response) order. The request path now calls
200
+ * `runPagesRequest`, which uses `mergeHeaders` directly; this wrapper is retained
201
+ * only for its existing tests and any external callers, so there is a single
202
+ * implementation to keep in sync. (deploy.ts still emits its own generated copy.)
206
203
  */
207
204
  function mergeWebResponse(middlewareHeaders, response, statusOverride) {
208
- const filteredMiddlewareHeaders = omitHeadersCaseInsensitive(middlewareHeaders, ["content-length"]);
209
- const status = statusOverride ?? response.status;
210
- const mergedHeaders = mergeResponseHeaders(filteredMiddlewareHeaders, response);
211
- const shouldDropBody = isNoBodyResponseStatus(status);
212
- const shouldStripStreamLength = isVinextStreamedHtmlResponse(response) && hasHeader(mergedHeaders, "content-length");
213
- if (!Object.keys(filteredMiddlewareHeaders).length && statusOverride === void 0 && !shouldDropBody && !shouldStripStreamLength) return response;
214
- if (shouldDropBody) {
215
- cancelResponseBody(response);
216
- stripHeaders(mergedHeaders, [
217
- "content-encoding",
218
- "content-length",
219
- "content-type",
220
- "transfer-encoding"
221
- ]);
222
- return new Response(null, {
223
- status,
224
- statusText: status === response.status ? response.statusText : void 0,
225
- headers: toWebHeaders(mergedHeaders)
226
- });
227
- }
228
- if (shouldStripStreamLength) stripHeaders(mergedHeaders, ["content-length"]);
229
- return new Response(response.body, {
230
- status,
231
- statusText: status === response.status ? response.statusText : void 0,
232
- headers: toWebHeaders(mergedHeaders)
233
- });
205
+ return mergeHeaders(response, middlewareHeaders, statusOverride);
234
206
  }
235
207
  /**
236
208
  * Send a compressed response if the content type is compressible and the
@@ -624,6 +596,44 @@ function resolveAppRouterAssetPath(pathname, assetPathPrefix, assetPrefix) {
624
596
  if (pathname.startsWith(nextStaticDir)) return pathname;
625
597
  return null;
626
598
  }
599
+ function isSsrManifest(value) {
600
+ if (!isUnknownRecord(value)) return false;
601
+ return Object.values(value).every((files) => Array.isArray(files) && files.every((file) => typeof file === "string"));
602
+ }
603
+ function readSsrManifest(clientDir) {
604
+ const manifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
605
+ if (!fs.existsSync(manifestPath)) return {};
606
+ let parsed;
607
+ try {
608
+ parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
609
+ } catch (error) {
610
+ console.warn(`[vinext] Ignoring unparseable SSR manifest at ${manifestPath}:`, error);
611
+ return {};
612
+ }
613
+ if (!isSsrManifest(parsed)) {
614
+ console.warn(`[vinext] Ignoring SSR manifest with unexpected shape at ${manifestPath}`);
615
+ return {};
616
+ }
617
+ return parsed;
618
+ }
619
+ function installPagesClientAssetGlobals(options) {
620
+ const ssrManifest = readSsrManifest(options.clientDir);
621
+ globalThis.__VINEXT_SSR_MANIFEST__ = Object.keys(ssrManifest).length > 0 ? ssrManifest : void 0;
622
+ const buildManifest = readClientBuildManifest(path.join(options.clientDir, ".vite", "manifest.json"));
623
+ const clientEntryManifest = readClientEntryManifest(options.clientDir);
624
+ const entryOptions = {
625
+ clientDir: options.clientDir,
626
+ assetsSubdir: options.assetsSubdir,
627
+ assetBase: options.assetBase,
628
+ ...buildManifest ? { buildManifest } : {}
629
+ };
630
+ globalThis.__VINEXT_CLIENT_ENTRY__ = options.clientEntryLookup === "pages-client-entry" ? findPagesClientEntryFileFromVinextManifest(clientEntryManifest, options.assetBase) ?? findPagesClientEntryFile(entryOptions) : findClientEntryFileFromVinextManifest(clientEntryManifest, options.assetBase) ?? findClientEntryFile(entryOptions);
631
+ if (buildManifest) {
632
+ const lazyChunks = computeLazyChunks(buildManifest).map((file) => manifestFileWithBase(file, options.assetBase));
633
+ globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks.length > 0 ? lazyChunks : void 0;
634
+ } else globalThis.__VINEXT_LAZY_CHUNKS__ = void 0;
635
+ return ssrManifest;
636
+ }
627
637
  /**
628
638
  * Start the App Router production server.
629
639
  *
@@ -653,9 +663,18 @@ async function startAppRouterServer(options) {
653
663
  const rscModule = await import(`${pathToFileURL(rscEntryPath).href}?t=${rscMtime}`);
654
664
  const rscHandler = resolveAppRouterHandler(rscModule.default);
655
665
  const appRouterAssetPrefix = typeof rscModule.__assetPrefix === "string" ? rscModule.__assetPrefix : "";
666
+ const appRouterBasePath = typeof rscModule.__basePath === "string" ? rscModule.__basePath : "";
656
667
  const appRouterInlineCss = rscModule.__inlineCss === true;
668
+ const appRouterHasPagesDir = rscModule.__hasPagesDir === true;
657
669
  globalThis.__VINEXT_INLINE_CSS__ = appRouterInlineCss ? collectInlineCssManifest(clientDir, appRouterAssetPrefix) : void 0;
658
670
  const appAssetPathPrefix = assetPrefixPathname(appRouterAssetPrefix);
671
+ const appAssetBase = appRouterBasePath ? `${appRouterBasePath}/` : "/";
672
+ if (appRouterHasPagesDir) installPagesClientAssetGlobals({
673
+ clientDir,
674
+ assetsSubdir: resolveAssetsDir(appRouterAssetPrefix),
675
+ assetBase: appAssetBase,
676
+ clientEntryLookup: "pages-client-entry"
677
+ });
659
678
  const seededRoutes = await resolveAppRouterPrerenderSeeder(rscModule)(path.dirname(rscEntryPath));
660
679
  if (seededRoutes > 0) console.log(`[vinext] Seeded ${seededRoutes} pre-rendered route${seededRoutes !== 1 ? "s" : ""} into memory cache`);
661
680
  const staticCache = await StaticFileCache.create(clientDir);
@@ -811,20 +830,12 @@ async function startPagesRouterServer(options) {
811
830
  contentDispositionType: vinextConfig.images.contentDispositionType,
812
831
  contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy
813
832
  } : void 0;
814
- let ssrManifest = {};
815
- const manifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
816
- if (fs.existsSync(manifestPath)) ssrManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
817
- const buildManifest = readClientBuildManifest(path.join(clientDir, ".vite", "manifest.json"));
818
- globalThis.__VINEXT_CLIENT_ENTRY__ = findClientEntryFile({
819
- buildManifest,
833
+ const ssrManifest = installPagesClientAssetGlobals({
820
834
  clientDir,
821
835
  assetsSubdir: resolveAssetsDir(assetPrefix),
822
- assetBase
836
+ assetBase,
837
+ clientEntryLookup: "any-client-entry"
823
838
  });
824
- if (buildManifest) {
825
- const lazyChunks = computeLazyChunks(buildManifest).map((file) => manifestFileWithBase(file, assetBase));
826
- globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks.length > 0 ? lazyChunks : void 0;
827
- } else globalThis.__VINEXT_LAZY_CHUNKS__ = void 0;
828
839
  const staticCache = await StaticFileCache.create(clientDir);
829
840
  const handleRequest = async (req, res) => {
830
841
  const rawUrl = req.url ?? "/";
@@ -915,20 +926,6 @@ async function startPagesRouterServer(options) {
915
926
  pathname = stripped;
916
927
  }
917
928
  }
918
- const basePathState = {
919
- basePath,
920
- hadBasePath
921
- };
922
- {
923
- const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
924
- const trailingSlashRedirect = normalizeTrailingSlash(pathname, basePath, trailingSlash, qs);
925
- if (trailingSlashRedirect) {
926
- const location = trailingSlashRedirect.headers.get("Location");
927
- res.writeHead(trailingSlashRedirect.status, location ? { Location: location } : void 0);
928
- res.end();
929
- return;
930
- }
931
- }
932
929
  let isDataReq = false;
933
930
  if (isNextDataPathname(pathname)) {
934
931
  const dataMatch = pagesBuildId ? parseNextDataPathname(pathname, pagesBuildId) : null;
@@ -948,167 +945,54 @@ async function startPagesRouterServer(options) {
948
945
  const reqHeaders = filterInternalHeaders(rawReqHeaders);
949
946
  const method = req.method ?? "GET";
950
947
  const hasBody = method !== "GET" && method !== "HEAD";
951
- let webRequest = new Request(`${protocol}://${hostHeader}${url}`, {
948
+ const result = await runPagesRequest(new Request(`${protocol}://${hostHeader}${url}`, {
952
949
  method,
953
950
  headers: reqHeaders,
954
951
  body: hasBody ? readNodeStream(req) : void 0,
955
952
  duplex: hasBody ? "half" : void 0
956
- });
957
- const reqCtx = requestContextFromRequest(webRequest);
958
- const requestHostname = i18nConfig ? new URL(webRequest.url).hostname : "";
959
- const matchPathname = i18nConfig ? normalizeDefaultLocalePathname(pathname, i18nConfig, { hostname: requestHostname }) : pathname;
960
- if (configRedirects.length) {
961
- const redirect = matchRedirect(matchPathname, configRedirects, reqCtx, basePathState);
962
- if (redirect) {
963
- const dest = sanitizeDestination(basePath && hadBasePath && !isExternalUrl(redirect.destination) && !hasBasePath(redirect.destination, basePath) ? basePath + redirect.destination : redirect.destination);
964
- res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
965
- res.end();
966
- return;
967
- }
968
- }
969
- let resolvedUrl = url;
970
- const middlewareHeaders = {};
971
- let middlewareStatus;
972
- if (typeof runMiddleware === "function") {
973
- const result = await runMiddleware(webRequest, void 0, { isDataRequest });
974
- if (result.waitUntilPromises && result.waitUntilPromises.length > 0) Promise.allSettled(result.waitUntilPromises);
975
- if (!result.continue) {
976
- if (result.redirectUrl) {
977
- const redirectHeaders = { Location: result.redirectUrl };
978
- if (result.responseHeaders) for (const [key, value] of result.responseHeaders) {
979
- const existing = redirectHeaders[key];
980
- if (existing === void 0) redirectHeaders[key] = value;
981
- else if (Array.isArray(existing)) existing.push(value);
982
- else redirectHeaders[key] = [existing, value];
983
- }
984
- res.writeHead(result.redirectStatus ?? 307, redirectHeaders);
985
- res.end();
986
- return;
987
- }
988
- if (result.response) {
989
- const body = Buffer.from(await result.response.arrayBuffer());
990
- const respHeaders = {};
991
- result.response.headers.forEach((value, key) => {
992
- if (key === "set-cookie") return;
993
- respHeaders[key] = value;
994
- });
995
- const setCookies = result.response.headers.getSetCookie?.() ?? [];
996
- if (setCookies.length > 0) respHeaders["set-cookie"] = setCookies;
997
- if (result.response.statusText) res.writeHead(result.response.status, result.response.statusText, respHeaders);
998
- else res.writeHead(result.response.status, respHeaders);
999
- res.end(body);
1000
- return;
1001
- }
1002
- }
1003
- if (result.responseHeaders) for (const [key, value] of result.responseHeaders) if (key === "set-cookie") {
1004
- const existing = middlewareHeaders[key];
1005
- if (Array.isArray(existing)) existing.push(value);
1006
- else if (existing) middlewareHeaders[key] = [existing, value];
1007
- else middlewareHeaders[key] = [value];
1008
- } else middlewareHeaders[key] = value;
1009
- if (result.rewriteUrl) resolvedUrl = result.rewriteUrl;
1010
- middlewareStatus = result.status ?? result.rewriteStatus;
1011
- }
1012
- const { postMwReqCtx, request: postMwReq } = applyMiddlewareRequestHeaders(middlewareHeaders, webRequest, { preserveCredentialHeaders: isExternalUrl(resolvedUrl) });
1013
- webRequest = postMwReq;
1014
- let resolvedPathname = resolvedUrl.split("?")[0];
1015
- const matchResolvedPathname = (p) => i18nConfig ? normalizeDefaultLocalePathname(p, i18nConfig, { hostname: requestHostname }) : p;
1016
- if (configHeaders.length) applyConfigHeadersToHeaderRecord(middlewareHeaders, {
953
+ }), {
954
+ basePath,
955
+ trailingSlash,
956
+ i18nConfig,
957
+ configRedirects,
958
+ configRewrites,
1017
959
  configHeaders,
1018
- pathname: matchPathname,
1019
- requestContext: reqCtx,
1020
- basePathState
1021
- });
1022
- if (isExternalUrl(resolvedUrl)) {
1023
- await sendWebResponse(mergeWebResponse(middlewareHeaders, await proxyExternalRequest(webRequest, resolvedUrl), void 0), req, res, compress);
1024
- return;
1025
- }
1026
- if (staticLookupPath !== "/" && !staticLookupPath.startsWith("/api/") && !staticLookupPath.startsWith(`/_next/static/`) && await tryServeStatic(req, res, clientDir, staticLookupPath, compress, staticCache, middlewareHeaders)) return;
1027
- let configRewriteFired = false;
1028
- if (configRewrites.beforeFiles?.length) {
1029
- const rewritten = matchRewrite(matchResolvedPathname(resolvedPathname), configRewrites.beforeFiles, postMwReqCtx, basePathState);
1030
- if (rewritten) {
1031
- if (isExternalUrl(rewritten)) {
1032
- await sendWebResponse(await proxyExternalRequest(webRequest, rewritten), req, res, compress);
1033
- return;
1034
- }
1035
- resolvedUrl = mergeRewriteQuery(resolvedUrl, rewritten);
1036
- resolvedPathname = resolvedUrl.split("?")[0];
1037
- configRewriteFired = true;
960
+ hadBasePath,
961
+ isDataReq,
962
+ isDataRequest,
963
+ ctx: void 0,
964
+ rawSearch: rawQs,
965
+ matchPageRoute: matchPageRoute ?? null,
966
+ runMiddleware: typeof runMiddleware === "function" ? runMiddleware : null,
967
+ renderPage: typeof renderPage === "function" ? (request, resolvedUrl, options, stagedHeaders) => renderPage(request, resolvedUrl, ssrManifest, void 0, stagedHeaders, options) : null,
968
+ handleApi: typeof handleApi === "function" ? (request, apiUrl) => handleApi(request, apiUrl, createNodeExecutionContext()) : null,
969
+ serveStaticFile: async (requestPathname, stagedHeaders) => {
970
+ if (requestPathname === "/" || requestPathname.startsWith("/api/") || requestPathname.startsWith(`/_next/static/`)) return false;
971
+ return tryServeStatic(req, res, clientDir, requestPathname, compress, staticCache, stagedHeaders);
1038
972
  }
1039
- }
1040
- if (basePath && !hadBasePath && !configRewriteFired) {
1041
- res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
1042
- res.end("This page could not be found");
1043
- return;
1044
- }
1045
- const apiLookupUrl = stripI18nLocaleForApiRoute(resolvedUrl, vinextConfig?.i18n ?? null);
1046
- const apiLookupPathname = apiLookupUrl.split("?")[0];
1047
- if (apiLookupPathname.startsWith("/api/") || apiLookupPathname === "/api") {
1048
- let response;
1049
- if (typeof handleApi === "function") response = await handleApi(webRequest, apiLookupUrl, createNodeExecutionContext());
1050
- else response = new Response("404 - API route not found", { status: 404 });
1051
- const mergedResponse = mergeWebResponse(middlewareHeaders, response, middlewareStatus);
1052
- if (!mergedResponse.body) {
1053
- await sendWebResponse(mergedResponse, req, res, compress);
973
+ });
974
+ if (result.type === "handled") return;
975
+ if (result.type === "response") {
976
+ const { response } = result;
977
+ if (isVinextStreamedHtmlResponse(response) || !response.body || result.defaultContentType === void 0) {
978
+ await sendWebResponse(response, req, res, compress);
1054
979
  return;
1055
980
  }
1056
- const responseBody = Buffer.from(await mergedResponse.arrayBuffer());
1057
- const ct = mergedResponse.headers.get("content-type") ?? "application/octet-stream";
1058
- const responseHeaders = mergeResponseHeaders({}, mergedResponse);
1059
- const finalStatusText = mergedResponse.statusText || void 0;
1060
- sendCompressed(req, res, responseBody, ct, mergedResponse.status, responseHeaders, compress, finalStatusText);
1061
- return;
1062
- }
1063
- const pageMatch = matchPageRoute ? matchPageRoute(resolvedPathname, webRequest) : null;
1064
- if ((!pageMatch || pageMatch.route.isDynamic) && configRewrites.afterFiles?.length) {
1065
- const rewritten = matchRewrite(matchResolvedPathname(resolvedPathname), configRewrites.afterFiles, postMwReqCtx, basePathState);
1066
- if (rewritten) {
1067
- if (isExternalUrl(rewritten)) {
1068
- await sendWebResponse(await proxyExternalRequest(webRequest, rewritten), req, res, compress);
1069
- return;
1070
- }
1071
- resolvedUrl = mergeRewriteQuery(resolvedUrl, rewritten);
1072
- resolvedPathname = resolvedUrl.split("?")[0];
1073
- }
1074
- }
1075
- let response;
1076
- if (typeof renderPage === "function") {
1077
- const middlewareResponseHeaders = toWebHeaders(middlewareHeaders);
1078
- const renderPageMatch = matchPageRoute ? matchPageRoute(resolvedPathname, webRequest) : null;
1079
- const shouldDeferErrorPageOnMiss = !isDataReq && !!matchPageRoute && !renderPageMatch;
1080
- const dataRenderOptions = isDataReq ? { isDataReq: true } : void 0;
1081
- response = await renderPage(webRequest, resolvedUrl, ssrManifest, void 0, middlewareResponseHeaders, shouldDeferErrorPageOnMiss ? { renderErrorPageOnMiss: false } : dataRenderOptions);
1082
- let matchedFallbackRewrite = false;
1083
- if (response && response.status === 404 && shouldDeferErrorPageOnMiss && configRewrites.fallback?.length) {
1084
- const fallbackRewrite = matchRewrite(matchResolvedPathname(resolvedPathname), configRewrites.fallback, postMwReqCtx, basePathState);
1085
- if (fallbackRewrite) {
1086
- if (isExternalUrl(fallbackRewrite)) {
1087
- await sendWebResponse(await proxyExternalRequest(webRequest, fallbackRewrite), req, res, compress);
1088
- return;
1089
- }
1090
- matchedFallbackRewrite = true;
1091
- response = await renderPage(webRequest, mergeRewriteQuery(resolvedUrl, fallbackRewrite), ssrManifest, void 0, middlewareResponseHeaders, dataRenderOptions);
1092
- }
1093
- }
1094
- if (response && response.status === 404 && shouldDeferErrorPageOnMiss && !matchedFallbackRewrite) response = await renderPage(webRequest, resolvedUrl, ssrManifest, void 0, middlewareResponseHeaders);
1095
- }
1096
- if (!response) {
1097
- res.writeHead(404);
1098
- res.end("This page could not be found");
1099
- return;
1100
- }
1101
- const shouldStreamPagesResponse = isVinextStreamedHtmlResponse(response);
1102
- const mergedResponse = mergeWebResponse(middlewareHeaders, response, middlewareStatus);
1103
- if (shouldStreamPagesResponse || !mergedResponse.body) {
1104
- await sendWebResponse(mergedResponse, req, res, compress);
981
+ const responseBody = Buffer.from(await response.arrayBuffer());
982
+ const ct = response.headers.get("content-type") ?? result.defaultContentType;
983
+ const responseHeaders = {};
984
+ response.headers.forEach((v, k) => {
985
+ if (k === "set-cookie") return;
986
+ responseHeaders[k] = v;
987
+ });
988
+ const setCookies = response.headers.getSetCookie?.() ?? [];
989
+ if (setCookies.length > 0) responseHeaders["set-cookie"] = setCookies;
990
+ const finalStatusText = response.statusText || void 0;
991
+ sendCompressed(req, res, responseBody, ct, response.status, responseHeaders, compress, finalStatusText);
1105
992
  return;
1106
993
  }
1107
- const responseBody = Buffer.from(await mergedResponse.arrayBuffer());
1108
- const ct = mergedResponse.headers.get("content-type") ?? "text/html";
1109
- const responseHeaders = mergeResponseHeaders({}, mergedResponse);
1110
- const finalStatusText = mergedResponse.statusText || void 0;
1111
- sendCompressed(req, res, responseBody, ct, mergedResponse.status, responseHeaders, compress, finalStatusText);
994
+ res.writeHead(404);
995
+ res.end("This page could not be found");
1112
996
  } catch (e) {
1113
997
  console.error("[vinext] Server error:", e);
1114
998
  if (!res.headersSent) {
@@ -5,6 +5,24 @@
5
5
  * Exported for unit testing in isolation (no process-state mutation).
6
6
  */
7
7
  declare function peerDisconnectCode(err: unknown): string | undefined;
8
+ /**
9
+ * Pure predicate: returns `true` when `err` is a benign failure from a
10
+ * dynamic `import()` of a static asset URL — the "URL dependency" pattern
11
+ * that Next.js tolerates at build time. Two shapes are recognised:
12
+ *
13
+ * - `ERR_UNKNOWN_FILE_EXTENSION`: the asset resolved on disk but is not an
14
+ * ES module (e.g. a co-located `./style.css`). The error message ends in
15
+ * the offending extension in quotes: `... extension ".css" for /path`.
16
+ * - `ERR_MODULE_NOT_FOUND`: the asset URL did not resolve (the chunk lives
17
+ * in `dist/server/` but the source asset does not). Node attaches the
18
+ * unresolved specifier on `err.url`; we match only when it points at a
19
+ * static-asset extension.
20
+ *
21
+ * Anything outside this allow-list (including missing `.js`/`.mjs`/`.ts`
22
+ * modules with no extension) returns `false` so real bugs still crash.
23
+ * Exported for unit testing in isolation.
24
+ */
25
+ declare function isBenignAssetImportError(err: unknown): boolean;
8
26
  /**
9
27
  * Test-only: returns whether the backstop has been installed in this
10
28
  * process. Used by the unit test to assert idempotent install via the
@@ -13,4 +31,4 @@ declare function peerDisconnectCode(err: unknown): string | undefined;
13
31
  declare function isSocketErrorBackstopInstalled(): boolean;
14
32
  declare function installSocketErrorBackstop(): void;
15
33
  //#endregion
16
- export { installSocketErrorBackstop, isSocketErrorBackstopInstalled, peerDisconnectCode };
34
+ export { installSocketErrorBackstop, isBenignAssetImportError, isSocketErrorBackstopInstalled, peerDisconnectCode };
@@ -15,9 +15,10 @@
15
15
  * `uncaughtException`, where this listener filters it.
16
16
  *
17
17
  * Filters strictly on peer-disconnect codes (ECONNRESET / EPIPE /
18
- * ECONNABORTED) and synchronously re-throws everything else,
19
- * preserving Node's default crash semantics for genuine bugs. This
20
- * is more conservative than Next.js's equivalent
18
+ * ECONNABORTED) plus benign static-asset `import()` rejections (see
19
+ * `isBenignAssetImportError`), and synchronously re-throws everything
20
+ * else, preserving Node's default crash semantics for genuine bugs.
21
+ * This is more conservative than Next.js's equivalent
21
22
  * (`router-server.ts`'s log-only handler), which silently swallows
22
23
  * every uncaught — vinext keeps real bugs surfacing.
23
24
  *
@@ -89,6 +90,68 @@ function peerDisconnectCode(err) {
89
90
  const code = err?.code;
90
91
  return code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" ? code : void 0;
91
92
  }
93
+ const STATIC_ASSET_IMPORT_EXTENSIONS = new Set([
94
+ ".css",
95
+ ".scss",
96
+ ".sass",
97
+ ".less",
98
+ ".styl",
99
+ ".png",
100
+ ".jpg",
101
+ ".jpeg",
102
+ ".gif",
103
+ ".svg",
104
+ ".webp",
105
+ ".avif",
106
+ ".ico",
107
+ ".bmp",
108
+ ".woff",
109
+ ".woff2",
110
+ ".ttf",
111
+ ".otf",
112
+ ".eot",
113
+ ".txt",
114
+ ".md",
115
+ ".csv",
116
+ ".xml",
117
+ ".pdf",
118
+ ".wasm"
119
+ ]);
120
+ /**
121
+ * Pure predicate: returns `true` when `err` is a benign failure from a
122
+ * dynamic `import()` of a static asset URL — the "URL dependency" pattern
123
+ * that Next.js tolerates at build time. Two shapes are recognised:
124
+ *
125
+ * - `ERR_UNKNOWN_FILE_EXTENSION`: the asset resolved on disk but is not an
126
+ * ES module (e.g. a co-located `./style.css`). The error message ends in
127
+ * the offending extension in quotes: `... extension ".css" for /path`.
128
+ * - `ERR_MODULE_NOT_FOUND`: the asset URL did not resolve (the chunk lives
129
+ * in `dist/server/` but the source asset does not). Node attaches the
130
+ * unresolved specifier on `err.url`; we match only when it points at a
131
+ * static-asset extension.
132
+ *
133
+ * Anything outside this allow-list (including missing `.js`/`.mjs`/`.ts`
134
+ * modules with no extension) returns `false` so real bugs still crash.
135
+ * Exported for unit testing in isolation.
136
+ */
137
+ function isBenignAssetImportError(err) {
138
+ if (err === null || typeof err !== "object") return false;
139
+ const code = err.code;
140
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
141
+ const message = err.message ?? "";
142
+ const match = /extension\s+"([^"]+)"/.exec(message);
143
+ return match != null && STATIC_ASSET_IMPORT_EXTENSIONS.has(match[1].toLowerCase());
144
+ }
145
+ if (code === "ERR_MODULE_NOT_FOUND") {
146
+ const url = err.url;
147
+ if (typeof url !== "string") return false;
148
+ const pathname = url.split("?", 1)[0].split("#", 1)[0];
149
+ const dot = pathname.lastIndexOf(".");
150
+ if (dot === -1) return false;
151
+ return STATIC_ASSET_IMPORT_EXTENSIONS.has(pathname.slice(dot).toLowerCase());
152
+ }
153
+ return false;
154
+ }
92
155
  /**
93
156
  * Test-only: returns whether the backstop has been installed in this
94
157
  * process. Used by the unit test to assert idempotent install via the
@@ -104,7 +167,16 @@ function installSocketErrorBackstop() {
104
167
  proc[SOCKET_BACKSTOP_FLAG] = true;
105
168
  const debug = process.env.VINEXT_DEBUG_SOCKET_ERRORS === "1";
106
169
  if (debug) console.warn("[vinext] socket-error backstop installed");
170
+ const absorbBenignAssetImport = (reason, kind) => {
171
+ if (!isBenignAssetImportError(reason)) return false;
172
+ if (debug) {
173
+ const code = reason?.code;
174
+ console.warn(`[vinext] absorbed ${kind} ${code} (asset URL import)`);
175
+ }
176
+ return true;
177
+ };
107
178
  process.on("uncaughtException", (err) => {
179
+ if (absorbBenignAssetImport(err, "uncaughtException")) return;
108
180
  if (process.env.VINEXT_PRERENDER === "1") throw err;
109
181
  const code = peerDisconnectCode(err);
110
182
  if (code) {
@@ -114,6 +186,7 @@ function installSocketErrorBackstop() {
114
186
  throw err;
115
187
  });
116
188
  process.on("unhandledRejection", (reason) => {
189
+ if (absorbBenignAssetImport(reason, "unhandledRejection")) return;
117
190
  if (process.env.VINEXT_PRERENDER === "1") throw reason;
118
191
  const code = peerDisconnectCode(reason);
119
192
  if (code) {
@@ -124,4 +197,4 @@ function installSocketErrorBackstop() {
124
197
  });
125
198
  }
126
199
  //#endregion
127
- export { installSocketErrorBackstop, isSocketErrorBackstopInstalled, peerDisconnectCode };
200
+ export { installSocketErrorBackstop, isBenignAssetImportError, isSocketErrorBackstopInstalled, peerDisconnectCode };
@@ -49,14 +49,22 @@ function getHashFragmentDomNode(hash) {
49
49
  const element = document.getElementById(fragment) ?? document.getElementsByName(fragment)[0];
50
50
  return element instanceof HTMLElement ? element : null;
51
51
  }
52
+ function isInDocumentHead(node) {
53
+ const head = node.ownerDocument?.head;
54
+ return head != null && head.contains(node);
55
+ }
52
56
  function findNextScrollTarget(node) {
53
57
  if (!(node instanceof Element)) return null;
58
+ if (isInDocumentHead(node)) return { kind: "document-top" };
54
59
  let target = node;
55
60
  while (!(target instanceof HTMLElement) || shouldSkipElement(target)) {
56
61
  if (target.nextElementSibling === null) return null;
57
62
  target = target.nextElementSibling;
58
63
  }
59
- return target;
64
+ return {
65
+ kind: "element",
66
+ element: target
67
+ };
60
68
  }
61
69
  function scrollToElement(target, hash) {
62
70
  if (hash !== null) {
@@ -79,9 +87,19 @@ var AppRouterScrollTargetInner = class extends React$1.Component {
79
87
  if (intent === null) return;
80
88
  if (this.props.commitId === null || intent.commitId !== this.props.commitId) return;
81
89
  let target;
82
- if (intent.hash !== null) target = getHashFragmentDomNode(intent.hash);
83
- else target = findNextScrollTarget(findDOMNode(this));
84
- if (target === null) return;
90
+ if (intent.hash !== null) {
91
+ target = getHashFragmentDomNode(intent.hash);
92
+ if (target === null) return;
93
+ } else {
94
+ const next = findNextScrollTarget(findDOMNode(this));
95
+ if (next === null) return;
96
+ if (next.kind === "document-top") {
97
+ if (consumeAppRouterScrollIntent(intent, this.props.commitId) === null) return;
98
+ document.documentElement.scrollTop = 0;
99
+ return;
100
+ }
101
+ target = next.element;
102
+ }
85
103
  const consumed = consumeAppRouterScrollIntent(intent, this.props.commitId);
86
104
  if (consumed === null) return;
87
105
  scrollToElement(target, consumed.hash);