vinext 0.1.0 → 0.1.2

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 (205) hide show
  1. package/README.md +2 -5
  2. package/dist/build/assets-ignore.d.ts +32 -0
  3. package/dist/build/assets-ignore.js +48 -0
  4. package/dist/build/client-build-config.d.ts +33 -1
  5. package/dist/build/client-build-config.js +66 -1
  6. package/dist/check.js +4 -3
  7. package/dist/cli.js +2 -0
  8. package/dist/client/navigation-runtime.d.ts +11 -2
  9. package/dist/client/navigation-runtime.js +1 -1
  10. package/dist/client/vinext-next-data.d.ts +2 -1
  11. package/dist/client/window-next.d.ts +6 -4
  12. package/dist/config/config-matchers.d.ts +31 -5
  13. package/dist/config/config-matchers.js +50 -3
  14. package/dist/config/next-config.d.ts +29 -3
  15. package/dist/config/next-config.js +32 -2
  16. package/dist/deploy.js +47 -304
  17. package/dist/entries/app-rsc-entry.d.ts +8 -2
  18. package/dist/entries/app-rsc-entry.js +61 -5
  19. package/dist/entries/app-rsc-manifest.js +20 -2
  20. package/dist/entries/pages-client-entry.js +1 -1
  21. package/dist/entries/pages-server-entry.js +16 -7
  22. package/dist/index.d.ts +0 -2
  23. package/dist/index.js +233 -280
  24. package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
  25. package/dist/plugins/dynamic-preload-metadata.js +415 -0
  26. package/dist/plugins/og-assets.js +2 -2
  27. package/dist/plugins/optimize-imports.d.ts +8 -4
  28. package/dist/plugins/optimize-imports.js +16 -12
  29. package/dist/plugins/postcss.js +18 -14
  30. package/dist/plugins/require-context.d.ts +6 -0
  31. package/dist/plugins/require-context.js +184 -0
  32. package/dist/plugins/sass.d.ts +53 -24
  33. package/dist/plugins/sass.js +249 -1
  34. package/dist/plugins/wasm-module-import.d.ts +15 -0
  35. package/dist/plugins/wasm-module-import.js +50 -0
  36. package/dist/routing/app-route-graph.d.ts +35 -2
  37. package/dist/routing/app-route-graph.js +179 -8
  38. package/dist/routing/file-matcher.js +1 -1
  39. package/dist/routing/route-pattern.d.ts +2 -1
  40. package/dist/routing/route-pattern.js +16 -1
  41. package/dist/server/api-handler.js +4 -0
  42. package/dist/server/app-browser-entry.js +155 -215
  43. package/dist/server/app-browser-error.d.ts +4 -1
  44. package/dist/server/app-browser-error.js +7 -1
  45. package/dist/server/app-browser-history-controller.d.ts +104 -0
  46. package/dist/server/app-browser-history-controller.js +210 -0
  47. package/dist/server/app-browser-interception-context.d.ts +2 -1
  48. package/dist/server/app-browser-interception-context.js +15 -2
  49. package/dist/server/app-browser-navigation-controller.d.ts +13 -2
  50. package/dist/server/app-browser-navigation-controller.js +83 -4
  51. package/dist/server/app-browser-popstate.d.ts +12 -3
  52. package/dist/server/app-browser-popstate.js +19 -4
  53. package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
  54. package/dist/server/app-browser-rsc-redirect.js +30 -8
  55. package/dist/server/app-browser-state.d.ts +3 -0
  56. package/dist/server/app-browser-state.js +10 -10
  57. package/dist/server/app-browser-visible-commit.js +10 -8
  58. package/dist/server/app-fallback-renderer.d.ts +2 -1
  59. package/dist/server/app-fallback-renderer.js +3 -1
  60. package/dist/server/app-history-state.d.ts +45 -1
  61. package/dist/server/app-history-state.js +109 -1
  62. package/dist/server/app-middleware.js +1 -0
  63. package/dist/server/app-optimistic-routing.js +22 -1
  64. package/dist/server/app-page-boundary-render.d.ts +2 -1
  65. package/dist/server/app-page-boundary-render.js +45 -21
  66. package/dist/server/app-page-cache.js +9 -7
  67. package/dist/server/app-page-dispatch.d.ts +14 -0
  68. package/dist/server/app-page-dispatch.js +21 -6
  69. package/dist/server/app-page-element-builder.d.ts +23 -2
  70. package/dist/server/app-page-element-builder.js +58 -17
  71. package/dist/server/app-page-execution.d.ts +1 -1
  72. package/dist/server/app-page-execution.js +32 -17
  73. package/dist/server/app-page-render.d.ts +7 -1
  74. package/dist/server/app-page-render.js +11 -16
  75. package/dist/server/app-page-request.d.ts +9 -6
  76. package/dist/server/app-page-request.js +14 -10
  77. package/dist/server/app-page-response.d.ts +2 -2
  78. package/dist/server/app-page-response.js +2 -2
  79. package/dist/server/app-page-route-wiring.d.ts +3 -1
  80. package/dist/server/app-page-route-wiring.js +10 -8
  81. package/dist/server/app-page-stream.d.ts +37 -7
  82. package/dist/server/app-page-stream.js +36 -6
  83. package/dist/server/app-pages-bridge.d.ts +16 -0
  84. package/dist/server/app-pages-bridge.js +23 -3
  85. package/dist/server/app-route-handler-cache.d.ts +1 -0
  86. package/dist/server/app-route-handler-cache.js +1 -0
  87. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  88. package/dist/server/app-route-handler-dispatch.js +2 -0
  89. package/dist/server/app-route-handler-execution.d.ts +1 -0
  90. package/dist/server/app-route-handler-execution.js +1 -0
  91. package/dist/server/app-route-handler-response.js +11 -10
  92. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  93. package/dist/server/app-route-handler-runtime.js +15 -3
  94. package/dist/server/app-rsc-handler.d.ts +1 -0
  95. package/dist/server/app-rsc-handler.js +5 -4
  96. package/dist/server/app-rsc-response-finalizer.js +1 -1
  97. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  98. package/dist/server/app-rsc-route-matching.js +29 -4
  99. package/dist/server/app-server-action-execution.d.ts +22 -1
  100. package/dist/server/app-server-action-execution.js +73 -12
  101. package/dist/server/app-ssr-entry.d.ts +6 -0
  102. package/dist/server/app-ssr-entry.js +19 -3
  103. package/dist/server/app-ssr-stream.js +9 -1
  104. package/dist/server/dev-lockfile.js +2 -1
  105. package/dist/server/dev-server.d.ts +1 -1
  106. package/dist/server/dev-server.js +97 -43
  107. package/dist/server/headers.d.ts +8 -1
  108. package/dist/server/headers.js +8 -1
  109. package/dist/server/instrumentation-runtime.d.ts +6 -0
  110. package/dist/server/instrumentation-runtime.js +8 -0
  111. package/dist/server/isr-cache.d.ts +37 -1
  112. package/dist/server/isr-cache.js +85 -1
  113. package/dist/server/isr-decision.d.ts +79 -0
  114. package/dist/server/isr-decision.js +70 -0
  115. package/dist/server/metadata-route-response.js +5 -3
  116. package/dist/server/middleware-runtime.d.ts +13 -0
  117. package/dist/server/middleware-runtime.js +11 -7
  118. package/dist/server/middleware.js +1 -0
  119. package/dist/server/navigation-planner.d.ts +62 -1
  120. package/dist/server/navigation-planner.js +193 -3
  121. package/dist/server/navigation-trace.d.ts +12 -2
  122. package/dist/server/navigation-trace.js +11 -1
  123. package/dist/server/normalize-path.d.ts +0 -8
  124. package/dist/server/normalize-path.js +3 -1
  125. package/dist/server/otel-tracer-extension.d.ts +45 -0
  126. package/dist/server/otel-tracer-extension.js +89 -0
  127. package/dist/server/pages-api-route.d.ts +14 -3
  128. package/dist/server/pages-api-route.js +6 -1
  129. package/dist/server/pages-asset-tags.d.ts +15 -4
  130. package/dist/server/pages-asset-tags.js +18 -12
  131. package/dist/server/pages-data-route.js +5 -1
  132. package/dist/server/pages-node-compat.d.ts +5 -11
  133. package/dist/server/pages-node-compat.js +175 -118
  134. package/dist/server/pages-page-data.d.ts +38 -7
  135. package/dist/server/pages-page-data.js +64 -18
  136. package/dist/server/pages-page-handler.d.ts +10 -2
  137. package/dist/server/pages-page-handler.js +49 -20
  138. package/dist/server/pages-page-response.d.ts +55 -2
  139. package/dist/server/pages-page-response.js +74 -6
  140. package/dist/server/pages-readiness.d.ts +36 -0
  141. package/dist/server/pages-readiness.js +21 -0
  142. package/dist/server/pages-request-pipeline.d.ts +113 -0
  143. package/dist/server/pages-request-pipeline.js +230 -0
  144. package/dist/server/pages-revalidate.d.ts +15 -0
  145. package/dist/server/pages-revalidate.js +19 -0
  146. package/dist/server/prod-server.d.ts +45 -3
  147. package/dist/server/prod-server.js +182 -234
  148. package/dist/server/socket-error-backstop.d.ts +19 -1
  149. package/dist/server/socket-error-backstop.js +77 -4
  150. package/dist/shims/app-router-scroll.js +22 -4
  151. package/dist/shims/cache-runtime.js +39 -2
  152. package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
  153. package/dist/shims/dynamic-preload-chunks.js +77 -0
  154. package/dist/shims/dynamic.d.ts +4 -0
  155. package/dist/shims/dynamic.js +4 -2
  156. package/dist/shims/error-boundary.d.ts +17 -7
  157. package/dist/shims/error-boundary.js +8 -1
  158. package/dist/shims/error.js +37 -11
  159. package/dist/shims/fetch-cache.d.ts +22 -1
  160. package/dist/shims/fetch-cache.js +28 -1
  161. package/dist/shims/hash-scroll.d.ts +1 -0
  162. package/dist/shims/hash-scroll.js +3 -1
  163. package/dist/shims/head.js +6 -1
  164. package/dist/shims/headers.d.ts +16 -2
  165. package/dist/shims/headers.js +37 -1
  166. package/dist/shims/image-config.js +7 -1
  167. package/dist/shims/internal/app-route-detection.d.ts +6 -3
  168. package/dist/shims/internal/app-route-detection.js +10 -6
  169. package/dist/shims/internal/app-router-context.d.ts +5 -0
  170. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  171. package/dist/shims/internal/link-status-registry.js +42 -0
  172. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  173. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  174. package/dist/shims/internal/utils.d.ts +1 -0
  175. package/dist/shims/link.js +20 -6
  176. package/dist/shims/metadata.d.ts +6 -2
  177. package/dist/shims/metadata.js +32 -14
  178. package/dist/shims/navigation.d.ts +9 -18
  179. package/dist/shims/navigation.js +96 -23
  180. package/dist/shims/router-state.d.ts +1 -0
  181. package/dist/shims/router-state.js +2 -0
  182. package/dist/shims/router.d.ts +6 -3
  183. package/dist/shims/router.js +156 -22
  184. package/dist/shims/script-nonce-context.d.ts +1 -1
  185. package/dist/shims/script-nonce-context.js +11 -3
  186. package/dist/shims/server.d.ts +17 -1
  187. package/dist/shims/server.js +31 -6
  188. package/dist/shims/slot.js +1 -1
  189. package/dist/shims/unified-request-context.js +1 -0
  190. package/dist/typegen.js +1 -0
  191. package/dist/utils/client-build-manifest.d.ts +8 -1
  192. package/dist/utils/client-build-manifest.js +41 -6
  193. package/dist/utils/client-entry-manifest.d.ts +11 -0
  194. package/dist/utils/client-entry-manifest.js +29 -0
  195. package/dist/utils/client-runtime-metadata.d.ts +45 -0
  196. package/dist/utils/client-runtime-metadata.js +63 -0
  197. package/dist/utils/hash.d.ts +17 -1
  198. package/dist/utils/hash.js +36 -1
  199. package/dist/utils/lazy-chunks.d.ts +27 -1
  200. package/dist/utils/lazy-chunks.js +65 -1
  201. package/dist/utils/manifest-paths.d.ts +20 -2
  202. package/dist/utils/manifest-paths.js +38 -3
  203. package/dist/utils/path.d.ts +2 -1
  204. package/dist/utils/path.js +5 -1
  205. package/package.json +6 -2
