@hypen-space/core 0.4.2 → 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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +379 -0
  3. package/dist/app.js +111 -18
  4. package/dist/app.js.map +4 -4
  5. package/dist/components/builtin.d.ts +50 -0
  6. package/dist/components/builtin.js +114 -21
  7. package/dist/components/builtin.js.map +5 -5
  8. package/dist/context.d.ts +90 -0
  9. package/dist/discovery.d.ts +90 -0
  10. package/dist/discovery.js +121 -23
  11. package/dist/discovery.js.map +5 -5
  12. package/dist/disposable.d.ts +122 -0
  13. package/dist/engine.browser.d.ts +101 -0
  14. package/dist/engine.browser.js +192 -5
  15. package/dist/engine.browser.js.map +5 -4
  16. package/dist/engine.d.ts +85 -0
  17. package/dist/engine.js +192 -5
  18. package/dist/engine.js.map +5 -4
  19. package/dist/events.d.ts +78 -0
  20. package/dist/hypen.d.ts +127 -0
  21. package/dist/index.browser.d.ts +25 -0
  22. package/dist/index.browser.js +116 -19
  23. package/dist/index.browser.js.map +6 -6
  24. package/dist/index.d.ts +76 -0
  25. package/dist/index.js +373 -1235
  26. package/dist/index.js.map +10 -15
  27. package/dist/loader.d.ts +51 -0
  28. package/dist/logger.d.ts +141 -0
  29. package/dist/managed-router.d.ts +59 -0
  30. package/dist/plugin.d.ts +39 -0
  31. package/dist/remote/client.d.ts +154 -0
  32. package/dist/remote/client.js +34 -2
  33. package/dist/remote/client.js.map +3 -3
  34. package/dist/remote/index.d.ts +13 -0
  35. package/dist/remote/index.js +472 -26
  36. package/dist/remote/index.js.map +8 -7
  37. package/dist/remote/server.d.ts +173 -0
  38. package/dist/remote/server.js +472 -26
  39. package/dist/remote/server.js.map +8 -7
  40. package/dist/remote/session.d.ts +115 -0
  41. package/dist/remote/types.d.ts +98 -0
  42. package/dist/renderer.d.ts +54 -0
  43. package/dist/renderer.js +6 -2
  44. package/dist/renderer.js.map +3 -3
  45. package/dist/resolver.d.ts +95 -0
  46. package/dist/result.d.ts +134 -0
  47. package/dist/retry.d.ts +150 -0
  48. package/dist/router.d.ts +94 -0
  49. package/dist/state.d.ts +42 -0
  50. package/dist/types.d.ts +30 -0
  51. package/package.json +2 -1
  52. package/src/app.ts +162 -41
  53. package/src/components/builtin.ts +3 -4
  54. package/src/discovery.ts +2 -2
  55. package/src/engine.browser.ts +27 -4
  56. package/src/engine.ts +27 -4
  57. package/src/index.browser.ts +0 -2
  58. package/src/index.ts +3 -2
  59. package/src/managed-router.ts +5 -6
  60. package/src/remote/server.ts +116 -21
  61. package/src/result.ts +48 -0
  62. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  63. package/wasm-browser/package.json +1 -1
  64. package/wasm-node/hypen_engine_bg.wasm +0 -0
  65. package/wasm-node/package.json +5 -3
  66. package/src/module-registry.ts +0 -76
package/dist/index.js CHANGED
@@ -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
  }
@@ -895,7 +988,11 @@ class BaseRenderer {
895
988
 
896
989
  class ConsoleRenderer {
897
990
  applyPatches(patches) {
898
- log3.debug("Patches:", patches);
991
+ console.group("Hypen Patches");
992
+ for (const patch of patches) {
993
+ console.log(patch);
994
+ }
995
+ console.groupEnd();
899
996
  }
900
997
  }
901
998
 
@@ -1796,135 +1893,280 @@ class RemoteEngine {
1796
1893
  }
1797
1894
  }
1798
1895
 
