@ivogt/rsc-router 0.0.0-experimental.1
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 +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -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 +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,212 @@
|
|
|
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 { generateServerTiming, logMetrics } from "./metrics.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 ? "[Router.match]" : "[Router.matchPartial]";
|
|
135
|
+
|
|
136
|
+
let allIds: string[];
|
|
137
|
+
let segmentsToRender: ResolvedSegment[];
|
|
138
|
+
|
|
139
|
+
if (ctx.isFullMatch) {
|
|
140
|
+
// Full match (document request) - all segments are rendered
|
|
141
|
+
allIds = allSegments.map((s) => s.id);
|
|
142
|
+
segmentsToRender = allSegments;
|
|
143
|
+
} else {
|
|
144
|
+
// Partial match (navigation) - filter and handle intercepts
|
|
145
|
+
// When intercepting, tell browser to keep its current segments + add modal
|
|
146
|
+
// This prevents the browser from discarding the current page content
|
|
147
|
+
// If client sent empty segments (HMR recovery), use segment IDs from allSegments
|
|
148
|
+
allIds = ctx.interceptResult
|
|
149
|
+
? ctx.clientSegmentIds.length > 0
|
|
150
|
+
? [...ctx.clientSegmentIds, ...state.interceptSegments.map((s) => s.id)]
|
|
151
|
+
: allSegments.map((s) => s.id) // Use actual segments, not matchedIds
|
|
152
|
+
: [...state.matchedIds, ...state.interceptSegments.map((s) => s.id)];
|
|
153
|
+
|
|
154
|
+
// Filter out segments with null components (client already has them)
|
|
155
|
+
// BUT always include loader segments - they carry data even with null component
|
|
156
|
+
segmentsToRender = allSegments.filter(
|
|
157
|
+
(s) => s.component !== null || s.type === "loader"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (process.env.NODE_ENV === "development") {
|
|
162
|
+
console.log(
|
|
163
|
+
`${logPrefix} All segments:`,
|
|
164
|
+
allSegments
|
|
165
|
+
.map((s) => `${s.id}(${s.type}, component=${s.component !== null})`)
|
|
166
|
+
.join(", ")
|
|
167
|
+
);
|
|
168
|
+
console.log(
|
|
169
|
+
`${logPrefix} Segments to render:`,
|
|
170
|
+
segmentsToRender.map((s) => s.id).join(", ")
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Output metrics if enabled
|
|
175
|
+
let serverTiming: string | undefined;
|
|
176
|
+
if (ctx.metricsStore) {
|
|
177
|
+
logMetrics(ctx.request.method, ctx.pathname, ctx.metricsStore);
|
|
178
|
+
serverTiming = generateServerTiming(ctx.metricsStore);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
segments: segmentsToRender,
|
|
183
|
+
matched: allIds,
|
|
184
|
+
diff: segmentsToRender.map((s) => s.id),
|
|
185
|
+
params: ctx.matched.params,
|
|
186
|
+
serverTiming,
|
|
187
|
+
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
|
188
|
+
routeMiddleware:
|
|
189
|
+
ctx.routeMiddleware.length > 0 ? ctx.routeMiddleware : undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Collect segments from pipeline and build MatchResult
|
|
195
|
+
*
|
|
196
|
+
* This is the main entry point for building the final result after
|
|
197
|
+
* the pipeline has processed all segments.
|
|
198
|
+
*/
|
|
199
|
+
export async function collectMatchResult<TEnv>(
|
|
200
|
+
pipeline: AsyncGenerator<ResolvedSegment>,
|
|
201
|
+
ctx: MatchContext<TEnv>,
|
|
202
|
+
state: MatchPipelineState
|
|
203
|
+
): Promise<MatchResult> {
|
|
204
|
+
const allSegments = await collectSegments(pipeline);
|
|
205
|
+
|
|
206
|
+
// Update state with collected segments if not already set
|
|
207
|
+
if (state.segments.length === 0) {
|
|
208
|
+
state.segments = allSegments;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return buildMatchResult(allSegments, ctx, state);
|
|
212
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Metrics Utilities
|
|
3
|
+
*
|
|
4
|
+
* Performance metrics collection and reporting for RSC Router.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MetricsStore } from "../server/context";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a metrics store for the request if debugPerformance is enabled
|
|
11
|
+
*/
|
|
12
|
+
export function createMetricsStore(
|
|
13
|
+
debugPerformance: boolean
|
|
14
|
+
): MetricsStore | undefined {
|
|
15
|
+
if (!debugPerformance) return undefined;
|
|
16
|
+
return {
|
|
17
|
+
enabled: true,
|
|
18
|
+
requestStart: performance.now(),
|
|
19
|
+
metrics: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Log metrics to console in a formatted way
|
|
25
|
+
*/
|
|
26
|
+
export function logMetrics(
|
|
27
|
+
method: string,
|
|
28
|
+
pathname: string,
|
|
29
|
+
metricsStore: MetricsStore
|
|
30
|
+
): void {
|
|
31
|
+
const total = performance.now() - metricsStore.requestStart;
|
|
32
|
+
|
|
33
|
+
// Find max label length for alignment
|
|
34
|
+
const maxLabelLen = Math.max(
|
|
35
|
+
...metricsStore.metrics.map((m) => m.label.length),
|
|
36
|
+
20
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
console.log(`[RSC Perf] ${method} ${pathname} (${total.toFixed(1)}ms)`);
|
|
40
|
+
|
|
41
|
+
for (const m of metricsStore.metrics) {
|
|
42
|
+
const paddedLabel = m.label.padEnd(maxLabelLen);
|
|
43
|
+
console.log(` ${paddedLabel} ${m.duration.toFixed(1)}ms`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate Server-Timing header value from metrics
|
|
49
|
+
* Format: metric-name;dur=X.XX
|
|
50
|
+
*/
|
|
51
|
+
export function generateServerTiming(metricsStore: MetricsStore): string {
|
|
52
|
+
return metricsStore.metrics
|
|
53
|
+
.map((m) => {
|
|
54
|
+
// Convert label to valid Server-Timing name (alphanumeric and hyphens)
|
|
55
|
+
const name = m.label
|
|
56
|
+
.replace(/:/g, "-")
|
|
57
|
+
.replace(/[^a-zA-Z0-9-]/g, "")
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
return `${name};dur=${m.duration.toFixed(2)}`;
|
|
60
|
+
})
|
|
61
|
+
.join(", ");
|
|
62
|
+
}
|