lego-dom 2.0.2 → 2.0.4-beta
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/.md +78 -0
- package/CHANGELOG.md +34 -0
- package/lego.js +1 -1
- package/main.js +332 -49
- package/main.min.js +2 -8
- package/monitoring-plugin.js +5 -5
- package/package.json +1 -1
package/.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# LegoDOM Codebase Audit Report
|
|
2
|
+
|
|
3
|
+
## 1. Component to Block Renaming
|
|
4
|
+
The renaming of "Concept" from `Component` to `Block` appears to be consistent across the core codebase.
|
|
5
|
+
|
|
6
|
+
- **Core Logic (`main.js`)**:
|
|
7
|
+
- `Lego.block()` is correctly defined and used.
|
|
8
|
+
- `registry` and logic maps are clean.
|
|
9
|
+
- No residual `Component` terminology found in core logic.
|
|
10
|
+
- `Lego.define` is safely aliased to `Lego.block` for backward compatibility.
|
|
11
|
+
|
|
12
|
+
- **Vite Plugin (`vite-plugin.js`)**:
|
|
13
|
+
- Uses `virtual:lego-blocks`.
|
|
14
|
+
- Scans for `.lego` files in `src/blocks`.
|
|
15
|
+
- Imports helper functions from `parse-lego.js`.
|
|
16
|
+
|
|
17
|
+
- **Tests**:
|
|
18
|
+
- `tests/naming.test.js` and `tests/parse-lego.test.js` utilize correct `Block` terminology.
|
|
19
|
+
- All tests passed successfully.
|
|
20
|
+
|
|
21
|
+
- **Minor Inconsistencies (Non-Critical)**:
|
|
22
|
+
- `examples/vite-app/src/app.js`: Imports the virtual module as `registerComponents`.
|
|
23
|
+
- `examples/benchmark.html`: User-facing text still refers to "Components".
|
|
24
|
+
|
|
25
|
+
## 2. `.lego` File Parsing Logic Issues (Critical)
|
|
26
|
+
|
|
27
|
+
A significant fragility was identified in how `.lego` files (Single File Blocks) are parsed in both `main.js` (runtime loader) and `parse-lego.js` (build-time plugin).
|
|
28
|
+
|
|
29
|
+
### The Issue
|
|
30
|
+
The parsing logic relies on a regex that strictly expects an object literal export:
|
|
31
|
+
```javascript
|
|
32
|
+
/export\s+default\s+({[\s\S]*})/
|
|
33
|
+
```
|
|
34
|
+
This fails if a user defines a variable and exports it, or uses any other export syntax.
|
|
35
|
+
|
|
36
|
+
**Example Failure Case:**
|
|
37
|
+
```javascript
|
|
38
|
+
<script>
|
|
39
|
+
const logic = {
|
|
40
|
+
data: { count: 0 }
|
|
41
|
+
};
|
|
42
|
+
export default logic;
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Consequences:**
|
|
47
|
+
1. **Runtime (`defineLegoFile` in `main.js`)**:
|
|
48
|
+
The fallback mechanism tries to execute the entire script content inside a `new Function('return ' + script)`. This results in a `SyntaxError` because statements like `const` or `export` are not valid in a return statement.
|
|
49
|
+
|
|
50
|
+
2. **Build-time (`vite-plugin.js`)**:
|
|
51
|
+
The generated code becomes invalid JavaScript:
|
|
52
|
+
```javascript
|
|
53
|
+
Lego.block('name', `tpl`, const logic = ...; export default logic;, 'styles');
|
|
54
|
+
```
|
|
55
|
+
This causes the build or runtime execution to crash with a SyntaxError.
|
|
56
|
+
|
|
57
|
+
### Recommendation
|
|
58
|
+
Refactor the parsing logic to be more robust.
|
|
59
|
+
- **For Build-time**: Parse the script properly or strip the `export default` text and wrap the rest in an IIFE or leave it as a valid expression if possible. A true JS parser would be safest.
|
|
60
|
+
- **For Runtime**: Similar logic needed.
|
|
61
|
+
|
|
62
|
+
## 3. Code Duplication
|
|
63
|
+
There is significant logic duplication between `main.js` (`defineLegoFile`) and `parse-lego.js`.
|
|
64
|
+
- Both contain identical regex for parsing `<template>`, `<script>`, `<style>`.
|
|
65
|
+
- Both contain identical `deriveBlockName` logic.
|
|
66
|
+
- Both contain the flawed `export default` extraction logic.
|
|
67
|
+
|
|
68
|
+
**Recommendation**: Extract the parsing logic into a shared module that can be used by both `main.js` (if bundled or imported) and `vite-plugin.js`, or verify if `parse-lego.js` can be imported by `main.js` (currently `main.js` is a single file likely for CDN usage, complicating imports).
|
|
69
|
+
|
|
70
|
+
## 4. Security Warning
|
|
71
|
+
`main.js` line 915 uses `new Function` to evaluate the script block.
|
|
72
|
+
```javascript
|
|
73
|
+
const logicObj = new Function(`return ${script}`)();
|
|
74
|
+
```
|
|
75
|
+
While this is standard for some lightweight Runtime compilations, it assumes trusted content. The duplication of this risky logic (duplicated from `parse-lego.js` intent) increases the surface area for bugs.
|
|
76
|
+
|
|
77
|
+
## Summary
|
|
78
|
+
The renaming is successful. However, the `Single File Block` logic (`.lego` files) is brittle regarding script exports and should be addressed to prevent user frustration.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
|
+
## [2.0.4-beta] - 2026-01-20
|
|
5
|
+
|
|
6
|
+
### Fixes
|
|
7
|
+
|
|
8
|
+
- **Auto-Discovery Logic:** Fixed a bug where elements present in the initial HTML were not detected by the `config.loader`. The discovery logic now scans the DOM during `Lego.init()` as well as on subsequent mutations.
|
|
9
|
+
- **b-stylesheets Support:** Fully implemented `b-stylesheets` to allow blocks to adopt constructable stylesheets.
|
|
10
|
+
- **SSR/Node Compatibility:** Added checks for `window`, `document`, and `Node` throughout the core to ensure LegoDOM can be imported and executed in Node.js/SSR environments without crashing.
|
|
11
|
+
- **Global Export:** `Lego` is now correctly exported to `global` in Node.js environments.
|
|
12
|
+
|
|
13
|
+
## [2.0.4-alpha] - 2026-01-20
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
|
|
17
|
+
- **SSR/Node Compatibility:** Added checks for `window`, `document`, and `Node` throughout the core to ensure LegoDOM can be imported and executed in Node.js/SSR environments without crashing.
|
|
18
|
+
- **Global Export:** `Lego` is now correctly exported to `global` in Node.js environments.
|
|
19
|
+
|
|
20
|
+
## [2.0.3] - 2026-01-19
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
- **Reactive Persistence (`$db`):** New helper for persisting state to `localStorage` with automatic hydration, debouncing, and cross-tab synchronization.
|
|
25
|
+
```html
|
|
26
|
+
<user-prefs b-logic="{
|
|
27
|
+
theme: $db('app.theme').default('light'),
|
|
28
|
+
volume: $db('app.vol').default(50).debounce(300)
|
|
29
|
+
}"></user-prefs>
|
|
30
|
+
```
|
|
31
|
+
- **Auto-Load:** Values are automatically hydrated from `localStorage` on block creation.
|
|
32
|
+
- **Auto-Save:** Changes are automatically persisted (with optional debounce).
|
|
33
|
+
- **Cross-Tab Sync:** Updates in one tab automatically reflect in other tabs.
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
|
|
37
|
+
- Added `docs/guide/persistence.md` covering the `$db` API, namespacing best practices, and performance warnings.
|
|
4
38
|
|
|
5
39
|
## [2.0.2] - 2026-01-19
|
|
6
40
|
|
package/lego.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import './main.js';
|
|
2
|
-
export const Lego = window.Lego;
|
|
2
|
+
export const Lego = (typeof window !== 'undefined' ? window.Lego : global.Lego);
|
package/main.js
CHANGED
|
@@ -5,7 +5,47 @@ const Lego = (() => {
|
|
|
5
5
|
|
|
6
6
|
const legoFileLogic = new Map();
|
|
7
7
|
const sharedStates = new Map();
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
// LRU Cache for compiled expressions (prevents unbounded memory growth)
|
|
10
|
+
class LRUCache {
|
|
11
|
+
constructor(limit = 1000) {
|
|
12
|
+
this.limit = limit;
|
|
13
|
+
this.cache = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(key) {
|
|
17
|
+
if (!this.cache.has(key)) return undefined;
|
|
18
|
+
|
|
19
|
+
// Move to end (most recently used) by deleting and re-inserting
|
|
20
|
+
const value = this.cache.get(key);
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
this.cache.set(key, value);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(key, value) {
|
|
27
|
+
// If key exists, delete it first so we can re-insert at end
|
|
28
|
+
if (this.cache.has(key)) {
|
|
29
|
+
this.cache.delete(key);
|
|
30
|
+
} else if (this.cache.size >= this.limit) {
|
|
31
|
+
// Evict oldest (first item in Map)
|
|
32
|
+
const firstKey = this.cache.keys().next().value;
|
|
33
|
+
this.cache.delete(firstKey);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.cache.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get size() {
|
|
40
|
+
return this.cache.size;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clear() {
|
|
44
|
+
this.cache.clear();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const expressionCache = new LRUCache(1000); // Max 1000 cached expressions
|
|
9
49
|
|
|
10
50
|
const styleRegistry = new Map();
|
|
11
51
|
let styleConfig = {};
|
|
@@ -98,10 +138,13 @@ const Lego = (() => {
|
|
|
98
138
|
|
|
99
139
|
const createBatcher = () => {
|
|
100
140
|
let queued = false;
|
|
101
|
-
const
|
|
141
|
+
const blocksToUpdate = new Set();
|
|
102
142
|
let isProcessing = false;
|
|
103
143
|
|
|
104
|
-
//
|
|
144
|
+
// FIX: Queue for updates arriving during processing (prevents drops)
|
|
145
|
+
const pendingQueue = new Set();
|
|
146
|
+
|
|
147
|
+
// Optimization: Single timer for all updated hooks instead of one per block
|
|
105
148
|
let updatedTimer = null;
|
|
106
149
|
const pendingUpdated = new Set();
|
|
107
150
|
|
|
@@ -119,41 +162,200 @@ const Lego = (() => {
|
|
|
119
162
|
}, 50); // Global debounce
|
|
120
163
|
};
|
|
121
164
|
|
|
165
|
+
const processPendingQueue = () => {
|
|
166
|
+
if (pendingQueue.size > 0) {
|
|
167
|
+
// Move pending to main queue
|
|
168
|
+
pendingQueue.forEach(el => blocksToUpdate.add(el));
|
|
169
|
+
pendingQueue.clear();
|
|
170
|
+
|
|
171
|
+
// Trigger next batch if not already queued
|
|
172
|
+
if (!queued && blocksToUpdate.size > 0) {
|
|
173
|
+
queued = true;
|
|
174
|
+
requestAnimationFrame(processBatch);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const processBatch = () => {
|
|
180
|
+
isProcessing = true;
|
|
181
|
+
const batch = Array.from(blocksToUpdate);
|
|
182
|
+
blocksToUpdate.clear();
|
|
183
|
+
queued = false;
|
|
184
|
+
|
|
185
|
+
// Simple synchronous render loop (faster for small batches, no overhead)
|
|
186
|
+
batch.forEach(el => {
|
|
187
|
+
if (el.isConnected) render(el);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Batch complete: Queue updated hooks efficiently
|
|
191
|
+
batch.forEach(el => pendingUpdated.add(el));
|
|
192
|
+
scheduleUpdatedHooks();
|
|
193
|
+
isProcessing = false;
|
|
194
|
+
|
|
195
|
+
// FIX: Process pending queue after batch completes
|
|
196
|
+
processPendingQueue();
|
|
197
|
+
};
|
|
198
|
+
|
|
122
199
|
return {
|
|
123
200
|
add: (el) => {
|
|
124
|
-
if (!el
|
|
125
|
-
|
|
201
|
+
if (!el) return;
|
|
202
|
+
|
|
203
|
+
// FIX: Queue during processing instead of dropping
|
|
204
|
+
if (isProcessing) {
|
|
205
|
+
pendingQueue.add(el);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
blocksToUpdate.add(el);
|
|
126
210
|
if (queued) return;
|
|
127
211
|
queued = true;
|
|
128
212
|
|
|
129
|
-
requestAnimationFrame(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
requestAnimationFrame(processBatch);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const globalBatcher = createBatcher();
|
|
219
|
+
|
|
220
|
+
// --- Persistence Layer ($db) ---
|
|
221
|
+
const dbBindings = new Map(); // storageKey -> Set<{target, prop}>
|
|
222
|
+
const dbTimers = new Map(); // storageKey -> timerId
|
|
223
|
+
|
|
224
|
+
const registerDbBinding = (target, prop, key) => {
|
|
225
|
+
if (!dbBindings.has(key)) dbBindings.set(key, new Set());
|
|
226
|
+
dbBindings.get(key).add({ target, prop });
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Storage quota management (FIFO eviction)
|
|
230
|
+
const storageKeys = new Map(); // key -> { timestamp, size }
|
|
231
|
+
|
|
232
|
+
const evictOldestKeys = (bytesNeeded) => {
|
|
233
|
+
const entries = Array.from(storageKeys.entries())
|
|
234
|
+
.filter(([key]) => key.startsWith('lego:')) // Only evict Lego keys
|
|
235
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp); // Oldest first
|
|
236
|
+
|
|
237
|
+
let freedBytes = 0;
|
|
238
|
+
const evicted = [];
|
|
239
|
+
|
|
240
|
+
for (const [key, meta] of entries) {
|
|
241
|
+
if (freedBytes >= bytesNeeded) break;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
localStorage.removeItem(key);
|
|
245
|
+
freedBytes += meta.size;
|
|
246
|
+
evicted.push(key);
|
|
247
|
+
storageKeys.delete(key);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.error(`[Lego] Failed to evict ${key}:`, e);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return evicted;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const scheduleSave = (key, value, debounce) => {
|
|
257
|
+
if (dbTimers.has(key)) clearTimeout(dbTimers.get(key));
|
|
258
|
+
const save = () => {
|
|
259
|
+
try {
|
|
260
|
+
const serialized = JSON.stringify(value);
|
|
261
|
+
const sizeBytes = new Blob([serialized]).size;
|
|
134
262
|
|
|
135
|
-
|
|
136
|
-
|
|
263
|
+
// Attempt write
|
|
264
|
+
localStorage.setItem(key, serialized);
|
|
137
265
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
266
|
+
// Track for eviction (prefix with 'lego:' for identification)
|
|
267
|
+
const storageKey = key.startsWith('lego:') ? key : `lego:${key}`;
|
|
268
|
+
storageKeys.set(storageKey, {
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
size: sizeBytes
|
|
142
271
|
});
|
|
272
|
+
|
|
273
|
+
dbTimers.delete(key);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
if (e.name === 'QuotaExceededError') {
|
|
276
|
+
console.warn(`[Lego] Storage quota exceeded for key: ${key}`);
|
|
277
|
+
|
|
278
|
+
// Attempt eviction and retry
|
|
279
|
+
try {
|
|
280
|
+
const serialized = JSON.stringify(value);
|
|
281
|
+
const sizeBytes = new Blob([serialized]).size;
|
|
282
|
+
const evicted = evictOldestKeys(sizeBytes * 2); // Free 2x needed space
|
|
283
|
+
|
|
284
|
+
if (evicted.length > 0) {
|
|
285
|
+
console.log(`[Lego] Evicted ${evicted.length} old keys, retrying save`);
|
|
286
|
+
localStorage.setItem(key, serialized);
|
|
287
|
+
|
|
288
|
+
const storageKey = key.startsWith('lego:') ? key : `lego:${key}`;
|
|
289
|
+
storageKeys.set(storageKey, {
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
size: sizeBytes
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
config.onError(new Error(`Storage quota exceeded and no keys available for eviction`), 'quota', key);
|
|
295
|
+
}
|
|
296
|
+
} catch (retryErr) {
|
|
297
|
+
config.onError(new Error(`Critical: Could not save ${key} even after eviction`), 'quota-critical', key);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
console.error(`[Lego] Storage Error (${key}):`, e);
|
|
301
|
+
}
|
|
143
302
|
}
|
|
144
303
|
};
|
|
304
|
+
if (debounce > 0) {
|
|
305
|
+
dbTimers.set(key, setTimeout(save, debounce));
|
|
306
|
+
} else {
|
|
307
|
+
save();
|
|
308
|
+
}
|
|
145
309
|
};
|
|
146
310
|
|
|
147
|
-
|
|
311
|
+
// Cross-tab Synchronization
|
|
312
|
+
if (typeof window !== 'undefined') {
|
|
313
|
+
window.addEventListener('storage', (e) => {
|
|
314
|
+
if (!e.key || !dbBindings.has(e.key)) return;
|
|
315
|
+
try {
|
|
316
|
+
const newValue = JSON.parse(e.newValue);
|
|
317
|
+
dbBindings.get(e.key).forEach(({ target, prop, el, batcher }) => {
|
|
318
|
+
target[prop] = newValue; // Update raw data
|
|
319
|
+
if (el && batcher) batcher.add(el); // Trigger re-render
|
|
320
|
+
});
|
|
321
|
+
} catch (e) { }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const dbMetadata = new WeakMap(); // target -> { prop: { key, debounce } }
|
|
326
|
+
|
|
327
|
+
const isNode = (val) => typeof Node !== 'undefined' && val instanceof Node;
|
|
148
328
|
|
|
149
329
|
const reactive = (obj, el, batcher = globalBatcher) => {
|
|
150
|
-
if (obj === null || typeof obj !== 'object' || obj
|
|
330
|
+
if (obj === null || typeof obj !== 'object' || isNode(obj)) return obj;
|
|
151
331
|
if (proxyCache.has(obj)) return proxyCache.get(obj);
|
|
152
332
|
|
|
333
|
+
// Hydrate $db descriptors
|
|
334
|
+
for (const k in obj) {
|
|
335
|
+
const desc = obj[k];
|
|
336
|
+
if (desc && desc.__type === 'lego-db') {
|
|
337
|
+
let val = desc._default;
|
|
338
|
+
try {
|
|
339
|
+
const item = localStorage.getItem(desc.key);
|
|
340
|
+
if (item !== null) val = JSON.parse(item);
|
|
341
|
+
} catch (e) { }
|
|
342
|
+
|
|
343
|
+
obj[k] = val; // Replace descriptor with value
|
|
344
|
+
|
|
345
|
+
// Register metadata
|
|
346
|
+
if (!dbMetadata.has(obj)) dbMetadata.set(obj, {});
|
|
347
|
+
dbMetadata.get(obj)[k] = { key: desc.key, debounce: desc._debounce };
|
|
348
|
+
|
|
349
|
+
// Register for cross-tab updates
|
|
350
|
+
if (!dbBindings.has(desc.key)) dbBindings.set(desc.key, new Set());
|
|
351
|
+
dbBindings.get(desc.key).add({ target: obj, prop: k, el, batcher });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
153
355
|
const handler = {
|
|
154
356
|
get: (t, k) => {
|
|
155
357
|
const val = Reflect.get(t, k);
|
|
156
|
-
if (val !== null && typeof val === 'object' && !(val
|
|
358
|
+
if (val !== null && typeof val === 'object' && !isNode(val)) {
|
|
157
359
|
return reactive(val, el, batcher);
|
|
158
360
|
}
|
|
159
361
|
return val;
|
|
@@ -161,7 +363,14 @@ const Lego = (() => {
|
|
|
161
363
|
set: (t, k, v) => {
|
|
162
364
|
const old = t[k];
|
|
163
365
|
const r = Reflect.set(t, k, v);
|
|
164
|
-
if (old !== v)
|
|
366
|
+
if (old !== v) {
|
|
367
|
+
batcher.add(el);
|
|
368
|
+
// Check for DB binding
|
|
369
|
+
const meta = dbMetadata.get(t);
|
|
370
|
+
if (meta && meta[k]) {
|
|
371
|
+
scheduleSave(meta[k].key, v, meta[k].debounce);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
165
374
|
return r;
|
|
166
375
|
},
|
|
167
376
|
deleteProperty: (t, k) => {
|
|
@@ -618,15 +827,69 @@ const Lego = (() => {
|
|
|
618
827
|
};
|
|
619
828
|
|
|
620
829
|
const unsnap = (el) => {
|
|
830
|
+
// 1. Call unmounted lifecycle hook first
|
|
621
831
|
if (el._studs && typeof el._studs.unmounted === 'function') {
|
|
622
832
|
try { el._studs.unmounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in unmounted:`, e); }
|
|
623
833
|
}
|
|
624
834
|
|
|
835
|
+
// 2. Recursively unsnap children in shadowRoot
|
|
625
836
|
if (el.shadowRoot) {
|
|
626
837
|
[...el.shadowRoot.children].forEach(unsnap);
|
|
627
838
|
}
|
|
628
839
|
|
|
840
|
+
// 3. Remove from active blocks tracking
|
|
629
841
|
activeBlocks.delete(el);
|
|
842
|
+
|
|
843
|
+
// 4. Break circular references
|
|
844
|
+
if (el._studs) {
|
|
845
|
+
// Clear element references in state object
|
|
846
|
+
el._studs.$element = null;
|
|
847
|
+
|
|
848
|
+
// Clear $vars references to DOM elements
|
|
849
|
+
if (el._studs.$vars) {
|
|
850
|
+
Object.keys(el._studs.$vars).forEach(key => {
|
|
851
|
+
el._studs.$vars[key] = null;
|
|
852
|
+
});
|
|
853
|
+
el._studs.$vars = null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Break the proxy -> element reference
|
|
857
|
+
el._studs = null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (el.hasOwnProperty('state')) delete el.state;
|
|
861
|
+
// 6. Clear private data (bindings, flags, etc.)
|
|
862
|
+
// This removes references to DOM nodes in bindings array
|
|
863
|
+
const data = privateData.get(el);
|
|
864
|
+
if (data) {
|
|
865
|
+
// Clear bindings that hold node references
|
|
866
|
+
if (data.bindings) {
|
|
867
|
+
data.bindings.forEach(binding => {
|
|
868
|
+
binding.node = null;
|
|
869
|
+
binding.anchor = null;
|
|
870
|
+
});
|
|
871
|
+
data.bindings = null;
|
|
872
|
+
}
|
|
873
|
+
data.anchor = null;
|
|
874
|
+
privateData.delete(el);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// 7. Clear from for-loop pools (prevents pooled nodes from keeping refs)
|
|
878
|
+
if (forPools.has(el)) {
|
|
879
|
+
const pool = forPools.get(el);
|
|
880
|
+
pool.forEach((node, key) => {
|
|
881
|
+
if (node && node._studs) {
|
|
882
|
+
node._studs = null;
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
pool.clear();
|
|
886
|
+
forPools.delete(el);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// 8. Note: proxyCache is a WeakMap so we can't delete from it,
|
|
890
|
+
// but breaking the circular refs above allows GC to clean it up
|
|
891
|
+
|
|
892
|
+
// 9. Recursively unsnap light DOM children
|
|
630
893
|
[...el.children].forEach(unsnap);
|
|
631
894
|
};
|
|
632
895
|
|
|
@@ -665,14 +928,25 @@ const Lego = (() => {
|
|
|
665
928
|
|
|
666
929
|
resolvedElements.forEach(el => {
|
|
667
930
|
if (el) {
|
|
668
|
-
const
|
|
931
|
+
const block = document.createElement(match.tagName);
|
|
669
932
|
// Atomic swap: MutationObserver in init() will pick this up
|
|
670
|
-
el.replaceChildren(
|
|
933
|
+
el.replaceChildren(block);
|
|
671
934
|
}
|
|
672
935
|
});
|
|
673
936
|
};
|
|
674
937
|
|
|
938
|
+
// $db Factory
|
|
939
|
+
const dbFactory = (key) => ({
|
|
940
|
+
__type: 'lego-db',
|
|
941
|
+
key,
|
|
942
|
+
_default: undefined,
|
|
943
|
+
_debounce: 0,
|
|
944
|
+
default(v) { this._default = v; return this; },
|
|
945
|
+
debounce(ms) { this._debounce = ms; return this; }
|
|
946
|
+
});
|
|
947
|
+
|
|
675
948
|
const publicAPI = {
|
|
949
|
+
db: dbFactory,
|
|
676
950
|
snap,
|
|
677
951
|
unsnap,
|
|
678
952
|
init: async (root = document.body, options = {}) => {
|
|
@@ -702,33 +976,38 @@ const Lego = (() => {
|
|
|
702
976
|
registry[t.getAttribute('b-id')] = t;
|
|
703
977
|
});
|
|
704
978
|
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
.then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
|
|
721
|
-
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
|
|
722
|
-
}
|
|
723
|
-
}
|
|
979
|
+
const checkAndLoad = (n) => {
|
|
980
|
+
if (n.nodeType !== Node.ELEMENT_NODE) return;
|
|
981
|
+
snap(n);
|
|
982
|
+
// Auto-Discovery: Check if tag is unknown and loader is configured
|
|
983
|
+
const tagName = n.tagName.toLowerCase();
|
|
984
|
+
if (tagName.includes('-') && !registry[tagName] && config.loader && !activeBlocks.has(n)) {
|
|
985
|
+
const result = config.loader(tagName);
|
|
986
|
+
if (result) {
|
|
987
|
+
const promise = (typeof result === 'string')
|
|
988
|
+
? fetch(result).then(r => r.text())
|
|
989
|
+
: result;
|
|
990
|
+
|
|
991
|
+
Promise.resolve(promise)
|
|
992
|
+
.then(legoFile => publicAPI.defineLegoFile(legoFile, tagName + '.lego'))
|
|
993
|
+
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
|
|
724
994
|
}
|
|
725
|
-
}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const observer = new MutationObserver(m => m.forEach(r => {
|
|
999
|
+
r.addedNodes.forEach(checkAndLoad);
|
|
726
1000
|
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
|
|
727
1001
|
}));
|
|
728
1002
|
observer.observe(root, { childList: true, subtree: true });
|
|
729
1003
|
|
|
1004
|
+
// Initial Scan for everything
|
|
1005
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1006
|
+
checkAndLoad(root);
|
|
1007
|
+
while (walker.nextNode()) checkAndLoad(walker.currentNode);
|
|
1008
|
+
|
|
730
1009
|
root._studs = Lego.globals;
|
|
731
|
-
snap(root);
|
|
1010
|
+
// snap(root); // parsing moved to checkAndLoad
|
|
732
1011
|
bind(root, root);
|
|
733
1012
|
render(root);
|
|
734
1013
|
|
|
@@ -740,7 +1019,7 @@ const Lego = (() => {
|
|
|
740
1019
|
document.head.appendChild(script);
|
|
741
1020
|
}
|
|
742
1021
|
Lego.route('/_/studio', 'lego-studio');
|
|
743
|
-
Lego.route('/_/studio/:
|
|
1022
|
+
Lego.route('/_/studio/:block', 'lego-studio');
|
|
744
1023
|
}
|
|
745
1024
|
|
|
746
1025
|
if (routes.length > 0) {
|
|
@@ -772,15 +1051,16 @@ const Lego = (() => {
|
|
|
772
1051
|
},
|
|
773
1052
|
globals: reactive({
|
|
774
1053
|
$route: {
|
|
775
|
-
url: window.location.pathname,
|
|
1054
|
+
url: typeof window !== 'undefined' ? window.location.pathname : '/',
|
|
776
1055
|
route: '',
|
|
777
1056
|
params: {},
|
|
778
1057
|
query: {},
|
|
779
1058
|
method: 'GET',
|
|
780
1059
|
body: null
|
|
781
1060
|
},
|
|
782
|
-
$go: (path, ...targets) => _go(path, ...targets)(document.body)
|
|
783
|
-
|
|
1061
|
+
$go: (path, ...targets) => _go(path, ...targets)(document.body),
|
|
1062
|
+
$db: dbFactory
|
|
1063
|
+
}, typeof document !== 'undefined' ? document.body : null),
|
|
784
1064
|
defineLegoFile: (content, filename = 'block.lego') => {
|
|
785
1065
|
let template = '';
|
|
786
1066
|
let script = '{}';
|
|
@@ -828,8 +1108,7 @@ const Lego = (() => {
|
|
|
828
1108
|
}
|
|
829
1109
|
|
|
830
1110
|
const name = deriveBlockName(filename);
|
|
831
|
-
//
|
|
832
|
-
// Safe-ish because it's coming from the "Server" (trusted source in this architecture)
|
|
1111
|
+
// Inject $db helper into the scope
|
|
833
1112
|
const logicObj = new Function(`return ${script}`)();
|
|
834
1113
|
|
|
835
1114
|
if (style) {
|
|
@@ -883,4 +1162,8 @@ const Lego = (() => {
|
|
|
883
1162
|
|
|
884
1163
|
if (typeof window !== 'undefined') {
|
|
885
1164
|
window.Lego = Lego;
|
|
1165
|
+
} else if (typeof global !== 'undefined') {
|
|
1166
|
+
global.Lego = Lego;
|
|
886
1167
|
}
|
|
1168
|
+
|
|
1169
|
+
|
package/main.min.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
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", `
|
|
1
|
+
const Lego=(()=>{const y={},F=new WeakMap,S=new WeakMap,$=new WeakMap,L=new Set,z=new Map,U=new Map;class te{constructor(s=1e3){this.limit=s,this.cache=new Map}get(s){if(!this.cache.has(s))return;const a=this.cache.get(s);return this.cache.delete(s),this.cache.set(s,a),a}set(s,a){if(this.cache.has(s))this.cache.delete(s);else if(this.cache.size>=this.limit){const n=this.cache.keys().next().value;this.cache.delete(n)}this.cache.set(s,a)}get size(){return this.cache.size}clear(){this.cache.clear()}}const G=new te(1e3),K=new Map;let J={};const h={onError:(e,s,a)=>{console.error(`[Lego Error] [${s}]`,e,a)},metrics:{},syntax:"brackets"},x=()=>h.syntax==="brackets"?["[[","]]"]:["{{","}}"],k=()=>{const[e,s]=x(),a=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`${a}(.*?)${n}`,"g")},q=[],le=e=>typeof e!="string"?e:e.replace(/[&<>"']/g,s=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[s]),se=e=>{const a=e.split("/").pop().replace(/\.lego$/,"").replace(/_/g,"-").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();if(!a.includes("-"))throw new Error(`[Lego] Invalid block definition: "${e}". Block names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);return a},ne=(e,s)=>{if(typeof e=="function"){const n=Array.from(document.querySelectorAll("*")).filter(t=>t.tagName.includes("-"));return[].concat(e(n))}if(e.startsWith("#")){const n=document.getElementById(e.slice(1));return n?[n]:[]}const a=s?.querySelectorAll(e)||[];return a.length>0?[...a]:[...document.querySelectorAll(e)]},j=(e,...s)=>a=>{const n=async(t,c=null,r=!0,o={})=>{if(r){const d={legoTargets:s.filter(l=>typeof l=="string"),method:t,body:c};history.pushState(d,"",e)}await I(s.length?s:null,a)};return{get:(t=!0,c={})=>n("GET",null,t,c),post:(t,c=!0,r={})=>n("POST",t,c,r),put:(t,c=!0,r={})=>n("PUT",t,c,r),patch:(t,c=!0,r={})=>n("PATCH",t,c,r),delete:(t=!0,c={})=>n("DELETE",null,t,c)}},oe=(()=>{let e=!1;const s=new Set;let a=!1;const n=new Set;let t=null;const c=new Set,r=()=>{t&&clearTimeout(t),t=setTimeout(()=>{c.forEach(d=>{const l=d._studs;if(l&&typeof l.updated=="function")try{l.updated.call(l)}catch(u){console.error("[Lego] Error in updated hook:",u)}}),c.clear(),t=null},50)},o=()=>{n.size>0&&(n.forEach(d=>s.add(d)),n.clear(),!e&&s.size>0&&(e=!0,requestAnimationFrame(i)))},i=()=>{a=!0;const d=Array.from(s);s.clear(),e=!1,d.forEach(l=>{l.isConnected&&D(l)}),d.forEach(l=>c.add(l)),r(),a=!1,o()};return{add:d=>{if(d){if(a){n.add(d);return}s.add(d),!e&&(e=!0,requestAnimationFrame(i))}}}})(),w=new Map,O=new Map,ue=(e,s,a)=>{w.has(a)||w.set(a,new Set),w.get(a).add({target:e,prop:s})},M=new Map,re=e=>{const s=Array.from(M.entries()).filter(([t])=>t.startsWith("lego:")).sort((t,c)=>t[1].timestamp-c[1].timestamp);let a=0;const n=[];for(const[t,c]of s){if(a>=e)break;try{localStorage.removeItem(t),a+=c.size,n.push(t),M.delete(t)}catch(r){console.error(`[Lego] Failed to evict ${t}:`,r)}}return n},ae=(e,s,a)=>{O.has(e)&&clearTimeout(O.get(e));const n=()=>{try{const t=JSON.stringify(s),c=new Blob([t]).size;localStorage.setItem(e,t);const r=e.startsWith("lego:")?e:`lego:${e}`;M.set(r,{timestamp:Date.now(),size:c}),O.delete(e)}catch(t){if(t.name==="QuotaExceededError"){console.warn(`[Lego] Storage quota exceeded for key: ${e}`);try{const c=JSON.stringify(s),r=new Blob([c]).size,o=re(r*2);if(o.length>0){console.log(`[Lego] Evicted ${o.length} old keys, retrying save`),localStorage.setItem(e,c);const i=e.startsWith("lego:")?e:`lego:${e}`;M.set(i,{timestamp:Date.now(),size:r})}else h.onError(new Error("Storage quota exceeded and no keys available for eviction"),"quota",e)}catch{h.onError(new Error(`Critical: Could not save ${e} even after eviction`),"quota-critical",e)}}else console.error(`[Lego] Storage Error (${e}):`,t)}};a>0?O.set(e,setTimeout(n,a)):n()};typeof window<"u"&&window.addEventListener("storage",e=>{if(!(!e.key||!w.has(e.key)))try{const s=JSON.parse(e.newValue);w.get(e.key).forEach(({target:a,prop:n,el:t,batcher:c})=>{a[n]=s,t&&c&&c.add(t)})}catch{}});const C=new WeakMap,V=e=>typeof Node<"u"&&e instanceof Node,B=(e,s,a=oe)=>{if(e===null||typeof e!="object"||V(e))return e;if(F.has(e))return F.get(e);for(const c in e){const r=e[c];if(r&&r.__type==="lego-db"){let o=r._default;try{const i=localStorage.getItem(r.key);i!==null&&(o=JSON.parse(i))}catch{}e[c]=o,C.has(e)||C.set(e,{}),C.get(e)[c]={key:r.key,debounce:r._debounce},w.has(r.key)||w.set(r.key,new Set),w.get(r.key).add({target:e,prop:c,el:s,batcher:a})}}const n={get:(c,r)=>{const o=Reflect.get(c,r);return o!==null&&typeof o=="object"&&!V(o)?B(o,s,a):o},set:(c,r,o)=>{const i=c[r],d=Reflect.set(c,r,o);if(i!==o){a.add(s);const l=C.get(c);l&&l[r]&&ae(l[r].key,o,l[r].debounce)}return d},deleteProperty:(c,r)=>{const o=Reflect.deleteProperty(c,r);return a.add(s),o}},t=new Proxy(e,n);return F.set(e,t),t},X=(e,s={})=>{try{return new Function("scope","global",`with(global) { with(scope) { return (${e}); } }`)(s,Lego.globals)}catch{return{}}},_=e=>(S.has(e)||S.set(e,{snapped:!1,bindings:null,bound:!1,rendering:!1,anchor:null,hasGlobalDependency:!1}),S.get(e)),H=(e,s)=>{if(!e)return"";const a=e.trim().split(".");let n=s;for(const t of a){if(n==null)return"";n=n[t]}return n??""},P=(e,s)=>{if(!e)return;let a=e.parentElement||(e.getRootNode?e.getRootNode().host:null);for(;a;){const n=a.tagName?a.tagName.toLowerCase():"";if(n&&(s==="*"&&y[n]||n===s.toLowerCase()))return a;a=a.parentElement||a.getRootNode&&a.getRootNode().host}},b=(e,s,a=!1)=>{if(/\b(function|eval|import|class|module|deploy|constructor|__proto__)\b/.test(e)){console.warn(`[Lego] Security Warning: Blocked dangerous expression "${e}"`);return}try{const n=s.state||{};let t=G.get(e);t||(t=new Function("global","self","event","helpers",`
|
|
5
2
|
with(helpers) {
|
|
6
3
|
with(this) {
|
|
7
4
|
return ${e}
|
|
8
5
|
}
|
|
9
6
|
}
|
|
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);
|
|
7
|
+
`),G.set(e,t));const c={$ancestors:o=>P(s.self,o),$registry:o=>U.get(o.toLowerCase()),$element:s.self,$route:Lego.globals.$route,$go:(o,...i)=>j(o,...i)(s.self),$emit:(o,i)=>{s.self.dispatchEvent(new CustomEvent(o,{detail:i,bubbles:!0,composed:!0}))}},r=t.call(n,s.global,s.self,s.event,c);return typeof r=="function"?r.call(n,s.event):r}catch(n){if(a)throw n;h.onError(n,"render-error",s.self);return}},Q=(e,s)=>{if(e.type==="checkbox")e.checked!==!!s&&(e.checked=!!s);else{const a=s==null?"":String(s);e.value!==a&&(e.value=a)}},W=(e,s,a=null)=>{const n=s._studs,t=o=>{const i=_(o);if(!i.bound){if(o.hasAttributes()){const d=o.attributes;for(let l=0;l<d.length;l++){const u=d[l];if(u.name.startsWith("@")){const g=u.name.slice(1);o.addEventListener(g,p=>{try{let f=n;if(a){const E=b(a.listName,{state:n,global:Lego.globals,self:s})[a.index];f=Object.assign(Object.create(n),{[a.name]:E})}b(u.value,{state:f,global:Lego.globals,self:o,event:p},!0)}catch(f){h.onError(f,"event-handler",o)}})}}if(o.hasAttribute("b-sync")){const l=o.getAttribute("b-sync"),u=()=>{try{let g,p;if(a&&l.startsWith(a.name+".")){const E=b(a.listName,{state:n,global:Lego.globals,self:s})[a.index];if(!E)return;const N=l.split(".").slice(1);p=N.pop(),g=N.reduce((T,v)=>T[v],E)}else{const m=l.split(".");p=m.pop(),g=m.reduce((E,N)=>E[N],n)}const f=o.type==="checkbox"?o.checked:o.value;g&&g[p]!==f&&(g[p]=f)}catch(g){h.onError(g,"sync-update",o)}};o.addEventListener("input",u),o.addEventListener("change",u)}if(o.hasAttribute("b-var")){const l=o.getAttribute("b-var");n.$vars&&(n.$vars[l]=o)}}i.bound=!0}};e instanceof Element&&t(e);const c=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);let r;for(;r=c.nextNode();)t(r)},ce=e=>{const s=[],a=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let n;for(;n=a.nextNode();){if((r=>{let o=r.parentNode;for(;o&&o!==e;){if(o.hasAttribute&&o.hasAttribute("b-for"))return!0;o=o.parentNode}return!1})(n))continue;const c=r=>{if(/\bglobal\b/.test(r)){const o=e.host||e;_(o).hasGlobalDependency=!0}};if(n.nodeType===Node.ELEMENT_NODE){if(n.hasAttribute("b-if")){const o=n.getAttribute("b-if");c(o);const i=document.createComment(`b-if: ${o}`),d=_(n);d.anchor=i,s.push({type:"b-if",node:n,anchor:i,expr:o})}if(n.hasAttribute("b-show")){const o=n.getAttribute("b-show");c(o),s.push({type:"b-show",node:n,expr:o})}if(n.hasAttribute("b-for")){const o=n.getAttribute("b-for").match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/);o&&(c(o[2]),s.push({type:"b-for",node:n,itemName:o[1],listName:o[2].trim(),template:n.cloneNode(!0)}),n.innerHTML="")}if(n.hasAttribute("b-text")&&s.push({type:"b-text",node:n,path:n.getAttribute("b-text")}),n.hasAttribute("b-html")){const o=n.getAttribute("b-html");c(o),s.push({type:"b-html",node:n,expr:o})}n.hasAttribute("b-sync")&&s.push({type:"b-sync",node:n});const[r]=x();[...n.attributes].forEach(o=>{o.value.includes(r)&&(c(o.value),s.push({type:"attr",node:n,attrName:o.name,template:o.value}))})}else if(n.nodeType===Node.TEXT_NODE){const[r]=x();n.textContent.includes(r)&&(c(n.textContent),s.push({type:"text",node:n,template:n.textContent}))}}return s},ie=(e,s)=>{const a=c=>{if(c.nodeType===Node.TEXT_NODE){c._tpl===void 0&&(c._tpl=c.textContent);const r=c._tpl.replace(k(),(o,i)=>b(i.trim(),{state:s,global:Lego.globals,self:c})??"");c.textContent!==r&&(c.textContent=r)}else if(c.nodeType===Node.ELEMENT_NODE){const[r]=x();[...c.attributes].forEach(o=>{if(o._tpl===void 0&&(o._tpl=o.value),o._tpl.includes(r)){const i=o._tpl.replace(k(),(d,l)=>b(l.trim(),{state:s,global:Lego.globals,self:c})??"");o.value!==i&&(o.value=i,o.name==="class"&&(c.className=i))}})}};a(e);const n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT);let t;for(;t=n.nextNode();)a(t)},D=e=>{const s=e._studs;if(!s)return;const a=_(e);if(!a.rendering){a.rendering=!0,h.metrics&&h.metrics.onRenderStart&&h.metrics.onRenderStart(e);try{const n=e.shadowRoot||e;a.bindings||(a.bindings=ce(n)),a.bindings.forEach(t=>{if(t.type==="b-if"){const c=!!b(t.expr,{state:s,global:Lego.globals,self:t.node}),r=!!t.node.parentNode;c&&!r?t.anchor.parentNode&&t.anchor.parentNode.replaceChild(t.node,t.anchor):!c&&r&&t.node.parentNode.replaceChild(t.anchor,t.node)}if(t.type==="b-show"&&(t.node.style.display=b(t.expr,{state:s,global:Lego.globals,self:t.node})?"":"none"),t.type==="b-text"&&(t.node.textContent=H(t.path,s)),t.type==="b-html"&&(t.node.innerHTML=b(t.expr,{state:s,global:Lego.globals,self:t.node})||""),t.type==="b-sync"&&Q(t.node,H(t.node.getAttribute("b-sync"),s)),t.type==="text"){const c=t.template.replace(k(),(r,o)=>b(o.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.textContent!==c&&(t.node.textContent=c)}if(t.type==="attr"){const c=t.template.replace(k(),(r,o)=>b(o.trim(),{state:s,global:Lego.globals,self:t.node})??"");t.node.getAttribute(t.attrName)!==c&&(t.node.setAttribute(t.attrName,c),t.attrName==="class"&&(t.node.className=c))}if(t.type==="b-for"){const c=b(t.listName,{state:s,global:Lego.globals,self:e})||[];$.has(t.node)||$.set(t.node,new Map);const r=$.get(t.node),o=new Set;c.forEach((i,d)=>{const l=i&&typeof i=="object"?i.__id||(i.__id=Math.random()):`${d}-${i}`;o.add(l);let u=r.get(l);u||(u=t.template.cloneNode(!0),u.removeAttribute("b-for"),r.set(l,u),W(u,e,{name:t.itemName,listName:t.listName,index:d}));const g=Object.assign(Object.create(s),{[t.itemName]:i});ie(u,g),u.querySelectorAll("[b-sync]").forEach(p=>{const f=p.getAttribute("b-sync");if(f.startsWith(t.itemName+".")){const m=b(t.listName,{state:s,global:Lego.globals,self:e});Q(p,H(f.split(".").slice(1).join("."),m[d]))}}),t.node.children[d]!==u&&t.node.insertBefore(u,t.node.children[d]||null)});for(const[i,d]of r.entries())o.has(i)||(d.remove(),r.delete(i))}}),s===Lego.globals&&L.forEach(t=>{_(t).hasGlobalDependency&&D(t)})}catch(n){h.onError(n,"render",e)}finally{h.metrics&&h.metrics.onRenderEnd&&h.metrics.onRenderEnd(e),a.rendering=!1}}},A=e=>{if(!e||e.nodeType!==Node.ELEMENT_NODE)return;const s=_(e),a=e.tagName.toLowerCase(),n=y[a];if(n&&!s.snapped){s.snapped=!0;const c=n.content.cloneNode(!0),r=e.attachShadow({mode:"open"}),o=(n.getAttribute("b-stylesheets")||"").split(/\s+/).filter(Boolean);if(o.length>0){const f=o.flatMap(m=>K.get(m)||[]);f.length>0&&(r.adoptedStyleSheets=[...f])}const i=P(e,"*")||P(e.getRootNode().host,"*"),d=i&&i.state?i.state:{},l=z.get(a)||{},u=X(n.getAttribute("b-logic")||n.getAttribute("b-data")||"{}"),g=X(e.getAttribute("b-logic")||e.getAttribute("b-data")||"{}",d);e._studs=B({...l,...u,...g,$vars:{},$element:e,get $parent(){return P(e,"*")},$emit:(f,m)=>{e.dispatchEvent(new CustomEvent(f,{detail:m,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(c);const p=r.querySelector("style");if(p&&(p.textContent=p.textContent.replace(/\bself\b/g,":host")),W(r,e),L.add(e),D(e),[...r.children].forEach(A),typeof e._studs.mounted=="function")try{e._studs.mounted.call(e._studs)}catch(f){h.onError(f,"mounted",e)}}let t=e.parentElement;for(;t&&!t._studs;)t=t.parentElement;t&&t._studs&&W(e,t),[...e.children].forEach(A)},R=e=>{if(e._studs&&typeof e._studs.unmounted=="function")try{e._studs.unmounted.call(e._studs)}catch(a){console.error("[Lego] Error in unmounted:",a)}e.shadowRoot&&[...e.shadowRoot.children].forEach(R),L.delete(e),e._studs&&(e._studs.$element=null,e._studs.$vars&&(Object.keys(e._studs.$vars).forEach(a=>{e._studs.$vars[a]=null}),e._studs.$vars=null),e._studs=null),e.hasOwnProperty("state")&&delete e.state;const s=S.get(e);if(s&&(s.bindings&&(s.bindings.forEach(a=>{a.node=null,a.anchor=null}),s.bindings=null),s.anchor=null,S.delete(e)),$.has(e)){const a=$.get(e);a.forEach((n,t)=>{n&&n._studs&&(n._studs=null)}),a.clear(),$.delete(e)}[...e.children].forEach(R)},I=async(e=null,s=null)=>{const a=window.location.pathname,n=window.location.search,t=q.find(d=>d.regex.test(a));if(!t)return;let c=[];if(e)c=e.flatMap(d=>ne(d,s));else{const d=document.querySelector("lego-router");d&&(c=[d])}if(c.length===0)return;const r=a.match(t.regex).slice(1),o=Object.fromEntries(t.paramNames.map((d,l)=>[d,r[l]])),i=Object.fromEntries(new URLSearchParams(n));t.middleware&&!await t.middleware(o,Lego.globals)||(Lego.globals.$route.url=a+n,Lego.globals.$route.route=t.path,Lego.globals.$route.params=o,Lego.globals.$route.query=i,Lego.globals.$route.method=history.state?.method||"GET",Lego.globals.$route.body=history.state?.body||null,c.forEach(d=>{if(d){const l=document.createElement(t.tagName);d.replaceChildren(l)}}))},Z=e=>({__type:"lego-db",key:e,_default:void 0,_debounce:0,default(s){return this._default=s,this},debounce(s){return this._debounce=s,this}}),Y={db:Z,snap:A,unsnap:R,init:async(e=document.body,s={})=>{(!e||typeof e.nodeType!="number")&&(e=document.body),J=s.styles||{},h.loader=s.loader;const a=Object.entries(J).map(async([r,o])=>{const i=await Promise.all(o.map(async d=>{try{const u=await(await fetch(d)).text(),g=new CSSStyleSheet;return await g.replace(u),g}catch(l){return console.error(`[Lego] Failed to load stylesheet: ${d}`,l),null}}));K.set(r,i.filter(d=>d!==null))});await Promise.all(a),document.querySelectorAll("template[b-id]").forEach(r=>{y[r.getAttribute("b-id")]=r});const n=r=>{if(r.nodeType!==Node.ELEMENT_NODE)return;A(r);const o=r.tagName.toLowerCase();if(o.includes("-")&&!y[o]&&h.loader&&!L.has(r)){const i=h.loader(o);if(i){const d=typeof i=="string"?fetch(i).then(l=>l.text()):i;Promise.resolve(d).then(l=>Y.defineLegoFile(l,o+".lego")).catch(l=>console.error(`[Lego] Failed to load ${o}:`,l))}}};new MutationObserver(r=>r.forEach(o=>{o.addedNodes.forEach(n),o.removedNodes.forEach(i=>i.nodeType===Node.ELEMENT_NODE&&R(i))})).observe(e,{childList:!0,subtree:!0});const c=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT);for(n(e);c.nextNode();)n(c.currentNode);if(e._studs=Lego.globals,W(e,e),D(e),s.studio){if(!y["lego-studio"]){const r=document.createElement("script");r.src="https://unpkg.com/@legodom/studio@0.0.2/dist/lego-studio.js",r.onerror=()=>console.warn("[Lego] Failed to load Studio from CDN"),document.head.appendChild(r)}Lego.route("/_/studio","lego-studio"),Lego.route("/_/studio/:block","lego-studio")}q.length>0&&(window.addEventListener("popstate",r=>{const o=r.state?.legoTargets||null;I(o)}),document.addEventListener("submit",r=>{r.preventDefault()}),document.addEventListener("click",r=>{const i=r.composedPath().find(d=>d.tagName==="A"&&(d.hasAttribute("b-target")||d.hasAttribute("b-link")));if(i){r.preventDefault();const d=i.getAttribute("href"),l=i.getAttribute("b-target"),u=l?l.split(/\s+/).filter(Boolean):[],g=i.getAttribute("b-link")!=="false";Lego.globals.$go(d,...u).get(g)}}),I())},globals:B({$route:{url:typeof window<"u"?window.location.pathname:"/",route:"",params:{},query:{},method:"GET",body:null},$go:(e,...s)=>j(e,...s)(document.body),$db:Z},typeof document<"u"?document.body:null),defineLegoFile:(e,s="block.lego")=>{let a="",n="{}",t="",c="",r=e;const o=/<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i;for(;r;){const l=r.match(o);if(!l)break;const u=l[1].toLowerCase(),g=l[2],p=l[0],f=l.index,m=`</${u}>`,E=f+p.length,N=r.indexOf(m,E);if(N===-1){console.warn(`[Lego] Unclosed <${u}> tag in ${s}`);break}const T=r.slice(E,N);if(u==="template"){a=T.trim();const v=g.match(/b-stylesheets=["']([^"']+)["']/);v&&(t=v[1])}else if(u==="script"){const v=T.trim(),ee=v.match(/export\s+default\s+({[\s\S]*})/);n=ee?ee[1]:v}else u==="style"&&(c=T.trim());r=r.slice(N+m.length)}const i=se(s),d=new Function(`return ${n}`)();c&&(a=`<style>${c}</style>`+a),y[i]=document.createElement("template"),y[i].innerHTML=a,y[i].setAttribute("b-stylesheets",t),z.set(i,d),document.querySelectorAll(i).forEach(l=>!_(l).snapped&&A(l))},block:(e,s,a={},n="")=>{const t=document.createElement("template");t.setAttribute("b-id",e),t.setAttribute("b-stylesheets",n),t.innerHTML=s,y[e]=t,z.set(e,a);try{U.set(e.toLowerCase(),B({...a},document.body))}catch(c){h.onError(c,"define",e)}document.querySelectorAll(e).forEach(A)},get define(){return this.block},getActiveBlocksCount:()=>L.size,getLegos:()=>Object.keys(y),config:h,route:(e,s,a=null)=>{const n=[],t=e.replace(/:([^\/]+)/g,(c,r)=>(n.push(r),"([^/]+)"));q.push({path:e,regex:new RegExp(`^${t}$`),tagName:s,paramNames:n,middleware:a})}};return Y})();typeof window<"u"?window.Lego=Lego:typeof global<"u"&&(global.Lego=Lego);
|
package/monitoring-plugin.js
CHANGED
|
@@ -9,7 +9,7 @@ export const Monitoring = {
|
|
|
9
9
|
renders: 0,
|
|
10
10
|
errors: 0,
|
|
11
11
|
slowRenders: 0,
|
|
12
|
-
|
|
12
|
+
blocks: new Map(), // tagName -> { count, avgTime }
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
// Configuration options
|
|
@@ -83,7 +83,7 @@ export const Monitoring = {
|
|
|
83
83
|
this.metrics.renders = 0;
|
|
84
84
|
this.metrics.errors = 0;
|
|
85
85
|
this.metrics.slowRenders = 0;
|
|
86
|
-
this.metrics.
|
|
86
|
+
this.metrics.blocks.clear();
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
|
|
@@ -99,11 +99,11 @@ export const Monitoring = {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
if (!this.metrics.
|
|
103
|
-
this.metrics.
|
|
102
|
+
if (!this.metrics.blocks.has(tagName)) {
|
|
103
|
+
this.metrics.blocks.set(tagName, { count: 0, totalTime: 0, avg: 0 });
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const stats = this.metrics.
|
|
106
|
+
const stats = this.metrics.blocks.get(tagName);
|
|
107
107
|
stats.count++;
|
|
108
108
|
stats.totalTime += duration;
|
|
109
109
|
stats.avg = stats.totalTime / stats.count;
|