@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.33
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 +2 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +44 -0
- package/skills/hooks/SKILL.md +20 -0
- package/skills/loader/SKILL.md +55 -15
- package/src/__internal.ts +92 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +85 -1
- package/src/browser/react/Link.tsx +14 -2
- package/src/browser/types.ts +19 -0
- package/src/index.rsc.ts +2 -35
- package/src/index.ts +1 -37
- package/src/router/match-api.ts +1 -1
- package/src/router/prerender-match.ts +2 -0
- package/src/router/segment-resolution/helpers.ts +1 -1
- package/src/server/request-context.ts +10 -2
package/README.md
CHANGED
|
@@ -275,7 +275,8 @@ All handler typing styles are supported, but they solve different problems:
|
|
|
275
275
|
Example of a scoped local name inside a mounted module:
|
|
276
276
|
|
|
277
277
|
```tsx
|
|
278
|
-
import type { Handler
|
|
278
|
+
import type { Handler } from "@rangojs/router";
|
|
279
|
+
import type { ScopedRouteMap } from "@rangojs/router/__internal";
|
|
279
280
|
|
|
280
281
|
type BlogRoutes = ScopedRouteMap<"blog">;
|
|
281
282
|
|
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.33",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -204,3 +204,47 @@ export const urlpatterns = urls(({ path, layout }) => [
|
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Navigating to `/shop/widget` produces: `Home / Shop / widget`
|
|
207
|
+
|
|
208
|
+
## Custom Handles
|
|
209
|
+
|
|
210
|
+
Create your own handle with `createHandle()`:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { createHandle } from "@rangojs/router";
|
|
214
|
+
|
|
215
|
+
// Default: flatten into array
|
|
216
|
+
export const PageTitle = createHandle<string, string>(
|
|
217
|
+
(segments) => segments.flat().at(-1) ?? "Default Title",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// No collect function: default flattens into T[]
|
|
221
|
+
export const Warnings = createHandle<string>();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The Vite `exposeInternalIds` plugin auto-injects a stable `$$id` based on
|
|
225
|
+
file path and export name. No manual naming required for project-local code.
|
|
226
|
+
|
|
227
|
+
### Handles in 3rd-party packages
|
|
228
|
+
|
|
229
|
+
The `exposeInternalIds` plugin skips `node_modules/`, so handles defined in
|
|
230
|
+
published packages won't get auto-injected IDs. Pass a manual tag as the
|
|
231
|
+
second argument to `createHandle()`:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { createHandle } from "@rangojs/router";
|
|
235
|
+
|
|
236
|
+
// With a collect function (reducer): collect is first arg, tag is second
|
|
237
|
+
export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
238
|
+
collectBreadcrumbs,
|
|
239
|
+
"__my_package_breadcrumbs__",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Without a collect function: pass undefined, then the tag
|
|
243
|
+
export const Warnings = createHandle<string>(
|
|
244
|
+
undefined,
|
|
245
|
+
"__my_package_warnings__",
|
|
246
|
+
);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The tag must be globally unique and stable across builds. Without it,
|
|
250
|
+
`createHandle` throws in development mode.
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -58,6 +58,26 @@ function NavigationControls() {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
#### Skipping revalidation
|
|
62
|
+
|
|
63
|
+
Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// Update search params without server round-trip
|
|
67
|
+
router.push("/products?color=blue", { revalidate: false });
|
|
68
|
+
router.replace("/products?page=3", { revalidate: false });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Link to="/products?color=blue" revalidate={false}>
|
|
75
|
+
Blue
|
|
76
|
+
</Link>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Plain `<a>` tags can opt in via `data-revalidate="false"`.
|
|
80
|
+
|
|
61
81
|
### useSegments()
|
|
62
82
|
|
|
63
83
|
Access current URL path and matched route segments:
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -65,19 +65,24 @@ export const urlpatterns = urls(({ path, loader }) => [
|
|
|
65
65
|
|
|
66
66
|
## Consuming Loader Data
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Loaders are the **live data layer** — they resolve fresh on every request.
|
|
69
|
+
The way you consume them depends on whether you're in a server component
|
|
70
|
+
(route handler) or a client component.
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
> **IMPORTANT: Prefer consuming loaders in client components.** Keeping data
|
|
73
|
+
> fetching in loaders and consumption in client components creates a clean
|
|
74
|
+
> separation: the server-side handler renders static markup that can be
|
|
75
|
+
> freely cached with `cache()`, while loader data stays fresh on every
|
|
76
|
+
> request. When you consume loaders in server handlers via `ctx.use()`, the
|
|
77
|
+
> handler output depends on the loader data, which means caching the handler
|
|
78
|
+
> also caches the data — defeating the purpose of the live data layer.
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
const { product } = await useLoader(ProductLoader);
|
|
76
|
-
return <h1>{product.name}</h1>;
|
|
77
|
-
}
|
|
78
|
-
```
|
|
80
|
+
### In Client Components (Preferred)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
Client components use `useLoader()` from `@rangojs/router/client`.
|
|
83
|
+
The loader **must** be registered with `loader()` in the route's DSL
|
|
84
|
+
segments so the framework knows to resolve it during SSR and stream
|
|
85
|
+
the data to the client:
|
|
81
86
|
|
|
82
87
|
```typescript
|
|
83
88
|
"use client";
|
|
@@ -90,6 +95,42 @@ function ProductDetails() {
|
|
|
90
95
|
}
|
|
91
96
|
```
|
|
92
97
|
|
|
98
|
+
```typescript
|
|
99
|
+
// Route definition — loader() registration required for client consumption
|
|
100
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
101
|
+
loader(ProductLoader), // Required for useLoader() in client components
|
|
102
|
+
]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### In Route Handlers (Server Components)
|
|
106
|
+
|
|
107
|
+
In server components, use `ctx.use(Loader)` directly in the route handler.
|
|
108
|
+
This doesn't require `loader()` registration in the DSL — it works
|
|
109
|
+
standalone. **However**, prefer client-side consumption when possible (see
|
|
110
|
+
note above).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { ProductLoader } from "./loaders/product";
|
|
114
|
+
|
|
115
|
+
// Route handler — server component
|
|
116
|
+
path("/product/:slug", async (ctx) => {
|
|
117
|
+
const { product } = await ctx.use(ProductLoader);
|
|
118
|
+
return <h1>{product.name}</h1>;
|
|
119
|
+
}, { name: "product" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When you do register with `loader()` in the DSL, `ctx.use()` returns the
|
|
123
|
+
same memoized result — loaders never run twice per request.
|
|
124
|
+
|
|
125
|
+
**Never use `useLoader()` in server components** — it is a client-only API.
|
|
126
|
+
|
|
127
|
+
### Summary
|
|
128
|
+
|
|
129
|
+
| Context | API | `loader()` DSL required? |
|
|
130
|
+
| ---------------------------- | ------------------- | ------------------------ |
|
|
131
|
+
| Client component (preferred) | `useLoader(Loader)` | **Yes** |
|
|
132
|
+
| Route handler (server) | `ctx.use(Loader)` | No |
|
|
133
|
+
|
|
93
134
|
## Loader Context
|
|
94
135
|
|
|
95
136
|
Loaders receive the same context as route handlers:
|
|
@@ -538,13 +579,12 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
|
|
|
538
579
|
]),
|
|
539
580
|
]);
|
|
540
581
|
|
|
541
|
-
// pages/product.tsx
|
|
542
|
-
import { useLoader } from "@rangojs/router/client";
|
|
582
|
+
// pages/product.tsx — server component (route handler)
|
|
543
583
|
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
544
584
|
|
|
545
|
-
async function ProductPage() {
|
|
546
|
-
const { product } = await
|
|
547
|
-
const { cart } = await
|
|
585
|
+
async function ProductPage(ctx) {
|
|
586
|
+
const { product } = await ctx.use(ProductLoader);
|
|
587
|
+
const { cart } = await ctx.use(CartLoader);
|
|
548
588
|
|
|
549
589
|
return (
|
|
550
590
|
<div>
|
package/src/__internal.ts
CHANGED
|
@@ -164,6 +164,98 @@ export type {
|
|
|
164
164
|
*/
|
|
165
165
|
export type { InternalHandlerContext } from "./types.js";
|
|
166
166
|
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Rendering (Internal)
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @internal
|
|
173
|
+
* Builds React element trees from route segments.
|
|
174
|
+
*/
|
|
175
|
+
export { renderSegments } from "./segment-system.js";
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Error Utilities (Internal)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @internal
|
|
183
|
+
* Error sanitization and network error utilities.
|
|
184
|
+
*/
|
|
185
|
+
export { sanitizeError, NetworkError, isNetworkError } from "./errors.js";
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Type Utilities (Internal)
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @internal
|
|
193
|
+
* Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>.
|
|
194
|
+
*/
|
|
195
|
+
export type { ScopedRouteMap } from "./types.js";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @internal
|
|
199
|
+
* Type-level utilities for reverse URL generation.
|
|
200
|
+
*/
|
|
201
|
+
export type { MergeRoutes, SanitizePrefix } from "./reverse.js";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @internal
|
|
205
|
+
* Individual telemetry event types.
|
|
206
|
+
*/
|
|
207
|
+
export type {
|
|
208
|
+
RequestStartEvent,
|
|
209
|
+
RequestEndEvent,
|
|
210
|
+
RequestErrorEvent,
|
|
211
|
+
RequestTimeoutEvent,
|
|
212
|
+
LoaderStartEvent,
|
|
213
|
+
LoaderEndEvent,
|
|
214
|
+
LoaderErrorEvent,
|
|
215
|
+
HandlerErrorEvent,
|
|
216
|
+
CacheDecisionEvent,
|
|
217
|
+
RevalidationDecisionEvent,
|
|
218
|
+
} from "./router/telemetry.js";
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Pre-render / Static Handler Guards (Internal)
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @internal
|
|
226
|
+
* Type guard for prerender handler definitions.
|
|
227
|
+
*/
|
|
228
|
+
export { isPrerenderHandler } from "./prerender.js";
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @internal
|
|
232
|
+
* Type guard for static handler definitions.
|
|
233
|
+
*/
|
|
234
|
+
export { isStaticHandler } from "./static-handler.js";
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// URL Pattern Internals
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @internal
|
|
242
|
+
* Sentinel used to tag response-type route entries.
|
|
243
|
+
*/
|
|
244
|
+
export { RESPONSE_TYPE } from "./urls.js";
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Route Match Debug (Internal)
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @internal
|
|
252
|
+
* Debug utilities for route matching performance analysis.
|
|
253
|
+
*/
|
|
254
|
+
export {
|
|
255
|
+
enableMatchDebug,
|
|
256
|
+
getMatchDebugStats,
|
|
257
|
+
} from "./router/pattern-matching.js";
|
|
258
|
+
|
|
167
259
|
// ============================================================================
|
|
168
260
|
// Debug Utilities (Internal)
|
|
169
261
|
// ============================================================================
|
|
@@ -117,6 +117,7 @@ export function setupLinkInterception(
|
|
|
117
117
|
// Read navigation options from data attributes (set by Link component)
|
|
118
118
|
const scrollAttr = link.getAttribute("data-scroll");
|
|
119
119
|
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
120
121
|
|
|
121
122
|
const navigateOptions: NavigateOptions = {};
|
|
122
123
|
if (scrollAttr === "false") {
|
|
@@ -125,6 +126,9 @@ export function setupLinkInterception(
|
|
|
125
126
|
if (replaceAttr === "true") {
|
|
126
127
|
navigateOptions.replace = true;
|
|
127
128
|
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
128
132
|
|
|
129
133
|
onNavigate(href, navigateOptions);
|
|
130
134
|
};
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
createNavigationTransaction,
|
|
11
11
|
resolveNavigationState,
|
|
12
12
|
} from "./navigation-transaction.js";
|
|
13
|
+
import { buildHistoryState } from "./history-state.js";
|
|
14
|
+
import {
|
|
15
|
+
handleNavigationStart,
|
|
16
|
+
handleNavigationEnd,
|
|
17
|
+
ensureHistoryKey,
|
|
18
|
+
} from "./scroll-restoration.js";
|
|
13
19
|
|
|
14
20
|
// addTransitionType is only available in React experimental
|
|
15
21
|
const addTransitionType: ((type: string) => void) | undefined =
|
|
@@ -18,7 +24,6 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
18
24
|
import { setupLinkInterception } from "./link-interceptor.js";
|
|
19
25
|
import { createPartialUpdater } from "./partial-update.js";
|
|
20
26
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
21
|
-
import { handleNavigationEnd } from "./scroll-restoration.js";
|
|
22
27
|
import type { EventController } from "./event-controller.js";
|
|
23
28
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
24
29
|
import {
|
|
@@ -114,6 +119,85 @@ export function createNavigationBridge(
|
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
// Shallow navigation: skip RSC fetch when revalidate is false
|
|
123
|
+
// and the pathname hasn't changed (search param / hash only change).
|
|
124
|
+
if (
|
|
125
|
+
options?.revalidate === false &&
|
|
126
|
+
targetUrl.pathname === new URL(window.location.href).pathname
|
|
127
|
+
) {
|
|
128
|
+
// Preserve intercept context from the current history entry so that
|
|
129
|
+
// popstate uses the correct cache key (:intercept suffix) and restores
|
|
130
|
+
// the right full-page vs modal semantics.
|
|
131
|
+
const currentHistoryState = window.history.state;
|
|
132
|
+
const isIntercept = currentHistoryState?.intercept === true;
|
|
133
|
+
const interceptSourceUrl = isIntercept
|
|
134
|
+
? currentHistoryState?.sourceUrl
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
138
|
+
|
|
139
|
+
// Copy current segments to the new history key so back/forward restores instantly
|
|
140
|
+
const currentKey = store.getHistoryKey();
|
|
141
|
+
const currentCache = store.getCachedSegments(currentKey);
|
|
142
|
+
if (currentCache?.segments) {
|
|
143
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
144
|
+
store.cacheSegmentsForHistory(
|
|
145
|
+
historyKey,
|
|
146
|
+
currentCache.segments,
|
|
147
|
+
currentHandleData,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Save current scroll position before changing URL
|
|
152
|
+
handleNavigationStart();
|
|
153
|
+
|
|
154
|
+
// Snapshot old state before pushState/replaceState overwrites it
|
|
155
|
+
const oldState = window.history.state;
|
|
156
|
+
|
|
157
|
+
// Update browser URL (carry intercept context into history state)
|
|
158
|
+
const historyState = buildHistoryState(
|
|
159
|
+
resolvedState,
|
|
160
|
+
{
|
|
161
|
+
intercept: isIntercept || undefined,
|
|
162
|
+
sourceUrl: interceptSourceUrl,
|
|
163
|
+
},
|
|
164
|
+
{},
|
|
165
|
+
);
|
|
166
|
+
if (options.replace) {
|
|
167
|
+
window.history.replaceState(historyState, "", url);
|
|
168
|
+
} else {
|
|
169
|
+
window.history.pushState(historyState, "", url);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Ensure new history entry has a scroll restoration key
|
|
173
|
+
ensureHistoryKey();
|
|
174
|
+
|
|
175
|
+
// Notify useLocationState() hooks when state changes
|
|
176
|
+
const hasOldState =
|
|
177
|
+
oldState &&
|
|
178
|
+
typeof oldState === "object" &&
|
|
179
|
+
("state" in oldState ||
|
|
180
|
+
Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
|
|
181
|
+
const hasNewState =
|
|
182
|
+
historyState &&
|
|
183
|
+
("state" in historyState ||
|
|
184
|
+
Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
|
|
185
|
+
if (hasOldState || hasNewState) {
|
|
186
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Update store history key so future navigations reference the right cache
|
|
190
|
+
store.setHistoryKey(historyKey);
|
|
191
|
+
store.setCurrentUrl(url);
|
|
192
|
+
|
|
193
|
+
// Notify hooks — location updates, state stays idle
|
|
194
|
+
eventController.setLocation(targetUrl);
|
|
195
|
+
|
|
196
|
+
// Handle post-navigation scroll
|
|
197
|
+
handleNavigationEnd({ scroll: options.scroll });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
117
201
|
// Only abort pending requests when navigating to a different route
|
|
118
202
|
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
119
203
|
const currentPath = new URL(window.location.href).pathname;
|
|
@@ -80,6 +80,16 @@ export interface LinkProps extends Omit<
|
|
|
80
80
|
* Force full document navigation instead of SPA
|
|
81
81
|
*/
|
|
82
82
|
reloadDocument?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether to revalidate server data on navigation.
|
|
85
|
+
* Set to `false` to skip the RSC server fetch and only update the URL.
|
|
86
|
+
*
|
|
87
|
+
* Only takes effect when the pathname stays the same (search param / hash changes).
|
|
88
|
+
* If the pathname changes, this option is ignored and a full navigation occurs.
|
|
89
|
+
*
|
|
90
|
+
* @default true
|
|
91
|
+
*/
|
|
92
|
+
revalidate?: boolean;
|
|
83
93
|
/**
|
|
84
94
|
* Prefetch strategy for the link destination
|
|
85
95
|
* @default "none"
|
|
@@ -170,6 +180,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
170
180
|
replace = false,
|
|
171
181
|
scroll = true,
|
|
172
182
|
reloadDocument = false,
|
|
183
|
+
revalidate,
|
|
173
184
|
prefetch = "none",
|
|
174
185
|
state,
|
|
175
186
|
children,
|
|
@@ -262,9 +273,9 @@ export const Link: ForwardRefExoticComponent<
|
|
|
262
273
|
resolvedState = currentState;
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
ctx.navigate(to, { replace, scroll, state: resolvedState });
|
|
276
|
+
ctx.navigate(to, { replace, scroll, state: resolvedState, revalidate });
|
|
266
277
|
},
|
|
267
|
-
[to, isExternal, reloadDocument, replace, scroll, ctx, onClick],
|
|
278
|
+
[to, isExternal, reloadDocument, replace, scroll, revalidate, ctx, onClick],
|
|
268
279
|
);
|
|
269
280
|
|
|
270
281
|
const handleMouseEnter = useCallback(() => {
|
|
@@ -340,6 +351,7 @@ export const Link: ForwardRefExoticComponent<
|
|
|
340
351
|
data-external={isExternal ? "" : undefined}
|
|
341
352
|
data-scroll={scroll === false ? "false" : undefined}
|
|
342
353
|
data-replace={replace ? "true" : undefined}
|
|
354
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
343
355
|
{...props}
|
|
344
356
|
>
|
|
345
357
|
<LinkContext.Provider value={to}>{children}</LinkContext.Provider>
|
package/src/browser/types.ts
CHANGED
|
@@ -232,6 +232,25 @@ export type HistoryState =
|
|
|
232
232
|
export interface NavigateOptions {
|
|
233
233
|
replace?: boolean;
|
|
234
234
|
scroll?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Whether to revalidate server data on navigation.
|
|
237
|
+
* Set to `false` to skip the RSC server fetch and only update the URL.
|
|
238
|
+
*
|
|
239
|
+
* Only takes effect when the pathname stays the same (search param / hash changes).
|
|
240
|
+
* If the pathname changes, this option is ignored and a full navigation occurs.
|
|
241
|
+
*
|
|
242
|
+
* All location-aware hooks (`useSearchParams`, `useNavigation`, etc.) still update.
|
|
243
|
+
* Server components do not re-render.
|
|
244
|
+
*
|
|
245
|
+
* @default true
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```tsx
|
|
249
|
+
* router.push("/products?color=blue", { revalidate: false });
|
|
250
|
+
* router.replace("/products?page=3", { revalidate: false });
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
revalidate?: boolean;
|
|
235
254
|
/**
|
|
236
255
|
* State to pass to history.pushState/replaceState
|
|
237
256
|
* Accessible via useLocationState() hook.
|
package/src/index.rsc.ts
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
// Re-export all universal exports from index.ts
|
|
13
13
|
export {
|
|
14
|
-
// Universal rendering utilities
|
|
15
|
-
renderSegments,
|
|
16
14
|
// Error classes
|
|
17
15
|
RouteNotFoundError,
|
|
18
16
|
DataNotFoundError,
|
|
@@ -21,9 +19,6 @@ export {
|
|
|
21
19
|
HandlerError,
|
|
22
20
|
BuildError,
|
|
23
21
|
InvalidHandlerError,
|
|
24
|
-
NetworkError,
|
|
25
|
-
isNetworkError,
|
|
26
|
-
sanitizeError,
|
|
27
22
|
RouterError,
|
|
28
23
|
Skip,
|
|
29
24
|
isSkip,
|
|
@@ -40,7 +35,6 @@ export type {
|
|
|
40
35
|
TrailingSlashMode,
|
|
41
36
|
// Handler types
|
|
42
37
|
Handler,
|
|
43
|
-
ScopedRouteMap,
|
|
44
38
|
HandlerContext,
|
|
45
39
|
ExtractParams,
|
|
46
40
|
GenericParams,
|
|
@@ -120,7 +114,6 @@ export { nonce } from "./rsc/nonce.js";
|
|
|
120
114
|
// Pre-render handler API
|
|
121
115
|
export {
|
|
122
116
|
Prerender,
|
|
123
|
-
isPrerenderHandler,
|
|
124
117
|
type PrerenderHandlerDefinition,
|
|
125
118
|
type PrerenderPassthroughContext,
|
|
126
119
|
type PrerenderOptions,
|
|
@@ -130,16 +123,11 @@ export {
|
|
|
130
123
|
} from "./prerender.js";
|
|
131
124
|
|
|
132
125
|
// Static handler API
|
|
133
|
-
export {
|
|
134
|
-
Static,
|
|
135
|
-
isStaticHandler,
|
|
136
|
-
type StaticHandlerDefinition,
|
|
137
|
-
} from "./static-handler.js";
|
|
126
|
+
export { Static, type StaticHandlerDefinition } from "./static-handler.js";
|
|
138
127
|
|
|
139
128
|
// Django-style URL patterns (RSC/server context)
|
|
140
129
|
export {
|
|
141
130
|
urls,
|
|
142
|
-
RESPONSE_TYPE,
|
|
143
131
|
type PathHelpers,
|
|
144
132
|
type PathOptions,
|
|
145
133
|
type UrlPatterns,
|
|
@@ -207,8 +195,6 @@ export type {
|
|
|
207
195
|
ReverseFunction,
|
|
208
196
|
ExtractLocalRoutes,
|
|
209
197
|
ParamsFor,
|
|
210
|
-
SanitizePrefix,
|
|
211
|
-
MergeRoutes,
|
|
212
198
|
} from "./reverse.js";
|
|
213
199
|
export { scopedReverse, createReverse } from "./reverse.js";
|
|
214
200
|
|
|
@@ -221,12 +207,6 @@ export type {
|
|
|
221
207
|
RouteParams,
|
|
222
208
|
} from "./search-params.js";
|
|
223
209
|
|
|
224
|
-
// Debug utilities for route matching (development only)
|
|
225
|
-
export {
|
|
226
|
-
enableMatchDebug,
|
|
227
|
-
getMatchDebugStats,
|
|
228
|
-
} from "./router/pattern-matching.js";
|
|
229
|
-
|
|
230
210
|
// Location state (universal)
|
|
231
211
|
export {
|
|
232
212
|
createLocationState,
|
|
@@ -242,20 +222,7 @@ export type { PathResponse } from "./href-client.js";
|
|
|
242
222
|
export { createConsoleSink } from "./router/telemetry.js";
|
|
243
223
|
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
244
224
|
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
245
|
-
export type {
|
|
246
|
-
TelemetrySink,
|
|
247
|
-
TelemetryEvent,
|
|
248
|
-
RequestStartEvent,
|
|
249
|
-
RequestEndEvent,
|
|
250
|
-
RequestErrorEvent,
|
|
251
|
-
RequestTimeoutEvent,
|
|
252
|
-
LoaderStartEvent,
|
|
253
|
-
LoaderEndEvent,
|
|
254
|
-
LoaderErrorEvent,
|
|
255
|
-
HandlerErrorEvent,
|
|
256
|
-
CacheDecisionEvent,
|
|
257
|
-
RevalidationDecisionEvent,
|
|
258
|
-
} from "./router/telemetry.js";
|
|
225
|
+
export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
|
|
259
226
|
|
|
260
227
|
// Timeout types and error class
|
|
261
228
|
export { RouterTimeoutError } from "./router/timeout.js";
|
package/src/index.ts
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
* import from "@rangojs/router/client"
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// Universal rendering utilities (work on both server and client)
|
|
14
|
-
export { renderSegments } from "./segment-system.js";
|
|
15
|
-
|
|
16
13
|
// Error classes (can be used on both server and client)
|
|
17
14
|
export {
|
|
18
15
|
RouteNotFoundError,
|
|
@@ -22,9 +19,6 @@ export {
|
|
|
22
19
|
HandlerError,
|
|
23
20
|
BuildError,
|
|
24
21
|
InvalidHandlerError,
|
|
25
|
-
NetworkError,
|
|
26
|
-
isNetworkError,
|
|
27
|
-
sanitizeError,
|
|
28
22
|
RouterError,
|
|
29
23
|
Skip,
|
|
30
24
|
isSkip,
|
|
@@ -41,7 +35,6 @@ export type {
|
|
|
41
35
|
TrailingSlashMode,
|
|
42
36
|
// Handler types
|
|
43
37
|
Handler, // Supports params object, path pattern, or route name
|
|
44
|
-
ScopedRouteMap, // Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>
|
|
45
38
|
HandlerContext,
|
|
46
39
|
ExtractParams,
|
|
47
40
|
GenericParams,
|
|
@@ -194,20 +187,6 @@ export function createReverse(): never {
|
|
|
194
187
|
throw serverOnlyStubError("createReverse");
|
|
195
188
|
}
|
|
196
189
|
|
|
197
|
-
/**
|
|
198
|
-
* Error-throwing stub for server-only `enableMatchDebug` function.
|
|
199
|
-
*/
|
|
200
|
-
export function enableMatchDebug(): never {
|
|
201
|
-
throw serverOnlyStubError("enableMatchDebug");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Error-throwing stub for server-only `getMatchDebugStats` function.
|
|
206
|
-
*/
|
|
207
|
-
export function getMatchDebugStats(): never {
|
|
208
|
-
throw serverOnlyStubError("getMatchDebugStats");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
190
|
// Error-throwing stubs for server-only route helpers
|
|
212
191
|
export function layout(): never {
|
|
213
192
|
throw serverOnlyStubError("layout");
|
|
@@ -268,8 +247,6 @@ export type {
|
|
|
268
247
|
ReverseFunction,
|
|
269
248
|
ExtractLocalRoutes,
|
|
270
249
|
ParamsFor,
|
|
271
|
-
SanitizePrefix,
|
|
272
|
-
MergeRoutes,
|
|
273
250
|
} from "./reverse.js";
|
|
274
251
|
// scopedReverse() helper for handlers to get locally-typed reverse
|
|
275
252
|
export { scopedReverse } from "./reverse.js";
|
|
@@ -289,20 +266,7 @@ export type { PathResponse } from "./href-client.js";
|
|
|
289
266
|
export { createConsoleSink } from "./router/telemetry.js";
|
|
290
267
|
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
291
268
|
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
292
|
-
export type {
|
|
293
|
-
TelemetrySink,
|
|
294
|
-
TelemetryEvent,
|
|
295
|
-
RequestStartEvent,
|
|
296
|
-
RequestEndEvent,
|
|
297
|
-
RequestErrorEvent,
|
|
298
|
-
RequestTimeoutEvent,
|
|
299
|
-
LoaderStartEvent,
|
|
300
|
-
LoaderEndEvent,
|
|
301
|
-
LoaderErrorEvent,
|
|
302
|
-
HandlerErrorEvent,
|
|
303
|
-
CacheDecisionEvent,
|
|
304
|
-
RevalidationDecisionEvent,
|
|
305
|
-
} from "./router/telemetry.js";
|
|
269
|
+
export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
|
|
306
270
|
|
|
307
271
|
// Timeout types and error class
|
|
308
272
|
export { RouterTimeoutError } from "./router/timeout.js";
|
package/src/router/match-api.ts
CHANGED
|
@@ -117,6 +117,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
117
117
|
deleteCookie: () => {},
|
|
118
118
|
header: () => {},
|
|
119
119
|
setStatus: () => {},
|
|
120
|
+
_setStatus: () => {},
|
|
120
121
|
use: (() => {
|
|
121
122
|
throw new Error("use() not available during pre-rendering");
|
|
122
123
|
}) as any,
|
|
@@ -346,6 +347,7 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
346
347
|
deleteCookie: () => {},
|
|
347
348
|
header: () => {},
|
|
348
349
|
setStatus: () => {},
|
|
350
|
+
_setStatus: () => {},
|
|
349
351
|
use: (() => {
|
|
350
352
|
throw new Error("use() not available during static pre-rendering");
|
|
351
353
|
}) as any,
|
|
@@ -95,6 +95,8 @@ export interface RequestContext<
|
|
|
95
95
|
header(name: string, value: string): void;
|
|
96
96
|
/** Set the response status code */
|
|
97
97
|
setStatus(status: number): void;
|
|
98
|
+
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
99
|
+
_setStatus(status: number): void;
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
102
|
* Access loader data or push handle data.
|
|
@@ -301,6 +303,7 @@ export type PublicRequestContext<
|
|
|
301
303
|
| "_reportBackgroundError"
|
|
302
304
|
| "_debugPerformance"
|
|
303
305
|
| "_metricsStore"
|
|
306
|
+
| "_setStatus"
|
|
304
307
|
| "res"
|
|
305
308
|
>;
|
|
306
309
|
|
|
@@ -629,8 +632,13 @@ export function createRequestContext<TEnv>(
|
|
|
629
632
|
|
|
630
633
|
setStatus(status: number): void {
|
|
631
634
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
632
|
-
|
|
633
|
-
|
|
635
|
+
stubResponse = new Response(null, {
|
|
636
|
+
status,
|
|
637
|
+
headers: stubResponse.headers,
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
_setStatus(status: number): void {
|
|
634
642
|
stubResponse = new Response(null, {
|
|
635
643
|
status,
|
|
636
644
|
headers: stubResponse.headers,
|