lego-dom 0.0.7 → 0.0.9

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 (147) hide show
  1. package/.github/workflows/deploy-docs.yml +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -314
  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.CEznyRAY.js +3 -0
  18. package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.lean.js +1 -0
  19. package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.js +1 -0
  20. package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.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.DC8Li09k.js +13 -0
  26. package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.lean.js +1 -0
  27. package/docs/.vitepress/dist/assets/app.BfblNDJy.js +1 -0
  28. package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.Crdp7-Zp.js +1 -0
  29. package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.C18E44rY.js +9 -0
  30. package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +19 -0
  31. package/docs/.vitepress/dist/assets/chunks/theme.VX3itTW6.js +2 -0
  32. package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.js +34 -0
  33. package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.lean.js +1 -0
  34. package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.js +28 -0
  35. package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.lean.js +1 -0
  36. package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.js +338 -0
  37. package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.lean.js +1 -0
  38. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.js +13 -0
  39. package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.lean.js +1 -0
  40. package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.js +297 -0
  41. package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.lean.js +1 -0
  42. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.js +182 -0
  43. package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.lean.js +1 -0
  44. package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.js +174 -0
  45. package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.lean.js +1 -0
  46. package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.js +1 -0
  47. package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.lean.js +1 -0
  48. package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.js +140 -0
  49. package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.lean.js +1 -0
  50. package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.js +107 -0
  51. package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.lean.js +1 -0
  52. package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.js +2 -0
  53. package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.lean.js +1 -0
  54. package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.js +304 -0
  55. package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.lean.js +1 -0
  56. package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.js +33 -0
  57. package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.lean.js +1 -0
  58. package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.js +135 -0
  59. package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.lean.js +1 -0
  60. package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.js +193 -0
  61. package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.lean.js +1 -0
  62. package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.js +187 -0
  63. package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.lean.js +1 -0
  64. package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.js +119 -0
  65. package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.lean.js +1 -0
  66. package/docs/.vitepress/dist/assets/index.md.M4_o26kF.js +23 -0
  67. package/docs/.vitepress/dist/assets/index.md.M4_o26kF.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 +52 -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 +17 -0
  130. package/examples/vite-app/README.md +71 -0
  131. package/examples/vite-app/index.html +49 -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/components/todo-list.lego +242 -0
  136. package/examples/vite-app/src/main.js +11 -0
  137. package/examples/vite-app/vite.config.js +17 -0
  138. package/examples.js +99 -0
  139. package/go.html +117 -0
  140. package/lego.js +2 -0
  141. package/main.js +41 -35
  142. package/package.json +39 -6
  143. package/parse-lego.js +119 -0
  144. package/parse-lego.test.js +107 -0
  145. package/vite-plugin.js +133 -0
  146. package/.ignore/auto.html +0 -135
  147. package/.ignore/test.html +0 -73
