@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
  }
@@ -853,6 +946,7 @@ var init_app = __esm(() => {
853
946
 
854
947
  // src/engine.ts
855
948
  init_logger();
949
+ init_result();
856
950
  import { WasmEngine } from "../wasm-node/hypen_engine.js";
857
951
  var log3 = frameworkLoggers.engine;
858
952
  function unwrapForWasm(value) {
@@ -899,7 +993,11 @@ class Engine {
899
993
  }
900
994
  renderSource(source) {
901
995
  const engine = this.ensureInitialized();
902
- engine.renderSource(source);
996
+ try {
997
+ engine.renderSource(source);
998
+ } catch (err) {
999
+ throw classifyEngineError(err);
1000
+ }
903
1001
  }
904
1002
  renderLazyComponent(source) {
905
1003
  const engine = this.ensureInitialized();
@@ -907,14 +1005,22 @@ class Engine {
907
1005
  }
908
1006
  renderInto(source, parentNodeId, state) {
909
1007
  const engine = this.ensureInitialized();
910
- engine.renderInto(source, parentNodeId, unwrapForWasm(state));
1008
+ try {
1009
+ engine.renderInto(source, parentNodeId, unwrapForWasm(state));
1010
+ } catch (err) {
1011
+ throw classifyEngineError(err);
1012
+ }
911
1013
  }
912
1014
  notifyStateChange(paths, values) {
913
1015
  const engine = this.ensureInitialized();
914
1016
  if (paths.length === 0) {
915
1017
  return;
916
1018
  }
917
- engine.updateStateSparse(paths, unwrapForWasm(values));
1019
+ try {
1020
+ engine.updateStateSparse(paths, unwrapForWasm(values));
1021
+ } catch (err) {
1022
+ throw classifyEngineError(err);
1023
+ }
918
1024
  log3.debug("State changed (sparse):", paths);
919
1025
  }
920
1026
  notifyStateChangeFull(paths, currentState) {
@@ -930,7 +1036,11 @@ class Engine {
930
1036
  }
931
1037
  dispatchAction(name, payload) {
932
1038
  const engine = this.ensureInitialized();
933
- engine.dispatchAction(name, payload ?? null);
1039
+ try {
1040
+ engine.dispatchAction(name, payload ?? null);
1041
+ } catch (err) {
1042
+ throw classifyEngineError(err);
1043
+ }
934
1044
  }
935
1045
  onAction(actionName, handler) {
936
1046
  const engine = this.ensureInitialized();
@@ -956,6 +1066,297 @@ class Engine {
956
1066
  }
957
1067
  }
958
1068
 
1069
+ // src/discovery.ts
1070
+ init_logger();
1071
+ import { existsSync, readdirSync, readFileSync, watch } from "fs";
1072
+ import { join, basename, resolve, relative } from "path";
1073
+ async function discoverComponents(baseDir, options = {}) {
1074
+ const {
1075
+ patterns = ["single-file", "folder", "sibling", "index"],
1076
+ recursive = true,
1077
+ debug = false
1078
+ } = options;
1079
+ const log4 = debug ? (...args) => frameworkLoggers.discovery.debug(...args) : () => {};
1080
+ const resolvedDir = resolve(baseDir);
1081
+ const components = [];
1082
+ const seen = new Set;
1083
+ log4("Scanning directory:", resolvedDir);
1084
+ log4("Patterns:", patterns);
1085
+ const addTwoFileComponent = (name, hypenPath, modulePath) => {
1086
+ if (seen.has(name)) {
1087
+ log4(`Skipping duplicate: ${name}`);
1088
+ return;
1089
+ }
1090
+ seen.add(name);
1091
+ const templateRaw = readFileSync(hypenPath, "utf-8");
1092
+ const template = templateRaw.trim();
1093
+ components.push({
1094
+ name,
1095
+ hypenPath,
1096
+ modulePath,
1097
+ template,
1098
+ hasModule: modulePath !== null,
1099
+ isSingleFile: false
1100
+ });
1101
+ log4(`Found: ${name} (two-file, ${modulePath ? "with module" : "stateless"})`);
1102
+ };
1103
+ const addSingleFileComponent = (name, modulePath, template) => {
1104
+ if (seen.has(name)) {
1105
+ log4(`Skipping duplicate: ${name}`);
1106
+ return;
1107
+ }
1108
+ seen.add(name);
1109
+ components.push({
1110
+ name,
1111
+ hypenPath: null,
1112
+ modulePath,
1113
+ template,
1114
+ hasModule: true,
1115
+ isSingleFile: true
1116
+ });
1117
+ log4(`Found: ${name} (single-file with inline template)`);
1118
+ };
1119
+ const scanForFolderComponents = (dir) => {
1120
+ if (!existsSync(dir))
1121
+ return;
1122
+ const entries = readdirSync(dir, { withFileTypes: true });
1123
+ for (const entry of entries) {
1124
+ if (!entry.isDirectory())
1125
+ continue;
1126
+ const folderPath = join(dir, entry.name);
1127
+ const componentName = entry.name;
1128
+ if (patterns.includes("folder")) {
1129
+ const hypenPath = join(folderPath, "component.hypen");
1130
+ if (existsSync(hypenPath)) {
1131
+ const modulePath = join(folderPath, "component.ts");
1132
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
1133
+ continue;
1134
+ }
1135
+ }
1136
+ if (patterns.includes("index")) {
1137
+ const hypenPath = join(folderPath, "index.hypen");
1138
+ if (existsSync(hypenPath)) {
1139
+ const modulePath = join(folderPath, "index.ts");
1140
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
1141
+ continue;
1142
+ }
1143
+ }
1144
+ if (recursive) {
1145
+ scanForFolderComponents(folderPath);
1146
+ }
1147
+ }
1148
+ };
1149
+ const scanForSiblingComponents = (dir) => {
1150
+ if (!existsSync(dir))
1151
+ return;
1152
+ const entries = readdirSync(dir, { withFileTypes: true });
1153
+ for (const entry of entries) {
1154
+ if (entry.isDirectory()) {
1155
+ if (recursive) {
1156
+ scanForSiblingComponents(join(dir, entry.name));
1157
+ }
1158
+ continue;
1159
+ }
1160
+ if (!entry.name.endsWith(".hypen"))
1161
+ continue;
1162
+ const hypenPath = join(dir, entry.name);
1163
+ const baseName = basename(entry.name, ".hypen");
1164
+ if (baseName === "component" || baseName === "index")
1165
+ continue;
1166
+ const modulePath = join(dir, `${baseName}.ts`);
1167
+ addTwoFileComponent(baseName, hypenPath, existsSync(modulePath) ? modulePath : null);
1168
+ }
1169
+ };
1170
+ const scanForSingleFileComponents = async (dir) => {
1171
+ if (!existsSync(dir))
1172
+ return;
1173
+ const entries = readdirSync(dir, { withFileTypes: true });
1174
+ for (const entry of entries) {
1175
+ if (entry.isDirectory()) {
1176
+ if (recursive) {
1177
+ await scanForSingleFileComponents(join(dir, entry.name));
1178
+ }
1179
+ continue;
1180
+ }
1181
+ if (!entry.name.endsWith(".ts"))
1182
+ continue;
1183
+ if (entry.name.startsWith(".") || entry.name.includes(".test.") || entry.name.includes(".spec."))
1184
+ continue;
1185
+ const baseName = basename(entry.name, ".ts");
1186
+ if (baseName === "component" || baseName === "index")
1187
+ continue;
1188
+ const hypenPath = join(dir, `${baseName}.hypen`);
1189
+ if (existsSync(hypenPath))
1190
+ continue;
1191
+ const modulePath = join(dir, entry.name);
1192
+ const content = readFileSync(modulePath, "utf-8");
1193
+ if (content.includes(".ui(") || content.includes(".ui(hypen")) {
1194
+ try {
1195
+ const moduleExport = await import(modulePath);
1196
+ const module = moduleExport.default;
1197
+ if (module && typeof module === "object" && module.template) {
1198
+ addSingleFileComponent(baseName, modulePath, module.template);
1199
+ }
1200
+ } catch (e) {
1201
+ log4(`Failed to import potential single-file component: ${entry.name}`, e);
1202
+ }
1203
+ }
1204
+ }
1205
+ };
1206
+ if (patterns.includes("folder") || patterns.includes("index")) {
1207
+ scanForFolderComponents(resolvedDir);
1208
+ }
1209
+ if (patterns.includes("sibling")) {
1210
+ scanForSiblingComponents(resolvedDir);
1211
+ }
1212
+ if (patterns.includes("single-file")) {
1213
+ await scanForSingleFileComponents(resolvedDir);
1214
+ }
1215
+ log4(`Discovered ${components.length} components`);
1216
+ return components;
1217
+ }
1218
+ async function loadDiscoveredComponents(components) {
1219
+ const { app: app2 } = await Promise.resolve().then(() => (init_app(), exports_app));
1220
+ const loaded = new Map;
1221
+ for (const component of components) {
1222
+ let module;
1223
+ let template = component.template;
1224
+ if (component.modulePath) {
1225
+ const moduleExport = await import(component.modulePath);
1226
+ module = moduleExport.default;
1227
+ if (component.isSingleFile && module.template) {
1228
+ template = module.template;
1229
+ }
1230
+ } else {
1231
+ module = app2.defineState({}).build();
1232
+ }
1233
+ loaded.set(component.name, {
1234
+ name: component.name,
1235
+ module,
1236
+ template
1237
+ });
1238
+ }
1239
+ return loaded;
1240
+ }
1241
+ function watchComponents(baseDir, options = {}) {
1242
+ const resolvedDir = resolve(baseDir);
1243
+ const {
1244
+ onChange,
1245
+ onAdd,
1246
+ onRemove,
1247
+ onUpdate,
1248
+ debug = false,
1249
+ ...discoveryOptions
1250
+ } = options;
1251
+ const log4 = debug ? (...args) => frameworkLoggers.discovery.debug("[watch]", ...args) : () => {};
1252
+ let currentComponents = new Map;
1253
+ let debounceTimer = null;
1254
+ const initialScan = async () => {
1255
+ const components = await discoverComponents(resolvedDir, discoveryOptions);
1256
+ currentComponents = new Map(components.map((c) => [c.name, c]));
1257
+ onChange?.(components);
1258
+ };
1259
+ const rescan = async () => {
1260
+ if (debounceTimer) {
1261
+ clearTimeout(debounceTimer);
1262
+ }
1263
+ debounceTimer = setTimeout(async () => {
1264
+ log4("Rescanning...");
1265
+ const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
1266
+ const newMap = new Map(newComponents.map((c) => [c.name, c]));
1267
+ for (const [name, component] of newMap) {
1268
+ const existing = currentComponents.get(name);
1269
+ if (!existing) {
1270
+ log4("Added:", name);
1271
+ onAdd?.(component);
1272
+ } else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
1273
+ log4("Updated:", name);
1274
+ onUpdate?.(component);
1275
+ }
1276
+ }
1277
+ for (const name of currentComponents.keys()) {
1278
+ if (!newMap.has(name)) {
1279
+ log4("Removed:", name);
1280
+ onRemove?.(name);
1281
+ }
1282
+ }
1283
+ currentComponents = newMap;
1284
+ onChange?.(newComponents);
1285
+ }, 100);
1286
+ };
1287
+ const watcher = watch(resolvedDir, { recursive: true }, (event, filename) => {
1288
+ if (!filename)
1289
+ return;
1290
+ if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
1291
+ log4("File changed:", filename);
1292
+ rescan();
1293
+ }
1294
+ });
1295
+ initialScan();
1296
+ return {
1297
+ stop: () => {
1298
+ watcher.close();
1299
+ if (debounceTimer) {
1300
+ clearTimeout(debounceTimer);
1301
+ }
1302
+ }
1303
+ };
1304
+ }
1305
+ async function generateComponentsCode(baseDir, options = {}) {
1306
+ const components = await discoverComponents(baseDir, options);
1307
+ const resolvedDir = resolve(baseDir);
1308
+ const outputBase = options.outputDir ? resolve(options.outputDir) : resolvedDir;
1309
+ let code = `/**
1310
+ * Auto-generated component imports
1311
+ * Generated by @hypen-space/core discovery
1312
+ */
1313
+
1314
+ `;
1315
+ for (const component of components) {
1316
+ let importPath = null;
1317
+ if (component.modulePath) {
1318
+ const rel = relative(outputBase, component.modulePath).replace(/\.ts$/, ".js");
1319
+ importPath = rel.startsWith(".") ? rel : "./" + rel;
1320
+ }
1321
+ if (importPath) {
1322
+ code += `import ${component.name}Module from "${importPath}";
1323
+ `;
1324
+ }
1325
+ }
1326
+ code += `
1327
+ import { app } from "@hypen-space/core";
1328
+
1329
+ `;
1330
+ for (const component of components) {
1331
+ if (component.isSingleFile) {
1332
+ code += `export const ${component.name} = {
1333
+ module: ${component.name}Module,
1334
+ template: ${component.name}Module.template,
1335
+ };
1336
+
1337
+ `;
1338
+ } else if (component.hasModule) {
1339
+ const templateJson = JSON.stringify(component.template);
1340
+ code += `export const ${component.name} = {
1341
+ module: ${component.name}Module,
1342
+ template: ${templateJson},
1343
+ };
1344
+
1345
+ `;
1346
+ } else {
1347
+ const templateJson = JSON.stringify(component.template);
1348
+ code += `const ${component.name}Module = app.defineState({}).build();
1349
+ export const ${component.name} = {
1350
+ module: ${component.name}Module,
1351
+ template: ${templateJson},
1352
+ };
1353
+
1354
+ `;
1355
+ }
1356
+ }
1357
+ return code;
1358
+ }
1359
+
959
1360
  // src/remote/server.ts
960
1361
  init_app();
961
1362
 
@@ -1091,6 +1492,8 @@ class RemoteServer {
1091
1492
  _module = null;
1092
1493
  _moduleName = "App";
1093
1494
  _ui = "";
1495
+ _sourceDir = null;
1496
+ _discoveredComponents = new Map;
1094
1497
  _config = {};
1095
1498
  _sessionConfig = {};
1096
1499
  _onConnectionCallbacks = [];
@@ -1108,6 +1511,10 @@ class RemoteServer {
1108
1511
  this._ui = dsl;
1109
1512
  return this;
1110
1513
  }
1514
+ source(dir) {
1515
+ this._sourceDir = dir;
1516
+ return this;
1517
+ }
1111
1518
  config(config2) {
1112
1519
  this._config = { ...this._config, ...config2 };
1113
1520
  return this;
@@ -1124,12 +1531,15 @@ class RemoteServer {
1124
1531
  this._onDisconnectionCallbacks.push(callback);
1125
1532
  return this;
1126
1533
  }
1127
- listen(port) {
1534
+ async listen(port) {
1128
1535
  if (!this._module) {
1129
1536
  throw new Error("Module not set. Call .module() before .listen()");
1130
1537
  }
1538
+ if (this._sourceDir) {
1539
+ await this.discoverFromSource();
1540
+ }
1131
1541
  if (!this._ui) {
1132
- throw new Error("UI not set. Call .ui() before .listen()");
1542
+ throw new Error("UI not set. Call .ui() or .source() before .listen()");
1133
1543
  }
1134
1544
  this.sessionManager = new SessionManager(this._sessionConfig);
1135
1545
  const finalPort = port ?? this._config.port ?? 3000;
@@ -1183,12 +1593,42 @@ class RemoteServer {
1183
1593
  const port = this._config.port ?? 3000;
1184
1594
  return `ws://${hostname}:${port}`;
1185
1595
  }
1596
+ async discoverFromSource() {
1597
+ const dir = this._sourceDir;
1598
+ log4.info(`Discovering components from ${dir}...`);
1599
+ const discovered = await discoverComponents(dir);
1600
+ const loaded = await loadDiscoveredComponents(discovered);
1601
+ for (const [name, { module, template }] of loaded) {
1602
+ this._discoveredComponents.set(name, { template, module });
1603
+ }
1604
+ log4.info(`Discovered ${this._discoveredComponents.size} components: ${Array.from(this._discoveredComponents.keys()).join(", ")}`);
1605
+ if (!this._ui) {
1606
+ const entry = this._discoveredComponents.get(this._moduleName);
1607
+ if (entry) {
1608
+ this._ui = entry.template;
1609
+ }
1610
+ }
1611
+ }
1612
+ setupComponentResolver(engine) {
1613
+ if (this._discoveredComponents.size === 0)
1614
+ return;
1615
+ engine.setComponentResolver((componentName, _contextPath) => {
1616
+ const comp = this._discoveredComponents.get(componentName);
1617
+ if (!comp)
1618
+ return null;
1619
+ return {
1620
+ source: comp.template,
1621
+ path: componentName
1622
+ };
1623
+ });
1624
+ }
1186
1625
  async handleOpen(ws) {
1187
1626
  try {
1188
1627
  const clientId = `client_${this.nextClientId++}`;
1189
1628
  const connectedAt = new Date;
1190
1629
  const engine = new Engine;
1191
1630
  await engine.init();
1631
+ this.setupComponentResolver(engine);
1192
1632
  const moduleInstance = new HypenModuleInstance(engine, this._module);
1193
1633
  const clientData = {
1194
1634
  id: clientId,
@@ -1436,8 +1876,14 @@ class RemoteServer {
1436
1876
  });
1437
1877
  }
1438
1878
  }
1439
- function serve(options) {
1440
- const server = new RemoteServer().module(options.moduleName ?? "App", options.module).ui(options.ui);
1879
+ async function serve(options) {
1880
+ const server = new RemoteServer().module(options.moduleName ?? "App", options.module);
1881
+ if (options.source) {
1882
+ server.source(options.source);
1883
+ }
1884
+ if (options.ui) {
1885
+ server.ui(options.ui);
1886
+ }
1441
1887
  if (options.port || options.hostname) {
1442
1888
  server.config({
1443
1889
  port: options.port,
@@ -1460,4 +1906,4 @@ export {
1460
1906
  RemoteServer
1461
1907
  };
1462
1908
 
1463
- //# debugId=1E9F674E53CBE95764756E2164756E21
1909
+ //# debugId=A4EC0E36B723B34E64756E2164756E21