@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.
- package/README.md +1 -1
- package/dist/app.d.ts +79 -22
- package/dist/app.js +111 -18
- package/dist/app.js.map +4 -4
- package/dist/components/builtin.js +114 -21
- package/dist/components/builtin.js.map +5 -5
- package/dist/discovery.d.ts +1 -1
- package/dist/discovery.js +112 -19
- package/dist/discovery.js.map +5 -5
- package/dist/engine.browser.d.ts +6 -0
- package/dist/engine.browser.js +192 -5
- package/dist/engine.browser.js.map +5 -4
- package/dist/engine.d.ts +6 -0
- package/dist/engine.js +192 -5
- package/dist/engine.js.map +5 -4
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +111 -18
- package/dist/index.browser.js.map +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +117 -21
- package/dist/index.js.map +6 -6
- package/dist/managed-router.d.ts +3 -4
- package/dist/remote/client.js +34 -2
- package/dist/remote/client.js.map +3 -3
- package/dist/remote/index.js +472 -26
- package/dist/remote/index.js.map +8 -7
- package/dist/remote/server.d.ts +46 -15
- package/dist/remote/server.js +472 -26
- package/dist/remote/server.js.map +8 -7
- package/dist/result.d.ts +18 -0
- package/package.json +1 -1
- package/src/app.ts +162 -41
- package/src/components/builtin.ts +3 -4
- package/src/discovery.ts +2 -2
- package/src/engine.browser.ts +27 -4
- package/src/engine.ts +27 -4
- package/src/index.browser.ts +0 -2
- package/src/index.ts +3 -2
- package/src/managed-router.ts +5 -6
- package/src/remote/server.ts +116 -21
- package/src/result.ts +48 -0
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/package.json +1 -1
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/package.json +1 -1
- package/dist/module-registry.d.ts +0 -42
- package/src/module-registry.ts +0 -76
package/dist/remote/server.js
CHANGED
|
@@ -361,7 +361,20 @@ function all(results) {
|
|
|
361
361
|
}
|
|
362
362
|
return Ok(values);
|
|
363
363
|
}
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
+
router;
|
|
706
774
|
globalContext;
|
|
707
775
|
stateChangeCallbacks = [];
|
|
708
|
-
constructor(engine, definition,
|
|
776
|
+
constructor(engine, definition, router, globalContext) {
|
|
709
777
|
this.engine = engine;
|
|
710
778
|
this.definition = definition;
|
|
711
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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=
|
|
1909
|
+
//# debugId=A4EC0E36B723B34E64756E2164756E21
|