mono-jsx 0.6.6 â 0.6.7
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 +148 -57
- package/index.mjs +5 -0
- package/jsx-runtime.mjs +281 -192
- package/package.json +1 -1
- package/setup.mjs +2 -0
- package/types/html.d.ts +4 -5
- package/types/index.d.ts +11 -0
- package/types/jsx-runtime.d.ts +5 -4
- package/types/mono.d.ts +74 -19
- package/types/render.d.ts +3 -3
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in
|
|
|
14
14
|
- đĨˇ [htmx](#using-htmx) integration
|
|
15
15
|
- đ Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
16
16
|
|
|
17
|
+
Playground: https://val.town/x/ije/mono-jsx
|
|
18
|
+
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
19
21
|
mono-jsx supports all modern JavaScript runtimes including Node.js, Deno, Bun, and Cloudflare Workers.
|
|
@@ -30,7 +32,7 @@ deno add npm:mono-jsx
|
|
|
30
32
|
bun add mono-jsx
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
### Setup JSX Runtime
|
|
34
36
|
|
|
35
37
|
To use mono-jsx as your JSX runtime, add the following configuration to your `tsconfig.json` (or `deno.json` for Deno):
|
|
36
38
|
|
|
@@ -43,12 +45,6 @@ To use mono-jsx as your JSX runtime, add the following configuration to your `ts
|
|
|
43
45
|
}
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
Alternatively, you can use a pragma directive in your JSX file:
|
|
47
|
-
|
|
48
|
-
```js
|
|
49
|
-
/** @jsxImportSource mono-jsx */
|
|
50
|
-
```
|
|
51
|
-
|
|
52
48
|
You can also run `mono-jsx setup` to automatically add the configuration to your project:
|
|
53
49
|
|
|
54
50
|
```bash
|
|
@@ -62,6 +58,18 @@ deno run -A npm:mono-jsx setup
|
|
|
62
58
|
bunx mono-jsx setup
|
|
63
59
|
```
|
|
64
60
|
|
|
61
|
+
### Zero Configuration
|
|
62
|
+
|
|
63
|
+
Alternatively, you can use `@jsxImportSource` pragma directive without installation mono-jsx(no package.json/tsconfig/node_modules), the runtime(deno/bun) automatically installs mono-jsx to your computer:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// Deno, Valtown
|
|
67
|
+
/** @jsxImportSource https://esm.sh/mono-jsx */
|
|
68
|
+
|
|
69
|
+
// Bun
|
|
70
|
+
/** @jsxImportSource mono-jsx */
|
|
71
|
+
```
|
|
72
|
+
|
|
65
73
|
## Usage
|
|
66
74
|
|
|
67
75
|
mono-jsx allows you to return an `<html>` JSX element as a `Response` object in the `fetch` handler:
|
|
@@ -82,7 +90,7 @@ For Deno/Bun users, you can run the `app.tsx` directly:
|
|
|
82
90
|
|
|
83
91
|
```bash
|
|
84
92
|
deno serve app.tsx
|
|
85
|
-
bun
|
|
93
|
+
bun app.tsx
|
|
86
94
|
```
|
|
87
95
|
|
|
88
96
|
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:
|
|
@@ -150,11 +158,13 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
|
|
|
150
158
|
```tsx
|
|
151
159
|
<a
|
|
152
160
|
style={{
|
|
161
|
+
display: "inline-flex",
|
|
162
|
+
gap: "0.5em",
|
|
153
163
|
color: "black",
|
|
154
164
|
"::after": { content: "âŠī¸" },
|
|
155
165
|
":hover": { textDecoration: "underline" },
|
|
156
166
|
"@media (prefers-color-scheme: dark)": { color: "white" },
|
|
157
|
-
"& .icon": { width: "1em", height: "1em"
|
|
167
|
+
"& .icon": { width: "1em", height: "1em" },
|
|
158
168
|
}}
|
|
159
169
|
>
|
|
160
170
|
<img class="icon" src="link.png" />
|
|
@@ -192,7 +202,7 @@ function App() {
|
|
|
192
202
|
|
|
193
203
|
### Using `html` Tag Function
|
|
194
204
|
|
|
195
|
-
mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML
|
|
205
|
+
mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML` property.
|
|
196
206
|
|
|
197
207
|
```tsx
|
|
198
208
|
function App() {
|
|
@@ -200,7 +210,7 @@ function App() {
|
|
|
200
210
|
}
|
|
201
211
|
```
|
|
202
212
|
|
|
203
|
-
The `html`
|
|
213
|
+
The `html` function is globally available without importing. You can also use `css` and `js` functions for CSS and JavaScript:
|
|
204
214
|
|
|
205
215
|
```tsx
|
|
206
216
|
function App() {
|
|
@@ -230,8 +240,7 @@ function Button() {
|
|
|
230
240
|
}
|
|
231
241
|
```
|
|
232
242
|
|
|
233
|
-
|
|
234
|
-
> Event handlers are never called on the server-side. They're serialized to strings and sent to the client. **This means you should NOT use server-side variables or functions in event handlers.**
|
|
243
|
+
Event handlers are never called on the server-side. They're serialized to strings and sent to the client. **This means you should NOT use server-side variables or functions in event handlers.**
|
|
235
244
|
|
|
236
245
|
```tsx
|
|
237
246
|
import { doSomething } from "some-library";
|
|
@@ -361,7 +370,7 @@ interface AppSignals {
|
|
|
361
370
|
function Header(this: FC<{}, AppSignals>) {
|
|
362
371
|
return (
|
|
363
372
|
<header>
|
|
364
|
-
<h1 style={
|
|
373
|
+
<h1 style={{ color: this.app.themeColor }}>Welcome to mono-jsx!</h1>
|
|
365
374
|
</header>
|
|
366
375
|
)
|
|
367
376
|
}
|
|
@@ -369,7 +378,7 @@ function Header(this: FC<{}, AppSignals>) {
|
|
|
369
378
|
function Footer(this: FC<{}, AppSignals>) {
|
|
370
379
|
return (
|
|
371
380
|
<footer>
|
|
372
|
-
<p style={
|
|
381
|
+
<p style={{ color: this.app.themeColor }}>(c) 2025 mono-jsx.</p>
|
|
373
382
|
</footer>
|
|
374
383
|
)
|
|
375
384
|
}
|
|
@@ -416,6 +425,9 @@ function App(this: FC<{ input: string }>) {
|
|
|
416
425
|
}
|
|
417
426
|
```
|
|
418
427
|
|
|
428
|
+
> [!TIP]
|
|
429
|
+
> You can use `this.$` as a shorthand for `this.computed` to create computed signals.
|
|
430
|
+
|
|
419
431
|
### Using Effects
|
|
420
432
|
|
|
421
433
|
You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
|
|
@@ -473,7 +485,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
473
485
|
|
|
474
486
|
### Using `<toggle>` Element with Signals
|
|
475
487
|
|
|
476
|
-
The `<toggle>` element conditionally renders content based on the `show` prop
|
|
488
|
+
The `<toggle>` element conditionally renders content based on the `show` prop. You can use signals to control the visibility of the content on the client side.
|
|
477
489
|
|
|
478
490
|
```tsx
|
|
479
491
|
function App(this: FC<{ show: boolean }>) {
|
|
@@ -499,7 +511,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
499
511
|
|
|
500
512
|
### Using `<switch>` Element with Signals
|
|
501
513
|
|
|
502
|
-
The `<switch>` element renders different content based on the value
|
|
514
|
+
The `<switch>` element renders different content based on the `value` prop. Elements with matching `slot` attributes are displayed when their value matches, otherwise default slots are shown. Like `<toggle>`, you can use signals to control the value on the client side.
|
|
503
515
|
|
|
504
516
|
```tsx
|
|
505
517
|
function App(this: FC<{ lang: "en" | "zh" | "đ" }>) {
|
|
@@ -617,7 +629,7 @@ mono-jsx binds a scoped signals object to `this` of your component functions. Th
|
|
|
617
629
|
|
|
618
630
|
The `this` object has the following built-in properties:
|
|
619
631
|
|
|
620
|
-
- `app`: The app signals
|
|
632
|
+
- `app`: The app global signals..
|
|
621
633
|
- `context`: The context defined on the root `<html>` element.
|
|
622
634
|
- `request`: The request object from the `fetch` handler.
|
|
623
635
|
- `refs`: A map of refs defined in the component.
|
|
@@ -625,12 +637,13 @@ The `this` object has the following built-in properties:
|
|
|
625
637
|
- `effect`: A method to create side effects.
|
|
626
638
|
|
|
627
639
|
```ts
|
|
628
|
-
type FC<Signals = {}, AppSignals = {}, Context = {}> = {
|
|
629
|
-
readonly app: AppSignals;
|
|
640
|
+
type FC<Signals = {}, AppSignals = {}, Context = {}, Refs = {}, AppRefs = {}> = {
|
|
641
|
+
readonly app: AppSignals & { refs: AppRefs; url: WithParams<URL> }
|
|
630
642
|
readonly context: Context;
|
|
631
|
-
readonly request: Request
|
|
632
|
-
readonly refs:
|
|
643
|
+
readonly request: WithParams<Request>;
|
|
644
|
+
readonly refs: Refs;
|
|
633
645
|
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
646
|
+
readonly $: FC["computed"];
|
|
634
647
|
readonly effect: (fn: () => void | (() => void)) => void;
|
|
635
648
|
} & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
|
|
636
649
|
```
|
|
@@ -641,10 +654,10 @@ See the [Using Signals](#using-signals) section for more details on how to use s
|
|
|
641
654
|
|
|
642
655
|
### Using Refs
|
|
643
656
|
|
|
644
|
-
You can use `this.refs` to access refs in your components.
|
|
657
|
+
You can use `this.refs` to access refs in your components. Refs are defined using the `ref` attribute in JSX, and they allow you to access DOM elements directly. The `refs` object is a map of ref names to DOM elements.
|
|
645
658
|
|
|
646
659
|
```tsx
|
|
647
|
-
function App(this: FC) {
|
|
660
|
+
function App(this: Refs<FC, { input: HTMLInputElement }>) {
|
|
648
661
|
this.effect(() => {
|
|
649
662
|
this.refs.input?.addEventListener("input", (evt) => {
|
|
650
663
|
console.log("Input changed:", evt.target.value);
|
|
@@ -660,12 +673,55 @@ function App(this: FC) {
|
|
|
660
673
|
}
|
|
661
674
|
```
|
|
662
675
|
|
|
676
|
+
You can also use `this.app.refs` to access app-level refs:
|
|
677
|
+
|
|
678
|
+
```tsx
|
|
679
|
+
function Layout(this: Refs<FC, {}, { h1: HTMLH1Element }>) {
|
|
680
|
+
return (
|
|
681
|
+
<>
|
|
682
|
+
<header>
|
|
683
|
+
<h1 ref={this.refs.h1}>Welcome to mono-jsx!</h1>
|
|
684
|
+
</header>
|
|
685
|
+
<main>
|
|
686
|
+
<slot />
|
|
687
|
+
</main>
|
|
688
|
+
</>
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Element `<componet>` also support `ref` attribute, which allows you to control the component rendering manually. The `ref` will be a `ComponentElement` that has the `name`, `props`, and `refresh` properties:
|
|
694
|
+
|
|
695
|
+
- `name`: The name of the component to render.
|
|
696
|
+
- `props`: The props to pass to the component.
|
|
697
|
+
- `refresh`: A method to re-render the component with the current name and props.
|
|
698
|
+
|
|
699
|
+
```tsx
|
|
700
|
+
function App(this: Refs<FC, { component: ComponentElement }>) {
|
|
701
|
+
this.effect(() => {
|
|
702
|
+
// updating the component name and props will trigger a re-render of the component
|
|
703
|
+
this.refs.component.name = "Foo";
|
|
704
|
+
this.refs.component.props = {};
|
|
705
|
+
|
|
706
|
+
const timer = setInterval(() => {
|
|
707
|
+
// re-render the component
|
|
708
|
+
this.refs.component.refresh();
|
|
709
|
+
}, 1000);
|
|
710
|
+
return () => clearInterval(timer); // cleanup
|
|
711
|
+
});
|
|
712
|
+
return (
|
|
713
|
+
<div>
|
|
714
|
+
<component ref={this.refs.component} />
|
|
715
|
+
</div>
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
|
|
663
719
|
### Using Context
|
|
664
720
|
|
|
665
721
|
You can use the `context` property in `this` to access context values in your components. The context is defined on the root `<html>` element:
|
|
666
722
|
|
|
667
723
|
```tsx
|
|
668
|
-
function Dash(this: FC
|
|
724
|
+
function Dash(this: Context<FC, { auth: { uuid: string; name: string } }>) {
|
|
669
725
|
const { auth } = this.context;
|
|
670
726
|
return (
|
|
671
727
|
<div>
|
|
@@ -788,7 +844,7 @@ async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
|
788
844
|
this.show = false;
|
|
789
845
|
return (
|
|
790
846
|
<div>
|
|
791
|
-
<toggle
|
|
847
|
+
<toggle show={this.show}>
|
|
792
848
|
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
793
849
|
</toggle>
|
|
794
850
|
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
@@ -836,7 +892,7 @@ export default {
|
|
|
836
892
|
}
|
|
837
893
|
```
|
|
838
894
|
|
|
839
|
-
## Using Router(SPA)
|
|
895
|
+
## Using Router(SPA Mode)
|
|
840
896
|
|
|
841
897
|
mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On client side, it listens all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
|
|
842
898
|
|
|
@@ -853,13 +909,11 @@ const routes = {
|
|
|
853
909
|
export default {
|
|
854
910
|
fetch: (req) => (
|
|
855
911
|
<html request={req} routes={routes}>
|
|
856
|
-
<
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
</nav>
|
|
862
|
-
</header>
|
|
912
|
+
<nav>
|
|
913
|
+
<a href="/">Home</a>
|
|
914
|
+
<a href="/about">About</a>
|
|
915
|
+
<a href="/blog">Blog</a>
|
|
916
|
+
</nav>
|
|
863
917
|
<router />
|
|
864
918
|
</html>
|
|
865
919
|
)
|
|
@@ -872,12 +926,10 @@ mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/W
|
|
|
872
926
|
- â
Cloudflare Workers
|
|
873
927
|
- â
Nodejs (>= 24)
|
|
874
928
|
|
|
875
|
-
For Bun users, mono-jsx provides a `
|
|
929
|
+
For Bun users, mono-jsx provides a `buildRoutes` function that uses Bun's built-in server routing:
|
|
876
930
|
|
|
877
931
|
```tsx
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
import { monoRoutes } from "mono-jsx"
|
|
932
|
+
import { buildRoutes } from "mono-jsx"
|
|
881
933
|
|
|
882
934
|
const routes = {
|
|
883
935
|
"/": Home,
|
|
@@ -886,13 +938,18 @@ const routes = {
|
|
|
886
938
|
"/post/:id": Post,
|
|
887
939
|
}
|
|
888
940
|
|
|
889
|
-
|
|
890
|
-
routes:
|
|
891
|
-
<html request={
|
|
941
|
+
Bun.serve({
|
|
942
|
+
routes: buildRoutes((req) => (
|
|
943
|
+
<html request={req} routes={routes}>
|
|
944
|
+
<nav>
|
|
945
|
+
<a href="/">Home</a>
|
|
946
|
+
<a href="/about">About</a>
|
|
947
|
+
<a href="/blog">Blog</a>
|
|
948
|
+
</nav>
|
|
892
949
|
<router />
|
|
893
950
|
</html>
|
|
894
951
|
))
|
|
895
|
-
}
|
|
952
|
+
})
|
|
896
953
|
```
|
|
897
954
|
|
|
898
955
|
### Using Route `params`
|
|
@@ -909,18 +966,36 @@ function Post(this: FC) {
|
|
|
909
966
|
}
|
|
910
967
|
```
|
|
911
968
|
|
|
912
|
-
### Using
|
|
969
|
+
### Using `this.app.url` Signal
|
|
913
970
|
|
|
914
|
-
|
|
971
|
+
`this.app.url` is an app-level signal that contains the current route url and parameters. The `this.app.url` signal is automatically updated when the route changes, so you can use it to display the current URL in your components, or control view with `<toggle>` or `<switch>` elements:
|
|
915
972
|
|
|
916
973
|
```tsx
|
|
917
|
-
|
|
918
|
-
const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
|
|
974
|
+
function App(this: FC) {
|
|
919
975
|
return (
|
|
920
|
-
<
|
|
921
|
-
<
|
|
922
|
-
|
|
923
|
-
|
|
976
|
+
<div>
|
|
977
|
+
<h1>Current Pathname: {this.$(() => this.app.url.pathname)}</h1>
|
|
978
|
+
</div>
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### Navigation between Pages
|
|
984
|
+
|
|
985
|
+
To navigate between pages, you can use `<a>` elements with `href` attributes that match the defined routes. The router will intercept the click events of these links and fetch the corresponding route component without reloading the page:
|
|
986
|
+
|
|
987
|
+
```tsx
|
|
988
|
+
function App() {
|
|
989
|
+
export default {
|
|
990
|
+
fetch: (req) => (
|
|
991
|
+
<html request={req} routes={routes}>
|
|
992
|
+
<nav>
|
|
993
|
+
<a href="/">Home</a>
|
|
994
|
+
<a href="/about">About</a>
|
|
995
|
+
<a href="/blog">Blog</a>
|
|
996
|
+
</nav>
|
|
997
|
+
<router />
|
|
998
|
+
</html>
|
|
924
999
|
)
|
|
925
1000
|
}
|
|
926
1001
|
```
|
|
@@ -933,19 +1008,35 @@ Links under the `<nav>` element will be treated as navigation links by the route
|
|
|
933
1008
|
export default {
|
|
934
1009
|
fetch: (req) => (
|
|
935
1010
|
<html request={req} routes={routes}>
|
|
936
|
-
<
|
|
937
|
-
<
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
</nav>
|
|
942
|
-
</header>
|
|
1011
|
+
<nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
|
|
1012
|
+
<a href="/">Home</a>
|
|
1013
|
+
<a href="/about">About</a>
|
|
1014
|
+
<a href="/blog">Blog</a>
|
|
1015
|
+
</nav>
|
|
943
1016
|
<router />
|
|
944
1017
|
</html>
|
|
945
1018
|
)
|
|
946
1019
|
}
|
|
947
1020
|
```
|
|
948
1021
|
|
|
1022
|
+
### Using DB/Storage in Route Components
|
|
1023
|
+
|
|
1024
|
+
Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components.
|
|
1025
|
+
|
|
1026
|
+
```tsx
|
|
1027
|
+
async function Post(this: FC) {
|
|
1028
|
+
const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
|
|
1029
|
+
return (
|
|
1030
|
+
<article>
|
|
1031
|
+
<h2>{post.title}<h2>
|
|
1032
|
+
<section>
|
|
1033
|
+
{html(post.content)}
|
|
1034
|
+
</section>
|
|
1035
|
+
</article>
|
|
1036
|
+
)
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
949
1040
|
### Fallback(404)
|
|
950
1041
|
|
|
951
1042
|
You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// index.ts
|
|
2
|
+
function buildRoutes(handler) {
|
|
3
|
+
const { routes = {} } = handler(Symbol.for("mono.setup"));
|
|
4
|
+
return monoRoutes(routes, handler);
|
|
5
|
+
}
|
|
2
6
|
function monoRoutes(routes, handler) {
|
|
3
7
|
const handlers = {};
|
|
4
8
|
for (const [path, fc] of Object.entries(routes)) {
|
|
@@ -10,5 +14,6 @@ function monoRoutes(routes, handler) {
|
|
|
10
14
|
return handlers;
|
|
11
15
|
}
|
|
12
16
|
export {
|
|
17
|
+
buildRoutes,
|
|
13
18
|
monoRoutes
|
|
14
19
|
};
|