@typed/router 0.32.0 → 1.0.0-beta.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.
Files changed (87) hide show
  1. package/README.md +129 -2
  2. package/dist/AST.d.ts +96 -0
  3. package/dist/AST.d.ts.map +1 -0
  4. package/dist/AST.js +32 -0
  5. package/dist/CurrentRoute.d.ts +18 -0
  6. package/dist/CurrentRoute.d.ts.map +1 -0
  7. package/dist/CurrentRoute.js +18 -0
  8. package/dist/Matcher.d.ts +209 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +633 -0
  11. package/dist/Parser.d.ts +92 -0
  12. package/dist/Parser.d.ts.map +1 -0
  13. package/dist/Parser.js +1 -0
  14. package/dist/Path.d.ts +216 -0
  15. package/dist/Path.d.ts.map +1 -0
  16. package/dist/Path.js +248 -0
  17. package/dist/Route.d.ts +57 -0
  18. package/dist/Route.d.ts.map +1 -0
  19. package/dist/Route.js +151 -0
  20. package/dist/Router.d.ts +9 -0
  21. package/dist/Router.d.ts.map +1 -0
  22. package/dist/Router.js +8 -0
  23. package/dist/Uri.d.ts +115 -0
  24. package/dist/Uri.d.ts.map +1 -0
  25. package/dist/Uri.js +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4 -0
  29. package/package.json +32 -73
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +496 -0
  33. package/src/Matcher.ts +1375 -325
  34. package/src/Parser.ts +276 -0
  35. package/src/Path.test.ts +318 -0
  36. package/src/Path.ts +691 -0
  37. package/src/Route.test.ts +268 -0
  38. package/src/Route.ts +316 -0
  39. package/src/Router.ts +33 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/CurrentRoute/package.json +0 -6
  43. package/LICENSE +0 -21
  44. package/MatchInput/package.json +0 -6
  45. package/Matcher/package.json +0 -6
  46. package/RouteGuard/package.json +0 -6
  47. package/RouteMatch/package.json +0 -6
  48. package/dist/cjs/CurrentRoute.js +0 -170
  49. package/dist/cjs/CurrentRoute.js.map +0 -1
  50. package/dist/cjs/MatchInput.js +0 -96
  51. package/dist/cjs/MatchInput.js.map +0 -1
  52. package/dist/cjs/Matcher.js +0 -138
  53. package/dist/cjs/Matcher.js.map +0 -1
  54. package/dist/cjs/RouteGuard.js +0 -78
  55. package/dist/cjs/RouteGuard.js.map +0 -1
  56. package/dist/cjs/RouteMatch.js +0 -49
  57. package/dist/cjs/RouteMatch.js.map +0 -1
  58. package/dist/cjs/index.js +0 -53
  59. package/dist/cjs/index.js.map +0 -1
  60. package/dist/dts/CurrentRoute.d.ts +0 -94
  61. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  62. package/dist/dts/MatchInput.d.ts +0 -143
  63. package/dist/dts/MatchInput.d.ts.map +0 -1
  64. package/dist/dts/Matcher.d.ts +0 -121
  65. package/dist/dts/Matcher.d.ts.map +0 -1
  66. package/dist/dts/RouteGuard.d.ts +0 -94
  67. package/dist/dts/RouteGuard.d.ts.map +0 -1
  68. package/dist/dts/RouteMatch.d.ts +0 -50
  69. package/dist/dts/RouteMatch.d.ts.map +0 -1
  70. package/dist/dts/index.d.ts +0 -24
  71. package/dist/dts/index.d.ts.map +0 -1
  72. package/dist/esm/CurrentRoute.js +0 -152
  73. package/dist/esm/CurrentRoute.js.map +0 -1
  74. package/dist/esm/MatchInput.js +0 -79
  75. package/dist/esm/MatchInput.js.map +0 -1
  76. package/dist/esm/Matcher.js +0 -130
  77. package/dist/esm/Matcher.js.map +0 -1
  78. package/dist/esm/RouteGuard.js +0 -57
  79. package/dist/esm/RouteGuard.js.map +0 -1
  80. package/dist/esm/RouteMatch.js +0 -29
  81. package/dist/esm/RouteMatch.js.map +0 -1
  82. package/dist/esm/index.js +0 -24
  83. package/dist/esm/index.js.map +0 -1
  84. package/dist/esm/package.json +0 -4
  85. package/src/MatchInput.ts +0 -303
  86. package/src/RouteGuard.ts +0 -217
  87. package/src/RouteMatch.ts +0 -104
