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
@@ -15,9 +15,10 @@
15
15
  * `uncaughtException`, where this listener filters it.
16
16
  *
17
17
  * Filters strictly on peer-disconnect codes (ECONNRESET / EPIPE /
18
- * ECONNABORTED) and synchronously re-throws everything else,
19
- * preserving Node's default crash semantics for genuine bugs. This
20
- * is more conservative than Next.js's equivalent
18
+ * ECONNABORTED) plus benign static-asset `import()` rejections (see
19
+ * `isBenignAssetImportError`), and synchronously re-throws everything
20
+ * else, preserving Node's default crash semantics for genuine bugs.
21
+ * This is more conservative than Next.js's equivalent
21
22
  * (`router-server.ts`'s log-only handler), which silently swallows
22
23
  * every uncaught — vinext keeps real bugs surfacing.
23
24
  *
@@ -89,6 +90,68 @@ function peerDisconnectCode(err) {
89
90
  const code = err?.code;
90
91
  return code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" ? code : void 0;
91
92
  }
93
+ const STATIC_ASSET_IMPORT_EXTENSIONS = new Set([
94
+ ".css",
95
+ ".scss",
96
+ ".sass",
97
+ ".less",
98
+ ".styl",
99
+ ".png",
100
+ ".jpg",
101
+ ".jpeg",
102
+ ".gif",
103
+ ".svg",
104
+ ".webp",
105
+ ".avif",
106
+ ".ico",
107
+ ".bmp",
108
+ ".woff",
109
+ ".woff2",
110
+ ".ttf",
111
+ ".otf",
112
+ ".eot",
113
+ ".txt",
114
+ ".md",
115
+ ".csv",
116
+ ".xml",
117
+ ".pdf",
118
+ ".wasm"
119
+ ]);
120
+ /**
121
+ * Pure predicate: returns `true` when `err` is a benign failure from a
122
+ * dynamic `import()` of a static asset URL — the "URL dependency" pattern
123
+ * that Next.js tolerates at build time. Two shapes are recognised:
124
+ *
125
+ * - `ERR_UNKNOWN_FILE_EXTENSION`: the asset resolved on disk but is not an
126
+ * ES module (e.g. a co-located `./style.css`). The error message ends in
127
+ * the offending extension in quotes: `... extension ".css" for /path`.
128
+ * - `ERR_MODULE_NOT_FOUND`: the asset URL did not resolve (the chunk lives
129
+ * in `dist/server/` but the source asset does not). Node attaches the
130
+ * unresolved specifier on `err.url`; we match only when it points at a
131
+ * static-asset extension.
132
+ *
133
+ * Anything outside this allow-list (including missing `.js`/`.mjs`/`.ts`
134
+ * modules with no extension) returns `false` so real bugs still crash.
135
+ * Exported for unit testing in isolation.
136
+ */
137
+ function isBenignAssetImportError(err) {
138
+ if (err === null || typeof err !== "object") return false;
139
+ const code = err.code;
140
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
141
+ const message = err.message ?? "";
142
+ const match = /extension\s+"([^"]+)"/.exec(message);
143
+ return match != null && STATIC_ASSET_IMPORT_EXTENSIONS.has(match[1].toLowerCase());
144
+ }
145
+ if (code === "ERR_MODULE_NOT_FOUND") {
146
+ const url = err.url;
147
+ if (typeof url !== "string") return false;
148
+ const pathname = url.split("?", 1)[0].split("#", 1)[0];
149
+ const dot = pathname.lastIndexOf(".");
150
+ if (dot === -1) return false;
151
+ return STATIC_ASSET_IMPORT_EXTENSIONS.has(pathname.slice(dot).toLowerCase());
152
+ }
153
+ return false;
154
+ }
92
155
  /**
93
156
  * Test-only: returns whether the backstop has been installed in this
94
157
  * process. Used by the unit test to assert idempotent install via the
@@ -104,7 +167,16 @@ function installSocketErrorBackstop() {
104
167
  proc[SOCKET_BACKSTOP_FLAG] = true;
105
168
  const debug = process.env.VINEXT_DEBUG_SOCKET_ERRORS === "1";
106
169
  if (debug) console.warn("[vinext] socket-error backstop installed");
170
+ const absorbBenignAssetImport = (reason, kind) => {
171
+ if (!isBenignAssetImportError(reason)) return false;
172
+ if (debug) {
173
+ const code = reason?.code;
174
+ console.warn(`[vinext] absorbed ${kind} ${code} (asset URL import)`);
175
+ }
176
+ return true;
177
+ };
107
178
  process.on("uncaughtException", (err) => {
179
+ if (absorbBenignAssetImport(err, "uncaughtException")) return;
108
180
  if (process.env.VINEXT_PRERENDER === "1") throw err;
109
181
  const code = peerDisconnectCode(err);
110
182
  if (code) {
@@ -114,6 +186,7 @@ function installSocketErrorBackstop() {
114
186
  throw err;
115
187
  });
116
188
  process.on("unhandledRejection", (reason) => {
189
+ if (absorbBenignAssetImport(reason, "unhandledRejection")) return;
117
190
  if (process.env.VINEXT_PRERENDER === "1") throw reason;
118
191
  const code = peerDisconnectCode(reason);
119
192
  if (code) {
@@ -124,4 +197,4 @@ function installSocketErrorBackstop() {
124
197
  });
125
198
  }
126
199
  //#endregion
127
- export { installSocketErrorBackstop, isSocketErrorBackstopInstalled, peerDisconnectCode };
200
+ export { installSocketErrorBackstop, isBenignAssetImportError, isSocketErrorBackstopInstalled, peerDisconnectCode };
@@ -49,14 +49,22 @@ function getHashFragmentDomNode(hash) {
49
49
  const element = document.getElementById(fragment) ?? document.getElementsByName(fragment)[0];
50
50
  return element instanceof HTMLElement ? element : null;
51
51
  }
52
+ function isInDocumentHead(node) {
53
+ const head = node.ownerDocument?.head;
54
+ return head != null && head.contains(node);
55
+ }
52
56
  function findNextScrollTarget(node) {
53
57
  if (!(node instanceof Element)) return null;
58
+ if (isInDocumentHead(node)) return { kind: "document-top" };
54
59
  let target = node;
55
60
  while (!(target instanceof HTMLElement) || shouldSkipElement(target)) {
56
61
  if (target.nextElementSibling === null) return null;
57
62
  target = target.nextElementSibling;
58
63
  }
59
- return target;
64
+ return {
65
+ kind: "element",
66
+ element: target
67
+ };
60
68
  }
61
69
  function scrollToElement(target, hash) {
62
70
  if (hash !== null) {
@@ -79,9 +87,19 @@ var AppRouterScrollTargetInner = class extends React$1.Component {
79
87
  if (intent === null) return;
80
88
  if (this.props.commitId === null || intent.commitId !== this.props.commitId) return;
81
89
  let target;
82
- if (intent.hash !== null) target = getHashFragmentDomNode(intent.hash);
83
- else target = findNextScrollTarget(findDOMNode(this));
84
- if (target === null) return;
90
+ if (intent.hash !== null) {
91
+ target = getHashFragmentDomNode(intent.hash);
92
+ if (target === null) return;
93
+ } else {
94
+ const next = findNextScrollTarget(findDOMNode(this));
95
+ if (next === null) return;
96
+ if (next.kind === "document-top") {
97
+ if (consumeAppRouterScrollIntent(intent, this.props.commitId) === null) return;
98
+ document.documentElement.scrollTop = 0;
99
+ return;
100
+ }
101
+ target = next.element;
102
+ }
85
103
  const consumed = consumeAppRouterScrollIntent(intent, this.props.commitId);
86
104
  if (consumed === null) return;
87
105
  scrollToElement(target, consumed.hash);
@@ -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, getCurrentFetchSoftTags } from "./fetch-cache.js";
6
7
  //#region src/shims/cache-runtime.ts
7
8
  /**
8
9
  * "use cache" runtime
@@ -268,8 +269,13 @@ function registerCachedFunction(fn, id, variant, options = {}) {
268
269
  }
269
270
  if (isDev) return executeWithContext(fn, args, cacheVariant);
270
271
  const handler = getDataCacheHandler();
271
- const existing = await handler.get(cacheKey, { kind: "FETCH" });
272
+ const softTags = getCurrentFetchSoftTags();
273
+ const existing = await handler.get(cacheKey, {
274
+ kind: "FETCH",
275
+ softTags
276
+ });
272
277
  if (existing?.value && existing.value.kind === "FETCH" && existing.cacheState !== "stale") try {
278
+ propagateCacheTagsToRequest(existing.value.tags);
273
279
  if (rsc && existing.value.data.headers["x-vinext-rsc"] === "1") {
274
280
  const stream = uint8ToStream(base64ToUint8(existing.value.data.body));
275
281
  const result = await rsc.createFromReadableStream(stream);
@@ -282,6 +288,7 @@ function registerCachedFunction(fn, id, variant, options = {}) {
282
288
  } catch {}
283
289
  const { result, ctx, effectiveLife } = await runCachedFunctionWithContext(fn, args, cacheVariant);
284
290
  recordRequestScopedCacheLife(effectiveLife);
291
+ propagateCacheTagsToRequest(ctx.tags);
285
292
  const revalidateSeconds = effectiveLife.revalidate ?? cacheLifeProfiles.default.revalidate ?? 900;
286
293
  try {
287
294
  let body;
@@ -318,8 +325,11 @@ function registerCachedFunction(fn, id, variant, options = {}) {
318
325
  value: fn.length,
319
326
  configurable: true
320
327
  });
328
+ cachedFn[USE_CACHE_FUNCTION_SYMBOL] = true;
321
329
  return cachedFn;
322
330
  }
331
+ /** @internal Symbol used to identify "use cache" wrapper functions. */
332
+ const USE_CACHE_FUNCTION_SYMBOL = Symbol.for("vinext.useCacheFunction");
323
333
  function throwPrivateUseCacheInsidePublicUseCacheError() {
324
334
  const error = /* @__PURE__ */ new Error("\"use cache: private\" must not be used within \"use cache\". It can only be nested inside of another \"use cache: private\".");
325
335
  const ctx = getRequestContext();
@@ -336,6 +346,30 @@ function recordRequestScopedCacheControl(cacheControl) {
336
346
  function recordRequestScopedCacheLife(cacheLife) {
337
347
  _setRequestScopedCacheLife(cacheLife);
338
348
  }
349
+ /**
350
+ * Bubble a `"use cache"` scope's tags toward where they can drive invalidation.
351
+ *
352
+ * When this cache is nested inside another (`parentCtx` present), the tags flow
353
+ * into the parent scope so they end up on the outer cache entry — mirroring
354
+ * Next.js's `propagateCacheLifeAndTagsToRevalidateStore`. The outermost scope
355
+ * (no parent) instead records onto the surrounding request's collected tags, so
356
+ * the enclosing page / route-handler ISR entry carries them and `revalidateTag`
357
+ * can evict the rendered output (issue #1453).
358
+ *
359
+ * Used by both the data cache HIT and MISS paths. On MISS the parent-bubble for
360
+ * the *executed* scope also happens in `runCachedFunctionWithContext`; this keeps
361
+ * the HIT path (where that function never runs) correct without dropping a nested
362
+ * inner entry's stored tags. Deduped to keep tag lists tidy.
363
+ */
364
+ function propagateCacheTagsToRequest(tags) {
365
+ if (!tags || tags.length === 0) return;
366
+ const parentCtx = cacheContextStorage.getStore();
367
+ if (parentCtx) {
368
+ for (const tag of tags) if (!parentCtx.tags.includes(tag)) parentCtx.tags.push(tag);
369
+ return;
370
+ }
371
+ addCollectedRequestTags(tags);
372
+ }
339
373
  async function executeWithContext(fn, args, variant) {
340
374
  const { result, ctx: _ctx, effectiveLife } = await runCachedFunctionWithContext(fn, args, variant);
341
375
  recordRequestScopedCacheLife(effectiveLife);
@@ -360,7 +394,10 @@ async function runCachedFunctionWithContext(fn, args, variant) {
360
394
  const result = await cacheContextStorage.run(ctx, () => fn(...args));
361
395
  if (ctx.invalidDynamicUsageError) throw ctx.invalidDynamicUsageError;
362
396
  const effectiveLife = resolveCacheLife(ctx.lifeConfigs);
363
- if (parentCtx) parentCtx.lifeConfigs.push(effectiveLife);
397
+ if (parentCtx) {
398
+ parentCtx.lifeConfigs.push(effectiveLife);
399
+ for (const tag of ctx.tags) if (!parentCtx.tags.includes(tag)) parentCtx.tags.push(tag);
400
+ }
364
401
  if (parentCtx && eagerError && (effectiveLife.revalidate === 0 || effectiveLife.expire !== void 0 && effectiveLife.expire < DYNAMIC_EXPIRE)) parentCtx.dynamicNestedCacheError ??= eagerError;
365
402
  if (typeof process !== "undefined" && (process.env.VINEXT_PRERENDER === "1" || process.env.NODE_ENV === "development") && ctx.dynamicNestedCacheError) {
366
403
  if (effectiveLife.revalidate === 0 && !ctx.hasExplicitRevalidate) throw new Error(getNestedCacheZeroRevalidateErrorMessage(), { cause: ctx.dynamicNestedCacheError });
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ //#region src/shims/dynamic-preload-chunks.d.ts
4
+ declare function DynamicPreloadChunks(props: {
5
+ moduleIds?: readonly string[];
6
+ }): React.FunctionComponentElement<React.FragmentProps> | null;
7
+ //#endregion
8
+ export { DynamicPreloadChunks };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { useScriptNonce } from "./script-nonce-context.js";
3
+ import React from "react";
4
+ import * as ReactDOM from "react-dom";
5
+ //#region src/shims/dynamic-preload-chunks.tsx
6
+ /**
7
+ * Preload links for rendered next/dynamic() boundaries.
8
+ *
9
+ * This MUST be a "use client" component. next/dynamic() can be called from
10
+ * either a Server Component or a Client Component. If this rendered in the
11
+ * environment of the call site, a Server-Component call site would render it in
12
+ * the RSC environment, where the script-nonce React context is unavailable
13
+ * (createContext is not callable in react-server), so emitted preload links
14
+ * would drop the request CSP nonce — a CSP violation under
15
+ * `script-src 'nonce-…' 'strict-dynamic'`.
16
+ *
17
+ * Marking it "use client" forces it into the SSR pass (where vinext installs
18
+ * the ScriptNonceProvider via withScriptNonce()), so the nonce is available
19
+ * regardless of whether the dynamic() call site is a Server or Client
20
+ * Component. This mirrors Next.js's <PreloadChunks> ('use client') and vinext's
21
+ * own next/script shim.
22
+ *
23
+ * Deliberate divergence from Next.js: for CSS we render
24
+ * `<link rel="stylesheet">` WITHOUT `as="style"`. Next.js emits `as="style"`,
25
+ * but per the HTML spec `as` is only meaningful on `rel="preload"`/`modulepreload`
26
+ * — on `rel="stylesheet"` it is ignored by browsers and is semantically wrong.
27
+ * React keys stylesheet resources on href + precedence, not `as`, so omitting it
28
+ * is safe. This is an intentional, documented difference, not a parity bug.
29
+ */
30
+ function dynamicPreloadHref(file) {
31
+ if (file.startsWith("/") || file.startsWith("http://") || file.startsWith("https://") || file.startsWith("//")) return file;
32
+ return `/${file}`;
33
+ }
34
+ function resolveDynamicPreloadFiles(moduleIds) {
35
+ if (!moduleIds || moduleIds.length === 0) return [];
36
+ const preloadMap = globalThis.__VINEXT_DYNAMIC_PRELOADS__;
37
+ if (!preloadMap) return [];
38
+ const files = [];
39
+ const seen = /* @__PURE__ */ new Set();
40
+ for (const moduleId of moduleIds) for (const file of preloadMap[moduleId] ?? []) {
41
+ if (seen.has(file)) continue;
42
+ seen.add(file);
43
+ files.push(file);
44
+ }
45
+ return files;
46
+ }
47
+ function DynamicPreloadChunks(props) {
48
+ const nonce = useScriptNonce();
49
+ if (typeof window !== "undefined") return null;
50
+ const files = resolveDynamicPreloadFiles(props.moduleIds);
51
+ if (files.length === 0) return null;
52
+ const stylesheets = [];
53
+ for (const file of files) {
54
+ const href = dynamicPreloadHref(file);
55
+ if (href.endsWith(".css")) {
56
+ stylesheets.push(React.createElement("link", {
57
+ key: href,
58
+ rel: "stylesheet",
59
+ href,
60
+ nonce,
61
+ precedence: "dynamic"
62
+ }));
63
+ continue;
64
+ }
65
+ if (href.endsWith(".js") && typeof ReactDOM.preload === "function") {
66
+ const preloadOptions = {
67
+ as: "script",
68
+ fetchPriority: "low",
69
+ nonce
70
+ };
71
+ ReactDOM.preload(href, preloadOptions);
72
+ }
73
+ }
74
+ return stylesheets.length > 0 ? React.createElement(React.Fragment, null, ...stylesheets) : null;
75
+ }
76
+ //#endregion
77
+ export { DynamicPreloadChunks };
@@ -16,6 +16,10 @@ type LoaderFn<P> = () => LoaderComponent<P>;
16
16
  type DynamicOptions<P> = {
17
17
  loading?: ComponentType<DynamicLoadingProps>;
18
18
  loader?: Loader<P>;
19
+ loadableGenerated?: {
20
+ modules?: readonly string[];
21
+ };
22
+ modules?: readonly string[];
19
23
  ssr?: boolean;
20
24
  };
21
25
  type Loader<P> = LoaderFn<P> | LoaderComponent<P>;
@@ -1,3 +1,4 @@
1
+ import { DynamicPreloadChunks } from "./dynamic-preload-chunks.js";
1
2
  import React from "react";
2
3
  //#region src/shims/dynamic.ts
3
4
  /**
@@ -121,8 +122,9 @@ function flushPreloads() {
121
122
  return Promise.all(pending);
122
123
  }
123
124
  function dynamic(dynamicInput, options) {
124
- const { loader: dynamicLoader, loading: LoadingComponent, ssr = true } = normalizeDynamicOptions(dynamicInput, options);
125
+ const { loader: dynamicLoader, loadableGenerated, loading: LoadingComponent, modules, ssr = true } = normalizeDynamicOptions(dynamicInput, options);
125
126
  const loader = dynamicLoader ? normalizeLoader(dynamicLoader) : () => Promise.resolve(() => null);
127
+ const preloadModuleIds = loadableGenerated?.modules ?? modules;
126
128
  if (!ssr) {
127
129
  if (isServer) {
128
130
  const SSRFalse = (_props) => LoadingComponent ? React.createElement(LoadingComponent, createDynamicLoadingProps({ pastDelay: false })) : null;
@@ -174,7 +176,7 @@ function dynamic(dynamicInput, options) {
174
176
  resetKey: 0
175
177
  }, lazyElement);
176
178
  }
177
- return React.createElement(React.Suspense, { fallback }, content);
179
+ return React.createElement(React.Fragment, null, React.createElement(DynamicPreloadChunks, { moduleIds: preloadModuleIds }), React.createElement(React.Suspense, { fallback }, content));
178
180
  };
179
181
  ServerDynamic.displayName = "DynamicServer";
180
182
  return ServerDynamic;
@@ -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<{
@@ -58,6 +58,16 @@ declare function ErrorBoundary({
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,11 +88,11 @@ 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> {
@@ -102,11 +112,11 @@ 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> {
@@ -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 };
@@ -1,5 +1,6 @@
1
+ import { AppRouterContext } from "./internal/app-router-context.js";
1
2
  import { RouterContext } from "./internal/router-context.js";
2
- import { appRouterInstance, isNextRouterError } from "./navigation.js";
3
+ import { isNextRouterError, usePathname } from "./navigation.js";
3
4
  import React from "react";
4
5
  //#region src/shims/error.tsx
5
6
  /**
@@ -39,33 +40,53 @@ function ErrorComponent({ statusCode, title }) {
39
40
  margin: 0
40
41
  } }, displayTitle + "."))));
41
42
  }
43
+ const _CatchErrorAppRouterContext = AppRouterContext ?? React.createContext(null);
42
44
  var _CatchError = class extends React.Component {
43
- static contextType = RouterContext;
45
+ static contextType = _CatchErrorAppRouterContext;
44
46
  static displayName = "unstable_catchError(Next.CatchError)";
45
- state = { error: null };
47
+ constructor(props) {
48
+ super(props);
49
+ this.state = {
50
+ error: null,
51
+ previousPathname: props.pathname
52
+ };
53
+ }
46
54
  static getDerivedStateFromError(thrownValue) {
47
55
  if (isNextRouterError(thrownValue)) throw thrownValue;
48
56
  return { error: { thrownValue } };
49
57
  }
58
+ static getDerivedStateFromProps(props, state) {
59
+ if (props.pathname !== state.previousPathname && state.error) return {
60
+ error: null,
61
+ previousPathname: props.pathname
62
+ };
63
+ return {
64
+ error: state.error,
65
+ previousPathname: props.pathname
66
+ };
67
+ }
50
68
  reset = () => {
51
69
  this.setState({ error: null });
52
70
  };
53
71
  unstable_retry = () => {
54
- if (this.context !== null) throw new Error("`unstable_retry()` can only be used in the App Router. Use `reset()` in the Pages Router.");
55
- if (typeof window === "undefined") throw new Error("`unstable_retry()` can only be used on the client. Call it from a user interaction handler inside the error fallback.");
72
+ if (this.props.isPagesRouter) throw new Error("`unstable_retry()` can only be used in the App Router. Use `reset()` in the Pages Router.");
56
73
  React.startTransition(() => {
57
- appRouterInstance.refresh();
74
+ this.context?.refresh();
58
75
  this.reset();
59
76
  });
60
77
  };
61
78
  render() {
62
79
  if (this.state.error) {
80
+ const Fallback = this.props.fallback;
63
81
  const errorInfo = {
64
82
  error: this.state.error.thrownValue,
65
83
  reset: this.reset,
66
84
  unstable_retry: this.unstable_retry
67
85
  };
68
- return this.props.fallback(this.props.forwardedProps, errorInfo);
86
+ return React.createElement(Fallback, {
87
+ props: this.props.props,
88
+ errorInfo
89
+ });
69
90
  }
70
91
  return this.props.children;
71
92
  }
@@ -79,13 +100,18 @@ var _CatchError = class extends React.Component {
79
100
  * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/catch-error.tsx
80
101
  */
81
102
  function unstable_catchError(fallback) {
82
- const TypedCatchError = _CatchError;
103
+ const Fallback = ({ props, errorInfo }) => fallback(props, errorInfo);
104
+ Fallback.displayName = fallback.name || "CatchErrorFallback";
83
105
  function CatchErrorBoundary(allProps) {
84
106
  const { children, ...rest } = allProps;
107
+ const pathname = usePathname();
108
+ const isPagesRouter = React.useContext(RouterContext) !== null;
85
109
  const forwardedProps = rest;
86
- return React.createElement(TypedCatchError, {
87
- fallback,
88
- forwardedProps
110
+ return React.createElement(_CatchError, {
111
+ fallback: Fallback,
112
+ isPagesRouter,
113
+ pathname,
114
+ props: forwardedProps
89
115
  }, children);
90
116
  }
91
117
  CatchErrorBoundary.displayName = `unstable_catchError(${fallback.name || "CatchErrorFallback"})`;
@@ -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
  *
@@ -62,6 +75,14 @@ declare function getCollectedFetchTags(): string[];
62
75
  * affected route, without permanently coupling a shared fetch entry to one path.
63
76
  */
64
77
  declare function setCurrentFetchSoftTags(tags: string[]): void;
78
+ /**
79
+ * Read the path-derived soft tags for the current render.
80
+ *
81
+ * Used by the "use cache" runtime to pass soft tags to the cache handler
82
+ * so that `revalidatePath()` invalidates "use cache" entries during the
83
+ * affected route's next request, even when the entry carries no hard tags.
84
+ */
85
+ declare function getCurrentFetchSoftTags(): string[];
65
86
  declare function setCurrentFetchCacheMode(mode: FetchCacheMode | null): void;
66
87
  /**
67
88
  * Install the patched fetch and reset per-request tag state.
@@ -110,4 +131,4 @@ declare function ensureFetchPatch(): void;
110
131
  */
111
132
  declare function getOriginalFetch(): typeof globalThis.fetch;
112
133
  //#endregion
113
- export { FetchCacheMode, FetchCacheState, _resetPendingRefetches, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
134
+ export { FetchCacheMode, FetchCacheState, _resetPendingRefetches, addCollectedRequestTags, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getCurrentFetchSoftTags, 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
@@ -352,6 +369,16 @@ function getCollectedFetchTags() {
352
369
  function setCurrentFetchSoftTags(tags) {
353
370
  _getState().currentFetchSoftTags = [...tags];
354
371
  }
372
+ /**
373
+ * Read the path-derived soft tags for the current render.
374
+ *
375
+ * Used by the "use cache" runtime to pass soft tags to the cache handler
376
+ * so that `revalidatePath()` invalidates "use cache" entries during the
377
+ * affected route's next request, even when the entry carries no hard tags.
378
+ */
379
+ function getCurrentFetchSoftTags() {
380
+ return _getState().currentFetchSoftTags;
381
+ }
355
382
  function setCurrentFetchCacheMode(mode) {
356
383
  _getState().currentFetchCacheMode = mode;
357
384
  }
@@ -711,4 +738,4 @@ function getOriginalFetch() {
711
738
  return originalFetch;
712
739
  }
713
740
  //#endregion
714
- export { _resetPendingRefetches, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getOriginalFetch, peekCacheableFetchObservations, peekDynamicFetchObservations, runWithFetchCache, runWithFetchDedupe, setCurrentFetchCacheMode, setCurrentFetchSoftTags, withFetchCache };
741
+ export { _resetPendingRefetches, addCollectedRequestTags, consumeDynamicFetchObservations, ensureFetchPatch, getCollectedFetchTags, getCurrentFetchSoftTags, 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
  };