@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Params and Search Types
|
|
2
|
+
|
|
3
|
+
## Typed Search Params
|
|
4
|
+
|
|
5
|
+
Add a `search` schema to `path()` options for type-safe query parameters:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Route definition with search schema
|
|
9
|
+
path("/search", SearchPage, {
|
|
10
|
+
name: "search",
|
|
11
|
+
search: { q: "string", page: "number?", sort: "string?" },
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Handler with typed search params
|
|
16
|
+
|
|
17
|
+
`Handler<"name">` automatically resolves route params and search params from the
|
|
18
|
+
global `GeneratedRouteMap` (the gen file). No explicit route map import needed:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// pages/search.tsx
|
|
22
|
+
import type { Handler } from "@rangojs/router";
|
|
23
|
+
|
|
24
|
+
export const SearchPage: Handler<"search"> = (ctx) => {
|
|
25
|
+
// ctx.search is typed: { q: string; page?: number; sort?: string }
|
|
26
|
+
const { q, page, sort } = ctx.search;
|
|
27
|
+
return <SearchResults q={q} page={page} sort={sort} />;
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This avoids circular references because `Handler` defaults to `GeneratedRouteMap`
|
|
32
|
+
(from `router.named-routes.gen.ts`) instead of `RegisteredRoutes` (which depends on `router.tsx`).
|
|
33
|
+
|
|
34
|
+
You can also pass an explicit route map for per-module isolation (opt-in,
|
|
35
|
+
after running `npx rango generate`). With a local map, the route name is
|
|
36
|
+
**dot-prefixed** so params and search resolve from `routes`, not the global map:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import type { Handler } from "@rangojs/router";
|
|
40
|
+
import type { routes } from "./urls.gen.js";
|
|
41
|
+
|
|
42
|
+
export const SearchPage: Handler<".search", routes> = (ctx) => { ... };
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Note the difference: `Handler<"search">` (no dot) resolves against the global
|
|
46
|
+
`GeneratedRouteMap`; `Handler<".search", routes>` resolves against the local
|
|
47
|
+
`routes` map. Mixing them — `Handler<"search", routes>` — silently ignores
|
|
48
|
+
`routes` for param/search inference and only uses it for local `ctx.reverse(".x")`.
|
|
49
|
+
|
|
50
|
+
Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
|
|
51
|
+
Values are automatically coerced from query string (e.g., `"2"` becomes `2` for numbers).
|
|
52
|
+
Routes without a `search` schema keep the standard `URLSearchParams` behavior.
|
|
53
|
+
|
|
54
|
+
### RouteSearchParams and RouteParams utility types
|
|
55
|
+
|
|
56
|
+
Extract typed params by route name for use in component props, return types, or anywhere:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
60
|
+
|
|
61
|
+
// RouteSearchParams<"name"> resolves the search schema to a typed object
|
|
62
|
+
type SP = RouteSearchParams<"search">;
|
|
63
|
+
// { q: string | undefined; page?: number; sort?: string }
|
|
64
|
+
|
|
65
|
+
// RouteParams<"name"> resolves URL params from the route pattern
|
|
66
|
+
type P = RouteParams<"blogPost">;
|
|
67
|
+
// { slug: string }
|
|
68
|
+
|
|
69
|
+
// Optional URL params (`:slug?`) resolve to `string | undefined`
|
|
70
|
+
// because absent segments are omitted from `ctx.params` at runtime.
|
|
71
|
+
type C = RouteParams<"checkout">;
|
|
72
|
+
// { step?: string }
|
|
73
|
+
// → ctx.params.step is `string | undefined`; use `?? "default"` to coalesce.
|
|
74
|
+
|
|
75
|
+
// Use in component props
|
|
76
|
+
interface SearchResultsProps {
|
|
77
|
+
params: RouteSearchParams<"search">;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both default to the global route map (`RegisteredRoutes` or `GeneratedRouteMap`).
|
|
82
|
+
Pass an explicit route map as the second type argument when needed:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import type { routes } from "./urls.gen.js";
|
|
86
|
+
|
|
87
|
+
type SP = RouteSearchParams<"search", routes>;
|
|
88
|
+
type P = RouteParams<"blogPost", routes>;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Generated route types
|
|
92
|
+
|
|
93
|
+
In the generated `router.named-routes.gen.ts`, routes with search schemas
|
|
94
|
+
use `{ path, search }` objects:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// router.named-routes.gen.ts (auto-generated)
|
|
98
|
+
export const NamedRoutes = {
|
|
99
|
+
"search.index": {
|
|
100
|
+
path: "/search",
|
|
101
|
+
search: { q: "string", page: "number?", sort: "string?" },
|
|
102
|
+
},
|
|
103
|
+
"home.index": "/", // No search schema -> plain string
|
|
104
|
+
} as const;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You never open a `.gen.ts` by hand. Treat the generated types as call-site
|
|
108
|
+
honesty checks, not modules to read:
|
|
109
|
+
|
|
110
|
+
- **Do not import `router.named-routes.gen.ts` directly**, and don't reach for
|
|
111
|
+
`Rango.GeneratedRouteMap`. It is the whole-app manifest, auto-wired
|
|
112
|
+
globally — `Handler<"name">` and `ctx.reverse("name")` already see it.
|
|
113
|
+
- **Per-module `*.gen.ts` imports are fine** — they are the opt-in local-route
|
|
114
|
+
pattern for `useReverse(routes)` and explicit local handler typing
|
|
115
|
+
(`Handler<".name", routes>`). See `/links`.
|
|
116
|
+
|
|
117
|
+
If a type error points at a generated map instead of your call site, that's a
|
|
118
|
+
smell — fix the call site (or regenerate), never edit the generated file.
|
|
119
|
+
|
|
120
|
+
## Loader Type Safety
|
|
121
|
+
|
|
122
|
+
Loaders have typed return values:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// loaders/product.ts
|
|
126
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
127
|
+
return {
|
|
128
|
+
id: ctx.params.slug,
|
|
129
|
+
name: "Widget",
|
|
130
|
+
price: 99,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// In server component - type is inferred
|
|
135
|
+
import { useLoader } from "@rangojs/router/client";
|
|
136
|
+
|
|
137
|
+
async function ProductPage() {
|
|
138
|
+
const product = await useLoader(ProductLoader);
|
|
139
|
+
// product: { id: string; name: string; price: number }
|
|
140
|
+
return <h1>{product.name}</h1>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// In client component - same type
|
|
144
|
+
"use client";
|
|
145
|
+
import { useLoader } from "@rangojs/router/client";
|
|
146
|
+
|
|
147
|
+
function ProductPrice() {
|
|
148
|
+
const { data } = useLoader(ProductLoader);
|
|
149
|
+
// data: { id: string; name: string; price: number }
|
|
150
|
+
const product = data;
|
|
151
|
+
return <span>${product.price}</span>;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Route Types
|
|
2
|
+
|
|
3
|
+
## Route Definition with Type-Safe Names
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// urls.tsx
|
|
7
|
+
import { urls } from "@rangojs/router";
|
|
8
|
+
|
|
9
|
+
export const urlpatterns = urls(({ path, layout }) => [
|
|
10
|
+
path("/", HomePage, { name: "home" }),
|
|
11
|
+
path("/products", ProductsPage, { name: "products" }),
|
|
12
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
13
|
+
path("/cart", CartPage, { name: "cart" }),
|
|
14
|
+
path("/checkout/:step?", CheckoutPage, { name: "checkout" }),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// Route names are inferred from the { name } option
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Type-Safe href()
|
|
21
|
+
|
|
22
|
+
### Server: ctx.reverse with route names
|
|
23
|
+
|
|
24
|
+
In route handlers, `ctx.reverse()` uses two namespaces:
|
|
25
|
+
|
|
26
|
+
- **`.name`** — local route, resolved within the current `include()` scope
|
|
27
|
+
- **`name`** — global route, from the named-routes definition
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import type { Handler } from "@rangojs/router";
|
|
31
|
+
|
|
32
|
+
export const ProductHandler: Handler<"shop.product"> = (ctx) => {
|
|
33
|
+
ctx.reverse(".cart"); // Local: /shop/cart
|
|
34
|
+
ctx.reverse(".product", { slug: "widget" }); // Local: /shop/product/widget
|
|
35
|
+
ctx.reverse("blog.post", { slug: "1" }); // Global: /blog/1
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For type-safe local names, generate a route types file with `npx rango generate urls/shop.tsx`
|
|
40
|
+
and pass it as the second generic to `Handler` or `Prerender`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import type { Handler } from "@rangojs/router";
|
|
44
|
+
import type { routes } from "./shop.gen.js";
|
|
45
|
+
|
|
46
|
+
export const ProductHandler: Handler<"shop.product", routes> = (ctx) => {
|
|
47
|
+
ctx.reverse(".cart"); // Type-safe local name
|
|
48
|
+
ctx.reverse(".product", { slug: "widget" }); // Type-safe local with params
|
|
49
|
+
ctx.reverse("blog.post", { slug: "hi" }); // Type-safe global name
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Client: href + useHref
|
|
54
|
+
|
|
55
|
+
On the client, `href()` validates paths against registered route patterns at compile time:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
"use client";
|
|
59
|
+
import { href, useHref, Link } from "@rangojs/router/client";
|
|
60
|
+
|
|
61
|
+
// href() validates absolute paths via PatternToPath types
|
|
62
|
+
href("/about"); // Valid path
|
|
63
|
+
href("/blog/hello"); // Matches /blog/:slug
|
|
64
|
+
|
|
65
|
+
// useHref() auto-prefixes with include() mount
|
|
66
|
+
function ShopNav() {
|
|
67
|
+
const href = useHref();
|
|
68
|
+
return <Link to={href("/cart")}>Cart</Link>; // "/shop/cart"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`href()` and the `Rango.Path` type read from `RegisteredRoutes` when you augment
|
|
73
|
+
it, otherwise from the auto-generated `GeneratedRouteMap` — so `rango generate`
|
|
74
|
+
alone type-checks `href()` paths with no manual augmentation. The augmentation
|
|
75
|
+
below is only needed for **`Rango.PathResponse`** (response-payload inference), which
|
|
76
|
+
`GeneratedRouteMap` cannot provide:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
declare global {
|
|
80
|
+
namespace Rango {
|
|
81
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For wrapper helpers, type the path parameter as `Rango.Path`. It is ambient (no
|
|
87
|
+
import) and shares `href()`'s compile-time path checking, so a wrapper stays in
|
|
88
|
+
sync with your routes automatically:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { href } from "@rangojs/router/client";
|
|
92
|
+
|
|
93
|
+
export const appHref = (path: Rango.Path): string => href(path);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For response-route payloads, `Rango.PathResponse<T>` is the ambient lookup. It
|
|
97
|
+
accepts a route _pattern_ **or** a concrete path, so it also serves as the return
|
|
98
|
+
type of a typed `fetch` wrapper. It only resolves once `RegisteredRoutes` carries
|
|
99
|
+
response metadata:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { href } from "@rangojs/router/client";
|
|
103
|
+
|
|
104
|
+
type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
|
|
105
|
+
type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
|
|
106
|
+
|
|
107
|
+
// Response inferred from the concrete path passed in:
|
|
108
|
+
async function get<T extends Rango.Path>(
|
|
109
|
+
path: T,
|
|
110
|
+
): Promise<Rango.PathResponse<T>> {
|
|
111
|
+
return fetch(href(path)).then((r) => r.json());
|
|
112
|
+
}
|
|
113
|
+
const product = await get("/api/products/42"); // Product (bare value)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
117
|
+
route can match several patterns and union their responses.
|
|
118
|
+
|
|
119
|
+
`Rango.PathResponse` describes the JSON **wire** shape, not the handler's raw
|
|
120
|
+
return. A `path.json()` handler returning `{ createdAt: Date }` resolves here to
|
|
121
|
+
`{ createdAt: string }` (bare value), matching what `r.json()` yields. This
|
|
122
|
+
is applied via the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
123
|
+
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). A separate
|
|
124
|
+
`Rango.FlightSerialize<T>` models the higher-fidelity RSC Flight boundary
|
|
125
|
+
(loaders / RSC props, where `Date` is preserved) — do **not** use it for
|
|
126
|
+
`path.json()`.
|
|
127
|
+
|
|
128
|
+
### Overriding serialization globally
|
|
129
|
+
|
|
130
|
+
For your own types, the zero-config way to control the JSON wire shape is a
|
|
131
|
+
`toJSON()` method — `Rango.JsonSerialize` honors it, and it matches the runtime
|
|
132
|
+
exactly (`JSON.stringify` calls `toJSON()`):
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
class Money {
|
|
136
|
+
constructor(private cents: number) {}
|
|
137
|
+
toJSON(): number {
|
|
138
|
+
return this.cents;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Rango.JsonSerialize<Money> is number; Rango.PathResponse reflects it.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
To override a transform for types you **don't** own (or for the Flight boundary,
|
|
145
|
+
which has no `toJSON()`), augment its override slot. Because `Rango.JsonSerialize`
|
|
146
|
+
/ `Rango.FlightSerialize` are type _aliases_ (TS can't merge those), you provide a
|
|
147
|
+
single member that is your **complete** transform, delegating to the built-in for
|
|
148
|
+
the cases you don't change:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
declare global {
|
|
152
|
+
namespace Rango {
|
|
153
|
+
interface JsonSerializeOverride<T> {
|
|
154
|
+
app: T extends Decimal ? string : Rango.JsonSerializeBuiltin<T>;
|
|
155
|
+
}
|
|
156
|
+
interface FlightSerializeOverride<T> {
|
|
157
|
+
app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Rango.JsonSerialize<Decimal> -> string; Rango.FlightSerialize<Money> -> number;
|
|
162
|
+
// everything else stays on the built-in, recursively (nested fields too).
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Rules: provide **exactly one** member (the slot is read as
|
|
166
|
+
`Override<T>[keyof Override<T>]`, so multiple members union and conflict).
|
|
167
|
+
Overrides win over `toJSON()` and apply at every nesting level. Caveat for JSON:
|
|
168
|
+
the `path.json()` runtime is plain `JSON.stringify`, which only honors `toJSON()`,
|
|
169
|
+
so a `JsonSerializeOverride` that disagrees with what the runtime emits will lie —
|
|
170
|
+
prefer `toJSON()` for your own types and use the slot only for types you can't
|
|
171
|
+
modify.
|
|
172
|
+
|
|
173
|
+
See `/links` for full URL generation guide.
|
|
174
|
+
|
|
175
|
+
## Stable identity: `path#export`
|
|
176
|
+
|
|
177
|
+
Loaders, handles, cached functions (`functionId`), and server actions
|
|
178
|
+
(`actionId`) all share one identity scheme: `{modulePath}#{exportName}`,
|
|
179
|
+
injected at build by the `exposeInternalIds` and `exposeActionId` Vite plugins.
|
|
180
|
+
This is also the identity React server actions carry across the Flight boundary,
|
|
181
|
+
which is why a `revalidate()` predicate sees an action as a `path#export` string:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
revalidate(
|
|
185
|
+
({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined,
|
|
186
|
+
);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`actionId` is the only stable reference React exposes across the Flight boundary,
|
|
190
|
+
so it stays as the floor and escape hatch. The hand-written-string surface
|
|
191
|
+
(`actionId?.includes("cart.ts#")`) is brittle: a renamed action or moved file
|
|
192
|
+
silently stops matching with no compile error. Prefer **`ctx.isAction()`** in a
|
|
193
|
+
revalidate predicate — it resolves the action's id from an imported reference, so
|
|
194
|
+
a rename is a type error in one place instead of silent drift:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { addToCart, removeFromCart } from "./actions/cart";
|
|
198
|
+
import * as CartActions from "./actions/cart";
|
|
199
|
+
|
|
200
|
+
revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
|
|
201
|
+
revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
|
|
202
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`ctx.isAction()` (only available on the revalidate predicate's context) returns a
|
|
206
|
+
raw boolean — combine with `|| undefined` for the "revalidate on match, else
|
|
207
|
+
defer" intent. It resolves the reference the same way the router derives
|
|
208
|
+
`actionId` (`$id` in production, `$$id` in dev), so matching
|
|
209
|
+
works in both modes. `actionId` stays available for advanced cases.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-cache
|
|
3
|
-
description: Function-level caching with "use cache" directive for RSC data functions and components
|
|
3
|
+
description: Function-level caching with the "use cache" directive for RSC data functions and components in @rangojs/router. Use when a single function or component should memoize its own output, not a whole route/segment subtree or HTTP response.
|
|
4
4
|
argument-hint: [profile-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -10,6 +10,12 @@ Function-level caching for async server functions and RSC components. Caches
|
|
|
10
10
|
return values with TTL + stale-while-revalidate. Complementary to the route-level
|
|
11
11
|
`cache()` DSL and build-time `Static()`/`Prerender()`.
|
|
12
12
|
|
|
13
|
+
## Not this skill if…
|
|
14
|
+
|
|
15
|
+
- You want to cache a whole route or a rendered subtree of segments — that is
|
|
16
|
+
the segment-level `cache()` DSL: see `/caching`.
|
|
17
|
+
- You are unsure which cache layer you need — start at `/cache-guide`.
|
|
18
|
+
|
|
13
19
|
## Basic Usage
|
|
14
20
|
|
|
15
21
|
### File-level (all exports cached with default profile)
|
|
@@ -63,12 +69,23 @@ createRouter({
|
|
|
63
69
|
short: { ttl: 60, swr: 120 },
|
|
64
70
|
long: { ttl: 3600, swr: 7200 },
|
|
65
71
|
products: { ttl: 300, swr: 600, tags: ["products"] },
|
|
72
|
+
// Opt-in: a stale entry re-executes in the foreground during a server
|
|
73
|
+
// action's revalidation render (fresh action response), instead of SWR.
|
|
74
|
+
cms: { ttl: 300, swr: 600, foregroundOnAction: true },
|
|
66
75
|
},
|
|
67
76
|
});
|
|
68
77
|
```
|
|
69
78
|
|
|
70
79
|
- `"use cache"` (no name) resolves to `default`.
|
|
71
80
|
- `"use cache: short"` resolves to the `short` profile.
|
|
81
|
+
- `foregroundOnAction: true` (default false): a stale entry serves stale +
|
|
82
|
+
revalidates in the background on a plain navigation (SWR), but re-executes in
|
|
83
|
+
the FOREGROUND during a server action's revalidation render so the action
|
|
84
|
+
response reflects a fresh value (only the store write is deferred). Use it for
|
|
85
|
+
mutation-related cached data; incidental TTL staleness on an ordinary action
|
|
86
|
+
stays SWR so the action is not turned into a synchronous cache-refresh barrier.
|
|
87
|
+
For strong read-your-own-writes after a mutation, prefer `updateTag()` (a hard
|
|
88
|
+
purge, so the action's own re-render is a fresh foreground miss).
|
|
72
89
|
- Unknown profile names throw at runtime, on the first invocation of the cached
|
|
73
90
|
function (the Vite transform does not validate names at build/boot). The error
|
|
74
91
|
is actionable -- it names the missing profile and shows the `createRouter({
|
|
@@ -136,6 +153,13 @@ const locale = cookies().get("locale")?.value ?? "en";
|
|
|
136
153
|
const data = await getCachedData(locale); // locale is now in the cache key
|
|
137
154
|
```
|
|
138
155
|
|
|
156
|
+
The guard does not reach into LOADER bodies consumed inside the cached
|
|
157
|
+
function (`await ctx.use(loader)`): loaders always run fresh, so their reads
|
|
158
|
+
are exempt — but the CONSUMED VALUE is captured into the shared cache entry
|
|
159
|
+
like any other computed data. Same rule across `cache()` and the PPR shell:
|
|
160
|
+
handler/cached-scope consumption = baked copy, client-side `useLoader` = live
|
|
161
|
+
(the consumption-lane rule, `/rango` → Invariants).
|
|
162
|
+
|
|
139
163
|
### Side-Effect Guards
|
|
140
164
|
|
|
141
165
|
These ctx methods **throw** inside a `"use cache"` function because their effects
|
|
@@ -329,8 +353,11 @@ export async function getProducts() {
|
|
|
329
353
|
Writes to the same `SegmentCacheStore` as `cache()` DSL, `Static()`, and `Prerender()`.
|
|
330
354
|
One store, one configuration.
|
|
331
355
|
|
|
332
|
-
Cache entries (and `cacheProfiles`) can be tagged via `cache({ tags })` or
|
|
333
|
-
a `"use cache"` function
|
|
356
|
+
Cache entries (and `cacheProfiles`) can be tagged via `cache({ tags })` or runtime
|
|
357
|
+
`cacheTag(...tags)`. `cacheTag` has two forms: inside a `"use cache"` function it
|
|
358
|
+
tags that entry; called during a request render outside `"use cache"` it tags the
|
|
359
|
+
request's document/shell artifact (rides `_requestTags`) instead of throwing. The
|
|
360
|
+
built-in
|
|
334
361
|
`MemorySegmentCacheStore` and `CFCacheStore` index by tag. Invalidate on demand
|
|
335
362
|
with `updateTag(...tags)` (awaitable, read-your-own-writes; for server actions) or
|
|
336
363
|
`revalidateTag(...tags)` (background, non-blocking; for route handlers/webhooks).
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vercel
|
|
3
|
-
description: Deploy a Rango app to Vercel Functions (Build Output API v3)
|
|
3
|
+
description: Deploy a Rango app to Vercel Functions (Build Output API v3). Use when deploying a Rango app to Vercel, or asking how the router works with Vercel Functions and the Build Output API.
|
|
4
4
|
argument-hint:
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: view-transitions
|
|
3
|
-
description: Configure React View Transitions on layouts, routes, and parallel slots in @rangojs/router
|
|
3
|
+
description: Configure React View Transitions on layouts, routes, and parallel slots in @rangojs/router. Use when navigation should animate smoothly between pages, or wiring up React View Transitions on a route or layout.
|
|
4
4
|
argument-hint: [layout|route|parallel|intercept]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -209,6 +209,10 @@ interface TransitionConfig {
|
|
|
209
209
|
default?: string | Record<string, string>; // fallback for any phase
|
|
210
210
|
name?: string; // explicit view-transition-name
|
|
211
211
|
viewTransition?: "auto" | false; // boundary opt-out (see below)
|
|
212
|
+
// Conditional gate, evaluated server-side AFTER the route handler. Return
|
|
213
|
+
// false to drop this transition for the request, so the navigation streams its
|
|
214
|
+
// loading() fallback instead of holding. See the gate section below.
|
|
215
|
+
when?: (ctx: TransitionWhenContext) => boolean;
|
|
212
216
|
}
|
|
213
217
|
```
|
|
214
218
|
|
|
@@ -217,6 +221,45 @@ interface TransitionConfig {
|
|
|
217
221
|
- `name` lets you participate in cross-page morphs by name (advanced; you usually don't need this on a layout/route-level wrap).
|
|
218
222
|
- `viewTransition` toggles whether rango places its own `<ViewTransition>` boundary. `"auto"` (default) wraps as described above; `false` opts out — see the next section.
|
|
219
223
|
|
|
224
|
+
## Conditional transitions (`when`)
|
|
225
|
+
|
|
226
|
+
`transition({ when })` gates the hold per request. The predicate runs **server-side, AFTER the route handler** and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
|
|
227
|
+
|
|
228
|
+
Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for post-handler reads:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import type { TransitionWhenContext } from "@rangojs/router";
|
|
232
|
+
|
|
233
|
+
// Hold only when the handler marked this request (handler sets, gate reads):
|
|
234
|
+
transition({ when: (ctx) => ctx.get(KeepScroll) === true });
|
|
235
|
+
|
|
236
|
+
// Hold only when arriving from a specific page (the navigation SOURCE):
|
|
237
|
+
transition({
|
|
238
|
+
when: ({ currentUrl }) => currentUrl?.pathname.startsWith("/list") === true,
|
|
239
|
+
});
|
|
240
|
+
transition({ when: ({ fromRouteName }) => fromRouteName === "products.list" });
|
|
241
|
+
|
|
242
|
+
// Hold only after a specific action revalidated the route:
|
|
243
|
+
transition({
|
|
244
|
+
when: ({ actionId }) => actionId === "src/actions/cart.ts#addToCart",
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| field | meaning | populated |
|
|
249
|
+
| ------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
250
|
+
| `currentUrl` / `currentParams` / `fromRouteName` | navigation **source** | soft nav + action-success; `undefined` on initial full load and action/PE error paths |
|
|
251
|
+
| `nextUrl` / `nextParams` | navigation **target** | always |
|
|
252
|
+
| `toRouteName` (and `fromRouteName`) | route **name** | when the route is named (undefined for unnamed/auto-generated) |
|
|
253
|
+
| `actionId` / `actionUrl` / `actionResult` / `formData` | the server action that triggered this render | action-triggered renders only |
|
|
254
|
+
| `method` | `"GET"` (nav) / `"POST"` (action) | always |
|
|
255
|
+
| `get` / `env` | read handler/middleware vars + app env | always |
|
|
256
|
+
|
|
257
|
+
A predicate that throws is reported to `router.onError` (phase `"rendering"`) and treated as no-hold (conservative).
|
|
258
|
+
|
|
259
|
+
**Same-route content-holds need the transition present on the FIRST render.** The same-route hold works by giving the route a param-agnostic key so a param change reconciles instead of remounting — but that key is established when the route first mounts. A source gate that returns `false` on the initial full load (where `currentUrl`/`currentParams`/`fromRouteName` are undefined) drops the transition before the route mounts, so the route mounts _outside_ a transition scope and **every** later same-route param nav remounts (flashing the skeleton) regardless of what the gate decides on those navs. Write source gates so they hold when there is no source — e.g. `({ currentParams }) => currentParams?.tab !== "raw"` (true on the initial load) rather than `=== "details"` (false on the initial load) — when the same-route content-hold must engage. This only affects same-route param navigations; action-only or cross-route gating is unaffected (no shared param key is in play).
|
|
260
|
+
|
|
261
|
+
**Prefetch / cache caveat.** The gate runs during resolution, so a **prefetched** navigation decides at prefetch time — `currentUrl`/`currentParams`/`fromRouteName` reflect the page the prefetch fired from, not necessarily the click-time source — and a `cache()`/prerender hit replays the stored transition without re-running the predicate. A source-sensitive gate can therefore be frozen to prefetch/store-time state. This covers ~99% of navigations; if yours must reflect the exact click-time source, source-scope the prefetch (`<Link prefetchKey=":source">`) and don't `cache()` that segment.
|
|
262
|
+
|
|
220
263
|
## Opting out of the router boundary (place your own `<ViewTransition>`)
|
|
221
264
|
|
|
222
265
|
By default a `transition()` segment gets a rango-placed `<ViewTransition>` boundary — a cross-fade of the whole outlet/route. If you'd rather animate specific elements yourself (place `<ViewTransition name="...">` in your components), set `viewTransition: false`. The router then contributes **no boundary of its own** but still:
|
|
@@ -249,6 +249,14 @@ export interface EventController {
|
|
|
249
249
|
resolvedIds?: string[],
|
|
250
250
|
): void;
|
|
251
251
|
getHandleState(): HandleState;
|
|
252
|
+
/**
|
|
253
|
+
* Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
|
|
254
|
+
* leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
|
|
255
|
+
* Used while a deferred handle is resolving: the route has changed (so
|
|
256
|
+
* `useSegments` must reflect the new segment ids) but `useHandle` still holds
|
|
257
|
+
* its previous value until the deferred snapshot is applied.
|
|
258
|
+
*/
|
|
259
|
+
setRouteSegmentIds(matched: string[]): void;
|
|
252
260
|
|
|
253
261
|
// Params operations
|
|
254
262
|
setParams(params: Record<string, string>): void;
|
|
@@ -269,6 +277,13 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
269
277
|
result: null,
|
|
270
278
|
};
|
|
271
279
|
|
|
280
|
+
// Shared empty inflight-actions list. getState() hands back this exact reference
|
|
281
|
+
// whenever no action is inflight (the overwhelmingly common case), so the derived
|
|
282
|
+
// snapshot's `inflightActions` is referentially stable across notifies instead of
|
|
283
|
+
// a fresh [] each call. Read-only by contract (consumers only read length/spread),
|
|
284
|
+
// same as the shared DEFAULT_ACTION_STATE.
|
|
285
|
+
const EMPTY_INFLIGHT_ACTIONS: InflightAction[] = [];
|
|
286
|
+
|
|
272
287
|
/**
|
|
273
288
|
* Check if a subscription ID matches an action's full ID.
|
|
274
289
|
*
|
|
@@ -363,7 +378,19 @@ export function createEventController(
|
|
|
363
378
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
364
379
|
const handleListeners = new Set<HandleListener>();
|
|
365
380
|
|
|
366
|
-
const
|
|
381
|
+
const notifyStateListeners = makeDebouncedNotifier(stateListeners);
|
|
382
|
+
|
|
383
|
+
// Memoized derived snapshot. Every state mutation already funnels through
|
|
384
|
+
// notify(), so invalidating here (synchronously, before the debounced fire)
|
|
385
|
+
// means a getState() call between two mutations reuses the same object — an
|
|
386
|
+
// unchanged state returns the SAME reference — while any real change recomputes
|
|
387
|
+
// on the next read. Kept null when dirty.
|
|
388
|
+
let cachedDerivedState: DerivedNavigationState | null = null;
|
|
389
|
+
|
|
390
|
+
function notify(): void {
|
|
391
|
+
cachedDerivedState = null;
|
|
392
|
+
notifyStateListeners();
|
|
393
|
+
}
|
|
367
394
|
|
|
368
395
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
369
396
|
|
|
@@ -389,14 +416,21 @@ export function createEventController(
|
|
|
389
416
|
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
390
417
|
|
|
391
418
|
function getState(): DerivedNavigationState {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
419
|
+
if (cachedDerivedState) return cachedDerivedState;
|
|
420
|
+
|
|
421
|
+
// Skip the spread/filter/map entirely when idle — the common case — and hand
|
|
422
|
+
// back the shared frozen empty list for referential stability.
|
|
423
|
+
const inflightActionsList: InflightAction[] =
|
|
424
|
+
inflightActions.size === 0
|
|
425
|
+
? EMPTY_INFLIGHT_ACTIONS
|
|
426
|
+
: [...inflightActions.values()]
|
|
427
|
+
.filter((a) => a.phase !== "settling")
|
|
428
|
+
.map((a) => ({
|
|
429
|
+
id: a.id,
|
|
430
|
+
actionId: a.actionId,
|
|
431
|
+
payload: a.payload,
|
|
432
|
+
startedAt: a.startedAt,
|
|
433
|
+
}));
|
|
400
434
|
|
|
401
435
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
402
436
|
const isVisibleNavigation =
|
|
@@ -406,7 +440,7 @@ export function createEventController(
|
|
|
406
440
|
|
|
407
441
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
408
442
|
|
|
409
|
-
|
|
443
|
+
cachedDerivedState = {
|
|
410
444
|
state,
|
|
411
445
|
isStreaming,
|
|
412
446
|
// True when a navigation is active (fetching or streaming, before
|
|
@@ -422,9 +456,14 @@ export function createEventController(
|
|
|
422
456
|
: null,
|
|
423
457
|
inflightActions: inflightActionsList,
|
|
424
458
|
};
|
|
459
|
+
return cachedDerivedState;
|
|
425
460
|
}
|
|
426
461
|
|
|
427
462
|
function getActionState(actionId: string): TrackedActionState {
|
|
463
|
+
// Nothing inflight — skip building/scanning the list and return the shared
|
|
464
|
+
// idle snapshot (the same reference use-action falls back to).
|
|
465
|
+
if (inflightActions.size === 0) return DEFAULT_ACTION_STATE;
|
|
466
|
+
|
|
428
467
|
const entry = [...inflightActions.values()]
|
|
429
468
|
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
430
469
|
.reduce<ActionEntry | undefined>((best, a) => {
|
|
@@ -860,6 +899,18 @@ export function createEventController(
|
|
|
860
899
|
};
|
|
861
900
|
}
|
|
862
901
|
|
|
902
|
+
function setRouteSegmentIds(matched: string[]): void {
|
|
903
|
+
const next = filterRouteSegmentIds(matched);
|
|
904
|
+
if (
|
|
905
|
+
next.length === routeSegmentIds.length &&
|
|
906
|
+
next.every((id, i) => id === routeSegmentIds[i])
|
|
907
|
+
) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
routeSegmentIds = next;
|
|
911
|
+
notifyHandles();
|
|
912
|
+
}
|
|
913
|
+
|
|
863
914
|
// ========================================================================
|
|
864
915
|
// Subscriptions
|
|
865
916
|
// ========================================================================
|
|
@@ -928,6 +979,7 @@ export function createEventController(
|
|
|
928
979
|
// Handles
|
|
929
980
|
setHandleData,
|
|
930
981
|
getHandleState,
|
|
982
|
+
setRouteSegmentIds,
|
|
931
983
|
|
|
932
984
|
// Params
|
|
933
985
|
setParams,
|
package/src/browser/logging.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Module-level debug flag for guarding call sites whose debug arguments do
|
|
5
|
+
* non-trivial work (joins, maps, spreads, object literals). Because the Vite
|
|
6
|
+
* transform folds INTERNAL_RANGO_DEBUG to a literal, an `if (IS_BROWSER_DEBUG)`
|
|
7
|
+
* guard is minifier-DCE-able — the whole block drops from the production bundle.
|
|
8
|
+
* Prefer this over isBrowserDebugEnabled() at hot call sites: a const guard folds
|
|
9
|
+
* where a function call may not.
|
|
10
|
+
*/
|
|
11
|
+
export const IS_BROWSER_DEBUG: boolean = INTERNAL_RANGO_DEBUG;
|
|
12
|
+
|
|
3
13
|
interface BrowserLogContext {
|
|
4
14
|
requestId: string;
|
|
5
15
|
txId: string;
|
|
@@ -53,3 +63,21 @@ export function debugLog(msg: string, ...args: unknown[]): void {
|
|
|
53
63
|
console.log(msg, ...args);
|
|
54
64
|
}
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Boot-sequence debug log: one line per initial-document step (flight decode,
|
|
69
|
+
* handle stream, bridge wiring, initial tree build, hydration commit), each
|
|
70
|
+
* stamped with performance.now() so the gap BEFORE hydrateRoot is visible.
|
|
71
|
+
* The initial document path was otherwise silent — FE debug only started
|
|
72
|
+
* talking at the first soft navigation, so a boot stall (e.g. an await that
|
|
73
|
+
* holds initBrowserApp, and with it hydrateRoot) was invisible.
|
|
74
|
+
*/
|
|
75
|
+
export function bootLog(step: string, details?: Record<string, unknown>): void {
|
|
76
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
77
|
+
const prefix = `[Browser][boot] ${step} @ ${Math.round(performance.now())}ms`;
|
|
78
|
+
if (details) {
|
|
79
|
+
console.log(prefix, details);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(prefix);
|
|
83
|
+
}
|