mono-jsx 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -22
- package/index.mjs +13 -1
- package/jsx-runtime.mjs +473 -252
- package/package.json +1 -1
- package/types/index.d.ts +6 -1
- package/types/jsx.d.ts +3 -5
- package/types/mono.d.ts +24 -11
- package/types/render.d.ts +12 -0
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
6
6
|
|
|
7
7
|
- 🚀 No build step needed
|
|
8
|
-
- 🦋 Lightweight (
|
|
8
|
+
- 🦋 Lightweight (10KB gzipped), zero dependencies
|
|
9
9
|
- 🚦 Signals as reactive primitives
|
|
10
|
-
-
|
|
10
|
+
- ⚡️ Use web comonents, no virtual DOM
|
|
11
|
+
- 💡 Complete Web API TypeScript definitions
|
|
11
12
|
- ⏳ Streaming rendering
|
|
13
|
+
- 🗂️ Built-in router
|
|
12
14
|
- 🥷 [htmx](#using-htmx) integration
|
|
13
15
|
- 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
14
16
|
|
|
@@ -83,13 +85,13 @@ deno serve app.tsx
|
|
|
83
85
|
bun run app.tsx
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start
|
|
88
|
+
If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start your app in development mode:
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
npx wrangler dev app.tsx
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
**Node.js doesn't support JSX syntax or declarative fetch servers**,
|
|
94
|
+
**Node.js doesn't support JSX syntax or declarative fetch servers**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
|
|
93
95
|
|
|
94
96
|
```tsx
|
|
95
97
|
// app.tsx
|
|
@@ -106,14 +108,14 @@ serve({
|
|
|
106
108
|
});
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
And you'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
|
|
110
112
|
|
|
111
113
|
```bash
|
|
112
114
|
npx tsx app.tsx
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
> [!NOTE]
|
|
116
|
-
> Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx
|
|
118
|
+
> Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx.
|
|
117
119
|
|
|
118
120
|
## Using JSX
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ mono-jsx adopts standard HTML property names, avoiding React's custom naming con
|
|
|
131
133
|
|
|
132
134
|
mono-jsx allows you to compose the `class` property using arrays of strings, objects, or expressions:
|
|
133
135
|
|
|
134
|
-
```
|
|
136
|
+
```tsx
|
|
135
137
|
<div
|
|
136
138
|
class={[
|
|
137
139
|
"container box",
|
|
@@ -145,7 +147,7 @@ mono-jsx allows you to compose the `class` property using arrays of strings, obj
|
|
|
145
147
|
|
|
146
148
|
mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes), [pseudo elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements), [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries), and [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting) in the `style` property:
|
|
147
149
|
|
|
148
|
-
```
|
|
150
|
+
```tsx
|
|
149
151
|
<a
|
|
150
152
|
style={{
|
|
151
153
|
color: "black",
|
|
@@ -164,7 +166,7 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
|
|
|
164
166
|
|
|
165
167
|
mono-jsx uses [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) elements to render slotted content (equivalent to React's `children` property). You can also add the `name` attribute to define named slots:
|
|
166
168
|
|
|
167
|
-
```
|
|
169
|
+
```tsx
|
|
168
170
|
function Container() {
|
|
169
171
|
return (
|
|
170
172
|
<div class="container">
|
|
@@ -192,7 +194,7 @@ function App() {
|
|
|
192
194
|
|
|
193
195
|
mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML`:
|
|
194
196
|
|
|
195
|
-
```
|
|
197
|
+
```tsx
|
|
196
198
|
function App() {
|
|
197
199
|
return <div>{html`<h1>Hello world!</h1>`}</div>;
|
|
198
200
|
}
|
|
@@ -200,7 +202,7 @@ function App() {
|
|
|
200
202
|
|
|
201
203
|
The `html` tag function is globally available without importing. You can also use `css` and `js` tag functions for CSS and JavaScript:
|
|
202
204
|
|
|
203
|
-
```
|
|
205
|
+
```tsx
|
|
204
206
|
function App() {
|
|
205
207
|
return (
|
|
206
208
|
<head>
|
|
@@ -218,7 +220,7 @@ function App() {
|
|
|
218
220
|
|
|
219
221
|
mono-jsx lets you write event handlers directly in JSX, similar to React:
|
|
220
222
|
|
|
221
|
-
```
|
|
223
|
+
```tsx
|
|
222
224
|
function Button() {
|
|
223
225
|
return (
|
|
224
226
|
<button onClick={(evt) => alert("BOOM!")}>
|
|
@@ -293,7 +295,7 @@ export default {
|
|
|
293
295
|
|
|
294
296
|
You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
|
|
295
297
|
|
|
296
|
-
```
|
|
298
|
+
```tsx
|
|
297
299
|
async function* Chat(props: { prompt: string }) {
|
|
298
300
|
const stream = await openai.chat.completions.create({
|
|
299
301
|
model: "gpt-4",
|
|
@@ -626,7 +628,7 @@ The `this` object has the following built-in properties:
|
|
|
626
628
|
type FC<Signals = {}, AppSignals = {}, Context = {}> = {
|
|
627
629
|
readonly app: AppSignals;
|
|
628
630
|
readonly context: Context;
|
|
629
|
-
readonly request: Request;
|
|
631
|
+
readonly request: Request & { params?: Record<string, string> };
|
|
630
632
|
readonly refs: Record<string, HTMLElement | null>;
|
|
631
633
|
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
632
634
|
readonly effect: (fn: () => void | (() => void)) => void;
|
|
@@ -716,7 +718,7 @@ export default {
|
|
|
716
718
|
|
|
717
719
|
mono-jsx renders your `<html>` as a readable stream, allowing async components to render asynchronously. You can use `placeholder` to display a loading state while waiting for async components to render:
|
|
718
720
|
|
|
719
|
-
```
|
|
721
|
+
```tsx
|
|
720
722
|
async function Sleep({ ms }) {
|
|
721
723
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
722
724
|
return <slot />;
|
|
@@ -735,7 +737,7 @@ export default {
|
|
|
735
737
|
|
|
736
738
|
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
737
739
|
|
|
738
|
-
```
|
|
740
|
+
```tsx
|
|
739
741
|
export default {
|
|
740
742
|
fetch: (req) => (
|
|
741
743
|
<html>
|
|
@@ -749,7 +751,7 @@ export default {
|
|
|
749
751
|
|
|
750
752
|
You can add the `catch` attribute to handle errors in the async component. The `catch` attribute should be a function that returns a JSX element:
|
|
751
753
|
|
|
752
|
-
```
|
|
754
|
+
```tsx
|
|
753
755
|
async function Hello() {
|
|
754
756
|
throw new Error("Something went wrong!");
|
|
755
757
|
return <p>Hello world!</p>;
|
|
@@ -764,11 +766,208 @@ export default {
|
|
|
764
766
|
}
|
|
765
767
|
```
|
|
766
768
|
|
|
769
|
+
|
|
770
|
+
## Lazy Rendering
|
|
771
|
+
|
|
772
|
+
Since mono-jsx renders html on server side, and no hydration JS sent to client side. To render a component dynamically on client side, you can use the `<component>` element to ask the server to render a component and send the html back to client:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
export default {
|
|
776
|
+
fetch: (req) => (
|
|
777
|
+
<html components={{ Foo }}>
|
|
778
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
779
|
+
</html>
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
You can use `<toggle>` element to control when to render the component:
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
788
|
+
this.show = false;
|
|
789
|
+
return (
|
|
790
|
+
<div>
|
|
791
|
+
<toggle value={this.show}>
|
|
792
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
793
|
+
</toggle>
|
|
794
|
+
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
795
|
+
</div>
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export default {
|
|
800
|
+
fetch: (req) => (
|
|
801
|
+
<html components={{ Foo }}>
|
|
802
|
+
<Lazy />
|
|
803
|
+
</html>
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
You also can use signal `name` or `props`, change the signal value will trigger the component to re-render with new name or props:
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
import { Profile, Projects, Settings } from "./pages.tsx"
|
|
812
|
+
|
|
813
|
+
async function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
|
|
814
|
+
this.page = "Projects";
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<>
|
|
818
|
+
<div class="tab">
|
|
819
|
+
<button onClick={e => this.page = "Profile"}>Profile</botton>
|
|
820
|
+
<button onClick={e => this.page = "Projects"}>Projects</botton>
|
|
821
|
+
<button onClick={e => this.page = "Settings"}>Settings</botton>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="page">
|
|
824
|
+
<component name={this.page} placeholder={<p>Loading...</p>} />
|
|
825
|
+
</div>
|
|
826
|
+
</>
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export default {
|
|
831
|
+
fetch: (req) => (
|
|
832
|
+
<html components={{ Profile, Projects, Settings }}>
|
|
833
|
+
<Dash />
|
|
834
|
+
</html>
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## Using Router(SPA)
|
|
840
|
+
|
|
841
|
+
mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On client side, it hijacks all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
|
|
842
|
+
|
|
843
|
+
To use the router, you need to define your routes as a mapping of URL patterns to components and pass it to the `<html>` element as `routes` prop. The `request` prop is also required to match the current URL against the defined routes.
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
const routes = {
|
|
847
|
+
"/": Home,
|
|
848
|
+
"/about": About,
|
|
849
|
+
"/blog": Blog,
|
|
850
|
+
"/post/:id": Post,
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export default {
|
|
854
|
+
fetch: (req) => (
|
|
855
|
+
<html request={req} routes={routes}>
|
|
856
|
+
<header>
|
|
857
|
+
<nav>
|
|
858
|
+
<a href="/">Home</a>
|
|
859
|
+
<a href="/about">About</a>
|
|
860
|
+
<a href="/blog">Blog</a>
|
|
861
|
+
</nav>
|
|
862
|
+
</header>
|
|
863
|
+
<router />
|
|
864
|
+
</html>
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to match a route:
|
|
870
|
+
|
|
871
|
+
- ✅ Deno
|
|
872
|
+
- ✅ Cloudflare Workers
|
|
873
|
+
- ✅ Nodejs (>= 24)
|
|
874
|
+
|
|
875
|
+
For Bun users, mono-jsx provides a `monoRoutes` function that uses Bun's builtin routing:
|
|
876
|
+
|
|
877
|
+
```tsx
|
|
878
|
+
// bun app.tsx
|
|
879
|
+
|
|
880
|
+
import { monoRoutes } from "mono-jsx"
|
|
881
|
+
|
|
882
|
+
const routes = {
|
|
883
|
+
"/": Home,
|
|
884
|
+
"/about": About,
|
|
885
|
+
"/blog": Blog,
|
|
886
|
+
"/post/:id": Post,
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export default {
|
|
890
|
+
routes: monoRoutes(routes, (request) => (
|
|
891
|
+
<html request={request}>
|
|
892
|
+
<router />
|
|
893
|
+
</html>
|
|
894
|
+
))
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Using Route `params`
|
|
899
|
+
|
|
900
|
+
When you define a route with a parameter (e.g., `/post/:id`), mono-jsx will automatically extract the parameter from the URL and make it available in the route component. The `params` object is available in the `request` property of the component's `this` context.
|
|
901
|
+
You can access the `params` object in your route components to get the values of the parameters defined in the route pattern:
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
```tsx
|
|
905
|
+
// router pattern: "/post/:id"
|
|
906
|
+
function Post(this: FC) {
|
|
907
|
+
this.request.url // "http://localhost:3000/post/123"
|
|
908
|
+
this.request.params?.id // "123"
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Using DB/Storage in Route Components
|
|
913
|
+
|
|
914
|
+
Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components. For example, if you're using a SQL database, you can use the `sql` tag function to query the database:
|
|
915
|
+
|
|
916
|
+
```tsx
|
|
917
|
+
async function Post(this: FC) {
|
|
918
|
+
const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
|
|
919
|
+
return (
|
|
920
|
+
<article>
|
|
921
|
+
<h2>{post.title}<h2>
|
|
922
|
+
<div>html`${post.content}`</div>
|
|
923
|
+
</article>
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Nav Links
|
|
929
|
+
|
|
930
|
+
Links under a `<nav>` element will be treated as navigation links by the router. When the `href` of a link matches a route, A active class will be added to the link element. By default, the active class is `active`, but you can customize it by setting the `data-active-class` attribute on the `<nav>` element. You can also add a custom style for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element:
|
|
931
|
+
|
|
932
|
+
```tsx
|
|
933
|
+
export default {
|
|
934
|
+
fetch: (req) => (
|
|
935
|
+
<html request={req} routes={routes}>
|
|
936
|
+
<header>
|
|
937
|
+
<nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
|
|
938
|
+
<a href="/">Home</a>
|
|
939
|
+
<a href="/about">About</a>
|
|
940
|
+
<a href="/blog">Blog</a>
|
|
941
|
+
</nav>
|
|
942
|
+
</header>
|
|
943
|
+
<router />
|
|
944
|
+
</html>
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Setting Fallback(404) Content
|
|
950
|
+
|
|
951
|
+
You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL:
|
|
952
|
+
|
|
953
|
+
```tsx
|
|
954
|
+
export default {
|
|
955
|
+
fetch: (req) => (
|
|
956
|
+
<html request={req} routes={routes}>
|
|
957
|
+
<router>
|
|
958
|
+
<p>Page Not Found</p>
|
|
959
|
+
<p>Back to <a href="/">Home</a></p>
|
|
960
|
+
</router>
|
|
961
|
+
</html>
|
|
962
|
+
)
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
767
966
|
## Customizing html Response
|
|
768
967
|
|
|
769
968
|
You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
|
|
770
969
|
|
|
771
|
-
```
|
|
970
|
+
```tsx
|
|
772
971
|
export default {
|
|
773
972
|
fetch: (req) => (
|
|
774
973
|
<html
|
|
@@ -789,7 +988,7 @@ export default {
|
|
|
789
988
|
|
|
790
989
|
mono-jsx integrates with [htmx](https://htmx.org/) and [typed-htmx](https://github.com/Desdaemon/typed-htmx). To use htmx, add the `htmx` attribute to the root `<html>` element:
|
|
791
990
|
|
|
792
|
-
```
|
|
991
|
+
```tsx
|
|
793
992
|
export default {
|
|
794
993
|
fetch: (req) => {
|
|
795
994
|
const url = new URL(req.url);
|
|
@@ -817,7 +1016,7 @@ export default {
|
|
|
817
1016
|
|
|
818
1017
|
You can add htmx [extensions](https://htmx.org/docs/#extensions) by adding the `htmx-ext-*` attribute to the root `<html>` element:
|
|
819
1018
|
|
|
820
|
-
```
|
|
1019
|
+
```tsx
|
|
821
1020
|
export default {
|
|
822
1021
|
fetch: (req) => (
|
|
823
1022
|
<html htmx htmx-ext-response-targets htmx-ext-ws>
|
|
@@ -833,7 +1032,7 @@ export default {
|
|
|
833
1032
|
|
|
834
1033
|
You can specify the htmx version by setting the `htmx` attribute to a specific version:
|
|
835
1034
|
|
|
836
|
-
```
|
|
1035
|
+
```tsx
|
|
837
1036
|
export default {
|
|
838
1037
|
fetch: (req) => (
|
|
839
1038
|
<html htmx="2.0.4" htmx-ext-response-targets="2.0.2" htmx-ext-ws="2.0.2">
|
|
@@ -849,7 +1048,7 @@ export default {
|
|
|
849
1048
|
|
|
850
1049
|
By default, mono-jsx installs htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also install htmx manually with your own CDN or local copy:
|
|
851
1050
|
|
|
852
|
-
```
|
|
1051
|
+
```tsx
|
|
853
1052
|
export default {
|
|
854
1053
|
fetch: (req) => (
|
|
855
1054
|
<html>
|
package/index.mjs
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
// index.ts
|
|
2
|
-
|
|
2
|
+
function monoRoutes(routes, handler) {
|
|
3
|
+
const handlers = {};
|
|
4
|
+
for (const [path, fc] of Object.entries(routes)) {
|
|
5
|
+
handlers[path] = (request) => {
|
|
6
|
+
Reflect.set(request, "x-route", fc);
|
|
7
|
+
return handler(request);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return handlers;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
monoRoutes
|
|
14
|
+
};
|