@hypen-space/core 0.2.1 → 0.2.3
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 +199 -154
- package/dist/src/app.js +2 -1
- package/dist/src/app.js.map +3 -3
- package/dist/src/engine.browser.js +2 -2
- package/dist/src/engine.browser.js.map +2 -2
- package/dist/src/engine.js +4 -3
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.js +18 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugin.js +126 -0
- package/dist/src/plugin.js.map +10 -0
- package/dist/src/resolver.js +102 -0
- package/dist/src/resolver.js.map +10 -0
- package/dist/src/router.js +37 -18
- package/dist/src/router.js.map +3 -3
- package/dist/src/state.js +9 -1
- package/dist/src/state.js.map +3 -3
- package/package.json +15 -2
- package/src/app.ts +1 -0
- package/src/engine.browser.ts +1 -1
- package/src/engine.ts +4 -2
- package/src/index.ts +21 -1
- package/src/plugin.ts +219 -0
- package/src/resolver.ts +216 -0
- package/src/router.ts +43 -21
- package/src/state.ts +20 -0
- package/wasm-browser/README.md +425 -0
- package/wasm-browser/hypen_engine.d.ts +151 -0
- package/wasm-browser/hypen_engine.js +811 -0
- package/wasm-browser/hypen_engine_bg.js +736 -0
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/hypen_engine_bg.wasm.d.ts +30 -0
- package/wasm-browser/package.json +15 -0
- package/wasm-node/README.md +425 -0
- package/wasm-node/hypen_engine.d.ts +97 -0
- package/wasm-node/hypen_engine.js +751 -0
- package/wasm-node/hypen_engine_bg.js +736 -0
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/hypen_engine_bg.wasm.d.ts +30 -0
- package/wasm-node/package.json +11 -0
package/src/router.ts
CHANGED
|
@@ -27,6 +27,7 @@ export class HypenRouter {
|
|
|
27
27
|
private state: RouteState;
|
|
28
28
|
private subscribers = new Set<RouteChangeCallback>();
|
|
29
29
|
private isInitialized = false;
|
|
30
|
+
private isUpdating = false;
|
|
30
31
|
|
|
31
32
|
constructor() {
|
|
32
33
|
// Create observable state for reactivity
|
|
@@ -68,6 +69,8 @@ export class HypenRouter {
|
|
|
68
69
|
|
|
69
70
|
// Listen for hash changes
|
|
70
71
|
window.addEventListener("hashchange", () => {
|
|
72
|
+
// Don't respond to hashchange events we triggered ourselves
|
|
73
|
+
if (this.isUpdating) return;
|
|
71
74
|
const newPath = this.getPathFromBrowser();
|
|
72
75
|
this.updatePath(newPath, false);
|
|
73
76
|
});
|
|
@@ -150,26 +153,37 @@ export class HypenRouter {
|
|
|
150
153
|
updateBrowser: boolean,
|
|
151
154
|
replace: boolean = false
|
|
152
155
|
) {
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
// Prevent re-entrant updates
|
|
157
|
+
if (this.isUpdating) return;
|
|
158
|
+
|
|
159
|
+
this.isUpdating = true;
|
|
160
|
+
try {
|
|
161
|
+
const oldPath = this.state.currentPath;
|
|
162
|
+
this.state.previousPath = oldPath;
|
|
163
|
+
this.state.currentPath = path;
|
|
164
|
+
this.state.query = this.parseQuery();
|
|
165
|
+
|
|
166
|
+
// Notify subscribers synchronously before any browser events
|
|
167
|
+
this.notifySubscribers();
|
|
168
|
+
|
|
169
|
+
// Update browser URL if needed
|
|
170
|
+
if (updateBrowser && typeof window !== "undefined") {
|
|
171
|
+
const url = "#" + path;
|
|
172
|
+
if (replace) {
|
|
173
|
+
window.history.replaceState(null, "", url);
|
|
174
|
+
} else {
|
|
175
|
+
window.history.pushState(null, "", url);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Manually trigger hashchange event
|
|
179
|
+
const hashChangeEvent = new HashChangeEvent("hashchange", {
|
|
180
|
+
oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
|
|
181
|
+
newURL: window.location.href,
|
|
182
|
+
});
|
|
183
|
+
window.dispatchEvent(hashChangeEvent);
|
|
165
184
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const hashChangeEvent = new HashChangeEvent("hashchange", {
|
|
169
|
-
oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
|
|
170
|
-
newURL: window.location.href,
|
|
171
|
-
});
|
|
172
|
-
window.dispatchEvent(hashChangeEvent);
|
|
185
|
+
} finally {
|
|
186
|
+
this.isUpdating = false;
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
|
|
@@ -272,7 +286,11 @@ export class HypenRouter {
|
|
|
272
286
|
this.subscribers.add(callback);
|
|
273
287
|
|
|
274
288
|
// Call immediately with current state
|
|
275
|
-
|
|
289
|
+
try {
|
|
290
|
+
callback(this.getState());
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("[HypenRouter] Error in route subscriber:", error);
|
|
293
|
+
}
|
|
276
294
|
|
|
277
295
|
// Return unsubscribe function
|
|
278
296
|
return () => {
|
|
@@ -286,7 +304,11 @@ export class HypenRouter {
|
|
|
286
304
|
private notifySubscribers() {
|
|
287
305
|
const routeState = this.getState();
|
|
288
306
|
this.subscribers.forEach((callback) => {
|
|
289
|
-
|
|
307
|
+
try {
|
|
308
|
+
callback(routeState);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error("[HypenRouter] Error in route subscriber:", error);
|
|
311
|
+
}
|
|
290
312
|
});
|
|
291
313
|
}
|
|
292
314
|
|
package/src/state.ts
CHANGED
|
@@ -91,11 +91,20 @@ function deepClone<T>(obj: T): T {
|
|
|
91
91
|
// Handle plain objects
|
|
92
92
|
const objClone: any = {};
|
|
93
93
|
visited.set(value, objClone);
|
|
94
|
+
|
|
95
|
+
// Clone string keys
|
|
94
96
|
for (const key in value) {
|
|
95
97
|
if (value.hasOwnProperty(key)) {
|
|
96
98
|
objClone[key] = cloneInternal(value[key]);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
|
|
102
|
+
// Clone Symbol keys (not enumerable by for...in)
|
|
103
|
+
const symbolKeys = Object.getOwnPropertySymbols(value);
|
|
104
|
+
for (const sym of symbolKeys) {
|
|
105
|
+
objClone[sym] = cloneInternal(value[sym]);
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
return objClone;
|
|
100
109
|
}
|
|
101
110
|
|
|
@@ -191,7 +200,14 @@ export function createObservableState<T extends object>(
|
|
|
191
200
|
// Use default options if not provided
|
|
192
201
|
const opts: StateObserverOptions = options || { onChange: () => {} };
|
|
193
202
|
|
|
203
|
+
// Handle null/undefined by using an empty object
|
|
204
|
+
// This allows modules to start with null state
|
|
205
|
+
if (initialState === null || initialState === undefined) {
|
|
206
|
+
initialState = {} as T;
|
|
207
|
+
}
|
|
208
|
+
|
|
194
209
|
// Detect and reject primitive wrapper objects (Number, String, Boolean)
|
|
210
|
+
// These cannot be properly proxied due to internal slots
|
|
195
211
|
if (
|
|
196
212
|
initialState instanceof Number ||
|
|
197
213
|
initialState instanceof String ||
|
|
@@ -203,6 +219,10 @@ export function createObservableState<T extends object>(
|
|
|
203
219
|
);
|
|
204
220
|
}
|
|
205
221
|
|
|
222
|
+
// Clone the initial state to ensure each observable has its own copy
|
|
223
|
+
// This prevents multiple modules from sharing the same underlying state object
|
|
224
|
+
initialState = deepClone(initialState);
|
|
225
|
+
|
|
206
226
|
// Keep a snapshot of the last known state
|
|
207
227
|
let lastSnapshot = deepClone(initialState);
|
|
208
228
|
const pathPrefix = opts.pathPrefix || "";
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Hypen Engine
|
|
2
|
+
|
|
3
|
+
The core reactive rendering engine for Hypen, written in Rust. Compiles to WASM for web/desktop or native binaries with UniFFI for mobile platforms.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Hypen Engine is a platform-agnostic UI engine that:
|
|
8
|
+
- **Expands** Hypen DSL components into an intermediate representation (IR)
|
|
9
|
+
- **Tracks** reactive dependencies between state and UI nodes
|
|
10
|
+
- **Reconciles** UI trees efficiently using keyed diffing
|
|
11
|
+
- **Generates** minimal platform-agnostic patches for renderers
|
|
12
|
+
- **Routes** actions and events between UI and application logic
|
|
13
|
+
- **Serializes** for Remote UI scenarios (client-server streaming)
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────────────────┐
|
|
19
|
+
│ Hypen Engine │
|
|
20
|
+
├─────────────────────────────────────────────────────────┤
|
|
21
|
+
│ Parser → IR → Reactive Graph → Reconciler → Patches │
|
|
22
|
+
│ ↓ ↓ │
|
|
23
|
+
│ Component Registry Platform Renderer│
|
|
24
|
+
│ Dependency Tracking (Web/iOS/Android)│
|
|
25
|
+
│ State Management │
|
|
26
|
+
└─────────────────────────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Core Systems
|
|
30
|
+
|
|
31
|
+
1. **IR & Component Expansion** (`src/ir/`)
|
|
32
|
+
- Canonical intermediate representation
|
|
33
|
+
- Component registry and resolution
|
|
34
|
+
- Props/slots expansion with defaults
|
|
35
|
+
- Stable NodeId generation
|
|
36
|
+
|
|
37
|
+
2. **Reactive System** (`src/reactive/`)
|
|
38
|
+
- Dependency graph tracking `${state.*}` bindings
|
|
39
|
+
- Dirty marking on state changes
|
|
40
|
+
- Scheduling for efficient updates
|
|
41
|
+
|
|
42
|
+
3. **Reconciliation** (`src/reconcile/`)
|
|
43
|
+
- Virtual instance tree (no platform objects)
|
|
44
|
+
- Keyed children diffing algorithm
|
|
45
|
+
- Minimal patch generation
|
|
46
|
+
|
|
47
|
+
4. **Patch Types** (Platform-agnostic):
|
|
48
|
+
- `Create(id, type, props)` - Create new node
|
|
49
|
+
- `SetProp(id, name, value)` - Update property
|
|
50
|
+
- `SetText(id, text)` - Update text content
|
|
51
|
+
- `Insert(parent, id, before?)` - Insert into tree
|
|
52
|
+
- `Move(parent, id, before?)` - Reorder node
|
|
53
|
+
- `Remove(id)` - Remove from tree
|
|
54
|
+
- `AttachEvent(id, event)` / `DetachEvent(id, event)`
|
|
55
|
+
|
|
56
|
+
5. **Action/Event Routing** (`src/dispatch/`)
|
|
57
|
+
- Map `@actions.*` to module handlers
|
|
58
|
+
- Forward UI events (click, input, etc.)
|
|
59
|
+
- Stable dispatch contract for SDKs
|
|
60
|
+
|
|
61
|
+
6. **Lifecycle Management** (`src/lifecycle/`)
|
|
62
|
+
- Module lifecycle (created/destroyed)
|
|
63
|
+
- Component lifecycle (mount/unmount)
|
|
64
|
+
- Resource cache (images/fonts) with pluggable fetcher
|
|
65
|
+
|
|
66
|
+
7. **Remote UI Serialization** (`src/serialize/`)
|
|
67
|
+
- Initial tree serialization
|
|
68
|
+
- Incremental patch streaming
|
|
69
|
+
- Revision tracking and optional integrity hashes
|
|
70
|
+
- JSON/CBOR format support
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Basic Example
|
|
75
|
+
|
|
76
|
+
```rust
|
|
77
|
+
use hypen_engine::{Engine, ir::{Element, Value, Component}};
|
|
78
|
+
use serde_json::json;
|
|
79
|
+
|
|
80
|
+
// Create engine
|
|
81
|
+
let mut engine = Engine::new();
|
|
82
|
+
|
|
83
|
+
// Register a custom component
|
|
84
|
+
engine.register_component(Component::new("Greeting", |props| {
|
|
85
|
+
Element::new("Text")
|
|
86
|
+
.with_prop("text", Value::Binding("${state.name}".to_string()))
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// Set render callback
|
|
90
|
+
engine.set_render_callback(|patches| {
|
|
91
|
+
for patch in patches {
|
|
92
|
+
println!("Patch: {:?}", patch);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Register action handler
|
|
97
|
+
engine.on_action("greet", |action| {
|
|
98
|
+
println!("Hello from action: {:?}", action);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Render UI
|
|
102
|
+
let ui = Element::new("Column")
|
|
103
|
+
.with_child(Element::new("Greeting"));
|
|
104
|
+
|
|
105
|
+
engine.render(&ui);
|
|
106
|
+
|
|
107
|
+
// Update state
|
|
108
|
+
engine.update_state(json!({
|
|
109
|
+
"name": "Alice"
|
|
110
|
+
}));
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### With Module Host
|
|
114
|
+
|
|
115
|
+
```rust
|
|
116
|
+
use hypen_engine::lifecycle::{Module, ModuleInstance};
|
|
117
|
+
|
|
118
|
+
// Create module definition
|
|
119
|
+
let module = Module::new("ProfilePage")
|
|
120
|
+
.with_actions(vec!["signIn".to_string(), "signOut".to_string()])
|
|
121
|
+
.with_state_keys(vec!["user".to_string()])
|
|
122
|
+
.with_persist(true);
|
|
123
|
+
|
|
124
|
+
// Create module instance
|
|
125
|
+
let instance = ModuleInstance::new(
|
|
126
|
+
module,
|
|
127
|
+
json!({ "user": null })
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
engine.set_module(instance);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Compilation Targets
|
|
134
|
+
|
|
135
|
+
### Native (Development)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cargo build
|
|
139
|
+
cargo test
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### WASM (Web/Desktop)
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Install wasm-pack (one time)
|
|
146
|
+
cargo install wasm-pack
|
|
147
|
+
|
|
148
|
+
# Build for all WASM targets
|
|
149
|
+
./build-wasm.sh
|
|
150
|
+
|
|
151
|
+
# Or build manually for specific targets:
|
|
152
|
+
wasm-pack build --target bundler # For webpack/vite
|
|
153
|
+
wasm-pack build --target nodejs # For Node.js
|
|
154
|
+
wasm-pack build --target web # For vanilla JS
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Output directories:
|
|
158
|
+
- `pkg/bundler/` - For use with bundlers (webpack, vite, rollup)
|
|
159
|
+
- `pkg/nodejs/` - For Node.js
|
|
160
|
+
- `pkg/web/` - For vanilla HTML/JS (see `example.html`)
|
|
161
|
+
|
|
162
|
+
### JavaScript/TypeScript API
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import init, { WasmEngine } from './pkg/web/hypen_engine.js';
|
|
166
|
+
|
|
167
|
+
// Initialize WASM
|
|
168
|
+
await init();
|
|
169
|
+
|
|
170
|
+
// Create engine
|
|
171
|
+
const engine = new WasmEngine();
|
|
172
|
+
|
|
173
|
+
// Set render callback
|
|
174
|
+
engine.setRenderCallback((patches) => {
|
|
175
|
+
console.log('Patches:', patches);
|
|
176
|
+
// Apply patches to your renderer
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Register action handlers
|
|
180
|
+
engine.onAction('increment', (action) => {
|
|
181
|
+
console.log('Increment action:', action);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Initialize module
|
|
185
|
+
engine.setModule(
|
|
186
|
+
'CounterModule',
|
|
187
|
+
['increment', 'decrement'],
|
|
188
|
+
['count'],
|
|
189
|
+
{ count: 0 }
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Render Hypen DSL
|
|
193
|
+
const source = `
|
|
194
|
+
Column {
|
|
195
|
+
Text("Count: \${state.count}")
|
|
196
|
+
Button("@actions.increment") { Text("+1") }
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
engine.renderSource(source);
|
|
200
|
+
|
|
201
|
+
// Update state
|
|
202
|
+
engine.updateState({ count: 42 });
|
|
203
|
+
|
|
204
|
+
// Dispatch action
|
|
205
|
+
engine.dispatchAction('increment', { amount: 1 });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Testing WASM Build
|
|
209
|
+
|
|
210
|
+
Open `example.html` in a web server:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Using Python
|
|
214
|
+
python3 -m http.server 8000
|
|
215
|
+
|
|
216
|
+
# Using Node.js
|
|
217
|
+
npx serve .
|
|
218
|
+
|
|
219
|
+
# Then visit: http://localhost:8000/example.html
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Mobile (UniFFI)
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Generate Swift/Kotlin bindings (coming soon)
|
|
226
|
+
cargo install uniffi_bindgen
|
|
227
|
+
uniffi-bindgen generate src/hypen_engine.udl --language swift
|
|
228
|
+
uniffi-bindgen generate src/hypen_engine.udl --language kotlin
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Project Structure
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
hypen-engine-rs/
|
|
235
|
+
├── src/
|
|
236
|
+
│ ├── lib.rs # Public API
|
|
237
|
+
│ ├── engine.rs # Main orchestrator
|
|
238
|
+
│ ├── wasi.rs # WASI interfaces
|
|
239
|
+
│ ├── ir/ # IR & component expansion
|
|
240
|
+
│ │ ├── node.rs # NodeId, Element, Props, Value
|
|
241
|
+
│ │ ├── component.rs # Component registry
|
|
242
|
+
│ │ └── expand.rs # AST → IR lowering
|
|
243
|
+
│ ├── reactive/ # Reactive system
|
|
244
|
+
│ │ ├── binding.rs # ${state.*} parsing
|
|
245
|
+
│ │ ├── graph.rs # Dependency tracking
|
|
246
|
+
│ │ └── scheduler.rs # Dirty marking
|
|
247
|
+
│ ├── reconcile/ # Reconciliation
|
|
248
|
+
│ │ ├── tree.rs # Instance tree
|
|
249
|
+
│ │ ├── diff.rs # Diffing algorithm
|
|
250
|
+
│ │ └── patch.rs # Patch types
|
|
251
|
+
│ ├── dispatch/ # Events & actions
|
|
252
|
+
│ │ ├── action.rs # Action dispatcher
|
|
253
|
+
│ │ └── event.rs # Event router
|
|
254
|
+
│ ├── lifecycle/ # Lifecycle
|
|
255
|
+
│ │ ├── module.rs # Module lifecycle
|
|
256
|
+
│ │ ├── component.rs # Component lifecycle
|
|
257
|
+
│ │ └── resource.rs # Resource cache
|
|
258
|
+
│ └── serialize/ # Serialization
|
|
259
|
+
│ └── remote.rs # Remote UI protocol
|
|
260
|
+
├── Cargo.toml
|
|
261
|
+
└── README.md
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Key Data Structures
|
|
265
|
+
|
|
266
|
+
### Element (IR Node)
|
|
267
|
+
```rust
|
|
268
|
+
pub struct Element {
|
|
269
|
+
pub element_type: String, // "Column", "Text", etc.
|
|
270
|
+
pub props: IndexMap<String, Value>, // Properties
|
|
271
|
+
pub children: Vec<Element>, // Child elements
|
|
272
|
+
pub key: Option<String>, // For reconciliation
|
|
273
|
+
pub events: IndexMap<String, String>, // event → action
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Value (Props)
|
|
278
|
+
```rust
|
|
279
|
+
pub enum Value {
|
|
280
|
+
Static(serde_json::Value), // Literal values
|
|
281
|
+
Binding(String), // ${state.user.name}
|
|
282
|
+
Action(String), // @actions.signIn
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Patch (Output)
|
|
287
|
+
```rust
|
|
288
|
+
pub enum Patch {
|
|
289
|
+
Create { id, element_type, props },
|
|
290
|
+
SetProp { id, name, value },
|
|
291
|
+
SetText { id, text },
|
|
292
|
+
Insert { parent_id, id, before_id? },
|
|
293
|
+
Move { parent_id, id, before_id? },
|
|
294
|
+
Remove { id },
|
|
295
|
+
AttachEvent { id, event_name },
|
|
296
|
+
DetachEvent { id, event_name },
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Integration with Parser
|
|
301
|
+
|
|
302
|
+
The engine integrates with the Hypen parser from `../parser`:
|
|
303
|
+
|
|
304
|
+
```rust
|
|
305
|
+
use hypen_parser::parse_component;
|
|
306
|
+
use hypen_engine::ast_to_ir;
|
|
307
|
+
|
|
308
|
+
let source = r#"
|
|
309
|
+
Column {
|
|
310
|
+
Text("Hello, ${state.name}")
|
|
311
|
+
Button("@actions.greet") { Text("Greet") }
|
|
312
|
+
}
|
|
313
|
+
"#;
|
|
314
|
+
|
|
315
|
+
let ast = parse_component(source)?;
|
|
316
|
+
let element = ast_to_ir(&ast); // Convert AST → IR
|
|
317
|
+
engine.render(&element);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Full Example with Parser
|
|
321
|
+
|
|
322
|
+
```rust
|
|
323
|
+
use hypen_engine::{Engine, ast_to_ir};
|
|
324
|
+
use hypen_parser::parse_component;
|
|
325
|
+
use serde_json::json;
|
|
326
|
+
|
|
327
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
328
|
+
let mut engine = Engine::new();
|
|
329
|
+
|
|
330
|
+
// Set render callback
|
|
331
|
+
engine.set_render_callback(|patches| {
|
|
332
|
+
println!("Patches: {:#?}", patches);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Parse Hypen DSL
|
|
336
|
+
let source = r#"
|
|
337
|
+
Column {
|
|
338
|
+
Text("Count: ${state.count}")
|
|
339
|
+
Button("@actions.increment") { Text("+1") }
|
|
340
|
+
}
|
|
341
|
+
"#;
|
|
342
|
+
|
|
343
|
+
let ast = parse_component(source)?;
|
|
344
|
+
let element = ast_to_ir(&ast);
|
|
345
|
+
|
|
346
|
+
// Render
|
|
347
|
+
engine.render(&element);
|
|
348
|
+
|
|
349
|
+
// Update state
|
|
350
|
+
engine.update_state(json!({"count": 42}));
|
|
351
|
+
|
|
352
|
+
Ok(())
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Performance Considerations
|
|
357
|
+
|
|
358
|
+
- **Keyed reconciliation**: Use `key` props for list items to minimize DOM churn
|
|
359
|
+
- **Dependency tracking**: Only re-render nodes affected by state changes
|
|
360
|
+
- **Lazy evaluation**: Bindings are resolved on-demand during reconciliation
|
|
361
|
+
- **Resource caching**: Images/fonts are cached with configurable eviction
|
|
362
|
+
|
|
363
|
+
## Remote UI Protocol
|
|
364
|
+
|
|
365
|
+
For client-server streaming:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
// Initial tree (client connects)
|
|
369
|
+
{
|
|
370
|
+
"type": "initialTree",
|
|
371
|
+
"module": "ProfilePage",
|
|
372
|
+
"state": { "user": null },
|
|
373
|
+
"patches": [...],
|
|
374
|
+
"revision": 0
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// State update (server → client)
|
|
378
|
+
{
|
|
379
|
+
"type": "stateUpdate",
|
|
380
|
+
"module": "ProfilePage",
|
|
381
|
+
"state": { "user": { "name": "Alice" } }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Incremental patches (server → client)
|
|
385
|
+
{
|
|
386
|
+
"type": "patch",
|
|
387
|
+
"module": "ProfilePage",
|
|
388
|
+
"patches": [{ "type": "setProp", ... }],
|
|
389
|
+
"revision": 42
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Action dispatch (client → server)
|
|
393
|
+
{
|
|
394
|
+
"type": "dispatchAction",
|
|
395
|
+
"module": "ProfilePage",
|
|
396
|
+
"action": "signIn",
|
|
397
|
+
"payload": { "provider": "google" }
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Testing
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# Run all tests
|
|
405
|
+
cargo test
|
|
406
|
+
|
|
407
|
+
# Run with output
|
|
408
|
+
cargo test -- --nocapture
|
|
409
|
+
|
|
410
|
+
# Test specific module
|
|
411
|
+
cargo test reactive::
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Contributing
|
|
415
|
+
|
|
416
|
+
This is part of the Hypen project. See the main repository for contribution guidelines.
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
See main Hypen project for license information.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
**Status**: ✅ Core systems implemented, WASM integration in progress
|
|
425
|
+
**Next**: Full keyed reconciliation, UniFFI bindings, platform renderers
|