vinext 1.0.0-beta.2 → 1.0.0-beta.3

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 (126) hide show
  1. package/README.md +6 -9
  2. package/dist/build/client-build-config.js +7 -1
  3. package/dist/check.js +4 -0
  4. package/dist/config/config-matchers.js +4 -1
  5. package/dist/config/next-config.d.ts +12 -3
  6. package/dist/config/next-config.js +4 -0
  7. package/dist/entries/app-browser-entry.d.ts +4 -10
  8. package/dist/entries/app-browser-entry.js +20 -4
  9. package/dist/entries/app-rsc-entry.js +3 -0
  10. package/dist/entries/app-rsc-manifest.js +19 -0
  11. package/dist/entries/pages-client-entry.js +0 -1
  12. package/dist/entries/pages-server-entry.js +10 -3
  13. package/dist/index.js +112 -43
  14. package/dist/init-cloudflare.d.ts +3 -1
  15. package/dist/init-cloudflare.js +59 -5
  16. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  17. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  18. package/dist/plugins/import-meta-url.js +17 -6
  19. package/dist/plugins/require-context.js +20 -15
  20. package/dist/plugins/transform-cache.d.ts +19 -0
  21. package/dist/plugins/transform-cache.js +36 -0
  22. package/dist/routing/app-route-graph.d.ts +14 -6
  23. package/dist/routing/app-route-graph.js +106 -16
  24. package/dist/server/api-handler.d.ts +2 -0
  25. package/dist/server/api-handler.js +3 -3
  26. package/dist/server/app-browser-entry.js +103 -31
  27. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  28. package/dist/server/app-browser-navigation-controller.js +1 -1
  29. package/dist/server/app-page-cache-finalizer.js +1 -1
  30. package/dist/server/app-page-cache.d.ts +1 -1
  31. package/dist/server/app-page-cache.js +16 -2
  32. package/dist/server/app-page-dispatch.d.ts +8 -0
  33. package/dist/server/app-page-dispatch.js +31 -6
  34. package/dist/server/app-page-element-builder.d.ts +2 -0
  35. package/dist/server/app-page-element-builder.js +33 -7
  36. package/dist/server/app-page-head.d.ts +3 -3
  37. package/dist/server/app-page-head.js +58 -13
  38. package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
  39. package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
  40. package/dist/server/app-page-probe.d.ts +4 -0
  41. package/dist/server/app-page-probe.js +6 -2
  42. package/dist/server/app-page-request.d.ts +3 -0
  43. package/dist/server/app-page-request.js +1 -1
  44. package/dist/server/app-page-response.js +1 -1
  45. package/dist/server/app-page-route-wiring.d.ts +7 -0
  46. package/dist/server/app-page-route-wiring.js +211 -49
  47. package/dist/server/app-pages-bridge.d.ts +1 -1
  48. package/dist/server/app-pages-bridge.js +2 -1
  49. package/dist/server/app-route-handler-cache.js +4 -0
  50. package/dist/server/app-route-handler-dispatch.js +9 -3
  51. package/dist/server/app-route-handler-response.js +1 -1
  52. package/dist/server/app-route-module-loader.d.ts +6 -0
  53. package/dist/server/app-route-module-loader.js +3 -0
  54. package/dist/server/app-router-entry.js +6 -3
  55. package/dist/server/app-rsc-errors.js +10 -0
  56. package/dist/server/app-rsc-handler.js +21 -9
  57. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  58. package/dist/server/app-rsc-route-matching.js +6 -0
  59. package/dist/server/app-ssr-entry.js +4 -4
  60. package/dist/server/cache-control.d.ts +2 -2
  61. package/dist/server/cache-control.js +2 -1
  62. package/dist/server/dev-server.d.ts +3 -1
  63. package/dist/server/dev-server.js +145 -312
  64. package/dist/server/headers.d.ts +2 -2
  65. package/dist/server/headers.js +4 -3
  66. package/dist/server/isr-cache.d.ts +10 -33
  67. package/dist/server/isr-cache.js +33 -43
  68. package/dist/server/isr-decision.d.ts +2 -2
  69. package/dist/server/pages-api-route.d.ts +2 -0
  70. package/dist/server/pages-api-route.js +4 -1
  71. package/dist/server/pages-dev-hydration.js +0 -1
  72. package/dist/server/pages-get-initial-props.d.ts +1 -1
  73. package/dist/server/pages-get-initial-props.js +1 -4
  74. package/dist/server/pages-i18n.js +64 -16
  75. package/dist/server/pages-node-compat.d.ts +2 -0
  76. package/dist/server/pages-node-compat.js +7 -3
  77. package/dist/server/pages-page-data.d.ts +61 -9
  78. package/dist/server/pages-page-data.js +334 -74
  79. package/dist/server/pages-page-handler.d.ts +5 -1
  80. package/dist/server/pages-page-handler.js +115 -19
  81. package/dist/server/pages-page-response.d.ts +5 -3
  82. package/dist/server/pages-page-response.js +23 -26
  83. package/dist/server/pages-request-pipeline.d.ts +7 -0
  84. package/dist/server/pages-request-pipeline.js +5 -1
  85. package/dist/server/pages-revalidate.d.ts +1 -1
  86. package/dist/server/pages-revalidate.js +54 -8
  87. package/dist/server/pages-router-entry.d.ts +1 -0
  88. package/dist/server/pages-router-entry.js +10 -4
  89. package/dist/server/prod-server.d.ts +2 -1
  90. package/dist/server/prod-server.js +37 -13
  91. package/dist/server/revalidation-host.d.ts +11 -0
  92. package/dist/server/revalidation-host.js +19 -0
  93. package/dist/server/worker-revalidation-context.d.ts +13 -0
  94. package/dist/server/worker-revalidation-context.js +29 -0
  95. package/dist/shims/cache-handler.d.ts +1 -1
  96. package/dist/shims/cache-handler.js +10 -7
  97. package/dist/shims/cache-runtime.d.ts +6 -0
  98. package/dist/shims/cache-runtime.js +4 -1
  99. package/dist/shims/cache.js +1 -1
  100. package/dist/shims/error.js +1 -1
  101. package/dist/shims/fetch-cache.js +1 -1
  102. package/dist/shims/font-google-base.js +17 -8
  103. package/dist/shims/font-local.js +41 -10
  104. package/dist/shims/headers.d.ts +10 -13
  105. package/dist/shims/headers.js +85 -36
  106. package/dist/shims/internal/cookie-serialize.d.ts +10 -9
  107. package/dist/shims/internal/cookie-serialize.js +9 -8
  108. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  109. package/dist/shims/internal/interpolate-as.js +27 -2
  110. package/dist/shims/link.js +18 -6
  111. package/dist/shims/metadata.d.ts +7 -17
  112. package/dist/shims/metadata.js +74 -24
  113. package/dist/shims/navigation.js +1 -1
  114. package/dist/shims/request-context.d.ts +4 -1
  115. package/dist/shims/router.js +18 -7
  116. package/dist/shims/script.js +1 -1
  117. package/dist/shims/server.d.ts +7 -6
  118. package/dist/shims/server.js +156 -57
  119. package/dist/shims/unified-request-context.d.ts +21 -2
  120. package/dist/shims/unified-request-context.js +82 -1
  121. package/dist/utils/cache-control-metadata.d.ts +2 -1
  122. package/dist/utils/cache-control-metadata.js +5 -1
  123. package/dist/utils/html-limited-bots.js +1 -1
  124. package/dist/utils/protocol-headers.d.ts +7 -1
  125. package/dist/utils/protocol-headers.js +7 -1
  126. package/package.json +1 -1
