mono-jsx 0.10.0-beta.4 → 0.10.0-beta.6

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
@@ -964,8 +964,6 @@ The `<component>` element also supports the `ref` prop, which allows you to cont
964
964
  - `refresh`: A method to re-render the component with the current name and props.
965
965
 
966
966
  ```tsx
967
- import type { ComponentElement } from "mono-jsx";
968
-
969
967
  function App(this: WithRefs<FC, { component: ComponentElement }>) {
970
968
  this.effect(() => {
971
969
  // updating the component name and props will trigger a re-render of the component
@@ -1110,8 +1108,21 @@ You can access the `params` object in your route components to get the values of
1110
1108
  ```tsx
1111
1109
  // router pattern: "/post/:id"
1112
1110
  function Post(this: FC) {
1113
- this.request.url // "http://localhost:3000/post/123"
1114
- this.request.params?.id // "123"
1111
+ console.log(this.request.url) // "http://localhost:3000/post/123"
1112
+ console.log(this.request.params?.id) // "123"
1113
+ }
1114
+ ```
1115
+
1116
+ You can use `this.app.url` signal to get route URL and parameters:
1117
+
1118
+ ```tsx
1119
+ function Post(this: FC) {
1120
+ return (
1121
+ <div>
1122
+ <p>Current URL: {this.$(() => this.app.url.href)}</p>
1123
+ <p>Post id: {this.$(() => this.app.url.params?.id)}</p>
1124
+ </div>
1125
+ )
1115
1126
  }
1116
1127
  ```
1117
1128
 
@@ -1227,7 +1238,7 @@ The `hidden` prop can be used to hide the formslot payload from the form handler
1227
1238
 
1228
1239
  ### Using `this.app.url` Signal
1229
1240
 
1230
- `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 the view with `<show>`, `<hidden>` or `<switch>` elements:
1241
+ The `this.app.url` in a component is an app-level signal that contains the current route URL and parameters. It is automatically updated when the route changes, so you can use it to display the current URL in your components or control the view with `<show>`, `<hidden>` or `<switch>` elements:
1231
1242
 
1232
1243
  ```tsx
1233
1244
  function App(this: FC) {
@@ -1258,6 +1269,23 @@ export default {
1258
1269
  }
1259
1270
  ```
1260
1271
 
1272
+ You can also use the `navigate` method of the `<router>` element to navigate to a new route programmatically.
1273
+
1274
+ ```tsx
1275
+ function App(this: FC<{}, { router: RouterElement }>) {
1276
+ return (
1277
+ <>
1278
+ <header>
1279
+ <button
1280
+ onClick={() => this.refs.router.navigate("/about", { replace: false, refresh: false })}
1281
+ >About</button>
1282
+ </header>
1283
+ <router ref={this.refs.router} />
1284
+ </>
1285
+ )
1286
+ }
1287
+ ```
1288
+
1261
1289
  ### Nav Links
1262
1290
 
1263
1291
  Links under the `<nav>` element will be treated as navigation links by the router. When the `href` of a nav link matches a route, an 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` prop on the `<nav>` element. You can add styles for the active link using nested CSS selectors in the `style` prop of the `<nav>` element.
@@ -1277,6 +1305,63 @@ export default {
1277
1305
  }
1278
1306
  ```
1279
1307
 
1308
+ ## Adding Page Metadata
1309
+
1310
+ You can add metadata to the route component by setting the `metadata` property on the route component.
1311
+
1312
+ ```tsx
1313
+ function Home(this: FC) {
1314
+ return <p>Home</p>
1315
+ }
1316
+ Home.metadata = {
1317
+ title: "Home",
1318
+ description: "Home page",
1319
+ }
1320
+
1321
+ const routes = {
1322
+ "/": Home,
1323
+ }
1324
+ ```
1325
+
1326
+ Or use `getMetadata` property on the route component to dynamically generate the metadata.
1327
+
1328
+ ```tsx
1329
+ async function Post(this: FC) {
1330
+ const post = await getPost(this.request.params.slug)
1331
+ return <div>
1332
+ <h1>{post.title}</h1>
1333
+ <h2>{post.description}</h2>
1334
+ <div>{post.content}</div>
1335
+ </div>
1336
+ }
1337
+
1338
+ Post.getMetadata = async function(this: FC) {
1339
+ const post = await getPost(this.request.params.slug)
1340
+ return {
1341
+ title: post.title,
1342
+ description: post.description,
1343
+ }
1344
+ }
1345
+
1346
+ const routes = {
1347
+ "/post/:slug": Post,
1348
+ }
1349
+ ```
1350
+
1351
+ You can also add metadata to the root `<html>` element by setting the `metadata` prop on the root `<html>` element. This will be added to all the pages in your app.
1352
+
1353
+ ```tsx
1354
+ export default {
1355
+ fetch: (req) => (
1356
+ <html request={req} routes={routes} metadata={{ title: "My App" }}>
1357
+ <head>
1358
+ <metadata />
1359
+ </head>
1360
+ </html>
1361
+ )
1362
+ }
1363
+ ```
1364
+
1280
1365
  ### Fallback (404)
1281
1366
 
1282
1367
  You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
@@ -1294,6 +1379,30 @@ export default {
1294
1379
  }
