@typed/ui 1.0.0-beta.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -23
- package/dist/HttpRouter.js +1 -1
- package/package.json +23 -18
- package/src/HttpRouter.test.ts +15 -15
- package/src/HttpRouter.ts +1 -1
- package/src/Link.test.ts +1 -2
- package/tsconfig.json +0 -6
package/README.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> **Beta:** This package is in beta; APIs may change.
|
|
4
4
|
|
|
5
|
-
`@typed/ui`
|
|
5
|
+
`@typed/ui` is the **web integration layer** for `@typed/router` and `@typed/template`. It bridges typed-smol's routing and template system with the browser and Effect's HTTP stack.
|
|
6
|
+
|
|
7
|
+
## Capabilities
|
|
8
|
+
|
|
9
|
+
- **Link** — A typed anchor component that intercepts same-origin clicks and navigates via `Navigation.navigate` instead of a full page reload. Keeps routing SPA-style while preserving normal `<a>` semantics (href, target, keyboard, right-click).
|
|
10
|
+
- **SSR** — `ssrForHttp` compiles a router Matcher into HttpRouter GET handlers for server-side rendering. Requests are parsed, matched, and the corresponding Fx is rendered to HTML. `handleHttpServerError` adds global middleware for 404/400/500.
|
|
6
11
|
|
|
7
12
|
## Dependencies
|
|
8
13
|
|
|
@@ -27,25 +32,29 @@ Renders an `<a href="...">` that intercepts same-origin, same-document clicks an
|
|
|
27
32
|
|
|
28
33
|
```ts
|
|
29
34
|
function Link<const Opts extends LinkOptions>(
|
|
30
|
-
options: Opts
|
|
31
|
-
): Fx<
|
|
35
|
+
options: Opts,
|
|
36
|
+
): Fx<
|
|
37
|
+
RenderEvent,
|
|
38
|
+
Renderable.ErrorFromObject<Opts>,
|
|
39
|
+
Renderable.ServicesFromObject<Opts> | Scope | RenderTemplate
|
|
40
|
+
>;
|
|
32
41
|
```
|
|
33
42
|
|
|
34
43
|
**`LinkOptions`**
|
|
35
44
|
|
|
36
|
-
| Property
|
|
37
|
-
|
|
38
|
-
| `href` | `Renderable<string, any, any>`
|
|
39
|
-
| `content` | `Renderable<string \| number \| boolean \| null \| undefined \| void \| RenderEvent, any, any>` | Yes
|
|
40
|
-
| `replace` | `boolean`
|
|
45
|
+
| Property | Type | Required | Description |
|
|
46
|
+
| --------- | ----------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------- |
|
|
47
|
+
| `href` | `Renderable<string, any, any>` | Yes | Target URL. |
|
|
48
|
+
| `content` | `Renderable<string \| number \| boolean \| null \| undefined \| void \| RenderEvent, any, any>` | Yes | Link body (text or template content). |
|
|
49
|
+
| `replace` | `boolean` | No | If `true`, use history replace instead of push. Default: `false`. |
|
|
41
50
|
|
|
42
|
-
In addition, `LinkOptions` accepts standard anchor event handlers (e.g. `onclick`), `ref`, and other writable `HTMLAnchorElement` properties. Custom `onclick` runs first; if the event is not `preventDefault
|
|
51
|
+
In addition, `LinkOptions` accepts standard anchor event handlers (e.g. `onclick`), `ref`, and other writable `HTMLAnchorElement` properties. Custom `onclick` runs first; if the event is not `preventDefault`'d, the built-in navigation handler runs.
|
|
43
52
|
|
|
44
53
|
---
|
|
45
54
|
|
|
46
55
|
### `ssrForHttp`
|
|
47
56
|
|
|
48
|
-
Registers route handlers on an Effect **HttpRouter** for server-side rendering. The matcher
|
|
57
|
+
Registers route handlers on an Effect **HttpRouter** for server-side rendering. The matcher's routes are compiled and each case is exposed as a GET route; requests are parsed, matched, and the corresponding Fx is rendered to HTML. Requires **Router** and **Scope** to be provided elsewhere; other matcher services remain in the effect requirement.
|
|
49
58
|
|
|
50
59
|
**Overloads:**
|
|
51
60
|
|
|
@@ -53,13 +62,13 @@ Registers route handlers on an Effect **HttpRouter** for server-side rendering.
|
|
|
53
62
|
// (router, matcher)
|
|
54
63
|
function ssrForHttp<E, R>(
|
|
55
64
|
router: HttpRouter,
|
|
56
|
-
input: Matcher<RenderEvent, E, R
|
|
57
|
-
): Effect.Effect<void, never, Exclude<R, Scope | Router
|
|
65
|
+
input: Matcher<RenderEvent, E, R>,
|
|
66
|
+
): Effect.Effect<void, never, Exclude<R, Scope | Router>>;
|
|
58
67
|
|
|
59
68
|
// (matcher)(router) — curried
|
|
60
69
|
function ssrForHttp<E, R>(
|
|
61
|
-
input: Matcher<RenderEvent, E, R
|
|
62
|
-
): (router: HttpRouter) => Effect.Effect<void, never, Exclude<R, Scope | Router
|
|
70
|
+
input: Matcher<RenderEvent, E, R>,
|
|
71
|
+
): (router: HttpRouter) => Effect.Effect<void, never, Exclude<R, Scope | Router>>;
|
|
63
72
|
```
|
|
64
73
|
|
|
65
74
|
- **`router`** — Effect `HttpRouter` to attach GET handlers to.
|
|
@@ -71,14 +80,14 @@ function ssrForHttp<E, R>(
|
|
|
71
80
|
|
|
72
81
|
Adds global middleware to an **HttpRouter** that catches `HttpServerError` and returns appropriate HTTP responses:
|
|
73
82
|
|
|
74
|
-
| Error reason
|
|
75
|
-
|
|
76
|
-
| `RouteNotFound`
|
|
77
|
-
| `RequestParseError`
|
|
78
|
-
| `InternalError` / `ResponseError` | 500
|
|
83
|
+
| Error reason | Status |
|
|
84
|
+
| --------------------------------- | ------ |
|
|
85
|
+
| `RouteNotFound` | 404 |
|
|
86
|
+
| `RequestParseError` | 400 |
|
|
87
|
+
| `InternalError` / `ResponseError` | 500 |
|
|
79
88
|
|
|
80
89
|
```ts
|
|
81
|
-
function handleHttpServerError(router: HttpRouter): Effect.Effect<void, never, HttpRouter
|
|
90
|
+
function handleHttpServerError(router: HttpRouter): Effect.Effect<void, never, HttpRouter>;
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
Use after registering routes (e.g. after `ssrForHttp`) so unhandled route and parse errors are converted to 404/400/500 instead of failing the server.
|
|
@@ -91,9 +100,8 @@ import { html } from "@typed/template";
|
|
|
91
100
|
|
|
92
101
|
// In a template: link that navigates via Navigation (no full reload)
|
|
93
102
|
const nav = html`<nav>
|
|
94
|
-
${Link({ href: "/", content: "Home" })}
|
|
95
|
-
${Link({ href: "/todos", content: "Todos" })}
|
|
103
|
+
${Link({ href: "/", content: "Home" })} ${Link({ href: "/todos", content: "Todos" })}
|
|
96
104
|
</nav>`;
|
|
97
105
|
```
|
|
98
106
|
|
|
99
|
-
For SSR, provide the router and matcher to `ssrForHttp` when setting up the HTTP server; see Effect
|
|
107
|
+
For SSR, provide the router and matcher to `ssrForHttp` when setting up the HTTP server; see Effect's `HttpRouter` and the TodoMVC example structure.
|
package/dist/HttpRouter.js
CHANGED
|
@@ -42,7 +42,7 @@ function getStatus(error) {
|
|
|
42
42
|
}
|
|
43
43
|
function toRoute(entry, currentServices) {
|
|
44
44
|
return {
|
|
45
|
-
"~effect/http/HttpRouter/Route": "~effect/http/HttpRouter/Route",
|
|
45
|
+
["~effect/http/HttpRouter/Route"]: "~effect/http/HttpRouter/Route",
|
|
46
46
|
method: "GET",
|
|
47
47
|
path: entry.route.path,
|
|
48
48
|
handler: Effect.gen(function* () {
|
package/package.json
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed/ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"publishConfig": {
|
|
5
|
-
"access": "public"
|
|
6
|
-
},
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
7
4
|
"type": "module",
|
|
8
5
|
"exports": {
|
|
9
6
|
".": {
|
|
@@ -15,21 +12,29 @@
|
|
|
15
12
|
"import": "./dist/*.js"
|
|
16
13
|
}
|
|
17
14
|
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "[ -d dist ] || rm -f tsconfig.tsbuildinfo; tsc",
|
|
20
|
+
"test": "vitest run --passWithNoTests"
|
|
21
|
+
},
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@effect/platform-node": "
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"@typed/
|
|
23
|
-
"@typed/
|
|
24
|
-
"@typed/template": "
|
|
25
|
-
"
|
|
23
|
+
"@effect/platform-node": "catalog:",
|
|
24
|
+
"@typed/fx": "workspace:*",
|
|
25
|
+
"@typed/id": "workspace:*",
|
|
26
|
+
"@typed/navigation": "workspace:*",
|
|
27
|
+
"@typed/router": "workspace:*",
|
|
28
|
+
"@typed/template": "workspace:*",
|
|
29
|
+
"effect": "catalog:",
|
|
30
|
+
"happy-dom": "catalog:"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
|
-
"typescript": "
|
|
29
|
-
"vitest": "
|
|
33
|
+
"typescript": "catalog:",
|
|
34
|
+
"vitest": "catalog:"
|
|
30
35
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
]
|
|
40
|
+
}
|
package/src/HttpRouter.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { NodeHttpServer } from "@effect/platform-node";
|
|
2
|
-
import assert from "
|
|
3
|
-
import { describe, it } from "vitest";
|
|
2
|
+
import { assert, describe, it } from "vitest";
|
|
4
3
|
import { Effect, Layer } from "effect";
|
|
5
4
|
import { Fx } from "@typed/fx";
|
|
5
|
+
import { Ids } from "@typed/id";
|
|
6
6
|
import { Navigation } from "@typed/navigation";
|
|
7
7
|
import { CurrentRoute } from "@typed/router/CurrentRoute";
|
|
8
8
|
import * as Matcher from "@typed/router/Matcher";
|
|
@@ -22,7 +22,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
22
22
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
23
23
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
24
24
|
HttpRouter.serve,
|
|
25
|
-
Layer.provideMerge([NodeHttpServer.layerTest]),
|
|
25
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
26
26
|
);
|
|
27
27
|
return Effect.gen(function* () {
|
|
28
28
|
const response = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -39,7 +39,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
39
39
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
40
40
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
41
41
|
HttpRouter.serve,
|
|
42
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
42
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
43
43
|
);
|
|
44
44
|
return Effect.gen(function* () {
|
|
45
45
|
const response = yield* HttpClient.get("/users/123").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -58,7 +58,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
58
58
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
59
59
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
60
60
|
HttpRouter.serve,
|
|
61
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
61
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
62
62
|
);
|
|
63
63
|
return Effect.gen(function* () {
|
|
64
64
|
const response = yield* HttpClient.get("/search?q=test").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -85,7 +85,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
85
85
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
86
86
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
87
87
|
HttpRouter.serve,
|
|
88
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
88
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
89
89
|
);
|
|
90
90
|
return Effect.gen(function* () {
|
|
91
91
|
const homeResponse = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -110,7 +110,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
110
110
|
).pipe(
|
|
111
111
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
112
112
|
HttpRouter.serve,
|
|
113
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
113
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
114
114
|
);
|
|
115
115
|
return Effect.gen(function* () {
|
|
116
116
|
const response = yield* HttpClient.get("/notfound");
|
|
@@ -125,7 +125,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
125
125
|
);
|
|
126
126
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
127
127
|
HttpRouter.serve,
|
|
128
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
128
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
129
129
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
130
130
|
);
|
|
131
131
|
return Effect.gen(function* () {
|
|
@@ -144,7 +144,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
144
144
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
145
145
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
146
146
|
HttpRouter.serve,
|
|
147
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
147
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
148
148
|
);
|
|
149
149
|
return Effect.gen(function* () {
|
|
150
150
|
const response = yield* HttpClient.get("/home");
|
|
@@ -167,7 +167,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
167
167
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
168
168
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
169
169
|
HttpRouter.serve,
|
|
170
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
170
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
171
171
|
);
|
|
172
172
|
return Effect.gen(function* () {
|
|
173
173
|
const listResponse = yield* HttpClient.get("/api/users").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -192,7 +192,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
192
192
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
193
193
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
194
194
|
HttpRouter.serve,
|
|
195
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
195
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
196
196
|
);
|
|
197
197
|
return Effect.gen(function* () {
|
|
198
198
|
const response = yield* HttpClient.get("/test").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -213,7 +213,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
213
213
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
214
214
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
215
215
|
HttpRouter.serve,
|
|
216
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
216
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
217
217
|
);
|
|
218
218
|
return Effect.gen(function* () {
|
|
219
219
|
const response = yield* HttpClient.get("/users").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -233,7 +233,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
233
233
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
234
234
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
235
235
|
HttpRouter.serve,
|
|
236
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
236
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
237
237
|
);
|
|
238
238
|
return Effect.gen(function* () {
|
|
239
239
|
const response = yield* HttpClient.get("/home").pipe(Effect.flatMap((r) => r.text));
|
|
@@ -263,7 +263,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
263
263
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
264
264
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
265
265
|
HttpRouter.serve,
|
|
266
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
266
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
267
267
|
Layer.provide(CurrentRoute.extend(api)),
|
|
268
268
|
);
|
|
269
269
|
return Effect.gen(function* () {
|
|
@@ -284,7 +284,7 @@ describe("typed/ui/HttpRouter", () => {
|
|
|
284
284
|
const Live = HttpRouter.use(ssrForHttp(matcher)).pipe(
|
|
285
285
|
Layer.provide(StaticHtmlRenderTemplate),
|
|
286
286
|
HttpRouter.serve,
|
|
287
|
-
Layer.provideMerge(NodeHttpServer.layerTest),
|
|
287
|
+
Layer.provideMerge([Ids.Test(), NodeHttpServer.layerTest]),
|
|
288
288
|
);
|
|
289
289
|
return Effect.gen(function* () {
|
|
290
290
|
const response = yield* HttpClient.get("/about").pipe(Effect.flatMap((r) => r.text));
|
package/src/HttpRouter.ts
CHANGED
|
@@ -76,7 +76,7 @@ function toRoute(
|
|
|
76
76
|
currentServices: ServiceMap.ServiceMap<never>,
|
|
77
77
|
): Route<any, any> {
|
|
78
78
|
return {
|
|
79
|
-
"~effect/http/HttpRouter/Route": "~effect/http/HttpRouter/Route",
|
|
79
|
+
["~effect/http/HttpRouter/Route"]: "~effect/http/HttpRouter/Route",
|
|
80
80
|
method: "GET",
|
|
81
81
|
path: entry.route.path,
|
|
82
82
|
handler: Effect.gen(function* () {
|
package/src/Link.test.ts
CHANGED