mono-jsx 0.6.3 → 0.6.5

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
@@ -10,7 +10,7 @@ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in
10
10
  - ⚡️ Use web components, no virtual DOM
11
11
  - 💡 Complete Web API TypeScript definitions
12
12
  - ⏳ Streaming rendering
13
- - 🗂️ Built-in router
13
+ - 🗂️ Built-in router(SPA mode)
14
14
  - 🥷 [htmx](#using-htmx) integration
15
15
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
16
16
 
@@ -150,11 +150,13 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
150
150
  ```tsx
151
151
  <a
152
152
  style={{
153
+ display: "inline-flex",
154
+ gap: "0.5em",
153
155
  color: "black",
154
156
  "::after": { content: "↩️" },
155
157
  ":hover": { textDecoration: "underline" },
156
158
  "@media (prefers-color-scheme: dark)": { color: "white" },
157
- "& .icon": { width: "1em", height: "1em", marginRight: "0.5em" },
159
+ "& .icon": { width: "1em", height: "1em" },
158
160
  }}
159
161
  >
160
162
  <img class="icon" src="link.png" />
@@ -361,7 +363,7 @@ interface AppSignals {
361
363
  function Header(this: FC<{}, AppSignals>) {
362
364
  return (
363
365
  <header>
364
- <h1 style={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
366
+ <h1 style={{ color: this.app.themeColor }}>Welcome to mono-jsx!</h1>
365
367
  </header>
366
368
  )
367
369
  }
@@ -369,7 +371,7 @@ function Header(this: FC<{}, AppSignals>) {
369
371
  function Footer(this: FC<{}, AppSignals>) {
370
372
  return (
371
373
  <footer>
372
- <p style={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
374
+ <p style={{ color: this.app.themeColor }}>(c) 2025 mono-jsx.</p>
373
375
  </footer>
374
376
  )
375
377
  }
@@ -836,9 +838,9 @@ export default {
836
838
  }
837
839
  ```
838
840
 
839
- ## Using Router(SPA)
841
+ ## Using Router(SPA Mode)
840
842
 
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.
843
+ 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
844
 
843
845
  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
846
 
@@ -853,13 +855,11 @@ const routes = {
853
855
  export default {
854
856
  fetch: (req) => (
855
857
  <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>
858
+ <nav>
859
+ <a href="/">Home</a>
860
+ <a href="/about">About</a>
861
+ <a href="/blog">Blog</a>
862
+ </nav>
863
863
  <router />
864
864
  </html>
865
865
  )
@@ -872,12 +872,10 @@ mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/W
872
872
  - ✅ Cloudflare Workers
873
873
  - ✅ Nodejs (>= 24)
874
874
 
875
- For Bun users, mono-jsx provides a `monoRoutes` function that uses Bun's builtin routing:
875
+ For Bun users, mono-jsx provides a `buildRoutes` function that uses Bun's built-in server routing:
876
876
 
877
877
  ```tsx
878
- // bun app.tsx
879
-
880
- import { monoRoutes } from "mono-jsx"
878
+ import { buildRoutes } from "mono-jsx"
881
879
 
882
880
  const routes = {
883
881
  "/": Home,
@@ -886,13 +884,18 @@ const routes = {
886
884
  "/post/:id": Post,
887
885
  }
888
886
 
889
- export default {
890
- routes: monoRoutes(routes, (request) => (
891
- <html request={request}>
887
+ Bun.serve({
888
+ routes: buildRoutes((req) => (
889
+ <html request={req} routes={routes}>
890
+ <nav>
891
+ <a href="/">Home</a>
892
+ <a href="/about">About</a>
893
+ <a href="/blog">Blog</a>
894
+ </nav>
892
895
  <router />
893
896
  </html>
894
897
  ))
895
- }
898
+ })
896
899
  ```
897
900
 
898
901
  ### Using Route `params`
@@ -909,46 +912,66 @@ function Post(this: FC) {
909
912
  }
910
913
  ```
911
914
 
912
- ### Using DB/Storage in Route Components
915
+ ### Navigation between Pages
913
916
 
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:
917
+ 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:
915
918
 
916
919
  ```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>
920
+ function App() {
921
+ export default {
922
+ fetch: (req) => (
923
+ <html request={req} routes={routes}>
924
+ <nav>
925
+ <a href="/">Home</a>
926
+ <a href="/about">About</a>
927
+ <a href="/blog">Blog</a>
928
+ </nav>
929
+ <router />
930
+ </html>
924
931
  )
925
932
  }
926
933
  ```
927
934
 
928
935
  ### Nav Links
929
936
 
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:
937
+ Links under the `<nav>` element will be treated as navigation links by the router. When the `href` of a nav 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 add style for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element.
931
938
 
932
939
  ```tsx
933
940
  export default {
934
941
  fetch: (req) => (
935
942
  <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
+ <nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
944
+ <a href="/">Home</a>
945
+ <a href="/about">About</a>
946
+ <a href="/blog">Blog</a>
947
+ </nav>
943
948
  <router />
944
949
  </html>
945
950
  )
946
951
  }
947
952
  ```
948
953
 
954
+ ### Using DB/Storage in Route Components
955
+
956
+ Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components.
957
+
958
+ ```tsx
959
+ async function Post(this: FC) {
960
+ const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
961
+ return (
962
+ <article>
963
+ <h2>{post.title}<h2>
964
+ <section>
965
+ {html(post.content)}
966
+ </section>
967
+ </article>
968
+ )
969
+ }
970
+ ```
971
+
949
972
  ### Fallback(404)
950
973
 
951
- You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL:
974
+ You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
952
975
 
953
976
  ```tsx
954
977
  export default {
@@ -1044,9 +1067,9 @@ export default {
1044
1067
  }
1045
1068
  ```
1046
1069
 
1047
- #### Installing htmx Manually
1070
+ #### Setup htmx Manually
1048
1071
 
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:
1072
+ By default, mono-jsx imports htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also setup htmx manually with your own CDN or local copy:
1050
1073
 
1051
1074
  ```tsx
1052
1075
  export default {
package/index.mjs CHANGED
@@ -1,4 +1,8 @@
1
1
  // index.ts
2
+ function buildRoutes(handler) {
3
+ const { routes = {} } = handler(Symbol.for("mono.peek"));
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
  };
package/jsx-runtime.mjs CHANGED
@@ -1,11 +1,6 @@
1
- // runtime/index.ts
2
- var CX_JS = `{var i=new Set("animation-iteration-count,aspect-ratio,border-image-outset,border-image-slice,border-image-width,box-flex-group,box-flex,box-ordinal-group,column-count,columns,fill-opacity,flex-grow,flex-negative,flex-order,flex-positive,flex-shrink,flex,flood-opacity,font-weight,grid-area,grid-column-end,grid-column-span,grid-column-start,grid-column,grid-row-end,grid-row-span,grid-row-start,grid-row,line-clamp,line-height,opacity,order,orphans,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width,tab-size,widows,z-index,zoom".split(",")),r=e=>typeof e=="string",o=e=>typeof e=="object"&&e!==null;var n=e=>r(e)?e:o(e)?Array.isArray(e)?e.map(n).filter(Boolean).join(" "):Object.entries(e).filter(([,t])=>!!t).map(([t])=>t).join(" "):"";window.$cx=n;}`;
3
- var STYLE_JS = `{var c=new Set("animation-iteration-count,aspect-ratio,border-image-outset,border-image-slice,border-image-width,box-flex-group,box-flex,box-ordinal-group,column-count,columns,fill-opacity,flex-grow,flex-negative,flex-order,flex-positive,flex-shrink,flex,flood-opacity,font-weight,grid-area,grid-column-end,grid-column-span,grid-column-start,grid-column,grid-row-end,grid-row-span,grid-row-start,grid-row,line-clamp,line-height,opacity,order,orphans,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width,tab-size,widows,z-index,zoom".split(",")),a=e=>typeof e=="string",l=e=>typeof e=="object"&&e!==null,u=e=>e.replace(/[a-z][A-Z]/g,o=>o.charAt(0)+"-"+o.charAt(1).toLowerCase());var p=e=>{let o=[],i=[],t=new g;for(let[n,r]of Object.entries(e))switch(n.charCodeAt(0)){case 58:i.push(null,n+"{"+s(r)+"}");break;case 64:i.push(n+"{",null,"{"+s(r)+"}}");break;case 38:i.push(null,n.slice(1)+"{"+s(r)+"}");break;default:o.push([n,r])}return o.length>0&&(t.inline=s(o)),i.length>0&&(t.css=i),t},f=(e,o)=>{let{inline:i,css:t}=p(o);if(t){let n="[data-css-"+Date.now().toString(36)+"]";document.head.appendChild(document.createElement("style")).textContent=(i?n+"{"+i+"}":"")+t.map(r=>r===null?n:r).join(""),e.getAttributeNames().forEach(r=>r.startsWith("data-css-")&&e.removeAttribute(r)),e.setAttribute(n.slice(1,-1),"")}else i&&e.setAttribute("style",i)},s=e=>{if(l(e)){let o="";for(let[i,t]of Array.isArray(e)?e:Object.entries(e))if(a(t)||typeof t=="number"){let n=u(i),r=typeof t=="number"?c.has(n)?""+t:t+"px":""+t;o+=(o?";":"")+n+":"+(n==="content"?JSON.stringify(r):r)}return o}return""},g=(()=>{function e(){}return e.prototype=Object.freeze(Object.create(null)),e})();window.$applyStyle=f;}`;
4
- var EVENT_JS = `{var w=window;w.$emit=(e,f,s)=>f.call(w.$signals?.(s)??e.target,e.type==="mount"?e.target:e);w.$onsubmit=(e,f,s)=>{e.preventDefault();f.call(w.$signals?.(s)??e.target,new FormData(e.target),e)};}`;
5
- var SIGNALS_JS = `{let p;const m=window,E=new Map,b=new Map,d=e=>b.get(e)??b.set(e,S(e)).get(e),u=(e,t)=>e.getAttribute(t),y=(e,t,r)=>e.setAttribute(t,r),M=(e,t)=>e.replaceChildren(...t),v=()=>Object.create(null),S=e=>{const t=v(),r=(c,l)=>{t[c]=l},n=new Map,i=(c,l)=>{let o=n.get(c);return o||(o=new Set,n.set(c,o)),o.add(l),()=>{o.delete(l),o.size===0&&n.delete(c)}},s=new Proxy(v(),{get:(c,l)=>document.querySelector("[data-ref='"+e+":"+l+"']")});return new Proxy(t,{get:(c,l,o)=>{switch(l){case"$init":return r;case"$watch":return i;case"app":return d(0);case"refs":return s;default:return p?.(e,l),Reflect.get(c,l,o)}},set:(c,l,o,a)=>{if(o!==Reflect.get(c,l,a)){const f=n.get(l);return f&&queueMicrotask(()=>f.forEach(g=>g())),Reflect.set(c,l,o,a)}return!1}})},k=(e,t,r)=>{if(t==="toggle"){let n;return()=>{if(!n){const i=e.firstElementChild;i&&i.tagName==="TEMPLATE"&&i.hasAttribute("m-slot")?n=[...i.content.childNodes]:n=[...e.childNodes]}M(e,r()?n:[])}}if(t==="switch"){let n,i=u(e,"match"),s,c,l=o=>s.get(o)??s.set(o,[]).get(o);return()=>{if(!s){s=new Map,c=[];for(const o of e.childNodes)if(o.nodeType===1&&o.tagName==="TEMPLATE"&&o.hasAttribute("m-slot")){for(const a of o.content.childNodes)a.nodeType===1&&a.hasAttribute("slot")?l(u(a,"slot")).push(a):c.push(a);o.remove()}else i?l(i).push(o):c.push(o)}n=""+r(),M(e,s.has(n)?s.get(n):c)}}if(t&&t.length>2&&t.startsWith("[")&&t.endsWith("]")){let n=t.slice(1,-1),i=e.parentElement;return i.tagName==="M-GROUP"&&(i=i.previousElementSibling),()=>{const s=r();s===!1||s===null||s===void 0?i.removeAttribute(n):typeof s=="object"&&s!==null&&(n==="class"||n==="style"||n==="props")?n==="class"?y(i,n,$cx(s)):n==="style"?$applyStyle(i,s):y(i,n,JSON.stringify(s)):y(i,n,s===!0?"":s)}}return()=>e.textContent=""+r()},N=e=>{const t=e.indexOf(":");return t>0?[Number(e.slice(0,t)),e.slice(t+1)]:null},w=async e=>{const t=e();return t!==void 0?t:(await new Promise(r=>setTimeout(r,0)),w(e))},T=(e,t)=>customElements.define(e,class extends HTMLElement{disposes=[];connectedCallback(){t(this)}disconnectedCallback(){this.disposes.forEach(r=>r()),this.disposes.length=0}});T("m-signal",e=>{const t=d(Number(u(e,"scope"))),r=u(e,"key");if(r)e.disposes.push(t.$watch(r,k(e,u(e,"mode"),()=>t[r])));else{const n=Number(u(e,"computed"));w(()=>E.get(n)).then(([i,s])=>{const c=k(e,u(e,"mode"),i.bind(t));s.forEach(l=>{const[o,a]=N(l);e.disposes.push(d(o).$watch(a,c))})})}}),T("m-effect",e=>{const{disposes:t}=e,r=Number(u(e,"scope")),n=Number(u(e,"n")),i=new Array(n);t.push(()=>{i.forEach(s=>typeof s=="function"&&s()),i.length=0});for(let s=0;s<n;s++){const c="$ME_"+r+"_"+s;w(()=>m[c]).then(l=>{const o=[],a=d(r),f=()=>{i[s]=l.call(a)};p=(g,h)=>o.push([g,h]),f(),p=void 0;for(const[g,h]of o)t.push(d(g).$watch(h,f))},()=>{})}}),m.$signals=e=>e!==void 0?d(e):void 0,m.$MS=(e,t)=>{const[r,n]=N(e);d(r).$init(n,t)},m.$MC=(e,t,r)=>{E.set(e,[t,r])},m.$merge=(e,...t)=>{for(const[r,...n]of t){let i=e;const s=n.pop();for(const c of n)i=i[c];i[s]=r}return e};}`;
6
- var SUSPENSE_JS = `{const s=new Map,i=e=>e.getAttribute("chunk-id"),l=(e,t)=>customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}});l("m-portal",e=>{s.set(i(e),e)}),l("m-chunk",e=>{setTimeout(()=>{const t=e.firstChild?.content.childNodes,o=i(e),n=s.get(o);n&&(e.hasAttribute("next")?n.before(...t):(s.delete(o),e.hasAttribute("done")?n.remove():n.replaceWith(...t)),e.remove())})});}`;
7
- var LAZY_JS = `{const r=document,o=(t,e)=>t.getAttribute(e),i=(t,e=[])=>t.replaceChildren(...e);customElements.define("m-component",class extends HTMLElement{static observedAttributes=["name","props"];#e;#i;#n;#t;#s;async#r(){const t={"x-component":this.#e,"x-props":this.#i??"{}","x-runtime-flag":""+$runtimeFlag,"x-scope-seq":""+$scopeSeq},e=new AbortController;this.#s?.abort(),this.#s=e,i(this,this.#n);const s=await fetch(location.href,{headers:t,signal:e.signal});if(!s.ok)throw i(this),new Error("Failed to fetch component '"+name+"'");const[l,n]=await s.json();this.innerHTML=l,n&&(r.body.appendChild(r.createElement("script")).textContent=n)}connectedCallback(){setTimeout(()=>{if(!this.#e){const t=o(this,"name"),e=o(this,"props");if(!t)throw new Error("Component name is required");this.#e=t,this.#i=e?.startsWith("base64,")?atob(e.slice(7)):null,this.#n=[...this.childNodes]}this.#r()})}disconnectedCallback(){i(this,this.#n),this.#s?.abort(),this.#t&&clearTimeout(this.#t),this.#s=void 0,this.#t=void 0}attributeChangedCallback(t,e,s){this.#e&&s&&e!==s&&(t==="name"?this.#e=s:t==="props"&&(this.#i=s),this.#t&&clearTimeout(this.#t),this.#t=setTimeout(()=>{this.#t=void 0,this.#r()},20))}});}`;
8
- var ROUTER_JS = `{const n=document,a=t=>t.split("#",1)[0];customElements.define("m-router",class extends HTMLElement{#t;#e;#s;#i;async#o(t){const i={"x-route":"true","x-runtime-flag":""+$runtimeFlag,"x-scope-seq":""+$scopeSeq},e=new AbortController;this.#i?.abort(),this.#i=e;const s=await fetch(t,{headers:i,signal:e.signal});if(s.status===404){this.replaceChildren(...this.#t);return}if(!s.ok)throw this.replaceChildren(),new Error("Failed to fetch route: "+s.status+" "+s.statusText);const[o,r]=await s.json();this.innerHTML=o,r&&(n.body.appendChild(n.createElement("script")).textContent=r)}#n(){n.querySelectorAll("nav a").forEach(t=>{const{href:i,classList:e}=t,s=t.closest("nav").getAttribute("data-active-class")??"active";a(i)===a(location.href)?e.add(s):e.remove(s)})}#a(t){this.#o(t),this.#n()}connectedCallback(){setTimeout(()=>{if(!this.#t)if(this.getAttribute("status")==="404")this.#t=[...this.childNodes];else{this.#t=[];for(const t of this.childNodes)if(t.nodeType===1&&t.tagName==="TEMPLATE"&&t.hasAttribute("m-slot")){this.#t.push(...t.content.childNodes),t.remove();break}}}),this.#e=t=>{if(t.defaultPrevented||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey||!(t.target instanceof HTMLAnchorElement))return;const{download:i,href:e,rel:s,target:o}=t.target;i||s==="external"||o==="_blank"||!e||!e.startsWith(location.origin)||a(e)===a(location.href)||(t.preventDefault(),history.pushState({},"",e),this.#a(e))},this.#s=()=>this.#a(location.href),addEventListener("popstate",this.#s),n.addEventListener("click",this.#e),setTimeout(()=>this.#n())}disconnectedCallback(){removeEventListener("popstate",this.#s),n.removeEventListener("click",this.#e),this.#i?.abort(),this.#s=void 0,this.#e=void 0,this.#i=void 0}});}`;
1
+ // render.ts
2
+ import { F_CX, F_EVENT, F_LAZY, F_ROUTER, F_SIGNALS, F_STYLE, F_SUSPENSE } from "./runtime/index.mjs";
3
+ import { CX_JS, EVENT_JS, LAZY_JS, ROUTER_JS, SIGNALS_JS, STYLE_JS, SUSPENSE_JS } from "./runtime/index.mjs";
9
4
 
10
5
  // runtime/utils.ts
11
6
  var regexpHtmlSafe = /["'&<>]/;
@@ -121,18 +116,12 @@ var $vnode = Symbol.for("jsx.vnode");
121
116
  var $fragment = Symbol.for("jsx.fragment");
122
117
  var $html = Symbol.for("jsx.html");
123
118
  var $signal = Symbol.for("mono.signal");
119
+ var $peek = Symbol.for("mono.peek");
124
120
 
125
121
  // version.ts
126
- var VERSION = "0.6.3";
122
+ var VERSION = "0.6.4";
127
123
 
128
124
  // render.ts
129
- var RUNTIME_CX = 1;
130
- var RUNTIME_STYLE = 2;
131
- var RUNTIME_EVENT = 4;
132
- var RUNTIME_SIGNALS = 8;
133
- var RUNTIME_SUSPENSE = 16;
134
- var RUNTIME_LAZY = 32;
135
- var RUNTIME_ROUTER = 64;
136
125
  var cdn = "https://raw.esm.sh";
137
126
  var encoder = new TextEncoder();
138
127
  var customElements = /* @__PURE__ */ new Map();
@@ -296,38 +285,38 @@ async function render(node, options, write, writeJS, componentMode) {
296
285
  };
297
286
  const finalize = async () => {
298
287
  let js = "";
299
- if (rc.flags.runtime & RUNTIME_CX && !(runtimeFlag & RUNTIME_CX)) {
300
- runtimeFlag |= RUNTIME_CX;
288
+ if (rc.flags.runtime & F_CX && !(runtimeFlag & F_CX)) {
289
+ runtimeFlag |= F_CX;
301
290
  js += CX_JS;
302
291
  }
303
- if (rc.flags.runtime & RUNTIME_STYLE && !(runtimeFlag & RUNTIME_STYLE)) {
304
- runtimeFlag |= RUNTIME_STYLE;
292
+ if (rc.flags.runtime & F_STYLE && !(runtimeFlag & F_STYLE)) {
293
+ runtimeFlag |= F_STYLE;
305
294
  js += STYLE_JS;
306
295
  }
307
- if (rc.mfs.size > 0 && !(runtimeFlag & RUNTIME_EVENT)) {
308
- runtimeFlag |= RUNTIME_EVENT;
296
+ if (rc.mfs.size > 0 && !(runtimeFlag & F_EVENT)) {
297
+ runtimeFlag |= F_EVENT;
309
298
  js += EVENT_JS;
310
299
  }
311
- if (signals.store.size + rc.mcs.size + signals.effects.length > 0 && !(runtimeFlag & RUNTIME_SIGNALS)) {
312
- runtimeFlag |= RUNTIME_SIGNALS;
300
+ if ((signals.store.size > 0 || rc.mcs.size > 0 || signals.effects.length > 0) && !(runtimeFlag & F_SIGNALS)) {
301
+ runtimeFlag |= F_SIGNALS;
313
302
  js += SIGNALS_JS;
314
303
  }
315
- if (suspenses.length > 0 && !(runtimeFlag & RUNTIME_SUSPENSE)) {
316
- runtimeFlag |= RUNTIME_SUSPENSE;
304
+ if (suspenses.length > 0 && !(runtimeFlag & F_SUSPENSE)) {
305
+ runtimeFlag |= F_SUSPENSE;
317
306
  js += SUSPENSE_JS;
318
307
  }
319
- if (rc.flags.runtime & RUNTIME_LAZY && !(runtimeFlag & RUNTIME_LAZY)) {
320
- runtimeFlag |= RUNTIME_LAZY;
308
+ if (rc.flags.runtime & F_LAZY && !(runtimeFlag & F_LAZY)) {
309
+ runtimeFlag |= F_LAZY;
321
310
  js += LAZY_JS;
322
311
  }
323
- if (rc.flags.runtime & RUNTIME_ROUTER && !(runtimeFlag & RUNTIME_ROUTER)) {
324
- runtimeFlag |= RUNTIME_ROUTER;
312
+ if (rc.flags.runtime & F_ROUTER && !(runtimeFlag & F_ROUTER)) {
313
+ runtimeFlag |= F_ROUTER;
325
314
  js += ROUTER_JS;
326
315
  }
327
316
  if (js.length > 0) {
328
317
  js = "(()=>{" + js + "})();/* --- */window.$runtimeFlag=" + runtimeFlag + ";";
329
318
  }
330
- if (runtimeFlag & RUNTIME_LAZY || runtimeFlag & RUNTIME_ROUTER) {
319
+ if (runtimeFlag & F_LAZY || runtimeFlag & F_ROUTER) {
331
320
  js += "window.$scopeSeq=" + rc.flags.scope + ";";
332
321
  }
333
322
  if (rc.mfs.size > 0) {
@@ -436,8 +425,27 @@ async function renderNode(rc, node, stripSlotProp) {
436
425
  }
437
426
  // `<toggle>` element
438
427
  case "toggle": {
439
- const { show, children } = props;
428
+ let { show, hidden, children } = props;
440
429
  if (children !== void 0) {
430
+ if (show === void 0 && hidden !== void 0) {
431
+ if (isSignal(hidden)) {
432
+ let { scope, key, value } = hidden[$signal];
433
+ if (typeof key === "string") {
434
+ key = {
435
+ compute: "()=>!this[" + JSON.stringify(key) + "]",
436
+ deps: /* @__PURE__ */ new Set([scope + ":" + key])
437
+ };
438
+ } else {
439
+ key = {
440
+ compute: "()=>!(" + key.compute + ")()",
441
+ deps: key.deps
442
+ };
443
+ }
444
+ show = Signal(scope, key, !value);
445
+ } else {
446
+ show = !hidden;
447
+ }
448
+ }
441
449
  if (isSignal(show)) {
442
450
  const { scope, key, value } = show[$signal];
443
451
  let buf = '<m-signal mode="toggle" scope="' + scope + '" ';
@@ -547,7 +555,7 @@ async function renderNode(rc, node, stripSlotProp) {
547
555
  buf += "<m-group>" + attrModifiers + "</m-group>";
548
556
  }
549
557
  write(buf);
550
- rc.flags.runtime |= RUNTIME_LAZY;
558
+ rc.flags.runtime |= F_LAZY;
551
559
  break;
552
560
  }
553
561
  // `<router>` element
@@ -574,7 +582,7 @@ async function renderNode(rc, node, stripSlotProp) {
574
582
  }
575
583
  buf += "</m-router>";
576
584
  write(buf);
577
- rc.flags.runtime |= RUNTIME_ROUTER;
585
+ rc.flags.runtime |= F_ROUTER;
578
586
  break;
579
587
  }
580
588
  default: {
@@ -647,9 +655,9 @@ function renderAttr(rc, attrName, attrValue, stripSlotProp) {
647
655
  }
648
656
  if (signal) {
649
657
  if (attrName === "class") {
650
- rc.flags.runtime |= RUNTIME_CX;
658
+ rc.flags.runtime |= F_CX;
651
659
  } else if (attrName === "style") {
652
- rc.flags.runtime |= RUNTIME_STYLE;
660
+ rc.flags.runtime |= F_STYLE;
653
661
  }
654
662
  signalValue = signal;
655
663
  attrValue = signal[$signal].value;
@@ -881,6 +889,7 @@ function createSignals(scopeId, appSignals, context = new NullProtoObj(), reques
881
889
  case "refs":
882
890
  return refs;
883
891
  case "computed":
892
+ case "$":
884
893
  return computed;
885
894
  case "effect":
886
895
  return (effect) => {
@@ -937,7 +946,7 @@ function computedProps({ fcCtx }, props) {
937
946
  });
938
947
  if (patches.length > 0) {
939
948
  const { scopeId } = fcCtx;
940
- const compute = "()=>$merge(" + JSON.stringify(staticProps) + ",[" + patches.join("],[") + "])";
949
+ const compute = "()=>$patch(" + JSON.stringify(staticProps) + ",[" + patches.join("],[") + "])";
941
950
  return Signal(scopeId, { compute, deps }, staticProps);
942
951
  }
943
952
  }
@@ -977,6 +986,9 @@ var jsx = (tag, props = new NullProtoObj(), key) => {
977
986
  props.key = key;
978
987
  }
979
988
  if (tag === "html") {
989
+ if (props.request === $peek) {
990
+ return props;
991
+ }
980
992
  const renderOptions = new NullProtoObj();
981
993
  const optionsKeys = /* @__PURE__ */ new Set(["app", "context", "components", "routes", "request", "status", "headers", "htmx"]);
982
994
  for (const [key2, value] of Object.entries(props)) {
@@ -1011,7 +1023,12 @@ Object.assign(globalThis, {
1011
1023
  });
1012
1024
  export {
1013
1025
  Fragment,
1026
+ JSX,
1027
+ html as css,
1028
+ html,
1029
+ html as js,
1014
1030
  jsx,
1015
1031
  jsx as jsxDEV,
1032
+ jsxEscape,
1016
1033
  jsx as jsxs
1017
1034
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
package/types/html.d.ts CHANGED
@@ -374,6 +374,7 @@ export namespace HTML {
374
374
  imageSrcSet?: string;
375
375
  imageSizes?: string;
376
376
  referrerPolicy?: ReferrerPolicy;
377
+ rel?: string;
377
378
  sizes?: string;
378
379
  type?: string;
379
380
  }
@@ -425,6 +426,7 @@ export namespace HTML {
425
426
  charSet?: string;
426
427
  httpEquiv?: string;
427
428
  name?: string;
429
+ content?: string;
428
430
  media?: string;
429
431
  }
430
432
 
package/types/index.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import type { FC } from "./jsx.d.ts";
2
2
 
3
+ /**
4
+ * `buildRoute` creates a routing map for bun server.
5
+ */
6
+ export function buildRoute(
7
+ handler: (req: Request) => Response,
8
+ ): Record<string, (req: Request) => Response>;
9
+
10
+ /**
11
+ * `monoRoutes` creates a routing map for bun server.
12
+ * @deprecated Use `buildRoute` instead.
13
+ */
3
14
  export function monoRoutes(
4
15
  routes: Record<string, FC<any> | Promise<{ default: FC<any> }>>,
5
16
  handler: (req: Request) => Response,
@@ -1,7 +1,8 @@
1
1
  import type { FC, VNode } from "./jsx.d.ts";
2
2
 
3
- export function jsx(tag: string | FC, props: Record<string, unknown>, key?: string | number): VNode;
3
+ export const html: JSX.Raw;
4
+ export const JSX: typeof globalThis.JSX;
5
+ export const Fragment: (props: Record<string, unknown>) => VNode;
6
+ export const jsx: (tag: string | FC, props: Record<string, unknown>, key?: string | number) => VNode;
4
7
 
5
- export function Fragment(props: Record<string, unknown>): VNode;
6
-
7
- export { jsx as jsxDEV, jsx as jsxs };
8
+ export { html as css, html as js, jsx as jsxDEV, jsx as jsxs };
package/types/mono.d.ts CHANGED
@@ -91,20 +91,27 @@ export interface AsyncComponentAttributes {
91
91
 
92
92
  export interface Elements {
93
93
  /**
94
- * The `toggle` element is a builtin element that toggles the visibility of its children.
94
+ * The `toggle` element is a built-in element that toggles the visibility of its children.
95
95
  */
96
96
  toggle: BaseAttributes & {
97
- show?: boolean | 0 | 1;
97
+ show?: any;
98
+ hidden?: any;
98
99
  };
99
100
  /**
100
- * The `switch` element is a builtin element that chooses one of its children based on the `slot` attribute to display.
101
+ * The `switch` element is a built-in element that chooses one of its children based on the `slot` attribute to display.
101
102
  * It is similar to a switch statement in programming languages.
102
103
  */
103
104
  switch: BaseAttributes & {
104
105
  value?: string | number | boolean | null;
105
106
  };
106
107
  /**
107
- * The `component` element is a builtin element that is used to load components lazily,
108
+ * The `for` element is a built-in element for list rendering with signals.
109
+ */
110
+ for: BaseAttributes & {
111
+ items?: unknown[];
112
+ };
113
+ /**
114
+ * The `component` element is a built-in element that is used to load components lazily,
108
115
  * which can improve performance by reducing the initial load time of the application.
109
116
  */
110
117
  component: BaseAttributes & AsyncComponentAttributes & {
@@ -112,7 +119,7 @@ export interface Elements {
112
119
  props?: Record<string, unknown>;
113
120
  };
114
121
  /**
115
- * The `router` element is a builtin element that implements client-side routing.
122
+ * The `router` element is a built-in element that implements client-side routing.
116
123
  */
117
124
  router: BaseAttributes & AsyncComponentAttributes & {};
118
125
  }
@@ -130,17 +137,17 @@ declare global {
130
137
  */
131
138
  type FC<Signals = {}, AppSignals = {}, Context = {}> = {
132
139
  /**
133
- * Application signals.
140
+ * The global signals shared across the application.
134
141
  */
135
142
  readonly app: AppSignals;
136
143
  /**
137
- * Rendering context.
144
+ * The rendering context.
138
145
  *
139
146
  * **⚠ This is a server-side only API.**
140
147
  */
141
148
  readonly context: Context;
142
149
  /**
143
- * Current request object.
150
+ * The `request` object contains the current request information.
144
151
  *
145
152
  * **⚠ This is a server-side only API.**
146
153
  */
@@ -153,10 +160,14 @@ declare global {
153
160
  * The `computed` method is used to create a computed signal.
154
161
  */
155
162
  readonly computed: <T = unknown>(fn: () => T) => T;
163
+ /**
164
+ * `this.$(fn)` is just a shortcut for `this.computed(fn)`.
165
+ */
166
+ readonly $: <T = unknown>(fn: () => T) => T;
156
167
  /**
157
168
  * The `effect` method is used to create a side effect.
158
169
  * **The effect function is only called on client side.**
159
170
  */
160
171
  readonly effect: (fn: () => void | (() => void)) => void;
161
- } & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
172
+ } & Omit<Signals, "app" | "context" | "request" | "refs" | "computed" | "$" | "effect">;
162
173
  }
package/types/render.d.ts CHANGED
@@ -11,7 +11,7 @@ type HtmxExts = {
11
11
  | boolean;
12
12
  };
13
13
 
14
- export type FCModule = FC<any> | Promise<{ default: FC<any> }>;
14
+ export type MaybeModule<T> = T | Promise<{ default: T }>;
15
15
 
16
16
  /**
17
17
  * Render options for the `render` function.
@@ -28,11 +28,11 @@ export interface RenderOptions extends Partial<HtmxExts> {
28
28
  /**
29
29
  * Components to be rendered by the `<lazy>` element.
30
30
  */
31
- components?: Record<string, FCModule>;
31
+ components?: Record<string, MaybeModule<FC<any>>>;
32
32
  /**
33
33
  * Routes to be used by the `<router>` element.
34
34
  */
35
- routes?: Record<string, FCModule>;
35
+ routes?: Record<string, MaybeModule<FC<any>>>;
36
36
  /**
37
37
  * Current `Request` object to be passed to components.
38
38
  */