1295
1380
  ```
1296
1381
 
1382
+ ### Route Caching
1383
+
1384
+ By default, the router client caches the html content from the server. To disable the caching, you can add the `dynamic` option to the route component.
1385
+
1386
+ ```tsx
1387
+ // Home is a static route that can be cached on the client side
1388
+ function Home(this: FC) {
1389
+ return <p>Home</p>
1390
+ }
1391
+
1392
+ // Dash is a dynamic route that will not be cached on the client side
1393
+ // it will be fetched from the server on every navigation
1394
+ function Dash(this: FC) {
1395
+ const user = this.session.get<{ name: string }>("user")
1396
+ return <p>Welcome back, {user?.name}!</p>
1397
+ }
1398
+ Dash.dynamic = true;
1399
+
1400
+ const routes = {
1401
+ "/": Home,
1402
+ "/dash": Dash,
1403
+ }
1404
+ ```
1405
+
1297
1406
  ## Using Session
1298
1407
 
1299
1408
  mono-jsx provides a built-in session storage that allows you to manage sessions. To use session storage, you need to set the `session` prop on the root `<html>` element with the `cookie.secret` option.
package/jsx-runtime.mjs CHANGED
@@ -8,6 +8,16 @@ var JSX = {
8
8
  }
9
9
  };
10
10
 
11
+ // symbols.ts
12
+ var $fragment = /* @__PURE__ */ Symbol.for("jsx.fragment");
13
+ var $html = /* @__PURE__ */ Symbol.for("jsx.html");
14
+ var $vnode = /* @__PURE__ */ Symbol.for("jsx.vnode");
15
+ var $setup = /* @__PURE__ */ Symbol.for("mono.setup");
16
+ var $rpc = /* @__PURE__ */ Symbol.for("mono.rpc");
17
+
18
+ // version.ts
19
+ var VERSION = "0.10.0-beta.6";
20
+
11
21
  // runtime/index.ts
12
22
  var EVENT = 1;
13
23
  var CX = 2;
@@ -19,19 +29,21 @@ var SIGNALS = 64;
19
29
  var SUSPENSE = 128;
20
30
  var COMPONENT = 256;
21
31
  var ROUTER = 512;
22
- var FORM = 1024;
23
- var RPC = 2048;
32
+ var REDIRECT = 1024;
33
+ var FORM = 2048;
34
+ var RPC = 4096;
24
35
  var EVENT_JS = `{var w=window,m=new Map;w.$F=m.set.bind(m);w.$fmap=m;w.$emit=(e,i,s)=>m.get(i).call(w.$signals?.(s)??e.target,e.type==="mount"?e.target:e);w.$onsubmit=(e,i,s)=>{e.preventDefault();m.get(i).call(w.$signals?.(s)??e.target,new FormData(e.target),e)};}`;
25
36
  var CX_JS = `{var n=t=>typeof t=="string"?t:typeof t=="object"&&t!==null?(Array.isArray(t)?t.map(n).filter(Boolean):Object.entries(t).filter(([,e])=>!!e).map(([e])=>e)).join(" "):"";window.$cx=n;}`;
26
- var STYLE_JS = `{var f=/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;var p=new Set;var g=t=>typeof t=="object"&&t!==null,u=t=>g(t)&&(t.constructor===Object||t.constructor===void 0),d=t=>t.replace(/[a-z][A-Z]/g,n=>n.charAt(0)+"-"+n.charAt(1).toLowerCase());var b=t=>{let n=0;for(let e=0;e<t.length;e++)n=(n<<5)-n+t.charCodeAt(e)|0;return n>>>0};var h=(t,n)=>{let{inline:e,css:o}=y(n);if(o){let r="data-css-",s=b((e??"")+o.join("")),i=r+s.toString(36),l="["+i+"]";p.has(s)||(p.add(s),document.head.appendChild(document.createElement("style")).textContent=(e?l+"{"+e+"}":"")+o.map(a=>a===null?l:a).join("")),t.getAttributeNames().forEach(a=>a.startsWith(r)&&t.removeAttribute(a)),t.setAttribute(i,"")}else e&&t.setAttribute("style",e)},y=t=>{let n,e=[],o=new x;for(let[r,s]of Object.entries(t))switch(r.charCodeAt(0)){case 58:u(s)&&e.push(r.startsWith("::view-")?"":null,r+c(s));break;case 64:u(s)&&(r.startsWith("@keyframes ")?e.push(r+"{"+Object.entries(s).map(([i,l])=>u(l)?i+c(l):"").join("")+"}"):r.startsWith("@view-")?e.push(r+c(s)):e.push(r+"{",null,c(s)+"}"));break;case 38:u(s)&&e.push(null,r.slice(1)+c(s));break;default:n??={},n[r]=s}return n&&(o.inline=c(n).slice(1,-1)),e.length>0&&(o.css=e),o},c=t=>{let n="";for(let[e,o]of Object.entries(t)){let r=typeof o;if(r==="string"||r==="number"){let s=d(e),i=r==="number"?f.test(e)?""+o:o+"px":""+o;n+=(n?";":"")+s+":"+(s==="content"?JSON.stringify(i):i)}}return"{"+n+"}"},x=(()=>{function t(){}return t.prototype=Object.create(null),t})();window.$applyStyle=h;}`;
37
+ var STYLE_JS = `{var f=/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;var p=new Set;var d=t=>typeof t=="object"&&t!==null,u=t=>d(t)&&(t.constructor===Object||t.constructor===void 0),g=t=>t.replace(/[a-z][A-Z]/g,n=>n.charAt(0)+"-"+n.charAt(1).toLowerCase()),y=t=>{let n=0;for(let e=0;e<t.length;e++)n=(n<<5)-n+t.charCodeAt(e)|0;return n>>>0};var b=(t,n)=>{let{inline:e,css:s}=h(n);if(s){let r="data-css-",o=y((e??"")+s.join("")),i=r+o.toString(36),l="["+i+"]";p.has(o)||(p.add(o),document.head.appendChild(document.createElement("style")).textContent=(e?l+"{"+e+"}":"")+s.map(a=>a===null?l:a).join("")),t.getAttributeNames().forEach(a=>a.startsWith(r)&&t.removeAttribute(a)),t.setAttribute(i,"")}else e&&t.setAttribute("style",e)},h=t=>{let n,e=[],s=new x;for(let[r,o]of Object.entries(t))switch(r.charCodeAt(0)){case 58:u(o)&&e.push(r.startsWith("::view-")?"":null,r+c(o));break;case 64:u(o)&&(r.startsWith("@keyframes ")?e.push(r+"{"+Object.entries(o).map(([i,l])=>u(l)?i+c(l):"").join("")+"}"):r.startsWith("@view-")?e.push(r+c(o)):e.push(r+"{",null,c(o)+"}"));break;case 38:u(o)&&e.push(null,r.slice(1)+c(o));break;default:n??={},n[r]=o}return n&&(s.inline=c(n).slice(1,-1)),e.length>0&&(s.css=e),s},c=t=>{let n="";for(let[e,s]of Object.entries(t)){let r=typeof s;if(r==="string"||r==="number"){let o=g(e),i=r==="number"?f.test(e)?""+s:s+"px":""+s;n+=(n?";":"")+o+":"+(o==="content"?JSON.stringify(i):i)}}return"{"+n+"}"},x=(()=>{function t(){}return t.prototype=Object.create(null),t})();window.$applyStyle=b;}`;
27
38
  var RENDER_ATTR_JS = `{var s=(l,n,r)=>{let e=l.parentElement;return e.tagName==="M-GROUP"&&(e=e.previousElementSibling),()=>{let t=r();n==="value"?e.value=String(t):n==="checked"?e.checked=!!t:typeof t=="boolean"?e.toggleAttribute(n,t):t==null?e.removeAttribute(n):typeof t=="object"?n==="class"?e.setAttribute(n,$cx(t)):n==="style"?$applyStyle(e,t):e.setAttribute(n,JSON.stringify(t)):e.setAttribute(n,String(t))}};window.$renderAttr=s;}`;
28
39
  var RENDER_TOGGLE_JS = `{var i=(e,s)=>{let t,l=()=>e.replaceChildren(...s()?t:[]);return()=>{if(!t){let n=e.firstElementChild;n&&n.tagName==="TEMPLATE"&&n.hasAttribute("m-slot")?t=[...n.content.childNodes]:t=[...e.childNodes]}e.hasAttribute("vt")&&document.startViewTransition?document.startViewTransition(l):l()}};window.$renderToggle=i;}`;
29
40
  var RENDER_SWITCH_JS = `{var a=(l,u)=>{let s,r=l.getAttribute("value"),t,i,o=e=>t.get(e)??t.set(e,[]).get(e),d=()=>l.replaceChildren(...t.has(s)?t.get(s):i);return()=>{if(!t){t=new Map,i=[];for(let e of l.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):i.push(n);e.remove()}else r?o(r).push(e):i.push(e)}s=""+u(),l.hasAttribute("vt")&&document.startViewTransition?document.startViewTransition(d):d()}};window.$renderSwitch=a;}`;
30
41
  var SIGNALS_JS = `{let h;const a=window,b=document,y=new Map,E=new Map,k=new Map,v=new Map,$=new Map,m=new Map,l=n=>y.get(n)??y.set(n,V(n)).get(n),F=()=>Object.create(null),p=(n,e)=>n.getAttribute(e),V=n=>{const e=F(),t=(o,c)=>{e[o]=c},s=new Map,r=(o,c)=>{let u=s.get(o);return u||(u=new Set,s.set(o,u)),u.add(c),()=>{u.delete(c),u.size===0&&s.delete(o)}},i=new Proxy(F(),{get:(o,c)=>b.querySelector("[data-ref='"+n+":"+c+"']")});return new Proxy(e,{get:(o,c,u)=>{switch(c){case"$init":return t;case"$watch":return r;case"app":return l(0);case"refs":return i;default:return h?.(n,c),Reflect.get(o,c,u)}},set:(o,c,u,g)=>{if(u!==Reflect.get(o,c,g)){const f=s.get(c);return f&&queueMicrotask(()=>f.forEach(d=>d())),Reflect.set(o,c,u,g)}return!1}})},M=(n,e,t)=>{switch(e){case"toggle":return $renderToggle(n,t);case"switch":return $renderSwitch(n,t);case"html":return()=>n.innerHTML=""+t()}if(e&&e.length>2&&e.startsWith("[")&&e.endsWith("]"))return $renderAttr(n,e.slice(1,-1),t);const s=n.parentElement,r=()=>n.textContent=""+t();if(b.startViewTransition&&s.hasAttribute("data-vt")){const i=s.getAttribute("data-vt");return i&&(s.style.viewTransitionName=i),()=>b.startViewTransition(r)}return r},S=n=>{const e=n.indexOf(":");return e>0?[Number(n.slice(0,e)),n.slice(e+1)]:null},q=(n,e,t,s)=>{const r=n.get(t);if(r!==void 0){s(r);return}const i=e.get(t);i?i.push(s):e.set(t,[s])},N=(n,e)=>{const t=a.$fmap?.get(n);if(t){e(t);return}const s=m.get(n);s?s.push(e):m.set(n,[e])};if(typeof a.$F=="function"){const n=a.$F;a.$F=(e,t)=>{n(e,t);const s=m.get(e);s&&(m.delete(e),s.forEach(r=>r(t)))}}const T=n=>typeof n=="function"&&n(),A=(n,e)=>customElements.define(n,class extends HTMLElement{disposes=[];connectedCallback(){e(this)}disconnectedCallback(){this.disposes.forEach(t=>t()),this.disposes.length=0}});A("m-signal",n=>{const e=Number(p(n,"scope")),t=l(e),s=p(n,"key");if(s)n.disposes.push(t.$watch(s,M(n,p(n,"mode"),()=>t[s])));else{const r=Number(p(n,"computed")),i=e+":"+r;q(E,v,i,o=>{N(r,c=>{const u=M(n,p(n,"mode"),c.bind(t));o.forEach(g=>{const[f,d]=S(g);n.disposes.push(l(f).$watch(d,u))})})})}}),A("m-effect",n=>{const{disposes:e}=n,t=Number(p(n,"scope"));q(k,$,t,s=>{const r=s.length,i=new Array(r);e.push(()=>{i.forEach(T),i.length=0});for(let o=0;o<r;o++)N(s[o],c=>{const u=[],g=l(t),f=()=>{T(i[o]),i[o]=c.call(g)};h=(d,w)=>u.push([d,w]),f(),h=void 0;for(const[d,w]of u)e.push(l(d).$watch(w,f))})})}),a.$S=(n,e)=>{const[t,s]=S(n);l(t).$init(s,e)},a.$C=(n,e,t)=>{const s=n+":"+e;E.set(s,t);const r=v.get(s);r&&(v.delete(s),r.forEach(i=>i(t)))},a.$E=(n,...e)=>{k.set(n,e);const t=$.get(n);t&&($.delete(n),t.forEach(s=>s(e)))},a.$patch=(n,...e)=>{for(const[t,...s]of e){let r=s.pop(),i=n;for(const o of s)i=i[o];i[r]=t}return n},a.$signals=n=>n!==void 0?l(n):void 0;}`;
31
42
  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())})});}`;
