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.
Files changed (74) hide show
  1. package/.legodom +87 -0
  2. package/CHANGELOG.md +87 -3
  3. package/cdn.html +10 -5
  4. package/docs/.vitepress/config.js +23 -7
  5. package/docs/api/config.md +95 -0
  6. package/docs/api/define.md +29 -2
  7. package/docs/api/directives.md +10 -2
  8. package/docs/api/index.md +1 -0
  9. package/docs/contributing/01-welcome.md +2 -0
  10. package/docs/contributing/02-registry.md +37 -3
  11. package/docs/contributing/06-init.md +13 -2
  12. package/docs/contributing/07-observer.md +3 -0
  13. package/docs/contributing/08-snap.md +15 -1
  14. package/docs/contributing/10-studs.md +3 -1
  15. package/docs/contributing/11-scanner.md +13 -0
  16. package/docs/contributing/12-render.md +32 -10
  17. package/docs/contributing/13-directives.md +19 -1
  18. package/docs/contributing/14-events.md +1 -1
  19. package/docs/contributing/15-router.md +49 -1
  20. package/docs/contributing/16-state.md +9 -10
  21. package/docs/contributing/17-legodom.md +1 -8
  22. package/docs/contributing/index.md +23 -4
  23. package/docs/examples/form.md +1 -1
  24. package/docs/examples/index.md +3 -3
  25. package/docs/examples/routing.md +10 -10
  26. package/docs/examples/sfc-showcase.md +1 -1
  27. package/docs/examples/todo-app.md +7 -7
  28. package/docs/guide/cdn-usage.md +44 -18
  29. package/docs/guide/components.md +18 -12
  30. package/docs/guide/directives.md +131 -22
  31. package/docs/guide/directory-structure.md +248 -0
  32. package/docs/guide/faq.md +210 -0
  33. package/docs/guide/getting-started.md +14 -10
  34. package/docs/guide/index.md +1 -1
  35. package/docs/guide/lifecycle.md +32 -0
  36. package/docs/guide/quick-start.md +4 -4
  37. package/docs/guide/reactivity.md +2 -2
  38. package/docs/guide/routing.md +69 -8
  39. package/docs/guide/server-side.md +134 -0
  40. package/docs/guide/sfc.md +96 -13
  41. package/docs/guide/templating.md +62 -57
  42. package/docs/index.md +9 -9
  43. package/docs/router/basic-routing.md +8 -8
  44. package/docs/router/cold-entry.md +2 -2
  45. package/docs/router/history.md +7 -7
  46. package/docs/router/index.md +1 -1
  47. package/docs/router/resolver.md +5 -5
  48. package/docs/router/surgical-swaps.md +5 -5
  49. package/docs/tutorial/01-project-setup.md +152 -0
  50. package/docs/tutorial/02-your-first-component.md +226 -0
  51. package/docs/tutorial/03-adding-routes.md +279 -0
  52. package/docs/tutorial/04-multi-page-app.md +329 -0
  53. package/docs/tutorial/05-state-and-globals.md +285 -0
  54. package/docs/tutorial/index.md +40 -0
  55. package/examples/vite-app/index.html +1 -0
  56. package/examples/vite-app/src/app.js +2 -2
  57. package/examples/vite-app/src/components/side-menu.lego +46 -0
  58. package/examples/vite-app/vite.config.js +2 -1
  59. package/main.js +261 -72
  60. package/main.min.js +7 -0
  61. package/monitoring-plugin.js +111 -0
  62. package/package.json +4 -2
  63. package/parse-lego.js +49 -22
  64. package/tests/error.test.js +74 -0
  65. package/tests/main.test.js +2 -2
  66. package/tests/memory.test.js +68 -0
  67. package/tests/monitoring.test.js +74 -0
  68. package/tests/naming.test.js +74 -0
  69. package/tests/parse-lego.test.js +2 -2
  70. package/tests/security.test.js +67 -0
  71. package/tests/server.test.js +114 -0
  72. package/tests/syntax.test.js +67 -0
  73. package/vite-plugin.js +3 -2
  74. package/docs/guide/contributing.md +0 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "1.0.0",
