mono-jsx 0.3.3 → 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.
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=>{const t=o(e),s=n[t];s&&setTimeout(()=>{s.replaceWith(...e.firstChild.content.childNodes),delete n[t],e.remove()})});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,46 +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 { rendering, placeholder, catch: catchFC, ...fcProps } = props ?? /* @__PURE__ */ Object.create(null);
362
362
  try {
363
- const v = tag.call(createThis(fcIndex, rc.appState, rc.context, rc.request), fcProps);
364
- const { children } = fcProps;
365
363
  const fcSlots = children !== void 0 ? Array.isArray(children) ? isVNode(children) ? [children] : children : [children] : void 0;
366
- if (v instanceof Promise) {
367
- if ((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
- rc.suspenses.push(v.then(async (node2) => {
372
- write("<m-chunk " + chunkIdAttr + "><template>");
373
- await renderNode({ ...rc, fcIndex, fcSlots }, node2);
374
- write("</template></m-chunk>");
375
- }));
376
- write("<m-portal " + chunkIdAttr + ">");
377
- if (placeholder) {
378
- 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
+ }));
379
388
  }
380
- write("</m-portal>");
381
- }
382
- } else if (isObject(v) && Symbol.asyncIterator in v) {
383
- if ((rendering ?? tag.rendering) === "eager") {
384
- for await (const c of v) {
385
- 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);
386
425
  }
387
- } else {
388
- }
389
- } else if (isObject(v) && Symbol.iterator in v && !isVNode(v)) {
390
- for (const node2 of v) {
391
- await renderNode({ ...rc, fcIndex, fcSlots }, node2);
392
426
  }
393
- } else {
427
+ } else if (v || v === 0) {
394
428
  await renderNode({ ...rc, fcIndex, fcSlots }, v);
395
429
  }
396
430
  } catch (err) {
397
431
  if (err instanceof Error) {
398
- if (typeof catchFC === "function") {
399
- await renderNode({ ...rc, fcIndex }, catchFC(err));
432
+ if (props.catch) {
433
+ await renderNode({ ...rc, fcIndex }, props.catch(err));
400
434
  } else {
401
435
  write('<pre style="color:red;font-size:1rem"><code>' + escapeHTML(err.message) + "</code></pre>");
402
436
  console.error(err);
@@ -672,7 +706,7 @@ function render(node, renderOptions = {}) {
672
706
  write("<script>/* app.js (generated by mono-jsx) */" + js + "<\/script>");
673
707
  }
674
708
  if (suspenses.length > 0) {
675
- await Promise.all(suspenses.splice(0, suspenses.length));
709
+ await Promise.all(suspenses.splice(0, suspenses.length).map((suspense) => suspense.then(write)));
676
710
  await finalize();
677
711
  }
678
712
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
package/types/css.d.ts CHANGED
@@ -6107,7 +6107,8 @@ export interface StandardShorthandProperties<TLength = (string & {}) | 0, TTime
6107
6107
  }
6108
6108
 
6109
6109
  export interface StandardProperties<TLength = (string & {}) | 0, TTime = string & {}>
6110
- extends StandardLonghandProperties<TLength, TTime>, StandardShorthandProperties<TLength, TTime> {}
6110
+ extends StandardLonghandProperties<TLength, TTime>, StandardShorthandProperties<TLength, TTime>
6111
+ {}
6111
6112
 
6112
6113
  export interface VendorLonghandProperties<TLength = (string & {}) | 0, TTime = string & {}> {
6113
6114
  /**
@@ -8018,7 +8019,8 @@ export interface VendorShorthandProperties<TLength = (string & {}) | 0, TTime =
8018
8019
  }
8019
8020
 
8020
8021
  export interface VendorProperties<TLength = (string & {}) | 0, TTime = string & {}>
8021
- extends VendorLonghandProperties<TLength, TTime>, VendorShorthandProperties<TLength, TTime> {}
8022
+ extends VendorLonghandProperties<TLength, TTime>, VendorShorthandProperties<TLength, TTime>
8023
+ {}
8022
8024
 
8023
8025
  export interface ObsoleteProperties<TLength = (string & {}) | 0, TTime = string & {}> {
8024
8026
  /**
@@ -9143,7 +9145,8 @@ export interface Properties<TLength = (string & {}) | 0, TTime = string & {}>
9143
9145
  StandardProperties<TLength, TTime>,
9144
9146
  VendorProperties<TLength, TTime>,
9145
9147
  ObsoleteProperties<TLength, TTime>,
9146
- SvgProperties<TLength, TTime> {}
9148
+ SvgProperties<TLength, TTime>
9149
+ {}
9147
9150
 
9148
9151
  export type AtRules =
9149
9152
  | "@charset"
package/types/mono.d.ts CHANGED
@@ -66,7 +66,7 @@ export interface CSSProperties extends BaseCSSProperties, AtRuleCSSProperties, P
66
66
  [key: `&${" " | "." | "["}${string}`]: CSSProperties;
67
67
  }
68
68
 
69
- export type ChildType = JSX.Element | JSX.Element[] | string | number | bigint | boolean | null;
69
+ export type ChildType = JSX.Element | (JSX.Element | string | null)[] | string | number | bigint | boolean | null;
70
70
 
71
71
  export interface BaseAttributes {
72
72
  children?: ChildType | ChildType[];
@@ -76,16 +76,16 @@ export interface BaseAttributes {
76
76
 
77
77
  export interface AsyncComponentAttributes {
78
78
  /**
79
- * Error handler.
79
+ * Try to catch errors in the component.
80
80
  */
81
81
  catch?: (err: any) => JSX.Element;
82
82
  /**
83
- * Loading spinner.
83
+ * The loading spinner for the async component.
84
84
  */
85
- placeholder?: JSX.Element | string;
85
+ placeholder?: JSX.Element;
86
86
  /**
87
87
  * Rendering mode
88
- * - `eager`: render immediately
88
+ * - `eager`: render async component eagerly
89
89
  */
90
90
  rendering?: "eager";
91
91
  }