32
43
  var COMPONENT_JS = `{const e=document,a=(t,s)=>t.getAttribute(s);customElements.define("m-component",class extends HTMLElement{static observedAttributes=["name","props"];#t;#s;#r;#h;#i;#e=new Map;#a=!0;async#l(){if(!this.#t){this.#n("");return}const t=this.#s||"{}",s=this.#t+t,i={"x-component":this.#t,"x-props":t,"x-flags":$FLAGS},n=new AbortController;if(this.#h?.abort(),this.#h=n,this.#e.has(s)){this.#n(this.#e.get(s));return}this.#r?.length&&this.#n(this.#r);const r=await fetch(location.href,{headers:i,signal:n.signal});if(!r.ok)throw this.#n(""),new Error("Failed to fetch component '"+this.#t+"'");const[h,o]=await r.json();this.#e.set(s,h),this.#n(h),o&&(e.body.appendChild(e.createElement("script")).textContent=o)}#n(t){const s=()=>typeof t=="string"?this.innerHTML=t:this.replaceChildren(...t);this.hasAttribute("vt")&&e.startViewTransition&&!this.#a?e.startViewTransition(s):s(),this.#a=!1}get name(){return this.#t??null}set name(t){t&&t!==this.#t&&(this.#t=t,this.#o())}get props(){return this.#s?JSON.parse(this.#s):void 0}set props(t){const s=typeof t=="string"?t:JSON.stringify(t);s&&s!==this.#s&&(this.#s=s,this.#o())}attributeChangedCallback(t,s,i){this.#t&&i&&(t==="name"?this.name=i:t==="props"&&(this.props=i))}connectedCallback(){setTimeout(()=>{if(!this.#r){const t=a(this,"props");this.#t=a(this,"name"),this.#s=t?.startsWith("base64,")?atob(t.slice(7)):void 0,this.#r=[...this.childNodes]}this.#l()})}disconnectedCallback(){this.#e.clear(),this.#h?.abort(),this.#h=void 0,this.#i&&clearTimeout(this.#i),this.#i=void 0}#o(){this.#i&&clearTimeout(this.#i),this.#i=setTimeout(()=>{this.#i=void 0,this.#l()},50)}refresh(){this.#t&&this.#e.delete(this.#t+(this.#s||"{}")),this.#o()}});}`;
