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 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 (8KB gzipped), zero dependencies
8
+ - 🦋 Lightweight (10KB gzipped), zero dependencies
9
9
  - 🚦 Signals as reactive primitives
10
- - 🗂️ Complete Web API TypeScript definitions
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 local development:
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**, so we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
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
- You'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
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 runtime.
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- ```jsx
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
- console.log("Welcome to mono-jsx!");
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
+ };