mono-jsx 0.9.14 → 0.10.0-beta.2

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
@@ -11,6 +11,7 @@ mono-jsx is a JSX runtime that renders the `<html>` element to a `Response` obje
11
11
  - 💡 Complete Web API TypeScript definitions
12
12
  - ⏳ Streaming rendering
13
13
  - 🗂️ Built-in router (SPA mode)
14
+ - 📡 Built-in remote procedure call (RPC) API
14
15
  - 🔑 Session storage
15
16
  - 🥷 [htmx](#using-htmx) integration
16
17
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
@@ -1295,7 +1296,7 @@ export default {
1295
1296
 
1296
1297
  ## Using Session
1297
1298
 
1298
- mono-jsx provides a built-in session storage that allows you to manage user sessions. To use the session storage, you need to set the `session` prop on the root `<html>` element with the `cookie.secret` option.
1299
+ 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.
1299
1300
 
1300
1301
  ```tsx
1301
1302
  function Index(this: FC) {
@@ -1356,29 +1357,96 @@ function Component(this: FC) {
1356
1357
  }
1357
1358
  ```
1358
1359
 
1359
- ## Caching
1360
+ > [!WARNING]
1361
+ > The session storage stores data with cookies. **Therefore, you should never store sensitive data in the session storage.**
1360
1362
 
1361
- mono-jsx renders HTML dynamically per request; large apps may tax your CPU resources. To improve rendering performance, mono-jsx introduces two built-in elements that can cache the rendered HTML of the children:
1363
+ ## Using RPC
1362
1364
 
1363
- - `<cache>` with specified `key` and `maxAge`
1364
- - `<static>` for elements that rarely change, such as `<svg>`
1365
+ mono-jsx provides a built-in RPC API that allows you to call functions on the server from the client side. You can use the `createRPC` function to create a RPC object that contains the functions you want to call on the client side, and then pass it to the `expose` prop on the root `<html>` element.
1365
1366
 
1366
1367
  ```tsx
1367
- function BlogPage() {
1368
+ import { createRPC } from "mono-jsx"
1369
+
1370
+ const rpc = createRPC({
1371
+ whoami: () => ({ name: "John" })
1372
+ })
1373
+
1374
+ function App(this: FC<{ user?: { name: string } }>) {
1368
1375
  return (
1369
- <cache key="blog" maxAge={86400}>
1370
- <Blog />
1371
- </cache>
1376
+ <div>
1377
+ <show when={this.user}>
1378
+ <p>Welcome, {this.user!.name}!</p>
1379
+ </show>
1380
+ <button onClick={async () => this.user = await rpc.whoami()}>Who am I?</button>
1381
+ </div>
1382
+ )
1383
+ }
1384
+
1385
+ export default {
1386
+ fetch: (req) => (
1387
+ <html request={req} expose={{ rpc }}>
1388
+ <App />
1389
+ </html>
1372
1390
  )
1373
1391
  }
1392
+ ```
1393
+
1394
+ > [!NOTE]
1395
+ > mono-jsx sends the rpc invoke result to the client side as a JSON object.
1396
+
1397
+ ### Accessing request info in RPC functions
1398
+
1399
+ You can access the request info in RPC functions by using the `this` scope:
1374
1400
 
1375
- function Icon() {
1401
+ - `this.request`: The [request](#accessing-request-info) object.
1402
+ - `this.context`: The [context](#using-context) object.
1403
+ - `this.session`: The [session](#session-storage-api) storage.
1404
+
1405
+ ```ts
1406
+ type Admin = {
1407
+ isAdmin: (id: number) => boolean;
1408
+ }
1409
+
1410
+ const rpc = createRPC({
1411
+ whoami: function (this: WithContext<RPC, { admin: Admin }>) {
1412
+ const { admin } = this.context;
1413
+ const user = this.session.get<{ name: string }>("user")
1414
+ return {
1415
+ ip: this.request.headers.get("x-real-ip"),
1416
+ isAdmin: user ? admin.isAdmin(user.id) : false,
1417
+ user: user,
1418
+ }
1419
+ },
1420
+ })
1421
+
1422
+ function App(this: FC) {
1376
1423
  return (
1377
- <static>
1378
- <svg>...</svg>
1379
- </static>
1424
+ <button onClick={async () => console.log(await rpc.whoami())}>Who am I?</button>
1380
1425
  )
1381
1426
  }
1427
+
1428
+ export default {
1429
+ fetch: (req) => (
1430
+ <html
1431
+ request={req}
1432
+ expose={{ rpc }}
1433
+ session={{ cookie: { secret: "..." } }}
1434
+ context={{ admin: { isAdmin: (id: number) => id === 1 } }}
1435
+ >
1436
+ <App />
1437
+ </html>
1438
+ )
1439
+ }
1440
+ ```
1441
+
1442
+ To use `this` in RPC functions, you can't use the arrow function syntax. You need to use the function declaration syntax. And the `RPC` type is defined as follows:
1443
+
1444
+ ```ts
1445
+ type RPC<Context extends Record<string, unknown> = {}> = {
1446
+ request: Request;
1447
+ context: Context;
1448
+ session: Session;
1449
+ }
1382
1450
  ```
1383
1451
 
1384
1452
  ## Customizing HTML Response
package/index.mjs CHANGED
@@ -1,9 +1,6 @@
1
1
  // index.ts
2
2
  function buildRoutes(handler) {
3
3
  const { routes = {} } = handler(/* @__PURE__ */ Symbol.for("mono.setup"));
4
- return monoRoutes(routes, handler);
5
- }
6
- function monoRoutes(routes, handler) {
7
4
  const handlers = {};
8
5
  for (const [path, fc] of Object.entries(routes)) {
9
6
  handlers[path] = (request) => {
@@ -13,7 +10,17 @@ function monoRoutes(routes, handler) {
13
10
  }
14
11
  return handlers;
15
12
  }
13
+ var rpcIndex = 0;
14
+ function createRPC(rpcFunctions) {
15
+ for (const [key, value] of Object.entries(rpcFunctions)) {
16
+ if (typeof value !== "function") {
17
+ throw new Error(`createRPC: ${key} is not a function`);
18
+ }
19
+ }
20
+ Reflect.set(rpcFunctions, /* @__PURE__ */ Symbol.for("mono.rpc"), rpcIndex++);
21
+ return rpcFunctions;
22
+ }
16
23
  export {
17
24
  buildRoutes,
18
- monoRoutes
25
+ createRPC
19
26
  };
package/jsx-runtime.mjs CHANGED
@@ -20,9 +20,10 @@ var SUSPENSE = 128;
20
20
  var COMPONENT = 256;
21
21
  var ROUTER = 512;
22
22
  var FORM = 1024;
23
+ var RPC = 2048;
23
24
  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)};}`;
24
25
  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;}`;
25
- 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 u=t=>!!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());var d=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}=b(n);if(o){let r="data-css-",s=d((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)},b=t=>{let n,e=[],o=new y;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=g(e),i=r==="number"?f.test(e)?""+o:o+"px":""+o;n+=(n?";":"")+s+":"+(s==="content"?JSON.stringify(i):i)}}return"{"+n+"}"},y=(()=>{function t(){}return t.prototype=Object.create(null),t})();window.$applyStyle=h;}`;
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;}`;
26
27
  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;}`;
27
28
  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;}`;