33
- var ROUTER_JS = `{const l=window,a=document,n=location,h=t=>t.origin===n.origin&&r(t)===r(n),r=({pathname:t,search:s})=>t+s;customElements.define("m-router",class extends HTMLElement{#s;#t=new Map;#r=r(n);#e;#o=!0;#i;#n;async#h(t){const s=new AbortController,i={"x-route":"true","x-flags":$FLAGS};this.#s?.abort(),this.#s=s;const e=await fetch(t,{headers:i,signal:s.signal});if(e.status===404)return null;if(!e.ok)throw this.replaceChildren(),new Error("Failed to fetch route: "+e.status+" "+e.statusText);return e.json()}#a(t){const s=()=>typeof t=="string"?this.innerHTML=t:this.replaceChildren(...t);this.hasAttribute("vt")&&a.startViewTransition&&!this.#o?a.startViewTransition(s):s(),this.#o=!1}#l(){a.querySelectorAll("nav a").forEach(t=>{const{href:s,classList:i}=t,e=t.closest("nav")?.getAttribute("data-active-class")??"active";h(new URL(s))?i.add(e):i.remove(e)})}async#c(t,s){this.#r=r(new URL(t,n.href));const i=this.#h(t).then(e=>{if(e){const[o,c]=e;return this.#t.set(t,o),this.#a(o),c}else this.#t.delete(t),this.#a(this.#e??[]),typeof $signals<"u"&&($signals(0).url=new URL(t))});this.#t.has(t)?this.#a(this.#t.get(t)):await i,history[s?.replace?"replaceState":"pushState"]({},"",t),this.#l(),window.scrollTo(0,0),i.then(e=>{e&&(a.body.appendChild(a.createElement("script")).textContent=e)})}navigate(t,s){const i=new URL(t,n.href);if(i.origin!==n.origin||t.startsWith("#")){n.href=t;return}h(i)||this.#c(t,s)}connectedCallback(){setTimeout(()=>{if(!this.#e)if(this.hasAttribute("fallback"))this.removeAttribute("fallback"),this.#e=[...this.childNodes];else{this.#e=[];for(const t of this.childNodes)if(t.nodeType===1&&t.tagName==="TEMPLATE"&&t.hasAttribute("m-fallback")){this.#e.push(...t.content.childNodes),t.remove();break}}}),this.#i=t=>{if(t.defaultPrevented||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey||!(t.target instanceof HTMLAnchorElement))return;const s=t.target.getAttribute("href");if(!s||s.startsWith("#"))return;const{download:i,href:e,rel:o,target:c}=t.target;i||o==="external"||c==="_blank"||!e.startsWith(n.origin)||(t.preventDefault(),this.navigate(e))},this.#n=()=>{r(n)!==this.#r&&this.#c(n.href)},l.addEventListener("popstate",this.#n),a.addEventListener("click",this.#i),setTimeout(()=>this.#l()),l.$router=this}disconnectedCallback(){l.removeEventListener("popstate",this.#n),a.removeEventListener("click",this.#i),delete l.$router,this.#s?.abort(),this.#s=void 0,this.#t.clear(),this.#i=void 0,this.#n=void 0}});}`;
34
- var FORM_JS = `{const d=window.document,c=(n,e)=>n.getAttribute(e),u=(n,e)=>n.appendChild(e);customElements.define("m-invalid",class extends HTMLElement{connectedCallback(){const n=c(this,"for"),e=this.closest("form"),m=this.textContent;if(n&&e&&m)for(const i of n.split(",")){const s=e.elements.namedItem(i.trim());if(s){const l=()=>{s.removeEventListener("input",l),s.setCustomValidity("")};s.addEventListener("input",l),s.setCustomValidity(m),s.focus()}}this.remove()}}),window.$onrfs=async n=>{n.preventDefault();const e=n.target;if(!e.checkValidity())return;const m=new FormData(e),i=[...e.elements];for(const l of i)l._disabled=l.disabled,l.disabled=!0;const s=await fetch(location.href,{method:"POST",headers:{"x-route-form":"true","x-flags":$FLAGS},body:m});if(s.ok){const[l,p]=await s.json(),E=d.createElement("template"),b=new Map;e.querySelectorAll("m-formslot").forEach(t=>{t.innerHTML=""}),E.innerHTML=l;for(const t of i)t.disabled=t._disabled,delete t._disabled;for(const t of E.content.childNodes){if(t.nodeType===1){const o=t,r=c(o,"formslot"),a=r?'m-formslot[name="'+r+'"]':"m-formslot",f=r?e.querySelector(a)??d.querySelector(a):e.querySelector(a);if(f){f.innerHTML="",b.set(o,f);continue}}u(e,t)}for(const[t,o]of b){const r=c(o,"onupdate"),a=c(o,"scope");switch(c(o,"mode")){case"insertbefore":o.before(t);break;case"insertafter":o.after(t);break;default:u(o,t)}r&&$fmap.get(Number(r))?.call($signals?.(Number(a))??o,{type:"update",target:o})}setTimeout(()=>{i.some(t=>!t.validity.valid)||e.reset()},0),p&&(u(d.body,d.createElement("script")).textContent=p+";document.currentScript.remove();")}};}`;
44
+ var ROUTER_JS = `{const o=window,r=document,s=location,d=e=>e.origin===s.origin&&l(e)===l(s),l=({pathname:e,search:t})=>e+t;customElements.define("m-router",class extends HTMLElement{#e;#i=new Map;#s=l(s);#t;#a=!0;#n;#r;async#c(e){this.#e?.abort(),this.#e=new AbortController;const t=await fetch(e,{headers:{"x-route":"true","x-flags":$FLAGS},signal:this.#e.signal});if(t.status===404)return null;if(!t.ok)throw this.replaceChildren(),new Error("Failed to fetch route: "+t.status+" "+t.statusText);const i=await t.json();if(!Array.isArray(i))throw new Error(i?.error?i.error:"Invalid response from server");return i}#o(e){const t=()=>typeof e=="string"?this.innerHTML=e:this.replaceChildren(...e);this.hasAttribute("vt")&&r.startViewTransition&&!this.#a?r.startViewTransition(t):t(),this.#a=!1}#l(){r.querySelectorAll("nav a").forEach(e=>{const{href:t,classList:i}=e,n=e.closest("nav")?.getAttribute("data-active-class")??"active";d(new URL(t))?i.add(n):i.remove(n)})}async#h(e,t){this.#s=l(e);let i,n=this.#i.get(this.#s);if(n!==void 0&&!t?.refresh&&!this.hasAttribute("no-cache"))this.#o(n);else{const h=await this.#c(e);typeof $signals<"u"&&($signals(0).url=e);let a,c;h?[a,i,c]=h:a=this.#t??[],c||this.#i.set(this.#s,a),this.#o(a)}history[t?.replace?"replaceState":"pushState"]({},"",e),this.#l(),window.scrollTo(0,0),i&&(r.body.appendChild(r.createElement("script")).textContent=i)}navigate(e,t){const i=new URL(e,s.href);if(i.origin!==s.origin||e.startsWith("#")){s.href=e;return}d(i)||this.#h(i,t)}connectedCallback(){if(o.$router)throw new Error("Only one <m-router> element is allowed on the page");o.$router=this,setTimeout(()=>{if(!this.#t)if(this.hasAttribute("fallback"))this.removeAttribute("fallback"),this.#t=[...this.childNodes];else{this.#t=[];for(const e of this.childNodes)if(e.nodeType===1&&e.tagName==="TEMPLATE"&&e.hasAttribute("m-fallback")){this.#t.push(...e.content.childNodes),e.remove();break}}this.#i.set(s.href,[...this.childNodes])}),this.#n=e=>{if(e.defaultPrevented||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey||!(e.target instanceof HTMLAnchorElement))return;const t=e.target.getAttribute("href");if(!t||t.startsWith("#"))return;const{download:i,href:n,rel:h,target:a}=e.target;i||h==="external"||a==="_blank"||!n.startsWith(s.origin)||(e.preventDefault(),this.navigate(n))},this.#r=()=>{l(s)!==this.#s&&this.#h(new URL(s.href))},o.addEventListener("popstate",this.#r),r.addEventListener("click",this.#n),setTimeout(()=>this.#l())}disconnectedCallback(){o.removeEventListener("popstate",this.#r),r.removeEventListener("click",this.#n),delete o.$router,this.#e?.abort(),this.#e=void 0,this.#i.clear(),this.#n=void 0,this.#r=void 0}});}`;
45
+ var REDIRECT_JS = `{customElements.define("m-redirect",class extends HTMLElement{connectedCallback(){const e=this.getAttribute("to"),t=this.hasAttribute("replace");e&&($router?$router.navigate(e,{replace:t}):location.href=e)}});}`;
46
+ var FORM_JS = `{const d=window.document,c=(n,e)=>n.getAttribute(e),u=(n,e)=>n.appendChild(e);customElements.define("m-invalid",class extends HTMLElement{connectedCallback(){const n=c(this,"for"),e=this.closest("form"),m=this.textContent;if(n&&e&&m)for(const i of n.split(",")){const s=e.elements.namedItem(i.trim());if(s){const l=()=>{s.removeEventListener("input",l),s.setCustomValidity("")};s.addEventListener("input",l),s.setCustomValidity(m),s.focus()}}this.remove()}}),window.$onRFS=async n=>{n.preventDefault();const e=n.target;if(!e.checkValidity())return;const m=new FormData(e),i=[...e.elements];for(const l of i)l._disabled=l.disabled,l.disabled=!0;const s=await fetch(location.href,{method:"POST",headers:{"x-route-form":"true","x-flags":$FLAGS},body:m});if(s.ok){const[l,p]=await s.json(),E=d.createElement("template"),b=new Map;e.querySelectorAll("m-formslot").forEach(t=>{t.innerHTML=""}),E.innerHTML=l;for(const t of i)t.disabled=t._disabled,delete t._disabled;for(const t of E.content.childNodes){if(t.nodeType===1){const o=t,r=c(o,"formslot"),a=r?'m-formslot[name="'+r+'"]':"m-formslot",f=r?e.querySelector(a)??d.querySelector(a):e.querySelector(a);if(f){f.innerHTML="",b.set(o,f);continue}}u(e,t)}for(const[t,o]of b){const r=c(o,"onupdate"),a=c(o,"scope");switch(c(o,"mode")){case"insertbefore":o.before(t);break;case"insertafter":o.after(t);break;default:u(o,t)}r&&$fmap.get(Number(r))?.call($signals?.(Number(a))??o,{type:"update",target:o})}setTimeout(()=>{i.some(t=>!t.validity.valid)||e.reset()},0),p&&(u(d.body,d.createElement("script")).textContent=p+";document.currentScript.remove();")}};}`;
35
47
  var RPC_JS = `{window.$RPC=(e,t)=>new Proxy(Object.create(null),{get(s,r){if(t.includes(r))return(...i)=>fetch(location.href,{method:"POST",body:JSON.stringify({fn:r,args:i}),headers:{"x-rpc":"true","x-rpc-id":e.toString()}}).then(async o=>{const{error:n,result:u}=await o.json();if(n)throw new Error(n);return u})}});}`;
