@typed/router 0.31.0 → 1.0.0-beta.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.
Files changed (88) hide show
  1. package/README.md +111 -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 +191 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +597 -0
  11. package/dist/Parser.d.ts +96 -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 +25 -69
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +476 -0
  33. package/src/Matcher.ts +1269 -328
  34. package/src/Parser.ts +282 -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 +31 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/tsconfig.json +6 -0
  43. package/CurrentRoute/package.json +0 -6
  44. package/LICENSE +0 -21
  45. package/MatchInput/package.json +0 -6
  46. package/Matcher/package.json +0 -6
  47. package/RouteGuard/package.json +0 -6
  48. package/RouteMatch/package.json +0 -6
  49. package/dist/cjs/CurrentRoute.js +0 -170
  50. package/dist/cjs/CurrentRoute.js.map +0 -1
  51. package/dist/cjs/MatchInput.js +0 -96
  52. package/dist/cjs/MatchInput.js.map +0 -1
  53. package/dist/cjs/Matcher.js +0 -138
  54. package/dist/cjs/Matcher.js.map +0 -1
  55. package/dist/cjs/RouteGuard.js +0 -78
  56. package/dist/cjs/RouteGuard.js.map +0 -1
  57. package/dist/cjs/RouteMatch.js +0 -49
  58. package/dist/cjs/RouteMatch.js.map +0 -1
  59. package/dist/cjs/index.js +0 -53
  60. package/dist/cjs/index.js.map +0 -1
  61. package/dist/dts/CurrentRoute.d.ts +0 -94
  62. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  63. package/dist/dts/MatchInput.d.ts +0 -135
  64. package/dist/dts/MatchInput.d.ts.map +0 -1
  65. package/dist/dts/Matcher.d.ts +0 -121
  66. package/dist/dts/Matcher.d.ts.map +0 -1
  67. package/dist/dts/RouteGuard.d.ts +0 -94
  68. package/dist/dts/RouteGuard.d.ts.map +0 -1
  69. package/dist/dts/RouteMatch.d.ts +0 -50
  70. package/dist/dts/RouteMatch.d.ts.map +0 -1
  71. package/dist/dts/index.d.ts +0 -24
  72. package/dist/dts/index.d.ts.map +0 -1
  73. package/dist/esm/CurrentRoute.js +0 -152
  74. package/dist/esm/CurrentRoute.js.map +0 -1
  75. package/dist/esm/MatchInput.js +0 -79
  76. package/dist/esm/MatchInput.js.map +0 -1
  77. package/dist/esm/Matcher.js +0 -130
  78. package/dist/esm/Matcher.js.map +0 -1
  79. package/dist/esm/RouteGuard.js +0 -57
  80. package/dist/esm/RouteGuard.js.map +0 -1
  81. package/dist/esm/RouteMatch.js +0 -29
  82. package/dist/esm/RouteMatch.js.map +0 -1
  83. package/dist/esm/index.js +0 -24
  84. package/dist/esm/index.js.map +0 -1
  85. package/dist/esm/package.json +0 -4
  86. package/src/MatchInput.ts +0 -282
  87. package/src/RouteGuard.ts +0 -217
  88. package/src/RouteMatch.ts +0 -104
@@ -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
- })