@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -108,6 +108,33 @@ path.text("/api/data", () => "plain text version", { name: "dataText" }),
|
|
|
108
108
|
Without an RSC primary, there is no `text/html` candidate — the Accept header
|
|
109
109
|
picks among the response-type candidates directly.
|
|
110
110
|
|
|
111
|
+
## Type Safety For Negotiated Paths
|
|
112
|
+
|
|
113
|
+
`router.named-routes.gen.ts` validates route names, params, search, `href()`, and
|
|
114
|
+
the `Rango.Path` type, but it does not carry response payload metadata. For MIME or
|
|
115
|
+
response payload types, use one of these surfaces:
|
|
116
|
+
|
|
117
|
+
- `RouteResponse<typeof patterns, "routeName">` for a specific response variant
|
|
118
|
+
by route name. This is the clearest option when several MIME variants share
|
|
119
|
+
one URL pattern.
|
|
120
|
+
- `Rango.PathResponse<"/products/:id">` (ambient, no import) for global lookup by URL pattern or concrete path after the app
|
|
121
|
+
registers `typeof router.routeMap`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// router.tsx
|
|
125
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
126
|
+
|
|
127
|
+
declare global {
|
|
128
|
+
namespace Rango {
|
|
129
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`RegisteredRoutes` is what exposes the richer routeMap entries containing
|
|
135
|
+
response payload metadata. Without it, URL-pattern response lookup has paths but
|
|
136
|
+
no payloads, so response types resolve to `ResponseEnvelope<never>`.
|
|
137
|
+
|
|
111
138
|
## How It Works
|
|
112
139
|
|
|
113
140
|
1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observability
|
|
3
|
+
description: Debug Rango request performance with debugPerformance, Server-Timing, structured telemetry, and tracing
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Observability
|
|
8
|
+
|
|
9
|
+
Use this when you need to understand request latency, cache decisions,
|
|
10
|
+
revalidation behavior, loader overlap, or production traces.
|
|
11
|
+
|
|
12
|
+
Rango exposes two complementary observability surfaces:
|
|
13
|
+
|
|
14
|
+
1. **Performance timeline** (`debugPerformance`) — per-request waterfall for
|
|
15
|
+
local or targeted debugging. It prints to the console and emits
|
|
16
|
+
`Server-Timing`.
|
|
17
|
+
2. **Structured telemetry** (`telemetry`) — lifecycle events sent to a pluggable
|
|
18
|
+
sink for production monitoring, OpenTelemetry, or custom metrics.
|
|
19
|
+
|
|
20
|
+
The essentials are below. The exported `TelemetryEvent` union type
|
|
21
|
+
(`import type { TelemetryEvent } from "@rangojs/router"`) is the full event
|
|
22
|
+
contract — every event kind and its fields are typed there.
|
|
23
|
+
|
|
24
|
+
## Performance timeline
|
|
25
|
+
|
|
26
|
+
Enable globally while debugging:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createRouter } from "@rangojs/router";
|
|
30
|
+
|
|
31
|
+
const router = createRouter({
|
|
32
|
+
document: Document,
|
|
33
|
+
urls: urlpatterns,
|
|
34
|
+
debugPerformance: true,
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or enable for selected requests from middleware:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
middleware(async (ctx, next) => {
|
|
42
|
+
if (ctx.url.searchParams.has("debug")) {
|
|
43
|
+
ctx.debugPerformance();
|
|
44
|
+
}
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Call `ctx.debugPerformance()` before `await next()`. The request then prints a
|
|
50
|
+
shared-axis waterfall and adds a `Server-Timing` header.
|
|
51
|
+
|
|
52
|
+
Read the timeline as intervals:
|
|
53
|
+
|
|
54
|
+
- `handler:total` is the whole router request.
|
|
55
|
+
- `render:total` / `ssr-render-html` show the render pass.
|
|
56
|
+
- `loader:*` rows should overlap render work. If a loader starts only after the
|
|
57
|
+
render bar, it is serialized latency.
|
|
58
|
+
- Cache, route matching, middleware pre/post, RSC serialization, and SSR phases
|
|
59
|
+
appear as separate spans, so the slow phase is visible without guessing.
|
|
60
|
+
|
|
61
|
+
## Structured telemetry
|
|
62
|
+
|
|
63
|
+
Use telemetry when you want durable production events rather than a one-request
|
|
64
|
+
debug waterfall.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { createRouter, createConsoleSink } from "@rangojs/router";
|
|
68
|
+
|
|
69
|
+
const router = createRouter({
|
|
70
|
+
document: Document,
|
|
71
|
+
urls: urlpatterns,
|
|
72
|
+
telemetry: createConsoleSink(),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For OpenTelemetry:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { createRouter, createOTelSink } from "@rangojs/router";
|
|
80
|
+
import { trace } from "@opentelemetry/api";
|
|
81
|
+
|
|
82
|
+
const router = createRouter({
|
|
83
|
+
document: Document,
|
|
84
|
+
urls: urlpatterns,
|
|
85
|
+
telemetry: createOTelSink(trace.getTracer("my-app")),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Custom sinks implement `emit(event)`:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createRouter } from "@rangojs/router";
|
|
93
|
+
|
|
94
|
+
const router = createRouter({
|
|
95
|
+
document: Document,
|
|
96
|
+
urls: urlpatterns,
|
|
97
|
+
telemetry: {
|
|
98
|
+
emit(event) {
|
|
99
|
+
myMetrics.record(event);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Events include `request.start/end/error`, `loader.start/end/error`,
|
|
106
|
+
`handler.error`, `cache.decision`, and `revalidation.decision`.
|
|
107
|
+
|
|
108
|
+
## Debugging revalidation and stale data
|
|
109
|
+
|
|
110
|
+
When stale UI or unexpected partial renders are the question, use all three
|
|
111
|
+
layers together:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { createConsoleSink, createRouter } from "@rangojs/router";
|
|
115
|
+
|
|
116
|
+
const router = createRouter({
|
|
117
|
+
document: Document,
|
|
118
|
+
urls: urlpatterns,
|
|
119
|
+
debugPerformance: true,
|
|
120
|
+
telemetry: createConsoleSink(),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then inspect:
|
|
125
|
+
|
|
126
|
+
- `revalidation.decision` telemetry to see which segment re-ran or skipped.
|
|
127
|
+
- cache spans / `cache.decision` events to see hit, miss, stale, and background
|
|
128
|
+
revalidation behavior.
|
|
129
|
+
- loader spans to confirm live loaders overlap the render rather than blocking
|
|
130
|
+
first paint.
|
|
131
|
+
- the `Server-Timing` header to compare local logs with browser-network timing.
|
|
132
|
+
|
|
133
|
+
## Zero-overhead defaults
|
|
134
|
+
|
|
135
|
+
`debugPerformance` is off by default, and `telemetry` emits nothing unless a sink
|
|
136
|
+
is configured. Per-request `ctx.debugPerformance()` lets you turn on the
|
|
137
|
+
waterfall only for the route, user, or query param you are investigating.
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -8,9 +8,6 @@ argument-hint: [@slot-name]
|
|
|
8
8
|
|
|
9
9
|
Parallel routes render multiple components simultaneously in named slots.
|
|
10
10
|
|
|
11
|
-
Canonical semantics reference:
|
|
12
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
-
|
|
14
11
|
## Basic Parallel Routes
|
|
15
12
|
|
|
16
13
|
```typescript
|
|
@@ -92,6 +89,73 @@ path("/dashboard/:id", (ctx) => {
|
|
|
92
89
|
])
|
|
93
90
|
```
|
|
94
91
|
|
|
92
|
+
## Setting Handles (Meta, Breadcrumbs)
|
|
93
|
+
|
|
94
|
+
Parallel slot handlers can call `ctx.use(Meta)` or `ctx.use(Breadcrumbs)` to
|
|
95
|
+
push handle data. The data is associated with the **parent** layout or route
|
|
96
|
+
segment, not the parallel segment itself. This is because parallels execute
|
|
97
|
+
after their parent handler and inherit its segment scope.
|
|
98
|
+
|
|
99
|
+
This works well for document-level metadata — the handle data follows the
|
|
100
|
+
parent's lifecycle (appears when the parent is mounted, removed when it
|
|
101
|
+
unmounts).
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
parallel({
|
|
105
|
+
"@meta": (ctx) => {
|
|
106
|
+
const meta = ctx.use(Meta);
|
|
107
|
+
meta({ title: "Product Detail" });
|
|
108
|
+
meta({ name: "description", content: "..." });
|
|
109
|
+
return null; // UI-less slot, only sets metadata
|
|
110
|
+
},
|
|
111
|
+
"@sidebar": (ctx) => <Sidebar />,
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Multiple parallels on the same parent can each push handle data — they all
|
|
116
|
+
accumulate under the parent segment ID.
|
|
117
|
+
|
|
118
|
+
### Pattern: `@meta` slot for per-route metadata overrides
|
|
119
|
+
|
|
120
|
+
A dedicated `@meta` parallel slot lets routes define metadata separately from
|
|
121
|
+
their handler logic. The layout sets defaults via a title template, and each
|
|
122
|
+
route overrides via its own `@meta` slot. Since child segments push after
|
|
123
|
+
parents and `collectMeta` uses last-wins deduplication, overrides work
|
|
124
|
+
naturally.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Layout sets defaults
|
|
128
|
+
layout((ctx) => {
|
|
129
|
+
ctx.use(Meta)({ title: { template: "%s | Store", default: "Store" } });
|
|
130
|
+
return <StoreLayout />;
|
|
131
|
+
}, () => [
|
|
132
|
+
// Route with @meta override — decoupled from handler rendering
|
|
133
|
+
path("/:slug", ProductPage, { name: "product" }, () => [
|
|
134
|
+
parallel({
|
|
135
|
+
"@meta": async (ctx) => {
|
|
136
|
+
const product = await ctx.use(ProductLoader);
|
|
137
|
+
const meta = ctx.use(Meta);
|
|
138
|
+
meta({ title: product.name });
|
|
139
|
+
meta({ name: "description", content: product.description });
|
|
140
|
+
meta({
|
|
141
|
+
"script:ld+json": {
|
|
142
|
+
"@context": "https://schema.org",
|
|
143
|
+
"@type": "Product",
|
|
144
|
+
name: product.name,
|
|
145
|
+
description: product.description,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return null; // UI-less slot
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
]),
|
|
152
|
+
])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This keeps the route handler focused on rendering UI while metadata
|
|
156
|
+
(title, description, Open Graph, JSON-LD) lives in a composable slot that
|
|
157
|
+
can be added, removed, or swapped per route without touching the handler.
|
|
158
|
+
|
|
95
159
|
## Parallel Routes with Loaders
|
|
96
160
|
|
|
97
161
|
Add loaders and loading states to parallel routes:
|
|
@@ -109,6 +173,126 @@ parallel(
|
|
|
109
173
|
)
|
|
110
174
|
```
|
|
111
175
|
|
|
176
|
+
### Streaming Behavior
|
|
177
|
+
|
|
178
|
+
Parallels with `loading()` are **independent streaming units**. They don't
|
|
179
|
+
block the parent layout or sibling routes during SSR:
|
|
180
|
+
|
|
181
|
+
- **With `loading()`**: The skeleton renders immediately. The loader runs
|
|
182
|
+
in the background and streams data to the client when ready. The rest
|
|
183
|
+
of the page (layout, route content, other parallels) renders without
|
|
184
|
+
waiting.
|
|
185
|
+
- **Without `loading()`**: The parallel's loaders block the parent layout's
|
|
186
|
+
rendering. Use this when the data must be available before the page
|
|
187
|
+
paints (e.g., critical above-the-fold content).
|
|
188
|
+
- **SPA navigation**: Parallel loaders resolve in the background. The
|
|
189
|
+
existing parallel UI stays visible — no skeleton flash on route changes
|
|
190
|
+
within the same layout.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Sidebar streams independently — page renders immediately
|
|
194
|
+
parallel(
|
|
195
|
+
{ "@sidebar": () => <Sidebar /> },
|
|
196
|
+
() => [loader(SlowSidebarLoader), loading(<SidebarSkeleton />)]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// Cart data blocks layout — must be ready before paint
|
|
200
|
+
parallel(
|
|
201
|
+
{ "@cartBadge": () => <CartBadge /> },
|
|
202
|
+
() => [loader(CartCountLoader)] // No loading() = awaited
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Composable Slots via `handler.use`
|
|
207
|
+
|
|
208
|
+
Slot handlers can carry their own loader, loading, error/notFound boundaries, revalidation, and transition defaults via `.use`. The mount site then declares **just the slot names** — no per-call data wiring.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const CartSummary: Handler = async (ctx) => {
|
|
212
|
+
const cart = await ctx.use(CartLoader);
|
|
213
|
+
return <CartSummaryView cart={cart} />;
|
|
214
|
+
};
|
|
215
|
+
CartSummary.use = () => [
|
|
216
|
+
loader(CartLoader),
|
|
217
|
+
loading(<CartSkeleton />),
|
|
218
|
+
revalidate(revalidateCartData),
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// Same slot, no copy-pasted plumbing across layouts.
|
|
222
|
+
layout(<DashboardLayout />, () => [
|
|
223
|
+
parallel({ "@cart": CartSummary }),
|
|
224
|
+
path("/dashboard", DashboardIndex, { name: "dashboard.index" }),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
layout(<AccountLayout />, () => [
|
|
228
|
+
parallel({ "@cart": CartSummary }),
|
|
229
|
+
path("/account", AccountIndex, { name: "account.index" }),
|
|
230
|
+
]);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an independent streaming unit, exactly as in the **Streaming Behavior** section above.
|
|
234
|
+
|
|
235
|
+
The `parallel` mount site has the narrowest allow-list for `handler.use` items — slots cannot bring their own middleware or layout, only `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, and `transition`. See [skills/handler-use](../handler-use/SKILL.md) for the full table and merge rules.
|
|
236
|
+
|
|
237
|
+
`transition` is allowed in the slot allow-list, but slot-level rendering does **not** currently apply a `<ViewTransition>` wrapper — only the layout/route wraps take effect at render time. For a modal-only morph today, use an element-level React `<ViewTransition>` inside the slot's component. The reverse direction is the useful guarantee: a layout-level `transition()` fires when the layout's default outlet content changes but **not** when a `<ParallelOutlet />` mounts new content (modal opens are not subtree updates of the layout VT). See [skills/view-transitions](../view-transitions/SKILL.md) for the wrap rules and the intercept caveat.
|
|
238
|
+
|
|
239
|
+
### Two scopes for explicit `use`: shared (broadcast) and slot-local
|
|
240
|
+
|
|
241
|
+
`parallel({...slots}, () => [...use])` runs the shared `use()` callback **once per slot** ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)) — items in that callback land on every slot's entry. That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`). (Slots cannot bring `middleware` or `layout` — see the allowed-types note above.)
|
|
242
|
+
|
|
243
|
+
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:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// @cart gets a custom skeleton; @notifs keeps its handler.use default.
|
|
247
|
+
parallel({
|
|
248
|
+
"@cart": {
|
|
249
|
+
handler: Cart,
|
|
250
|
+
use: () => [loading(<CustomCartSkeleton />)],
|
|
251
|
+
},
|
|
252
|
+
"@notifs": Notifs,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Opt one slot out of streaming while siblings still stream the broadcast.
|
|
256
|
+
parallel(
|
|
257
|
+
{
|
|
258
|
+
"@cart": { handler: Cart, use: () => [loading(false)] },
|
|
259
|
+
"@notifs": Notifs,
|
|
260
|
+
},
|
|
261
|
+
() => [loading(<BroadcastSkeleton />)],
|
|
262
|
+
);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
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. See [skills/handler-use § `loading()` is a single-assignment item — scope it correctly](../handler-use/SKILL.md#loading-is-a-single-assignment-item--scope-it-correctly) for the full reasoning.
|
|
266
|
+
|
|
267
|
+
## Slot Override Semantics
|
|
268
|
+
|
|
269
|
+
When multiple `parallel()` calls define the same slot name, **the last
|
|
270
|
+
definition wins**. Earlier definitions of that slot are removed. Other
|
|
271
|
+
slots from the earlier call are preserved.
|
|
272
|
+
|
|
273
|
+
This enables composition patterns where included routes override
|
|
274
|
+
parent-defined slots:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
layout(DashboardLayout, () => [
|
|
278
|
+
// Base slots
|
|
279
|
+
parallel({
|
|
280
|
+
"@sidebar": () => <DefaultSidebar />,
|
|
281
|
+
"@footer": () => <Footer />,
|
|
282
|
+
}),
|
|
283
|
+
|
|
284
|
+
// Override just @sidebar — @footer is preserved
|
|
285
|
+
parallel({ "@sidebar": () => <CustomSidebar /> }),
|
|
286
|
+
|
|
287
|
+
path("/", DashboardIndex, { name: "index" }),
|
|
288
|
+
])
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
After resolution, the layout has two parallel entries:
|
|
292
|
+
|
|
293
|
+
- `{ "@footer": () => <Footer /> }` (first call, `@sidebar` removed)
|
|
294
|
+
- `{ "@sidebar": () => <CustomSidebar /> }` (second call, wins)
|
|
295
|
+
|
|
112
296
|
## Multiple Parallel Slots
|
|
113
297
|
|
|
114
298
|
```typescript
|
|
@@ -153,7 +337,7 @@ parallel(
|
|
|
153
337
|
() => [
|
|
154
338
|
loader(CartLoader),
|
|
155
339
|
// Revalidate when cart actions occur
|
|
156
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
340
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
157
341
|
]
|
|
158
342
|
)
|
|
159
343
|
```
|
|
@@ -162,6 +346,13 @@ Revalidating only the parallel does not re-run outer handlers/layouts.
|
|
|
162
346
|
If the slot reads `ctx.get()` data established above it, opt the outer
|
|
163
347
|
segment into revalidation as well.
|
|
164
348
|
|
|
349
|
+
A `revalidate()` callback may return a hard `boolean`, a soft
|
|
350
|
+
`{ defaultShouldRevalidate }` object, or nothing (`void` / `null` /
|
|
351
|
+
`undefined`) to defer to the next revalidator. See
|
|
352
|
+
[loader/SKILL.md#revalidate-return-shapes](../loader/SKILL.md#revalidate-return-shapes)
|
|
353
|
+
for the full contract — it's the same across `loader()`, `path()`,
|
|
354
|
+
`layout()`, `parallel()`, and `intercept()`.
|
|
355
|
+
|
|
165
356
|
### Revalidation Contracts for Parallel Dependencies
|
|
166
357
|
|
|
167
358
|
Prefer named revalidation contracts shared by both the upstream producer and
|
|
@@ -170,7 +361,7 @@ the parallel consumer:
|
|
|
170
361
|
```typescript
|
|
171
362
|
// revalidation-contracts.ts
|
|
172
363
|
export const revalidateCartData = ({ actionId }) =>
|
|
173
|
-
actionId?.includes("src/actions/cart.ts#")
|
|
364
|
+
actionId?.includes("src/actions/cart.ts#") || undefined;
|
|
174
365
|
|
|
175
366
|
layout(CartLayout, () => [
|
|
176
367
|
revalidate(revalidateCartData), // producer reruns
|
|
@@ -288,7 +479,7 @@ export const shopPatterns = urls(({
|
|
|
288
479
|
() => [
|
|
289
480
|
loader(CartLoader),
|
|
290
481
|
loading(<CartSkeleton />),
|
|
291
|
-
revalidate(({ actionId }) => actionId?.includes("Cart")
|
|
482
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
|
|
292
483
|
]
|
|
293
484
|
),
|
|
294
485
|
|