@@ -104,6 +104,10 @@ const SELF_CLOSING_HEAD_TAGS = new Set([
104
104
  ]);
105
105
  /** Tags whose content is raw text — closing-tag sequences must be escaped during SSR. */
106
106
  const RAW_CONTENT_TAGS = new Set(["script", "style"]);
107
+ const INLINE_CLOSE_TAG_RES = {
108
+ script: /<\/(script)/gi,
109
+ style: /<\/(style)/gi
110
+ };
107
111
  function warnDisallowedHeadTag(tag) {
108
112
  if (process.env.NODE_ENV !== "production") console.warn(`[vinext] <Head> ignoring disallowed tag <${tag}>. Only ${ALLOWED_HEAD_TAGS_LIST} are allowed.`);
109
113
  }
@@ -247,7 +251,8 @@ function escapeAttr(s) {
247
251
  * context but prevents the HTML parser from seeing a closing tag.
248
252
  */
249
253
  function escapeInlineContent(content, tag) {
250
- const pattern = new RegExp(`<\\/(${tag})`, "gi");
254
+ const pattern = INLINE_CLOSE_TAG_RES[tag];
255
+ if (!pattern) return content;
251
256
  return content.replace(pattern, "<\\/$1");
252
257
  }
253
258
  function getDangerouslySetInnerHTML(value) {
@@ -18,12 +18,24 @@ type HeadersAccessPhase = "render" | "action" | "route-handler";
18
18
  type VinextHeadersShimState = {
19
19
  headersContext: HeadersContext | null;
20
20
  dynamicUsageDetected: boolean;
21
- renderRequestApiUsage: Set<RenderRequestApiKind>; /** Error recorded by throwIfInsideCacheScope for dev diagnostics, persists even if caught by user code. */
21
+ renderRequestApiUsage: Set<RenderRequestApiKind>;
22
+ connectionProbe: ConnectionProbeState | null; /** Error recorded by throwIfInsideCacheScope for dev diagnostics, persists even if caught by user code. */
22
23
  invalidDynamicUsageError: unknown;
23
24
  pendingSetCookies: string[];
24
25
  draftModeCookieHeader: string | null;
25
26
  phase: HeadersAccessPhase;
26
27
  };
28
+ type ConnectionProbeState = {
29
+ interrupted: boolean;
30
+ interrupt: () => void;
31
+ pending: Promise<never>;
32
+ };
33
+ type ConnectionProbeResult<T> = {
34
+ completed: true;
35
+ result: T;
36
+ } | {
37
+ completed: false;
38
+ };
27
39
  /**
28
40
  * Dynamic usage flag — set when a component calls connection(), cookies(),
29
41
  * headers(), or noStore() during rendering. When true, ISR caching is
@@ -35,6 +47,8 @@ type VinextHeadersShimState = {
35
47
  */
36
48
  declare function markDynamicUsage(): void;
37
49
  declare function markRenderRequestApiUsage(kind: RenderRequestApiKind): void;
50
+ declare function runWithConnectionProbe<T>(fn: () => T | Promise<T>): Promise<ConnectionProbeResult<T>>;
51
+ declare function suspendConnectionProbe(): Promise<never> | null;
38
52
  declare function peekRenderRequestApiUsage(): RenderRequestApiKind[];
39
53
  declare function consumeRenderRequestApiUsage(): RenderRequestApiKind[];
40
54
  /**
@@ -236,4 +250,4 @@ declare class RequestCookies {
236
250
  toString(): string;
237
251
  }
238
252
  //#endregion
239
- export { HeadersAccessPhase, HeadersContext, type RequestCookies, VinextHeadersShimState, applyMiddlewareRequestHeaders, consumeDynamicUsage, consumeInvalidDynamicUsageError, consumeRenderRequestApiUsage, cookies, draftMode, getAndClearPendingCookies, getDraftModeCookieHeader, getHeadersAccessPhase, getHeadersContext, headers, headersContextFromRequest, isDraftModeRequest, markDynamicUsage, markRenderRequestApiUsage, peekDynamicUsage, peekRenderRequestApiUsage, runWithHeadersContext, setHeadersAccessPhase, setHeadersContext, throwIfInsideCacheScope };
253
+ export { HeadersAccessPhase, HeadersContext, type RequestCookies, VinextHeadersShimState, applyMiddlewareRequestHeaders, consumeDynamicUsage, consumeInvalidDynamicUsageError, consumeRenderRequestApiUsage, cookies, draftMode, getAndClearPendingCookies, getDraftModeCookieHeader, getHeadersAccessPhase, getHeadersContext, headers, headersContextFromRequest, isDraftModeRequest, markDynamicUsage, markRenderRequestApiUsage, peekDynamicUsage, peekRenderRequestApiUsage, runWithConnectionProbe, runWithHeadersContext, setHeadersAccessPhase, setHeadersContext, suspendConnectionProbe, throwIfInsideCacheScope };
@@ -21,6 +21,7 @@ const _fallbackState = _g[_FALLBACK_KEY] ??= {
21
21
  headersContext: null,
22
22
  dynamicUsageDetected: false,
23
23
  renderRequestApiUsage: /* @__PURE__ */ new Set(),
24
+ connectionProbe: null,
24
25
  invalidDynamicUsageError: null,
25
26
  pendingSetCookies: [],
26
27
  draftModeCookieHeader: null,
@@ -115,6 +116,39 @@ function markDynamicUsage() {
115
116
  function markRenderRequestApiUsage(kind) {
116
117
  _getState().renderRequestApiUsage.add(kind);
117
118
  }
119
+ async function runWithConnectionProbe(fn) {
120
+ const state = _getState();
121
+ const previousProbe = state.connectionProbe;
122
+ let interruptProbe = () => {};
123
+ const interrupted = new Promise((resolve) => {
124
+ interruptProbe = () => resolve({ completed: false });
125
+ });
126
+ const probe = {
127
+ interrupted: false,
128
+ interrupt() {
129
+ if (probe.interrupted) return;
130
+ probe.interrupted = true;
131
+ interruptProbe();
132
+ },
133
+ pending: new Promise(() => {})
134
+ };
135
+ state.connectionProbe = probe;
136
+ try {
137
+ const completed = Promise.resolve().then(fn).then((result) => ({
138
+ completed: true,
139
+ result
140
+ }));
141
+ return await Promise.race([completed, interrupted]);
142
+ } finally {
143
+ state.connectionProbe = previousProbe;
144
+ }
145
+ }
146
+ function suspendConnectionProbe() {
147
+ const probe = _getState().connectionProbe;
148
+ if (!probe) return null;
149
+ probe.interrupt();
150
+ return probe.pending;
151
+ }
118
152
  function peekRenderRequestApiUsage() {
119
153
  return [..._getState().renderRequestApiUsage].sort();
120
154
  }
@@ -260,6 +294,7 @@ function runWithHeadersContext(ctx, fn) {
260
294
  uCtx.headersContext = ctx;
261
295
  uCtx.dynamicUsageDetected = false;
262
296
  uCtx.renderRequestApiUsage = /* @__PURE__ */ new Set();
297
+ uCtx.connectionProbe = null;
263
298
  uCtx.pendingSetCookies = [];
264
299
  uCtx.draftModeCookieHeader = null;
265
300
  uCtx.phase = "render";
@@ -268,6 +303,7 @@ function runWithHeadersContext(ctx, fn) {
268
303
  headersContext: ctx,
269
304
  dynamicUsageDetected: false,
270
305
  renderRequestApiUsage: /* @__PURE__ */ new Set(),
306
+ connectionProbe: null,
271
307
  invalidDynamicUsageError: null,
272
308
  pendingSetCookies: [],
273
309
  draftModeCookieHeader: null,
@@ -690,4 +726,4 @@ var RequestCookies = class {
690
726
  }
691
727
  };
692
728
  //#endregion
693
- export { applyMiddlewareRequestHeaders, consumeDynamicUsage, consumeInvalidDynamicUsageError, consumeRenderRequestApiUsage, cookies, draftMode, getAndClearPendingCookies, getDraftModeCookieHeader, getHeadersAccessPhase, getHeadersContext, headers, headersContextFromRequest, isDraftModeRequest, markDynamicUsage, markRenderRequestApiUsage, peekDynamicUsage, peekRenderRequestApiUsage, runWithHeadersContext, setHeadersAccessPhase, setHeadersContext, throwIfInsideCacheScope };
729
+ export { applyMiddlewareRequestHeaders, consumeDynamicUsage, consumeInvalidDynamicUsageError, consumeRenderRequestApiUsage, cookies, draftMode, getAndClearPendingCookies, getDraftModeCookieHeader, getHeadersAccessPhase, getHeadersContext, headers, headersContextFromRequest, isDraftModeRequest, markDynamicUsage, markRenderRequestApiUsage, peekDynamicUsage, peekRenderRequestApiUsage, runWithConnectionProbe, runWithHeadersContext, setHeadersAccessPhase, setHeadersContext, suspendConnectionProbe, throwIfInsideCacheScope };
@@ -1,5 +1,6 @@
1
1
  import ipaddr from "ipaddr.js";
2
2
  //#region src/shims/image-config.ts
3
+ const globRegexCache = /* @__PURE__ */ new Map();
3
4
  /**
4
5
  * Convert a glob pattern (with `*` and `**`) to a RegExp.
5
6
  *
@@ -14,6 +15,9 @@ import ipaddr from "ipaddr.js";
14
15
  * Literal characters are escaped for regex safety.
15
16
  */
16
17
  function globToRegex(pattern, separator) {
18
+ const key = `${separator}\0${pattern}`;
19
+ const cached = globRegexCache.get(key);
20
+ if (cached !== void 0) return cached;
17
21
  let regexStr = "^";
18
22
  const doubleStar = separator === "." ? ".+" : ".*";
19
23
  const singleStar = separator === "." ? "[^.]+" : "[^/]+";
@@ -27,7 +31,9 @@ function globToRegex(pattern, separator) {
27
31
  }
28
32
  }
29
33
  regexStr += "$";
30
- return new RegExp(regexStr);
34
+ const re = new RegExp(regexStr);
35
+ globRegexCache.set(key, re);
36
+ return re;
31
37
  }
32
38
  /**
33
39
  * Check whether a URL matches a single remote pattern.
@@ -27,9 +27,12 @@ declare function getPagesRouterComponentsMap(): PagesRouterComponentsMap;
27
27
  * Pages Router map when the href matches an App Router route. No-op when the
28
28
  * manifest is absent, the URL is external, or no app route matches.
29
29
  *
30
- * `pathname` is the basePath-stripped path — matching Next.js's
31
- * `router.components[urlPathname]` key (see the source link in this file's
32
- * leading comment).
30
+ * `pathname` is the basePath-stripped, trailing-slash-stripped path —
31
+ * matching Next.js's `removeTrailingSlash(removeBasePath(pathname))` key used
32
+ * at read time (router.ts:1442). Stripping here ensures the write and read
33
+ * keys agree regardless of whether the caller normalised trailing slashes
34
+ * first (e.g. `link.tsx` normalises to match `trailingSlash` config before
35
+ * calling, while `router.prefetch()` passes the raw user-supplied URL).
33
36
  */
34
37
  declare function markAppRouteDetectedOnPrefetch(href: string, basePath: string): void;
35
38
  //#endregion
@@ -1,4 +1,4 @@
1
- import { stripBasePath } from "../../utils/base-path.js";
1
+ import { removeTrailingSlash, stripBasePath } from "../../utils/base-path.js";
2
2
  import { createRouteTrieCache, matchRouteWithTrie } from "../../routing/route-matching.js";
3
3
  //#region src/shims/internal/app-route-detection.ts
4
4
  const appRouteTrieCache = createRouteTrieCache();
@@ -52,15 +52,19 @@ function matchesAppRoute(href, basePath) {
52
52
  * Pages Router map when the href matches an App Router route. No-op when the
53
53
  * manifest is absent, the URL is external, or no app route matches.
54
54
  *
55
- * `pathname` is the basePath-stripped path — matching Next.js's
56
- * `router.components[urlPathname]` key (see the source link in this file's
57
- * leading comment).
55
+ * `pathname` is the basePath-stripped, trailing-slash-stripped path —
56
+ * matching Next.js's `removeTrailingSlash(removeBasePath(pathname))` key used
57
+ * at read time (router.ts:1442). Stripping here ensures the write and read
58
+ * keys agree regardless of whether the caller normalised trailing slashes
59
+ * first (e.g. `link.tsx` normalises to match `trailingSlash` config before
60
+ * calling, while `router.prefetch()` passes the raw user-supplied URL).
58
61
  */
59
62
  function markAppRouteDetectedOnPrefetch(href, basePath) {
60
63
  if (typeof window === "undefined") return;
61
64
  if (!matchesAppRoute(href, basePath)) return;
62
- const pathname = resolveSameOriginPathname(href, basePath);
63
- if (pathname === null) return;
65
+ const rawPathname = resolveSameOriginPathname(href, basePath);
66
+ if (rawPathname === null) return;
67
+ const pathname = removeTrailingSlash(rawPathname);
64
68
  getPagesRouterComponentsMap()[pathname] = { __appRouter: true };
65
69
  }
66
70
  //#endregion
@@ -16,6 +16,11 @@ type AppRouterInstance = {
16
16
  push(href: string, options?: NavigateOptions): void;
17
17
  replace(href: string, options?: NavigateOptions): void;
18
18
  prefetch(href: string, options?: PrefetchOptions): void;
19
+ /**
20
+ * Perform an experimental gesture transition navigation.
21
+ * Only available when experimental.gestureTransition is enabled.
22
+ */
23
+ experimental_gesturePush?(href: string, options?: NavigateOptions): void;
19
24
  };
20
25
  declare const AppRouterContext: React$1.Context<AppRouterInstance | null> | null;
21
26
  declare const GlobalLayoutRouterContext: React$1.Context<unknown> | null;
@@ -0,0 +1,43 @@
1
+ //#region src/shims/internal/link-status-registry.d.ts
2
+ /**
3
+ * Link-status pending registry.
4
+ *
5
+ * Tracks the single <Link> that started the most recent App Router navigation
6
+ * so its `useLinkStatus()` pending state can be reset when a *different*
7
+ * navigation begins — a different <Link> click, `router.push`/`router.replace`,
8
+ * a form submission, shallow routing via raw `history.pushState`, or browser
9
+ * back/forward. Without this, a Link's pending indicator stays "sticky" after
10
+ * an interrupting navigation, because the Link's own completion handler is the
11
+ * only thing that would otherwise clear it.
12
+ *
13
+ * Mirrors Next.js's `linkForMostRecentNavigation` /
14
+ * `setLinkForCurrentNavigation` in
15
+ * packages/next/src/client/components/links.ts, adapted to vinext's per-<Link>
16
+ * React state model: instead of an optimistic-status dispatcher, we hold the
17
+ * link's `setPending` setter.
18
+ */
19
+ type PendingLinkSetter = (pending: boolean) => void;
20
+ /**
21
+ * Mark `setter` as the link that started the most recent navigation, resetting
22
+ * the previously-tracked link's pending state to idle so only the last-clicked
23
+ * link shows a pending state.
24
+ */
25
+ declare function setLinkForCurrentNavigation(setter: PendingLinkSetter): void;
26
+ /**
27
+ * Stop tracking `setter` if it is the current navigation link. Called when a
28
+ * <Link> finishes its own navigation or unmounts so we never hold a stale
29
+ * reference to an unmounted component's setter.
30
+ */
31
+ declare function clearLinkForCurrentNavigation(setter: PendingLinkSetter): void;
32
+ /**
33
+ * Reset any link that is currently showing a pending state. Invoked at the
34
+ * start of every App Router navigation so that navigations not initiated by the
35
+ * tracked link — `router.push`/`router.replace`, form submissions, shallow
36
+ * routing, and browser back/forward — clear a stale pending indicator. A
37
+ * link-initiated navigation registers itself first via
38
+ * `setLinkForCurrentNavigation`; the matching call here consumes that marker and
39
+ * keeps the link pending.
40
+ */
41
+ declare function notifyLinkNavigationStart(): void;
42
+ //#endregion
43
+ export { PendingLinkSetter, clearLinkForCurrentNavigation, notifyLinkNavigationStart, setLinkForCurrentNavigation };
@@ -0,0 +1,42 @@
1
+ //#region src/shims/internal/link-status-registry.ts
2
+ let linkSetterForMostRecentNavigation = null;
3
+ let currentNavigationIsLinkInitiated = false;
4
+ /**
5
+ * Mark `setter` as the link that started the most recent navigation, resetting
6
+ * the previously-tracked link's pending state to idle so only the last-clicked
7
+ * link shows a pending state.
8
+ */
9
+ function setLinkForCurrentNavigation(setter) {
10
+ if (linkSetterForMostRecentNavigation && linkSetterForMostRecentNavigation !== setter) linkSetterForMostRecentNavigation(false);
11
+ linkSetterForMostRecentNavigation = setter;
12
+ currentNavigationIsLinkInitiated = true;
13
+ }
14
+ /**
15
+ * Stop tracking `setter` if it is the current navigation link. Called when a
16
+ * <Link> finishes its own navigation or unmounts so we never hold a stale
17
+ * reference to an unmounted component's setter.
18
+ */
19
+ function clearLinkForCurrentNavigation(setter) {
20
+ if (linkSetterForMostRecentNavigation === setter) linkSetterForMostRecentNavigation = null;
21
+ }
22
+ /**
23
+ * Reset any link that is currently showing a pending state. Invoked at the
24
+ * start of every App Router navigation so that navigations not initiated by the
25
+ * tracked link — `router.push`/`router.replace`, form submissions, shallow
26
+ * routing, and browser back/forward — clear a stale pending indicator. A
27
+ * link-initiated navigation registers itself first via
28
+ * `setLinkForCurrentNavigation`; the matching call here consumes that marker and
29
+ * keeps the link pending.
30
+ */
31
+ function notifyLinkNavigationStart() {
32
+ if (currentNavigationIsLinkInitiated) {
33
+ currentNavigationIsLinkInitiated = false;
34
+ return;
35
+ }
36
+ if (linkSetterForMostRecentNavigation) {
37
+ linkSetterForMostRecentNavigation(false);
38
+ linkSetterForMostRecentNavigation = null;
39
+ }
40
+ }
41
+ //#endregion
42
+ export { clearLinkForCurrentNavigation, notifyLinkNavigationStart, setLinkForCurrentNavigation };
@@ -0,0 +1,27 @@
1
+ //#region src/shims/internal/route-pattern-for-warning.d.ts
2
+ /**
3
+ * Side-effect-free accessor for the current render's route pattern, used by
4
+ * Next.js-parity diagnostics such as the Link shim's
5
+ * "Invalid href ... in page: '...'" `console.error`.
6
+ *
7
+ * Mirrors Next.js's `router.pathname`, which is the *route pattern* (e.g.
8
+ * `/posts/[id]`), not the resolved URL. During Pages Router SSR the route
9
+ * pattern lives on the request-scoped SSR context; the server-only
10
+ * `router-state.ts` publishes an accessor for it under a well-known
11
+ * `Symbol.for` handle. We read it through that handle rather than importing
12
+ * `router.ts` directly — importing `router.ts` would pull its browser-only
13
+ * `installWindowNext()` side effect into every consumer of the Link shim
14
+ * (including the App Router client bundle), clobbering `window.next.router`.
15
+ *
16
+ * On the client (or when no accessor is registered, e.g. App Router) we fall
17
+ * back to `window.location.pathname`, then to `"/"`.
18
+ */
19
+ /**
20
+ * Register the server-side route-pattern accessor. Called once by the
21
+ * server-only router-state module on import. Idempotent.
22
+ * @internal
23
+ */
24
+ declare function registerRoutePatternForWarningAccessor(accessor: () => string | null): void;
25
+ declare function getCurrentRoutePathnameForWarning(): string;
26
+ //#endregion
27
+ export { getCurrentRoutePathnameForWarning, registerRoutePatternForWarningAccessor };
@@ -0,0 +1,40 @@
1
+ //#region src/shims/internal/route-pattern-for-warning.ts
2
+ /**
3
+ * Side-effect-free accessor for the current render's route pattern, used by
4
+ * Next.js-parity diagnostics such as the Link shim's
5
+ * "Invalid href ... in page: '...'" `console.error`.
6
+ *
7
+ * Mirrors Next.js's `router.pathname`, which is the *route pattern* (e.g.
8
+ * `/posts/[id]`), not the resolved URL. During Pages Router SSR the route
9
+ * pattern lives on the request-scoped SSR context; the server-only
10
+ * `router-state.ts` publishes an accessor for it under a well-known
11
+ * `Symbol.for` handle. We read it through that handle rather than importing
12
+ * `router.ts` directly — importing `router.ts` would pull its browser-only
13
+ * `installWindowNext()` side effect into every consumer of the Link shim
14
+ * (including the App Router client bundle), clobbering `window.next.router`.
15
+ *
16
+ * On the client (or when no accessor is registered, e.g. App Router) we fall
17
+ * back to `window.location.pathname`, then to `"/"`.
18
+ */
19
+ const ROUTE_PATTERN_FOR_WARNING_ACCESSOR_KEY = Symbol.for("vinext.router.routePatternForWarningAccessor");
20
+ /**
21
+ * Register the server-side route-pattern accessor. Called once by the
22
+ * server-only router-state module on import. Idempotent.
23
+ * @internal
24
+ */
25
+ function registerRoutePatternForWarningAccessor(accessor) {
26
+ globalThis[ROUTE_PATTERN_FOR_WARNING_ACCESSOR_KEY] = accessor;
27
+ }
28
+ function getCurrentRoutePathnameForWarning() {
29
+ if (typeof window === "undefined") {
30
+ const accessor = globalThis[ROUTE_PATTERN_FOR_WARNING_ACCESSOR_KEY];
31
+ if (accessor) try {
32
+ const pattern = accessor();
33
+ if (pattern) return pattern;
34
+ } catch {}
35
+ return "/";
36
+ }
37
+ return window.location?.pathname ?? "/";
38
+ }
39
+ //#endregion
40
+ export { getCurrentRoutePathnameForWarning, registerRoutePatternForWarningAccessor };
@@ -21,6 +21,7 @@ type NEXT_DATA = {
21
21
  statusCode: number;
22
22
  name?: string;
23
23
  };
24
+ isExperimentalCompile?: boolean;
24
25
  gsp?: boolean;
25
26
  gssp?: boolean;
26
27
  customServer?: boolean;
@@ -12,12 +12,14 @@ import { markAppRouteDetectedOnPrefetch } from "./internal/app-route-detection.j
12
12
  import { isAbsoluteOrProtocolRelativeUrl, normalizePathTrailingSlash, resolveRelativeHref, toBrowserNavigationHref, toSameOriginAppPath, withBasePath } from "./url-utils.js";
13
13
  import { appendSearchParamsToUrl, urlQueryToSearchParams } from "../utils/query.js";
14
14
  import { getCurrentBrowserLocale } from "./client-locale.js";
15
+ import { getCurrentRoutePathnameForWarning } from "./internal/route-pattern-for-warning.js";
15
16
  import { getNavigationRuntime, hasAppNavigationRuntime, registerNavigationRuntimeFunctions } from "../client/navigation-runtime.js";
16
17
  import { createRscRequestHeaders, createRscRequestUrl, stripRscCacheBustingSearchParam, stripRscSuffix } from "../server/app-rsc-cache-busting.js";
17
18
  import { getMountedSlotsHeader, getPrefetchCache, getPrefetchInterceptionContext, getPrefetchedUrls, hasPrefetchCacheEntryForNavigation, navigateClientSide, prefetchRscResponse } from "./navigation.js";
18
19
  import { navigatePagesRouterLink } from "../client/pages-router-link-navigation.js";
19
20
  import { getI18nContext } from "./i18n-context.js";
20
21
  import { canLinkIntentPrefetch, canLinkPrefetch, getLinkPrefetchHref } from "./link-prefetch.js";
22
+ import { clearLinkForCurrentNavigation, notifyLinkNavigationStart, setLinkForCurrentNavigation } from "./internal/link-status-registry.js";
21
23
  import React, { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
22
24
  import { jsx } from "react/jsx-runtime";
23
25
  //#region src/shims/link.tsx
@@ -37,6 +39,7 @@ const LinkStatusContext = createContext({ pending: false });
37
39
  function useLinkStatus() {
38
40
  return useContext(LinkStatusContext);
39
41
  }
42
+ if (typeof window !== "undefined") registerNavigationRuntimeFunctions({ notifyLinkNavigationStart });
40
43
  /** basePath from next.config.js, injected by the plugin at build time */
41
44
  const __basePath = process.env.__NEXT_ROUTER_BASEPATH ?? "";
42
45
  /** trailingSlash from next.config.js, injected by the plugin at build time */
@@ -45,7 +48,7 @@ const __prefetchInlining = process.env.__VINEXT_PREFETCH_INLINING === "true";
45
48
  const linkPrefetchRouteTrieCache = createRouteTrieCache();
46
49
  function resolveHref(href) {
47
50
  if (typeof href === "string") return href;
48
- let url = href.pathname ?? "/";
51
+ let url = href.pathname ?? "";
49
52
  if (href.query) {
50
53
  const params = urlQueryToSearchParams(href.query);
51
54
  url = appendSearchParamsToUrl(url, params);
@@ -81,17 +84,19 @@ function normalizeRepeatedSlashes(url) {
81
84
  * `resolveHref`. We mirror that behaviour (no dedup) for exact parity.
82
85
  *
83
86
  * Note: Next.js uses `router.pathname` (the route pattern, e.g.
84
- * `/posts/[id]`) for the "in page" segment of the message. We do not have
85
- * cheap access to the route pattern from inside the Link shim, so we
86
- * fall back to `window.location.pathname` (or `"/"` during SSR). The text
87
- * is cosmetic and is not asserted by the Next.js compat test.
87
+ * `/posts/[id]`) for the "in page" segment of the message. The Next.js
88
+ * compat test asserts this exact text (`in page: '/my/path/[name]'`), so we
89
+ * source it from the current render's route pattern via
90
+ * `getCurrentRoutePathnameForWarning()`: the Pages Router SSR context's route
91
+ * pattern on the server, `window.location.pathname` on the client, falling
92
+ * back to `"/"`.
88
93
  */
89
94
  function warnAndNormalizeRepeatedSlashesInHref(urlAsString) {
90
95
  if (urlAsString.startsWith("//")) return urlAsString;
91
96
  const urlProtoMatch = urlAsString.match(/^[a-z][a-z0-9+.-]*:\/\//i);
92
97
  const urlAsStringNoProto = urlProtoMatch ? urlAsString.slice(urlProtoMatch[0].length) : urlAsString;
93
98
  if (!(urlAsStringNoProto.split("?", 1)[0] || "").match(/(\/\/|\\)/)) return urlAsString;
94
- const pathname = typeof window !== "undefined" && window.location ? window.location.pathname : "/";
99
+ const pathname = getCurrentRoutePathnameForWarning();
95
100
  console.error(`Invalid href '${urlAsString}' passed to next/router in page: '${pathname}'. Repeated forward-slashes (//) or backslashes \\ are not valid in the href.`);
96
101
  const normalizedNoProto = normalizeRepeatedSlashes(urlAsStringNoProto);
97
102
  return (urlProtoMatch ? urlProtoMatch[0] : "") + normalizedNoProto;
@@ -381,10 +386,16 @@ const Link = forwardRef(function Link({ href, as, replace = false, prefetch: pre
381
386
  const fullHref = normalizePathTrailingSlash(withBasePath(normalizedHref, __basePath), __trailingSlash);
382
387
  const [pending, setPending] = useState(false);
383
388
  const mountedRef = useRef(true);
389
+ const setPendingRef = useRef(null);
390
+ if (setPendingRef.current === null) setPendingRef.current = (next) => {
391
+ if (mountedRef.current) setPending(next);
392
+ };
384
393
  useEffect(() => {
385
394
  mountedRef.current = true;
395
+ const setter = setPendingRef.current;
386
396
  return () => {
387
397
  mountedRef.current = false;
398
+ if (setter) clearLinkForCurrentNavigation(setter);
388
399
  };
389
400
  }, []);
390
401
  const internalRef = useRef(null);
@@ -497,10 +508,13 @@ const Link = forwardRef(function Link({ href, as, replace = false, prefetch: pre
497
508
  if (navEvent.defaultPrevented) return;
498
509
  } catch {}
499
510
  if (getNavigationRuntime()?.functions.navigate) {
511
+ const setter = setPendingRef.current;
512
+ if (setter) setLinkForCurrentNavigation(setter);
500
513
  setPending(true);
501
514
  React.startTransition(() => {
502
515
  navigateClientSide(navigateHref, replace ? "replace" : "push", scroll, true).finally(() => {
503
516
  if (mountedRef.current) setPending(false);
517
+ if (setter) clearLinkForCurrentNavigation(setter);
504
518
  });
505
519
  });
506
520
  return;
@@ -271,11 +271,15 @@ declare function resolveModuleMetadata(mod: Record<string, unknown>, params?: Re
271
271
  type MetadataHeadProps = {
272
272
  metadata: Metadata;
273
273
  pathname?: string;
274
+ trailingSlash?: boolean;
274
275
  };
275
- declare function renderMetadataToHtml(metadata: Metadata, pathname?: string): string;
276
+ declare function renderMetadataToHtml(metadata: Metadata, pathname?: string, options?: {
277
+ trailingSlash?: boolean;
278
+ }): string;
276
279
  declare function MetadataHead({
277
280
  metadata,
278
- pathname
281
+ pathname,
282
+ trailingSlash
279
283
  }: MetadataHeadProps): React.JSX.Element;
280
284
  //#endregion
281
285
  export { DEFAULT_VIEWPORT, Metadata, MetadataHead, MetadataMergeEntry, Viewport, ViewportHead, mergeMetadata, mergeMetadataEntries, mergeViewport, postProcessMetadata, renderMetadataToHtml, resolveModuleMetadata, resolveModuleViewport };
@@ -287,18 +287,35 @@ function formatResolvedMetadataUrl(url) {
287
287
  if (url.pathname === "/" && url.search === "" && url.hash === "") return url.origin;
288
288
  return url.href;
289
289
  }
290
- function resolveMetadataUrl(url, metadataBase) {
290
+ const TRAILING_SLASH_FILE_REGEX = /^(?:\/((?!\.well-known(?:\/.*)?)((?:[^/]+\/)*)([^/]+\.\w+)))(\/?|$)/i;
291
+ function resolveMetadataUrl(url, metadataBase, trailingSlash) {
291
292
  const value = stringifyUrl(url);
292
- if (isAbsoluteOrProtocolRelativeUrl(value) || !metadataBase) return value;
293
+ if (!metadataBase) return value;
293
294
  try {
294
- return formatResolvedMetadataUrl(new URL(joinMetadataPath(metadataBase.pathname, value), metadataBase));
295
+ const isAbsolute = isAbsoluteOrProtocolRelativeUrl(value);
296
+ const composed = isAbsolute ? new URL(value, metadataBase) : new URL(joinMetadataPath(metadataBase.pathname, value), metadataBase);
297
+ if (isAbsolute && composed.origin !== metadataBase.origin) return value;
298
+ if (trailingSlash === true && composed.search === "") {
299
+ if (composed.pathname !== "/" && !composed.pathname.endsWith("/") && !TRAILING_SLASH_FILE_REGEX.test(composed.pathname)) composed.pathname += "/";
300
+ }
301
+ const result = formatResolvedMetadataUrl(composed);
302
+ if (trailingSlash === true && result === metadataBase.origin) return `${metadataBase.origin}/`;
303
+ return result;
295
304
  } catch {
296
305
  return value;
297
306
  }
298
307
  }
299
- function resolveCanonicalUrl(url, metadataBase, pathname) {
300
- if (url instanceof URL) return resolveMetadataUrl(url, metadataBase);
301
- return resolveMetadataUrl(resolveRelativeMetadataUrl(url, pathname), metadataBase);
308
+ function resolveCanonicalUrl(url, metadataBase, pathname, trailingSlash) {
309
+ if (url instanceof URL) return resolveMetadataUrl(url, metadataBase, trailingSlash);
310
+ return resolveMetadataUrl(resolveRelativeMetadataUrl(url, pathname), metadataBase, trailingSlash);
311
+ }
312
+ function resolveAlternateUrl(url, metadataBase, pathname, trailingSlash) {
313
+ if (url instanceof URL) {
314
+ const resolvedUrl = new URL(pathname, url);
315
+ url.searchParams.forEach((value, key) => resolvedUrl.searchParams.set(key, value));
316
+ return resolveMetadataUrl(resolvedUrl, metadataBase, trailingSlash);
317
+ }
318
+ return resolveCanonicalUrl(url, metadataBase, pathname, trailingSlash);
302
319
  }
303
320
  function isSocialImageDescriptor(value) {
304
321
  return typeof value === "object" && !(value instanceof URL);
@@ -359,13 +376,14 @@ function renderMetadataElementToHtml(node) {
359
376
  default: return "";
360
377
  }
361
378
  }
362
- function renderMetadataToHtml(metadata, pathname = "/") {
379
+ function renderMetadataToHtml(metadata, pathname = "/", options) {
363
380
  return renderMetadataElementToHtml(MetadataHead({
364
381
  metadata,
365
- pathname
382
+ pathname,
383
+ trailingSlash: options?.trailingSlash
366
384
  }));
367
385
  }
368
- function MetadataHead({ metadata, pathname = "/" }) {
386
+ function MetadataHead({ metadata, pathname = "/", trailingSlash }) {
369
387
  const elements = [];
370
388
  let key = 0;
371
389
  const base = metadata.metadataBase;
@@ -474,7 +492,7 @@ function MetadataHead({ metadata, pathname = "/" }) {
474
492
  }, key++));
475
493
  if (og.url) elements.push(/* @__PURE__ */ jsx("meta", {
476
494
  property: "og:url",
477
- content: resolveUrl(og.url)
495
+ content: resolveCanonicalUrl(og.url, base, pathname, trailingSlash)
478
496
  }, key++));
479
497
  if (og.siteName) elements.push(/* @__PURE__ */ jsx("meta", {
480
498
  property: "og:site_name",
@@ -691,22 +709,22 @@ function MetadataHead({ metadata, pathname = "/" }) {
691
709
  const alt = metadata.alternates;
692
710
  if (alt.canonical) elements.push(/* @__PURE__ */ jsx("link", {
693
711
  rel: "canonical",
694
- href: resolveCanonicalUrl(alt.canonical, base, pathname)
712
+ href: resolveCanonicalUrl(alt.canonical, base, pathname, trailingSlash)
695
713
  }, key++));
696
714
  if (alt.languages) for (const [lang, href] of Object.entries(alt.languages)) elements.push(/* @__PURE__ */ jsx("link", {
697
715
  rel: "alternate",
698
716
  hrefLang: lang,
699
- href: resolveUrl(href)
717
+ href: resolveAlternateUrl(href, base, pathname, trailingSlash)
700
718
  }, key++));
701
719
  if (alt.media) for (const [media, href] of Object.entries(alt.media)) elements.push(/* @__PURE__ */ jsx("link", {
702
720
  rel: "alternate",
703
721
  media,
704
- href: resolveUrl(href)
722
+ href: resolveAlternateUrl(href, base, pathname, trailingSlash)
705
723
  }, key++));
706
724
  if (alt.types) for (const [type, href] of Object.entries(alt.types)) elements.push(/* @__PURE__ */ jsx("link", {
707
725
  rel: "alternate",
708
726
  type,
709
- href: resolveUrl(href)
727
+ href: resolveAlternateUrl(href, base, pathname, trailingSlash)
710
728
  }, key++));
711
729
  }
712
730
  if (metadata.verification) {