vinext 0.1.5 → 0.1.7

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 (93) hide show
  1. package/README.md +2 -2
  2. package/dist/build/client-build-config.d.ts +4 -6
  3. package/dist/build/client-build-config.js +27 -7
  4. package/dist/build/google-fonts/find-font-files-in-css.d.ts +16 -0
  5. package/dist/build/google-fonts/find-font-files-in-css.js +28 -0
  6. package/dist/build/prerender.js +1 -2
  7. package/dist/build/report.d.ts +1 -2
  8. package/dist/build/report.js +2 -11
  9. package/dist/build/run-prerender.js +1 -1
  10. package/dist/check.js +3 -3
  11. package/dist/client/pages-router-link-navigation.d.ts +10 -0
  12. package/dist/client/pages-router-link-navigation.js +3 -3
  13. package/dist/deploy.js +3 -3
  14. package/dist/entries/app-rsc-entry.d.ts +2 -1
  15. package/dist/entries/app-rsc-entry.js +35 -9
  16. package/dist/entries/app-rsc-manifest.js +76 -46
  17. package/dist/entries/pages-server-entry.js +1 -0
  18. package/dist/index.js +116 -74
  19. package/dist/init.d.ts +5 -4
  20. package/dist/init.js +27 -8
  21. package/dist/plugins/fonts.js +3 -1
  22. package/dist/plugins/middleware-export-validation.d.ts +5 -0
  23. package/dist/plugins/middleware-export-validation.js +36 -0
  24. package/dist/plugins/og-asset-ownership.d.ts +27 -0
  25. package/dist/plugins/og-asset-ownership.js +285 -0
  26. package/dist/plugins/og-assets.js +28 -10
  27. package/dist/plugins/remove-console.js +1 -1
  28. package/dist/plugins/strip-server-exports.js +2 -1
  29. package/dist/server/accept-encoding.d.ts +29 -0
  30. package/dist/server/accept-encoding.js +95 -0
  31. package/dist/server/app-action-request.d.ts +4 -0
  32. package/dist/server/app-action-request.js +9 -0
  33. package/dist/server/app-bfcache-identity.d.ts +12 -2
  34. package/dist/server/app-bfcache-identity.js +52 -23
  35. package/dist/server/app-browser-entry.js +8 -5
  36. package/dist/server/app-browser-navigation-controller.js +1 -1
  37. package/dist/server/app-browser-rsc-redirect.d.ts +2 -1
  38. package/dist/server/app-browser-rsc-redirect.js +8 -1
  39. package/dist/server/app-browser-state.js +1 -1
  40. package/dist/server/app-browser-visible-commit.js +1 -1
  41. package/dist/server/app-history-state.d.ts +1 -2
  42. package/dist/server/app-history-state.js +1 -1
  43. package/dist/server/app-middleware.d.ts +1 -0
  44. package/dist/server/app-middleware.js +1 -0
  45. package/dist/server/app-page-dispatch.d.ts +2 -2
  46. package/dist/server/app-page-head.js +2 -2
  47. package/dist/server/app-page-method.js +1 -1
  48. package/dist/server/app-page-request.d.ts +7 -0
  49. package/dist/server/app-page-request.js +19 -1
  50. package/dist/server/app-page-stream.js +2 -1
  51. package/dist/server/app-prerender-endpoints.d.ts +3 -3
  52. package/dist/server/app-prerender-endpoints.js +3 -4
  53. package/dist/server/app-route-handler-cache.js +1 -1
  54. package/dist/server/app-route-handler-dispatch.js +2 -2
  55. package/dist/server/app-route-handler-execution.js +3 -2
  56. package/dist/server/app-route-handler-policy.d.ts +1 -1
  57. package/dist/server/app-route-handler-policy.js +1 -6
  58. package/dist/server/app-route-module-loader.d.ts +62 -14
  59. package/dist/server/app-route-module-loader.js +61 -10
  60. package/dist/server/app-rsc-errors.d.ts +12 -1
  61. package/dist/server/app-rsc-errors.js +22 -2
  62. package/dist/server/app-rsc-handler.d.ts +16 -14
  63. package/dist/server/app-rsc-handler.js +47 -44
  64. package/dist/server/app-rsc-response-finalizer.js +5 -1
  65. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  66. package/dist/server/app-rsc-route-matching.js +13 -0
  67. package/dist/server/app-server-action-execution.js +1 -1
  68. package/dist/server/app-ssr-entry.js +9 -7
  69. package/dist/server/headers.d.ts +5 -1
  70. package/dist/server/headers.js +5 -1
  71. package/dist/server/metadata-route-response.d.ts +3 -3
  72. package/dist/server/middleware-runtime.d.ts +2 -1
  73. package/dist/server/middleware-runtime.js +19 -8
  74. package/dist/server/middleware.d.ts +2 -2
  75. package/dist/server/middleware.js +26 -11
  76. package/dist/server/pages-request-pipeline.d.ts +3 -1
  77. package/dist/server/pages-request-pipeline.js +4 -2
  78. package/dist/server/prod-server.d.ts +1 -1
  79. package/dist/server/prod-server.js +85 -72
  80. package/dist/shims/dynamic.js +1 -1
  81. package/dist/shims/font-google-base.d.ts +1 -0
  82. package/dist/shims/font-google-base.js +6 -21
  83. package/dist/shims/internal/interpolate-as.d.ts +25 -0
  84. package/dist/shims/internal/interpolate-as.js +196 -0
  85. package/dist/shims/link.js +24 -1
  86. package/dist/shims/navigation.js +1 -1
  87. package/dist/shims/navigation.react-server.js +1 -1
  88. package/dist/shims/router.d.ts +1 -3
  89. package/dist/shims/router.js +62 -18
  90. package/dist/typegen.js +2 -10
  91. package/dist/utils/project.d.ts +7 -1
  92. package/dist/utils/project.js +16 -2
  93. package/package.json +3 -3
