@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.
- package/README.md +1 -1
- package/dist/app.d.ts +379 -0
- package/dist/app.js +111 -18
- package/dist/app.js.map +4 -4
- package/dist/components/builtin.d.ts +50 -0
- package/dist/components/builtin.js +114 -21
- package/dist/components/builtin.js.map +5 -5
- package/dist/context.d.ts +90 -0
- package/dist/discovery.d.ts +90 -0
- package/dist/discovery.js +121 -23
- package/dist/discovery.js.map +5 -5
- package/dist/disposable.d.ts +122 -0
- package/dist/engine.browser.d.ts +101 -0
- package/dist/engine.browser.js +192 -5
- package/dist/engine.browser.js.map +5 -4
- package/dist/engine.d.ts +85 -0
- package/dist/engine.js +192 -5
- package/dist/engine.js.map +5 -4
- package/dist/events.d.ts +78 -0
- package/dist/hypen.d.ts +127 -0
- package/dist/index.browser.d.ts +25 -0
- package/dist/index.browser.js +116 -19
- package/dist/index.browser.js.map +6 -6
- package/dist/index.d.ts +76 -0
- package/dist/index.js +373 -1235
- package/dist/index.js.map +10 -15
- package/dist/loader.d.ts +51 -0
- package/dist/logger.d.ts +141 -0
- package/dist/managed-router.d.ts +59 -0
- package/dist/plugin.d.ts +39 -0
- package/dist/remote/client.d.ts +154 -0
- package/dist/remote/client.js +34 -2
- package/dist/remote/client.js.map +3 -3
- package/dist/remote/index.d.ts +13 -0
- package/dist/remote/index.js +472 -26
- package/dist/remote/index.js.map +8 -7
- package/dist/remote/server.d.ts +173 -0
- package/dist/remote/server.js +472 -26
- package/dist/remote/server.js.map +8 -7
- package/dist/remote/session.d.ts +115 -0
- package/dist/remote/types.d.ts +98 -0
- package/dist/renderer.d.ts +54 -0
- package/dist/renderer.js +6 -2
- package/dist/renderer.js.map +3 -3
- package/dist/resolver.d.ts +95 -0
- package/dist/result.d.ts +134 -0
- package/dist/retry.d.ts +150 -0
- package/dist/router.d.ts +94 -0
- package/dist/state.d.ts +42 -0
- package/dist/types.d.ts +30 -0
- package/package.json +2 -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 +5 -3
- 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
|
-
|
|
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
|
}
|
|
@@ -895,7 +988,11 @@ class BaseRenderer {
|
|
|
895
988
|
|
|
896
989
|
class ConsoleRenderer {
|
|
897
990
|
applyPatches(patches) {
|
|
898
|
-
|
|
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/
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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
|
-
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
engine.renderLazyComponent(source);
|
|
1987
|
+
getSourcePath(source) {
|
|
1988
|
+
return source.type === "local" ? source.path : source.url;
|
|
1852
1989
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
engine.renderInto(source, parentNodeId, unwrapForWasm(state));
|
|
1990
|
+
clearCache() {
|
|
1991
|
+
this.cache.clear();
|
|
1856
1992
|
}
|
|
1857
|
-
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1863
|
-
log9.debug("State changed (sparse):", paths);
|
|
2013
|
+
return imports;
|
|
1864
2014
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2030
|
+
const router = context.router;
|
|
2031
|
+
if (!router) {
|
|
2032
|
+
log9.error("Router not found in context");
|
|
2033
|
+
return;
|
|
1875
2034
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
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/
|
|
2095
|
+
// src/index.ts
|
|
1905
2096
|
init_app();
|
|
1906
2097
|
|
|
1907
|
-
// src/
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
2357
|
+
//# debugId=94AD8348A299084864756E2164756E21
|