@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
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { app } from "../app.js";
|
|
7
|
-
import type { HypenRouter } from "../router.js";
|
|
8
7
|
import { frameworkLoggers } from "../logger.js";
|
|
9
8
|
|
|
10
9
|
const log = frameworkLoggers.router;
|
|
@@ -36,7 +35,7 @@ export const Router = app
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
// Subscribe to router changes
|
|
39
|
-
const router =
|
|
38
|
+
const router = context.router;
|
|
40
39
|
if (!router) {
|
|
41
40
|
log.error("Router not found in context");
|
|
42
41
|
return;
|
|
@@ -149,7 +148,7 @@ export const Link = app
|
|
|
149
148
|
{ name: "__Link" }
|
|
150
149
|
)
|
|
151
150
|
.onAction("navigate", ({ state, context }) => {
|
|
152
|
-
const router =
|
|
151
|
+
const router = context?.router;
|
|
153
152
|
if (!router) {
|
|
154
153
|
log.error("Link requires router context");
|
|
155
154
|
return;
|
|
@@ -162,7 +161,7 @@ export const Link = app
|
|
|
162
161
|
if (!context) return;
|
|
163
162
|
|
|
164
163
|
// Check if current path matches this link's target
|
|
165
|
-
const router =
|
|
164
|
+
const router = context.router;
|
|
166
165
|
if (router) {
|
|
167
166
|
router.onNavigate((routeState) => {
|
|
168
167
|
state.isActive = routeState.currentPath === state.to;
|
package/src/discovery.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface DiscoveryOptions {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Recursively scan subdirectories
|
|
46
|
-
* Default:
|
|
46
|
+
* Default: true
|
|
47
47
|
*/
|
|
48
48
|
recursive?: boolean;
|
|
49
49
|
|
|
@@ -95,7 +95,7 @@ export async function discoverComponents(
|
|
|
95
95
|
): Promise<DiscoveredComponent[]> {
|
|
96
96
|
const {
|
|
97
97
|
patterns = ["single-file", "folder", "sibling", "index"],
|
|
98
|
-
recursive =
|
|
98
|
+
recursive = true,
|
|
99
99
|
debug = false,
|
|
100
100
|
} = options;
|
|
101
101
|
|
package/src/engine.browser.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { frameworkLoggers } from "./logger.js";
|
|
7
|
+
import { classifyEngineError } from "./result.js";
|
|
7
8
|
import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
8
9
|
|
|
9
10
|
// Re-export types so existing consumers of "./engine.browser.js" still work
|
|
@@ -125,10 +126,16 @@ export class Engine {
|
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
128
|
* Parse and render Hypen DSL source code
|
|
129
|
+
* @throws {ParseError} if the source fails to parse
|
|
130
|
+
* @throws {RenderError} if rendering fails
|
|
128
131
|
*/
|
|
129
132
|
renderSource(source: string): void {
|
|
130
133
|
const engine = this.ensureInitialized();
|
|
131
|
-
|
|
134
|
+
try {
|
|
135
|
+
engine.renderSource(source);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw classifyEngineError(err);
|
|
138
|
+
}
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
/**
|
|
@@ -141,15 +148,22 @@ export class Engine {
|
|
|
141
148
|
|
|
142
149
|
/**
|
|
143
150
|
* Render a component into a specific parent node (subtree rendering)
|
|
151
|
+
* @throws {ParseError} if the source fails to parse
|
|
152
|
+
* @throws {RenderError} if the parent node is not found or rendering fails
|
|
144
153
|
*/
|
|
145
154
|
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
146
155
|
const engine = this.ensureInitialized();
|
|
147
156
|
const safeState = JSON.parse(JSON.stringify(state));
|
|
148
|
-
|
|
157
|
+
try {
|
|
158
|
+
engine.renderInto(source, parentNodeId, safeState);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw classifyEngineError(err);
|
|
161
|
+
}
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/**
|
|
152
165
|
* Notify the engine of state changes using sparse updates
|
|
166
|
+
* @throws {StateError} if the state patch is invalid
|
|
153
167
|
*/
|
|
154
168
|
notifyStateChange(paths: string[], values: Record<string, any>): void {
|
|
155
169
|
const engine = this.ensureInitialized();
|
|
@@ -159,7 +173,11 @@ export class Engine {
|
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
const plainValues = JSON.parse(JSON.stringify(values));
|
|
162
|
-
|
|
176
|
+
try {
|
|
177
|
+
engine.updateStateSparse(paths, plainValues);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw classifyEngineError(err);
|
|
180
|
+
}
|
|
163
181
|
log.debug("State changed (sparse):", paths);
|
|
164
182
|
}
|
|
165
183
|
|
|
@@ -189,11 +207,16 @@ export class Engine {
|
|
|
189
207
|
|
|
190
208
|
/**
|
|
191
209
|
* Dispatch an action
|
|
210
|
+
* @throws {HypenError} if the action dispatch fails
|
|
192
211
|
*/
|
|
193
212
|
dispatchAction(name: string, payload?: any): void {
|
|
194
213
|
const engine = this.ensureInitialized();
|
|
195
214
|
log.debug(`Action dispatched: ${name}`);
|
|
196
|
-
|
|
215
|
+
try {
|
|
216
|
+
engine.dispatchAction(name, payload ?? null);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
throw classifyEngineError(err);
|
|
219
|
+
}
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
/**
|
package/src/engine.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// WASM module types
|
|
7
7
|
import { WasmEngine } from "../wasm-node/hypen_engine.js";
|
|
8
8
|
import { frameworkLoggers } from "./logger.js";
|
|
9
|
+
import { classifyEngineError } from "./result.js";
|
|
9
10
|
import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
10
11
|
|
|
11
12
|
// Re-export types so existing consumers of "./engine.js" still work
|
|
@@ -89,10 +90,16 @@ export class Engine {
|
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
92
|
* Parse and render Hypen DSL source code
|
|
93
|
+
* @throws {ParseError} if the source fails to parse
|
|
94
|
+
* @throws {RenderError} if rendering fails
|
|
92
95
|
*/
|
|
93
96
|
renderSource(source: string): void {
|
|
94
97
|
const engine = this.ensureInitialized();
|
|
95
|
-
|
|
98
|
+
try {
|
|
99
|
+
engine.renderSource(source);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw classifyEngineError(err);
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/**
|
|
@@ -105,14 +112,21 @@ export class Engine {
|
|
|
105
112
|
|
|
106
113
|
/**
|
|
107
114
|
* Render a component into a specific parent node (subtree rendering)
|
|
115
|
+
* @throws {ParseError} if the source fails to parse
|
|
116
|
+
* @throws {RenderError} if the parent node is not found or rendering fails
|
|
108
117
|
*/
|
|
109
118
|
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
110
119
|
const engine = this.ensureInitialized();
|
|
111
|
-
|
|
120
|
+
try {
|
|
121
|
+
engine.renderInto(source, parentNodeId, unwrapForWasm(state));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw classifyEngineError(err);
|
|
124
|
+
}
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
/**
|
|
115
128
|
* Notify the engine of state changes using sparse updates
|
|
129
|
+
* @throws {StateError} if the state patch is invalid
|
|
116
130
|
*/
|
|
117
131
|
notifyStateChange(paths: string[], values: Record<string, any>): void {
|
|
118
132
|
const engine = this.ensureInitialized();
|
|
@@ -121,7 +135,11 @@ export class Engine {
|
|
|
121
135
|
return;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
|
|
138
|
+
try {
|
|
139
|
+
engine.updateStateSparse(paths, unwrapForWasm(values));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw classifyEngineError(err);
|
|
142
|
+
}
|
|
125
143
|
log.debug("State changed (sparse):", paths);
|
|
126
144
|
}
|
|
127
145
|
|
|
@@ -149,10 +167,15 @@ export class Engine {
|
|
|
149
167
|
|
|
150
168
|
/**
|
|
151
169
|
* Dispatch an action
|
|
170
|
+
* @throws {HypenError} if the action dispatch fails
|
|
152
171
|
*/
|
|
153
172
|
dispatchAction(name: string, payload?: any): void {
|
|
154
173
|
const engine = this.ensureInitialized();
|
|
155
|
-
|
|
174
|
+
try {
|
|
175
|
+
engine.dispatchAction(name, payload ?? null);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw classifyEngineError(err);
|
|
178
|
+
}
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
/**
|
package/src/index.browser.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -74,9 +74,7 @@ export type { StateProxy, ItemProxy } from "./hypen.js";
|
|
|
74
74
|
|
|
75
75
|
export type {
|
|
76
76
|
IEngine,
|
|
77
|
-
RouterContext,
|
|
78
77
|
ActionContext,
|
|
79
|
-
ActionNext,
|
|
80
78
|
GlobalContext,
|
|
81
79
|
LifecycleHandler,
|
|
82
80
|
ActionHandlerContext,
|
|
@@ -245,6 +243,9 @@ export {
|
|
|
245
243
|
ActionError,
|
|
246
244
|
ConnectionError,
|
|
247
245
|
StateError,
|
|
246
|
+
ParseError,
|
|
247
|
+
RenderError,
|
|
248
|
+
classifyEngineError,
|
|
248
249
|
} from "./result.js";
|
|
249
250
|
export type { Result } from "./result.js";
|
|
250
251
|
|
package/src/managed-router.ts
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { IEngine, HypenModuleDefinition } from "./app.js";
|
|
12
|
-
import { HypenModuleInstance } from "./app.js";
|
|
12
|
+
import { HypenModuleInstance, HypenApp } from "./app.js";
|
|
13
13
|
import type { HypenRouter, RouteState } from "./router.js";
|
|
14
14
|
import type { HypenGlobalContext } from "./context.js";
|
|
15
|
-
import { ModuleRegistry } from "./module-registry.js";
|
|
16
15
|
import { createLogger } from "./logger.js";
|
|
17
16
|
|
|
18
17
|
const log = createLogger("ManagedRouter");
|
|
@@ -20,7 +19,7 @@ const log = createLogger("ManagedRouter");
|
|
|
20
19
|
export interface RouteDefinition {
|
|
21
20
|
/** Route path pattern (e.g., "/", "/profile/:id") */
|
|
22
21
|
path: string;
|
|
23
|
-
/** Component name — used to look up in
|
|
22
|
+
/** Component name — used to look up in app registry */
|
|
24
23
|
component: string;
|
|
25
24
|
/** Inline module definition (alternative to registry lookup) */
|
|
26
25
|
module?: HypenModuleDefinition;
|
|
@@ -29,7 +28,7 @@ export interface RouteDefinition {
|
|
|
29
28
|
export class ManagedRouter {
|
|
30
29
|
private router: HypenRouter;
|
|
31
30
|
private engine: IEngine;
|
|
32
|
-
private registry:
|
|
31
|
+
private registry: HypenApp;
|
|
33
32
|
private globalContext: HypenGlobalContext;
|
|
34
33
|
private routes: RouteDefinition[] = [];
|
|
35
34
|
private activeModule: HypenModuleInstance | null = null;
|
|
@@ -41,7 +40,7 @@ export class ManagedRouter {
|
|
|
41
40
|
constructor(
|
|
42
41
|
router: HypenRouter,
|
|
43
42
|
engine: IEngine,
|
|
44
|
-
registry:
|
|
43
|
+
registry: HypenApp,
|
|
45
44
|
globalContext: HypenGlobalContext
|
|
46
45
|
) {
|
|
47
46
|
this.router = router;
|
|
@@ -161,7 +160,7 @@ export class ManagedRouter {
|
|
|
161
160
|
const instance = new HypenModuleInstance(
|
|
162
161
|
this.engine,
|
|
163
162
|
namedDef as HypenModuleDefinition<object>,
|
|
164
|
-
|
|
163
|
+
this.router,
|
|
165
164
|
this.globalContext
|
|
166
165
|
);
|
|
167
166
|
|
package/src/remote/server.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RemoteServer - Stream Hypen apps over WebSocket with Session Management
|
|
3
3
|
*
|
|
4
|
-
* Usage:
|
|
4
|
+
* Usage with inline UI:
|
|
5
5
|
* ```typescript
|
|
6
6
|
* import { RemoteServer, app } from "@hypen-space/core";
|
|
7
7
|
*
|
|
8
8
|
* const counter = app
|
|
9
9
|
* .defineState({ count: 0 })
|
|
10
10
|
* .onAction("increment", ({ state }) => state.count++)
|
|
11
|
-
* .onDisconnect(async ({ state, session }) => {
|
|
12
|
-
* await redis.set(`session:${session.id}`, JSON.stringify(state));
|
|
13
|
-
* })
|
|
14
|
-
* .onReconnect(async ({ session, restore }) => {
|
|
15
|
-
* const saved = await redis.get(`session:${session.id}`);
|
|
16
|
-
* if (saved) restore(JSON.parse(saved));
|
|
17
|
-
* })
|
|
18
|
-
* .onExpire(async ({ session }) => {
|
|
19
|
-
* await redis.del(`session:${session.id}`);
|
|
20
|
-
* })
|
|
21
11
|
* .build();
|
|
22
12
|
*
|
|
23
13
|
* new RemoteServer()
|
|
24
14
|
* .module("Counter", counter)
|
|
25
15
|
* .ui(`Column { Text("Count: \${state.count}") }`)
|
|
26
|
-
* .session({ ttl: 3600, concurrent: "kick-old" })
|
|
27
16
|
* .listen(3000);
|
|
28
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* Usage with directory-based discovery (auto-resolves imports):
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { RemoteServer } from "@hypen-space/core";
|
|
22
|
+
* import chatModule from "./apps/chat/Chat/component.js";
|
|
23
|
+
*
|
|
24
|
+
* await new RemoteServer()
|
|
25
|
+
* .source("./apps/chat")
|
|
26
|
+
* .module("Chat", chatModule)
|
|
27
|
+
* .listen(3001);
|
|
28
|
+
* ```
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { Engine } from "../engine.js";
|
|
32
32
|
import { HypenModuleInstance } from "../app.js";
|
|
33
|
-
import type { HypenModule } from "../app.js";
|
|
33
|
+
import type { HypenModule, HypenModuleDefinition } from "../app.js";
|
|
34
34
|
import type {
|
|
35
35
|
RemoteMessage,
|
|
36
36
|
RemoteClient,
|
|
@@ -44,10 +44,15 @@ import type {
|
|
|
44
44
|
Session,
|
|
45
45
|
SessionConfig,
|
|
46
46
|
} from "./types.js";
|
|
47
|
-
import type { Patch } from "../types.js";
|
|
47
|
+
import type { Patch, ResolvedComponent } from "../types.js";
|
|
48
48
|
import type { ServerWebSocket } from "bun";
|
|
49
49
|
import { SessionManager } from "./session.js";
|
|
50
50
|
import { frameworkLoggers } from "../logger.js";
|
|
51
|
+
import {
|
|
52
|
+
discoverComponents,
|
|
53
|
+
loadDiscoveredComponents,
|
|
54
|
+
type DiscoveredComponent,
|
|
55
|
+
} from "../discovery.js";
|
|
51
56
|
|
|
52
57
|
const log = frameworkLoggers.remote;
|
|
53
58
|
|
|
@@ -72,6 +77,8 @@ export class RemoteServer {
|
|
|
72
77
|
private _module: HypenModule<any> | null = null;
|
|
73
78
|
private _moduleName: string = "App";
|
|
74
79
|
private _ui: string = "";
|
|
80
|
+
private _sourceDir: string | null = null;
|
|
81
|
+
private _discoveredComponents: Map<string, { template: string; module?: HypenModuleDefinition<any> }> = new Map();
|
|
75
82
|
private _config: RemoteServerConfig = {};
|
|
76
83
|
private _sessionConfig: SessionConfig = {};
|
|
77
84
|
private _onConnectionCallbacks: Array<(client: RemoteClient) => void> = [];
|
|
@@ -98,6 +105,26 @@ export class RemoteServer {
|
|
|
98
105
|
return this;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Set a source directory for automatic component discovery.
|
|
110
|
+
*
|
|
111
|
+
* All .hypen and .ts components in the directory (and subdirectories) are
|
|
112
|
+
* discovered automatically. The entry component's template is used as the
|
|
113
|
+
* UI, so calling .ui() is optional when .source() is set.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* new RemoteServer()
|
|
118
|
+
* .source("./apps/chat")
|
|
119
|
+
* .module("Chat", chatModule)
|
|
120
|
+
* .listen(3001);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
source(dir: string): this {
|
|
124
|
+
this._sourceDir = dir;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
/**
|
|
102
129
|
* Set server configuration
|
|
103
130
|
*/
|
|
@@ -133,13 +160,20 @@ export class RemoteServer {
|
|
|
133
160
|
/**
|
|
134
161
|
* Start the WebSocket server
|
|
135
162
|
*/
|
|
136
|
-
listen(port?: number): this {
|
|
163
|
+
async listen(port?: number): Promise<this> {
|
|
137
164
|
if (!this._module) {
|
|
138
165
|
throw new Error("Module not set. Call .module() before .listen()");
|
|
139
166
|
}
|
|
140
167
|
|
|
168
|
+
// Run component discovery if source directory is set
|
|
169
|
+
if (this._sourceDir) {
|
|
170
|
+
await this.discoverFromSource();
|
|
171
|
+
}
|
|
172
|
+
|
|
141
173
|
if (!this._ui) {
|
|
142
|
-
throw new Error(
|
|
174
|
+
throw new Error(
|
|
175
|
+
"UI not set. Call .ui() or .source() before .listen()"
|
|
176
|
+
);
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
// Initialize session manager
|
|
@@ -214,6 +248,56 @@ export class RemoteServer {
|
|
|
214
248
|
return `ws://${hostname}:${port}`;
|
|
215
249
|
}
|
|
216
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Discover components from the source directory.
|
|
253
|
+
* Populates _discoveredComponents and sets _ui from the entry component
|
|
254
|
+
* if not already set via .ui().
|
|
255
|
+
*/
|
|
256
|
+
private async discoverFromSource(): Promise<void> {
|
|
257
|
+
const dir = this._sourceDir!;
|
|
258
|
+
log.info(`Discovering components from ${dir}...`);
|
|
259
|
+
|
|
260
|
+
const discovered = await discoverComponents(dir);
|
|
261
|
+
const loaded = await loadDiscoveredComponents(discovered);
|
|
262
|
+
|
|
263
|
+
for (const [name, { module, template }] of loaded) {
|
|
264
|
+
this._discoveredComponents.set(name, { template, module });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
log.info(
|
|
268
|
+
`Discovered ${this._discoveredComponents.size} components: ${Array.from(this._discoveredComponents.keys()).join(", ")}`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// If .ui() was not called, use the entry component's template
|
|
272
|
+
if (!this._ui) {
|
|
273
|
+
const entry = this._discoveredComponents.get(this._moduleName);
|
|
274
|
+
if (entry) {
|
|
275
|
+
this._ui = entry.template;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Set up a component resolver on a client engine so that imports
|
|
282
|
+
* and component references in templates are resolved from discovered
|
|
283
|
+
* components.
|
|
284
|
+
*/
|
|
285
|
+
private setupComponentResolver(engine: Engine): void {
|
|
286
|
+
if (this._discoveredComponents.size === 0) return;
|
|
287
|
+
|
|
288
|
+
engine.setComponentResolver(
|
|
289
|
+
(componentName: string, _contextPath: string | null) => {
|
|
290
|
+
const comp = this._discoveredComponents.get(componentName);
|
|
291
|
+
if (!comp) return null;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
source: comp.template,
|
|
295
|
+
path: componentName,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
217
301
|
/**
|
|
218
302
|
* Handle new WebSocket connection
|
|
219
303
|
* Waits for hello message before fully initializing
|
|
@@ -227,6 +311,9 @@ export class RemoteServer {
|
|
|
227
311
|
const engine = new Engine();
|
|
228
312
|
await engine.init();
|
|
229
313
|
|
|
314
|
+
// Wire up component resolver if we have discovered components
|
|
315
|
+
this.setupComponentResolver(engine);
|
|
316
|
+
|
|
230
317
|
// Create module instance
|
|
231
318
|
const moduleInstance = new HypenModuleInstance(engine, this._module!);
|
|
232
319
|
|
|
@@ -618,19 +705,27 @@ export class RemoteServer {
|
|
|
618
705
|
/**
|
|
619
706
|
* Convenience function to create and start a RemoteServer
|
|
620
707
|
*/
|
|
621
|
-
export function serve(options: {
|
|
708
|
+
export async function serve(options: {
|
|
622
709
|
module: HypenModule<any>;
|
|
623
710
|
moduleName?: string;
|
|
624
|
-
ui
|
|
711
|
+
ui?: string;
|
|
712
|
+
source?: string;
|
|
625
713
|
port?: number;
|
|
626
714
|
hostname?: string;
|
|
627
715
|
session?: SessionConfig;
|
|
628
716
|
onConnection?: (client: RemoteClient) => void;
|
|
629
717
|
onDisconnection?: (client: RemoteClient) => void;
|
|
630
|
-
}): RemoteServer {
|
|
718
|
+
}): Promise<RemoteServer> {
|
|
631
719
|
const server = new RemoteServer()
|
|
632
|
-
.module(options.moduleName ?? "App", options.module)
|
|
633
|
-
|
|
720
|
+
.module(options.moduleName ?? "App", options.module);
|
|
721
|
+
|
|
722
|
+
if (options.source) {
|
|
723
|
+
server.source(options.source);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (options.ui) {
|
|
727
|
+
server.ui(options.ui);
|
|
728
|
+
}
|
|
634
729
|
|
|
635
730
|
if (options.port || options.hostname) {
|
|
636
731
|
server.config({
|
package/src/result.ts
CHANGED
|
@@ -258,3 +258,51 @@ export class StateError extends HypenError {
|
|
|
258
258
|
this.path = path;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Error thrown when parsing Hypen DSL source fails
|
|
264
|
+
*/
|
|
265
|
+
export class ParseError extends HypenError {
|
|
266
|
+
readonly source?: string;
|
|
267
|
+
|
|
268
|
+
constructor(message: string, source?: string, cause?: unknown) {
|
|
269
|
+
super('PARSE_ERROR', message, {
|
|
270
|
+
context: { source },
|
|
271
|
+
cause: cause instanceof Error ? cause : undefined,
|
|
272
|
+
});
|
|
273
|
+
this.name = 'ParseError';
|
|
274
|
+
this.source = source;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Error thrown when rendering fails
|
|
280
|
+
*/
|
|
281
|
+
export class RenderError extends HypenError {
|
|
282
|
+
constructor(message: string, cause?: unknown) {
|
|
283
|
+
super('RENDER_ERROR', message, {
|
|
284
|
+
cause: cause instanceof Error ? cause : undefined,
|
|
285
|
+
});
|
|
286
|
+
this.name = 'RenderError';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Classify a WASM engine error string into the appropriate HypenError subclass.
|
|
292
|
+
* The engine returns JsValue error strings with known prefixes.
|
|
293
|
+
*/
|
|
294
|
+
export function classifyEngineError(err: unknown): HypenError {
|
|
295
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
296
|
+
|
|
297
|
+
if (message.startsWith('Parse error:')) {
|
|
298
|
+
return new ParseError(message);
|
|
299
|
+
}
|
|
300
|
+
if (message.startsWith('Invalid state:') || message.startsWith('Invalid state patch:')) {
|
|
301
|
+
return new StateError(message);
|
|
302
|
+
}
|
|
303
|
+
if (message.startsWith('Parent node not found:')) {
|
|
304
|
+
return new RenderError(message);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return new RenderError(message);
|
|
308
|
+
}
|
|
Binary file
|
|
Binary file
|
package/wasm-node/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"Hypen Contributors"
|
|
5
5
|
],
|
|
6
6
|
"description": "A Rust implementation of the Hypen engine",
|
|
7
|
-
"version": "0.4.
|
|
7
|
+
"version": "0.4.2",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -16,5 +16,7 @@
|
|
|
16
16
|
"hypen_engine.d.ts"
|
|
17
17
|
],
|
|
18
18
|
"main": "hypen_engine.js",
|
|
19
|
-
"types": "hypen_engine.d.ts"
|
|
20
|
-
|
|
19
|
+
"types": "hypen_engine.d.ts",
|
|
20
|
+
"type": "commonjs",
|
|
21
|
+
"sideEffects": false
|
|
22
|
+
}
|
package/src/module-registry.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module Registry — maps component names to module definitions.
|
|
3
|
-
*
|
|
4
|
-
* Used by the import resolution pipeline and ManagedRouter to look up
|
|
5
|
-
* module definitions by their component name (e.g., "HomePage", "ProfilePage").
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { HypenModuleDefinition } from "./app.js";
|
|
9
|
-
import { createLogger } from "./logger.js";
|
|
10
|
-
|
|
11
|
-
const log = createLogger("ModuleRegistry");
|
|
12
|
-
|
|
13
|
-
export class ModuleRegistry {
|
|
14
|
-
private definitions = new Map<string, HypenModuleDefinition>();
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Register a module definition under a component name.
|
|
18
|
-
*/
|
|
19
|
-
register(name: string, definition: HypenModuleDefinition): void {
|
|
20
|
-
if (this.definitions.has(name)) {
|
|
21
|
-
log.warn(`Module "${name}" is already registered. Overwriting.`);
|
|
22
|
-
}
|
|
23
|
-
this.definitions.set(name, definition);
|
|
24
|
-
log.debug(`Registered module definition: ${name}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get a module definition by component name.
|
|
29
|
-
*/
|
|
30
|
-
get(name: string): HypenModuleDefinition | undefined {
|
|
31
|
-
return this.definitions.get(name);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check if a module definition exists.
|
|
36
|
-
*/
|
|
37
|
-
has(name: string): boolean {
|
|
38
|
-
return this.definitions.has(name);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get all registered definitions.
|
|
43
|
-
*/
|
|
44
|
-
getAll(): Map<string, HypenModuleDefinition> {
|
|
45
|
-
return new Map(this.definitions);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get all registered component names.
|
|
50
|
-
*/
|
|
51
|
-
getNames(): string[] {
|
|
52
|
-
return Array.from(this.definitions.keys());
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Unregister a module definition.
|
|
57
|
-
*/
|
|
58
|
-
unregister(name: string): void {
|
|
59
|
-
this.definitions.delete(name);
|
|
60
|
-
log.debug(`Unregistered module definition: ${name}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Number of registered definitions.
|
|
65
|
-
*/
|
|
66
|
-
get size(): number {
|
|
67
|
-
return this.definitions.size;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Clear all registered definitions.
|
|
72
|
-
*/
|
|
73
|
-
clear(): void {
|
|
74
|
-
this.definitions.clear();
|
|
75
|
-
}
|
|
76
|
-
}
|