@typed/router 0.0.2

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.
@@ -0,0 +1,56 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
2
+ import * as Layer from '@effect/io/Layer'
3
+ import { flow } from '@fp-ts/data/Function'
4
+ import * as Context from '@typed/context'
5
+ import * as Fx from '@typed/fx'
6
+ import * as html from '@typed/html'
7
+ import * as Path from '@typed/path'
8
+ import * as Route from '@typed/route'
9
+
10
+ export interface RouteMatch<R, E, P extends string> {
11
+ readonly route: Route.Route<R, P>
12
+
13
+ readonly layout?: Fx.Fx<R, E, html.Renderable>
14
+
15
+ readonly match: (params: Fx.Fx<never, never, Path.ParamsOf<P>>) => Fx.Fx<R, E, html.Renderable>
16
+
17
+ readonly provideEnvironment: (environment: Context.Context<R>) => RouteMatch<never, E, P>
18
+
19
+ readonly provideService: <S>(tag: Context.Tag<S>, service: S) => RouteMatch<Exclude<R, S>, E, P>
20
+
21
+ readonly provideLayer: <R2, S>(
22
+ layer: Layer.Layer<R2, never, S>,
23
+ ) => RouteMatch<R2 | Exclude<R, S>, E, P>
24
+ }
25
+
26
+ export function RouteMatch<R, P extends string, R2, E2, R3, E3>(
27
+ route: Route.Route<R, P>,
28
+ match: (params: Fx.Fx<never, never, Path.ParamsOf<P>>) => Fx.Fx<R2, E2, html.Renderable>,
29
+ layout?: Fx.Fx<R3, E3, html.Renderable>,
30
+ ): RouteMatch<R | R2 | R3, E2 | E3, P> {
31
+ const routeMatch: RouteMatch<R | R2 | R3, E2 | E3, P> = {
32
+ route,
33
+ match,
34
+ layout,
35
+ provideEnvironment: (env) =>
36
+ RouteMatch(
37
+ Route.provideEnvironment(env)(route),
38
+ flow(match, Fx.provideSomeEnvironment(env)),
39
+ layout ? Fx.provideEnvironment(env)(layout) : undefined,
40
+ ),
41
+ provideService: (tag, service) =>
42
+ RouteMatch(
43
+ Route.provideService(tag, service)(route),
44
+ flow(match, Fx.provideService(tag)(service)),
45
+ layout ? Fx.provideService(tag)(service)(layout) : undefined,
46
+ ),
47
+ provideLayer: (layer) =>
48
+ RouteMatch(
49
+ Route.provideLayer(layer)(route),
50
+ flow(match, Fx.provideSomeLayer(layer)),
51
+ layout ? Fx.provideSomeLayer(layer)(layout) : undefined,
52
+ ),
53
+ }
54
+
55
+ return routeMatch
56
+ }
@@ -0,0 +1,253 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
2
+ import * as Effect from '@effect/io/Effect'
3
+ import * as Fiber from '@effect/io/Fiber'
4
+ import * as Layer from '@effect/io/Layer'
5
+ import { pipe } from '@fp-ts/data/Function'
6
+ import * as Option from '@fp-ts/data/Option'
7
+ import * as Context from '@typed/context'
8
+ import * as Fx from '@typed/fx'
9
+ import * as html from '@typed/html'
10
+ import { RenderContext } from '@typed/html'
11
+ import * as Path from '@typed/path'
12
+ import * as Route from '@typed/route'
13
+
14
+ import { RouteMatch } from './RouteMatch.js'
15
+ import { currentPath, Redirect, redirectTo, Router } from './router.js'
16
+
17
+ export interface RouteMatcher<R = never, E = never> {
18
+ // Where things are actually stored immutably
19
+ readonly routes: ReadonlyMap<Route.Route<any, any>, RouteMatch<any, any, any>>
20
+
21
+ // Add Routes
22
+
23
+ readonly match: <R2, P extends string, R3, E3>(
24
+ route: Route.Route<R2, P>,
25
+ f: (params: Path.ParamsOf<P>) => Fx.Fx<R3, E3, html.Renderable>,
26
+ ) => RouteMatcher<R | R2, E | E3>
27
+
28
+ readonly matchFx: <R2, P extends string, R3, E3>(
29
+ route: Route.Route<R2, P>,
30
+ f: (params: Fx.Fx<never, never, Path.ParamsOf<P>>) => Fx.Fx<R3, E3, html.Renderable>,
31
+ ) => RouteMatcher<R | R2, E | E3>
32
+
33
+ readonly matchEffect: <R2, P extends string, R3, E3>(
34
+ route: Route.Route<R2, P>,
35
+ f: (params: Path.ParamsOf<P>) => Effect.Effect<R3, E3, html.Renderable>,
36
+ ) => RouteMatcher<R | R2, E | E3>
37
+
38
+ // Add Layout
39
+
40
+ readonly withLayout: <R2, E2>(fx: Fx.Fx<R2, E2, html.Renderable>) => RouteMatcher<R | R2, E | E2>
41
+
42
+ // Provide resources
43
+
44
+ readonly provideEnvironment: <R2>(
45
+ environment: Context.Context<R2>,
46
+ ) => RouteMatcher<Exclude<R, R2>, E>
47
+
48
+ readonly provideService: <R2>(
49
+ tag: Context.Tag<R2>,
50
+ service: R2,
51
+ ) => RouteMatcher<Exclude<R, R2>, E>
52
+
53
+ readonly provideLayer: <R2, S>(
54
+ layer: Layer.Layer<R2, never, S>,
55
+ ) => RouteMatcher<Exclude<R, S> | R2, E>
56
+
57
+ // Runners that turn a RouterMatcher back into an Fx.
58
+ // Error handling should be handled after converting to an Fx for maximum flexibility.
59
+
60
+ readonly notFound: <R2, E2, R3 = never, E3 = never>(
61
+ f: (path: string) => Fx.Fx<R2, E2, html.Renderable>,
62
+ options?: FallbackOptions<R3, E3>,
63
+ ) => Fx.Fx<Router | R | R2 | R3, E | E2 | E3, html.Renderable>
64
+
65
+ readonly notFoundEffect: <R2, E2, R3 = never, E3 = never>(
66
+ f: (path: string) => Effect.Effect<R2, E2, html.Renderable>,
67
+ options?: FallbackOptions<R3, E3>,
68
+ ) => Fx.Fx<Router | R | R2 | R3, E | E2 | E3, html.Renderable>
69
+
70
+ readonly redirectTo: <R2, P extends string>(
71
+ route: Route.Route<R2, P>,
72
+ ...params: [keyof Path.ParamsOf<P>] extends [never]
73
+ ? // eslint-disable-next-line @typescript-eslint/ban-types
74
+ [{}?]
75
+ : [(path: string) => Path.ParamsOf<P>]
76
+ ) => Fx.Fx<Router | R | R2, E | Redirect, html.Renderable>
77
+
78
+ /**
79
+ * @internal
80
+ */
81
+ readonly run: Fx.Fx<Router | R, E | Redirect, html.Renderable | null>
82
+ }
83
+
84
+ export interface FallbackOptions<R, E> {
85
+ readonly layout?: Fx.Fx<R, E, html.Renderable>
86
+ }
87
+
88
+ export function RouteMatcher<R, E>(routes: RouteMatcher<R, E>['routes']): RouteMatcher<R, E> {
89
+ const matcher: RouteMatcher<R, E> = {
90
+ routes,
91
+ matchFx: (route, f) => RouteMatcher(new Map(routes).set(route, RouteMatch(route, f))),
92
+ match: (route, f) =>
93
+ RouteMatcher(new Map(routes).set(route, RouteMatch(route, Fx.switchMap(f)))),
94
+ matchEffect: (route, f) =>
95
+ RouteMatcher(new Map(routes).set(route, RouteMatch(route, Fx.switchMapEffect(f)))),
96
+ withLayout: (layout) =>
97
+ RouteMatcher(
98
+ new Map(
99
+ Array.from(routes).map(([k, match]) => [
100
+ k,
101
+ RouteMatch(match.route, match.match, match.layout ?? layout),
102
+ ]),
103
+ ),
104
+ ),
105
+ provideEnvironment: (environment) =>
106
+ RouteMatcher(
107
+ new Map(Array.from(routes).map(([k, v]) => [k, v.provideEnvironment(environment)])),
108
+ ),
109
+ provideService: (tag, service) =>
110
+ RouteMatcher(
111
+ new Map(Array.from(routes).map(([k, v]) => [k, v.provideService(tag, service)])),
112
+ ),
113
+ provideLayer: (layer) =>
114
+ RouteMatcher(new Map(Array.from(routes).map(([k, v]) => [k, v.provideLayer(layer)]))),
115
+ notFound: <R2, E2, R3, E3>(
116
+ f: (path: string) => Fx.Fx<R2, E2, html.Renderable>,
117
+ options: FallbackOptions<R3, E3> = {},
118
+ ) =>
119
+ Router.withFx((router) =>
120
+ Fx.gen(function* ($) {
121
+ const { environment } = yield* $(RenderContext.get)
122
+ // Create stable references to the route matchers
123
+ const matchers = Array.from(routes.values()).map(
124
+ (v) => [v, runRouteMatch(router, v)] as const,
125
+ )
126
+
127
+ const renderFallback = pipe(currentPath, Fx.switchMap(f))
128
+ const fallbackMatch = RouteMatch(Route.base, () => renderFallback, options?.layout)
129
+
130
+ let previousFiber: Fiber.RuntimeFiber<any, any> | undefined
131
+ let previousLayout: Fx.Fx<any, any, html.Renderable> | undefined
132
+ let previousRender: Fx.Fx<any, any, html.Renderable> | undefined
133
+
134
+ const samplePreviousValues = () => ({
135
+ fiber: previousFiber,
136
+ layout: previousLayout,
137
+ render: previousRender,
138
+ })
139
+
140
+ // This function helps us to ensure shared layouts are only rendered once
141
+ // and the outlet content is changed
142
+ const verifyShouldRerender = (
143
+ match: RouteMatch<any, any, any>,
144
+ render: Fx.Fx<any, any, html.Renderable>,
145
+ ): Effect.Effect<any, any, Option.Option<Fx.Fx<any, any, html.Renderable>>> =>
146
+ Effect.gen(function* ($) {
147
+ const previous = samplePreviousValues()
148
+
149
+ // Update the previous values
150
+ previousRender = render
151
+ previousLayout = match.layout
152
+ previousFiber = undefined
153
+
154
+ // Skip rerendering if the render function is the same
155
+ if (previous.render === render) {
156
+ return Option.none
157
+ }
158
+
159
+ // Interrupt the previous fiber if it exists
160
+ if (previous.fiber) {
161
+ yield* $(Fiber.interrupt(previous.fiber))
162
+ }
163
+
164
+ // If we have a layout, we need to render it and use the route outlet.
165
+ if (match.layout) {
166
+ // Render into the route outlet
167
+ previousFiber = yield* $(
168
+ pipe(render, Fx.observe(router.outlet.set), Effect.forkScoped),
169
+ )
170
+
171
+ return Option.some(match.layout)
172
+ }
173
+
174
+ // If we don't have a layout, but we did, we need to clear the outlet
175
+ if (previous.layout) {
176
+ yield* $(router.outlet.set(null))
177
+ }
178
+
179
+ // Otherwise use the render function directly
180
+ return Option.some(render)
181
+ })
182
+
183
+ return pipe(
184
+ router.currentPath,
185
+ environment === 'browser' ? Fx.skipRepeats : Fx.take(1),
186
+ Fx.switchMapEffect((path) =>
187
+ Effect.gen(function* ($) {
188
+ yield* $(Effect.logDebug(`[@typed/router] Matching path: ${path}.`))
189
+
190
+ // Attempt to find the best match
191
+ for (const [match, render] of matchers) {
192
+ yield* $(
193
+ Effect.logDebug(`[@typed/router] Matching against: ${match.route.path}.`),
194
+ )
195
+
196
+ const result = yield* $(match.route.match(path))
197
+
198
+ if (Option.isSome(result)) {
199
+ yield* $(
200
+ Effect.logDebug(`[@typed/router] Matched against: ${match.route.path}.`),
201
+ )
202
+
203
+ return yield* $(verifyShouldRerender(match, render))
204
+ }
205
+ }
206
+
207
+ yield* $(Effect.logDebug(`[@typed/router] Rendering fallback.`))
208
+
209
+ // If we didn't find a match, render the not found page
210
+ return yield* $(verifyShouldRerender(fallbackMatch, renderFallback))
211
+ }),
212
+ ),
213
+ Fx.compact,
214
+ Fx.skipRepeats, // Stable render references are used to avoid mounting the same component twice
215
+ Fx.switchLatest,
216
+ )
217
+ }),
218
+ ),
219
+ notFoundEffect: (f) => matcher.notFound((path) => Fx.fromEffect(f(path))),
220
+ redirectTo: ((route, ...params) =>
221
+ matcher.notFound(() => redirectTo.fx(route, ...params))) as RouteMatcher<R, E>['redirectTo'],
222
+ run: Fx.suspend(() => matcher.notFoundEffect(Effect.never)),
223
+ }
224
+
225
+ return matcher
226
+ }
227
+
228
+ export namespace RouteMatcher {
229
+ export const empty = RouteMatcher<never, never>(new Map())
230
+
231
+ export const concat = <R, E, R2, E2>(
232
+ matcher: RouteMatcher<R, E>,
233
+ matcher2: RouteMatcher<R2, E2>,
234
+ ): RouteMatcher<R | R2, E | E2> => RouteMatcher(new Map([...matcher.routes, ...matcher2.routes]))
235
+ }
236
+
237
+ export const { matchFx, match, matchEffect } = RouteMatcher<never, never>(new Map())
238
+
239
+ function runRouteMatch<R, E, P extends string>(
240
+ router: Router,
241
+ { route, match }: RouteMatch<R, E, P>,
242
+ ): Fx.Fx<R, E, html.Renderable> {
243
+ return Fx.gen(function* ($) {
244
+ const env = yield* $(Effect.environment<R>())
245
+ const nestedRouter = router.define(route)
246
+ const params = pipe(nestedRouter.params, Fx.provideEnvironment(env))
247
+
248
+ return pipe(
249
+ match(params as unknown as Fx.Fx<never, never, Path.ParamsOf<P>>),
250
+ Router.provideFx(nestedRouter as Router),
251
+ )
252
+ })
253
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './router.js'
2
+ export * from './Link.js'
3
+ export * from './RouteMatcher.js'
4
+ export * from './RouteMatch.js'
package/src/router.ts ADDED
@@ -0,0 +1,279 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
2
+ import * as Effect from '@effect/io/Effect'
3
+ import * as Layer from '@effect/io/Layer'
4
+ import * as Scope from '@effect/io/Scope'
5
+ import { pipe } from '@fp-ts/data/Function'
6
+ import * as Option from '@fp-ts/data/Option'
7
+ import * as Context from '@typed/context'
8
+ import { Location, History, addWindowListener } from '@typed/dom'
9
+ import * as Fx from '@typed/fx'
10
+ import * as html from '@typed/html'
11
+ import { RenderContext } from '@typed/html'
12
+ import * as Path from '@typed/path'
13
+ import * as Route from '@typed/route'
14
+
15
+ export interface Router<out R = never, in out P extends string = string> {
16
+ /**
17
+ * The base route the Router is starting from.
18
+ */
19
+ readonly route: Route.Route<R, P>
20
+
21
+ /**
22
+ * The current path of the application
23
+ */
24
+ readonly currentPath: Fx.RefSubject<string>
25
+
26
+ /**
27
+ * The current matched params of the router
28
+ */
29
+ readonly params: Fx.Fx<R, never, Path.ParamsOf<P>>
30
+
31
+ /**
32
+ * The current outlet of this Router
33
+ */
34
+ readonly outlet: Fx.RefSubject<html.Renderable>
35
+
36
+ /**
37
+ * Helper for constructing a path from a route relative to the router.
38
+ */
39
+ readonly createPath: <R2 extends Route.Route<any, string>, P extends Route.ParamsOf<R2>>(
40
+ route: R2,
41
+ ...[params]: [keyof P] extends [never] ? [] : [P]
42
+ ) => Effect.Effect<
43
+ R,
44
+ never,
45
+ Path.PathJoin<
46
+ [Path.Interpolate<Route.PathOf<R>, Route.ParamsOf<R>>, Path.Interpolate<Route.PathOf<R2>, P>]
47
+ >
48
+ >
49
+
50
+ /**
51
+ * Helper for constructing a nested router
52
+ */
53
+ readonly define: <R2, Path2 extends string>(
54
+ route: Route.Route<R2, Path2>,
55
+ ) => Router<R | R2, Path.PathJoin<[P, Path2]>>
56
+
57
+ /**
58
+ * Provide all the resources needed for a Router
59
+ */
60
+ readonly provideEnvironment: (environment: Context.Context<R>) => Router<never, P>
61
+ }
62
+
63
+ export const Router = Object.assign(function makeRouter<R = never, P extends string = string>(
64
+ route: Route.Route<R, P>,
65
+ currentPath: Fx.RefSubject<string>,
66
+ ): Router<R, P> {
67
+ const outlet = Fx.RefSubject.unsafeMake((): html.Renderable => null)
68
+
69
+ const createPath = <R2 extends Route.Route<any, string>, P extends Route.ParamsOf<R2>>(
70
+ other: R2,
71
+ ...[params]: [keyof P] extends [never] ? [] : [P]
72
+ ): Effect.Effect<
73
+ R,
74
+ never,
75
+ Path.PathJoin<
76
+ [Path.Interpolate<Route.PathOf<R>, Route.ParamsOf<R>>, Path.Interpolate<Route.PathOf<R2>, P>]
77
+ >
78
+ > =>
79
+ Effect.gen(function* ($) {
80
+ const path = yield* $(currentPath.get)
81
+ const baseParams = yield* $(route.match(path))
82
+
83
+ if (Option.isNone(baseParams)) {
84
+ return yield* $(
85
+ Effect.dieMessage(
86
+ `Can not create path when the parent can not be matched.
87
+ Parent Route: ${route.path}
88
+ Current Route: ${other.path}
89
+ Current Path: ${path}`,
90
+ ),
91
+ )
92
+ }
93
+
94
+ return route.concat(other).make({ ...baseParams.value, ...params } as any) as any
95
+ })
96
+
97
+ const router: Router<R, P> = {
98
+ route,
99
+ currentPath,
100
+ params: pipe(currentPath, Fx.switchMapEffect(route.match), Fx.compact, Fx.skipRepeats),
101
+ outlet,
102
+ createPath: createPath as Router<R, P>['createPath'],
103
+ define: <R2, Path2 extends string>(other: Route.Route<R2, Path2>) =>
104
+ makeRouter(route.concat(other), currentPath),
105
+ provideEnvironment: (env) => provideEnvironment(env)(router),
106
+ }
107
+
108
+ return router
109
+ },
110
+ Context.Tag<Router>())
111
+
112
+ export const outlet: Fx.Fx<RenderContext | Router, never, html.Renderable> = RenderContext.withFx(
113
+ ({ environment }) =>
114
+ Router.withFx((r) =>
115
+ environment === 'browser'
116
+ ? r.outlet
117
+ : pipe(
118
+ r.outlet,
119
+ Fx.skipUntil((x) => x !== null),
120
+ Fx.take(1),
121
+ ),
122
+ ),
123
+ )
124
+
125
+ export const currentPath: Fx.Fx<Router, never, string> = Router.withFx((r) => r.currentPath)
126
+
127
+ export function provideEnvironment<R>(environment: Context.Context<R>) {
128
+ return <P extends string>(router: Router<R, P>): Router<never, P> => {
129
+ const provided: Router<never, P> = {
130
+ ...router,
131
+ params: pipe(router.params, Fx.provideEnvironment(environment)),
132
+ route: Route.provideEnvironment(environment)(router.route),
133
+ createPath: ((other, ...params) =>
134
+ Effect.provideEnvironment<R>(environment)(router.createPath(other, ...params))) as Router<
135
+ never,
136
+ P
137
+ >['createPath'],
138
+ provideEnvironment: (env) => provideEnvironment(env)(provided),
139
+ }
140
+
141
+ return provided
142
+ }
143
+ }
144
+
145
+ export interface Redirect {
146
+ readonly _tag: 'Redirect'
147
+ readonly path: string
148
+ }
149
+
150
+ export namespace Redirect {
151
+ export const make = (path: string): Redirect => ({ _tag: 'Redirect', path })
152
+
153
+ export const is = (r: unknown): r is Redirect =>
154
+ typeof r === 'object' && r !== null && '_tag' in r && r._tag === 'Redirect'
155
+ }
156
+
157
+ export function redirect(path: string) {
158
+ return Effect.fail<Redirect>(Redirect.make(path))
159
+ }
160
+
161
+ redirect.fx = (path: string) => Fx.fail<Redirect>(Redirect.make(path))
162
+
163
+ export const redirectTo = <R, P extends string>(
164
+ route: Route.Route<R, P>,
165
+ ...[params]: [keyof Path.ParamsOf<P>] extends [never] ? [{}?] : [Path.ParamsOf<P>]
166
+ ): Effect.Effect<Router, Redirect, never> =>
167
+ pipe(
168
+ Router.withEffect((r) => r.createPath(route as any, params as any)),
169
+ Effect.flatMap(redirect),
170
+ )
171
+
172
+ redirectTo.fx = <R, P extends string>(
173
+ route: Route.Route<R, P>,
174
+ ...params: [keyof Path.ParamsOf<P>] extends [never] ? [{}?] : [(path: string) => Path.ParamsOf<P>]
175
+ ): Fx.Fx<Router, Redirect, never> =>
176
+ pipe(
177
+ Router.withEffect((r) => r.createPath(route as any, params as any)),
178
+ Fx.fromEffect,
179
+ Fx.switchMap(redirect.fx),
180
+ )
181
+
182
+ export const makeRouter = (
183
+ currentPath?: Fx.RefSubject<string>,
184
+ ): Effect.Effect<Location | History | Window | Document | Scope.Scope, never, Router> =>
185
+ Effect.gen(function* ($) {
186
+ const history = yield* $(History.get)
187
+ const location = yield* $(Location.get)
188
+
189
+ if (!currentPath) {
190
+ currentPath = Fx.RefSubject.unsafeMake(() => getCurrentPath(location))
191
+ }
192
+
193
+ // Patch history events to emit an event when the path changes
194
+ const historyEvents = yield* $(patchHistory)
195
+
196
+ // Update the current path when events occur:
197
+ // - popstate
198
+ // - hashchange
199
+ // - history events
200
+ yield* $(
201
+ pipe(
202
+ Fx.mergeAll(addWindowListener('popstate'), addWindowListener('hashchange'), historyEvents),
203
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204
+ Fx.switchMapEffect(() => currentPath!.set(getCurrentPath(location))),
205
+ Fx.forkScoped,
206
+ ),
207
+ )
208
+
209
+ // Listen to path changes and update the current history location, if necessary
210
+ yield* $(
211
+ pipe(
212
+ currentPath,
213
+ Fx.skipRepeats,
214
+ Fx.observe((path) =>
215
+ Effect.sync(() => {
216
+ if (path !== getCurrentPath(location)) {
217
+ history.pushState({}, '', path)
218
+ }
219
+ }),
220
+ ),
221
+ Effect.forkScoped,
222
+ ),
223
+ )
224
+
225
+ // Make our base router
226
+ return Router(Route.base, currentPath) as Router
227
+ })
228
+
229
+ export const live = (
230
+ currentPath?: Fx.RefSubject<string>,
231
+ ): Layer.Layer<Location | History | Window | Document, never, Router<never, string>> =>
232
+ Router.layerSoped(makeRouter(currentPath))
233
+
234
+ export function getCurrentPath(location: Location | HTMLAnchorElement) {
235
+ return location.pathname + location.search + location.hash
236
+ }
237
+
238
+ const patchHistory = Effect.gen(function* ($) {
239
+ const history = yield* $(History.get)
240
+ const historyEvents = Fx.Subject.unsafeMake<never, void>()
241
+ const runtime = yield* $(Effect.runtime<never>())
242
+
243
+ patchHistory_(history, () => runtime.unsafeRun(historyEvents.event()))
244
+
245
+ return historyEvents
246
+ })
247
+
248
+ function patchHistory_(history: History, sendEvent: () => void) {
249
+ const pushState = history.pushState.bind(history)
250
+ const replaceState = history.replaceState.bind(history)
251
+ const go = history.go.bind(history)
252
+ const back = history.back.bind(history)
253
+ const forward = history.forward.bind(history)
254
+
255
+ history.pushState = function (state, title, url) {
256
+ pushState(state, title, url)
257
+ sendEvent()
258
+ }
259
+
260
+ history.replaceState = function (state, title, url) {
261
+ replaceState(state, title, url)
262
+ sendEvent()
263
+ }
264
+
265
+ history.go = function (delta) {
266
+ go(delta)
267
+ sendEvent()
268
+ }
269
+
270
+ history.back = function () {
271
+ back()
272
+ sendEvent()
273
+ }
274
+
275
+ history.forward = function () {
276
+ forward()
277
+ sendEvent()
278
+ }
279
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["src/**/*.test.ts"]
4
+ }