package/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const Lego = (() => {
2
2
  const registry = {}, proxyCache = new WeakMap(), privateData = new WeakMap();
3
3
  const forPools = new WeakMap();
4
-
4
+
5
5
  const sfcLogic = new Map();
6
6
  const sharedStates = new Map(); // Track singleton states for $registry
7
7
  const routes = [];
@@ -17,22 +17,22 @@ const Lego = (() => {
17
17
  let queued = false;
18
18
  const componentsToUpdate = new Set();
19
19
  let isProcessing = false;
20
-
20
+
21
21
  return {
22
22
  add: (el) => {
23
- if (!el || isProcessing) return;
23
+ if (!el || isProcessing) return;
24
24
  componentsToUpdate.add(el);
25
25
  if (queued) return;
26
26
  queued = true;
27
-
27
+
28
28
  requestAnimationFrame(() => {
29
29
  isProcessing = true;
30
30
  const batch = Array.from(componentsToUpdate);
31
31
  componentsToUpdate.clear();
32
32
  queued = false;
33
-
33
+
34
34
  batch.forEach(el => render(el));
35
-
35
+
36
36
  setTimeout(() => {
37
37
  batch.forEach(el => {
38
38
  const state = el._studs;
@@ -124,7 +124,7 @@ const Lego = (() => {
124
124
  const safeEval = (expr, context) => {
125
125
  try {
126
126
  const scope = context.state || {};
127
-
127
+
128
128
  const helpers = {
129
129
  $ancestors: (tag) => findAncestorState(context.self, tag),
130
130
  // Helper to access shared state by tag name
@@ -146,7 +146,7 @@ const Lego = (() => {
146
146
  }
147
147
  }
148
148
  `);
149
-
149
+
150
150
  const result = func.call(scope, context.global, context.self, context.event, helpers);
151
151
  if (typeof result === 'function') return result.call(scope, context.event);
152
152
  return result;
@@ -167,10 +167,10 @@ const Lego = (() => {
167
167
  const bind = (container, componentRoot, loopCtx = null) => {
168
168
  const state = componentRoot._studs;
169
169
  const elements = container instanceof Element ? [container, ...container.querySelectorAll('*')] : container.querySelectorAll('*');
170
-
170
+
171
171
  elements.forEach(child => {
172
172
  const childData = getPrivateData(child);
173
- if (childData.bound) return;
173
+ if (childData.bound) return;
174
174
 
175
175
  [...child.attributes].forEach(attr => {
176
176
  if (attr.name.startsWith('@')) {
@@ -234,14 +234,14 @@ const Lego = (() => {
234
234
  if (node.hasAttribute('b-for')) {
235
235
  const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+(.+)\s*$/);
236
236
  if (match) {
237
- bindings.push({
238
- type: 'b-for',
239
- node,
240
- itemName: match[1],
241
- listName: match[2].trim(),
242
- template: node.innerHTML
237
+ bindings.push({
238
+ type: 'b-for',
239
+ node,
240
+ itemName: match[1],
241
+ listName: match[2].trim(),
242
+ template: node.innerHTML
243
243
  });
244
- node.innerHTML = '';
244
+ node.innerHTML = '';
245
245
  }
246
246
  }
247
247
  if (node.hasAttribute('b-text')) bindings.push({ type: 'b-text', node, path: node.getAttribute('b-text') });
@@ -326,12 +326,12 @@ const Lego = (() => {
326
326
  }
327
327
  const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
328
328
  updateNodeBindings(child, localScope);
329
-
329
+
330
330
  child.querySelectorAll('[b-sync]').forEach(input => {
331
- const path = input.getAttribute('b-sync');
332
- if (path.startsWith(b.itemName + '.')) {
333
- syncModelValue(input, resolve(path.split('.').slice(1).join('.'), item));
334
- }
331
+ const path = input.getAttribute('b-sync');
332
+ if (path.startsWith(b.itemName + '.')) {
333
+ syncModelValue(input, resolve(path.split('.').slice(1).join('.'), item));
334
+ }
335
335
  });
336
336
  if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
337
337
  });
@@ -349,23 +349,23 @@ const Lego = (() => {
349
349
  if (!el || el.nodeType !== 1) return;
350
350
  const data = getPrivateData(el);
351
351
  const name = el.tagName.toLowerCase();
352
-
352
+
353
353
  if (registry[name] && !data.snapped) {
354
354
  data.snapped = true;
355
355
  const tpl = registry[name].content.cloneNode(true);
356
356
  const shadow = el.attachShadow({ mode: 'open' });
357
-
357
+
358
358
  const defaultLogic = sfcLogic.get(name) || {};
359
359
  const attrLogic = parseJSObject(el.getAttribute('b-data') || '{}');
360
360
  el._studs = reactive({ ...defaultLogic, ...attrLogic }, el);
361
-
361
+
362
362
  shadow.appendChild(tpl);
363
-
363
+
364
364
  const style = shadow.querySelector('style');
365
365
  if (style) {
366
366
  style.textContent = style.textContent.replace(/\bself\b/g, ':host');
367
367
  }
368
-
368
+
369
369
  bind(shadow, el);
370
370
  render(el);
371
371
 
@@ -373,9 +373,9 @@ const Lego = (() => {
373
373
  try { el._studs.mounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in mounted <${name}>:`, e); }
374
374
  }
375
375
  }
376
-
376
+
377
377
  let provider = el.parentElement;
378
- while(provider && !provider._studs) provider = provider.parentElement;
378
+ while (provider && !provider._studs) provider = provider.parentElement;
379
379
  if (provider && provider._studs) bind(el, provider);
380
380
 
381
381
  [...el.children].forEach(snap);
@@ -399,7 +399,7 @@ const Lego = (() => {
399
399
 
400
400
  if (match.middleware) {
401
401
  const allowed = await match.middleware(params, Lego.globals);
402
- if (!allowed) return;
402
+ if (!allowed) return;
403
403
  }
404
404
 
405
405
  Lego.globals.params = params;
@@ -409,13 +409,19 @@ const Lego = (() => {
409
409
  return {
410
410
  init: () => {
411
411
  document.querySelectorAll('template[b-id]').forEach(t => registry[t.getAttribute('b-id')] = t);
412
- const observer = new MutationObserver(m => m.forEach(r => {
413
- r.addedNodes.forEach(n => n.nodeType === 1 && snap(n));
414
- r.removedNodes.forEach(n => n.nodeType === 1 && unsnap(n));
412
+ const observer = new MutationObserver(m => m.forEach(r => {
413
+ r.addedNodes.forEach(n => n.nodeType === 1 && snap(n));
414
+ r.removedNodes.forEach(n => n.nodeType === 1 && unsnap(n));
415
415
  }));
416
416
  observer.observe(document.body, { childList: true, subtree: true });
417
+
418
+ // Also snap the root element (body) to catch attributes like @event
417
419
  snap(document.body);
418
420
 
421
+ // Bind body specifically to catch global listeners like @todo-added in go.html
422
+ // We pass a mock componentRoot that points to Lego.globals
423
+ bind(document.body, { _studs: Lego.globals, _data: { bound: false } });
424
+
419
425
  if (routes.length > 0) {
420
426
  window.addEventListener('popstate', _matchRoute);
421
427
  document.addEventListener('click', e => {
@@ -436,10 +442,10 @@ const Lego = (() => {
436
442
  t.innerHTML = templateHTML;
437
443
  registry[tagName] = t;
438
444
  sfcLogic.set(tagName, logic);
439
-
445
+
440
446
  // Initialize shared state for $registry singleton
441
447
  sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
442
-
448
+
443
449
  document.querySelectorAll(tagName).forEach(snap);
444
450
  },
445
451
  route: (path, tagName, middleware = null) => {
package/package.json CHANGED
@@ -1,16 +1,49 @@
1
1
  {
2
2
  "name": "lego-dom",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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
- "author": "",
8
+ "exports": {
9
+ ".": "./lego.js",
10
+ "./main.js": "./main.js",
11
+ "./lego.js": "./lego.js",
12
+ "./vite-plugin": "./vite-plugin.js",
13
+ "./parse-lego": "./parse-lego.js"
14
+ },
15
+ "keywords": [
16
+ "framework",
17
+ "sfc",
18
+ "components",
19
+ "lego",
20
+ "legokit"
21
+ ],
22
+ "author": "Tersoo Ortserga",
9
23
  "scripts": {
10
- "test": "vitest run"
24
+ "test": "vitest run",
25
+ "docs:dev": "vitepress dev docs",
26
+ "docs:build": "vitepress build docs",
27
+ "docs:preview": "vitepress preview docs"
11
28
  },
12
29
  "devDependencies": {
13
- "vitest": "^1.0.0",
14
- "jsdom": "^22.0.0"
30
+ "jsdom": "^22.0.0",
31
+ "vitepress": "^1.6.4",
32
+ "vitest": "^1.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "vite": "^4.0.0 || ^5.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "vite": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "fast-glob": "^3.3.2"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/rayattack/legodom.git"
15
48
  }
16
49
  }
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('Lego 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 Lego 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 Lego 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
+ }