1799
- // src/engine.ts
1800
- init_logger();
1801
- import { WasmEngine } from "../wasm-node/hypen_engine.js";
1802
- var log9 = frameworkLoggers.engine;
1803
- function unwrapForWasm(value) {
1804
- if (value === null || typeof value !== "object") {
1805
- return value;
1806
- }
1807
- if (typeof value.__getSnapshot === "function") {
1808
- return value.__getSnapshot();
1896
+ // src/resolver.ts
1897
+ class ComponentResolver {
1898
+ cache = new Map;
1899
+ options;
1900
+ moduleRegistry;
1901
+ constructor(options = {}) {
1902
+ this.options = {
1903
+ baseDir: options.baseDir || process.cwd(),
1904
+ cache: options.cache ?? true,
1905
+ customFetch: options.customFetch || this.defaultFetch.bind(this),
1906
+ moduleRegistry: options.moduleRegistry
1907
+ };
1908
+ this.moduleRegistry = options.moduleRegistry;
1809
1909
  }
1810
- try {
1811
- return structuredClone(value);
1812
- } catch {
1813
- return JSON.parse(JSON.stringify(value));
1910
+ async resolve(importStmt) {
1911
+ if (this.moduleRegistry) {
1912
+ const names = importStmt.clause.type === "named" ? importStmt.clause.names : [importStmt.clause.name];
1913
+ const allFound = names.every((name) => this.moduleRegistry.has(name));
1914
+ if (allFound) {
1915
+ const result = {};
1916
+ for (const name of names) {
1917
+ const def = this.moduleRegistry.get(name);
1918
+ result[name] = {
1919
+ module: def,
1920
+ template: def?.template || ""
1921
+ };
1922
+ }
1923
+ return result;
1924
+ }
1925
+ }
1926
+ const sourcePath = this.getSourcePath(importStmt.source);
1927
+ if (this.options.cache && this.cache.has(sourcePath)) {
1928
+ const cached = this.cache.get(sourcePath);
1929
+ return this.extractComponents(importStmt.clause, cached);
1930
+ }
1931
+ let component;
1932
+ if (importStmt.source.type === "local") {
1933
+ component = await this.resolveLocal(importStmt.source.path);
1934
+ } else {
1935
+ component = await this.resolveUrl(importStmt.source.url);
1936
+ }
1937
+ if (this.options.cache) {
1938
+ this.cache.set(sourcePath, component);
1939
+ }
1940
+ return this.extractComponents(importStmt.clause, component);
1814
1941
  }
1815
- }
1816
-
1817
- class Engine {
1818
- wasmEngine = null;
1819
- initialized = false;
1820
- async init() {
1821
- if (this.initialized)
1822
- return;
1823
- this.wasmEngine = new WasmEngine;
1824
- this.initialized = true;
1942
+ async resolveLocal(path) {
1943
+ const { resolve, join } = await import("path");
1944
+ const { readFile } = await import("fs/promises");
1945
+ const basePath = resolve(this.options.baseDir, path);
1946
+ const hypenPath = basePath.endsWith(".hypen") ? basePath : `${basePath}.hypen`;
1947
+ const template = await readFile(hypenPath, "utf-8");
1948
+ const modulePath = hypenPath.replace(/\.hypen$/, ".ts");
1949
+ let module = {};
1950
+ try {
1951
+ module = await import(modulePath);
1952
+ } catch {}
1953
+ return { module, template };
1825
1954
  }
1826
- ensureInitialized() {
1827
- if (!this.wasmEngine) {
1828
- throw new Error("Engine not initialized. Call init() first.");
1955
+ async resolveUrl(url) {
1956
+ try {
1957
+ const response = await this.options.customFetch(url);
1958
+ const data = JSON.parse(response);
1959
+ if (!data.module || !data.template) {
1960
+ throw new Error(`Invalid component format from ${url}. Expected { module, template }`);
1961
+ }
1962
+ return data;
1963
+ } catch (error) {
1964
+ throw new Error(`Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`);
1829
1965
  }
1830
- return this.wasmEngine;
1831
1966
  }
1832
- setRenderCallback(callback) {
1833
- const engine = this.ensureInitialized();
1834
- engine.setRenderCallback((patches) => {
1835
- callback(patches);
1836
- });
1967
+ async defaultFetch(url) {
1968
+ const response = await fetch(url);
1969
+ if (!response.ok) {
1970
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1971
+ }
1972
+ return response.text();
1837
1973
  }
1838
- setComponentResolver(resolver) {
1839
- const engine = this.ensureInitialized();
1840
- engine.setComponentResolver((componentName, contextPath) => {
1841
- const result = resolver(componentName, contextPath);
1974
+ extractComponents(clause, component) {
1975
+ if (clause.type === "default") {
1976
+ return {
1977
+ [clause.name]: component
1978
+ };
1979
+ } else {
1980
+ const result = {};
1981
+ for (const name of clause.names) {
1982
+ result[name] = component;
1983
+ }
1842
1984
  return result;
1843
- });
1844
- }
1845
- renderSource(source) {
1846
- const engine = this.ensureInitialized();
1847
- engine.renderSource(source);
1985
+ }
1848
1986
  }
1849
- renderLazyComponent(source) {
1850
- const engine = this.ensureInitialized();
1851
- engine.renderLazyComponent(source);
1987
+ getSourcePath(source) {
1988
+ return source.type === "local" ? source.path : source.url;
1852
1989
  }
1853
- renderInto(source, parentNodeId, state) {
1854
- const engine = this.ensureInitialized();
1855
- engine.renderInto(source, parentNodeId, unwrapForWasm(state));
1990
+ clearCache() {
1991
+ this.cache.clear();
1856
1992
  }
1857
- notifyStateChange(paths, values) {
1858
- const engine = this.ensureInitialized();
1859
- if (paths.length === 0) {
1860
- return;
1993
+ static parseImports(text) {
1994
+ const imports = [];
1995
+ const importRegex = /import\s+(?:(\{[^}]*\})|(\w+))\s+from\s+["']([^"']+)["']/g;
1996
+ let match2;
1997
+ while ((match2 = importRegex.exec(text)) !== null) {
1998
+ const [, namedImports, defaultImport, source] = match2;
1999
+ if (!source)
2000
+ continue;
2001
+ let clause;
2002
+ if (namedImports) {
2003
+ const names = namedImports.slice(1, -1).split(",").map((n) => n.trim()).filter((n) => n.length > 0);
2004
+ clause = { type: "named", names };
2005
+ } else if (defaultImport) {
2006
+ clause = { type: "default", name: defaultImport };
2007
+ } else {
2008
+ continue;
2009
+ }
2010
+ const sourceObj = source.startsWith("http://") || source.startsWith("https://") ? { type: "url", url: source } : { type: "local", path: source };
2011
+ imports.push({ clause, source: sourceObj });
1861
2012
  }
1862
- engine.updateStateSparse(paths, unwrapForWasm(values));
1863
- log9.debug("State changed (sparse):", paths);
2013
+ return imports;
1864
2014
  }
1865
- notifyStateChangeFull(paths, currentState) {
1866
- const engine = this.ensureInitialized();
1867
- engine.updateState(unwrapForWasm(currentState));
1868
- if (paths.length > 0) {
1869
- log9.debug("State changed (full):", paths);
1870
- }
2015
+ }
2016
+
2017
+ // src/components/builtin.ts
2018
+ init_app();
2019
+ init_logger();
2020
+ var log9 = frameworkLoggers.router;
2021
+ var Router = app.defineState({
2022
+ currentPath: "/",
2023
+ matchedRoute: null,
2024
+ routeParams: {}
2025
+ }, { name: "__Router" }).onCreated((state, context) => {
2026
+ if (!context) {
2027
+ log9.error("Requires global context");
2028
+ return;
1871
2029
  }
1872
- updateState(statePatch) {
1873
- const engine = this.ensureInitialized();
1874
- engine.updateState(unwrapForWasm(statePatch));
2030
+ const router = context.router;
2031
+ if (!router) {
2032
+ log9.error("Router not found in context");
2033
+ return;
1875
2034
  }
1876
- dispatchAction(name, payload) {
1877
- const engine = this.ensureInitialized();
1878
- engine.dispatchAction(name, payload ?? null);
2035
+ const hypenEngine = context.__hypenEngine;
2036
+ const updateRouteVisibility = async (currentPath) => {
2037
+ const routeElements = document.querySelectorAll('[data-hypen-type="route"]');
2038
+ let matchFound = false;
2039
+ for (let index = 0;index < routeElements.length; index++) {
2040
+ const routeEl = routeElements[index];
2041
+ const htmlEl = routeEl;
2042
+ const routePath = htmlEl.dataset.routePath || "/";
2043
+ const isMatch = routePath === currentPath;
2044
+ htmlEl.style.display = isMatch ? "flex" : "none";
2045
+ if (isMatch) {
2046
+ matchFound = true;
2047
+ const componentName = htmlEl.dataset.routeComponent;
2048
+ const isLazy = htmlEl.dataset.routeLazy === "true";
2049
+ const hasContent = htmlEl.children.length > 0;
2050
+ if (componentName && !hasContent && hypenEngine) {
2051
+ try {
2052
+ await hypenEngine.renderLazyRoute(routePath, componentName, htmlEl);
2053
+ } catch (err) {
2054
+ log9.error(`Failed to render route ${routePath}:`, err);
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+ if (!matchFound) {
2060
+ log9.warn(`No route matched path: ${currentPath}. Available routes:`, Array.from(routeElements).map((el) => el.dataset.routePath));
2061
+ }
2062
+ };
2063
+ setTimeout(() => {
2064
+ updateRouteVisibility(state.currentPath);
2065
+ }, 100);
2066
+ router.onNavigate((routeState) => {
2067
+ state.currentPath = routeState.currentPath;
2068
+ state.routeParams = routeState.params;
2069
+ updateRouteVisibility(routeState.currentPath);
2070
+ });
2071
+ }).build();
2072
+ var Route = app.defineState({}, { name: "__Route" }).build();
2073
+ var Link = app.defineState({
2074
+ to: "/",
2075
+ isActive: false
2076
+ }, { name: "__Link" }).onAction("navigate", ({ state, context }) => {
2077
+ const router = context?.router;
2078
+ if (!router) {
2079
+ log9.error("Link requires router context");
2080
+ return;
1879
2081
  }
1880
- onAction(actionName, handler) {
1881
- const engine = this.ensureInitialized();
1882
- engine.onAction(actionName, (action) => {
1883
- Promise.resolve(handler(action)).catch((err) => log9.error("Action handler error:", err));
2082
+ const targetPath = state.to;
2083
+ router.push(targetPath);
2084
+ }).onCreated((state, context) => {
2085
+ if (!context)
2086
+ return;
2087
+ const router = context.router;
2088
+ if (router) {
2089
+ router.onNavigate((routeState) => {
2090
+ state.isActive = routeState.currentPath === state.to;
1884
2091
  });
1885
2092
  }
1886
- setModule(name, actions, stateKeys, initialState) {
1887
- const engine = this.ensureInitialized();
1888
- engine.setModule(name, actions, stateKeys, initialState);
1889
- }
1890
- getRevision() {
1891
- const engine = this.ensureInitialized();
1892
- return Number(engine.getRevision());
1893
- }
1894
- clearTree() {
1895
- const engine = this.ensureInitialized();
1896
- engine.clearTree();
1897
- }
1898
- debugParseComponent(source) {
1899
- const engine = this.ensureInitialized();
1900
- return engine.debugParseComponent(source);
1901
- }
1902
- }
2093
+ }).build();
1903
2094
 
1904
- // src/remote/server.ts
2095
+ // src/index.ts
1905
2096
  init_app();
1906
2097
 
1907
- // src/remote/session.ts
1908
- class SessionManager {
1909
- activeSessions = new Map;
1910
- pendingSessions = new Map;
1911
- sessionConnections = new Map;
1912
- config;
1913
- constructor(config2 = {}) {
1914
- this.config = {
1915
- ttl: config2.ttl ?? 3600,
1916
- concurrent: config2.concurrent ?? "kick-old",
1917
- generateId: config2.generateId ?? (() => crypto.randomUUID())
1918
- };
1919
- }
1920
- getTtl() {
1921
- return this.config.ttl;
1922
- }
1923
- getConcurrentPolicy() {
1924
- return this.config.concurrent;
1925
- }
1926
- createSession(props) {
1927
- const now = new Date;
2098
+ // src/hypen.ts
2099
+ function createBindingProxy(root) {
2100
+ const handler = {
2101
+ get(_, prop) {
2102
+ if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
2103
+ return () => `\${${root}}`;
2104
+ }
2105
+ if (typeof prop === "symbol") {
2106
+ return;
2107
+ }
2108
+ if (prop === "toJSON") {
2109
+ return () => `\${${root}}`;
2110
+ }
2111
+ return createBindingProxy(`${root}.${prop}`);
2112
+ },
2113
+ has() {
2114
+ return true;
2115
+ },
2116
+ ownKeys() {
2117
+ return [];
2118
+ },
2119
+ getOwnPropertyDescriptor() {
2120
+ return {
2121
+ configurable: true,
2122
+ enumerable: true
2123
+ };
2124
+ }
2125
+ };
2126
+ return new Proxy({}, handler);
2127
+ }
2128
+ var state = createBindingProxy("state");
2129
+ var item = createBindingProxy("item");
2130
+ var index = {
2131
+ [Symbol.toPrimitive]: () => "${index}",
2132
+ toString: () => "${index}",
2133
+ valueOf: () => "${index}",
2134
+ toJSON: () => "${index}"
2135
+ };
2136
+ function hypen(strings, ...expressions) {
2137
+ let result = strings[0];
2138
+ for (let i = 0;i < expressions.length; i++) {
2139
+ const expr = expressions[i];
2140
+ result += String(expr);
2141
+ result += strings[i + 1];
2142
+ }
2143
+ return result.trim();
2144
+ }
2145
+
2146
+ // src/index.ts
2147
+ init_state();
2148
+
2149
+ // src/remote/session.ts
2150
+ class SessionManager {
2151
+ activeSessions = new Map;
2152
+ pendingSessions = new Map;
2153
+ sessionConnections = new Map;
2154
+ config;
2155
+ constructor(config2 = {}) {
2156
+ this.config = {
2157
+ ttl: config2.ttl ?? 3600,
2158
+ concurrent: config2.concurrent ?? "kick-old",
2159
+ generateId: config2.generateId ?? (() => crypto.randomUUID())
2160
+ };
2161
+ }
2162
+ getTtl() {
2163
+ return this.config.ttl;
2164
+ }
2165
+ getConcurrentPolicy() {
2166
+ return this.config.concurrent;
2167
+ }
2168
+ createSession(props) {
2169
+ const now = new Date;
1928
2170
  const session = {
1929
2171
  id: this.config.generateId(),
1930
2172
  ttl: this.config.ttl,
@@ -2028,1108 +2270,11 @@ class SessionManager {
2028
2270
  }
2029
2271
  }
2030
2272
 
2031
- // src/remote/server.ts
2032
- init_logger();
2033
- var log10 = frameworkLoggers.remote;
2034
-
2035
- class RemoteServer {
2036
- _module = null;
2037
- _moduleName = "App";
2038
- _ui = "";
2039
- _config = {};
2040
- _sessionConfig = {};
2041
- _onConnectionCallbacks = [];
2042
- _onDisconnectionCallbacks = [];
2043
- clients = new Map;
2044
- nextClientId = 1;
2045
- server = null;
2046
- sessionManager = null;
2047
- module(name, module) {
2048
- this._moduleName = name;
2049
- this._module = module;
2050
- return this;
2051
- }
2052
- ui(dsl) {
2053
- this._ui = dsl;
2054
- return this;
2055
- }
2056
- config(config2) {
2057
- this._config = { ...this._config, ...config2 };
2058
- return this;
2059
- }
2060
- session(config2) {
2061
- this._sessionConfig = config2;
2062
- return this;
2063
- }
2064
- onConnection(callback) {
2065
- this._onConnectionCallbacks.push(callback);
2066
- return this;
2067
- }
2068
- onDisconnection(callback) {
2069
- this._onDisconnectionCallbacks.push(callback);
2070
- return this;
2071
- }
2072
- listen(port) {
2073
- if (!this._module) {
2074
- throw new Error("Module not set. Call .module() before .listen()");
2075
- }
2076
- if (!this._ui) {
2077
- throw new Error("UI not set. Call .ui() before .listen()");
2078
- }
2079
- this.sessionManager = new SessionManager(this._sessionConfig);
2080
- const finalPort = port ?? this._config.port ?? 3000;
2081
- const hostname = this._config.hostname ?? "0.0.0.0";
2082
- this.server = Bun.serve({
2083
- port: finalPort,
2084
- hostname,
2085
- websocket: {
2086
- open: (ws) => this.handleOpen(ws),
2087
- message: (ws, message) => this.handleMessage(ws, message),
2088
- close: (ws) => this.handleClose(ws)
2089
- },
2090
- fetch: (req, server) => {
2091
- const url = new URL(req.url);
2092
- if (server.upgrade(req, { data: undefined })) {
2093
- return;
2094
- }
2095
- if (url.pathname === "/health") {
2096
- return new Response("OK", { status: 200 });
2097
- }
2098
- if (url.pathname === "/stats") {
2099
- const stats = this.sessionManager?.getStats() ?? {
2100
- activeSessions: 0,
2101
- pendingSessions: 0,
2102
- totalConnections: 0
2103
- };
2104
- return new Response(JSON.stringify(stats), {
2105
- headers: { "Content-Type": "application/json" }
2106
- });
2107
- }
2108
- return new Response("Hypen Remote Server", { status: 200 });
2109
- }
2110
- });
2111
- log10.info(`Hypen app streaming on ws://${hostname}:${finalPort}`);
2112
- return this;
2113
- }
2114
- stop() {
2115
- if (this.server) {
2116
- this.server.stop();
2117
- this.server = null;
2118
- }
2119
- if (this.sessionManager) {
2120
- this.sessionManager.destroy();
2121
- this.sessionManager = null;
2122
- }
2123
- }
2124
- get url() {
2125
- if (!this.server)
2126
- return null;
2127
- const hostname = this._config.hostname ?? "localhost";
2128
- const port = this._config.port ?? 3000;
2129
- return `ws://${hostname}:${port}`;
2130
- }
2131
- async handleOpen(ws) {
2132
- try {
2133
- const clientId = `client_${this.nextClientId++}`;
2134
- const connectedAt = new Date;
2135
- const engine = new Engine;
2136
- await engine.init();
2137
- const moduleInstance = new HypenModuleInstance(engine, this._module);
2138
- const clientData = {
2139
- id: clientId,
2140
- engine,
2141
- moduleInstance,
2142
- revision: 0,
2143
- connectedAt,
2144
- sessionId: "",
2145
- helloReceived: false
2146
- };
2147
- this.clients.set(ws, clientData);
2148
- clientData.helloTimeout = setTimeout(() => {
2149
- if (!clientData.helloReceived) {
2150
- this.initializeSession(ws, clientData, undefined, undefined);
2151
- }
2152
- }, 1000);
2153
- } catch (error) {
2154
- log10.error("Error handling WebSocket open:", error);
2155
- ws.close(1011, "Internal server error");
2156
- }
2157
- }
2158
- async initializeSession(ws, clientData, requestedSessionId, props) {
2159
- if (clientData.helloReceived)
2160
- return;
2161
- clientData.helloReceived = true;
2162
- if (clientData.helloTimeout) {
2163
- clearTimeout(clientData.helloTimeout);
2164
- clientData.helloTimeout = undefined;
2165
- }
2166
- let session;
2167
- let isNew = true;
2168
- let isRestored = false;
2169
- let restoredState = null;
2170
- if (requestedSessionId && this.sessionManager) {
2171
- const resumed = this.sessionManager.resumeSession(requestedSessionId);
2172
- if (resumed) {
2173
- session = resumed.session;
2174
- restoredState = resumed.savedState;
2175
- isNew = false;
2176
- isRestored = true;
2177
- await this.triggerReconnect(clientData, session, restoredState);
2178
- } else {
2179
- const activeSession = this.sessionManager.getActiveSession(requestedSessionId);
2180
- if (activeSession) {
2181
- const handled = await this.handleConcurrentConnection(ws, clientData, activeSession, props);
2182
- if (!handled)
2183
- return;
2184
- session = activeSession;
2185
- isNew = false;
2186
- } else {
2187
- session = this.sessionManager.createSession(props);
2188
- }
2189
- }
2190
- } else if (this.sessionManager) {
2191
- session = this.sessionManager.createSession(props);
2192
- } else {
2193
- session = {
2194
- id: crypto.randomUUID(),
2195
- ttl: 3600,
2196
- createdAt: new Date,
2197
- lastConnectedAt: new Date,
2198
- props
2199
- };
2200
- }
2201
- clientData.sessionId = session.id;
2202
- this.sessionManager?.trackConnection(session.id, ws);
2203
- const sessionAck = {
2204
- type: "sessionAck",
2205
- sessionId: session.id,
2206
- isNew,
2207
- isRestored
2208
- };
2209
- ws.send(JSON.stringify(sessionAck));
2210
- this.setupRenderCallback(ws, clientData);
2211
- const initialPatches = [];
2212
- clientData.engine.setRenderCallback((patches) => {
2213
- initialPatches.push(...patches);
2214
- });
2215
- clientData.engine.renderSource(this._ui);
2216
- this.setupRenderCallback(ws, clientData);
2217
- const initialMessage = {
2218
- type: "initialTree",
2219
- module: this._moduleName,
2220
- state: clientData.moduleInstance.getState(),
2221
- patches: initialPatches,
2222
- revision: 0
2223
- };
2224
- ws.send(JSON.stringify(initialMessage));
2225
- const client = {
2226
- id: clientData.id,
2227
- socket: ws,
2228
- connectedAt: clientData.connectedAt
2229
- };
2230
- this._onConnectionCallbacks.forEach((cb) => cb(client));
2231
- }
2232
- setupRenderCallback(ws, clientData) {
2233
- clientData.engine.setRenderCallback((patches) => {
2234
- const data = this.clients.get(ws);
2235
- if (!data)
2236
- return;
2237
- data.revision++;
2238
- const message = {
2239
- type: "patch",
2240
- module: this._moduleName,
2241
- patches,
2242
- revision: data.revision
2243
- };
2244
- ws.send(JSON.stringify(message));
2245
- if (this.sessionManager?.getConcurrentPolicy() === "allow-multiple" && data.sessionId) {
2246
- const connections = this.sessionManager.getConnections(data.sessionId);
2247
- if (connections) {
2248
- for (const conn of connections) {
2249
- if (conn !== ws) {
2250
- conn.send(JSON.stringify(message));
2251
- }
2252
- }
2253
- }
2254
- }
2255
- });
2256
- }
2257
- async handleConcurrentConnection(ws, clientData, existingSession, props) {
2258
- const policy = this.sessionManager?.getConcurrentPolicy() ?? "kick-old";
2259
- switch (policy) {
2260
- case "kick-old": {
2261
- const existingConnections = this.sessionManager?.getConnections(existingSession.id);
2262
- if (existingConnections) {
2263
- for (const conn of existingConnections) {
2264
- const oldWs = conn;
2265
- const expiredMsg = {
2266
- type: "sessionExpired",
2267
- sessionId: existingSession.id,
2268
- reason: "kicked"
2269
- };
2270
- oldWs.send(JSON.stringify(expiredMsg));
2271
- oldWs.close(1000, "Session taken by new connection");
2272
- }
2273
- }
2274
- return true;
2275
- }
2276
- case "reject-new": {
2277
- const expiredMsg = {
2278
- type: "sessionExpired",
2279
- sessionId: existingSession.id,
2280
- reason: "kicked"
2281
- };
2282
- ws.send(JSON.stringify(expiredMsg));
2283
- ws.close(1000, "Session already active");
2284
- return false;
2285
- }
2286
- case "allow-multiple": {
2287
- return true;
2288
- }
2289
- default:
2290
- return true;
2291
- }
2292
- }
2293
- async triggerReconnect(clientData, session, savedState) {
2294
- const handler = this._module?.handlers.onReconnect;
2295
- if (!handler)
2296
- return;
2297
- let restored = false;
2298
- const restore = (state) => {
2299
- restored = true;
2300
- clientData.moduleInstance.updateState(state);
2301
- };
2302
- await handler({ session, restore });
2303
- }
2304
- handleMessage(ws, message) {
2305
- try {
2306
- const clientData = this.clients.get(ws);
2307
- if (!clientData)
2308
- return;
2309
- const msg = JSON.parse(message.toString());
2310
- switch (msg.type) {
2311
- case "hello": {
2312
- const helloMsg = msg;
2313
- this.initializeSession(ws, clientData, helloMsg.sessionId, helloMsg.props);
2314
- break;
2315
- }
2316
- case "dispatchAction": {
2317
- const actionMsg = msg;
2318
- clientData.engine.dispatchAction(actionMsg.action, actionMsg.payload);
2319
- break;
2320
- }
2321
- default:
2322
- break;
2323
- }
2324
- } catch (error) {
2325
- log10.error("Error handling WebSocket message:", error);
2326
- }
2327
- }
2328
- async handleClose(ws) {
2329
- const clientData = this.clients.get(ws);
2330
- if (!clientData)
2331
- return;
2332
- if (clientData.helloTimeout) {
2333
- clearTimeout(clientData.helloTimeout);
2334
- }
2335
- const currentState = clientData.moduleInstance.getState();
2336
- if (clientData.sessionId && this._module?.handlers.onDisconnect) {
2337
- const session = this.sessionManager?.getActiveSession(clientData.sessionId);
2338
- if (session) {
2339
- await this._module.handlers.onDisconnect({
2340
- state: currentState,
2341
- session
2342
- });
2343
- }
2344
- }
2345
- if (clientData.sessionId && this.sessionManager) {
2346
- this.sessionManager.untrackConnection(clientData.sessionId, ws);
2347
- if (this.sessionManager.getConnectionCount(clientData.sessionId) === 0) {
2348
- const session = this.sessionManager.getActiveSession(clientData.sessionId);
2349
- if (session) {
2350
- this.sessionManager.suspendSession(clientData.sessionId, currentState, async (expiredSession) => {
2351
- if (this._module?.handlers.onExpire) {
2352
- await this._module.handlers.onExpire({ session: expiredSession });
2353
- }
2354
- });
2355
- }
2356
- }
2357
- }
2358
- await clientData.moduleInstance.destroy();
2359
- this.clients.delete(ws);
2360
- const client = {
2361
- id: clientData.id,
2362
- socket: ws,
2363
- connectedAt: clientData.connectedAt
2364
- };
2365
- this._onDisconnectionCallbacks.forEach((cb) => cb(client));
2366
- }
2367
- getClientCount() {
2368
- return this.clients.size;
2369
- }
2370
- getSessionStats() {
2371
- return this.sessionManager?.getStats() ?? {
2372
- activeSessions: 0,
2373
- pendingSessions: 0,
2374
- totalConnections: 0
2375
- };
2376
- }
2377
- broadcast(message) {
2378
- const json = JSON.stringify(message);
2379
- this.clients.forEach((_, ws) => {
2380
- ws.send(json);
2381
- });
2382
- }
2383
- }
2384
- function serve(options) {
2385
- const server = new RemoteServer().module(options.moduleName ?? "App", options.module).ui(options.ui);
2386
- if (options.port || options.hostname) {
2387
- server.config({
2388
- port: options.port,
2389
- hostname: options.hostname
2390
- });
2391
- }
2392
- if (options.session) {
2393
- server.session(options.session);
2394
- }
2395
- if (options.onConnection) {
2396
- server.onConnection(options.onConnection);
2397
- }
2398
- if (options.onDisconnection) {
2399
- server.onDisconnection(options.onDisconnection);
2400
- }
2401
- return server.listen(options.port);
2402
- }
2403
-
2404
- // src/loader.ts
2405
- init_logger();
2406
- import { existsSync, readdirSync, readFileSync } from "fs";
2407
- import { join } from "path";
2408
- var log11 = frameworkLoggers.loader;
2409
-
2410
- class ComponentLoader {
2411
- components = new Map;
2412
- register(name, module, template, path) {
2413
- this.components.set(name, {
2414
- name,
2415
- module,
2416
- template,
2417
- path: path || name
2418
- });
2419
- }
2420
- get(name) {
2421
- return this.components.get(name);
2422
- }
2423
- has(name) {
2424
- return this.components.has(name);
2425
- }
2426
- getNames() {
2427
- return Array.from(this.components.keys());
2428
- }
2429
- getAll() {
2430
- return Array.from(this.components.values());
2431
- }
2432
- clear() {
2433
- this.components.clear();
2434
- }
2435
- async loadFromDirectory(name, dirPath) {
2436
- try {
2437
- const modulePath = join(dirPath, "component.ts");
2438
- const moduleExport = await import(modulePath);
2439
- const module = moduleExport.default;
2440
- const templatePath = join(dirPath, "component.hypen");
2441
- const template = readFileSync(templatePath, "utf-8");
2442
- this.register(name, module, template, dirPath);
2443
- log11.debug(`Loaded component: ${name} from ${dirPath}`);
2444
- } catch (error) {
2445
- log11.error(`Failed to load component ${name} from ${dirPath}:`, error);
2446
- throw error;
2447
- }
2448
- }
2449
- async loadFromComponentsDir(baseDir) {
2450
- try {
2451
- if (!existsSync(baseDir)) {
2452
- log11.warn(`Components directory not found: ${baseDir}`);
2453
- return;
2454
- }
2455
- const entries = readdirSync(baseDir, { withFileTypes: true });
2456
- for (const entry of entries) {
2457
- if (!entry.isDirectory())
2458
- continue;
2459
- const componentDir = join(baseDir, entry.name);
2460
- const hypenPath = join(componentDir, "component.hypen");
2461
- if (existsSync(hypenPath)) {
2462
- await this.loadFromDirectory(entry.name, componentDir);
2463
- }
2464
- }
2465
- log11.debug(`Loaded ${this.components.size} components from ${baseDir}`);
2466
- } catch (error) {
2467
- log11.error(`Failed to load components from ${baseDir}:`, error);
2468
- throw error;
2469
- }
2470
- }
2471
- }
2472
- var componentLoader = new ComponentLoader;
2473
-
2474
- // src/resolver.ts
2475
- class ComponentResolver {
2476
- cache = new Map;
2477
- options;
2478
- moduleRegistry;
2479
- constructor(options = {}) {
2480
- this.options = {
2481
- baseDir: options.baseDir || process.cwd(),
2482
- cache: options.cache ?? true,
2483
- customFetch: options.customFetch || this.defaultFetch.bind(this),
2484
- moduleRegistry: options.moduleRegistry
2485
- };
2486
- this.moduleRegistry = options.moduleRegistry;
2487
- }
2488
- async resolve(importStmt) {
2489
- if (this.moduleRegistry) {
2490
- const names = importStmt.clause.type === "named" ? importStmt.clause.names : [importStmt.clause.name];
2491
- const allFound = names.every((name) => this.moduleRegistry.has(name));
2492
- if (allFound) {
2493
- const result = {};
2494
- for (const name of names) {
2495
- const def = this.moduleRegistry.get(name);
2496
- result[name] = {
2497
- module: def,
2498
- template: def?.template || ""
2499
- };
2500
- }
2501
- return result;
2502
- }
2503
- }
2504
- const sourcePath = this.getSourcePath(importStmt.source);
2505
- if (this.options.cache && this.cache.has(sourcePath)) {
2506
- const cached = this.cache.get(sourcePath);
2507
- return this.extractComponents(importStmt.clause, cached);
2508
- }
2509
- let component;
2510
- if (importStmt.source.type === "local") {
2511
- component = await this.resolveLocal(importStmt.source.path);
2512
- } else {
2513
- component = await this.resolveUrl(importStmt.source.url);
2514
- }
2515
- if (this.options.cache) {
2516
- this.cache.set(sourcePath, component);
2517
- }
2518
- return this.extractComponents(importStmt.clause, component);
2519
- }
2520
- async resolveLocal(path) {
2521
- const { resolve, join: join2 } = await import("path");
2522
- const { readFile } = await import("fs/promises");
2523
- const basePath = resolve(this.options.baseDir, path);
2524
- const hypenPath = basePath.endsWith(".hypen") ? basePath : `${basePath}.hypen`;
2525
- const template = await readFile(hypenPath, "utf-8");
2526
- const modulePath = hypenPath.replace(/\.hypen$/, ".ts");
2527
- let module = {};
2528
- try {
2529
- module = await import(modulePath);
2530
- } catch {}
2531
- return { module, template };
2532
- }
2533
- async resolveUrl(url) {
2534
- try {
2535
- const response = await this.options.customFetch(url);
2536
- const data = JSON.parse(response);
2537
- if (!data.module || !data.template) {
2538
- throw new Error(`Invalid component format from ${url}. Expected { module, template }`);
2539
- }
2540
- return data;
2541
- } catch (error) {
2542
- throw new Error(`Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`);
2543
- }
2544
- }
2545
- async defaultFetch(url) {
2546
- const response = await fetch(url);
2547
- if (!response.ok) {
2548
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2549
- }
2550
- return response.text();
2551
- }
2552
- extractComponents(clause, component) {
2553
- if (clause.type === "default") {
2554
- return {
2555
- [clause.name]: component
2556
- };
2557
- } else {
2558
- const result = {};
2559
- for (const name of clause.names) {
2560
- result[name] = component;
2561
- }
2562
- return result;
2563
- }
2564
- }
2565
- getSourcePath(source) {
2566
- return source.type === "local" ? source.path : source.url;
2567
- }
2568
- clearCache() {
2569
- this.cache.clear();
2570
- }
2571
- static parseImports(text) {
2572
- const imports = [];
2573
- const importRegex = /import\s+(?:(\{[^}]*\})|(\w+))\s+from\s+["']([^"']+)["']/g;
2574
- let match2;
2575
- while ((match2 = importRegex.exec(text)) !== null) {
2576
- const [, namedImports, defaultImport, source] = match2;
2577
- if (!source)
2578
- continue;
2579
- let clause;
2580
- if (namedImports) {
2581
- const names = namedImports.slice(1, -1).split(",").map((n) => n.trim()).filter((n) => n.length > 0);
2582
- clause = { type: "named", names };
2583
- } else if (defaultImport) {
2584
- clause = { type: "default", name: defaultImport };
2585
- } else {
2586
- continue;
2587
- }
2588
- const sourceObj = source.startsWith("http://") || source.startsWith("https://") ? { type: "url", url: source } : { type: "local", path: source };
2589
- imports.push({ clause, source: sourceObj });
2590
- }
2591
- return imports;
2592
- }
2593
- }
2594
-
2595
- // src/discovery.ts
2596
- init_logger();
2597
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, watch } from "fs";
2598
- import { join as join2, basename, resolve } from "path";
2599
- async function discoverComponents(baseDir, options = {}) {
2600
- const {
2601
- patterns = ["single-file", "folder", "sibling", "index"],
2602
- recursive = false,
2603
- debug = false
2604
- } = options;
2605
- const log12 = debug ? (...args) => frameworkLoggers.discovery.debug(...args) : () => {};
2606
- const resolvedDir = resolve(baseDir);
2607
- const components = [];
2608
- const seen = new Set;
2609
- log12("Scanning directory:", resolvedDir);
2610
- log12("Patterns:", patterns);
2611
- const addTwoFileComponent = (name, hypenPath, modulePath) => {
2612
- if (seen.has(name)) {
2613
- log12(`Skipping duplicate: ${name}`);
2614
- return;
2615
- }
2616
- seen.add(name);
2617
- const templateRaw = readFileSync2(hypenPath, "utf-8");
2618
- const template = templateRaw.trim();
2619
- components.push({
2620
- name,
2621
- hypenPath,
2622
- modulePath,
2623
- template,
2624
- hasModule: modulePath !== null,
2625
- isSingleFile: false
2626
- });
2627
- log12(`Found: ${name} (two-file, ${modulePath ? "with module" : "stateless"})`);
2628
- };
2629
- const addSingleFileComponent = (name, modulePath, template) => {
2630
- if (seen.has(name)) {
2631
- log12(`Skipping duplicate: ${name}`);
2632
- return;
2633
- }
2634
- seen.add(name);
2635
- components.push({
2636
- name,
2637
- hypenPath: null,
2638
- modulePath,
2639
- template,
2640
- hasModule: true,
2641
- isSingleFile: true
2642
- });
2643
- log12(`Found: ${name} (single-file with inline template)`);
2644
- };
2645
- const scanForFolderComponents = (dir) => {
2646
- if (!existsSync2(dir))
2647
- return;
2648
- const entries = readdirSync2(dir, { withFileTypes: true });
2649
- for (const entry of entries) {
2650
- if (!entry.isDirectory())
2651
- continue;
2652
- const folderPath = join2(dir, entry.name);
2653
- const componentName = entry.name;
2654
- if (patterns.includes("folder")) {
2655
- const hypenPath = join2(folderPath, "component.hypen");
2656
- if (existsSync2(hypenPath)) {
2657
- const modulePath = join2(folderPath, "component.ts");
2658
- addTwoFileComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2659
- continue;
2660
- }
2661
- }
2662
- if (patterns.includes("index")) {
2663
- const hypenPath = join2(folderPath, "index.hypen");
2664
- if (existsSync2(hypenPath)) {
2665
- const modulePath = join2(folderPath, "index.ts");
2666
- addTwoFileComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2667
- continue;
2668
- }
2669
- }
2670
- if (recursive) {
2671
- scanForFolderComponents(folderPath);
2672
- }
2673
- }
2674
- };
2675
- const scanForSiblingComponents = (dir) => {
2676
- if (!existsSync2(dir))
2677
- return;
2678
- const entries = readdirSync2(dir, { withFileTypes: true });
2679
- for (const entry of entries) {
2680
- if (entry.isDirectory()) {
2681
- if (recursive) {
2682
- scanForSiblingComponents(join2(dir, entry.name));
2683
- }
2684
- continue;
2685
- }
2686
- if (!entry.name.endsWith(".hypen"))
2687
- continue;
2688
- const hypenPath = join2(dir, entry.name);
2689
- const baseName = basename(entry.name, ".hypen");
2690
- if (baseName === "component" || baseName === "index")
2691
- continue;
2692
- const modulePath = join2(dir, `${baseName}.ts`);
2693
- addTwoFileComponent(baseName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2694
- }
2695
- };
2696
- const scanForSingleFileComponents = async (dir) => {
2697
- if (!existsSync2(dir))
2698
- return;
2699
- const entries = readdirSync2(dir, { withFileTypes: true });
2700
- for (const entry of entries) {
2701
- if (entry.isDirectory()) {
2702
- if (recursive) {
2703
- await scanForSingleFileComponents(join2(dir, entry.name));
2704
- }
2705
- continue;
2706
- }
2707
- if (!entry.name.endsWith(".ts"))
2708
- continue;
2709
- if (entry.name.startsWith(".") || entry.name.includes(".test.") || entry.name.includes(".spec."))
2710
- continue;
2711
- const baseName = basename(entry.name, ".ts");
2712
- if (baseName === "component" || baseName === "index")
2713
- continue;
2714
- const hypenPath = join2(dir, `${baseName}.hypen`);
2715
- if (existsSync2(hypenPath))
2716
- continue;
2717
- const modulePath = join2(dir, entry.name);
2718
- const content = readFileSync2(modulePath, "utf-8");
2719
- if (content.includes(".ui(") || content.includes(".ui(hypen")) {
2720
- try {
2721
- const moduleExport = await import(modulePath);
2722
- const module = moduleExport.default;
2723
- if (module && typeof module === "object" && module.template) {
2724
- addSingleFileComponent(baseName, modulePath, module.template);
2725
- }
2726
- } catch (e) {
2727
- log12(`Failed to import potential single-file component: ${entry.name}`, e);
2728
- }
2729
- }
2730
- }
2731
- };
2732
- if (patterns.includes("folder") || patterns.includes("index")) {
2733
- scanForFolderComponents(resolvedDir);
2734
- }
2735
- if (patterns.includes("sibling")) {
2736
- scanForSiblingComponents(resolvedDir);
2737
- }
2738
- if (patterns.includes("single-file")) {
2739
- await scanForSingleFileComponents(resolvedDir);
2740
- }
2741
- log12(`Discovered ${components.length} components`);
2742
- return components;
2743
- }
2744
- async function loadDiscoveredComponents(components) {
2745
- const { app: app2 } = await Promise.resolve().then(() => (init_app(), exports_app));
2746
- const loaded = new Map;
2747
- for (const component of components) {
2748
- let module;
2749
- let template = component.template;
2750
- if (component.modulePath) {
2751
- const moduleExport = await import(component.modulePath);
2752
- module = moduleExport.default;
2753
- if (component.isSingleFile && module.template) {
2754
- template = module.template;
2755
- }
2756
- } else {
2757
- module = app2.defineState({}).build();
2758
- }
2759
- loaded.set(component.name, {
2760
- name: component.name,
2761
- module,
2762
- template
2763
- });
2764
- }
2765
- return loaded;
2766
- }
2767
- function watchComponents(baseDir, options = {}) {
2768
- const resolvedDir = resolve(baseDir);
2769
- const {
2770
- onChange,
2771
- onAdd,
2772
- onRemove,
2773
- onUpdate,
2774
- debug = false,
2775
- ...discoveryOptions
2776
- } = options;
2777
- const log12 = debug ? (...args) => frameworkLoggers.discovery.debug("[watch]", ...args) : () => {};
2778
- let currentComponents = new Map;
2779
- let debounceTimer = null;
2780
- const initialScan = async () => {
2781
- const components = await discoverComponents(resolvedDir, discoveryOptions);
2782
- currentComponents = new Map(components.map((c) => [c.name, c]));
2783
- onChange?.(components);
2784
- };
2785
- const rescan = async () => {
2786
- if (debounceTimer) {
2787
- clearTimeout(debounceTimer);
2788
- }
2789
- debounceTimer = setTimeout(async () => {
2790
- log12("Rescanning...");
2791
- const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
2792
- const newMap = new Map(newComponents.map((c) => [c.name, c]));
2793
- for (const [name, component] of newMap) {
2794
- const existing = currentComponents.get(name);
2795
- if (!existing) {
2796
- log12("Added:", name);
2797
- onAdd?.(component);
2798
- } else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
2799
- log12("Updated:", name);
2800
- onUpdate?.(component);
2801
- }
2802
- }
2803
- for (const name of currentComponents.keys()) {
2804
- if (!newMap.has(name)) {
2805
- log12("Removed:", name);
2806
- onRemove?.(name);
2807
- }
2808
- }
2809
- currentComponents = newMap;
2810
- onChange?.(newComponents);
2811
- }, 100);
2812
- };
2813
- const watcher = watch(resolvedDir, { recursive: true }, (event, filename) => {
2814
- if (!filename)
2815
- return;
2816
- if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
2817
- log12("File changed:", filename);
2818
- rescan();
2819
- }
2820
- });
2821
- initialScan();
2822
- return {
2823
- stop: () => {
2824
- watcher.close();
2825
- if (debounceTimer) {
2826
- clearTimeout(debounceTimer);
2827
- }
2828
- }
2829
- };
2830
- }
2831
- async function generateComponentsCode(baseDir, options = {}) {
2832
- const components = await discoverComponents(baseDir, options);
2833
- const resolvedDir = resolve(baseDir);
2834
- let code = `/**
2835
- * Auto-generated component imports
2836
- * Generated by @hypen-space/core discovery
2837
- */
2838
-
2839
- `;
2840
- for (const component of components) {
2841
- const relativePath = component.modulePath ? "./" + component.modulePath.replace(resolvedDir + "/", "").replace(/\.ts$/, ".js") : null;
2842
- if (relativePath) {
2843
- code += `import ${component.name}Module from "${relativePath}";
2844
- `;
2845
- }
2846
- }
2847
- code += `
2848
- import { app } from "@hypen-space/core";
2849
-
2850
- `;
2851
- for (const component of components) {
2852
- if (component.isSingleFile) {
2853
- code += `export const ${component.name} = {
2854
- module: ${component.name}Module,
2855
- template: ${component.name}Module.template,
2856
- };
2857
-
2858
- `;
2859
- } else if (component.hasModule) {
2860
- const templateJson = JSON.stringify(component.template);
2861
- code += `export const ${component.name} = {
2862
- module: ${component.name}Module,
2863
- template: ${templateJson},
2864
- };
2865
-
2866
- `;
2867
- } else {
2868
- const templateJson = JSON.stringify(component.template);
2869
- code += `const ${component.name}Module = app.defineState({}).build();
2870
- export const ${component.name} = {
2871
- module: ${component.name}Module,
2872
- template: ${templateJson},
2873
- };
2874
-
2875
- `;
2876
- }
2877
- }
2878
- return code;
2879
- }
2880
-
2881
- // src/plugin.ts
2882
- init_logger();
2883
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
2884
- import { dirname, basename as basename2, join as join3, resolve as resolve2 } from "path";
2885
- function findModulePath(hypenPath, patterns) {
2886
- const dir = dirname(hypenPath);
2887
- const baseName = basename2(hypenPath, ".hypen");
2888
- const parentDir = dirname(dir);
2889
- const folderName = basename2(dir);
2890
- for (const pattern of patterns) {
2891
- let candidatePath = null;
2892
- switch (pattern) {
2893
- case "sibling":
2894
- candidatePath = join3(dir, `${baseName}.ts`);
2895
- break;
2896
- case "component":
2897
- if (baseName === "component") {
2898
- candidatePath = join3(dir, "component.ts");
2899
- }
2900
- break;
2901
- case "index":
2902
- if (baseName === "index") {
2903
- candidatePath = join3(dir, "index.ts");
2904
- }
2905
- break;
2906
- }
2907
- if (candidatePath && existsSync3(candidatePath)) {
2908
- return candidatePath;
2909
- }
2910
- }
2911
- return null;
2912
- }
2913
- function getComponentName(hypenPath) {
2914
- const baseName = basename2(hypenPath, ".hypen");
2915
- if (baseName === "component" || baseName === "index") {
2916
- return basename2(dirname(hypenPath));
2917
- }
2918
- return baseName;
2919
- }
2920
- function parseImports(text) {
2921
- const imports = [];
2922
- const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+["']([^"']+)["']/g;
2923
- let match2;
2924
- while ((match2 = importRegex.exec(text)) !== null) {
2925
- const [, namedImports, defaultImport, source] = match2;
2926
- if (!source)
2927
- continue;
2928
- let names;
2929
- if (namedImports) {
2930
- names = namedImports.split(",").map((n) => n.trim()).filter((n) => n.length > 0);
2931
- } else if (defaultImport) {
2932
- names = [defaultImport];
2933
- } else {
2934
- continue;
2935
- }
2936
- imports.push({ names, source });
2937
- }
2938
- return imports;
2939
- }
2940
- function removeImports(text) {
2941
- return text.replace(/import\s+(?:\{[^}]+\}|\w+)\s+from\s+["'][^"']+["']\s*/g, "");
2942
- }
2943
- function hypenPlugin(options = {}) {
2944
- const { debug = false, patterns = ["sibling", "component", "index"] } = options;
2945
- const log12 = debug ? (...args) => frameworkLoggers.plugin.debug(...args) : () => {};
2946
- return {
2947
- name: "hypen-loader",
2948
- async setup(build) {
2949
- build.onLoad({ filter: /\.hypen$/ }, async (args) => {
2950
- const hypenPath = resolve2(args.path);
2951
- log12("Loading:", hypenPath);
2952
- const templateRaw = readFileSync3(hypenPath, "utf-8");
2953
- const imports = parseImports(templateRaw);
2954
- const template = removeImports(templateRaw).trim();
2955
- if (imports.length > 0) {
2956
- log12("Found imports:", imports);
2957
- }
2958
- const componentName = getComponentName(hypenPath);
2959
- log12("Component name:", componentName);
2960
- const modulePath = findModulePath(hypenPath, patterns);
2961
- log12("Module path:", modulePath);
2962
- let contents;
2963
- if (modulePath) {
2964
- const relativeModulePath = modulePath.replace(/\.ts$/, ".js");
2965
- contents = `
2966
- import _module from "${relativeModulePath}";
2967
- export const module = _module;
2968
- export const template = ${JSON.stringify(template)};
2969
- export const name = ${JSON.stringify(componentName)};
2970
- export default { module: _module, template: ${JSON.stringify(template)}, name: ${JSON.stringify(componentName)} };
2971
- `;
2972
- } else {
2973
- log12("No module found, creating stateless component");
2974
- contents = `
2975
- import { app } from "@hypen-space/core";
2976
- const _module = app.defineState({}).build();
2977
- export const module = _module;
2978
- export const template = ${JSON.stringify(template)};
2979
- export const name = ${JSON.stringify(componentName)};
2980
- export default { module: _module, template: ${JSON.stringify(template)}, name: ${JSON.stringify(componentName)} };
2981
- `;
2982
- }
2983
- return {
2984
- contents,
2985
- loader: "js"
2986
- };
2987
- });
2988
- }
2989
- };
2990
- }
2991
- var defaultHypenPlugin = hypenPlugin();
2992
- function registerHypenPlugin(options) {
2993
- Bun.plugin(hypenPlugin(options));
2994
- }
2995
- var plugin_default = hypenPlugin;
2996
-
2997
- // src/components/builtin.ts
2998
- init_app();
2999
- init_logger();
3000
- var log12 = frameworkLoggers.router;
3001
- var Router = app.defineState({
3002
- currentPath: "/",
3003
- matchedRoute: null,
3004
- routeParams: {}
3005
- }, { name: "__Router" }).onCreated((state, context) => {
3006
- if (!context) {
3007
- log12.error("Requires global context");
3008
- return;
3009
- }
3010
- const router = context.__router;
3011
- if (!router) {
3012
- log12.error("Router not found in context");
3013
- return;
3014
- }
3015
- const hypenEngine = context.__hypenEngine;
3016
- const updateRouteVisibility = async (currentPath) => {
3017
- const routeElements = document.querySelectorAll('[data-hypen-type="route"]');
3018
- let matchFound = false;
3019
- for (let index = 0;index < routeElements.length; index++) {
3020
- const routeEl = routeElements[index];
3021
- const htmlEl = routeEl;
3022
- const routePath = htmlEl.dataset.routePath || "/";
3023
- const isMatch = routePath === currentPath;
3024
- htmlEl.style.display = isMatch ? "flex" : "none";
3025
- if (isMatch) {
3026
- matchFound = true;
3027
- const componentName = htmlEl.dataset.routeComponent;
3028
- const isLazy = htmlEl.dataset.routeLazy === "true";
3029
- const hasContent = htmlEl.children.length > 0;
3030
- if (componentName && !hasContent && hypenEngine) {
3031
- try {
3032
- await hypenEngine.renderLazyRoute(routePath, componentName, htmlEl);
3033
- } catch (err) {
3034
- log12.error(`Failed to render route ${routePath}:`, err);
3035
- }
3036
- }
3037
- }
3038
- }
3039
- if (!matchFound) {
3040
- log12.warn(`No route matched path: ${currentPath}. Available routes:`, Array.from(routeElements).map((el) => el.dataset.routePath));
3041
- }
3042
- };
3043
- setTimeout(() => {
3044
- updateRouteVisibility(state.currentPath);
3045
- }, 100);
3046
- router.onNavigate((routeState) => {
3047
- state.currentPath = routeState.currentPath;
3048
- state.routeParams = routeState.params;
3049
- updateRouteVisibility(routeState.currentPath);
3050
- });
3051
- }).build();
3052
- var Route = app.defineState({}, { name: "__Route" }).build();
3053
- var Link = app.defineState({
3054
- to: "/",
3055
- isActive: false
3056
- }, { name: "__Link" }).onAction("navigate", ({ state, context }) => {
3057
- const router = context?.__router;
3058
- if (!router) {
3059
- log12.error("Link requires router context");
3060
- return;
3061
- }
3062
- const targetPath = state.to;
3063
- router.push(targetPath);
3064
- }).onCreated((state, context) => {
3065
- if (!context)
3066
- return;
3067
- const router = context.__router;
3068
- if (router) {
3069
- router.onNavigate((routeState) => {
3070
- state.isActive = routeState.currentPath === state.to;
3071
- });
3072
- }
3073
- }).build();
3074
-
3075
- // src/index.ts
3076
- init_app();
3077
-
3078
- // src/hypen.ts
3079
- function createBindingProxy(root) {
3080
- const handler = {
3081
- get(_, prop) {
3082
- if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
3083
- return () => `\${${root}}`;
3084
- }
3085
- if (typeof prop === "symbol") {
3086
- return;
3087
- }
3088
- if (prop === "toJSON") {
3089
- return () => `\${${root}}`;
3090
- }
3091
- return createBindingProxy(`${root}.${prop}`);
3092
- },
3093
- has() {
3094
- return true;
3095
- },
3096
- ownKeys() {
3097
- return [];
3098
- },
3099
- getOwnPropertyDescriptor() {
3100
- return {
3101
- configurable: true,
3102
- enumerable: true
3103
- };
3104
- }
3105
- };
3106
- return new Proxy({}, handler);
3107
- }
3108
- var state = createBindingProxy("state");
3109
- var item = createBindingProxy("item");
3110
- var index = {
3111
- [Symbol.toPrimitive]: () => "${index}",
3112
- toString: () => "${index}",
3113
- valueOf: () => "${index}",
3114
- toJSON: () => "${index}"
3115
- };
3116
- function hypen(strings, ...expressions) {
3117
- let result = strings[0];
3118
- for (let i = 0;i < expressions.length; i++) {
3119
- const expr = expressions[i];
3120
- result += String(expr);
3121
- result += strings[i + 1];
3122
- }
3123
- return result.trim();
3124
- }
3125
-
3126
2273
  // src/index.ts
3127
- init_state();
3128
2274
  init_result();
3129
2275
  init_logger();
3130
2276
  export {
3131
2277
  withRetry,
3132
- watchComponents,
3133
2278
  usingSync,
3134
2279
  using,
3135
2280
  unwrapProxy,
@@ -3139,16 +2284,13 @@ export {
3139
2284
  state,
3140
2285
  setLogLevel,
3141
2286
  setDebugMode,
3142
- serve,
3143
2287
  retryResult,
3144
2288
  retry,
3145
- registerHypenPlugin,
3146
2289
  match,
3147
2290
  mapErr,
3148
2291
  map,
3149
2292
  logger,
3150
2293
  log,
3151
- loadDiscoveredComponents,
3152
2294
  item,
3153
2295
  isStateProxy,
3154
2296
  isOk,
@@ -3156,13 +2298,11 @@ export {
3156
2298
  isDisposable,
3157
2299
  isDebugMode,
3158
2300
  index,
3159
- hypenPlugin,
3160
2301
  hypen,
3161
2302
  hasElementDisposables,
3162
2303
  getStateSnapshot,
3163
2304
  getLogLevel,
3164
2305
  getElementDisposables,
3165
- generateComponentsCode,
3166
2306
  fromTry,
3167
2307
  fromPromise,
3168
2308
  frameworkLoggers,
@@ -3175,15 +2315,13 @@ export {
3175
2315
  disposableListener,
3176
2316
  disposableInterval,
3177
2317
  disposableAbortController,
3178
- discoverComponents,
3179
2318
  disableLogging,
3180
- defaultHypenPlugin,
3181
2319
  createObservableState,
3182
2320
  createLogger,
3183
2321
  createEventEmitter,
3184
2322
  configureLogger,
3185
2323
  compositeDisposable,
3186
- componentLoader,
2324
+ classifyEngineError,
3187
2325
  batchStateUpdates,
3188
2326
  app,
3189
2327
  all,
@@ -3194,8 +2332,9 @@ export {
3194
2332
  Route,
3195
2333
  RetryPresets,
3196
2334
  RetryConditions,
3197
- RemoteServer,
2335
+ RenderError,
3198
2336
  RemoteEngine,
2337
+ ParseError,
3199
2338
  Ok,
3200
2339
  Logger,
3201
2340
  Link,
@@ -3211,9 +2350,8 @@ export {
3211
2350
  ConsoleRenderer,
3212
2351
  ConnectionError,
3213
2352
  ComponentResolver,
3214
- ComponentLoader,
3215
2353
  BaseRenderer,
3216
2354
  ActionError
3217
2355
  };
3218
2356
 
3219
- //# debugId=C03FB9EC8278859464756E2164756E21
2357
+ //# debugId=94AD8348A299084864756E2164756E21