@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,269 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { nextCompatEnabled } from '../compat';
|
|
4
|
+
import type { ResolvedConfig } from '../config';
|
|
5
|
+
import type { ResponseFinalizer, ResponseFinalizerContext } from '../extensions';
|
|
6
|
+
import { getNextConfig } from './next/config-loader';
|
|
7
|
+
import {
|
|
8
|
+
evaluateConditions,
|
|
9
|
+
parseConditions,
|
|
10
|
+
substitute,
|
|
11
|
+
type RewriteCondition,
|
|
12
|
+
type RewriteRequestContext,
|
|
13
|
+
} from './next/rewrites';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_EXPIRE_TIME = 31536000;
|
|
16
|
+
|
|
17
|
+
interface HeaderRule {
|
|
18
|
+
regex: RegExp;
|
|
19
|
+
paramNames: string[];
|
|
20
|
+
headers: [string, string][];
|
|
21
|
+
has: RewriteCondition[];
|
|
22
|
+
missing: RewriteCondition[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface HeaderEntry {
|
|
26
|
+
source?: string;
|
|
27
|
+
headers?: unknown;
|
|
28
|
+
has?: unknown;
|
|
29
|
+
missing?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RevalidateRule {
|
|
33
|
+
regex: RegExp;
|
|
34
|
+
revalidateSeconds: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ConfigHeader {
|
|
38
|
+
key?: unknown;
|
|
39
|
+
value?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cacheControlFinalizers(config: ResolvedConfig): ResponseFinalizer[] {
|
|
43
|
+
if (!nextCompatEnabled(config)) return [];
|
|
44
|
+
|
|
45
|
+
let rules: Promise<HeaderRule[]> | undefined;
|
|
46
|
+
let pagesRevalidateRules: RevalidateRule[] | undefined;
|
|
47
|
+
const finalizer: ResponseFinalizer = async ctx => {
|
|
48
|
+
rules ??= loadHeaderRules();
|
|
49
|
+
applyConfigHeaders(ctx, await rules);
|
|
50
|
+
pagesRevalidateRules ??= loadPagesRevalidateRules(config.root);
|
|
51
|
+
applyDefaultIsrHeader(ctx, pagesRevalidateRules);
|
|
52
|
+
};
|
|
53
|
+
return [finalizer];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadHeaderRules(): Promise<HeaderRule[]> {
|
|
57
|
+
const producer = getNextConfig().headers;
|
|
58
|
+
if (typeof producer !== 'function') return [];
|
|
59
|
+
|
|
60
|
+
let entries: unknown;
|
|
61
|
+
try {
|
|
62
|
+
entries = await (producer as () => unknown)();
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(entries)) return [];
|
|
67
|
+
|
|
68
|
+
const rules: HeaderRule[] = [];
|
|
69
|
+
for (const entry of entries as HeaderEntry[]) {
|
|
70
|
+
if (!entry?.source || !Array.isArray(entry.headers)) continue;
|
|
71
|
+
const compiled = compileHeaderSource(entry.source);
|
|
72
|
+
if (!compiled) continue;
|
|
73
|
+
const has = parseConditions(entry.has);
|
|
74
|
+
const missing = parseConditions(entry.missing);
|
|
75
|
+
if (!has || !missing) continue;
|
|
76
|
+
const headers = headerPairs(entry.headers);
|
|
77
|
+
if (headers.length === 0) continue;
|
|
78
|
+
rules.push({ ...compiled, headers, has, missing });
|
|
79
|
+
}
|
|
80
|
+
return rules;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Compile a next.config `headers`/`redirects` source pattern into a pathname regex. Mirrors the
|
|
85
|
+
* rewrites source compiler for `:param` tokens but, unlike it, escapes literal regex-special characters
|
|
86
|
+
* in plain segments (the `.` in `/favicon.ico`, say) instead of rejecting the rule - Next's
|
|
87
|
+
* path-to-regexp treats those as literals. Raw regex bodies still only appear inside `:param(...)`.
|
|
88
|
+
*/
|
|
89
|
+
function compileHeaderSource(source: string): { regex: RegExp; paramNames: string[] } | null {
|
|
90
|
+
const paramNames: string[] = [];
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
for (const segment of source.split('/').filter(Boolean)) {
|
|
93
|
+
const custom = /^:([A-Za-z_$][\w$]*)\((.*)\)$/.exec(segment);
|
|
94
|
+
if (custom?.[1] !== undefined && custom[2] !== undefined) {
|
|
95
|
+
paramNames.push(custom[1]);
|
|
96
|
+
parts.push(`(${custom[2]})`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const catchAll = /^:([A-Za-z_$][\w$]*)[*+]$/.exec(segment);
|
|
100
|
+
if (catchAll?.[1]) {
|
|
101
|
+
paramNames.push(catchAll[1]);
|
|
102
|
+
parts.push('(.*)');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const optional = /^:([A-Za-z_$][\w$]*)\?$/.exec(segment);
|
|
106
|
+
if (optional?.[1]) {
|
|
107
|
+
paramNames.push(optional[1]);
|
|
108
|
+
parts.push('([^/]*)');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const param = /^:([A-Za-z_$][\w$]*)$/.exec(segment);
|
|
112
|
+
if (param?.[1]) {
|
|
113
|
+
paramNames.push(param[1]);
|
|
114
|
+
parts.push('([^/]+)');
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Plain literal segment: escape regex-special characters so `.` etc. match
|
|
118
|
+
// literally rather than acting as regex metacharacters.
|
|
119
|
+
parts.push(segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
120
|
+
}
|
|
121
|
+
return { regex: new RegExp(`^/${parts.join('/')}/?$`), paramNames };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function headerPairs(headers: unknown[]): [string, string][] {
|
|
125
|
+
const pairs: [string, string][] = [];
|
|
126
|
+
for (const header of headers as ConfigHeader[]) {
|
|
127
|
+
if (typeof header?.key !== 'string' || typeof header.value !== 'string') continue;
|
|
128
|
+
pairs.push([header.key, header.value]);
|
|
129
|
+
}
|
|
130
|
+
return pairs;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyConfigHeaders(ctx: ResponseFinalizerContext, rules: readonly HeaderRule[]): void {
|
|
134
|
+
if (rules.length === 0) return;
|
|
135
|
+
const url = ctx.request.url;
|
|
136
|
+
const requestCtx: RewriteRequestContext = {
|
|
137
|
+
host: url.host,
|
|
138
|
+
headers: ctx.request.headers,
|
|
139
|
+
cookies: parseCookieHeader(ctx.request.headers.get('cookie')),
|
|
140
|
+
query: url.searchParams,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Config `Set-Cookie` headers must be appended (each entry is its own header,
|
|
144
|
+
// never overwriting) and must precede the response's own Set-Cookie headers —
|
|
145
|
+
// Next applies config headers at the router layer before the route's own
|
|
146
|
+
// cookies (set-cookies suite asserts [...nextConfigHeaders, ...cookies]).
|
|
147
|
+
const configSetCookies: string[] = [];
|
|
148
|
+
for (const rule of rules) {
|
|
149
|
+
const match = rule.regex.exec(url.pathname);
|
|
150
|
+
if (!match) continue;
|
|
151
|
+
const params: Record<string, string> = {};
|
|
152
|
+
rule.paramNames.forEach((name, index) => {
|
|
153
|
+
params[name] = match[index + 1] ?? '';
|
|
154
|
+
});
|
|
155
|
+
const conditionParams = evaluateConditions(rule, requestCtx);
|
|
156
|
+
if (conditionParams === null) continue;
|
|
157
|
+
Object.assign(params, conditionParams);
|
|
158
|
+
for (const [key, value] of rule.headers) {
|
|
159
|
+
const resolved = substitute(value, params).pathname;
|
|
160
|
+
if (key.toLowerCase() === 'set-cookie') configSetCookies.push(resolved);
|
|
161
|
+
else ctx.headers.set(key, resolved);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (configSetCookies.length > 0) {
|
|
165
|
+
const existing =
|
|
166
|
+
(ctx.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
|
|
167
|
+
ctx.headers.delete('set-cookie');
|
|
168
|
+
for (const cookie of configSetCookies) ctx.headers.append('set-cookie', cookie);
|
|
169
|
+
for (const cookie of existing) ctx.headers.append('set-cookie', cookie);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function applyDefaultIsrHeader(
|
|
174
|
+
ctx: ResponseFinalizerContext,
|
|
175
|
+
pagesRevalidateRules: readonly RevalidateRule[],
|
|
176
|
+
): void {
|
|
177
|
+
const pagesRevalidateSeconds = pagesRevalidateRules.find(rule =>
|
|
178
|
+
rule.regex.test(ctx.request.url.pathname),
|
|
179
|
+
)?.revalidateSeconds;
|
|
180
|
+
const existing = ctx.headers.get('cache-control');
|
|
181
|
+
// An RSC payload is never browser-cacheable: the flight response embeds the
|
|
182
|
+
// render's data, so a cached copy replays stale content after a revalidation
|
|
183
|
+
// (actions "should invalidate client cache when path is revalidated"). Match
|
|
184
|
+
// the RSC document path in register-segment, which stamps `private, no-store`.
|
|
185
|
+
if (isRscPayload(ctx)) {
|
|
186
|
+
if (existing === null || isBrowserCacheable(existing)) {
|
|
187
|
+
ctx.headers.set('cache-control', 'private, no-store');
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (existing && (pagesRevalidateSeconds === undefined || existing !== 'no-store')) return;
|
|
192
|
+
const revalidateSeconds =
|
|
193
|
+
pagesRevalidateSeconds ??
|
|
194
|
+
(ctx.routeMode === 'isr' ? ctx.hints?.revalidateSeconds : undefined);
|
|
195
|
+
if (typeof revalidateSeconds !== 'number' || revalidateSeconds <= 0) return;
|
|
196
|
+
const config = getNextConfig();
|
|
197
|
+
const expireTime =
|
|
198
|
+
typeof config.expireTime === 'number' ? config.expireTime : DEFAULT_EXPIRE_TIME;
|
|
199
|
+
const swr = Math.max(0, expireTime - revalidateSeconds);
|
|
200
|
+
ctx.headers.set('cache-control', `s-maxage=${revalidateSeconds}, stale-while-revalidate=${swr}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A flight response: core's `data` route kind, or an `_rsc` cache-busted URL. */
|
|
204
|
+
function isRscPayload(ctx: ResponseFinalizerContext): boolean {
|
|
205
|
+
return ctx.routeKind === 'data' || ctx.request.url.searchParams.has('_rsc');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** True for a cache-control a browser may reuse (no `no-store`/`no-cache`). */
|
|
209
|
+
function isBrowserCacheable(value: string): boolean {
|
|
210
|
+
return !/\bno-store\b|\bno-cache\b/i.test(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function loadPagesRevalidateRules(root: string): RevalidateRule[] {
|
|
214
|
+
const pagesDir = path.join(root, 'pages');
|
|
215
|
+
if (!existsSync(pagesDir)) return [];
|
|
216
|
+
const rules: RevalidateRule[] = [];
|
|
217
|
+
for (const file of walkFiles(pagesDir)) {
|
|
218
|
+
const source = readFileSync(file, 'utf8');
|
|
219
|
+
if (!/\bgetStaticProps\b/.test(source)) continue;
|
|
220
|
+
const match = /\brevalidate\s*:\s*(\d+)/.exec(source);
|
|
221
|
+
if (!match?.[1]) continue;
|
|
222
|
+
const regex = pagesRouteRegex(path.relative(pagesDir, file));
|
|
223
|
+
if (!regex) continue;
|
|
224
|
+
rules.push({ regex, revalidateSeconds: Number(match[1]) });
|
|
225
|
+
}
|
|
226
|
+
return rules;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function walkFiles(dir: string): string[] {
|
|
230
|
+
const files: string[] = [];
|
|
231
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
232
|
+
const file = path.join(dir, entry.name);
|
|
233
|
+
if (entry.isDirectory()) files.push(...walkFiles(file));
|
|
234
|
+
else if (/\.(?:tsx|ts|jsx|js|mjs)$/.test(entry.name)) files.push(file);
|
|
235
|
+
}
|
|
236
|
+
return files;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function pagesRouteRegex(relative: string): RegExp | undefined {
|
|
240
|
+
const withoutExt = relative.replace(/\.(?:tsx|ts|jsx|js|mjs)$/, '').split(path.sep).join('/');
|
|
241
|
+
const route = withoutExt === 'index' ? '' : withoutExt.replace(/\/index$/, '');
|
|
242
|
+
const parts = route.split('/').filter(Boolean);
|
|
243
|
+
const pattern = parts
|
|
244
|
+
.map(part => {
|
|
245
|
+
if (/^\[\[\.\.\.[^\]]+\]\]$/.test(part)) return '(?:/.*)?';
|
|
246
|
+
if (/^\[\.\.\.[^\]]+\]$/.test(part)) return '/.+';
|
|
247
|
+
if (/^\[[^\]]+\]$/.test(part)) return '/[^/]+';
|
|
248
|
+
return `/${escapeRegex(part)}`;
|
|
249
|
+
})
|
|
250
|
+
.join('');
|
|
251
|
+
return new RegExp(`^${pattern || '/'}\\/?$`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function escapeRegex(value: string): string {
|
|
255
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseCookieHeader(header: string | null): Record<string, string> {
|
|
259
|
+
const cookies: Record<string, string> = {};
|
|
260
|
+
if (!header) return cookies;
|
|
261
|
+
for (const part of header.split(';')) {
|
|
262
|
+
const index = part.indexOf('=');
|
|
263
|
+
if (index === -1) continue;
|
|
264
|
+
const name = part.slice(0, index).trim();
|
|
265
|
+
if (!name) continue;
|
|
266
|
+
cookies[name] = decodeURIComponent(part.slice(index + 1).trim());
|
|
267
|
+
}
|
|
268
|
+
return cookies;
|
|
269
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Client basePath helpers (COMPAT - ships to the browser).
|
|
2
|
+
//
|
|
3
|
+
// The server injects `window.__PNEXT_BASE_PATH__` when `next.config.basePath` is set. The core router
|
|
4
|
+
// treats every same-origin URL as soft-navigable, but under a basePath an absolute same-host URL
|
|
5
|
+
// *outside* the basePath (a `basePath:false` rewrite target, say) must be a real MPA navigation, not a
|
|
6
|
+
// soft swap. These helpers give the core router that boundary check without a next.config dependency.
|
|
7
|
+
//
|
|
8
|
+
// Core reads them through a registered hook. A pure-core app never sets the global, so basePath() is ''
|
|
9
|
+
// and every same-origin URL stays soft-navigable exactly as before.
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__PNEXT_BASE_PATH__?: string;
|
|
14
|
+
}
|
|
15
|
+
var __PNEXT_BASE_PATH__: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The configured basePath ('' when unset). Isomorphic: the client reads the
|
|
20
|
+
* `window` global the server injects (register-render basePathScript); the
|
|
21
|
+
* server reads the `globalThis` global set when next.config is loaded
|
|
22
|
+
* (config-loader setNextConfig). Neither branch imports the esbuild-backed
|
|
23
|
+
* config loader, so this stays safe to ship to the browser.
|
|
24
|
+
*/
|
|
25
|
+
export function basePath(): string {
|
|
26
|
+
if (process.browser || typeof window !== 'undefined') return window.__PNEXT_BASE_PATH__ ?? '';
|
|
27
|
+
return globalThis.__PNEXT_BASE_PATH__ ?? '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Prefix a root-relative in-app path with the configured basePath (Next's
|
|
32
|
+
* addBasePath). Absolute URLs, protocol-relative URLs, non-root-relative
|
|
33
|
+
* strings, and paths already under the basePath are returned unchanged. A
|
|
34
|
+
* no-basePath app is always a no-op.
|
|
35
|
+
*/
|
|
36
|
+
export function addBasePath(path: string): string {
|
|
37
|
+
const base = basePath();
|
|
38
|
+
if (!base) return path;
|
|
39
|
+
if (!path.startsWith('/') || path.startsWith('//')) return path;
|
|
40
|
+
if (path === '/') return base;
|
|
41
|
+
if (path === base || path.startsWith(`${base}/`)) return path;
|
|
42
|
+
return `${base}${path}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Strip the configured basePath prefix from an in-app path (inverse of addBasePath). */
|
|
46
|
+
export function stripBasePath(path: string): string {
|
|
47
|
+
const base = basePath();
|
|
48
|
+
if (!base) return path;
|
|
49
|
+
if (path === base) return '/';
|
|
50
|
+
if (path.startsWith(`${base}/`)) return path.slice(base.length);
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* True when `url` is same-origin but falls outside the configured basePath - i.e. the router must
|
|
56
|
+
* hard-navigate to it (a `basePath:false` target). Always false when no basePath is configured.
|
|
57
|
+
*/
|
|
58
|
+
export function isOutsideBasePath(url: URL): boolean {
|
|
59
|
+
const base = basePath();
|
|
60
|
+
if (!base) return false;
|
|
61
|
+
if ((process.browser || typeof window !== 'undefined') && url.origin !== window.location.origin) return false;
|
|
62
|
+
const path = url.pathname;
|
|
63
|
+
return path !== base && !path.startsWith(`${base}/`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Next preserves the incoming document's stylesheet precedence on soft nav.
|
|
2
|
+
// Shared links otherwise retain their old position while newly-needed links
|
|
3
|
+
// append, which reverses ties in the CSS cascade after a route transition.
|
|
4
|
+
export function reconcileNextStylesheets(incoming: Document) {
|
|
5
|
+
const order = new Map<string, number>();
|
|
6
|
+
for (const link of incoming.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"][href]')) {
|
|
7
|
+
const href = absoluteHref(link);
|
|
8
|
+
if (href && nextStylesheet(href) && !order.has(href)) order.set(href, order.size);
|
|
9
|
+
}
|
|
10
|
+
if (order.size < 2) return;
|
|
11
|
+
|
|
12
|
+
const current = [...document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"][href]')].filter(
|
|
13
|
+
link => order.has(link.href),
|
|
14
|
+
);
|
|
15
|
+
if (current.length < 2) return;
|
|
16
|
+
|
|
17
|
+
const rank = (link: HTMLLinkElement) => order.get(link.href) ?? 0;
|
|
18
|
+
if (current.every((link, index) => index === 0 || rank(current[index - 1]!) <= rank(link))) return;
|
|
19
|
+
|
|
20
|
+
const sorted = [...current].sort((left, right) => rank(left) - rank(right));
|
|
21
|
+
let previous = sorted[0]!;
|
|
22
|
+
for (const link of sorted.slice(1)) {
|
|
23
|
+
if (previous.nextSibling !== link) previous.after(link);
|
|
24
|
+
previous = link;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function absoluteHref(link: HTMLLinkElement) {
|
|
29
|
+
const href = link.getAttribute('href');
|
|
30
|
+
return href ? new URL(href, location.href).href : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nextStylesheet(href: string) {
|
|
34
|
+
const url = new URL(href, location.href);
|
|
35
|
+
return url.origin === location.origin && url.pathname.startsWith('/assets/');
|
|
36
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Client-side control-flow error handling (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// notFound()/forbidden()/unauthorized()/redirect() called during a CLIENT-side render throw the same
|
|
4
|
+
// control-flow errors the server throws. React routes them to the nearest error boundary, but - exactly
|
|
5
|
+
// like Next - a boundary must NOT render its error UI for one: it must RE-THROW so the control error
|
|
6
|
+
// propagates. Client-side there is no server render pass to catch it, so this runtime intercepts the
|
|
7
|
+
// propagated error and drives the browser to the correct outcome:
|
|
8
|
+
//
|
|
9
|
+
// - notFound / forbidden / unauthorized: soft-navigate to the CURRENT url so the server re-renders
|
|
10
|
+
// it. On that render the same control-flow call fires server-side (deterministic for the same
|
|
11
|
+
// inputs) and the server resolves the nearest boundary with the right status.
|
|
12
|
+
// - redirect / permanentRedirect: navigate to the target (soft for same-origin, hard for
|
|
13
|
+
// cross-origin), matching redirect()'s browser path.
|
|
14
|
+
//
|
|
15
|
+
// Detection reuses the core control-flow predicates so the digest scheme stays single-sourced.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
isForbiddenError,
|
|
19
|
+
isNotFoundError,
|
|
20
|
+
isRedirectError,
|
|
21
|
+
isUnauthorizedError,
|
|
22
|
+
type PNextRedirectError,
|
|
23
|
+
} from '../../../api/navigation';
|
|
24
|
+
import { isPostpone } from '../../../ppr-postpone';
|
|
25
|
+
import { softNavigate } from '../../../api/router';
|
|
26
|
+
|
|
27
|
+
/** True for a Next control-flow error that a boundary must re-throw, not render. */
|
|
28
|
+
export function isControlFlowError(error: unknown): boolean {
|
|
29
|
+
return (
|
|
30
|
+
isRedirectError(error) ||
|
|
31
|
+
isNotFoundError(error) ||
|
|
32
|
+
isForbiddenError(error) ||
|
|
33
|
+
isUnauthorizedError(error) ||
|
|
34
|
+
isPostpone(error)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Drive the browser to the outcome a client-side control-flow error demands.
|
|
40
|
+
* Returns true when the error was a control-flow error (and has been handled),
|
|
41
|
+
* false otherwise (a real error the caller must surface to a boundary).
|
|
42
|
+
*/
|
|
43
|
+
export function handleControlFlowError(error: unknown): boolean {
|
|
44
|
+
if (isRedirectError(error)) {
|
|
45
|
+
applyRedirect(error);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (isNotFoundError(error) || isForbiddenError(error) || isUnauthorizedError(error)) {
|
|
49
|
+
// Re-render the current URL server-side: the same control-flow call fires
|
|
50
|
+
// during that render and the server resolves the matching boundary + status.
|
|
51
|
+
// replace:true keeps history intact (the URL did not change); scroll:false
|
|
52
|
+
// avoids a jump. Fall back to a full reload if the soft-nav itself fails.
|
|
53
|
+
if (process.browser || typeof window !== 'undefined') {
|
|
54
|
+
void softNavigate(location.href, { replace: true, scroll: false }).catch(() => {
|
|
55
|
+
location.reload();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (isPostpone(error)) {
|
|
61
|
+
// A postpone escaping to the client has nothing to resume against here; a
|
|
62
|
+
// reload re-renders from the server. Treated as handled so it never lands
|
|
63
|
+
// in an error boundary as a "real" error.
|
|
64
|
+
if (process.browser || typeof window !== 'undefined') location.reload();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyRedirect(error: PNextRedirectError): void {
|
|
71
|
+
if (!process.browser && typeof window === 'undefined') return;
|
|
72
|
+
const target = error.location;
|
|
73
|
+
let sameOrigin = false;
|
|
74
|
+
try {
|
|
75
|
+
sameOrigin = new URL(target, location.href).origin === location.origin;
|
|
76
|
+
} catch {
|
|
77
|
+
sameOrigin = false;
|
|
78
|
+
}
|
|
79
|
+
const replace = error.status === 303 || error.status === 307 || error.status === 308;
|
|
80
|
+
if (sameOrigin) {
|
|
81
|
+
void softNavigate(target, { replace }).catch(() => {
|
|
82
|
+
hardNavigate(target, replace);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
hardNavigate(target, replace);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hardNavigate(target: string, replace: boolean): void {
|
|
90
|
+
if (replace) location.replace(target);
|
|
91
|
+
else location.assign(target);
|
|
92
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Client-side error boundary (COMPAT).
|
|
2
|
+
//
|
|
3
|
+
// A preact class error boundary wrapping a hydrated route/island subtree so a CLIENT-side throw
|
|
4
|
+
// post-hydration is caught and rendered by the route's `error.js` instead of tearing down the whole
|
|
5
|
+
// preact tree. Mirrors Next's boundary:
|
|
6
|
+
//
|
|
7
|
+
// - Control-flow errors are RE-THROWN, never rendered - the install layer's window handler drives
|
|
8
|
+
// them to the correct navigation outcome. A boundary that rendered UI for these would swallow the
|
|
9
|
+
// control flow.
|
|
10
|
+
// - Client-thrown errors have NO digest (only server errors carry one), so the error prop is a plain
|
|
11
|
+
// Error whose message is the throw's String(value); a thrown undefined/null/string passes through
|
|
12
|
+
// unwrapped, matching Next's client channel.
|
|
13
|
+
// - reset() clears the caught error and re-renders the children in place, recovering a
|
|
14
|
+
// client-component error whose failure lived in the hydrated subtree. unstable_retry() additionally
|
|
15
|
+
// soft-refreshes, which is what a data-dependent (server) failure needs.
|
|
16
|
+
//
|
|
17
|
+
// When no route error component is available the boundary mounts global-error.
|
|
18
|
+
|
|
19
|
+
import { Component, h, type ComponentChildren, type ComponentType } from 'preact';
|
|
20
|
+
import { isControlFlowError, handleControlFlowError } from './control-flow';
|
|
21
|
+
import { isNotFoundError } from '../../../api/navigation';
|
|
22
|
+
import { untagPrimitiveThrow } from './primitive-throw';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// KNOWN GAP (preact core, not fully fixable from an options hook): a CLIENT
|
|
26
|
+
// `throw undefined` / `throw null` can arrive here as a synthetic TypeError when
|
|
27
|
+
// preact dereferences `thrown.then` before boundary interception.
|
|
28
|
+
// Normalize that sentinel back to raw nullish values so error UIs render as
|
|
29
|
+
// `undefined`/`null`, matching Next behavior.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export type RouteErrorComponent = ComponentType<{
|
|
33
|
+
error: unknown;
|
|
34
|
+
reset: () => void;
|
|
35
|
+
unstable_retry?: () => void;
|
|
36
|
+
}>;
|
|
37
|
+
export type RouteNotFoundComponent = ComponentType<Record<string, never>>;
|
|
38
|
+
|
|
39
|
+
interface BoundaryProps {
|
|
40
|
+
/** The route's nearest error.js component; when absent the boundary escalates. */
|
|
41
|
+
errorComponent?: RouteErrorComponent;
|
|
42
|
+
notFoundComponent?: RouteNotFoundComponent;
|
|
43
|
+
/**
|
|
44
|
+
* Next's default 404 UI, handed in by the generated runtime - and emitted by it ONLY when some route
|
|
45
|
+
* can reach this fallback (no not-found.* of its own). An app whose boundary routes all have one
|
|
46
|
+
* never ships error-ui.ts.
|
|
47
|
+
*/
|
|
48
|
+
builtInNotFound?: RouteNotFoundComponent;
|
|
49
|
+
/** Soft-refresh invoked by reset() after clearing the error. */
|
|
50
|
+
onReset?: () => void;
|
|
51
|
+
children?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BoundaryState {
|
|
55
|
+
// Raw thrown value passed to the error component. A client `throw undefined`/
|
|
56
|
+
// `null`/a string reaches error.js unwrapped (Next passes the raw value on the
|
|
57
|
+
// client channel), so `hasError` — NOT truthiness of `error` — gates rendering:
|
|
58
|
+
// a falsy `undefined`/`null` throw is still a caught error, not "no error".
|
|
59
|
+
error: unknown;
|
|
60
|
+
hasError: boolean;
|
|
61
|
+
notFound: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a client throw into the Error the boundary component receives.
|
|
66
|
+
* Client-side non-Error throws pass through unwrapped (String(value)); no digest
|
|
67
|
+
* is ever attached (digests are a server-error-only signal).
|
|
68
|
+
*/
|
|
69
|
+
export function toClientError(value: unknown): Error {
|
|
70
|
+
if (value instanceof Error) return value;
|
|
71
|
+
// `throw undefined` / `throw null` / `throw 'msg'` → message is String(value).
|
|
72
|
+
return new Error(String(value));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeBoundaryError(error: unknown): unknown {
|
|
76
|
+
// Preact-core interop tag (see primitive-throw.ts): a `throw undefined`/`null`
|
|
77
|
+
// arrives here re-thrown as a tagged Error so it survived preact's own
|
|
78
|
+
// `e.then` suspense check without crashing. Unwrap back to the raw value.
|
|
79
|
+
const untagged = untagPrimitiveThrow(error);
|
|
80
|
+
if (untagged !== error) return untagged;
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
// Fallback for any crash that slips through un-tagged (defense in depth).
|
|
83
|
+
if (error.message === "Cannot read properties of undefined (reading 'then')") return undefined;
|
|
84
|
+
if (error.message === "Cannot read properties of null (reading 'then')") return null;
|
|
85
|
+
}
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A boundary that catches (or escalates) a real error marks it here so the
|
|
90
|
+
// window-listener last resort (install.ts) knows the error was already handled
|
|
91
|
+
// by the nearest-boundary path and must NOT also drive it to global-error.
|
|
92
|
+
// WeakSet-keyed (only object throws are markable; a primitive throw that reaches
|
|
93
|
+
// the window is treated as unhandled, matching "the boundary couldn't hold it").
|
|
94
|
+
const handledErrors = new WeakSet<object>();
|
|
95
|
+
|
|
96
|
+
export function markErrorHandled(error: unknown): void {
|
|
97
|
+
if (error !== null && typeof error === 'object') handledErrors.add(error);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function wasErrorHandled(error: unknown): boolean {
|
|
101
|
+
return error !== null && typeof error === 'object' && handledErrors.has(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ClientErrorBoundary extends Component<BoundaryProps, BoundaryState> {
|
|
105
|
+
state: BoundaryState = { error: null, hasError: false, notFound: false };
|
|
106
|
+
|
|
107
|
+
static getDerivedStateFromError(error: unknown): Partial<BoundaryState> {
|
|
108
|
+
if (isNotFoundError(error)) return { notFound: true, hasError: false, error: null };
|
|
109
|
+
// Control-flow errors must not be captured as state — let componentDidCatch
|
|
110
|
+
// re-throw so they propagate past this boundary.
|
|
111
|
+
if (isControlFlowError(error)) return {};
|
|
112
|
+
// Pass the RAW thrown value to error.js (no Error wrapping): a client
|
|
113
|
+
// `throw undefined`/`null`/'msg' renders as `${error}` = 'undefined'/'null'/'msg'.
|
|
114
|
+
return { error: normalizeBoundaryError(error), hasError: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
componentDidCatch(error: unknown): void {
|
|
118
|
+
if (isNotFoundError(error)) {
|
|
119
|
+
markErrorHandled(error);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (isControlFlowError(error)) {
|
|
123
|
+
// Re-throw past this boundary: React/Next semantics for control errors.
|
|
124
|
+
// The install layer's window handler catches the propagated error and
|
|
125
|
+
// performs the navigation; we still throw so no error UI renders here.
|
|
126
|
+
handleControlFlowError(error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
// A real error the boundary can render (it has a local error.js): mark it so the window-listener
|
|
130
|
+
// last resort ignores it. When there is NO errorComponent this boundary re-throws in render()
|
|
131
|
+
// instead of rendering anything - leave the error UNMARKED so an outer VDOM boundary can still
|
|
132
|
+
// catch it, and so the window-listener last resort recognizes a truly-uncaught error and mounts
|
|
133
|
+
// global-error.
|
|
134
|
+
if (this.props.errorComponent) markErrorHandled(error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `reset()` re-renders the boundary's children in place - no server round trip. That is the whole
|
|
139
|
+
* difference from `unstable_retry()`, and it matters beyond the request count: a refresh swaps the
|
|
140
|
+
* body, so every node the reset subtree just rendered is replaced a moment later, and anything
|
|
141
|
+
* holding one of those nodes (a click in flight, a test's element handle) loses it.
|
|
142
|
+
*/
|
|
143
|
+
reset = (): void => {
|
|
144
|
+
this.setState({ error: null, hasError: false, notFound: false });
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Re-run the SERVER render, then re-render: recovers data-dependent failures. A CLIENT-component
|
|
149
|
+
* error carries no digest (digests are a server-only signal) - its failure lives in the hydrated
|
|
150
|
+
* tree, so re-rendering in place recovers it and a soft-refresh would only swap the body, detaching
|
|
151
|
+
* the very nodes a retry-then-interact loop reaches next. Only a server error's stale data needs the
|
|
152
|
+
* round trip.
|
|
153
|
+
*/
|
|
154
|
+
unstable_retry = (): void => {
|
|
155
|
+
const digest = (this.state.error as { digest?: unknown } | null)?.digest;
|
|
156
|
+
this.setState({ error: null, hasError: false, notFound: false });
|
|
157
|
+
if (typeof digest === 'string' && digest.length > 0) this.props.onReset?.();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// A client-side notFound() that renders a LOCAL not-found.js has no server
|
|
161
|
+
// round trip, so the `<meta name="robots" content="noindex">` the server
|
|
162
|
+
// would emit for a not-found render is missing. Mirror it on the client:
|
|
163
|
+
// inject the tag while the local boundary is showing, remove it on recovery.
|
|
164
|
+
private addedNoindex = false;
|
|
165
|
+
|
|
166
|
+
private syncNotFoundRobots(): void {
|
|
167
|
+
if (typeof document === 'undefined') return;
|
|
168
|
+
const shouldShow = this.state.notFound && Boolean(this.props.notFoundComponent);
|
|
169
|
+
if (shouldShow && !this.addedNoindex) {
|
|
170
|
+
// Don't stomp an existing robots tag (SSR/metadata already set one).
|
|
171
|
+
if (!document.querySelector('meta[name="robots"]')) {
|
|
172
|
+
const meta = document.createElement('meta');
|
|
173
|
+
meta.setAttribute('name', 'robots');
|
|
174
|
+
meta.setAttribute('content', 'noindex');
|
|
175
|
+
meta.setAttribute('data-pnext-notfound-robots', '');
|
|
176
|
+
document.head.appendChild(meta);
|
|
177
|
+
this.addedNoindex = true;
|
|
178
|
+
}
|
|
179
|
+
} else if (!shouldShow && this.addedNoindex) {
|
|
180
|
+
document.querySelector('meta[name="robots"][data-pnext-notfound-robots]')?.remove();
|
|
181
|
+
this.addedNoindex = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
componentDidMount(): void {
|
|
186
|
+
this.syncNotFoundRobots();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
componentDidUpdate(): void {
|
|
190
|
+
this.syncNotFoundRobots();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
componentWillUnmount(): void {
|
|
194
|
+
if (this.addedNoindex) {
|
|
195
|
+
document.querySelector('meta[name="robots"][data-pnext-notfound-robots]')?.remove();
|
|
196
|
+
this.addedNoindex = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
render(): ComponentChildren {
|
|
201
|
+
if (this.state.notFound) {
|
|
202
|
+
const NotFoundComponent = this.props.notFoundComponent ?? this.props.builtInNotFound;
|
|
203
|
+
return NotFoundComponent ? h(NotFoundComponent, {}) : null;
|
|
204
|
+
}
|
|
205
|
+
if (!this.state.hasError) return this.props.children;
|
|
206
|
+
const { error } = this.state;
|
|
207
|
+
const ErrorComponent = this.props.errorComponent;
|
|
208
|
+
if (!ErrorComponent) {
|
|
209
|
+
// Preserve primitive throws for global-error.js directly (window `error`
|
|
210
|
+
// loses raw null/undefined/string). Bubble Error objects to the next outer
|
|
211
|
+
// ClientErrorBoundary so nearest in-tree boundary semantics stay intact.
|
|
212
|
+
if (error === null || typeof error !== 'object') {
|
|
213
|
+
// global-error rides the deferred tier (see ./lazy) — a primitive throw
|
|
214
|
+
// with no error.js is the one path that reaches it from a render.
|
|
215
|
+
void import('./global-error').then(m => m.escalateToGlobalError(error));
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
throw error as Error;
|
|
219
|
+
}
|
|
220
|
+
return h(ErrorComponent, { error, reset: this.reset, unstable_retry: this.unstable_retry });
|
|
221
|
+
}
|
|
222
|
+
}
|