@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2
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/{CLAUDE.md → AGENTS.md} +4 -0
- package/README.md +122 -30
- package/dist/bin/rango.js +245 -63
- package/dist/vite/index.js +859 -418
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +72 -22
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +34 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/typesafety/SKILL.md +35 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +114 -18
- package/src/browser/navigation-client.ts +126 -44
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +166 -27
- package/src/browser/prefetch/fetch.ts +52 -39
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +70 -14
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +143 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +454 -436
- package/src/browser/types.ts +60 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +346 -87
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +3 -102
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +8 -37
- package/src/index.ts +40 -66
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +108 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +123 -11
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +22 -15
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +53 -12
- package/src/router/middleware.ts +172 -85
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +200 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +429 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +1 -0
- package/src/router.ts +88 -15
- package/src/rsc/handler.ts +546 -359
- package/src/rsc/index.ts +0 -20
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +25 -8
- package/src/rsc/rsc-rendering.ts +35 -43
- package/src/rsc/server-action.ts +16 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +10 -1
- package/src/search-params.ts +16 -13
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +148 -16
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +182 -34
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +48 -15
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +174 -211
- package/src/vite/router-discovery.ts +169 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +78 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -281,7 +281,7 @@ import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
|
281
281
|
|
|
282
282
|
// RouteSearchParams<"name"> resolves the search schema to a typed object
|
|
283
283
|
type SP = RouteSearchParams<"search">;
|
|
284
|
-
// { q: string; page?: number; sort?: string }
|
|
284
|
+
// { q: string | undefined; page?: number; sort?: string }
|
|
285
285
|
|
|
286
286
|
// RouteParams<"name"> resolves URL params from the route pattern
|
|
287
287
|
type P = RouteParams<"blogPost">;
|
|
@@ -334,7 +334,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
334
334
|
});
|
|
335
335
|
|
|
336
336
|
// In server component - type is inferred
|
|
337
|
-
import { useLoader } from "@rangojs/router";
|
|
337
|
+
import { useLoader } from "@rangojs/router/client";
|
|
338
338
|
|
|
339
339
|
async function ProductPage() {
|
|
340
340
|
const product = await useLoader(ProductLoader);
|
|
@@ -369,8 +369,18 @@ interface PaginationData {
|
|
|
369
369
|
perPage: number;
|
|
370
370
|
}
|
|
371
371
|
export const Pagination = createVar<PaginationData>();
|
|
372
|
+
|
|
373
|
+
// Non-cacheable var — reading inside cache() or "use cache" throws at runtime
|
|
374
|
+
const Session = createVar<SessionData>({ cache: false });
|
|
372
375
|
```
|
|
373
376
|
|
|
377
|
+
`createVar` accepts an optional options object. The `cache` option (default
|
|
378
|
+
`true`) controls whether the var's values can be read inside cache scopes.
|
|
379
|
+
Write-level escalation is also supported: `ctx.set(Var, value, { cache: false })`
|
|
380
|
+
marks a specific write as non-cacheable even if the var itself is cacheable.
|
|
381
|
+
"Least cacheable wins" — if either says `cache: false`, the value throws on
|
|
382
|
+
read inside `cache()` or `"use cache"`.
|
|
383
|
+
|
|
374
384
|
### Producer (handler or middleware)
|
|
375
385
|
|
|
376
386
|
```typescript
|
|
@@ -414,26 +424,30 @@ Both approaches coexist: `ctx.get("user")` (global via Vars) and
|
|
|
414
424
|
Handles have typed data:
|
|
415
425
|
|
|
416
426
|
```typescript
|
|
417
|
-
//
|
|
418
|
-
import {
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// In client - typed array
|
|
427
|
+
// Built-in Breadcrumbs handle — import from "@rangojs/router"
|
|
428
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
429
|
+
// Type: Handle<BreadcrumbItem, BreadcrumbItem[]>
|
|
430
|
+
// BreadcrumbItem: { label: string; href: string; content?: ReactNode | Promise<ReactNode> }
|
|
431
|
+
|
|
432
|
+
// In route handler — push is fully typed
|
|
433
|
+
path("/shop/product/:slug", (ctx) => {
|
|
434
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
435
|
+
breadcrumb({ label: "Products", href: "/shop/products" });
|
|
436
|
+
return <ProductPage />;
|
|
437
|
+
}, { name: "product" });
|
|
438
|
+
|
|
439
|
+
// In client — typed array
|
|
440
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
433
441
|
function BreadcrumbNav() {
|
|
434
442
|
const crumbs = useHandle(Breadcrumbs);
|
|
435
|
-
// crumbs:
|
|
443
|
+
// crumbs: BreadcrumbItem[]
|
|
436
444
|
}
|
|
445
|
+
|
|
446
|
+
// Custom handles also work the same way
|
|
447
|
+
import { createHandle } from "@rangojs/router";
|
|
448
|
+
export const PageTitle = createHandle<string, string>(
|
|
449
|
+
(segments) => segments.flat().at(-1) ?? "Default Title"
|
|
450
|
+
);
|
|
437
451
|
```
|
|
438
452
|
|
|
439
453
|
## Ref Prop Type Safety (Loaders & Handles)
|
|
@@ -447,14 +461,12 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
447
461
|
return { product: await fetchProduct(ctx.params.slug) };
|
|
448
462
|
});
|
|
449
463
|
|
|
450
|
-
//
|
|
451
|
-
export const Breadcrumbs = createHandle<{ label: string; href: string }>();
|
|
464
|
+
// Built-in Breadcrumbs — or any custom handle created with createHandle()
|
|
452
465
|
|
|
453
466
|
// Client component — typeof infers all generics
|
|
454
467
|
("use client");
|
|
455
|
-
import { useLoader, useHandle } from "@rangojs/router/client";
|
|
468
|
+
import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
456
469
|
import type { ProductLoader } from "../loaders";
|
|
457
|
-
import type { Breadcrumbs } from "../handles";
|
|
458
470
|
|
|
459
471
|
function MyComponent({
|
|
460
472
|
loader,
|
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, isPassthroughHandler } 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
|
// ============================================================================
|
package/src/bin/rango.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
writeCombinedRouteTypes,
|
|
7
7
|
detectUnresolvableIncludes,
|
|
8
8
|
detectUnresolvableIncludesForUrlsFile,
|
|
9
|
+
findNestedRouterConflict,
|
|
10
|
+
formatNestedRouterConflictError,
|
|
9
11
|
type UnresolvableInclude,
|
|
10
12
|
} from "../build/generate-route-types.ts";
|
|
11
13
|
|
|
@@ -205,6 +207,14 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
205
207
|
console.warn("");
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFiles);
|
|
211
|
+
if (nestedRouterConflict) {
|
|
212
|
+
console.error(
|
|
213
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
// Phase 3: Write all outputs (only reached if diagnostics pass or --static)
|
|
209
219
|
for (const urlsFile of urlsFiles) {
|
|
210
220
|
writePerModuleRouteTypesForFile(urlsFile);
|
|
@@ -259,6 +269,14 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
259
269
|
process.exit(1);
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
const nestedRouterConflict = findNestedRouterConflict(routerEntries);
|
|
273
|
+
if (nestedRouterConflict) {
|
|
274
|
+
console.error(
|
|
275
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
262
280
|
let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
|
|
263
281
|
try {
|
|
264
282
|
const mod = await import("../build/runtime-discovery.ts");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutable app version — updated after HMR revalidation.
|
|
3
|
+
* Read by prefetch, navigation, and context code.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let currentVersion: string | undefined;
|
|
7
|
+
|
|
8
|
+
export function getAppVersion(): string | undefined {
|
|
9
|
+
return currentVersion;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setAppVersion(version: string | undefined): void {
|
|
13
|
+
currentVersion = version;
|
|
14
|
+
}
|
|
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
|
|
|
79
79
|
state: "idle" | "loading";
|
|
80
80
|
/** Whether any operation is streaming */
|
|
81
81
|
isStreaming: boolean;
|
|
82
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
83
|
+
isNavigating: boolean;
|
|
82
84
|
/** Current committed location */
|
|
83
85
|
location: NavigationLocation;
|
|
84
86
|
/** URL being navigated to (null if idle) */
|
|
@@ -389,6 +391,9 @@ export function createEventController(
|
|
|
389
391
|
return {
|
|
390
392
|
state,
|
|
391
393
|
isStreaming,
|
|
394
|
+
// True when a navigation is active (fetching or streaming, before
|
|
395
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
396
|
+
isNavigating: currentNavigation !== null,
|
|
392
397
|
location,
|
|
393
398
|
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
399
|
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
@@ -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
|
};
|
|
@@ -4,12 +4,19 @@ import type {
|
|
|
4
4
|
NavigateOptionsInternal,
|
|
5
5
|
ResolvedSegment,
|
|
6
6
|
} from "./types.js";
|
|
7
|
+
import { setAppVersion } from "./app-version.js";
|
|
7
8
|
import * as React from "react";
|
|
8
9
|
import { startTransition } from "react";
|
|
9
10
|
import {
|
|
10
11
|
createNavigationTransaction,
|
|
11
12
|
resolveNavigationState,
|
|
12
13
|
} from "./navigation-transaction.js";
|
|
14
|
+
import { buildHistoryState } from "./history-state.js";
|
|
15
|
+
import {
|
|
16
|
+
handleNavigationStart,
|
|
17
|
+
handleNavigationEnd,
|
|
18
|
+
ensureHistoryKey,
|
|
19
|
+
} from "./scroll-restoration.js";
|
|
13
20
|
|
|
14
21
|
// addTransitionType is only available in React experimental
|
|
15
22
|
const addTransitionType: ((type: string) => void) | undefined =
|
|
@@ -18,7 +25,6 @@ const addTransitionType: ((type: string) => void) | undefined =
|
|
|
18
25
|
import { setupLinkInterception } from "./link-interceptor.js";
|
|
19
26
|
import { createPartialUpdater } from "./partial-update.js";
|
|
20
27
|
import { generateHistoryKey } from "./navigation-store.js";
|
|
21
|
-
import { handleNavigationEnd } from "./scroll-restoration.js";
|
|
22
28
|
import type { EventController } from "./event-controller.js";
|
|
23
29
|
import { isInterceptOnlyCache } from "./intercept-utils.js";
|
|
24
30
|
import {
|
|
@@ -35,11 +41,6 @@ if (typeof Symbol.dispose === "undefined") {
|
|
|
35
41
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
/** Get IDs of non-loader segments (layouts, routes, parallels). */
|
|
39
|
-
function getNonLoaderSegmentIds(segments: ResolvedSegment[]): string[] {
|
|
40
|
-
return segments.filter((s) => s.type !== "loader").map((s) => s.id);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
44
|
export { createNavigationTransaction };
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -67,8 +68,8 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
67
68
|
export function createNavigationBridge(
|
|
68
69
|
config: NavigationBridgeConfigWithController,
|
|
69
70
|
): NavigationBridge {
|
|
70
|
-
const { store, client, eventController, onUpdate, renderSegments
|
|
71
|
-
|
|
71
|
+
const { store, client, eventController, onUpdate, renderSegments } = config;
|
|
72
|
+
let version = config.version;
|
|
72
73
|
|
|
73
74
|
// Create shared partial updater
|
|
74
75
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -76,7 +77,7 @@ export function createNavigationBridge(
|
|
|
76
77
|
client,
|
|
77
78
|
onUpdate,
|
|
78
79
|
renderSegments,
|
|
79
|
-
version,
|
|
80
|
+
getVersion: () => version,
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
return {
|
|
@@ -114,6 +115,85 @@ export function createNavigationBridge(
|
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Shallow navigation: skip RSC fetch when revalidate is false
|
|
119
|
+
// and the pathname hasn't changed (search param / hash only change).
|
|
120
|
+
if (
|
|
121
|
+
options?.revalidate === false &&
|
|
122
|
+
targetUrl.pathname === new URL(window.location.href).pathname
|
|
123
|
+
) {
|
|
124
|
+
// Preserve intercept context from the current history entry so that
|
|
125
|
+
// popstate uses the correct cache key (:intercept suffix) and restores
|
|
126
|
+
// the right full-page vs modal semantics.
|
|
127
|
+
const currentHistoryState = window.history.state;
|
|
128
|
+
const isIntercept = currentHistoryState?.intercept === true;
|
|
129
|
+
const interceptSourceUrl = isIntercept
|
|
130
|
+
? currentHistoryState?.sourceUrl
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
133
|
+
const historyKey = generateHistoryKey(url, { intercept: isIntercept });
|
|
134
|
+
|
|
135
|
+
// Copy current segments to the new history key so back/forward restores instantly
|
|
136
|
+
const currentKey = store.getHistoryKey();
|
|
137
|
+
const currentCache = store.getCachedSegments(currentKey);
|
|
138
|
+
if (currentCache?.segments) {
|
|
139
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
140
|
+
store.cacheSegmentsForHistory(
|
|
141
|
+
historyKey,
|
|
142
|
+
currentCache.segments,
|
|
143
|
+
currentHandleData,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Save current scroll position before changing URL
|
|
148
|
+
handleNavigationStart();
|
|
149
|
+
|
|
150
|
+
// Snapshot old state before pushState/replaceState overwrites it
|
|
151
|
+
const oldState = window.history.state;
|
|
152
|
+
|
|
153
|
+
// Update browser URL (carry intercept context into history state)
|
|
154
|
+
const historyState = buildHistoryState(
|
|
155
|
+
resolvedState,
|
|
156
|
+
{
|
|
157
|
+
intercept: isIntercept || undefined,
|
|
158
|
+
sourceUrl: interceptSourceUrl,
|
|
159
|
+
},
|
|
160
|
+
{},
|
|
161
|
+
);
|
|
162
|
+
if (options.replace) {
|
|
163
|
+
window.history.replaceState(historyState, "", url);
|
|
164
|
+
} else {
|
|
165
|
+
window.history.pushState(historyState, "", url);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Ensure new history entry has a scroll restoration key
|
|
169
|
+
ensureHistoryKey();
|
|
170
|
+
|
|
171
|
+
// Notify useLocationState() hooks when state changes
|
|
172
|
+
const hasOldState =
|
|
173
|
+
oldState &&
|
|
174
|
+
typeof oldState === "object" &&
|
|
175
|
+
("state" in oldState ||
|
|
176
|
+
Object.keys(oldState).some((k) => k.startsWith("__rsc_ls_")));
|
|
177
|
+
const hasNewState =
|
|
178
|
+
historyState &&
|
|
179
|
+
("state" in historyState ||
|
|
180
|
+
Object.keys(historyState).some((k) => k.startsWith("__rsc_ls_")));
|
|
181
|
+
if (hasOldState || hasNewState) {
|
|
182
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update store history key so future navigations reference the right cache
|
|
186
|
+
store.setHistoryKey(historyKey);
|
|
187
|
+
store.setCurrentUrl(url);
|
|
188
|
+
|
|
189
|
+
// Notify hooks — location updates, state stays idle
|
|
190
|
+
eventController.setLocation(targetUrl);
|
|
191
|
+
|
|
192
|
+
// Handle post-navigation scroll
|
|
193
|
+
handleNavigationEnd({ scroll: options.scroll });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
117
197
|
// Only abort pending requests when navigating to a different route
|
|
118
198
|
// Same-route navigation (e.g., /todos -> /todos) should not cancel in-flight actions
|
|
119
199
|
const currentPath = new URL(window.location.href).pathname;
|
|
@@ -189,7 +269,7 @@ export function createNavigationBridge(
|
|
|
189
269
|
!isLeavingIntercept &&
|
|
190
270
|
!options?._skipCache;
|
|
191
271
|
|
|
192
|
-
|
|
272
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
193
273
|
...options,
|
|
194
274
|
state: resolvedState,
|
|
195
275
|
skipLoadingState: hasUsableCache,
|
|
@@ -200,7 +280,7 @@ export function createNavigationBridge(
|
|
|
200
280
|
await fetchPartialUpdate(
|
|
201
281
|
url,
|
|
202
282
|
hasUsableCache
|
|
203
|
-
?
|
|
283
|
+
? cachedSegments!.map((s) => s.id)
|
|
204
284
|
: options?._skipCache
|
|
205
285
|
? [] // Action redirect: send no segments so server renders everything fresh
|
|
206
286
|
: undefined,
|
|
@@ -224,7 +304,7 @@ export function createNavigationBridge(
|
|
|
224
304
|
);
|
|
225
305
|
} catch (error) {
|
|
226
306
|
// Server-side redirect with location state: the current transaction's
|
|
227
|
-
//
|
|
307
|
+
// cleanup resets loading state. Re-navigate to the redirect
|
|
228
308
|
// target carrying the server-set state into history.pushState.
|
|
229
309
|
if (error instanceof ServerRedirect) {
|
|
230
310
|
const redirectUrl = validateRedirectOrigin(
|
|
@@ -260,6 +340,8 @@ export function createNavigationBridge(
|
|
|
260
340
|
}
|
|
261
341
|
|
|
262
342
|
throw error;
|
|
343
|
+
} finally {
|
|
344
|
+
tx[Symbol.dispose]();
|
|
263
345
|
}
|
|
264
346
|
},
|
|
265
347
|
|
|
@@ -269,7 +351,7 @@ export function createNavigationBridge(
|
|
|
269
351
|
async refresh(): Promise<void> {
|
|
270
352
|
eventController.abortNavigation();
|
|
271
353
|
|
|
272
|
-
|
|
354
|
+
const tx = createNavigationTransaction(
|
|
273
355
|
store,
|
|
274
356
|
eventController,
|
|
275
357
|
window.location.href,
|
|
@@ -299,6 +381,8 @@ export function createNavigationBridge(
|
|
|
299
381
|
return;
|
|
300
382
|
}
|
|
301
383
|
throw error;
|
|
384
|
+
} finally {
|
|
385
|
+
tx[Symbol.dispose]();
|
|
302
386
|
}
|
|
303
387
|
},
|
|
304
388
|
|
|
@@ -364,6 +448,12 @@ export function createNavigationBridge(
|
|
|
364
448
|
store.setCurrentUrl(url);
|
|
365
449
|
store.setPath(new URL(url).pathname);
|
|
366
450
|
|
|
451
|
+
// Restore router identity from cache so subsequent navigations
|
|
452
|
+
// don't falsely detect an app switch.
|
|
453
|
+
if (cached?.routerId) {
|
|
454
|
+
store.setRouterId?.(cached.routerId);
|
|
455
|
+
}
|
|
456
|
+
|
|
367
457
|
// Render from cache - force await to skip loading fallbacks
|
|
368
458
|
try {
|
|
369
459
|
const root = await renderSegments(cachedSegments, {
|
|
@@ -389,6 +479,7 @@ export function createNavigationBridge(
|
|
|
389
479
|
cachedHandleData,
|
|
390
480
|
params: cachedParams,
|
|
391
481
|
},
|
|
482
|
+
scroll: { restore: true, isStreaming },
|
|
392
483
|
};
|
|
393
484
|
const hasTransition = cachedSegments.some((s) => s.transition);
|
|
394
485
|
if (hasTransition) {
|
|
@@ -402,14 +493,11 @@ export function createNavigationBridge(
|
|
|
402
493
|
onUpdate(popstateUpdate);
|
|
403
494
|
}
|
|
404
495
|
|
|
405
|
-
// Restore scroll position for back/forward navigation
|
|
406
|
-
handleNavigationEnd({ restore: true, isStreaming });
|
|
407
|
-
|
|
408
496
|
// SWR: If stale, trigger background revalidation
|
|
409
497
|
if (isStale) {
|
|
410
498
|
debugLog("[Browser] Cache is stale, background revalidating...");
|
|
411
499
|
// Background revalidation - don't await, just fire and forget
|
|
412
|
-
const segmentIds =
|
|
500
|
+
const segmentIds = cachedSegments.map((s) => s.id);
|
|
413
501
|
|
|
414
502
|
const tx = createNavigationTransaction(
|
|
415
503
|
store,
|
|
@@ -457,7 +545,7 @@ export function createNavigationBridge(
|
|
|
457
545
|
}
|
|
458
546
|
|
|
459
547
|
// Fetch if not cached
|
|
460
|
-
|
|
548
|
+
const tx = createNavigationTransaction(store, eventController, url, {
|
|
461
549
|
replace: true,
|
|
462
550
|
});
|
|
463
551
|
|
|
@@ -498,6 +586,8 @@ export function createNavigationBridge(
|
|
|
498
586
|
}
|
|
499
587
|
|
|
500
588
|
throw error;
|
|
589
|
+
} finally {
|
|
590
|
+
tx[Symbol.dispose]();
|
|
501
591
|
}
|
|
502
592
|
},
|
|
503
593
|
|
|
@@ -549,6 +639,12 @@ export function createNavigationBridge(
|
|
|
549
639
|
window.removeEventListener("pageshow", handlePageShow);
|
|
550
640
|
};
|
|
551
641
|
},
|
|
642
|
+
|
|
643
|
+
updateVersion(newVersion: string): void {
|
|
644
|
+
version = newVersion;
|
|
645
|
+
setAppVersion(newVersion);
|
|
646
|
+
store.clearHistoryCache();
|
|
647
|
+
},
|
|
552
648
|
};
|
|
553
649
|
}
|
|
554
650
|
|