28
29
  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;}`;
@@ -31,6 +32,7 @@ var SUSPENSE_JS = `{const i=new Map,o=e=>e.getAttribute("chunk-id"),l=(e,t)=>cus
31
32
  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()}});}`;
32
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}});}`;
33
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();")}};}`;
35
+ 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})}});}`;
34
36
 
35
37
  // runtime/utils.ts
36
38
  var regexpIsNonDimensional = /^(-|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)/;
@@ -38,7 +40,7 @@ var regexpHtmlSafe = /["'&<>]/;
38
40
  var isString = (v) => typeof v === "string";
39
41
  var isFunction = (v) => typeof v === "function";
40
42
  var isObject = (v) => typeof v === "object" && v !== null;
41
- var isPlainObject = (v) => !!v && (v.constructor === Object || v.constructor === void 0);
43
+ var isPlainObject = (v) => isObject(v) && (v.constructor === Object || v.constructor === void 0);
42
44
  var toHyphenCase = (k) => k.replace(/[a-z][A-Z]/g, (m) => m.charAt(0) + "-" + m.charAt(1).toLowerCase());
43
45
  var IdGen = class extends Map {
44
46
  #seq = 0;
@@ -174,9 +176,10 @@ var $fragment = /* @__PURE__ */ Symbol.for("jsx.fragment");
174
176
  var $html = /* @__PURE__ */ Symbol.for("jsx.html");
175
177
  var $vnode = /* @__PURE__ */ Symbol.for("jsx.vnode");
176
178
  var $setup = /* @__PURE__ */ Symbol.for("mono.setup");
179
+ var $rpc = /* @__PURE__ */ Symbol.for("mono.rpc");
177
180
 
178
181
  // version.ts
179
- var VERSION = "0.9.14";
182
+ var VERSION = "0.10.0-beta.1";
180
183
 
181
184
  // render.ts
182
185
  var FunctionIdGenerator = class extends Map {
@@ -226,18 +229,66 @@ var isVNode = (v) => Array.isArray(v) && v.length === 3 && v[2] === $vnode;
226
229
  var isReactive = (v) => v instanceof Signal || v instanceof Compute;
227
230
  var isFC = (v) => isFunction(v) && v.name.charCodeAt(0) <= /*Z*/
228
231
  90;
232
+ var identifierRegex = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
229
233
  var escapeCSSText = (str) => str.replace(/[><]/g, (m) => m.charCodeAt(0) === 60 ? "&lt;" : "&gt;");
230
234
  var toAttrStringLit = (str) => '"' + escapeHTML(str) + '"';
235
+ var errorStringify = (err) => err instanceof Error ? err.message : String(err);
231
236
  function renderToWebStream(root, options) {
232
237
  const { routes, components } = options;
233
238
  const request = options.request;
234
239
  const headers = new Headers();
235
240
  const reqHeaders = request?.headers;
236
- const compHeader = reqHeaders?.get("x-component");
241
+ const componentName = reqHeaders?.get("x-component");
237
242
  const routeForm = reqHeaders?.has("x-route-form");
243
+ const rpc = reqHeaders?.has("x-rpc");
244
+ if (rpc) {
245
+ if (!request || request.method !== "POST") {
246
+ return new Response(null, { status: 405 });
247
+ }
248
+ const rpcIdHeader = reqHeaders?.get("x-rpc-id");
249
+ if (!rpcIdHeader) {
250
+ return Response.json({ error: "RPC ID is required" }, { status: 400 });
251
+ }
252
+ const rpcId = Number(rpcIdHeader);
253
+ if (!Number.isInteger(rpcId)) {
254
+ return Response.json({ error: "RPC ID is invalid" }, { status: 400 });
255
+ }
256
+ const { session, context, expose } = options;
257
+ let rpcTarget;
258
+ if (expose) {
259
+ for (const value of Object.values(expose)) {
260
+ if (isPlainObject(value) && value[$rpc] === rpcId) {
261
+ rpcTarget = value;
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ if (!rpcTarget) {
267
+ return Response.json({ error: "RPC target not found" }, { status: 404 });
268
+ }
269
+ return request.json().then(async (payload) => {
270
+ const { fn, args } = isObject(payload) ? payload : {};
271
+ if (!isString(fn) || !Array.isArray(args)) {
272
+ return Response.json({ error: "RPC payload is invalid" }, { status: 400 });
273
+ }
274
+ const rpcFunction = rpcTarget[fn];
275
+ if (!isFunction(rpcFunction)) {
276
+ return Response.json({ error: "RPC function not found: " + fn }, { status: 404 });
277
+ }
278
+ try {
279
+ const rpcSession = session ? await createSession(request, session) : void 0;
280
+ const result = await rpcFunction.apply(createRPCThis(request, context, rpcSession), args);
281
+ return Response.json({ result });
282
+ } catch (err) {
283
+ return Response.json({ error: errorStringify(err) }, { status: 500 });
284
+ }
285
+ }).catch((err) => {
286
+ return Response.json({ error: "Failed to parse RPC payload: " + errorStringify(err) }, { status: 400 });
287
+ });
288
+ }
238
289
  let status = options.status;
239
290
  let routeFC = request ? Reflect.get(request, "routeFC") : void 0;
240
- let component = compHeader ? compHeader.startsWith("@comp_") ? componentsMap.getById(Number(compHeader.slice(6))) : components?.[compHeader] : null;
291
+ let component = componentName ? componentName.startsWith("@comp_") ? componentsMap.getById(Number(componentName.slice(6))) : components?.[componentName] : null;
241
292
  if (request) {
242
293
  request.URL = new URL(request.url);
243
294
  }
@@ -336,8 +387,8 @@ function renderToWebStream(root, options) {
336
387
  }),
337
388
  { headers }
338
389
  );
339
- } else if (compHeader) {
340
- return new Response("Component not found: " + component, { status: 404 });
390
+ } else if (componentName) {
391
+ return new Response("Component not found: " + componentName, { status: 404 });
341
392
  }
342
393
  headers.set("content-type", "text/html; charset=utf-8");
343
394
  headers.set("transfer-encoding", "chunked");
@@ -457,6 +508,20 @@ async function render(node, options, write, writeJS, componentMode, routeForm) {
457
508
  }
458
509
  fidGenerator.clear();
459
510
  }
511
+ if (options.expose) {
512
+ for (const [key, value] of Object.entries(options.expose)) {
513
+ if (identifierRegex.test(key)) {
514
+ if (isPlainObject(value)) {
515
+ if ($rpc in value) {
516
+ treeshake(RPC, RPC_JS, true);
517
+ js += "window." + key + "=$RPC(" + value[$rpc] + "," + stringify(Object.keys(value)) + ");";
518
+ }
519
+ }
520
+ } else {
521
+ console.warn("[mono-jsx] The key of the `expose` prop is not a valid JavaScript identifier: " + key);
522
+ }
523
+ }
524
+ }
460
525
  if (runtimeFlag & COMPONENT || runtimeFlag & ROUTER || runtimeFlag & FORM) {
461
526
  const { scope, chunk } = flags;
462
527
  js += 'window.$FLAGS="' + scope + "|" + chunk + "|" + runtimeFlag + "|" + fidGenerator.seq + '";';
@@ -765,9 +830,9 @@ async function renderNode(rc, node, stripSlotProp) {
765
830
  const now = Date.now();
766
831
  const value = cache.get(key);
767
832
  if (value && (!value.expiresAt || value.expiresAt > now)) {
768
- write("<!-- " + tag + " -->");
833
+ write("<!-- cache-hit -->");
769
834
  write(value.html);
770
- write("<!-- /" + tag + " -->");
835
+ write("<!-- /cache -->");
771
836
  } else {
772
837
  let buf = "";
773
838
  await renderChildren(
@@ -1291,6 +1356,19 @@ function createThisProxy(rc, scopeId) {
1291
1356
  });
1292
1357
  return thisProxy;
1293
1358
  }
1359
+ function createRPCThis(request, context, session) {
1360
+ const scope = new NullPrototypeObject();
1361
+ Object.assign(scope, { request, context: context ?? {} });
1362
+ Object.defineProperty(scope, "session", {
1363
+ get() {
1364
+ if (!session) {
1365
+ throw new TypeError("[mono-jsx] The `session` prop in the `<html>` element is required.");
1366
+ }
1367
+ return session;
1368
+ }
1369
+ });
1370
+ return scope;
1371
+ }
1294
1372
  async function createSession(request, options) {
1295
1373
  let sessionId;
1296
1374
  let sessionStore = /* @__PURE__ */ new Map();
@@ -1382,7 +1460,7 @@ var jsx = (tag, props = new NullPrototypeObject(), key) => {
1382
1460
  return props;
1383
1461
  }
1384
1462
  const renderOptions = new NullPrototypeObject();
1385
- const optionsKeys = /* @__PURE__ */ new Set(["app", "context", "components", "routes", "request", "session", "status", "headers", "htmx"]);
1463
+ const optionsKeys = /* @__PURE__ */ new Set(["app", "context", "components", "expose", "routes", "request", "session", "status", "headers", "htmx"]);
1386
1464
  for (const [key2, value] of Object.entries(props)) {
1387
1465
  if (optionsKeys.has(key2) || key2.startsWith("htmx-ext-")) {
1388
1466
  renderOptions[key2] = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.9.14",
3
+ "version": "0.10.0-beta.2",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
package/setup.mjs CHANGED
@@ -57,7 +57,9 @@ export default {
57
57
  async function install() {
58
58
  if (globalThis.Deno) {
59
59
  await new Deno.Command("deno", {
60
- args: ["add", "npm:mono-jsx"]
60
+ args: ["add", "npm:mono-jsx"],
61
+ stdout: "inherit",
62
+ stderr: "inherit"
61
63
  }).spawn().status;
62
64
  } else {
63
65
  let npm = "npm";
@@ -67,7 +69,7 @@ async function install() {
67
69
  npm = "pnpm";
68
70
  }
69
71
  const { spawnSync } = await import("node:child_process");
70
- spawnSync(npm, ["add", "mono-jsx"]);
72
+ spawnSync(npm, ["add", "mono-jsx"], { stdio: "inherit" });
71
73
  }
72
74
  }
73
75
  async function setup() {
@@ -78,18 +80,12 @@ async function setup() {
78
80
  if (globalThis.Deno && await exists("deno.jsonc")) {
79
81
  await install();
80
82
  console.log("Please add the following options to your deno.jsonc file:");
81
- console.log(
82
- [
83
- `{`,
84
- ` "compilerOptions": {`,
85
- ` %c"jsx": "react-jsx",`,
86
- ` "jsxImportSource": "mono-jsx",%c`,
87
- ` }`,
88
- `}`
89
- ].join("\n"),
90
- "color:green",
91
- ""
92
- );
83
+ console.log("{");
84
+ console.log(' "compilerOptions": {');
85
+ console.log(' \x1B[32m"jsx": "react-jsx",\x1B[0m');
86
+ console.log(' \x1B[32m"jsxImportSource": "mono-jsx",\x1B[0m');
87
+ console.log(" }");
88
+ console.log("}");
93
89
  return;
94
90
  }
95
91
  let tsConfigFilename = globalThis.Deno ? "deno.json" : "tsconfig.json";
@@ -100,21 +96,20 @@ async function setup() {
100
96
  } catch {
101
97
  }
102
98
  const compilerOptions = tsConfig.compilerOptions ?? (tsConfig.compilerOptions = {});
103
- if (compilerOptions.jsx === "react-jsx" && compilerOptions.jsxImportSource === "mono-jsx") {
104
- console.log("%cmono-jsx already setup.", "color:grey");
105
- return;
106
- }
99
+ compilerOptions.lib ??= globalThis.Deno ? ["dom", "esnext", "deno.ns"] : ["dom", "esnext"];
100
+ compilerOptions.jsx = "react-jsx";
101
+ compilerOptions.jsxImportSource = "mono-jsx";
107
102
  if (!globalThis.Deno) {
108
- compilerOptions.lib ??= ["dom", "esnext"];
109
103
  compilerOptions.module ??= "esnext";
110
104
  compilerOptions.moduleResolution ??= "bundler";
111
105
  compilerOptions.allowImportingTsExtensions ??= true;
112
106
  compilerOptions.noEmit ??= true;
113
107
  }
114
- compilerOptions.jsx = "react-jsx";
115
- compilerOptions.jsxImportSource = "mono-jsx";
116
108
  await writeFile(tsConfigFilename, JSON.stringify(tsConfig, null, 2));
117
- console.log("\u2705 mono-jsx setup complete.");
109
+ console.log("\x1B[32m\u2705 mono-jsx setup complete.\x1B[0m");
110
+ }
111
+ if (import.meta.main) {
112
+ await setup();
118
113
  }
119
114
  async function exists(path) {
120
115
  try {
package/types/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { ComponentType } from "./jsx.d.ts";
2
-
3
1
  /**
4
2
  * `buildRoutes` creates a routing map for bun server.
5
3
  */
@@ -7,11 +5,6 @@ export function buildRoutes(
7
5
  handler: (req: Request) => Response,
8
6
  ): Record<string, (req: Request) => Response>;
9
7
 
10
- /**
11
- * `monoRoutes` creates a routing map for bun server.
12
- * @deprecated Use `buildRoutes` instead.
13
- */
14
- export function monoRoutes(
15
- routes: Record<string, ComponentType<any> | Promise<{ default: ComponentType<any> }>>,
16
- handler: (req: Request) => Response,
17
- ): Record<string, (req: Request) => Response>;
8
+ export function createRPC<V extends Record<string, (...args: any[]) => any>>(
9
+ rpcFunctions: V,
10
+ ): { [K in keyof V]: (...args: Parameters<V[K]>) => Promise<Awaited<ReturnType<V[K]>>> };
package/types/mono.d.ts CHANGED
@@ -195,19 +195,19 @@ declare global {
195
195
  */
196
196
  readonly app: {
197
197
  /**
198
- * The `app.refs` object stores variables in the application scope.
198
+ * The `app.refs` stores variables in the application scope.
199
199
  * It is similar to `refs`, but it is shared across all components in the application.
200
200
  *
201
201
  * **⚠ This is a client-side only API.**
202
202
  */
203
203
  readonly refs: Record<string, HTMLElement>;
204
204
  /**
205
- * The `app.url` object contains the current URL.
205
+ * The `app.url` represents the current URL.
206
206
  */
207
207
  readonly url: WithParams<URL>;
208
208
  };
209
209
  /**
210
- * The `request` object contains the current request.
210
+ * The `request` represents the current request.
211
211
  *
212
212
  * **⚠ This is a server-side only API.**
213
213
  */
@@ -220,30 +220,61 @@ declare global {
220
220
  }
221
221
  >;
222
222
  /**
223
- * The `session` object contains the current session.
223
+ * The `context` represents the current context.
224
+ *
225
+ * **⚠ This is a server-side only API.**
226
+ */
227
+ readonly context: Record<string, unknown>;
228
+ /**
229
+ * The `session` represents the current session.
224
230
  *
225
231
  * **⚠ This is a server-side only API.**
226
232
  */
227
233
  readonly session: Session;
228
234
  }
229
235
 
236
+ /**
237
+ * The `RPC` represents the current RPC request context.
238
+ */
239
+ interface RPC<Context extends Record<string, unknown> = {}> {
240
+ /**
241
+ * The `request` represents the current request.
242
+ *
243
+ * **⚠ This is a server-side only API.**
244
+ */
245
+ request: Request;
246
+ /**
247
+ * The `context` represents the current context.
248
+ *
249
+ * **⚠ This is a server-side only API.**
250
+ */
251
+ context: Context;
252
+ /**
253
+ * The `session` represents the current session.
254
+ *
255
+ * **⚠ This is a server-side only API.**
256
+ */
257
+ session: Session;
258
+ }
259
+
230
260
  /**
231
261
  * Defines the `this.app` type.
232
262
  */
233
- type WithAppSignals<T, AppSignals = {}, AppRefs = Record<string, HTMLElement>> = T extends FC<infer S, infer R> ? FC<S, R> & {
263
+ type WithAppSignals<T, AppSignals = {}, AppRefs extends Record<string, HTMLElement> = Record<string, HTMLElement>> = T extends
264
+ FC<infer S, infer R> ? FC<S, R> & {
234
265
  /**
235
266
  * The global signals shared across the application.
236
267
  */
237
268
  readonly app: {
238
269
  /**
239
- * The `app.refs` object stores variables in the application scope.
270
+ * The `app.refs` stores variables in the application scope.
240
271
  * It is similar to `refs`, but it is shared across all components in the application.
241
272
  *
242
273
  * **⚠ This is a client-side only API.**
243
274
  */
244
275
  readonly refs: AppRefs;
245
276
  /**
246
- * The `app.url` object contains the current URL.
277
+ * The `app.url` represents the current URL.
247
278
  */
248
279
  readonly url: WithParams<URL>;
249
280
  } & Omit<AppSignals, "refs" | "url">;
@@ -253,7 +284,7 @@ declare global {
253
284
  /**
254
285
  * Defines the `this.context` type.
255
286
  */
256
- type WithContext<T, Context = {}> = T extends FC<infer S, infer R> ? FC<S, R> & {
287
+ type WithContext<T, Context extends Record<string, unknown>> = T extends FC<infer S, infer R> ? FC<S, R> & {
257
288
  /**
258
289
  * The rendering context object.
259
290
  *
@@ -261,6 +292,9 @@ declare global {
261
292
  */
262
293
  readonly context: Context;
263
294
  }
295
+ : T extends RPC ? RPC & {
296
+ readonly context: Context;
297
+ }
264
298
  : never;
265
299
  }
266
300
 
package/types/render.d.ts CHANGED
@@ -80,6 +80,10 @@ export interface RenderOptions extends Partial<HtmxExts> {
80
80
  * Components to be rendered by the `<lazy>` element.
81
81
  */
82
82
  components?: Record<string, MaybeModule<ComponentType<any>>>;
83
+ /**
84
+ * The functions to be exposed server variables to the client.
85
+ */
86
+ expose?: Record<string, unknown>;
83
87
  /**
84
88
  * Routes to be used by the `<router>` element.
85
89
  */