@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/src/host/errors.ts
CHANGED
|
@@ -64,6 +64,21 @@ export class NoRouteMatchError extends HostRouterError {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* True when `err` is a NoRouteMatchError — including one thrown by a
|
|
69
|
+
* DUPLICATED @rangojs/router copy in the module graph (a workspace pinning a
|
|
70
|
+
* second version), whose class identity differs so a bare `instanceof` misses
|
|
71
|
+
* it and an unmatched-host 404 becomes an opaque 500. Use this in worker
|
|
72
|
+
* catch blocks instead of `instanceof`; it also matches by the stable `name`
|
|
73
|
+
* the constructor stamps.
|
|
74
|
+
*/
|
|
75
|
+
export function isNoRouteMatchError(err: unknown): err is NoRouteMatchError {
|
|
76
|
+
return (
|
|
77
|
+
err instanceof NoRouteMatchError ||
|
|
78
|
+
(err instanceof Error && err.name === "NoRouteMatchError")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
export class InvalidHandlerError extends HostRouterError {
|
|
68
83
|
constructor(handler: unknown, options?: ErrorOptions) {
|
|
69
84
|
super(`Invalid handler type: ${typeof handler}`, options);
|
package/src/host/index.ts
CHANGED
package/src/index.rsc.ts
CHANGED
|
@@ -65,6 +65,12 @@ export type {
|
|
|
65
65
|
ErrorPhase,
|
|
66
66
|
OnErrorContext,
|
|
67
67
|
OnErrorCallback,
|
|
68
|
+
// View transition types (also exported from the default entry; route
|
|
69
|
+
// definitions resolve to this react-server entry, so they must be here too)
|
|
70
|
+
TransitionConfig,
|
|
71
|
+
TransitionWhenFn,
|
|
72
|
+
TransitionWhenContext,
|
|
73
|
+
ViewTransitionClass,
|
|
68
74
|
} from "./types.js";
|
|
69
75
|
|
|
70
76
|
// Router options type (server-only, so import directly)
|
|
@@ -97,7 +103,6 @@ export {
|
|
|
97
103
|
loading,
|
|
98
104
|
parallel,
|
|
99
105
|
intercept,
|
|
100
|
-
when,
|
|
101
106
|
errorBoundary,
|
|
102
107
|
notFoundBoundary,
|
|
103
108
|
transition,
|
|
@@ -117,7 +122,6 @@ export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
|
117
122
|
export {
|
|
118
123
|
DEFAULT_DEFER_TIMEOUT_MS,
|
|
119
124
|
type DeferOptions,
|
|
120
|
-
type DeferredHandleEntry,
|
|
121
125
|
type HandlePush,
|
|
122
126
|
type HandlePushFn,
|
|
123
127
|
} from "./defer.js";
|
|
@@ -153,6 +157,7 @@ export {
|
|
|
153
157
|
urls,
|
|
154
158
|
type PathHelpers,
|
|
155
159
|
type PathOptions,
|
|
160
|
+
type PartialPrerenderProps,
|
|
156
161
|
type UrlPatterns,
|
|
157
162
|
type IncludeOptions,
|
|
158
163
|
type IncludeItem,
|
|
@@ -303,6 +308,7 @@ export type {
|
|
|
303
308
|
RouterTracingConfig,
|
|
304
309
|
TracePhase,
|
|
305
310
|
TracePhaseToggles,
|
|
311
|
+
TracingToggleOptions,
|
|
306
312
|
} from "./router/tracing.js";
|
|
307
313
|
|
|
308
314
|
// Timeout types and error class
|
package/src/index.ts
CHANGED
|
@@ -82,7 +82,12 @@ export { createLoader } from "./loader.js";
|
|
|
82
82
|
|
|
83
83
|
// Route definition types (safe to import anywhere)
|
|
84
84
|
export type { RouteHelpers, RouteHandlers } from "./route-definition.js";
|
|
85
|
-
export type {
|
|
85
|
+
export type {
|
|
86
|
+
TransitionConfig,
|
|
87
|
+
TransitionWhenFn,
|
|
88
|
+
TransitionWhenContext,
|
|
89
|
+
ViewTransitionClass,
|
|
90
|
+
} from "./types.js";
|
|
86
91
|
|
|
87
92
|
// Composition types for reusable callback factories
|
|
88
93
|
export type {
|
|
@@ -105,6 +110,7 @@ export type {
|
|
|
105
110
|
TextResponsePathFn,
|
|
106
111
|
RouteResponse,
|
|
107
112
|
ProblemDetails,
|
|
113
|
+
PartialPrerenderProps,
|
|
108
114
|
} from "./urls.js";
|
|
109
115
|
|
|
110
116
|
// Middleware context types
|
|
@@ -143,7 +149,6 @@ export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
|
143
149
|
export {
|
|
144
150
|
DEFAULT_DEFER_TIMEOUT_MS,
|
|
145
151
|
type DeferOptions,
|
|
146
|
-
type DeferredHandleEntry,
|
|
147
152
|
type HandlePush,
|
|
148
153
|
type HandlePushFn,
|
|
149
154
|
} from "./defer.js";
|
|
@@ -279,9 +284,6 @@ export function parallel(): never {
|
|
|
279
284
|
export function intercept(): never {
|
|
280
285
|
throw serverOnlyStubError("intercept");
|
|
281
286
|
}
|
|
282
|
-
export function when(): never {
|
|
283
|
-
throw serverOnlyStubError("when");
|
|
284
|
-
}
|
|
285
287
|
export function errorBoundary(): never {
|
|
286
288
|
throw serverOnlyStubError("errorBoundary");
|
|
287
289
|
}
|
|
@@ -341,14 +343,17 @@ export {
|
|
|
341
343
|
// Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
|
|
342
344
|
// `Rango.PathResponse`, declared in href-client.ts) — no import needed.
|
|
343
345
|
|
|
344
|
-
// Telemetry types only — the createConsoleSink/createOTelSink
|
|
345
|
-
// server-only and live in index.rsc.ts (the
|
|
346
|
-
// bare `@rangojs/router` import). Re-exporting
|
|
347
|
-
// (default/client) entry would pull telemetry.ts and
|
|
348
|
-
// the client module graph; both tree-shake to zero bytes
|
|
349
|
-
// bundle analysis output and slow build-time module
|
|
350
|
-
//
|
|
351
|
-
//
|
|
346
|
+
// Telemetry types only — the createConsoleSink / createOTelSink /
|
|
347
|
+
// createOTelTracing VALUES are server-only and live in index.rsc.ts (the
|
|
348
|
+
// `react-server` condition of the bare `@rangojs/router` import). Re-exporting
|
|
349
|
+
// them as values from this (default/client) entry would pull telemetry.ts and
|
|
350
|
+
// telemetry-otel.ts into the client module graph; both tree-shake to zero bytes
|
|
351
|
+
// but still appear in bundle analysis output and slow build-time module
|
|
352
|
+
// resolution. The factory values are NOT re-exported from `@rangojs/router/server`
|
|
353
|
+
// either — that subpath is internal, not user-facing (see server.ts header).
|
|
354
|
+
// Non-RSC server code imports these TYPES from the root and obtains the factory
|
|
355
|
+
// VALUES from its own router definition module, which resolves to index.rsc.ts
|
|
356
|
+
// under the `react-server` condition.
|
|
352
357
|
export type {
|
|
353
358
|
OTelTracer,
|
|
354
359
|
OTelActiveSpanTracer,
|
|
@@ -384,6 +389,7 @@ export type {
|
|
|
384
389
|
RouterTracingConfig,
|
|
385
390
|
TracePhase,
|
|
386
391
|
TracePhaseToggles,
|
|
392
|
+
TracingToggleOptions,
|
|
387
393
|
} from "./router/tracing.js";
|
|
388
394
|
|
|
389
395
|
// Timeout types and error class
|
package/src/internal-debug.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Runtime fallback for non-Vite contexts (Node, tests, raw imports). In the Vite
|
|
2
|
+
// pipeline -- dev and build, every environment -- the router discovery plugin's
|
|
3
|
+
// transform replaces this module with the build-time-resolved flag (see
|
|
4
|
+
// vite/inject-client-debug.ts), so the CLIENT debug flag no longer depends on a
|
|
5
|
+
// `__RANGO_DEBUG__` define being delivered to the browser (which dev did only as
|
|
6
|
+
// an injected global whose presence varied across consumer setups).
|
|
7
|
+
//
|
|
8
|
+
// Keep this module to the single INTERNAL_RANGO_DEBUG export: the transform
|
|
9
|
+
// replaces the whole file, so a second export would be dropped from the bundle.
|
|
3
10
|
export const INTERNAL_RANGO_DEBUG: boolean =
|
|
4
|
-
typeof
|
|
5
|
-
|
|
6
|
-
: typeof process !== "undefined" &&
|
|
7
|
-
Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
|
|
8
|
-
|
|
9
|
-
declare const __RANGO_DEBUG__: boolean;
|
|
11
|
+
typeof process !== "undefined" &&
|
|
12
|
+
Boolean((process as any).env?.INTERNAL_RANGO_DEBUG);
|
package/src/prerender.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
ExtractParams,
|
|
34
34
|
} from "./types.js";
|
|
35
35
|
import type { Handle } from "./handle.js";
|
|
36
|
+
import type { HandlePush } from "./defer.js";
|
|
36
37
|
import type { ContextVar } from "./context-var.js";
|
|
37
38
|
import type { ReverseFunction } from "./reverse.js";
|
|
38
39
|
import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
|
|
@@ -165,8 +166,14 @@ export interface BuildContext<TParams> {
|
|
|
165
166
|
(key: string, value: any): void;
|
|
166
167
|
};
|
|
167
168
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Push handle data (frozen into pre-rendered output at build time). Returns
|
|
171
|
+
* the full push function, including `.defer()` — a deferred slot resolved by
|
|
172
|
+
* a deep async component during the prerender render is awaited before the
|
|
173
|
+
* artifact is baked (resolve-by-default), so the baked output holds the
|
|
174
|
+
* resolved value.
|
|
175
|
+
*/
|
|
176
|
+
use: <T>(handle: Handle<T>) => HandlePush<T>;
|
|
170
177
|
|
|
171
178
|
/** Synthetic URL built from pattern + params (no real request). */
|
|
172
179
|
url: URL;
|
|
@@ -222,8 +229,14 @@ export interface StaticBuildContext {
|
|
|
222
229
|
(key: string, value: any): void;
|
|
223
230
|
};
|
|
224
231
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Push handle data (frozen into pre-rendered output at build time). Returns
|
|
234
|
+
* the full push function, including `.defer()` — a deferred slot resolved by
|
|
235
|
+
* a deep async component during the prerender render is awaited before the
|
|
236
|
+
* artifact is baked (resolve-by-default), so the baked output holds the
|
|
237
|
+
* resolved value.
|
|
238
|
+
*/
|
|
239
|
+
use: <T>(handle: Handle<T>) => HandlePush<T>;
|
|
227
240
|
|
|
228
241
|
/** URL generation by route name. */
|
|
229
242
|
reverse: BuildReverseFunction;
|
package/src/redirect-origin.ts
CHANGED
|
@@ -60,6 +60,20 @@ export function resolveExternalRedirect(
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The safe same-origin landing for a blocked redirect.
|
|
65
|
+
*
|
|
66
|
+
* Every guard that neutralizes a cross-origin/unsafe redirect target sends the
|
|
67
|
+
* browser here instead: the app's basename root, or `"/"` when unset. Kept
|
|
68
|
+
* beside the resolvers so the "where does a blocked redirect go" answer lives
|
|
69
|
+
* in ONE place -- the server 3xx guard (`rsc/redirect-guard.ts`) and the
|
|
70
|
+
* shell-HIT degradation path (`rsc/rsc-rendering.ts`) must agree, or a blocked
|
|
71
|
+
* redirect lands differently depending on which exit it took.
|
|
72
|
+
*/
|
|
73
|
+
export function safeSameOriginLanding(basename: string | undefined): string {
|
|
74
|
+
return basename && basename !== "/" ? basename : "/";
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
/**
|
|
64
78
|
* Out-of-band brand for `redirect(url, { external: true })`.
|
|
65
79
|
*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface RenderErrorThrowerProps {
|
|
6
|
+
error: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client component that throws the given error during render, so the nearest
|
|
11
|
+
* error boundary catches it. Errors thrown during render are caught by error
|
|
12
|
+
* boundaries; async errors (rejected promises) are not -- which is why the
|
|
13
|
+
* navigation bridge funnels processing failures through this component instead
|
|
14
|
+
* of letting them surface as uncaught rejections.
|
|
15
|
+
*/
|
|
16
|
+
export function RenderErrorThrower({
|
|
17
|
+
error,
|
|
18
|
+
}: RenderErrorThrowerProps): ReactNode {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { Suspense, use } from "react";
|
|
4
|
-
import { invariant } from "./errors";
|
|
5
4
|
import { OutletProvider } from "./outlet-provider.js";
|
|
6
5
|
import type { ResolvedSegment } from "./types.js";
|
|
7
6
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
@@ -22,7 +21,9 @@ export function RouteContentWrapper({
|
|
|
22
21
|
fallback,
|
|
23
22
|
segmentId,
|
|
24
23
|
}: {
|
|
25
|
-
|
|
24
|
+
// Normally a pending promise (use() suspends -> fallback). forceAwait paths
|
|
25
|
+
// pass an already-resolved node so Suspender renders it without suspending.
|
|
26
|
+
content: Promise<ReactNode> | ReactNode;
|
|
26
27
|
fallback?: ReactNode;
|
|
27
28
|
segmentId?: string;
|
|
28
29
|
}): ReactNode {
|
|
@@ -41,9 +42,15 @@ const Suspender = ({
|
|
|
41
42
|
}: {
|
|
42
43
|
content: Promise<ReactNode> | ReactNode;
|
|
43
44
|
}): ReactNode => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Normally content is a pending promise -> use() suspends and the wrapping
|
|
46
|
+
// Suspense shows the loading() fallback. forceAwait paths (popstate,
|
|
47
|
+
// stale-revalidation, fully-prefetched nav) instead pass the ALREADY-RESOLVED
|
|
48
|
+
// node so first render does not suspend for a microtask and flash the loading()
|
|
49
|
+
// fallback on a NORMAL (non-transition) commit. The wrapper tree
|
|
50
|
+
// (RouteContentWrapper > Suspense > Suspender) is identical either way, so this
|
|
51
|
+
// preserves tree structure (see docs/tree-structure.md) — only whether use()
|
|
52
|
+
// suspends differs, exactly like LoaderResolver's resolved-data branch.
|
|
53
|
+
return content instanceof Promise ? use(content) : content;
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type EntryPropSegments,
|
|
18
18
|
type HelperContext,
|
|
19
19
|
type InterceptEntry,
|
|
20
|
+
type InterceptConfig,
|
|
20
21
|
} from "../server/context";
|
|
21
22
|
import { invariant } from "../errors";
|
|
22
23
|
import { validateUserRouteName } from "../route-name.js";
|
|
@@ -36,7 +37,6 @@ import type {
|
|
|
36
37
|
ErrorBoundaryItem,
|
|
37
38
|
NotFoundBoundaryItem,
|
|
38
39
|
LayoutItem,
|
|
39
|
-
WhenItem,
|
|
40
40
|
CacheItem,
|
|
41
41
|
TransitionItem,
|
|
42
42
|
UseItems,
|
|
@@ -265,34 +265,6 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
|
265
265
|
return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
|
|
266
266
|
};
|
|
267
267
|
|
|
268
|
-
/**
|
|
269
|
-
* When helper - defines a condition for intercept activation
|
|
270
|
-
*
|
|
271
|
-
* Only valid inside intercept() use() callback. The when() function
|
|
272
|
-
* is captured by the intercept and stored in its `when` array.
|
|
273
|
-
* During soft navigation, all when() conditions must return true
|
|
274
|
-
* for the intercept to activate.
|
|
275
|
-
*/
|
|
276
|
-
const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
277
|
-
const { store, ctx } = requireDslContext(
|
|
278
|
-
"when() must be called inside intercept()",
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// The when() function needs to be captured by the intercept's tempParent
|
|
282
|
-
// which should have a `when` array. If not present, we're not inside intercept()
|
|
283
|
-
const parent = ctx.parent as any;
|
|
284
|
-
if (!parent || !("when" in parent)) {
|
|
285
|
-
invariant(
|
|
286
|
-
false,
|
|
287
|
-
"when() can only be used inside intercept() use() callback",
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const name = `$${store.getNextIndex("when")}`;
|
|
292
|
-
parent.when.push(fn);
|
|
293
|
-
return { name, type: "when" } as WhenItem;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
268
|
/**
|
|
297
269
|
* Cache helper - defines caching configuration for segments
|
|
298
270
|
*
|
|
@@ -701,8 +673,19 @@ const intercept = (
|
|
|
701
673
|
slotName: `@${string}`,
|
|
702
674
|
routeName: string,
|
|
703
675
|
handler: any,
|
|
676
|
+
configOrUse?: InterceptConfig | (() => any[]),
|
|
704
677
|
use?: () => any[],
|
|
705
678
|
) => {
|
|
679
|
+
// arg4 discrimination: a function is the use() callback (no config); an object
|
|
680
|
+
// is the config carrying `when`. With config given, the use() callback is
|
|
681
|
+
// arg5. Keeps the no-config form intercept(slot, route, handler, () => [...])
|
|
682
|
+
// working unchanged.
|
|
683
|
+
const config: InterceptConfig | undefined =
|
|
684
|
+
typeof configOrUse === "function" || configOrUse == null
|
|
685
|
+
? undefined
|
|
686
|
+
: configOrUse;
|
|
687
|
+
const useFn = typeof configOrUse === "function" ? configOrUse : use;
|
|
688
|
+
|
|
706
689
|
const { store, ctx } = requireDslContext(
|
|
707
690
|
"intercept() must be called inside urls()",
|
|
708
691
|
);
|
|
@@ -740,9 +723,17 @@ const intercept = (
|
|
|
740
723
|
when: [], // Selector conditions for conditional interception
|
|
741
724
|
};
|
|
742
725
|
|
|
726
|
+
// Conditional interception: `when` from the config object — a single selector
|
|
727
|
+
// or an array (ALL must return true to activate). Replaces the former when()
|
|
728
|
+
// use-item captured inside the callback.
|
|
729
|
+
if (config?.when) {
|
|
730
|
+
const selectors = Array.isArray(config.when) ? config.when : [config.when];
|
|
731
|
+
entry.when.push(...selectors);
|
|
732
|
+
}
|
|
733
|
+
|
|
743
734
|
// Merge handler.use defaults with explicit use
|
|
744
735
|
const handlerUseFn = resolveHandlerUse(handler);
|
|
745
|
-
const mergedUse = mergeHandlerUse(handlerUseFn,
|
|
736
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, useFn, "intercept");
|
|
746
737
|
|
|
747
738
|
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
748
739
|
if (mergedUse) {
|
|
@@ -759,7 +750,6 @@ const intercept = (
|
|
|
759
750
|
notFoundBoundary: entry.notFoundBoundary,
|
|
760
751
|
loader: entry.loader,
|
|
761
752
|
layout: capturedLayouts, // Capture layout() calls
|
|
762
|
-
when: entry.when, // Capture when() conditions
|
|
763
753
|
get loading() {
|
|
764
754
|
return entry.loading;
|
|
765
755
|
},
|
|
@@ -1114,7 +1104,6 @@ export {
|
|
|
1114
1104
|
revalidate,
|
|
1115
1105
|
parallel,
|
|
1116
1106
|
intercept,
|
|
1117
|
-
when,
|
|
1118
1107
|
errorBoundary,
|
|
1119
1108
|
notFoundBoundary,
|
|
1120
1109
|
route,
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
revalidate,
|
|
9
9
|
parallel,
|
|
10
10
|
intercept,
|
|
11
|
-
when,
|
|
12
11
|
errorBoundary,
|
|
13
12
|
notFoundBoundary,
|
|
14
13
|
route,
|
|
@@ -40,7 +39,6 @@ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
|
|
|
40
39
|
loading,
|
|
41
40
|
errorBoundary,
|
|
42
41
|
notFoundBoundary,
|
|
43
|
-
when,
|
|
44
42
|
cache,
|
|
45
43
|
transition,
|
|
46
44
|
} as unknown as RouteHelpers<T, TEnv>;
|
|
@@ -29,12 +29,10 @@ import type {
|
|
|
29
29
|
ParallelUseItem,
|
|
30
30
|
InterceptUseItem,
|
|
31
31
|
LoaderUseItem,
|
|
32
|
-
WhenItem,
|
|
33
32
|
CacheItem,
|
|
34
33
|
TransitionItem,
|
|
35
34
|
UseItems,
|
|
36
35
|
} from "../route-types.js";
|
|
37
|
-
import type { InterceptWhenFn } from "../server/context";
|
|
38
36
|
|
|
39
37
|
// Re-export route item types for backward compatibility
|
|
40
38
|
export type {
|
|
@@ -52,12 +50,12 @@ export type {
|
|
|
52
50
|
RouteUseItem,
|
|
53
51
|
ParallelUseItem,
|
|
54
52
|
InterceptUseItem,
|
|
55
|
-
WhenItem,
|
|
56
53
|
CacheItem,
|
|
57
54
|
} from "../route-types.js";
|
|
58
55
|
|
|
59
56
|
// Re-export intercept selector types for use in handlers
|
|
60
57
|
export type {
|
|
58
|
+
InterceptConfig,
|
|
61
59
|
InterceptSelectorContext,
|
|
62
60
|
InterceptSegmentsState,
|
|
63
61
|
InterceptWhenFn,
|
|
@@ -151,8 +149,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
151
149
|
* so they take precedence on `loading()` and other last-write-wins
|
|
152
150
|
* fields.
|
|
153
151
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
// Not generic over the slots record: an inferred type parameter makes the
|
|
153
|
+
// object literal an inference site, which suppresses contextual typing of
|
|
154
|
+
// arrow slot handlers (`(ctx) => ...` was implicit any).
|
|
155
|
+
parallel: (
|
|
156
|
+
slots: Record<
|
|
156
157
|
`@${string}`,
|
|
157
158
|
| Handler<any, any, TEnv>
|
|
158
159
|
| ReactNode
|
|
@@ -161,8 +162,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
161
162
|
use?: () => UseItems<ParallelUseItem>;
|
|
162
163
|
}
|
|
163
164
|
>,
|
|
164
|
-
>(
|
|
165
|
-
slots: TSlots,
|
|
166
165
|
use?: () => UseItems<ParallelUseItem>,
|
|
167
166
|
) => ParallelItem;
|
|
168
167
|
/**
|
|
@@ -183,10 +182,26 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
183
182
|
* loader(CardModalLoader),
|
|
184
183
|
* revalidate(() => false),
|
|
185
184
|
* ])
|
|
185
|
+
*
|
|
186
|
+
* // Conditional activation via the config object's `when` selector
|
|
187
|
+
* intercept("@modal", "card", <CardModal />, {
|
|
188
|
+
* when: ({ from }) => from.pathname.startsWith("/board"),
|
|
189
|
+
* })
|
|
190
|
+
*
|
|
191
|
+
* // Config + other use-items: config is arg 4, use is arg 5
|
|
192
|
+
* intercept(
|
|
193
|
+
* "@modal",
|
|
194
|
+
* "card",
|
|
195
|
+
* <CardModal />,
|
|
196
|
+
* { when: ({ from }) => from.pathname.startsWith("/board") },
|
|
197
|
+
* () => [loader(CardDetailLoader)],
|
|
198
|
+
* )
|
|
186
199
|
* ```
|
|
187
200
|
* @param slotName - Named slot (prefixed with @) where intercept renders
|
|
188
201
|
* @param routeName - Route name to intercept
|
|
189
202
|
* @param handler - Component or handler for intercepted render
|
|
203
|
+
* @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
|
|
204
|
+
* callback directly when there is no config
|
|
190
205
|
* @param use - Optional callback for loaders, middleware, revalidate, etc.
|
|
191
206
|
*/
|
|
192
207
|
intercept: {
|
|
@@ -195,6 +210,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
195
210
|
slotName: `@${string}`,
|
|
196
211
|
routeName: `.${K}`,
|
|
197
212
|
handler: ReactNode | Handler<ExtractRouteParams<T, K>, {}, TEnv>,
|
|
213
|
+
config?:
|
|
214
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
215
|
+
| (() => UseItems<InterceptUseItem>),
|
|
198
216
|
use?: () => UseItems<InterceptUseItem>,
|
|
199
217
|
): InterceptItem;
|
|
200
218
|
// Global: unprefixed, params inferred from global route map
|
|
@@ -202,6 +220,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
202
220
|
slotName: `@${string}`,
|
|
203
221
|
routeName: K,
|
|
204
222
|
handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
|
|
223
|
+
config?:
|
|
224
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
225
|
+
| (() => UseItems<InterceptUseItem>),
|
|
205
226
|
use?: () => UseItems<InterceptUseItem>,
|
|
206
227
|
): InterceptItem;
|
|
207
228
|
};
|
|
@@ -347,40 +368,6 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
347
368
|
notFoundBoundary: (
|
|
348
369
|
fallback: ReactNode | NotFoundBoundaryHandler,
|
|
349
370
|
) => NotFoundBoundaryItem;
|
|
350
|
-
/**
|
|
351
|
-
* Define a condition for when an intercept should activate
|
|
352
|
-
*
|
|
353
|
-
* Only valid inside intercept() use() callback. When multiple when() calls
|
|
354
|
-
* are present, ALL must return true for the intercept to activate.
|
|
355
|
-
* If no when() is defined, the intercept always activates on soft navigation.
|
|
356
|
-
*
|
|
357
|
-
* Context properties:
|
|
358
|
-
* - `from` - Source URL (where user is navigating from)
|
|
359
|
-
* - `to` - Destination URL (where user is navigating to)
|
|
360
|
-
* - `params` - Matched route params
|
|
361
|
-
* - `segments` - Client's current segments with `path` and `ids`
|
|
362
|
-
*
|
|
363
|
-
* ```typescript
|
|
364
|
-
* // Only intercept when coming from the board page
|
|
365
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
366
|
-
* when(({ from }) => from.pathname.startsWith("/board")),
|
|
367
|
-
* loader(CardDetailLoader),
|
|
368
|
-
* ])
|
|
369
|
-
*
|
|
370
|
-
* // Use segments to check current route context
|
|
371
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
372
|
-
* when(({ segments }) => segments.path[0] === "kanban"),
|
|
373
|
-
* ])
|
|
374
|
-
*
|
|
375
|
-
* // Multiple conditions (AND logic)
|
|
376
|
-
* intercept("@modal", "card", <CardModal />, () => [
|
|
377
|
-
* when(({ from }) => from.pathname.startsWith("/board")),
|
|
378
|
-
* when(({ segments }) => segments.ids.includes("kanban-layout")),
|
|
379
|
-
* ])
|
|
380
|
-
* ```
|
|
381
|
-
* @param fn - Selector function receiving navigation context, returns boolean
|
|
382
|
-
*/
|
|
383
|
-
when: (fn: InterceptWhenFn) => WhenItem;
|
|
384
371
|
/**
|
|
385
372
|
* Define cache configuration for segments
|
|
386
373
|
*
|
|
@@ -472,6 +459,13 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
472
459
|
* transition({}) is startTransition + ViewTransition under the default and
|
|
473
460
|
* startTransition only when the router sets viewTransition: false.
|
|
474
461
|
*
|
|
462
|
+
* Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
|
|
463
|
+
* request. It runs server-side AFTER the route handler (so it can read state
|
|
464
|
+
* the handler set via `ctx.get(...)`); returning false drops this transition
|
|
465
|
+
* for the request, so the navigation streams its loading() skeleton instead of
|
|
466
|
+
* holding. This is a post-handler predicate — distinct from intercept()'s
|
|
467
|
+
* match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
|
|
468
|
+
*
|
|
475
469
|
* ```typescript
|
|
476
470
|
* // Attach to a single route
|
|
477
471
|
* path("/about", AboutPage, { name: "about" }, () => [
|
|
@@ -488,10 +482,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
488
482
|
* path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
489
483
|
* transition({ viewTransition: false }),
|
|
490
484
|
* ])
|
|
485
|
+
*
|
|
486
|
+
* // Hold only when the handler decided to (post-handler predicate):
|
|
487
|
+
* path("/product/:id", ProductPage, { name: "product" }, () => [
|
|
488
|
+
* transition({ when: (ctx) => ctx.get(KeepScroll) === true }),
|
|
489
|
+
* ])
|
|
491
490
|
* ```
|
|
492
491
|
* @param config - ViewTransition configuration (enter, exit, update, share,
|
|
493
|
-
* default, name)
|
|
494
|
-
* boundary (createRouter({ viewTransition }) sets the app-wide default)
|
|
492
|
+
* default, name), `viewTransition: "auto" | false` to toggle the router
|
|
493
|
+
* boundary (createRouter({ viewTransition }) sets the app-wide default), and
|
|
494
|
+
* `when: (ctx) => boolean` to gate the transition per request post-handler
|
|
495
495
|
* @param children - Optional callback returning child routes to wrap
|
|
496
496
|
*/
|
|
497
497
|
transition: {
|
|
@@ -15,8 +15,8 @@ export type {
|
|
|
15
15
|
RouteUseItem,
|
|
16
16
|
ParallelUseItem,
|
|
17
17
|
InterceptUseItem,
|
|
18
|
-
WhenItem,
|
|
19
18
|
CacheItem,
|
|
19
|
+
InterceptConfig,
|
|
20
20
|
InterceptSelectorContext,
|
|
21
21
|
InterceptSegmentsState,
|
|
22
22
|
InterceptWhenFn,
|
|
@@ -30,7 +30,6 @@ export {
|
|
|
30
30
|
revalidate,
|
|
31
31
|
parallel,
|
|
32
32
|
intercept,
|
|
33
|
-
when,
|
|
34
33
|
errorBoundary,
|
|
35
34
|
notFoundBoundary,
|
|
36
35
|
loader,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type { AllUseItems
|
|
1
|
+
import type { AllUseItems } from "../route-types.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The set of valid use-item `type` discriminants — the single runtime source of
|
|
5
5
|
* truth for "is this a well-formed use item?" shape validation.
|
|
6
6
|
*
|
|
7
7
|
* Declared via a `Record<...>` so that adding a member to the union without
|
|
8
|
-
* updating this map is a compile error.
|
|
9
|
-
* are valid inside intercept() even though WhenItem is not part of AllUseItems
|
|
10
|
-
* (it lives only in InterceptUseItem). This is shape validation only; per-mount-
|
|
8
|
+
* updating this map is a compile error. This is shape validation only; per-mount-
|
|
11
9
|
* site rules remain the narrower hand-written tables in resolve-handler-use.ts.
|
|
12
10
|
*/
|
|
13
|
-
const USE_ITEM_TYPES: Record<AllUseItems["type"]
|
|
11
|
+
const USE_ITEM_TYPES: Record<AllUseItems["type"], true> = {
|
|
14
12
|
layout: true,
|
|
15
13
|
route: true,
|
|
16
14
|
middleware: true,
|
|
@@ -21,7 +19,6 @@ const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
|
|
|
21
19
|
loading: true,
|
|
22
20
|
errorBoundary: true,
|
|
23
21
|
notFoundBoundary: true,
|
|
24
|
-
when: true,
|
|
25
22
|
cache: true,
|
|
26
23
|
transition: true,
|
|
27
24
|
include: true,
|