mono-jsx 0.10.0-beta.13 → 0.10.0-beta.15

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
@@ -1340,11 +1340,6 @@ const routes = {
1340
1340
  }
1341
1341
  ```
1342
1342
 
1343
- > [!TIP]
1344
- > You can use `:invalid` CSS selector to style the form elements with invalid state.
1345
-
1346
- ### Handling Route Form Submissions
1347
-
1348
1343
  You can return regular HTML elements in the form handler function, by default it will be appended to the form element. You can add the `mode` attribute on the `<form route>` element to decide where the content should be inserted.
1349
1344
 
1350
1345
  - **"append"** (default): Append the handler output into the form element.
@@ -1366,7 +1361,12 @@ MyRoute.FormHandler = function(this: FC, data: FormData) {
1366
1361
  }
1367
1362
  ```
1368
1363
 
1369
- mono-jsx also provides a built-in `<formslot>` element that is used to control where the form handler content should be inserted. If any `<formslot>` element exists the `mode` attribute on the `<form route>` element will be ignored. It accepts the following modes:
1364
+ > [!TIP]
1365
+ > You can use `:invalid` CSS selector to style the form elements with invalid state.
1366
+
1367
+ ### Using `<formslot>` element
1368
+
1369
+ mono-jsx provides a built-in `<formslot>` element that is used to control where the form handler content should be inserted. If any `<formslot>` element exists the `mode` attribute on the `<form route>` element will be ignored. It accepts the following modes:
1370
1370
 
1371
1371
  - **"replaceChildren"** (default): Replace children of the `<formslot>` element with the returned HTML.
1372
1372
  - **"insertafter"**: Insert HTML after the `<formslot>` element.
@@ -1393,26 +1393,28 @@ MyRoute.FormHandler = function(this: FC, data: FormData) {
1393
1393
  }
1394
1394
  ```
1395
1395
 
1396
- You can add the `name` prop to specify the name of the formslot element. And `formslot` prop to specify the name of the slot to insert the HTML into.
1396
+ You can add the `name` prop to specify the name of the formslot element. And use `formslot` prop in the form handler function to specify the name of the slot to insert the HTML into.
1397
1397
 
1398
1398
  ```tsx
1399
1399
  function MyRoute(this: FC) {
1400
1400
  return (
1401
1401
  <div>
1402
- <formslot name="info" /> { /* <- "This is info message" will be inserted into the formslot element after submitting the form */ }
1403
1402
  <form route>
1404
1403
  <button type="submit">Send</button>
1405
- <formslot name="error" /> { /* <- "This is error message" will be inserted into the formslot element after submitting the form */ }
1404
+ <formslot name="info" /> { /* <- "This is info message" will be inserted here */ }
1405
+ <formslot name="error" /> { /* <- "This is error message" will be inserted here */ }
1406
1406
  </form>
1407
1407
  </div>
1408
1408
  )
1409
1409
  }
1410
1410
 
1411
1411
  MyRoute.FormHandler = function(this: FC, data: FormData) {
1412
- return <>
1413
- <p formslot="info">This is info message</p>
1414
- <p formslot="error">This is error message</p>
1415
- </>
1412
+ return (
1413
+ <>
1414
+ <p formslot="info">This is info message</p>
1415
+ <p formslot="error">This is error message</p>
1416
+ </>
1417
+ )
1416
1418
  }
