@typed/ui 1.0.0-beta.4 → 1.0.0-beta.5
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/README.md +53 -9
- package/dist/Alert.d.ts +78 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +37 -0
- package/dist/Button.d.ts +144 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +38 -0
- package/dist/Carousel.d.ts +547 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +387 -0
- package/dist/Checkbox.d.ts +203 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +140 -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 +516 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +371 -0
- package/dist/Component.d.ts +127 -0
- package/dist/Component.d.ts.map +1 -0
- package/dist/Component.js +60 -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 +355 -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 +1695 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +987 -0
- package/dist/Grid.d.ts +569 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +379 -0
- package/dist/Group.d.ts +147 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +63 -0
- package/dist/Heading.d.ts +86 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +48 -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 +196 -53
- package/dist/Link.d.ts +67 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +91 -37
- package/dist/Listbox.d.ts +437 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +316 -0
- package/dist/Menu.d.ts +972 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +731 -0
- package/dist/Menubar.d.ts +367 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +265 -0
- package/dist/Meter.d.ts +217 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +94 -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 +64 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +50 -0
- package/dist/NativePopover.d.ts +41 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +46 -0
- package/dist/Popover.d.ts +241 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +141 -0
- package/dist/RadioGroup.d.ts +432 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +291 -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 +529 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +407 -0
- package/dist/Separator.d.ts +54 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +26 -0
- package/dist/Slider.d.ts +188 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +96 -0
- package/dist/SpinButton.d.ts +188 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +96 -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 +187 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +116 -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 +591 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +346 -0
- package/dist/Toolbar.d.ts +366 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +245 -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 +591 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +434 -0
- package/dist/TreeGrid.d.ts +645 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +420 -0
- package/dist/VisuallyHidden.d.ts +54 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +27 -0
- package/dist/WindowSplitter.d.ts +376 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +222 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -0
- package/package.json +45 -18
- package/src/HttpRouter.test.ts +0 -256
- package/src/HttpRouter.ts +0 -165
- package/src/Link.test.ts +0 -84
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
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 Scope from "effect/Scope";
|
|
7
6
|
import * as Context from "effect/Context";
|
|
8
|
-
import {
|
|
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 { Ids } from "@typed/id";
|
|
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.context();
|
|
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 http
|
|
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 http
|
|
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
|
|
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
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(Ids.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 = Context.merge(Context.merge(currentServices, preparedServices), Context.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.provideContext(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
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
|
+
}
|
package/dist/Link.d.ts
CHANGED
|
@@ -1,36 +1,75 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
1
|
import type { Scope } from "effect/Scope";
|
|
3
|
-
import
|
|
4
|
-
import { type
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
import * as Fx from "@typed/fx/Fx";
|
|
3
|
+
import { type NavigationError, Navigation } from "@typed/navigation";
|
|
4
|
+
import { type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
|
|
5
|
+
import * as Dom from "./Dom.js";
|
|
6
|
+
import type { HostResult } from "./Dom/Types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Native anchor options plus Typed navigation behavior.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* ## Why
|
|
12
|
+
*
|
|
13
|
+
* `href` and content remain renderable, while `replace` selects history policy.
|
|
14
|
+
* Ordinary anchor options—including target, download, rel, and user click
|
|
15
|
+
* handlers—stay available through the DOM host model.
|
|
16
|
+
*
|
|
17
|
+
* ## Ownership and lifetime
|
|
18
|
+
*
|
|
19
|
+
* Options are inert. Rendering owns subscriptions to renderable values and the
|
|
20
|
+
* click listener for its Effect Scope; the `Navigation` service owns history.
|
|
21
|
+
*
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
* @category models
|
|
24
|
+
*/
|
|
25
|
+
export type LinkOptions = Dom.HostOptions<HTMLAnchorElement> & Dom.ElementOptions<HTMLAnchorElement> & {
|
|
25
26
|
readonly href: Renderable<string, any, any>;
|
|
26
27
|
readonly content: Renderable<string | number | boolean | null | undefined | void | RenderEvent, any, any>;
|
|
27
28
|
readonly replace?: boolean;
|
|
28
|
-
}
|
|
29
|
+
};
|
|
30
|
+
type LinkError<Options extends object> = Renderable.ErrorFromObject<Options> | NavigationError;
|
|
31
|
+
type LinkServices<Options extends object> = Renderable.ServicesFromObject<Options> | Navigation | Scope | RenderTemplate;
|
|
29
32
|
/**
|
|
30
|
-
* Renders
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* Renders a native anchor with safe, eligible same-origin SPA navigation.
|
|
34
|
+
*
|
|
35
|
+
* Modified clicks, downloads, external URLs, non-HTTP(S) destinations, and
|
|
36
|
+
* non-self targets retain browser behavior. Unsafe executable schemes are
|
|
37
|
+
* rendered as `about:blank` and their clicks are canceled. A user click handler
|
|
38
|
+
* runs first through DOM host event chaining; `preventDefault()` prevents the
|
|
39
|
+
* internal navigation decision.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* ## Why
|
|
43
|
+
*
|
|
44
|
+
* `Link` keeps the anchor as the integration boundary: native opening, copying,
|
|
45
|
+
* status previews, modifier keys, downloads, and external navigation continue
|
|
46
|
+
* to work. Only an unmodified primary click to same-origin HTTP(S) is handed to
|
|
47
|
+
* Typed's Effect-native `Navigation` service.
|
|
48
|
+
*
|
|
49
|
+
* ## Ownership and lifetime
|
|
50
|
+
*
|
|
51
|
+
* Calling `Link` starts no work. Running the returned Fx subscribes to dynamic
|
|
52
|
+
* href/content and installs a real, non-passive DOM click listener in its
|
|
53
|
+
* Effect Scope. Scope finalization removes only those resources. Navigation
|
|
54
|
+
* failures and required services remain visible in the Fx type. A custom host
|
|
55
|
+
* must preserve the sanitized href, chained click handler, content, and native
|
|
56
|
+
* anchor behavior.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { Link } from "@typed/ui/Link"
|
|
61
|
+
*
|
|
62
|
+
* const profile = Link({
|
|
63
|
+
* href: "/account/profile",
|
|
64
|
+
* content: "Profile",
|
|
65
|
+
* replace: false
|
|
66
|
+
* })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @since 1.0.0
|
|
70
|
+
* @category components
|
|
33
71
|
*/
|
|
34
|
-
export declare function Link<const
|
|
72
|
+
export declare function Link<const Options extends object>(options: Options, ..._: Options extends LinkOptions ? [] : ["Link options must satisfy LinkOptions"]): Fx.Fx<RenderEvent, LinkError<Options>, LinkServices<Options>>;
|
|
73
|
+
export declare function Link<const Options extends object, const Host extends HostResult>(options: Options, host: Dom.HostOverride<Dom.HostProps<HTMLAnchorElement>, Renderable.Any, Host>, ..._: Options extends LinkOptions ? [] : ["Link options must satisfy LinkOptions"]): Fx.Fx<RenderEvent, LinkError<Options> | Fx.Error<Host>, LinkServices<Options> | Fx.Services<Host>>;
|
|
35
74
|
export {};
|
|
36
75
|
//# sourceMappingURL=Link.d.ts.map
|
package/dist/Link.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../src/Link.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../src/Link.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,KAAK,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC,iBAAiB,CAAC,GAC1D,GAAG,CAAC,cAAc,CAAC,iBAAiB,CAAC,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAC1B,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,WAAW,EACjE,GAAG,EACH,GAAG,CACJ,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEJ,KAAK,SAAS,CAAC,OAAO,SAAS,MAAM,IAAI,UAAU,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC;AAE/F,KAAK,YAAY,CAAC,OAAO,SAAS,MAAM,IACpC,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,GACtC,UAAU,GACV,KAAK,GACL,cAAc,CAAC;AA2EnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,MAAM,EAC/C,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,OAAO,SAAS,WAAW,GAAG,EAAE,GAAG,CAAC,uCAAuC,CAAC,GACjF,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AACjE,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,CAAC,IAAI,SAAS,UAAU,EAC9E,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,EAC9E,GAAG,CAAC,EAAE,OAAO,SAAS,WAAW,GAAG,EAAE,GAAG,CAAC,uCAAuC,CAAC,GACjF,EAAE,CAAC,EAAE,CACN,WAAW,EACX,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EACnC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAC1C,CAAC"}
|
package/dist/Link.js
CHANGED
|
@@ -1,45 +1,99 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import * as Stream from "effect/Stream";
|
|
3
|
+
import * as Fx from "@typed/fx/Fx";
|
|
4
|
+
import { Navigation } from "@typed/navigation";
|
|
5
5
|
import { EventHandler, html, } from "@typed/template";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import * as Dom from "./Dom.js";
|
|
7
|
+
function makeLinkClickHandler(replace) {
|
|
8
|
+
// Classification and native cancellation must run during event dispatch;
|
|
9
|
+
// only the resulting router navigation is lazy.
|
|
10
|
+
return EventHandler.make((event) => {
|
|
11
|
+
const anchor = Dom.currentTarget(event);
|
|
12
|
+
const href = anchor.href;
|
|
13
|
+
if (event.defaultPrevented ||
|
|
14
|
+
event.button !== 0 ||
|
|
15
|
+
event.metaKey ||
|
|
16
|
+
event.altKey ||
|
|
17
|
+
event.ctrlKey ||
|
|
18
|
+
event.shiftKey)
|
|
10
19
|
return;
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
20
|
+
const target = anchor.target;
|
|
21
|
+
if (target && target !== "_self")
|
|
13
22
|
return;
|
|
14
|
-
|
|
15
|
-
const target = getUrl(nav.origin, href);
|
|
16
|
-
if (target.origin !== nav.origin)
|
|
23
|
+
if (anchor.hasAttribute("download"))
|
|
17
24
|
return;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
if (!isSafeLinkHref(href)) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const destination = new URL(href);
|
|
30
|
+
if (destination.protocol !== "http:" && destination.protocol !== "https:")
|
|
31
|
+
return;
|
|
32
|
+
if (destination.origin !== anchor.ownerDocument.defaultView?.location.origin)
|
|
33
|
+
return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
return Effect.flatMap(Navigation, (navigation) => navigation.navigate(href, { history: replace ? "replace" : "push" }));
|
|
36
|
+
}, { passive: false });
|
|
37
|
+
}
|
|
38
|
+
function legacyProps(options) {
|
|
39
|
+
const modern = options.props;
|
|
40
|
+
const props = {};
|
|
41
|
+
for (const [key, value] of Object.entries(options)) {
|
|
42
|
+
if (key === "content" ||
|
|
43
|
+
key === "href" ||
|
|
44
|
+
key === "props" ||
|
|
45
|
+
key === "ref" ||
|
|
46
|
+
key === "replace" ||
|
|
47
|
+
key[0] === "@" ||
|
|
48
|
+
(key[0] === "o" && key[1] === "n") ||
|
|
49
|
+
(modern !== undefined && Object.hasOwn(modern, key)))
|
|
50
|
+
continue;
|
|
51
|
+
props[key] = value;
|
|
52
|
+
}
|
|
53
|
+
return props;
|
|
22
54
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export function Link(options) {
|
|
29
|
-
return gen(function* () {
|
|
30
|
-
const { replace = false, onclick, content: children, ...rest } = options;
|
|
31
|
-
const replace$ = yield* RefSubject.make(replace);
|
|
32
|
-
const navigationHandler = makeLinkClickHandler(replace$);
|
|
33
|
-
const userHandler = onclick ? EventHandler.fromEffectOrEventHandler(onclick) : undefined;
|
|
34
|
-
const clickHandler = userHandler
|
|
35
|
-
? EventHandler.make(Effect.fn(function* (ev) {
|
|
36
|
-
yield* userHandler.handler(ev);
|
|
37
|
-
if (ev.defaultPrevented)
|
|
38
|
-
return;
|
|
39
|
-
yield* navigationHandler.handler(ev);
|
|
40
|
-
}), { ...userHandler.options, preventDefault: true })
|
|
41
|
-
: navigationHandler;
|
|
42
|
-
const props = { ...rest, onclick: clickHandler };
|
|
43
|
-
return html `<a ...${props}>${children}</a>`;
|
|
55
|
+
function internalProps(options) {
|
|
56
|
+
return () => ({
|
|
57
|
+
...legacyProps(options),
|
|
58
|
+
href: sanitizeLinkHref(options.href),
|
|
59
|
+
onclick: makeLinkClickHandler(options.replace ?? false),
|
|
44
60
|
});
|
|
45
61
|
}
|
|
62
|
+
// Keep the implementation erased: constraining it to LinkOptions widens the
|
|
63
|
+
// Renderable `any` slots and loses the exact error/service inference above.
|
|
64
|
+
export function Link(options, host, ..._) {
|
|
65
|
+
const render = Dom.renderHost();
|
|
66
|
+
return render(options, host, internalProps(options), options.content, (props, content) => html `<a ...${props}>${content}</a>`);
|
|
67
|
+
}
|
|
68
|
+
function sanitizeLinkHref(href) {
|
|
69
|
+
if (Fx.isFx(href))
|
|
70
|
+
return Fx.map(href, neutralizeExecutableHref);
|
|
71
|
+
if (Stream.isStream(href)) {
|
|
72
|
+
return Stream.map(href, neutralizeExecutableHref);
|
|
73
|
+
}
|
|
74
|
+
if (Effect.isEffect(href))
|
|
75
|
+
return Effect.map(href, neutralizeExecutableHref);
|
|
76
|
+
if (Array.isArray(href)) {
|
|
77
|
+
return href.map((value) => sanitizeLinkHref(value));
|
|
78
|
+
}
|
|
79
|
+
return (typeof href === "string" ? neutralizeExecutableHref(href) : href);
|
|
80
|
+
}
|
|
81
|
+
function neutralizeExecutableHref(href) {
|
|
82
|
+
return isSafeLinkHref(href) ? href : "about:blank";
|
|
83
|
+
}
|
|
84
|
+
function isSafeLinkHref(href) {
|
|
85
|
+
try {
|
|
86
|
+
switch (new URL(href, "http://localhost").protocol) {
|
|
87
|
+
case "http:":
|
|
88
|
+
case "https:":
|
|
89
|
+
case "mailto:":
|
|
90
|
+
case "tel:":
|
|
91
|
+
return true;
|
|
92
|
+
default:
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|