mono-jsx 0.3.4 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +52 -13
  2. package/jsx-runtime.mjs +65 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -281,6 +281,53 @@ function App() {
281
281
  }
282
282
  ```
283
283
 
284
+ ## Async Components
285
+
286
+ mono-jsx supports async components that return a `Promise` or an async function. With [streaming rendering](#streaming-rendering), async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
287
+
288
+ ```tsx
289
+ async function Loader(props: { url: string }) {
290
+ const data = await fetch(url).then((res) => res.json());
291
+ return <JsonViewer data={data} />;
292
+ }
293
+
294
+ default {
295
+ fetch: (req) => (
296
+ <html>
297
+ <Loader url="https://api.example.com/data" placeholder={<p>Loading...</p>} />
298
+ </html>
299
+ ),
300
+ };
301
+ ```
302
+
303
+ You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
304
+
305
+ ```jsx
306
+ async function* Chat(props: { prompt: string }) {
307
+ const stream = await openai.chat.completions.create({
308
+ model: "gpt-4",
309
+ messages: [{ role: "user", content: prompt }],
310
+ stream: true,
311
+ });
312
+
313
+ for await (const event of stream) {
314
+ const text = event.choices[0]?.delta.content;
315
+ if (text) {
316
+ yield <span>{text}</span>;
317
+ }
318
+ }
319
+ }
320
+
321
+
322
+ default {
323
+ fetch: (req) => (
324
+ <html>
325
+ <Chat prompt="Tell me a story" placeholder={<span style="color:grey">●</span>} />
326
+ </html>
327
+ ),
328
+ };
329
+ ```
330
+
284
331
  ## Reactive
285
332
 
286
333
  mono-jsx provides a minimal state runtime for updating the view based on client-side state changes.
@@ -575,9 +622,7 @@ async function Sleep({ ms }) {
575
622
  export default {
576
623
  fetch: (req) => (
577
624
  <html>
578
- <h1>Welcome to mono-jsx!</h1>
579
-
580
- <Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
625
+ <Sleep ms={1000} placeholder={<p>Loading...</p>}>
581
626
  <p>After 1 second</p>
582
627
  </Sleep>
583
628
  </html>
@@ -588,16 +633,9 @@ export default {
588
633
  You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
589
634
 
590
635
  ```jsx