1417
1419
  ```
1418
1420
 
@@ -1424,7 +1426,7 @@ function MyRoute(this: FC) {
1424
1426
  <form>
1425
1427
  <input type="text" name="message" placeholder="Type Message..." />
1426
1428
  <button type="submit">Send</button>
1427
- <formslot hiddenonUpdate={(evt) => console.log("message updated:", evt.target.textContent)} />
1429
+ <formslot hidden onUpdate={(evt) => console.log("message updated:", evt.target.textContent)} />
1428
1430
  </form>
1429
1431
  )
1430
1432
  }
package/jsx-runtime.mjs CHANGED
@@ -16,7 +16,7 @@ var $setup = /* @__PURE__ */ Symbol.for("mono.setup");
16
16
  var $rpc = /* @__PURE__ */ Symbol.for("mono.rpc");
17
17
 
18
18
  // version.ts
19
- var VERSION = "0.10.0-beta.13";
19
+ var VERSION = "0.10.0-beta.15";
20
20
 
21
21
  // runtime/index.ts
22
22
  var EVENT = 1;
@@ -43,7 +43,7 @@ var SUSPENSE_JS = `{const i=new Map,o=e=>e.getAttribute("chunk-id"),l=(e,t)=>cus
43
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()}});}`;
44
44
  var ROUTER_JS = `{const o=window,r=document,n=location,d=e=>e.origin===n.origin&&l(e)===l(n),l=({pathname:e,search:t})=>e+t;customElements.define("m-router",class extends HTMLElement{#e;#i=new Map;#n=l(n);#t;#a=!0;#s;#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,s=e.closest("nav")?.getAttribute("data-active-class")??"active";d(new URL(t))?i.add(s):i.remove(s)})}async#h(e,t){this.#n=l(e);let i,s=this.#i.get(this.#n);if(s!==void 0&&!t?.refresh&&!this.hasAttribute("no-cache"))this.#o(s);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.#n,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,n.href);if(i.origin!==n.origin||e.startsWith("#")){n.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(n.href,[...this.childNodes])}),this.#s=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:s,rel:h,target:a}=e.target;i||h==="external"||a==="_blank"||!s.startsWith(n.origin)||(e.preventDefault(),this.navigate(s))},this.#r=()=>{l(n)!==this.#n&&this.#h(new URL(n.href))},o.addEventListener("popstate",this.#r),r.addEventListener("click",this.#s),setTimeout(()=>this.#l())}disconnectedCallback(){o.removeEventListener("popstate",this.#r),r.removeEventListener("click",this.#s),delete o.$router,this.#e?.abort(),this.#e=void 0,this.#i.clear(),this.#s=void 0,this.#r=void 0}});}`;
45
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{document:c}=window,r=(n,e)=>n.getAttribute(e),y=(n,e="m-formslot")=>n.querySelector(e)??c.querySelector(e);customElements.define("m-invalid",class extends HTMLElement{connectedCallback(){const n=r(this,"for"),e=this.closest("form"),i=this.textContent;if(n&&e&&i)for(const d of n.split(",")){const l=e.elements.namedItem(d.trim());if(l){const t=()=>{l.removeEventListener("input",t),l.setCustomValidity("")};l.addEventListener("input",t),l.setCustomValidity(i),l.focus()}}this.remove()}}),window.$onRFS=async n=>{n.preventDefault();const e=n.target;if(!e.checkValidity())return;const i=r(e,"data-submitting-class")??"submitting",d=new FormData(e),l=[...e.elements];for(const t of l)t._disabled=t.disabled,t.disabled=!0;e.querySelectorAll("formslot").forEach(t=>t.innerHTML=""),e.classList.add(i);try{const t=await fetch(location.href,{method:"POST",headers:{"x-route-form":"true","x-flags":$FLAGS},body:d});if(t.ok){const[g,u]=await t.json(),f=new Map,p=c.createElement("template");p.innerHTML=g;const{content:a}=p;a.querySelectorAll("[formslot]").forEach(s=>{const o=r(s,"formslot");if(o){const m=y(e,'m-formslot[name="'+o+'"]');m&&(s.remove(),f.set(s,m))}});const E=y(e);if(E)f.set(a,E);else{const s=r(e,"mode");s==="replace"?e.replaceWith(a):s==="prepend"?e.prepend(a):e.append(a)}for(const[s,o]of f){const m=r(o,"scope"),b=r(o,"mode"),h=r(o,"onupdate");b==="insertbefore"?o.before(s):b==="insertafter"?o.after(s):o.replaceChildren(s),h&&$fmap.get(Number(h))?.call($signals?.(Number(m))??o,{type:"update",target:o})}setTimeout(()=>e.checkValidity()&&e.reset()),u&&(c.body.appendChild(c.createElement("script")).textContent=u+";document.currentScript.remove();")}}finally{e.classList.remove(i);for(const t of l)t.disabled=t._disabled,delete t._disabled}};}`;
46
+ var FORM_JS = `{const{document:d}=window,r=(l,e)=>l.getAttribute(e);customElements.define("m-invalid",class extends HTMLElement{connectedCallback(){const l=r(this,"for"),e=this.closest("form"),i=this.textContent;if(l&&e&&i)for(const f of l.split(",")){const n=e.elements.namedItem(f.trim());if(n){const t=()=>{n.removeEventListener("input",t),n.setCustomValidity("")};n.addEventListener("input",t),n.setCustomValidity(i),n.focus()}}this.remove()}}),window.$onRFS=async l=>{l.preventDefault();const e=l.target;if(!e.checkValidity())return;const i=r(e,"data-submitting-class")??"submitting",f=new FormData(e),n=[...e.elements];for(const t of n)t._disabled=t.disabled,t.disabled=!0;e.querySelectorAll("formslot").forEach(t=>t.innerHTML=""),e.classList.add(i);try{const t=await fetch(location.href,{method:"POST",headers:{"x-route-form":"true","x-flags":$FLAGS},body:f});if(t.ok){const[y,p]=await t.json(),u=new Map,E=d.createElement("template");E.innerHTML=y;const{content:a}=E;a.querySelectorAll("[formslot]").forEach(s=>{const o=r(s,"formslot");if(o){const m='m-formslot[name="'+o+'"]',c=e.querySelector(m)??d.querySelector(m);c&&(s.remove(),u.set(s,c))}});const b=e.querySelector("m-formslot:not([name])");if(b)u.set(a,b);else{const s=r(e,"mode");s==="replace"?e.replaceWith(a):s==="prepend"?e.prepend(a):e.append(a)}for(const[s,o]of u){const m=r(o,"scope"),c=r(o,"mode"),h=r(o,"onupdate");c==="insertbefore"?o.before(s):c==="insertafter"?o.after(s):o.replaceChildren(s),h&&$fmap.get(Number(h))?.call($signals?.(Number(m))??o,{type:"update",target:o})}setTimeout(()=>e.checkValidity()&&e.reset()),p&&(d.body.appendChild(d.createElement("script")).textContent=p+";document.currentScript.remove();")}}finally{e.classList.remove(i);for(const t of n)t.disabled=t._disabled,delete t._disabled}};}`;
47
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})}});}`;
48
48
 
49
49
  // runtime/utils.ts
@@ -172,19 +172,34 @@ var escapeHTML = (str) => {
172
172
 
173
173
  // render.ts
174
174
  var FunctionIdGenerator = class extends Map {
175
- constructor(seq = 0) {
176
- super();
177
- this.seq = seq;
178
- }
175
+ seq = 0;
176
+ #fnRef = /* @__PURE__ */ new Map();
179
177
  genId(fn) {
178
+ if (typeof fn === "string") {
179
+ let id2 = this.get(fn);
180
+ if (id2 === void 0) {
181
+ id2 = this.seq++;
182
+ this.set(fn, id2);
183
+ }
184
+ return id2;
185
+ }
186
+ const cached = this.#fnRef.get(fn);
187
+ if (cached !== void 0) {
188
+ return cached;
189
+ }
180
190
  const fnStr = String(fn);
181
191
  let id = this.get(fnStr);
182
192
  if (id === void 0) {
183
193
  id = this.seq++;
184
194
  this.set(fnStr, id);
185
195
  }
196
+ this.#fnRef.set(fn, id);
186
197
  return id;
187
198
  }
199
+ clear() {
200
+ super.clear();
201
+ this.#fnRef.clear();
202
+ }
188
203
  };
189
204
  var Signal = class {
190
205
  constructor(scope, key, value) {
@@ -209,28 +224,34 @@ var Ref = class {
209
224
  };
210
225
  var IdGen = class extends Map {
211
226
  #seq = 0;
227
+ #byId = /* @__PURE__ */ new Map();
212
228
  gen(v) {
213
- return this.get(v) ?? this.set(v, this.#seq++).get(v);
229
+ const existing = this.get(v);
230
+ if (existing !== void 0) {
231
+ return existing;
232
+ }
233
+ const id = this.#seq++;
234
+ this.set(v, id);
235
+ this.#byId.set(id, v);
236
+ return id;
214
237
  }
215
238
  getById(id) {
216
- for (const [v, i] of this.entries()) {
217
- if (i === id) {
218
- return v;
219
- }
220
- }
239
+ return this.#byId.get(id);
221
240
  }
222
241
  };
223
- var cdn = "https://raw.esm.sh";
242
+ var stringify = JSON.stringify;
243
+ var subtle = crypto.subtle;
224
244
  var encoder = new TextEncoder();
245
+ var identifierRegex = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
246
+ var cdn = "https://raw.esm.sh";
225
247
  var voidTags = new Set("area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(","));
226
248
  var defaultMetadata = { viewport: "width=device-width, initial-scale=1.0" };
227
- var cache = /* @__PURE__ */ new Map();
228
249
  var componentsMap = new IdGen();
229
- var subtle = crypto.subtle;
230
- var stringify = JSON.stringify;
250
+ var cache = /* @__PURE__ */ new Map();
251
+ var urlPatternCache = /* @__PURE__ */ new Map();
252
+ var hmacKeyCache = /* @__PURE__ */ new Map();
231
253
  var isVNode = (v) => Array.isArray(v) && v.length === 3 && v[2] === $vnode;
232
254
  var isReactive = (v) => v instanceof Signal || v instanceof Compute;
233
- var identifierRegex = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
234
255
  var escapeCSSText = (str) => str.replace(/[><]/g, (m) => m.charCodeAt(0) === 60 ? "&lt;" : "&gt;");
235
256
  var toAttrStringLit = (str) => '"' + escapeHTML(str) + '"';
236
257
  var errorStringify = (err) => err instanceof Error ? err.message : String(err);
@@ -241,6 +262,15 @@ function renderToWebStream(root, options) {
241
262
  const reqHeaders = request?.headers;
242
263
  const componentName = reqHeaders?.get("x-component");
243
264
  const routeForm = reqHeaders?.has("x-route-form");
265
+ if (request) {
266
+ request.URL = new URL(request.url);
267
+ if (options.onFetch) {
268
+ const res = options.onFetch(request, options.context);
269
+ if (res !== void 0) {
270
+ return res;
271
+ }
272
+ }
273
+ }
244
274
  if (reqHeaders?.has("x-rpc")) {
245
275
  if (!request || request.method !== "POST") {
246
276
  return new Response(null, { status: 405 });
@@ -289,28 +319,24 @@ function renderToWebStream(root, options) {
289
319
  let status = options.status;
290
320
  let routeFC = request ? Reflect.get(request, "routeFC") : void 0;
291
321
  let component = componentName ? componentName.startsWith("@comp_") ? componentsMap.getById(Number(componentName.slice(6))) : components?.[componentName] : null;
292
- if (request) {
293
- request.URL = new URL(request.url);
294
- }
295
322
  if (routes && !routeFC) {
296
323
  if (request) {
297
- const patterns = Object.keys(routes);
298
- const dynamicPatterns = [];
299
- for (const pattern of patterns) {
300
- if (pattern.includes(":") || pattern.includes("*")) {
301
- dynamicPatterns.push(pattern);
302
- } else if (request.URL.pathname === pattern) {
303
- routeFC = routes[pattern];
304
- break;
305
- }
306
- }
324
+ routeFC = routes[request.URL.pathname];
307
325
  if (!routeFC) {
308
- for (const path of dynamicPatterns) {
309
- const match = new URLPattern({ pathname: path }).exec(request.url);
310
- if (match) {
311
- routeFC = routes[path];
312
- request.params = match.pathname.groups;
313
- break;
326
+ for (const pattern of Object.keys(routes)) {
327
+ if (pattern.includes(":") || pattern.includes("*")) {
328
+ let urlPattern = urlPatternCache.get(pattern);
329
+ let match;
330
+ if (!urlPattern) {
331
+ urlPattern = new URLPattern({ pathname: pattern });
332
+ urlPatternCache.set(pattern, urlPattern);
333
+ }
334
+ match = urlPattern.exec(request.URL);
335
+ if (match) {
336
+ routeFC = routes[pattern];
337
+ request.params = match.pathname.groups;
338
+ break;
339
+ }
314
340
  }
315
341
  }
316
342
  }
@@ -352,8 +378,8 @@ function renderToWebStream(root, options) {
352
378
  }
353
379
  let propsHeader = reqHeaders?.get("x-props");
354
380
  let props = propsHeader ? JSON.parse(propsHeader) : {};
355
- let html2 = "";
356
- let js = "";
381
+ const htmlChunks = [];
382
+ const jsChunks = [];
357
383
  let buf = "";
358
384
  let vnode = [component, props, $vnode];
359
385
  if (routeForm && request?.method === "POST") {
@@ -366,11 +392,17 @@ function renderToWebStream(root, options) {
366
392
  await render(
367
393
  vnode,
368
394
  options,
369
- (chunk) => html2 += chunk,
370
- (chunk) => js += chunk,
395
+ (chunk) => {
396
+ htmlChunks.push(chunk);
397
+ },
398
+ (chunk) => {
399
+ jsChunks.push(chunk);
400
+ },
371
401
  true,
372
402
  routeForm
373
403
  );
404
+ const html2 = htmlChunks.join("");
405
+ const js = jsChunks.join("");
374
406
  buf = "[" + stringify(html2);
375
407
  if (js) {
376
408
  buf += "," + stringify(js);
@@ -397,7 +429,19 @@ function renderToWebStream(root, options) {
397
429
  return new Response(
398
430
  new ReadableStream({
399
431
  async start(controller) {
400
- const write = (chunk) => controller.enqueue(encoder.encode(chunk));
432
+ let buffer = "";
433
+ const flush = () => {
434
+ if (buffer.length) {
435
+ controller.enqueue(encoder.encode(buffer));
436
+ buffer = "";
437
+ }
438
+ };
439
+ const write = (chunk) => {
440
+ buffer += chunk;
441
+ if (buffer.length >= 64 * 1024) {
442
+ flush();
443
+ }
444
+ };
401
445
  Reflect.set(options, "routeFC", routeFC);
402
446
  try {
403
447
  write("<!DOCTYPE html>");
@@ -414,6 +458,7 @@ function renderToWebStream(root, options) {
414
458
  console.error(err);
415
459
  write("<script>console.error(" + stringify(errorStringify(err)) + ")<\/script>");
416
460
  } finally {
461
+ flush();
417
462
  controller.close();
418
463
  }
419
464
  }
@@ -541,7 +586,7 @@ async function render(node, options, write, writeJS, componentMode, routeForm) {
541
586
  ]);
542
587
  const signature = await subtle.sign(
543
588
  "HMAC",
544
- await subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
589
+ await importHmacKey(secret),
545
590
  encoder.encode(data)
546
591
  );
547
592
  cookie += btoa(data) + "." + btoa(String.fromCharCode(...new Uint8Array(signature)));
@@ -643,16 +688,23 @@ async function renderNode(rc, node, stripSlotProp) {
643
688
  case "slot": {
644
689
  const fcSlots = rc.fc?.slots;
645
690
  if (fcSlots) {
646
- let slots;
691
+ const slots = [];
647
692
  if (props.name) {
648
- slots = fcSlots.filter((v) => isVNode(v) && v[1].slot === props.name);
693
+ for (let i = 0, n = fcSlots.length; i < n; i++) {
694
+ const v = fcSlots[i];
695
+ if (isVNode(v) && v[1].slot === props.name) {
696
+ slots.push(v);
697
+ }
698
+ }
649
699
  } else {
650
- slots = fcSlots.filter((v) => !isVNode(v) || !v[1].slot);
651
- }
652
- if (slots.length === 0) {
653
- slots = props.children;
700
+ for (let i = 0, n = fcSlots.length; i < n; i++) {
701
+ const v = fcSlots[i];
702
+ if (!isVNode(v) || !v[1].slot) {
703
+ slots.push(v);
704
+ }
705
+ }
654
706
  }
655
- await renderChildren(rc, slots, true);
707
+ await renderChildren(rc, slots.length === 0 ? props.children : slots, true);
656
708
  }
657
709
  break;
658
710
  }
@@ -780,10 +832,16 @@ async function renderNode(rc, node, stripSlotProp) {
780
832
  attrs += renderViewTransitionAttr(props);
781
833
  let buf = "<m-component" + attrs + ">";
782
834
  if (pending) {
783
- const write2 = (chunk) => {
784
- buf += chunk;
785
- };
786
- await renderChildren({ ...rc, write: write2 }, pending);
835
+ const chunks = [];
836
+ await renderChildren(
837
+ forkRenderContext(rc, {
838
+ write: (chunk) => {
839
+ chunks.push(chunk);
840
+ }
841
+ }),
842
+ pending
843
+ );
844
+ buf += chunks.join("");
787
845
  }
788
846
  buf += "</m-component>";
789
847
  if (attrModifiers) {
@@ -820,10 +878,16 @@ async function renderNode(rc, node, stripSlotProp) {
820
878
  if (routeFC) {
821
879
  buf += "<template m-fallback>";
822
880
  }
823
- const write2 = (chunk) => {
824
- buf += chunk;
825
- };
826
- await renderChildren({ ...rc, write: write2 }, children);
881
+ const chunks = [];
882
+ await renderChildren(
883
+ forkRenderContext(rc, {
884
+ write: (chunk) => {
885
+ chunks.push(chunk);
886
+ }
887
+ }),
888
+ children
889
+ );
890
+ buf += chunks.join("");
827
891
  if (routeFC) {
828
892
  buf += "</template>";
829
893
  }
@@ -848,7 +912,8 @@ async function renderNode(rc, node, stripSlotProp) {
848
912
  metadata = await getMetadata.call(createInvokeScope(request, context, session));
849
913
  }
850
914
  let buf = '<meta charset="utf-8">';
851
- for (const [key, value] of Object.entries(Object.assign(defaultMetadata, rc.metadata, metadata))) {
915
+ const mergedMeta = Object.assign({}, defaultMetadata, rc.metadata, metadata);
916
+ for (const [key, value] of Object.entries(mergedMeta)) {
852
917
  if (value) {
853
918
  if (key === "title") {
854
919
  buf += "<title>" + escapeHTML(value) + "</title>";
@@ -873,17 +938,17 @@ async function renderNode(rc, node, stripSlotProp) {
873
938
  write(value.html);
874
939
  write("<!-- /" + tag + " -->");
875
940
  } else {
876
- let buf = "";
941
+ const chunks = [];
877
942
  await renderChildren(
878
- {
879
- ...rc,
943
+ forkRenderContext(rc, {
880
944
  write: (chunk) => {
881
- buf += chunk;
945
+ chunks.push(chunk);
882
946
  }
883
- },
947
+ }),
884
948
  children,
885
949
  true
886
950
  );
951
+ const buf = chunks.join("");
887
952
  cache.set(key, { html: buf, expiresAt: typeof maxAge === "number" && maxAge > 0 ? now + maxAge * 1e3 : void 0 });
888
953
  rc.write(buf);
889
954
  }
@@ -936,12 +1001,16 @@ async function renderNode(rc, node, stripSlotProp) {
936
1001
  }
937
1002
  buf += ">";
938
1003
  if (children) {
939
- await renderChildren({
940
- ...rc,
941
- write: (chunk) => {
942
- buf += chunk;
943
- }
944
- }, children);
1004
+ const chunks = [];
1005
+ await renderChildren(
1006
+ forkRenderContext(rc, {
1007
+ write: (chunk) => {
1008
+ chunks.push(chunk);
1009
+ }
1010
+ }),
1011
+ children
1012
+ );
1013
+ buf += chunks.join("");
945
1014
  }
946
1015
  write(buf + "</m-" + tag + ">");
947
1016
  rc.flags.runtime |= FORM;
@@ -992,7 +1061,11 @@ async function renderNode(rc, node, stripSlotProp) {
992
1061
  write(attrModifiers);
993
1062
  }
994
1063
  if (!noChildren) {
995
- await renderChildren(tag === "svg" ? { ...rc, svg: true } : rc, props.children);
1064
+ if (tag === "svg") {
1065
+ await renderChildren(forkRenderContext(rc, { svg: true }), props.children);
1066
+ } else {
1067
+ await renderChildren(rc, props.children);
1068
+ }
996
1069
  }
997
1070
  if (!isSvgSelfClosingElement) {
998
1071
  write("</" + tag + ">");
@@ -1020,6 +1093,9 @@ async function renderChildren(rc, children, stripSlotProp) {
1020
1093
  await renderNode(rc, children, stripSlotProp);
1021
1094
  }
1022
1095
  }
1096
+ function forkRenderContext(rc, overrides) {
1097
+ return Object.assign(Object.create(rc), overrides);
1098
+ }
1023
1099
  async function renderFC(rc, fcFn, props, eager) {
1024
1100
  const { write } = rc;
1025
1101
  const { children } = props;
@@ -1037,8 +1113,9 @@ async function renderFC(rc, fcFn, props, eager) {
1037
1113
  promise = promise.catch(catchFn);
1038
1114
  }
1039
1115
  if (eager || (props.rendering ?? fcFn.rendering) === "eager") {
1040
- await renderNode({ ...rc, fc }, await promise);
1041
- markSignals(rc, signals);
1116
+ const fcRc = forkRenderContext(rc, { fc });
1117
+ await renderNode(fcRc, await promise);
1118
+ markSignals(fcRc, signals);
1042
1119
  } else {
1043
1120
  const chunkIdAttr = 'chunk-id="' + (rc.flags.chunk++).toString(36) + '"';
1044
1121
  write("<m-portal " + chunkIdAttr + ">");
@@ -1047,22 +1124,26 @@ async function renderFC(rc, fcFn, props, eager) {
1047
1124
  }
1048
1125
  write("</m-portal>");
1049
1126
  rc.suspenses.push(promise.then(async (node) => {
1050
- let buf = "";
1051
- let write2 = (chunk) => {
1052
- buf += chunk;
1053
- };
1054
- buf += "<m-chunk " + chunkIdAttr + "><template>";
1055
- await renderNode({ ...rc, fc, write: write2 }, node);
1056
- markSignals({ ...rc, write: write2 }, signals);
1057
- return buf + "</template></m-chunk>";
1127
+ const chunks = [];
1128
+ const chunkRc = forkRenderContext(rc, {
1129
+ fc,
1130
+ write: (chunk) => {
1131
+ chunks.push(chunk);
1132
+ }
1133
+ });
1134
+ chunks.push("<m-chunk " + chunkIdAttr + "><template>");
1135
+ await renderNode(chunkRc, node);
1136
+ markSignals(chunkRc, signals);
1137
+ return chunks.join("") + "</template></m-chunk>";
1058
1138
  }));
1059
1139
  }
1060
1140
  } else if (Symbol.asyncIterator in v) {
1061
1141
  if (eager || (props.rendering ?? fcFn.rendering) === "eager") {
1142
+ const fcRc = forkRenderContext(rc, { fc });
1062
1143
  for await (const c of v) {
1063
- await renderNode({ ...rc, fc }, c);
1144
+ await renderNode(fcRc, c);
1064
1145
  }
1065
- markSignals(rc, signals);
1146
+ markSignals(fcRc, signals);
1066
1147
  } else {
1067
1148
  const chunkIdAttr = 'chunk-id="' + (rc.flags.chunk++).toString(36) + '"';
1068
1149
  write("<m-portal " + chunkIdAttr + ">");
@@ -1072,32 +1153,37 @@ async function renderFC(rc, fcFn, props, eager) {
1072
1153
  write("</m-portal>");
1073
1154
  const iter = () => rc.suspenses.push(
1074
1155
  v.next().then(async ({ done, value }) => {
1075
- let buf = "<m-chunk " + chunkIdAttr;
1076
- let write2 = (chunk) => {
1077
- buf += chunk;
1078
- };
1156
+ const chunks = [];
1157
+ const chunkRc = forkRenderContext(rc, {
1158
+ fc,
1159
+ write: (chunk) => {
1160
+ chunks.push(chunk);
1161
+ }
1162
+ });
1079
1163
  if (done) {
1080
- buf += " done>";
1081
- markSignals({ ...rc, write: write2 }, signals);
1082
- return buf + "</m-chunk>";
1164
+ chunks.push("<m-chunk " + chunkIdAttr + " done>");
1165
+ markSignals(chunkRc, signals);
1166
+ return chunks.join("") + "</m-chunk>";
1083
1167
  }
1084
- buf += " next><template>";
1085
- await renderNode({ ...rc, fc, write: write2 }, value);
1168
+ chunks.push("<m-chunk " + chunkIdAttr + " next><template>");
1169
+ await renderNode(chunkRc, value);
1086
1170
  iter();
1087
- return buf + "</template></m-chunk>";
1171
+ return chunks.join("") + "</template></m-chunk>";
1088
1172
  })
1089
1173
  );
1090
1174
  iter();
1091
1175
  }
1092
1176
  } else if (Symbol.iterator in v) {
1177
+ const fcRc = forkRenderContext(rc, { fc });
1093
1178
  for (const node of v) {
1094
- await renderNode({ ...rc, fc }, node);
1179
+ await renderNode(fcRc, node);
1095
1180
  }
1096
- markSignals(rc, signals);
1181
+ markSignals(fcRc, signals);
1097
1182
  }
1098
1183
  } else if (v) {
1099
- await renderNode({ ...rc, fc }, v);
1100
- markSignals(rc, signals);
1184
+ const fcRc = forkRenderContext(rc, { fc });
1185
+ await renderNode(fcRc, v);
1186
+ markSignals(fcRc, signals);
1101
1187
  }
1102
1188
  } catch (err) {
1103
1189
  if (catchFn) {
@@ -1457,7 +1543,7 @@ async function createSession(request, options) {
1457
1543
  signature = atob(signature);
1458
1544
  const verified = await subtle.verify(
1459
1545
  "HMAC",
1460
- await subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]),
1546
+ await importHmacKey(secret),
1461
1547
  Uint8Array.from(signature, (char) => char.charCodeAt(0)),
1462
1548
  encoder.encode(data)
1463
1549
  );
@@ -1500,6 +1586,14 @@ function traverseProps(obj, callback, path = []) {
1500
1586
  }
1501
1587
  return copy;
1502
1588
  }
1589
+ function importHmacKey(secret) {
1590
+ let key = hmacKeyCache.get(secret);
1591
+ if (!key) {
1592
+ key = subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
1593
+ hmacKeyCache.set(secret, key);
1594
+ }
1595
+ return key;
1596
+ }
1503
1597
 
1504
1598
  // jsx-runtime.ts
1505
1599
  var Fragment = $fragment;
@@ -1524,7 +1618,8 @@ var jsx = (tag, props = new NullPrototypeObject(), key) => {
1524
1618
  "status",
1525
1619
  "headers",
1526
1620
  "htmx",
1527
- "metadata"
1621
+ "metadata",
1622
+ "onFetch"
1528
1623
  ]);
1529
1624
  for (const [key2, value] of Object.entries(props)) {
1530
1625
  if (optionsKeys.has(key2) || key2.startsWith("htmx-ext-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.10.0-beta.13",
3
+ "version": "0.10.0-beta.15",
4
4
  "description": "`<html>` as a `Response` (Yet another JSX runtime for server-side rendering).",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
package/types/index.d.ts CHANGED
@@ -5,6 +5,9 @@ export function buildRoutes(
5
5
  handler: (req: Request) => Response,
6
6
  ): Record<string, (req: Request) => Response>;
7
7
 
8
+ /**
9
+ * `createRPC` creates a RPC for the given functions.
10
+ */
8
11
  export function createRPC<V extends Record<string, (...args: any[]) => any>>(
9
12
  rpcFunctions: V,
10
13
  ): { [K in keyof V]: (...args: Parameters<V[K]>) => Promise<Awaited<ReturnType<V[K]>>> };
package/types/render.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference path="./htmx.d.ts" />
2
2
 
3
- import type { ComponentType, MaybeModule, Metadata } from "./jsx.d.ts";
3
+ import type { ComponentType, MaybeModule, MaybePromise, Metadata } from "./jsx.d.ts";
4
4
 
5
5
  /**
6
6
  * Htmx extensions.
@@ -120,4 +120,15 @@ export interface RenderOptions extends Partial<HtmxExts> {
120
120
  * @defaultValue `false`
121
121
  */
122
122
  htmx?: number | string | boolean;
123
+ /**
124
+ * Runs once per render when {@link RenderOptions.request | `request`} is provided, immediately
125
+ * after `request.URL` is set and before RPC handling, routing, and HTML streaming.
126
+ *
127
+ * Return a `Response` or `Promise<Response>` to send that response and skip the rest of the
128
+ * pipeline. Return `undefined` or omit a return value to continue with normal rendering.
129
+ *
130
+ * @param request - The current request; `URL` is a parsed {@link URL} for `request.url`.
131
+ * @param context - The same object as the {@link RenderOptions.context | `context`} option, if any.
132
+ */
133
+ onFetch?: (request: Request & { URL?: URL }, context: RenderOptions["context"]) => MaybePromise<Response> | void;
123
134
  }