lego-dom 0.0.7 → 0.0.8

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 (143) hide show
  1. package/.github/workflows/deploy-docs.yml +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/docs/.vitepress/config.js +107 -0
  5. package/docs/.vitepress/dist/404.html +22 -0
  6. package/docs/.vitepress/dist/api/define.html +35 -0
  7. package/docs/.vitepress/dist/api/directives.html +32 -0
  8. package/docs/.vitepress/dist/api/globals.html +27 -0
  9. package/docs/.vitepress/dist/api/index.html +25 -0
  10. package/docs/.vitepress/dist/api/lifecycle.html +38 -0
  11. package/docs/.vitepress/dist/api/route.html +34 -0
  12. package/docs/.vitepress/dist/api/vite-plugin.html +37 -0
  13. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.js +11 -0
  14. package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.lean.js +1 -0
  15. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.js +8 -0
  16. package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.lean.js +1 -0
  17. package/docs/.vitepress/dist/assets/api_globals.md.DOjt7AV0.js +3 -0
  18. package/docs/.vitepress/dist/assets/api_globals.md.DOjt7AV0.lean.js +1 -0
  19. package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.js +1 -0
  20. package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.lean.js +1 -0
  21. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.js +14 -0
  22. package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.lean.js +1 -0
  23. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.js +10 -0
  24. package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.lean.js +1 -0
  25. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DNn9VhL5.js +13 -0
  26. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DNn9VhL5.lean.js +1 -0
  27. package/docs/.vitepress/dist/assets/app.BG5s3B0P.js +1 -0
  28. package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DQmuWC2Z.js +1 -0
  29. package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.BO-PSxt1.js +9 -0
  30. package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +19 -0
  31. package/docs/.vitepress/dist/assets/chunks/theme.DA-iSa9B.js +2 -0
  32. package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.js +34 -0
  33. package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.lean.js +1 -0
  34. package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.js +30 -0
  35. package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.lean.js +1 -0
  36. package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.js +338 -0
  37. package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.lean.js +1 -0
  38. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.js +13 -0
  39. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.lean.js +1 -0
  40. package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.js +297 -0
  41. package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.lean.js +1 -0
  42. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.js +182 -0
  43. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.lean.js +1 -0
  44. package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.js +174 -0
  45. package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.lean.js +1 -0
  46. package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.js +1 -0
  47. package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.lean.js +1 -0
  48. package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.js +140 -0
  49. package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.lean.js +1 -0
  50. package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.js +107 -0
  51. package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.lean.js +1 -0
  52. package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.js +2 -0
  53. package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.lean.js +1 -0
  54. package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.js +304 -0
  55. package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.lean.js +1 -0
  56. package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.js +33 -0
  57. package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.lean.js +1 -0
  58. package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.js +135 -0
  59. package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.lean.js +1 -0
  60. package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.js +193 -0
  61. package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.lean.js +1 -0
  62. package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.js +187 -0
  63. package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.lean.js +1 -0
  64. package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.js +119 -0
  65. package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.lean.js +1 -0
  66. package/docs/.vitepress/dist/assets/index.md.xV1taCED.js +23 -0
  67. package/docs/.vitepress/dist/assets/index.md.xV1taCED.lean.js +1 -0
  68. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  69. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  70. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  71. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  72. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  73. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  74. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  75. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  76. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  77. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  78. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  79. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  80. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  81. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  82. package/docs/.vitepress/dist/assets/style.eycE2Jhw.css +1 -0
  83. package/docs/.vitepress/dist/examples/form.html +58 -0
  84. package/docs/.vitepress/dist/examples/index.html +368 -0
  85. package/docs/.vitepress/dist/examples/routing.html +362 -0
  86. package/docs/.vitepress/dist/examples/sfc-showcase.html +37 -0
  87. package/docs/.vitepress/dist/examples/todo-app.html +321 -0
  88. package/docs/.vitepress/dist/guide/cdn-usage.html +206 -0
  89. package/docs/.vitepress/dist/guide/components.html +198 -0
  90. package/docs/.vitepress/dist/guide/contributing.html +25 -0
  91. package/docs/.vitepress/dist/guide/directives.html +164 -0
  92. package/docs/.vitepress/dist/guide/getting-started.html +131 -0
  93. package/docs/.vitepress/dist/guide/index.html +26 -0
  94. package/docs/.vitepress/dist/guide/lifecycle.html +328 -0
  95. package/docs/.vitepress/dist/guide/quick-start.html +57 -0
  96. package/docs/.vitepress/dist/guide/reactivity.html +159 -0
  97. package/docs/.vitepress/dist/guide/routing.html +217 -0
  98. package/docs/.vitepress/dist/guide/sfc.html +211 -0
  99. package/docs/.vitepress/dist/guide/templating.html +143 -0
  100. package/docs/.vitepress/dist/hashmap.json +1 -0
  101. package/docs/.vitepress/dist/index.html +47 -0
  102. package/docs/.vitepress/dist/logo.svg +38 -0
  103. package/docs/.vitepress/dist/vp-icons.css +1 -0
  104. package/docs/api/define.md +31 -0
  105. package/docs/api/directives.md +42 -0
  106. package/docs/api/globals.md +29 -0
  107. package/docs/api/index.md +29 -0
  108. package/docs/api/lifecycle.md +40 -0
  109. package/docs/api/route.md +37 -0
  110. package/docs/api/vite-plugin.md +58 -0
  111. package/docs/examples/form.md +42 -0
  112. package/docs/examples/index.md +104 -0
  113. package/docs/examples/routing.md +409 -0
  114. package/docs/examples/sfc-showcase.md +34 -0
  115. package/docs/examples/todo-app.md +383 -0
  116. package/docs/guide/cdn-usage.md +320 -0
  117. package/docs/guide/components.md +394 -0
  118. package/docs/guide/contributing.md +32 -0
  119. package/docs/guide/directives.md +430 -0
  120. package/docs/guide/getting-started.md +233 -0
  121. package/docs/guide/index.md +88 -0
  122. package/docs/guide/lifecycle.md +493 -0
  123. package/docs/guide/quick-start.md +46 -0
  124. package/docs/guide/reactivity.md +394 -0
  125. package/docs/guide/routing.md +373 -0
  126. package/docs/guide/sfc.md +381 -0
  127. package/docs/guide/templating.md +383 -0
  128. package/docs/index.md +126 -0
  129. package/docs/public/logo.svg +38 -0
  130. package/examples/vite-app/README.md +71 -0
  131. package/examples/vite-app/index.html +45 -0
  132. package/examples/vite-app/package.json +16 -0
  133. package/examples/vite-app/src/components/greeting-card.lego +41 -0
  134. package/examples/vite-app/src/components/sample-component.lego +75 -0
  135. package/examples/vite-app/src/main.js +11 -0
  136. package/examples/vite-app/vite.config.js +16 -0
  137. package/examples.js +99 -0
  138. package/package.json +33 -5
  139. package/parse-lego.js +119 -0
  140. package/parse-lego.test.js +107 -0
  141. package/vite-plugin.js +133 -0
  142. package/.ignore/auto.html +0 -135
  143. package/.ignore/test.html +0 -73
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <style>
3
+ self {
4
+ display: block;
5
+ padding: 1rem;
6
+ background: #f3f4f6;
7
+ border-radius: 0.5rem;
8
+ margin: 0.5rem 0;
9
+ }
10
+
11
+ input {
12
+ padding: 0.5rem;
13
+ border: 1px solid #d1d5db;
14
+ border-radius: 0.25rem;
15
+ margin-right: 0.5rem;
16
+ font-size: 1rem;
17
+ }
18
+
19
+ .greeting {
20
+ margin-top: 1rem;
21
+ padding: 1rem;
22
+ background: white;
23
+ border-radius: 0.25rem;
24
+ font-size: 1.25rem;
25
+ color: #1f2937;
26
+ }
27
+ </style>
28
+
29
+ <div>
30
+ <input b-sync="name" type="text" placeholder="Enter your name">
31
+ <div class="greeting" b-if="name">
32
+ Hello, {{ name }}! 👋
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ export default {
39
+ name: ''
40
+ }
41
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <style>
3
+ self {
4
+ display: block;
5
+ padding: 1.5rem;
6
+ margin: 1rem 0;
7
+ border: 2px solid #4f46e5;
8
+ border-radius: 0.5rem;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ color: white;
11
+ font-family: system-ui, -apple-system, sans-serif;
12
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
13
+ }
14
+
15
+ .header {
16
+ font-size: 1.5rem;
17
+ font-weight: bold;
18
+ margin-bottom: 0.5rem;
19
+ }
20
+
21
+ .content {
22
+ margin: 1rem 0;
23
+ line-height: 1.6;
24
+ }
25
+
26
+ button {
27
+ background: white;
28
+ color: #4f46e5;
29
+ border: none;
30
+ padding: 0.5rem 1rem;
31
+ border-radius: 0.25rem;
32
+ font-weight: 600;
33
+ cursor: pointer;
34
+ transition: transform 0.2s;
35
+ }
36
+
37
+ button:hover {
38
+ transform: scale(1.05);
39
+ }
40
+
41
+ .count {
42
+ display: inline-block;
43
+ background: rgba(255, 255, 255, 0.2);
44
+ padding: 0.25rem 0.75rem;
45
+ border-radius: 0.25rem;
46
+ margin-left: 0.5rem;
47
+ }
48
+ </style>
49
+
50
+ <div class="header">{{ title }}</div>
51
+ <div class="content">
52
+ <p>This is a sample LegoJS component loaded from a .lego file!</p>
53
+ <p>Message: {{ message }}</p>
54
+ </div>
55
+ <button @click="incrementCount()">
56
+ Click me!
57
+ <span class="count">{{ count }}</span>
58
+ </button>
59
+ </template>
60
+
61
+ <script>
62
+ export default {
63
+ title: 'Sample Component',
64
+ message: 'Hello from .lego file!',
65
+ count: 0,
66
+
67
+ incrementCount() {
68
+ this.count++;
69
+ },
70
+
71
+ mounted() {
72
+ console.log('Sample component mounted!');
73
+ }
74
+ }
75
+ </script>
@@ -0,0 +1,11 @@
1
+ // Import LegoJS core
2
+ import { Lego } from 'lego-dom/main.js';
3
+
4
+ // Import virtual module that auto-discovers and registers all .lego components
5
+ import registerComponents from 'virtual:lego-components';
6
+
7
+ // Register all auto-discovered components
8
+ registerComponents();
9
+
10
+ // Initialize LegoJS
11
+ console.log('LegoJS initialized with auto-discovered components!');
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+ import legoPlugin from 'lego-dom/vite-plugin';
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ legoPlugin({
7
+ componentsDir: './src/components',
8
+ include: ['**/*.lego']
9
+ })
10
+ ],
11
+ resolve: {
12
+ alias: {
13
+ 'lego-dom/main.js': new URL('../../main.js', import.meta.url).pathname
14
+ }
15
+ }
16
+ });
package/examples.js ADDED
@@ -0,0 +1,99 @@
1
+ export const examples = {
2
+ counter: `<!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <title>LegoJS Counter</title>
6
+ </head>
7
+ <body>
8
+ <template b-id="click-counter">
9
+ <style>
10
+ button { font-size: 1.2rem; padding: 0.5rem 1rem; }
11
+ </style>
12
+ <p>Count: {{ count }}</p>
13
+ <button @click="count++">Increment</button>
14
+ </template>
15
+
16
+ <click-counter b-data="{ count: 0 }"></click-counter>
17
+
18
+ <script src="https://unpkg.com/lego-dom/main.js"></script>
19
+ </body>
20
+ </html>`,
21
+
22
+ todo: `<!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
27
+ <title>Todo App - LegoJS</title>
28
+ <style>
29
+ body {
30
+ font-family: system-ui, -apple-system, sans-serif;
31
+ max-width: 600px;
32
+ margin: 2rem auto;
33
+ padding: 0 1rem;
34
+ background: #f5f5f5;
35
+ }
36
+ h1 {
37
+ text-align: center;
38
+ color: #333;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+
44
+ <script src="https://unpkg.com/lego-dom/main.js"></script>
45
+
46
+
47
+
48
+ <script>
49
+ </script>
50
+ </body>
51
+ </html>`,
52
+
53
+ form: `<!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <title>LegoJS Form</title>
57
+ </head>
58
+ <body>
59
+ <template b-id="login-form">
60
+ <form @submit.prevent="login()">
61
+ <div>
62
+ <label>Email:</label>
63
+ <input type="email" b-sync="email">
64
+ </div>
65
+
66
+ <div>
67
+ <label>Password:</label>
68
+ <input type="password" b-sync="password">
69
+ </div>
70
+
71
+ <p b-if="error" style="color: red">{{ error }}</p>
72
+
73
+ <button type="submit">Login</button>
74
+ </form>
75
+ </template>
76
+
77
+ <login-form></login-form>
78
+
79
+ <script src="https://unpkg.com/lego-dom/main.js"></script>
80
+
81
+ <script>
82
+ Lego.define('login-form', document.querySelector('template[b-id="login-form"]').innerHTML, {
83
+ email: '',
84
+ password: '',
85
+ error: '',
86
+
87
+ login() {
88
+ if (!this.email || !this.password) {
89
+ this.error = 'Please fill in all fields';
90
+ return;
91
+ }
92
+ alert(\`Logging in as \${this.email}\`);
93
+ this.error = '';
94
+ }
95
+ });
96
+ </script>
97
+ </body>
98
+ </html>`
99
+ };
package/package.json CHANGED
@@ -1,16 +1,44 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
+ "license": "MIT",
4
5
  "description": "A feature-rich web components + SFC frontend framework",
