fibrae 0.1.0
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/dist/components.d.ts +40 -0
- package/dist/components.js +63 -0
- package/dist/components.js.map +1 -0
- package/dist/core.d.ts +25 -0
- package/dist/core.js +46 -0
- package/dist/core.js.map +1 -0
- package/dist/dom.d.ts +16 -0
- package/dist/dom.js +67 -0
- package/dist/dom.js.map +1 -0
- package/dist/fiber-render.d.ts +33 -0
- package/dist/fiber-render.js +1069 -0
- package/dist/fiber-render.js.map +1 -0
- package/dist/h.d.ts +19 -0
- package/dist/h.js +26 -0
- package/dist/h.js.map +1 -0
- package/dist/hydration.d.ts +30 -0
- package/dist/hydration.js +375 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-runtime/index.d.ts +29 -0
- package/dist/jsx-runtime/index.js +61 -0
- package/dist/jsx-runtime/index.js.map +1 -0
- package/dist/render.d.ts +19 -0
- package/dist/render.js +325 -0
- package/dist/render.js.map +1 -0
- package/dist/router/History.d.ts +129 -0
- package/dist/router/History.js +241 -0
- package/dist/router/History.js.map +1 -0
- package/dist/router/Link.d.ts +52 -0
- package/dist/router/Link.js +131 -0
- package/dist/router/Link.js.map +1 -0
- package/dist/router/Navigator.d.ts +108 -0
- package/dist/router/Navigator.js +225 -0
- package/dist/router/Navigator.js.map +1 -0
- package/dist/router/Route.d.ts +65 -0
- package/dist/router/Route.js +143 -0
- package/dist/router/Route.js.map +1 -0
- package/dist/router/Router.d.ts +167 -0
- package/dist/router/Router.js +328 -0
- package/dist/router/Router.js.map +1 -0
- package/dist/router/RouterBuilder.d.ts +128 -0
- package/dist/router/RouterBuilder.js +112 -0
- package/dist/router/RouterBuilder.js.map +1 -0
- package/dist/router/RouterOutlet.d.ts +57 -0
- package/dist/router/RouterOutlet.js +132 -0
- package/dist/router/RouterOutlet.js.map +1 -0
- package/dist/router/RouterState.d.ts +102 -0
- package/dist/router/RouterState.js +94 -0
- package/dist/router/RouterState.js.map +1 -0
- package/dist/router/index.d.ts +28 -0
- package/dist/router/index.js +31 -0
- package/dist/router/index.js.map +1 -0
- package/dist/runtime.d.ts +55 -0
- package/dist/runtime.js +68 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scope-utils.d.ts +14 -0
- package/dist/scope-utils.js +29 -0
- package/dist/scope-utils.js.map +1 -0
- package/dist/server.d.ts +112 -0
- package/dist/server.js +313 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.d.ts +136 -0
- package/dist/shared.js +53 -0
- package/dist/shared.js.map +1 -0
- package/dist/tracking.d.ts +23 -0
- package/dist/tracking.js +53 -0
- package/dist/tracking.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router module for organizing and matching routes.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Effect HttpApiGroup/HttpApi patterns:
|
|
5
|
+
* - Router.group("name") creates a group for organizing routes
|
|
6
|
+
* - Router.make("name") creates the top-level router
|
|
7
|
+
* - Routes are added via .add(route)
|
|
8
|
+
* - Router holds the complete route tree for efficient matching
|
|
9
|
+
*
|
|
10
|
+
* SSR Integration:
|
|
11
|
+
* - Router.serverLayer() - For SSR rendering with loaders
|
|
12
|
+
* - Router.browserLayer() - For client hydration with initial state
|
|
13
|
+
*/
|
|
14
|
+
import * as Option from "effect/Option";
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Layer from "effect/Layer";
|
|
17
|
+
import * as Context from "effect/Context";
|
|
18
|
+
import { Registry as AtomRegistry } from "@effect-atom/atom";
|
|
19
|
+
import { History, MemoryHistoryLive } from "./History.js";
|
|
20
|
+
import { Navigator, NavigatorLive } from "./Navigator.js";
|
|
21
|
+
import { RouterHandlers } from "./RouterBuilder.js";
|
|
22
|
+
import { RouterStateAtom } from "./RouterState.js";
|
|
23
|
+
/**
|
|
24
|
+
* Create a route group with the given name.
|
|
25
|
+
* Routes are added via group.add(route).
|
|
26
|
+
*/
|
|
27
|
+
export function group(name) {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
routes: [],
|
|
31
|
+
add(route) {
|
|
32
|
+
return {
|
|
33
|
+
...this,
|
|
34
|
+
routes: [...this.routes, route],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a router with the given name.
|
|
41
|
+
* Groups are added via router.add(group).
|
|
42
|
+
*/
|
|
43
|
+
export function make(name) {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
groups: [],
|
|
47
|
+
add(g) {
|
|
48
|
+
return {
|
|
49
|
+
...this,
|
|
50
|
+
groups: [...this.groups, g],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
matchRoute(pathname) {
|
|
54
|
+
// Try to match against each route in each group
|
|
55
|
+
for (const g of this.groups) {
|
|
56
|
+
for (const route of g.routes) {
|
|
57
|
+
const match = route.match(pathname);
|
|
58
|
+
if (Option.isSome(match)) {
|
|
59
|
+
return Option.some({
|
|
60
|
+
groupName: g.name,
|
|
61
|
+
route,
|
|
62
|
+
params: match.value,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Option.none();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Service tag for the current route's rendered element.
|
|
73
|
+
* Used by SSR to provide the matched route's component.
|
|
74
|
+
*/
|
|
75
|
+
export class CurrentRouteElement extends Context.Tag("fibrae/CurrentRouteElement")() {
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse search params from URL search string.
|
|
79
|
+
*/
|
|
80
|
+
function parseSearchParams(search) {
|
|
81
|
+
const params = {};
|
|
82
|
+
const searchString = search.startsWith("?") ? search.slice(1) : search;
|
|
83
|
+
if (!searchString)
|
|
84
|
+
return params;
|
|
85
|
+
const searchParams = new URLSearchParams(searchString);
|
|
86
|
+
searchParams.forEach((value, key) => {
|
|
87
|
+
params[key] = value;
|
|
88
|
+
});
|
|
89
|
+
return params;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Strip basePath prefix from pathname for route matching.
|
|
93
|
+
*/
|
|
94
|
+
function stripBasePath(pathname, basePath) {
|
|
95
|
+
if (!basePath || basePath === "/") {
|
|
96
|
+
return pathname;
|
|
97
|
+
}
|
|
98
|
+
// Normalize: remove trailing slash from basePath
|
|
99
|
+
const normalizedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
100
|
+
if (pathname.startsWith(normalizedBase)) {
|
|
101
|
+
const stripped = pathname.slice(normalizedBase.length);
|
|
102
|
+
// Ensure we return "/" not "" for root
|
|
103
|
+
return stripped || "/";
|
|
104
|
+
}
|
|
105
|
+
return pathname;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a server-side layer for SSR rendering.
|
|
109
|
+
*
|
|
110
|
+
* This layer:
|
|
111
|
+
* 1. Matches the pathname against the router
|
|
112
|
+
* 2. Runs the matched route's loader
|
|
113
|
+
* 3. Renders the component with loader data
|
|
114
|
+
* 4. Provides the rendered element and dehydrated state
|
|
115
|
+
*
|
|
116
|
+
* Usage in SSR:
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const serverLayer = Router.serverLayer({
|
|
119
|
+
* router: AppRouter,
|
|
120
|
+
* pathname: "/posts/42",
|
|
121
|
+
* search: "?sort=date",
|
|
122
|
+
* basePath: "/ssr/router"
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* const { element, dehydratedState } = yield* Router.CurrentRouteElement;
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function serverLayer(options) {
|
|
129
|
+
const { router, pathname, search = "", basePath = "" } = options;
|
|
130
|
+
const searchParams = parseSearchParams(search);
|
|
131
|
+
// Strip basePath from pathname for route matching
|
|
132
|
+
const matchPathname = stripBasePath(pathname, basePath);
|
|
133
|
+
// Create memory history for SSR (static, no navigation)
|
|
134
|
+
const historyLayer = MemoryHistoryLive({
|
|
135
|
+
initialPathname: pathname,
|
|
136
|
+
initialSearch: search ? `?${search.replace(/^\?/, "")}` : "",
|
|
137
|
+
});
|
|
138
|
+
// Create navigator layer with basePath - needs History and AtomRegistry
|
|
139
|
+
// We provide History here, AtomRegistry comes from outside
|
|
140
|
+
const navigatorLayer = Layer.provideMerge(NavigatorLive(router, { basePath }), historyLayer);
|
|
141
|
+
// Create route element layer
|
|
142
|
+
const routeElementLayer = Layer.effect(CurrentRouteElement, Effect.gen(function* () {
|
|
143
|
+
const routerHandlers = yield* RouterHandlers;
|
|
144
|
+
const registry = yield* AtomRegistry.AtomRegistry;
|
|
145
|
+
// Match route using stripped pathname
|
|
146
|
+
const match = router.matchRoute(matchPathname);
|
|
147
|
+
if (Option.isNone(match)) {
|
|
148
|
+
return yield* Effect.fail(new Error(`No route matched pathname: ${matchPathname}`));
|
|
149
|
+
}
|
|
150
|
+
const { route, params } = match.value;
|
|
151
|
+
// Get handler for this route
|
|
152
|
+
const handler = routerHandlers.getHandler(route.name);
|
|
153
|
+
if (Option.isNone(handler)) {
|
|
154
|
+
return yield* Effect.fail(new Error(`No handler found for route: ${route.name}`));
|
|
155
|
+
}
|
|
156
|
+
// Execute loader and render component
|
|
157
|
+
const loaderCtx = { path: params, searchParams };
|
|
158
|
+
const loaderData = yield* handler.value.loader(loaderCtx);
|
|
159
|
+
const element = handler.value.component({
|
|
160
|
+
loaderData,
|
|
161
|
+
path: params,
|
|
162
|
+
searchParams,
|
|
163
|
+
});
|
|
164
|
+
const state = {
|
|
165
|
+
routeName: route.name,
|
|
166
|
+
params,
|
|
167
|
+
searchParams,
|
|
168
|
+
loaderData,
|
|
169
|
+
};
|
|
170
|
+
// Set RouterStateAtom so it gets included in dehydrated state
|
|
171
|
+
registry.set(RouterStateAtom, Option.some(state));
|
|
172
|
+
return { element, state };
|
|
173
|
+
}));
|
|
174
|
+
return Layer.mergeAll(historyLayer, navigatorLayer, routeElementLayer);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create a browser layer for client-side hydration.
|
|
178
|
+
*
|
|
179
|
+
* This layer:
|
|
180
|
+
* 1. Sets up browser history with popstate listener
|
|
181
|
+
* 2. Checks RouterStateAtom for hydrated SSR state
|
|
182
|
+
* 3. If hydrated, uses that for initial render (skips loader)
|
|
183
|
+
* 4. Provides Navigator for subsequent navigation
|
|
184
|
+
*
|
|
185
|
+
* SSR hydration works automatically via atom hydration - no need to
|
|
186
|
+
* pass initialState manually. The RouterStateAtom is hydrated from
|
|
187
|
+
* __FIBRAE_STATE__ before this layer is created.
|
|
188
|
+
*
|
|
189
|
+
* Usage in client:
|
|
190
|
+
* ```typescript
|
|
191
|
+
* // Hydrate atoms first (includes RouterStateAtom)
|
|
192
|
+
* hydrate(container, app, window.__FIBRAE_STATE__);
|
|
193
|
+
*
|
|
194
|
+
* // Browser layer reads from hydrated RouterStateAtom
|
|
195
|
+
* const browserLayer = Router.browserLayer({
|
|
196
|
+
* router: AppRouter,
|
|
197
|
+
* basePath: "/ssr/router"
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function browserLayer(options) {
|
|
202
|
+
const { router, basePath = "" } = options;
|
|
203
|
+
// Import BrowserHistoryLive dynamically to avoid server-side issues
|
|
204
|
+
// For now, we'll use a scoped effect to create it
|
|
205
|
+
const historyLayer = Layer.scoped(History, Effect.gen(function* () {
|
|
206
|
+
const registry = yield* AtomRegistry.AtomRegistry;
|
|
207
|
+
const { Atom } = yield* Effect.promise(() => import("@effect-atom/atom"));
|
|
208
|
+
// Create location atom with initial browser location
|
|
209
|
+
const getBrowserLocation = () => ({
|
|
210
|
+
pathname: window.location.pathname,
|
|
211
|
+
search: window.location.search,
|
|
212
|
+
hash: window.location.hash,
|
|
213
|
+
state: window.history.state,
|
|
214
|
+
});
|
|
215
|
+
const locationAtom = Atom.make(getBrowserLocation());
|
|
216
|
+
// Subscribe to popstate for browser back/forward
|
|
217
|
+
const handlePopState = () => {
|
|
218
|
+
registry.set(locationAtom, getBrowserLocation());
|
|
219
|
+
};
|
|
220
|
+
window.addEventListener("popstate", handlePopState);
|
|
221
|
+
// Cleanup on scope close
|
|
222
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
223
|
+
window.removeEventListener("popstate", handlePopState);
|
|
224
|
+
}));
|
|
225
|
+
// Track history index for canGoBack
|
|
226
|
+
let historyIndex = 0;
|
|
227
|
+
const parseLocation = (href, state) => {
|
|
228
|
+
const url = href.startsWith("/")
|
|
229
|
+
? new URL(href, "http://localhost")
|
|
230
|
+
: new URL(href);
|
|
231
|
+
return {
|
|
232
|
+
pathname: url.pathname,
|
|
233
|
+
search: url.search,
|
|
234
|
+
hash: url.hash,
|
|
235
|
+
state,
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
location: locationAtom,
|
|
240
|
+
push: (path, state) => Effect.sync(() => {
|
|
241
|
+
const location = parseLocation(path, state);
|
|
242
|
+
const fullPath = `${location.pathname}${location.search}${location.hash}`;
|
|
243
|
+
window.history.pushState(state, "", fullPath);
|
|
244
|
+
historyIndex++;
|
|
245
|
+
registry.set(locationAtom, { ...location, state });
|
|
246
|
+
}),
|
|
247
|
+
replace: (path, state) => Effect.sync(() => {
|
|
248
|
+
const location = parseLocation(path, state);
|
|
249
|
+
const fullPath = `${location.pathname}${location.search}${location.hash}`;
|
|
250
|
+
window.history.replaceState(state, "", fullPath);
|
|
251
|
+
registry.set(locationAtom, { ...location, state });
|
|
252
|
+
}),
|
|
253
|
+
back: Effect.sync(() => {
|
|
254
|
+
window.history.back();
|
|
255
|
+
}),
|
|
256
|
+
forward: Effect.sync(() => {
|
|
257
|
+
window.history.forward();
|
|
258
|
+
}),
|
|
259
|
+
go: (n) => Effect.sync(() => {
|
|
260
|
+
window.history.go(n);
|
|
261
|
+
}),
|
|
262
|
+
canGoBack: Effect.sync(() => historyIndex > 0),
|
|
263
|
+
};
|
|
264
|
+
}));
|
|
265
|
+
const navigatorLayer = NavigatorLive(router, { basePath });
|
|
266
|
+
// Create route element layer - checks RouterStateAtom for hydrated state
|
|
267
|
+
// If hydrated, uses that; otherwise matches and runs loader
|
|
268
|
+
const routeElementLayer = Layer.effect(CurrentRouteElement, Effect.gen(function* () {
|
|
269
|
+
const routerHandlers = yield* RouterHandlers;
|
|
270
|
+
const registry = yield* AtomRegistry.AtomRegistry;
|
|
271
|
+
// Check if RouterStateAtom was hydrated from SSR
|
|
272
|
+
const hydratedState = registry.get(RouterStateAtom);
|
|
273
|
+
if (Option.isSome(hydratedState)) {
|
|
274
|
+
// Hydration mode: reuse SSR data from RouterStateAtom
|
|
275
|
+
const state = hydratedState.value;
|
|
276
|
+
const handler = routerHandlers.getHandler(state.routeName);
|
|
277
|
+
if (Option.isNone(handler)) {
|
|
278
|
+
return yield* Effect.fail(new Error(`No handler found for route: ${state.routeName}`));
|
|
279
|
+
}
|
|
280
|
+
// Render component with SSR loader data (skip loader)
|
|
281
|
+
const element = handler.value.component({
|
|
282
|
+
loaderData: state.loaderData,
|
|
283
|
+
path: state.params,
|
|
284
|
+
searchParams: state.searchParams,
|
|
285
|
+
});
|
|
286
|
+
// Cast RouterState to DehydratedRouterState (same shape)
|
|
287
|
+
const dehydratedState = {
|
|
288
|
+
routeName: state.routeName,
|
|
289
|
+
params: state.params,
|
|
290
|
+
searchParams: state.searchParams,
|
|
291
|
+
loaderData: state.loaderData,
|
|
292
|
+
};
|
|
293
|
+
return { element, state: dehydratedState };
|
|
294
|
+
}
|
|
295
|
+
// Non-hydration mode: match and run loader
|
|
296
|
+
const pathname = window.location.pathname;
|
|
297
|
+
const matchPathname = stripBasePath(pathname, basePath);
|
|
298
|
+
const search = window.location.search;
|
|
299
|
+
const searchParams = parseSearchParams(search);
|
|
300
|
+
const match = router.matchRoute(matchPathname);
|
|
301
|
+
if (Option.isNone(match)) {
|
|
302
|
+
return yield* Effect.fail(new Error(`No route matched pathname: ${matchPathname}`));
|
|
303
|
+
}
|
|
304
|
+
const { route, params } = match.value;
|
|
305
|
+
const handler = routerHandlers.getHandler(route.name);
|
|
306
|
+
if (Option.isNone(handler)) {
|
|
307
|
+
return yield* Effect.fail(new Error(`No handler found for route: ${route.name}`));
|
|
308
|
+
}
|
|
309
|
+
const loaderCtx = { path: params, searchParams };
|
|
310
|
+
const loaderData = yield* handler.value.loader(loaderCtx);
|
|
311
|
+
const element = handler.value.component({
|
|
312
|
+
loaderData,
|
|
313
|
+
path: params,
|
|
314
|
+
searchParams,
|
|
315
|
+
});
|
|
316
|
+
const state = {
|
|
317
|
+
routeName: route.name,
|
|
318
|
+
params,
|
|
319
|
+
searchParams,
|
|
320
|
+
loaderData,
|
|
321
|
+
};
|
|
322
|
+
// Set RouterStateAtom for DI access
|
|
323
|
+
registry.set(RouterStateAtom, Option.some(state));
|
|
324
|
+
return { element, state };
|
|
325
|
+
}));
|
|
326
|
+
return Layer.mergeAll(historyLayer, Layer.provideMerge(navigatorLayer, historyLayer), routeElementLayer);
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=Router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Router.js","sourceRoot":"","sources":["../../src/router/Router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAwB,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkCnD;;;GAGG;AACH,MAAM,UAAU,KAAK,CAA4B,IAAU;IACzD,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,EAAE;QACV,GAAG,CAAC,KAAY;YACd,OAAO;gBACL,GAAG,IAAI;gBACP,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;aAChC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAA4B,IAAU;IACxD,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,EAAE;QACV,GAAG,CAAC,CAAa;YACf,OAAO;gBACL,GAAG,IAAI;gBACP,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;aAC5B,CAAC;QACJ,CAAC;QACD,UAAU,CAAC,QAAgB;YAKzB,gDAAgD;YAChD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;oBAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACpC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;wBACzB,OAAO,MAAM,CAAC,IAAI,CAAC;4BACjB,SAAS,EAAE,CAAC,CAAC,IAAI;4BACjB,KAAK;4BACL,MAAM,EAAE,KAAK,CAAC,KAAK;yBACpB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC;AA4DD;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,EAG/E;CAAG;AAEN;;GAEG;AACH,SAAS,iBAAiB,CAAC,MAAc;IACvC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,YAAY,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACvE,IAAI,CAAC,YAAY;QAAE,OAAO,MAAM,CAAC;IAEjC,MAAM,YAAY,GAAG,IAAI,eAAe,CAAC,YAAY,CAAC,CAAC;IACvD,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,QAAgB,EAAE,QAAgB;IACvD,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QAClC,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,iDAAiD;IACjD,MAAM,cAAc,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACjF,IAAI,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACvD,uCAAuC;QACvC,OAAO,QAAQ,IAAI,GAAG,CAAC;IACzB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW,CACzB,OAA2B;IAE3B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,EAAE,EAAE,QAAQ,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IACjE,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE/C,kDAAkD;IAClD,MAAM,aAAa,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAExD,wDAAwD;IACxD,MAAM,YAAY,GAAG,iBAAiB,CAAC;QACrC,eAAe,EAAE,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KAC7D,CAAC,CAAC;IAEH,wEAAwE;IACxE,2DAA2D;IAC3D,MAAM,cAAc,GAAG,KAAK,CAAC,YAAY,CACvC,aAAa,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,EACnC,YAAY,CACb,CAAC;IAEF,6BAA6B;IAC7B,MAAM,iBAAiB,GAAG,KAAK,CAAC,MAAM,CACpC,mBAAmB,EACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC;QAElD,sCAAsC;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAC/C,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,8BAA8B,aAAa,EAAE,CAAC,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC;QAEtC,6BAA6B;QAC7B,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,+BAA+B,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACpF,CAAC;QAED,sCAAsC;QACtC,MAAM,SAAS,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACjD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE1D,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;YACtC,UAAU;YACV,IAAI,EAAE,MAAM;YACZ,YAAY;SACb,CAAC,CAAC;QAEH,MAAM,KAAK,GAA0B;YACnC,SAAS,EAAE,KAAK,CAAC,IAAI;YACrB,MAAM;YACN,YAAY;YACZ,UAAU;SACX,CAAC;QAEF,8DAA8D;QAC9D,QAAQ,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAElD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,KAAK,CAAC,QAAQ,CACnB,YAAY,EACZ,cAAc,EACd,iBAAiB,CAClB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,YAAY,CAC1B,OAA4B;IAE5B,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IAE1C,oEAAoE;IACpE,kDAAkD;IAClD,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAC/B,OAAO,EACP,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC;QAClD,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC;QAE1E,qDAAqD;QACrD,MAAM,kBAAkB,GAAG,GAAoB,EAAE,CAAC,CAAC;YACjD,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAClC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM;YAC9B,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI;YAC1B,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK;SAC5B,CAAC,CAAC;QAEH,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAkB,kBAAkB,EAAE,CAAC,CAAC;QAEtE,iDAAiD;QACjD,MAAM,cAAc,GAAG,GAAG,EAAE;YAC1B,QAAQ,CAAC,GAAG,CAAC,YAAY,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACnD,CAAC,CAAC;QAEF,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAEpD,yBAAyB;QACzB,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YACf,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QACzD,CAAC,CAAC,CACH,CAAC;QAEF,oCAAoC;QACpC,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,KAAe,EAAmB,EAAE;YACvE,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAC9B,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,kBAAkB,CAAC;gBACnC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;YAClB,OAAO;gBACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK;aACN,CAAC;QACJ,CAAC,CAAC;QAEF,OAAO;YACL,QAAQ,EAAE,YAAY;YAEtB,IAAI,EAAE,CAAC,IAAY,EAAE,KAAe,EAAE,EAAE,CACtC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC5C,MAAM,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC1E,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;gBAC9C,YAAY,EAAE,CAAC;gBACf,QAAQ,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC,CAAC;YAEJ,OAAO,EAAE,CAAC,IAAY,EAAE,KAAe,EAAE,EAAE,CACzC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC5C,MAAM,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC1E,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;gBACjD,QAAQ,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC,CAAC;YAEJ,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACrB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACxB,CAAC,CAAC;YAEF,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACxB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAC3B,CAAC,CAAC;YAEF,EAAE,EAAE,CAAC,CAAS,EAAE,EAAE,CAChB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC,CAAC;YAEJ,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,YAAY,GAAG,CAAC,CAAC;SAC/C,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,cAAc,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAE3D,yEAAyE;IACzE,4DAA4D;IAC5D,MAAM,iBAAiB,GAAG,KAAK,CAAC,MAAM,CACpC,mBAAmB,EACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC;QAElD,iDAAiD;QACjD,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAEpD,IAAI,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;YACjC,sDAAsD;YACtD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC;YAClC,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC3D,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,+BAA+B,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YACzF,CAAC;YAED,sDAAsD;YACtD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;gBACtC,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,IAAI,EAAE,KAAK,CAAC,MAAM;gBAClB,YAAY,EAAE,KAAK,CAAC,YAAY;aACjC,CAAC,CAAC;YAEH,yDAAyD;YACzD,MAAM,eAAe,GAA0B;gBAC7C,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,UAAU,EAAE,KAAK,CAAC,UAAU;aAC7B,CAAC;YAEF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;QAC7C,CAAC;QAED,2CAA2C;QAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC1C,MAAM,aAAa,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtC,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAE/C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAC/C,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,8BAA8B,aAAa,EAAE,CAAC,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC;QAEtC,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,+BAA+B,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACpF,CAAC;QAED,MAAM,SAAS,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACjD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE1D,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;YACtC,UAAU;YACV,IAAI,EAAE,MAAM;YACZ,YAAY;SACb,CAAC,CAAC;QAEH,MAAM,KAAK,GAA0B;YACnC,SAAS,EAAE,KAAK,CAAC,IAAI;YACrB,MAAM;YACN,YAAY;YACZ,UAAU;SACX,CAAC;QAEF,oCAAoC;QACpC,QAAQ,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAElD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,KAAK,CAAC,QAAQ,CACnB,YAAY,EACZ,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,YAAY,CAAC,EAChD,iBAAiB,CAClB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouterBuilder module for implementing route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Effect HttpApiBuilder patterns:
|
|
5
|
+
* - RouterBuilder.group(router, "groupName", (handlers) => Effect.gen(...))
|
|
6
|
+
* - handlers.handle("routeName", { loader, component })
|
|
7
|
+
* - RouterBuilder.router(Router) builds the final Layer
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Context from "effect/Context";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import type { Router } from "./Router.js";
|
|
14
|
+
import type { Route } from "./Route.js";
|
|
15
|
+
import type { VElement } from "../shared.js";
|
|
16
|
+
/**
|
|
17
|
+
* Context for loader/component execution.
|
|
18
|
+
* Provides decoded path and search parameters.
|
|
19
|
+
*/
|
|
20
|
+
export interface LoaderContext<PathParams extends Record<string, unknown> = Record<string, unknown>, SearchParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
21
|
+
readonly path: PathParams;
|
|
22
|
+
readonly searchParams: SearchParams;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Component props including loader data and route parameters.
|
|
26
|
+
*/
|
|
27
|
+
export interface ComponentProps<LoaderData = unknown, PathParams extends Record<string, unknown> = Record<string, unknown>, SearchParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
28
|
+
readonly loaderData: LoaderData;
|
|
29
|
+
readonly path: PathParams;
|
|
30
|
+
readonly searchParams: SearchParams;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Loader return type - can be a plain value or an Effect.
|
|
34
|
+
* Plain values are automatically wrapped in Effect.succeed.
|
|
35
|
+
*/
|
|
36
|
+
export type LoaderResult<T, E = never, R = never> = T | Effect.Effect<T, E, R>;
|
|
37
|
+
/**
|
|
38
|
+
* Handler configuration for a route.
|
|
39
|
+
* Loader fetches data, component renders with that data.
|
|
40
|
+
*
|
|
41
|
+
* - loader is optional - defaults to returning null
|
|
42
|
+
* - loader can return a plain value or an Effect (plain values auto-wrapped)
|
|
43
|
+
*/
|
|
44
|
+
export interface HandlerConfig<LoaderData = unknown, PathParams extends Record<string, unknown> = Record<string, unknown>, SearchParams extends Record<string, unknown> = Record<string, unknown>, R = never, E = never> {
|
|
45
|
+
readonly loader?: (ctx: LoaderContext<PathParams, SearchParams>) => LoaderResult<LoaderData, E, R>;
|
|
46
|
+
readonly component: (props: ComponentProps<LoaderData, PathParams, SearchParams>) => VElement;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A registered route handler (type-erased for storage).
|
|
50
|
+
* Use executeRoute for type-safe execution.
|
|
51
|
+
*/
|
|
52
|
+
export interface RouteHandler {
|
|
53
|
+
readonly routeName: string;
|
|
54
|
+
readonly route: Route;
|
|
55
|
+
readonly loader: (ctx: LoaderContext) => Effect.Effect<unknown>;
|
|
56
|
+
readonly component: (props: ComponentProps) => VElement;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handlers builder for a route group.
|
|
60
|
+
* Accumulates handler registrations for routes in the group.
|
|
61
|
+
*/
|
|
62
|
+
export interface GroupHandlers<GroupName extends string = string> {
|
|
63
|
+
readonly groupName: GroupName;
|
|
64
|
+
readonly handlers: readonly RouteHandler[];
|
|
65
|
+
/**
|
|
66
|
+
* Register a handler for a route in this group.
|
|
67
|
+
*/
|
|
68
|
+
readonly handle: <RouteName extends string, LoaderData, PathParams extends Record<string, unknown>, SearchParams extends Record<string, unknown>, R, E>(routeName: RouteName, config: HandlerConfig<LoaderData, PathParams, SearchParams, R, E>) => GroupHandlers<GroupName>;
|
|
69
|
+
}
|
|
70
|
+
declare const RouterHandlers_base: Context.TagClass<RouterHandlers, "fibrae/RouterHandlers", {
|
|
71
|
+
readonly handlers: ReadonlyMap<string, RouteHandler>;
|
|
72
|
+
readonly getHandler: (routeName: string) => Option.Option<RouteHandler>;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Tag for the RouterHandlers service.
|
|
76
|
+
* Provides access to all registered handlers.
|
|
77
|
+
*/
|
|
78
|
+
export declare class RouterHandlers extends RouterHandlers_base {
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a Layer that provides handlers for a route group.
|
|
82
|
+
*
|
|
83
|
+
* The build callback can return either:
|
|
84
|
+
* - GroupHandlers directly (simple case)
|
|
85
|
+
* - Effect<GroupHandlers> (when you need Effect context in handlers)
|
|
86
|
+
*
|
|
87
|
+
* Usage:
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // Simple - no Effect wrapper needed
|
|
90
|
+
* const AppRoutesLive = RouterBuilder.group(
|
|
91
|
+
* AppRouter,
|
|
92
|
+
* "app",
|
|
93
|
+
* (handlers) => handlers
|
|
94
|
+
* .handle("home", { component: () => <HomePage /> })
|
|
95
|
+
* .handle("posts", { loader: () => fetchPosts(), component: ... })
|
|
96
|
+
* )
|
|
97
|
+
*
|
|
98
|
+
* // With Effect context (when handlers need services)
|
|
99
|
+
* const AppRoutesLive = RouterBuilder.group(
|
|
100
|
+
* AppRouter,
|
|
101
|
+
* "app",
|
|
102
|
+
* (handlers) => Effect.gen(function* () {
|
|
103
|
+
* const config = yield* Config;
|
|
104
|
+
* return handlers.handle("home", { ... });
|
|
105
|
+
* })
|
|
106
|
+
* )
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function group<GroupName extends string>(router: Router, groupName: GroupName, build: (handlers: GroupHandlers<GroupName>) => GroupHandlers<GroupName>): Layer.Layer<RouterHandlers, never, never>;
|
|
110
|
+
export declare function group<GroupName extends string, R>(router: Router, groupName: GroupName, build: (handlers: GroupHandlers<GroupName>) => Effect.Effect<GroupHandlers<GroupName>, never, R>): Layer.Layer<RouterHandlers, never, R>;
|
|
111
|
+
/**
|
|
112
|
+
* Merge multiple group handler layers into a single RouterHandlers layer.
|
|
113
|
+
*
|
|
114
|
+
* Usage:
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const AppRouterLive = RouterBuilder.router(AppRouter).pipe(
|
|
117
|
+
* Layer.provide(AppRoutesLive),
|
|
118
|
+
* Layer.provide(ApiRoutesLive)
|
|
119
|
+
* )
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function router(_router: Router): Layer.Layer<RouterHandlers>;
|
|
123
|
+
/**
|
|
124
|
+
* Execute a route's loader and render its component.
|
|
125
|
+
* Returns the rendered VElement.
|
|
126
|
+
*/
|
|
127
|
+
export declare function executeRoute(handler: RouteHandler, ctx: LoaderContext): Effect.Effect<VElement>;
|
|
128
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouterBuilder module for implementing route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Effect HttpApiBuilder patterns:
|
|
5
|
+
* - RouterBuilder.group(router, "groupName", (handlers) => Effect.gen(...))
|
|
6
|
+
* - handlers.handle("routeName", { loader, component })
|
|
7
|
+
* - RouterBuilder.router(Router) builds the final Layer
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Context from "effect/Context";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
/**
|
|
14
|
+
* Tag for the RouterHandlers service.
|
|
15
|
+
* Provides access to all registered handlers.
|
|
16
|
+
*/
|
|
17
|
+
export class RouterHandlers extends Context.Tag("fibrae/RouterHandlers")() {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create handlers builder for a route group.
|
|
21
|
+
*/
|
|
22
|
+
function makeGroupHandlers(groupName, group) {
|
|
23
|
+
const routesByName = new Map(group.routes.map((r) => [r.name, r]));
|
|
24
|
+
const buildHandlers = (handlers) => ({
|
|
25
|
+
groupName,
|
|
26
|
+
handlers,
|
|
27
|
+
handle(routeName, config) {
|
|
28
|
+
const maybeRoute = Option.fromNullable(routesByName.get(routeName));
|
|
29
|
+
if (Option.isNone(maybeRoute)) {
|
|
30
|
+
throw new Error(`Route "${routeName}" not found in group "${groupName}"`);
|
|
31
|
+
}
|
|
32
|
+
const route = maybeRoute.value;
|
|
33
|
+
// Normalize loader: default to returning null, wrap plain values in Effect
|
|
34
|
+
const normalizedLoader = (ctx) => {
|
|
35
|
+
if (!config.loader) {
|
|
36
|
+
return Effect.succeed(null);
|
|
37
|
+
}
|
|
38
|
+
const result = config.loader(ctx);
|
|
39
|
+
return (Effect.isEffect(result) ? result : Effect.succeed(result));
|
|
40
|
+
};
|
|
41
|
+
const handler = {
|
|
42
|
+
routeName,
|
|
43
|
+
route,
|
|
44
|
+
loader: normalizedLoader,
|
|
45
|
+
component: config.component,
|
|
46
|
+
};
|
|
47
|
+
return buildHandlers([...handlers, handler]);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return buildHandlers([]);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Find a route group by name in a router.
|
|
54
|
+
*/
|
|
55
|
+
function findGroup(router, groupName) {
|
|
56
|
+
const maybeGroup = Option.fromNullable(router.groups.find((g) => g.name === groupName));
|
|
57
|
+
if (Option.isNone(maybeGroup)) {
|
|
58
|
+
throw new Error(`Group "${groupName}" not found in router "${router.name}"`);
|
|
59
|
+
}
|
|
60
|
+
return maybeGroup.value;
|
|
61
|
+
}
|
|
62
|
+
export function group(router, groupName, build) {
|
|
63
|
+
const routeGroup = findGroup(router, groupName);
|
|
64
|
+
const initialHandlers = makeGroupHandlers(groupName, routeGroup);
|
|
65
|
+
return Layer.effect(RouterHandlers, Effect.gen(function* () {
|
|
66
|
+
const result = build(initialHandlers);
|
|
67
|
+
const builtHandlers = Effect.isEffect(result) ? yield* result : result;
|
|
68
|
+
const handlersMap = new Map(builtHandlers.handlers.map((h) => [h.routeName, h]));
|
|
69
|
+
return {
|
|
70
|
+
handlers: handlersMap,
|
|
71
|
+
getHandler(routeName) {
|
|
72
|
+
const handler = handlersMap.get(routeName);
|
|
73
|
+
return handler ? Option.some(handler) : Option.none();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Merge multiple group handler layers into a single RouterHandlers layer.
|
|
80
|
+
*
|
|
81
|
+
* Usage:
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const AppRouterLive = RouterBuilder.router(AppRouter).pipe(
|
|
84
|
+
* Layer.provide(AppRoutesLive),
|
|
85
|
+
* Layer.provide(ApiRoutesLive)
|
|
86
|
+
* )
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function router(_router) {
|
|
90
|
+
// Start with an empty handlers layer
|
|
91
|
+
return Layer.succeed(RouterHandlers, {
|
|
92
|
+
handlers: new Map(),
|
|
93
|
+
getHandler(_routeName) {
|
|
94
|
+
return Option.none();
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Execute a route's loader and render its component.
|
|
100
|
+
* Returns the rendered VElement.
|
|
101
|
+
*/
|
|
102
|
+
export function executeRoute(handler, ctx) {
|
|
103
|
+
return Effect.gen(function* () {
|
|
104
|
+
const loaderData = yield* handler.loader(ctx);
|
|
105
|
+
return handler.component({
|
|
106
|
+
loaderData,
|
|
107
|
+
path: ctx.path,
|
|
108
|
+
searchParams: ctx.searchParams,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=RouterBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouterBuilder.js","sourceRoot":"","sources":["../../src/router/RouterBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouterOutlet - Reactive route rendering component.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to Navigator.currentRoute and renders the matched route's component.
|
|
5
|
+
* When route changes, runs the new route's loader and renders the component.
|
|
6
|
+
*
|
|
7
|
+
* For SSR hydration, the RouterStateAtom is pre-populated and the loader is skipped
|
|
8
|
+
* on first render.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```typescript
|
|
12
|
+
* function App() {
|
|
13
|
+
* return (
|
|
14
|
+
* <div>
|
|
15
|
+
* <Nav />
|
|
16
|
+
* <RouterOutlet />
|
|
17
|
+
* </div>
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import * as Stream from "effect/Stream";
|
|
23
|
+
import { Registry as AtomRegistry } from "@effect-atom/atom";
|
|
24
|
+
import { Navigator } from "./Navigator.js";
|
|
25
|
+
import { RouterHandlers } from "./RouterBuilder.js";
|
|
26
|
+
import type { VElement } from "../shared.js";
|
|
27
|
+
/**
|
|
28
|
+
* Props for RouterOutlet component.
|
|
29
|
+
*
|
|
30
|
+
* @deprecated Props are no longer needed - SSR hydration is handled via RouterStateAtom.
|
|
31
|
+
*/
|
|
32
|
+
export interface RouterOutletProps {
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use RouterStateAtom hydration instead.
|
|
35
|
+
* Initial loader data from SSR - skips loader on first render.
|
|
36
|
+
*/
|
|
37
|
+
readonly initialLoaderData?: unknown;
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use RouterStateAtom hydration instead.
|
|
40
|
+
* Initial route name from SSR - used with initialLoaderData.
|
|
41
|
+
*/
|
|
42
|
+
readonly initialRouteName?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* RouterOutlet component for reactive route rendering.
|
|
46
|
+
*
|
|
47
|
+
* The RouterOutlet:
|
|
48
|
+
* 1. Checks if RouterStateAtom has data (SSR hydration case)
|
|
49
|
+
* 2. Subscribes to Navigator.currentRoute for navigation changes
|
|
50
|
+
* 3. When route changes, runs the matched route's loader
|
|
51
|
+
* 4. Updates RouterStateAtom with the full state (for DI access)
|
|
52
|
+
* 5. Renders the route's component with loader data
|
|
53
|
+
*
|
|
54
|
+
* For SSR hydration, the RouterStateAtom is pre-populated by the server,
|
|
55
|
+
* so the first render uses that data and skips the loader.
|
|
56
|
+
*/
|
|
57
|
+
export declare function RouterOutlet(_props?: RouterOutletProps): Stream.Stream<VElement, unknown, Navigator | RouterHandlers | AtomRegistry.AtomRegistry>;
|