lego-dom 1.5.1 → 2.0.1
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/CHANGELOG.md +32 -0
- package/main.js +30 -27
- package/main.min.js +8 -2
- package/package.json +1 -1
- package/parse-lego.js +17 -17
- package/vite-plugin.js +16 -16
- package/parse-lego.test.js +0 -107
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# Changelog
|
|
4
|
+
|
|
5
|
+
## [2.0.1] - 2026-01-19
|
|
6
|
+
|
|
7
|
+
### The "Blocks" Update Refactor 🧱
|
|
8
|
+
|
|
9
|
+
Welcome to **LegoDOM v2**! This release represents a massive architectural shift to align the codebase and mental model with the "Lego" metaphor. We have moved away from Vue-centric terminology ("SFC", "Component", "Data") to Lego-centric terminology ("Lego File", "Block", "Logic").
|
|
10
|
+
|
|
11
|
+
### Breaking Changes 🚨
|
|
12
|
+
|
|
13
|
+
- **Terminological Refactor (Runtime & Docs):**
|
|
14
|
+
- **Components → Blocks:** The fundamental unit of UI is now a "Block".
|
|
15
|
+
- **SFC → Lego File:** Protocol-agnostic Single File Blocks.
|
|
16
|
+
|
|
17
|
+
- **API Renames:**
|
|
18
|
+
- `Lego.define()` is now **`Lego.block()`**. (Legacy alias maintained)
|
|
19
|
+
- `Lego.defineSFC()` is now **`Lego.defineLegoFile()`**.
|
|
20
|
+
- Internal: `deriveComponentName` is now `deriveBlockName`.
|
|
21
|
+
|
|
22
|
+
- **Attribute Renames:**
|
|
23
|
+
- `b-data` is now **`b-logic`**. (Legacy alias maintained)
|
|
24
|
+
- `b-styles` is now **`b-stylesheets`**.
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- **Public State API:**
|
|
29
|
+
- `element.state` is now the official public API for accessing the reactive proxy.
|
|
30
|
+
- `element._studs` is considered internal/private.
|
|
31
|
+
|
|
32
|
+
- **Lego Studio Audit:**
|
|
33
|
+
- `lego-studio` has been refactored to use "Block" terminology throughout the UI and codebase.
|
|
34
|
+
|
|
3
35
|
## [1.5.1] - 2026-01-19
|
|
4
36
|
|
|
5
37
|
### Breaking Changes 🚨
|
package/main.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const Lego = (() => {
|
|
2
2
|
const registry = {}, proxyCache = new WeakMap(), privateData = new WeakMap();
|
|
3
3
|
const forPools = new WeakMap();
|
|
4
|
-
const
|
|
4
|
+
const activeBlocks = new Set();
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const legoFileLogic = new Map();
|
|
7
7
|
const sharedStates = new Map();
|
|
8
8
|
const expressionCache = new Map(); // Cache for compiled expressions
|
|
9
9
|
|
|
@@ -39,7 +39,7 @@ const Lego = (() => {
|
|
|
39
39
|
}[m]));
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const deriveBlockName = (filename) => {
|
|
43
43
|
const basename = filename.split('/').pop().replace(/\.lego$/, '');
|
|
44
44
|
// 1. Convert snake_case to kebab-case
|
|
45
45
|
// 2. Convert PascalCase/camelCase to kebab-case
|
|
@@ -49,7 +49,7 @@ const Lego = (() => {
|
|
|
49
49
|
.toLowerCase();
|
|
50
50
|
|
|
51
51
|
if (!name.includes('-')) {
|
|
52
|
-
throw new Error(`[Lego] Invalid
|
|
52
|
+
throw new Error(`[Lego] Invalid block definition: "${filename}". Block names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);
|
|
53
53
|
}
|
|
54
54
|
return name;
|
|
55
55
|
};
|
|
@@ -68,7 +68,7 @@ const Lego = (() => {
|
|
|
68
68
|
const el = document.getElementById(query.slice(1));
|
|
69
69
|
return el ? [el] : [];
|
|
70
70
|
}
|
|
71
|
-
// Scoped search first (within the calling
|
|
71
|
+
// Scoped search first (within the calling block), then global fallback
|
|
72
72
|
const scoped = contextEl?.querySelectorAll(query) || [];
|
|
73
73
|
return scoped.length > 0 ? [...scoped] : [...document.querySelectorAll(query)];
|
|
74
74
|
};
|
|
@@ -274,8 +274,8 @@ const Lego = (() => {
|
|
|
274
274
|
}
|
|
275
275
|
};
|
|
276
276
|
|
|
277
|
-
const bind = (container,
|
|
278
|
-
const state =
|
|
277
|
+
const bind = (container, blockRoot, loopCtx = null) => {
|
|
278
|
+
const state = blockRoot._studs;
|
|
279
279
|
|
|
280
280
|
const bindNode = (child) => {
|
|
281
281
|
const childData = getPrivateData(child);
|
|
@@ -291,7 +291,7 @@ const Lego = (() => {
|
|
|
291
291
|
try {
|
|
292
292
|
let evalScope = state;
|
|
293
293
|
if (loopCtx) {
|
|
294
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self:
|
|
294
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
|
|
295
295
|
const item = list[loopCtx.index];
|
|
296
296
|
evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
|
|
297
297
|
}
|
|
@@ -309,7 +309,7 @@ const Lego = (() => {
|
|
|
309
309
|
try {
|
|
310
310
|
let target, last;
|
|
311
311
|
if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
|
|
312
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self:
|
|
312
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
|
|
313
313
|
const item = list[loopCtx.index];
|
|
314
314
|
if (!item) return;
|
|
315
315
|
const subPath = prop.split('.').slice(1);
|
|
@@ -357,7 +357,7 @@ const Lego = (() => {
|
|
|
357
357
|
let curr = n.parentNode;
|
|
358
358
|
while (curr && curr !== container) {
|
|
359
359
|
if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
|
|
360
|
-
// Only stop at Shadow Roots or explicit boundaries, NOT
|
|
360
|
+
// Only stop at Shadow Roots or explicit boundaries, NOT block tags in Light DOM
|
|
361
361
|
// The parent MUST be able to bind data to the slots of its children.
|
|
362
362
|
curr = curr.parentNode;
|
|
363
363
|
}
|
|
@@ -462,7 +462,7 @@ const Lego = (() => {
|
|
|
462
462
|
if (config.metrics && config.metrics.onRenderStart) config.metrics.onRenderStart(el);
|
|
463
463
|
|
|
464
464
|
try {
|
|
465
|
-
// Use shadowRoot if it's a
|
|
465
|
+
// Use shadowRoot if it's a block, otherwise render the element itself (light DOM)
|
|
466
466
|
const target = el.shadowRoot || el;
|
|
467
467
|
if (!data.bindings) data.bindings = scanForBindings(target);
|
|
468
468
|
|
|
@@ -525,9 +525,9 @@ const Lego = (() => {
|
|
|
525
525
|
}
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
-
// Global Broadcast: Only notify
|
|
528
|
+
// Global Broadcast: Only notify blocks that depend on globals
|
|
529
529
|
if (state === Lego.globals) {
|
|
530
|
-
|
|
530
|
+
activeBlocks.forEach(comp => {
|
|
531
531
|
if (getPrivateData(comp).hasGlobalDependency) render(comp);
|
|
532
532
|
});
|
|
533
533
|
}
|
|
@@ -558,12 +558,12 @@ const Lego = (() => {
|
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
// TIER 1: Logic from Lego.
|
|
561
|
+
// TIER 1: Logic from Lego.block (Lego File)
|
|
562
562
|
// TIER 2: Logic from the <template b-data="..."> attribute
|
|
563
563
|
// TIER 3: Logic from the <my-comp b-data="..."> tag
|
|
564
|
-
const scriptLogic =
|
|
565
|
-
const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
|
|
566
|
-
const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
|
|
564
|
+
const scriptLogic = legoFileLogic.get(name) || {};
|
|
565
|
+
const templateLogic = parseJSObject(templateNode.getAttribute('b-logic') || templateNode.getAttribute('b-data') || '{}');
|
|
566
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-logic') || el.getAttribute('b-data') || '{}');
|
|
567
567
|
|
|
568
568
|
el._studs = reactive({
|
|
569
569
|
...scriptLogic,
|
|
@@ -593,7 +593,7 @@ const Lego = (() => {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
bind(shadow, el);
|
|
596
|
-
|
|
596
|
+
activeBlocks.add(el);
|
|
597
597
|
render(el);
|
|
598
598
|
|
|
599
599
|
[...shadow.children].forEach(snap);
|
|
@@ -619,7 +619,7 @@ const Lego = (() => {
|
|
|
619
619
|
[...el.shadowRoot.children].forEach(unsnap);
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
-
|
|
622
|
+
activeBlocks.delete(el);
|
|
623
623
|
[...el.children].forEach(unsnap);
|
|
624
624
|
};
|
|
625
625
|
|
|
@@ -701,7 +701,7 @@ const Lego = (() => {
|
|
|
701
701
|
snap(n);
|
|
702
702
|
// Auto-Discovery: Check if tag is unknown and loader is configured
|
|
703
703
|
const tagName = n.tagName.toLowerCase();
|
|
704
|
-
if (tagName.includes('-') && !registry[tagName] && config.loader && !
|
|
704
|
+
if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
|
|
705
705
|
const result = config.loader(tagName);
|
|
706
706
|
if (result) {
|
|
707
707
|
// Handle Promise (user does custom fetch) vs String (we fetch)
|
|
@@ -710,7 +710,7 @@ const Lego = (() => {
|
|
|
710
710
|
: result;
|
|
711
711
|
|
|
712
712
|
Promise.resolve(promise)
|
|
713
|
-
.then(
|
|
713
|
+
.then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
|
|
714
714
|
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
|
|
715
715
|
}
|
|
716
716
|
}
|
|
@@ -774,7 +774,7 @@ const Lego = (() => {
|
|
|
774
774
|
},
|
|
775
775
|
$go: (path, ...targets) => _go(path, ...targets)(document.body)
|
|
776
776
|
}, document.body),
|
|
777
|
-
|
|
777
|
+
defineLegoFile: (content, filename = 'block.lego') => {
|
|
778
778
|
let template = '';
|
|
779
779
|
let script = '{}';
|
|
780
780
|
let stylesAttr = '';
|
|
@@ -820,7 +820,7 @@ const Lego = (() => {
|
|
|
820
820
|
remaining = remaining.slice(contentEnd + closeTag.length);
|
|
821
821
|
}
|
|
822
822
|
|
|
823
|
-
const name =
|
|
823
|
+
const name = deriveBlockName(filename);
|
|
824
824
|
// We must eval the script to get the object.
|
|
825
825
|
// Safe-ish because it's coming from the "Server" (trusted source in this architecture)
|
|
826
826
|
const logicObj = new Function(`return ${script}`)();
|
|
@@ -832,18 +832,18 @@ const Lego = (() => {
|
|
|
832
832
|
registry[name] = document.createElement('template');
|
|
833
833
|
registry[name].innerHTML = template;
|
|
834
834
|
registry[name].setAttribute('b-stylesheets', stylesAttr);
|
|
835
|
-
|
|
835
|
+
legoFileLogic.set(name, logicObj);
|
|
836
836
|
|
|
837
837
|
// Upgrade existing elements
|
|
838
838
|
document.querySelectorAll(name).forEach(el => !getPrivateData(el).snapped && snap(el));
|
|
839
839
|
},
|
|
840
|
-
|
|
840
|
+
block: (tagName, templateHTML, logic = {}, styles = "") => {
|
|
841
841
|
const t = document.createElement('template');
|
|
842
842
|
t.setAttribute('b-id', tagName);
|
|
843
843
|
t.setAttribute('b-stylesheets', styles);
|
|
844
844
|
t.innerHTML = templateHTML;
|
|
845
845
|
registry[tagName] = t;
|
|
846
|
-
|
|
846
|
+
legoFileLogic.set(tagName, logic);
|
|
847
847
|
|
|
848
848
|
// Initialize shared state with try-catch safety
|
|
849
849
|
try {
|
|
@@ -854,8 +854,11 @@ const Lego = (() => {
|
|
|
854
854
|
|
|
855
855
|
document.querySelectorAll(tagName).forEach(snap);
|
|
856
856
|
},
|
|
857
|
+
// Alias for backward compatibility
|
|
858
|
+
get define() { return this.block },
|
|
859
|
+
|
|
857
860
|
// For specific test validation
|
|
858
|
-
|
|
861
|
+
getActiveBlocksCount: () => activeBlocks.size,
|
|
859
862
|
getLegos: () => Object.keys(registry),
|
|
860
863
|
config, // Expose config for customization
|
|
861
864
|
route: (path, tagName, middleware = null) => {
|
package/main.min.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
const Lego
|
|
1
|
+
const Lego = (() => {
|
|
2
|
+
const b = {}, M = new WeakMap, O = new WeakMap, P = new WeakMap, v = new Set, D = new Map, B = new Map, H = new Map, j = new Map; let q = {}; const p = { onError: (e, s, c) => { console.error(`[Lego Error] [${s}]`, e, c) }, metrics: {}, syntax: "brackets" }, $ = () => p.syntax === "brackets" ? ["[[", "]]"] : ["{{", "}}"], L = () => { const [e, s] = $(), c = e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), o = s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`${c}(.*?)${o}`, "g") }, R = [], ee = e => typeof e != "string" ? e : e.replace(/[&<>"']/g, s => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[s]), K = e => { const c = e.split("/").pop().replace(/\.lego$/, "").replace(/_/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); if (!c.includes("-")) throw new Error(`[Lego] Invalid component definition: "${e}". Component names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`); return c }, V = (e, s) => { if (typeof e == "function") { const o = Array.from(document.querySelectorAll("*")).filter(t => t.tagName.includes("-")); return [].concat(e(o)) } if (e.startsWith("#")) { const o = document.getElementById(e.slice(1)); return o ? [o] : [] } const c = s?.querySelectorAll(e) || []; return c.length > 0 ? [...c] : [...document.querySelectorAll(e)] }, G = (e, ...s) => c => { const o = async (t, a = null, r = !0, n = {}) => { if (r) { const d = { legoTargets: s.filter(i => typeof i == "string"), method: t, body: a }; history.pushState(d, "", e) } await W(s.length ? s : null, c) }; return { get: (t = !0, a = {}) => o("GET", null, t, a), post: (t, a = !0, r = {}) => o("POST", t, a, r), put: (t, a = !0, r = {}) => o("PUT", t, a, r), patch: (t, a = !0, r = {}) => o("PATCH", t, a, r), delete: (t = !0, a = {}) => o("DELETE", null, t, a) } }, J = (() => { let e = !1; const s = new Set; let c = !1, o = null; const t = new Set, a = () => { o && clearTimeout(o), o = setTimeout(() => { t.forEach(r => { const n = r._studs; if (n && typeof n.updated == "function") try { n.updated.call(n) } catch (l) { console.error("[Lego] Error in updated hook:", l) } }), t.clear(), o = null }, 50) }; return { add: r => { !r || c || (s.add(r), !e && (e = !0, requestAnimationFrame(() => { c = !0; const n = Array.from(s); s.clear(), e = !1, n.forEach(l => x(l)), n.forEach(l => t.add(l)), a(), c = !1 }))) } } })(), S = (e, s, c = J) => { if (e === null || typeof e != "object" || e instanceof Node) return e; if (M.has(e)) return M.get(e); const o = { get: (a, r) => { const n = Reflect.get(a, r); return n !== null && typeof n == "object" && !(n instanceof Node) ? S(n, s, c) : n }, set: (a, r, n) => { const l = a[r], d = Reflect.set(a, r, n); return l !== n && c.add(s), d }, deleteProperty: (a, r) => { const n = Reflect.deleteProperty(a, r); return c.add(s), n } }, t = new Proxy(e, o); return M.set(e, t), t }, U = e => { try { return new Function(`return (${e})`)() } catch (s) { return console.error("[Lego] Failed to parse b-data:", e, s), {} } }, N = e => (O.has(e) || O.set(e, { snapped: !1, bindings: null, bound: !1, rendering: !1, anchor: null, hasGlobalDependency: !1 }), O.get(e)), F = (e, s) => { if (!e) return ""; const c = e.trim().split("."); let o = s; for (const t of c) { if (o == null) return ""; o = o[t] } return o ?? "" }, Z = (e, s) => { let c = e.parentElement || e.getRootNode().host; for (; c;) { if (c.tagName && c.tagName.toLowerCase() === s.toLowerCase()) return c; c = c.parentElement || c.getRootNode && c.getRootNode().host } }, h = (e, s, c = !1) => {
|
|
3
|
+
if (/\b(function|eval|import|class|module|deploy|constructor|__proto__)\b/.test(e)) { console.warn(`[Lego] Security Warning: Blocked dangerous expression "${e}"`); return } try {
|
|
4
|
+
const o = s.state || {}; let t = H.get(e); t || (t = new Function("global", "self", "event", "helpers", `
|
|
2
5
|
with(helpers) {
|
|
3
6
|
with(this) {
|
|
4
7
|
return ${e}
|
|
5
8
|
}
|
|
6
9
|
}
|
|
7
|
-
`),H.set(e,t));const a={$ancestors:n=>Z(s.self,n),$registry:n=>B.get(n.toLowerCase()),$element:s.self,$route:Lego.globals.$route,$go:(n,...l)=>G(n,...l)(s.self),$emit:(n,l)=>{s.self.dispatchEvent(new CustomEvent(n,{detail:l,bubbles:!0,composed:!0}))}},r=t.call(o,s.global,s.self,s.event,a);return typeof r=="function"?r.call(o,s.event):r}catch(o){if(c)throw o;p.onError(o,"render-error",s.self);return}},I=(e,s)=>{if(e.type==="checkbox")e.checked!==!!s&&(e.checked=!!s);else{const c=s==null?"":String(s);e.value!==c&&(e.value=c)}},C=(e,s,c=null)=>{const o=s._studs,t=n=>{const l=N(n);if(!l.bound){if(n.hasAttributes()){const d=n.attributes;for(let i=0;i<d.length;i++){const u=d[i];if(u.name.startsWith("@")){const f=u.name.slice(1);n.addEventListener(f,g=>{try{let m=o;if(c){const E=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];m=Object.assign(Object.create(o),{[c.name]:E})}h(u.value,{state:m,global:Lego.globals,self:n,event:g},!0)}catch(m){p.onError(m,"event-handler",n)}})}}if(n.hasAttribute("b-sync")){const i=n.getAttribute("b-sync"),u=()=>{try{let f,g;if(c&&i.startsWith(c.name+".")){const E=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];if(!E)return;const w=i.split(".").slice(1);g=w.pop(),f=w.reduce((T,_)=>T[_],E)}else{const y=i.split(".");g=y.pop(),f=y.reduce((E,w)=>E[w],o)}const m=n.type==="checkbox"?n.checked:n.value;f&&f[g]!==m&&(f[g]=m)}catch(f){p.onError(f,"sync-update",n)}};n.addEventListener("input",u),n.addEventListener("change",u)}if(n.hasAttribute("b-var")){const i=n.getAttribute("b-var");o.$vars&&(o.$vars[i]=n)}}l.bound=!0}};e instanceof Element&&t(e);const a=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let r;for(;r=a.nextNode();)t(r)},Y=e=>{const s=[],c=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let o;for(;o=c.nextNode();){if((r=>{let n=r.parentNode;for(;n&&n!==e;){if(n.hasAttribute&&n.hasAttribute("b-for"))return!0;n=n.parentNode}return!1})(o))continue;const a=r=>{if(/\bglobal\b/.test(r)){const n=e.host||e;N(n).hasGlobalDependency=!0}};if(o.nodeType===Node.ELEMENT_NODE){if(o.hasAttribute("b-if")){const n=o.getAttribute("b-if");a(n);const l=document.createComment(`b-if: ${n}`),d=N(o);d.anchor=l,s.push({type:"b-if",node:o,anchor:l,expr:n})}if(o.hasAttribute("b-show")){const n=o.getAttribute("b-show");a(n),s.push({type:"b-show",node:o,expr:n})}if(o.hasAttribute("b-for")){const n=o.getAttribute("b-for").match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/);n&&(a(n[2]),s.push({type:"b-for",node:o,itemName:n[1],listName:n[2].trim(),template:o.cloneNode(!0)}),o.innerHTML="")}if(o.hasAttribute("b-text")&&s.push({type:"b-text",node:o,path:o.getAttribute("b-text")}),o.hasAttribute("b-html")){const n=o.getAttribute("b-html");a(n),s.push({type:"b-html",node:o,expr:n})}o.hasAttribute("b-sync")&&s.push({type:"b-sync",node:o});const[r]=$();[...o.attributes].forEach(n=>{n.value.includes(r)&&(a(n.value),s.push({type:"attr",node:o,attrName:n.name,template:n.value}))})}else if(o.nodeType===Node.TEXT_NODE){const[r]=$();o.textContent.includes(r)&&(a(o.textContent),s.push({type:"text",node:o,template:o.textContent}))}}return s},Q=(e,s)=>{const c=a=>{if(a.nodeType===Node.TEXT_NODE){a._tpl===void 0&&(a._tpl=a.textContent);const r=a._tpl.replace(L(),(n,l)=>h(l.trim(),{state:s,global:Lego.globals,self:a})??"");a.textContent!==r&&(a.textContent=r)}else if(a.nodeType===Node.ELEMENT_NODE){const[r]=$();[...a.attributes].forEach(n=>{if(n._tpl===void 0&&(n._tpl=n.value),n._tpl.includes(r)){const l=n._tpl.replace(L(),(d,i)=>h(i.trim(),{state:s,global:Lego.globals,self:a})??"");n.value!==l&&(n.value=l,n.name==="class"&&(a.className=l))}})}};c(e);const o=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let t;for(;t=o.nextNode();)c(t)},x=e=>{const s=e._studs;if(!s)return;const c=N(e);if(!c.rendering){c.rendering=!0,p.metrics&&p.metrics.onRenderStart&&p.metrics.onRenderStart(e);try{const o=e.shadowRoot||e;c.bindings||(c.bindings=Y(o)),c.bindings.forEach(t=>{if(t.type==="b-if"){const a=!!h(t.expr,{state:s,global:Lego.globals,self:t.node}),r=!!t.node.parentNode;a&&!r?t.anchor.parentNode&&t.anchor.parentNode.replaceChild(t.node,t.anchor):!a&&r&&t.node.parentNode.replaceChild(t.anchor,t.node)}if(t.type==="b-show"&&(t.node.style.display=h(t.expr,{state:s,global:Lego.globals,self:t.node})?"":"none"),t.type==="b-text"&&(t.node.textContent=F(t.path,s)),t.type==="b-html"&&(t.node.innerHTML=h(t.expr,{state:s,global:Lego.globals,self:t.node})||""),t.type==="b-sync"&&I(t.node,F(t.node.getAttribute("b-sync"),s)),t.type==="text"){const a=t.template.replace(L(),(r,n)=>h(n.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.textContent!==a&&(t.node.textContent=a)}if(t.type==="attr"){const a=t.template.replace(L(),(r,n)=>h(n.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.getAttribute(t.attrName)!==a&&(t.node.setAttribute(t.attrName,a),t.attrName==="class"&&(t.node.className=a))}if(t.type==="b-for"){const a=h(t.listName,{state:s,global:Lego.globals,self:e})||[];P.has(t.node)||P.set(t.node,new Map);const r=P.get(t.node),n=new Set;a.forEach((l,d)=>{const i=l&&typeof l=="object"?l.__id||(l.__id=Math.random()):`${d}-${l}`;n.add(i);let u=r.get(i);u||(u=t.template.cloneNode(!0),u.removeAttribute("b-for"),r.set(i,u),C(u,e,{name:t.itemName,listName:t.listName,index:d}));const f=Object.assign(Object.create(s),{[t.itemName]:l});Q(u,f),u.querySelectorAll("[b-sync]").forEach(g=>{const m=g.getAttribute("b-sync");if(m.startsWith(t.itemName+".")){const y=h(t.listName,{state:s,global:Lego.globals,self:e});I(g,F(m.split(".").slice(1).join("."),y[d]))}}),t.node.children[d]!==u&&t.node.insertBefore(u,t.node.children[d]||null)});for(const[l,d]of r.entries())n.has(l)||(d.remove(),r.delete(l))}}),s===Lego.globals&&v.forEach(t=>{N(t).hasGlobalDependency&&x(t)})}catch(o){p.onError(o,"render",e)}finally{p.metrics&&p.metrics.onRenderEnd&&p.metrics.onRenderEnd(e),c.rendering=!1}}},A=e=>{if(!e||e.nodeType!==Node.ELEMENT_NODE)return;const s=N(e),c=e.tagName.toLowerCase(),o=b[c];if(o&&!s.snapped){s.snapped=!0;const a=o.content.cloneNode(!0),r=e.attachShadow({mode:"open"}),n=(o.getAttribute("b-stylesheets")||"").split(/\s+/).filter(Boolean);if(n.length>0){const f=n.flatMap(g=>j.get(g)||[]);f.length>0&&(r.adoptedStyleSheets=[...f])}const l=D.get(c)||{},d=U(o.getAttribute("b-data")||"{}"),i=U(e.getAttribute("b-data")||"{}");e._studs=S({...l,...d,...i,$vars:{},$element:e,$emit:(f,g)=>{e.dispatchEvent(new CustomEvent(f,{detail:g,bubbles:!0,composed:!0}))},get $route(){return Lego.globals.$route},get $go(){return Lego.globals.$go}},e),Object.defineProperty(e,"state",{get(){return this._studs},set(f){Object.assign(this._studs,f)},configurable:!0,enumerable:!1}),r.appendChild(a);const u=r.querySelector("style");if(u&&(u.textContent=u.textContent.replace(/\bself\b/g,":host")),C(r,e),v.add(e),x(e),[...r.children].forEach(A),typeof e._studs.mounted=="function")try{e._studs.mounted.call(e._studs)}catch(f){p.onError(f,"mounted",e)}}let t=e.parentElement;for(;t&&!t._studs;)t=t.parentElement;t&&t._studs&&C(e,t),[...e.children].forEach(A)},k=e=>{if(e._studs&&typeof e._studs.unmounted=="function")try{e._studs.unmounted.call(e._studs)}catch(s){console.error("[Lego] Error in unmounted:",s)}e.shadowRoot&&[...e.shadowRoot.children].forEach(k),v.delete(e),[...e.children].forEach(k)},W=async(e=null,s=null)=>{const c=window.location.pathname,o=window.location.search,t=R.find(d=>d.regex.test(c));if(!t)return;let a=[];if(e)a=e.flatMap(d=>V(d,s));else{const d=document.querySelector("lego-router");d&&(a=[d])}if(a.length===0)return;const r=c.match(t.regex).slice(1),n=Object.fromEntries(t.paramNames.map((d,i)=>[d,r[i]])),l=Object.fromEntries(new URLSearchParams(o));t.middleware&&!await t.middleware(n,Lego.globals)||(Lego.globals.$route.url=c+o,Lego.globals.$route.route=t.path,Lego.globals.$route.params=n,Lego.globals.$route.query=l,Lego.globals.$route.method=history.state?.method||"GET",Lego.globals.$route.body=history.state?.body||null,a.forEach(d=>{if(d){const i=document.createElement(t.tagName);d.replaceChildren(i)}}))},z={snap:A,unsnap:k,init:async(e=document.body,s={})=>{(!e||typeof e.nodeType!="number")&&(e=document.body),q=s.styles||{},p.loader=s.loader;const c=Object.entries(q).map(async([t,a])=>{const r=await Promise.all(a.map(async n=>{try{const d=await(await fetch(n)).text(),i=new CSSStyleSheet;return await i.replace(d),i}catch(l){return console.error(`[Lego] Failed to load stylesheet: ${n}`,l),null}}));j.set(t,r.filter(n=>n!==null))});if(await Promise.all(c),document.querySelectorAll("template[b-id]").forEach(t=>{b[t.getAttribute("b-id")]=t}),new MutationObserver(t=>t.forEach(a=>{a.addedNodes.forEach(r=>{if(r.nodeType===Node.ELEMENT_NODE){A(r);const n=r.tagName.toLowerCase();if(n.includes("-")&&!b[n]&&p.loader&&!v.has(r)){const l=p.loader(n);if(l){const d=typeof l=="string"?fetch(l).then(i=>i.text()):l;Promise.resolve(d).then(i=>z.defineSFC(i,n+".lego")).catch(i=>console.error(`[Lego] Failed to load ${n}:`,i))}}}}),a.removedNodes.forEach(r=>r.nodeType===Node.ELEMENT_NODE&&k(r))})).observe(e,{childList:!0,subtree:!0}),e._studs=Lego.globals,A(e),C(e,e),x(e),s.studio){if(!b["lego-studio"]){const t=document.createElement("script");t.src="https://unpkg.com/@legodom/studio@0.0.2/dist/lego-studio.js",t.onerror=()=>console.warn("[Lego] Failed to load Studio from CDN"),document.head.appendChild(t)}Lego.route("/_/studio","lego-studio"),Lego.route("/_/studio/:component","lego-studio")}R.length>0&&(window.addEventListener("popstate",t=>{const a=t.state?.legoTargets||null;W(a)}),document.addEventListener("submit",t=>{t.preventDefault()}),document.addEventListener("click",t=>{const r=t.composedPath().find(n=>n.tagName==="A"&&(n.hasAttribute("b-target")||n.hasAttribute("b-link")));if(r){t.preventDefault();const n=r.getAttribute("href"),l=r.getAttribute("b-target"),d=l?l.split(/\s+/).filter(Boolean):[],i=r.getAttribute("b-link")!=="false";Lego.globals.$go(n,...d).get(i)}}),W())},globals:S({$route:{url:window.location.pathname,route:"",params:{},query:{},method:"GET",body:null},$go:(e,...s)=>G(e,...s)(document.body)},document.body),defineSFC:(e,s="component.lego")=>{let c="",o="{}",t="",a="",r=e;const n=/<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i;for(;r;){const i=r.match(n);if(!i)break;const u=i[1].toLowerCase(),f=i[2],g=i[0],m=i.index,y=`</${u}>`,E=m+g.length,w=r.indexOf(y,E);if(w===-1){console.warn(`[Lego] Unclosed <${u}> tag in ${s}`);break}const T=r.slice(E,w);if(u==="template"){c=T.trim();const _=f.match(/b-stylesheets=["']([^"']+)["']/);_&&(t=_[1])}else if(u==="script"){const _=T.trim(),X=_.match(/export\s+default\s+({[\s\S]*})/);o=X?X[1]:_}else u==="style"&&(a=T.trim());r=r.slice(w+y.length)}const l=K(s),d=new Function(`return ${o}`)();a&&(c=`<style>${a}</style>`+c),b[l]=document.createElement("template"),b[l].innerHTML=c,b[l].setAttribute("b-stylesheets",t),D.set(l,d),document.querySelectorAll(l).forEach(i=>!N(i).snapped&&A(i))},define:(e,s,c={},o="")=>{const t=document.createElement("template");t.setAttribute("b-id",e),t.setAttribute("b-stylesheets",o),t.innerHTML=s,b[e]=t,D.set(e,c);try{B.set(e.toLowerCase(),S({...c},document.body))}catch(a){p.onError(a,"define",e)}document.querySelectorAll(e).forEach(A)},getActiveComponentsCount:()=>v.size,getLegos:()=>Object.keys(b),config:p,route:(e,s,c=null)=>{const o=[],t=e.replace(/:([^\/]+)/g,(a,r)=>(o.push(r),"([^/]+)"));R.push({path:e,regex:new RegExp(`^${t}$`),tagName:s,paramNames:o,middleware:c})}};return z})();typeof window<"u"&&(window.Lego=Lego);
|
|
10
|
+
`), H.set(e, t)); const a = { $ancestors: n => Z(s.self, n), $registry: n => B.get(n.toLowerCase()), $element: s.self, $route: Lego.globals.$route, $go: (n, ...l) => G(n, ...l)(s.self), $emit: (n, l) => { s.self.dispatchEvent(new CustomEvent(n, { detail: l, bubbles: !0, composed: !0 })) } }, r = t.call(o, s.global, s.self, s.event, a); return typeof r == "function" ? r.call(o, s.event) : r
|
|
11
|
+
} catch (o) { if (c) throw o; p.onError(o, "render-error", s.self); return }
|
|
12
|
+
}, I = (e, s) => { if (e.type === "checkbox") e.checked !== !!s && (e.checked = !!s); else { const c = s == null ? "" : String(s); e.value !== c && (e.value = c) } }, C = (e, s, c = null) => { const o = s._studs, t = n => { const l = N(n); if (!l.bound) { if (n.hasAttributes()) { const d = n.attributes; for (let i = 0; i < d.length; i++) { const u = d[i]; if (u.name.startsWith("@")) { const f = u.name.slice(1); n.addEventListener(f, g => { try { let m = o; if (c) { const E = h(c.listName, { state: o, global: Lego.globals, self: s })[c.index]; m = Object.assign(Object.create(o), { [c.name]: E }) } h(u.value, { state: m, global: Lego.globals, self: n, event: g }, !0) } catch (m) { p.onError(m, "event-handler", n) } }) } } if (n.hasAttribute("b-sync")) { const i = n.getAttribute("b-sync"), u = () => { try { let f, g; if (c && i.startsWith(c.name + ".")) { const E = h(c.listName, { state: o, global: Lego.globals, self: s })[c.index]; if (!E) return; const w = i.split(".").slice(1); g = w.pop(), f = w.reduce((T, _) => T[_], E) } else { const y = i.split("."); g = y.pop(), f = y.reduce((E, w) => E[w], o) } const m = n.type === "checkbox" ? n.checked : n.value; f && f[g] !== m && (f[g] = m) } catch (f) { p.onError(f, "sync-update", n) } }; n.addEventListener("input", u), n.addEventListener("change", u) } if (n.hasAttribute("b-var")) { const i = n.getAttribute("b-var"); o.$vars && (o.$vars[i] = n) } } l.bound = !0 } }; e instanceof Element && t(e); const a = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT); let r; for (; r = a.nextNode();)t(r) }, Y = e => { const s = [], c = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); let o; for (; o = c.nextNode();) { if ((r => { let n = r.parentNode; for (; n && n !== e;) { if (n.hasAttribute && n.hasAttribute("b-for")) return !0; n = n.parentNode } return !1 })(o)) continue; const a = r => { if (/\bglobal\b/.test(r)) { const n = e.host || e; N(n).hasGlobalDependency = !0 } }; if (o.nodeType === Node.ELEMENT_NODE) { if (o.hasAttribute("b-if")) { const n = o.getAttribute("b-if"); a(n); const l = document.createComment(`b-if: ${n}`), d = N(o); d.anchor = l, s.push({ type: "b-if", node: o, anchor: l, expr: n }) } if (o.hasAttribute("b-show")) { const n = o.getAttribute("b-show"); a(n), s.push({ type: "b-show", node: o, expr: n }) } if (o.hasAttribute("b-for")) { const n = o.getAttribute("b-for").match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/); n && (a(n[2]), s.push({ type: "b-for", node: o, itemName: n[1], listName: n[2].trim(), template: o.cloneNode(!0) }), o.innerHTML = "") } if (o.hasAttribute("b-text") && s.push({ type: "b-text", node: o, path: o.getAttribute("b-text") }), o.hasAttribute("b-html")) { const n = o.getAttribute("b-html"); a(n), s.push({ type: "b-html", node: o, expr: n }) } o.hasAttribute("b-sync") && s.push({ type: "b-sync", node: o }); const [r] = $();[...o.attributes].forEach(n => { n.value.includes(r) && (a(n.value), s.push({ type: "attr", node: o, attrName: n.name, template: n.value })) }) } else if (o.nodeType === Node.TEXT_NODE) { const [r] = $(); o.textContent.includes(r) && (a(o.textContent), s.push({ type: "text", node: o, template: o.textContent })) } } return s }, Q = (e, s) => { const c = a => { if (a.nodeType === Node.TEXT_NODE) { a._tpl === void 0 && (a._tpl = a.textContent); const r = a._tpl.replace(L(), (n, l) => h(l.trim(), { state: s, global: Lego.globals, self: a }) ?? ""); a.textContent !== r && (a.textContent = r) } else if (a.nodeType === Node.ELEMENT_NODE) { const [r] = $();[...a.attributes].forEach(n => { if (n._tpl === void 0 && (n._tpl = n.value), n._tpl.includes(r)) { const l = n._tpl.replace(L(), (d, i) => h(i.trim(), { state: s, global: Lego.globals, self: a }) ?? ""); n.value !== l && (n.value = l, n.name === "class" && (a.className = l)) } }) } }; c(e); const o = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); let t; for (; t = o.nextNode();)c(t) }, x = e => { const s = e._studs; if (!s) return; const c = N(e); if (!c.rendering) { c.rendering = !0, p.metrics && p.metrics.onRenderStart && p.metrics.onRenderStart(e); try { const o = e.shadowRoot || e; c.bindings || (c.bindings = Y(o)), c.bindings.forEach(t => { if (t.type === "b-if") { const a = !!h(t.expr, { state: s, global: Lego.globals, self: t.node }), r = !!t.node.parentNode; a && !r ? t.anchor.parentNode && t.anchor.parentNode.replaceChild(t.node, t.anchor) : !a && r && t.node.parentNode.replaceChild(t.anchor, t.node) } if (t.type === "b-show" && (t.node.style.display = h(t.expr, { state: s, global: Lego.globals, self: t.node }) ? "" : "none"), t.type === "b-text" && (t.node.textContent = F(t.path, s)), t.type === "b-html" && (t.node.innerHTML = h(t.expr, { state: s, global: Lego.globals, self: t.node }) || ""), t.type === "b-sync" && I(t.node, F(t.node.getAttribute("b-sync"), s)), t.type === "text") { const a = t.template.replace(L(), (r, n) => h(n.trim(), { state: s, global: Lego.globals, self: t.node }) ?? ""); t.node.textContent !== a && (t.node.textContent = a) } if (t.type === "attr") { const a = t.template.replace(L(), (r, n) => h(n.trim(), { state: s, global: Lego.globals, self: t.node }) ?? ""); t.node.getAttribute(t.attrName) !== a && (t.node.setAttribute(t.attrName, a), t.attrName === "class" && (t.node.className = a)) } if (t.type === "b-for") { const a = h(t.listName, { state: s, global: Lego.globals, self: e }) || []; P.has(t.node) || P.set(t.node, new Map); const r = P.get(t.node), n = new Set; a.forEach((l, d) => { const i = l && typeof l == "object" ? l.__id || (l.__id = Math.random()) : `${d}-${l}`; n.add(i); let u = r.get(i); u || (u = t.template.cloneNode(!0), u.removeAttribute("b-for"), r.set(i, u), C(u, e, { name: t.itemName, listName: t.listName, index: d })); const f = Object.assign(Object.create(s), { [t.itemName]: l }); Q(u, f), u.querySelectorAll("[b-sync]").forEach(g => { const m = g.getAttribute("b-sync"); if (m.startsWith(t.itemName + ".")) { const y = h(t.listName, { state: s, global: Lego.globals, self: e }); I(g, F(m.split(".").slice(1).join("."), y[d])) } }), t.node.children[d] !== u && t.node.insertBefore(u, t.node.children[d] || null) }); for (const [l, d] of r.entries()) n.has(l) || (d.remove(), r.delete(l)) } }), s === Lego.globals && v.forEach(t => { N(t).hasGlobalDependency && x(t) }) } catch (o) { p.onError(o, "render", e) } finally { p.metrics && p.metrics.onRenderEnd && p.metrics.onRenderEnd(e), c.rendering = !1 } } }, A = e => { if (!e || e.nodeType !== Node.ELEMENT_NODE) return; const s = N(e), c = e.tagName.toLowerCase(), o = b[c]; if (o && !s.snapped) { s.snapped = !0; const a = o.content.cloneNode(!0), r = e.attachShadow({ mode: "open" }), n = (o.getAttribute("b-stylesheets") || "").split(/\s+/).filter(Boolean); if (n.length > 0) { const f = n.flatMap(g => j.get(g) || []); f.length > 0 && (r.adoptedStyleSheets = [...f]) } const l = D.get(c) || {}, d = U(o.getAttribute("b-data") || "{}"), i = U(e.getAttribute("b-data") || "{}"); e._studs = S({ ...l, ...d, ...i, $vars: {}, $element: e, $emit: (f, g) => { e.dispatchEvent(new CustomEvent(f, { detail: g, bubbles: !0, composed: !0 })) }, get $route() { return Lego.globals.$route }, get $go() { return Lego.globals.$go } }, e), Object.defineProperty(e, "state", { get() { return this._studs }, set(f) { Object.assign(this._studs, f) }, configurable: !0, enumerable: !1 }), r.appendChild(a); const u = r.querySelector("style"); if (u && (u.textContent = u.textContent.replace(/\bself\b/g, ":host")), C(r, e), v.add(e), x(e), [...r.children].forEach(A), typeof e._studs.mounted == "function") try { e._studs.mounted.call(e._studs) } catch (f) { p.onError(f, "mounted", e) } } let t = e.parentElement; for (; t && !t._studs;)t = t.parentElement; t && t._studs && C(e, t), [...e.children].forEach(A) }, k = e => { if (e._studs && typeof e._studs.unmounted == "function") try { e._studs.unmounted.call(e._studs) } catch (s) { console.error("[Lego] Error in unmounted:", s) } e.shadowRoot && [...e.shadowRoot.children].forEach(k), v.delete(e), [...e.children].forEach(k) }, W = async (e = null, s = null) => { const c = window.location.pathname, o = window.location.search, t = R.find(d => d.regex.test(c)); if (!t) return; let a = []; if (e) a = e.flatMap(d => V(d, s)); else { const d = document.querySelector("lego-router"); d && (a = [d]) } if (a.length === 0) return; const r = c.match(t.regex).slice(1), n = Object.fromEntries(t.paramNames.map((d, i) => [d, r[i]])), l = Object.fromEntries(new URLSearchParams(o)); t.middleware && !await t.middleware(n, Lego.globals) || (Lego.globals.$route.url = c + o, Lego.globals.$route.route = t.path, Lego.globals.$route.params = n, Lego.globals.$route.query = l, Lego.globals.$route.method = history.state?.method || "GET", Lego.globals.$route.body = history.state?.body || null, a.forEach(d => { if (d) { const i = document.createElement(t.tagName); d.replaceChildren(i) } })) }, z = { snap: A, unsnap: k, init: async (e = document.body, s = {}) => { (!e || typeof e.nodeType != "number") && (e = document.body), q = s.styles || {}, p.loader = s.loader; const c = Object.entries(q).map(async ([t, a]) => { const r = await Promise.all(a.map(async n => { try { const d = await (await fetch(n)).text(), i = new CSSStyleSheet; return await i.replace(d), i } catch (l) { return console.error(`[Lego] Failed to load stylesheet: ${n}`, l), null } })); j.set(t, r.filter(n => n !== null)) }); if (await Promise.all(c), document.querySelectorAll("template[b-id]").forEach(t => { b[t.getAttribute("b-id")] = t }), new MutationObserver(t => t.forEach(a => { a.addedNodes.forEach(r => { if (r.nodeType === Node.ELEMENT_NODE) { A(r); const n = r.tagName.toLowerCase(); if (n.includes("-") && !b[n] && p.loader && !v.has(r)) { const l = p.loader(n); if (l) { const d = typeof l == "string" ? fetch(l).then(i => i.text()) : l; Promise.resolve(d).then(i => z.defineLegoFile(i, n + ".lego")).catch(i => console.error(`[Lego] Failed to load ${n}:`, i)) } } } }), a.removedNodes.forEach(r => r.nodeType === Node.ELEMENT_NODE && k(r)) })).observe(e, { childList: !0, subtree: !0 }), e._studs = Lego.globals, A(e), C(e, e), x(e), s.studio) { if (!b["lego-studio"]) { const t = document.createElement("script"); t.src = "https://unpkg.com/@legodom/studio@0.0.2/dist/lego-studio.js", t.onerror = () => console.warn("[Lego] Failed to load Studio from CDN"), document.head.appendChild(t) } Lego.route("/_/studio", "lego-studio"), Lego.route("/_/studio/:component", "lego-studio") } R.length > 0 && (window.addEventListener("popstate", t => { const a = t.state?.legoTargets || null; W(a) }), document.addEventListener("submit", t => { t.preventDefault() }), document.addEventListener("click", t => { const r = t.composedPath().find(n => n.tagName === "A" && (n.hasAttribute("b-target") || n.hasAttribute("b-link"))); if (r) { t.preventDefault(); const n = r.getAttribute("href"), l = r.getAttribute("b-target"), d = l ? l.split(/\s+/).filter(Boolean) : [], i = r.getAttribute("b-link") !== "false"; Lego.globals.$go(n, ...d).get(i) } }), W()) }, globals: S({ $route: { url: window.location.pathname, route: "", params: {}, query: {}, method: "GET", body: null }, $go: (e, ...s) => G(e, ...s)(document.body) }, document.body), defineSFC: (e, s = "component.lego") => { let c = "", o = "{}", t = "", a = "", r = e; const n = /<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i; for (; r;) { const i = r.match(n); if (!i) break; const u = i[1].toLowerCase(), f = i[2], g = i[0], m = i.index, y = `</${u}>`, E = m + g.length, w = r.indexOf(y, E); if (w === -1) { console.warn(`[Lego] Unclosed <${u}> tag in ${s}`); break } const T = r.slice(E, w); if (u === "template") { c = T.trim(); const _ = f.match(/b-stylesheets=["']([^"']+)["']/); _ && (t = _[1]) } else if (u === "script") { const _ = T.trim(), X = _.match(/export\s+default\s+({[\s\S]*})/); o = X ? X[1] : _ } else u === "style" && (a = T.trim()); r = r.slice(w + y.length) } const l = K(s), d = new Function(`return ${o}`)(); a && (c = `<style>${a}</style>` + c), b[l] = document.createElement("template"), b[l].innerHTML = c, b[l].setAttribute("b-stylesheets", t), D.set(l, d), document.querySelectorAll(l).forEach(i => !N(i).snapped && A(i)) }, define: (e, s, c = {}, o = "") => { const t = document.createElement("template"); t.setAttribute("b-id", e), t.setAttribute("b-stylesheets", o), t.innerHTML = s, b[e] = t, D.set(e, c); try { B.set(e.toLowerCase(), S({ ...c }, document.body)) } catch (a) { p.onError(a, "define", e) } document.querySelectorAll(e).forEach(A) }, getActiveBlocksCount: () => v.size, getLegos: () => Object.keys(b), config: p, route: (e, s, c = null) => { const o = [], t = e.replace(/:([^\/]+)/g, (a, r) => (o.push(r), "([^/]+)")); R.push({ path: e, regex: new RegExp(`^${t}$`), tagName: s, paramNames: o, middleware: c }) } }; return z
|
|
13
|
+
})(); typeof window < "u" && (window.Lego = Lego);
|
package/package.json
CHANGED
package/parse-lego.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parser for .lego Single File
|
|
2
|
+
* Parser for .lego Single File Blocks (SFC)
|
|
3
3
|
* Extracts template, script, and style sections from .lego files
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* Parse a .lego file content into structured sections
|
|
8
8
|
* @param {string} content - Raw .lego file content
|
|
9
9
|
* @param {string} filename - Filename for error reporting
|
|
10
|
-
* @returns {{template: string, script: string, style: string, stylesAttr: string,
|
|
10
|
+
* @returns {{template: string, script: string, style: string, stylesAttr: string, blockName: string}}
|
|
11
11
|
*/
|
|
12
|
-
export function parseLego(content, filename = '
|
|
12
|
+
export function parseLego(content, filename = 'block.lego') {
|
|
13
13
|
const result = {
|
|
14
14
|
template: '',
|
|
15
15
|
script: '',
|
|
16
16
|
style: '',
|
|
17
17
|
stylesAttr: '',
|
|
18
|
-
|
|
18
|
+
blockName: deriveBlockName(filename)
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
let remaining = content;
|
|
@@ -75,23 +75,23 @@ export function parseLego(content, filename = 'component.lego') {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Derive
|
|
79
|
-
* e.g., "sample-
|
|
78
|
+
* Derive block name from filename
|
|
79
|
+
* e.g., "sample-block.lego" -> "sample-block"
|
|
80
80
|
* @param {string} filename
|
|
81
81
|
* @returns {string}
|
|
82
82
|
*/
|
|
83
|
-
export function
|
|
83
|
+
export function deriveBlockName(filename) {
|
|
84
84
|
const basename = filename.split('/').pop();
|
|
85
85
|
return basename.replace(/\.lego$/, '');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Generate Lego.
|
|
89
|
+
* Generate Lego.block() code from parsed .lego file
|
|
90
90
|
* @param {object} parsed - Parsed .lego file object
|
|
91
91
|
* @returns {string} - JavaScript code string
|
|
92
92
|
*/
|
|
93
|
-
export function
|
|
94
|
-
const {
|
|
93
|
+
export function generateBlockCall(parsed) {
|
|
94
|
+
const { blockName, template, script, style, stylesAttr } = parsed;
|
|
95
95
|
|
|
96
96
|
// Build template HTML
|
|
97
97
|
let templateHTML = '';
|
|
@@ -115,8 +115,8 @@ export function generateDefineCall(parsed) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// Generate the Lego.
|
|
119
|
-
return `Lego.
|
|
118
|
+
// Generate the Lego.block call
|
|
119
|
+
return `Lego.block('${blockName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
@@ -137,15 +137,15 @@ export function validateLego(parsed) {
|
|
|
137
137
|
const errors = [];
|
|
138
138
|
|
|
139
139
|
if (!parsed.template && !parsed.script && !parsed.style) {
|
|
140
|
-
errors.push('
|
|
140
|
+
errors.push('Block must have at least one section: <template>, <script>, or <style>');
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
if (!parsed.
|
|
144
|
-
errors.push('Unable to derive
|
|
143
|
+
if (!parsed.blockName) {
|
|
144
|
+
errors.push('Unable to derive block name from filename');
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
if (parsed.
|
|
148
|
-
errors.push(`
|
|
147
|
+
if (parsed.blockName && !/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(parsed.blockName)) {
|
|
148
|
+
errors.push(`Block name "${parsed.blockName}" must be kebab-case with at least one hyphen (e.g., "my-block")`);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
return {
|
package/vite-plugin.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vite plugin for Lego Single File
|
|
2
|
+
* Vite plugin for Lego Single File Blocks
|
|
3
3
|
* Auto-discovers and transforms .lego files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { parseLego,
|
|
6
|
+
import { parseLego, generateBlockCall, validateLego } from './parse-lego.js';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import fg from 'fast-glob';
|
|
9
9
|
|
|
10
|
-
const VIRTUAL_MODULE_ID = 'virtual:lego-
|
|
10
|
+
const VIRTUAL_MODULE_ID = 'virtual:lego-blocks';
|
|
11
11
|
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Vite plugin for Lego SFC support
|
|
15
15
|
* @param {object} options - Plugin options
|
|
16
|
-
* @param {string} options.
|
|
16
|
+
* @param {string} options.blocksDir - Directory to search for .lego files
|
|
17
17
|
* @param {string[]} options.include - Glob patterns to include
|
|
18
18
|
* @returns {import('vite').Plugin}
|
|
19
19
|
*/
|
|
20
20
|
export default function legoPlugin(options = {}) {
|
|
21
21
|
const {
|
|
22
|
-
|
|
22
|
+
blocksDir = './src/blocks',
|
|
23
23
|
include = ['**/*.lego'],
|
|
24
24
|
importPath = 'lego-dom'
|
|
25
25
|
} = options;
|
|
@@ -30,7 +30,7 @@ export default function legoPlugin(options = {}) {
|
|
|
30
30
|
|
|
31
31
|
const getSearchPath = () => {
|
|
32
32
|
const root = config?.root || process.cwd();
|
|
33
|
-
return path.resolve(root,
|
|
33
|
+
return path.resolve(root, blocksDir);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const scanFiles = async () => {
|
|
@@ -75,13 +75,13 @@ export default function legoPlugin(options = {}) {
|
|
|
75
75
|
server.watcher.add(searchPath);
|
|
76
76
|
server.watcher.on('add', (file) => {
|
|
77
77
|
if (file.endsWith('.lego')) {
|
|
78
|
-
console.log(`[vite-plugin-lego] New
|
|
78
|
+
console.log(`[vite-plugin-lego] New block detected: ${path.basename(file)}`);
|
|
79
79
|
scanFiles().then(invalidateVirtualModule);
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
server.watcher.on('unlink', (file) => {
|
|
83
83
|
if (file.endsWith('.lego')) {
|
|
84
|
-
console.log(`[vite-plugin-lego]
|
|
84
|
+
console.log(`[vite-plugin-lego] Block removed: ${path.basename(file)}`);
|
|
85
85
|
scanFiles().then(invalidateVirtualModule);
|
|
86
86
|
}
|
|
87
87
|
});
|
|
@@ -90,7 +90,7 @@ export default function legoPlugin(options = {}) {
|
|
|
90
90
|
async buildStart() {
|
|
91
91
|
await scanFiles();
|
|
92
92
|
if (legoFiles.length > 0) {
|
|
93
|
-
console.log(`[vite-plugin-lego] Discovered ${legoFiles.length}
|
|
93
|
+
console.log(`[vite-plugin-lego] Discovered ${legoFiles.length} block(s):`);
|
|
94
94
|
legoFiles.forEach(file => {
|
|
95
95
|
const name = path.basename(file);
|
|
96
96
|
console.log(` - ${name}`);
|
|
@@ -105,13 +105,13 @@ export default function legoPlugin(options = {}) {
|
|
|
105
105
|
},
|
|
106
106
|
|
|
107
107
|
async load(id) {
|
|
108
|
-
// Handle virtual module that imports all .lego
|
|
108
|
+
// Handle virtual module that imports all .lego blocks
|
|
109
109
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
110
110
|
const imports = legoFiles.map((file, index) =>
|
|
111
|
-
`import
|
|
111
|
+
`import block${index} from '${file}?lego-block';`
|
|
112
112
|
).join('\n');
|
|
113
113
|
|
|
114
|
-
const exports = `export default function
|
|
114
|
+
const exports = `export default function registerBlocks() {\n // Blocks are auto-registered when imported\n}`;
|
|
115
115
|
|
|
116
116
|
return `${imports}\n\n${exports}`;
|
|
117
117
|
}
|
|
@@ -130,14 +130,14 @@ export default function legoPlugin(options = {}) {
|
|
|
130
130
|
throw new Error(`Invalid .lego file "${filename}":\n${validation.errors.join('\n')}`);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const blockCall = generateBlockCall(parsed);
|
|
134
134
|
|
|
135
135
|
return `
|
|
136
136
|
import { Lego } from '${importPath}';
|
|
137
137
|
|
|
138
|
-
${
|
|
138
|
+
${blockCall}
|
|
139
139
|
|
|
140
|
-
export default '${parsed.
|
|
140
|
+
export default '${parsed.blockName}';
|
|
141
141
|
`;
|
|
142
142
|
}
|
|
143
143
|
},
|
|
@@ -145,7 +145,7 @@ export default '${parsed.componentName}';
|
|
|
145
145
|
handleHotUpdate({ file, server }) {
|
|
146
146
|
if (file.endsWith('.lego')) {
|
|
147
147
|
console.log(`[vite-plugin-lego] Hot reload: ${path.basename(file)}`);
|
|
148
|
-
// Trigger full reload for
|
|
148
|
+
// Trigger full reload for block content changes
|
|
149
149
|
server.ws.send({
|
|
150
150
|
type: 'full-reload',
|
|
151
151
|
path: '*'
|
package/parse-lego.test.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseLego, generateDefineCall, validateLego, deriveComponentName } from './parse-lego.js';
|
|
3
|
-
|
|
4
|
-
describe('Lego SFC Parser', () => {
|
|
5
|
-
describe('deriveComponentName', () => {
|
|
6
|
-
it('should derive component name from filename', () => {
|
|
7
|
-
expect(deriveComponentName('sample-component.lego')).toBe('sample-component');
|
|
8
|
-
expect(deriveComponentName('path/to/my-button.lego')).toBe('my-button');
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe('parseLego', () => {
|
|
13
|
-
it('should parse all three sections', () => {
|
|
14
|
-
const content = `
|
|
15
|
-
<template>
|
|
16
|
-
<h1>{{ title }}</h1>
|
|
17
|
-
</template>
|
|
18
|
-
|
|
19
|
-
<script>
|
|
20
|
-
export default {
|
|
21
|
-
title: 'Hello'
|
|
22
|
-
}
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<style>
|
|
26
|
-
self { color: red; }
|
|
27
|
-
</style>
|
|
28
|
-
`;
|
|
29
|
-
|
|
30
|
-
const result = parseLego(content, 'test-component.lego');
|
|
31
|
-
expect(result.componentName).toBe('test-component');
|
|
32
|
-
expect(result.template).toContain('<h1>{{ title }}</h1>');
|
|
33
|
-
expect(result.script).toContain('export default');
|
|
34
|
-
expect(result.style).toContain('self { color: red; }');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should handle components with only template', () => {
|
|
38
|
-
const content = '<template><p>Hello</p></template>';
|
|
39
|
-
const result = parseLego(content, 'simple.lego');
|
|
40
|
-
expect(result.template).toBe('<p>Hello</p>');
|
|
41
|
-
expect(result.script).toBe('');
|
|
42
|
-
expect(result.style).toBe('');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle components with only script', () => {
|
|
46
|
-
const content = '<script>export default { count: 0 }</script>';
|
|
47
|
-
const result = parseLego(content, 'logic.lego');
|
|
48
|
-
expect(result.script).toContain('count: 0');
|
|
49
|
-
expect(result.template).toBe('');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('validateLego', () => {
|
|
54
|
-
it('should validate correct component name', () => {
|
|
55
|
-
const parsed = {
|
|
56
|
-
componentName: 'my-component',
|
|
57
|
-
template: '<div>Test</div>',
|
|
58
|
-
script: '',
|
|
59
|
-
style: ''
|
|
60
|
-
};
|
|
61
|
-
const result = validateLego(parsed);
|
|
62
|
-
expect(result.valid).toBe(true);
|
|
63
|
-
expect(result.errors).toHaveLength(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should reject invalid component names', () => {
|
|
67
|
-
const parsed = {
|
|
68
|
-
componentName: 'MyComponent', // Not kebab-case
|
|
69
|
-
template: '<div>Test</div>',
|
|
70
|
-
script: '',
|
|
71
|
-
style: ''
|
|
72
|
-
};
|
|
73
|
-
const result = validateLego(parsed);
|
|
74
|
-
expect(result.valid).toBe(false);
|
|
75
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should require at least one section', () => {
|
|
79
|
-
const parsed = {
|
|
80
|
-
componentName: 'empty-component',
|
|
81
|
-
template: '',
|
|
82
|
-
script: '',
|
|
83
|
-
style: ''
|
|
84
|
-
};
|
|
85
|
-
const result = validateLego(parsed);
|
|
86
|
-
expect(result.valid).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('generateDefineCall', () => {
|
|
91
|
-
it('should generate valid Lego.define call', () => {
|
|
92
|
-
const parsed = {
|
|
93
|
-
componentName: 'test-comp',
|
|
94
|
-
template: '<button>Click</button>',
|
|
95
|
-
script: 'export default { count: 0 }',
|
|
96
|
-
style: 'self { color: blue; }'
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const result = generateDefineCall(parsed);
|
|
100
|
-
expect(result).toContain('Lego.define');
|
|
101
|
-
expect(result).toContain('test-comp');
|
|
102
|
-
expect(result).toContain('<button>Click</button>');
|
|
103
|
-
expect(result).toContain('{ count: 0 }');
|
|
104
|
-
expect(result).toContain('self { color: blue; }');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
});
|