591
- async function Sleep({ ms }) {
592
- await new Promise((resolve) => setTimeout(resolve, ms));
593
- return <slot />;
594
- }
595
-
596
636
  export default {
597
637
  fetch: (req) => (
598
638
  <html>
599
- <h1>Welcome to mono-jsx!</h1>
600
-
601
639
  <Sleep ms={1000} rendering="eager">
602
640
  <p>After 1 second</p>
603
641
  </Sleep>
@@ -606,7 +644,7 @@ export default {
606
644
  };
607
645
  ```
608
646
 
609
- You can add the `catch` attribute to handle errors in async components. The `catch` attribute should be a function that returns a JSX element:
647
+ You can add the `catch` attribute to handle errors in the async component. The `catch` attribute should be a function that returns a JSX element:
610
648
 
611
649
  ```jsx
612
650
  async function Hello() {
@@ -617,7 +655,7 @@ async function Hello() {
617
655
  export default {
618
656
  fetch: (req) => (
619
657
  <html>
620
- <Hello catch={err => <p>{err.messaage}</p>} />
658
+ <Hello catch={err => <p>{err.message}</p>} />
621
659
  </html>
622
660
  ),
623
661
  };
@@ -625,7 +663,7 @@ export default {
625
663
 
626
664
  ## Customizing html Response
627
665
 
628
- Add `status` or `headers` attributes to the root `<html>` element to customize the response:
666
+ You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
629
667
 
630
668
  ```jsx
631
669
  export default {
@@ -635,6 +673,7 @@ export default {
635
673
  headers={{
636
674
  cacheControl: "public, max-age=0, must-revalidate",
637
675
  setCookie: "name=value",
676
+ "x-foo": "bar",
638
677
  }}
639
678
  >
640
679
  <h1>Page Not Found</h1>
package/jsx-runtime.mjs CHANGED
@@ -7,7 +7,7 @@ var $computed = Symbol.for("mono.computed");
7
7
 
8
8
  // runtime/index.ts
9
9
  var STATE_JS = `const m=new Map,u=e=>m.get(e)??m.set(e,b(e)).get(e),f=(e,t)=>e.getAttribute(t),d=(e,t)=>e.hasAttribute(t),b=e=>{const t=Object.create(null),r=new Map,n=(i,c)=>{let a=c;Object.defineProperty(t,i,{get:()=>a,set:o=>{if(o!==a){const l=r.get(i);l&&queueMicrotask(()=>l.forEach(h=>h())),a=o}}})},s=(i,c)=>{let a=r.get(i);a||(a=[],r.set(i,a)),a.push(c)};return e>0&&Object.defineProperty(t,"app",{get:()=>u(0).store,enumerable:!1,configurable:!1}),{store:t,define:n,watch:s}},g=(e,t,r)=>{if(t==="toggle"){let n;return()=>{if(!n){const s=e.firstElementChild;s&&s.tagName==="TEMPLATE"&&d(s,"m-slot")?(n=s.content.childNodes,e.innerHTML=""):n=e.childNodes}r()?e.append(...n):e.innerHTML=""}}if(t==="switch"){let n=f(e,"match"),s,i,c=o=>s.get(o)??s.set(o,[]).get(o),a;return()=>{if(!s){s=new Map,i=[];for(const o of e.childNodes)if(o.nodeType===1&&o.tagName==="TEMPLATE"&&d(o,"m-slot")){for(const l of o.content.childNodes)l.nodeType===1&&d(l,"slot")?c(f(l,"slot")).push(l):i.push(l);o.remove()}else n?c(n).push(o):i.push(o)}a=r(),e.innerHTML="",e.append(...s.has(a)?s.get(a):i)}}if(t&&t.length>2&&t.startsWith("[")&&t.endsWith("]")){let n=t.slice(1,-1),s=e.parentElement;return s.tagName==="M-GROUP"&&(s=s.previousElementSibling),()=>{const i=r();i===!1?s.removeAttribute(n):(n==="class"||n==="style")&&i&&typeof i=="object"?s.setAttribute(n,n==="class"?$cx(i):$styleToCSS(i)):s.setAttribute(n,i===!0?"":""+i)}}return()=>e.textContent=""+r()},p=e=>{const t=e.indexOf(":");if(t>0)return[Number(e.slice(0,t)),e.slice(t+1)];throw new Error("Invalid state key")};customElements.define("m-state",class extends HTMLElement{connectedCallback(){const e=this,t=f(e,"key");if(t){const r=u(Number(f(e,"fc")));r.watch(t,g(e,f(e,"mode"),()=>r.store[t]));return}}}),Object.assign(window,{$state:e=>e!==void 0?u(e).store:void 0,$defineState:(e,t)=>{const[r,n]=p(e);u(r).define(n,t)},$defineComputed:(e,t,r)=>{const n=document.querySelector("m-state[computed='"+e+"']");if(n){const s=u(Number(f(n,"fc"))).store,i=g(n,f(n,"mode"),t.bind(s));for(const c of r){const[a,o]=p(c);u(a).watch(o,i)}}}});`;
10
- var SUSPENSE_JS = `const n={},o=e=>e.getAttribute("chunk-id");c("m-portal",e=>{n[o(e)]=e}),c("m-chunk",e=>{setTimeout(()=>{const t=o(e),s=n[t];s&&(s.replaceWith(...e.firstChild.content.childNodes),e.remove(),delete n[t])})});function c(e,t){customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}})}`;
10
+ var SUSPENSE_JS = `const s=new Map,i=(e,t)=>e.hasAttribute(t),l=e=>e.getAttribute("chunk-id"),c=(e,t)=>customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}});c("m-portal",e=>{s.set(l(e),e)}),c("m-chunk",e=>{setTimeout(()=>{const t=e.firstChild?.content.childNodes,o=l(e),n=s.get(o);n&&(i(e,"next")?n.before(...t):(s.delete(o),i(e,"done")?n.remove():n.replaceWith(...t)),e.remove())})});`;
11
11
  var UTILS_JS = {
12
12
  /** cx.js (225 bytes) */
13
13
  cx: `var n=e=>typeof e=="string",o=e=>typeof e=="object"&&e!==null;function t(e){return n(e)?e:o(e)?Array.isArray(e)?e.map(t).filter(Boolean).join(" "):Object.entries(e).filter(([,r])=>!!r).map(([r])=>r).join(" "):""}window.$cx=t;`,
@@ -357,53 +357,80 @@ async function renderNode(rc, node, stripSlotProp) {
357
357
  break;
358
358
  }
359
359
  if (typeof tag === "function") {
360
+ const { children } = props;
360
361
  const fcIndex = ++rc.status.fcIndex;
361
- const { children, rendering, placeholder, catch: catchFC } = props;
362
362
  try {
363
- const v = tag.call(createThis(fcIndex, rc.appState, rc.context, rc.request), props);
364
363
  const fcSlots = children !== void 0 ? Array.isArray(children) ? isVNode(children) ? [children] : children : [children] : void 0;
365
- const eager = (rendering ?? tag.rendering) === "eager";
366
- if (v instanceof Promise) {
367
- if (eager) {
368
- await renderNode({ ...rc, fcIndex, fcSlots }, await v);
369
- } else {
370
- const chunkIdAttr = 'chunk-id="' + (rc.status.chunkIndex++).toString(36) + '"';
371
- rc.suspenses.push(v.then(async (node2) => {
372
- let buf = "<m-chunk " + chunkIdAttr + "><template>";
373
- await renderNode({
374
- ...rc,
375
- fcIndex,
376
- fcSlots,
377
- write: (chunk) => {
378
- buf += chunk;
379
- }
380
- }, node2);
381
- return buf + "</template></m-chunk>";
382
- }));
383
- write("<m-portal " + chunkIdAttr + ">");
384
- if (placeholder) {
385
- await renderNode({ ...rc, fcIndex }, placeholder);
364
+ const v = tag.call(createThis(fcIndex, rc.appState, rc.context, rc.request), props);
365
+ if (isObject(v) && !isVNode(v)) {
366
+ if (v instanceof Promise) {
367
+ if ((props.rendering ?? tag.rendering) === "eager") {
368
+ await renderNode({ ...rc, fcIndex, fcSlots }, await v);
369
+ } else {
370
+ const chunkIdAttr = 'chunk-id="' + (rc.status.chunkIndex++).toString(36) + '"';
371
+ write("<m-portal " + chunkIdAttr + ">");
372
+ if (props.placeholder) {
373
+ await renderNode({ ...rc, fcIndex }, props.placeholder);
374
+ }
375
+ write("</m-portal>");
376
+ rc.suspenses.push(v.then(async (node2) => {
377
+ let buf = "<m-chunk " + chunkIdAttr + "><template>";
378
+ await renderNode({
379
+ ...rc,
380
+ fcIndex,
381
+ fcSlots,
382
+ write: (chunk) => {
383
+ buf += chunk;
384
+ }
385
+ }, node2);
386
+ return buf + "</template></m-chunk>";
387
+ }));
386
388
  }
387
- write("</m-portal>");
388
- }
389
- } else if (isObject(v) && Symbol.asyncIterator in v) {
390
- if (eager) {
391
- for await (const c of v) {
392
- await renderNode({ ...rc, fcIndex, fcSlots }, c);
389
+ } else if (Symbol.asyncIterator in v) {
390
+ if ((props.rendering ?? tag.rendering) === "eager") {
391
+ for await (const c of v) {
392
+ await renderNode({ ...rc, fcIndex, fcSlots }, c);
393
+ }
394
+ } else {
395
+ const chunkIdAttr = 'chunk-id="' + (rc.status.chunkIndex++).toString(36) + '"';
396
+ write("<m-portal " + chunkIdAttr + ">");
397
+ if (props.placeholder) {
398
+ await renderNode({ ...rc, fcIndex }, props.placeholder);
399
+ }
400
+ write("</m-portal>");
401
+ const iter = () => rc.suspenses.push(
402
+ v.next().then(async ({ done, value }) => {
403
+ let buf = "<m-chunk " + chunkIdAttr;
404
+ if (done) {
405
+ return buf + " done></m-chunk>";
406
+ }
407
+ buf += " next><template>";
408
+ await renderNode({
409
+ ...rc,
410
+ fcIndex,
411
+ fcSlots,
412
+ write: (chunk) => {
413
+ buf += chunk;
414
+ }
415
+ }, value);
416
+ iter();
417
+ return buf + "</template></m-chunk>";
418
+ })
419
+ );
420
+ iter();
421
+ }
422
+ } else if (Symbol.iterator in v) {
423
+ for (const node2 of v) {
424
+ await renderNode({ ...rc, fcIndex, fcSlots }, node2);
393
425
  }
394
- } else {
395
- }
396
- } else if (isObject(v) && Symbol.iterator in v && !isVNode(v)) {
397
- for (const node2 of v) {
398
- await renderNode({ ...rc, fcIndex, fcSlots }, node2);
399
426
  }
400
- } else {
427
+ } else if (v || v === 0) {
401
428
  await renderNode({ ...rc, fcIndex, fcSlots }, v);
402
429
  }
403
430
  } catch (err) {
404
431
  if (err instanceof Error) {
405
- if (typeof catchFC === "function") {
406
- await renderNode({ ...rc, fcIndex }, catchFC(err));
432
+ if (props.catch) {
433
+ await renderNode({ ...rc, fcIndex }, props.catch(err));
407
434
  } else {
408
435
  write('<pre style="color:red;font-size:1rem"><code>' + escapeHTML(err.message) + "</code></pre>");
409
436
  console.error(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",