5
6
  "main": "main.js",
6
7
  "type": "module",
7
- "keywords": ["framework", "sfc", "components", "lego", "legokit"],
8
+ "exports": {
9
+ ".": "./main.js",
10
+ "./main.js": "./main.js",
11
+ "./vite-plugin": "./vite-plugin.js",
12
+ "./parse-lego": "./parse-lego.js"
13
+ },
14
+ "keywords": [
15
+ "framework",
16
+ "sfc",
17
+ "components",
18
+ "lego",
19
+ "legokit"
20
+ ],
8
21
  "author": "",
9
22
  "scripts": {
10
- "test": "vitest run"
23
+ "test": "vitest run",
24
+ "docs:dev": "vitepress dev docs",
25
+ "docs:build": "vitepress build docs",
26
+ "docs:preview": "vitepress preview docs"
11
27
  },
12
28
  "devDependencies": {
13
- "vitest": "^1.0.0",
14
- "jsdom": "^22.0.0"
29
+ "jsdom": "^22.0.0",
30
+ "vitepress": "^1.6.4",
31
+ "vitest": "^1.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "vite": "^4.0.0 || ^5.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "vite": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "fast-glob": "^3.3.2"
15
43
  }
16
44
  }
package/parse-lego.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Parser for .lego Single File Components
3
+ * Extracts template, script, and style sections from .lego files
4
+ */
5
+
6
+ /**
7
+ * Parse a .lego file content into structured sections
8
+ * @param {string} content - Raw .lego file content
9
+ * @param {string} filename - Filename for error reporting
10
+ * @returns {{template: string, script: string, style: string, componentName: string}}
11
+ */
12
+ export function parseLego(content, filename = 'component.lego') {
13
+ const result = {
14
+ template: '',
15
+ script: '',
16
+ style: '',
17
+ componentName: deriveComponentName(filename)
18
+ };
19
+
20
+ // Extract template section
21
+ const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
22
+ if (templateMatch) {
23
+ result.template = templateMatch[1].trim();
24
+ }
25
+
26
+ // Extract script section
27
+ const scriptMatch = content.match(/<script>([\s\S]*?)<\/script>/);
28
+ if (scriptMatch) {
29
+ result.script = scriptMatch[1].trim();
30
+ }
31
+
32
+ // Extract style section
33
+ const styleMatch = content.match(/<style>([\s\S]*?)<\/style>/);
34
+ if (styleMatch) {
35
+ result.style = styleMatch[1].trim();
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ /**
42
+ * Derive component name from filename
43
+ * e.g., "sample-component.lego" -> "sample-component"
44
+ * @param {string} filename
45
+ * @returns {string}
46
+ */
47
+ export function deriveComponentName(filename) {
48
+ const basename = filename.split('/').pop();
49
+ return basename.replace(/\.lego$/, '');
50
+ }
51
+
52
+ /**
53
+ * Generate Lego.define() code from parsed .lego file
54
+ * @param {object} parsed - Parsed .lego file object
55
+ * @returns {string} - JavaScript code string
56
+ */
57
+ export function generateDefineCall(parsed) {
58
+ const { componentName, template, script, style } = parsed;
59
+
60
+ // Build template HTML
61
+ let templateHTML = '';
62
+ if (style) {
63
+ templateHTML += `<style>${style}</style>\n`;
64
+ }
65
+ if (template) {
66
+ templateHTML += template;
67
+ }
68
+
69
+ // Extract logic object from script
70
+ let logicCode = '{}';
71
+ if (script) {
72
+ // Try to extract default export
73
+ const defaultExportMatch = script.match(/export\s+default\s+({[\s\S]*})/);
74
+ if (defaultExportMatch) {
75
+ logicCode = defaultExportMatch[1];
76
+ } else {
77
+ // If no export default, assume entire script is the logic object
78
+ logicCode = script;
79
+ }
80
+ }
81
+
82
+ // Generate the Lego.define call
83
+ return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode});`;
84
+ }
85
+
86
+ /**
87
+ * Escape backticks and ${} in template strings
88
+ * @param {string} str
89
+ * @returns {string}
90
+ */
91
+ function escapeTemplate(str) {
92
+ return str.replace(/`/g, '\\`').replace(/\$/g, '\\$');
93
+ }
94
+
95
+ /**
96
+ * Validate .lego file structure
97
+ * @param {object} parsed - Parsed .lego file object
98
+ * @returns {{valid: boolean, errors: string[]}}
99
+ */
100
+ export function validateLego(parsed) {
101
+ const errors = [];
102
+
103
+ if (!parsed.template && !parsed.script && !parsed.style) {
104
+ errors.push('Component must have at least one section: <template>, <script>, or <style>');
105
+ }
106
+
107
+ if (!parsed.componentName) {
108
+ errors.push('Unable to derive component name from filename');
109
+ }
110
+
111
+ if (parsed.componentName && !/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(parsed.componentName)) {
112
+ errors.push(`Component name "${parsed.componentName}" must be kebab-case with at least one hyphen (e.g., "my-component")`);
113
+ }
114
+
115
+ return {
116
+ valid: errors.length === 0,
117
+ errors
118
+ };
119
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseLego, generateDefineCall, validateLego, deriveComponentName } from './parse-lego.js';
3
+
4
+ describe('LegoJS SFC Parser', () => {
5
+ describe('deriveComponentName', () => {
6
+ it('should derive component name from filename', () => {
7
+ expect(deriveComponentName('sample-component.lego')).toBe('sample-component');
8
+ expect(deriveComponentName('path/to/my-button.lego')).toBe('my-button');
9
+ });
10
+ });
11
+
12
+ describe('parseLego', () => {
13
+ it('should parse all three sections', () => {
14
+ const content = `
15
+ <template>
16
+ <h1>{{ title }}</h1>
17
+ </template>
18
+
19
+ <script>
20
+ export default {
21
+ title: 'Hello'
22
+ }
23
+ </script>
24
+
25
+ <style>
26
+ self { color: red; }
27
+ </style>
28
+ `;
29
+
30
+ const result = parseLego(content, 'test-component.lego');
31
+ expect(result.componentName).toBe('test-component');
32
+ expect(result.template).toContain('<h1>{{ title }}</h1>');
33
+ expect(result.script).toContain('export default');
34
+ expect(result.style).toContain('self { color: red; }');
35
+ });
36
+
37
+ it('should handle components with only template', () => {
38
+ const content = '<template><p>Hello</p></template>';
39
+ const result = parseLego(content, 'simple.lego');
40
+ expect(result.template).toBe('<p>Hello</p>');
41
+ expect(result.script).toBe('');
42
+ expect(result.style).toBe('');
43
+ });
44
+
45
+ it('should handle components with only script', () => {
46
+ const content = '<script>export default { count: 0 }</script>';
47
+ const result = parseLego(content, 'logic.lego');
48
+ expect(result.script).toContain('count: 0');
49
+ expect(result.template).toBe('');
50
+ });
51
+ });
52
+
53
+ describe('validateLego', () => {
54
+ it('should validate correct component name', () => {
55
+ const parsed = {
56
+ componentName: 'my-component',
57
+ template: '<div>Test</div>',
58
+ script: '',
59
+ style: ''
60
+ };
61
+ const result = validateLego(parsed);
62
+ expect(result.valid).toBe(true);
63
+ expect(result.errors).toHaveLength(0);
64
+ });
65
+
66
+ it('should reject invalid component names', () => {
67
+ const parsed = {
68
+ componentName: 'MyComponent', // Not kebab-case
69
+ template: '<div>Test</div>',
70
+ script: '',
71
+ style: ''
72
+ };
73
+ const result = validateLego(parsed);
74
+ expect(result.valid).toBe(false);
75
+ expect(result.errors.length).toBeGreaterThan(0);
76
+ });
77
+
78
+ it('should require at least one section', () => {
79
+ const parsed = {
80
+ componentName: 'empty-component',
81
+ template: '',
82
+ script: '',
83
+ style: ''
84
+ };
85
+ const result = validateLego(parsed);
86
+ expect(result.valid).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('generateDefineCall', () => {
91
+ it('should generate valid Lego.define call', () => {
92
+ const parsed = {
93
+ componentName: 'test-comp',
94
+ template: '<button>Click</button>',
95
+ script: 'export default { count: 0 }',
96
+ style: 'self { color: blue; }'
97
+ };
98
+
99
+ const result = generateDefineCall(parsed);
100
+ expect(result).toContain('Lego.define');
101
+ expect(result).toContain('test-comp');
102
+ expect(result).toContain('<button>Click</button>');
103
+ expect(result).toContain('{ count: 0 }');
104
+ expect(result).toContain('self { color: blue; }');
105
+ });
106
+ });
107
+ });
package/vite-plugin.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Vite plugin for LegoJS Single File Components
3
+ * Auto-discovers and transforms .lego files
4
+ */
5
+
6
+ import { parseLego, generateDefineCall, validateLego } from './parse-lego.js';
7
+ import path from 'path';
8
+ import fg from 'fast-glob';
9
+
10
+ const VIRTUAL_MODULE_ID = 'virtual:lego-components';
11
+ const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
12
+
13
+ /**
14
+ * Vite plugin for LegoJS SFC support
15
+ * @param {object} options - Plugin options
16
+ * @param {string} options.componentsDir - Directory to search for .lego files
17
+ * @param {string[]} options.include - Glob patterns to include
18
+ * @returns {import('vite').Plugin}
19
+ */
20
+ export default function legoPlugin(options = {}) {
21
+ const {
22
+ componentsDir = './src/components',
23
+ include = ['**/*.lego']
24
+ } = options;
25
+
26
+ let config;
27
+ let legoFiles = [];
28
+
29
+ return {
30
+ name: 'vite-plugin-lego',
31
+
32
+ configResolved(resolvedConfig) {
33
+ config = resolvedConfig;
34
+ },
35
+
36
+ async buildStart() {
37
+ // Auto-discover .lego files
38
+ const root = config?.root || process.cwd();
39
+ const searchPath = path.resolve(root, componentsDir);
40
+
41
+ try {
42
+ legoFiles = await fg(include, {
43
+ cwd: searchPath,
44
+ absolute: true
45
+ });
46
+
47
+ if (legoFiles.length > 0) {
48
+ console.log(`[vite-plugin-lego] Discovered ${legoFiles.length} component(s):`);
49
+ legoFiles.forEach(file => {
50
+ const name = path.basename(file);
51
+ console.log(` - ${name}`);
52
+ });
53
+ }
54
+ } catch (err) {
55
+ console.warn(`[vite-plugin-lego] Could not scan for .lego files in ${searchPath}:`, err.message);
56
+ legoFiles = [];
57
+ }
58
+ },
59
+
60
+ resolveId(id) {
61
+ if (id === VIRTUAL_MODULE_ID) {
62
+ return RESOLVED_VIRTUAL_MODULE_ID;
63
+ }
64
+ },
65
+
66
+ async load(id) {
67
+ // Handle virtual module that imports all .lego components
68
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
69
+ const imports = legoFiles.map((file, index) =>
70
+ `import component${index} from '${file}?lego-component';`
71
+ ).join('\n');
72
+
73
+ const exports = `export default function registerComponents() {\n // Components are auto-registered when imported\n}`;
74
+
75
+ return `${imports}\n\n${exports}`;
76
+ }
77
+
78
+ // Handle individual .lego files
79
+ if (id.endsWith('.lego') || id.includes('.lego?')) {
80
+ const filePath = id.split('?')[0];
81
+ const fs = await import('fs');
82
+ const content = fs.readFileSync(filePath, 'utf-8');
83
+ const filename = path.basename(filePath);
84
+
85
+ const parsed = parseLego(content, filename);
86
+ const validation = validateLego(parsed);
87
+
88
+ if (!validation.valid) {
89
+ throw new Error(`Invalid .lego file "${filename}":\n${validation.errors.join('\n')}`);
90
+ }
91
+
92
+ const defineCall = generateDefineCall(parsed);
93
+
94
+ // Return as module that executes the define call
95
+ return `
96
+ import { Lego } from 'lego-dom/main.js';
97
+
98
+ ${defineCall}
99
+
100
+ export default '${parsed.componentName}';
101
+ `;
102
+ }
103
+ },
104
+
105
+ handleHotUpdate({ file, server }) {
106
+ if (file.endsWith('.lego')) {
107
+ console.log(`[vite-plugin-lego] Hot reload: ${path.basename(file)}`);
108
+ // Trigger full reload for .lego files
109
+ server.ws.send({
110
+ type: 'full-reload',
111
+ path: '*'
112
+ });
113
+ }
114
+ },
115
+
116
+ transform(code, id) {
117
+ // Transform .lego files during build
118
+ if (id.endsWith('.lego') && !id.includes('?')) {
119
+ const parsed = parseLego(code, path.basename(id));
120
+ const validation = validateLego(parsed);
121
+
122
+ if (!validation.valid) {
123
+ throw new Error(`Invalid .lego file:\n${validation.errors.join('\n')}`);
124
+ }
125
+
126
+ return {
127
+ code: generateDefineCall(parsed),
128
+ map: null
129
+ };
130
+ }
131
+ }
132
+ };
133
+ }