@@ -28,7 +28,7 @@ declare function isProxyFile(filePath: string): boolean;
28
28
  */
29
29
  declare function resolveMiddlewareHandler(mod: MiddlewareModule, filePath: string): (request: NextRequest, event: NextFetchEvent) => Response | undefined | void | Promise<Response | undefined | void>;
30
30
  /**
31
- * Find the proxy or middleware file in the project root.
31
+ * Find the proxy or middleware file at the selected app/pages convention level.
32
32
  * Checks for proxy.ts (Next.js 16) first, then falls back to middleware.ts.
33
33
  * If middleware.ts is found, logs a deprecation warning.
34
34
  *
@@ -44,7 +44,7 @@ declare function resolveMiddlewareHandler(mod: MiddlewareModule, filePath: strin
44
44
  * @see https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts
45
45
  * (search for "MIDDLEWARE_FILENAME" + "file convention is deprecated")
46
46
  */
47
- declare function findMiddlewareFile(root: string, fileMatcher: ValidFileMatcher): string | null;
47
+ declare function findMiddlewareFile(root: string, fileMatcher: ValidFileMatcher, conventionDir?: string): string | null;
48
48
  /**
49
49
  * Load and execute middleware for a given request.
50
50
  *
@@ -13,7 +13,8 @@ import path from "node:path";
13
13
  * https://github.com/vercel/next.js/blob/canary/packages/next/src/build/templates/middleware.ts
14
14
  */
15
15
  function isProxyFile(filePath) {
16
- return path.basename(filePath).replace(/\.\w+$/, "") === "proxy";
16
+ const base = path.basename(filePath);
17
+ return base === "proxy" || base.startsWith("proxy.");
17
18
  }
