import express from "express"; import morgan from "morgan"; import { nanoid } from "nanoid"; import { Readable } from "stream"; const app = express(); const port = 3000; app.use(morgan("combined")); const h = (component, children = [], props = {}) => { return { component, children: Array.isArray(children) ? children : [children], props, }; }; const attribAlias = { className: "class" }; const getAlias = (key) => attribAlias[key] || key; const selfClosing = { br: true, input: true, }; const renderTag = (tag, props = {}) => { if (!tag) return ""; const attrib = Object.entries(props) .map(([k, v]) => `${getAlias(k)}="${v}"`) .join(" "); if (selfClosing[tag]) return `<${tag}${attrib ? ` ${attrib}` : ""} />`; return { start: `<${tag}${attrib ? ` ${attrib}` : ""}>`, end: `` }; }; const renderTree = (readable, item, promises) => { // console.log("renderTree item", item); if (item === undefined || item === null) { return; } else if (["string", "number", "boolean"].includes(typeof item)) { if (!readable) return item; readable.push(item); } else if (typeof item === "object") { // weird js - we can destructure item even if it is a string const { component, children, props } = item; if (typeof component === "string") { const result = renderTag(component, props); if (typeof result === "string") { if (!readable) return result; else readable.push(result); } if (!readable) { return `${result.start}${children.map((child) => renderTree(readable, child, promises) )}${result.end}`; } else { readable.push(result.start); children.forEach((child) => renderTree(readable, child, promises)); readable.push(result.end); } } else if (typeof component === "function") { const result = component({ ...props, children }); if (result instanceof Promise) { const id = `${component.name}-${nanoid()}`; promises.push({ result, id }); renderTree(readable, h("template", "", { id })); } else { renderTree(readable, result, promises); } } } }; const getReplacer = (id, rendered) => { return renderTree( undefined, h( "script", `document.querySelector("#${id}").insertAdjacentHTML("afterEnd", "${rendered}");document.querySelector("#${id}").remove()` ) ); }; const render = async (readable, tree) => { if (!tree) throw new Error("Pass a component to render"); if (!readable) throw new Error("Pass an readable to output to"); const promises = []; renderTree(readable, tree, promises); console.log("renderTree done", promises); const toWait = []; for (const promise of promises) { const wait = promise.result.then((value) => { const result = renderTree(undefined, value); const rendered = renderTree(undefined, result); readable.push(getReplacer(promise.id, rendered)); }); toWait.push(wait); } return Promise.all(toWait); }; function Button({ value }) { return h("button", value); } async function AwaitMe({ wait = 1 }) { await new Promise((resolve) => setTimeout(resolve, wait * 1000)); return h("p", `i came after ${wait}s`); } function App() { return h( "div", [ h("h1", "Hello world"), h(AwaitMe, undefined, { wait: 5 }), h(Button, null, { value: "Click Me" }), h(AwaitMe), h("br"), h("p", "hi"), ], { className: "bg-blue" } ); } app.get("/", async (req, res) => { const readable = new Readable({ read() { // lie, keep telling there is more data return true; }, }); readable.pipe(res); await render(readable, h(App)); // end the response readable.push(null); }); app.listen(port, () => { console.log(`Example app listening on port ${port}`); });