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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lego-dom",
|
|
3
|
-
"version": "1.
|
|
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": "
|
|
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: '',
|
|
17
|
+
stylesAttr: '',
|
|
18
18
|
componentName: deriveComponentName(filename)
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
+
});
|
package/tests/main.test.js
CHANGED
|
@@ -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>
|
|
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>
|
|
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
|
+
});
|
package/tests/parse-lego.test.js
CHANGED
|
@@ -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
|
|
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('
|
|
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
|
+
});
|