@@ -9,9 +9,9 @@ import { AppRouterContext } from "./internal/app-router-context.js";
9
9
  import { resolveDirectHybridClientRouteOwner } from "./internal/hybrid-client-route-owner-direct.js";
10
10
  import { toBrowserNavigationHref, toSameOriginAppPath, withBasePath } from "./url-utils.js";
11
11
  import { retryScrollTo, scrollToHashTarget } from "./hash-scroll.js";
12
+ import { isBotUserAgent } from "../utils/html-limited-bots.js";
12
13
  import { GLOBAL_ACCESSORS_KEY, ServerInsertedHTMLContext, _registerStateAccessors, clearClientHydrationContext, clearServerInsertedHTML, flushServerInsertedHTML, getBfcacheIdMapContext, getBfcacheSegmentIdContext, getLayoutSegmentContext, getNavigationContext, registerServerInsertedHTMLCallback, renderServerInsertedHTML, setNavigationContext } from "./navigation-context-state.js";
13
14
  import { BailoutToCSRError, DynamicServerError, HTTP_ERROR_FALLBACK_ERROR_CODE, RedirectType, decodeRedirectError, forbidden, getAccessFallbackHTTPStatus, isBailoutToCSRError, isDynamicServerError, isHTTPAccessFallbackError, isNextRouterError, isRedirectError, notFound, permanentRedirect, redirect, unauthorized, unstable_rethrow } from "./navigation-errors.js";
14
- import { isBotUserAgent } from "../utils/html-limited-bots.js";
15
15
  import { clearAppNavigationFailureTarget, stageAppNavigationFailureTarget } from "../client/app-nav-failure-handler.js";
16
16
  import { beginAppRouterScrollIntent, clearAppRouterScrollIntent, consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent } from "./app-router-scroll-state.js";
17
17
  import { VINEXT_RSC_COMPATIBILITY_ID_HEADER, createRscRequestHeaders, createRscRequestUrl, stripRscCacheBustingSearchParam, stripRscSuffix } from "../server/app-rsc-cache-busting.js";
@@ -33,7 +33,10 @@ type ExecutionContextLike = {
33
33
  * execution context. Typed as `unknown` to keep this module runtime-agnostic;
34
34
  * CDN cache adapters that know the concrete shape narrow it themselves.
35
35
  */
36
- cache?: unknown;
36
+ cache?: unknown; /** Server-owned origin for credential-bearing Pages revalidation loopbacks. */
37
+ trustedRevalidateOrigin?: string; /** Worker-owned in-process dispatcher for authenticated Pages revalidation. */
38
+ dispatchPagesRevalidate?: (request: Request) => Promise<Response>; /** Marks a request currently executing through the internal revalidation dispatcher. */
39
+ isInternalPagesRevalidation?: boolean;
37
40
  };
