vinext 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/build/assets-ignore.d.ts +32 -0
  2. package/dist/build/assets-ignore.js +48 -0
  3. package/dist/build/client-build-config.d.ts +27 -1
  4. package/dist/build/client-build-config.js +58 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/client/navigation-runtime.d.ts +8 -0
  7. package/dist/client/navigation-runtime.js +1 -1
  8. package/dist/client/vinext-next-data.d.ts +2 -1
  9. package/dist/config/config-matchers.d.ts +20 -1
  10. package/dist/config/config-matchers.js +35 -1
  11. package/dist/config/next-config.d.ts +16 -3
  12. package/dist/config/next-config.js +30 -2
  13. package/dist/deploy.js +40 -304
  14. package/dist/entries/app-rsc-entry.d.ts +8 -2
  15. package/dist/entries/app-rsc-entry.js +54 -4
  16. package/dist/entries/app-rsc-manifest.js +20 -2
  17. package/dist/entries/pages-server-entry.js +9 -1
  18. package/dist/index.js +162 -217
  19. package/dist/plugins/postcss.js +18 -14
  20. package/dist/plugins/require-context.d.ts +6 -0
  21. package/dist/plugins/require-context.js +184 -0
  22. package/dist/routing/app-route-graph.d.ts +12 -1
  23. package/dist/routing/app-route-graph.js +137 -5
  24. package/dist/routing/route-pattern.d.ts +2 -1
  25. package/dist/routing/route-pattern.js +16 -1
  26. package/dist/server/api-handler.js +4 -0
  27. package/dist/server/app-browser-entry.js +84 -39
  28. package/dist/server/app-browser-interception-context.d.ts +2 -1
  29. package/dist/server/app-browser-interception-context.js +15 -2
  30. package/dist/server/app-browser-navigation-controller.d.ts +11 -1
  31. package/dist/server/app-browser-navigation-controller.js +77 -1
  32. package/dist/server/app-browser-popstate.d.ts +12 -3
  33. package/dist/server/app-browser-popstate.js +19 -4
  34. package/dist/server/app-browser-state.d.ts +3 -0
  35. package/dist/server/app-browser-state.js +6 -3
  36. package/dist/server/app-browser-visible-commit.js +9 -7
  37. package/dist/server/app-history-state.d.ts +45 -1
  38. package/dist/server/app-history-state.js +109 -1
  39. package/dist/server/app-page-boundary-render.js +41 -19
  40. package/dist/server/app-page-dispatch.d.ts +6 -0
  41. package/dist/server/app-page-dispatch.js +3 -1
  42. package/dist/server/app-page-element-builder.d.ts +1 -0
  43. package/dist/server/app-page-element-builder.js +22 -10
  44. package/dist/server/app-page-render.d.ts +6 -0
  45. package/dist/server/app-page-render.js +5 -3
  46. package/dist/server/app-page-request.d.ts +8 -6
  47. package/dist/server/app-page-request.js +12 -9
  48. package/dist/server/app-page-response.d.ts +2 -2
  49. package/dist/server/app-page-response.js +1 -1
  50. package/dist/server/app-page-route-wiring.js +2 -1
  51. package/dist/server/app-page-stream.d.ts +37 -2
  52. package/dist/server/app-page-stream.js +36 -3
  53. package/dist/server/app-pages-bridge.d.ts +16 -0
  54. package/dist/server/app-pages-bridge.js +23 -3
  55. package/dist/server/app-route-handler-cache.d.ts +1 -0
  56. package/dist/server/app-route-handler-cache.js +1 -0
  57. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  58. package/dist/server/app-route-handler-dispatch.js +2 -0
  59. package/dist/server/app-route-handler-execution.d.ts +1 -0
  60. package/dist/server/app-route-handler-execution.js +1 -0
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  62. package/dist/server/app-route-handler-runtime.js +3 -2
  63. package/dist/server/app-rsc-handler.d.ts +1 -0
  64. package/dist/server/app-rsc-handler.js +4 -3
  65. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  66. package/dist/server/app-rsc-route-matching.js +29 -4
  67. package/dist/server/app-server-action-execution.d.ts +11 -1
  68. package/dist/server/app-server-action-execution.js +68 -10
  69. package/dist/server/app-ssr-entry.d.ts +6 -0
  70. package/dist/server/app-ssr-entry.js +17 -1
  71. package/dist/server/dev-server.d.ts +1 -1
  72. package/dist/server/dev-server.js +54 -31
  73. package/dist/server/isr-cache.d.ts +37 -1
  74. package/dist/server/isr-cache.js +85 -1
  75. package/dist/server/navigation-planner.js +5 -3
  76. package/dist/server/navigation-trace.d.ts +1 -1
  77. package/dist/server/pages-node-compat.d.ts +2 -0
  78. package/dist/server/pages-node-compat.js +4 -0
  79. package/dist/server/pages-page-data.d.ts +10 -7
  80. package/dist/server/pages-page-data.js +4 -2
  81. package/dist/server/pages-page-handler.d.ts +9 -2
  82. package/dist/server/pages-page-handler.js +29 -16
  83. package/dist/server/pages-page-response.d.ts +11 -2
  84. package/dist/server/pages-page-response.js +8 -1
  85. package/dist/server/pages-readiness.d.ts +36 -0
  86. package/dist/server/pages-readiness.js +21 -0
  87. package/dist/server/pages-request-pipeline.d.ts +99 -0
  88. package/dist/server/pages-request-pipeline.js +209 -0
  89. package/dist/server/pages-revalidate.d.ts +15 -0
  90. package/dist/server/pages-revalidate.js +19 -0
  91. package/dist/server/prod-server.d.ts +6 -2
  92. package/dist/server/prod-server.js +101 -217
  93. package/dist/server/socket-error-backstop.d.ts +19 -1
  94. package/dist/server/socket-error-backstop.js +77 -4
  95. package/dist/shims/app-router-scroll.js +22 -4
  96. package/dist/shims/cache-runtime.js +31 -1
  97. package/dist/shims/error-boundary.d.ts +21 -11
  98. package/dist/shims/error-boundary.js +8 -1
  99. package/dist/shims/fetch-cache.d.ts +14 -1
  100. package/dist/shims/fetch-cache.js +18 -1
  101. package/dist/shims/hash-scroll.d.ts +1 -0
  102. package/dist/shims/hash-scroll.js +3 -1
  103. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  104. package/dist/shims/internal/link-status-registry.js +42 -0
  105. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  106. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  107. package/dist/shims/internal/utils.d.ts +1 -0
  108. package/dist/shims/link.js +20 -6
  109. package/dist/shims/navigation.d.ts +2 -2
  110. package/dist/shims/navigation.js +63 -7
  111. package/dist/shims/router-state.d.ts +1 -0
  112. package/dist/shims/router-state.js +2 -0
  113. package/dist/shims/router.d.ts +6 -3
  114. package/dist/shims/router.js +128 -21
  115. package/dist/utils/client-build-manifest.d.ts +8 -1
  116. package/dist/utils/client-build-manifest.js +30 -5
  117. package/dist/utils/client-entry-manifest.d.ts +11 -0
  118. package/dist/utils/client-entry-manifest.js +29 -0
  119. package/package.json +5 -1
