@hypen-space/core 0.2.11 → 0.3.0

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 (48) hide show
  1. package/README.md +182 -11
  2. package/dist/src/app.js +470 -44
  3. package/dist/src/app.js.map +7 -5
  4. package/dist/src/components/builtin.js +470 -44
  5. package/dist/src/components/builtin.js.map +7 -5
  6. package/dist/src/discovery.js +559 -65
  7. package/dist/src/discovery.js.map +8 -6
  8. package/dist/src/engine.browser.js +2 -2
  9. package/dist/src/engine.browser.js.map +2 -2
  10. package/dist/src/engine.js +18 -9
  11. package/dist/src/engine.js.map +3 -3
  12. package/dist/src/index.browser.js +863 -82
  13. package/dist/src/index.browser.js.map +11 -7
  14. package/dist/src/index.js +1591 -125
  15. package/dist/src/index.js.map +17 -10
  16. package/dist/src/remote/client.js +525 -35
  17. package/dist/src/remote/client.js.map +7 -4
  18. package/dist/src/remote/index.js +1796 -35
  19. package/dist/src/remote/index.js.map +13 -4
  20. package/dist/src/router.js +55 -29
  21. package/dist/src/router.js.map +3 -3
  22. package/dist/src/state.js +57 -29
  23. package/dist/src/state.js.map +3 -3
  24. package/package.json +8 -2
  25. package/src/app.ts +292 -13
  26. package/src/discovery.ts +123 -18
  27. package/src/disposable.ts +281 -0
  28. package/src/engine.browser.ts +1 -1
  29. package/src/engine.ts +29 -10
  30. package/src/hypen.ts +209 -0
  31. package/src/index.ts +147 -11
  32. package/src/logger.ts +338 -0
  33. package/src/remote/client.ts +263 -56
  34. package/src/remote/index.ts +25 -1
  35. package/src/remote/server.ts +652 -0
  36. package/src/remote/session.ts +256 -0
  37. package/src/remote/types.ts +68 -1
  38. package/src/result.ts +260 -0
  39. package/src/retry.ts +306 -0
  40. package/src/state.ts +103 -45
  41. package/wasm-browser/README.md +4 -0
  42. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  43. package/wasm-browser/package.json +1 -1
  44. package/wasm-node/README.md +4 -0
  45. package/wasm-node/hypen_engine_bg.wasm +0 -0
  46. package/wasm-node/package.json +1 -1
  47. package/wasm-browser/hypen_engine_bg.js +0 -736
  48. package/wasm-node/hypen_engine_bg.js +0 -736
