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 +81 -13
- package/index.mjs +11 -4
- package/jsx-runtime.mjs +88 -10
- package/package.json +1 -1
- package/setup.mjs +17 -22
- package/types/index.d.ts +3 -10
- package/types/mono.d.ts +42 -8
- package/types/render.d.ts +4 -0
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
|
|
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
|
-
|
|
1360
|
+
> [!WARNING]
|
|
1361
|
+
> The session storage stores data with cookies. **Therefore, you should never store sensitive data in the session storage.**
|
|
1360
1362
|
|
|
1361
|
-
|
|
1363
|
+
## Using RPC
|
|
1362
1364
|
|
|
1363
|
-
-
|
|
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
|
-
|
|
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
|
-
<
|
|
1370
|
-
<
|
|
1371
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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.
|
|
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 ? "<" : ">");
|
|
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
|
|
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 =
|
|
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 (
|
|
340
|
-
return new Response("Component not found: " +
|
|
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("<!--
|
|
833
|
+
write("<!-- cache-hit -->");
|
|
769
834
|
write(value.html);
|
|
770
|
-
write("<!-- /
|
|
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
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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`
|
|
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`
|
|
205
|
+
* The `app.url` represents the current URL.
|
|
206
206
|
*/
|
|
207
207
|
readonly url: WithParams<URL>;
|
|
208
208
|
};
|
|
209
209
|
/**
|
|
210
|
-
* The `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 `
|
|
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
|
|
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`
|
|
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`
|
|
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
|
|
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
|
*/
|