@@ -3,6 +3,7 @@ import { getRequestContext, isInsideUnifiedScope, runWithUnifiedStateMutation }
3
3
  import { VINEXT_RSC_MARKER_HEADER } from "../server/headers.js";
4
4
  import { markDynamicUsage } from "./headers.js";
5
5
  import { _registerCacheContextAccessor, _setRequestScopedCacheLife, cacheLifeProfiles, getDataCacheHandler } from "./cache.js";
6
+ import { addCollectedRequestTags } from "./fetch-cache.js";
6
7
  //#region src/shims/cache-runtime.ts
7
8
  /**
8
9
  * "use cache" runtime
@@ -270,6 +271,7 @@ function registerCachedFunction(fn, id, variant, options = {}) {
270
271
  const handler = getDataCacheHandler();
271
272
  const existing = await handler.get(cacheKey, { kind: "FETCH" });
272
273
  if (existing?.value && existing.value.kind === "FETCH" && existing.cacheState !== "stale") try {
274
+ propagateCacheTagsToRequest(existing.value.tags);
273
275
  if (rsc && existing.value.data.headers["x-vinext-rsc"] === "1") {
274
276
  const stream = uint8ToStream(base64ToUint8(existing.value.data.body));
275
277
  const result = await rsc.createFromReadableStream(stream);
@@ -282,6 +284,7 @@ function registerCachedFunction(fn, id, variant, options = {}) {
282
284
  } catch {}
283
285
  const { result, ctx, effectiveLife } = await runCachedFunctionWithContext(fn, args, cacheVariant);
284
286
  recordRequestScopedCacheLife(effectiveLife);
287
+ propagateCacheTagsToRequest(ctx.tags);
285
288
  const revalidateSeconds = effectiveLife.revalidate ?? cacheLifeProfiles.default.revalidate ?? 900;
286
289
  try {
287
290
  let body;
@@ -336,6 +339,30 @@ function recordRequestScopedCacheControl(cacheControl) {
336
339
  function recordRequestScopedCacheLife(cacheLife) {
337
340
  _setRequestScopedCacheLife(cacheLife);
338
341
  }
342
+ /**
343
+ * Bubble a `"use cache"` scope's tags toward where they can drive invalidation.
344
+ *
345
+ * When this cache is nested inside another (`parentCtx` present), the tags flow
346
+ * into the parent scope so they end up on the outer cache entry — mirroring
347
+ * Next.js's `propagateCacheLifeAndTagsToRevalidateStore`. The outermost scope
348
+ * (no parent) instead records onto the surrounding request's collected tags, so
349
+ * the enclosing page / route-handler ISR entry carries them and `revalidateTag`
350
+ * can evict the rendered output (issue #1453).
351
+ *
352
+ * Used by both the data cache HIT and MISS paths. On MISS the parent-bubble for
353
+ * the *executed* scope also happens in `runCachedFunctionWithContext`; this keeps
354
+ * the HIT path (where that function never runs) correct without dropping a nested
355
+ * inner entry's stored tags. Deduped to keep tag lists tidy.
356
+ */
357
+ function propagateCacheTagsToRequest(tags) {
358
+ if (!tags || tags.length === 0) return;
359
+ const parentCtx = cacheContextStorage.getStore();
360
+ if (parentCtx) {
361
+ for (const tag of tags) if (!parentCtx.tags.includes(tag)) parentCtx.tags.push(tag);
362
+ return;
363
+ }
364
+ addCollectedRequestTags(tags);
365
+ }
339
366
  async function executeWithContext(fn, args, variant) {
340
367
  const { result, ctx: _ctx, effectiveLife } = await runCachedFunctionWithContext(fn, args, variant);
341
368
  recordRequestScopedCacheLife(effectiveLife);
@@ -360,7 +387,10 @@ async function runCachedFunctionWithContext(fn, args, variant) {
360
387
  const result = await cacheContextStorage.run(ctx, () => fn(...args));
361
388
  if (ctx.invalidDynamicUsageError) throw ctx.invalidDynamicUsageError;
362
389
  const effectiveLife = resolveCacheLife(ctx.lifeConfigs);
363
- if (parentCtx) parentCtx.lifeConfigs.push(effectiveLife);
390
+ if (parentCtx) {
391
+ parentCtx.lifeConfigs.push(effectiveLife);
392
+ for (const tag of ctx.tags) if (!parentCtx.tags.includes(tag)) parentCtx.tags.push(tag);
393
+ }
364
394
  if (parentCtx && eagerError && (effectiveLife.revalidate === 0 || effectiveLife.expire !== void 0 && effectiveLife.expire < DYNAMIC_EXPIRE)) parentCtx.dynamicNestedCacheError ??= eagerError;
365
395
  if (typeof process !== "undefined" && (process.env.VINEXT_PRERENDER === "1" || process.env.NODE_ENV === "development") && ctx.dynamicNestedCacheError) {
366
396
  if (effectiveLife.revalidate === 0 && !ctx.hasExplicitRevalidate) throw new Error(getNestedCacheZeroRevalidateErrorMessage(), { cause: ctx.dynamicNestedCacheError });
@@ -17,11 +17,11 @@ type RedirectBoundaryState = {
17
17
  redirectType: "push" | "replace" | null;
18
18
  };
19
19
  type ErrorBoundaryInnerProps = {
20
- pathname: string;
20
+ pathname: string | null;
21
21
  } & ErrorBoundaryProps;
22
22
  type ErrorBoundaryState = {
23
23
  error: CapturedError | null;
24
- previousPathname: string;
24
+ previousPathname: string | null;
25
25
  previousResetKey: string | null;
26
26
  };
27
27
  declare class RedirectErrorBoundary extends React.Component<{
@@ -31,7 +31,7 @@ declare class RedirectErrorBoundary extends React.Component<{
31
31
  children?: React.ReactNode;
32
32
  });
33
33
  static getDerivedStateFromError(error: unknown): RedirectBoundaryState;
34
- render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
34
+ render(): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
35
35
  }
36
36
  declare function RedirectBoundary({
37
37
  children
@@ -51,13 +51,23 @@ declare class ErrorBoundaryInner extends React.Component<ErrorBoundaryInnerProps
51
51
  componentDidMount(): void;
52
52
  componentWillUnmount(): void;
53
53
  reset: () => void;
54
- render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
54
+ render(): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
55
55
  }
56
56
  declare function ErrorBoundary({
57
57
  fallback,
58
58
  children,
59
59
  resetKey
60
60
  }: ErrorBoundaryProps): React.JSX.Element;
61
+ declare function GlobalErrorBoundary({
62
+ fallback,
63
+ children
64
+ }: {
65
+ fallback: React.ComponentType<{
66
+ error: unknown;
67
+ reset: () => void;
68
+ }>;
69
+ children: React.ReactNode;
70
+ }): React.JSX.Element;
61
71
  type NotFoundBoundaryProps = {
62
72
  fallback: React.ReactNode;
63
73
  children: React.ReactNode;
@@ -78,18 +88,18 @@ type ForbiddenBoundaryProps = {
78
88
  resetKey?: string | null;
79
89
  };
80
90
  type ForbiddenBoundaryInnerProps = {
81
- pathname: string;
91
+ pathname: string | null;
82
92
  } & ForbiddenBoundaryProps;
83
93
  type ForbiddenBoundaryState = {
84
94
  forbidden: boolean;
85
- previousPathname: string;
95
+ previousPathname: string | null;
86
96
  previousResetKey: string | null;
87
97
  };
88
98
  declare class ForbiddenBoundaryInner extends React.Component<ForbiddenBoundaryInnerProps, ForbiddenBoundaryState> {
89
99
  constructor(props: ForbiddenBoundaryInnerProps);
90
100
  static getDerivedStateFromProps(props: ForbiddenBoundaryInnerProps, state: ForbiddenBoundaryState): ForbiddenBoundaryState | null;
91
101
  static getDerivedStateFromError(error: unknown): Partial<ForbiddenBoundaryState>;
92
- render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
102
+ render(): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
93
103
  }
94
104
  declare function ForbiddenBoundary({
95
105
  fallback,
@@ -102,18 +112,18 @@ type UnauthorizedBoundaryProps = {
102
112
  resetKey?: string | null;
103
113
  };
104
114
  type UnauthorizedBoundaryInnerProps = {
105
- pathname: string;
115
+ pathname: string | null;
106
116
  } & UnauthorizedBoundaryProps;
107
117
  type UnauthorizedBoundaryState = {
108
118
  unauthorized: boolean;
109
- previousPathname: string;
119
+ previousPathname: string | null;
110
120
  previousResetKey: string | null;
111
121
  };
112
122
  declare class UnauthorizedBoundaryInner extends React.Component<UnauthorizedBoundaryInnerProps, UnauthorizedBoundaryState> {
113
123
  constructor(props: UnauthorizedBoundaryInnerProps);
114
124
  static getDerivedStateFromProps(props: UnauthorizedBoundaryInnerProps, state: UnauthorizedBoundaryState): UnauthorizedBoundaryState | null;
115
125
  static getDerivedStateFromError(error: unknown): Partial<UnauthorizedBoundaryState>;
116
- render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
126
+ render(): string | number | bigint | boolean | React.JSX.Element | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | null | undefined;
117
127
  }
118
128
  declare function UnauthorizedBoundary({
119
129
  fallback,
@@ -140,4 +150,4 @@ declare class DevRecoveryBoundary extends React.Component<DevRecoveryBoundaryPro
140
150
  render(): React.ReactNode;
141
151
  }
142
152
  //#endregion
143
- export { DevRecoveryBoundary, DevRecoveryBoundaryProps, ErrorBoundary, ErrorBoundaryInner, ErrorBoundaryProps, ErrorBoundaryState, ForbiddenBoundary, ForbiddenBoundaryInner, NotFoundBoundary, RedirectBoundary, RedirectErrorBoundary, UnauthorizedBoundary, UnauthorizedBoundaryInner };
153
+ export { DevRecoveryBoundary, DevRecoveryBoundaryProps, ErrorBoundary, ErrorBoundaryInner, ErrorBoundaryProps, ErrorBoundaryState, ForbiddenBoundary, ForbiddenBoundaryInner, GlobalErrorBoundary, NotFoundBoundary, RedirectBoundary, RedirectErrorBoundary, UnauthorizedBoundary, UnauthorizedBoundaryInner };
@@ -146,6 +146,13 @@ function ErrorBoundary({ fallback, children, resetKey }) {
146
146
  children
147
147
  });
148
148
  }
149
+ function GlobalErrorBoundary({ fallback, children }) {
150
+ return /* @__PURE__ */ jsx(ErrorBoundaryInner, {
151
+ pathname: usePathname(),
152
+ fallback,
153
+ children
154
+ });
155
+ }
149
156
  /**
150
157
  * Inner class component that catches notFound() errors and renders the
151
158
  * not-found.tsx fallback. Resets on the caller's segment reset key when one is
@@ -323,4 +330,4 @@ var DevRecoveryBoundary = class extends React.Component {
323
330
  }
324
331
  };
325
332
  //#endregion
326
- export { DevRecoveryBoundary, ErrorBoundary, ErrorBoundaryInner, ForbiddenBoundary, ForbiddenBoundaryInner, NotFoundBoundary, RedirectBoundary, RedirectErrorBoundary, UnauthorizedBoundary, UnauthorizedBoundaryInner };
333
+ export { DevRecoveryBoundary, ErrorBoundary, ErrorBoundaryInner, ForbiddenBoundary, ForbiddenBoundaryInner, GlobalErrorBoundary, NotFoundBoundary, RedirectBoundary, RedirectErrorBoundary, UnauthorizedBoundary, UnauthorizedBoundaryInner };
@@ -54,6 +54,19 @@ declare function consumeDynamicFetchObservations(): string[];
54
54
  * fetch tags used during rendering.
55
55
  */
56
56
  declare function getCollectedFetchTags(): string[];
57
+ /**
58
+ * Append cache tags to the current request's collected tags.
59
+ *
60
+ * Mirrors Next.js's `propagateCacheLifeAndTagsToRevalidateStore`: tags declared
61
+ * inside a `"use cache"` function (via `cacheTag()`, persisted on the data cache
62
+ * entry) must also bubble up to the surrounding page / route-handler ISR entry
63
+ * so `revalidateTag()` / `revalidatePath()` can evict the rendered output, not
64
+ * just the inner data cache entry. Without this, a cached `"use cache"` result
65
+ * keeps being served from a stale page/route entry after its tag is revalidated
66
+ * (issue #1453). Tags are already encoded by the caller; deduped to match the
67
+ * tagged-fetch path. A no-op for empty input.
68
+ */
69
+ declare function addCollectedRequestTags(tags: readonly string[]): void;
57
70
  /**
58
71
  * Set path-derived implicit tags for fetch cache reads in the current render.
59
72
  *
@@ -110,4 +123,4 @@ declare function ensureFetchPatch(): void;
110
123
  */
111
124
  declare function getOriginalFetch(): typeof globalThis.fetch;
112
125
  //#endregion
113
- export { FetchCacheMode, FetchCacheState, _resetPendingRefetches, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
126
+ export { FetchCacheMode, FetchCacheState, _resetPendingRefetches, addCollectedRequestTags, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
@@ -343,6 +343,23 @@ function getCollectedFetchTags() {
343
343
  return [..._getState().currentRequestTags];
344
344
  }
345
345
  /**
346
+ * Append cache tags to the current request's collected tags.
347
+ *
348
+ * Mirrors Next.js's `propagateCacheLifeAndTagsToRevalidateStore`: tags declared
349
+ * inside a `"use cache"` function (via `cacheTag()`, persisted on the data cache
350
+ * entry) must also bubble up to the surrounding page / route-handler ISR entry
351
+ * so `revalidateTag()` / `revalidatePath()` can evict the rendered output, not
352
+ * just the inner data cache entry. Without this, a cached `"use cache"` result
353
+ * keeps being served from a stale page/route entry after its tag is revalidated
354
+ * (issue #1453). Tags are already encoded by the caller; deduped to match the
355
+ * tagged-fetch path. A no-op for empty input.
356
+ */
357
+ function addCollectedRequestTags(tags) {
358
+ if (tags.length === 0) return;
359
+ const reqTags = _getState().currentRequestTags;
360
+ for (const tag of tags) if (!reqTags.includes(tag)) reqTags.push(tag);
361
+ }
362
+ /**
346
363
  * Set path-derived implicit tags for fetch cache reads in the current render.
347
364
  *
348
365
  * These are intentionally not persisted on fetch entries. They mirror Next.js
@@ -711,4 +728,4 @@ function getOriginalFetch() {
711
728
  return originalFetch;
712
729
  }
713
730
  //#endregion
714
- export { _resetPendingRefetches, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
731
+ export { _resetPendingRefetches, addCollectedRequestTags, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
@@ -3,6 +3,7 @@ declare function decodeHashFragment(fragment: string): string;
3
3
  declare function scrollToHashTarget(hash: string): void;
4
4
  declare function scrollToHashTargetOnNextFrame(hash: string): void;
5
5
  declare function retryScrollTo(x: number, y: number, opts?: {
6
+ minFrames?: number;
6
7
  shouldContinue?: () => boolean;
7
8
  }): void;
8
9
  //#endregion
@@ -25,12 +25,14 @@ function scrollToHashTargetOnNextFrame(hash) {
25
25
  });
26
26
  }
27
27
  function retryScrollTo(x, y, opts) {
28
+ const minFrames = opts?.minFrames ?? 0;
28
29
  const shouldContinue = opts?.shouldContinue ?? (() => true);
29
30
  let attempts = 0;
30
31
  const restore = () => {
31
32
  if (!shouldContinue()) return;
32
33
  window.scrollTo(x, y);
33
- if (!shouldContinue() || Math.abs(window.scrollY - y) <= 1 || attempts >= 60) return;
34
+ const reachedTarget = Math.abs(window.scrollY - y) <= 1;
35
+ if (!shouldContinue() || reachedTarget && attempts >= minFrames || attempts >= 60) return;
34
36
  attempts += 1;
35
37
  requestAnimationFrame(restore);
36
38
  };
@@ -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;
@@ -225,7 +225,7 @@ declare function clearPendingPathname(navId: number): void;
225
225
  * Returns the current pathname.
226
226
  * Server: from request context. Client: from window.location.
227
227
  */
228
- declare function usePathname(): string;
228
+ declare function usePathname(): string | null;
229
229
  /**
230
230
  * Returns the current search params as a read-only URLSearchParams.
231
231
  */
@@ -233,7 +233,7 @@ declare function useSearchParams(): ReadonlyURLSearchParams;
233
233
  /**
234
234
  * Returns the dynamic params for the current route.
235
235
  */
236
- declare function useParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(): T;
236
+ declare function useParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(): T | null;
237
237
  /**
238
238
  * Commit pending client navigation state to committed snapshots.
239
239
  *