@hypen-space/core 0.2.1 → 0.2.2
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/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# @hypen-space/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The reactive runtime for Hypen - a declarative UI language that separates what your UI looks like from how it behaves.
|
|
4
|
+
|
|
5
|
+
## What is Hypen?
|
|
6
|
+
|
|
7
|
+
Hypen lets you write UI templates in a clean, declarative syntax:
|
|
8
|
+
|
|
9
|
+
```hypen
|
|
10
|
+
Column {
|
|
11
|
+
Text("Hello, ${state.user.name}!")
|
|
12
|
+
Button(onClick: @actions.logout) {
|
|
13
|
+
Text("Sign Out")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `@hypen-space/core` package provides the engine that:
|
|
19
|
+
|
|
20
|
+
1. **Parses** your Hypen templates
|
|
21
|
+
2. **Tracks** reactive state bindings (like `${state.user.name}`)
|
|
22
|
+
3. **Generates patches** when state changes (instead of re-rendering everything)
|
|
23
|
+
4. **Dispatches actions** triggered from the UI (like `@actions.logout`)
|
|
24
|
+
|
|
25
|
+
You provide a renderer that applies those patches to your platform (DOM, Canvas, Native, etc).
|
|
4
26
|
|
|
5
27
|
## Installation
|
|
6
28
|
|
|
@@ -10,12 +32,54 @@ npm install @hypen-space/core
|
|
|
10
32
|
bun add @hypen-space/core
|
|
11
33
|
```
|
|
12
34
|
|
|
35
|
+
## Core Concepts
|
|
36
|
+
|
|
37
|
+
### Templates
|
|
38
|
+
|
|
39
|
+
Hypen templates describe your UI structure. Components can have:
|
|
40
|
+
|
|
41
|
+
- **Arguments**: `Text("Hello")` or `Button(disabled: true)`
|
|
42
|
+
- **Children**: Nested inside `{ }` braces
|
|
43
|
+
- **Applicators**: Chained styling like `.padding(16).color(blue)`
|
|
44
|
+
|
|
45
|
+
### State Bindings
|
|
46
|
+
|
|
47
|
+
Use `${state.path}` to bind template values to your module's state:
|
|
48
|
+
|
|
49
|
+
```hypen
|
|
50
|
+
Text("Count: ${state.count}")
|
|
51
|
+
Text("User: ${state.user.name}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When state changes, only the affected parts of the UI update.
|
|
55
|
+
|
|
56
|
+
### Actions
|
|
57
|
+
|
|
58
|
+
Use `@actions.name` to dispatch events from the UI to your module:
|
|
59
|
+
|
|
60
|
+
```hypen
|
|
61
|
+
Button(onClick: @actions.increment) { Text("+") }
|
|
62
|
+
Button(onClick: @actions.submitForm) { Text("Submit") }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Patches
|
|
66
|
+
|
|
67
|
+
The engine doesn't manipulate the UI directly. Instead, it emits **patches** - minimal instructions describing what changed:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
{ type: "Create", id: "1", elementType: "Text", props: { text: "Hello" } }
|
|
71
|
+
{ type: "SetProp", id: "1", name: "text", value: "Hello, World" }
|
|
72
|
+
{ type: "Remove", id: "1" }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Your renderer applies these patches to the actual platform.
|
|
76
|
+
|
|
13
77
|
## Quick Start
|
|
14
78
|
|
|
15
79
|
```typescript
|
|
16
80
|
import { Engine, app } from "@hypen-space/core";
|
|
17
81
|
|
|
18
|
-
// 1. Define
|
|
82
|
+
// 1. Define your module's state and actions
|
|
19
83
|
const counter = app
|
|
20
84
|
.defineState({ count: 0 })
|
|
21
85
|
.onAction("increment", ({ state }) => state.count++)
|
|
@@ -26,14 +90,14 @@ const counter = app
|
|
|
26
90
|
const engine = new Engine();
|
|
27
91
|
await engine.init();
|
|
28
92
|
|
|
29
|
-
// 3.
|
|
93
|
+
// 3. Connect your renderer
|
|
30
94
|
engine.setRenderCallback((patches) => {
|
|
31
|
-
// Apply patches to your platform (DOM, Canvas, Native, etc.)
|
|
32
95
|
myRenderer.applyPatches(patches);
|
|
33
96
|
});
|
|
34
97
|
|
|
35
|
-
// 4. Register module and render
|
|
98
|
+
// 4. Register the module and render
|
|
36
99
|
engine.setModule("counter", counter.actions, counter.stateKeys, counter.initialState);
|
|
100
|
+
|
|
37
101
|
engine.renderSource(`
|
|
38
102
|
Column {
|
|
39
103
|
Text("Count: \${state.count}")
|
|
@@ -45,75 +109,9 @@ engine.renderSource(`
|
|
|
45
109
|
`);
|
|
46
110
|
```
|
|
47
111
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
Auto-discover `.hypen` components from the filesystem:
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
import { discoverComponents, loadDiscoveredComponents, watchComponents } from "@hypen-space/core";
|
|
54
|
-
|
|
55
|
-
// Discover components in a directory
|
|
56
|
-
const components = await discoverComponents("./src/components", {
|
|
57
|
-
patterns: ["folder", "sibling", "index"], // Naming conventions
|
|
58
|
-
recursive: false,
|
|
59
|
-
debug: true,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Load discovered components with their modules
|
|
63
|
-
const loaded = await loadDiscoveredComponents(components);
|
|
64
|
-
|
|
65
|
-
// Watch for changes (hot reload)
|
|
66
|
-
const watcher = watchComponents("./src/components", {
|
|
67
|
-
onAdd: (c) => console.log("Added:", c.name),
|
|
68
|
-
onUpdate: (c) => console.log("Updated:", c.name),
|
|
69
|
-
onRemove: (name) => console.log("Removed:", name),
|
|
70
|
-
onChange: (all) => console.log("All components:", all.length),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Stop watching
|
|
74
|
-
watcher.stop();
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Supported Patterns
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
# Folder-based (recommended)
|
|
81
|
-
Counter/
|
|
82
|
-
├── component.hypen
|
|
83
|
-
└── component.ts
|
|
112
|
+
## Modules
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
Counter.hypen
|
|
87
|
-
Counter.ts
|
|
88
|
-
|
|
89
|
-
# Index-based
|
|
90
|
-
Counter/
|
|
91
|
-
├── index.hypen
|
|
92
|
-
└── index.ts
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Component Loader
|
|
96
|
-
|
|
97
|
-
Register and manage components:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
import { componentLoader, ComponentLoader } from "@hypen-space/core";
|
|
101
|
-
|
|
102
|
-
// Use the global loader
|
|
103
|
-
componentLoader.register("Counter", counterModule, counterTemplate);
|
|
104
|
-
componentLoader.get("Counter"); // Get component
|
|
105
|
-
componentLoader.has("Counter"); // Check if exists
|
|
106
|
-
componentLoader.getNames(); // List all names
|
|
107
|
-
|
|
108
|
-
// Or create your own
|
|
109
|
-
const loader = new ComponentLoader();
|
|
110
|
-
await loader.loadFromDirectory("Counter", "./src/components/Counter");
|
|
111
|
-
await loader.loadFromComponentsDir("./src/components"); // Auto-load all
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Module System
|
|
115
|
-
|
|
116
|
-
Create stateful modules with the fluent `app` builder:
|
|
114
|
+
Modules manage state and handle actions. Use the `app` builder to define them:
|
|
117
115
|
|
|
118
116
|
```typescript
|
|
119
117
|
import { app } from "@hypen-space/core";
|
|
@@ -126,13 +124,13 @@ interface UserState {
|
|
|
126
124
|
const userModule = app
|
|
127
125
|
.defineState<UserState>({ user: null, loading: false })
|
|
128
126
|
|
|
129
|
-
//
|
|
127
|
+
// Called when module is created
|
|
130
128
|
.onCreated(async ({ state, next }) => {
|
|
131
129
|
state.loading = true;
|
|
132
|
-
next(); // Sync state to engine
|
|
130
|
+
next(); // Sync state changes to engine
|
|
133
131
|
})
|
|
134
132
|
|
|
135
|
-
//
|
|
133
|
+
// Handle actions dispatched from UI
|
|
136
134
|
.onAction("loadUser", async ({ state, action, next }) => {
|
|
137
135
|
const userId = action.payload?.id;
|
|
138
136
|
state.user = await fetchUser(userId);
|
|
@@ -145,16 +143,19 @@ const userModule = app
|
|
|
145
143
|
next();
|
|
146
144
|
})
|
|
147
145
|
|
|
146
|
+
// Called when module is destroyed
|
|
148
147
|
.onDestroyed(({ state }) => {
|
|
149
|
-
console.log("
|
|
148
|
+
console.log("Cleanup");
|
|
150
149
|
})
|
|
151
150
|
|
|
152
151
|
.build();
|
|
153
152
|
```
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
The `next()` callback synchronizes state changes with the engine after async operations.
|
|
155
|
+
|
|
156
|
+
## State
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
State is automatically tracked via Proxy. Mutations trigger UI updates:
|
|
158
159
|
|
|
159
160
|
```typescript
|
|
160
161
|
import { createObservableState, batchStateUpdates, getStateSnapshot } from "@hypen-space/core";
|
|
@@ -169,19 +170,50 @@ const state = createObservableState({ count: 0, items: [] }, {
|
|
|
169
170
|
state.count = 5;
|
|
170
171
|
state.items.push("item");
|
|
171
172
|
|
|
172
|
-
// Batch multiple updates
|
|
173
|
+
// Batch multiple updates into one render cycle
|
|
173
174
|
batchStateUpdates(state, () => {
|
|
174
175
|
state.count = 10;
|
|
175
176
|
state.items = ["a", "b", "c"];
|
|
176
177
|
});
|
|
177
178
|
|
|
178
|
-
// Get immutable snapshot
|
|
179
|
+
// Get an immutable snapshot
|
|
179
180
|
const snapshot = getStateSnapshot(state);
|
|
180
181
|
```
|
|
181
182
|
|
|
183
|
+
## Custom Renderers
|
|
184
|
+
|
|
185
|
+
Extend `BaseRenderer` to render to any platform:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { BaseRenderer } from "@hypen-space/core";
|
|
189
|
+
|
|
190
|
+
class MyRenderer extends BaseRenderer {
|
|
191
|
+
protected onCreate(id: string, type: string, props: Record<string, any>) {
|
|
192
|
+
// Create an element
|
|
193
|
+
}
|
|
194
|
+
protected onSetProp(id: string, name: string, value: any) {
|
|
195
|
+
// Update a property
|
|
196
|
+
}
|
|
197
|
+
protected onSetText(id: string, text: string) {
|
|
198
|
+
// Set text content
|
|
199
|
+
}
|
|
200
|
+
protected onInsert(parentId: string, id: string, beforeId?: string) {
|
|
201
|
+
// Insert into parent
|
|
202
|
+
}
|
|
203
|
+
protected onMove(parentId: string, id: string, beforeId?: string) {
|
|
204
|
+
// Reorder element
|
|
205
|
+
}
|
|
206
|
+
protected onRemove(id: string) {
|
|
207
|
+
// Remove element
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
See `@hypen-space/web` for a DOM renderer implementation.
|
|
213
|
+
|
|
182
214
|
## Routing
|
|
183
215
|
|
|
184
|
-
Built-in hash
|
|
216
|
+
Built-in hash or pathname routing:
|
|
185
217
|
|
|
186
218
|
```typescript
|
|
187
219
|
import { HypenRouter } from "@hypen-space/core";
|
|
@@ -191,46 +223,90 @@ const router = new HypenRouter();
|
|
|
191
223
|
router.navigate("/products/123");
|
|
192
224
|
|
|
193
225
|
router.subscribe((state) => {
|
|
194
|
-
console.log("
|
|
195
|
-
console.log("Params:", state.params);
|
|
226
|
+
console.log("Path:", state.currentPath);
|
|
227
|
+
console.log("Params:", state.params); // { id: "123" }
|
|
196
228
|
console.log("Query:", state.query);
|
|
197
229
|
});
|
|
198
230
|
```
|
|
199
231
|
|
|
200
|
-
|
|
232
|
+
Use built-in components in templates:
|
|
201
233
|
|
|
202
|
-
|
|
234
|
+
```hypen
|
|
235
|
+
Router {
|
|
236
|
+
Route(path: "/") { HomePage }
|
|
237
|
+
Route(path: "/products/:id") { ProductPage }
|
|
238
|
+
}
|
|
239
|
+
Link(to: "/products/42") { Text("View Product") }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Component Discovery
|
|
243
|
+
|
|
244
|
+
For larger apps, organize components as files and auto-discover them:
|
|
203
245
|
|
|
204
246
|
```typescript
|
|
205
|
-
import {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Route(path: "/") { HomePage }
|
|
210
|
-
// Route(path: "/products") { ProductList }
|
|
211
|
-
// }
|
|
212
|
-
// Link(to: "/products") { Text("View Products") }
|
|
247
|
+
import { discoverComponents, loadDiscoveredComponents } from "@hypen-space/core";
|
|
248
|
+
|
|
249
|
+
const components = await discoverComponents("./src/components");
|
|
250
|
+
const loaded = await loadDiscoveredComponents(components);
|
|
213
251
|
```
|
|
214
252
|
|
|
215
|
-
|
|
253
|
+
Supported file patterns:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Counter/
|
|
257
|
+
├── component.hypen # Template
|
|
258
|
+
└── component.ts # Module
|
|
216
259
|
|
|
217
|
-
|
|
260
|
+
Counter.hypen # Or sibling files
|
|
261
|
+
Counter.ts
|
|
262
|
+
|
|
263
|
+
Counter/
|
|
264
|
+
├── index.hypen # Or index-based
|
|
265
|
+
└── index.ts
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Watch for changes (hot reload):
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { watchComponents } from "@hypen-space/core";
|
|
272
|
+
|
|
273
|
+
const watcher = watchComponents("./src/components", {
|
|
274
|
+
onUpdate: (c) => console.log("Updated:", c.name),
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Component Loader
|
|
279
|
+
|
|
280
|
+
Register components programmatically:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { componentLoader, ComponentLoader } from "@hypen-space/core";
|
|
284
|
+
|
|
285
|
+
// Global loader
|
|
286
|
+
componentLoader.register("Counter", counterModule, counterTemplate);
|
|
287
|
+
componentLoader.get("Counter");
|
|
288
|
+
componentLoader.has("Counter");
|
|
289
|
+
|
|
290
|
+
// Or create your own
|
|
291
|
+
const loader = new ComponentLoader();
|
|
292
|
+
await loader.loadFromComponentsDir("./src/components");
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Remote UI
|
|
296
|
+
|
|
297
|
+
Connect to a Hypen server for server-driven UI:
|
|
218
298
|
|
|
219
299
|
```typescript
|
|
220
300
|
import { RemoteEngine } from "@hypen-space/core";
|
|
221
301
|
|
|
222
302
|
const remote = new RemoteEngine("ws://localhost:3000", {
|
|
223
303
|
autoReconnect: true,
|
|
224
|
-
reconnectInterval: 3000,
|
|
225
|
-
maxReconnectAttempts: 10,
|
|
226
304
|
});
|
|
227
305
|
|
|
228
306
|
remote
|
|
229
307
|
.onPatches((patches) => renderer.applyPatches(patches))
|
|
230
308
|
.onStateUpdate((state) => console.log("Server state:", state))
|
|
231
|
-
.onConnect(() => console.log("Connected"))
|
|
232
|
-
.onDisconnect(() => console.log("Disconnected"))
|
|
233
|
-
.onError((error) => console.error("Error:", error));
|
|
309
|
+
.onConnect(() => console.log("Connected"));
|
|
234
310
|
|
|
235
311
|
await remote.connect();
|
|
236
312
|
remote.dispatchAction("loadData", { page: 1 });
|
|
@@ -238,86 +314,55 @@ remote.dispatchAction("loadData", { page: 1 });
|
|
|
238
314
|
|
|
239
315
|
## Global Context
|
|
240
316
|
|
|
241
|
-
|
|
317
|
+
Share state and events across modules:
|
|
242
318
|
|
|
243
319
|
```typescript
|
|
244
320
|
import { HypenGlobalContext } from "@hypen-space/core";
|
|
245
321
|
|
|
246
322
|
const context = new HypenGlobalContext();
|
|
247
323
|
|
|
248
|
-
context.registerModule("auth",
|
|
249
|
-
context.registerModule("cart",
|
|
324
|
+
context.registerModule("auth", authModule);
|
|
325
|
+
context.registerModule("cart", cartModule);
|
|
250
326
|
|
|
251
|
-
|
|
252
|
-
const
|
|
327
|
+
// Cross-module access
|
|
328
|
+
const user = context.getModule("auth").getState().user;
|
|
253
329
|
|
|
254
|
-
// Event
|
|
255
|
-
context.on("userLoggedIn", (user) =>
|
|
330
|
+
// Event bus
|
|
331
|
+
context.on("userLoggedIn", (user) => { /* ... */ });
|
|
256
332
|
context.emit("userLoggedIn", { id: "1", name: "Ian" });
|
|
257
333
|
```
|
|
258
334
|
|
|
259
|
-
##
|
|
260
|
-
|
|
261
|
-
Implement the `Renderer` interface for any platform:
|
|
335
|
+
## Browser vs Node.js
|
|
262
336
|
|
|
263
337
|
```typescript
|
|
264
|
-
|
|
338
|
+
// Node.js / Bundler - WASM loads automatically
|
|
339
|
+
import { Engine } from "@hypen-space/core";
|
|
340
|
+
const engine = new Engine();
|
|
341
|
+
await engine.init();
|
|
265
342
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
protected onSetProp(id: string, name: string, value: any) {
|
|
271
|
-
// Set property
|
|
272
|
-
}
|
|
273
|
-
protected onSetText(id: string, text: string) {
|
|
274
|
-
// Set text
|
|
275
|
-
}
|
|
276
|
-
protected onInsert(parentId: string, id: string, beforeId?: string) {
|
|
277
|
-
// Insert into parent
|
|
278
|
-
}
|
|
279
|
-
protected onMove(parentId: string, id: string, beforeId?: string) {
|
|
280
|
-
// Move element
|
|
281
|
-
}
|
|
282
|
-
protected onRemove(id: string) {
|
|
283
|
-
// Remove element
|
|
284
|
-
}
|
|
285
|
-
}
|
|
343
|
+
// Browser - specify WASM path
|
|
344
|
+
import { BrowserEngine } from "@hypen-space/core";
|
|
345
|
+
const engine = new BrowserEngine();
|
|
346
|
+
await engine.init({ wasmPath: "/hypen_engine_bg.wasm" });
|
|
286
347
|
```
|
|
287
348
|
|
|
288
|
-
## Exports
|
|
349
|
+
## Package Exports
|
|
289
350
|
|
|
290
351
|
| Export | Description |
|
|
291
352
|
|--------|-------------|
|
|
292
|
-
| `@hypen-space/core` | Main entry
|
|
293
|
-
| `@hypen-space/core/engine` | Low-level WASM engine
|
|
294
|
-
| `@hypen-space/core/engine/browser` | Browser-optimized engine
|
|
295
|
-
| `@hypen-space/core/app` | Module builder
|
|
353
|
+
| `@hypen-space/core` | Main entry point |
|
|
354
|
+
| `@hypen-space/core/engine` | Low-level WASM engine |
|
|
355
|
+
| `@hypen-space/core/engine/browser` | Browser-optimized engine |
|
|
356
|
+
| `@hypen-space/core/app` | Module builder |
|
|
296
357
|
| `@hypen-space/core/state` | Observable state utilities |
|
|
297
|
-
| `@hypen-space/core/renderer` | Abstract renderer
|
|
358
|
+
| `@hypen-space/core/renderer` | Abstract renderer |
|
|
298
359
|
| `@hypen-space/core/router` | Routing system |
|
|
299
|
-
| `@hypen-space/core/events` | Typed event emitter |
|
|
300
360
|
| `@hypen-space/core/context` | Global context |
|
|
301
361
|
| `@hypen-space/core/remote` | Remote UI protocol |
|
|
302
|
-
| `@hypen-space/core/remote/client` | WebSocket client |
|
|
303
362
|
| `@hypen-space/core/loader` | Component loader |
|
|
304
363
|
| `@hypen-space/core/discovery` | Component discovery |
|
|
305
364
|
| `@hypen-space/core/components` | Built-in Router, Route, Link |
|
|
306
365
|
|
|
307
|
-
## Browser vs Node.js
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
// Node.js / Bundler (auto WASM init)
|
|
311
|
-
import { Engine } from "@hypen-space/core";
|
|
312
|
-
const engine = new Engine();
|
|
313
|
-
await engine.init();
|
|
314
|
-
|
|
315
|
-
// Browser (explicit WASM path)
|
|
316
|
-
import { BrowserEngine } from "@hypen-space/core";
|
|
317
|
-
const engine = new BrowserEngine();
|
|
318
|
-
await engine.init({ wasmPath: "/hypen_engine_bg.wasm" });
|
|
319
|
-
```
|
|
320
|
-
|
|
321
366
|
## Requirements
|
|
322
367
|
|
|
323
368
|
- Node.js >= 18.0.0
|