lego-dom 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/main.js +24 -3
- package/main.min.js +7 -0
- package/package.json +3 -1
- package/vite-plugin.js +0 -14
- package/.github/workflows/deploy-docs.yml +0 -56
- package/.legodom +0 -87
- package/docs/.vitepress/config.js +0 -162
- package/docs/api/config.md +0 -95
- package/docs/api/define.md +0 -58
- package/docs/api/directives.md +0 -50
- package/docs/api/globals.md +0 -29
- package/docs/api/index.md +0 -30
- package/docs/api/lifecycle.md +0 -40
- package/docs/api/route.md +0 -37
- package/docs/api/vite-plugin.md +0 -58
- package/docs/contributing/01-welcome.md +0 -38
- package/docs/contributing/02-registry.md +0 -133
- package/docs/contributing/03-batcher.md +0 -110
- package/docs/contributing/04-reactivity.md +0 -87
- package/docs/contributing/05-caching.md +0 -59
- package/docs/contributing/06-init.md +0 -136
- package/docs/contributing/07-observer.md +0 -72
- package/docs/contributing/08-snap.md +0 -140
- package/docs/contributing/09-diffing.md +0 -69
- package/docs/contributing/10-studs.md +0 -78
- package/docs/contributing/11-scanner.md +0 -117
- package/docs/contributing/12-render.md +0 -138
- package/docs/contributing/13-directives.md +0 -243
- package/docs/contributing/14-events.md +0 -57
- package/docs/contributing/15-router.md +0 -57
- package/docs/contributing/16-state.md +0 -47
- package/docs/contributing/17-legodom.md +0 -48
- package/docs/contributing/index.md +0 -24
- package/docs/examples/form.md +0 -42
- package/docs/examples/index.md +0 -104
- package/docs/examples/routing.md +0 -409
- package/docs/examples/sfc-showcase.md +0 -34
- package/docs/examples/todo-app.md +0 -383
- package/docs/guide/cdn-usage.md +0 -328
- package/docs/guide/components.md +0 -412
- package/docs/guide/directives.md +0 -539
- package/docs/guide/directory-structure.md +0 -248
- package/docs/guide/faq.md +0 -210
- package/docs/guide/getting-started.md +0 -262
- package/docs/guide/index.md +0 -88
- package/docs/guide/lifecycle.md +0 -525
- package/docs/guide/quick-start.md +0 -49
- package/docs/guide/reactivity.md +0 -415
- package/docs/guide/routing.md +0 -334
- package/docs/guide/server-side.md +0 -134
- package/docs/guide/sfc.md +0 -420
- package/docs/guide/templating.md +0 -388
- package/docs/index.md +0 -160
- package/docs/public/logo.svg +0 -17
- package/docs/router/basic-routing.md +0 -103
- package/docs/router/cold-entry.md +0 -91
- package/docs/router/history.md +0 -69
- package/docs/router/index.md +0 -73
- package/docs/router/resolver.md +0 -74
- package/docs/router/surgical-swaps.md +0 -134
- package/docs/tutorial/01-project-setup.md +0 -152
- package/docs/tutorial/02-your-first-component.md +0 -226
- package/docs/tutorial/03-adding-routes.md +0 -279
- package/docs/tutorial/04-multi-page-app.md +0 -329
- package/docs/tutorial/05-state-and-globals.md +0 -285
- package/docs/tutorial/index.md +0 -40
- package/examples/vite-app/README.md +0 -71
- package/examples/vite-app/index.html +0 -42
- package/examples/vite-app/package.json +0 -18
- package/examples/vite-app/src/app.css +0 -3
- package/examples/vite-app/src/app.js +0 -29
- package/examples/vite-app/src/components/app-navbar.lego +0 -34
- package/examples/vite-app/src/components/customers/customer-details.lego +0 -24
- package/examples/vite-app/src/components/customers/customer-orders.lego +0 -21
- package/examples/vite-app/src/components/customers/order-list.lego +0 -55
- package/examples/vite-app/src/components/greeting-card.lego +0 -41
- package/examples/vite-app/src/components/sample-component.lego +0 -75
- package/examples/vite-app/src/components/shells/customers-shell.lego +0 -21
- package/examples/vite-app/src/components/side-menu.lego +0 -46
- package/examples/vite-app/src/components/todo-list.lego +0 -239
- package/examples/vite-app/src/components/widgets/user-card.lego +0 -27
- package/examples/vite-app/vite.config.js +0 -22
- package/tests/error.test.js +0 -74
- package/tests/main.test.js +0 -103
- package/tests/memory.test.js +0 -68
- package/tests/monitoring.test.js +0 -74
- package/tests/naming.test.js +0 -74
- package/tests/parse-lego.test.js +0 -65
- package/tests/security.test.js +0 -67
- package/tests/server.test.js +0 -114
- package/tests/syntax.test.js +0 -67
package/tests/parse-lego.test.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
parseLego,
|
|
4
|
-
deriveComponentName,
|
|
5
|
-
generateDefineCall,
|
|
6
|
-
validateLego
|
|
7
|
-
} from '../parse-lego.js';
|
|
8
|
-
|
|
9
|
-
describe('parse-lego tests', () => {
|
|
10
|
-
it('should derive component name correctly', () => {
|
|
11
|
-
expect(deriveComponentName('user-card.lego')).toBe('user-card');
|
|
12
|
-
expect(deriveComponentName('src/components/nav-bar.lego')).toBe('nav-bar');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should extract attributes from template', () => {
|
|
16
|
-
const sfcContent = `
|
|
17
|
-
<template b-styles="tailwind chartist" b-id="ignored">
|
|
18
|
-
<div class="p-4">Hello [[name]]</div>
|
|
19
|
-
</template>
|
|
20
|
-
<script>
|
|
21
|
-
export default { mounted() { console.log('hi') } }
|
|
22
|
-
</script>
|
|
23
|
-
<style>
|
|
24
|
-
div { color: red; }
|
|
25
|
-
</style>
|
|
26
|
-
`;
|
|
27
|
-
const parsed = parseLego(sfcContent, 'user-profile.lego');
|
|
28
|
-
expect(parsed.componentName).toBe('user-profile');
|
|
29
|
-
expect(parsed.stylesAttr).toBe('tailwind chartist');
|
|
30
|
-
expect(parsed.template).toContain('[[name]]');
|
|
31
|
-
expect(parsed.script).toContain('mounted');
|
|
32
|
-
expect(parsed.style).toContain('color: red');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should generate Lego.define call with 4th argument', () => {
|
|
36
|
-
const sfcContent = `<template b-styles="tailwind">Hi</template>`;
|
|
37
|
-
const parsed = parseLego(sfcContent, 'test.lego');
|
|
38
|
-
const defineCall = generateDefineCall(parsed);
|
|
39
|
-
|
|
40
|
-
expect(defineCall).toContain("Lego.define('test'");
|
|
41
|
-
expect(defineCall).toContain(", 'tailwind');");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should escape correctly in template', () => {
|
|
45
|
-
const complexContent = {
|
|
46
|
-
componentName: 'test-comp',
|
|
47
|
-
template: 'I cost $5 and use `backticks`',
|
|
48
|
-
script: 'export default {}',
|
|
49
|
-
style: '',
|
|
50
|
-
stylesAttr: 'css-set'
|
|
51
|
-
};
|
|
52
|
-
const escapedCall = generateDefineCall(complexContent);
|
|
53
|
-
expect(escapedCall).toContain('\\$5');
|
|
54
|
-
expect(escapedCall).toContain('\\`backticks\\`');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should validate lego component names', () => {
|
|
58
|
-
const validResult = validateLego({ componentName: 'valid-name', template: '<div></div>' });
|
|
59
|
-
expect(validResult.valid).toBe(true);
|
|
60
|
-
|
|
61
|
-
const invalidResult = validateLego({ componentName: 'badname' });
|
|
62
|
-
expect(invalidResult.valid).toBe(false);
|
|
63
|
-
expect(invalidResult.errors.some(e => e.includes('kebab-case'))).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
});
|
package/tests/security.test.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/server.test.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/syntax.test.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
|
22
|
-
|
|
23
|
-
const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
|
|
24
|
-
eval(libCode);
|
|
25
|
-
|
|
26
|
-
describe('LegoDOM Configurable Syntax', () => {
|
|
27
|
-
beforeEach(async () => {
|
|
28
|
-
document.body.innerHTML = '';
|
|
29
|
-
window.Lego.config.syntax = 'brackets'; // Explicitly set default (though it's default in main.js now)
|
|
30
|
-
await window.Lego.init(document.body);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
window.Lego.config.syntax = 'brackets'; // Reset
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should render [[ ]] by default', async () => {
|
|
38
|
-
window.Lego.define('default-syntax', '<div>[[ msg ]]</div>');
|
|
39
|
-
const el = document.createElement('default-syntax');
|
|
40
|
-
el.setAttribute('b-data', "{ msg: 'Hello' }");
|
|
41
|
-
document.body.appendChild(el);
|
|
42
|
-
await new Promise(r => setTimeout(r, 100));
|
|
43
|
-
expect(el.shadowRoot.textContent).toBe('Hello');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should render {{ }} when syntax is mustache', async () => {
|
|
47
|
-
window.Lego.config.syntax = 'mustache';
|
|
48
|
-
|
|
49
|
-
window.Lego.define('mustache-syntax', '<div>{{ msg }}</div>');
|
|
50
|
-
const el = document.createElement('mustache-syntax');
|
|
51
|
-
el.setAttribute('b-data', "{ msg: 'Mustache World' }");
|
|
52
|
-
document.body.appendChild(el);
|
|
53
|
-
await new Promise(r => setTimeout(r, 100));
|
|
54
|
-
expect(el.shadowRoot.textContent).toBe('Mustache World');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should ignore {{ }} when syntax is brackets', async () => {
|
|
58
|
-
// Default is brackets
|
|
59
|
-
// {{ msg }} should be treated as literal text
|
|
60
|
-
window.Lego.define('mixed-syntax', '<div>{{ msg }} - [[ msg ]]</div>');
|
|
61
|
-
const el = document.createElement('mixed-syntax');
|
|
62
|
-
el.setAttribute('b-data', "{ msg: 'Active' }");
|
|
63
|
-
document.body.appendChild(el);
|
|
64
|
-
await new Promise(r => setTimeout(r, 100));
|
|
65
|
-
expect(el.shadowRoot.textContent).toBe('{{ msg }} - Active');
|
|
66
|
-
});
|
|
67
|
-
});
|