aboutsummaryrefslogtreecommitdiff
path: root/priv
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-01-23 00:09:45 +0000
committerGitHub <noreply@github.com>2024-01-23 00:09:45 +0000
commit24f6962aa457d32319756f6217aafde7b0a9c752 (patch)
tree42119d9b073f56eabe9dda4ae2065ef4b2086e6a /priv
parent45e671ac32de95ae1a0a9f9e98da8645d01af3cf (diff)
downloadlustre-24f6962aa457d32319756f6217aafde7b0a9c752.tar.gz
lustre-24f6962aa457d32319756f6217aafde7b0a9c752.zip
✨ Add universal components that can run on the server (#39)
* :heavy_plus_sign: Add gleam_erlang gleam_otp and gleam_json dependencies. * :sparkles: Add json encoders for elememnts and attributes. * :sparkles: Add the ability to perform an effect with a custom dispatch function. * :construction: Experiment with a server-side component runtime. * :construction: Expose special server click events. * :construction: Experiment with a server-side component runtime. * :construction: Experiment with a server-side component runtime. * :construction: Experiment with a server-side component runtime. * :construction: Create a basic server component client bundle. * :construction: Create a basic server component demo. * :bug: Fixed a bug where the runtime stopped performing patches. * :refactor: Roll back introduction of shadow dom. * :recycle: Refactor to Custom Element-based approach to encapsulating server components. * :truck: Move some things around. * :sparkles: Add a minified version of the server component runtime. * :wrench: Add lustre/server/* to internal modules. * :recycle: on_attribute_change and on_client_event handlers are now functions not dicts. * :recycle: Refactor server component event handling to no longer need explicit tags. * :fire: Remove unnecessary attempt to stringify events. * :memo: Start documeint lustre/server functions. * :construction: Experiment with a js implementation of the server component backend runtime. * :recycle: Experiment with an API that makes heavier use of conditional complilation. * :recycle: Big refactor to unify server components, client components, and client apps. * :bug: Fixed some bugs with client runtimes. * :recycle: Update examples to new lustre api/ * :truck: Move server demo into examples/ folder/ * :wrench: Add lustre/runtime to internal modules. * :construction: Experiment with a diffing implementation. * :wrench: Hide internal modules from docs. * :heavy_plus_sign: Update deps to latest versions. * :recycle: Move diffing and vdom code into separate internal modules. * :sparkles: Bring server components to feature parity with client components. * :recycle: Update server component demo. * :bug: Fix bug where attribute changes weren't properly broadcast. * :fire: Remove unused 'Patch' type. * :recycle: Stub out empty js implementations so we can build for js. * :memo: Docs for the docs gods. * :recycle: Rename lustre.server_component to lustre.component.
Diffstat (limited to 'priv')
-rw-r--r--priv/lustre-server-component.min.mjs1
-rw-r--r--priv/lustre-server-component.mjs558
2 files changed, 559 insertions, 0 deletions
diff --git a/priv/lustre-server-component.min.mjs b/priv/lustre-server-component.min.mjs
new file mode 100644
index 0000000..bc05808
--- /dev/null
+++ b/priv/lustre-server-component.min.mjs
@@ -0,0 +1 @@
+var f=class{withFields(e){let n=Object.keys(this).map(l=>l in e?e[l]:this[l]);return new this.constructor(...n)}},w=class{static fromArray(e,n){let l=n||new p;return e.reduceRight((s,i)=>new $(i,s),l)}[Symbol.iterator](){return new k(this)}toArray(){return[...this]}atLeastLength(e){for(let n of this){if(e<=0)return!0;e--}return e<=0}hasLength(e){for(let n of this){if(e<=0)return!1;e--}return e===0}countLength(){let e=0;for(let n of this)e++;return e}};var k=class{#t;constructor(e){this.#t=e}next(){if(this.#t instanceof p)return{done:!0};{let{head:e,tail:n}=this.#t;return this.#t=n,{value:e,done:!1}}}},p=class extends w{},$=class extends w{constructor(e,n){super(),this.head=e,this.tail=n}};var x=class t extends f{static isResult(e){return e instanceof t}},h=class extends x{constructor(e){super(),this[0]=e}isOk(){return!0}},c=class extends x{constructor(e){super(),this[0]=e}isOk(){return!1}};function d(t,e,n,l,s,i){let r=new globalThis.Error(s);r.gleam_error=t,r.module=e,r.line=n,r.fn=l;for(let a in i)r[a]=i[a];return r}var Bt=new DataView(new ArrayBuffer(8));var Q=5,A=Math.pow(2,Q),Dt=A-1,Rt=A/2,Ut=A/4;function O(t,e){if(t.isOk()){let n=t[0];return new h(e(n))}else{if(t.isOk())throw d("case_no_match","gleam/result",67,"map","No case clause matched",{values:[t]});{let n=t[0];return new c(n)}}}function o(t,e,n,l){if(e?.tag&&t?.nodeType===1){let s=e.tag.toUpperCase(),i=e.namespace||"http://www.w3.org/1999/xhtml";return t.nodeName===s&&t.namespaceURI==i?te(t,e,n,l):D(t,e,n,l)}return e?.tag?D(t,e,n,l):typeof e?.content=="string"?t?.nodeType===3?ne(t,e):re(t,e):document.createComment(["[internal lustre error] I couldn't work out how to render this element. This","function should only be called internally by lustre's runtime: if you think","this is an error, please open an issue at","https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new"].join(" "))}function R(t,e,n){for(let l of e[0]){let s=l[0];if(s==="0")o(t,l[1],n,t.parentNode);else{let i=Array.from(s),r=i.slice(0,-1).join(""),a=i.slice(-1)[0],u=t.querySelector(`[data-lustre-key="${s}"]`)??t.querySelector(`[data-lustre-key="${r}"]`).childNodes[a];o(u,l[1],n,u.parentNode)}}for(let l of e[1]){let s=l[0],i=Array.from(s),r=i.slice(0,-1).join(""),a=i.slice(-1)[0];(t.querySelector(`[data-lustre-key="${s}"]`)??t.querySelector(`[data-lustre-key="${r}"]`).childNodes[a]).remove()}for(let l of e[2]){let s=l[0],i=s==="0"?t:t.querySelector(`[data-lustre-key="${s}"]`);i.$lustre??={__registered_events:new Set};for(let r of l[0])g(i,r.name,r.value,n);for(let r of l[1])if(i.$lustre.__registered_events.has(r)){let a=r.slice(2).toLowerCase();i.removeEventListener(a,i.$lustre[`${r}Handler`]),i.$lustre.__registered_events.delete(r),delete i.$lustre[r],delete i.$lustre[`${r}Handler`]}else i.removeAttribute(r)}return t}function D(t,e,n,l=null){let s=e.namespace?document.createElementNS(e.namespace,e.tag):document.createElement(e.tag);s.$lustre={__registered_events:new Set};let i="";for(let r of e.attrs)r[0]==="class"?g(s,r[0],`${s.className} ${r[1]}`):r[0]==="style"?g(s,r[0],`${s.style.cssText} ${r[1]}`):r[0]==="dangerous-unescaped-html"?i+=r[1]:r[0]!==""&&g(s,r[0],r[1],n);if(customElements.get(e.tag))s._slot=e.children;else if(e.tag==="slot"){let r=new p,a=l;for(;a;)if(a._slot){r=a._slot;break}else a=a.parentNode;for(let u of r)s.appendChild(o(null,u,n,s))}else if(i)s.innerHTML=i;else for(let r of e.children)s.appendChild(o(null,r,n,s));return t&&t.replaceWith(s),s}function te(t,e,n,l){let s=t.attributes,i=new Map;t.$lustre??={__registered_events:new Set};for(let r of e.attrs)r[0]==="class"&&i.has("class")?i.set(r[0],`${i.get("class")} ${r[1]}`):r[0]==="style"&&i.has("style")?i.set(r[0],`${i.get("style")} ${r[1]}`):r[0]==="dangerous-unescaped-html"&&i.has("dangerous-unescaped-html")?i.set(r[0],`${i.get("dangerous-unescaped-html")} ${r[1]}`):r[0]!==""&&i.set(r[0],r[1]);for(let{name:r,value:a}of s)if(!i.has(r))t.removeAttribute(r);else{let u=i.get(r);u!==a&&(g(t,r,u,n),i.delete(r))}for(let r of t.$lustre.__registered_events)if(!i.has(r)){let a=r.slice(2).toLowerCase();t.removeEventListener(a,t.$lustre[`${r}Handler`]),t.$lustre.__registered_events.delete(r),delete t.$lustre[r],delete t.$lustre[`${r}Handler`]}for(let[r,a]of i)g(t,r,a,n);if(customElements.get(e.tag))t._slot=e.children;else if(e.tag==="slot"){let r=t.firstChild,a=new p,u=l;for(;u;)if(u._slot){a=u._slot;break}else u=u.parentNode;for(;r;)Array.isArray(a)&&a.length?o(r,a.shift(),n,t):a.head&&(o(r,a.head,n,t),a=a.tail),r=r.nextSibling;for(let _ of a)t.appendChild(o(null,_,n,t))}else if(i.has("dangerous-unescaped-html"))t.innerHTML=i.get("dangerous-unescaped-html");else{let r=t.firstChild,a=e.children;for(;r;)if(Array.isArray(a)&&a.length){let u=r.nextSibling;o(r,a.shift(),n,t),r=u}else if(a.head){let u=r.nextSibling;o(r,a.head,n,t),a=a.tail,r=u}else{let u=r.nextSibling;r.remove(),r=u}for(let u of a)t.appendChild(o(null,u,n,t))}return t}function g(t,e,n,l){switch(typeof n){case(e.startsWith("data-lustre-on-")&&"string"):{if(!n){t.removeAttribute(e),t.removeEventListener(s,t.$lustre[`${e}Handler`]);break}if(t.hasAttribute(e))break;let s=e.slice(15).toLowerCase(),i=r=>l(se(r));t.$lustre[`${e}Handler`]&&t.removeEventListener(s,t.$lustre[`${e}Handler`]),t.addEventListener(s,i),t.$lustre[e]=n,t.$lustre[`${e}Handler`]=i,t.$lustre.__registered_events.add(e),t.setAttribute(e,n);break}case"string":t.getAttribute(e)!==n&&t.setAttribute(e,n),n===""&&t.removeAttribute(e),e==="value"&&t.value!==n&&(t.value=n);break;case(e.startsWith("on")&&"function"):{if(t.$lustre[e]===n)break;let s=e.slice(2).toLowerCase(),i=r=>O(n(r),l);t.$lustre[`${e}Handler`]&&t.removeEventListener(s,t.$lustre[`${e}Handler`]),t.addEventListener(s,i),t.$lustre[e]=n,t.$lustre[`${e}Handler`]=i,t.$lustre.__registered_events.add(e);break}default:t[e]=n}}function re(t,e){let n=document.createTextNode(e.content);return t&&t.replaceWith(n),n}function ne(t,e){let n=t.nodeValue,l=e.content;return l?(n!==l&&(t.nodeValue=l),t):(t?.remove(),null)}function se(t){let e=t.target,n=e.getAttribute(`data-lustre-on-${t.type}`),l=JSON.parse(e.getAttribute("data-lustre-data")||"{}"),s=JSON.parse(e.getAttribute("data-lustre-include")||"[]");switch(t.type){case"input":case"change":s.push("target.value");break}return{tag:n,data:s.reduce((i,r)=>{let a=r.split(".");for(let u=0,_=i,b=t;u<a.length;u++)u===a.length-1?_[a[u]]=b[a[u]]:(_[a[u]]??={},b=b[a[u]],_=_[a[u]]);return i},l)}}var S=class extends HTMLElement{static get observedAttributes(){return["route"]}#t=null;#r=null;#e=null;constructor(){super(),this.#t=new MutationObserver(e=>{let n=[];for(let l of e)if(l.type==="attributes"){let{attributeName:s,oldValue:i}=l,r=this.getAttribute(s);if(i!==r)try{n.push([s,JSON.parse(r)])}catch{n.push([s,r])}}n.length&&this.#e?.send(JSON.stringify([5,n]))})}connectedCallback(){this.#r=document.createElement("div"),this.appendChild(this.#r)}attributeChangedCallback(e,n,l){switch(e){case"route":if(!l)this.#e?.close(),this.#e=null;else if(n!==l){let s=this.getAttribute("id"),i=l+(s?`?id=${s}`:"");this.#e?.close(),this.#e=new WebSocket(`ws://${window.location.host}${i}`),this.#e.addEventListener("message",({data:r})=>{let[a,...u]=JSON.parse(r);switch(a){case 0:return this.diff(u);case 1:return this.emit(u);case 2:return this.init(u)}})}}}init([e,n]){let l=[];for(let s of e)s in this?l.push([s,this[s]]):this.hasAttribute(s)&&l.push([s,this.getAttribute(s)]),Object.defineProperty(this,s,{get(){return this[`_${s}`]??this.getAttribute(s)},set(i){let r=this[s];typeof i=="string"?this.setAttribute(s,i):this[`_${s}`]=i,r!==i&&this.#e?.send(JSON.stringify([5,[[s,i]]]))}});this.#t.observe(this,{attributeFilter:e,attributeOldValue:!0,attributes:!0,characterData:!1,characterDataOldValue:!1,childList:!1,subtree:!1}),this.morph(n),l.length&&this.#e?.send(JSON.stringify([5,l]))}morph(e){this.#r=o(this.#r,e,n=>{this.#e?.send(JSON.stringify([4,n.tag,n.data]))})}diff([e]){this.#r=R(this.#r,e,n=>{this.#e?.send(JSON.stringify([4,n.tag,n.data]))})}emit([e,n]){this.dispatchEvent(new CustomEvent(e,{detail:n}))}disconnectedCallback(){this.#e?.close()}};window.customElements.define("lustre-server-component",S);export{S as LustreServerComponent};
diff --git a/priv/lustre-server-component.mjs b/priv/lustre-server-component.mjs
new file mode 100644
index 0000000..8230074
--- /dev/null
+++ b/priv/lustre-server-component.mjs
@@ -0,0 +1,558 @@
+// build/dev/javascript/lustre/lustre/internals/constants.mjs
+var diff = 0;
+var emit = 1;
+var init = 2;
+var event = 4;
+var attrs = 5;
+
+// build/dev/javascript/prelude.mjs
+var CustomType = class {
+ withFields(fields) {
+ let properties = Object.keys(this).map(
+ (label) => label in fields ? fields[label] : this[label]
+ );
+ return new this.constructor(...properties);
+ }
+};
+var List = class {
+ static fromArray(array, tail) {
+ let t = tail || new Empty();
+ return array.reduceRight((xs, x) => new NonEmpty(x, xs), t);
+ }
+ [Symbol.iterator]() {
+ return new ListIterator(this);
+ }
+ toArray() {
+ return [...this];
+ }
+ atLeastLength(desired) {
+ for (let _ of this) {
+ if (desired <= 0)
+ return true;
+ desired--;
+ }
+ return desired <= 0;
+ }
+ hasLength(desired) {
+ for (let _ of this) {
+ if (desired <= 0)
+ return false;
+ desired--;
+ }
+ return desired === 0;
+ }
+ countLength() {
+ let length2 = 0;
+ for (let _ of this)
+ length2++;
+ return length2;
+ }
+};
+var ListIterator = class {
+ #current;
+ constructor(current) {
+ this.#current = current;
+ }
+ next() {
+ if (this.#current instanceof Empty) {
+ return { done: true };
+ } else {
+ let { head, tail } = this.#current;
+ this.#current = tail;
+ return { value: head, done: false };
+ }
+ }
+};
+var Empty = class extends List {
+};
+var NonEmpty = class extends List {
+ constructor(head, tail) {
+ super();
+ this.head = head;
+ this.tail = tail;
+ }
+};
+var Result = class _Result extends CustomType {
+ static isResult(data) {
+ return data instanceof _Result;
+ }
+};
+var Ok = class extends Result {
+ constructor(value) {
+ super();
+ this[0] = value;
+ }
+ isOk() {
+ return true;
+ }
+};
+var Error = class extends Result {
+ constructor(detail) {
+ super();
+ this[0] = detail;
+ }
+ isOk() {
+ return false;
+ }
+};
+function makeError(variant, module, line, fn, message, extra) {
+ let error = new globalThis.Error(message);
+ error.gleam_error = variant;
+ error.module = module;
+ error.line = line;
+ error.fn = fn;
+ for (let k in extra)
+ error[k] = extra[k];
+ return error;
+}
+
+// build/dev/javascript/gleam_stdlib/dict.mjs
+var tempDataView = new DataView(new ArrayBuffer(8));
+var SHIFT = 5;
+var BUCKET_SIZE = Math.pow(2, SHIFT);
+var MASK = BUCKET_SIZE - 1;
+var MAX_INDEX_NODE = BUCKET_SIZE / 2;
+var MIN_ARRAY_NODE = BUCKET_SIZE / 4;
+
+// build/dev/javascript/gleam_stdlib/gleam/result.mjs
+function map2(result, fun) {
+ if (result.isOk()) {
+ let x = result[0];
+ return new Ok(fun(x));
+ } else if (!result.isOk()) {
+ let e = result[0];
+ return new Error(e);
+ } else {
+ throw makeError(
+ "case_no_match",
+ "gleam/result",
+ 67,
+ "map",
+ "No case clause matched",
+ { values: [result] }
+ );
+ }
+}
+
+// build/dev/javascript/lustre/vdom.ffi.mjs
+function morph(prev, curr, dispatch, parent) {
+ if (curr?.tag && prev?.nodeType === 1) {
+ const nodeName = curr.tag.toUpperCase();
+ const ns = curr.namespace || "http://www.w3.org/1999/xhtml";
+ if (prev.nodeName === nodeName && prev.namespaceURI == ns) {
+ return morphElement(prev, curr, dispatch, parent);
+ } else {
+ return createElement(prev, curr, dispatch, parent);
+ }
+ }
+ if (curr?.tag) {
+ return createElement(prev, curr, dispatch, parent);
+ }
+ if (typeof curr?.content === "string") {
+ return prev?.nodeType === 3 ? morphText(prev, curr) : createText(prev, curr);
+ }
+ return document.createComment(
+ [
+ "[internal lustre error] I couldn't work out how to render this element. This",
+ "function should only be called internally by lustre's runtime: if you think",
+ "this is an error, please open an issue at",
+ "https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new"
+ ].join(" ")
+ );
+}
+function patch(root, diff2, dispatch) {
+ for (const created of diff2[0]) {
+ const key = created[0];
+ if (key === "0") {
+ morph(root, created[1], dispatch, root.parentNode);
+ } else {
+ const segments = Array.from(key);
+ const parentKey = segments.slice(0, -1).join("");
+ const indexKey = segments.slice(-1)[0];
+ const prev = root.querySelector(`[data-lustre-key="${key}"]`) ?? root.querySelector(`[data-lustre-key="${parentKey}"]`).childNodes[indexKey];
+ morph(prev, created[1], dispatch, prev.parentNode);
+ }
+ }
+ for (const removed of diff2[1]) {
+ const key = removed[0];
+ const segments = Array.from(key);
+ const parentKey = segments.slice(0, -1).join("");
+ const indexKey = segments.slice(-1)[0];
+ const prev = root.querySelector(`[data-lustre-key="${key}"]`) ?? root.querySelector(`[data-lustre-key="${parentKey}"]`).childNodes[indexKey];
+ prev.remove();
+ }
+ for (const updated of diff2[2]) {
+ const key = updated[0];
+ const prev = key === "0" ? root : root.querySelector(`[data-lustre-key="${key}"]`);
+ prev.$lustre ??= { __registered_events: /* @__PURE__ */ new Set() };
+ for (const created of updated[0]) {
+ morphAttr(prev, created.name, created.value, dispatch);
+ }
+ for (const removed of updated[1]) {
+ if (prev.$lustre.__registered_events.has(removed)) {
+ const event2 = removed.slice(2).toLowerCase();
+ prev.removeEventListener(event2, prev.$lustre[`${removed}Handler`]);
+ prev.$lustre.__registered_events.delete(removed);
+ delete prev.$lustre[removed];
+ delete prev.$lustre[`${removed}Handler`];
+ } else {
+ prev.removeAttribute(removed);
+ }
+ }
+ }
+ return root;
+}
+function createElement(prev, curr, dispatch, parent = null) {
+ const el = curr.namespace ? document.createElementNS(curr.namespace, curr.tag) : document.createElement(curr.tag);
+ el.$lustre = {
+ __registered_events: /* @__PURE__ */ new Set()
+ };
+ let dangerousUnescapedHtml = "";
+ for (const attr of curr.attrs) {
+ if (attr[0] === "class") {
+ morphAttr(el, attr[0], `${el.className} ${attr[1]}`);
+ } else if (attr[0] === "style") {
+ morphAttr(el, attr[0], `${el.style.cssText} ${attr[1]}`);
+ } else if (attr[0] === "dangerous-unescaped-html") {
+ dangerousUnescapedHtml += attr[1];
+ } else if (attr[0] !== "") {
+ morphAttr(el, attr[0], attr[1], dispatch);
+ }
+ }
+ if (customElements.get(curr.tag)) {
+ el._slot = curr.children;
+ } else if (curr.tag === "slot") {
+ let children = new Empty();
+ let parentWithSlot = parent;
+ while (parentWithSlot) {
+ if (parentWithSlot._slot) {
+ children = parentWithSlot._slot;
+ break;
+ } else {
+ parentWithSlot = parentWithSlot.parentNode;
+ }
+ }
+ for (const child of children) {
+ el.appendChild(morph(null, child, dispatch, el));
+ }
+ } else if (dangerousUnescapedHtml) {
+ el.innerHTML = dangerousUnescapedHtml;
+ } else {
+ for (const child of curr.children) {
+ el.appendChild(morph(null, child, dispatch, el));
+ }
+ }
+ if (prev)
+ prev.replaceWith(el);
+ return el;
+}
+function morphElement(prev, curr, dispatch, parent) {
+ const prevAttrs = prev.attributes;
+ const currAttrs = /* @__PURE__ */ new Map();
+ prev.$lustre ??= { __registered_events: /* @__PURE__ */ new Set() };
+ for (const currAttr of curr.attrs) {
+ if (currAttr[0] === "class" && currAttrs.has("class")) {
+ currAttrs.set(currAttr[0], `${currAttrs.get("class")} ${currAttr[1]}`);
+ } else if (currAttr[0] === "style" && currAttrs.has("style")) {
+ currAttrs.set(currAttr[0], `${currAttrs.get("style")} ${currAttr[1]}`);
+ } else if (currAttr[0] === "dangerous-unescaped-html" && currAttrs.has("dangerous-unescaped-html")) {
+ currAttrs.set(
+ currAttr[0],
+ `${currAttrs.get("dangerous-unescaped-html")} ${currAttr[1]}`
+ );
+ } else if (currAttr[0] !== "") {
+ currAttrs.set(currAttr[0], currAttr[1]);
+ }
+ }
+ for (const { name, value: prevValue } of prevAttrs) {
+ if (!currAttrs.has(name)) {
+ prev.removeAttribute(name);
+ } else {
+ const value = currAttrs.get(name);
+ if (value !== prevValue) {
+ morphAttr(prev, name, value, dispatch);
+ currAttrs.delete(name);
+ }
+ }
+ }
+ for (const name of prev.$lustre.__registered_events) {
+ if (!currAttrs.has(name)) {
+ const event2 = name.slice(2).toLowerCase();
+ prev.removeEventListener(event2, prev.$lustre[`${name}Handler`]);
+ prev.$lustre.__registered_events.delete(name);
+ delete prev.$lustre[name];
+ delete prev.$lustre[`${name}Handler`];
+ }
+ }
+ for (const [name, value] of currAttrs) {
+ morphAttr(prev, name, value, dispatch);
+ }
+ if (customElements.get(curr.tag)) {
+ prev._slot = curr.children;
+ } else if (curr.tag === "slot") {
+ let prevChild = prev.firstChild;
+ let currChild = new Empty();
+ let parentWithSlot = parent;
+ while (parentWithSlot) {
+ if (parentWithSlot._slot) {
+ currChild = parentWithSlot._slot;
+ break;
+ } else {
+ parentWithSlot = parentWithSlot.parentNode;
+ }
+ }
+ while (prevChild) {
+ if (Array.isArray(currChild) && currChild.length) {
+ morph(prevChild, currChild.shift(), dispatch, prev);
+ } else if (currChild.head) {
+ morph(prevChild, currChild.head, dispatch, prev);
+ currChild = currChild.tail;
+ }
+ prevChild = prevChild.nextSibling;
+ }
+ for (const child of currChild) {
+ prev.appendChild(morph(null, child, dispatch, prev));
+ }
+ } else if (currAttrs.has("dangerous-unescaped-html")) {
+ prev.innerHTML = currAttrs.get("dangerous-unescaped-html");
+ } else {
+ let prevChild = prev.firstChild;
+ let currChild = curr.children;
+ while (prevChild) {
+ if (Array.isArray(currChild) && currChild.length) {
+ const next = prevChild.nextSibling;
+ morph(prevChild, currChild.shift(), dispatch, prev);
+ prevChild = next;
+ } else if (currChild.head) {
+ const next = prevChild.nextSibling;
+ morph(prevChild, currChild.head, dispatch, prev);
+ currChild = currChild.tail;
+ prevChild = next;
+ } else {
+ const next = prevChild.nextSibling;
+ prevChild.remove();
+ prevChild = next;
+ }
+ }
+ for (const child of currChild) {
+ prev.appendChild(morph(null, child, dispatch, prev));
+ }
+ }
+ return prev;
+}
+function morphAttr(el, name, value, dispatch) {
+ switch (typeof value) {
+ case (name.startsWith("data-lustre-on-") && "string"): {
+ if (!value) {
+ el.removeAttribute(name);
+ el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
+ break;
+ }
+ if (el.hasAttribute(name))
+ break;
+ const event2 = name.slice(15).toLowerCase();
+ const handler = (e) => dispatch(serverEventHandler(e));
+ if (el.$lustre[`${name}Handler`]) {
+ el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
+ }
+ el.addEventListener(event2, handler);
+ el.$lustre[name] = value;
+ el.$lustre[`${name}Handler`] = handler;
+ el.$lustre.__registered_events.add(name);
+ el.setAttribute(name, value);
+ break;
+ }
+ case "string":
+ if (el.getAttribute(name) !== value)
+ el.setAttribute(name, value);
+ if (value === "")
+ el.removeAttribute(name);
+ if (name === "value" && el.value !== value)
+ el.value = value;
+ break;
+ case (name.startsWith("on") && "function"): {
+ if (el.$lustre[name] === value)
+ break;
+ const event2 = name.slice(2).toLowerCase();
+ const handler = (e) => map2(value(e), dispatch);
+ if (el.$lustre[`${name}Handler`]) {
+ el.removeEventListener(event2, el.$lustre[`${name}Handler`]);
+ }
+ el.addEventListener(event2, handler);
+ el.$lustre[name] = value;
+ el.$lustre[`${name}Handler`] = handler;
+ el.$lustre.__registered_events.add(name);
+ break;
+ }
+ default:
+ el[name] = value;
+ }
+}
+function createText(prev, curr) {
+ const el = document.createTextNode(curr.content);
+ if (prev)
+ prev.replaceWith(el);
+ return el;
+}
+function morphText(prev, curr) {
+ const prevValue = prev.nodeValue;
+ const currValue = curr.content;
+ if (!currValue) {
+ prev?.remove();
+ return null;
+ }
+ if (prevValue !== currValue)
+ prev.nodeValue = currValue;
+ return prev;
+}
+function serverEventHandler(event2) {
+ const el = event2.target;
+ const tag = el.getAttribute(`data-lustre-on-${event2.type}`);
+ const data = JSON.parse(el.getAttribute("data-lustre-data") || "{}");
+ const include = JSON.parse(el.getAttribute("data-lustre-include") || "[]");
+ switch (event2.type) {
+ case "input":
+ case "change":
+ include.push("target.value");
+ break;
+ }
+ return {
+ tag,
+ data: include.reduce((data2, property) => {
+ const path = property.split(".");
+ for (let i = 0, o = data2, e = event2; i < path.length; i++) {
+ if (i === path.length - 1) {
+ o[path[i]] = e[path[i]];
+ } else {
+ o[path[i]] ??= {};
+ e = e[path[i]];
+ o = o[path[i]];
+ }
+ }
+ return data2;
+ }, data)
+ };
+}
+
+// src/server-component.mjs
+var LustreServerComponent = class extends HTMLElement {
+ static get observedAttributes() {
+ return ["route"];
+ }
+ #observer = null;
+ #root = null;
+ #socket = null;
+ constructor() {
+ super();
+ this.#observer = new MutationObserver((mutations) => {
+ const changed = [];
+ for (const mutation of mutations) {
+ if (mutation.type === "attributes") {
+ const { attributeName: name, oldValue: prev } = mutation;
+ const next = this.getAttribute(name);
+ if (prev !== next) {
+ try {
+ changed.push([name, JSON.parse(next)]);
+ } catch {
+ changed.push([name, next]);
+ }
+ }
+ }
+ }
+ if (changed.length) {
+ this.#socket?.send(JSON.stringify([attrs, changed]));
+ }
+ });
+ }
+ connectedCallback() {
+ this.#root = document.createElement("div");
+ this.appendChild(this.#root);
+ }
+ attributeChangedCallback(name, prev, next) {
+ switch (name) {
+ case "route": {
+ if (!next) {
+ this.#socket?.close();
+ this.#socket = null;
+ } else if (prev !== next) {
+ const id = this.getAttribute("id");
+ const route = next + (id ? `?id=${id}` : "");
+ this.#socket?.close();
+ this.#socket = new WebSocket(`ws://${window.location.host}${route}`);
+ this.#socket.addEventListener("message", ({ data }) => {
+ const [kind, ...payload] = JSON.parse(data);
+ switch (kind) {
+ case diff:
+ return this.diff(payload);
+ case emit:
+ return this.emit(payload);
+ case init:
+ return this.init(payload);
+ }
+ });
+ }
+ }
+ }
+ }
+ init([attrs2, vdom]) {
+ const initial = [];
+ for (const attr of attrs2) {
+ if (attr in this) {
+ initial.push([attr, this[attr]]);
+ } else if (this.hasAttribute(attr)) {
+ initial.push([attr, this.getAttribute(attr)]);
+ }
+ Object.defineProperty(this, attr, {
+ get() {
+ return this[`_${attr}`] ?? this.getAttribute(attr);
+ },
+ set(value) {
+ const prev = this[attr];
+ if (typeof value === "string") {
+ this.setAttribute(attr, value);
+ } else {
+ this[`_${attr}`] = value;
+ }
+ if (prev !== value) {
+ this.#socket?.send(
+ JSON.stringify([attrs, [[attr, value]]])
+ );
+ }
+ }
+ });
+ }
+ this.#observer.observe(this, {
+ attributeFilter: attrs2,
+ attributeOldValue: true,
+ attributes: true,
+ characterData: false,
+ characterDataOldValue: false,
+ childList: false,
+ subtree: false
+ });
+ this.morph(vdom);
+ if (initial.length) {
+ this.#socket?.send(JSON.stringify([attrs, initial]));
+ }
+ }
+ morph(vdom) {
+ this.#root = morph(this.#root, vdom, (msg) => {
+ this.#socket?.send(JSON.stringify([event, msg.tag, msg.data]));
+ });
+ }
+ diff([diff2]) {
+ this.#root = patch(this.#root, diff2, (msg) => {
+ this.#socket?.send(JSON.stringify([event, msg.tag, msg.data]));
+ });
+ }
+ emit([event2, data]) {
+ this.dispatchEvent(new CustomEvent(event2, { detail: data }));
+ }
+ disconnectedCallback() {
+ this.#socket?.close();
+ }
+};
+window.customElements.define("lustre-server-component", LustreServerComponent);
+export {
+ LustreServerComponent
+};