@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- 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/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -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/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -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 +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -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 +30 -126
- 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 +44 -65
- 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 +76 -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 +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -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 +155 -19
- package/src/index.ts +251 -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 +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -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 +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -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 +291 -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 +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -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 +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -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 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -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 +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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 +266 -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 +462 -0
- package/src/vite/router-discovery.ts +918 -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 +221 -0
- package/src/vite/utils/shared-utils.ts +170 -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
|
@@ -11,6 +11,7 @@ Inspect the route manifest to verify parent relationships, shortCodes, and route
|
|
|
11
11
|
## Quick Access
|
|
12
12
|
|
|
13
13
|
In development, visit:
|
|
14
|
+
|
|
14
15
|
```
|
|
15
16
|
http://localhost:PORT/__debug_manifest
|
|
16
17
|
```
|
|
@@ -62,13 +63,13 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
62
63
|
|
|
63
64
|
## ShortCode Format
|
|
64
65
|
|
|
65
|
-
| Prefix | Meaning
|
|
66
|
-
|
|
67
|
-
| **M**
|
|
68
|
-
| **L**
|
|
69
|
-
| **C**
|
|
70
|
-
| **R**
|
|
71
|
-
| **P**
|
|
66
|
+
| Prefix | Meaning |
|
|
67
|
+
| ------ | ---------------------------------------- |
|
|
68
|
+
| **M** | Mount index (multiple `.routes()` calls) |
|
|
69
|
+
| **L** | Layout |
|
|
70
|
+
| **C** | Cache boundary |
|
|
71
|
+
| **R** | Route |
|
|
72
|
+
| **P** | Parallel slot |
|
|
72
73
|
|
|
73
74
|
Example: `M0L0L1C0R0` = Mount 0 → Root Layout → Nested Layout → Cache → Route
|
|
74
75
|
|
|
@@ -85,7 +86,7 @@ Example: `M0L0L1C0R0` = Mount 0 → Root Layout → Nested Layout → Cache →
|
|
|
85
86
|
import {
|
|
86
87
|
serializeManifest,
|
|
87
88
|
compareManifests,
|
|
88
|
-
formatManifestDiff
|
|
89
|
+
formatManifestDiff,
|
|
89
90
|
} from "@rangojs/router/__internal";
|
|
90
91
|
|
|
91
92
|
const oldManifest = await router.debugManifest();
|
|
@@ -99,10 +100,13 @@ console.log(formatManifestDiff(diff));
|
|
|
99
100
|
## Common Issues
|
|
100
101
|
|
|
101
102
|
### Routes have `parentShortCode: null`
|
|
103
|
+
|
|
102
104
|
Routes should have a layout parent. Check that `urls()` handler is being wrapped in root layout.
|
|
103
105
|
|
|
104
106
|
### Missing layouts in hierarchy
|
|
107
|
+
|
|
105
108
|
Verify `layout()` calls wrap child routes correctly.
|
|
106
109
|
|
|
107
110
|
### Wrong mount index
|
|
111
|
+
|
|
108
112
|
Multiple `.routes()` calls create separate mounts (M0, M1, etc.). Use `include()` to share context.
|
|
@@ -14,14 +14,14 @@ Configure document cache in router:
|
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { createRouter } from "@rangojs/router";
|
|
17
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
17
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
18
18
|
import { urlpatterns } from "./urls";
|
|
19
19
|
|
|
20
|
-
const router = createRouter<
|
|
20
|
+
const router = createRouter<AppBindings>({
|
|
21
21
|
document: Document,
|
|
22
22
|
urls: urlpatterns,
|
|
23
|
-
documentCache: (
|
|
24
|
-
store: new CFCacheStore({ ctx:
|
|
23
|
+
documentCache: (_env, ctx) => ({
|
|
24
|
+
store: new CFCacheStore({ ctx: ctx! }),
|
|
25
25
|
skipPaths: ["/api", "/admin"],
|
|
26
26
|
debug: process.env.NODE_ENV === "development",
|
|
27
27
|
}),
|
|
@@ -58,9 +58,9 @@ export const urlpatterns = urls(({ path, cache }) => [
|
|
|
58
58
|
```typescript
|
|
59
59
|
createRouter({
|
|
60
60
|
// ...
|
|
61
|
-
documentCache: (
|
|
61
|
+
documentCache: (_env, ctx) => ({
|
|
62
62
|
// Cache store (required)
|
|
63
|
-
store: new CFCacheStore({ ctx:
|
|
63
|
+
store: new CFCacheStore({ ctx: ctx! }),
|
|
64
64
|
|
|
65
65
|
// Skip specific paths
|
|
66
66
|
skipPaths: ["/api", "/admin"],
|
|
@@ -102,6 +102,7 @@ Request → Check Cache
|
|
|
102
102
|
## Cache Status Header
|
|
103
103
|
|
|
104
104
|
Response includes `x-document-cache-status`:
|
|
105
|
+
|
|
105
106
|
- `HIT` - Fresh cache hit
|
|
106
107
|
- `STALE` - Served stale, revalidating in background
|
|
107
108
|
- `MISS` - Cache miss, response was generated fresh
|
|
@@ -109,6 +110,7 @@ Response includes `x-document-cache-status`:
|
|
|
109
110
|
## Cache Key Generation
|
|
110
111
|
|
|
111
112
|
Default keys differentiate:
|
|
113
|
+
|
|
112
114
|
- HTML requests: `{pathname}:html`
|
|
113
115
|
- RSC partials: `{pathname}:{segmentHash}:rsc`
|
|
114
116
|
|
|
@@ -132,14 +134,14 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
132
134
|
```typescript
|
|
133
135
|
// router.tsx
|
|
134
136
|
import { createRouter } from "@rangojs/router";
|
|
135
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
137
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
136
138
|
import { urlpatterns } from "./urls";
|
|
137
139
|
|
|
138
|
-
const router = createRouter<
|
|
140
|
+
const router = createRouter<AppBindings>({
|
|
139
141
|
document: Document,
|
|
140
142
|
urls: urlpatterns,
|
|
141
|
-
documentCache: (
|
|
142
|
-
store: new CFCacheStore({ ctx:
|
|
143
|
+
documentCache: (_env, ctx) => ({
|
|
144
|
+
store: new CFCacheStore({ ctx: ctx! }),
|
|
143
145
|
skipPaths: ["/api"],
|
|
144
146
|
debug: process.env.NODE_ENV === "development",
|
|
145
147
|
}),
|
|
@@ -170,11 +172,11 @@ export const urlpatterns = urls(({ path, layout, cache, loader }) => [
|
|
|
170
172
|
|
|
171
173
|
## Document Cache vs Segment Cache
|
|
172
174
|
|
|
173
|
-
| Feature
|
|
174
|
-
|
|
175
|
-
| Granularity
|
|
176
|
-
| Opt-in
|
|
177
|
-
| Use case
|
|
178
|
-
| Key includes | URL + segment hash
|
|
175
|
+
| Feature | Document Cache | Segment Cache |
|
|
176
|
+
| ------------ | -------------------------- | --------------------- |
|
|
177
|
+
| Granularity | Full response | Individual segments |
|
|
178
|
+
| Opt-in | `documentCache` in cache() | `cache({ ttl, swr })` |
|
|
179
|
+
| Use case | Static pages | Dynamic compositions |
|
|
180
|
+
| Key includes | URL + segment hash | Route params |
|
|
179
181
|
|
|
180
182
|
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fonts
|
|
3
|
+
description: Load and configure web fonts with preload hints for optimal performance
|
|
4
|
+
argument-hint: [provider]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Fonts
|
|
8
|
+
|
|
9
|
+
Load web fonts in the Document component with `<link rel="preload">` for optimal performance. Fonts are declared in `<head>` alongside your stylesheet.
|
|
10
|
+
|
|
11
|
+
## Google Fonts
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// src/document.tsx
|
|
15
|
+
"use client";
|
|
16
|
+
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
19
|
+
import styles from "./index.css?url";
|
|
20
|
+
|
|
21
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
22
|
+
return (
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
{/* Preconnect to Google Fonts */}
|
|
26
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
27
|
+
<link
|
|
28
|
+
rel="preconnect"
|
|
29
|
+
href="https://fonts.gstatic.com"
|
|
30
|
+
crossOrigin="anonymous"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
{/* Load font stylesheet */}
|
|
34
|
+
<link
|
|
35
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
36
|
+
rel="stylesheet"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* App styles */}
|
|
40
|
+
<link rel="preload" href={styles} as="style" />
|
|
41
|
+
<link rel="stylesheet" href={styles} />
|
|
42
|
+
<MetaTags />
|
|
43
|
+
</head>
|
|
44
|
+
<body>{children}</body>
|
|
45
|
+
</html>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then reference the font in CSS:
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
/* src/index.css */
|
|
54
|
+
body {
|
|
55
|
+
font-family: "Inter", sans-serif;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or with Tailwind (see `/tailwind`):
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
/* src/index.css */
|
|
63
|
+
@theme {
|
|
64
|
+
--font-sans: "Inter", sans-serif;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Self-Hosted Fonts
|
|
69
|
+
|
|
70
|
+
Place font files in `public/fonts/` and use `@font-face`:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
/* src/index.css */
|
|
74
|
+
@font-face {
|
|
75
|
+
font-family: "CustomFont";
|
|
76
|
+
src: url("/fonts/custom-regular.woff2") format("woff2");
|
|
77
|
+
font-weight: 400;
|
|
78
|
+
font-style: normal;
|
|
79
|
+
font-display: swap;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@font-face {
|
|
83
|
+
font-family: "CustomFont";
|
|
84
|
+
src: url("/fonts/custom-bold.woff2") format("woff2");
|
|
85
|
+
font-weight: 700;
|
|
86
|
+
font-style: normal;
|
|
87
|
+
font-display: swap;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
body {
|
|
91
|
+
font-family: "CustomFont", sans-serif;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Preload the most critical weight in the Document:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
99
|
+
return (
|
|
100
|
+
<html lang="en">
|
|
101
|
+
<head>
|
|
102
|
+
<link
|
|
103
|
+
rel="preload"
|
|
104
|
+
href="/fonts/custom-regular.woff2"
|
|
105
|
+
as="font"
|
|
106
|
+
type="font/woff2"
|
|
107
|
+
crossOrigin="anonymous"
|
|
108
|
+
/>
|
|
109
|
+
<link rel="preload" href={styles} as="style" />
|
|
110
|
+
<link rel="stylesheet" href={styles} />
|
|
111
|
+
<MetaTags />
|
|
112
|
+
</head>
|
|
113
|
+
<body>{children}</body>
|
|
114
|
+
</html>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Fontsource (Recommended for Vite)
|
|
120
|
+
|
|
121
|
+
`@fontsource-variable` packages are the recommended approach with Vite. Fonts are installed as npm dependencies, bundled by Vite, and served from your own domain -- no external requests, no privacy concerns, no FOUT from slow CDNs.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pnpm add @fontsource-variable/inter
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Import the font CSS in your stylesheet. Vite resolves the `@fontsource-variable` import from `node_modules` and bundles the woff2 files as hashed assets automatically:
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
/* src/index.css */
|
|
131
|
+
@import "@fontsource-variable/inter";
|
|
132
|
+
|
|
133
|
+
body {
|
|
134
|
+
font-family: "Inter Variable", sans-serif;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With Tailwind:
|
|
139
|
+
|
|
140
|
+
```css
|
|
141
|
+
/* src/index.css */
|
|
142
|
+
@import "@fontsource-variable/inter";
|
|
143
|
+
@import "tailwindcss";
|
|
144
|
+
|
|
145
|
+
@theme {
|
|
146
|
+
--font-sans: "Inter Variable", sans-serif;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Why this is preferred over Google Fonts with Vite:
|
|
151
|
+
|
|
152
|
+
- No external network requests at runtime -- fonts are bundled into your build output
|
|
153
|
+
- No `<link rel="preconnect">` or extra stylesheet needed in the Document
|
|
154
|
+
- Variable font = single file covers all weights, smaller total download
|
|
155
|
+
- Vite handles cache-busting via content hashes
|
|
156
|
+
- Works offline and in edge deployments (Cloudflare Workers) without external dependencies
|
|
157
|
+
|
|
158
|
+
Browse available fonts at fontsource.org. Use `@fontsource-variable/*` for variable fonts and `@fontsource/*` for static fonts.
|
|
159
|
+
|
|
160
|
+
## Performance Tips
|
|
161
|
+
|
|
162
|
+
- Prefer `@fontsource-variable` with Vite for self-hosted, zero-config font loading
|
|
163
|
+
- Use `font-display: swap` to prevent invisible text during font load
|
|
164
|
+
- Preload only the most critical font weight (usually regular 400)
|
|
165
|
+
- Prefer `woff2` format for smaller file sizes
|
|
166
|
+
- Use variable fonts when multiple weights are needed to reduce total file count
|
|
167
|
+
- `<link rel="preconnect">` eliminates DNS + TLS latency for external font providers
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handler-use
|
|
3
|
+
description: Attach default loaders, middleware, parallels, and other use items directly to handlers via handler.use, and compose them with explicit use() at mount sites
|
|
4
|
+
argument-hint: "[handler]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Handler-Attached `.use`
|
|
8
|
+
|
|
9
|
+
A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site.
|
|
10
|
+
|
|
11
|
+
This lets handlers be **self-contained, reusable units** — a page brings its own loader, a layout brings its own middleware, a parallel slot brings its own data + skeleton — without forcing every caller to wire the same items at every mount site.
|
|
12
|
+
|
|
13
|
+
Canonical implementation reference:
|
|
14
|
+
[src/route-definition/resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts)
|
|
15
|
+
|
|
16
|
+
## Defining a handler with `.use`
|
|
17
|
+
|
|
18
|
+
Attach `.use` to the function (or to the branded definition for `Static()`/`Prerender()`/`Passthrough()`):
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
loader,
|
|
23
|
+
middleware,
|
|
24
|
+
loading,
|
|
25
|
+
createLoader,
|
|
26
|
+
type Handler,
|
|
27
|
+
} from "@rangojs/router";
|
|
28
|
+
|
|
29
|
+
export const ProductLoader = createLoader(async (ctx) =>
|
|
30
|
+
fetchProduct(ctx.params.slug),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const ProductPage: Handler<"/product/:slug"> = async (ctx) => {
|
|
34
|
+
const product = await ctx.use(ProductLoader);
|
|
35
|
+
return <ProductView product={product} />;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
ProductPage.use = () => [
|
|
39
|
+
loader(ProductLoader),
|
|
40
|
+
loading(<ProductSkeleton />),
|
|
41
|
+
middleware(async (ctx, next) => {
|
|
42
|
+
await next();
|
|
43
|
+
ctx.header("Cache-Control", "private, max-age=60");
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Now `ProductPage` carries its loader, loading state, and response-header middleware regardless of where it is mounted.
|
|
49
|
+
|
|
50
|
+
## Allowed items per mount site
|
|
51
|
+
|
|
52
|
+
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
|
+
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route`, `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when`, `transition` |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
|
+
|
|
62
|
+
If `handler.use()` returns a disallowed item for a mount site, registration throws:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
handler.use() returned middleware() which is not valid inside parallel().
|
|
66
|
+
Allowed types: revalidate, loader, loading, errorBoundary, notFoundBoundary, transition.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The narrowest contract is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
|
|
70
|
+
|
|
71
|
+
## Composition with explicit `use()`
|
|
72
|
+
|
|
73
|
+
Every mount site that takes a `use` callback merges in this order:
|
|
74
|
+
|
|
75
|
+
1. **`handler.use()` items first** — the handler's defaults.
|
|
76
|
+
2. **Explicit `use()` items second** — overrides specified at the mount site.
|
|
77
|
+
|
|
78
|
+
Items of the same kind from the explicit `use()` follow the existing override rules of that item type. The most important ones for composition:
|
|
79
|
+
|
|
80
|
+
- **`loading()`** — last definition wins, so explicit `loading()` replaces the handler's default.
|
|
81
|
+
- **`parallel({ "@slot": … })`** — the last `parallel()` call wins per slot name. Other slots from earlier calls are preserved (see `skills/parallel`).
|
|
82
|
+
- **`loader()`, `middleware()`, etc.** — accumulate; both the handler's and the explicit ones run.
|
|
83
|
+
|
|
84
|
+
Skip the boilerplate: if neither `handler.use` nor explicit `use()` is provided, no merge happens.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Handler brings a loader + a (placeholder) loading; explicit use replaces loading.
|
|
88
|
+
const SidebarSlot: Handler = async (ctx) => {
|
|
89
|
+
const data = await ctx.use(SidebarLoader);
|
|
90
|
+
return <Sidebar data={data} />;
|
|
91
|
+
};
|
|
92
|
+
SidebarSlot.use = () => [
|
|
93
|
+
loader(SidebarLoader),
|
|
94
|
+
loading(<DefaultSidebarSkeleton />),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
parallel({ "@sidebar": SidebarSlot }, () => [
|
|
98
|
+
// Replaces the default skeleton; SidebarLoader from handler.use still runs.
|
|
99
|
+
loading(<SiteSpecificSidebarSkeleton />),
|
|
100
|
+
]);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Composable parallel slots (the main pay-off)
|
|
104
|
+
|
|
105
|
+
The parallel slot site is where `handler.use` shines. A slot handler that owns its data/loading lets a layout declare **just** the slot names — every loader, skeleton, and revalidation contract travels with the slot itself.
|
|
106
|
+
|
|
107
|
+
### Without `handler.use` (every caller wires it up)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
layout(<DashboardLayout />, () => [
|
|
111
|
+
parallel({ "@cart": CartSummary }, () => [
|
|
112
|
+
loader(CartLoader),
|
|
113
|
+
loading(<CartSkeleton />),
|
|
114
|
+
revalidate(revalidateCartData),
|
|
115
|
+
]),
|
|
116
|
+
parallel({ "@notifs": NotificationPanel }, () => [
|
|
117
|
+
loader(NotificationsLoader),
|
|
118
|
+
loading(<NotifsSkeleton />),
|
|
119
|
+
revalidate(revalidateNotifs),
|
|
120
|
+
]),
|
|
121
|
+
path("/dashboard", DashboardIndex, { name: "dashboard.index" }),
|
|
122
|
+
]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every layout that wants `@cart` must repeat the same loader/loading/revalidate triplet.
|
|
126
|
+
|
|
127
|
+
### With `handler.use` (slot owns its dependencies)
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const CartSummary: Handler = async (ctx) => {
|
|
131
|
+
const cart = await ctx.use(CartLoader);
|
|
132
|
+
return <CartSummaryView cart={cart} />;
|
|
133
|
+
};
|
|
134
|
+
CartSummary.use = () => [
|
|
135
|
+
loader(CartLoader),
|
|
136
|
+
loading(<CartSkeleton />),
|
|
137
|
+
revalidate(revalidateCartData),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const NotificationPanel: Handler = async (ctx) => {
|
|
141
|
+
const items = await ctx.use(NotificationsLoader);
|
|
142
|
+
return <NotificationsView items={items} />;
|
|
143
|
+
};
|
|
144
|
+
NotificationPanel.use = () => [
|
|
145
|
+
loader(NotificationsLoader),
|
|
146
|
+
loading(<NotifsSkeleton />),
|
|
147
|
+
revalidate(revalidateNotifs),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Mount sites become declarative — no per-call data wiring.
|
|
151
|
+
layout(<DashboardLayout />, () => [
|
|
152
|
+
parallel({ "@cart": CartSummary, "@notifs": NotificationPanel }),
|
|
153
|
+
path("/dashboard", DashboardIndex, { name: "dashboard.index" }),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
layout(<AccountLayout />, () => [
|
|
157
|
+
// Same slot, same defaults, zero re-wiring.
|
|
158
|
+
parallel({ "@cart": CartSummary }),
|
|
159
|
+
path("/account", AccountIndex, { name: "account.index" }),
|
|
160
|
+
]);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Each slot handler is now a portable, self-contained unit. Different layouts can use the same slot without copying data plumbing.
|
|
164
|
+
|
|
165
|
+
### Streaming behavior is per-slot
|
|
166
|
+
|
|
167
|
+
A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an independent streaming unit — its loader does not block the parent layout. Two slot handlers with their own loading skeletons stream independently.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
parallel({
|
|
171
|
+
"@cart": CartSummary, // handler.use loading() → streams independently
|
|
172
|
+
"@cartBadge": CartBadge, // no loading() anywhere → awaited before paint
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
|
|
177
|
+
|
|
178
|
+
`parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
|
|
179
|
+
|
|
180
|
+
For single-assignment items like `loading()`, broadcasting overwrites every slot's `handler.use` default. Pass a **slot descriptor** `{ handler, use }` instead: items in the descriptor's `use` apply only to that slot.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
parallel({
|
|
184
|
+
"@meta": MetaSlot,
|
|
185
|
+
"@sidebar": {
|
|
186
|
+
handler: SidebarSlot,
|
|
187
|
+
use: () => [loading(<SidebarSkeleton />)], // ← only @sidebar
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Per-slot merge order is **handler.use → shared use → slot-local use** (narrowest scope wins for last-write-wins items like `loading()`):
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
parallel(
|
|
196
|
+
{
|
|
197
|
+
"@cart": {
|
|
198
|
+
handler: Cart,
|
|
199
|
+
use: () => [loading(<CartSkeleton />)], // wins for @cart
|
|
200
|
+
},
|
|
201
|
+
"@notifs": Notifs, // gets <BroadcastSkeleton />
|
|
202
|
+
},
|
|
203
|
+
() => [
|
|
204
|
+
loader(SharedAnalyticsLoader), // accumulates on every slot
|
|
205
|
+
loading(<BroadcastSkeleton />), // applies to slots without slot-local
|
|
206
|
+
],
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use the descriptor's `use` for `loading(false)` too — opting one slot out of streaming without affecting siblings:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
parallel(
|
|
214
|
+
{
|
|
215
|
+
"@cart": { handler: Cart, use: () => [loading(false)] }, // @cart awaits
|
|
216
|
+
"@notifs": Notifs, // @notifs still streams with broadcast skeleton
|
|
217
|
+
},
|
|
218
|
+
() => [loading(<BroadcastSkeleton />)],
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Rule of thumb: shared `use` is for items that legitimately apply to every slot. Slot-local `use` is for per-slot precision — especially `loading()` and `loading(false)`.
|
|
223
|
+
|
|
224
|
+
### Replacing a whole slot from a parent's `handler.use`
|
|
225
|
+
|
|
226
|
+
A handler can publish a default `parallel({...})` set via its `.use`, and the mount site can replace any individual slot by re-declaring it. Last `parallel()` per slot name wins (see `skills/parallel` § Slot Override Semantics).
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const ProductPage: Handler<"/product/:slug"> = (ctx) => (
|
|
230
|
+
<article>
|
|
231
|
+
<ProductHero slug={ctx.params.slug} />
|
|
232
|
+
<ParallelOutlet name="@related" />
|
|
233
|
+
<ParallelOutlet name="@reviews" />
|
|
234
|
+
</article>
|
|
235
|
+
);
|
|
236
|
+
ProductPage.use = () => [
|
|
237
|
+
parallel({
|
|
238
|
+
"@related": DefaultRelatedProducts,
|
|
239
|
+
"@reviews": DefaultReviews,
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
244
|
+
// Override @related only; @reviews keeps the default from handler.use.
|
|
245
|
+
parallel({ "@related": SiteSpecificRelated }),
|
|
246
|
+
]);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Other mount sites
|
|
250
|
+
|
|
251
|
+
### Pages (`path()`)
|
|
252
|
+
|
|
253
|
+
Page handlers can carry middleware, loaders, error boundaries, parallel slots, etc. — anything from the `path` row of the table above.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const CheckoutPage: Handler<"/checkout"> = async (ctx) => { /* … */ };
|
|
257
|
+
CheckoutPage.use = () => [
|
|
258
|
+
middleware(requireAuth),
|
|
259
|
+
loader(CartLoader),
|
|
260
|
+
errorBoundary(<CheckoutError />),
|
|
261
|
+
notFoundBoundary(<CheckoutNotFound />),
|
|
262
|
+
];
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Layouts (`layout()`)
|
|
266
|
+
|
|
267
|
+
Layout handlers can carry middleware that runs for every child route, plus default parallels, includes, etc.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const AdminLayout: Handler = (ctx) => {
|
|
271
|
+
const user = ctx.get(CurrentUser);
|
|
272
|
+
return <Admin user={user} />;
|
|
273
|
+
};
|
|
274
|
+
AdminLayout.use = () => [
|
|
275
|
+
middleware(requireAdmin),
|
|
276
|
+
parallel({ "@adminNotifs": AdminNotifsSlot }),
|
|
277
|
+
];
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Intercepts (`intercept()`)
|
|
281
|
+
|
|
282
|
+
Intercept handlers can carry their own middleware chain, loaders, and even nested layouts/routes for the modal shell.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const QuickViewModal: Handler = async (ctx) => {
|
|
286
|
+
const product = await ctx.use(ProductLoader);
|
|
287
|
+
return <QuickView product={product} />;
|
|
288
|
+
};
|
|
289
|
+
QuickViewModal.use = () => [
|
|
290
|
+
loader(ProductLoader),
|
|
291
|
+
loading(<QuickViewSkeleton />),
|
|
292
|
+
layout(<ModalChrome />),
|
|
293
|
+
];
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## `loading()` is a single-assignment item — scope it correctly
|
|
297
|
+
|
|
298
|
+
Most `use` items accumulate when merged: `handler.use` `middleware()` runs _and_ explicit `middleware()` runs; both `loader()` registrations apply. `loading()` is different — it mutates `entry.loading` directly, last call wins ([dsl-helpers.ts `loadingFn`](../../src/route-definition/dsl-helpers.ts)).
|
|
299
|
+
|
|
300
|
+
For pages, layouts, and intercepts that's straightforward: explicit `loading()` at the mount site replaces any `loading()` from `handler.use`. The merge order is `handler.use → explicit`, so the explicit one is the last writer and wins.
|
|
301
|
+
|
|
302
|
+
For parallel slots, the shared `parallel(..., () => [...])` callback is **broadcast** to every slot in the call. A single `loading()` placed there lands on every slot, overwriting each slot's `handler.use` default. To scope `loading()` to one slot, use the **slot descriptor** form:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const Cart: Handler = async (ctx) => { /* … */ };
|
|
306
|
+
Cart.use = () => [loader(CartLoader), loading(<CartSkeleton />)];
|
|
307
|
+
|
|
308
|
+
const Notifs: Handler = async (ctx) => { /* … */ };
|
|
309
|
+
Notifs.use = () => [loader(NotifsLoader), loading(<NotifsSkeleton />)];
|
|
310
|
+
|
|
311
|
+
// ✅ @cart gets a custom skeleton; @notifs keeps its handler.use default.
|
|
312
|
+
parallel({
|
|
313
|
+
"@cart": {
|
|
314
|
+
handler: Cart,
|
|
315
|
+
use: () => [loading(<CustomCartSkeleton />)],
|
|
316
|
+
},
|
|
317
|
+
"@notifs": Notifs,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ✅ Opt one slot out of streaming while siblings still stream the broadcast.
|
|
321
|
+
parallel(
|
|
322
|
+
{
|
|
323
|
+
"@cart": { handler: Cart, use: () => [loading(false)] },
|
|
324
|
+
"@notifs": Notifs,
|
|
325
|
+
},
|
|
326
|
+
() => [loading(<BroadcastSkeleton />)],
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-local is the narrowest scope, so it wins for last-write-wins items like `loading()`. Items that accumulate within the parallel allow-list (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) compose across all three layers regardless.
|
|
331
|
+
|
|
332
|
+
Other things to keep in mind about `loading()`:
|
|
333
|
+
|
|
334
|
+
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading: false` handling in [match-middleware/segment-resolution.ts](../../src/router/match-middleware/segment-resolution.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
|
|
335
|
+
|
|
336
|
+
Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
|
|
337
|
+
|
|
338
|
+
## Edge cases & gotchas
|
|
339
|
+
|
|
340
|
+
- **ReactNode handlers cannot have `.use`.** A bare JSX element passed as a handler (e.g., `path("/about", <About />)`) has no function to attach properties to. Pass a function or branded definition instead.
|
|
341
|
+
- **Branded handlers** — `Static()`, `Prerender()`, and `Passthrough()` are positional constructors (not object-arg). Construct first, then attach `.use` to the returned definition:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
const ProductPage = Prerender(async (ctx) => {
|
|
345
|
+
const product = await fetchProduct(ctx.params.slug);
|
|
346
|
+
return <ProductView product={product} />;
|
|
347
|
+
});
|
|
348
|
+
ProductPage.use = () => [loader(ProductLoader)];
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- **Items can be flat or nested arrays.** `handler.use()` results are flattened with `.flat(3)` before validation, so factory helpers that return arrays inline work the same as in regular `use()` callbacks.
|
|
352
|
+
- **Validation runs at registration / first match**, not at handler definition. A handler doesn't know its mount site at definition time — the same handler used in a `path()` and an `intercept()` is validated against each mount's allowed-types set when registered.
|
|
353
|
+
- **No silent shadowing.** If a disallowed item slips through (e.g., a layout factory returning `cache()` from a slot's `handler.use`), the runtime throws with the offending type and mount site named.
|
|
354
|
+
|
|
355
|
+
## Cross-references
|
|
356
|
+
|
|
357
|
+
- `skills/route` — `path()` mount site basics
|
|
358
|
+
- `skills/layout` — `layout()` mount site basics
|
|
359
|
+
- `skills/parallel` — parallel slot semantics, slot override rules, streaming behavior
|
|
360
|
+
- `skills/intercept` — intercept mount site basics
|
|
361
|
+
- `skills/loader` — defining `createLoader` and reading via `ctx.use()`
|
|
362
|
+
- `skills/middleware` — middleware semantics and ordering
|