@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.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- 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
|
+
}
|