@rangojs/router 0.0.0-experimental.002d056c
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 +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- 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 +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- 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 +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -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 +479 -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 +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- 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 +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- 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 +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- 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 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- 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 +1242 -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 +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -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 +237 -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 +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- 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 +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- 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 -0
- package/src/use-loader.tsx +354 -0
- 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 +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -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/plugins/expose-action-id.ts +363 -0
- 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 +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -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/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment Resolution Middleware
|
|
3
|
+
*
|
|
4
|
+
* Resolves route segments when cache misses. Skips if cache hit.
|
|
5
|
+
*
|
|
6
|
+
* FLOW DIAGRAM
|
|
7
|
+
* ============
|
|
8
|
+
*
|
|
9
|
+
* source (from cache-lookup)
|
|
10
|
+
* |
|
|
11
|
+
* v
|
|
12
|
+
* +---------------------------+
|
|
13
|
+
* | Iterate source first! | <-- CRITICAL: Must drain source
|
|
14
|
+
* | yield* source | to let cache-lookup run
|
|
15
|
+
* +---------------------------+
|
|
16
|
+
* |
|
|
17
|
+
* v
|
|
18
|
+
* +---------------------+
|
|
19
|
+
* | state.cacheHit? |──yes──> return (cache already yielded)
|
|
20
|
+
* +---------------------+
|
|
21
|
+
* | no
|
|
22
|
+
* v
|
|
23
|
+
* +---------------------+
|
|
24
|
+
* | isFullMatch? |
|
|
25
|
+
* +---------------------+
|
|
26
|
+
* |
|
|
27
|
+
* +-----+-----+
|
|
28
|
+
* | |
|
|
29
|
+
* yes no
|
|
30
|
+
* | |
|
|
31
|
+
* v v
|
|
32
|
+
* resolveAll resolveAllWithRevalidation
|
|
33
|
+
* Segments Segments
|
|
34
|
+
* | |
|
|
35
|
+
* | | (compares with prev state)
|
|
36
|
+
* | | (handles null components)
|
|
37
|
+
* | |
|
|
38
|
+
* +-----------+
|
|
39
|
+
* |
|
|
40
|
+
* v
|
|
41
|
+
* +---------------------------+
|
|
42
|
+
* | Update state: |
|
|
43
|
+
* | - state.segments |
|
|
44
|
+
* | - state.matchedIds |
|
|
45
|
+
* +---------------------------+
|
|
46
|
+
* |
|
|
47
|
+
* v
|
|
48
|
+
* yield all resolved segments
|
|
49
|
+
* |
|
|
50
|
+
* v
|
|
51
|
+
* next middleware
|
|
52
|
+
*
|
|
53
|
+
*
|
|
54
|
+
* RESOLUTION MODES
|
|
55
|
+
* ================
|
|
56
|
+
*
|
|
57
|
+
* Full Match (document request):
|
|
58
|
+
* - Uses resolveAllSegments()
|
|
59
|
+
* - No revalidation logic (nothing to compare against)
|
|
60
|
+
* - Simple resolution of all route entries
|
|
61
|
+
*
|
|
62
|
+
* Partial Match (navigation):
|
|
63
|
+
* - Uses resolveAllSegmentsWithRevalidation()
|
|
64
|
+
* - Compares current vs previous params/URL
|
|
65
|
+
* - Sets component = null for segments client already has
|
|
66
|
+
* - Respects custom revalidation rules
|
|
67
|
+
*
|
|
68
|
+
*
|
|
69
|
+
* CRITICAL: SOURCE ITERATION
|
|
70
|
+
* ==========================
|
|
71
|
+
*
|
|
72
|
+
* The middleware MUST iterate the source generator before checking cacheHit:
|
|
73
|
+
*
|
|
74
|
+
* for await (const segment of source) { yield segment; }
|
|
75
|
+
*
|
|
76
|
+
* This is because:
|
|
77
|
+
* 1. Generator middleware are lazy (don't execute until iterated)
|
|
78
|
+
* 2. cache-lookup sets state.cacheHit during iteration
|
|
79
|
+
* 3. Without draining source first, cache-lookup never runs
|
|
80
|
+
*
|
|
81
|
+
* Incorrect pattern:
|
|
82
|
+
* if (!state.cacheHit) { ... } // cacheHit still false!
|
|
83
|
+
* yield* source; // Too late, already resolved
|
|
84
|
+
*
|
|
85
|
+
* Correct pattern:
|
|
86
|
+
* yield* source; // Let cache-lookup set cacheHit
|
|
87
|
+
* if (state.cacheHit) return; // Now we can check
|
|
88
|
+
*/
|
|
89
|
+
import type { ResolvedSegment } from "../../types.js";
|
|
90
|
+
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
91
|
+
import { getRouterContext } from "../router-context.js";
|
|
92
|
+
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates segment resolution middleware
|
|
96
|
+
*
|
|
97
|
+
* Only runs on cache miss (state.cacheHit === false).
|
|
98
|
+
* Uses resolveAllSegmentsWithRevalidation from RouterContext to resolve segments.
|
|
99
|
+
*/
|
|
100
|
+
export function withSegmentResolution<TEnv>(
|
|
101
|
+
ctx: MatchContext<TEnv>,
|
|
102
|
+
state: MatchPipelineState,
|
|
103
|
+
): GeneratorMiddleware<ResolvedSegment> {
|
|
104
|
+
return async function* (
|
|
105
|
+
source: AsyncGenerator<ResolvedSegment>,
|
|
106
|
+
): AsyncGenerator<ResolvedSegment> {
|
|
107
|
+
const pipelineStart = performance.now();
|
|
108
|
+
const ms = ctx.metricsStore;
|
|
109
|
+
|
|
110
|
+
// IMPORTANT: Always iterate source first to give cache-lookup a chance
|
|
111
|
+
// to run and set state.cacheHit. Without this, cache-lookup never executes!
|
|
112
|
+
for await (const segment of source) {
|
|
113
|
+
yield segment;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If cache hit, segments were already yielded by cache lookup
|
|
117
|
+
if (state.cacheHit) {
|
|
118
|
+
if (ms) {
|
|
119
|
+
ms.metrics.push({
|
|
120
|
+
label: "pipeline:segment-resolve",
|
|
121
|
+
duration: performance.now() - pipelineStart,
|
|
122
|
+
startTime: pipelineStart - ms.requestStart,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { resolveAllSegmentsWithRevalidation, resolveAllSegments } =
|
|
129
|
+
getRouterContext<TEnv>();
|
|
130
|
+
|
|
131
|
+
const Store = ctx.Store;
|
|
132
|
+
|
|
133
|
+
if (ctx.isFullMatch) {
|
|
134
|
+
// Full match (document request) - simple resolution without revalidation
|
|
135
|
+
const segments = await Store.run(() =>
|
|
136
|
+
resolveAllSegments(
|
|
137
|
+
ctx.entries,
|
|
138
|
+
ctx.routeKey,
|
|
139
|
+
ctx.matched.params,
|
|
140
|
+
ctx.handlerContext,
|
|
141
|
+
ctx.loaderPromises,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Update state with resolved segments
|
|
146
|
+
state.segments = segments;
|
|
147
|
+
state.matchedIds = segments.map((s: { id: string }) => s.id);
|
|
148
|
+
|
|
149
|
+
// Yield all resolved segments
|
|
150
|
+
for (const segment of segments) {
|
|
151
|
+
yield segment;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// Partial match (navigation) - resolution with revalidation logic
|
|
155
|
+
const result = await Store.run(() =>
|
|
156
|
+
resolveAllSegmentsWithRevalidation(
|
|
157
|
+
ctx.entries,
|
|
158
|
+
ctx.routeKey,
|
|
159
|
+
ctx.matched.params,
|
|
160
|
+
ctx.handlerContext,
|
|
161
|
+
ctx.clientSegmentSet,
|
|
162
|
+
ctx.prevParams,
|
|
163
|
+
ctx.request,
|
|
164
|
+
ctx.prevUrl,
|
|
165
|
+
ctx.url,
|
|
166
|
+
ctx.loaderPromises,
|
|
167
|
+
ctx.actionContext,
|
|
168
|
+
ctx.interceptResult,
|
|
169
|
+
ctx.localRouteName,
|
|
170
|
+
ctx.pathname,
|
|
171
|
+
ctx.stale,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Update state with resolved segments
|
|
176
|
+
state.segments = result.segments;
|
|
177
|
+
state.matchedIds = result.matchedIds;
|
|
178
|
+
|
|
179
|
+
// Yield all resolved segments
|
|
180
|
+
for (const segment of result.segments) {
|
|
181
|
+
yield segment;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (ms) {
|
|
186
|
+
ms.metrics.push({
|
|
187
|
+
label: "pipeline:segment-resolve",
|
|
188
|
+
duration: performance.now() - pipelineStart,
|
|
189
|
+
startTime: pipelineStart - ms.requestStart,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match Pipelines
|
|
3
|
+
*
|
|
4
|
+
* Composes async generator middleware into pipelines for route matching.
|
|
5
|
+
* The pipeline transforms navigation requests into resolved UI segments.
|
|
6
|
+
*
|
|
7
|
+
* PIPELINE ARCHITECTURE OVERVIEW
|
|
8
|
+
* ==============================
|
|
9
|
+
*
|
|
10
|
+
* The router uses a pipeline of async generator middleware to process requests.
|
|
11
|
+
* Each middleware can:
|
|
12
|
+
* 1. Produce segments (yield)
|
|
13
|
+
* 2. Transform segments from upstream
|
|
14
|
+
* 3. Observe segments without modifying them
|
|
15
|
+
* 4. Trigger side effects (caching, background revalidation)
|
|
16
|
+
*
|
|
17
|
+
* REQUEST FLOW DIAGRAM
|
|
18
|
+
* ====================
|
|
19
|
+
*
|
|
20
|
+
* Navigation Request
|
|
21
|
+
* |
|
|
22
|
+
* v
|
|
23
|
+
* +------------------+
|
|
24
|
+
* | Create Context | MatchContext: routes, params, client state
|
|
25
|
+
* +------------------+
|
|
26
|
+
* |
|
|
27
|
+
* v
|
|
28
|
+
* +------------------+
|
|
29
|
+
* | Select Pipeline | Full (document) vs Partial (navigation)
|
|
30
|
+
* +------------------+
|
|
31
|
+
* |
|
|
32
|
+
* v
|
|
33
|
+
* ==================== PIPELINE EXECUTION ====================
|
|
34
|
+
* | |
|
|
35
|
+
* | empty() ─────> [1] ─────> [2] ─────> [3] ─────> [4] ───>|───> segments
|
|
36
|
+
* | | | | | | |
|
|
37
|
+
* | | cache | segment |intercept | cache | bg |
|
|
38
|
+
* | | lookup | resolve | resolve | store | reval |
|
|
39
|
+
* | |
|
|
40
|
+
* ============================================================
|
|
41
|
+
* |
|
|
42
|
+
* v
|
|
43
|
+
* +------------------+
|
|
44
|
+
* | Collect Result | Filter segments, build MatchResult
|
|
45
|
+
* +------------------+
|
|
46
|
+
* |
|
|
47
|
+
* v
|
|
48
|
+
* RSC Stream Response
|
|
49
|
+
*
|
|
50
|
+
*
|
|
51
|
+
* MIDDLEWARE EXECUTION ORDER
|
|
52
|
+
* ==========================
|
|
53
|
+
*
|
|
54
|
+
* Middleware compose in reverse order (rightmost = innermost, runs first):
|
|
55
|
+
*
|
|
56
|
+
* compose(A, B, C)(source) => source -> C -> B -> A -> output
|
|
57
|
+
*
|
|
58
|
+
* For the partial match pipeline:
|
|
59
|
+
*
|
|
60
|
+
* compose(
|
|
61
|
+
* withBackgroundRevalidation, // [5] Outermost - triggers SWR
|
|
62
|
+
* withCacheStore, // [4] Stores segments in cache
|
|
63
|
+
* withInterceptResolution, // [3] Resolves intercept segments
|
|
64
|
+
* withSegmentResolution, // [2] Resolves on cache miss
|
|
65
|
+
* withCacheLookup // [1] Innermost - checks cache first
|
|
66
|
+
* )
|
|
67
|
+
*
|
|
68
|
+
* Execution flow for cache MISS:
|
|
69
|
+
*
|
|
70
|
+
* empty() yields nothing
|
|
71
|
+
* -> [1] cache-lookup: no cache, passes through
|
|
72
|
+
* -> [2] segment-resolution: resolves segments, yields them
|
|
73
|
+
* -> [3] intercept-resolution: resolves intercepts, yields them
|
|
74
|
+
* -> [4] cache-store: observes all, stores in cache
|
|
75
|
+
* -> [5] bg-revalidation: no-op (wasn't stale)
|
|
76
|
+
* -> output: all segments
|
|
77
|
+
*
|
|
78
|
+
* Execution flow for cache HIT (stale):
|
|
79
|
+
*
|
|
80
|
+
* empty() yields nothing
|
|
81
|
+
* -> [1] cache-lookup: HIT! yields cached segments + fresh loaders
|
|
82
|
+
* -> [2] segment-resolution: sees cacheHit=true, skips
|
|
83
|
+
* -> [3] intercept-resolution: extracts intercepts from cache
|
|
84
|
+
* -> [4] cache-store: sees cacheHit=true, skips
|
|
85
|
+
* -> [5] bg-revalidation: triggers waitUntil() to revalidate
|
|
86
|
+
* -> output: cached segments + fresh loader data
|
|
87
|
+
*
|
|
88
|
+
*
|
|
89
|
+
* PIPELINE VARIANT
|
|
90
|
+
* ================
|
|
91
|
+
*
|
|
92
|
+
* createMatchPartialPipeline handles both full (document) and partial
|
|
93
|
+
* (navigation) requests. The middleware steps adapt based on ctx.isFullMatch:
|
|
94
|
+
* - cache-lookup/store work for both
|
|
95
|
+
* - background-revalidation is a no-op for full matches (no stale state)
|
|
96
|
+
* - intercept-resolution is a no-op for full matches (no previous navigation)
|
|
97
|
+
*/
|
|
98
|
+
import type { ResolvedSegment } from "../types.js";
|
|
99
|
+
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
100
|
+
import type { GeneratorMiddleware } from "./match-middleware/index.js";
|
|
101
|
+
import {
|
|
102
|
+
withBackgroundRevalidation,
|
|
103
|
+
withCacheLookup,
|
|
104
|
+
withCacheStore,
|
|
105
|
+
withInterceptResolution,
|
|
106
|
+
withSegmentResolution,
|
|
107
|
+
} from "./match-middleware/index.js";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compose multiple async generator middleware into a single middleware
|
|
111
|
+
*
|
|
112
|
+
* Middleware are applied in reverse order (rightmost runs first, innermost).
|
|
113
|
+
* For the pipeline:
|
|
114
|
+
* compose(A, B, C)(source)
|
|
115
|
+
*
|
|
116
|
+
* The flow is: source -> C -> B -> A -> output
|
|
117
|
+
* Where C is the innermost (runs first on input) and A is outermost (runs last).
|
|
118
|
+
*/
|
|
119
|
+
export function compose<T>(
|
|
120
|
+
...middleware: GeneratorMiddleware<T>[]
|
|
121
|
+
): GeneratorMiddleware<T> {
|
|
122
|
+
if (middleware.length === 0) {
|
|
123
|
+
return (source) => source;
|
|
124
|
+
}
|
|
125
|
+
if (middleware.length === 1) {
|
|
126
|
+
return middleware[0];
|
|
127
|
+
}
|
|
128
|
+
return (source) => {
|
|
129
|
+
// Apply middleware in reverse order (rightmost first)
|
|
130
|
+
return middleware.reduceRight((prev, fn) => fn(prev), source);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create an empty async generator (source for pipeline)
|
|
136
|
+
*/
|
|
137
|
+
export async function* empty<T>(): AsyncGenerator<T> {
|
|
138
|
+
// Yields nothing - used as the initial source for the pipeline
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create the match partial pipeline
|
|
143
|
+
*
|
|
144
|
+
* Pipeline order (innermost to outermost):
|
|
145
|
+
* 1. cache-lookup - Check cache first, yield cached segments if hit
|
|
146
|
+
* 2. segment-resolution - Resolve segments if cache miss
|
|
147
|
+
* 3. intercept-resolution - Resolve intercept segments
|
|
148
|
+
* 4. cache-store - Store segments in cache
|
|
149
|
+
* 5. background-revalidation - Trigger SWR if cache was stale
|
|
150
|
+
*
|
|
151
|
+
* Data flow:
|
|
152
|
+
* - empty() produces no segments
|
|
153
|
+
* - cache-lookup either yields cached segments OR passes through to segment-resolution
|
|
154
|
+
* - segment-resolution resolves fresh segments on cache miss
|
|
155
|
+
* - intercept-resolution adds intercept segments
|
|
156
|
+
* - cache-store observes and caches segments
|
|
157
|
+
* - background-revalidation triggers SWR revalidation if needed
|
|
158
|
+
*/
|
|
159
|
+
export function createMatchPartialPipeline<TEnv>(
|
|
160
|
+
ctx: MatchContext<TEnv>,
|
|
161
|
+
state: MatchPipelineState,
|
|
162
|
+
): AsyncGenerator<ResolvedSegment> {
|
|
163
|
+
// Build the middleware chain
|
|
164
|
+
const pipeline = compose<ResolvedSegment>(
|
|
165
|
+
// Outermost - observes segments and triggers background revalidation
|
|
166
|
+
withBackgroundRevalidation(ctx, state),
|
|
167
|
+
// Observes and stores segments in cache
|
|
168
|
+
withCacheStore(ctx, state),
|
|
169
|
+
// Adds intercept segments after main segments
|
|
170
|
+
withInterceptResolution(ctx, state),
|
|
171
|
+
// Resolves segments on cache miss
|
|
172
|
+
withSegmentResolution(ctx, state),
|
|
173
|
+
// Innermost - checks cache first
|
|
174
|
+
withCacheLookup(ctx, state),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Start with empty source - cache lookup or segment resolution will produce segments
|
|
178
|
+
return pipeline(empty());
|
|
179
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match Result Collection
|
|
3
|
+
*
|
|
4
|
+
* Collects segments from the pipeline and builds the final MatchResult.
|
|
5
|
+
* This is the final stage of the match pipeline.
|
|
6
|
+
*
|
|
7
|
+
* COLLECTION FLOW
|
|
8
|
+
* ===============
|
|
9
|
+
*
|
|
10
|
+
* Pipeline Generator
|
|
11
|
+
* |
|
|
12
|
+
* v
|
|
13
|
+
* +---------------------------+
|
|
14
|
+
* | collectSegments() | Drain async generator
|
|
15
|
+
* | for await...push |
|
|
16
|
+
* +---------------------------+
|
|
17
|
+
* |
|
|
18
|
+
* v
|
|
19
|
+
* +---------------------------+
|
|
20
|
+
* | buildMatchResult() | Transform to MatchResult
|
|
21
|
+
* +---------------------------+
|
|
22
|
+
* |
|
|
23
|
+
* |
|
|
24
|
+
* +-----+-----+
|
|
25
|
+
* | |
|
|
26
|
+
* Full Partial
|
|
27
|
+
* Match Match
|
|
28
|
+
* | |
|
|
29
|
+
* v v
|
|
30
|
+
* All segs Filter:
|
|
31
|
+
* rendered - null components out
|
|
32
|
+
* - keep loaders
|
|
33
|
+
* - handle intercepts
|
|
34
|
+
* | |
|
|
35
|
+
* +-----------+
|
|
36
|
+
* |
|
|
37
|
+
* v
|
|
38
|
+
* MatchResult {
|
|
39
|
+
* segments, // Segments to render
|
|
40
|
+
* matched, // All segment IDs
|
|
41
|
+
* diff, // Changed segment IDs
|
|
42
|
+
* params, // Route params
|
|
43
|
+
* slots, // Intercept slot data
|
|
44
|
+
* serverTiming // Performance metrics
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
*
|
|
48
|
+
* FULL VS PARTIAL MATCH
|
|
49
|
+
* =====================
|
|
50
|
+
*
|
|
51
|
+
* Full Match (document request):
|
|
52
|
+
* - All segments are rendered
|
|
53
|
+
* - allIds = all segment IDs
|
|
54
|
+
* - No filtering needed
|
|
55
|
+
*
|
|
56
|
+
* Partial Match (navigation):
|
|
57
|
+
* - Filter out null components (client already has them)
|
|
58
|
+
* - BUT keep loader segments (they carry data)
|
|
59
|
+
* - Handle intercepts specially (preserve client page + add modal)
|
|
60
|
+
*
|
|
61
|
+
*
|
|
62
|
+
* SEGMENT FILTERING RULES
|
|
63
|
+
* =======================
|
|
64
|
+
*
|
|
65
|
+
* For partial match, segments are filtered:
|
|
66
|
+
*
|
|
67
|
+
* Keep if:
|
|
68
|
+
* - component !== null (needs rendering)
|
|
69
|
+
* - type === "loader" (carries data even with null component)
|
|
70
|
+
*
|
|
71
|
+
* Skip if:
|
|
72
|
+
* - component === null AND type !== "loader"
|
|
73
|
+
* - (Client already has this segment's UI)
|
|
74
|
+
*
|
|
75
|
+
*
|
|
76
|
+
* INTERCEPT HANDLING
|
|
77
|
+
* ==================
|
|
78
|
+
*
|
|
79
|
+
* When intercepting (modal over current page):
|
|
80
|
+
*
|
|
81
|
+
* allIds = client segments + intercept segments
|
|
82
|
+
*
|
|
83
|
+
* This tells the client:
|
|
84
|
+
* 1. Keep your current segments
|
|
85
|
+
* 2. Add these intercept segments to the modal slot
|
|
86
|
+
*
|
|
87
|
+
* The page stays visible, modal renders on top.
|
|
88
|
+
*
|
|
89
|
+
*
|
|
90
|
+
* MATCHRESULT STRUCTURE
|
|
91
|
+
* =====================
|
|
92
|
+
*
|
|
93
|
+
* {
|
|
94
|
+
* segments: ResolvedSegment[] // Segments to serialize and render
|
|
95
|
+
* matched: string[] // All segment IDs for this route
|
|
96
|
+
* diff: string[] // Which segments changed (for client diffing)
|
|
97
|
+
* params: Record<string,string> // Route parameters
|
|
98
|
+
* slots?: Record<string, {...}> // Named slot data for intercepts
|
|
99
|
+
* serverTiming?: string // Server-Timing header value
|
|
100
|
+
* routeMiddleware?: [...] // Route middleware results
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* The client uses this to:
|
|
104
|
+
* 1. Render segments[] to the UI tree
|
|
105
|
+
* 2. Update internal state with matched[]
|
|
106
|
+
* 3. Diff against previous state with diff[]
|
|
107
|
+
* 4. Render slot content if slots present
|
|
108
|
+
*/
|
|
109
|
+
import type { MatchResult, ResolvedSegment } from "../types.js";
|
|
110
|
+
import type { MatchContext, MatchPipelineState } from "./match-context.js";
|
|
111
|
+
import { debugLog } from "./logging.js";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Collect all segments from an async generator
|
|
115
|
+
*/
|
|
116
|
+
export async function collectSegments(
|
|
117
|
+
generator: AsyncGenerator<ResolvedSegment>,
|
|
118
|
+
): Promise<ResolvedSegment[]> {
|
|
119
|
+
const segments: ResolvedSegment[] = [];
|
|
120
|
+
for await (const segment of generator) {
|
|
121
|
+
segments.push(segment);
|
|
122
|
+
}
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build the final MatchResult from collected segments and context
|
|
128
|
+
*/
|
|
129
|
+
export function buildMatchResult<TEnv>(
|
|
130
|
+
allSegments: ResolvedSegment[],
|
|
131
|
+
ctx: MatchContext<TEnv>,
|
|
132
|
+
state: MatchPipelineState,
|
|
133
|
+
): MatchResult {
|
|
134
|
+
const logPrefix = ctx.isFullMatch
|
|
135
|
+
? "[Router.match]"
|
|
136
|
+
: "[Router.matchPartial]";
|
|
137
|
+
|
|
138
|
+
let allIds: string[];
|
|
139
|
+
let segmentsToRender: ResolvedSegment[];
|
|
140
|
+
|
|
141
|
+
if (ctx.isFullMatch) {
|
|
142
|
+
// Full match (document request) - all segments are rendered
|
|
143
|
+
// Deduplicate by segment ID (defense-in-depth). The primary dedup is in
|
|
144
|
+
// resolveAllSegments, but this guards against any path that bypasses it.
|
|
145
|
+
// include() scopes can produce entries that resolve the same shared layout,
|
|
146
|
+
// and duplicate IDs change the client's React tree depth causing remounts.
|
|
147
|
+
const seen = new Set<string>();
|
|
148
|
+
segmentsToRender = [];
|
|
149
|
+
for (const s of allSegments) {
|
|
150
|
+
if (!seen.has(s.id)) {
|
|
151
|
+
seen.add(s.id);
|
|
152
|
+
segmentsToRender.push(s);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
allIds = segmentsToRender.map((s) => s.id);
|
|
156
|
+
} else {
|
|
157
|
+
// Partial match (navigation) - filter and handle intercepts
|
|
158
|
+
// When intercepting, tell browser to keep its current segments + add modal
|
|
159
|
+
// This prevents the browser from discarding the current page content
|
|
160
|
+
// If client sent empty segments (HMR recovery), use segment IDs from allSegments
|
|
161
|
+
allIds = ctx.interceptResult
|
|
162
|
+
? ctx.clientSegmentIds.length > 0
|
|
163
|
+
? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
|
|
164
|
+
: allSegments.map((s) => s.id) // Use actual segments, not matchedIds
|
|
165
|
+
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
166
|
+
|
|
167
|
+
// Deduplicate allIds (defense-in-depth for partial match path)
|
|
168
|
+
allIds = [...new Set(allIds)];
|
|
169
|
+
|
|
170
|
+
// Filter out segments with null components (client already has them)
|
|
171
|
+
// BUT always include loader segments - they carry data even with null component
|
|
172
|
+
segmentsToRender = allSegments.filter(
|
|
173
|
+
(s) => s.component !== null || s.type === "loader",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
debugLog(logPrefix, "all segments", {
|
|
178
|
+
segments: allSegments.map((s) => ({
|
|
179
|
+
id: s.id,
|
|
180
|
+
type: s.type,
|
|
181
|
+
hasComponent: s.component !== null,
|
|
182
|
+
})),
|
|
183
|
+
});
|
|
184
|
+
debugLog(logPrefix, "segments to render", {
|
|
185
|
+
segmentIds: segmentsToRender.map((s) => s.id),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
segments: segmentsToRender,
|
|
190
|
+
matched: allIds,
|
|
191
|
+
diff: segmentsToRender.map((s) => s.id),
|
|
192
|
+
params: ctx.matched.params,
|
|
193
|
+
routeName: ctx.routeKey,
|
|
194
|
+
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
195
|
+
routeMiddleware:
|
|
196
|
+
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Collect segments from pipeline and build MatchResult
|
|
202
|
+
*
|
|
203
|
+
* This is the main entry point for building the final result after
|
|
204
|
+
* the pipeline has processed all segments.
|
|
205
|
+
*/
|
|
206
|
+
export async function collectMatchResult<TEnv>(
|
|
207
|
+
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
208
|
+
ctx: MatchContext<TEnv>,
|
|
209
|
+
state: MatchPipelineState,
|
|
210
|
+
): Promise<MatchResult> {
|
|
211
|
+
const allSegments = await collectSegments(pipeline);
|
|
212
|
+
|
|
213
|
+
// Update state with collected segments if not already set
|
|
214
|
+
if (state.segments.length === 0) {
|
|
215
|
+
state.segments = allSegments;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return buildMatchResult(allSegments, ctx, state);
|
|
219
|
+
}
|