@hypen-space/core 0.4.3 → 0.4.5

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +79 -22
  3. package/dist/app.js +111 -18
  4. package/dist/app.js.map +4 -4
  5. package/dist/components/builtin.js +114 -21
  6. package/dist/components/builtin.js.map +5 -5
  7. package/dist/discovery.d.ts +1 -1
  8. package/dist/discovery.js +112 -19
  9. package/dist/discovery.js.map +5 -5
  10. package/dist/engine.browser.d.ts +6 -0
  11. package/dist/engine.browser.js +192 -5
  12. package/dist/engine.browser.js.map +5 -4
  13. package/dist/engine.d.ts +6 -0
  14. package/dist/engine.js +192 -5
  15. package/dist/engine.js.map +5 -4
  16. package/dist/index.browser.d.ts +1 -1
  17. package/dist/index.browser.js +111 -18
  18. package/dist/index.browser.js.map +5 -5
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +117 -21
  21. package/dist/index.js.map +6 -6
  22. package/dist/managed-router.d.ts +3 -4
  23. package/dist/remote/client.js +34 -2
  24. package/dist/remote/client.js.map +3 -3
  25. package/dist/remote/index.js +472 -26
  26. package/dist/remote/index.js.map +8 -7
  27. package/dist/remote/server.d.ts +46 -15
  28. package/dist/remote/server.js +472 -26
  29. package/dist/remote/server.js.map +8 -7
  30. package/dist/result.d.ts +18 -0
  31. package/package.json +1 -1
  32. package/src/app.ts +162 -41
  33. package/src/components/builtin.ts +3 -4
  34. package/src/discovery.ts +2 -2
  35. package/src/engine.browser.ts +27 -4
  36. package/src/engine.ts +27 -4
  37. package/src/index.browser.ts +0 -2
  38. package/src/index.ts +3 -2
  39. package/src/managed-router.ts +5 -6
  40. package/src/remote/server.ts +116 -21
  41. package/src/result.ts +48 -0
  42. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  43. package/wasm-browser/package.json +1 -1
  44. package/wasm-node/hypen_engine_bg.wasm +0 -0
  45. package/wasm-node/package.json +1 -1
  46. package/dist/module-registry.d.ts +0 -42
  47. package/src/module-registry.ts +0 -76
@@ -361,7 +361,20 @@ function all(results) {
361
361
  }
362
362
  return Ok(values);
363
363
  }