36
48
 
37
49
  // runtime/utils.ts
@@ -42,19 +54,6 @@ var isFunction = (v) => typeof v === "function";
42
54
  var isObject = (v) => typeof v === "object" && v !== null;
43
55
  var isPlainObject = (v) => isObject(v) && (v.constructor === Object || v.constructor === void 0);
44
56
  var toHyphenCase = (k) => k.replace(/[a-z][A-Z]/g, (m) => m.charAt(0) + "-" + m.charAt(1).toLowerCase());
45
- var IdGen = class extends Map {
46
- #seq = 0;
47
- gen(v) {
48
- return this.get(v) ?? this.set(v, this.#seq++).get(v);
49
- }
50
- getById(id) {
51
- for (const [v, i] of this.entries()) {
52
- if (i === id) {
53
- return v;
54
- }
55
- }
56
- }
57
- };
58
57
  var hashCode = (str) => {
59
58
  let hash = 0;
60
59
  for (let i = 0; i < str.length; i++) {
@@ -171,16 +170,6 @@ var escapeHTML = (str) => {
171
170
  return lastIndex !== index ? html2 + str.slice(lastIndex, index) : html2;
172
171
  };
173
172
 
174
- // symbols.ts
175
- var $fragment = /* @__PURE__ */ Symbol.for("jsx.fragment");
176
- var $html = /* @__PURE__ */ Symbol.for("jsx.html");
177
- var $vnode = /* @__PURE__ */ Symbol.for("jsx.vnode");
178
- var $setup = /* @__PURE__ */ Symbol.for("mono.setup");
179
- var $rpc = /* @__PURE__ */ Symbol.for("mono.rpc");
180
-
181
- // version.ts
182
- var VERSION = "0.10.0-beta.4";
183
-
184
173
  // render.ts
185
174
  var FunctionIdGenerator = class extends Map {
186
175
  constructor(seq = 0) {
@@ -218,6 +207,19 @@ var Ref = class {
218
207
  this.name = name;
219
208
  }
220
209
  };
210
+ var IdGen = class extends Map {
211
+ #seq = 0;
212
+ gen(v) {
213
+ return this.get(v) ?? this.set(v, this.#seq++).get(v);
214
+ }
215
+ getById(id) {
216
+ for (const [v, i] of this.entries()) {
217
+ if (i === id) {
218
+ return v;
219
+ }
220
+ }
221
+ }
222
+ };
221
223
  var cdn = "https://raw.esm.sh";
222
224
  var encoder = new TextEncoder();
223
225
  var voidTags = new Set("area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(","));
@@ -227,8 +229,6 @@ var subtle = crypto.subtle;
227
229
  var stringify = JSON.stringify;
228
230
  var isVNode = (v) => Array.isArray(v) && v.length === 3 && v[2] === $vnode;
229
231
  var isReactive = (v) => v instanceof Signal || v instanceof Compute;
230
- var isFC = (v) => isFunction(v) && v.name.charCodeAt(0) <= /*Z*/
231
- 90;
232
232
  var identifierRegex = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
233
233
  var escapeCSSText = (str) => str.replace(/[><]/g, (m) => m.charCodeAt(0) === 60 ? "&lt;" : "&gt;");
234
234
  var toAttrStringLit = (str) => '"' + escapeHTML(str) + '"';
@@ -240,8 +240,7 @@ function renderToWebStream(root, options) {
240
240
  const reqHeaders = request?.headers;
241
241
  const componentName = reqHeaders?.get("x-component");
242
242
  const routeForm = reqHeaders?.has("x-route-form");
243
- const rpc = reqHeaders?.has("x-rpc");
244
- if (rpc) {
243
+ if (reqHeaders?.has("x-rpc")) {
245
244
  if (!request || request.method !== "POST") {
246
245
  return new Response(null, { status: 405 });
247
246
  }
@@ -277,7 +276,7 @@ function renderToWebStream(root, options) {
277
276
  }
278
277
  try {
279
278
  const rpcSession = session ? await createSession(request, session) : void 0;
280
- const result = await rpcFunction.apply(createRPCThis(request, context, rpcSession), args);
279
+ const result = await rpcFunction.apply(createInvokeScope(request, context, rpcSession), args);
281
280
  return Response.json({ result });
282
281
  } catch (err) {
283
282
  return Response.json({ error: errorStringify(err) }, { status: 500 });
@@ -333,7 +332,7 @@ function renderToWebStream(root, options) {
333
332
  }
334
333
  if (reqHeaders?.has("x-route") || routeForm) {
335
334
  if (!routeFC) {
336
- return Response.json({ error: { message: "Route not found" }, status }, { headers, status });
335
+ return Response.json({ error: "Route not found", status }, { headers, status });
337
336
  }
338
337
  component = routeFC;
339
338
  }
@@ -344,23 +343,24 @@ function renderToWebStream(root, options) {
344
343
  async start(controller) {
345
344
  try {
346
345
  if (component instanceof Promise) {
347
- const module = await component;
348
- component = module.default;
349
- if (module.FormHandler && !component.FormHandler) {
350
- component.FormHandler = module.FormHandler;
346
+ const { default: defaultExport, ...rest } = await component;
347
+ if (isFunction(defaultExport)) {
348
+ component = defaultExport;
349
+ Object.assign(component, rest);
351
350
  }
352
351
  }
353
352
  let propsHeader = reqHeaders?.get("x-props");
354
353
  let props = propsHeader ? JSON.parse(propsHeader) : {};
355
354
  let html2 = "";
356
355
  let js = "";
357
- let json = "";
356
+ let buf = "";
358
357
  let vnode = [component, props, $vnode];
359
358
  if (routeForm && request?.method === "POST") {
360
- if (typeof component.FormHandler !== "function") {
359
+ const FormHandler = component.FormHandler;
360
+ if (!FormHandler || !isFunction(FormHandler)) {
361
361
  throw new Error(component.name + ".FormHandler is undefined or not a function");
362
362
  }
363
- vnode = [component.FormHandler, await request.formData(), $vnode];
363
+ vnode = [FormHandler, await request.formData(), $vnode];
364
364
  }
365
365
  await render(
366
366
  vnode,
@@ -370,15 +370,16 @@ function renderToWebStream(root, options) {
370
370
  true,
371
371
  routeForm
372
372
  );
373
- json = "[" + stringify(html2);
373
+ buf = "[" + stringify(html2);
374
374
  if (js) {
375
- json += "," + stringify(js);
375
+ buf += "," + stringify(js);
376
376
  }
377
- controller.enqueue(encoder.encode(json + "]"));
377
+ if (component.dynamic) {
378
+ buf += ",true";
379
+ }
380
+ controller.enqueue(encoder.encode(buf + "]"));
378
381
  } catch (err) {
379
- controller.enqueue(encoder.encode(
380
- '["",' + stringify('console.log("[mono-jsx]",' + stringify(err.stack)) + "]"
381
- ));
382
+ controller.enqueue(encoder.encode(stringify({ error: errorStringify(err) })));
382
383
  console.error(err);
383
384
  } finally {
384
385
  controller.close();
@@ -417,7 +418,7 @@ function renderToWebStream(root, options) {
417
418
  );
418
419
  }
419
420
  async function render(node, options, write, writeJS, componentMode, routeForm) {
420
- const { app, context, request, routeFC } = options;
421
+ const { app, context, request, routeFC, metadata } = options;
421
422
  const suspenses = [];
422
423
  const signals = {
423
424
  app: {},
@@ -432,14 +433,14 @@ async function render(node, options, write, writeJS, componentMode, routeForm) {
432
433
  request,
433
434
  signals,
434
435
  routeFC,
436
+ metadata,
435
437
  flags: { scope: 0, chunk: 0, runtime: 0 },
436
- fidGenerator: new FunctionIdGenerator(),
437
- extraJS: []
438
+ fidGenerator: new FunctionIdGenerator()
438
439
  };
439
440
  signals.app = Object.assign(createThisProxy(rc, 0), app);
440
441
  let runtimeFlag = 0;
441
442
  const finalize = async () => {
442
- const { extraJS, fidGenerator, session, flags } = rc;
443
+ const { fidGenerator, session, flags } = rc;
443
444
  const computes = signals.computes;
444
445
  const hasEffect = signals.effects.length > 0;
445
446
  const treeshake = (flag, code, force) => {
@@ -461,16 +462,14 @@ async function render(node, options, write, writeJS, componentMode, routeForm) {
461
462
  treeshake(SUSPENSE, SUSPENSE_JS, suspenses.length > 0);
462
463
  treeshake(COMPONENT, COMPONENT_JS);
463
464
  treeshake(ROUTER, ROUTER_JS);
465
+ treeshake(REDIRECT, REDIRECT_JS);
464
466
  treeshake(FORM, FORM_JS);
465
- if (js.length > 0) {
466
- js = "(()=>{" + js + "})();/*!*/";
467
- }
468
467
  if (runtimeFlag & ROUTER && runtimeFlag & SIGNALS && request) {
469
468
  const { params } = request;
470
469
  const url = "new URL(" + stringify(request.url) + ")";
471
470
  const urlWithParams = params ? "Object.assign(" + url + "," + stringify(params) + ")" : url;
472
471
  if (componentMode) {
473
- if (!routeForm) {
472
+ if (!routeForm && params) {
474
473
  js += "$signals(0).url=" + urlWithParams + ";";
475
474
  }
476
475
  } else {
@@ -562,12 +561,9 @@ async function render(node, options, write, writeJS, componentMode, routeForm) {
562
561
  if (sameSite) {
563
562
  cookie += "; SameSite=" + sameSite;
564
563
  }
565
- js += "document.cookie=" + toAttrStringLit(cookie) + ";";
564
+ js = "document.cookie=" + toAttrStringLit(cookie) + ";" + js;
566
565
  }
567
566
  }
568
- if (extraJS.length > 0) {
569
- js += extraJS.splice(0, extraJS.length).join("");
570
- }
571
567
  if (js.length > 0) {
572
568
  writeJS(js);
573
569
  }
@@ -695,7 +691,11 @@ async function renderNode(rc, node, stripSlotProp) {
695
691
  // `<switch>` element
696
692
  case "switch": {
697
693
  const { value: valueProp, children } = props;
698
- if (children !== void 0) {
694
+ if (rc.svg && valueProp === void 0) {
695
+ write("<switch>");
696
+ await renderChildren(rc, children);
697
+ write("</switch>");
698
+ } else if (children !== void 0) {
699
699
  let slots = Array.isArray(children) ? isVNode(children) ? [children] : children : [children];
700
700
  let matchedSlot;
701
701
  let namedSlots = [];
@@ -761,11 +761,11 @@ async function renderNode(rc, node, stripSlotProp) {
761
761
  };
762
762
  if (isVNode(as)) {
763
763
  const [fc, props2] = as;
764
- if (isFC(fc)) {
764
+ if (isFunction(fc)) {
765
765
  attrs += ' name="@comp_' + componentsMap.gen(fc) + '"';
766
766
  writeAttr("props", props2);
767
767
  }
768
- } else if (isFC(is)) {
768
+ } else if (isFunction(is)) {
769
769
  attrs += ' name="@comp_' + componentsMap.gen(is) + '"';
770
770
  writeAttr("props");
771
771
  } else if (props.name) {
@@ -803,7 +803,14 @@ async function renderNode(rc, node, stripSlotProp) {
803
803
  }
804
804
  write("<m-router" + attrs + ">");
805
805
  if (routeFC) {
806
- await renderFC(rc, routeFC instanceof Promise ? (await routeFC).default : routeFC, {}, true);
806
+ if (routeFC instanceof Promise) {
807
+ routeFC = (await routeFC).default;
808
+ if (!routeFC || !isFunction(routeFC)) {
809
+ console.warn("[mono-jsx] <router> The `default` export is not a function component.");
810
+ break;
811
+ }
812
+ }
813
+ await renderFC(rc, routeFC, {}, true);
807
814
  }
808
815
  if (children) {
809
816
  if (routeFC) {
@@ -822,6 +829,32 @@ async function renderNode(rc, node, stripSlotProp) {
822
829
  rc.flags.runtime |= ROUTER;
823
830
  break;
824
831
  }
832
+ case "metadata": {
833
+ if (rc.svg) {
834
+ write("<metadata>");
835
+ await renderChildren(rc, props.children);
836
+ write("</metadata>");
837
+ } else if (rc.routeFC) {
838
+ let { metadata, getMetadata } = await rc.routeFC;
839
+ if (isFunction(getMetadata)) {
840
+ const { request, context, session } = rc;
841
+ if (!request) {
842
+ throw new TypeError("[mono-jsx] The `request` prop in the `<html>` element is required.");
843
+ }
844
+ metadata = await getMetadata.call(createInvokeScope(request, context, session));
845
+ }
846
+ let buf = "";
847
+ for (const [key, value] of Object.entries(Object.assign({}, rc.metadata, metadata))) {
848
+ if (key === "title") {
849
+ buf += "<title>" + value + "</title>";
850
+ } else {
851
+ buf += "<meta name=" + toAttrStringLit(key) + " content=" + toAttrStringLit(value) + ">";
852
+ }
853
+ }
854
+ write(buf);
855
+ }
856
+ break;
857
+ }
825
858
  case "cache":
826
859
  case "static": {
827
860
  const { $stack, key = $stack, maxAge, children } = props;
@@ -857,10 +890,15 @@ async function renderNode(rc, node, stripSlotProp) {
857
890
  }
858
891
  case "redirect": {
859
892
  const { to, replace } = props;
860
- if (to) {
861
- rc.extraJS.push(
862
- '{let u=decodeURI("' + encodeURI(String(to)) + '");if(window.$router){$router.navigate(u' + (replace ? ",!1" : "") + ")}else{location.href=u}}"
863
- );
893
+ if (isString(to) || to instanceof URL) {
894
+ let buf = "<m-redirect";
895
+ buf += " to=" + toAttrStringLit(String(to));
896
+ if (replace) {
897
+ buf += " replace";
898
+ }
899
+ buf += "></m-redirect>";
900
+ write(buf);
901
+ rc.flags.runtime |= REDIRECT;
864
902
  }
865
903
  break;
866
904
  }
@@ -915,8 +953,8 @@ async function renderNode(rc, node, stripSlotProp) {
915
953
  }
916
954
  let buffer = "<" + tag;
917
955
  let attrModifiers = "";
918
- let isSingleEl = props.children === void 0;
919
- let isSvgSelfClosingElement = rc.svg && isSingleEl;
956
+ let noChildren = props.children === void 0;
957
+ let isSvgSelfClosingElement = rc.svg && noChildren;
920
958
  for (let [propName, propValue] of Object.entries(props)) {
921
959
  switch (propName) {
922
960
  case "children":
@@ -924,7 +962,7 @@ async function renderNode(rc, node, stripSlotProp) {
924
962
  break;
925
963
  case "route":
926
964
  if (tag === "form") {
927
- buffer += ' onsubmit="$onrfs(event)"';
965
+ buffer += ' onsubmit="$onRFS(event)"';
928
966
  rc.flags.runtime |= FORM;
929
967
  break;
930
968
  }
@@ -947,7 +985,7 @@ async function renderNode(rc, node, stripSlotProp) {
947
985
  if (attrModifiers) {
948
986
  write(attrModifiers);
949
987
  }
950
- if (!isSingleEl) {
988
+ if (!noChildren) {
951
989
  await renderChildren(tag === "svg" ? { ...rc, svg: true } : rc, props.children);
952
990
  }
953
991
  if (!isSvgSelfClosingElement) {
@@ -1061,7 +1099,7 @@ async function renderFC(rc, fcFn, props, eager) {
1061
1099
  });
1062
1100
  } else {
1063
1101
  console.error(err);
1064
- write("<script>console.error(" + stringify(err instanceof Error ? err.stack ?? err.message : String(err)) + ")<\/script>");
1102
+ write("<script>console.error(" + stringify(err) + ")<\/script>");
1065
1103
  }
1066
1104
  }
1067
1105
  }
@@ -1357,7 +1395,7 @@ function createThisProxy(rc, scopeId) {
1357
1395
  });
1358
1396
  return thisProxy;
1359
1397
  }
1360
- function createRPCThis(request, context, session) {
1398
+ function createInvokeScope(request, context, session) {
1361
1399
  const scope = new NullPrototypeObject();
1362
1400
  Object.assign(scope, { request, context: context ?? {} });
1363
1401
  Object.defineProperty(scope, "session", {
@@ -1469,7 +1507,19 @@ var jsx = (tag, props = new NullPrototypeObject(), key) => {
1469
1507
  return props;
1470
1508
  }
1471
1509
  const renderOptions = new NullPrototypeObject();
1472
- const optionsKeys = /* @__PURE__ */ new Set(["app", "context", "components", "expose", "routes", "request", "session", "status", "headers", "htmx"]);
1510
+ const optionsKeys = /* @__PURE__ */ new Set([
1511
+ "app",
1512
+ "context",
1513
+ "components",
1514
+ "expose",
1515
+ "routes",
1516
+ "request",
1517
+ "session",
1518
+ "status",
1519
+ "headers",
1520
+ "htmx",
1521
+ "metadata"
1522
+ ]);
1473
1523
  for (const [key2, value] of Object.entries(props)) {
1474
1524
  if (optionsKeys.has(key2) || key2.startsWith("htmx-ext-")) {
1475
1525
  renderOptions[key2] = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.10.0-beta.4",
3
+ "version": "0.10.0-beta.6",
4
4
  "description": "`<html>` as a `Response` (Yet another JSX runtime for server-side rendering).",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
@@ -17,4 +17,6 @@ declare global {
17
17
  // extends the `<html>` element attributes
18
18
  interface HtmlCustomAttributes extends RenderOptions {}
19
19
  }
20
+ type ComponentElement = Mono.ComponentElement;
21
+ type RouterElement = Mono.RouterElement;
20
22
  }
package/types/jsx.d.ts CHANGED
@@ -4,6 +4,8 @@ export type ChildPrimitiveType = JSX.Element | string | number | bigint | boolea
4
4
  export type ChildType = MaybeArray<MaybeGetter<ChildPrimitiveType>>;
5
5
  export type MaybeArray<T> = T | T[];
6
6
  export type MaybeGetter<T> = T | { get: () => T };
7
+ export type MaybePromise<T> = T | Promise<T>;
8
+ export type MaybeModule<T> = T | Promise<{ default: T } & ComponentConfig>;
7
9
  export type MaybePromiseOrGenerator<T> = T | Promise<T> | Generator<T> | AsyncGenerator<T>;
8
10
 
9
11
  export interface BaseAttributes {
@@ -54,11 +56,18 @@ export type VNode = readonly [
54
56
  $vnode: symbol,
55
57
  ];
56
58
 
57
- export interface ComponentType<P = {}> {
58
- (props: P): MaybePromiseOrGenerator<VNode | string | null>;
59
+ export interface ComponentConfig {
60
+ FormHandler?: ComponentType<FormData>;
61
+ getMetadata?: () => MaybePromise<Record<string, string>>;
62
+ metadata?: Record<string, string>;
63
+ dynamic?: boolean;
59
64
  rendering?: string;
60
65
  }
61
66
 
67
+ export interface ComponentType<P = {}> extends ComponentConfig {
68
+ (props: P): MaybePromiseOrGenerator<VNode | string | null>;
69
+ }
70
+
62
71
  export interface MonoBuiltinElements {
63
72
  /**
64
73
  * A built-in element of mono-jsx that toggles the visibility of its children.
package/types/mono.d.ts CHANGED
@@ -4,7 +4,7 @@ export type WithParams<T> = T & { params?: Record<string, string> };
4
4
 
5
5
  export interface Elements {
6
6
  /**
7
- * A built-in element of mono-jsx that is used to load components lazily,
7
+ * The `<component>` element is used to load components lazily,
8
8
  * which can improve performance by reducing the initial load time of the application.
9
9
  * @mono-jsx
10
10
  */
@@ -37,7 +37,7 @@ export interface Elements {
37
37
  };
38
38
 
39
39
  /**
40
- * A built-in element of mono-jsx that provides SPA routing.
40
+ * The `<router>` element provides SPA routing.
41
41
  * @mono-jsx
42
42
  */
43
43
  router: BaseAttributes & AsyncComponentAttributes & {
@@ -57,7 +57,7 @@ export interface Elements {
57
57
  };
58
58
 
59
59
  /**
60
- * A built-in element of mono-jsx that caches the rendered content of the child nodes
60
+ * The `<cache>` element caches the rendered content of the child nodes
61
61
  * with the given key and TTL.
62
62
  * @mono-jsx
63
63
  */
@@ -73,14 +73,14 @@ export interface Elements {
73
73
  };
74
74
 
75
75
  /**
76
- * A built-in element of mono-jsx that treats the child nodes as static content,
76
+ * The `<static>` element treats the child nodes as static content,
77
77
  * When the child nodes are rendered once, they will be cached in memory and reused on subsequent renders.
78
78
  * @mono-jsx
79
79
  */
80
80
  static: BaseAttributes;
81
81
 
82
82
  /**
83
- * A built-in element of mono-jsx that redirects to the given URL in the client side.
83
+ * The `<redirect>` element redirects to the given URL in the client side.
84
84
  * @mono-jsx
85
85
  */
86
86
  redirect: {
@@ -96,7 +96,7 @@ export interface Elements {
96
96
  };
97
97
 
98
98
  /**
99
- * A built-in element of mono-jsx that sets custom validation
99
+ * The `<invalid>` element sets custom validation
100
100
  * state for the form elements.
101
101
  * @mono-jsx
102
102
  */
@@ -108,7 +108,7 @@ export interface Elements {
108
108
  };
109
109
 
110
110
  /**
111
- * A built-in element of mono-jsx that is used to display the content of the route form
111
+ * The `<formslot>` element is used to display the content of the route form
112
112
  * in the `form` element.
113
113
  * @mono-jsx
114
114
  */
@@ -147,11 +147,29 @@ export type ComponentElement = {
147
147
  refresh: () => Promise<void>;
148
148
  };
149
149
 
150
+ /**
151
+ * The options for the `navigate` method of the `<router>` element.
152
+ */
153
+ export type RouterNavigateOptions = {
154
+ /**
155
+ * If true, `history.replaceState` will be called instead of `history.pushState`.
156
+ * This is useful when you want to replace the current navigation without adding a new entry to the history.
157
+ */
158
+ replace?: boolean;
159
+ /**
160
+ * If true, the router will ignore the cache and fetch the page HTML from the server.
161
+ */
162
+ refresh?: boolean;
163
+ };
164
+
150
165
  /**
151
166
  * The `<router>` element.
152
167
  */
153
168
  export type RouterElement = {
154
- navigate: (url: string | URL, options?: { replace?: boolean }) => Promise<void>;
169
+ /**
170
+ * Navigates to the given URL.
171
+ */
172
+ navigate: (url: string | URL, options?: RouterNavigateOptions) => Promise<void>;
155
173
  };
156
174
 
157
175
  /**
package/types/render.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  /// <reference path="./htmx.d.ts" />
2
2
 
3
- import { inflate } from "node:zlib";
4
- import type { ComponentType } from "./jsx.d.ts";
5
-
6
- export type MaybeModule<T> = T | Promise<{ default: T; FormHandler?: Function }>;
3
+ import type { ComponentType, MaybeModule } from "./jsx.d.ts";
7
4
 
8
5
  /**
9
6
  * Htmx extensions.
@@ -89,6 +86,10 @@ export interface RenderOptions extends Partial<HtmxExts> {
89
86
  * Routes to be used by the `<router>` element.
90
87
  */
91
88
  routes?: Record<string, MaybeModule<ComponentType<any>>>;
89
+ /**
90
+ * Global metadata.
91
+ */
92
+ metadata?: Record<string, string>;
92
93
  /**
93
94
  * Current `Request` object to be passed to components.
94
95
  */