mono-jsx 0.6.6 → 0.6.8

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 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
- ## Setup JSX Runtime
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 run app.tsx
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", marginRight: "0.5em" },
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` tag function is globally available without importing. You can also use `css` and `js` tag functions for CSS and JavaScript:
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
- > [!NOTE]
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={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
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={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
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 of a signal. Elements with matching `slot` attributes are displayed when their value matches, otherwise default slots are shown:
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 defined on the root `<html>` element.
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 & { params?: Record<string, string> };
632
- readonly refs: Record<string, HTMLElement | null>;
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. Define refs in your component using the `ref` attribute:
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<{}, {}, { auth: { uuid: string; name: string } }>) {
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 value={this.show}>
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
- <header>
857
- <nav>
858
- <a href="/">Home</a>
859
- <a href="/about">About</a>
860
- <a href="/blog">Blog</a>
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 `monoRoutes` function that uses Bun's built-in routing:
929
+ For Bun users, mono-jsx provides a `buildRoutes` function that uses Bun's built-in server routing:
876
930
 
877
931
  ```tsx
878
- // bun app.tsx
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
- export default {
890
- routes: monoRoutes(routes, (request) => (
891
- <html request={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 DB/Storage in Route Components
969
+ ### Using `this.app.url` Signal
913
970
 
914
- Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components.
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
- async function Post(this: FC) {
918
- const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
974
+ function App(this: FC) {
919
975
  return (
920
- <article>
921
- <h2>{post.title}<h2>
922
- <div>html`${post.content}`</div>
923
- </article>
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
- <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>
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
  };