364
- var HypenError, ActionError, ConnectionError, StateError;
364
+ function classifyEngineError(err) {
365
+ const message = err instanceof Error ? err.message : String(err);
366
+ if (message.startsWith("Parse error:")) {
367
+ return new ParseError(message);
368
+ }
369
+ if (message.startsWith("Invalid state:") || message.startsWith("Invalid state patch:")) {
370
+ return new StateError(message);
371
+ }
372
+ if (message.startsWith("Parent node not found:")) {
373
+ return new RenderError(message);
374
+ }
375
+ return new RenderError(message);
376
+ }
377
+ var HypenError, ActionError, ConnectionError, StateError, ParseError, RenderError;
365
378
  var init_result = __esm(() => {
366
379
  HypenError = class HypenError extends Error {
367
380
  code;
@@ -411,6 +424,25 @@ var init_result = __esm(() => {
411
424
  this.path = path;
412
425
  }
413
426
  };
427
+ ParseError = class ParseError extends HypenError {
428
+ source;
429
+ constructor(message, source, cause) {
430
+ super("PARSE_ERROR", message, {
431
+ context: { source },
432
+ cause: cause instanceof Error ? cause : undefined
433
+ });
434
+ this.name = "ParseError";
435
+ this.source = source;
436
+ }
437
+ };
438
+ RenderError = class RenderError extends HypenError {
439
+ constructor(message, cause) {
440
+ super("RENDER_ERROR", message, {
441
+ cause: cause instanceof Error ? cause : undefined
442
+ });
443
+ this.name = "RenderError";
444
+ }
445
+ };
414
446
  });
415
447
 
416
448
  // src/logger.ts
@@ -632,9 +664,11 @@ class HypenAppBuilder {
632
664
  expireHandler;
633
665
  errorHandler;
634
666
  template;
635
- constructor(initialState, options) {
667
+ _registry;
668
+ constructor(initialState, options, registry) {
636
669
  this.initialState = initialState;
637
670
  this.options = options || {};
671
+ this._registry = registry;
638
672
  }
639
673
  onCreated(fn) {
640
674
  this.createdHandler = fn;
@@ -670,7 +704,7 @@ class HypenAppBuilder {
670
704
  }
671
705
  build() {
672
706
  const stateKeys = this.initialState !== null && typeof this.initialState === "object" ? Object.keys(this.initialState) : [];
673
- return {
707
+ const definition = {
674
708
  name: this.options.name,
675
709
  actions: Array.from(this.actionHandlers.keys()),
676
710
  stateKeys,
@@ -688,12 +722,46 @@ class HypenAppBuilder {
688
722
  onError: this.errorHandler
689
723
  }
690
724
  };
725
+ if (this.options.name && this._registry) {
726
+ this._registry.set(this.options.name, definition);
727
+ }
728
+ return definition;
691
729
  }
692
730
  }
693
731
 
694
732
  class HypenApp {
733
+ _registry = new Map;
695
734
  defineState(initial, options) {
696
- return new HypenAppBuilder(initial, options);
735
+ return new HypenAppBuilder(initial, options, this._registry);
736
+ }
737
+ module(name) {
738
+ const registry = this._registry;
739
+ return {
740
+ defineState: (initial, options) => {
741
+ return new HypenAppBuilder(initial, { ...options, name }, registry);
742
+ }
743
+ };
744
+ }
745
+ get(name) {
746
+ return this._registry.get(name);
747
+ }
748
+ has(name) {
749
+ return this._registry.has(name);
750
+ }
751
+ get components() {
752
+ return this._registry;
753
+ }
754
+ getNames() {
755
+ return Array.from(this._registry.keys());
756
+ }
757
+ get size() {
758
+ return this._registry.size;
759
+ }
760
+ unregister(name) {
761
+ this._registry.delete(name);
762
+ }
763
+ clear() {
764
+ this._registry.clear();
697
765
  }
698
766
  }
699
767
 
@@ -702,13 +770,13 @@ class HypenModuleInstance {
702
770
  definition;
703
771
  state;
704
772
  isDestroyed = false;
705
- routerContext;
773
+ router;
706
774
  globalContext;
707
775
  stateChangeCallbacks = [];
708
- constructor(engine, definition, routerContext, globalContext) {
776
+ constructor(engine, definition, router, globalContext) {
709
777
  this.engine = engine;
710
778
  this.definition = definition;
711
- this.routerContext = routerContext;
779
+ this.router = router ?? null;
712
780
  this.globalContext = globalContext;
713
781
  const statePrefix = (definition.name || "").toLowerCase();
714
782
  this.state = createObservableState(definition.initialState, {
@@ -730,14 +798,10 @@ class HypenModuleInstance {
730
798
  payload: action.payload,
731
799
  sender: action.sender
732
800
  };
733
- const next = {
734
- router: this.routerContext?.root || null
735
- };
736
801
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
737
802
  const result = await this.executeAction(actionName, handler, {
738
803
  action: actionCtx,
739
804
  state: this.state,
740
- next,
741
805
  context
742
806
  });
743
807
  if (!result.ok) {
@@ -750,6 +814,21 @@ class HypenModuleInstance {
750
814
  }
751
815
  });
752
816
  }
817
+ this.engine.onAction("__hypen_bind", (action) => {
818
+ const payload = action.payload;
819
+ if (!payload?.path)
820
+ return;
821
+ const segments = payload.path.split(".");
822
+ let target = this.state;
823
+ for (let i = 0;i < segments.length - 1; i++) {
824
+ const seg = segments[i];
825
+ target = target?.[seg];
826
+ if (target == null)
827
+ return;
828
+ }
829
+ const lastSeg = segments[segments.length - 1];
830
+ target[lastSeg] = payload.value;
831
+ });
753
832
  this.callCreatedHandler();
754
833
  }
755
834
  createGlobalContextAPI() {
@@ -763,11 +842,9 @@ class HypenModuleInstance {
763
842
  getModuleIds: () => ctx.getModuleIds(),
764
843
  getGlobalState: () => ctx.getGlobalState(),
765
844
  emit: (event, payload) => ctx.emit(event, payload),
766
- on: (event, handler) => ctx.on(event, handler)
845
+ on: (event, handler) => ctx.on(event, handler),
846
+ router: this.router
767
847
  };
768
- if (ctx.__router) {
769
- api.__router = ctx.__router;
770
- }
771
848
  if (ctx.__hypenEngine) {
772
849
  api.__hypenEngine = ctx.__hypenEngine;
773
850
  }
@@ -818,7 +895,15 @@ class HypenModuleInstance {
818
895
  async callCreatedHandler() {
819
896
  if (this.definition.handlers.onCreated) {
820
897
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
821
- await this.definition.handlers.onCreated(this.state, context);
898
+ try {
899
+ await this.definition.handlers.onCreated(this.state, context);
900
+ } catch (e) {
901
+ const error = e instanceof HypenError ? e : new ActionError("onCreated", e);
902
+ const shouldRethrow = await this.handleError(error, { lifecycle: "created" });
903
+ if (shouldRethrow) {
904
+ throw error;
905
+ }
906
+ }
822
907
  }
823
908
  }
824
909
  onStateChange(callback) {
@@ -828,7 +913,15 @@ class HypenModuleInstance {
828
913
  if (this.isDestroyed)
829
914
  return;
830
915
  if (this.definition.handlers.onDestroyed) {
831
- await this.definition.handlers.onDestroyed(this.state);
916
+ try {
917
+ await this.definition.handlers.onDestroyed(this.state);
918
+ } catch (e) {
919
+ const error = e instanceof HypenError ? e : new ActionError("onDestroyed", e);
920
+ const shouldRethrow = await this.handleError(error, { lifecycle: "destroyed" });
921
+ if (shouldRethrow) {
922
+ throw error;
923
+ }
924
+ }
832
925
  }
833
926
  this.isDestroyed = true;
834
927
  }
@@ -1378,6 +1471,7 @@ class RemoteEngine {
1378
1471
 
1379
1472
  // src/engine.ts
1380
1473
  init_logger();
1474
+ init_result();
1381
1475
  import { WasmEngine } from "../wasm-node/hypen_engine.js";
1382
1476
  var log5 = frameworkLoggers.engine;
1383
1477
  function unwrapForWasm(value) {
@@ -1424,7 +1518,11 @@ class Engine {
1424
1518
  }
1425
1519
  renderSource(source) {
1426
1520
  const engine = this.ensureInitialized();
1427
- engine.renderSource(source);
1521
+ try {
1522
+ engine.renderSource(source);
1523
+ } catch (err) {
1524
+ throw classifyEngineError(err);
1525
+ }
1428
1526
  }
1429
1527
  renderLazyComponent(source) {
1430
1528
  const engine = this.ensureInitialized();
@@ -1432,14 +1530,22 @@ class Engine {
1432
1530
  }
1433
1531
  renderInto(source, parentNodeId, state) {
1434
1532
  const engine = this.ensureInitialized();
1435
- engine.renderInto(source, parentNodeId, unwrapForWasm(state));
1533
+ try {
1534
+ engine.renderInto(source, parentNodeId, unwrapForWasm(state));
1535
+ } catch (err) {
1536
+ throw classifyEngineError(err);
1537
+ }
1436
1538
  }
1437
1539
  notifyStateChange(paths, values) {
1438
1540
  const engine = this.ensureInitialized();
1439
1541
  if (paths.length === 0) {
1440
1542
  return;
1441
1543
  }
1442
- engine.updateStateSparse(paths, unwrapForWasm(values));
1544
+ try {
1545
+ engine.updateStateSparse(paths, unwrapForWasm(values));
1546
+ } catch (err) {
1547
+ throw classifyEngineError(err);
1548
+ }
1443
1549
  log5.debug("State changed (sparse):", paths);
1444
1550
  }
1445
1551
  notifyStateChangeFull(paths, currentState) {
@@ -1455,7 +1561,11 @@ class Engine {
1455
1561
  }
1456
1562
  dispatchAction(name, payload) {
1457
1563
  const engine = this.ensureInitialized();
1458
- engine.dispatchAction(name, payload ?? null);
1564
+ try {
1565
+ engine.dispatchAction(name, payload ?? null);
1566
+ } catch (err) {
1567
+ throw classifyEngineError(err);
1568
+ }
1459
1569
  }
1460
1570
  onAction(actionName, handler) {
1461
1571
  const engine = this.ensureInitialized();
@@ -1481,6 +1591,297 @@ class Engine {
1481
1591
  }
1482
1592
  }
1483
1593
 
1594
+ // src/discovery.ts
1595
+ init_logger();
1596
+ import { existsSync, readdirSync, readFileSync, watch } from "fs";
1597
+ import { join, basename, resolve, relative } from "path";
1598
+ async function discoverComponents(baseDir, options = {}) {
1599
+ const {
1600
+ patterns = ["single-file", "folder", "sibling", "index"],
1601
+ recursive = true,
1602
+ debug = false
1603
+ } = options;
1604
+ const log6 = debug ? (...args) => frameworkLoggers.discovery.debug(...args) : () => {};
1605
+ const resolvedDir = resolve(baseDir);
1606
+ const components = [];
1607
+ const seen = new Set;
1608
+ log6("Scanning directory:", resolvedDir);
1609
+ log6("Patterns:", patterns);
1610
+ const addTwoFileComponent = (name, hypenPath, modulePath) => {
1611
+ if (seen.has(name)) {
1612
+ log6(`Skipping duplicate: ${name}`);
1613
+ return;
1614
+ }
1615
+ seen.add(name);
1616
+ const templateRaw = readFileSync(hypenPath, "utf-8");
1617
+ const template = templateRaw.trim();
1618
+ components.push({
1619
+ name,
1620
+ hypenPath,
1621
+ modulePath,
1622
+ template,
1623
+ hasModule: modulePath !== null,
1624
+ isSingleFile: false
1625
+ });
1626
+ log6(`Found: ${name} (two-file, ${modulePath ? "with module" : "stateless"})`);
1627
+ };
1628
+ const addSingleFileComponent = (name, modulePath, template) => {
1629
+ if (seen.has(name)) {
1630
+ log6(`Skipping duplicate: ${name}`);
1631
+ return;
1632
+ }
1633
+ seen.add(name);
1634
+ components.push({
1635
+ name,
1636
+ hypenPath: null,
1637
+ modulePath,
1638
+ template,
1639
+ hasModule: true,
1640
+ isSingleFile: true
1641
+ });
1642
+ log6(`Found: ${name} (single-file with inline template)`);
1643
+ };
1644
+ const scanForFolderComponents = (dir) => {
1645
+ if (!existsSync(dir))
1646
+ return;
1647
+ const entries = readdirSync(dir, { withFileTypes: true });
1648
+ for (const entry of entries) {
1649
+ if (!entry.isDirectory())
1650
+ continue;
1651
+ const folderPath = join(dir, entry.name);
1652
+ const componentName = entry.name;
1653
+ if (patterns.includes("folder")) {
1654
+ const hypenPath = join(folderPath, "component.hypen");
1655
+ if (existsSync(hypenPath)) {
1656
+ const modulePath = join(folderPath, "component.ts");
1657
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
1658
+ continue;
1659
+ }
1660
+ }
1661
+ if (patterns.includes("index")) {
1662
+ const hypenPath = join(folderPath, "index.hypen");
1663
+ if (existsSync(hypenPath)) {
1664
+ const modulePath = join(folderPath, "index.ts");
1665
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
1666
+ continue;
1667
+ }
1668
+ }
1669
+ if (recursive) {
1670
+ scanForFolderComponents(folderPath);
1671
+ }
1672
+ }
1673
+ };
1674
+ const scanForSiblingComponents = (dir) => {
1675
+ if (!existsSync(dir))
1676
+ return;
1677
+ const entries = readdirSync(dir, { withFileTypes: true });
1678
+ for (const entry of entries) {
1679
+ if (entry.isDirectory()) {
1680
+ if (recursive) {
1681
+ scanForSiblingComponents(join(dir, entry.name));
1682
+ }
1683
+ continue;
1684
+ }
1685
+ if (!entry.name.endsWith(".hypen"))
1686
+ continue;
1687
+ const hypenPath = join(dir, entry.name);
1688
+ const baseName = basename(entry.name, ".hypen");
1689
+ if (baseName === "component" || baseName === "index")
1690
+ continue;
1691
+ const modulePath = join(dir, `${baseName}.ts`);
1692
+ addTwoFileComponent(baseName, hypenPath, existsSync(modulePath) ? modulePath : null);
1693
+ }
1694
+ };
1695
+ const scanForSingleFileComponents = async (dir) => {
1696
+ if (!existsSync(dir))
1697
+ return;
1698
+ const entries = readdirSync(dir, { withFileTypes: true });
1699
+ for (const entry of entries) {
1700
+ if (entry.isDirectory()) {
1701
+ if (recursive) {
1702
+ await scanForSingleFileComponents(join(dir, entry.name));
1703
+ }
1704
+ continue;
1705
+ }
1706
+ if (!entry.name.endsWith(".ts"))
1707
+ continue;
1708
+ if (entry.name.startsWith(".") || entry.name.includes(".test.") || entry.name.includes(".spec."))
1709
+ continue;
1710
+ const baseName = basename(entry.name, ".ts");
1711
+ if (baseName === "component" || baseName === "index")
1712
+ continue;
1713
+ const hypenPath = join(dir, `${baseName}.hypen`);
1714
+ if (existsSync(hypenPath))
1715
+ continue;
1716
+ const modulePath = join(dir, entry.name);
1717
+ const content = readFileSync(modulePath, "utf-8");
1718
+ if (content.includes(".ui(") || content.includes(".ui(hypen")) {
1719
+ try {
1720
+ const moduleExport = await import(modulePath);
1721
+ const module = moduleExport.default;
1722
+ if (module && typeof module === "object" && module.template) {
1723
+ addSingleFileComponent(baseName, modulePath, module.template);
1724
+ }
1725
+ } catch (e) {
1726
+ log6(`Failed to import potential single-file component: ${entry.name}`, e);
1727
+ }
1728
+ }
1729
+ }
1730
+ };
1731
+ if (patterns.includes("folder") || patterns.includes("index")) {
1732
+ scanForFolderComponents(resolvedDir);
1733
+ }
1734
+ if (patterns.includes("sibling")) {
1735
+ scanForSiblingComponents(resolvedDir);
1736
+ }
1737
+ if (patterns.includes("single-file")) {
1738
+ await scanForSingleFileComponents(resolvedDir);
1739
+ }
1740
+ log6(`Discovered ${components.length} components`);
1741
+ return components;
1742
+ }
1743
+ async function loadDiscoveredComponents(components) {
1744
+ const { app: app2 } = await Promise.resolve().then(() => (init_app(), exports_app));
1745
+ const loaded = new Map;
1746
+ for (const component of components) {
1747
+ let module;
1748
+ let template = component.template;
1749
+ if (component.modulePath) {
1750
+ const moduleExport = await import(component.modulePath);
1751
+ module = moduleExport.default;
1752
+ if (component.isSingleFile && module.template) {
1753
+ template = module.template;
1754
+ }
1755
+ } else {
1756
+ module = app2.defineState({}).build();
1757
+ }
1758
+ loaded.set(component.name, {
1759
+ name: component.name,
1760
+ module,
1761
+ template
1762
+ });
1763
+ }
1764
+ return loaded;
1765
+ }
1766
+ function watchComponents(baseDir, options = {}) {
1767
+ const resolvedDir = resolve(baseDir);
1768
+ const {
1769
+ onChange,
1770
+ onAdd,
1771
+ onRemove,
1772
+ onUpdate,
1773
+ debug = false,
1774
+ ...discoveryOptions
1775
+ } = options;
1776
+ const log6 = debug ? (...args) => frameworkLoggers.discovery.debug("[watch]", ...args) : () => {};
1777
+ let currentComponents = new Map;
1778
+ let debounceTimer = null;
1779
+ const initialScan = async () => {
1780
+ const components = await discoverComponents(resolvedDir, discoveryOptions);
1781
+ currentComponents = new Map(components.map((c) => [c.name, c]));
1782
+ onChange?.(components);
1783
+ };
1784
+ const rescan = async () => {
1785
+ if (debounceTimer) {
1786
+ clearTimeout(debounceTimer);
1787
+ }
1788
+ debounceTimer = setTimeout(async () => {
1789
+ log6("Rescanning...");
1790
+ const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
1791
+ const newMap = new Map(newComponents.map((c) => [c.name, c]));
1792
+ for (const [name, component] of newMap) {
1793
+ const existing = currentComponents.get(name);
1794
+ if (!existing) {
1795
+ log6("Added:", name);
1796
+ onAdd?.(component);
1797
+ } else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
1798
+ log6("Updated:", name);
1799
+ onUpdate?.(component);
1800
+ }
1801
+ }
1802
+ for (const name of currentComponents.keys()) {
1803
+ if (!newMap.has(name)) {
1804
+ log6("Removed:", name);
1805
+ onRemove?.(name);
1806
+ }
1807
+ }
1808
+ currentComponents = newMap;
1809
+ onChange?.(newComponents);
1810
+ }, 100);
1811
+ };
1812
+ const watcher = watch(resolvedDir, { recursive: true }, (event, filename) => {
1813
+ if (!filename)
1814
+ return;
1815
+ if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
1816
+ log6("File changed:", filename);
1817
+ rescan();
1818
+ }
1819
+ });
1820
+ initialScan();
1821
+ return {
1822
+ stop: () => {
1823
+ watcher.close();
1824
+ if (debounceTimer) {
1825
+ clearTimeout(debounceTimer);
1826
+ }
1827
+ }
1828
+ };
1829
+ }
1830
+ async function generateComponentsCode(baseDir, options = {}) {
1831
+ const components = await discoverComponents(baseDir, options);
1832
+ const resolvedDir = resolve(baseDir);
1833
+ const outputBase = options.outputDir ? resolve(options.outputDir) : resolvedDir;
1834
+ let code = `/**
1835
+ * Auto-generated component imports
1836
+ * Generated by @hypen-space/core discovery
1837
+ */
1838
+
1839
+ `;
1840
+ for (const component of components) {
1841
+ let importPath = null;
1842
+ if (component.modulePath) {
1843
+ const rel = relative(outputBase, component.modulePath).replace(/\.ts$/, ".js");
1844
+ importPath = rel.startsWith(".") ? rel : "./" + rel;
1845
+ }
1846
+ if (importPath) {
1847
+ code += `import ${component.name}Module from "${importPath}";
1848
+ `;
1849
+ }
1850
+ }
1851
+ code += `
1852
+ import { app } from "@hypen-space/core";
1853
+
1854
+ `;
1855
+ for (const component of components) {
1856
+ if (component.isSingleFile) {
1857
+ code += `export const ${component.name} = {
1858
+ module: ${component.name}Module,
1859
+ template: ${component.name}Module.template,
1860
+ };
1861
+
1862
+ `;
1863
+ } else if (component.hasModule) {
1864
+ const templateJson = JSON.stringify(component.template);
1865
+ code += `export const ${component.name} = {
1866
+ module: ${component.name}Module,
1867
+ template: ${templateJson},
1868
+ };
1869
+
1870
+ `;
1871
+ } else {
1872
+ const templateJson = JSON.stringify(component.template);
1873
+ code += `const ${component.name}Module = app.defineState({}).build();
1874
+ export const ${component.name} = {
1875
+ module: ${component.name}Module,
1876
+ template: ${templateJson},
1877
+ };
1878
+
1879
+ `;
1880
+ }
1881
+ }
1882
+ return code;
1883
+ }
1884
+
1484
1885
  // src/remote/server.ts
1485
1886
  init_app();
1486
1887
 
@@ -1616,6 +2017,8 @@ class RemoteServer {
1616
2017
  _module = null;
1617
2018
  _moduleName = "App";
1618
2019
  _ui = "";
2020
+ _sourceDir = null;
2021
+ _discoveredComponents = new Map;
1619
2022
  _config = {};
1620
2023
  _sessionConfig = {};
1621
2024
  _onConnectionCallbacks = [];
@@ -1633,6 +2036,10 @@ class RemoteServer {
1633
2036
  this._ui = dsl;
1634
2037
  return this;
1635
2038
  }
2039
+ source(dir) {
2040
+ this._sourceDir = dir;
2041
+ return this;
2042
+ }
1636
2043
  config(config2) {
1637
2044
  this._config = { ...this._config, ...config2 };
1638
2045
  return this;
@@ -1649,12 +2056,15 @@ class RemoteServer {
1649
2056
  this._onDisconnectionCallbacks.push(callback);
1650
2057
  return this;
1651
2058
  }
1652
- listen(port) {
2059
+ async listen(port) {
1653
2060
  if (!this._module) {
1654
2061
  throw new Error("Module not set. Call .module() before .listen()");
1655
2062
  }
2063
+ if (this._sourceDir) {
2064
+ await this.discoverFromSource();
2065
+ }
1656
2066
  if (!this._ui) {
1657
- throw new Error("UI not set. Call .ui() before .listen()");
2067
+ throw new Error("UI not set. Call .ui() or .source() before .listen()");
1658
2068
  }
1659
2069
  this.sessionManager = new SessionManager(this._sessionConfig);
1660
2070
  const finalPort = port ?? this._config.port ?? 3000;
@@ -1708,12 +2118,42 @@ class RemoteServer {
1708
2118
  const port = this._config.port ?? 3000;
1709
2119
  return `ws://${hostname}:${port}`;
1710
2120
  }
2121
+ async discoverFromSource() {
2122
+ const dir = this._sourceDir;
2123
+ log6.info(`Discovering components from ${dir}...`);
2124
+ const discovered = await discoverComponents(dir);
2125
+ const loaded = await loadDiscoveredComponents(discovered);
2126
+ for (const [name, { module, template }] of loaded) {
2127
+ this._discoveredComponents.set(name, { template, module });
2128
+ }
2129
+ log6.info(`Discovered ${this._discoveredComponents.size} components: ${Array.from(this._discoveredComponents.keys()).join(", ")}`);
2130
+ if (!this._ui) {
2131
+ const entry = this._discoveredComponents.get(this._moduleName);
2132
+ if (entry) {
2133
+ this._ui = entry.template;
2134
+ }
2135
+ }
2136
+ }
2137
+ setupComponentResolver(engine) {
2138
+ if (this._discoveredComponents.size === 0)
2139
+ return;
2140
+ engine.setComponentResolver((componentName, _contextPath) => {
2141
+ const comp = this._discoveredComponents.get(componentName);
2142
+ if (!comp)
2143
+ return null;
2144
+ return {
2145
+ source: comp.template,
2146
+ path: componentName
2147
+ };
2148
+ });
2149
+ }
1711
2150
  async handleOpen(ws) {
1712
2151
  try {
1713
2152
  const clientId = `client_${this.nextClientId++}`;
1714
2153
  const connectedAt = new Date;
1715
2154
  const engine = new Engine;
1716
2155
  await engine.init();
2156
+ this.setupComponentResolver(engine);
1717
2157
  const moduleInstance = new HypenModuleInstance(engine, this._module);
1718
2158
  const clientData = {
1719
2159
  id: clientId,
@@ -1961,8 +2401,14 @@ class RemoteServer {
1961
2401
  });
1962
2402
  }
1963
2403
  }
1964
- function serve(options) {
1965
- const server = new RemoteServer().module(options.moduleName ?? "App", options.module).ui(options.ui);
2404
+ async function serve(options) {
2405
+ const server = new RemoteServer().module(options.moduleName ?? "App", options.module);
2406
+ if (options.source) {
2407
+ server.source(options.source);
2408
+ }
2409
+ if (options.ui) {
2410
+ server.ui(options.ui);
2411
+ }
1966
2412
  if (options.port || options.hostname) {
1967
2413
  server.config({
1968
2414
  port: options.port,
@@ -1987,4 +2433,4 @@ export {
1987
2433
  RemoteEngine
1988
2434
  };
1989
2435
 
1990
- //# debugId=BAD14814BBB641A464756E2164756E21
2436
+ //# debugId=B02EBC5F8B52753464756E2164756E21