18
19
  /**
19
20
  * Resolve the middleware/proxy handler function from a module's exports.
@@ -32,9 +33,8 @@ function resolveMiddlewareHandler(mod, filePath) {
32
33
  isProxy: isProxyFile(filePath)
33
34
  });
34
35
  }
35
- const MIDDLEWARE_LOCATIONS = ["", "src/"];
36
36
  /**
37
- * Find the proxy or middleware file in the project root.
37
+ * Find the proxy or middleware file at the selected app/pages convention level.
38
38
  * Checks for proxy.ts (Next.js 16) first, then falls back to middleware.ts.
39
39
  * If middleware.ts is found, logs a deprecation warning.
40
40
  *
@@ -50,18 +50,33 @@ const MIDDLEWARE_LOCATIONS = ["", "src/"];
50
50
  * @see https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts
51
51
  * (search for "MIDDLEWARE_FILENAME" + "file convention is deprecated")
52
52
  */
53
- function findMiddlewareFile(root, fileMatcher) {
54
- for (const dir of MIDDLEWARE_LOCATIONS) for (const ext of fileMatcher.dottedExtensions) {
55
- const fullPath = path.join(root, dir, `proxy${ext}`);
56
- if (fs.existsSync(fullPath)) return fullPath;
53
+ function findMiddlewareFile(root, fileMatcher, conventionDir = root) {
54
+ let proxyPath = null;
55
+ for (const ext of fileMatcher.dottedExtensions) {
56
+ const fullPath = path.join(conventionDir, `proxy${ext}`);
57
+ if (fs.existsSync(fullPath)) {
58
+ proxyPath = fullPath;
59
+ break;
60
+ }
57
61
  }
58
- for (const dir of MIDDLEWARE_LOCATIONS) for (const ext of fileMatcher.dottedExtensions) {
59
- const fullPath = path.join(root, dir, `middleware${ext}`);
62
+ let middlewarePath = null;
63
+ for (const ext of fileMatcher.dottedExtensions) {
64
+ const fullPath = path.join(conventionDir, `middleware${ext}`);
60
65
  if (fs.existsSync(fullPath)) {
61
- console.warn("The \"middleware\" file convention is deprecated. Please use \"proxy\" instead.\n\n To migrate automatically, run:\n npx @next/codemod@canary middleware-to-proxy .\n\n Learn more: https://nextjs.org/docs/messages/middleware-to-proxy");
62
- return fullPath;
66
+ middlewarePath = fullPath;
67
+ break;
63
68
  }
64
69
  }
70
+ if (proxyPath && middlewarePath) {
71
+ const relativeProxyPath = `./${path.relative(root, proxyPath)}`;
72
+ const relativeMiddlewarePath = `./${path.relative(root, middlewarePath)}`;
73
+ throw new Error(`Both middleware file "${relativeMiddlewarePath}" and proxy file "${relativeProxyPath}" are detected. Please use "${relativeProxyPath}" only. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`);
74
+ }
75
+ if (proxyPath) return proxyPath;
76
+ if (middlewarePath) {
77
+ console.warn("The \"middleware\" file convention is deprecated. Please use \"proxy\" instead.\n\n To migrate automatically, run:\n npx @next/codemod@canary middleware-to-proxy .\n\n Learn more: https://nextjs.org/docs/messages/middleware-to-proxy");
78
+ return middlewarePath;
79
+ }
65
80
  return null;
66
81
  }
67
82
  function isMiddlewareModule(value) {
@@ -106,7 +106,9 @@ type PagesPipelineResult = {
106
106
  * ASSUMPTION: request already has internal headers filtered and basePath stripped.
107
107
  * The adapter is responsible for that pre-processing before calling runPagesRequest.
108
108
  * The adapter also handles: open-redirect guard, _next/static 404, image optimization,
109
- * _next/data normalization, Node decode/normalize/400, public-file serving.
109
+ * _next/data normalization and classification: adapters must rewrite the data
110
+ * URL to its page pathname and set `isDataReq` (the source of truth here), Node
111
+ * decode/normalize/400, public-file serving.
110
112
  * runPagesRequest receives a "clean" request with basePath-stripped URL.
111
113
  */
112
114
  declare function runPagesRequest(request: Request, deps: PagesPipelineDeps): Promise<PagesPipelineResult>;
@@ -40,7 +40,9 @@ function wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath) {
40
40
  * ASSUMPTION: request already has internal headers filtered and basePath stripped.
41
41
  * The adapter is responsible for that pre-processing before calling runPagesRequest.
42
42
  * The adapter also handles: open-redirect guard, _next/static 404, image optimization,
43
- * _next/data normalization, Node decode/normalize/400, public-file serving.
43
+ * _next/data normalization and classification: adapters must rewrite the data
44
+ * URL to its page pathname and set `isDataReq` (the source of truth here), Node
45
+ * decode/normalize/400, public-file serving.
44
46
  * runPagesRequest receives a "clean" request with basePath-stripped URL.
45
47
  */
46
48
  async function runPagesRequest(request, deps) {
@@ -54,7 +56,7 @@ async function runPagesRequest(request, deps) {
54
56
  hadBasePath
55
57
  };
56
58
  {
57
- const trailingSlashRedirect = normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
59
+ const trailingSlashRedirect = isDataReq ? null : normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
58
60
  if (trailingSlashRedirect) return {
59
61
  type: "response",
60
62
  response: trailingSlashRedirect
@@ -1,3 +1,4 @@
1
+ import { negotiateEncoding } from "./accept-encoding.js";
1
2
  import { StaticFileCache } from "./static-file-cache.js";
2
3
  import { resolveRequestHost, trustProxy, trustedHosts } from "./proxy-trust.js";
3
4
  import { IncomingMessage, ServerResponse } from "node:http";
@@ -59,7 +60,6 @@ type ProdServerOptions = {
59
60
  declare const COMPRESSIBLE_TYPES: Set<string>;
60
61
  /** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
61
62
  declare const COMPRESS_THRESHOLD = 1024;
62
- declare function negotiateEncoding(req: IncomingMessage): "zstd" | "br" | "gzip" | "deflate" | null;
63
63
  /**
64
64
  * Merge middleware headers and a Web Response's headers into a single
65
65
  * record suitable for Node.js `res.writeHead()`. Uses `getSetCookie()`
@@ -19,6 +19,7 @@ import { readTrustedPrerenderRouteParamsFromHeaders, serializePrerenderRoutePara
19
19
  import { computeClientRuntimeMetadata } from "../utils/client-runtime-metadata.js";
20
20
  import { readPrerenderSecret } from "../build/server-manifest.js";
21
21
  import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
22
+ import { negotiateEncoding, parseAcceptedEncodings, selectContentEncoding } from "./accept-encoding.js";
22
23
  import fs from "node:fs";
23
24
  import path from "node:path";
24
25
  import fs$1 from "node:fs/promises";
@@ -167,25 +168,6 @@ const COMPRESSIBLE_TYPES = new Set([
167
168
  /** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
168
169
  const COMPRESS_THRESHOLD = 1024;
169
170
  /**
170
- * Parse the Accept-Encoding header and return the best supported encoding.
171
- * Preference order: zstd > br > gzip > deflate > identity.
172
- *
173
- * zstd decompresses ~3-5x faster than brotli at similar compression ratios.
174
- * Supported in Chrome 123+, Firefox 126+. Safari can decompress but doesn't
175
- * send zstd in Accept-Encoding, so it transparently falls back to br/gzip.
176
- */
177
- const HAS_ZSTD = typeof zlib.createZstdCompress === "function";
178
- function negotiateEncoding(req) {
179
- const accept = req.headers["accept-encoding"];
180
- if (!accept || typeof accept !== "string") return null;
181
- const lower = accept.toLowerCase();
182
- if (HAS_ZSTD && lower.includes("zstd")) return "zstd";
183
- if (lower.includes("br")) return "br";
184
- if (lower.includes("gzip")) return "gzip";
185
- if (lower.includes("deflate")) return "deflate";
186
- return null;
187
- }
188
- /**
189
171
  * Create a compression stream for the given encoding.
190
172
  */
191
173
  function createCompressor(encoding, mode = "default") {
@@ -265,6 +247,23 @@ function omitHeadersCaseInsensitive(headersRecord, targets) {
265
247
  }
266
248
  return filtered;
267
249
  }
250
+ function mergeVaryHeader(headers, value) {
251
+ const merged = { ...headers };
252
+ const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "vary");
253
+ if (!existingKey) {
254
+ merged.Vary = value;
255
+ return merged;
256
+ }
257
+ const rawVary = merged[existingKey];
258
+ const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
259
+ if (existingVary.trim().length === 0) {
260
+ merged[existingKey] = value;
261
+ return merged;
262
+ }
263
+ const values = existingVary.split(",").map((entry) => entry.trim().toLowerCase());
264
+ if (!values.includes("*") && !values.includes(value.toLowerCase())) merged[existingKey] = `${existingVary}, ${value}`;
265
+ return merged;
266
+ }
268
267
  function matchesIfNoneMatchHeader(ifNoneMatch, etag) {
269
268
  if (!ifNoneMatch) return false;
270
269
  if (ifNoneMatch === "*") return true;
@@ -318,33 +317,37 @@ function mergeWebResponse(middlewareHeaders, response, statusOverride) {
318
317
  function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true, statusText) {
319
318
  const buf = typeof body === "string" ? Buffer.from(body) : body;
320
319
  const baseType = contentType.split(";")[0].trim();
321
- const encoding = compress ? negotiateEncoding(req) : null;
320
+ const varyByEncoding = compress && COMPRESSIBLE_TYPES.has(baseType);
321
+ const encoding = compress ? negotiateEncoding(req) : "identity";
322
322
  const headersWithoutBodyHeaders = omitHeadersCaseInsensitive(extraHeaders, OMIT_BODY_HEADERS);
323
- const writeHead = (headers) => {
324
- if (statusText) res.writeHead(statusCode, statusText, headers);
325
- else res.writeHead(statusCode, headers);
323
+ const writeHead = (headers, responseStatus = statusCode, responseStatusText = statusText) => {
324
+ if (responseStatusText) res.writeHead(responseStatus, responseStatusText, headers);
325
+ else res.writeHead(responseStatus, headers);
326
326
  };
327
- if (encoding && COMPRESSIBLE_TYPES.has(baseType) && buf.length >= 1024) {
328
- const compressor = createCompressor(encoding);
329
- const rawVary = extraHeaders["Vary"] ?? extraHeaders["vary"];
330
- const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
331
- let varyValue;
332
- if (existingVary) varyValue = existingVary.toLowerCase().includes("accept-encoding") ? existingVary : existingVary + ", Accept-Encoding";
333
- else varyValue = "Accept-Encoding";
334
- writeHead({
327
+ if (encoding !== "identity" && varyByEncoding && buf.length >= 1024) {
328
+ writeHead(mergeVaryHeader({
335
329
  ...headersWithoutBodyHeaders,
336
330
  "Content-Type": contentType,
337
- "Content-Encoding": encoding,
338
- Vary: varyValue
339
- });
331
+ "Content-Encoding": encoding
332
+ }, "Accept-Encoding"));
333
+ if (req.method === "HEAD") {
334
+ res.end();
335
+ return;
336
+ }
337
+ const compressor = createCompressor(encoding);
340
338
  compressor.end(buf);
341
339
  pipeline(compressor, res, () => {});
342
340
  } else {
343
- writeHead({
341
+ const identityHeaders = {
344
342
  ...headersWithoutBodyHeaders,
345
343
  "Content-Type": contentType,
346
344
  "Content-Length": String(buf.length)
347
- });
345
+ };
346
+ writeHead(varyByEncoding ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
347
+ if (req.method === "HEAD") {
348
+ res.end();
349
+ return;
350
+ }
348
351
  res.end(buf);
349
352
  }
350
353
  }
@@ -378,24 +381,35 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
378
381
  }
379
382
  const entry = cache.lookup(lookupPath);
380
383
  if (!entry) return false;
384
+ const rawAe = compress ? req.headers["accept-encoding"] : void 0;
385
+ const parsed = typeof rawAe === "string" ? parseAcceptedEncodings(rawAe) : void 0;
386
+ const availableVariants = [
387
+ ...entry.zst ? ["zstd"] : [],
388
+ ...entry.br ? ["br"] : [],
389
+ ...entry.gz ? ["gzip"] : []
390
+ ];
391
+ const variesByEncoding = compress && availableVariants.length > 0;
392
+ const selected = parsed ? selectContentEncoding(parsed, availableVariants) : "identity";
393
+ const variant = selected === "zstd" ? entry.zst : selected === "br" ? entry.br : selected === "gzip" ? entry.gz : entry.original;
381
394
  const ifNoneMatch = req.headers["if-none-match"];
382
395
  if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, entry.etag)) {
383
- if (extraHeaders) res.writeHead(304, {
396
+ const notModifiedHeaders = variesByEncoding ? mergeVaryHeader({
384
397
  ...entry.notModifiedHeaders,
385
398
  ...extraHeaders
386
- });
387
- else res.writeHead(304, entry.notModifiedHeaders);
399
+ }, "Accept-Encoding") : {
400
+ ...entry.notModifiedHeaders,
401
+ ...extraHeaders
402
+ };
403
+ if (selected !== "identity") notModifiedHeaders["Content-Encoding"] = selected;
404
+ res.writeHead(304, notModifiedHeaders);
388
405
  res.end();
389
406
  return true;
390
407
  }
391
- const rawAe = compress ? req.headers["accept-encoding"] : void 0;
392
- const ae = typeof rawAe === "string" ? rawAe.toLowerCase() : void 0;
393
- const variant = ae ? ae.includes("zstd") && entry.zst || ae.includes("br") && entry.br || ae.includes("gzip") && entry.gz || entry.original : entry.original;
394
- if (extraHeaders) res.writeHead(responseStatus, {
408
+ const responseHeaders = {
395
409
  ...variant.headers,
396
410
  ...extraHeaders
397
- });
398
- else res.writeHead(responseStatus, variant.headers);
411
+ };
412
+ res.writeHead(responseStatus, variesByEncoding ? mergeVaryHeader(responseHeaders, "Accept-Encoding") : responseHeaders);
399
413
  if (omitBody || req.method === "HEAD") {
400
414
  res.end();
401
415
  return true;
@@ -428,18 +442,6 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
428
442
  const etag = isHashed && etagFromFilenameHash(resolved.path, ext) || `W/"${resolved.size}-${Math.floor(resolved.mtimeMs / 1e3)}"`;
429
443
  const baseType = ct.split(";")[0].trim();
430
444
  const isCompressible = compress && COMPRESSIBLE_TYPES.has(baseType);
431
- const ifNoneMatch = req.headers["if-none-match"];
432
- if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
433
- const notModifiedHeaders = {
434
- ETag: etag,
435
- "Cache-Control": cacheControl,
436
- ...isCompressible ? { Vary: "Accept-Encoding" } : void 0,
437
- ...extraHeaders
438
- };
439
- res.writeHead(304, notModifiedHeaders);
440
- res.end();
441
- return true;
442
- }
443
445
  const baseHeaders = {
444
446
  "Content-Type": ct,
445
447
  "Cache-Control": cacheControl,
@@ -448,12 +450,19 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
448
450
  };
449
451
  if (isCompressible) {
450
452
  const encoding = negotiateEncoding(req);
451
- if (encoding) {
452
- res.writeHead(responseStatus, {
453
+ const ifNoneMatch = req.headers["if-none-match"];
454
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
455
+ const notModifiedHeaders = mergeVaryHeader(baseHeaders, "Accept-Encoding");
456
+ if (encoding !== "identity") notModifiedHeaders["Content-Encoding"] = encoding;
457
+ res.writeHead(304, notModifiedHeaders);
458
+ res.end();
459
+ return true;
460
+ }
461
+ if (encoding !== "identity") {
462
+ res.writeHead(responseStatus, mergeVaryHeader({
453
463
  ...baseHeaders,
454
- "Content-Encoding": encoding,
455
- Vary: "Accept-Encoding"
456
- });
464
+ "Content-Encoding": encoding
465
+ }, "Accept-Encoding"));
457
466
  if (omitBody || req.method === "HEAD") {
458
467
  res.end();
459
468
  return true;
@@ -468,10 +477,17 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
468
477
  return true;
469
478
  }
470
479
  }
471
- res.writeHead(responseStatus, {
480
+ const ifNoneMatch = req.headers["if-none-match"];
481
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
482
+ res.writeHead(304, isCompressible ? mergeVaryHeader(baseHeaders, "Accept-Encoding") : baseHeaders);
483
+ res.end();
484
+ return true;
485
+ }
486
+ const identityHeaders = {
472
487
  ...baseHeaders,
473
488
  "Content-Length": String(resolved.size)
474
- });
489
+ };
490
+ res.writeHead(responseStatus, isCompressible ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
475
491
  if (omitBody || req.method === "HEAD") {
476
492
  res.end();
477
493
  return true;
@@ -568,25 +584,22 @@ async function sendWebResponse(webResponse, req, res, compress) {
568
584
  if (existing !== void 0) nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
569
585
  else nodeHeaders[key] = value;
570
586
  });
587
+ const alreadyEncoded = webResponse.headers.get("content-encoding") !== null;
571
588
  if (!webResponse.body) {
572
589
  writeHead(nodeHeaders);
573
590
  res.end();
574
591
  return;
575
592
  }
576
- const alreadyEncoded = webResponse.headers.has("content-encoding");
577
593
  const baseType = (webResponse.headers.get("content-type") ?? "").split(";")[0].trim();
578
- const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : null;
579
- const shouldCompress = !!(encoding && COMPRESSIBLE_TYPES.has(baseType));
594
+ const varyByEncoding = compress && !alreadyEncoded && COMPRESSIBLE_TYPES.has(baseType);
595
+ const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : "identity";
596
+ const shouldCompress = encoding !== "identity" && COMPRESSIBLE_TYPES.has(baseType);
580
597
  if (shouldCompress) {
581
598
  delete nodeHeaders["content-length"];
582
599
  delete nodeHeaders["Content-Length"];
583
600
  nodeHeaders["Content-Encoding"] = encoding;
584
- const existingVary = nodeHeaders["Vary"] ?? nodeHeaders["vary"];
585
- if (existingVary) {
586
- if (!String(existingVary).toLowerCase().includes("accept-encoding")) nodeHeaders["Vary"] = existingVary + ", Accept-Encoding";
587
- } else nodeHeaders["Vary"] = "Accept-Encoding";
588
601
  }
589
- writeHead(nodeHeaders);
602
+ writeHead(varyByEncoding ? mergeVaryHeader(nodeHeaders, "Accept-Encoding") : nodeHeaders);
590
603
  if (req.method === "HEAD") {
591
604
  cancelResponseBody(webResponse);
592
605
  res.end();
@@ -127,7 +127,7 @@ function dynamic(dynamicInput, options) {
127
127
  const preloadModuleIds = loadableGenerated?.modules ?? modules;
128
128
  if (!ssr) {
129
129
  if (isServer) {
130
- const SSRFalse = (_props) => LoadingComponent ? React.createElement(LoadingComponent, createDynamicLoadingProps({ pastDelay: false })) : null;
130
+ const SSRFalse = (_props) => LoadingComponent ? React.createElement(LoadingComponent, createDynamicLoadingProps()) : null;
131
131
  SSRFalse.displayName = "DynamicSSRFalse";
132
132
  return SSRFalse;
133
133
  }
@@ -19,6 +19,7 @@ type FontResult = {
19
19
  };
20
20
  type InternalGoogleFontRuntimeOptions = {
21
21
  selfHostedCSS?: string;
22
+ preloadUrls?: string[];
22
23
  adjustedFallbackCSS?: string;
23
24
  fontWeight?: number;
24
25
  fontStyle?: "normal" | "italic";
@@ -206,27 +206,12 @@ function getSSRFontPreloads() {
206
206
  return [...ssrFontPreloads];
207
207
  }
208
208
  /**
209
- * Extract font file URLs from @font-face CSS rules.
210
- * Parses url('...') references from the CSS text.
211
- */
212
- function extractFontUrlsFromCSS(css) {
213
- const urls = [];
214
- const urlRegex = /url\(['"]?([^'")]+)['"]?\)/g;
215
- let match;
216
- while ((match = urlRegex.exec(css)) !== null) {
217
- const url = match[1];
218
- if (url && url.startsWith("/")) urls.push(url);
219
- }
220
- return urls;
221
- }
222
- /**
223
- * Collect font file URLs from self-hosted CSS for preload link generation.
209
+ * Collect build-selected font file URLs for preload link generation.
224
210
  * Only collects on the server (SSR). Deduplicates by href using a Set for O(1) lookups.
225
211
  */
226
- function collectFontPreloadsFromCSS(css) {
212
+ function collectFontPreloads(urls) {
227
213
  if (typeof document !== "undefined") return;
228
- const urls = extractFontUrlsFromCSS(css);
229
- for (const href of urls) if (!ssrFontPreloadHrefs.has(href)) {
214
+ for (const href of urls) if (href.startsWith("/") && !ssrFontPreloadHrefs.has(href)) {
230
215
  ssrFontPreloadHrefs.add(href);
231
216
  ssrFontPreloads.push({
232
217
  href,
@@ -240,10 +225,10 @@ const injectedSelfHosted = /* @__PURE__ */ new Set();
240
225
  * Inject self-hosted @font-face CSS (from the build plugin).
241
226
  * This replaces the CDN <link> tag with inline CSS.
242
227
  */
243
- function injectSelfHostedCSS(css) {
228
+ function injectSelfHostedCSS(css, preloadUrls = []) {
229
+ collectFontPreloads(preloadUrls);
244
230
  if (injectedSelfHosted.has(css)) return;
245
231
  injectedSelfHosted.add(css);
246
- collectFontPreloadsFromCSS(css);
247
232
  if (typeof document === "undefined") {
248
233
  ssrFontStyles.push(css);
249
234
  return;
@@ -277,7 +262,7 @@ function createFontLoader(family) {
277
262
  internalStyle: internal?.fontStyle,
278
263
  google: true
279
264
  });
280
- if (internal?.selfHostedCSS) injectSelfHostedCSS(internal.selfHostedCSS);
265
+ if (internal?.selfHostedCSS) injectSelfHostedCSS(internal.selfHostedCSS, internal.preloadUrls);
281
266
  else {
282
267
  const url = buildGoogleFontsUrl(family, options);
283
268
  injectFontStylesheet(url);
@@ -0,0 +1,25 @@
1
+ import { UrlQuery } from "../../utils/query.js";
2
+
3
+ //#region src/shims/internal/interpolate-as.d.ts
4
+ /**
5
+ * Wire-compatible alias for Node's `querystring.ParsedUrlQuery`. Inlined here
6
+ * so this module has no dependency on the `querystring` types package.
7
+ */
8
+ type ParsedUrlQuery = {
9
+ [key: string]: string | string[] | undefined;
10
+ };
11
+ type DynamicRouteHrefProjection = {
12
+ href: string;
13
+ params: string[];
14
+ query: ParsedUrlQuery;
15
+ routePathname: string;
16
+ };
17
+ /**
18
+ * Resolve a bracket-pattern route href against its displayed href. Query
19
+ * values can be supplied directly (object-form hrefs) or parsed from the route
20
+ * href (string-form hrefs). A `?` after `#` is part of the fragment, not a
21
+ * query delimiter.
22
+ */
23
+ declare function interpolateDynamicRouteHref(routeHref: string, asHref: string, queryInput?: UrlQuery): DynamicRouteHrefProjection | null;
24
+ //#endregion
25
+ export { DynamicRouteHrefProjection, interpolateDynamicRouteHref };