@@ -15,39 +15,34 @@ function deepClone(obj) {
15
15
  if (obj === null || typeof obj !== "object") {
16
16
  return obj;
17
17
  }
18
+ if (typeof obj === "function") {
19
+ return obj;
20
+ }
21
+ if (typeof obj.__getSnapshot === "function") {
22
+ return obj.__getSnapshot();
23
+ }
24
+ if (obj instanceof WeakMap || obj instanceof WeakSet) {
25
+ return obj;
26
+ }
18
27
  const visited = new WeakMap;
19
28
  function cloneInternal(value) {
20
29
  if (value === null || typeof value !== "object") {
21
30
  return value;
22
31
  }
32
+ if (typeof value === "function") {
33
+ return value;
34
+ }
23
35
  if (visited.has(value)) {
24
36
  return visited.get(value);
25
37
  }
26
- if (value instanceof Date) {
27
- return new Date(value.getTime());
28
- }
29
- if (value instanceof RegExp) {
30
- return new RegExp(value.source, value.flags);
31
- }
32
- if (value instanceof Map) {
33
- const mapClone = new Map;
34
- visited.set(value, mapClone);
35
- for (const [k, v] of value.entries()) {
36
- mapClone.set(cloneInternal(k), cloneInternal(v));
37
- }
38
- return mapClone;
39
- }
40
- if (value instanceof Set) {
41
- const setClone = new Set;
42
- visited.set(value, setClone);
43
- for (const item of value.values()) {
44
- setClone.add(cloneInternal(item));
45
- }
46
- return setClone;
47
- }
48
38
  if (value instanceof WeakMap || value instanceof WeakSet) {
49
39
  return value;
50
40
  }
41
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
42
+ try {
43
+ return structuredClone(value);
44
+ } catch {}
45
+ }
51
46
  if (Array.isArray(value)) {
52
47
  const arrClone = [];
53
48
  visited.set(value, arrClone);
@@ -59,7 +54,7 @@ function deepClone(obj) {
59
54
  const objClone = {};
60
55
  visited.set(value, objClone);
61
56
  for (const key in value) {
62
- if (value.hasOwnProperty(key)) {
57
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
63
58
  objClone[key] = cloneInternal(value[key]);
64
59
  }
65
60
  }
@@ -161,12 +156,15 @@ function createObservableState(initialState, options) {
161
156
  }
162
157
  const proxyCache = new WeakMap;
163
158
  function createProxy(target, basePath) {
164
- if (proxyCache.has(target)) {
165
- return proxyCache.get(target);
166
- }
159
+ const cached = proxyCache.get(target);
160
+ if (cached)
161
+ return cached;
167
162
  const proxy = new Proxy(target, {
168
163
  get(obj, prop) {
169
- const value = obj[prop];
164
+ if (prop === IS_PROXY)
165
+ return true;
166
+ if (prop === RAW_TARGET)
167
+ return obj;
170
168
  if (prop === "__beginBatch") {
171
169
  return () => {
172
170
  batchDepth++;
@@ -183,16 +181,28 @@ function createObservableState(initialState, options) {
183
181
  if (prop === "__getSnapshot") {
184
182
  return () => deepClone(obj);
185
183
  }
184
+ const value = obj[prop];
186
185
  if (value && typeof value === "object") {
186
+ if (value[IS_PROXY]) {
187
+ return value;
188
+ }
187
189
  if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
188
190
  return value;
189
191
  }
190
- return createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
192
+ const cachedNested = proxyCache.get(value);
193
+ if (cachedNested) {
194
+ return cachedNested;
195
+ }
196
+ const nestedProxy = createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
197
+ return nestedProxy;
191
198
  }
192
199
  return value;
193
200
  },
194
201
  set(obj, prop, value) {
195
202
  const oldValue = obj[prop];
203
+ if (value && typeof value === "object" && value[IS_PROXY]) {
204
+ value = value[RAW_TARGET];
205
+ }
196
206
  obj[prop] = value;
197
207
  if (oldValue !== value) {
198
208
  scheduleBatch();
@@ -233,6 +243,344 @@ function getStateSnapshot(state) {
233
243
  }
234
244
  return deepClone(state);
235
245
  }
246
+ function isStateProxy(value) {
247
+ return value !== null && typeof value === "object" && value[IS_PROXY] === true;
248
+ }
249
+ function unwrapProxy(value) {
250
+ if (value !== null && typeof value === "object" && value[IS_PROXY]) {
251
+ return value[RAW_TARGET];
252
+ }
253
+ return value;
254
+ }
255
+ var IS_PROXY, RAW_TARGET;
256
+ var init_state = __esm(() => {
257
+ IS_PROXY = Symbol.for("hypen.isProxy");
258
+ RAW_TARGET = Symbol.for("hypen.rawTarget");
259
+ });
260
+
261
+ // src/result.ts
262
+ function Ok(value) {
263
+ return { ok: true, value };
264
+ }
265
+ function Err(error) {
266
+ return { ok: false, error };
267
+ }
268
+ function isOk(result) {
269
+ return result.ok;
270
+ }
271
+ function isErr(result) {
272
+ return !result.ok;
273
+ }
274
+ async function fromPromise(promise, mapError) {
275
+ try {
276
+ const value = await promise;
277
+ return Ok(value);
278
+ } catch (e) {
279
+ if (mapError) {
280
+ return Err(mapError(e));
281
+ }
282
+ return Err(e);
283
+ }
284
+ }
285
+ function fromTry(fn, mapError) {
286
+ try {
287
+ return Ok(fn());
288
+ } catch (e) {
289
+ if (mapError) {
290
+ return Err(mapError(e));
291
+ }
292
+ return Err(e);
293
+ }
294
+ }
295
+ function map(result, fn) {
296
+ if (result.ok) {
297
+ return Ok(fn(result.value));
298
+ }
299
+ return result;
300
+ }
301
+ function mapErr(result, fn) {
302
+ if (!result.ok) {
303
+ return Err(fn(result.error));
304
+ }
305
+ return result;
306
+ }
307
+ function flatMap(result, fn) {
308
+ if (result.ok) {
309
+ return fn(result.value);
310
+ }
311
+ return result;
312
+ }
313
+ function unwrap(result) {
314
+ if (result.ok) {
315
+ return result.value;
316
+ }
317
+ throw result.error;
318
+ }
319
+ function unwrapOr(result, defaultValue) {
320
+ if (result.ok) {
321
+ return result.value;
322
+ }
323
+ return defaultValue;
324
+ }
325
+ function unwrapOrElse(result, fn) {
326
+ if (result.ok) {
327
+ return result.value;
328
+ }
329
+ return fn(result.error);
330
+ }
331
+ function match(result, handlers) {
332
+ if (result.ok) {
333
+ return handlers.ok(result.value);
334
+ }
335
+ return handlers.err(result.error);
336
+ }
337
+ function all(results) {
338
+ const values = [];
339
+ for (const result of results) {
340
+ if (!result.ok) {
341
+ return result;
342
+ }
343
+ values.push(result.value);
344
+ }
345
+ return Ok(values);
346
+ }
347
+ var HypenError, ActionError, ConnectionError, StateError;
348
+ var init_result = __esm(() => {
349
+ HypenError = class HypenError extends Error {
350
+ code;
351
+ context;
352
+ cause;
353
+ constructor(code, message, options) {
354
+ super(message);
355
+ this.name = "HypenError";
356
+ this.code = code;
357
+ this.context = options?.context;
358
+ this.cause = options?.cause;
359
+ Object.setPrototypeOf(this, new.target.prototype);
360
+ }
361
+ };
362
+ ActionError = class ActionError extends HypenError {
363
+ actionName;
364
+ constructor(actionName, cause) {
365
+ super("ACTION_ERROR", `Action handler "${actionName}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
366
+ context: { actionName },
367
+ cause: cause instanceof Error ? cause : undefined
368
+ });
369
+ this.name = "ActionError";
370
+ this.actionName = actionName;
371
+ }
372
+ };
373
+ ConnectionError = class ConnectionError extends HypenError {
374
+ url;
375
+ attempt;
376
+ constructor(url, cause, attempt) {
377
+ super("CONNECTION_ERROR", `Connection to "${url}" failed${attempt ? ` (attempt ${attempt})` : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, {
378
+ context: { url, attempt },
379
+ cause: cause instanceof Error ? cause : undefined
380
+ });
381
+ this.name = "ConnectionError";
382
+ this.url = url;
383
+ this.attempt = attempt;
384
+ }
385
+ };
386
+ StateError = class StateError extends HypenError {
387
+ path;
388
+ constructor(message, path, cause) {
389
+ super("STATE_ERROR", message, {
390
+ context: { path },
391
+ cause: cause instanceof Error ? cause : undefined
392
+ });
393
+ this.name = "StateError";
394
+ this.path = path;
395
+ }
396
+ };
397
+ });
398
+
399
+ // src/logger.ts
400
+ function isProduction() {
401
+ if (typeof process !== "undefined" && process.env) {
402
+ return false;
403
+ }
404
+ return false;
405
+ }
406
+ function setLogLevel(level) {
407
+ config.level = level;
408
+ }
409
+ function getLogLevel() {
410
+ return config.level;
411
+ }
412
+ function configureLogger(options) {
413
+ config = { ...config, ...options };
414
+ }
415
+ function enableLogging() {
416
+ config.level = "debug";
417
+ }
418
+ function disableLogging() {
419
+ config.level = "none";
420
+ }
421
+ function shouldLog(level) {
422
+ return LOG_LEVEL_ORDER[level] >= LOG_LEVEL_ORDER[config.level];
423
+ }
424
+ function formatTag(tag, level) {
425
+ const timestamp = config.timestamps ? `${new Date().toISOString()} ` : "";
426
+ if (config.colors && level !== "none") {
427
+ const color = LOG_LEVEL_COLORS[level];
428
+ return `${timestamp}${color}[${tag}]${RESET_COLOR}`;
429
+ }
430
+ return `${timestamp}[${tag}]`;
431
+ }
432
+
433
+ class Logger {
434
+ tag;
435
+ constructor(tag) {
436
+ this.tag = tag;
437
+ }
438
+ debug(...args) {
439
+ if (!shouldLog("debug"))
440
+ return;
441
+ if (config.handler) {
442
+ config.handler.debug(this.tag, ...args);
443
+ } else {
444
+ console.log(formatTag(this.tag, "debug"), ...args);
445
+ }
446
+ }
447
+ info(...args) {
448
+ if (!shouldLog("info"))
449
+ return;
450
+ if (config.handler) {
451
+ config.handler.info(this.tag, ...args);
452
+ } else {
453
+ console.info(formatTag(this.tag, "info"), ...args);
454
+ }
455
+ }
456
+ warn(...args) {
457
+ if (!shouldLog("warn"))
458
+ return;
459
+ if (config.handler) {
460
+ config.handler.warn(this.tag, ...args);
461
+ } else {
462
+ console.warn(formatTag(this.tag, "warn"), ...args);
463
+ }
464
+ }
465
+ error(...args) {
466
+ if (!shouldLog("error"))
467
+ return;
468
+ if (config.handler) {
469
+ config.handler.error(this.tag, ...args);
470
+ } else {
471
+ console.error(formatTag(this.tag, "error"), ...args);
472
+ }
473
+ }
474
+ time(label, fn) {
475
+ if (!shouldLog("debug")) {
476
+ return fn();
477
+ }
478
+ const start = performance.now();
479
+ try {
480
+ return fn();
481
+ } finally {
482
+ const duration = performance.now() - start;
483
+ this.debug(`${label}: ${duration.toFixed(2)}ms`);
484
+ }
485
+ }
486
+ async timeAsync(label, fn) {
487
+ if (!shouldLog("debug")) {
488
+ return fn();
489
+ }
490
+ const start = performance.now();
491
+ try {
492
+ return await fn();
493
+ } finally {
494
+ const duration = performance.now() - start;
495
+ this.debug(`${label}: ${duration.toFixed(2)}ms`);
496
+ }
497
+ }
498
+ child(subTag) {
499
+ return new Logger(`${this.tag}:${subTag}`);
500
+ }
501
+ debugIf(condition, ...args) {
502
+ if (condition)
503
+ this.debug(...args);
504
+ }
505
+ warnIf(condition, ...args) {
506
+ if (condition)
507
+ this.warn(...args);
508
+ }
509
+ errorIf(condition, ...args) {
510
+ if (condition)
511
+ this.error(...args);
512
+ }
513
+ loggedOnce = new Set;
514
+ warnOnce(key, ...args) {
515
+ if (this.loggedOnce.has(key))
516
+ return;
517
+ this.loggedOnce.add(key);
518
+ this.warn(...args);
519
+ }
520
+ debugOnce(key, ...args) {
521
+ if (this.loggedOnce.has(key))
522
+ return;
523
+ this.loggedOnce.add(key);
524
+ this.debug(...args);
525
+ }
526
+ }
527
+ function createLogger(tag) {
528
+ return new Logger(tag);
529
+ }
530
+ var LOG_LEVEL_ORDER, LOG_LEVEL_COLORS, RESET_COLOR = "\x1B[0m", config, logger, log, frameworkLoggers;
531
+ var init_logger = __esm(() => {
532
+ LOG_LEVEL_ORDER = {
533
+ debug: 0,
534
+ info: 1,
535
+ warn: 2,
536
+ error: 3,
537
+ none: 4
538
+ };
539
+ LOG_LEVEL_COLORS = {
540
+ debug: "\x1B[36m",
541
+ info: "\x1B[32m",
542
+ warn: "\x1B[33m",
543
+ error: "\x1B[31m"
544
+ };
545
+ config = {
546
+ level: isProduction() ? "error" : "debug",
547
+ colors: true,
548
+ timestamps: false
549
+ };
550
+ logger = createLogger("Hypen");
551
+ log = {
552
+ debug: (tag, ...args) => {
553
+ if (!shouldLog("debug"))
554
+ return;
555
+ console.log(formatTag(tag, "debug"), ...args);
556
+ },
557
+ info: (tag, ...args) => {
558
+ if (!shouldLog("info"))
559
+ return;
560
+ console.info(formatTag(tag, "info"), ...args);
561
+ },
562
+ warn: (tag, ...args) => {
563
+ if (!shouldLog("warn"))
564
+ return;
565
+ console.warn(formatTag(tag, "warn"), ...args);
566
+ },
567
+ error: (tag, ...args) => {
568
+ if (!shouldLog("error"))
569
+ return;
570
+ console.error(formatTag(tag, "error"), ...args);
571
+ }
572
+ };
573
+ frameworkLoggers = {
574
+ engine: createLogger("Engine"),
575
+ router: createLogger("Router"),
576
+ state: createLogger("State"),
577
+ events: createLogger("Events"),
578
+ remote: createLogger("Remote"),
579
+ renderer: createLogger("Renderer"),
580
+ module: createLogger("Module"),
581
+ lifecycle: createLogger("Lifecycle")
582
+ };
583
+ });
236
584
 
237
585
  // src/app.ts
238
586
  var exports_app = {};
@@ -249,6 +597,11 @@ class HypenAppBuilder {
249
597
  createdHandler;
250
598
  actionHandlers = new Map;
251
599
  destroyedHandler;
600
+ disconnectHandler;
601
+ reconnectHandler;
602
+ expireHandler;
603
+ errorHandler;
604
+ template;
252
605
  constructor(initialState, options) {
253
606
  this.initialState = initialState;
254
607
  this.options = options || {};
@@ -265,6 +618,26 @@ class HypenAppBuilder {
265
618
  this.destroyedHandler = fn;
266
619
  return this;
267
620
  }
621
+ onDisconnect(fn) {
622
+ this.disconnectHandler = fn;
623
+ return this;
624
+ }
625
+ onReconnect(fn) {
626
+ this.reconnectHandler = fn;
627
+ return this;
628
+ }
629
+ onExpire(fn) {
630
+ this.expireHandler = fn;
631
+ return this;
632
+ }
633
+ onError(fn) {
634
+ this.errorHandler = fn;
635
+ return this;
636
+ }
637
+ ui(template) {
638
+ this.template = template;
639
+ return this.build();
640
+ }
268
641
  build() {
269
642
  const stateKeys = this.initialState !== null && typeof this.initialState === "object" ? Object.keys(this.initialState) : [];
270
643
  return {
@@ -274,10 +647,15 @@ class HypenAppBuilder {
274
647
  persist: this.options.persist,
275
648
  version: this.options.version,
276
649
  initialState: this.initialState,
650
+ template: this.template,
277
651
  handlers: {
278
652
  onCreated: this.createdHandler,
279
653
  onAction: this.actionHandlers,
280
- onDestroyed: this.destroyedHandler
654
+ onDestroyed: this.destroyedHandler,
655
+ onDisconnect: this.disconnectHandler,
656
+ onReconnect: this.reconnectHandler,
657
+ onExpire: this.expireHandler,
658
+ onError: this.errorHandler
281
659
  }
282
660
  };
283
661
  }
@@ -310,9 +688,9 @@ class HypenModuleInstance {
310
688
  });
311
689
  this.engine.setModule(definition.name || "AnonymousModule", definition.actions, definition.stateKeys, getStateSnapshot(this.state));
312
690
  for (const [actionName, handler] of definition.handlers.onAction) {
313
- console.log(`[ModuleInstance] Registering action handler: ${actionName} for module ${definition.name}`);
691
+ log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
314
692
  this.engine.onAction(actionName, async (action) => {
315
- console.log(`[ModuleInstance] Action handler fired: ${actionName}`, action);
693
+ log2.debug(`Action handler fired: ${actionName}`, action);
316
694
  const actionCtx = {
317
695
  name: action.name,
318
696
  payload: action.payload,
@@ -322,17 +700,19 @@ class HypenModuleInstance {
322
700
  router: this.routerContext?.root || null
323
701
  };
324
702
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
325
- try {
326
- await handler({
327
- action: actionCtx,
328
- state: this.state,
329
- next,
330
- context
331
- });
332
- console.log(`[ModuleInstance] Action handler completed: ${actionName}`);
333
- } catch (error) {
334
- console.error(`[ModuleInstance] Action handler error for ${actionName}:`, error);
335
- throw error;
703
+ const result = await this.executeAction(actionName, handler, {
704
+ action: actionCtx,
705
+ state: this.state,
706
+ next,
707
+ context
708
+ });
709
+ if (!result.ok) {
710
+ const shouldRethrow = await this.handleError(result.error, { actionName });
711
+ if (shouldRethrow) {
712
+ throw result.error;
713
+ }
714
+ } else {
715
+ log2.debug(`Action handler completed: ${actionName}`);
336
716
  }
337
717
  });
338
718
  }
@@ -359,6 +739,48 @@ class HypenModuleInstance {
359
739
  }
360
740
  return api;
361
741
  }
742
+ async executeAction(actionName, handler, ctx) {
743
+ try {
744
+ const result = handler(ctx);
745
+ await result;
746
+ return Ok(undefined);
747
+ } catch (e) {
748
+ return Err(new ActionError(actionName, e));
749
+ }
750
+ }
751
+ async handleError(error, context) {
752
+ const errorCtx = {
753
+ error,
754
+ state: this.state,
755
+ actionName: context.actionName,
756
+ lifecycle: context.lifecycle
757
+ };
758
+ if (this.definition.handlers.onError) {
759
+ try {
760
+ const result = await this.definition.handlers.onError(errorCtx);
761
+ if (result && typeof result === "object") {
762
+ if ("handled" in result && result.handled) {
763
+ return false;
764
+ }
765
+ if ("rethrow" in result && result.rethrow) {
766
+ return true;
767
+ }
768
+ }
769
+ } catch (handlerError) {
770
+ log2.error("Error in onError handler:", handlerError);
771
+ }
772
+ }
773
+ if (this.globalContext) {
774
+ const eventContext = context.actionName ? `action:${context.actionName}` : context.lifecycle ? `lifecycle:${context.lifecycle}` : "unknown";
775
+ this.globalContext.emit("error", {
776
+ message: error.message,
777
+ error,
778
+ context: eventContext
779
+ });
780
+ }
781
+ log2.error(`${context.actionName ? `Action "${context.actionName}"` : `Lifecycle "${context.lifecycle}"`} error:`, error);
782
+ return false;
783
+ }
362
784
  async callCreatedHandler() {
363
785
  if (this.definition.handlers.onCreated) {
364
786
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
@@ -386,8 +808,12 @@ class HypenModuleInstance {
386
808
  Object.assign(this.state, patch);
387
809
  }
388
810
  }
389
- var app;
811
+ var log2, app;
390
812
  var init_app = __esm(() => {
813
+ init_result();
814
+ init_state();
815
+ init_logger();
816
+ log2 = createLogger("ModuleInstance");
391
817
  app = new HypenApp;
392
818
  });
393
819
 
@@ -399,19 +825,19 @@ function removeImports(text) {
399
825
  }
400
826
  async function discoverComponents(baseDir, options = {}) {
401
827
  const {
402
- patterns = ["folder", "sibling", "index"],
828
+ patterns = ["single-file", "folder", "sibling", "index"],
403
829
  recursive = false,
404
830
  debug = false
405
831
  } = options;
406
- const log = debug ? (...args) => console.log("[discovery]", ...args) : () => {};
832
+ const log3 = debug ? (...args) => console.log("[discovery]", ...args) : () => {};
407
833
  const resolvedDir = resolve(baseDir);
408
834
  const components = [];
409
835
  const seen = new Set;
410
- log("Scanning directory:", resolvedDir);
411
- log("Patterns:", patterns);
412
- const addComponent = (name, hypenPath, modulePath) => {
836
+ log3("Scanning directory:", resolvedDir);
837
+ log3("Patterns:", patterns);
838
+ const addTwoFileComponent = (name, hypenPath, modulePath) => {
413
839
  if (seen.has(name)) {
414
- log(`Skipping duplicate: ${name}`);
840
+ log3(`Skipping duplicate: ${name}`);
415
841
  return;
416
842
  }
417
843
  seen.add(name);
@@ -422,9 +848,26 @@ async function discoverComponents(baseDir, options = {}) {
422
848
  hypenPath,
423
849
  modulePath,
424
850
  template,
425
- hasModule: modulePath !== null
851
+ hasModule: modulePath !== null,
852
+ isSingleFile: false
426
853
  });
427
- log(`Found: ${name} (${modulePath ? "with module" : "stateless"})`);
854
+ log3(`Found: ${name} (two-file, ${modulePath ? "with module" : "stateless"})`);
855
+ };
856
+ const addSingleFileComponent = (name, modulePath, template) => {
857
+ if (seen.has(name)) {
858
+ log3(`Skipping duplicate: ${name}`);
859
+ return;
860
+ }
861
+ seen.add(name);
862
+ components.push({
863
+ name,
864
+ hypenPath: null,
865
+ modulePath,
866
+ template,
867
+ hasModule: true,
868
+ isSingleFile: true
869
+ });
870
+ log3(`Found: ${name} (single-file with inline template)`);
428
871
  };
429
872
  const scanForFolderComponents = (dir) => {
430
873
  if (!existsSync(dir))
@@ -439,7 +882,7 @@ async function discoverComponents(baseDir, options = {}) {
439
882
  const hypenPath = join(folderPath, "component.hypen");
440
883
  if (existsSync(hypenPath)) {
441
884
  const modulePath = join(folderPath, "component.ts");
442
- addComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
885
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
443
886
  continue;
444
887
  }
445
888
  }
@@ -447,7 +890,7 @@ async function discoverComponents(baseDir, options = {}) {
447
890
  const hypenPath = join(folderPath, "index.hypen");
448
891
  if (existsSync(hypenPath)) {
449
892
  const modulePath = join(folderPath, "index.ts");
450
- addComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
893
+ addTwoFileComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
451
894
  continue;
452
895
  }
453
896
  }
@@ -474,7 +917,43 @@ async function discoverComponents(baseDir, options = {}) {
474
917
  if (baseName === "component" || baseName === "index")
475
918
  continue;
476
919
  const modulePath = join(dir, `${baseName}.ts`);
477
- addComponent(baseName, hypenPath, existsSync(modulePath) ? modulePath : null);
920
+ addTwoFileComponent(baseName, hypenPath, existsSync(modulePath) ? modulePath : null);
921
+ }
922
+ };
923
+ const scanForSingleFileComponents = async (dir) => {
924
+ if (!existsSync(dir))
925
+ return;
926
+ const entries = readdirSync(dir, { withFileTypes: true });
927
+ for (const entry of entries) {
928
+ if (entry.isDirectory()) {
929
+ if (recursive) {
930
+ await scanForSingleFileComponents(join(dir, entry.name));
931
+ }
932
+ continue;
933
+ }
934
+ if (!entry.name.endsWith(".ts"))
935
+ continue;
936
+ if (entry.name.startsWith(".") || entry.name.includes(".test.") || entry.name.includes(".spec."))
937
+ continue;
938
+ const baseName = basename(entry.name, ".ts");
939
+ if (baseName === "component" || baseName === "index")
940
+ continue;
941
+ const hypenPath = join(dir, `${baseName}.hypen`);
942
+ if (existsSync(hypenPath))
943
+ continue;
944
+ const modulePath = join(dir, entry.name);
945
+ const content = readFileSync(modulePath, "utf-8");
946
+ if (content.includes(".ui(") || content.includes(".ui(hypen")) {
947
+ try {
948
+ const moduleExport = await import(modulePath);
949
+ const module = moduleExport.default;
950
+ if (module && typeof module === "object" && module.template) {
951
+ addSingleFileComponent(baseName, modulePath, module.template);
952
+ }
953
+ } catch (e) {
954
+ log3(`Failed to import potential single-file component: ${entry.name}`, e);
955
+ }
956
+ }
478
957
  }
479
958
  };
480
959
  if (patterns.includes("folder") || patterns.includes("index")) {
@@ -483,7 +962,10 @@ async function discoverComponents(baseDir, options = {}) {
483
962
  if (patterns.includes("sibling")) {
484
963
  scanForSiblingComponents(resolvedDir);
485
964
  }
486
- log(`Discovered ${components.length} components`);
965
+ if (patterns.includes("single-file")) {
966
+ await scanForSingleFileComponents(resolvedDir);
967
+ }
968
+ log3(`Discovered ${components.length} components`);
487
969
  return components;
488
970
  }
489
971
  async function loadDiscoveredComponents(components) {
@@ -491,16 +973,20 @@ async function loadDiscoveredComponents(components) {
491
973
  const loaded = new Map;
492
974
  for (const component of components) {
493
975
  let module;
976
+ let template = component.template;
494
977
  if (component.modulePath) {
495
978
  const moduleExport = await import(component.modulePath);
496
979
  module = moduleExport.default;
980
+ if (component.isSingleFile && module.template) {
981
+ template = module.template;
982
+ }
497
983
  } else {
498
984
  module = app2.defineState({}).build();
499
985
  }
500
986
  loaded.set(component.name, {
501
987
  name: component.name,
502
988
  module,
503
- template: component.template
989
+ template
504
990
  });
505
991
  }
506
992
  return loaded;
@@ -515,7 +1001,7 @@ function watchComponents(baseDir, options = {}) {
515
1001
  debug = false,
516
1002
  ...discoveryOptions
517
1003
  } = options;
518
- const log = debug ? (...args) => console.log("[discovery:watch]", ...args) : () => {};
1004
+ const log3 = debug ? (...args) => console.log("[discovery:watch]", ...args) : () => {};
519
1005
  let currentComponents = new Map;
520
1006
  let debounceTimer = null;
521
1007
  const initialScan = async () => {
@@ -528,22 +1014,22 @@ function watchComponents(baseDir, options = {}) {
528
1014
  clearTimeout(debounceTimer);
529
1015
  }
530
1016
  debounceTimer = setTimeout(async () => {
531
- log("Rescanning...");
1017
+ log3("Rescanning...");
532
1018
  const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
533
1019
  const newMap = new Map(newComponents.map((c) => [c.name, c]));
534
1020
  for (const [name, component] of newMap) {
535
1021
  const existing = currentComponents.get(name);
536
1022
  if (!existing) {
537
- log("Added:", name);
1023
+ log3("Added:", name);
538
1024
  onAdd?.(component);
539
1025
  } else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
540
- log("Updated:", name);
1026
+ log3("Updated:", name);
541
1027
  onUpdate?.(component);
542
1028
  }
543
1029
  }
544
1030
  for (const name of currentComponents.keys()) {
545
1031
  if (!newMap.has(name)) {
546
- log("Removed:", name);
1032
+ log3("Removed:", name);
547
1033
  onRemove?.(name);
548
1034
  }
549
1035
  }
@@ -555,7 +1041,7 @@ function watchComponents(baseDir, options = {}) {
555
1041
  if (!filename)
556
1042
  return;
557
1043
  if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
558
- log("File changed:", filename);
1044
+ log3("File changed:", filename);
559
1045
  rescan();
560
1046
  }
561
1047
  });
@@ -590,8 +1076,15 @@ import { app } from "@hypen-space/core";
590
1076
 
591
1077
  `;
592
1078
  for (const component of components) {
593
- const templateJson = JSON.stringify(component.template);
594
- if (component.hasModule) {
1079
+ if (component.isSingleFile) {
1080
+ code += `export const ${component.name} = {
1081
+ module: ${component.name}Module,
1082
+ template: ${component.name}Module.template,
1083
+ };
1084
+
1085
+ `;
1086
+ } else if (component.hasModule) {
1087
+ const templateJson = JSON.stringify(component.template);
595
1088
  code += `export const ${component.name} = {
596
1089
  module: ${component.name}Module,
597
1090
  template: ${templateJson},
@@ -599,6 +1092,7 @@ import { app } from "@hypen-space/core";
599
1092
 
600
1093
  `;
601
1094
  } else {
1095
+ const templateJson = JSON.stringify(component.template);
602
1096
  code += `const ${component.name}Module = app.defineState({}).build();
603
1097
  export const ${component.name} = {
604
1098
  module: ${component.name}Module,
@@ -617,4 +1111,4 @@ export {
617
1111
  discoverComponents
618
1112
  };
619
1113
 
620
- //# debugId=310D95D3289A0E7E64756E2164756E21
1114
+ //# debugId=8B91C0622F0CDB2064756E2164756E21