package/src/AST.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { Cause } from "effect/Cause";
2
+ import { succeedSome } from "effect/Effect";
3
+ import type { Top } from "effect/Schema";
4
+ import type { Transformation } from "effect/SchemaTransformation";
5
+ import type { Fx } from "@typed/fx/Fx";
6
+ import type { RefSubject } from "@typed/fx/RefSubject/RefSubject";
7
+ import type { Guard } from "@typed/guard";
8
+ import type { AnyLayer, Layout as LayoutType, MatchHandler } from "./Matcher.js";
9
+
10
+ export type PathAst =
11
+ | PathAst.Literal
12
+ | PathAst.Parameter
13
+ | PathAst.Slash
14
+ | PathAst.Wildcard
15
+ | PathAst.QueryParams;
16
+
17
+ export declare namespace PathAst {
18
+ export type Literal = {
19
+ type: "literal";
20
+ value: string;
21
+ };
22
+ export type Parameter = {
23
+ type: "parameter";
24
+ name: string;
25
+ optional?: boolean;
26
+ regex?: string;
27
+ };
28
+
29
+ export type Wildcard = {
30
+ type: "wildcard";
31
+ };
32
+
33
+ export type Slash = {
34
+ type: "slash";
35
+ };
36
+
37
+ export type QueryParams = {
38
+ type: "query-params";
39
+ value: ReadonlyArray<PathAst.QueryParam>;
40
+ };
41
+
42
+ export type QueryParam = {
43
+ type: "query-param";
44
+ name: string;
45
+ value: PathAst;
46
+ };
47
+ }
48
+
49
+ export const literal = (value: string): PathAst.Literal => ({ type: "literal", value });
50
+ export const parameter = (name: string, optional?: boolean, regex?: string): PathAst.Parameter => ({
51
+ type: "parameter",
52
+ name,
53
+ ...(optional ? { optional } : {}),
54
+ ...(regex ? { regex } : {}),
55
+ });
56
+ export const wildcard = (): PathAst.Wildcard => ({ type: "wildcard" });
57
+ export const slash = (): PathAst.Slash => ({ type: "slash" });
58
+ export const queryParams = (value: ReadonlyArray<PathAst.QueryParam>): PathAst.QueryParams => ({
59
+ type: "query-params",
60
+ value,
61
+ });
62
+ export const queryParam = (name: string, value: PathAst): PathAst.QueryParam => ({
63
+ type: "query-param",
64
+ name,
65
+ value,
66
+ });
67
+
68
+ export type RouteAst = RouteAst.Path | RouteAst.Transform | RouteAst.Join;
69
+
70
+ export declare namespace RouteAst {
71
+ export interface Path {
72
+ type: "path";
73
+ path: PathAst;
74
+ }
75
+
76
+ export interface Transform {
77
+ type: "transform";
78
+ from: RouteAst;
79
+ to: Top;
80
+ transformation: Transformation<any, any, any, any>;
81
+ }
82
+
83
+ export interface Join {
84
+ type: "join";
85
+ parts: ReadonlyArray<RouteAst>;
86
+ }
87
+ }
88
+
89
+ export const path = (path: PathAst): RouteAst.Path => ({ type: "path", path });
90
+ export const transform = (
91
+ from: RouteAst,
92
+ to: Top,
93
+ transformation: Transformation<any, any, any, any>,
94
+ ): RouteAst.Transform => ({
95
+ type: "transform",
96
+ from,
97
+ to,
98
+ transformation,
99
+ });
100
+ export const join = (parts: ReadonlyArray<RouteAst>): RouteAst.Join => ({ type: "join", parts });
101
+
102
+ export type MatchAst =
103
+ | MatchAst.Route
104
+ | MatchAst.Layer
105
+ | MatchAst.Layout
106
+ | MatchAst.Prefixed
107
+ | MatchAst.Catch;
108
+
109
+ export declare namespace MatchAst {
110
+ export interface Route {
111
+ type: "route";
112
+ route: RouteAst;
113
+ guard: Guard<any, any, any, any>;
114
+ handler: MatchHandler<any, any, any, any>;
115
+ }
116
+
117
+ export interface Layer {
118
+ type: "layer";
119
+ matches: ReadonlyArray<MatchAst>;
120
+ deps: ReadonlyArray<AnyLayer>;
121
+ }
122
+
123
+ export interface Layout {
124
+ type: "layout";
125
+ matches: ReadonlyArray<MatchAst>;
126
+ layout: LayoutType<any, any, any, any, any, any, any>;
127
+ }
128
+
129
+ export interface Prefixed {
130
+ type: "prefixed";
131
+ matches: ReadonlyArray<MatchAst>;
132
+ prefix: RouteAst;
133
+ }
134
+
135
+ export interface Catch {
136
+ type: "catch";
137
+ matches: ReadonlyArray<MatchAst>;
138
+ f: (cause: RefSubject<Cause<any>>) => Fx<any, any, any>;
139
+ }
140
+ }
141
+
142
+ export const route = (
143
+ route: RouteAst,
144
+ handler: MatchHandler<any, any, any, any>,
145
+ guard: Guard<any, any, any, any> = succeedSome,
146
+ ): MatchAst.Route => ({ type: "route", route, guard, handler });
147
+
148
+ export const layer = (
149
+ matches: ReadonlyArray<MatchAst>,
150
+ deps: ReadonlyArray<AnyLayer>,
151
+ ): MatchAst.Layer => ({ type: "layer", matches, deps });
152
+
153
+ export const layout = (
154
+ matches: ReadonlyArray<MatchAst>,
155
+ layout: LayoutType<any, any, any, any, any, any, any>,
156
+ ): MatchAst.Layout => ({ type: "layout", matches, layout });
157
+
158
+ export const prefixed = (
159
+ matches: ReadonlyArray<MatchAst>,
160
+ prefix: RouteAst,
161
+ ): MatchAst.Prefixed => ({ type: "prefixed", matches, prefix });
162
+
163
+ export const catchCause = (
164
+ matches: ReadonlyArray<MatchAst>,
165
+ f: (cause: RefSubject<Cause<any>>) => Fx<any, any, any>,
166
+ ): MatchAst.Catch => ({ type: "catch", matches, f });
@@ -1,332 +1,31 @@
1
- /**
2
- * @since 1.0.0
3
- */
4
-
5
- import * as Context from "@typed/context"
6
- import * as Document from "@typed/dom/Document"
7
- import type * as Fx from "@typed/fx"
8
- import * as RefSubject from "@typed/fx/RefSubject"
9
- import * as Navigation from "@typed/navigation"
10
- import * as Route from "@typed/route"
11
- import type { Cause } from "effect"
12
- import * as Effect from "effect/Effect"
13
- import { dual, pipe } from "effect/Function"
14
- import type * as Layer from "effect/Layer"
15
- import * as Option from "effect/Option"
16
-
17
- /**
18
- * @since 1.0.0
19
- */
20
- export interface CurrentRoute {
21
- readonly route: Route.Route.Any
22
- readonly parent: Option.Option<CurrentRoute>
23
- }
24
-
25
- /**
26
- * @since 1.0.0
27
- */
28
- export const CurrentRoute: Context.Tagged<CurrentRoute> = Context.Tagged<CurrentRoute>("@typed/router/CurrentRoute")
29
-
30
- /**
31
- * @since 1.0.0
32
- */
33
- export function makeCurrentRoute<R extends Route.Route.Any>(
34
- route: R,
35
- parent: Option.Option<CurrentRoute> = Option.none()
36
- ): CurrentRoute {
37
- return {
38
- route,
39
- parent
40
- }
41
- }
42
-
43
- /**
44
- * @since 1.0.0
45
- */
46
- export function layer<R extends Route.Route.Any>(
47
- route: R,
48
- parent: Option.Option<CurrentRoute> = Option.none()
49
- ): Layer.Layer<CurrentRoute> {
50
- return CurrentRoute.layer(makeCurrentRoute(route, parent))
51
- }
52
-
53
- /**
54
- * @since 1.0.0
55
- */
56
- export const CurrentParams: RefSubject.Filtered<
57
- Readonly<Record<string, string | ReadonlyArray<string>>>,
58
- never,
59
- Navigation.Navigation | CurrentRoute
60
- > = RefSubject.filteredFromTag(
61
- Navigation.Navigation,
62
- (nav) =>
63
- RefSubject.filterMapEffect(
64
- nav.currentEntry,
65
- (e) => CurrentRoute.with(({ route }) => route.match(Navigation.getCurrentPathFromUrl(e.url)))
66
- )
67
- )
68
-
69
- /**
70
- * @since 1.0.0
71
- */
72
- export const withCurrentRoute: {
73
- <R extends Route.Route.Any>(
74
- route: R
75
- ): <A, E, R>(
76
- effect: Effect.Effect<A, E, R>
77
- ) => Effect.Effect<A, E, Exclude<R, CurrentRoute>>
78
-
79
- <A, E, R, R_ extends Route.Route.Any>(
80
- effect: Effect.Effect<A, E, R>,
81
- route: R_
82
- ): Effect.Effect<A, E, Exclude<R, CurrentRoute>>
83
- } = dual(
84
- 2,
85
- <A, E, R, R_ extends Route.Route.Any>(
86
- effect: Effect.Effect<A, E, R>,
87
- route: R_
88
- ): Effect.Effect<A, E, Exclude<R, CurrentRoute>> =>
89
- Effect.contextWithEffect((ctx) => {
90
- const parent = Context.getOption(ctx, CurrentRoute)
91
-
92
- if (Option.isNone(parent)) {
93
- return pipe(effect, CurrentRoute.provide(makeCurrentRoute(route)))
94
- }
95
-
96
- return pipe(
97
- effect,
98
- CurrentRoute.provide(
99
- makeCurrentRoute(parent.value.route.concat(route), parent)
100
- )
101
- )
102
- })
103
- )
104
-
105
- const makeHref_ = (
106
- currentPath: string,
107
- currentRoute: Route.Route.Any,
108
- route: Route.Route.Any,
109
- params: {} = {}
110
- ): Option.Option<string> => {
111
- const currentMatch = currentRoute.match(currentPath)
112
- if (Option.isNone(currentMatch)) return Option.none()
113
-
114
- const fullRoute = currentRoute.concat(route)
115
- const fullParams = { ...currentMatch.value, ...params }
116
-
117
- return Option.some(fullRoute.interpolate(fullParams as any))
118
- }
119
-
120
- /**
121
- * @since 1.0.0
122
- */
123
- export function makeHref<const R extends Route.Route.Any>(
124
- route: R,
125
- ...[params]: Route.Route.ParamsList<R>
126
- ): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute>
127
- export function makeHref<const R extends Route.Route.Any>(
128
- route: R,
129
- params: Route.Route.Params<R>
130
- ): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute>
131
-
132
- export function makeHref<const R extends Route.Route.Any>(
133
- route: R,
134
- ...[params]: Route.Route.ParamsList<R>
135
- ): RefSubject.Filtered<string, never, Navigation.Navigation | CurrentRoute> {
136
- return RefSubject.filterMapEffect(Navigation.CurrentPath, (currentPath) =>
137
- Effect.map(
138
- CurrentRoute,
139
- (currentRoute): Option.Option<string> => makeHref_(currentPath, currentRoute.route, route, params)
140
- ))
141
- }
142
-
143
- const isActive_ = (
144
- currentPath: string,
145
- currentRoute: Route.Route.Any,
146
- route: Route.Route.Any,
147
- params: any = {}
148
- ): boolean => {
149
- const currentMatch = currentRoute.match(currentPath)
150
- if (Option.isNone(currentMatch)) return false
151
-
152
- const fullRoute = currentRoute.concat(route)
153
- const fullParams = { ...currentMatch.value, ...params }
154
- const href: string = fullRoute.interpolate(fullParams as any)
155
- const [currentPathname, currentQuery] = splitByQuery(currentPath)
156
- const [hrefPathname, hrefQuery] = splitByQuery(href)
157
-
158
- return (
159
- (fullRoute.routeOptions.end
160
- ? currentPathname === hrefPathname
161
- : currentPathname.startsWith(hrefPathname)) &&
162
- compareQueries(currentQuery, hrefQuery)
163
- )
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as ServiceMap from "effect/ServiceMap";
4
+ import { Navigation } from "@typed/navigation/Navigation";
5
+ import { Parse, type Route } from "./Route.js";
6
+
7
+ export interface CurrentRouteTree {
8
+ readonly route: Route<string, any>;
9
+ readonly parent?: CurrentRouteTree | undefined;
10
+ }
11
+
12
+ export class CurrentRoute extends ServiceMap.Service<CurrentRoute, CurrentRouteTree>()(
13
+ "@typed/router/CurrentRoute",
14
+ {
15
+ make: Effect.map(Navigation.base, (base) => ({ route: Parse(base) })),
16
+ },
17
+ ) {
18
+ static readonly Default = Layer.effect(CurrentRoute, CurrentRoute.make);
19
+
20
+ static readonly extend = (route: Route.Any) =>
21
+ Layer.unwrap(
22
+ Effect.gen(function* () {
23
+ const services = yield* Effect.services<never>();
24
+ const parent = ServiceMap.getOrUndefined(services, CurrentRoute);
25
+ return Layer.succeed(CurrentRoute, {
26
+ route,
27
+ parent,
28
+ });
29
+ }),
30
+ );
164
31
  }
165
-
166
- function compareQueries(currentQuery: string, hrefQuery: string) {
167
- // if hrefQuery is empty, it means that the href is a pathname only
168
- if (!hrefQuery) return true
169
- // if currentQuery is empty, there is no match at this point
170
- if (!currentQuery) return false
171
- // if the queries are equal, there is a match
172
- if (currentQuery === hrefQuery) return true
173
-
174
- const currentQueryParams = new URLSearchParams(currentQuery)
175
- const hrefQueryParams = new URLSearchParams(hrefQuery)
176
-
177
- for (const key of hrefQueryParams.keys()) {
178
- const a = currentQueryParams.getAll(key).sort()
179
- const b = hrefQueryParams.getAll(key).sort()
180
-
181
- if (a.length !== b.length || !b.every((bx, i) => a[i] === bx)) return false
182
- }
183
-
184
- return true
185
- }
186
-
187
- function splitByQuery(path: string) {
188
- const queryIndex = path.indexOf("?")
189
- if (queryIndex > -1) {
190
- const pathname = path.slice(0, queryIndex)
191
- const query = path.slice(queryIndex + 1).trim()
192
- return [pathname, query] as const
193
- }
194
-
195
- return [path, ""] as const
196
- }
197
-
198
- /**
199
- * @since 1.0.0
200
- */
201
- export function isActive<R extends Route.Route.Any>(
202
- route: R,
203
- ...[params]: Route.Route.ParamsList<R>
204
- ): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute>
205
- export function isActive<R extends Route.Route.Any>(
206
- route: R,
207
- params: Route.Route.Params<R>
208
- ): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute>
209
- export function isActive<R extends Route.Route.Any>(
210
- route: R,
211
- ...[params]: Route.Route.ParamsList<R>
212
- ): RefSubject.Computed<boolean, never, Navigation.Navigation | CurrentRoute> {
213
- return RefSubject.mapEffect(
214
- Navigation.CurrentPath,
215
- (currentPath) =>
216
- CurrentRoute.with((currentRoute): boolean => isActive_(currentPath, currentRoute.route, route, params))
217
- )
218
- }
219
-
220
- /**
221
- * @since 1.0.0
222
- */
223
- export function decode<R extends Route.Route.Any>(
224
- route: R
225
- ): Fx.RefSubject.Filtered<
226
- Route.Route.Type<R>,
227
- Route.RouteDecodeError<R>,
228
- Navigation.Navigation | CurrentRoute | Route.Route.Context<R>
229
- > {
230
- return RefSubject.filteredFromTag(
231
- Navigation.Navigation,
232
- (nav) =>
233
- RefSubject.filterMapEffect(
234
- nav.currentEntry,
235
- (e) =>
236
- Effect.flatMap(CurrentRoute, ({ route: parent }) =>
237
- Effect.optionFromOptional(
238
- Route.decode(
239
- parent.concat(route) as R,
240
- Navigation.getCurrentPathFromUrl(e.url)
241
- )
242
- ))
243
- )
244
- )
245
- }
246
-
247
- /**
248
- * @since 1.0.0
249
- */
250
- export const browser: Layer.Layer<CurrentRoute, never, Document.Document> = CurrentRoute.layer(
251
- Effect.gen(function*() {
252
- const document = yield* Document.Document
253
- const base = document.querySelector("base")
254
- const baseHref = base ? getBasePathname(base.href) : "/"
255
-
256
- return {
257
- route: Route.parse(baseHref),
258
- parent: Option.none()
259
- }
260
- })
261
- )
262
-
263
- function getBasePathname(base: string): string {
264
- try {
265
- const url = new URL(base)
266
- return url.pathname
267
- } catch {
268
- return base
269
- }
270
- }
271
-
272
- /**
273
- * @since 1.0.0
274
- */
275
- export const server = (base: string = "/"): Layer.Layer<CurrentRoute> =>
276
- CurrentRoute.layer({ route: Route.parse(base), parent: Option.none() })
277
-
278
- const getSearchParams = (destination: Navigation.Destination) => destination.url.searchParams
279
-
280
- /**
281
- * @since 1.0.0
282
- */
283
- export const CurrentSearchParams: RefSubject.Computed<
284
- URLSearchParams,
285
- never,
286
- Navigation.Navigation
287
- > = RefSubject.map(Navigation.CurrentEntry, getSearchParams)
288
-
289
- /**
290
- * @since 1.0.0
291
- */
292
- export const CurrentState = RefSubject.computedFromTag(
293
- Navigation.Navigation,
294
- (n) => RefSubject.map(n.currentEntry, (e) => e.state)
295
- )
296
-
297
- /**
298
- * @since 1.0.0
299
- */
300
- export type NavigateOptions<R extends Route.Route.Any> = Route.Route.ParamsAreOptional<R> extends true ? [
301
- options?: Navigation.NavigateOptions & {
302
- readonly params?: Route.Route.Params<R> | undefined
303
- } & {
304
- readonly relative?: boolean
305
- }
306
- ]
307
- : [
308
- options: Navigation.NavigateOptions & {
309
- readonly params: Route.Route.Params<R>
310
- } & {
311
- readonly relative?: boolean
312
- }
313
- ]
314
-
315
- /**
316
- * @since 1.0.0
317
- */
318
- export const navigate = <R extends Route.Route.Any>(
319
- route: R,
320
- ...[options]: NavigateOptions<R>
321
- ): Effect.Effect<
322
- Navigation.Destination,
323
- Navigation.NavigationError | Cause.NoSuchElementException,
324
- Navigation.Navigation | CurrentRoute
325
- > =>
326
- Effect.gen(function*(_) {
327
- const params = options?.params ?? ({} as Route.Route.Params<R>)
328
- const path = options?.relative === false
329
- ? route.interpolate(params)
330
- : yield* _(makeHref<R>(route, params))
331
- return yield* _(Navigation.navigate(path))
332
- })