@rangojs/router 0.5.2 → 0.7.0
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/dist/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
LoaderDefinition,
|
|
4
|
+
LoaderOptions,
|
|
5
|
+
TransitionConfig,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import type { TrieMatchResult } from "../router/trie-matching.js";
|
|
8
|
+
import type { PathOptions } from "../urls/pattern-types.js";
|
|
9
|
+
import type { SearchSchema } from "../search-params.js";
|
|
10
|
+
import type { TypedLayoutItem, TypedRouteItem } from "../route-types.js";
|
|
11
|
+
import type { ExtractRoutes } from "../urls/type-extraction.js";
|
|
12
|
+
import type { UnnamedRoute } from "../urls/pattern-types.js";
|
|
13
|
+
|
|
14
|
+
declare const CLIENT_URL_ITEM_BRAND: unique symbol;
|
|
15
|
+
declare const CLIENT_URL_PATTERNS_BRAND: unique symbol;
|
|
16
|
+
|
|
17
|
+
/** Opaque value returned by a clientUrls() helper. */
|
|
18
|
+
export interface ClientUrlItem {
|
|
19
|
+
readonly [CLIENT_URL_ITEM_BRAND]: void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ClientUrlItemInput = ClientUrlItem | readonly ClientUrlItemInput[];
|
|
23
|
+
|
|
24
|
+
export type ClientUrlItems = readonly ClientUrlItemInput[];
|
|
25
|
+
|
|
26
|
+
export type ClientUrlUse = () => ClientUrlItems;
|
|
27
|
+
|
|
28
|
+
export type ClientPathOptions<
|
|
29
|
+
TName extends string = string,
|
|
30
|
+
TSearch extends SearchSchema = SearchSchema,
|
|
31
|
+
> = Pick<
|
|
32
|
+
PathOptions<TName, TSearch>,
|
|
33
|
+
"name" | "search" | "trailingSlash" | "ppr"
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
export type ClientPathFn = <
|
|
37
|
+
const TPattern extends string,
|
|
38
|
+
const TName extends string = UnnamedRoute,
|
|
39
|
+
const TSearch extends SearchSchema = {},
|
|
40
|
+
>(
|
|
41
|
+
pattern: TPattern,
|
|
42
|
+
component: ComponentType,
|
|
43
|
+
optionsOrUse?: ClientPathOptions<TName, TSearch> | ClientUrlUse,
|
|
44
|
+
use?: ClientUrlUse,
|
|
45
|
+
) => ClientUrlItem & TypedRouteItem<TName, TPattern, unknown, TSearch>;
|
|
46
|
+
|
|
47
|
+
export type ClientLayoutFn = <const TItems extends ClientUrlItems>(
|
|
48
|
+
component: ComponentType,
|
|
49
|
+
children: () => TItems,
|
|
50
|
+
) => ClientUrlItem & TypedLayoutItem<ExtractRoutes<TItems>>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Arguments a clientUrls() revalidate() predicate receives. A client-computable
|
|
54
|
+
* subset of the server ShouldRevalidateFn args — the predicate RUNS IN THE
|
|
55
|
+
* BROWSER (it is declared in a "use client" module and never crosses the
|
|
56
|
+
* projection boundary); only its decision is sent to the server. There is no
|
|
57
|
+
* `context` — no server handler context exists where this executes.
|
|
58
|
+
*/
|
|
59
|
+
export interface ClientRevalidateArgs {
|
|
60
|
+
/** Full URL of the page being navigated away from (current location). */
|
|
61
|
+
readonly currentUrl: URL;
|
|
62
|
+
/** Full URL of the navigation target (equals currentUrl for actions). */
|
|
63
|
+
readonly nextUrl: URL;
|
|
64
|
+
/** Route params of the held client route (definition-local match). */
|
|
65
|
+
readonly currentParams: Record<string, string>;
|
|
66
|
+
/** Route params for the navigation target (definition-local match). */
|
|
67
|
+
readonly nextParams: Record<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* The locked default decision for this loader, computed client-side with
|
|
70
|
+
* the same rules the server would apply: `true` on actions and when
|
|
71
|
+
* params/search changed, `false` otherwise. Return it for default behavior
|
|
72
|
+
* plus your own conditions.
|
|
73
|
+
*/
|
|
74
|
+
readonly defaultShouldRevalidate: boolean;
|
|
75
|
+
/** True when this is a stale history-entry background revalidation. */
|
|
76
|
+
readonly stale: boolean;
|
|
77
|
+
/** True when revalidation is triggered by a server action. */
|
|
78
|
+
readonly isAction: boolean;
|
|
79
|
+
/** The triggering server action's id, when isAction. */
|
|
80
|
+
readonly actionId?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ClientRevalidateFn = (args: ClientRevalidateArgs) => boolean;
|
|
84
|
+
|
|
85
|
+
export interface ClientUrlLoaderRecord {
|
|
86
|
+
readonly loader: LoaderDefinition<any, any>;
|
|
87
|
+
/** Client-run per-loader revalidation predicates; empty = locked defaults. */
|
|
88
|
+
readonly revalidate: readonly ClientRevalidateFn[];
|
|
89
|
+
/**
|
|
90
|
+
* loader(Def, { stream: "navigation" }): document renders await this loader
|
|
91
|
+
* before first flush (see {@link LoaderOptions}). Projected into the server
|
|
92
|
+
* tree, where the per-isSSR entry stamping applies — client navigations
|
|
93
|
+
* stream regardless.
|
|
94
|
+
*/
|
|
95
|
+
readonly stream?: "navigation";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The data-only subset of TransitionConfig a clientUrls() route may declare:
|
|
100
|
+
* ViewTransition classes/name plus the boundary opt-out. The `when` gate is a
|
|
101
|
+
* server-executed predicate and stays in the server tree — it cannot cross the
|
|
102
|
+
* "use client" projection boundary.
|
|
103
|
+
*/
|
|
104
|
+
export type ClientTransitionConfig = Pick<
|
|
105
|
+
TransitionConfig,
|
|
106
|
+
"enter" | "exit" | "update" | "share" | "default" | "name" | "viewTransition"
|
|
107
|
+
>;
|
|
108
|
+
|
|
109
|
+
export interface ClientUrlRouteRecord {
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly pattern: string;
|
|
112
|
+
readonly name: string | undefined;
|
|
113
|
+
readonly options: Readonly<ClientPathOptions> | undefined;
|
|
114
|
+
readonly component: ComponentType;
|
|
115
|
+
readonly layouts: readonly ComponentType[];
|
|
116
|
+
readonly loaders: readonly ClientUrlLoaderRecord[];
|
|
117
|
+
readonly loading: ReactNode | undefined;
|
|
118
|
+
readonly transition: Readonly<ClientTransitionConfig> | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A restricted intercept declared inside clientUrls(). Compared to the server
|
|
123
|
+
* intercept() there is no `when` selector, no middleware, and the target must
|
|
124
|
+
* be a dot-local NAMED route in the same definition — every field is
|
|
125
|
+
* JSON-projectable, which is what makes the declaration legal in a
|
|
126
|
+
* "use client" module. `targetName` is stored bare (no leading dot).
|
|
127
|
+
*/
|
|
128
|
+
export interface ClientUrlInterceptRecord {
|
|
129
|
+
readonly slotName: `@${string}`;
|
|
130
|
+
readonly targetName: string;
|
|
131
|
+
readonly component: ComponentType;
|
|
132
|
+
readonly loaders: readonly ClientUrlLoaderRecord[];
|
|
133
|
+
readonly loading: ReactNode | undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ClientUrlHelpers {
|
|
137
|
+
readonly path: ClientPathFn;
|
|
138
|
+
readonly layout: ClientLayoutFn;
|
|
139
|
+
/**
|
|
140
|
+
* Attach a projected loader. The optional use callback may contain
|
|
141
|
+
* revalidate() only — a CLIENT-RUN per-loader predicate; its decision (not
|
|
142
|
+
* the function) is sent with the revalidation request.
|
|
143
|
+
*
|
|
144
|
+
* Pass `{ stream: "navigation" }` to await this loader before first flush
|
|
145
|
+
* on DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders
|
|
146
|
+
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
147
|
+
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
|
|
148
|
+
* `ppr` group route the flag BAKES: the loader executes at shell capture
|
|
149
|
+
* and its settled return freezes into the shell (nested promises stay
|
|
150
|
+
* live holes).
|
|
151
|
+
*/
|
|
152
|
+
readonly loader: <TData>(
|
|
153
|
+
definition: LoaderDefinition<TData>,
|
|
154
|
+
optionsOrUse?: LoaderOptions | ClientUrlUse,
|
|
155
|
+
use?: ClientUrlUse,
|
|
156
|
+
) => ClientUrlItem;
|
|
157
|
+
readonly loading: (component: ReactNode) => ClientUrlItem;
|
|
158
|
+
/**
|
|
159
|
+
* Per-loader revalidation predicate, valid inside a loader() use callback
|
|
160
|
+
* only. Runs IN THE BROWSER with client-computable args; return true to
|
|
161
|
+
* re-run the loader, false to keep held data. Absent predicates (and
|
|
162
|
+
* requests that carry no decisions: no-JS, PE, prefetch, document loads)
|
|
163
|
+
* follow the locked server defaults.
|
|
164
|
+
*/
|
|
165
|
+
readonly revalidate: (fn: ClientRevalidateFn) => ClientUrlItem;
|
|
166
|
+
/**
|
|
167
|
+
* Declare an intercept for a named route in THIS definition. The target is
|
|
168
|
+
* dot-local (`.detail`); scoping is module-local — only navigations whose
|
|
169
|
+
* origin is inside this clientUrls() group render the intercept. `use` may
|
|
170
|
+
* contain loader() and loading() only.
|
|
171
|
+
*/
|
|
172
|
+
readonly intercept: (
|
|
173
|
+
slotName: `@${string}`,
|
|
174
|
+
targetName: `.${string}`,
|
|
175
|
+
component: ComponentType,
|
|
176
|
+
use?: ClientUrlUse,
|
|
177
|
+
) => ClientUrlItem;
|
|
178
|
+
/**
|
|
179
|
+
* Opt THIS route into transition-driven navigation: the canonical commit
|
|
180
|
+
* holds previous content instead of re-streaming the loading() fallback,
|
|
181
|
+
* and on experimental React the config's ViewTransition classes apply.
|
|
182
|
+
* Data-only — no `when` gate (server-executed; declare it in the server
|
|
183
|
+
* tree). Valid inside a path() use callback only.
|
|
184
|
+
*/
|
|
185
|
+
readonly transition: (config: ClientTransitionConfig) => ClientUrlItem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ClientUrlBuilder<TItems extends ClientUrlItems = ClientUrlItems> = (
|
|
189
|
+
helpers: ClientUrlHelpers,
|
|
190
|
+
) => TItems;
|
|
191
|
+
|
|
192
|
+
export interface ClientUrlPatterns<
|
|
193
|
+
TRoutes extends Record<string, any> = Record<string, any>,
|
|
194
|
+
> {
|
|
195
|
+
readonly __brand: "client-urls";
|
|
196
|
+
readonly routes: readonly ClientUrlRouteRecord[];
|
|
197
|
+
readonly intercepts: readonly ClientUrlInterceptRecord[];
|
|
198
|
+
readonly [CLIENT_URL_PATTERNS_BRAND]: void;
|
|
199
|
+
readonly _routes?: TRoutes;
|
|
200
|
+
match(pathname: string): TrieMatchResult | null;
|
|
201
|
+
}
|
package/src/client.rsc.tsx
CHANGED
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
Outlet,
|
|
16
16
|
ParallelOutlet,
|
|
17
17
|
useOutlet,
|
|
18
|
+
type OutletState,
|
|
18
19
|
useLoader,
|
|
19
20
|
ErrorBoundary,
|
|
20
21
|
type ErrorBoundaryProps,
|
|
@@ -31,6 +32,12 @@ export {
|
|
|
31
32
|
|
|
32
33
|
export { createLoader } from "./route-definition.js";
|
|
33
34
|
|
|
35
|
+
export { clientUrls } from "./client-urls/client-urls.js";
|
|
36
|
+
export type {
|
|
37
|
+
ClientUrlPatterns,
|
|
38
|
+
ClientUrlRouteRecord,
|
|
39
|
+
} from "./client-urls/client-urls.js";
|
|
40
|
+
|
|
34
41
|
// "use client" hooks the default ./client entry exports. They are client
|
|
35
42
|
// references in the RSC graph, identical in kind to useHref/useReverse/
|
|
36
43
|
// useHandle already forwarded below; forward them so the RSC client entry's
|
|
@@ -39,6 +46,11 @@ export { createLoader } from "./route-definition.js";
|
|
|
39
46
|
export { useRouter } from "./browser/react/use-router.js";
|
|
40
47
|
export { usePathname } from "./browser/react/use-pathname.js";
|
|
41
48
|
export { useSearchParams } from "./browser/react/use-search-params.js";
|
|
49
|
+
export type {
|
|
50
|
+
SearchParamsInit,
|
|
51
|
+
SetSearchParams,
|
|
52
|
+
SetSearchParamsOptions,
|
|
53
|
+
} from "./browser/react/use-search-params.js";
|
|
42
54
|
export { useParams } from "./browser/react/use-params.js";
|
|
43
55
|
// CSP nonce for userland head-script components (analytics/GTM/inline init);
|
|
44
56
|
// forwarded so the RSC client entry's hook surface matches the default entry.
|
package/src/client.tsx
CHANGED
|
@@ -141,17 +141,41 @@ function useSlotSegment(
|
|
|
141
141
|
* }
|
|
142
142
|
* ```
|
|
143
143
|
*/
|
|
144
|
-
export function Outlet({
|
|
144
|
+
export function Outlet({
|
|
145
|
+
name,
|
|
146
|
+
fallback,
|
|
147
|
+
}: { name?: `@${string}`; fallback?: ReactNode } = {}): ReactNode {
|
|
145
148
|
const context = useContext(OutletContext);
|
|
146
149
|
const namedSegment = useSlotSegment(context, name);
|
|
147
150
|
|
|
148
151
|
if (name) {
|
|
149
|
-
|
|
152
|
+
const slot = renderSlotContent(namedSegment);
|
|
153
|
+
// EXPERIMENT (Outlet fallback): layout-declared pending UI for a named
|
|
154
|
+
// slot — an outer Suspense catching whatever the slot's content leaves
|
|
155
|
+
// unhandled.
|
|
156
|
+
return fallback !== undefined ? (
|
|
157
|
+
<Suspense fallback={fallback}>{slot}</Suspense>
|
|
158
|
+
) : (
|
|
159
|
+
slot
|
|
160
|
+
);
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
// Default: render child content
|
|
153
164
|
const content = context?.content ?? null;
|
|
154
165
|
|
|
166
|
+
// EXPERIMENT (Outlet fallback): the layout owns the outlet position, so it
|
|
167
|
+
// may declare the placeholder for pending child content right where it
|
|
168
|
+
// renders — a plain Suspense boundary, no DSL wiring, no segment-resolution
|
|
169
|
+
// threading. With read-site useLoader suspension this covers loading()'s
|
|
170
|
+
// roles by construction: document SSR streams the fallback then the
|
|
171
|
+
// content; PPR postpones unresolved readers here (the outlet becomes the
|
|
172
|
+
// hole); same-route re-renders are held before commit so the fallback does
|
|
173
|
+
// NOT flash; cross-route navs show it as destination feedback. An explicit
|
|
174
|
+
// prop wins over the child segment's loading().
|
|
175
|
+
if (fallback !== undefined) {
|
|
176
|
+
return <Suspense fallback={fallback}>{content}</Suspense>;
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
// If this segment defines a loading component, wrap outlet content with Suspense
|
|
156
180
|
// The loading component becomes the Suspense fallback, shown during streaming/navigation
|
|
157
181
|
if (context?.loading) {
|
|
@@ -199,7 +223,7 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
|
199
223
|
// internal component and is intentionally not part of the public ./client API.
|
|
200
224
|
|
|
201
225
|
/**
|
|
202
|
-
* Hook to access outlet content programmatically
|
|
226
|
+
* Hook to access outlet content and descendant pending state programmatically.
|
|
203
227
|
*
|
|
204
228
|
* Alternative to using <Outlet /> component. Useful when you need
|
|
205
229
|
* direct access to the outlet content in your logic.
|
|
@@ -208,13 +232,21 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
|
208
232
|
* ```tsx
|
|
209
233
|
* function BlogLayout() {
|
|
210
234
|
* const outlet = useOutlet();
|
|
211
|
-
* return <div><h1>Blog</h1>{outlet}</div>;
|
|
235
|
+
* return <div aria-busy={outlet.pending}><h1>Blog</h1>{outlet.content}</div>;
|
|
212
236
|
* }
|
|
213
237
|
* ```
|
|
214
238
|
*/
|
|
215
|
-
export
|
|
239
|
+
export interface OutletState {
|
|
240
|
+
readonly content: ReactNode;
|
|
241
|
+
readonly pending: boolean;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function useOutlet(): OutletState {
|
|
216
245
|
const context = useContext(OutletContext);
|
|
217
|
-
return
|
|
246
|
+
return {
|
|
247
|
+
content: context?.content ?? null,
|
|
248
|
+
pending: context?.pending ?? false,
|
|
249
|
+
};
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
export {
|
|
@@ -227,6 +259,12 @@ export {
|
|
|
227
259
|
type UseLoaderOptions,
|
|
228
260
|
} from "./use-loader.js";
|
|
229
261
|
|
|
262
|
+
export { clientUrls } from "./client-urls/client-urls.js";
|
|
263
|
+
export type {
|
|
264
|
+
ClientUrlPatterns,
|
|
265
|
+
ClientUrlRouteRecord,
|
|
266
|
+
} from "./client-urls/client-urls.js";
|
|
267
|
+
|
|
230
268
|
/**
|
|
231
269
|
* Props for the ErrorBoundary component
|
|
232
270
|
*/
|
|
@@ -339,6 +377,11 @@ export { useNavigation } from "./browser/react/use-navigation.js";
|
|
|
339
377
|
export { useRouter } from "./browser/react/use-router.js";
|
|
340
378
|
export { usePathname } from "./browser/react/use-pathname.js";
|
|
341
379
|
export { useSearchParams } from "./browser/react/use-search-params.js";
|
|
380
|
+
export type {
|
|
381
|
+
SearchParamsInit,
|
|
382
|
+
SetSearchParams,
|
|
383
|
+
SetSearchParamsOptions,
|
|
384
|
+
} from "./browser/react/use-search-params.js";
|
|
342
385
|
export { useParams } from "./browser/react/use-params.js";
|
|
343
386
|
// CSP nonce for the active request, for userland components that inject their
|
|
344
387
|
// own <script>/<style> into the document head (analytics, GTM, inline init).
|
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { createElement } from "react";
|
|
2
3
|
import { isLoaderDataResult } from "./types.js";
|
|
4
|
+
import { LoaderRedirect } from "./loader-redirect.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Markers for loader-thrown AUTHORITY SIGNALS (notFound()/redirect()) on the
|
|
8
|
+
* reconstructed read-site error. Siblings of LOADER_ERROR_FALLBACK, routed
|
|
9
|
+
* differently by StreamedLoaderErrorBoundary: NOT_FOUND renders the
|
|
10
|
+
* server-rendered not-found UI riding the envelope; REDIRECT triggers a
|
|
11
|
+
* client replace-navigation to the (server-resolved, same-origin-guarded)
|
|
12
|
+
* target. Signals are control flow, not failures — they take precedence over
|
|
13
|
+
* the plain error-fallback path.
|
|
14
|
+
*
|
|
15
|
+
* Only decodeLoaderEntry (the read-site path — client components, so the
|
|
16
|
+
* throw lands in the router-owned boundary; during document SSR, Fizz emits
|
|
17
|
+
* the Suspense fallback and replays the throw at hydration) throws these.
|
|
18
|
+
* decodeLoaderResults runs during the SERVER tree build on forceAwait/action
|
|
19
|
+
* lanes, where a throw would collapse the whole payload — it routes signals
|
|
20
|
+
* through the errorFallback slot instead (same visual: the slot replaces the
|
|
21
|
+
* children under OutletProvider).
|
|
22
|
+
*/
|
|
23
|
+
export const LOADER_NOT_FOUND_FALLBACK: unique symbol = Symbol(
|
|
24
|
+
"rango.loaderNotFound",
|
|
25
|
+
);
|
|
26
|
+
export const LOADER_REDIRECT: unique symbol = Symbol("rango.loaderRedirect");
|
|
27
|
+
|
|
28
|
+
function throwSignal(result: any): void {
|
|
29
|
+
if (result.notFound === true) {
|
|
30
|
+
const err = new Error(result.error?.message ?? "Not found");
|
|
31
|
+
err.name = result.error?.name ?? "DataNotFoundError";
|
|
32
|
+
(err as any)[LOADER_NOT_FOUND_FALLBACK] = result.fallback ?? null;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
if (result.redirect) {
|
|
36
|
+
const err = new Error(result.error?.message ?? "Loader redirect");
|
|
37
|
+
err.name = "LoaderRedirect";
|
|
38
|
+
(err as any)[LOADER_REDIRECT] = result.redirect;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Non-throwing signal resolution for the aggregate path. Returns the node to
|
|
45
|
+
* place in the errorFallback slot, or undefined when the result carries no
|
|
46
|
+
* signal.
|
|
47
|
+
*/
|
|
48
|
+
function signalFallback(result: any): ReactNode | undefined {
|
|
49
|
+
if (result.notFound === true) {
|
|
50
|
+
return (result.fallback ?? null) as ReactNode;
|
|
51
|
+
}
|
|
52
|
+
if (result.redirect) {
|
|
53
|
+
const redirect = result.redirect as {
|
|
54
|
+
to: string;
|
|
55
|
+
state?: Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
return createElement(LoaderRedirect, {
|
|
58
|
+
to: redirect.to,
|
|
59
|
+
state: redirect.state,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
3
64
|
|
|
4
65
|
// Shared by segment-system (server) and LoaderResolver (client) so the
|
|
5
66
|
// legacy/ok/error-fallback/throw decode of resolved loader values lives once.
|
|
@@ -25,6 +86,16 @@ export function decodeLoaderResults(
|
|
|
25
86
|
continue;
|
|
26
87
|
}
|
|
27
88
|
|
|
89
|
+
// Authority signals take the errorFallback slot (this decode runs during
|
|
90
|
+
// the server tree build on forceAwait/action lanes — throwing here would
|
|
91
|
+
// collapse the payload). A signal outranks a plain error fallback and
|
|
92
|
+
// stops the scan: notFound renders the server-rendered 404 UI, redirect
|
|
93
|
+
// mounts LoaderRedirect which navigates.
|
|
94
|
+
const signal = signalFallback(result);
|
|
95
|
+
if (signal !== undefined) {
|
|
96
|
+
return { loaderData, errorFallback: signal };
|
|
97
|
+
}
|
|
98
|
+
|
|
28
99
|
// null/undefined is the producer's ONLY "no boundary found" sentinel
|
|
29
100
|
// (loader-resolution.ts sets fallback: null for the no-boundary and the
|
|
30
101
|
// fallback-render-threw cases). A matched boundary's rendered ReactNode can
|
|
@@ -50,3 +121,45 @@ export function decodeLoaderResults(
|
|
|
50
121
|
|
|
51
122
|
return { loaderData, errorFallback };
|
|
52
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Marker property carrying the errorBoundary() fallback on a read-site loader
|
|
127
|
+
* error. decodeLoaderEntry attaches the pre-rendered boundary node (produced
|
|
128
|
+
* server-side by loader-resolution) to the thrown error; the router-owned
|
|
129
|
+
* StreamedLoaderErrorBoundary above the readers (segment-system wires it for
|
|
130
|
+
* every loader-bearing segment) catches by this marker and renders the
|
|
131
|
+
* fallback — restoring the build-time errorFallback-swap contract for
|
|
132
|
+
* streamed loaders. Errors without the marker rethrow to the app's boundaries.
|
|
133
|
+
*/
|
|
134
|
+
export const LOADER_ERROR_FALLBACK: unique symbol = Symbol(
|
|
135
|
+
"rango.loaderErrorFallback",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Streaming useLoader: single-entry decode for read-site resolution. Mirrors
|
|
140
|
+
* decodeLoaderResults for one result. An error entry throws the reconstructed
|
|
141
|
+
* error (name/stack/code/cause preserved); when the entry carries an
|
|
142
|
+
* errorBoundary() fallback, the node rides the throw via
|
|
143
|
+
* LOADER_ERROR_FALLBACK for the boundary above the readers to render.
|
|
144
|
+
*/
|
|
145
|
+
export function decodeLoaderEntry(result: any): any {
|
|
146
|
+
if (!isLoaderDataResult(result)) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
if (result.ok) {
|
|
150
|
+
return result.data;
|
|
151
|
+
}
|
|
152
|
+
throwSignal(result);
|
|
153
|
+
const info = result.error;
|
|
154
|
+
const err = new Error(
|
|
155
|
+
info.message,
|
|
156
|
+
info.cause !== undefined ? { cause: info.cause } : undefined,
|
|
157
|
+
);
|
|
158
|
+
if (info.name) err.name = info.name;
|
|
159
|
+
if (info.stack) err.stack = info.stack;
|
|
160
|
+
if (info.code !== undefined) (err as { code?: string }).code = info.code;
|
|
161
|
+
if (result.fallback != null) {
|
|
162
|
+
(err as any)[LOADER_ERROR_FALLBACK] = result.fallback;
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -62,6 +62,20 @@ export class DataNotFoundError extends Error {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// name fallback covers cross-realm errors (Vite dev dupes, RSC serialization)
|
|
66
|
+
// where instanceof fails — same rationale as isRouteNotFoundError above. Every
|
|
67
|
+
// notFound() origin (segment resolution, loader envelopes, fetchable loaders)
|
|
68
|
+
// must use this rather than a bare instanceof, or the same thrown signal is
|
|
69
|
+
// honored on one path and escapes as a 500 on another.
|
|
70
|
+
export function isDataNotFoundError(
|
|
71
|
+
error: unknown,
|
|
72
|
+
): error is DataNotFoundError {
|
|
73
|
+
return (
|
|
74
|
+
error instanceof DataNotFoundError ||
|
|
75
|
+
(error instanceof Error && error.name === "DataNotFoundError")
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
/**
|
|
66
80
|
* Convenience function to throw a DataNotFoundError
|
|
67
81
|
* Shorter syntax for common not-found scenarios
|
|
@@ -92,14 +92,21 @@ async function resolveValues(values: unknown[]): Promise<unknown[]> {
|
|
|
92
92
|
export async function* resolvedHandleStream(
|
|
93
93
|
handleStore: HandleStore,
|
|
94
94
|
): AsyncGenerator<HandleData, void, unknown> {
|
|
95
|
-
// Drain stream() (NOT getData()) for the converged snapshot:
|
|
96
|
-
// store's
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
95
|
+
// Drain stream() (NOT getData()) for the converged snapshot: consuming a
|
|
96
|
+
// stream arms the store's late-push guard (LateHandlePushError for pushes
|
|
97
|
+
// after FULL settle — an async JSX subtree that suspended and later calls
|
|
98
|
+
// ctx.use(Handle)(...) after collection). getData() never arms it, so the
|
|
99
|
+
// late push would silently land.
|
|
100
|
+
//
|
|
101
|
+
// Scoped to the HANDLER barrier ("settled"), NOT full settle: both document
|
|
102
|
+
// consumers (ssr-root.tsx React.use, rsc-router.tsx pre-hydration drain)
|
|
103
|
+
// block on this generator's COMPLETION, so waiting for the auxiliary
|
|
104
|
+
// (loader) lane would hold SSR markup and hydration hostage to the slowest
|
|
105
|
+
// streaming loader. Loader pushes that beat the handler barrier are in this
|
|
106
|
+
// snapshot (the race); later ones ride metadata.handlesLate
|
|
107
|
+
// (handleStore.streamLate()), applied client-side post-hydration.
|
|
101
108
|
let snapshot: HandleData = {};
|
|
102
|
-
for await (const data of handleStore.stream()) {
|
|
109
|
+
for await (const data of handleStore.stream("settled")) {
|
|
103
110
|
snapshot = data;
|
|
104
111
|
}
|
|
105
112
|
yield await resolveDeferredHandleValues(snapshot);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import type { NavigateOptionsInternal } from "./browser/types.js";
|
|
5
|
+
import { useRouter } from "./browser/react/use-router.js";
|
|
6
|
+
import { validateExternalRedirect } from "./browser/validate-redirect-origin.js";
|
|
7
|
+
import { resolveSameOriginRedirect } from "./redirect-origin.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Executes a loader-thrown redirect() once mounted. The target was resolved
|
|
11
|
+
* through the soft-redirect same-origin rules server-side
|
|
12
|
+
* (wrapLoaderWithErrorHandling), so anything absolute AND cross-origin here is
|
|
13
|
+
* an explicit `{ external: true }` opt-in — those leave via location.replace
|
|
14
|
+
* (the SPA router cannot cross origins); everything else is a router
|
|
15
|
+
* replace-navigation. Renders nothing while the navigation commits.
|
|
16
|
+
*
|
|
17
|
+
* Own module (not route-content-wrapper.tsx) because BOTH consumers need it
|
|
18
|
+
* without a cycle: the StreamedLoaderErrorBoundary redirect branch (read-site
|
|
19
|
+
* throw path) and decodeLoaderResults (aggregate forceAwait/action path, which
|
|
20
|
+
* runs during the server tree build and cannot throw — it plants this element
|
|
21
|
+
* in the errorFallback slot instead).
|
|
22
|
+
*/
|
|
23
|
+
export function LoaderRedirect({
|
|
24
|
+
to,
|
|
25
|
+
state,
|
|
26
|
+
}: {
|
|
27
|
+
to: string;
|
|
28
|
+
/**
|
|
29
|
+
* Resolved `redirect(url, { state })` record off the loader-result marker.
|
|
30
|
+
* Delivered as the redirect navigation's state (same application path as
|
|
31
|
+
* the ServerRedirect lane in navigation-bridge.ts: buildHistoryState
|
|
32
|
+
* spreads the `__rsc_ls_*` keys into the target entry). `_skipCache`
|
|
33
|
+
* matches that lane — a cached segment commit must not skip the re-render
|
|
34
|
+
* that lets useLocationState read the fresh entry.
|
|
35
|
+
*/
|
|
36
|
+
state?: Record<string, unknown>;
|
|
37
|
+
}): ReactNode {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const origin = window.location.origin;
|
|
41
|
+
// Same-origin first: a target that resolves on-origin is a router
|
|
42
|
+
// navigation. Only when it does NOT is this the `{ external: true }`
|
|
43
|
+
// opt-in, which waives same-origin but NOT scheme safety — both branches
|
|
44
|
+
// go through the shared resolvers in redirect-origin.ts so this channel
|
|
45
|
+
// cannot drift from the fetch/action/document ones.
|
|
46
|
+
// Pure resolver (not validateRedirectOrigin) for the probe: an off-origin
|
|
47
|
+
// target here is the legitimate external opt-in, not a blocked redirect,
|
|
48
|
+
// so it must not log a "blocked" error on its way to the branch below.
|
|
49
|
+
const sameOrigin = resolveSameOriginRedirect(to, origin);
|
|
50
|
+
if (sameOrigin) {
|
|
51
|
+
const options: Omit<NavigateOptionsInternal, "replace"> | undefined =
|
|
52
|
+
state ? { state, _skipCache: true } : undefined;
|
|
53
|
+
void router.replace(sameOrigin, options);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const externalUrl = validateExternalRedirect(to, origin);
|
|
57
|
+
if (externalUrl) {
|
|
58
|
+
window.location.replace(externalUrl);
|
|
59
|
+
}
|
|
60
|
+
// Redirect targets are request-scoped constants; re-running on identity
|
|
61
|
+
// change only (StrictMode double-invoke is idempotent — same target).
|
|
62
|
+
}, [to, state, router]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
package/src/outlet-context.ts
CHANGED
|
@@ -3,9 +3,21 @@ import type { ResolvedSegment } from "./types";
|
|
|
3
3
|
|
|
4
4
|
export interface OutletContextValue {
|
|
5
5
|
content: ReactNode;
|
|
6
|
+
/** Unresolved client-route work owned by descendants of this outlet. */
|
|
7
|
+
pending?: boolean;
|
|
6
8
|
parallel?: ResolvedSegment[];
|
|
7
9
|
segment?: ResolvedSegment;
|
|
8
10
|
loaderData?: Record<string, any>;
|
|
11
|
+
/**
|
|
12
|
+
* SPIKE (streaming useLoader): per-loader UNDECODED results keyed by loader
|
|
13
|
+
* $$id. A value may be a pending Promise (loader still streaming) or an
|
|
14
|
+
* already-settled result entry. useLoader `use()`es a pending value at the
|
|
15
|
+
* read site — implicit suspension to the nearest consumer Suspense boundary —
|
|
16
|
+
* and decodes via decodeLoaderEntry. Populated instead of `loaderData` on
|
|
17
|
+
* streaming lanes (plain navs, document SSR); forceAwait/action lanes keep
|
|
18
|
+
* the resolved `loaderData` record.
|
|
19
|
+
*/
|
|
20
|
+
loaderStreams?: Record<string, unknown>;
|
|
9
21
|
parent?: OutletContextValue | null;
|
|
10
22
|
/** Loading component for Suspense fallback (from segment's loading() definition) */
|
|
11
23
|
loading?: ReactNode;
|
package/src/outlet-provider.tsx
CHANGED
|
@@ -12,12 +12,16 @@ export function OutletProvider({
|
|
|
12
12
|
parallel,
|
|
13
13
|
segment,
|
|
14
14
|
loaderData,
|
|
15
|
+
loaderStreams,
|
|
16
|
+
pending = false,
|
|
15
17
|
children,
|
|
16
18
|
}: {
|
|
17
19
|
content: ReactNode;
|
|
18
20
|
parallel?: ResolvedSegment[];
|
|
19
21
|
segment?: ResolvedSegment;
|
|
20
22
|
loaderData?: Record<string, any>;
|
|
23
|
+
loaderStreams?: Record<string, unknown>;
|
|
24
|
+
pending?: boolean;
|
|
21
25
|
children: ReactNode;
|
|
22
26
|
}): ReactNode {
|
|
23
27
|
// Get parent context to enable walking up the chain for loader lookups
|
|
@@ -29,10 +33,20 @@ export function OutletProvider({
|
|
|
29
33
|
parallel,
|
|
30
34
|
segment,
|
|
31
35
|
loaderData,
|
|
36
|
+
loaderStreams,
|
|
37
|
+
pending,
|
|
32
38
|
parent: parentContext,
|
|
33
39
|
loading: segment?.loading,
|
|
34
40
|
}),
|
|
35
|
-
[
|
|
41
|
+
[
|
|
42
|
+
content,
|
|
43
|
+
parallel,
|
|
44
|
+
segment,
|
|
45
|
+
loaderData,
|
|
46
|
+
loaderStreams,
|
|
47
|
+
pending,
|
|
48
|
+
parentContext,
|
|
49
|
+
],
|
|
36
50
|
);
|
|
37
51
|
|
|
38
52
|
return (
|