3
+ "version": "1.3.4",
4
4
  "license": "MIT",
5
5
  "description": "A feature-rich web components + SFC frontend framework",
6
6
  "main": "main.js",
@@ -22,17 +22,19 @@
22
22
  "author": "Tersoo Ortserga",
23
23
  "scripts": {
24
24
  "test": "vitest run",
25
+ "build": "esbuild main.js --minify --outfile=main.min.js",
25
26
  "docs:dev": "vitepress dev docs",
26
27
  "docs:build": "vitepress build docs",
27
28
  "docs:preview": "vitepress preview docs"
28
29
  },
29
30
  "devDependencies": {
31
+ "esbuild": "^0.27.2",
30
32
  "jsdom": "^22.0.0",
31
33
  "vitepress": "^1.6.4",
32
34
  "vitest": "^1.0.0"
33
35
  },
34
36
  "peerDependencies": {
35
- "vite": "^4.0.0 || ^5.0.0"
37
+ "vite": ">=4.0.0"
36
38
  },
37
39
  "peerDependenciesMeta": {
38
40
  "vite": {
package/parse-lego.js CHANGED
@@ -14,33 +14,61 @@ export function parseLego(content, filename = 'component.lego') {
14
14
  template: '',
15
15
  script: '',
16
16
  style: '',
17
- stylesAttr: '', // Stores the value of the b-styles attribute
17
+ stylesAttr: '',
18
18
  componentName: deriveComponentName(filename)
19
19
  };
20
20
 
21
- // Updated to capture attributes on the template tag (like b-styles)
22
- const templateMatch = content.match(/<template([\s\S]*?)>([\s\S]*?)<\/template>/);
23
- if (templateMatch) {
24
- const attrs = templateMatch[1];
25
- result.template = templateMatch[2].trim();
21
+ let remaining = content;
26
22
 
27
- // Extract b-styles value from the attributes string
28
- const bStylesMatch = attrs.match(/b-styles=["']([^"']+)["']/);
29
- if (bStylesMatch) {
30
- result.stylesAttr = bStylesMatch[1];
23
+ // Regex to match the start tag of a section, handling attributes with quotes correctly
24
+ // <(template|script|style) matches the tag name
25
+ // \b ensures we don't match templates inside "template-foo"
26
+ // (?: ... )* loops over attributes
27
+ // \s+ requires space before attributes
28
+ // (?:[^>"']|"[^"]*"|'[^']*')* matches attribute content, respecting quotes to skip >
29
+ const startTagRegex = /<(template|script|style)\b((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)*)>/i;
30
+
31
+ while (remaining) {
32
+ const match = remaining.match(startTagRegex);
33
+ if (!match) break;
34
+
35
+ const tagName = match[1].toLowerCase();
36
+ const attrs = match[2]; // Captures all attributes
37
+ const fullMatch = match[0];
38
+ const startIndex = match.index;
39
+
40
+ // Find the corresponding closing tag
41
+ const closeTag = `</${tagName}>`;
42
+
43
+ // Content starts after the opening tag
44
+ const contentStart = startIndex + fullMatch.length;
45
+
46
+ // Find the closing tag starting from where content began
47
+ const contentEnd = remaining.indexOf(closeTag, contentStart);
48
+
49
+ if (contentEnd === -1) {
50
+ // If no closing tag found, we can't safely parse this block
51
+ console.warn(`[Lego] Unclosed <${tagName}> tag in ${filename}`);
52
+ break;
31
53
  }
32
- }
33
54
 
34
- // Extract script section
35
- const scriptMatch = content.match(/<script>([\s\S]*?)<\/script>/);
36
- if (scriptMatch) {
37
- result.script = scriptMatch[1].trim();
38
- }
55
+ const innerContent = remaining.slice(contentStart, contentEnd);
56
+
57
+ if (tagName === 'template') {
58
+ result.template = innerContent.trim();
59
+ // Extract b-styles attribute if present
60
+ const bStylesMatch = attrs.match(/b-styles=["']([^"']+)["']/);
61
+ if (bStylesMatch) {
62
+ result.stylesAttr = bStylesMatch[1];
63
+ }
64
+ } else if (tagName === 'script') {
65
+ result.script = innerContent.trim();
66
+ } else if (tagName === 'style') {
67
+ result.style = innerContent.trim();
68
+ }
39
69
 
40
- // Extract style section
41
- const styleMatch = content.match(/<style>([\s\S]*?)<\/style>/);
42
- if (styleMatch) {
43
- result.style = styleMatch[1].trim();
70
+ // Advance past this block (content + closing tag)
71
+ remaining = remaining.slice(contentEnd + closeTag.length);
44
72
  }
45
73
 
46
74
  return result;
@@ -59,7 +87,6 @@ export function deriveComponentName(filename) {
59
87
 
60
88
  /**
61
89
  * Generate Lego.define() code from parsed .lego file
62
- * Updated to include the 4th argument for styles
63
90
  * @param {object} parsed - Parsed .lego file object
64
91
  * @returns {string} - JavaScript code string
65
92
  */
@@ -88,7 +115,7 @@ export function generateDefineCall(parsed) {
88
115
  }
89
116
  }
90
117
 
91
- // Generate the Lego.define call with the new 4th argument (stylesAttr)
118
+ // Generate the Lego.define call
92
119
  return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
93
120
  }
94
121
 
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ // Setup Mock DOM
7
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
8
+ runScripts: "dangerously",
9
+ resources: "usable"
10
+ });
11
+
12
+ global.window = dom.window;
13
+ global.document = dom.window.document;
14
+ Object.defineProperty(global, 'navigator', {
15
+ value: dom.window.navigator,
16
+ writable: true,
17
+ configurable: true
18
+ });
19
+ global.HTMLElement = dom.window.HTMLElement;
20
+ global.customElements = dom.window.customElements;
21
+ global.MutationObserver = dom.window.MutationObserver;
22
+ global.Node = dom.window.Node;
23
+ global.NodeFilter = dom.window.NodeFilter;
24
+ global.Element = dom.window.Element;
25
+ global.Event = dom.window.Event;
26
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
27
+
28
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
29
+ eval(libCode);
30
+
31
+ describe('LegoDOM Error Handling', () => {
32
+ beforeEach(async () => {
33
+ document.body.innerHTML = '';
34
+ await window.Lego.init(document.body);
35
+ });
36
+
37
+ it('should catch errors in rendering and call onError', async () => {
38
+ const errorSpy = vi.fn();
39
+ window.Lego.config.onError = errorSpy;
40
+
41
+ // Define a component that throws an error when accessing a property used in template
42
+ window.Lego.define('error-comp', '<div>[[ throwErr() ]]</div>', {
43
+ throwErr() {
44
+ throw new Error('Render Failure');
45
+ }
46
+ });
47
+
48
+ const el = document.createElement('error-comp');
49
+ document.body.appendChild(el);
50
+
51
+ await new Promise(r => setTimeout(r, 100));
52
+
53
+ expect(errorSpy).toHaveBeenCalled();
54
+ expect(errorSpy.mock.calls[0][1]).toBe('render-error');
55
+ });
56
+
57
+ it('should catch errors in event handlers', async () => {
58
+ const errorSpy = vi.fn();
59
+ window.Lego.config.onError = errorSpy;
60
+
61
+ window.Lego.define('btn-error', '<button @click="crash()">Crash</button>', {
62
+ crash() { throw new Error('Boom'); }
63
+ });
64
+
65
+ const el = document.createElement('btn-error');
66
+ document.body.appendChild(el);
67
+ await new Promise(r => setTimeout(r, 100));
68
+
69
+ el.shadowRoot.querySelector('button').click();
70
+
71
+ expect(errorSpy).toHaveBeenCalled();
72
+ expect(errorSpy.mock.calls[0][1]).toBe('event-handler');
73
+ });
74
+ });
@@ -54,7 +54,7 @@ describe('Lego JS Node Environment Tests', () => {
54
54
  });
55
55
 
56
56
  it('should reactively update text content', async () => {
57
- window.Lego.define('test-comp', '<span>{{msg}}</span>');
57
+ window.Lego.define('test-comp', '<span>[[msg]]</span>');
58
58
  const el = document.createElement('test-comp');
59
59
  el.setAttribute('b-data', "{ msg: 'hello' }");
60
60
  document.body.appendChild(el);
@@ -74,7 +74,7 @@ describe('Lego JS Node Environment Tests', () => {
74
74
  });
75
75
 
76
76
  it('should prevent XSS via auto-escaping', async () => {
77
- window.Lego.define('xss-comp', '<div>{{code}}</div>');
77
+ window.Lego.define('xss-comp', '<div>[[code]]</div>');
78
78
  const el = document.createElement('xss-comp');
79
79
  el.setAttribute('b-data', "{ code: '<script>alert(1)</script>' }");
80
80
  document.body.appendChild(el);
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
7
+ runScripts: "dangerously",
8
+ resources: "usable"
9
+ });
10
+
11
+ global.window = dom.window;
12
+ global.document = dom.window.document;
13
+ Object.defineProperty(global, 'navigator', {
14
+ value: dom.window.navigator,
15
+ writable: true,
16
+ configurable: true
17
+ });
18
+ global.HTMLElement = dom.window.HTMLElement;
19
+ global.customElements = dom.window.customElements;
20
+ global.MutationObserver = dom.window.MutationObserver;
21
+ global.Node = dom.window.Node;
22
+ global.NodeFilter = dom.window.NodeFilter;
23
+ global.Element = dom.window.Element;
24
+ global.Event = dom.window.Event;
25
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
26
+
27
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
28
+ eval(libCode);
29
+
30
+ describe('LegoDOM Memory Management', () => {
31
+ beforeEach(async () => {
32
+ document.body.innerHTML = '';
33
+ await window.Lego.init(document.body);
34
+ });
35
+
36
+ it('should cleanup activeComponents when elements are removed', async () => {
37
+ window.Lego.define('mem-comp', '<div>Hi</div>');
38
+
39
+ // Mount
40
+ const el = document.createElement('mem-comp');
41
+ document.body.appendChild(el);
42
+ await new Promise(r => setTimeout(r, 100)); // Wait for observer
43
+
44
+ expect(window.Lego.getActiveComponentsCount()).toBe(1);
45
+
46
+ // Unmount
47
+ el.remove();
48
+ await new Promise(r => setTimeout(r, 100)); // Wait for observer
49
+
50
+ expect(window.Lego.getActiveComponentsCount()).toBe(0);
51
+ });
52
+
53
+ it('should cleanup nested components recursively', async () => {
54
+ window.Lego.define('parent-comp', '<child-comp></child-comp>');
55
+ window.Lego.define('child-comp', '<span>Child</span>');
56
+
57
+ const el = document.createElement('parent-comp');
58
+ document.body.appendChild(el);
59
+ await new Promise(r => setTimeout(r, 100));
60
+
61
+ expect(window.Lego.getActiveComponentsCount()).toBe(2); // Parent + Child
62
+
63
+ el.remove();
64
+ await new Promise(r => setTimeout(r, 100));
65
+
66
+ expect(window.Lego.getActiveComponentsCount()).toBe(0);
67
+ });
68
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { Monitoring } from '../monitoring-plugin.js';
6
+
7
+ // Setup Mock DOM with Performance API
8
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
9
+ runScripts: "dangerously",
10
+ resources: "usable"
11
+ });
12
+
13
+ global.window = dom.window;
14
+ global.document = dom.window.document;
15
+ global.performance = {
16
+ mark: vi.fn(),
17
+ measure: vi.fn(),
18
+ getEntriesByName: vi.fn(() => [{ duration: 20 }]), // Mock 20ms render
19
+ clearMarks: vi.fn(),
20
+ clearMeasures: vi.fn()
21
+ };
22
+ global.HTMLElement = dom.window.HTMLElement;
23
+ global.customElements = dom.window.customElements;
24
+ global.MutationObserver = dom.window.MutationObserver;
25
+ global.Node = dom.window.Node;
26
+ global.NodeFilter = dom.window.NodeFilter;
27
+ global.Element = dom.window.Element;
28
+ global.Event = dom.window.Event;
29
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
30
+
31
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
32
+ eval(libCode);
33
+
34
+ describe('LegoDOM Monitoring Plugin', () => {
35
+ beforeEach(async () => {
36
+ document.body.innerHTML = '';
37
+ await window.Lego.init(document.body);
38
+ Monitoring.install(window.Lego, { reportToConsole: false });
39
+ window.Lego.metrics.reset();
40
+ });
41
+
42
+ it('should track render count and duration', async () => {
43
+ window.Lego.define('monitor-test', '<div>[[ msg ]]</div>');
44
+ const el = document.createElement('monitor-test');
45
+ el.setAttribute('b-data', "{ msg: 'hello' }");
46
+ document.body.appendChild(el);
47
+
48
+ await new Promise(r => setTimeout(r, 100));
49
+
50
+ const metrics = window.Lego.metrics.get();
51
+ expect(metrics.renders).toBeGreaterThan(0);
52
+
53
+ const stats = metrics.components.get('monitor-test');
54
+ expect(stats).toBeDefined();
55
+ expect(stats.count).toBe(1);
56
+ // Since we mocked getEntriesByName to return 20ms
57
+ expect(stats.avg).toBe(20);
58
+ expect(metrics.slowRenders).toBe(1); // 20ms > 16ms threshold
59
+ });
60
+
61
+ it('should track errors via the hooked onError handler', async () => {
62
+ window.Lego.define('monitor-error', '<div>[[ throwErr() ]]</div>', {
63
+ throwErr() { throw new Error('Monitor Fail'); }
64
+ });
65
+
66
+ const el = document.createElement('monitor-error');
67
+ document.body.appendChild(el);
68
+
69
+ await new Promise(r => setTimeout(r, 100));
70
+
71
+ const metrics = window.Lego.metrics.get();
72
+ expect(metrics.errors).toBe(1);
73
+ });
74
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
7
+ runScripts: "dangerously",
8
+ resources: "usable"
9
+ });
10
+
11
+ global.window = dom.window;
12
+ global.document = dom.window.document;
13
+ Object.defineProperty(global, 'navigator', { value: dom.window.navigator });
14
+ global.HTMLElement = dom.window.HTMLElement;
15
+ global.customElements = dom.window.customElements;
16
+ global.MutationObserver = dom.window.MutationObserver;
17
+ global.Node = dom.window.Node;
18
+ global.NodeFilter = dom.window.NodeFilter;
19
+ global.Element = dom.window.Element;
20
+ global.Event = dom.window.Event;
21
+ global.fetch = vi.fn();
22
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
23
+
24
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
25
+ eval(libCode);
26
+
27
+ describe('Component Naming Policy', () => {
28
+ beforeEach(() => {
29
+ document.body.innerHTML = '';
30
+ });
31
+
32
+ const define = (filename) => {
33
+ const sfc = `<template><div>Test</div></template>`;
34
+ window.Lego.defineSFC(sfc, filename);
35
+ };
36
+
37
+ it('should support kebab-case (standard)', () => {
38
+ define('user-card.lego');
39
+ expect(document.querySelector('user-card')).toBeDefined();
40
+ });
41
+
42
+ it('should auto-convert PascalCase to kebab-case', () => {
43
+ define('UserProfile.lego');
44
+ const el = document.createElement('user-profile');
45
+ document.body.appendChild(el);
46
+ expect(el.shadowRoot).toBeDefined();
47
+ });
48
+
49
+ it('should auto-convert camelCase to kebab-case', () => {
50
+ define('navBar.lego');
51
+ const el = document.createElement('nav-bar');
52
+ document.body.appendChild(el);
53
+ expect(el.shadowRoot).toBeDefined();
54
+ });
55
+
56
+ it('should auto-convert snake_case to kebab-case', () => {
57
+ define('data_table.lego');
58
+ const el = document.createElement('data-table');
59
+ document.body.appendChild(el);
60
+ expect(el.shadowRoot).toBeDefined();
61
+ });
62
+
63
+ it('should throw error for single-word PascalCase (no hyphen result)', () => {
64
+ expect(() => define('Button.lego')).toThrow(/Invalid component definition/);
65
+ });
66
+
67
+ it('should throw error for single-word lowercase', () => {
68
+ expect(() => define('table.lego')).toThrow(/Invalid component definition/);
69
+ });
70
+
71
+ it('should throw error for single-word brand name', () => {
72
+ expect(() => define('adidas.lego')).toThrow(/Invalid component definition/);
73
+ });
74
+ });
@@ -15,7 +15,7 @@ describe('parse-lego tests', () => {
15
15
  it('should extract attributes from template', () => {
16
16
  const sfcContent = `
17
17
  <template b-styles="tailwind chartist" b-id="ignored">
18
- <div class="p-4">Hello {{name}}</div>
18
+ <div class="p-4">Hello [[name]]</div>
19
19
  </template>
20
20
  <script>
21
21
  export default { mounted() { console.log('hi') } }
@@ -27,7 +27,7 @@ export default { mounted() { console.log('hi') } }
27
27
  const parsed = parseLego(sfcContent, 'user-profile.lego');
28
28
  expect(parsed.componentName).toBe('user-profile');
29
29
  expect(parsed.stylesAttr).toBe('tailwind chartist');
30
- expect(parsed.template).toContain('{{name}}');
30
+ expect(parsed.template).toContain('[[name]]');
31
31
  expect(parsed.script).toContain('mounted');
32
32
  expect(parsed.style).toContain('color: red');
33
33
  });
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
7
+ runScripts: "dangerously",
8
+ resources: "usable"
9
+ });
10
+
11
+ global.window = dom.window;
12
+ global.document = dom.window.document;
13
+ Object.defineProperty(global, 'navigator', {
14
+ value: dom.window.navigator,
15
+ writable: true,
16
+ configurable: true
17
+ });
18
+ global.HTMLElement = dom.window.HTMLElement;
19
+ global.customElements = dom.window.customElements;
20
+ global.MutationObserver = dom.window.MutationObserver;
21
+ global.Node = dom.window.Node;
22
+ global.NodeFilter = dom.window.NodeFilter;
23
+ global.Element = dom.window.Element;
24
+ global.Event = dom.window.Event;
25
+
26
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
27
+
28
+ // Load LegoDOM
29
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
30
+ eval(libCode);
31
+
32
+ describe('LegoDOM Security', () => {
33
+ beforeEach(async () => {
34
+ document.body.innerHTML = '';
35
+ await window.Lego.init(document.body);
36
+ });
37
+
38
+ it('should block dangerous expressions in safeEval', async () => {
39
+ // This test expects the framework to BLOCK access to Function constructor
40
+ window.Lego.define('pwn-comp', '<div>[[ (function(){ window.pwned = true; })() ]]</div>');
41
+ const el = document.createElement('pwn-comp');
42
+ document.body.appendChild(el);
43
+
44
+ await new Promise(r => setTimeout(r, 100));
45
+
46
+ // Should NOT have executed
47
+ expect(window.pwned).toBeUndefined();
48
+ });
49
+
50
+ it('should block access to global sensitive objects', async () => {
51
+ // Try to access global 'process' or 'window' explicitly if possible
52
+ window.Lego.define('env-comp', '<div>[[ window.location.href ]]</div>');
53
+ const el = document.createElement('env-comp');
54
+ document.body.appendChild(el);
55
+
56
+ await new Promise(r => setTimeout(r, 100));
57
+
58
+ // Ideally this should be blocked or restricted,
59
+ // but for now we focus on preventing arbitrary code execution via constructors
60
+ // If strict mode is on, accessing window might be allowed but defining new functions should be bad.
61
+ });
62
+
63
+ it('should safely render HTML with b-html but potentially expose XSS if unchecked', async () => {
64
+ // b-html doesn't exist yet, but we will add it.
65
+ // If we add it, we want to ensure it works.
66
+ });
67
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
7
+ runScripts: "dangerously",
8
+ resources: "usable"
9
+ });
10
+
11
+ global.window = dom.window;
12
+ global.document = dom.window.document;
13
+ Object.defineProperty(global, 'navigator', { value: dom.window.navigator });
14
+ global.HTMLElement = dom.window.HTMLElement;
15
+ global.customElements = dom.window.customElements;
16
+ global.MutationObserver = dom.window.MutationObserver;
17
+ global.Node = dom.window.Node;
18
+ global.NodeFilter = dom.window.NodeFilter;
19
+ global.Element = dom.window.Element;
20
+ global.Event = dom.window.Event;
21
+ global.fetch = vi.fn();
22
+ global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
23
+
24
+ const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
25
+ eval(libCode);
26
+
27
+ describe('LegoDOM Server-Side Components', () => {
28
+ beforeEach(async () => {
29
+ document.body.innerHTML = '';
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it('should auto-load undefined components via loader config', async () => {
34
+ // 1. Mock the server response
35
+ const mockSFC = `
36
+ <template>
37
+ <div class="remote-box">Loaded from Server: [[ msg ]]</div>
38
+ </template>
39
+ <script>
40
+ export default { msg: 'Remote Data' }
41
+ </script>
42
+ <style>
43
+ .remote-box { color: blue; }
44
+ </style>
45
+ `;
46
+
47
+ global.fetch.mockResolvedValue({
48
+ text: () => Promise.resolve(mockSFC)
49
+ });
50
+
51
+ // 2. Initialize with loader
52
+ await window.Lego.init(document.body, {
53
+ loader: (tag) => {
54
+ if (tag === 'remote-widget') return '/components/remote-widget.lego';
55
+ return null;
56
+ }
57
+ });
58
+
59
+ // 3. Inject undefined component
60
+ const el = document.createElement('remote-widget');
61
+ document.body.appendChild(el);
62
+
63
+ // Wait for fetch + parse + render
64
+ await new Promise(r => setTimeout(r, 100));
65
+
66
+ // 4. Verify fetch called
67
+ expect(global.fetch).toHaveBeenCalledWith('/components/remote-widget.lego');
68
+
69
+ // 5. Verify render
70
+ expect(el.shadowRoot).toBeDefined();
71
+ expect(el.shadowRoot.textContent).toContain('Loaded from Server: Remote Data');
72
+
73
+ // 6. Verify style injection
74
+ const styles = el.shadowRoot.querySelector('style');
75
+ expect(styles.textContent).toContain('.remote-box { color: blue; }');
76
+ });
77
+
78
+ it('should support async loader (Promise) for custom fetching', async () => {
79
+ // Simulate user doing their own authenticated fetch
80
+ const mockSFC = `<template><div>Async Auth Content</div></template>`;
81
+
82
+ await window.Lego.init(document.body, {
83
+ loader: async (tag) => {
84
+ if (tag === 'auth-widget') {
85
+ // Simulate network delay
86
+ await new Promise(r => setTimeout(r, 10));
87
+ return mockSFC;
88
+ }
89
+ }
90
+ });
91
+
92
+ const el = document.createElement('auth-widget');
93
+ document.body.appendChild(el);
94
+
95
+ // Wait for async loader
96
+ await new Promise(r => setTimeout(r, 100));
97
+
98
+ expect(el.shadowRoot).toBeDefined();
99
+ expect(el.shadowRoot.textContent).toContain('Async Auth Content');
100
+ });
101
+
102
+ it('should fail gracefully if loader returns null', async () => {
103
+ await window.Lego.init(document.body, {
104
+ loader: () => null
105
+ });
106
+
107
+ const el = document.createElement('unknown-widget');
108
+ document.body.appendChild(el);
109
+
110
+ await new Promise(r => setTimeout(r, 50));
111
+ expect(global.fetch).not.toHaveBeenCalled();
112
+ expect(el.shadowRoot).toBeNull();
113
+ });
114
+ });