@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.8a4d0430
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 +5 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4474 -863
- package/package.json +60 -51
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +50 -21
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +89 -30
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +318 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +285 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +258 -308
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +185 -73
- package/src/browser/react/NavigationProvider.tsx +51 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +32 -79
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +107 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +109 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +106 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +15 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +153 -19
- package/src/index.ts +211 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +211 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +59 -8
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +374 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +148 -35
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +692 -4257
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +764 -754
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +25 -13
- package/src/server/context.ts +182 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +430 -70
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +100 -31
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -1129
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/skills/route/SKILL.md
CHANGED
|
@@ -74,8 +74,45 @@ path("/product/:slug", async (ctx) => {
|
|
|
74
74
|
|
|
75
75
|
```typescript
|
|
76
76
|
path("/product/:slug", ProductPage, {
|
|
77
|
-
name: "product",
|
|
78
|
-
})
|
|
77
|
+
name: "product", // Route name for href() and navigation
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Typed Search Params
|
|
82
|
+
|
|
83
|
+
Add a `search` schema to get typed `ctx.search`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
path("/search", SearchPage, {
|
|
87
|
+
name: "search",
|
|
88
|
+
search: { q: "string", page: "number?", sort: "string?" },
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use `Handler<"name">` for typed search params (resolves from the generated route map automatically):
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import type { Handler } from "@rangojs/router";
|
|
96
|
+
|
|
97
|
+
export const SearchPage: Handler<"search"> = (ctx) => {
|
|
98
|
+
// ctx.search is typed: { q: string; page?: number; sort?: string }
|
|
99
|
+
const { q, page, sort } = ctx.search;
|
|
100
|
+
// ctx.searchParams is always URLSearchParams
|
|
101
|
+
return <SearchResults q={q} page={page} sort={sort} />;
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
|
|
106
|
+
Missing params are `undefined` regardless of required/optional. The required/optional
|
|
107
|
+
distinction is a consumer-facing contract (for `href()` and `reverse()` autocomplete).
|
|
108
|
+
|
|
109
|
+
Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
113
|
+
|
|
114
|
+
type SP = RouteSearchParams<"search">; // { q: string; page?: number; sort?: string }
|
|
115
|
+
type P = RouteParams<"blogPost">; // { slug: string }
|
|
79
116
|
```
|
|
80
117
|
|
|
81
118
|
## Route Children
|
|
@@ -90,17 +127,193 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
90
127
|
])
|
|
91
128
|
```
|
|
92
129
|
|
|
130
|
+
## Handler Data Ownership
|
|
131
|
+
|
|
132
|
+
When a route has children (orphan layouts, parallels), the handler executes
|
|
133
|
+
first. Use `ctx.set(key, value)` to share data with children, who read it
|
|
134
|
+
via `ctx.get(key)`. Caching wraps all segments together, so either all run
|
|
135
|
+
or none do.
|
|
136
|
+
|
|
137
|
+
### Typed context variables with createVar
|
|
138
|
+
|
|
139
|
+
Use `createVar<T>()` to create a typed token for `ctx.set()`/`ctx.get()`.
|
|
140
|
+
The token is imported by both the handler (producer) and layout (consumer),
|
|
141
|
+
making the data contract explicit and compile-time verified:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { createVar } from "@rangojs/router";
|
|
145
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
146
|
+
|
|
147
|
+
// Typed token -- shared between handler and layout
|
|
148
|
+
interface DashboardData {
|
|
149
|
+
title: string;
|
|
150
|
+
stats: { views: number };
|
|
151
|
+
}
|
|
152
|
+
const Dashboard = createVar<DashboardData>();
|
|
153
|
+
|
|
154
|
+
path("/dashboard/:id", async (ctx) => {
|
|
155
|
+
const data = await fetchDashboard(ctx.params.id);
|
|
156
|
+
ctx.set(Dashboard, data); // type-checked
|
|
157
|
+
return <DashboardPage data={data} />;
|
|
158
|
+
}, { name: "dashboard" }, () => [
|
|
159
|
+
layout((ctx) => {
|
|
160
|
+
const data = ctx.get(Dashboard); // typed as DashboardData | undefined
|
|
161
|
+
return (
|
|
162
|
+
<div>
|
|
163
|
+
<h1>{data?.title}</h1>
|
|
164
|
+
<Outlet />
|
|
165
|
+
<ParallelOutlet name="@sidebar" />
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}),
|
|
169
|
+
parallel({
|
|
170
|
+
"@sidebar": (ctx) => {
|
|
171
|
+
const data = ctx.get(Dashboard);
|
|
172
|
+
return <Sidebar stats={data?.stats} />;
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
String keys still work (`ctx.set("key", value)` / `ctx.get("key")`), but
|
|
179
|
+
`createVar<T>()` is preferred for type safety.
|
|
180
|
+
|
|
181
|
+
Only route handlers and middleware can call `ctx.set()`. Layouts, parallels,
|
|
182
|
+
and intercepts can only read via `ctx.get()`.
|
|
183
|
+
|
|
184
|
+
### Revalidation Contracts for Handler Data
|
|
185
|
+
|
|
186
|
+
Handler-first guarantees apply within a single full render pass. For partial
|
|
187
|
+
action revalidation, define named revalidation contracts and reuse them on both
|
|
188
|
+
the producer route and the consumer child segments.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// revalidation-contracts.ts
|
|
192
|
+
export const revalidateCheckoutData = ({ actionId }) =>
|
|
193
|
+
actionId?.includes("src/actions/checkout.ts#") ?? false;
|
|
194
|
+
|
|
195
|
+
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
196
|
+
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
197
|
+
layout(CheckoutLayout, () => [
|
|
198
|
+
revalidate(revalidateCheckoutData), // consumer reruns
|
|
199
|
+
parallel({ "@summary": CheckoutSummary }, () => [
|
|
200
|
+
revalidate(revalidateCheckoutData),
|
|
201
|
+
]),
|
|
202
|
+
]),
|
|
203
|
+
]);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If children depend on multiple upstream domains, compose multiple contracts on
|
|
207
|
+
the same segment (`revalidateAuthData`, `revalidateCheckoutData`, and so on).
|
|
208
|
+
|
|
209
|
+
For cleaner route trees, expose contract helpers and spread them:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { revalidate } from "@rangojs/router";
|
|
213
|
+
|
|
214
|
+
export const revalidateCheckout = () => [revalidate(revalidateCheckoutData)];
|
|
215
|
+
|
|
216
|
+
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
217
|
+
revalidateCheckout(),
|
|
218
|
+
layout(CheckoutLayout, () => [revalidateCheckout()]),
|
|
219
|
+
]);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For scope/revalidation guarantees and non-guarantees, see:
|
|
223
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
224
|
+
|
|
225
|
+
## Redirects
|
|
226
|
+
|
|
227
|
+
### Basic redirect
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { redirect } from "@rangojs/router";
|
|
231
|
+
|
|
232
|
+
path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Redirect with custom status
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Redirect with location state
|
|
242
|
+
|
|
243
|
+
Carry typed state through redirects (e.g. flash messages):
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { redirect, createLocationState } from "@rangojs/router";
|
|
247
|
+
|
|
248
|
+
export const FlashMessage = createLocationState<{ text: string }>({
|
|
249
|
+
flash: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
path(
|
|
253
|
+
"/save",
|
|
254
|
+
(ctx) => {
|
|
255
|
+
// ... save logic
|
|
256
|
+
return redirect("/dashboard", {
|
|
257
|
+
state: [FlashMessage({ text: "Item saved!" })],
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
{ name: "save" },
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// With custom status + state
|
|
264
|
+
path(
|
|
265
|
+
"/action",
|
|
266
|
+
(ctx) => {
|
|
267
|
+
return redirect("/target", {
|
|
268
|
+
status: 303,
|
|
269
|
+
state: [FlashMessage({ text: "Action complete" })],
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
{ name: "action" },
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Read the state on the target page with `useLocationState(FlashMessage)`. The
|
|
277
|
+
`{ flash: true }` option makes it auto-clear. Without `{ flash: true }`,
|
|
278
|
+
state persists on back/forward. See `/hooks` for details.
|
|
279
|
+
|
|
280
|
+
### ctx.setLocationState()
|
|
281
|
+
|
|
282
|
+
Attach location state to any server response (not just redirects):
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
path("/dashboard", (ctx) => {
|
|
286
|
+
ctx.setLocationState(ServerInfo({ data: "welcome" }));
|
|
287
|
+
return <Dashboard />;
|
|
288
|
+
}, { name: "dashboard" })
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
State flows to the browser via the RSC payload and is merged into
|
|
292
|
+
`history.pushState()`. Only works for SPA (partial) navigations.
|
|
293
|
+
|
|
93
294
|
## Handler Context
|
|
94
295
|
|
|
95
296
|
Every handler receives a context object:
|
|
96
297
|
|
|
97
298
|
```typescript
|
|
98
|
-
interface HandlerContext<TParams =
|
|
99
|
-
params: TParams;
|
|
100
|
-
request: Request;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
299
|
+
interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
|
|
300
|
+
params: TParams; // URL parameters
|
|
301
|
+
request: Request; // Original request
|
|
302
|
+
searchParams: URLSearchParams; // Query params (always URLSearchParams)
|
|
303
|
+
search: {} | ResolveSearchSchema<TSearch>; // Typed search params (from search schema)
|
|
304
|
+
url: URL; // Parsed URL
|
|
305
|
+
env: TEnv; // Environment (bindings + variables)
|
|
306
|
+
set(key: string, value: any): void; // Set context variable (untyped string key)
|
|
307
|
+
set<T>(contextVar: ContextVar<T>, value: T): void; // Set typed context variable
|
|
308
|
+
get(key: string): any; // Read context variable (untyped string key)
|
|
309
|
+
get<T>(contextVar: ContextVar<T>): T | undefined; // Read typed context variable
|
|
310
|
+
use<T>(handle: Handle<T>): T; // Access handles
|
|
311
|
+
reverse(
|
|
312
|
+
name: string,
|
|
313
|
+
params?: Record<string, string>,
|
|
314
|
+
search?: Record<string, unknown>,
|
|
315
|
+
): string; // URL generation
|
|
316
|
+
setLocationState(entries: LocationStateEntry[]): void; // Attach state to response
|
|
104
317
|
}
|
|
105
318
|
```
|
|
106
319
|
|
|
@@ -111,11 +324,11 @@ path("/product/:slug", (ctx) => {
|
|
|
111
324
|
// Access URL params
|
|
112
325
|
const { slug } = ctx.params;
|
|
113
326
|
|
|
114
|
-
// Access query params
|
|
115
|
-
const tab = ctx.
|
|
327
|
+
// Access query params (untyped - use search schema for typed access)
|
|
328
|
+
const tab = ctx.searchParams.get("tab");
|
|
116
329
|
|
|
117
|
-
// Access
|
|
118
|
-
const db = ctx.env.
|
|
330
|
+
// Access platform bindings
|
|
331
|
+
const db = ctx.env.DB;
|
|
119
332
|
|
|
120
333
|
// Access handles
|
|
121
334
|
const breadcrumbs = ctx.use(Breadcrumbs);
|
|
@@ -142,8 +355,7 @@ urls(({ path, layout }) => [
|
|
|
142
355
|
## Complete Example
|
|
143
356
|
|
|
144
357
|
```typescript
|
|
145
|
-
import { urls } from "@rangojs/router";
|
|
146
|
-
import { Breadcrumbs } from "./handles/breadcrumbs";
|
|
358
|
+
import { urls, Breadcrumbs } from "@rangojs/router";
|
|
147
359
|
|
|
148
360
|
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
149
361
|
// Simple route
|
|
@@ -50,20 +50,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
|
50
50
|
The `urls()` function provides a callback with all available DSL functions:
|
|
51
51
|
|
|
52
52
|
```typescript
|
|
53
|
-
urls(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
urls(
|
|
54
|
+
({
|
|
55
|
+
path, // Define a route
|
|
56
|
+
layout, // Wrap routes in a layout
|
|
57
|
+
parallel, // Define parallel routes (slots)
|
|
58
|
+
loader, // Add data loader
|
|
59
|
+
loading, // Add loading skeleton
|
|
60
|
+
cache, // Configure caching
|
|
61
|
+
middleware, // Add middleware
|
|
62
|
+
revalidate, // Control revalidation
|
|
63
|
+
intercept, // Intercept routes for modals
|
|
64
|
+
when, // Conditional rendering
|
|
65
|
+
}) => [
|
|
66
|
+
// Route definitions here
|
|
67
|
+
],
|
|
68
|
+
);
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
## Router Options
|
|
@@ -76,7 +78,7 @@ interface RSCRouterOptions<TEnv> {
|
|
|
76
78
|
// Document component wrapping entire app
|
|
77
79
|
document?: ComponentType<{ children: ReactNode }>;
|
|
78
80
|
|
|
79
|
-
// Enable performance
|
|
81
|
+
// Enable per-request performance timeline (console waterfall + Server-Timing header)
|
|
80
82
|
debugPerformance?: boolean;
|
|
81
83
|
|
|
82
84
|
// Default error boundary
|
|
@@ -97,11 +99,25 @@ interface RSCRouterOptions<TEnv> {
|
|
|
97
99
|
// Theme configuration
|
|
98
100
|
theme?: ThemeConfig | true;
|
|
99
101
|
|
|
102
|
+
// SSR options (streaming policy)
|
|
103
|
+
ssr?: SSROptions<TEnv>;
|
|
104
|
+
|
|
105
|
+
// Telemetry sink for structured lifecycle events
|
|
106
|
+
telemetry?: TelemetrySink;
|
|
107
|
+
|
|
100
108
|
// Connection warmup (default: true)
|
|
101
109
|
warmup?: boolean;
|
|
102
110
|
|
|
111
|
+
// Prefetch cache TTL in seconds (default: 300)
|
|
112
|
+
// Controls in-memory cache duration and Cache-Control max-age for prefetch responses.
|
|
113
|
+
// Set to false to disable prefetch caching.
|
|
114
|
+
prefetchCacheTTL?: number | false;
|
|
115
|
+
|
|
103
116
|
// CSP nonce provider (for router.fetch)
|
|
104
|
-
nonce?: (
|
|
117
|
+
nonce?: (
|
|
118
|
+
request: Request,
|
|
119
|
+
env: TEnv,
|
|
120
|
+
) => string | true | Promise<string | true>;
|
|
105
121
|
|
|
106
122
|
// RSC version string (for router.fetch)
|
|
107
123
|
version?: string;
|
|
@@ -160,7 +176,7 @@ import { createRouter } from "@rangojs/router";
|
|
|
160
176
|
import { Document } from "./document";
|
|
161
177
|
import { urlpatterns } from "./urls";
|
|
162
178
|
|
|
163
|
-
export const router = createRouter<
|
|
179
|
+
export const router = createRouter<AppBindings>({
|
|
164
180
|
document: Document,
|
|
165
181
|
urls: urlpatterns,
|
|
166
182
|
});
|
|
@@ -170,7 +186,7 @@ import { router } from "./router";
|
|
|
170
186
|
|
|
171
187
|
export default {
|
|
172
188
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
173
|
-
return router.fetch(request, {
|
|
189
|
+
return router.fetch(request, { env, ctx });
|
|
174
190
|
},
|
|
175
191
|
};
|
|
176
192
|
```
|
|
@@ -184,12 +200,12 @@ For per-request cache configuration (e.g., Cloudflare Workers with ExecutionCont
|
|
|
184
200
|
import { createRouter } from "@rangojs/router";
|
|
185
201
|
import { CFCacheStore } from "@rangojs/router/cache";
|
|
186
202
|
|
|
187
|
-
export const router = createRouter<
|
|
203
|
+
export const router = createRouter<AppBindings>({
|
|
188
204
|
document: Document,
|
|
189
205
|
urls: urlpatterns,
|
|
190
|
-
// Cache config receives env
|
|
191
|
-
cache: (
|
|
192
|
-
store: new CFCacheStore({ ctx:
|
|
206
|
+
// Cache config receives (env, ctx) separately
|
|
207
|
+
cache: (_env, ctx) => ({
|
|
208
|
+
store: new CFCacheStore({ ctx: ctx!, defaults: { ttl: 60 } }),
|
|
193
209
|
}),
|
|
194
210
|
});
|
|
195
211
|
|
|
@@ -198,7 +214,7 @@ import { router } from "./router";
|
|
|
198
214
|
|
|
199
215
|
export default {
|
|
200
216
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
201
|
-
return router.fetch(request, {
|
|
217
|
+
return router.fetch(request, { env, ctx });
|
|
202
218
|
},
|
|
203
219
|
};
|
|
204
220
|
```
|
|
@@ -286,10 +302,10 @@ export const shopPatterns = urls(({ path, layout }) => [
|
|
|
286
302
|
]);
|
|
287
303
|
|
|
288
304
|
// src/urls.tsx
|
|
289
|
-
import { urls
|
|
305
|
+
import { urls } from "@rangojs/router";
|
|
290
306
|
import { shopPatterns } from "./urls/shop";
|
|
291
307
|
|
|
292
|
-
export const urlpatterns = urls(({ path }) => [
|
|
308
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
293
309
|
path("/", HomePage, { name: "home" }),
|
|
294
310
|
include("/shop", shopPatterns, { name: "shop" }),
|
|
295
311
|
]);
|
|
@@ -298,23 +314,29 @@ export const urlpatterns = urls(({ path }) => [
|
|
|
298
314
|
## Environment Types
|
|
299
315
|
|
|
300
316
|
```typescript
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
// Bindings passed as TEnv to createRouter<TEnv>()
|
|
303
318
|
interface AppBindings {
|
|
304
319
|
DB: D1Database;
|
|
305
320
|
KV: KVNamespace;
|
|
306
321
|
}
|
|
307
322
|
|
|
323
|
+
// Variables declared via module augmentation
|
|
308
324
|
interface AppVariables {
|
|
309
325
|
user?: { id: string; name: string };
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const router = createRouter<AppEnv>({
|
|
328
|
+
const router = createRouter<AppBindings>({
|
|
315
329
|
document: Document,
|
|
316
330
|
urls: urlpatterns,
|
|
317
331
|
});
|
|
332
|
+
|
|
333
|
+
// Register types globally for implicit typing
|
|
334
|
+
declare global {
|
|
335
|
+
namespace RSCRouter {
|
|
336
|
+
interface Env extends AppBindings {}
|
|
337
|
+
interface Vars extends AppVariables {}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
318
340
|
```
|
|
319
341
|
|
|
320
342
|
## Connection Warmup
|
|
@@ -344,3 +366,74 @@ const router = createRouter({
|
|
|
344
366
|
|
|
345
367
|
The warmup request is relative to the current page path, so it works correctly
|
|
346
368
|
with subpath deployments (reverse proxy, base path).
|
|
369
|
+
|
|
370
|
+
## Telemetry
|
|
371
|
+
|
|
372
|
+
The router emits structured lifecycle events through a pluggable telemetry sink.
|
|
373
|
+
Zero overhead when not configured.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// Console sink for development
|
|
377
|
+
import { createRouter, createConsoleSink } from "@rangojs/router";
|
|
378
|
+
|
|
379
|
+
const router = createRouter({
|
|
380
|
+
document: Document,
|
|
381
|
+
urls: urlpatterns,
|
|
382
|
+
telemetry: createConsoleSink(),
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// OpenTelemetry for production
|
|
388
|
+
import { createRouter, createOTelSink } from "@rangojs/router";
|
|
389
|
+
import { trace } from "@opentelemetry/api";
|
|
390
|
+
|
|
391
|
+
const router = createRouter({
|
|
392
|
+
document: Document,
|
|
393
|
+
urls: urlpatterns,
|
|
394
|
+
telemetry: createOTelSink(trace.getTracer("my-app")),
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// Custom sink
|
|
400
|
+
const router = createRouter({
|
|
401
|
+
telemetry: {
|
|
402
|
+
emit(event) {
|
|
403
|
+
// Send to any observability backend
|
|
404
|
+
myTracer.record(event);
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Events emitted: `request.start/end/error`, `loader.start/end/error`,
|
|
411
|
+
`handler.error`, `cache.decision`, `revalidation.decision`.
|
|
412
|
+
|
|
413
|
+
## SSR Streaming Policy
|
|
414
|
+
|
|
415
|
+
Control whether HTML SSR responses stream progressively or wait for all content:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { createRouter, type SSRStreamMode } from "@rangojs/router";
|
|
419
|
+
|
|
420
|
+
const router = createRouter({
|
|
421
|
+
ssr: {
|
|
422
|
+
resolveStreaming: ({ request }) => {
|
|
423
|
+
const ua = request.headers.get("user-agent") ?? "";
|
|
424
|
+
// Bots that can't process streamed HTML get a fully resolved page
|
|
425
|
+
if (/Googlebot|bingbot/i.test(ua)) return "allReady";
|
|
426
|
+
return "stream";
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`SSRStreamMode` is `"stream" | "allReady"`:
|
|
433
|
+
|
|
434
|
+
- `"stream"` (default) — flush HTML as React renders. Suspense fallbacks appear first, then resolved content streams in. Best for real users (fastest TTFB).
|
|
435
|
+
- `"allReady"` — await `stream.allReady` before flushing. The full page arrives in one shot. Use for bots that cannot execute JavaScript or process chunked HTML.
|
|
436
|
+
|
|
437
|
+
The resolver receives `{ request, env, url }` and may be sync or async. It only runs on HTML SSR paths — RSC partials, `__rsc` requests, and response routes are unaffected.
|
|
438
|
+
|
|
439
|
+
When `resolveStreaming` is not configured, the default is `"stream"`.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind
|
|
3
|
+
description: Set up Tailwind CSS v4 with the Document component and CSS imports
|
|
4
|
+
argument-hint: [setup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tailwind CSS
|
|
8
|
+
|
|
9
|
+
Set up Tailwind CSS v4 with the Rango router. Styles are loaded through the Document component using Vite's `?url` CSS import.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D tailwindcss @tailwindcss/vite
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Vite Plugin
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// vite.config.ts
|
|
21
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
plugins: [
|
|
25
|
+
tailwindcss(),
|
|
26
|
+
// ... other plugins
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CSS Entry Point
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
/* src/index.css */
|
|
35
|
+
@import "tailwindcss";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Document Component
|
|
39
|
+
|
|
40
|
+
Import the CSS file with `?url` to get a hashed URL, then preload and link it in `<head>`:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// src/document.tsx
|
|
44
|
+
"use client";
|
|
45
|
+
|
|
46
|
+
import type { ReactNode } from "react";
|
|
47
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
48
|
+
import styles from "./index.css?url";
|
|
49
|
+
|
|
50
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
51
|
+
return (
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<link rel="preload" href={styles} as="style" />
|
|
55
|
+
<link rel="stylesheet" href={styles} />
|
|
56
|
+
<MetaTags />
|
|
57
|
+
</head>
|
|
58
|
+
<body className="font-sans antialiased text-slate-900 bg-slate-50">
|
|
59
|
+
{children}
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
|
|
67
|
+
|
|
68
|
+
## Customizing the Theme
|
|
69
|
+
|
|
70
|
+
Tailwind v4 uses CSS `@theme` for customization:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
/* src/index.css */
|
|
74
|
+
@import "tailwindcss";
|
|
75
|
+
|
|
76
|
+
@theme {
|
|
77
|
+
--font-sans: "Inter", system-ui, sans-serif;
|
|
78
|
+
--color-primary: #3b82f6;
|
|
79
|
+
--color-secondary: #64748b;
|
|
80
|
+
--breakpoint-3xl: 1920px;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Dark Mode
|
|
85
|
+
|
|
86
|
+
Combine with the Rango theme system (see `/theme`):
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const router = createRouter({
|
|
90
|
+
document: Document,
|
|
91
|
+
urls: urlpatterns,
|
|
92
|
+
theme: { attribute: "class" },
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then use Tailwind's `dark:` variant which reads the `class` attribute:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
|
|
100
|
+
Content
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## With Custom Fonts
|
|
105
|
+
|
|
106
|
+
Use `@fontsource-variable` for self-hosted fonts bundled by Vite (see `/fonts` for all options):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pnpm add @fontsource-variable/inter
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
/* src/index.css */
|
|
114
|
+
@import "@fontsource-variable/inter";
|
|
115
|
+
@import "tailwindcss";
|
|
116
|
+
|
|
117
|
+
@theme {
|
|
118
|
+
--font-sans: "Inter Variable", system-ui, sans-serif;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
No extra `<link>` tags needed in the Document -- Vite bundles the font files from `node_modules` automatically.
|
|
123
|
+
|
|
124
|
+
## Notes
|
|
125
|
+
|
|
126
|
+
- `?url` import is required -- bare CSS imports inject styles as a side effect and do not work with SSR streaming
|
|
127
|
+
- `<link rel="preload" as="style">` eliminates render-blocking by starting the download early
|
|
128
|
+
- Tailwind v4 does not need a `tailwind.config.js` -- use `@theme` in CSS instead
|
|
129
|
+
- The `@tailwindcss/vite` plugin handles content detection automatically
|
package/skills/theme/SKILL.md
CHANGED
|
@@ -25,31 +25,32 @@ const router = createRouter<Env>({
|
|
|
25
25
|
document: Document,
|
|
26
26
|
urls: urlpatterns,
|
|
27
27
|
theme: {
|
|
28
|
-
defaultTheme: "system",
|
|
28
|
+
defaultTheme: "system", // "light" | "dark" | "system"
|
|
29
29
|
themes: ["light", "dark"],
|
|
30
|
-
attribute: "class",
|
|
30
|
+
attribute: "class", // or "data-theme"
|
|
31
31
|
storageKey: "theme",
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Server (in loaders/middleware)
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import { createLoader
|
|
39
|
+
import { createLoader } from "@rangojs/router";
|
|
40
|
+
import type { Middleware } from "@rangojs/router";
|
|
40
41
|
|
|
41
42
|
// In a loader
|
|
42
|
-
export const SettingsLoader = createLoader(
|
|
43
|
-
const currentTheme = ctx.theme;
|
|
43
|
+
export const SettingsLoader = createLoader(async (ctx) => {
|
|
44
|
+
const currentTheme = ctx.theme; // read from cookie
|
|
44
45
|
return { theme: currentTheme };
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// In middleware
|
|
48
|
-
export const themeMiddleware =
|
|
49
|
+
export const themeMiddleware: Middleware = async (ctx, next) => {
|
|
49
50
|
// Set theme based on user preference
|
|
50
51
|
ctx.setTheme("dark");
|
|
51
52
|
await next();
|
|
52
|
-
}
|
|
53
|
+
};
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
## Client
|