38
41
  /**
39
42
  * Run `fn` with the given `ExecutionContext` available via
@@ -1159,13 +1159,12 @@ async function renderPagesNavigationTarget(url, target, props, options, assertSt
1159
1159
  assertStillCurrent();
1160
1160
  const React = (await import("react")).default;
1161
1161
  assertStillCurrent();
1162
- const rawPageProps = props.pageProps;
1163
- const pageProps = isUnknownRecord(rawPageProps) ? rawPageProps : {};
1162
+ const pageProps = Object.assign({}, props.pageProps);
1163
+ props.pageProps = pageProps;
1164
1164
  let element;
1165
1165
  if (AppComponent) element = React.createElement(AppComponent, {
1166
1166
  ...props,
1167
1167
  Component: PageComponent,
1168
- pageProps: rawPageProps,
1169
1168
  router: singletonRouter
1170
1169
  });
1171
1170
  else element = React.createElement(PageComponent, pageProps);
@@ -1312,8 +1311,8 @@ async function navigateClientHtml(url, fetchUrl, controller, navId, assertStillC
1312
1311
  if (!nextDataJson) scheduleHardNavigationAndThrow(url, "Navigation failed: missing __NEXT_DATA__ in response");
1313
1312
  const nextData = parseVinextNextDataJson(nextDataJson);
1314
1313
  const props = nextData.props && typeof nextData.props === "object" ? nextData.props : {};
1315
- const rawPageProps = props.pageProps;
1316
- const pageProps = isUnknownRecord(rawPageProps) ? rawPageProps : {};
1314
+ const pageProps = Object.assign({}, props.pageProps);
1315
+ props.pageProps = pageProps;
1317
1316
  let pageModuleUrl = nextData.__vinext?.pageModuleUrl;
1318
1317
  if (!pageModuleUrl) {
1319
1318
  const moduleMatch = html.match(/import\("([^"]+)"\);\s*\n\s*const PageComponent/);
@@ -1355,7 +1354,6 @@ async function navigateClientHtml(url, fetchUrl, controller, navId, assertStillC
1355
1354
  if (AppComponent) element = React.createElement(AppComponent, {
1356
1355
  ...props,
1357
1356
  Component: PageComponent,
1358
- pageProps: rawPageProps,
1359
1357
  router: singletonRouter
1360
1358
  });
1361
1359
  else element = React.createElement(PageComponent, pageProps);
@@ -1647,7 +1645,20 @@ async function performNavigation(url, as, options, mode, onStateUpdate) {
1647
1645
  const hashStr = extractHash(resolved);
1648
1646
  resolved = normalizePathTrailingSlash(`${projection.href.split(/[?#]/, 1)[0]}${searchStr ? `?${searchStr}` : ""}${hashStr}`, __trailingSlash);
1649
1647
  }
1650
- } else interpolatedRoute = resolved;
1648
+ } else {
1649
+ const missingParams = projection ? routePatternParts(projection.routePathname).filter((part) => part.startsWith(":") && !part.endsWith("*")).map((part) => part.slice(1, part.endsWith("+") ? -1 : void 0)).filter((paramName) => {
1650
+ const value = projection.query[paramName];
1651
+ return value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
1652
+ }) : [];
1653
+ const hasExplicitHrefPathname = typeof url === "string" || url.pathname !== void 0;
1654
+ const isMiddlewareMatch = options?.shallow !== true && getPagesMiddlewareDataHref(resolved, __basePath) !== null;
1655
+ if (missingParams.length > 0 && hasExplicitHrefPathname && !isMiddlewareMatch) {
1656
+ const asPathname = stripHash(resolved).split("?", 1)[0];
1657
+ const routePathname = projection?.routePathname ?? stripHash(resolvedRoute).split("?", 1)[0];
1658
+ throw new HrefInterpolationError(asPathname === routePathname ? `The provided \`href\` (${resolvedRoute}) value is missing query values (${missingParams.join(", ")}) to be interpolated properly. Read more: https://nextjs.org/docs/messages/href-interpolation-failed` : `The provided \`as\` value (${asPathname}) is incompatible with the \`href\` value (${routePathname}). Read more: https://nextjs.org/docs/messages/incompatible-href-as`);
1659
+ }
1660
+ interpolatedRoute = resolved;
1661
+ }
1651
1662
  }
1652
1663
  const full = normalizePathTrailingSlash(toBrowserNavigationHref(resolved, window.location.href, __basePath), __trailingSlash);
1653
1664
  const fullRouteUrl = interpolatedRoute !== resolved ? normalizePathTrailingSlash(toBrowserNavigationHref(interpolatedRoute, window.location.href, __basePath), __trailingSlash) : full;
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { escapeInlineContent } from "./head.js";
3
2
  import { useScriptNonce } from "./script-nonce-context.js";
4
3
  import { hasAppNavigationRuntimeBootstrap } from "../client/navigation-runtime.js";
5
4
  import { useBeforeInteractiveRegister } from "./before-interactive-context.js";
5
+ import { escapeInlineContent } from "./head.js";
6
6
  import React, { useEffect, useRef } from "react";
7
7
  import * as ReactDOM from "react-dom";
8
8
  //#region src/shims/script.tsx
@@ -230,15 +230,16 @@ declare class ResponseCookies {
230
230
  }]): this;
231
231
  get(...args: [name: string] | [options: {
232
232
  name: string;
233
- }]): CookieEntry | undefined;
233
+ }]): ResponseCookieEntry | undefined;
234
234
  has(name: string): boolean;
235
235
  getAll(...args: [name: string] | [options: {
236
236
  name: string;
237
- }] | []): CookieEntry[];
237
+ }] | []): ResponseCookieEntry[];
238
238
  delete(...args: [name: string] | [options: Omit<CookieOptions & {
239
239
  name: string;
240
240
  }, "maxAge" | "expires">]): this;
241
- [Symbol.iterator](): MapIterator<[string, CookieEntry]>;
241
+ [Symbol.iterator](): MapIterator<[string, ResponseCookieEntry]>;
242
+ toString(): string;
242
243
  /** Delete all Set-Cookie headers and re-append from the internal map. */
243
244
  private _syncHeaders;
244
245
  }
@@ -249,10 +250,11 @@ type CookieOptions = {
249
250
  expires?: Date | number;
250
251
  httpOnly?: boolean;
251
252
  secure?: boolean;
252
- sameSite?: true | false | "strict" | "lax" | "none" | "Strict" | "Lax" | "None";
253
+ sameSite?: true | false | "strict" | "lax" | "none";
253
254
  partitioned?: boolean;
254
255
  priority?: "low" | "medium" | "high";
255
256
  };
