mono-jsx 0.6.5 → 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 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:
@@ -194,7 +202,7 @@ function App() {
194
202
 
195
203
  ### Using `html` Tag Function
196
204
 
197
- 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.
198
206
 
199
207
  ```tsx
200
208
  function App() {
@@ -202,7 +210,7 @@ function App() {
202
210
  }
203
211
  ```
204
212
 
205
- 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:
206
214
 
207
215
  ```tsx
208
216
  function App() {
@@ -232,8 +240,7 @@ function Button() {
232
240
  }
233
241
  ```
234
242
 
235
- > [!NOTE]
236
- > 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.**
237
244
 
238
245
  ```tsx
239
246
  import { doSomething } from "some-library";
@@ -418,6 +425,9 @@ function App(this: FC<{ input: string }>) {
418
425
  }
419
426
  ```
420
427
 
428
+ > [!TIP]
429
+ > You can use `this.$` as a shorthand for `this.computed` to create computed signals.
430
+
421
431
  ### Using Effects
422
432
 
423
433
  You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
@@ -475,7 +485,7 @@ function App(this: FC<{ show: boolean }>) {
475
485
 
476
486
  ### Using `<toggle>` Element with Signals
477
487
 
478
- 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.
479
489
 
480
490
  ```tsx
481
491
  function App(this: FC<{ show: boolean }>) {
@@ -501,7 +511,7 @@ function App(this: FC<{ show: boolean }>) {
501
511
 
502
512
  ### Using `<switch>` Element with Signals
503
513
 
504
- 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.
505
515
 
506
516
  ```tsx
507
517
  function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
@@ -619,7 +629,7 @@ mono-jsx binds a scoped signals object to `this` of your component functions. Th
619
629
 
620
630
  The `this` object has the following built-in properties:
621
631
 
622
- - `app`: The app signals defined on the root `<html>` element.
632
+ - `app`: The app global signals..
623
633
  - `context`: The context defined on the root `<html>` element.
624
634
  - `request`: The request object from the `fetch` handler.
625
635
  - `refs`: A map of refs defined in the component.
@@ -627,12 +637,13 @@ The `this` object has the following built-in properties:
627
637
  - `effect`: A method to create side effects.
628
638
 
629
639
  ```ts
630
- type FC<Signals = {}, AppSignals = {}, Context = {}> = {
631
- readonly app: AppSignals;
640
+ type FC<Signals = {}, AppSignals = {}, Context = {}, Refs = {}, AppRefs = {}> = {
641
+ readonly app: AppSignals & { refs: AppRefs; url: WithParams<URL> }
632
642
  readonly context: Context;
633
- readonly request: Request & { params?: Record<string, string> };
634
- readonly refs: Record<string, HTMLElement | null>;
643
+ readonly request: WithParams<Request>;
644
+ readonly refs: Refs;
635
645
  readonly computed: <T = unknown>(fn: () => T) => T;
646
+ readonly $: FC["computed"];
636
647
  readonly effect: (fn: () => void | (() => void)) => void;
637
648
  } & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
638
649
  ```
@@ -643,10 +654,10 @@ See the [Using Signals](#using-signals) section for more details on how to use s
643
654
 
644
655
  ### Using Refs
645
656
 
646
- 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.
647
658
 
648
659
  ```tsx
649
- function App(this: FC) {
660
+ function App(this: Refs<FC, { input: HTMLInputElement }>) {
650
661
  this.effect(() => {
651
662
  this.refs.input?.addEventListener("input", (evt) => {
652
663
  console.log("Input changed:", evt.target.value);
@@ -662,12 +673,55 @@ function App(this: FC) {
662
673
  }
663
674
  ```
664
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
+
665
719
  ### Using Context
666
720
 
667
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:
668
722
 
669
723
  ```tsx
670
- function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
724
+ function Dash(this: Context<FC, { auth: { uuid: string; name: string } }>) {
671
725
  const { auth } = this.context;
672
726
  return (
673
727
  <div>
@@ -790,7 +844,7 @@ async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
790
844
  this.show = false;
791
845
  return (
792
846
  <div>
793
- <toggle value={this.show}>
847
+ <toggle show={this.show}>
794
848
  <component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
795
849
  </toggle>
796
850
  <button onClick={() => this.show = true }>Load `Foo` Component</button>
@@ -912,6 +966,20 @@ function Post(this: FC) {
912
966
  }
913
967
  ```
914
968
 
969
+ ### Using `this.app.url` Signal
970
+
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:
972
+
973
+ ```tsx
974
+ function App(this: FC) {
975
+ return (
976
+ <div>
977
+ <h1>Current Pathname: {this.$(() => this.app.url.pathname)}</h1>
978
+ </div>
979
+ )
980
+ }
981
+ ```
982
+
915
983
  ### Navigation between Pages
916
984
 
917
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:
package/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // index.ts
2
2
  function buildRoutes(handler) {
3
- const { routes = {} } = handler(Symbol.for("mono.peek"));
3
+ const { routes = {} } = handler(Symbol.for("mono.setup"));
4
4
  return monoRoutes(routes, handler);
5
5
  }
6
6
  function monoRoutes(routes, handler) {
package/jsx-runtime.mjs CHANGED
@@ -1,20 +1,36 @@
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";
1
+ // runtime/index.ts
2
+ var EVENT = 1;
3
+ var CX = 2;
4
+ var STYLE = 4;
5
+ var RENDER_ATTR = 8;
6
+ var RENDER_TOGGLE = 16;
7
+ var RENDER_SWITCH = 32;
8
+ var SIGNALS = 64;
9
+ var SUSPENSE = 128;
10
+ var LAZY = 256;
11
+ var ROUTER = 512;
12
+ 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)};}`;
13
+ var CX_JS = `{var n=e=>typeof e=="string"?e:typeof e=="object"&&e!==null?Array.isArray(e)?e.map(n).filter(Boolean).join(" "):Object.entries(e).filter(([,t])=>!!t).map(([t])=>t).join(" "):"";window.$cx=n;}`;
14
+ var STYLE_JS = `{var a=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;var l=e=>typeof e=="string";var u=e=>e.replace(/[a-z][A-Z]/g,r=>r.charAt(0)+"-"+r.charAt(1).toLowerCase());var p=e=>{let r=[],t=[],n=new g;for(let[o,s]of Object.entries(e))switch(o.charCodeAt(0)){case 58:t.push(null,o+"{"+c(s)+"}");break;case 64:t.push(o+"{",null,"{"+c(s)+"}}");break;case 38:t.push(null,o.slice(1)+"{"+c(s)+"}");break;default:r.push([o,s])}return r.length>0&&(n.inline=c(r)),t.length>0&&(n.css=t),n},f=(e,r)=>{let{inline:t,css:n}=p(r);if(n){let o="data-css-",s="["+o+(Date.now()+Math.random()).toString(36).replace(".","")+"]";document.head.appendChild(document.createElement("style")).textContent=(t?s+"{"+t+"}":"")+n.map(i=>i===null?s:i).join(""),e.getAttributeNames().forEach(i=>i.startsWith(o)&&e.removeAttribute(i)),e.setAttribute(s.slice(1,-1),"")}else t&&e.setAttribute("style",t)},c=e=>{if(typeof e=="object"&&e!==null){let r="";for(let[t,n]of Array.isArray(e)?e:Object.entries(e))if(l(n)||typeof n=="number"){let o=u(t),s=typeof n=="number"?a.test(t)?""+n:n+"px":""+n;r+=(r?";":"")+o+":"+(o==="content"?JSON.stringify(s):s)}return r}return""},g=(()=>{function e(){}return e.prototype=Object.freeze(Object.create(null)),e})();window.$applyStyle=f;}`;
15
+ var RENDER_ATTR_JS = `{var s=(l,t,r)=>{let n=l.parentElement;return n.tagName==="M-GROUP"&&(n=n.previousElementSibling),()=>{let e=r();e===!1||e===null||e===void 0?n.removeAttribute(t):typeof e=="object"&&e!==null&&(t==="class"||t==="style"||t==="props")?t==="class"?n.setAttribute(t,$cx(e)):t==="style"?$applyStyle(n,e):n.setAttribute(t,JSON.stringify(e)):n.setAttribute(t,e===!0?"":e)}};window.$renderAttr=s;}`;
16
+ var RENDER_TOGGLE_JS = `{var s=(n,l)=>{let e;return()=>{if(!e){let t=n.firstElementChild;t&&t.tagName==="TEMPLATE"&&t.hasAttribute("m-slot")?e=[...t.content.childNodes]:e=[...n.childNodes]}n.replaceChildren(...l()?e:[])}};window.$renderToggle=s;}`;
17
+ var RENDER_SWITCH_JS = `{var u=(s,d)=>{let r,i=s.getAttribute("value"),t,l,o=e=>t.get(e)??t.set(e,[]).get(e);return()=>{if(!t){t=new Map,l=[];for(let e of s.childNodes)if(e.nodeType===1&&e.tagName==="TEMPLATE"&&e.hasAttribute("m-slot")){for(let n of e.content.childNodes)n.nodeType===1&&n.hasAttribute("slot")?o(n.getAttribute("slot")).push(n):l.push(n);e.remove()}else i?o(i).push(e):l.push(e)}r=""+d(),s.replaceChildren(...t.has(r)?t.get(r):l)}};window.$renderSwitch=u;}`;
18
+ var SIGNALS_JS = `{let m;const h=window,b=new Map,y=new Map,f=n=>y.get(n)??y.set(n,S(n)).get(n),k=()=>Object.create(null),d=(n,e)=>n.getAttribute(e),S=n=>{const e=k(),t=(c,r)=>{e[c]=r},s=new Map,a=(c,r)=>{let i=s.get(c);return i||(i=new Set,s.set(c,i)),i.add(r),()=>{i.delete(r),i.size===0&&s.delete(c)}},o=new Proxy(k(),{get:(c,r)=>document.querySelector("[data-ref='"+n+":"+r+"']")});return new Proxy(e,{get:(c,r,i)=>{switch(r){case"$init":return t;case"$watch":return a;case"app":return f(0);case"refs":return o;default:return m?.(n,r),Reflect.get(c,r,i)}},set:(c,r,i,l)=>{if(i!==Reflect.get(c,r,l)){const u=s.get(r);return u&&queueMicrotask(()=>u.forEach(g=>g())),Reflect.set(c,r,i,l)}return!1}})},$=(n,e,t)=>{switch(e){case"toggle":return $renderToggle(n,t);case"switch":return $renderSwitch(n,t);case"html":return()=>n.innerHTML=""+t()}return e&&e.length>2&&e.startsWith("[")&&e.endsWith("]")?$renderAttr(n,e.slice(1,-1),t):()=>n.textContent=""+t()},v=n=>{const e=n.indexOf(":");return e>0?[Number(n.slice(0,e)),n.slice(e+1)]:null},p=async n=>{const e=n();return e!==void 0?e:(await new Promise(t=>setTimeout(t,0)),p(n))},E=(n,e)=>customElements.define(n,class extends HTMLElement{disposes=[];connectedCallback(){e(this)}disconnectedCallback(){this.disposes.forEach(t=>t()),this.disposes.length=0}});E("m-signal",n=>{const e=Number(d(n,"scope")),t=f(e),s=d(n,"key");if(s)n.disposes.push(t.$watch(s,$(n,d(n,"mode"),()=>t[s])));else{const a=Number(d(n,"computed"));p(()=>b.get(e*1e9+a)).then(([o,c])=>{const r=$(n,d(n,"mode"),o.bind(t));c.forEach(i=>{const[l,u]=v(i);n.disposes.push(f(l).$watch(u,r))})})}}),E("m-effect",n=>{const{disposes:e}=n,t=Number(d(n,"scope")),s=Number(d(n,"n")),a=new Array(s);e.push(()=>{a.forEach(o=>typeof o=="function"&&o()),a.length=0});for(let o=0;o<s;o++){const c="$ME_"+t+"_"+o;p(()=>h[c]).then(r=>{const i=[],l=f(t),u=()=>{a[o]=r.call(l)};m=(g,w)=>i.push([g,w]),u(),m=void 0;for(const[g,w]of i)e.push(f(g).$watch(w,u))},()=>{})}}),h.$MS=(n,e)=>{const[t,s]=v(n);f(t).$init(s,e)},h.$MC=(n,e,t,s)=>{b.set(n*1e9+e,[t,s])},h.$patch=(n,...e)=>{for(const[t,...s]of e){const a=s.pop();let o=n;for(const c of s)o=o[c];o[a]=t}return n},h.$signals=n=>n!==void 0?f(n):void 0;}`;
19
+ var SUSPENSE_JS = `{const i=new Map,o=e=>e.getAttribute("chunk-id"),l=(e,t)=>customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}});l("m-portal",e=>{i.set(o(e),e)}),l("m-chunk",e=>{setTimeout(()=>{const t=o(e),n=i.get(t),s=e.firstChild?.content.childNodes;n&&(e.hasAttribute("next")?s&&n.before(...s):(e.hasAttribute("done")?n.remove():s&&n.replaceWith(...s),i.delete(t)),e.remove())})});}`;
20
+ var LAZY_JS = `{const r=document,o=(t,s)=>t.getAttribute(s),i=(t,s=[])=>t.replaceChildren(...s);customElements.define("m-component",class extends HTMLElement{static observedAttributes=["name","props"];#t;#e;#i;#n;#s;async#r(){if(!this.#t){i(this);return}const t={"x-component":this.#t,"x-props":this.#e||"{}","x-flags":$FLAGS},s=new AbortController;this.#n?.abort(),this.#n=s,i(this,this.#i);const e=await fetch(location.href,{headers:t,signal:s.signal});if(!e.ok)throw i(this),new Error("Failed to fetch component '"+this.#t+"'");const[h,n]=await e.json();this.innerHTML=h,n&&(r.body.appendChild(r.createElement("script")).textContent=n)}get name(){return this.#t??null}set name(t){t&&t!==this.#t&&(this.#t=t,this.refresh())}get props(){return this.#e?JSON.parse(this.#e):void 0}set props(t){const s=typeof t=="string"?t:JSON.stringify(t);s&&s!==this.#e&&(this.#e=s,this.refresh())}connectedCallback(){setTimeout(()=>{if(!this.#i){const t=o(this,"props");this.#t=o(this,"name"),this.#e=t?.startsWith("base64,")?atob(t.slice(7)):void 0,this.#i=[...this.childNodes]}this.#r()})}disconnectedCallback(){i(this,this.#i),this.#n?.abort(),this.#n=void 0,this.#s&&clearTimeout(this.#s),this.#s=void 0}attributeChangedCallback(t,s,e){this.#t&&e&&(t==="name"?this.name=e:t==="props"&&(this.props=e))}refresh(){this.#s&&clearTimeout(this.#s),this.#s=setTimeout(()=>{this.#s=void 0,this.#r()},50)}});}`;
21
+ var ROUTER_JS = `{const a=document,n=location,l=t=>t.split("#",1)[0],c=t=>l(t)===l(n.href);customElements.define("m-router",class extends HTMLElement{#t;#e;#s;#i;async#o(t){const s=new AbortController,e={"x-route":"true","x-flags":$FLAGS};this.#i?.abort(),this.#i=s;const i=await fetch(t,{headers:e,signal:s.signal});if(i.status===404)return this.replaceChildren(...this.#t),!0;if(!i.ok)throw this.replaceChildren(),new Error("Failed to fetch route: "+i.status+" "+i.statusText);const[o,r]=await i.json();this.innerHTML=o,r&&(a.body.appendChild(a.createElement("script")).textContent=r)}#n(){a.querySelectorAll("nav a").forEach(t=>{const{href:s,classList:e}=t,i=t.closest("nav")?.getAttribute("data-active-class")??"active";c(s)?e.add(i):e.remove(i)})}navigate(t,s){const e=new URL(t,n.href);if(e.origin!==n.origin){n.href=t;return}c(e.href)||this.#a(t,s)}async#a(t,s){const e=await this.#o(t);s?.replace?history.replaceState({},"",t):history.pushState({},"",t),e&&typeof $signals<"u"&&($signals(0).url=new URL(t)),this.#n(),window.scrollTo(0,0)}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:s,href:e,rel:i,target:o}=t.target;s||i==="external"||o==="_blank"||!e.startsWith(n.origin)||(t.preventDefault(),this.navigate(e))},this.#s=()=>this.#a(n.href),addEventListener("popstate",this.#s),a.addEventListener("click",this.#e),setTimeout(()=>this.#n())}disconnectedCallback(){removeEventListener("popstate",this.#s),a.removeEventListener("click",this.#e),this.#i?.abort(),this.#i=void 0,this.#e=void 0,this.#s=void 0}});}`;
4
22
 
5
23
  // runtime/utils.ts
24
+ var regexpCssBareUnitProps = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
6
25
  var regexpHtmlSafe = /["'&<>]/;
7
- var cssBareUnitProps = new Set(
8
- "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(",")
9
- );
10
26
  var isString = (v) => typeof v === "string";
11
27
  var isObject = (v) => typeof v === "object" && v !== null;
12
28
  var toHyphenCase = (k) => k.replace(/[a-z][A-Z]/g, (m) => m.charAt(0) + "-" + m.charAt(1).toLowerCase());
13
29
  var cx = (className) => {
14
- if (isString(className)) {
30
+ if (typeof className === "string") {
15
31
  return className;
16
32
  }
17
- if (isObject(className)) {
33
+ if (typeof className === "object" && className !== null) {
18
34
  if (Array.isArray(className)) {
19
35
  return className.map(cx).filter(Boolean).join(" ");
20
36
  }
@@ -53,12 +69,12 @@ var styleToCSS = (style) => {
53
69
  return ret;
54
70
  };
55
71
  var renderStyle = (style) => {
56
- if (isObject(style)) {
72
+ if (typeof style === "object" && style !== null) {
57
73
  let css = "";
58
74
  for (const [k, v] of Array.isArray(style) ? style : Object.entries(style)) {
59
75
  if (isString(v) || typeof v === "number") {
60
76
  const cssKey = toHyphenCase(k);
61
- const cssValue = typeof v === "number" ? cssBareUnitProps.has(cssKey) ? "" + v : v + "px" : "" + v;
77
+ const cssValue = typeof v === "number" ? regexpCssBareUnitProps.test(k) ? "" + v : v + "px" : "" + v;
62
78
  css += (css ? ";" : "") + cssKey + ":" + (cssKey === "content" ? JSON.stringify(cssValue) : cssValue);
63
79
  }
64
80
  }
@@ -112,14 +128,14 @@ var escapeHTML = (str) => {
112
128
  };
113
129
 
114
130
  // symbols.ts
115
- var $vnode = Symbol.for("jsx.vnode");
116
131
  var $fragment = Symbol.for("jsx.fragment");
117
132
  var $html = Symbol.for("jsx.html");
133
+ var $setup = Symbol.for("mono.setup");
118
134
  var $signal = Symbol.for("mono.signal");
119
- var $peek = Symbol.for("mono.peek");
135
+ var $vnode = Symbol.for("jsx.vnode");
120
136
 
121
137
  // version.ts
122
- var VERSION = "0.6.4";
138
+ var VERSION = "0.6.6";
123
139
 
124
140
  // render.ts
125
141
  var cdn = "https://raw.esm.sh";
@@ -132,16 +148,43 @@ var hashCode = (s) => [...s].reduce((hash, c) => Math.imul(31, hash) + c.charCod
132
148
  var escapeCSSText = (str) => str.replace(/[<>]/g, (m) => m.charCodeAt(0) === 60 ? "&lt;" : "&gt;");
133
149
  var toAttrStringLit = (str) => '"' + escapeHTML(str) + '"';
134
150
  var toStr = (v, str) => v !== void 0 ? str(v) : "";
151
+ var stringify = JSON.stringify;
135
152
  var Ref = class {
136
153
  constructor(scope, name) {
137
154
  this.scope = scope;
138
155
  this.name = name;
139
156
  }
140
157
  };
141
- var IdGenImpl = class extends Map {
142
- _id = 0;
158
+ var IdGen = class extends Map {
159
+ #seq = 0;
143
160
  gen(v) {
144
- return this.get(v) ?? this.set(v, this._id++).get(v);
161
+ return this.get(v) ?? this.set(v, this.#seq++).get(v);
162
+ }
163
+ };
164
+ var IdGenManagerImpl = class {
165
+ #scopes = /* @__PURE__ */ new Map();
166
+ size = 0;
167
+ gen(key, scope = 0) {
168
+ let idGen = this.#scopes.get(scope);
169
+ if (!idGen) {
170
+ idGen = new IdGen();
171
+ this.#scopes.set(scope, idGen);
172
+ }
173
+ this.size++;
174
+ return idGen.gen(key);
175
+ }
176
+ toJS(callback) {
177
+ let js = "";
178
+ for (const [scope, gens] of this.#scopes) {
179
+ for (const [v, id] of gens.entries()) {
180
+ js += callback(scope, id, v);
181
+ }
182
+ }
183
+ return js;
184
+ }
185
+ clear() {
186
+ this.#scopes.clear();
187
+ this.size = 0;
145
188
  }
146
189
  };
147
190
  var JSX = {
@@ -152,13 +195,17 @@ var JSX = {
152
195
  }
153
196
  };
154
197
  function renderHtml(node, options) {
155
- const { request, routes, components, headers: headersInit } = options;
198
+ const { routes, components } = options;
199
+ const request = options.request;
156
200
  const headers = new Headers();
157
201
  const reqHeaders = request?.headers;
158
202
  const componentHeader = reqHeaders?.get("x-component");
159
203
  let routeFC = request ? Reflect.get(request, "x-route") : void 0;
160
204
  let component = componentHeader ? components?.[componentHeader] : null;
161
205
  let status = options.status;
206
+ if (request) {
207
+ request.URL = new URL(request.url);
208
+ }
162
209
  if (routes && !routeFC) {
163
210
  if (request) {
164
211
  const patterns = Object.keys(routes);
@@ -166,7 +213,7 @@ function renderHtml(node, options) {
166
213
  for (const pattern of patterns) {
167
214
  if (pattern.includes(":") || pattern.includes("*")) {
168
215
  dynamicPatterns.push(pattern);
169
- } else if (new URL(request.url).pathname === pattern) {
216
+ } else if (request.URL.pathname === pattern) {
170
217
  routeFC = routes[pattern];
171
218
  break;
172
219
  }
@@ -176,23 +223,23 @@ function renderHtml(node, options) {
176
223
  const match = new URLPattern({ pathname: path }).exec(request.url);
177
224
  if (match) {
178
225
  routeFC = routes[path];
179
- Object.assign(request, { params: match.pathname.groups });
226
+ request.params = match.pathname.groups;
180
227
  break;
181
228
  }
182
229
  }
183
230
  }
184
231
  } else {
185
- console.error("The `request` prop in the `<html>` element is required for routing.");
232
+ console.error("[mono-jsx] The `request` prop in the `<html>` element is required for routing.");
186
233
  }
187
234
  if (!routeFC) {
188
235
  status = 404;
189
236
  }
190
237
  }
191
238
  if (components && !request) {
192
- console.warn("The `components` prop in the `<html>` element is ignored when `request` is not provided.");
239
+ console.warn("[mono-jsx] The `components` prop in the `<html>` element is ignored when `request` is not provided.");
193
240
  }
194
- if (headersInit) {
195
- for (const [key, value] of Object.entries(headersInit)) {
241
+ if (options.headers) {
242
+ for (const [key, value] of Object.entries(options.headers)) {
196
243
  if (value) {
197
244
  headers.set(toHyphenCase(key), value);
198
245
  }
@@ -221,9 +268,9 @@ function renderHtml(node, options) {
221
268
  (chunk) => js += chunk,
222
269
  true
223
270
  );
224
- let json = "[" + JSON.stringify(html2);
271
+ let json = "[" + stringify(html2);
225
272
  if (js) {
226
- json += "," + JSON.stringify(js);
273
+ json += "," + stringify(js);
227
274
  }
228
275
  controller.enqueue(encoder.encode(json + "]"));
229
276
  } finally {
@@ -279,66 +326,66 @@ async function render(node, options, write, writeJS, componentMode) {
279
326
  signals,
280
327
  routeFC,
281
328
  eager: componentMode,
282
- flags: { scope: 0, chunk: 0, refs: 0, runtime: 0 },
283
- mcs: new IdGenImpl(),
284
- mfs: new IdGenImpl()
329
+ flags: { scope: 0, chunk: 0, runtime: 0 },
330
+ mcs: new IdGenManagerImpl(),
331
+ mfs: new IdGenManagerImpl()
285
332
  };
286
333
  const finalize = async () => {
334
+ const hasEffect = signals.effects.length > 0;
335
+ const treeshake = (flag, code, force) => {
336
+ if ((force || rc.flags.runtime & flag) && !(runtimeFlag & flag)) {
337
+ runtimeFlag |= flag;
338
+ js += code;
339
+ }
340
+ };
287
341
  let js = "";
288
- if (rc.flags.runtime & F_CX && !(runtimeFlag & F_CX)) {
289
- runtimeFlag |= F_CX;
290
- js += CX_JS;
291
- }
292
- if (rc.flags.runtime & F_STYLE && !(runtimeFlag & F_STYLE)) {
293
- runtimeFlag |= F_STYLE;
294
- js += STYLE_JS;
295
- }
296
- if (rc.mfs.size > 0 && !(runtimeFlag & F_EVENT)) {
297
- runtimeFlag |= F_EVENT;
298
- js += EVENT_JS;
299
- }
300
- if ((signals.store.size > 0 || rc.mcs.size > 0 || signals.effects.length > 0) && !(runtimeFlag & F_SIGNALS)) {
301
- runtimeFlag |= F_SIGNALS;
302
- js += SIGNALS_JS;
303
- }
304
- if (suspenses.length > 0 && !(runtimeFlag & F_SUSPENSE)) {
305
- runtimeFlag |= F_SUSPENSE;
306
- js += SUSPENSE_JS;
307
- }
308
- if (rc.flags.runtime & F_LAZY && !(runtimeFlag & F_LAZY)) {
309
- runtimeFlag |= F_LAZY;
310
- js += LAZY_JS;
311
- }
312
- if (rc.flags.runtime & F_ROUTER && !(runtimeFlag & F_ROUTER)) {
313
- runtimeFlag |= F_ROUTER;
314
- js += ROUTER_JS;
342
+ treeshake(CX, CX_JS);
343
+ treeshake(STYLE, STYLE_JS);
344
+ treeshake(EVENT, EVENT_JS, rc.mfs.size > 0);
345
+ if (signals.store.size > 0 || rc.mcs.size > 0 || hasEffect) {
346
+ treeshake(RENDER_ATTR, RENDER_ATTR_JS);
347
+ treeshake(RENDER_TOGGLE, RENDER_TOGGLE_JS);
348
+ treeshake(RENDER_SWITCH, RENDER_SWITCH_JS);
349
+ treeshake(SIGNALS, SIGNALS_JS, true);
315
350
  }
351
+ treeshake(SUSPENSE, SUSPENSE_JS, suspenses.length > 0);
352
+ treeshake(LAZY, LAZY_JS);
353
+ treeshake(ROUTER, ROUTER_JS);
316
354
  if (js.length > 0) {
317
- js = "(()=>{" + js + "})();/* --- */window.$runtimeFlag=" + runtimeFlag + ";";
355
+ js = "(()=>{" + js + "})();/* --- */";
318
356
  }
319
- if (runtimeFlag & F_LAZY || runtimeFlag & F_ROUTER) {
320
- js += "window.$scopeSeq=" + rc.flags.scope + ";";
357
+ if (runtimeFlag & LAZY || runtimeFlag & ROUTER) {
358
+ const { scope, chunk } = rc.flags;
359
+ js += 'window.$FLAGS="' + scope + "|" + chunk + "|" + runtimeFlag + '";';
321
360
  }
322
361
  if (rc.mfs.size > 0) {
323
- for (const [fn, i] of rc.mfs.entries()) {
324
- js += "function $MF_" + i + "(){(" + fn.toString() + ").apply(this,arguments)};";
325
- }
362
+ js += rc.mfs.toJS((scope, seq, fn) => "function $MF_" + scope + "_" + seq + "(){(" + fn.toString() + ").apply(this,arguments)};");
326
363
  rc.mfs.clear();
327
364
  }
328
- if (signals.effects.length > 0) {
365
+ if (hasEffect) {
329
366
  js += signals.effects.splice(0, signals.effects.length).join("");
330
367
  }
368
+ if (runtimeFlag & ROUTER && runtimeFlag & SIGNALS && request) {
369
+ const { params } = request;
370
+ const url = "new URL(" + stringify(request.url) + ")";
371
+ const urlWithParams = params ? "Object.assign(" + url + "," + stringify(params) + ")" : url;
372
+ if (componentMode) {
373
+ js += "$signals(0).url=" + urlWithParams + ";";
374
+ } else {
375
+ js += '$MS("0:url",' + urlWithParams + ");";
376
+ }
377
+ }
331
378
  if (signals.store.size > 0) {
332
379
  for (const [key, value] of signals.store.entries()) {
333
- js += "$MS(" + JSON.stringify(key) + (value !== void 0 ? "," + JSON.stringify(value) : "") + ");";
380
+ js += "$MS(" + stringify(key) + (value !== void 0 ? "," + stringify(value) : "") + ");";
334
381
  }
335
382
  signals.store.clear();
336
383
  }
337
384
  if (rc.mcs.size > 0) {
338
- for (const [mc, i] of rc.mcs.entries()) {
339
- const { compute, deps } = mc[$signal].key;
340
- js += "$MC(" + i + ",function(){return(" + compute.toString() + ").call(this)}," + JSON.stringify([...deps.values()]) + ");";
341
- }
385
+ js += rc.mcs.toJS((scope, seq, signal) => {
386
+ const { compute, deps } = signal[$signal].key;
387
+ return "$MC(" + scope + "," + seq + ",function(){return(" + compute.toString() + ").call(this)}," + stringify([...deps.values()]) + ");";
388
+ });
342
389
  rc.mcs.clear();
343
390
  }
344
391
  if (js.length > 0) {
@@ -351,8 +398,13 @@ async function render(node, options, write, writeJS, componentMode) {
351
398
  };
352
399
  let runtimeFlag = 0;
353
400
  if (componentMode && request) {
354
- rc.flags.scope = Number(request.headers.get("x-scope-seq")) || 0;
355
- runtimeFlag = Number(request.headers.get("x-runtime-flag")) || 0;
401
+ const headers = request.headers;
402
+ const flagsHeader = headers.get("x-flags")?.split("|");
403
+ if (flagsHeader?.length === 3) {
404
+ const [scope, chunk, runtime] = flagsHeader.map(Number);
405
+ Object.assign(rc.flags, { scope, chunk });
406
+ runtimeFlag = runtime;
407
+ }
356
408
  }
357
409
  await renderNode(rc, node);
358
410
  if (rc.flags.scope > 0 && !componentMode) {
@@ -373,22 +425,7 @@ async function renderNode(rc, node, stripSlotProp) {
373
425
  case "object":
374
426
  if (node === null) {
375
427
  } else if (isSignal(node)) {
376
- const { scope, key, value } = node[$signal];
377
- if (isString(key)) {
378
- let buf = '<m-signal scope="' + scope + '" key=' + toAttrStringLit(key) + ">";
379
- if (value !== void 0 && value !== null) {
380
- buf += escapeHTML(String(value));
381
- }
382
- buf += "</m-signal>";
383
- write(buf);
384
- } else {
385
- let buf = '<m-signal scope="' + scope + '" computed="' + rc.mcs.gen(node) + '">';
386
- if (value !== void 0) {
387
- buf += escapeHTML(String(value));
388
- }
389
- buf += "</m-signal>";
390
- write(buf);
391
- }
428
+ renderSignal(rc, node, void 0, node[$signal].value);
392
429
  } else if (isVNode(node)) {
393
430
  const [tag, props] = node;
394
431
  switch (tag) {
@@ -401,8 +438,13 @@ async function renderNode(rc, node, stripSlotProp) {
401
438
  }
402
439
  // XSS!
403
440
  case $html: {
404
- if (props.innerHTML) {
405
- write(props.innerHTML);
441
+ const { innerHTML } = props;
442
+ if (innerHTML) {
443
+ if (isSignal(innerHTML)) {
444
+ renderSignal(rc, innerHTML, "html", String(innerHTML[$signal].value), true);
445
+ } else {
446
+ write(innerHTML);
447
+ }
406
448
  }
407
449
  break;
408
450
  }
@@ -432,7 +474,7 @@ async function renderNode(rc, node, stripSlotProp) {
432
474
  let { scope, key, value } = hidden[$signal];
433
475
  if (typeof key === "string") {
434
476
  key = {
435
- compute: "()=>!this[" + JSON.stringify(key) + "]",
477
+ compute: "()=>!this[" + stringify(key) + "]",
436
478
  deps: /* @__PURE__ */ new Set([scope + ":" + key])
437
479
  };
438
480
  } else {
@@ -452,11 +494,12 @@ async function renderNode(rc, node, stripSlotProp) {
452
494
  if (isString(key)) {
453
495
  buf += "key=" + toAttrStringLit(key) + ">";
454
496
  } else {
455
- buf += 'computed="' + rc.mcs.gen(show) + '">';
497
+ buf += 'computed="' + rc.mcs.gen(show, rc.fcCtx?.scopeId) + '">';
456
498
  }
457
499
  if (!value) {
458
500
  buf += "<template m-slot>";
459
501
  }
502
+ rc.flags.runtime |= RENDER_TOGGLE;
460
503
  write(buf);
461
504
  await renderChildren(rc, children);
462
505
  write((!value ? "</template>" : "") + "</m-signal>");
@@ -479,8 +522,9 @@ async function renderNode(rc, node, stripSlotProp) {
479
522
  if (isString(key)) {
480
523
  stateful += "key=" + toAttrStringLit(key) + ">";
481
524
  } else {
482
- stateful += 'computed="' + rc.mcs.gen(valueProp) + '">';
525
+ stateful += 'computed="' + rc.mcs.gen(valueProp, rc.fcCtx?.scopeId) + '">';
483
526
  }
527
+ rc.flags.runtime |= RENDER_SWITCH;
484
528
  toSlotName = String(value);
485
529
  } else {
486
530
  toSlotName = String(valueProp);
@@ -524,22 +568,19 @@ async function renderNode(rc, node, stripSlotProp) {
524
568
  }
525
569
  // `<component>` element
526
570
  case "component": {
527
- const { placeholder } = props;
571
+ let { placeholder } = props;
528
572
  let attrs = "";
529
573
  let attrModifiers = "";
530
- for (const p of ["name", "props"]) {
574
+ for (const p of ["name", "props", "ref"]) {
531
575
  let propValue = props[p];
532
- const [attr, , attrSignal] = renderAttr(rc, p, propValue);
576
+ let [attr, , attrSignal] = renderAttr(rc, p, propValue);
533
577
  if (attrSignal) {
534
- const { scope, key, value } = attrSignal[$signal];
535
- attrModifiers += '<m-signal mode="[' + p + ']" scope="' + scope + '" ';
536
- if (isString(key)) {
537
- attrModifiers += "key=" + toAttrStringLit(key);
538
- } else {
539
- attrModifiers += 'computed="' + rc.mcs.gen(attrSignal) + '"';
540
- }
541
- attrModifiers += "></m-signal>";
542
- propValue = value;
578
+ const write2 = (chunk) => {
579
+ attrModifiers += chunk;
580
+ };
581
+ renderSignal({ ...rc, write: write2 }, attrSignal, [p]);
582
+ rc.flags.runtime |= RENDER_ATTR;
583
+ propValue = attrSignal[$signal].value;
543
584
  }
544
585
  attrs += attr;
545
586
  }
@@ -555,7 +596,7 @@ async function renderNode(rc, node, stripSlotProp) {
555
596
  buf += "<m-group>" + attrModifiers + "</m-group>";
556
597
  }
557
598
  write(buf);
558
- rc.flags.runtime |= F_LAZY;
599
+ rc.flags.runtime |= LAZY;
559
600
  break;
560
601
  }
561
602
  // `<router>` element
@@ -582,7 +623,7 @@ async function renderNode(rc, node, stripSlotProp) {
582
623
  }
583
624
  buf += "</m-router>";
584
625
  write(buf);
585
- rc.flags.runtime |= F_ROUTER;
626
+ rc.flags.runtime |= ROUTER;
586
627
  break;
587
628
  }
588
629
  default: {
@@ -606,14 +647,11 @@ async function renderNode(rc, node, stripSlotProp) {
606
647
  write(addonHtml);
607
648
  }
608
649
  if (signalValue) {
609
- const { scope, key } = signalValue[$signal];
610
- attrModifiers += "<m-signal mode=" + toAttrStringLit("[" + propName + "]") + ' scope="' + scope + '" ';
611
- if (isString(key)) {
612
- attrModifiers += "key=" + toAttrStringLit(key);
613
- } else {
614
- attrModifiers += 'computed="' + rc.mcs.gen(signalValue) + '"';
615
- }
616
- attrModifiers += "></m-signal>";
650
+ const write2 = (chunk) => {
651
+ attrModifiers += chunk;
652
+ };
653
+ renderSignal({ ...rc, write: write2 }, signalValue, [propName]);
654
+ rc.flags.runtime |= RENDER_ATTR;
617
655
  }
618
656
  buffer += attr;
619
657
  }
@@ -642,6 +680,15 @@ async function renderNode(rc, node, stripSlotProp) {
642
680
  break;
643
681
  }
644
682
  }
683
+ async function renderChildren(rc, children, stripSlotProp) {
684
+ if (Array.isArray(children) && !isVNode(children)) {
685
+ for (const child of children) {
686
+ await renderNode(rc, child, stripSlotProp);
687
+ }
688
+ } else {
689
+ await renderNode(rc, children, stripSlotProp);
690
+ }
691
+ }
645
692
  function renderAttr(rc, attrName, attrValue, stripSlotProp) {
646
693
  let attr = "";
647
694
  let addonHtml = "";
@@ -651,13 +698,37 @@ function renderAttr(rc, attrName, attrValue, stripSlotProp) {
651
698
  if (isSignal(attrValue)) {
652
699
  signal = attrValue;
653
700
  } else {
654
- signal = computedProps(rc, attrValue);
701
+ const { fcCtx } = rc;
702
+ if (fcCtx) {
703
+ const deps = /* @__PURE__ */ new Set();
704
+ const patches = [];
705
+ const staticProps = traverseProps(attrValue, (path, value) => {
706
+ const { scope, key } = value[$signal];
707
+ if (isString(key)) {
708
+ patches.push([
709
+ (scope !== fcCtx.scopeId ? "$signals(" + scope + ")" : "this") + "[" + stringify(key) + "]",
710
+ ...path
711
+ ].join(","));
712
+ deps.add(scope + ":" + key);
713
+ } else {
714
+ patches.push(["(" + key.compute.toString() + ")(),", ...path].join(","));
715
+ for (const dep of key.deps) {
716
+ deps.add(dep);
717
+ }
718
+ }
719
+ });
720
+ if (patches.length > 0) {
721
+ const { scopeId } = fcCtx;
722
+ const compute = "()=>$patch(" + stringify(staticProps) + ",[" + patches.join("],[") + "])";
723
+ signal = Signal(scopeId, { compute, deps }, staticProps);
724
+ }
725
+ }
655
726
  }
656
727
  if (signal) {
657
728
  if (attrName === "class") {
658
- rc.flags.runtime |= F_CX;
729
+ rc.flags.runtime |= CX;
659
730
  } else if (attrName === "style") {
660
- rc.flags.runtime |= F_STYLE;
731
+ rc.flags.runtime |= STYLE;
661
732
  }
662
733
  signalValue = signal;
663
734
  attrValue = signal[$signal].value;
@@ -683,16 +754,16 @@ function renderAttr(rc, attrName, attrValue, stripSlotProp) {
683
754
  break;
684
755
  case "props":
685
756
  if (isObject(attrValue) && !Array.isArray(attrValue)) {
686
- attr = ' props="base64,' + btoa(JSON.stringify(attrValue)) + '"';
757
+ attr = ' props="base64,' + btoa(stringify(attrValue)) + '"';
687
758
  }
688
759
  break;
689
760
  case "ref":
690
761
  if (typeof attrValue === "function") {
691
762
  const signals = rc.fcCtx?.signals;
692
763
  if (!signals) {
693
- console.error("Use `ref` outside of a component function");
764
+ console.error("[mono-jsx] Use `ref` outside of a component function");
694
765
  } else {
695
- const refId = rc.flags.refs++;
766
+ const refId = rc.fcCtx.refs++;
696
767
  const effects = signals[Symbol.for("effects")];
697
768
  effects.push("()=>(" + attrValue.toString() + ')(this.refs["' + refId + '"])');
698
769
  attr = " data-ref=" + toAttrStringLit(rc.fcCtx.scopeId + ":" + refId);
@@ -703,7 +774,8 @@ function renderAttr(rc, attrName, attrValue, stripSlotProp) {
703
774
  break;
704
775
  case "action":
705
776
  if (typeof attrValue === "function") {
706
- attr = ' onsubmit="$onsubmit(event,$MF_' + rc.mfs.gen(attrValue) + toStr(rc.fcCtx?.scopeId, (i) => "," + i) + ')"';
777
+ const scopeId = rc.fcCtx?.scopeId;
778
+ attr = ' onsubmit="$onsubmit(event,$MF_' + (scopeId ?? 0) + "_" + rc.mfs.gen(attrValue, scopeId) + toStr(scopeId, (i) => "," + i) + ')"';
707
779
  } else if (isString(attrValue)) {
708
780
  attr = " action=" + toAttrStringLit(attrValue);
709
781
  }
@@ -715,7 +787,8 @@ function renderAttr(rc, attrName, attrValue, stripSlotProp) {
715
787
  break;
716
788
  default:
717
789
  if (attrName.startsWith("on") && typeof attrValue === "function") {
718
- attr = " " + escapeHTML(attrName.toLowerCase()) + '="$emit(event,$MF_' + rc.mfs.gen(attrValue) + toStr(rc.fcCtx?.scopeId, (i) => "," + i) + ')"';
790
+ const scopeId = rc.fcCtx?.scopeId;
791
+ attr = " " + escapeHTML(attrName.toLowerCase()) + '="$emit(event,$MF_' + (scopeId ?? 0) + "_" + rc.mfs.gen(attrValue, scopeId) + toStr(scopeId, (i) => "," + i) + ')"';
719
792
  } else if (attrValue === false || attrValue === null || attrValue === void 0) {
720
793
  } else {
721
794
  attr = " " + escapeHTML(attrName);
@@ -732,7 +805,7 @@ async function renderFC(rc, fc, props) {
732
805
  const scopeId = ++rc.flags.scope;
733
806
  const signals = createSignals(scopeId, rc.signals.app, rc.context, rc.request);
734
807
  const slots = children !== void 0 ? Array.isArray(children) ? isVNode(children) ? [children] : children : [children] : void 0;
735
- const fcCtx = { scopeId, signals, slots };
808
+ const fcCtx = { scopeId, signals, slots, refs: 0 };
736
809
  try {
737
810
  const v = fc.call(signals, props);
738
811
  if (isObject(v) && !isVNode(v)) {
@@ -803,22 +876,34 @@ async function renderFC(rc, fc, props) {
803
876
  } catch (err) {
804
877
  if (err instanceof Error) {
805
878
  if (props.catch) {
806
- await renderNode(rc, props.catch(err));
879
+ await renderNode(rc, props.catch(err)).catch(() => {
880
+ });
807
881
  } else {
808
882
  console.error(err);
809
- write('<pre style="color:red;font-size:1rem"><code>' + escapeHTML(err.message) + "</code></pre>");
883
+ write("<script>console.error(" + stringify(err.stack ?? err.message) + ")<\/script>");
810
884
  }
811
885
  }
812
886
  }
813
887
  }
814
- async function renderChildren(rc, children, stripSlotProp) {
815
- if (Array.isArray(children) && !isVNode(children)) {
816
- for (const child of children) {
817
- await renderNode(rc, child, stripSlotProp);
888
+ function renderSignal(rc, signal, mode, content, html2) {
889
+ const { scope, key } = signal[$signal];
890
+ let buffer = "<m-signal";
891
+ if (mode) {
892
+ buffer += ' mode="';
893
+ if (isString(mode)) {
894
+ buffer += mode;
895
+ } else {
896
+ buffer += "[" + mode[0] + "]";
818
897
  }
898
+ buffer += '"';
899
+ }
900
+ buffer += ' scope="' + scope + '"';
901
+ if (isString(key)) {
902
+ buffer += " key=" + toAttrStringLit(key);
819
903
  } else {
820
- await renderNode(rc, children, stripSlotProp);
904
+ buffer += ' computed="' + rc.mcs.gen(signal, rc.fcCtx?.scopeId) + '"';
821
905
  }
906
+ rc.write(buffer + ">" + (html2 ? content : escapeHTML(String(content ?? ""))) + "</m-signal>");
822
907
  }
823
908
  var collectDeps;
824
909
  function Signal(scope, key, value) {
@@ -849,6 +934,14 @@ function createSignals(scopeId, appSignals, context = new NullProtoObj(), reques
849
934
  const store = new NullProtoObj();
850
935
  const signals = /* @__PURE__ */ new Map();
851
936
  const effects = [];
937
+ const refs = new Proxy(/* @__PURE__ */ Object.create(null), {
938
+ get(_, key) {
939
+ return new Ref(scopeId, key);
940
+ },
941
+ set() {
942
+ throw new Error("[mono-jsx] The `refs` object is read-only at SSR time.");
943
+ }
944
+ });
852
945
  const computed = (compute) => {
853
946
  const deps = /* @__PURE__ */ new Set();
854
947
  collectDeps = (scopeId2, key) => deps.add(scopeId2 + ":" + key);
@@ -856,11 +949,9 @@ function createSignals(scopeId, appSignals, context = new NullProtoObj(), reques
856
949
  collectDeps = void 0;
857
950
  return Signal(scopeId, { compute, deps }, value);
858
951
  };
859
- const refs = new Proxy(new NullProtoObj(), {
860
- get(_, key) {
861
- return new Ref(scopeId, key);
862
- }
863
- });
952
+ const markEffect = (effect) => {
953
+ effects.push(effect.toString());
954
+ };
864
955
  const mark = ({ signals: signals2, write }) => {
865
956
  if (effects.length > 0) {
866
957
  const n = effects.length;
@@ -892,71 +983,55 @@ function createSignals(scopeId, appSignals, context = new NullProtoObj(), reques
892
983
  case "$":
893
984
  return computed;
894
985
  case "effect":
895
- return (effect) => {
896
- effects.push(effect.toString());
897
- };
986
+ return markEffect;
898
987
  case Symbol.for("effects"):
899
988
  return effects;
900
989
  case Symbol.for("mark"):
901
990
  return mark;
902
- default:
903
- if (isString(key)) {
904
- const value = Reflect.get(target, key, receiver);
905
- if (value === void 0 && !Reflect.has(target, key)) {
906
- Reflect.set(target, key, void 0, receiver);
907
- }
908
- if (isSignal(value)) {
909
- return value;
910
- }
911
- if (collectDeps) {
912
- collectDeps(scopeId, key);
913
- return value;
914
- }
915
- let signal = signals.get(key);
916
- if (!signal) {
917
- signal = Signal(scopeId, key, value);
918
- signals.set(key, signal);
919
- }
920
- return signal;
991
+ case "url":
992
+ if (scopeId === 0) {
993
+ collectDeps?.(0, key);
994
+ return request ? request.URL ?? (request.URL = new URL(request.url)) : void 0;
995
+ }
996
+ // fallthrough
997
+ default: {
998
+ const value = Reflect.get(target, key, receiver);
999
+ if (typeof key === "symbol" || isSignal(value)) {
1000
+ return value;
1001
+ }
1002
+ if (value === void 0 && !Reflect.has(target, key)) {
1003
+ Reflect.set(target, key, void 0, receiver);
1004
+ }
1005
+ if (collectDeps) {
1006
+ collectDeps(scopeId, key);
1007
+ return value;
921
1008
  }
1009
+ let signal = signals.get(key);
1010
+ if (!signal) {
1011
+ signal = Signal(scopeId, key, value);
1012
+ signals.set(key, signal);
1013
+ }
1014
+ return signal;
1015
+ }
922
1016
  }
923
1017
  },
924
1018
  set(target, key, value, receiver) {
925
- signals.delete(key);
1019
+ if (isString(key)) {
1020
+ signals.delete(key);
1021
+ }
926
1022
  return Reflect.set(target, key, value, receiver);
927
1023
  }
928
1024
  });
929
1025
  return thisProxy;
930
1026
  }
931
- function computedProps({ fcCtx }, props) {
932
- if (fcCtx) {
933
- const deps = /* @__PURE__ */ new Set();
934
- const patches = [];
935
- const staticProps = traverseProps(props, (path, value) => {
936
- const { scope, key } = value[$signal];
937
- if (isString(key)) {
938
- patches.push([(scope !== fcCtx.scopeId ? "$signals(" + scope + ")" : "this") + "[" + JSON.stringify(key) + "]", ...path].join(","));
939
- deps.add(scope + ":" + key);
940
- } else {
941
- patches.push(["(" + key.compute.toString() + ")(),", ...path].join(","));
942
- for (const dep of key.deps) {
943
- deps.add(dep);
944
- }
945
- }
946
- });
947
- if (patches.length > 0) {
948
- const { scopeId } = fcCtx;
949
- const compute = "()=>$patch(" + JSON.stringify(staticProps) + ",[" + patches.join("],[") + "])";
950
- return Signal(scopeId, { compute, deps }, staticProps);
951
- }
952
- }
953
- return void 0;
1027
+ function markSignals(rc, signals) {
1028
+ signals[Symbol.for("mark")](rc);
954
1029
  }
955
1030
  function traverseProps(obj, callback, path = []) {
956
1031
  const isArray = Array.isArray(obj);
957
1032
  const copy = isArray ? new Array(obj.length) : new NullProtoObj();
958
1033
  for (const [k, value] of Object.entries(obj)) {
959
- const newPath = path.concat(isArray ? k : JSON.stringify(k));
1034
+ const newPath = path.concat(isArray ? k : stringify(k));
960
1035
  const key = isArray ? Number(k) : k;
961
1036
  if (isObject(value)) {
962
1037
  if (isSignal(value)) {
@@ -971,9 +1046,6 @@ function traverseProps(obj, callback, path = []) {
971
1046
  }
972
1047
  return copy;
973
1048
  }
974
- function markSignals(rc, signals) {
975
- signals[Symbol.for("mark")](rc);
976
- }
977
1049
 
978
1050
  // jsx-runtime.ts
979
1051
  var Fragment = $fragment;
@@ -986,7 +1058,7 @@ var jsx = (tag, props = new NullProtoObj(), key) => {
986
1058
  props.key = key;
987
1059
  }
988
1060
  if (tag === "html") {
989
- if (props.request === $peek) {
1061
+ if (props.request === $setup) {
990
1062
  return props;
991
1063
  }
992
1064
  const renderOptions = new NullProtoObj();
@@ -1012,7 +1084,7 @@ var jsxEscape = (value) => {
1012
1084
  };
1013
1085
  var html = (template, ...values) => [
1014
1086
  $html,
1015
- { innerHTML: isString(template) ? template : String.raw(template, ...values.map(jsxEscape)) },
1087
+ { innerHTML: isString(template) || isSignal(template) ? template : String.raw(template, ...values.map(jsxEscape)) },
1016
1088
  $vnode
1017
1089
  ];
1018
1090
  Object.assign(globalThis, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
package/setup.mjs CHANGED
@@ -34,6 +34,8 @@ async function setup() {
34
34
  compilerOptions.lib ??= ["dom", "es2022"];
35
35
  compilerOptions.module ??= "es2022";
36
36
  compilerOptions.moduleResolution ??= "bundler";
37
+ compilerOptions.allowImportingTsExtensions ??= true;
38
+ compilerOptions.noEmit ??= true;
37
39
  }
38
40
  compilerOptions.jsx = "react-jsx";
39
41
  compilerOptions.jsxImportSource = "mono-jsx";
package/types/html.d.ts CHANGED
@@ -44,11 +44,13 @@ export namespace HTML {
44
44
 
45
45
  /** Global HTML attributes from https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes */
46
46
  interface GlobalAttributes<T extends EventTarget> extends EventAttributes<T>, Aria.Attributes, Mono.BaseAttributes, JSX.HtmlTag {
47
+ // @mono-jsx
48
+ ref?: HTMLElement | ((el: T) => unknown);
47
49
  /** Defines a unique identifier (ID) which must be unique in the whole document. Its purpose is to identify the element when linking (using a fragment identifier), scripting, or styling (with CSS). */
48
50
  id?: string;
49
- /** A space-separated list of the classes of the element. Classes allow CSS and JavaScript to select and access specific elements via the [class selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) or functions like the method [`Document.getElementsByClassName()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName). */
51
+ /** Contains a space-separated list of the classes of the element. Classes are used by CSS and JavaScript to select and access specific elements. */
50
52
  class?: HTMLClass | HTMLClass[];
51
- /** Contains [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) styling declarations to be applied to the element. Note that it is recommended for styles to be defined in a separate file or files. This attribute and the [`<style>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style) element have mainly the purpose of allowing for quick styling, for example for testing purposes. */
53
+ /** Contains [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) styling declarations to be applied to the element. */
52
54
  style?: string | Mono.CSSProperties;
53
55
  /** An enumerated attribute indicating that the element is not yet, or is no longer, relevant. For example, it can be used to hide elements of the page that can't be used until the login process has been completed. The browser won't render such elements. This attribute must not be used to hide content that could legitimately be shown. */
54
56
  hidden?: boolean | "hidden" | "until-found";
@@ -828,9 +830,6 @@ export namespace HTML {
828
830
  }
829
831
 
830
832
  interface EventAttributes<T extends EventTarget> {
831
- // @mono-jsx
832
- ref?: ((el: T) => unknown) | HTMLElement | null;
833
-
834
833
  // Input Events
835
834
  onBeforeInput?: EventHandler<Event, T>;
836
835
  onInput?: EventHandler<Event, T>;
package/types/mono.d.ts CHANGED
@@ -104,42 +104,62 @@ export interface Elements {
104
104
  switch: BaseAttributes & {
105
105
  value?: string | number | boolean | null;
106
106
  };
107
- /**
108
- * The `for` element is a built-in element for list rendering with signals.
109
- */
110
- for: BaseAttributes & {
111
- items?: unknown[];
112
- };
113
107
  /**
114
108
  * The `component` element is a built-in element that is used to load components lazily,
115
109
  * which can improve performance by reducing the initial load time of the application.
116
110
  */
117
111
  component: BaseAttributes & AsyncComponentAttributes & {
118
- name: string;
112
+ name?: string;
119
113
  props?: Record<string, unknown>;
114
+ ref?: ComponentElement | ((el: ComponentElement) => void);
120
115
  };
121
116
  /**
122
117
  * The `router` element is a built-in element that implements client-side routing.
123
118
  */
124
- router: BaseAttributes & AsyncComponentAttributes & {};
119
+ router: BaseAttributes & AsyncComponentAttributes & {
120
+ /**
121
+ * The `base` attribute is used to set the base URL for the router.
122
+ */
123
+ base?: string;
124
+ ref?: RouterElement | ((el: RouterElement) => void);
125
+ };
125
126
  }
126
127
 
128
+ export type WithParams<T> = T & { params?: Record<string, string> };
129
+
127
130
  declare global {
128
131
  /**
129
- * The `html` function is used to create XSS-unsafe HTML elements.
132
+ * The `html` function is used to create XSS-unsafed HTML content.
130
133
  */
131
134
  var html: JSX.Raw;
135
+ /**
136
+ * The `css` function is an alias to `html`.
137
+ */
132
138
  var css: JSX.Raw;
139
+ /**
140
+ * The `js` function is an alias to `html`.
141
+ */
133
142
  var js: JSX.Raw;
134
-
135
143
  /**
136
- * mono-jsx `this` object that is bound to the function component.
144
+ * The `FC` type defines Signals/Context/Refs API.
137
145
  */
138
- type FC<Signals = {}, AppSignals = {}, Context = {}> = {
146
+ type FC<Signals = {}, AppSignals = {}, Context = {}, Refs = {}, AppRefs = {}> = {
139
147
  /**
140
148
  * The global signals shared across the application.
141
149
  */
142
- readonly app: AppSignals;
150
+ readonly app: {
151
+ /**
152
+ * The `app.refs` object is used to store variables in the application scope.
153
+ * It is similar to `refs`, but it is shared across all components in the application.
154
+ *
155
+ * **⚠ This is a client-side only API.**
156
+ */
157
+ readonly refs: AppRefs;
158
+ /**
159
+ * The `app.url` object contains the current URL information.
160
+ */
161
+ readonly url: WithParams<URL>;
162
+ } & Omit<AppSignals, "refs" | "url">;
143
163
  /**
144
164
  * The rendering context.
145
165
  *
@@ -151,23 +171,47 @@ declare global {
151
171
  *
152
172
  * **⚠ This is a server-side only API.**
153
173
  */
154
- readonly request: Request & { params?: Record<string, string> };
174
+ readonly request: WithParams<Request & { URL: URL }>;
155
175
  /**
156
- * The `refs` object is used to store references to DOM elements.
176
+ * The `refs` object is used to store variables in clide side.
177
+ *
178
+ * **⚠ This is a client-side only API.**
157
179
  */
158
- readonly refs: Record<string, HTMLElement | null>;
180
+ readonly refs: Refs;
159
181
  /**
160
182
  * The `computed` method is used to create a computed signal.
161
183
  */
162
184
  readonly computed: <T = unknown>(fn: () => T) => T;
163
185
  /**
164
- * `this.$(fn)` is just a shortcut for `this.computed(fn)`.
186
+ * `this.$(fn)` is a shortcut for `this.computed(fn)`.
165
187
  */
166
- readonly $: <T = unknown>(fn: () => T) => T;
188
+ readonly $: FC["computed"];
167
189
  /**
168
190
  * The `effect` method is used to create a side effect.
169
191
  * **The effect function is only called on client side.**
170
192
  */
171
193
  readonly effect: (fn: () => void | (() => void)) => void;
172
- } & Omit<Signals, "app" | "context" | "request" | "refs" | "computed" | "$" | "effect">;
194
+ } & Omit<Signals, "app" | "context" | "request" | "refs" | "forIndex" | "forItem" | "computed" | "$" | "effect">;
195
+ /**
196
+ * The `Refs` defines the `refs` types.
197
+ */
198
+ type Refs<T, R = {}, RR = {}> = T extends FC<infer S, infer A, infer C> ? FC<S, A, C, R, RR> : never;
199
+ /**
200
+ * The `Context` defines the `context` types.
201
+ */
202
+ type Context<T, C = {}> = T extends FC<infer S, infer A, infer _, infer R, infer RR> ? FC<S, A, C, R, RR> : never;
203
+ /**
204
+ * The `ComponentElement` type defines the component element.
205
+ */
206
+ type ComponentElement = {
207
+ name: string;
208
+ props: Record<string, unknown> | undefined;
209
+ refresh: () => Promise<void>;
210
+ };
211
+ /**
212
+ * The `RouterElement` type defines the router element.
213
+ */
214
+ type RouterElement = {
215
+ navigate: (url: string | URL, options?: { replace?: boolean }) => Promise<void>;
216
+ };
173
217
  }