@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,566 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { collectSegments, buildMatchResult, collectMatchResult } from "../match-result";
|
|
3
|
+
import type { ResolvedSegment } from "../../types";
|
|
4
|
+
import type { MatchContext, MatchPipelineState } from "../match-context";
|
|
5
|
+
import { createPipelineState } from "../match-context";
|
|
6
|
+
|
|
7
|
+
// Mock metrics module
|
|
8
|
+
vi.mock("../metrics", () => ({
|
|
9
|
+
logMetrics: vi.fn(),
|
|
10
|
+
generateServerTiming: vi.fn(() => "metric1;dur=10"),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Helper to create async generator from array
|
|
14
|
+
async function* fromArray<T>(items: T[]): AsyncGenerator<T> {
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
yield item;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper to create a minimal mock MatchContext
|
|
21
|
+
function createMockContext(overrides: Partial<MatchContext> = {}): MatchContext {
|
|
22
|
+
return {
|
|
23
|
+
request: new Request("https://example.com/test"),
|
|
24
|
+
url: new URL("https://example.com/test"),
|
|
25
|
+
pathname: "/test",
|
|
26
|
+
env: {},
|
|
27
|
+
bindings: {},
|
|
28
|
+
clientSegmentIds: [],
|
|
29
|
+
clientSegmentSet: new Set(),
|
|
30
|
+
stale: false,
|
|
31
|
+
prevUrl: new URL("https://example.com/prev"),
|
|
32
|
+
prevParams: {},
|
|
33
|
+
prevMatch: null,
|
|
34
|
+
matched: { path: "/test", params: {}, entries: [], score: 0 },
|
|
35
|
+
manifestEntry: {} as any,
|
|
36
|
+
entries: [],
|
|
37
|
+
routeKey: "test",
|
|
38
|
+
localRouteName: "test",
|
|
39
|
+
handlerContext: {} as any,
|
|
40
|
+
loaderPromises: new Map(),
|
|
41
|
+
metricsStore: undefined,
|
|
42
|
+
Store: {},
|
|
43
|
+
interceptContextMatch: null,
|
|
44
|
+
interceptSelectorContext: { prevMatched: null, prevParams: {} },
|
|
45
|
+
isSameRouteNavigation: false,
|
|
46
|
+
interceptResult: null,
|
|
47
|
+
cacheScope: null,
|
|
48
|
+
isIntercept: false,
|
|
49
|
+
isAction: false,
|
|
50
|
+
routeMiddleware: [],
|
|
51
|
+
isFullMatch: false,
|
|
52
|
+
...overrides,
|
|
53
|
+
} as MatchContext;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Helper to create a test segment
|
|
57
|
+
function createSegment(
|
|
58
|
+
id: string,
|
|
59
|
+
options: Partial<ResolvedSegment> = {}
|
|
60
|
+
): ResolvedSegment {
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
type: "route",
|
|
64
|
+
component: `Component_${id}`,
|
|
65
|
+
params: {},
|
|
66
|
+
...options,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("match-result", () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
vi.clearAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("collectSegments()", () => {
|
|
76
|
+
it("should collect all segments from generator", async () => {
|
|
77
|
+
const segments = [createSegment("seg1"), createSegment("seg2"), createSegment("seg3")];
|
|
78
|
+
|
|
79
|
+
const result = await collectSegments(fromArray(segments));
|
|
80
|
+
|
|
81
|
+
expect(result).toHaveLength(3);
|
|
82
|
+
expect(result[0].id).toBe("seg1");
|
|
83
|
+
expect(result[1].id).toBe("seg2");
|
|
84
|
+
expect(result[2].id).toBe("seg3");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return empty array for empty generator", async () => {
|
|
88
|
+
const result = await collectSegments(fromArray([]));
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should preserve segment properties", async () => {
|
|
93
|
+
const segment = createSegment("seg1", {
|
|
94
|
+
type: "layout",
|
|
95
|
+
component: "LayoutComponent",
|
|
96
|
+
loading: "LoadingComponent",
|
|
97
|
+
params: { id: "123" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await collectSegments(fromArray([segment]));
|
|
101
|
+
|
|
102
|
+
expect(result[0]).toEqual(segment);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("buildMatchResult() - full match", () => {
|
|
107
|
+
it("should include all segments for full match", () => {
|
|
108
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
109
|
+
const state = createPipelineState();
|
|
110
|
+
const segments = [createSegment("layout"), createSegment("page")];
|
|
111
|
+
|
|
112
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
113
|
+
|
|
114
|
+
expect(result.matched).toEqual(["layout", "page"]);
|
|
115
|
+
expect(result.segments).toEqual(segments);
|
|
116
|
+
expect(result.diff).toEqual(["layout", "page"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should include params from matched route", () => {
|
|
120
|
+
const ctx = createMockContext({
|
|
121
|
+
isFullMatch: true,
|
|
122
|
+
matched: { path: "/users/:id", params: { id: "123" }, entries: [], score: 0 },
|
|
123
|
+
});
|
|
124
|
+
const state = createPipelineState();
|
|
125
|
+
const segments = [createSegment("page")];
|
|
126
|
+
|
|
127
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
128
|
+
|
|
129
|
+
expect(result.params).toEqual({ id: "123" });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("buildMatchResult() - partial match", () => {
|
|
134
|
+
it("should use matchedIds from state for partial match", () => {
|
|
135
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
136
|
+
const state = createPipelineState();
|
|
137
|
+
state.matchedIds = ["seg1", "seg2"];
|
|
138
|
+
|
|
139
|
+
const segments = [createSegment("seg1"), createSegment("seg2")];
|
|
140
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
141
|
+
|
|
142
|
+
expect(result.matched).toEqual(["seg1", "seg2"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should filter out segments with null components", () => {
|
|
146
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
147
|
+
const state = createPipelineState();
|
|
148
|
+
state.matchedIds = ["seg1", "seg2"];
|
|
149
|
+
|
|
150
|
+
const segments = [
|
|
151
|
+
createSegment("seg1", { component: null }),
|
|
152
|
+
createSegment("seg2", { component: "RealComponent" }),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
156
|
+
|
|
157
|
+
expect(result.segments).toHaveLength(1);
|
|
158
|
+
expect(result.segments[0].id).toBe("seg2");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should include loader segments even with null component", () => {
|
|
162
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
163
|
+
const state = createPipelineState();
|
|
164
|
+
state.matchedIds = ["seg1"];
|
|
165
|
+
|
|
166
|
+
const segments = [
|
|
167
|
+
createSegment("seg1", { component: null, type: "loader" }),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
171
|
+
|
|
172
|
+
expect(result.segments).toHaveLength(1);
|
|
173
|
+
expect(result.segments[0].type).toBe("loader");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should include intercept segments in matched array", () => {
|
|
177
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
178
|
+
const state = createPipelineState();
|
|
179
|
+
state.matchedIds = ["page"];
|
|
180
|
+
state.interceptSegments = [createSegment("modal")];
|
|
181
|
+
|
|
182
|
+
const segments = [createSegment("page")];
|
|
183
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
184
|
+
|
|
185
|
+
expect(result.matched).toEqual(["page", "modal"]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("buildMatchResult() - intercept handling", () => {
|
|
190
|
+
it("should use clientSegmentIds when intercepting with client segments", () => {
|
|
191
|
+
const ctx = createMockContext({
|
|
192
|
+
isFullMatch: false,
|
|
193
|
+
clientSegmentIds: ["layout", "page"],
|
|
194
|
+
interceptResult: { route: "modal", slot: "@modal" } as any,
|
|
195
|
+
});
|
|
196
|
+
const state = createPipelineState();
|
|
197
|
+
state.matchedIds = ["layout", "page"];
|
|
198
|
+
state.interceptSegments = [createSegment("modal-content")];
|
|
199
|
+
|
|
200
|
+
const segments = [createSegment("page")];
|
|
201
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
202
|
+
|
|
203
|
+
// Should include client segments + intercept segments
|
|
204
|
+
expect(result.matched).toEqual(["layout", "page", "modal-content"]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should use segment IDs when intercepting without client segments (HMR)", () => {
|
|
208
|
+
const ctx = createMockContext({
|
|
209
|
+
isFullMatch: false,
|
|
210
|
+
clientSegmentIds: [],
|
|
211
|
+
interceptResult: { route: "modal", slot: "@modal" } as any,
|
|
212
|
+
});
|
|
213
|
+
const state = createPipelineState();
|
|
214
|
+
state.interceptSegments = [createSegment("modal-content")];
|
|
215
|
+
|
|
216
|
+
const segments = [createSegment("layout"), createSegment("page")];
|
|
217
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
218
|
+
|
|
219
|
+
// Should use actual segment IDs when client sent empty
|
|
220
|
+
expect(result.matched).toEqual(["layout", "page"]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("buildMatchResult() - slots", () => {
|
|
225
|
+
it("should include slots when present", () => {
|
|
226
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
227
|
+
const state = createPipelineState();
|
|
228
|
+
state.slots = {
|
|
229
|
+
"@modal": {
|
|
230
|
+
active: true,
|
|
231
|
+
segments: [createSegment("modal-seg")],
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const segments = [createSegment("page")];
|
|
236
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
237
|
+
|
|
238
|
+
expect(result.slots).toBeDefined();
|
|
239
|
+
expect(result.slots!["@modal"].active).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should not include slots when empty", () => {
|
|
243
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
244
|
+
const state = createPipelineState();
|
|
245
|
+
|
|
246
|
+
const segments = [createSegment("page")];
|
|
247
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
248
|
+
|
|
249
|
+
expect(result.slots).toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("buildMatchResult() - route middleware", () => {
|
|
254
|
+
it("should include routeMiddleware when present", () => {
|
|
255
|
+
const ctx = createMockContext({
|
|
256
|
+
isFullMatch: true,
|
|
257
|
+
routeMiddleware: [{ handler: "auth", params: {} }],
|
|
258
|
+
});
|
|
259
|
+
const state = createPipelineState();
|
|
260
|
+
|
|
261
|
+
const segments = [createSegment("page")];
|
|
262
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
263
|
+
|
|
264
|
+
expect(result.routeMiddleware).toHaveLength(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should not include routeMiddleware when empty", () => {
|
|
268
|
+
const ctx = createMockContext({
|
|
269
|
+
isFullMatch: true,
|
|
270
|
+
routeMiddleware: [],
|
|
271
|
+
});
|
|
272
|
+
const state = createPipelineState();
|
|
273
|
+
|
|
274
|
+
const segments = [createSegment("page")];
|
|
275
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
276
|
+
|
|
277
|
+
expect(result.routeMiddleware).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("buildMatchResult() - metrics", () => {
|
|
282
|
+
it("should generate server timing when metricsStore is present", () => {
|
|
283
|
+
const ctx = createMockContext({
|
|
284
|
+
isFullMatch: true,
|
|
285
|
+
metricsStore: {} as any,
|
|
286
|
+
});
|
|
287
|
+
const state = createPipelineState();
|
|
288
|
+
|
|
289
|
+
const segments = [createSegment("page")];
|
|
290
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
291
|
+
|
|
292
|
+
expect(result.serverTiming).toBe("metric1;dur=10");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should not include server timing without metricsStore", () => {
|
|
296
|
+
const ctx = createMockContext({
|
|
297
|
+
isFullMatch: true,
|
|
298
|
+
metricsStore: undefined,
|
|
299
|
+
});
|
|
300
|
+
const state = createPipelineState();
|
|
301
|
+
|
|
302
|
+
const segments = [createSegment("page")];
|
|
303
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
304
|
+
|
|
305
|
+
expect(result.serverTiming).toBeUndefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("collectMatchResult()", () => {
|
|
310
|
+
it("should collect segments and build result", async () => {
|
|
311
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
312
|
+
const state = createPipelineState();
|
|
313
|
+
const segments = [createSegment("seg1"), createSegment("seg2")];
|
|
314
|
+
|
|
315
|
+
const result = await collectMatchResult(fromArray(segments), ctx, state);
|
|
316
|
+
|
|
317
|
+
expect(result.matched).toEqual(["seg1", "seg2"]);
|
|
318
|
+
expect(result.segments).toHaveLength(2);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should update state.segments if not already set", async () => {
|
|
322
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
323
|
+
const state = createPipelineState();
|
|
324
|
+
const segments = [createSegment("seg1")];
|
|
325
|
+
|
|
326
|
+
await collectMatchResult(fromArray(segments), ctx, state);
|
|
327
|
+
|
|
328
|
+
expect(state.segments).toHaveLength(1);
|
|
329
|
+
expect(state.segments[0].id).toBe("seg1");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should not overwrite state.segments if already set", async () => {
|
|
333
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
334
|
+
const state = createPipelineState();
|
|
335
|
+
state.segments = [createSegment("existing")];
|
|
336
|
+
|
|
337
|
+
const segments = [createSegment("new")];
|
|
338
|
+
await collectMatchResult(fromArray(segments), ctx, state);
|
|
339
|
+
|
|
340
|
+
expect(state.segments).toHaveLength(1);
|
|
341
|
+
expect(state.segments[0].id).toBe("existing");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("edge cases", () => {
|
|
346
|
+
it("should handle large number of segments", async () => {
|
|
347
|
+
const segments = Array.from({ length: 100 }, (_, i) =>
|
|
348
|
+
createSegment(`seg${i}`)
|
|
349
|
+
);
|
|
350
|
+
const result = await collectSegments(fromArray(segments));
|
|
351
|
+
|
|
352
|
+
expect(result).toHaveLength(100);
|
|
353
|
+
expect(result[0].id).toBe("seg0");
|
|
354
|
+
expect(result[99].id).toBe("seg99");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should handle mixed segment types", () => {
|
|
358
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
359
|
+
const state = createPipelineState();
|
|
360
|
+
const segments = [
|
|
361
|
+
createSegment("layout", { type: "layout" }),
|
|
362
|
+
createSegment("page", { type: "route" }),
|
|
363
|
+
createSegment("loader1", { type: "loader" }),
|
|
364
|
+
createSegment("parallel", { type: "parallel", slot: "@sidebar" }),
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
368
|
+
|
|
369
|
+
expect(result.matched).toEqual(["layout", "page", "loader1", "parallel"]);
|
|
370
|
+
expect(result.segments).toHaveLength(4);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should handle segments with complex params", () => {
|
|
374
|
+
const ctx = createMockContext({
|
|
375
|
+
isFullMatch: true,
|
|
376
|
+
matched: {
|
|
377
|
+
path: "/users/:userId/posts/:postId",
|
|
378
|
+
params: { userId: "123", postId: "456" },
|
|
379
|
+
entries: [],
|
|
380
|
+
score: 0,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
const state = createPipelineState();
|
|
384
|
+
const segments = [createSegment("page", { params: { userId: "123", postId: "456" } })];
|
|
385
|
+
|
|
386
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
387
|
+
|
|
388
|
+
expect(result.params).toEqual({ userId: "123", postId: "456" });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should handle multiple intercept segments", () => {
|
|
392
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
393
|
+
const state = createPipelineState();
|
|
394
|
+
state.matchedIds = ["page"];
|
|
395
|
+
state.interceptSegments = [
|
|
396
|
+
createSegment("modal1"),
|
|
397
|
+
createSegment("modal2"),
|
|
398
|
+
createSegment("modal3"),
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
const segments = [createSegment("page")];
|
|
402
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
403
|
+
|
|
404
|
+
expect(result.matched).toEqual(["page", "modal1", "modal2", "modal3"]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("should handle multiple slots", () => {
|
|
408
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
409
|
+
const state = createPipelineState();
|
|
410
|
+
state.slots = {
|
|
411
|
+
"@modal": {
|
|
412
|
+
active: true,
|
|
413
|
+
segments: [createSegment("modal-content")],
|
|
414
|
+
},
|
|
415
|
+
"@sidebar": {
|
|
416
|
+
active: true,
|
|
417
|
+
segments: [createSegment("sidebar-content")],
|
|
418
|
+
},
|
|
419
|
+
"@drawer": {
|
|
420
|
+
active: false,
|
|
421
|
+
segments: [],
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const segments = [createSegment("page")];
|
|
426
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
427
|
+
|
|
428
|
+
expect(result.slots).toBeDefined();
|
|
429
|
+
expect(Object.keys(result.slots!)).toHaveLength(3);
|
|
430
|
+
expect(result.slots!["@modal"].active).toBe(true);
|
|
431
|
+
expect(result.slots!["@sidebar"].active).toBe(true);
|
|
432
|
+
expect(result.slots!["@drawer"].active).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("should handle segments with loading components", () => {
|
|
436
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
437
|
+
const state = createPipelineState();
|
|
438
|
+
const segments = [
|
|
439
|
+
createSegment("page", {
|
|
440
|
+
loading: "LoadingSpinner",
|
|
441
|
+
}),
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
445
|
+
|
|
446
|
+
expect(result.segments[0].loading).toBe("LoadingSpinner");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should handle segments with layout components", () => {
|
|
450
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
451
|
+
const state = createPipelineState();
|
|
452
|
+
const segments = [
|
|
453
|
+
createSegment("layout", {
|
|
454
|
+
type: "layout",
|
|
455
|
+
layout: "LayoutWrapper",
|
|
456
|
+
}),
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
460
|
+
|
|
461
|
+
expect(result.segments[0].layout).toBe("LayoutWrapper");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should handle partial match with all null components", () => {
|
|
465
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
466
|
+
const state = createPipelineState();
|
|
467
|
+
state.matchedIds = ["seg1", "seg2"];
|
|
468
|
+
|
|
469
|
+
const segments = [
|
|
470
|
+
createSegment("seg1", { component: null }),
|
|
471
|
+
createSegment("seg2", { component: null }),
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
475
|
+
|
|
476
|
+
expect(result.matched).toEqual(["seg1", "seg2"]);
|
|
477
|
+
expect(result.segments).toHaveLength(0); // All filtered out
|
|
478
|
+
expect(result.diff).toEqual([]); // No segments to render
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should handle empty segment list", () => {
|
|
482
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
483
|
+
const state = createPipelineState();
|
|
484
|
+
|
|
485
|
+
const result = buildMatchResult([], ctx, state);
|
|
486
|
+
|
|
487
|
+
expect(result.matched).toEqual([]);
|
|
488
|
+
expect(result.segments).toEqual([]);
|
|
489
|
+
expect(result.diff).toEqual([]);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should handle segments with namespace", () => {
|
|
493
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
494
|
+
const state = createPipelineState();
|
|
495
|
+
const segments = [
|
|
496
|
+
createSegment("intercept:modal:content", {
|
|
497
|
+
namespace: "intercept:modal",
|
|
498
|
+
}),
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
502
|
+
|
|
503
|
+
expect(result.segments[0].namespace).toBe("intercept:modal");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should handle mixed loaders and routes in partial match", () => {
|
|
507
|
+
const ctx = createMockContext({ isFullMatch: false });
|
|
508
|
+
const state = createPipelineState();
|
|
509
|
+
state.matchedIds = ["route1", "route2", "loader1", "loader2"];
|
|
510
|
+
|
|
511
|
+
const segments = [
|
|
512
|
+
createSegment("route1", { component: null, type: "route" }),
|
|
513
|
+
createSegment("route2", { component: "RouteComponent", type: "route" }),
|
|
514
|
+
createSegment("loader1", { component: null, type: "loader" }),
|
|
515
|
+
createSegment("loader2", { component: null, type: "loader" }),
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
519
|
+
|
|
520
|
+
// Loaders should be included even with null component
|
|
521
|
+
// Routes with null component should be filtered
|
|
522
|
+
expect(result.segments).toHaveLength(3); // route2, loader1, loader2
|
|
523
|
+
expect(result.segments.map((s) => s.id)).toEqual([
|
|
524
|
+
"route2",
|
|
525
|
+
"loader1",
|
|
526
|
+
"loader2",
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should preserve segment index", () => {
|
|
531
|
+
const ctx = createMockContext({ isFullMatch: true });
|
|
532
|
+
const state = createPipelineState();
|
|
533
|
+
const segments = [
|
|
534
|
+
createSegment("seg0", { index: 0 }),
|
|
535
|
+
createSegment("seg1", { index: 1 }),
|
|
536
|
+
createSegment("seg2", { index: 2 }),
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
540
|
+
|
|
541
|
+
expect(result.segments[0].index).toBe(0);
|
|
542
|
+
expect(result.segments[1].index).toBe(1);
|
|
543
|
+
expect(result.segments[2].index).toBe(2);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("should handle multiple route middleware", () => {
|
|
547
|
+
const ctx = createMockContext({
|
|
548
|
+
isFullMatch: true,
|
|
549
|
+
routeMiddleware: [
|
|
550
|
+
{ handler: "auth", params: {} },
|
|
551
|
+
{ handler: "logger", params: { level: "debug" } },
|
|
552
|
+
{ handler: "rateLimit", params: { max: "100" } },
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
const state = createPipelineState();
|
|
556
|
+
const segments = [createSegment("page")];
|
|
557
|
+
|
|
558
|
+
const result = buildMatchResult(segments, ctx, state);
|
|
559
|
+
|
|
560
|
+
expect(result.routeMiddleware).toHaveLength(3);
|
|
561
|
+
expect(result.routeMiddleware![0].handler).toBe("auth");
|
|
562
|
+
expect(result.routeMiddleware![1].handler).toBe("logger");
|
|
563
|
+
expect(result.routeMiddleware![2].handler).toBe("rateLimit");
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|