lego-dom 1.5.1 → 2.0.2
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 +47 -0
- package/main.js +43 -33
- 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,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# Changelog
|
|
4
|
+
|
|
5
|
+
## [2.0.2] - 2026-01-19
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Scoped Props (`b-logic`):** `b-logic` attributes now inherit the parent block's scope! You can pass state naturally to children without global variables.
|
|
10
|
+
```html
|
|
11
|
+
<!-- 'user' is resolved from the parent block's state -->
|
|
12
|
+
<user-avatar b-logic="{ user: user }"></user-avatar>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **`$parent` Helper:** Added `this.$parent` (and template access) to easily find the nearest ancestor Block. It intelligently skips non-block elements (divs, spans) and handles Shadow DOM boundaries.
|
|
16
|
+
```javascript
|
|
17
|
+
const parentBlock = this.$parent;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## [2.0.1] - 2026-01-19
|
|
21
|
+
|
|
22
|
+
### The "Blocks" Update Refactor 🧱
|
|
23
|
+
|
|
24
|
+
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").
|
|
25
|
+
|
|
26
|
+
### Breaking Changes 🚨
|
|
27
|
+
|
|
28
|
+
- **Terminological Refactor (Runtime & Docs):**
|
|
29
|
+
- **Components → Blocks:** The fundamental unit of UI is now a "Block".
|
|
30
|
+
- **SFC → Lego File:** Protocol-agnostic Single File Blocks.
|
|
31
|
+
|
|
32
|
+
- **API Renames:**
|
|
33
|
+
- `Lego.define()` is now **`Lego.block()`**. (Legacy alias maintained)
|
|
34
|
+
- `Lego.defineSFC()` is now **`Lego.defineLegoFile()`**.
|
|
35
|
+
- Internal: `deriveComponentName` is now `deriveBlockName`.
|
|
36
|
+
|
|
37
|
+
- **Attribute Renames:**
|
|
38
|
+
- `b-data` is now **`b-logic`**. (Legacy alias maintained)
|
|
39
|
+
- `b-styles` is now **`b-stylesheets`**.
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
|
|
43
|
+
- **Public State API:**
|
|
44
|
+
- `element.state` is now the official public API for accessing the reactive proxy.
|
|
45
|
+
- `element._studs` is considered internal/private.
|
|
46
|
+
|
|
47
|
+
- **Lego Studio Audit:**
|
|
48
|
+
- `lego-studio` has been refactored to use "Block" terminology throughout the UI and codebase.
|
|
49
|
+
|
|
3
50
|
## [1.5.1] - 2026-01-19
|
|
4
51
|
|
|
5
52
|
### 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
|
};
|
|
@@ -176,11 +176,10 @@ const Lego = (() => {
|
|
|
176
176
|
return p;
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
-
const parseJSObject = (raw) => {
|
|
179
|
+
const parseJSObject = (raw, scope = {}) => {
|
|
180
180
|
try {
|
|
181
|
-
return (new Function(`return (${raw})`))();
|
|
181
|
+
return (new Function('scope', 'global', `with(global) { with(scope) { return (${raw}); } }`))(scope, Lego.globals);
|
|
182
182
|
} catch (e) {
|
|
183
|
-
console.error(`[Lego] Failed to parse b-data:`, raw, e);
|
|
184
183
|
return {};
|
|
185
184
|
}
|
|
186
185
|
};
|
|
@@ -204,10 +203,13 @@ const Lego = (() => {
|
|
|
204
203
|
};
|
|
205
204
|
|
|
206
205
|
const findAncestor = (el, tagName) => {
|
|
207
|
-
|
|
206
|
+
if (!el) return undefined;
|
|
207
|
+
let parent = el.parentElement || (el.getRootNode ? el.getRootNode().host : null);
|
|
208
208
|
while (parent) {
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
const pName = parent.tagName ? parent.tagName.toLowerCase() : '';
|
|
210
|
+
if (pName) {
|
|
211
|
+
if (tagName === '*' && registry[pName]) return parent;
|
|
212
|
+
if (pName === tagName.toLowerCase()) return parent;
|
|
211
213
|
}
|
|
212
214
|
parent = parent.parentElement || (parent.getRootNode && parent.getRootNode().host);
|
|
213
215
|
}
|
|
@@ -274,8 +276,8 @@ const Lego = (() => {
|
|
|
274
276
|
}
|
|
275
277
|
};
|
|
276
278
|
|
|
277
|
-
const bind = (container,
|
|
278
|
-
const state =
|
|
279
|
+
const bind = (container, blockRoot, loopCtx = null) => {
|
|
280
|
+
const state = blockRoot._studs;
|
|
279
281
|
|
|
280
282
|
const bindNode = (child) => {
|
|
281
283
|
const childData = getPrivateData(child);
|
|
@@ -291,7 +293,7 @@ const Lego = (() => {
|
|
|
291
293
|
try {
|
|
292
294
|
let evalScope = state;
|
|
293
295
|
if (loopCtx) {
|
|
294
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self:
|
|
296
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
|
|
295
297
|
const item = list[loopCtx.index];
|
|
296
298
|
evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
|
|
297
299
|
}
|
|
@@ -309,7 +311,7 @@ const Lego = (() => {
|
|
|
309
311
|
try {
|
|
310
312
|
let target, last;
|
|
311
313
|
if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
|
|
312
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self:
|
|
314
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: blockRoot });
|
|
313
315
|
const item = list[loopCtx.index];
|
|
314
316
|
if (!item) return;
|
|
315
317
|
const subPath = prop.split('.').slice(1);
|
|
@@ -357,7 +359,7 @@ const Lego = (() => {
|
|
|
357
359
|
let curr = n.parentNode;
|
|
358
360
|
while (curr && curr !== container) {
|
|
359
361
|
if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
|
|
360
|
-
// Only stop at Shadow Roots or explicit boundaries, NOT
|
|
362
|
+
// Only stop at Shadow Roots or explicit boundaries, NOT block tags in Light DOM
|
|
361
363
|
// The parent MUST be able to bind data to the slots of its children.
|
|
362
364
|
curr = curr.parentNode;
|
|
363
365
|
}
|
|
@@ -462,7 +464,7 @@ const Lego = (() => {
|
|
|
462
464
|
if (config.metrics && config.metrics.onRenderStart) config.metrics.onRenderStart(el);
|
|
463
465
|
|
|
464
466
|
try {
|
|
465
|
-
// Use shadowRoot if it's a
|
|
467
|
+
// Use shadowRoot if it's a block, otherwise render the element itself (light DOM)
|
|
466
468
|
const target = el.shadowRoot || el;
|
|
467
469
|
if (!data.bindings) data.bindings = scanForBindings(target);
|
|
468
470
|
|
|
@@ -525,9 +527,9 @@ const Lego = (() => {
|
|
|
525
527
|
}
|
|
526
528
|
});
|
|
527
529
|
|
|
528
|
-
// Global Broadcast: Only notify
|
|
530
|
+
// Global Broadcast: Only notify blocks that depend on globals
|
|
529
531
|
if (state === Lego.globals) {
|
|
530
|
-
|
|
532
|
+
activeBlocks.forEach(comp => {
|
|
531
533
|
if (getPrivateData(comp).hasGlobalDependency) render(comp);
|
|
532
534
|
});
|
|
533
535
|
}
|
|
@@ -558,12 +560,16 @@ const Lego = (() => {
|
|
|
558
560
|
}
|
|
559
561
|
}
|
|
560
562
|
|
|
561
|
-
// TIER 1: Logic from Lego.
|
|
563
|
+
// TIER 1: Logic from Lego.block (Lego File)
|
|
562
564
|
// TIER 2: Logic from the <template b-data="..."> attribute
|
|
563
565
|
// TIER 3: Logic from the <my-comp b-data="..."> tag
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
const
|
|
566
|
+
// SCOPED PROPS: Use parent block state as scope for instance logic evaluation
|
|
567
|
+
const parentBlock = findAncestor(el, '*') || findAncestor(el.getRootNode().host, '*');
|
|
568
|
+
const parentScope = parentBlock && parentBlock.state ? parentBlock.state : {};
|
|
569
|
+
|
|
570
|
+
const scriptLogic = legoFileLogic.get(name) || {};
|
|
571
|
+
const templateLogic = parseJSObject(templateNode.getAttribute('b-logic') || templateNode.getAttribute('b-data') || '{}');
|
|
572
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-logic') || el.getAttribute('b-data') || '{}', parentScope);
|
|
567
573
|
|
|
568
574
|
el._studs = reactive({
|
|
569
575
|
...scriptLogic,
|
|
@@ -571,6 +577,7 @@ const Lego = (() => {
|
|
|
571
577
|
...instanceLogic,
|
|
572
578
|
$vars: {},
|
|
573
579
|
$element: el,
|
|
580
|
+
get $parent() { return findAncestor(el, '*') },
|
|
574
581
|
$emit: (name, detail) => {
|
|
575
582
|
el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
576
583
|
},
|
|
@@ -593,7 +600,7 @@ const Lego = (() => {
|
|
|
593
600
|
}
|
|
594
601
|
|
|
595
602
|
bind(shadow, el);
|
|
596
|
-
|
|
603
|
+
activeBlocks.add(el);
|
|
597
604
|
render(el);
|
|
598
605
|
|
|
599
606
|
[...shadow.children].forEach(snap);
|
|
@@ -619,7 +626,7 @@ const Lego = (() => {
|
|
|
619
626
|
[...el.shadowRoot.children].forEach(unsnap);
|
|
620
627
|
}
|
|
621
628
|
|
|
622
|
-
|
|
629
|
+
activeBlocks.delete(el);
|
|
623
630
|
[...el.children].forEach(unsnap);
|
|
624
631
|
};
|
|
625
632
|
|
|
@@ -701,7 +708,7 @@ const Lego = (() => {
|
|
|
701
708
|
snap(n);
|
|
702
709
|
// Auto-Discovery: Check if tag is unknown and loader is configured
|
|
703
710
|
const tagName = n.tagName.toLowerCase();
|
|
704
|
-
if (tagName.includes('-') && !registry[tagName] && config.loader && !
|
|
711
|
+
if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
|
|
705
712
|
const result = config.loader(tagName);
|
|
706
713
|
if (result) {
|
|
707
714
|
// Handle Promise (user does custom fetch) vs String (we fetch)
|
|
@@ -710,7 +717,7 @@ const Lego = (() => {
|
|
|
710
717
|
: result;
|
|
711
718
|
|
|
712
719
|
Promise.resolve(promise)
|
|
713
|
-
.then(
|
|
720
|
+
.then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
|
|
714
721
|
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
|
|
715
722
|
}
|
|
716
723
|
}
|
|
@@ -774,7 +781,7 @@ const Lego = (() => {
|
|
|
774
781
|
},
|
|
775
782
|
$go: (path, ...targets) => _go(path, ...targets)(document.body)
|
|
776
783
|
}, document.body),
|
|
777
|
-
|
|
784
|
+
defineLegoFile: (content, filename = 'block.lego') => {
|
|
778
785
|
let template = '';
|
|
779
786
|
let script = '{}';
|
|
780
787
|
let stylesAttr = '';
|
|
@@ -820,7 +827,7 @@ const Lego = (() => {
|
|
|
820
827
|
remaining = remaining.slice(contentEnd + closeTag.length);
|
|
821
828
|
}
|
|
822
829
|
|
|
823
|
-
const name =
|
|
830
|
+
const name = deriveBlockName(filename);
|
|
824
831
|
// We must eval the script to get the object.
|
|
825
832
|
// Safe-ish because it's coming from the "Server" (trusted source in this architecture)
|
|
826
833
|
const logicObj = new Function(`return ${script}`)();
|
|
@@ -832,18 +839,18 @@ const Lego = (() => {
|
|
|
832
839
|
registry[name] = document.createElement('template');
|
|
833
840
|
registry[name].innerHTML = template;
|
|
834
841
|
registry[name].setAttribute('b-stylesheets', stylesAttr);
|
|
835
|
-
|
|
842
|
+
legoFileLogic.set(name, logicObj);
|
|
836
843
|
|
|
837
844
|
// Upgrade existing elements
|
|
838
845
|
document.querySelectorAll(name).forEach(el => !getPrivateData(el).snapped && snap(el));
|
|
839
846
|
},
|
|
840
|
-
|
|
847
|
+
block: (tagName, templateHTML, logic = {}, styles = "") => {
|
|
841
848
|
const t = document.createElement('template');
|
|
842
849
|
t.setAttribute('b-id', tagName);
|
|
843
850
|
t.setAttribute('b-stylesheets', styles);
|
|
844
851
|
t.innerHTML = templateHTML;
|
|
845
852
|
registry[tagName] = t;
|
|
846
|
-
|
|
853
|
+
legoFileLogic.set(tagName, logic);
|
|
847
854
|
|
|
848
855
|
// Initialize shared state with try-catch safety
|
|
849
856
|
try {
|
|
@@ -854,8 +861,11 @@ const Lego = (() => {
|
|
|
854
861
|
|
|
855
862
|
document.querySelectorAll(tagName).forEach(snap);
|
|
856
863
|
},
|
|
864
|
+
// Alias for backward compatibility
|
|
865
|
+
get define() { return this.block },
|
|
866
|
+
|
|
857
867
|
// For specific test validation
|
|
858
|
-
|
|
868
|
+
getActiveBlocksCount: () => activeBlocks.size,
|
|
859
869
|
getLegos: () => Object.keys(registry),
|
|
860
870
|
config, // Expose config for customization
|
|
861
871
|
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
|
-
});
|