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.
- package/README.md +52 -13
- package/jsx-runtime.mjs +65 -38
- 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
|
-
<
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
366
|
-
if (v
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
...rc,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 (
|
|
406
|
-
await renderNode({ ...rc, fcIndex },
|
|
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);
|