@typed/ui 1.0.0-beta.0 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +78 -26
- package/dist/Alert.d.ts +72 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +40 -0
- package/dist/Button.d.ts +100 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +42 -0
- package/dist/Carousel.d.ts +346 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +264 -0
- package/dist/Checkbox.d.ts +168 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +146 -0
- package/dist/Collection.d.ts +254 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +218 -0
- package/dist/Combobox.d.ts +367 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +300 -0
- package/dist/Composite.d.ts +823 -0
- package/dist/Composite.d.ts.map +1 -0
- package/dist/Composite.js +615 -0
- package/dist/Dialog.d.ts +544 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +357 -0
- package/dist/Disclosure.d.ts +219 -0
- package/dist/Disclosure.d.ts.map +1 -0
- package/dist/Disclosure.js +128 -0
- package/dist/Dom/Events.d.ts +122 -0
- package/dist/Dom/Events.d.ts.map +1 -0
- package/dist/Dom/Events.js +192 -0
- package/dist/Dom/Props.d.ts +161 -0
- package/dist/Dom/Props.d.ts.map +1 -0
- package/dist/Dom/Props.js +110 -0
- package/dist/Dom/Refs.d.ts +58 -0
- package/dist/Dom/Refs.d.ts.map +1 -0
- package/dist/Dom/Refs.js +61 -0
- package/dist/Dom/Render.d.ts +59 -0
- package/dist/Dom/Render.d.ts.map +1 -0
- package/dist/Dom/Render.js +71 -0
- package/dist/Dom/Types.d.ts +570 -0
- package/dist/Dom/Types.d.ts.map +1 -0
- package/dist/Dom/Types.js +1 -0
- package/dist/Dom/index.d.ts +20 -0
- package/dist/Dom/index.d.ts.map +1 -0
- package/dist/Dom/index.js +8 -0
- package/dist/Dom.d.ts +14 -0
- package/dist/Dom.d.ts.map +1 -0
- package/dist/Dom.js +13 -0
- package/dist/Focusable.d.ts +85 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Focusable.js +35 -0
- package/dist/Form.d.ts +1727 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +1142 -0
- package/dist/Grid.d.ts +388 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +284 -0
- package/dist/Group.d.ts +128 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +71 -0
- package/dist/Heading.d.ts +87 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +58 -0
- package/dist/Hovercard.d.ts +297 -0
- package/dist/Hovercard.d.ts.map +1 -0
- package/dist/Hovercard.js +188 -0
- package/dist/HttpRouter.d.ts +129 -6
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +199 -56
- package/dist/Link.d.ts +63 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +84 -37
- package/dist/Listbox.d.ts +305 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +245 -0
- package/dist/Menu.d.ts +663 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +569 -0
- package/dist/Menubar.d.ts +249 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +207 -0
- package/dist/Meter.d.ts +157 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +87 -0
- package/dist/NativeDetails.d.ts +41 -0
- package/dist/NativeDetails.d.ts.map +1 -0
- package/dist/NativeDetails.js +40 -0
- package/dist/NativeDialog.d.ts +66 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +88 -0
- package/dist/NativePopover.d.ts +43 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +84 -0
- package/dist/Popover.d.ts +240 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +140 -0
- package/dist/RadioGroup.d.ts +330 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +241 -0
- package/dist/Role.d.ts +64 -0
- package/dist/Role.d.ts.map +1 -0
- package/dist/Role.js +27 -0
- package/dist/Select.d.ts +418 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +357 -0
- package/dist/Separator.d.ts +58 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +32 -0
- package/dist/Slider.d.ts +141 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +101 -0
- package/dist/SpinButton.d.ts +141 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +101 -0
- package/dist/Storybook.d.ts +76 -0
- package/dist/Storybook.d.ts.map +1 -0
- package/dist/Storybook.js +102 -0
- package/dist/Switch.d.ts +148 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +110 -0
- package/dist/Tab.d.ts +26 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +25 -0
- package/dist/Tabs.d.ts +411 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +262 -0
- package/dist/Toolbar.d.ts +248 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +187 -0
- package/dist/Tooltip.d.ts +296 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +172 -0
- package/dist/Tree.d.ts +405 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +333 -0
- package/dist/TreeGrid.d.ts +426 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +308 -0
- package/dist/VisuallyHidden.d.ts +68 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +44 -0
- package/dist/WindowSplitter.d.ts +336 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +305 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/package.json +47 -14
- package/src/HttpRouter.test.ts +0 -294
- package/src/HttpRouter.ts +0 -168
- package/src/Link.test.ts +0 -85
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -6
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { RefSubject } from "@typed/fx";
|
|
4
|
+
import { EventHandler, html, } from "@typed/template";
|
|
5
|
+
import * as Dom from "./Dom.js";
|
|
6
|
+
import * as NativePopover from "./NativePopover.js";
|
|
7
|
+
/** Schema for hovercard hydration state.
|
|
8
|
+
* @remarks
|
|
9
|
+
* ## Why
|
|
10
|
+
* Shared identity/open encoding keeps SSR and browser state compatible.
|
|
11
|
+
* ## Ownership and lifetime
|
|
12
|
+
* The immutable schema acquires no resources.
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
* @category Open state
|
|
15
|
+
*/
|
|
16
|
+
export const StateSchema = Schema.Struct({ id: Schema.String, open: Schema.Boolean });
|
|
17
|
+
/** Creates hydrated hovercard state.
|
|
18
|
+
* @remarks
|
|
19
|
+
* ## Why
|
|
20
|
+
* Visibility and identity can be tested without mounting UI.
|
|
21
|
+
* ## Ownership and lifetime
|
|
22
|
+
* The calling Effect Scope owns the returned RefSubject.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { Effect } from "effect"
|
|
26
|
+
* import * as Hovercard from "@typed/ui/Hovercard"
|
|
27
|
+
*
|
|
28
|
+
* const program = Effect.gen(function* () {
|
|
29
|
+
* return yield* Hovercard.makeState({ id: "author-card" })
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
* @since 1.0.0
|
|
33
|
+
* @category Open state
|
|
34
|
+
*/
|
|
35
|
+
export function makeState(initial) {
|
|
36
|
+
return RefSubject.hydrate(StateSchema, { id: initial.id, open: initial.open ?? false });
|
|
37
|
+
}
|
|
38
|
+
/** Sets hovercard visibility.
|
|
39
|
+
* @remarks
|
|
40
|
+
* ## Why
|
|
41
|
+
* Delayed focus/hover flows converge on one atomic state update.
|
|
42
|
+
* ## Ownership and lifetime
|
|
43
|
+
* The Effect uses the existing RefSubject lifetime and acquires no resource.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Effect } from "effect"
|
|
47
|
+
* import * as Hovercard from "@typed/ui/Hovercard"
|
|
48
|
+
*
|
|
49
|
+
* const program = Effect.gen(function* () {
|
|
50
|
+
* const state = yield* Hovercard.makeState({ id: "author-card" })
|
|
51
|
+
* yield* Hovercard.setOpen(state, true)
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
* @category Open state
|
|
56
|
+
*/
|
|
57
|
+
export function setOpen(state, open) {
|
|
58
|
+
return RefSubject.update(state, (current) => ({ ...current, open }));
|
|
59
|
+
}
|
|
60
|
+
const scheduleVersions = new WeakMap();
|
|
61
|
+
const scheduleOpen = Effect.fn(function* (state, open, delay) {
|
|
62
|
+
const version = (scheduleVersions.get(state) ?? 0) + 1;
|
|
63
|
+
scheduleVersions.set(state, version);
|
|
64
|
+
if (delay > 0)
|
|
65
|
+
yield* Effect.sleep(delay);
|
|
66
|
+
if (scheduleVersions.get(state) === version)
|
|
67
|
+
yield* setOpen(state, open);
|
|
68
|
+
});
|
|
69
|
+
function anchorInternalProps(options) {
|
|
70
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
71
|
+
return () => ({
|
|
72
|
+
"aria-controls": id,
|
|
73
|
+
onfocus: scheduleOpen(options.state, true, options.showDelay ?? 0),
|
|
74
|
+
onblur: EventHandler.make(Effect.fn(function* (event) {
|
|
75
|
+
const contentId = (yield* options.state).id;
|
|
76
|
+
const content = Dom.currentTarget(event).ownerDocument.getElementById(contentId);
|
|
77
|
+
if (event.relatedTarget instanceof Node && content?.contains(event.relatedTarget))
|
|
78
|
+
return;
|
|
79
|
+
yield* scheduleOpen(options.state, false, options.hideDelay ?? 0);
|
|
80
|
+
})),
|
|
81
|
+
onkeydown: EventHandler.make(Effect.fn(function* (event) {
|
|
82
|
+
if (event.key === "Escape")
|
|
83
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
84
|
+
})),
|
|
85
|
+
onmouseenter: scheduleOpen(options.state, true, options.showDelay ?? 0),
|
|
86
|
+
onmouseleave: scheduleOpen(options.state, false, options.hideDelay ?? 0),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Renders an anchor coordinating pointer and optional host-focus behavior.
|
|
90
|
+
* @remarks
|
|
91
|
+
* ## Why
|
|
92
|
+
* The default host is a non-focusable `<span>`, so it provides pointer behavior
|
|
93
|
+
* only. Give the span `tabindex`, or supply a natively focusable custom host,
|
|
94
|
+
* for direct focus, blur, and keydown handling. Unlike a tooltip, hovercard
|
|
95
|
+
* content may receive focus; host blur checks `relatedTarget` and stays open
|
|
96
|
+
* while focus moves inside the card. Nested-anchor focus does not bubble.
|
|
97
|
+
* ## Ownership and lifetime
|
|
98
|
+
* Running the Fx owns real DOM handlers and dynamic relationships in its Scope.
|
|
99
|
+
* Custom hosts must preserve `aria-controls` and all supplied handlers.
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { Effect } from "effect"
|
|
103
|
+
* import * as Hovercard from "@typed/ui/Hovercard"
|
|
104
|
+
*
|
|
105
|
+
* const program = Effect.gen(function* () {
|
|
106
|
+
* const state = yield* Hovercard.makeState({ id: "author-card" })
|
|
107
|
+
* return Hovercard.Anchor({
|
|
108
|
+
* state,
|
|
109
|
+
* content: "Ada Lovelace",
|
|
110
|
+
* props: { tabindex: 0 }
|
|
111
|
+
* })
|
|
112
|
+
* })
|
|
113
|
+
* ```
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
* @category Pointer and focus anchors
|
|
116
|
+
*/
|
|
117
|
+
export function Anchor(options, host) {
|
|
118
|
+
return Dom.renderHost()(options, host, anchorInternalProps(options), options.content, (props, content) => html `<span ...${props}>${content}</span>`);
|
|
119
|
+
}
|
|
120
|
+
function contentInternalProps(options) {
|
|
121
|
+
const id = RefSubject.map(options.state, (state) => state.id);
|
|
122
|
+
return ({ property }) => ({
|
|
123
|
+
id,
|
|
124
|
+
role: "dialog",
|
|
125
|
+
"aria-label": property("label", undefined),
|
|
126
|
+
"aria-labelledby": property("labelledBy", undefined),
|
|
127
|
+
popover: "manual",
|
|
128
|
+
onfocusin: scheduleOpen(options.state, true, 0),
|
|
129
|
+
onfocusout: EventHandler.make(Effect.fn(function* (event) {
|
|
130
|
+
const content = Dom.currentTarget(event);
|
|
131
|
+
if (event.relatedTarget instanceof Node && content.contains(event.relatedTarget))
|
|
132
|
+
return;
|
|
133
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
134
|
+
})),
|
|
135
|
+
onmouseenter: scheduleOpen(options.state, true, 0),
|
|
136
|
+
onmouseleave: EventHandler.make(Effect.fn(function* (event) {
|
|
137
|
+
const contentId = (yield* options.state).id;
|
|
138
|
+
if (event.relatedTarget instanceof Element &&
|
|
139
|
+
event.relatedTarget.getAttribute("aria-controls") === contentId)
|
|
140
|
+
return;
|
|
141
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
142
|
+
})),
|
|
143
|
+
onkeydown: EventHandler.make(Effect.fn(function* (event) {
|
|
144
|
+
if (event.key === "Escape")
|
|
145
|
+
yield* scheduleOpen(options.state, false, 0);
|
|
146
|
+
})),
|
|
147
|
+
ontoggle: EventHandler.make(Effect.fn((event) => setOpen(options.state, Dom.toggleState(event) === "open"))),
|
|
148
|
+
ref: Dom.composeRefs(options.state, NativePopover.ref(options.state)),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Renders named, focusable hovercard content in the native top layer.
|
|
152
|
+
* @remarks
|
|
153
|
+
* ## Why
|
|
154
|
+
* `role="dialog"` distinguishes interactive content from a tooltip. Native
|
|
155
|
+
* popover placement is retained, and focus/pointer transitions prevent closing
|
|
156
|
+
* while interaction remains inside the card.
|
|
157
|
+
* ## Ownership and lifetime
|
|
158
|
+
* Running the Fx owns handlers and NativePopover observation in its Scope. A
|
|
159
|
+
* custom host must preserve id, role, accessible name, manual popover,
|
|
160
|
+
* lifecycle handlers, and one hydration ref owner.
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { Effect } from "effect"
|
|
164
|
+
* import * as Hovercard from "@typed/ui/Hovercard"
|
|
165
|
+
*
|
|
166
|
+
* const program = Effect.gen(function* () {
|
|
167
|
+
* const state = yield* Hovercard.makeState({ id: "author-card" })
|
|
168
|
+
* return Hovercard.Content({ state, label: "Author", content: "Mathematician" })
|
|
169
|
+
* })
|
|
170
|
+
* ```
|
|
171
|
+
* @since 1.0.0
|
|
172
|
+
* @category Native content host
|
|
173
|
+
*/
|
|
174
|
+
export function Content(options, host) {
|
|
175
|
+
return Dom.renderHost()(options, host, contentInternalProps(options), options.content, (i, content) => {
|
|
176
|
+
return html `<div ...${i}>${content}</div>`;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Canonical widget alias for `Content`.
|
|
180
|
+
* @remarks
|
|
181
|
+
* ## Why
|
|
182
|
+
* The alias provides the widget name while `Content` names compound use.
|
|
183
|
+
* ## Ownership and lifetime
|
|
184
|
+
* It has exactly the same Scope and native popover ownership as `Content`.
|
|
185
|
+
* @since 1.0.0
|
|
186
|
+
* @category Native content host
|
|
187
|
+
*/
|
|
188
|
+
export const Hovercard = Content;
|
package/dist/HttpRouter.d.ts
CHANGED
|
@@ -1,13 +1,136 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { type HttpRouter, type Provided as HttpRouterProvided, type Request as HttpRouterRequest } from "effect/unstable/http/HttpRouter";
|
|
3
|
+
import * as HttpServerError from "effect/unstable/http/HttpServerError";
|
|
4
4
|
import { type Matcher, type Router } from "@typed/router";
|
|
5
|
+
import { type Navigation } from "@typed/navigation";
|
|
5
6
|
import { type RenderEvent } from "@typed/template";
|
|
6
|
-
type ProvidedForSsr =
|
|
7
|
+
type ProvidedForSsr = HttpRouterProvided | Navigation | Router;
|
|
8
|
+
type SsrForHttpRequirement<E, R> = Exclude<R, ProvidedForSsr> | HttpRouterRequest.From<"Error", E | HttpServerError.HttpServerError>;
|
|
9
|
+
type SsrForHttpEffect<E, R> = Effect.Effect<void, never, SsrForHttpRequirement<E, R>>;
|
|
10
|
+
/**
|
|
11
|
+
* Registers buffered, GET-only HTML routes for a matcher.
|
|
12
|
+
*
|
|
13
|
+
* Path captures take precedence over same-named query parameters. Rendering
|
|
14
|
+
* completes before the response is created; this adapter does not stream.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* ## Why
|
|
18
|
+
*
|
|
19
|
+
* Typed route matchers already describe decoding, guards, parent prefixes, and
|
|
20
|
+
* render output. `ssrForHttp` installs that same model in Effect's HttpRouter
|
|
21
|
+
* and buffers one complete HTML document for runtimes that require a known body.
|
|
22
|
+
*
|
|
23
|
+
* ## Ownership and lifetime
|
|
24
|
+
*
|
|
25
|
+
* Registration adds GET routes to the supplied Effect HttpRouter. Each request
|
|
26
|
+
* creates request-local memory navigation and current-route services; the
|
|
27
|
+
* request Effect owns rendering and all finalizers. Buffered rendering must
|
|
28
|
+
* complete before the response is returned. Rendering HTML does not mount or
|
|
29
|
+
* hydrate a browser client.
|
|
30
|
+
*
|
|
31
|
+
* ## Routing behavior
|
|
32
|
+
*
|
|
33
|
+
* Matcher cases sharing a path are tried in their compiled order. Path params
|
|
34
|
+
* overwrite same-named query params. Decode/guard failures continue candidate
|
|
35
|
+
* selection; a matched renderer failure stays in the typed request error channel.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { ssrForHttp } from "@typed/ui/HttpRouter"
|
|
40
|
+
* import { Effect } from "effect"
|
|
41
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
42
|
+
* import { Parse, match } from "@typed/router"
|
|
43
|
+
* import { html } from "@typed/template"
|
|
44
|
+
*
|
|
45
|
+
* const pages = match(Parse("/"), html`<h1>Home</h1>`)
|
|
46
|
+
* const routes = Effect.gen(function* () {
|
|
47
|
+
* const router = yield* HttpRouter.make
|
|
48
|
+
* yield* ssrForHttp(router, pages)
|
|
49
|
+
* return router
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category Buffered server rendering
|
|
55
|
+
*/
|
|
7
56
|
export declare const ssrForHttp: {
|
|
8
|
-
<E, R>(input: Matcher<RenderEvent, E, R>): (router: HttpRouter) =>
|
|
9
|
-
<E, R>(router: HttpRouter, input: Matcher<RenderEvent, E, R>):
|
|
57
|
+
<E, R>(input: Matcher<RenderEvent, E, R>): (router: HttpRouter) => SsrForHttpEffect<E, R>;
|
|
58
|
+
<E, R>(router: HttpRouter, input: Matcher<RenderEvent, E, R>): SsrForHttpEffect<E, R>;
|
|
10
59
|
};
|
|
11
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Registers streaming, GET-only HTML routes for a matcher.
|
|
62
|
+
*
|
|
63
|
+
* Path captures take precedence over same-named query parameters. HTML chunks
|
|
64
|
+
* are emitted as they are rendered via `renderToHtml` and `HttpServerResponse.stream`.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* ## Why
|
|
68
|
+
*
|
|
69
|
+
* Streaming lets the server send ordered renderer-owned HTML chunks before the
|
|
70
|
+
* whole page completes while preserving the same Typed matcher and Effect HTTP model.
|
|
71
|
+
*
|
|
72
|
+
* ## Ownership and lifetime
|
|
73
|
+
*
|
|
74
|
+
* Registration adds GET routes to the supplied router. Each request owns its
|
|
75
|
+
* navigation/router services and stream Scope. Client cancellation interrupts
|
|
76
|
+
* rendering and runs finalizers. The stream carries typed renderer failures;
|
|
77
|
+
* producing HTML alone does not attach client hydration behavior.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { streamingSsrForHttp } from "@typed/ui/HttpRouter"
|
|
82
|
+
* import { Effect } from "effect"
|
|
83
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
84
|
+
* import { Parse, match } from "@typed/router"
|
|
85
|
+
* import { html } from "@typed/template"
|
|
86
|
+
*
|
|
87
|
+
* const pages = match(Parse("/"), html`<h1>Streamed home</h1>`)
|
|
88
|
+
* const routes = Effect.gen(function* () {
|
|
89
|
+
* const router = yield* HttpRouter.make
|
|
90
|
+
* yield* streamingSsrForHttp(router, pages)
|
|
91
|
+
* return router
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @since 1.0.0
|
|
96
|
+
* @category Streaming server rendering
|
|
97
|
+
*/
|
|
98
|
+
export declare const streamingSsrForHttp: {
|
|
99
|
+
<E, R>(input: Matcher<RenderEvent, E, R>): (router: HttpRouter) => SsrForHttpEffect<E, R>;
|
|
100
|
+
<E, R>(router: HttpRouter, input: Matcher<RenderEvent, E, R>): SsrForHttpEffect<E, R>;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Converts Effect HTTP server errors into empty 400, 404, or 500 responses.
|
|
104
|
+
* Other failures remain in the global error channel.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* ## Why
|
|
108
|
+
*
|
|
109
|
+
* Effect's structured server failures have a safe transport status, whereas
|
|
110
|
+
* application/domain errors must remain visible for deliberate handling.
|
|
111
|
+
*
|
|
112
|
+
* ## Ownership and lifetime
|
|
113
|
+
*
|
|
114
|
+
* The middleware is installed on the supplied router and runs in each request
|
|
115
|
+
* lifetime. It allocates no long-lived resource and does not log, swallow, or
|
|
116
|
+
* reclassify non-HttpServerError failures.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { handleHttpServerError } from "@typed/ui/HttpRouter"
|
|
121
|
+
* import { Effect } from "effect"
|
|
122
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
123
|
+
*
|
|
124
|
+
* const router = Effect.gen(function* () {
|
|
125
|
+
* const router = yield* HttpRouter.make
|
|
126
|
+
* yield* handleHttpServerError(router)
|
|
127
|
+
* return router
|
|
128
|
+
* })
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
* @category HTTP error responses
|
|
133
|
+
*/
|
|
134
|
+
export declare function handleHttpServerError(router: HttpRouter): Effect.Effect<void, never, HttpRouterRequest<"GlobalError", unknown>>;
|
|
12
135
|
export {};
|
|
13
136
|
//# sourceMappingURL=HttpRouter.d.ts.map
|
package/dist/HttpRouter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAOxC,OAAO,EACL,KAAK,UAAU,EACf,KAAK,QAAQ,IAAI,kBAAkB,EACnC,KAAK,OAAO,IAAI,iBAAiB,EAGlC,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,eAAe,MAAM,sCAAsC,CAAC;AAGxE,OAAO,EAML,KAAK,OAAO,EAKZ,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAoC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAErF,KAAK,cAAc,GAAG,kBAAkB,GAAG,UAAU,GAAG,MAAM,CAAC;AAE/D,KAAK,qBAAqB,CAAC,CAAC,EAAE,CAAC,IAC3B,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,GAC1B,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,eAAe,CAAC,eAAe,CAAC,CAAC;AAEzE,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AA+EtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,UAAU,EAAE;IACvB,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAClD,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,mBAAmB,EAAE;IAChC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CACjD,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,yEAQvD"}
|
package/dist/HttpRouter.js
CHANGED
|
@@ -1,36 +1,182 @@
|
|
|
1
|
+
import { Fx } from "@typed/fx";
|
|
1
2
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import * as Exit from "effect/Exit";
|
|
3
3
|
import { dual } from "effect/Function";
|
|
4
4
|
import * as Layer from "effect/Layer";
|
|
5
5
|
import * as Option from "effect/Option";
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import { hasProperty } from "effect/Predicate";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import { RouteContext, } from "effect/unstable/http/HttpRouter";
|
|
9
10
|
import * as HttpServerError from "effect/unstable/http/HttpServerError";
|
|
10
11
|
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
11
12
|
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { compile, CurrentRoute, makeRouteExecutor, Join, Parse, } from "@typed/router";
|
|
14
|
+
import { Uuid7State } from "@typed/id/Uuid7";
|
|
14
15
|
import { initialMemory } from "@typed/navigation";
|
|
15
|
-
import { renderToHtmlString } from "@typed/template";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import { renderToHtml, renderToHtmlString } from "@typed/template";
|
|
17
|
+
const htmlResponseHeaders = { "content-type": "text/html; charset=utf-8" };
|
|
18
|
+
const bufferedResponse = () => Effect.fn((rendered, requestServices) => Effect.gen(function* () {
|
|
19
|
+
const html = yield* renderToHtmlString(rendered).pipe(Effect.provideContext(requestServices));
|
|
20
|
+
return HttpServerResponse.text(html, { headers: htmlResponseHeaders });
|
|
21
|
+
}));
|
|
22
|
+
const streamingResponse = () => Effect.fn((rendered, requestServices) => Effect.gen(function* () {
|
|
23
|
+
const context = Context.merge(requestServices, yield* Effect.context());
|
|
24
|
+
return HttpServerResponse.stream(Stream.provideContext(renderToHtml(rendered).pipe(Fx.provideContext(requestServices), Fx.toStream, Stream.encodeText), context), { headers: htmlResponseHeaders });
|
|
25
|
+
}));
|
|
26
|
+
function makeSsrForHttp(createResponse) {
|
|
27
|
+
return dual(2, (router, input) => {
|
|
28
|
+
return Effect.gen(function* () {
|
|
29
|
+
const matcher = Option.match(yield* Effect.serviceOption(CurrentRoute), {
|
|
30
|
+
onNone: () => input,
|
|
31
|
+
onSome: (parent) => input.prefix(parent.route),
|
|
32
|
+
});
|
|
33
|
+
const entries = compile(matcher.cases);
|
|
34
|
+
const currentServices = yield* Effect.context();
|
|
35
|
+
const candidatesByPath = new Map();
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const path = getMatcherPath(entry.route.path);
|
|
38
|
+
const candidates = candidatesByPath.get(path);
|
|
39
|
+
if (candidates === undefined) {
|
|
40
|
+
candidatesByPath.set(path, [entry]);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
candidates.push(entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
yield* router.addAll(Array.from(candidatesByPath, ([path, candidates]) => toRoute(path, candidates, currentServices, createResponse)));
|
|
21
47
|
});
|
|
22
|
-
const entries = compile(matcher.cases);
|
|
23
|
-
const currentServices = yield* Effect.services();
|
|
24
|
-
yield* router.addAll(entries.map((e) => toRoute(e, currentServices)));
|
|
25
48
|
});
|
|
26
|
-
}
|
|
49
|
+
}
|
|
50
|
+
function getMatcherPath(path) {
|
|
51
|
+
const queryStart = path.search(/\?[^/?]+=/);
|
|
52
|
+
return (queryStart < 0 ? path : path.slice(0, queryStart));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Registers buffered, GET-only HTML routes for a matcher.
|
|
56
|
+
*
|
|
57
|
+
* Path captures take precedence over same-named query parameters. Rendering
|
|
58
|
+
* completes before the response is created; this adapter does not stream.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* ## Why
|
|
62
|
+
*
|
|
63
|
+
* Typed route matchers already describe decoding, guards, parent prefixes, and
|
|
64
|
+
* render output. `ssrForHttp` installs that same model in Effect's HttpRouter
|
|
65
|
+
* and buffers one complete HTML document for runtimes that require a known body.
|
|
66
|
+
*
|
|
67
|
+
* ## Ownership and lifetime
|
|
68
|
+
*
|
|
69
|
+
* Registration adds GET routes to the supplied Effect HttpRouter. Each request
|
|
70
|
+
* creates request-local memory navigation and current-route services; the
|
|
71
|
+
* request Effect owns rendering and all finalizers. Buffered rendering must
|
|
72
|
+
* complete before the response is returned. Rendering HTML does not mount or
|
|
73
|
+
* hydrate a browser client.
|
|
74
|
+
*
|
|
75
|
+
* ## Routing behavior
|
|
76
|
+
*
|
|
77
|
+
* Matcher cases sharing a path are tried in their compiled order. Path params
|
|
78
|
+
* overwrite same-named query params. Decode/guard failures continue candidate
|
|
79
|
+
* selection; a matched renderer failure stays in the typed request error channel.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { ssrForHttp } from "@typed/ui/HttpRouter"
|
|
84
|
+
* import { Effect } from "effect"
|
|
85
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
86
|
+
* import { Parse, match } from "@typed/router"
|
|
87
|
+
* import { html } from "@typed/template"
|
|
88
|
+
*
|
|
89
|
+
* const pages = match(Parse("/"), html`<h1>Home</h1>`)
|
|
90
|
+
* const routes = Effect.gen(function* () {
|
|
91
|
+
* const router = yield* HttpRouter.make
|
|
92
|
+
* yield* ssrForHttp(router, pages)
|
|
93
|
+
* return router
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @since 1.0.0
|
|
98
|
+
* @category Buffered server rendering
|
|
99
|
+
*/
|
|
100
|
+
export const ssrForHttp = makeSsrForHttp(bufferedResponse());
|
|
101
|
+
/**
|
|
102
|
+
* Registers streaming, GET-only HTML routes for a matcher.
|
|
103
|
+
*
|
|
104
|
+
* Path captures take precedence over same-named query parameters. HTML chunks
|
|
105
|
+
* are emitted as they are rendered via `renderToHtml` and `HttpServerResponse.stream`.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* ## Why
|
|
109
|
+
*
|
|
110
|
+
* Streaming lets the server send ordered renderer-owned HTML chunks before the
|
|
111
|
+
* whole page completes while preserving the same Typed matcher and Effect HTTP model.
|
|
112
|
+
*
|
|
113
|
+
* ## Ownership and lifetime
|
|
114
|
+
*
|
|
115
|
+
* Registration adds GET routes to the supplied router. Each request owns its
|
|
116
|
+
* navigation/router services and stream Scope. Client cancellation interrupts
|
|
117
|
+
* rendering and runs finalizers. The stream carries typed renderer failures;
|
|
118
|
+
* producing HTML alone does not attach client hydration behavior.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { streamingSsrForHttp } from "@typed/ui/HttpRouter"
|
|
123
|
+
* import { Effect } from "effect"
|
|
124
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
125
|
+
* import { Parse, match } from "@typed/router"
|
|
126
|
+
* import { html } from "@typed/template"
|
|
127
|
+
*
|
|
128
|
+
* const pages = match(Parse("/"), html`<h1>Streamed home</h1>`)
|
|
129
|
+
* const routes = Effect.gen(function* () {
|
|
130
|
+
* const router = yield* HttpRouter.make
|
|
131
|
+
* yield* streamingSsrForHttp(router, pages)
|
|
132
|
+
* return router
|
|
133
|
+
* })
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @since 1.0.0
|
|
137
|
+
* @category Streaming server rendering
|
|
138
|
+
*/
|
|
139
|
+
export const streamingSsrForHttp = makeSsrForHttp(streamingResponse());
|
|
140
|
+
/**
|
|
141
|
+
* Converts Effect HTTP server errors into empty 400, 404, or 500 responses.
|
|
142
|
+
* Other failures remain in the global error channel.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* ## Why
|
|
146
|
+
*
|
|
147
|
+
* Effect's structured server failures have a safe transport status, whereas
|
|
148
|
+
* application/domain errors must remain visible for deliberate handling.
|
|
149
|
+
*
|
|
150
|
+
* ## Ownership and lifetime
|
|
151
|
+
*
|
|
152
|
+
* The middleware is installed on the supplied router and runs in each request
|
|
153
|
+
* lifetime. It allocates no long-lived resource and does not log, swallow, or
|
|
154
|
+
* reclassify non-HttpServerError failures.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* import { handleHttpServerError } from "@typed/ui/HttpRouter"
|
|
159
|
+
* import { Effect } from "effect"
|
|
160
|
+
* import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
161
|
+
*
|
|
162
|
+
* const router = Effect.gen(function* () {
|
|
163
|
+
* const router = yield* HttpRouter.make
|
|
164
|
+
* yield* handleHttpServerError(router)
|
|
165
|
+
* return router
|
|
166
|
+
* })
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
* @category HTTP error responses
|
|
171
|
+
*/
|
|
27
172
|
export function handleHttpServerError(router) {
|
|
28
173
|
return router.addGlobalMiddleware(Effect.catch((error) => HttpServerError.isHttpServerError(error)
|
|
29
|
-
? Effect.succeed(HttpServerResponse.
|
|
174
|
+
? Effect.succeed(HttpServerResponse.empty({ status: getStatus(error) }))
|
|
30
175
|
: Effect.fail(error)));
|
|
31
176
|
}
|
|
32
177
|
function getStatus(error) {
|
|
33
|
-
|
|
178
|
+
const reason = error.reason;
|
|
179
|
+
switch (reason._tag) {
|
|
34
180
|
case "RouteNotFound":
|
|
35
181
|
return 404;
|
|
36
182
|
case "RequestParseError":
|
|
@@ -38,20 +184,23 @@ function getStatus(error) {
|
|
|
38
184
|
case "InternalError":
|
|
39
185
|
case "ResponseError":
|
|
40
186
|
return 500;
|
|
187
|
+
default: {
|
|
188
|
+
const _exhaustive = reason;
|
|
189
|
+
return 500;
|
|
190
|
+
}
|
|
41
191
|
}
|
|
42
192
|
}
|
|
43
|
-
function toRoute(
|
|
193
|
+
function toRoute(path, candidates, currentServices, createResponse) {
|
|
44
194
|
return {
|
|
45
|
-
"~effect/http/HttpRouter/Route": "~effect/http/HttpRouter/Route",
|
|
195
|
+
["~effect/http/HttpRouter/Route"]: "~effect/http/HttpRouter/Route",
|
|
46
196
|
method: "GET",
|
|
47
|
-
path
|
|
197
|
+
path,
|
|
48
198
|
handler: Effect.gen(function* () {
|
|
49
|
-
const fiberId = yield* Effect.fiberId;
|
|
50
|
-
const rootScope = yield* Effect.scope;
|
|
51
199
|
const routeContext = yield* RouteContext;
|
|
52
200
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
53
201
|
const searchParams = yield* HttpServerRequest.ParsedSearchParams;
|
|
54
|
-
const
|
|
202
|
+
const requestUrl = Option.getOrElse(HttpServerRequest.toURL(request), () => new URL(request.url, "http://localhost"));
|
|
203
|
+
const provided = Layer.mergeAll(initialMemory({ url: requestUrl, origin: requestUrl.origin }).pipe(Layer.provide(Uuid7State.Default)), Layer.succeed(CurrentRoute, yield* Effect.serviceOption(CurrentRoute).pipe(Effect.map(Option.match({
|
|
55
204
|
onNone: () => ({
|
|
56
205
|
route: Parse(request.url),
|
|
57
206
|
parent: undefined,
|
|
@@ -61,39 +210,33 @@ function toRoute(entry, currentServices) {
|
|
|
61
210
|
parent,
|
|
62
211
|
}),
|
|
63
212
|
})))));
|
|
64
|
-
const input = { ...routeContext.params
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
const matchedParams = guardExit.value.value;
|
|
83
|
-
yield* prepared.commit;
|
|
84
|
-
const scope = yield* Scope.fork(rootScope);
|
|
85
|
-
const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
|
|
86
|
-
const preparedServices = prepared.services;
|
|
87
|
-
const handlerServices = ServiceMap.merge(ServiceMap.merge(currentServices, preparedServices), ServiceMap.make(Scope.Scope, scope));
|
|
88
|
-
const handlerFx = entry.handler(paramsRef);
|
|
89
|
-
const withLayouts = yield* layoutManager.apply(entry.layouts, matchedParams, handlerFx, preparedServices);
|
|
90
|
-
const withCatches = yield* catchManager.apply(entry.catches, withLayouts, preparedServices);
|
|
91
|
-
const html = yield* renderToHtmlString(withCatches).pipe(Effect.provideServices(handlerServices));
|
|
92
|
-
return HttpServerResponse.text(html, {
|
|
93
|
-
headers: { "content-type": "text/html; charset=utf-8" },
|
|
94
|
-
});
|
|
213
|
+
const input = { ...searchParams, ...routeContext.params };
|
|
214
|
+
const requestServices = Context.merge(currentServices, yield* Effect.context());
|
|
215
|
+
const executor = yield* makeRouteExecutor().pipe(Effect.provideContext(requestServices));
|
|
216
|
+
const rendered = yield* executor
|
|
217
|
+
.transition({
|
|
218
|
+
path: request.url,
|
|
219
|
+
input,
|
|
220
|
+
candidates,
|
|
221
|
+
layers: [provided],
|
|
222
|
+
})
|
|
223
|
+
.pipe(Effect.catchIf(isRouteDecodeError, (error) => new HttpServerError.HttpServerError({
|
|
224
|
+
reason: new HttpServerError.RequestParseError({ request, cause: error }),
|
|
225
|
+
})), Effect.catchIf(isRouteGuardError, () => new HttpServerError.HttpServerError({
|
|
226
|
+
reason: new HttpServerError.RouteNotFound({ request }),
|
|
227
|
+
})));
|
|
228
|
+
return yield* createResponse(rendered, requestServices);
|
|
95
229
|
}),
|
|
96
230
|
uninterruptible: false,
|
|
97
|
-
prefix:
|
|
231
|
+
prefix: Option.none(),
|
|
98
232
|
};
|
|
99
233
|
}
|
|
234
|
+
function hasTag(value, tag) {
|
|
235
|
+
return hasProperty(value, "_tag") && value._tag === tag;
|
|
236
|
+
}
|
|
237
|
+
function isRouteDecodeError(value) {
|
|
238
|
+
return hasTag(value, "RouteDecodeError");
|
|
239
|
+
}
|
|
240
|
+
function isRouteGuardError(value) {
|
|
241
|
+
return hasTag(value, "RouteGuardError");
|
|
242
|
+
}
|