@wular/pnext 0.0.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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,238 @@
1
+ // Client-side global-error mounting (COMPAT).
2
+ //
3
+ // When a client-side error escapes the root (no error.js boundary caught it, or the root boundary
4
+ // re-threw), Next replaces the ENTIRE document with the app's `global-error.js` component - or, when
5
+ // none exists (or global-error itself throws), a built-in fallback document.
6
+ //
7
+ // pnext hydrates a server-rendered document rather than owning a client render root, so "replace the
8
+ // document" here means rendering the global-error component (or the built-in fallback) into a fresh
9
+ // <body>, in place, client-side. Its reset() soft-refreshes to re-render from the server, and
10
+ // next/navigation hooks used inside the component keep working.
11
+ //
12
+ // Client-escaped errors carry NO digest, so the built-in fallback shows the no-digest message.
13
+
14
+ import { h, render, type ComponentType } from 'preact';
15
+ import { softNavigate } from '../../../api/router';
16
+
17
+ export type GlobalErrorComponent = ComponentType<{ error: unknown; reset: () => void }>;
18
+
19
+ let globalErrorMounted = false;
20
+
21
+ // The app-root global-error.js component, registered by installClientErrors so the in-tree
22
+ // ClientErrorBoundary can escalate straight to it WITHOUT bouncing a re-throw through the window
23
+ // `error` channel - that channel drops the value for a thrown undefined/null (the ErrorEvent's `.error`
24
+ // is undefined), so a primitive escape would be lost.
25
+ let registeredGlobalError: GlobalErrorComponent | undefined;
26
+
27
+ export function registerGlobalErrorComponent(component?: GlobalErrorComponent): void {
28
+ registeredGlobalError = component;
29
+ }
30
+
31
+ /**
32
+ * Escalate a boundary error with no local error.js to the global-error UI,
33
+ * using the registered app-root global-error.js when present. Passes the RAW
34
+ * thrown value through (matching Next: a client `throw undefined`/`null` reaches
35
+ * global-error.js unwrapped, so `${error}` renders 'undefined'/'null').
36
+ */
37
+ export function escalateToGlobalError(error: unknown): void {
38
+ mountGlobalError(error, registeredGlobalError);
39
+ // A client error that escapes every route boundary is, to the page, an UNCAUGHT error: Next still
40
+ // reports it to the window so window.onerror and Playwright's pageerror observe the original.
41
+ // mountGlobalError swaps the document silently, so surface the throw explicitly here. Skip
42
+ // primitive-sentinel escapes and anything non-Error - only a real Error carries a meaningful pageerror.
43
+ if ((process.browser || typeof window !== 'undefined') && error instanceof Error) {
44
+ reportUncaught(error);
45
+ }
46
+ }
47
+
48
+ // Errors already surfaced via reportUncaught: the later natural unhandled
49
+ // rejection of the same value is a duplicate and gets suppressed (install.ts).
50
+ const reportedUncaught = new WeakSet<Error>();
51
+
52
+ export function wasReportedUncaught(error: unknown): boolean {
53
+ return error instanceof Error && reportedUncaught.has(error);
54
+ }
55
+
56
+ // Report an escaped error to the window as an uncaught exception. Prefer the
57
+ // standard `reportError`; fall back to a synthetic 'error' event for engines
58
+ // without it. Wrapped so a reporting failure never masks the global-error UI.
59
+ function reportUncaught(error: Error): void {
60
+ reportedUncaught.add(error);
61
+ try {
62
+ if (typeof reportError === 'function') {
63
+ reportError(error);
64
+ return;
65
+ }
66
+ window.dispatchEvent(new ErrorEvent('error', { error, message: error.message }));
67
+ } catch {
68
+ // ignore — the global-error document is already shown.
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Replace the document body with the global-error UI. Idempotent: once mounted,
74
+ * a second escaping error does not stack another document (matches a single
75
+ * top-level replacement). A soft refresh via reset() clears the flag.
76
+ */
77
+ export function mountGlobalError(error: unknown, component?: GlobalErrorComponent): void {
78
+ if (typeof document === 'undefined' || globalErrorMounted) return;
79
+ globalErrorMounted = true;
80
+
81
+ document.documentElement.id = '__next_error__';
82
+ const root = document.createElement('div');
83
+ document.body.replaceChildren(root);
84
+
85
+ const reset = () => {
86
+ globalErrorMounted = false;
87
+ render(null, root);
88
+ // Re-render the current URL from the server (recovers server + client errors).
89
+ void softNavigate(location.href, { replace: true, scroll: false }).catch(() => {
90
+ location.reload();
91
+ });
92
+ };
93
+
94
+ if (component) {
95
+ try {
96
+ // A user global-error.js owns the whole document (<html>/<body>); render its
97
+ // subtree in place. If it THROWS during render, fall back to the built-in.
98
+ render(h(component, { error, reset }), root);
99
+ return;
100
+ } catch {
101
+ globalErrorMounted = true; // keep mounted; render the fallback below.
102
+ }
103
+ }
104
+
105
+ renderBuiltinFallback(root, error);
106
+ }
107
+
108
+ // Next's DefaultGlobalError document text/markup. A client-escaped error has no
109
+ // digest, so it always shows the non-server "Reload to try again, or go back."
110
+ // message and no ERROR <digest> footer.
111
+ function renderBuiltinFallback(root: HTMLElement, error: unknown): void {
112
+ const digest = (error as { digest?: unknown } | null | undefined)?.digest;
113
+ const isServerError = typeof digest === 'string' && digest.length > 0;
114
+ const message = isServerError
115
+ ? 'A server error occurred. Reload to try again.'
116
+ : 'Reload to try again, or go back.';
117
+
118
+ ensureThemeStyle();
119
+ Object.assign(root.style, {
120
+ fontFamily:
121
+ 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
122
+ height: '100vh',
123
+ display: 'flex',
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ } as Partial<CSSStyleDeclaration>);
127
+
128
+ const card = document.createElement('div');
129
+ Object.assign(card.style, {
130
+ marginTop: '-32px',
131
+ maxWidth: '325px',
132
+ padding: '32px 28px',
133
+ textAlign: 'left',
134
+ } as Partial<CSSStyleDeclaration>);
135
+
136
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
137
+ icon.setAttribute('width', '32');
138
+ icon.setAttribute('height', '32');
139
+ icon.setAttribute('viewBox', '-0.2 -1.5 32 32');
140
+ icon.setAttribute('fill', 'none');
141
+ icon.style.marginBottom = '24px';
142
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
143
+ path.setAttribute('d', 'M16.9328 0C18.0839.000116771 19.1334.658832 19.634 1.69531L31.4299 26.1309C32.0708 27.4588 31.1036 28.9999 29.6291 29H2.00215C.527541 29-.439628 27.4588.201371 26.1309L11.9973 1.69531C12.4979.658823 13.5474 7.75066e-05 14.6984 0H16.9328ZM3.59493 26H28.0363L16.9328 3H14.6984L3.59493 26ZM15.8156 19C16.9202 19.0001 17.8156 19.8955 17.8156 21C17.8156 22.1045 16.9202 22.9999 15.8156 23C14.7111 23 13.8156 22.1046 13.8156 21C13.8156 19.8954 14.7111 19 15.8156 19ZM17.3156 16.5H14.3156V8.5H17.3156V16.5Z');
144
+ path.setAttribute('fill', 'var(--next-error-title)');
145
+ icon.append(path);
146
+
147
+ const title = document.createElement('h1');
148
+ title.textContent = "This page couldn’t load";
149
+ Object.assign(title.style, {
150
+ fontSize: '24px',
151
+ fontWeight: '500',
152
+ letterSpacing: '-0.02em',
153
+ lineHeight: '32px',
154
+ margin: '0 0 12px 0',
155
+ color: 'var(--next-error-title)',
156
+ } as Partial<CSSStyleDeclaration>);
157
+
158
+ const body = document.createElement('p');
159
+ body.textContent = message;
160
+ Object.assign(body.style, {
161
+ fontSize: '14px',
162
+ fontWeight: '400',
163
+ lineHeight: '21px',
164
+ margin: '0 0 20px 0',
165
+ color: 'var(--next-error-message)',
166
+ } as Partial<CSSStyleDeclaration>);
167
+
168
+ const buttonGroup = document.createElement('div');
169
+ Object.assign(buttonGroup.style, { display: 'flex', gap: '8px', alignItems: 'center' });
170
+ const reloadForm = document.createElement('form');
171
+ reloadForm.style.margin = '0';
172
+ reloadForm.addEventListener('submit', event => {
173
+ event.preventDefault();
174
+ location.reload();
175
+ });
176
+ const reload = document.createElement('button');
177
+ reload.type = 'submit';
178
+ reload.textContent = 'Reload';
179
+ Object.assign(reload.style, buttonStyle(false));
180
+ reloadForm.append(reload);
181
+ buttonGroup.append(reloadForm);
182
+ if (!isServerError) {
183
+ const back = document.createElement('button');
184
+ back.type = 'button';
185
+ back.textContent = 'Back';
186
+ Object.assign(back.style, buttonStyle(true));
187
+ back.addEventListener('click', () => {
188
+ if (window.history.length > 1) window.history.back();
189
+ else window.location.href = '/';
190
+ });
191
+ buttonGroup.append(back);
192
+ }
193
+ card.append(icon, title, body, buttonGroup);
194
+ root.append(card);
195
+ if (isServerError) {
196
+ // Next's DefaultGlobalError digest footer (`ERROR <digest>`), fixed to the
197
+ // bottom so operators can correlate the redacted UI with server logs.
198
+ const footer = document.createElement('p');
199
+ footer.textContent = `ERROR ${digest}`;
200
+ Object.assign(footer.style, {
201
+ position: 'fixed',
202
+ bottom: '32px',
203
+ left: '0',
204
+ right: '0',
205
+ textAlign: 'center',
206
+ fontFamily: 'ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace',
207
+ fontSize: '12px',
208
+ lineHeight: '18px',
209
+ fontWeight: '400',
210
+ margin: '0',
211
+ color: 'var(--next-error-digest)',
212
+ } as Partial<CSSStyleDeclaration>);
213
+ root.append(footer);
214
+ }
215
+ if (typeof document.title !== 'undefined') document.title = '500: This page couldn’t load';
216
+ }
217
+
218
+ function buttonStyle(secondary: boolean): Partial<CSSStyleDeclaration> {
219
+ return {
220
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: '32px',
221
+ padding: '0 12px', fontSize: '14px', fontWeight: '500', lineHeight: '20px',
222
+ borderRadius: '6px', cursor: 'pointer',
223
+ color: secondary ? 'var(--next-error-btn-secondary-text)' : 'var(--next-error-btn-text)',
224
+ background: secondary ? 'var(--next-error-btn-secondary-bg)' : 'var(--next-error-btn-bg)',
225
+ border: secondary ? 'var(--next-error-btn-secondary-border)' : 'var(--next-error-btn-border)',
226
+ };
227
+ }
228
+
229
+ const THEME_STYLE_ID = '__pnext_global_error_theme__';
230
+
231
+ function ensureThemeStyle(): void {
232
+ if (document.getElementById(THEME_STYLE_ID)) return;
233
+ const style = document.createElement('style');
234
+ style.id = THEME_STYLE_ID;
235
+ style.textContent =
236
+ ':root{--next-error-bg:#fff;--next-error-text:#171717;--next-error-title:#171717;--next-error-message:#171717;--next-error-digest:#666666;--next-error-btn-text:#fff;--next-error-btn-bg:#171717;--next-error-btn-border:none;--next-error-btn-secondary-text:#171717;--next-error-btn-secondary-bg:transparent;--next-error-btn-secondary-border:1px solid rgba(0,0,0,.08)}@media (prefers-color-scheme:dark){:root{--next-error-bg:#0a0a0a;--next-error-text:#ededed;--next-error-title:#ededed;--next-error-message:#ededed;--next-error-digest:#a0a0a0;--next-error-btn-text:#0a0a0a;--next-error-btn-bg:#ededed;--next-error-btn-border:none;--next-error-btn-secondary-text:#ededed;--next-error-btn-secondary-bg:transparent;--next-error-btn-secondary-border:1px solid rgba(255,255,255,.14)}}body{margin:0;color:var(--next-error-text);background:var(--next-error-bg);}';
237
+ document.head.append(style);
238
+ }
@@ -0,0 +1,217 @@
1
+ // Client error runtime install (COMPAT, client half).
2
+ //
3
+ // Pairs with the server error pipeline: the server owns SSR throws (digest, redaction, boundary/default
4
+ // UI). This module owns CLIENT-side throws that happen AFTER hydration, which the server never sees.
5
+ //
6
+ // PRIMARY = the in-tree `ClientErrorBoundary` wraps every hydrated island and the client page mount.
7
+ // It gives Next's NEAREST-BOUNDARY semantics: the boundary renders the route's error.js, control-flow
8
+ // errors re-throw past it, and reset() soft-refreshes. A boundary that renders its error UI marks the
9
+ // error handled.
10
+ //
11
+ // LAST RESORT = this window error/unhandledrejection listener, handling ONLY what the boundary path
12
+ // could not:
13
+ // 1. CONTROL-FLOW errors that escape - driven to their navigation outcome, never shown.
14
+ // 2. ACTION errors - left to the action-error overlay, ignored here.
15
+ // 3. Errors ALREADY HANDLED by a boundary - ignored, so one failure is never rendered twice.
16
+ // 4. Anything else - a genuinely UNCAUGHT client error with no route error.js - escapes the root and
17
+ // the global-error UI replaces the document.
18
+ //
19
+ // Client-escaped errors carry NO digest, matching Next.
20
+
21
+ import { h, options, render } from 'preact';
22
+ import { handleControlFlowError, isControlFlowError } from './control-flow';
23
+ import {
24
+ wasErrorHandled,
25
+ type RouteErrorComponent,
26
+ type RouteNotFoundComponent,
27
+ } from './error-boundary';
28
+ import {
29
+ escalateToGlobalError,
30
+ mountGlobalError,
31
+ registerGlobalErrorComponent,
32
+ wasReportedUncaught,
33
+ type GlobalErrorComponent,
34
+ } from './global-error';
35
+ import { softRefreshRoute } from './soft-refresh';
36
+
37
+ export { ClientErrorBoundary, toClientError } from './error-boundary';
38
+ export { markErrorHandled, wasErrorHandled } from './error-boundary';
39
+ export type { RouteErrorComponent } from './error-boundary';
40
+ export { isControlFlowError, handleControlFlowError } from './control-flow';
41
+ export { mountGlobalError } from './global-error';
42
+ export { softRefreshRoute } from './soft-refresh';
43
+
44
+ export interface InstallOptions {
45
+ /**
46
+ * The route's nearest error.js component. Now consumed by the entry's
47
+ * ClientErrorBoundary (nearest-boundary primary path); retained here so the
48
+ * install signature stays stable and callers can still thread it.
49
+ */
50
+ errorComponent?: RouteErrorComponent;
51
+ notFoundComponent?: RouteNotFoundComponent;
52
+ /** The app's global-error.js component, when available (see gaps: build thread). */
53
+ globalErrorComponent?: GlobalErrorComponent;
54
+ /**
55
+ * Predicate identifying action errors already handled by the action overlay,
56
+ * so this runtime skips them. Defaults to never-skip when omitted.
57
+ */
58
+ isActionError?: (error: unknown) => boolean;
59
+ }
60
+
61
+ let installed = false;
62
+
63
+ // Set by installClientErrors so ./lazy can replay the errors that escaped
64
+ // before this chunk landed (a throw during hydration or in a first-paint
65
+ // effect) through exactly the same triage.
66
+ let escapedErrorHandler: ((error: unknown) => void) | undefined;
67
+
68
+ /** Replay pre-runtime escapes (./lazy buffers them) through the last resort. */
69
+ export function drainEscapedErrors(errors: unknown[]): void {
70
+ for (const error of errors) escapedErrorHandler?.(error);
71
+ }
72
+
73
+ export function installClientErrors(options: InstallOptions = {}): void {
74
+ if ((!process.browser && typeof window === 'undefined') || installed) return;
75
+ installed = true;
76
+
77
+ const { globalErrorComponent, isActionError } = options;
78
+ // Register so the in-tree ClientErrorBoundary can escalate a no-error.js route
79
+ // straight to global-error (handles `throw undefined`/`null`, which the window
80
+ // channel would drop).
81
+ registerGlobalErrorComponent(globalErrorComponent);
82
+
83
+ const onError = (error: unknown): void => {
84
+ // Already caught + rendered by a route boundary: the boundary owns it.
85
+ if (wasErrorHandled(error)) return;
86
+ // Control-flow first: navigate, never render (covers control errors thrown
87
+ // outside any boundary, and boundary-rethrown ones that reach the window).
88
+ if (isControlFlowError(error)) {
89
+ handleControlFlowError(error);
90
+ return;
91
+ }
92
+ // Action errors belong to the action-error overlay.
93
+ if (isActionError?.(error)) return;
94
+
95
+ // A genuinely uncaught client error with no route error.js to catch it —
96
+ // the global-error last resort replaces the document. Pass the raw value so
97
+ // a non-Error throw renders unwrapped in global-error.js (`${error}`).
98
+ mountGlobalError(error, globalErrorComponent);
99
+ };
100
+ escapedErrorHandler = onError;
101
+
102
+ window.addEventListener('error', event => {
103
+ // Ignore ResourceLoad errors (no `error` object) — only script throws carry one.
104
+ if ('error' in event && event.error === undefined) return;
105
+ // Our own synchronous reportUncaught echo: already mounted + reported.
106
+ if (wasReportedUncaught(event.error)) return;
107
+ if (event.error !== undefined) onError(event.error);
108
+ });
109
+ window.addEventListener('unhandledrejection', event => {
110
+ // The escape below already reported this value synchronously; without
111
+ // preventDefault the page would emit a SECOND uncaught-error report.
112
+ if (wasReportedUncaught(event.reason)) {
113
+ event.preventDefault();
114
+ return;
115
+ }
116
+ onError(event.reason);
117
+ });
118
+
119
+ installRenderEscapeEscalation(globalErrorComponent, isActionError);
120
+ installStreamErrorBoundaries(options.errorComponent, globalErrorComponent);
121
+ }
122
+
123
+ // Synchronous global-error escalation for a render throw no boundary held. Preact signals "nothing
124
+ // caught this" by re-throwing out of its internal boundary walk - which runs inside the microtask that
125
+ // flushes the re-render, so the value reaches the page as an *unhandled rejection* one task later. Next
126
+ // replaces the document during the click that threw, so intercept the escape here and mount
127
+ // global-error right away. The value is re-thrown untouched, so the page still reports it as uncaught
128
+ // and the window listener above still drives control-flow errors to their navigation.
129
+ function installRenderEscapeEscalation(
130
+ globalErrorComponent?: GlobalErrorComponent,
131
+ isActionError?: (error: unknown) => boolean,
132
+ ): void {
133
+ // `_catchError` in preact's source; `__e` in its published (mangled) build.
134
+ const hooks = options as unknown as Record<string, unknown>;
135
+ const key = (['_catchError', '__e'] as const).find(name => typeof hooks[name] === 'function');
136
+ if (!key) return;
137
+ const walkBoundaries = hooks[key] as (...args: unknown[]) => unknown;
138
+ hooks[key] = function patchedCatchError(this: unknown, ...args: unknown[]) {
139
+ try {
140
+ return walkBoundaries.apply(this, args);
141
+ } catch (error) {
142
+ // Handled by a boundary, control flow, or an action error: those already
143
+ // have an owner — only a genuinely uncaught error escapes to global-error.
144
+ // escalate (not mount): the error must also report as uncaught in THIS
145
+ // task, not a task later via the natural unhandled rejection.
146
+ if (!wasErrorHandled(error) && !isControlFlowError(error) && !isActionError?.(error)) {
147
+ escalateToGlobalError(error);
148
+ }
149
+ throw error;
150
+ }
151
+ };
152
+ }
153
+
154
+ // Streamed-Suspense server errors, routed to the route error.js boundary.
155
+ //
156
+ // When a server component inside a streaming Suspense boundary throws, the server can only flush a
157
+ // static replacement chunk - the page shell already streamed with status 200. Next's behavior is to
158
+ // render the NEAREST error.js boundary client-side with the serialized error (redacted message +
159
+ // digest) and working reset()/unstable_retry(). The server stamps the serialized error onto the marker;
160
+ // here we watch for those markers (initial scan plus a MutationObserver, since stream chunks land after
161
+ // the entry module runs) and mount the route error.js over them. Both callbacks soft-refresh, so a
162
+ // since-recovered server render replaces the boundary with real content.
163
+ const STREAM_ERROR_SELECTOR = 'pnext-error[data-pnext-stream-error][data-pnext-error-message]';
164
+
165
+ function installStreamErrorBoundaries(
166
+ errorComponent?: RouteErrorComponent,
167
+ globalErrorComponent?: GlobalErrorComponent,
168
+ ): void {
169
+ if (typeof document === 'undefined') return;
170
+ // Announce the runtime so the server's inline stream-error fallback script
171
+ // (renderer streamErrorDocumentScript) defers to this handler.
172
+ (window as unknown as Record<string, unknown>).__PNEXT_STREAM_ERROR_RUNTIME__ = true;
173
+
174
+ const mountMarker = (marker: Element): void => {
175
+ const message = marker.getAttribute('data-pnext-error-message') ?? '';
176
+ const digest = marker.getAttribute('data-pnext-error-digest') ?? undefined;
177
+ const error = new Error(message) as Error & { digest?: string };
178
+ if (digest) error.digest = digest;
179
+ if (!errorComponent) {
180
+ // No route error.js: Next escalates a streamed-Suspense server error to
181
+ // global-error (the built-in "This page couldn't load" document with the
182
+ // ERROR <digest> footer when none exists — default-error-page-ui suite).
183
+ mountGlobalError(error, globalErrorComponent);
184
+ return;
185
+ }
186
+ const host = document.createElement('div');
187
+ marker.replaceWith(host);
188
+ const reset = () => softRefreshRoute();
189
+ render(h(errorComponent, { error, reset, unstable_retry: reset }), host);
190
+ };
191
+
192
+ const scan = (root: ParentNode): void => {
193
+ for (const marker of Array.from(root.querySelectorAll(STREAM_ERROR_SELECTOR))) {
194
+ mountMarker(marker);
195
+ }
196
+ };
197
+
198
+ const observe = () => {
199
+ scan(document);
200
+ new MutationObserver(mutations => {
201
+ for (const mutation of mutations) {
202
+ for (const node of Array.from(mutation.addedNodes)) {
203
+ if (!(node instanceof Element)) continue;
204
+ if (node.matches(STREAM_ERROR_SELECTOR)) mountMarker(node);
205
+ else scan(node);
206
+ }
207
+ }
208
+ }).observe(document.documentElement, { childList: true, subtree: true });
209
+ };
210
+
211
+ if (document.readyState === 'loading') {
212
+ document.addEventListener('DOMContentLoaded', observe, { once: true });
213
+ } else {
214
+ observe();
215
+ }
216
+ }
217
+
@@ -0,0 +1,53 @@
1
+ // FIRST-PAINT half of the client error runtime. Everything ./install does only ever runs once
2
+ // something has already thrown, so it rides the router runtime's deferred tier instead of first
3
+ // paint. The in-tree ClientErrorBoundary stays eager: it renders.
4
+ //
5
+ // What DOES stay eager is a pair of capture-only listeners: an error thrown during hydration or in
6
+ // a first-paint effect fires long before the idle tier lands, and Next still shows global-error for
7
+ // it. They buffer the value, pull ./install right away, and hand it over - so a healthy page still
8
+ // never fetches the chunk.
9
+ import { registerDeferredInstall } from '../../../api/router';
10
+ import type { InstallOptions } from './install';
11
+
12
+ type ErrorRuntime = typeof import('./install');
13
+
14
+ export function installClientErrors(options: InstallOptions = {}): void {
15
+ if (!process.browser && typeof window === 'undefined') return;
16
+
17
+ let runtime: Promise<ErrorRuntime> | undefined;
18
+ const load = () =>
19
+ (runtime ??= import('./install').then(module => {
20
+ // Drop the capture listeners before the runtime's own go in, so a later
21
+ // error is triaged once rather than by both paths.
22
+ window.removeEventListener('error', onError);
23
+ window.removeEventListener('unhandledrejection', onRejection);
24
+ module.installClientErrors(options);
25
+ return module;
26
+ }));
27
+
28
+ const escaped: unknown[] = [];
29
+ const seen = new WeakSet<object>();
30
+ const capture = (error: unknown) => {
31
+ // The replay itself can throw (control-flow handling, a global-error
32
+ // render), which surfaces as another rejection here. Capturing the same
33
+ // value twice would loop, so object throws are only ever queued once and
34
+ // the chain swallows its own failures.
35
+ if (error !== null && typeof error === 'object') {
36
+ if (seen.has(error)) return;
37
+ seen.add(error);
38
+ }
39
+ escaped.push(error);
40
+ void load()
41
+ .then(module => module.drainEscapedErrors(escaped.splice(0)))
42
+ .catch(() => undefined);
43
+ };
44
+ // Resource-load failures carry no `error` object; only script throws do.
45
+ const onError = (event: ErrorEvent) => {
46
+ if (event.error !== undefined) capture(event.error);
47
+ };
48
+ const onRejection = (event: PromiseRejectionEvent) => capture(event.reason);
49
+ window.addEventListener('error', onError);
50
+ window.addEventListener('unhandledrejection', onRejection);
51
+
52
+ registerDeferredInstall(load);
53
+ }
@@ -0,0 +1,126 @@
1
+ // Preact-core interop: a component that throws a bare non-object value (undefined/null/a string/a
2
+ // number) crashes BEFORE any error boundary - or even `options._catchError` - ever sees it.
3
+ //
4
+ // Preact's diff.js catches the thrown value in a local try/catch and immediately does `if (e.then)` to
5
+ // detect a suspending thenable. When `e` is null/undefined, `e.then` itself throws a TypeError, which
6
+ // escapes diff() entirely - no boundary, no options hook. This is a genuine Preact-core limitation, not
7
+ // fixable from an options hook alone, because the crash happens inside preact's own catch block ahead
8
+ // of `options._catchError`.
9
+ //
10
+ // Fix: intercept the primitive throw ONE LEVEL BELOW preact's diff try/catch, at the component-function
11
+ // call site (wrapped via the `options.vnode` hook), and re-throw a tagged Error instead. A tagged Error
12
+ // has no `.then`, so preact proceeds normally into `options._catchError` and the normal boundary walk
13
+ // runs. Boundaries unwrap the tag to recover the original raw value before handing it to the error UI.
14
+
15
+ const RAW_VALUE = Symbol('pnext.primitiveThrowValue');
16
+
17
+ export interface TaggedPrimitiveThrow extends Error {
18
+ [RAW_VALUE]: unknown;
19
+ }
20
+
21
+ export function tagPrimitiveThrow(value: unknown): TaggedPrimitiveThrow {
22
+ const message = typeof value === 'string' ? value : String(value);
23
+ const error = new Error(message) as TaggedPrimitiveThrow;
24
+ error[RAW_VALUE] = value;
25
+ return error;
26
+ }
27
+
28
+ export function isTaggedPrimitiveThrow(error: unknown): error is TaggedPrimitiveThrow {
29
+ return error instanceof Error && RAW_VALUE in error;
30
+ }
31
+
32
+ /** Unwrap a tagged primitive-throw Error back to the raw value; pass through anything else. */
33
+ export function untagPrimitiveThrow(error: unknown): unknown {
34
+ return isTaggedPrimitiveThrow(error) ? error[RAW_VALUE] : error;
35
+ }
36
+
37
+ /**
38
+ * A value preact's diff.js catch block would crash on when narrowing for a suspending thenable
39
+ * (`e.then`): null and undefined - the only two JS values where the property access itself throws.
40
+ */
41
+ function crashesPreactThenCheck(value: unknown): boolean {
42
+ return value === null || value === undefined;
43
+ }
44
+
45
+ /**
46
+ * Wrap a function/class component so a thrown value that would crash preact's own `e.then` suspense
47
+ * check is re-thrown as a tagged Error instead. Safe to call repeatedly on the same component type -
48
+ * the wrapper is cached.
49
+ */
50
+ const wrapped = new WeakMap<object, unknown>();
51
+
52
+ // Marks a value this function already produced, so re-wrapping is a no-op. REQUIRED for idempotency
53
+ // across preact's own vnode clone-and-re-render path: a component that re-renders IN PLACE
54
+ // shallow-clones its existing vnode and fires `options.vnode` again on that clone - whose `.type` is
55
+ // already OUR wrapper from the previous pass. Without this marker the cache (keyed by the ORIGINAL
56
+ // function) misses and we wrap the wrapper again into a brand-new function object. That second wrapper
57
+ // becomes the vnode's stored type going forward, while any LATER vnode preact creates fresh for the
58
+ // same component position resolves back to the first, singly-wrapped value. Two different function
59
+ // references at the same tree position make preact treat it as a different component type - a full
60
+ // unmount and remount of that subtree instead of an update.
61
+ const ALREADY_WRAPPED = Symbol('pnext.primitiveThrowWrapped');
62
+
63
+ function copyMetadata(from: object, to: object): void {
64
+ for (const key of Object.getOwnPropertyNames(from)) {
65
+ if (key === 'length' || key === 'name' || key === 'prototype') continue;
66
+ const descriptor = Object.getOwnPropertyDescriptor(from, key);
67
+ if (descriptor) Object.defineProperty(to, key, descriptor);
68
+ }
69
+ for (const symbol of Object.getOwnPropertySymbols(from)) {
70
+ const descriptor = Object.getOwnPropertyDescriptor(from, symbol);
71
+ if (descriptor) Object.defineProperty(to, symbol, descriptor);
72
+ }
73
+ }
74
+
75
+ export function wrapComponentForPrimitiveThrows<T extends object>(type: T): T {
76
+ if ((type as { [ALREADY_WRAPPED]?: true })[ALREADY_WRAPPED]) return type;
77
+ const cached = wrapped.get(type);
78
+ if (cached) return cached as T;
79
+ if (typeof type !== 'function') return type;
80
+
81
+ const isClassComponent = Boolean((type as { prototype?: { render?: unknown } }).prototype?.render);
82
+ let result: unknown;
83
+
84
+ if (isClassComponent) {
85
+ // Class components: preact does `new Type(props, context)` then calls
86
+ // `.render()`. Subclass so `render()` is wrapped; static lifecycle methods
87
+ // (getDerivedStateFromError, etc.) and everything else pass through
88
+ // untouched via normal prototype-chain inheritance.
89
+ const Base = type as unknown as new (...args: unknown[]) => { render(...args: unknown[]): unknown };
90
+ class PrimitiveThrowSafe extends Base {
91
+ render(...args: unknown[]) {
92
+ try {
93
+ return super.render(...args);
94
+ } catch (e) {
95
+ if (crashesPreactThenCheck(e)) throw tagPrimitiveThrow(e);
96
+ throw e;
97
+ }
98
+ }
99
+ }
100
+ Object.defineProperty(PrimitiveThrowSafe, 'name', { value: Base.name });
101
+ // Copy static properties AND symbol-keyed metadata (displayName, getDerivedStateFromError,
102
+ // contextType, defaultProps, plus the framework's own client-reference/dynamic-reference/params-
103
+ // scope symbol markers stashed directly on component function objects). resolveServerTree branches
104
+ // on those, so losing them mis-routes a client-reference class through the server-component call
105
+ // path instead of the island path.
106
+ copyMetadata(Base, PrimitiveThrowSafe);
107
+ result = PrimitiveThrowSafe;
108
+ } else {
109
+ const fn = type as unknown as (...args: unknown[]) => unknown;
110
+ const wrappedFn = function pnextPrimitiveThrowSafe(this: unknown, ...args: unknown[]) {
111
+ try {
112
+ return fn.apply(this, args);
113
+ } catch (e) {
114
+ if (crashesPreactThenCheck(e)) throw tagPrimitiveThrow(e);
115
+ throw e;
116
+ }
117
+ };
118
+ Object.defineProperty(wrappedFn, 'name', { value: fn.name });
119
+ copyMetadata(fn, wrappedFn);
120
+ result = wrappedFn;
121
+ }
122
+
123
+ Object.defineProperty(result as object, ALREADY_WRAPPED, { value: true });
124
+ wrapped.set(type, result);
125
+ return result as T;
126
+ }
@@ -0,0 +1,14 @@
1
+ import { softNavigate } from '../../../api/router';
2
+
3
+ /**
4
+ * Soft-refresh the current URL from the server (re-runs the hydrated subtree so a data-dependent
5
+ * failure re-fetches on retry). Shared reset() behaviour for the ClientErrorBoundary the entry wraps
6
+ * around islands and the page mount. Kept in its own module so the entry can reach it without pulling
7
+ * ./install - which rides the deferred tier - into first paint.
8
+ */
9
+ export function softRefreshRoute(): void {
10
+ if (!process.browser && typeof window === 'undefined') return;
11
+ void softNavigate(location.href, { replace: true, scroll: false, remount: true }).catch(() => {
12
+ location.reload();
13
+ });
14
+ }