lego-dom 1.0.0 → 1.3.4
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/.legodom +87 -0
- package/CHANGELOG.md +87 -3
- package/cdn.html +10 -5
- package/docs/.vitepress/config.js +23 -7
- package/docs/api/config.md +95 -0
- package/docs/api/define.md +29 -2
- package/docs/api/directives.md +10 -2
- package/docs/api/index.md +1 -0
- package/docs/contributing/01-welcome.md +2 -0
- package/docs/contributing/02-registry.md +37 -3
- package/docs/contributing/06-init.md +13 -2
- package/docs/contributing/07-observer.md +3 -0
- package/docs/contributing/08-snap.md +15 -1
- package/docs/contributing/10-studs.md +3 -1
- package/docs/contributing/11-scanner.md +13 -0
- package/docs/contributing/12-render.md +32 -10
- package/docs/contributing/13-directives.md +19 -1
- package/docs/contributing/14-events.md +1 -1
- package/docs/contributing/15-router.md +49 -1
- package/docs/contributing/16-state.md +9 -10
- package/docs/contributing/17-legodom.md +1 -8
- package/docs/contributing/index.md +23 -4
- package/docs/examples/form.md +1 -1
- package/docs/examples/index.md +3 -3
- package/docs/examples/routing.md +10 -10
- package/docs/examples/sfc-showcase.md +1 -1
- package/docs/examples/todo-app.md +7 -7
- package/docs/guide/cdn-usage.md +44 -18
- package/docs/guide/components.md +18 -12
- package/docs/guide/directives.md +131 -22
- package/docs/guide/directory-structure.md +248 -0
- package/docs/guide/faq.md +210 -0
- package/docs/guide/getting-started.md +14 -10
- package/docs/guide/index.md +1 -1
- package/docs/guide/lifecycle.md +32 -0
- package/docs/guide/quick-start.md +4 -4
- package/docs/guide/reactivity.md +2 -2
- package/docs/guide/routing.md +69 -8
- package/docs/guide/server-side.md +134 -0
- package/docs/guide/sfc.md +96 -13
- package/docs/guide/templating.md +62 -57
- package/docs/index.md +9 -9
- package/docs/router/basic-routing.md +8 -8
- package/docs/router/cold-entry.md +2 -2
- package/docs/router/history.md +7 -7
- package/docs/router/index.md +1 -1
- package/docs/router/resolver.md +5 -5
- package/docs/router/surgical-swaps.md +5 -5
- package/docs/tutorial/01-project-setup.md +152 -0
- package/docs/tutorial/02-your-first-component.md +226 -0
- package/docs/tutorial/03-adding-routes.md +279 -0
- package/docs/tutorial/04-multi-page-app.md +329 -0
- package/docs/tutorial/05-state-and-globals.md +285 -0
- package/docs/tutorial/index.md +40 -0
- package/examples/vite-app/index.html +1 -0
- package/examples/vite-app/src/app.js +2 -2
- package/examples/vite-app/src/components/side-menu.lego +46 -0
- package/examples/vite-app/vite.config.js +2 -1
- package/main.js +261 -72
- package/main.min.js +7 -0
- package/monitoring-plugin.js +111 -0
- package/package.json +4 -2
- package/parse-lego.js +49 -22
- package/tests/error.test.js +74 -0
- package/tests/main.test.js +2 -2
- package/tests/memory.test.js +68 -0
- package/tests/monitoring.test.js +74 -0
- package/tests/naming.test.js +74 -0
- package/tests/parse-lego.test.js +2 -2
- package/tests/security.test.js +67 -0
- package/tests/server.test.js +114 -0
- package/tests/syntax.test.js +67 -0
- package/vite-plugin.js +3 -2
- package/docs/guide/contributing.md +0 -32
package/main.js
CHANGED
|
@@ -5,10 +5,31 @@ const Lego = (() => {
|
|
|
5
5
|
|
|
6
6
|
const sfcLogic = new Map();
|
|
7
7
|
const sharedStates = new Map();
|
|
8
|
+
const expressionCache = new Map(); // Cache for compiled expressions
|
|
8
9
|
|
|
9
10
|
const styleRegistry = new Map();
|
|
10
11
|
let styleConfig = {};
|
|
11
12
|
|
|
13
|
+
const config = {
|
|
14
|
+
onError: (err, type, el) => {
|
|
15
|
+
console.error(`[Lego Error] [${type}]`, err, el);
|
|
16
|
+
},
|
|
17
|
+
metrics: {}, // Performance hooks
|
|
18
|
+
syntax: 'brackets' // 'brackets' ([[ ]]) or 'mustache' ({{ }})
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getDelimiters = () => {
|
|
22
|
+
return config.syntax === 'brackets' ? ['[[', ']]'] : ['{{', '}}'];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createRegex = () => {
|
|
26
|
+
const [start, end] = getDelimiters();
|
|
27
|
+
// Escape special regex characters
|
|
28
|
+
const s = start.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
const e = end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
return new RegExp(`${s}(.*?)${e}`, 'g');
|
|
31
|
+
};
|
|
32
|
+
|
|
12
33
|
const routes = [];
|
|
13
34
|
|
|
14
35
|
const escapeHTML = (str) => {
|
|
@@ -18,6 +39,21 @@ const Lego = (() => {
|
|
|
18
39
|
}[m]));
|
|
19
40
|
};
|
|
20
41
|
|
|
42
|
+
const deriveComponentName = (filename) => {
|
|
43
|
+
const basename = filename.split('/').pop().replace(/\.lego$/, '');
|
|
44
|
+
// 1. Convert snake_case to kebab-case
|
|
45
|
+
// 2. Convert PascalCase/camelCase to kebab-case
|
|
46
|
+
const name = basename
|
|
47
|
+
.replace(/_/g, '-')
|
|
48
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
49
|
+
.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (!name.includes('-')) {
|
|
52
|
+
throw new Error(`[Lego] Invalid component definition: "${filename}". Component names must contain a hyphen (e.g. user-card.lego or UserCard.lego).`);
|
|
53
|
+
}
|
|
54
|
+
return name;
|
|
55
|
+
};
|
|
56
|
+
|
|
21
57
|
/**
|
|
22
58
|
* Enterprise Target Resolver
|
|
23
59
|
* Resolves strings (#id, tag-name) or functions into DOM Elements.
|
|
@@ -168,20 +204,37 @@ const Lego = (() => {
|
|
|
168
204
|
return undefined;
|
|
169
205
|
};
|
|
170
206
|
|
|
171
|
-
const safeEval = (expr, context) => {
|
|
207
|
+
const safeEval = (expr, context, throwError = false) => {
|
|
208
|
+
// 1. Security: Block dangerous patterns
|
|
209
|
+
if (/\b(function|eval|import|class|module|deploy|constructor|__proto__)\b/.test(expr)) {
|
|
210
|
+
console.warn(`[Lego] Security Warning: Blocked dangerous expression "${expr}"`);
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
172
214
|
try {
|
|
173
215
|
const scope = context.state || {};
|
|
174
216
|
|
|
217
|
+
// 2. Performance: Check cache
|
|
218
|
+
// We purposefully cache based on the expression string alone.
|
|
219
|
+
// The 'helpers' and 'state' are passed dynamically to the cached function.
|
|
220
|
+
let func = expressionCache.get(expr);
|
|
221
|
+
if (!func) {
|
|
222
|
+
// Removed inner try-catch to allow error propagation to the caller
|
|
223
|
+
func = new Function('global', 'self', 'event', 'helpers', `
|
|
224
|
+
with(helpers) {
|
|
225
|
+
with(this) {
|
|
226
|
+
return ${expr}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
expressionCache.set(expr, func);
|
|
231
|
+
}
|
|
232
|
+
|
|
175
233
|
const helpers = {
|
|
176
234
|
$ancestors: (tag) => findAncestorState(context.self, tag),
|
|
177
235
|
$registry: (tag) => sharedStates.get(tag.toLowerCase()),
|
|
178
236
|
$element: context.self,
|
|
179
237
|
$route: Lego.globals.$route,
|
|
180
|
-
/**
|
|
181
|
-
* The $go helper for surgical routing
|
|
182
|
-
* @param {string} path - URL to navigate to
|
|
183
|
-
* @param {...(string|function)} targets - Specific components or IDs to swap
|
|
184
|
-
*/
|
|
185
238
|
$go: (path, ...targets) => _go(path, ...targets)(context.self),
|
|
186
239
|
$emit: (name, detail) => {
|
|
187
240
|
context.self.dispatchEvent(new CustomEvent(name, {
|
|
@@ -192,18 +245,12 @@ const Lego = (() => {
|
|
|
192
245
|
}
|
|
193
246
|
};
|
|
194
247
|
|
|
195
|
-
const func = new Function('global', 'self', 'event', 'helpers', `
|
|
196
|
-
with(helpers) {
|
|
197
|
-
with(this) {
|
|
198
|
-
try { return ${expr} } catch(e) { return undefined; }
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
`);
|
|
202
|
-
|
|
203
248
|
const result = func.call(scope, context.global, context.self, context.event, helpers);
|
|
204
249
|
if (typeof result === 'function') return result.call(scope, context.event);
|
|
205
250
|
return result;
|
|
206
251
|
} catch (e) {
|
|
252
|
+
if (throwError) throw e;
|
|
253
|
+
config.onError(e, 'render-error', context.self);
|
|
207
254
|
return undefined;
|
|
208
255
|
}
|
|
209
256
|
};
|
|
@@ -219,51 +266,76 @@ const Lego = (() => {
|
|
|
219
266
|
|
|
220
267
|
const bind = (container, componentRoot, loopCtx = null) => {
|
|
221
268
|
const state = componentRoot._studs;
|
|
222
|
-
const elements = container instanceof Element ? [container, ...container.querySelectorAll('*')] : container.querySelectorAll('*');
|
|
223
269
|
|
|
224
|
-
|
|
270
|
+
const bindNode = (child) => {
|
|
225
271
|
const childData = getPrivateData(child);
|
|
226
272
|
if (childData.bound) return;
|
|
227
273
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
274
|
+
if (child.hasAttributes()) {
|
|
275
|
+
const attrs = child.attributes;
|
|
276
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
277
|
+
const attr = attrs[i];
|
|
278
|
+
if (attr.name.startsWith('@')) {
|
|
279
|
+
const eventName = attr.name.slice(1);
|
|
280
|
+
child.addEventListener(eventName, (event) => {
|
|
281
|
+
try {
|
|
282
|
+
let evalScope = state;
|
|
283
|
+
if (loopCtx) {
|
|
284
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
285
|
+
const item = list[loopCtx.index];
|
|
286
|
+
evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
|
|
287
|
+
}
|
|
288
|
+
safeEval(attr.value, { state: evalScope, global: Lego.globals, self: child, event }, true);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
config.onError(err, 'event-handler', child);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (child.hasAttribute('b-sync')) {
|
|
297
|
+
const prop = child.getAttribute('b-sync');
|
|
298
|
+
const updateState = () => {
|
|
299
|
+
try {
|
|
300
|
+
let target, last;
|
|
301
|
+
if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
|
|
302
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
303
|
+
const item = list[loopCtx.index];
|
|
304
|
+
if (!item) return;
|
|
305
|
+
const subPath = prop.split('.').slice(1);
|
|
306
|
+
last = subPath.pop();
|
|
307
|
+
target = subPath.reduce((o, k) => o[k], item);
|
|
308
|
+
} else {
|
|
309
|
+
const keys = prop.split('.');
|
|
310
|
+
last = keys.pop();
|
|
311
|
+
target = keys.reduce((o, k) => o[k], state);
|
|
312
|
+
}
|
|
313
|
+
const newVal = child.type === 'checkbox' ? child.checked : child.value;
|
|
314
|
+
if (target && target[last] !== newVal) target[last] = newVal;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
config.onError(err, 'sync-update', child);
|
|
237
317
|
}
|
|
238
|
-
|
|
239
|
-
|
|
318
|
+
};
|
|
319
|
+
child.addEventListener('input', updateState);
|
|
320
|
+
child.addEventListener('change', updateState);
|
|
240
321
|
}
|
|
241
|
-
});
|
|
242
322
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
249
|
-
const item = list[loopCtx.index];
|
|
250
|
-
if (!item) return;
|
|
251
|
-
const subPath = prop.split('.').slice(1);
|
|
252
|
-
last = subPath.pop();
|
|
253
|
-
target = subPath.reduce((o, k) => o[k], item);
|
|
254
|
-
} else {
|
|
255
|
-
const keys = prop.split('.');
|
|
256
|
-
last = keys.pop();
|
|
257
|
-
target = keys.reduce((o, k) => o[k], state);
|
|
323
|
+
// b-var: Register element reference
|
|
324
|
+
if (child.hasAttribute('b-var')) {
|
|
325
|
+
const varName = child.getAttribute('b-var');
|
|
326
|
+
if (state.$vars) {
|
|
327
|
+
state.$vars[varName] = child;
|
|
258
328
|
}
|
|
259
|
-
|
|
260
|
-
if (target && target[last] !== newVal) target[last] = newVal;
|
|
261
|
-
};
|
|
262
|
-
child.addEventListener('input', updateState);
|
|
263
|
-
child.addEventListener('change', updateState);
|
|
329
|
+
}
|
|
264
330
|
}
|
|
265
331
|
childData.bound = true;
|
|
266
|
-
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (container instanceof Element) bindNode(container);
|
|
335
|
+
|
|
336
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
337
|
+
let child;
|
|
338
|
+
while (child = walker.nextNode()) bindNode(child);
|
|
267
339
|
};
|
|
268
340
|
|
|
269
341
|
const scanForBindings = (container) => {
|
|
@@ -307,7 +379,7 @@ const Lego = (() => {
|
|
|
307
379
|
bindings.push({ type: 'b-show', node, expr });
|
|
308
380
|
}
|
|
309
381
|
if (node.hasAttribute('b-for')) {
|
|
310
|
-
const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+(
|
|
382
|
+
const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+([\s\S]+?)\s*$/);
|
|
311
383
|
if (match) {
|
|
312
384
|
checkGlobal(match[2]);
|
|
313
385
|
bindings.push({
|
|
@@ -315,22 +387,31 @@ const Lego = (() => {
|
|
|
315
387
|
node,
|
|
316
388
|
itemName: match[1],
|
|
317
389
|
listName: match[2].trim(),
|
|
318
|
-
template: node.
|
|
390
|
+
template: node.cloneNode(true) // Store the whole node as template
|
|
319
391
|
});
|
|
320
392
|
node.innerHTML = '';
|
|
321
393
|
}
|
|
322
394
|
}
|
|
323
395
|
if (node.hasAttribute('b-text')) bindings.push({ type: 'b-text', node, path: node.getAttribute('b-text') });
|
|
396
|
+
if (node.hasAttribute('b-html')) {
|
|
397
|
+
const expr = node.getAttribute('b-html');
|
|
398
|
+
checkGlobal(expr);
|
|
399
|
+
bindings.push({ type: 'b-html', node, expr });
|
|
400
|
+
}
|
|
324
401
|
if (node.hasAttribute('b-sync')) bindings.push({ type: 'b-sync', node });
|
|
402
|
+
const [start] = getDelimiters();
|
|
325
403
|
[...node.attributes].forEach(attr => {
|
|
326
|
-
if (attr.value.includes(
|
|
404
|
+
if (attr.value.includes(start)) {
|
|
327
405
|
checkGlobal(attr.value);
|
|
328
406
|
bindings.push({ type: 'attr', node, attrName: attr.name, template: attr.value });
|
|
329
407
|
}
|
|
330
408
|
});
|
|
331
|
-
} else if (node.nodeType === Node.TEXT_NODE
|
|
332
|
-
|
|
333
|
-
|
|
409
|
+
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
410
|
+
const [start] = getDelimiters();
|
|
411
|
+
if (node.textContent.includes(start)) {
|
|
412
|
+
checkGlobal(node.textContent);
|
|
413
|
+
bindings.push({ type: 'text', node, template: node.textContent });
|
|
414
|
+
}
|
|
334
415
|
}
|
|
335
416
|
}
|
|
336
417
|
return bindings;
|
|
@@ -340,13 +421,14 @@ const Lego = (() => {
|
|
|
340
421
|
const processNode = (node) => {
|
|
341
422
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
342
423
|
if (node._tpl === undefined) node._tpl = node.textContent;
|
|
343
|
-
const out = node._tpl.replace(
|
|
424
|
+
const out = node._tpl.replace(createRegex(), (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
|
|
344
425
|
if (node.textContent !== out) node.textContent = out;
|
|
345
426
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
427
|
+
const [start] = getDelimiters();
|
|
346
428
|
[...node.attributes].forEach(attr => {
|
|
347
429
|
if (attr._tpl === undefined) attr._tpl = attr.value;
|
|
348
|
-
if (attr._tpl.includes(
|
|
349
|
-
const out = attr._tpl.replace(
|
|
430
|
+
if (attr._tpl.includes(start)) {
|
|
431
|
+
const out = attr._tpl.replace(createRegex(), (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
|
|
350
432
|
if (attr.value !== out) {
|
|
351
433
|
attr.value = out;
|
|
352
434
|
if (attr.name === 'class') node.className = out;
|
|
@@ -367,6 +449,7 @@ const Lego = (() => {
|
|
|
367
449
|
const data = getPrivateData(el);
|
|
368
450
|
if (data.rendering) return;
|
|
369
451
|
data.rendering = true;
|
|
452
|
+
if (config.metrics && config.metrics.onRenderStart) config.metrics.onRenderStart(el);
|
|
370
453
|
|
|
371
454
|
try {
|
|
372
455
|
// Use shadowRoot if it's a component, otherwise render the element itself (light DOM)
|
|
@@ -386,13 +469,14 @@ const Lego = (() => {
|
|
|
386
469
|
}
|
|
387
470
|
if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, global: Lego.globals, self: b.node }) ? '' : 'none';
|
|
388
471
|
if (b.type === 'b-text') b.node.textContent = resolve(b.path, state);
|
|
472
|
+
if (b.type === 'b-html') b.node.innerHTML = safeEval(b.expr, { state, global: Lego.globals, self: b.node }) || '';
|
|
389
473
|
if (b.type === 'b-sync') syncModelValue(b.node, resolve(b.node.getAttribute('b-sync'), state));
|
|
390
474
|
if (b.type === 'text') {
|
|
391
|
-
const out = b.template.replace(
|
|
475
|
+
const out = b.template.replace(createRegex(), (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
|
|
392
476
|
if (b.node.textContent !== out) b.node.textContent = out;
|
|
393
477
|
}
|
|
394
478
|
if (b.type === 'attr') {
|
|
395
|
-
const out = b.template.replace(
|
|
479
|
+
const out = b.template.replace(createRegex(), (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
|
|
396
480
|
if (b.node.getAttribute(b.attrName) !== out) {
|
|
397
481
|
b.node.setAttribute(b.attrName, out);
|
|
398
482
|
if (b.attrName === 'class') b.node.className = out;
|
|
@@ -408,9 +492,8 @@ const Lego = (() => {
|
|
|
408
492
|
currentKeys.add(key);
|
|
409
493
|
let child = pool.get(key);
|
|
410
494
|
if (!child) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
child = temp.firstElementChild;
|
|
495
|
+
child = b.template.cloneNode(true);
|
|
496
|
+
child.removeAttribute('b-for');
|
|
414
497
|
pool.set(key, child);
|
|
415
498
|
bind(child, el, { name: b.itemName, listName: b.listName, index: i });
|
|
416
499
|
}
|
|
@@ -438,7 +521,10 @@ const Lego = (() => {
|
|
|
438
521
|
if (getPrivateData(comp).hasGlobalDependency) render(comp);
|
|
439
522
|
});
|
|
440
523
|
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
config.onError(err, 'render', el);
|
|
441
526
|
} finally {
|
|
527
|
+
if (config.metrics && config.metrics.onRenderEnd) config.metrics.onRenderEnd(el);
|
|
442
528
|
data.rendering = false;
|
|
443
529
|
}
|
|
444
530
|
};
|
|
@@ -469,11 +555,15 @@ const Lego = (() => {
|
|
|
469
555
|
const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
|
|
470
556
|
const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
|
|
471
557
|
|
|
472
|
-
// Priority: Script < Template < Instance
|
|
473
558
|
el._studs = reactive({
|
|
474
559
|
...scriptLogic,
|
|
475
560
|
...templateLogic,
|
|
476
561
|
...instanceLogic,
|
|
562
|
+
$vars: {},
|
|
563
|
+
$element: el,
|
|
564
|
+
$emit: (name, detail) => {
|
|
565
|
+
el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
566
|
+
},
|
|
477
567
|
get $route() { return Lego.globals.$route },
|
|
478
568
|
get $go() { return Lego.globals.$go }
|
|
479
569
|
}, el);
|
|
@@ -489,8 +579,10 @@ const Lego = (() => {
|
|
|
489
579
|
activeComponents.add(el);
|
|
490
580
|
render(el);
|
|
491
581
|
|
|
582
|
+
[...shadow.children].forEach(snap);
|
|
583
|
+
|
|
492
584
|
if (typeof el._studs.mounted === 'function') {
|
|
493
|
-
try { el._studs.mounted.call(el._studs); } catch (e) {
|
|
585
|
+
try { el._studs.mounted.call(el._studs); } catch (e) { config.onError(e, 'mounted', el); }
|
|
494
586
|
}
|
|
495
587
|
}
|
|
496
588
|
|
|
@@ -505,6 +597,11 @@ const Lego = (() => {
|
|
|
505
597
|
if (el._studs && typeof el._studs.unmounted === 'function') {
|
|
506
598
|
try { el._studs.unmounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in unmounted:`, e); }
|
|
507
599
|
}
|
|
600
|
+
|
|
601
|
+
if (el.shadowRoot) {
|
|
602
|
+
[...el.shadowRoot.children].forEach(unsnap);
|
|
603
|
+
}
|
|
604
|
+
|
|
508
605
|
activeComponents.delete(el);
|
|
509
606
|
[...el.children].forEach(unsnap);
|
|
510
607
|
};
|
|
@@ -551,14 +648,14 @@ const Lego = (() => {
|
|
|
551
648
|
});
|
|
552
649
|
};
|
|
553
650
|
|
|
554
|
-
|
|
555
|
-
init: async (root = document.body,
|
|
556
|
-
// If called as an event listener or with invalid root, fail over to document.body
|
|
651
|
+
const publicAPI = {
|
|
652
|
+
init: async (root = document.body, options = {}) => {
|
|
557
653
|
if (!root || typeof root.nodeType !== 'number') root = document.body;
|
|
558
|
-
styleConfig = styles;
|
|
654
|
+
styleConfig = options.styles || {};
|
|
655
|
+
config.loader = options.loader; // Register loader hook
|
|
559
656
|
|
|
560
657
|
// Pre-load all defined style sets into Constructable Stylesheets
|
|
561
|
-
const loadPromises = Object.entries(
|
|
658
|
+
const loadPromises = Object.entries(styleConfig).map(async ([key, urls]) => {
|
|
562
659
|
const sheets = await Promise.all(urls.map(async (url) => {
|
|
563
660
|
try {
|
|
564
661
|
const response = await fetch(url);
|
|
@@ -580,7 +677,26 @@ const Lego = (() => {
|
|
|
580
677
|
});
|
|
581
678
|
|
|
582
679
|
const observer = new MutationObserver(m => m.forEach(r => {
|
|
583
|
-
r.addedNodes.forEach(n =>
|
|
680
|
+
r.addedNodes.forEach(n => {
|
|
681
|
+
if (n.nodeType === Node.ELEMENT_NODE) {
|
|
682
|
+
snap(n);
|
|
683
|
+
// Auto-Discovery: Check if tag is unknown and loader is configured
|
|
684
|
+
const tagName = n.tagName.toLowerCase();
|
|
685
|
+
if (tagName.includes('-') && !registry[tagName] && config.loader && !activeComponents.has(n)) {
|
|
686
|
+
const result = config.loader(tagName);
|
|
687
|
+
if (result) {
|
|
688
|
+
// Handle Promise (user does custom fetch) vs String (we fetch)
|
|
689
|
+
const promise = (typeof result === 'string')
|
|
690
|
+
? fetch(result).then(r => r.text())
|
|
691
|
+
: result;
|
|
692
|
+
|
|
693
|
+
Promise.resolve(promise)
|
|
694
|
+
.then(sfc => publicAPI.defineSFC(sfc, tagName + '.lego'))
|
|
695
|
+
.catch(e => console.error(`[Lego] Failed to load ${tagName}:`, e));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
584
700
|
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
|
|
585
701
|
}));
|
|
586
702
|
observer.observe(root, { childList: true, subtree: true });
|
|
@@ -628,6 +744,69 @@ const Lego = (() => {
|
|
|
628
744
|
},
|
|
629
745
|
$go: (path, ...targets) => _go(path, ...targets)(document.body)
|
|
630
746
|
}, document.body),
|
|
747
|
+
defineSFC: (content, filename = 'component.lego') => {
|
|
748
|
+
let template = '';
|
|
749
|
+
let script = '{}';
|
|
750
|
+
let stylesAttr = '';
|
|
751
|
+
let style = '';
|
|
752
|
+
|
|
753
|
+
let remaining = content;
|
|
754
|
+
// Robust regex from parse-lego.js
|
|
755
|
+
const startTagRegex = /<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i;
|
|
756
|
+
|
|
757
|
+
while (remaining) {
|
|
758
|
+
const match = remaining.match(startTagRegex);
|
|
759
|
+
if (!match) break;
|
|
760
|
+
|
|
761
|
+
const tagName = match[1].toLowerCase();
|
|
762
|
+
const attrs = match[2];
|
|
763
|
+
const fullMatch = match[0];
|
|
764
|
+
const startIndex = match.index;
|
|
765
|
+
|
|
766
|
+
const closeTag = `</${tagName}>`;
|
|
767
|
+
const contentStart = startIndex + fullMatch.length;
|
|
768
|
+
const contentEnd = remaining.indexOf(closeTag, contentStart);
|
|
769
|
+
|
|
770
|
+
if (contentEnd === -1) {
|
|
771
|
+
console.warn(`[Lego] Unclosed <${tagName}> tag in ${filename}`);
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const innerContent = remaining.slice(contentStart, contentEnd);
|
|
776
|
+
|
|
777
|
+
if (tagName === 'template') {
|
|
778
|
+
template = innerContent.trim();
|
|
779
|
+
const bStylesMatch = attrs.match(/b-styles=["']([^"']+)["']/);
|
|
780
|
+
if (bStylesMatch) stylesAttr = bStylesMatch[1];
|
|
781
|
+
} else if (tagName === 'script') {
|
|
782
|
+
// Extract logic from script
|
|
783
|
+
const logic = innerContent.trim();
|
|
784
|
+
const defaultExport = logic.match(/export\s+default\s+({[\s\S]*})/);
|
|
785
|
+
script = defaultExport ? defaultExport[1] : logic;
|
|
786
|
+
} else if (tagName === 'style') {
|
|
787
|
+
style = innerContent.trim();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
remaining = remaining.slice(contentEnd + closeTag.length);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const name = deriveComponentName(filename);
|
|
794
|
+
// We must eval the script to get the object.
|
|
795
|
+
// Safe-ish because it's coming from the "Server" (trusted source in this architecture)
|
|
796
|
+
const logicObj = new Function(`return ${script}`)();
|
|
797
|
+
|
|
798
|
+
if (style) {
|
|
799
|
+
template = `<style>${style}</style>` + template;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
registry[name] = document.createElement('template');
|
|
803
|
+
registry[name].innerHTML = template;
|
|
804
|
+
registry[name].setAttribute('b-styles', stylesAttr);
|
|
805
|
+
sfcLogic.set(name, logicObj);
|
|
806
|
+
|
|
807
|
+
// Upgrade existing elements
|
|
808
|
+
document.querySelectorAll(name).forEach(el => !getPrivateData(el).snapped && snap(el));
|
|
809
|
+
},
|
|
631
810
|
define: (tagName, templateHTML, logic = {}, styles = "") => {
|
|
632
811
|
const t = document.createElement('template');
|
|
633
812
|
t.setAttribute('b-id', tagName);
|
|
@@ -636,10 +815,18 @@ const Lego = (() => {
|
|
|
636
815
|
registry[tagName] = t;
|
|
637
816
|
sfcLogic.set(tagName, logic);
|
|
638
817
|
|
|
639
|
-
|
|
818
|
+
// Initialize shared state with try-catch safety
|
|
819
|
+
try {
|
|
820
|
+
sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
|
|
821
|
+
} catch (e) {
|
|
822
|
+
config.onError(e, 'define', tagName);
|
|
823
|
+
}
|
|
640
824
|
|
|
641
825
|
document.querySelectorAll(tagName).forEach(snap);
|
|
642
826
|
},
|
|
827
|
+
// For specific test validation
|
|
828
|
+
getActiveComponentsCount: () => activeComponents.size,
|
|
829
|
+
config, // Expose config for customization
|
|
643
830
|
route: (path, tagName, middleware = null) => {
|
|
644
831
|
const paramNames = [];
|
|
645
832
|
const regexPath = path.replace(/:([^\/]+)/g, (_, name) => {
|
|
@@ -649,6 +836,8 @@ const Lego = (() => {
|
|
|
649
836
|
routes.push({ path, regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
|
|
650
837
|
}
|
|
651
838
|
};
|
|
839
|
+
|
|
840
|
+
return publicAPI;
|
|
652
841
|
})();
|
|
653
842
|
|
|
654
843
|
if (typeof window !== 'undefined') {
|
package/main.min.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const Lego=(()=>{const w={},M=new WeakMap,k=new WeakMap,O=new WeakMap,$=new Set,P=new Map,B=new Map,H=new Map,q=new Map;let G={};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)]},j=(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 F(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;return{add:o=>{!o||c||(s.add(o),!e&&(e=!0,requestAnimationFrame(()=>{c=!0;const t=Array.from(s);s.clear(),e=!1,t.forEach(a=>C(a)),setTimeout(()=>{t.forEach(a=>{const r=a._studs;if(r&&typeof r.updated=="function")try{r.updated.call(r)}catch(n){console.error("[Lego] Error in updated hook:",n)}}),c=!1},0)})))}}})(),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},I=e=>{try{return new Function(`return (${e})`)()}catch(s){return console.error("[Lego] Failed to parse b-data:",e,s),{}}},N=e=>(k.has(e)||k.set(e,{snapped:!1,bindings:null,bound:!1,rendering:!1,anchor:null,hasGlobalDependency:!1}),k.get(e)),D=(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._studs;c=c.parentElement||c.getRootNode&&c.getRootNode().host}},h=(e,s,c=!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 o=s.state||{};let t=H.get(e);t||(t=new Function("global","self","event","helpers",`
|
|
2
|
+
with(helpers) {
|
|
3
|
+
with(this) {
|
|
4
|
+
return ${e}
|
|
5
|
+
}
|
|
6
|
+
}
|
|
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)=>j(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}},z=(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)}},x=(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 y=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];m=Object.assign(Object.create(o),{[c.name]:y})}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 y=h(c.listName,{state:o,global:Lego.globals,self:s})[c.index];if(!y)return;const E=i.split(".").slice(1);g=E.pop(),f=E.reduce((T,A)=>T[A],y)}else{const b=i.split(".");g=b.pop(),f=b.reduce((y,E)=>y[E],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)},C=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=D(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"&&z(t.node,D(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})||[];O.has(t.node)||O.set(t.node,new Map);const r=O.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),x(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 b=h(t.listName,{state:s,global:Lego.globals,self:e});z(g,D(m.split(".").slice(1).join("."),b[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&&$.forEach(t=>{N(t).hasGlobalDependency&&C(t)})}catch(o){p.onError(o,"render",e)}finally{p.metrics&&p.metrics.onRenderEnd&&p.metrics.onRenderEnd(e),c.rendering=!1}}},v=e=>{if(!e||e.nodeType!==Node.ELEMENT_NODE)return;const s=N(e),c=e.tagName.toLowerCase(),o=w[c];if(o&&!s.snapped){s.snapped=!0;const a=o.content.cloneNode(!0),r=e.attachShadow({mode:"open"}),n=(o.getAttribute("b-styles")||"").split(/\s+/).filter(Boolean);if(n.length>0){const f=n.flatMap(g=>q.get(g)||[]);f.length>0&&(r.adoptedStyleSheets=[...f])}const l=P.get(c)||{},d=I(o.getAttribute("b-data")||"{}"),i=I(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),r.appendChild(a);const u=r.querySelector("style");if(u&&(u.textContent=u.textContent.replace(/\bself\b/g,":host")),x(r,e),$.add(e),C(e),[...r.children].forEach(v),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&&x(e,t),[...e.children].forEach(v)},W=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(W),$.delete(e),[...e.children].forEach(W)},F=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)}}))},U={init:async(e=document.body,s={})=>{(!e||typeof e.nodeType!="number")&&(e=document.body),G=s.styles||{},p.loader=s.loader;const c=Object.entries(G).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}}));q.set(t,r.filter(n=>n!==null))});await Promise.all(c),document.querySelectorAll("template[b-id]").forEach(t=>{w[t.getAttribute("b-id")]=t}),new MutationObserver(t=>t.forEach(a=>{a.addedNodes.forEach(r=>{if(r.nodeType===Node.ELEMENT_NODE){v(r);const n=r.tagName.toLowerCase();if(n.includes("-")&&!w[n]&&p.loader&&!$.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=>U.defineSFC(i,n+".lego")).catch(i=>console.error(`[Lego] Failed to load ${n}:`,i))}}}}),a.removedNodes.forEach(r=>r.nodeType===Node.ELEMENT_NODE&&W(r))})).observe(e,{childList:!0,subtree:!0}),e._studs=Lego.globals,v(e),x(e,e),C(e),R.length>0&&(window.addEventListener("popstate",t=>{const a=t.state?.legoTargets||null;F(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)}}),F())},globals:S({$route:{url:window.location.pathname,route:"",params:{},query:{},method:"GET",body:null},$go:(e,...s)=>j(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,b=`</${u}>`,y=m+g.length,E=r.indexOf(b,y);if(E===-1){console.warn(`[Lego] Unclosed <${u}> tag in ${s}`);break}const T=r.slice(y,E);if(u==="template"){c=T.trim();const A=f.match(/b-styles=["']([^"']+)["']/);A&&(t=A[1])}else if(u==="script"){const A=T.trim(),X=A.match(/export\s+default\s+({[\s\S]*})/);o=X?X[1]:A}else u==="style"&&(a=T.trim());r=r.slice(E+b.length)}const l=K(s),d=new Function(`return ${o}`)();a&&(c=`<style>${a}</style>`+c),w[l]=document.createElement("template"),w[l].innerHTML=c,w[l].setAttribute("b-styles",t),P.set(l,d),document.querySelectorAll(l).forEach(i=>!N(i).snapped&&v(i))},define:(e,s,c={},o="")=>{const t=document.createElement("template");t.setAttribute("b-id",e),t.setAttribute("b-styles",o),t.innerHTML=s,w[e]=t,P.set(e,c);try{B.set(e.toLowerCase(),S({...c},document.body))}catch(a){p.onError(a,"define",e)}document.querySelectorAll(e).forEach(v)},getActiveComponentsCount:()=>$.size,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 U})();typeof window<"u"&&(window.Lego=Lego);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LegoDOM Monitoring Plugin
|
|
3
|
+
* Tracks render performance and errors.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const Monitoring = {
|
|
7
|
+
// Metrics storage
|
|
8
|
+
metrics: {
|
|
9
|
+
renders: 0,
|
|
10
|
+
errors: 0,
|
|
11
|
+
slowRenders: 0,
|
|
12
|
+
components: new Map(), // tagName -> { count, avgTime }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
// Configuration options
|
|
16
|
+
options: {
|
|
17
|
+
slowRenderThreshold: 16, // ms (target 60fps)
|
|
18
|
+
reportToConsole: false,
|
|
19
|
+
reportUrl: null // Endpoint to send metrics to
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Install the plugin
|
|
24
|
+
* @param {object} Lego - The global Lego object
|
|
25
|
+
* @param {object} opts - Configuration options
|
|
26
|
+
*/
|
|
27
|
+
install(Lego, opts = {}) {
|
|
28
|
+
Object.assign(this.options, opts);
|
|
29
|
+
|
|
30
|
+
// Prevent double installation
|
|
31
|
+
if (Lego.metrics) {
|
|
32
|
+
// Just reset metrics if re-installing
|
|
33
|
+
if (Lego.metrics.reset) Lego.metrics.reset();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Ensure metrics object exists on config if not already
|
|
38
|
+
if (!Lego.config.metrics) Lego.config.metrics = {};
|
|
39
|
+
|
|
40
|
+
// 1. Hook into Render Cycle
|
|
41
|
+
Lego.config.metrics.onRenderStart = (el) => {
|
|
42
|
+
if (!el.__perfId) el.__perfId = Math.random().toString(36).substr(2, 9);
|
|
43
|
+
performance.mark(`lego-render-start-${el.__perfId}`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
Lego.config.metrics.onRenderEnd = (el) => {
|
|
47
|
+
const id = el.__perfId;
|
|
48
|
+
const startMark = `lego-render-start-${id}`;
|
|
49
|
+
const measureName = `lego-render-${el.tagName}-${id}`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
performance.measure(measureName, startMark);
|
|
53
|
+
const entry = performance.getEntriesByName(measureName).pop();
|
|
54
|
+
if (entry) {
|
|
55
|
+
this.recordMetric(el.tagName.toLowerCase(), entry.duration);
|
|
56
|
+
performance.clearMarks(startMark);
|
|
57
|
+
performance.clearMeasures(measureName);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Fallback or ignore if performance API fails
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// 2. Hook into Error Handler
|
|
65
|
+
const originalOnError = Lego.config.onError;
|
|
66
|
+
Lego.config.onError = (err, type, context) => {
|
|
67
|
+
this.metrics.errors++;
|
|
68
|
+
if (this.options.reportToConsole) {
|
|
69
|
+
console.groupCollapsed(`[Lego Monitoring] Error in ${type}`);
|
|
70
|
+
console.error(err);
|
|
71
|
+
console.log('Context:', context);
|
|
72
|
+
console.groupEnd();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Call original handler if it exists
|
|
76
|
+
if (originalOnError) originalOnError(err, type, context);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Expose metrics API
|
|
80
|
+
Lego.metrics = {
|
|
81
|
+
get: () => this.metrics,
|
|
82
|
+
reset: () => {
|
|
83
|
+
this.metrics.renders = 0;
|
|
84
|
+
this.metrics.errors = 0;
|
|
85
|
+
this.metrics.slowRenders = 0;
|
|
86
|
+
this.metrics.components.clear();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
console.log('[Lego] Monitoring Plugin Installed');
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
recordMetric(tagName, duration) {
|
|
94
|
+
this.metrics.renders++;
|
|
95
|
+
if (duration > this.options.slowRenderThreshold) {
|
|
96
|
+
this.metrics.slowRenders++;
|
|
97
|
+
if (this.options.reportToConsole) {
|
|
98
|
+
console.warn(`[Lego Slow Render] <${tagName}> took ${duration.toFixed(2)}ms`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this.metrics.components.has(tagName)) {
|
|
103
|
+
this.metrics.components.set(tagName, { count: 0, totalTime: 0, avg: 0 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stats = this.metrics.components.get(tagName);
|
|
107
|
+
stats.count++;
|
|
108
|
+
stats.totalTime += duration;
|
|
109
|
+
stats.avg = stats.totalTime / stats.count;
|
|
110
|
+
}
|
|
111
|
+
};
|