257
+ type ResponseCookieEntry = CookieEntry & CookieOptions;
256
258
  type MiddlewareResponseInit = {
257
259
  request?: {
258
260
  headers?: Headers;
@@ -323,8 +325,7 @@ type UserAgent = {
323
325
  *
324
326
  * Uses the platform's `waitUntil` (via the per-request ExecutionContext) when
325
327
  * available so the task survives past the response on Cloudflare Workers.
326
- * Falls back to a fire-and-forget microtask on runtimes without an execution
327
- * context (e.g. Node.js dev server).
328
+ * Node.js dev drains callbacks from its response finish/close lifecycle.
328
329
  *
329
330
  * Throws when called inside a cached scope — request-specific
330
331
  * side-effects must not leak into cached results.
@@ -1,9 +1,10 @@
1
+ import { bindRequestContextSnapshot, getRequestContext, isInsideUnifiedScope, queueAfterCallback } from "./unified-request-context.js";
1
2
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
2
3
  import { getRequestExecutionContext } from "./request-context.js";
3
4
  import { MIDDLEWARE_SET_COOKIE_HEADER } from "../utils/protocol-headers.js";
4
5
  import { MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_REWRITE_HEADER } from "../server/headers.js";
5
6
  import { encodeMiddlewareRequestHeaders } from "../utils/middleware-request-headers.js";
6
- import { serializeSetCookie, validateCookieName } from "./internal/cookie-serialize.js";
7
+ import { validateCookieAttributeValue, validateCookieName } from "./internal/cookie-serialize.js";
7
8
  import { parseEdgeRequestCookieHeader } from "../utils/parse-cookie.js";
8
9
  import { assertSafeNavigationUrl } from "./url-safety.js";
9
10
  //#region src/shims/server.ts
@@ -158,12 +159,8 @@ var NextResponse = class NextResponse extends Response {
158
159
  * Create a JSON response.
159
160
  */
160
161
  static json(body, init) {
161
- const headers = new Headers(init?.headers);
162
- if (!headers.has("content-type")) headers.set("content-type", "application/json");
163
- return new NextResponse(JSON.stringify(body), {
164
- ...init,
165
- headers
166
- });
162
+ const response = Response.json(body, init);
163
+ return new NextResponse(response.body, response);
167
164
  }
168
165
  /**
169
166
  * Create a redirect response.
@@ -417,7 +414,7 @@ var NextURL = class NextURL {
417
414
  };
418
415
  if (this._trailingSlash) nextConfig.trailingSlash = true;
419
416
  const config = {
420
- basePath: this._basePath,
417
+ basePath: this._configBasePath,
421
418
  nextConfig: Object.keys(nextConfig).length > 0 ? nextConfig : void 0
422
419
  };
423
420
  return new NextURL(this.href, void 0, config);
@@ -554,79 +551,54 @@ var ResponseCookies = class {
554
551
  _parsed = /* @__PURE__ */ new Map();
555
552
  constructor(headers) {
556
553
  this._headers = headers;
557
- for (const header of headers.getSetCookie()) {
558
- const eq = header.indexOf("=");
559
- if (eq === -1) continue;
560
- const cookieName = header.slice(0, eq);
561
- const semi = header.indexOf(";", eq);
562
- const raw = header.slice(eq + 1, semi === -1 ? void 0 : semi);
563
- let value;
564
- try {
565
- value = decodeURIComponent(raw);
566
- } catch {
567
- value = raw;
568
- }
569
- this._parsed.set(cookieName, {
570
- serialized: header,
571
- entry: {
572
- name: cookieName,
573
- value
574
- }
575
- });
554
+ const setCookie = headers.getSetCookie?.() ?? headers.get("set-cookie") ?? [];
555
+ const cookieStrings = Array.isArray(setCookie) ? setCookie : splitSetCookieString(setCookie);
556
+ for (const header of cookieStrings) {
557
+ const entry = parseSetCookieHeader(header);
558
+ if (entry) this._parsed.set(entry.name, entry);
576
559
  }
577
560
  }
578
561
  set(...args) {
579
562
  const [name, value, opts] = parseCookieSetArgs(args);
580
563
  validateCookieName(name);
581
- const sameSite = opts?.sameSite === true ? "Strict" : typeof opts?.sameSite === "string" ? opts.sameSite[0].toUpperCase() + opts.sameSite.slice(1) : void 0;
582
- const serialized = serializeSetCookie(name, value, {
583
- ...opts,
584
- expires: typeof opts?.expires === "number" ? new Date(opts.expires) : opts?.expires,
585
- sameSite
586
- });
587
- this._parsed.set(name, {
588
- serialized,
589
- entry: {
590
- name,
591
- value
592
- }
593
- });
564
+ const entry = normalizeResponseCookie(name, value, opts);
565
+ validateResponseCookieAttributes(entry);
566
+ this._parsed.set(name, entry);
594
567
  this._syncHeaders();
595
568
  return this;
596
569
  }
597
570
  get(...args) {
598
571
  const key = typeof args[0] === "string" ? args[0] : args[0].name;
599
- return this._parsed.get(key)?.entry;
572
+ return this._parsed.get(key);
600
573
  }
601
574
  has(name) {
602
575
  return this._parsed.has(name);
603
576
  }
604
577
  getAll(...args) {
605
- const all = [...this._parsed.values()].map((v) => v.entry);
578
+ const all = [...this._parsed.values()];
606
579
  if (args.length === 0) return all;
607
580
  const key = typeof args[0] === "string" ? args[0] : args[0].name;
608
581
  return all.filter((c) => c.name === key);
609
582
  }
610
583
  delete(...args) {
611
- const [name, opts] = typeof args[0] === "string" ? [args[0], void 0] : [args[0].name, args[0]];
584
+ const [name, options] = typeof args[0] === "string" ? [args[0], void 0] : [args[0].name, args[0]];
612
585
  return this.set({
586
+ ...options,
613
587
  name,
614
588
  value: "",
615
- expires: /* @__PURE__ */ new Date(0),
616
- path: opts?.path,
617
- domain: opts?.domain,
618
- httpOnly: opts?.httpOnly,
619
- secure: opts?.secure,
620
- sameSite: opts?.sameSite
589
+ expires: /* @__PURE__ */ new Date(0)
621
590
  });
622
591
  }
623
592
  [Symbol.iterator]() {
624
- return new Map([...this._parsed.values()].map(({ entry }) => [entry.name, entry])).entries();
593
+ return new Map([...this._parsed.values()].map((entry) => [entry.name, entry])).entries();
594
+ }
595
+ toString() {
596
+ return [...this._parsed.values()].map(stringifyResponseCookie).join("; ");
625
597
  }
626
598
  /** Delete all Set-Cookie headers and re-append from the internal map. */
627
599
  _syncHeaders() {
628
600
  this._headers.delete("Set-Cookie");
629
- for (const { serialized } of this._parsed.values()) this._headers.append("Set-Cookie", serialized);
601
+ for (const entry of this._parsed.values()) this._headers.append("Set-Cookie", stringifyResponseCookie(entry));
630
602
  }
631
603
  };
632
604
  var MiddlewareResponseCookies = class extends ResponseCookies {
@@ -654,6 +626,115 @@ var MiddlewareResponseCookies = class extends ResponseCookies {
654
626
  this._responseHeaders.set(MIDDLEWARE_SET_COOKIE_HEADER, cookies.join(","));
655
627
  }
656
628
  };
629
+ function normalizeResponseCookie(name, value, options) {
630
+ const cookie = {
631
+ name,
632
+ value,
633
+ ...options
634
+ };
635
+ if (typeof cookie.expires === "number") cookie.expires = new Date(cookie.expires);
636
+ if (cookie.maxAge) cookie.expires = new Date(Date.now() + cookie.maxAge * 1e3);
637
+ if (cookie.path == null) cookie.path = "/";
638
+ return cookie;
639
+ }
640
+ function parseSetCookieHeader(header) {
641
+ if (!header) return void 0;
642
+ const [[name, value], ...attributes] = parseResponseCookiePairs(header);
643
+ const { domain, expires, httponly, maxage, path, samesite, secure, partitioned, priority } = Object.fromEntries(attributes.map(([key, attributeValue]) => [key.toLowerCase().replaceAll("-", ""), attributeValue]));
644
+ return compactResponseCookie({
645
+ name,
646
+ value: decodeURIComponent(value),
647
+ domain,
648
+ ...expires && { expires: new Date(expires) },
649
+ ...httponly && { httpOnly: true },
650
+ ...typeof maxage === "string" && { maxAge: Number(maxage) },
651
+ path,
652
+ ...samesite && { sameSite: parseResponseCookieSameSite(samesite) },
653
+ ...secure && { secure: true },
654
+ ...priority && { priority: parseResponseCookiePriority(priority) },
655
+ ...partitioned && { partitioned: true }
656
+ });
657
+ }
658
+ function parseResponseCookiePairs(cookie) {
659
+ const map = /* @__PURE__ */ new Map();
660
+ for (const pair of cookie.split(/; */)) {
661
+ if (!pair) continue;
662
+ const splitAt = pair.indexOf("=");
663
+ if (splitAt === -1) {
664
+ map.set(pair, "true");
665
+ continue;
666
+ }
667
+ const key = pair.slice(0, splitAt);
668
+ const value = pair.slice(splitAt + 1);
669
+ try {
670
+ map.set(key, decodeURIComponent(value ?? "true"));
671
+ } catch {}
672
+ }
673
+ return map;
674
+ }
675
+ function compactResponseCookie(cookie) {
676
+ const compact = {};
677
+ for (const key in cookie) if (cookie[key]) compact[key] = cookie[key];
678
+ return compact;
679
+ }
680
+ function parseResponseCookieSameSite(value) {
681
+ const sameSite = value.toLowerCase();
682
+ return sameSite === "strict" || sameSite === "lax" || sameSite === "none" ? sameSite : void 0;
683
+ }
684
+ function parseResponseCookiePriority(value) {
685
+ const priority = value.toLowerCase();
686
+ return priority === "low" || priority === "medium" || priority === "high" ? priority : void 0;
687
+ }
688
+ function validateResponseCookieAttributes(cookie) {
689
+ if (cookie.path) validateCookieAttributeValue(cookie.path, "Path");
690
+ if (cookie.domain) validateCookieAttributeValue(cookie.domain, "Domain");
691
+ }
692
+ function stringifyResponseCookie(cookie) {
693
+ const attributes = [
694
+ cookie.path && `Path=${cookie.path}`,
695
+ (cookie.expires || cookie.expires === 0) && `Expires=${(typeof cookie.expires === "number" ? new Date(cookie.expires) : cookie.expires).toUTCString()}`,
696
+ typeof cookie.maxAge === "number" && `Max-Age=${cookie.maxAge}`,
697
+ cookie.domain && `Domain=${cookie.domain}`,
698
+ cookie.secure && "Secure",
699
+ cookie.httpOnly && "HttpOnly",
700
+ cookie.sameSite && `SameSite=${cookie.sameSite}`,
701
+ cookie.partitioned && "Partitioned",
702
+ cookie.priority && `Priority=${cookie.priority}`
703
+ ].filter(Boolean);
704
+ const stringified = `${cookie.name}=${encodeURIComponent(cookie.value ?? "")}`;
705
+ return attributes.length === 0 ? stringified : `${stringified}; ${attributes.join("; ")}`;
706
+ }
707
+ function splitSetCookieString(value) {
708
+ const cookies = [];
709
+ let position = 0;
710
+ let start = 0;
711
+ const skipWhitespace = () => {
712
+ while (position < value.length && /\s/.test(value.charAt(position))) position++;
713
+ return position < value.length;
714
+ };
715
+ while (position < value.length) {
716
+ start = position;
717
+ let separatorFound = false;
718
+ while (skipWhitespace()) {
719
+ if (value.charAt(position) !== ",") {
720
+ position++;
721
+ continue;
722
+ }
723
+ const lastComma = position++;
724
+ skipWhitespace();
725
+ const nextStart = position;
726
+ while (position < value.length && !"=;,".includes(value.charAt(position))) position++;
727
+ if (position < value.length && value.charAt(position) === "=") {
728
+ separatorFound = true;
729
+ position = nextStart;
730
+ cookies.push(value.substring(start, lastComma));
731
+ start = position;
732
+ } else position = lastComma + 1;
733
+ }
734
+ if (!separatorFound || position >= value.length) cookies.push(value.substring(start));
735
+ }
736
+ return cookies;
737
+ }
657
738
  /**
658
739
  * Parse the overloaded arguments for ResponseCookies.set():
659
740
  * - (name, value, options?) — positional form
@@ -730,18 +811,36 @@ function userAgent({ headers }) {
730
811
  *
731
812
  * Uses the platform's `waitUntil` (via the per-request ExecutionContext) when
732
813
  * available so the task survives past the response on Cloudflare Workers.
733
- * Falls back to a fire-and-forget microtask on runtimes without an execution
734
- * context (e.g. Node.js dev server).
814
+ * Node.js dev drains callbacks from its response finish/close lifecycle.
735
815
  *
736
816
  * Throws when called inside a cached scope — request-specific
737
817
  * side-effects must not leak into cached results.
738
818
  */
739
819
  function after(task) {
740
820
  _throwIfInsideCacheScope("after()");
741
- const guarded = (typeof task === "function" ? Promise.resolve().then(task) : task).catch((err) => {
742
- console.error("[vinext] after() task failed:", err);
743
- });
744
- getRequestExecutionContext()?.waitUntil(guarded);
821
+ const requestContext = isInsideUnifiedScope() ? getRequestContext() : null;
822
+ if (!requestContext) {
823
+ const executionContext = getRequestExecutionContext();
824
+ if (executionContext) {
825
+ if (typeof task !== "function" && (task == null || typeof task.then !== "function")) throw new TypeError("`after()`: Argument must be a promise or a function");
826
+ const promise = typeof task === "function" ? Promise.resolve().then(task) : task;
827
+ const guarded = Promise.resolve(promise).catch((error) => {
828
+ console.error("[vinext] after() task failed:", error);
829
+ });
830
+ executionContext.waitUntil(guarded);
831
+ return;
832
+ }
833
+ throw new Error("`after()` was called outside a request scope");
834
+ }
835
+ if (typeof task !== "function") {
836
+ if (task == null || typeof task.then !== "function") throw new TypeError("`after()`: Argument must be a promise or a function");
837
+ const guarded = Promise.resolve(task).catch((error) => {
838
+ console.error("[vinext] after() task failed:", error);
839
+ });
840
+ getRequestExecutionContext()?.waitUntil(guarded);
841
+ return;
842
+ }
843
+ queueAfterCallback(requestContext, bindRequestContextSnapshot(requestContext, task));
745
844
  }
746
845
  /**
747
846
  * connection() — signals that the response requires a live connection
@@ -18,13 +18,32 @@ import { NavigationState } from "./navigation-state.js";
18
18
  */
19
19
  type UnifiedRequestContext = {
20
20
  /** Cloudflare Workers ExecutionContext, or null on Node.js dev. */executionContext: ExecutionContextLike | null; /** Per-request cache for cacheForRequest(). Keyed by factory function reference. */
21
- requestCache: WeakMap<(...args: any[]) => any, unknown>;
21
+ requestCache: WeakMap<(...args: any[]) => any, unknown>; /** Shared lifecycle state for work deferred until the response closes. */
22
+ afterContext: AfterRequestContext;
22
23
  } & VinextHeadersShimState & I18nState & NavigationState & CacheState & PrivateCacheState & FetchCacheState & RouterState & HeadState & RootParamsState;
24
+ type AfterRequestContext = {
25
+ callbacks: Array<() => unknown>;
26
+ responseClosed: boolean;
27
+ pendingCallbacks: number;
28
+ completion: Promise<void> | null;
29
+ resolveCompletion: (() => void) | null;
30
+ };
23
31
  /**
24
32
  * Create a fresh `UnifiedRequestContext` with defaults for all fields.
25
33
  * Pass partial overrides for the fields you need to pre-populate.
26
34
  */
27
35
  declare function createRequestContext(opts?: Partial<UnifiedRequestContext>): UnifiedRequestContext;
36
+ /** Queue a callback until response close, or start it immediately once closed. */
37
+ declare function queueAfterCallback(ctx: UnifiedRequestContext, callback: () => unknown): void;
38
+ /** Bind a callback to every AsyncLocalStorage context active at registration. */
39
+ declare function bindRequestContextSnapshot<T>(ctx: UnifiedRequestContext, callback: () => T): () => T;
40
+ /**
41
+ * Release function-form `after()` work once the response body has closed.
42
+ * All queued callbacks start together, matching Next.js' unbounded PromiseQueue.
43
+ */
44
+ declare function closeAfterResponse(ctx: UnifiedRequestContext): Promise<void>;
45
+ /** Wrap a response so deferred callbacks start on stream completion or cancellation. */
46
+ declare function closeAfterResponseWithBody(response: Response, ctx: UnifiedRequestContext): Response;
28
47
  /**
29
48
  * Run `fn` within a unified request context scope.
30
49
  * All shim modules will read/write their state from `ctx` for the
@@ -65,4 +84,4 @@ declare function getRequestContext(): UnifiedRequestContext;
65
84
  */
66
85
  declare function isInsideUnifiedScope(): boolean;
67
86
  //#endregion
68
- export { UnifiedRequestContext, createRequestContext, getRequestContext, isInsideUnifiedScope, runWithRequestContext, runWithUnifiedStateMutation };
87
+ export { AfterRequestContext, UnifiedRequestContext, bindRequestContextSnapshot, closeAfterResponse, closeAfterResponseWithBody, createRequestContext, getRequestContext, isInsideUnifiedScope, queueAfterCallback, runWithRequestContext, runWithUnifiedStateMutation };
@@ -43,6 +43,13 @@ function createRequestContext(opts) {
43
43
  currentFetchDedupeEntries: /* @__PURE__ */ new Map(),
44
44
  executionContext: _getInheritedExecutionContext(),
45
45
  requestCache: /* @__PURE__ */ new WeakMap(),
46
+ afterContext: {
47
+ callbacks: [],
48
+ responseClosed: false,
49
+ pendingCallbacks: 0,
50
+ completion: null,
51
+ resolveCompletion: null
52
+ },
46
53
  ssrContext: null,
47
54
  ssrHeadChildren: [],
48
55
  documentInitialHead: [],
@@ -50,6 +57,80 @@ function createRequestContext(opts) {
50
57
  ...opts
51
58
  };
52
59
  }
60
+ function ensureAfterCompletion(ctx) {
61
+ const state = ctx.afterContext;
62
+ if (state.resolveCompletion && state.completion) return;
63
+ let resolveCompletion;
64
+ const completion = new Promise((resolve) => {
65
+ resolveCompletion = resolve;
66
+ });
67
+ state.completion = completion;
68
+ state.resolveCompletion = resolveCompletion;
69
+ ctx.executionContext?.waitUntil(completion);
70
+ }
71
+ function finishAfterCallbacksIfIdle(ctx) {
72
+ const state = ctx.afterContext;
73
+ if (!state.responseClosed || state.pendingCallbacks !== 0 || state.callbacks.length !== 0 || !state.resolveCompletion) return;
74
+ const resolveCompletion = state.resolveCompletion;
75
+ state.resolveCompletion = null;
76
+ resolveCompletion();
77
+ }
78
+ function startAfterCallback(ctx, callback) {
79
+ const state = ctx.afterContext;
80
+ ensureAfterCompletion(ctx);
81
+ state.pendingCallbacks += 1;
82
+ Promise.resolve().then(callback).catch((error) => {
83
+ console.error("[vinext] after() task failed:", error);
84
+ }).finally(() => {
85
+ state.pendingCallbacks -= 1;
86
+ finishAfterCallbacksIfIdle(ctx);
87
+ });
88
+ }
89
+ /** Queue a callback until response close, or start it immediately once closed. */
90
+ function queueAfterCallback(ctx, callback) {
91
+ ensureAfterCompletion(ctx);
92
+ if (ctx.afterContext.responseClosed) startAfterCallback(ctx, callback);
93
+ else ctx.afterContext.callbacks.push(callback);
94
+ }
95
+ /** Bind a callback to every AsyncLocalStorage context active at registration. */
96
+ function bindRequestContextSnapshot(ctx, callback) {
97
+ const constructor = _als.constructor;
98
+ try {
99
+ const runInSnapshot = constructor.snapshot?.();
100
+ if (runInSnapshot) return () => runInSnapshot(callback);
101
+ } catch {}
102
+ return () => _als.run(ctx, callback);
103
+ }
104
+ /**
105
+ * Release function-form `after()` work once the response body has closed.
106
+ * All queued callbacks start together, matching Next.js' unbounded PromiseQueue.
107
+ */
108
+ async function closeAfterResponse(ctx) {
109
+ const state = ctx.afterContext;
110
+ if (!state.responseClosed) {
111
+ state.responseClosed = true;
112
+ const callbacks = state.callbacks.splice(0);
113
+ for (const callback of callbacks) startAfterCallback(ctx, callback);
114
+ finishAfterCallbacksIfIdle(ctx);
115
+ }
116
+ return state.completion ?? Promise.resolve();
117
+ }
118
+ /** Wrap a response so deferred callbacks start on stream completion or cancellation. */
119
+ function closeAfterResponseWithBody(response, ctx) {
120
+ if (!response.body) {
121
+ queueMicrotask(() => void closeAfterResponse(ctx));
122
+ return response;
123
+ }
124
+ const passthrough = new TransformStream();
125
+ response.body.pipeTo(passthrough.writable).then(() => void closeAfterResponse(ctx), () => void closeAfterResponse(ctx));
126
+ const wrapped = new Response(passthrough.readable, {
127
+ status: response.status,
128
+ statusText: response.statusText,
129
+ headers: response.headers
130
+ });
131
+ wrapped.__vinextStreamedHtmlResponse = response.__vinextStreamedHtmlResponse;
132
+ return wrapped;
133
+ }
53
134
  function runWithRequestContext(ctx, fn) {
54
135
  return _als.run(ctx, fn);
55
136
  }
@@ -86,4 +167,4 @@ function isInsideUnifiedScope() {
86
167
  return _als.getStore() != null;
87
168
  }
88
169
  //#endregion
89
- export { createRequestContext, getRequestContext, isInsideUnifiedScope, runWithRequestContext, runWithUnifiedStateMutation };
170
+ export { bindRequestContextSnapshot, closeAfterResponse, closeAfterResponseWithBody, createRequestContext, getRequestContext, isInsideUnifiedScope, queueAfterCallback, runWithRequestContext, runWithUnifiedStateMutation };
@@ -1,4 +1,5 @@
1
1
  //#region src/utils/cache-control-metadata.d.ts
2
2
  declare function readCacheControlNumberField(ctx: Record<string, unknown> | undefined, field: string): number | undefined;
3
+ declare function readCacheControlRevalidateField(ctx: Record<string, unknown> | undefined): number | false | undefined;
3
4
  //#endregion
4
- export { readCacheControlNumberField };
5
+ export { readCacheControlNumberField, readCacheControlRevalidateField };
@@ -8,5 +8,9 @@ function readCacheControlNumberField(ctx, field) {
8
8
  const value = readRecordField(ctx, "cacheControl")?.[field] ?? ctx?.[field];
9
9
  return typeof value === "number" ? value : void 0;
10
10
  }
11
+ function readCacheControlRevalidateField(ctx) {
12
+ const value = readRecordField(ctx, "cacheControl")?.revalidate ?? ctx?.revalidate;
13
+ return typeof value === "number" || value === false ? value : void 0;
14
+ }
11
15
  //#endregion
12
- export { readCacheControlNumberField };
16
+ export { readCacheControlNumberField, readCacheControlRevalidateField };
@@ -1,5 +1,5 @@
1
1
  //#region src/utils/html-limited-bots.ts
2
- const HTML_LIMITED_BOT_UA_RE_STRING = String.raw`[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight`;
2
+ const HTML_LIMITED_BOT_UA_RE_STRING = String.raw`[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|meta-externalagent|meta-externalfetcher|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight`;
3
3
  const HEADLESS_BROWSER_BOT_UA_RE = /Googlebot(?!-)|Googlebot$/i;
4
4
  const htmlLimitedBotRegexCache = /* @__PURE__ */ new Map();
5
5
  function getHtmlLimitedBotRegex(htmlLimitedBots) {
@@ -7,6 +7,8 @@ declare const VINEXT_PRERENDER_SECRET_HEADER = "x-vinext-prerender-secret";
7
7
  declare const VINEXT_PRERENDER_ROUTE_PARAMS_HEADER = "x-vinext-prerender-route-params";
8
8
  /** Indicates a build-time prerender render is probing whether a route can be static. */
9
9
  declare const VINEXT_PRERENDER_SPECULATIVE_HEADER = "x-vinext-prerender-speculative";
10
+ /** Logical hostname carried only by authenticated Node revalidation loopbacks. */
11
+ declare const VINEXT_REVALIDATE_HOST_HEADER = "x-vinext-revalidate-host";
10
12
  /** Prefix for forwarded request headers (e.g. `x-middleware-request-cookie`). */
11
13
  declare const MIDDLEWARE_REQUEST_HEADER_PREFIX = "x-middleware-request-";
12
14
  /** Comma-separated list of header names that middleware wants to override. */
@@ -19,5 +21,9 @@ declare const MIDDLEWARE_CACHE_HEADER = "x-middleware-cache";
19
21
  declare const MIDDLEWARE_SKIP_HEADER = "x-middleware-skip";
20
22
  /** Generic prefix for all middleware internal headers. */
21
23
  declare const MIDDLEWARE_HEADER_PREFIX = "x-middleware-";
24
+ /** Authenticates an internal Pages Router on-demand revalidation request. */
25
+ declare const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
26
+ /** Restricts on-demand revalidation to paths that were already generated. */
27
+ declare const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = "x-prerender-revalidate-if-generated";
22
28
  //#endregion
23
- export { MIDDLEWARE_CACHE_HEADER, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_SET_COOKIE_HEADER, MIDDLEWARE_SKIP_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER };
29
+ export { MIDDLEWARE_CACHE_HEADER, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_SET_COOKIE_HEADER, MIDDLEWARE_SKIP_HEADER, PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER, VINEXT_REVALIDATE_HOST_HEADER };
@@ -7,6 +7,8 @@ const VINEXT_PRERENDER_SECRET_HEADER = "x-vinext-prerender-secret";
7
7
  const VINEXT_PRERENDER_ROUTE_PARAMS_HEADER = "x-vinext-prerender-route-params";
8
8
  /** Indicates a build-time prerender render is probing whether a route can be static. */
9
9
  const VINEXT_PRERENDER_SPECULATIVE_HEADER = "x-vinext-prerender-speculative";
10
+ /** Logical hostname carried only by authenticated Node revalidation loopbacks. */
11
+ const VINEXT_REVALIDATE_HOST_HEADER = "x-vinext-revalidate-host";
10
12
  /** Prefix for forwarded request headers (e.g. `x-middleware-request-cookie`). */
11
13
  const MIDDLEWARE_REQUEST_HEADER_PREFIX = "x-middleware-request-";
12
14
  /** Comma-separated list of header names that middleware wants to override. */
@@ -19,5 +21,9 @@ const MIDDLEWARE_CACHE_HEADER = "x-middleware-cache";
19
21
  const MIDDLEWARE_SKIP_HEADER = "x-middleware-skip";
20
22
  /** Generic prefix for all middleware internal headers. */
21
23
  const MIDDLEWARE_HEADER_PREFIX = "x-middleware-";
24
+ /** Authenticates an internal Pages Router on-demand revalidation request. */
25
+ const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
26
+ /** Restricts on-demand revalidation to paths that were already generated. */
27
+ const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = "x-prerender-revalidate-if-generated";
22
28
  //#endregion
23
- export { MIDDLEWARE_CACHE_HEADER, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_SET_COOKIE_HEADER, MIDDLEWARE_SKIP_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER };
29
+ export { MIDDLEWARE_CACHE_HEADER, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_OVERRIDE_HEADERS, MIDDLEWARE_REQUEST_HEADER_PREFIX, MIDDLEWARE_SET_COOKIE_HEADER, MIDDLEWARE_SKIP_HEADER, PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER, VINEXT_REVALIDATE_HOST_HEADER };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinext",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "Run Next.js apps on Vite. Drop-in replacement for the next CLI.",
5
5
  "license": "MIT",
6
6
  "repository": {