@hypen-space/core 0.2.0 → 0.2.1
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 +328 -0
- package/dist/src/components/builtin.js +89 -0
- package/dist/src/components/builtin.js.map +10 -0
- package/dist/src/discovery.js +234 -0
- package/dist/src/discovery.js.map +10 -0
- package/dist/src/engine.browser.js.map +2 -2
- package/dist/src/engine.js +2 -2
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.js +25 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader.js +77 -0
- package/dist/src/loader.js.map +10 -0
- package/package.json +19 -1
- package/src/components/builtin.ts +169 -0
- package/src/discovery.ts +416 -0
- package/src/engine.browser.ts +1 -2
- package/src/engine.ts +2 -2
- package/src/index.ts +29 -0
- package/src/loader.ts +136 -0
- package/dist/engine.d.ts +0 -101
- package/dist/events.d.ts +0 -78
- package/dist/index.browser.d.ts +0 -13
- package/dist/index.d.ts +0 -33
- package/dist/remote/index.d.ts +0 -6
- package/dist/router.d.ts +0 -93
- package/dist/state.d.ts +0 -30
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @hypen-space/core
|
|
2
|
+
|
|
3
|
+
Platform-agnostic reactive UI runtime for the Hypen declarative UI language.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hypen-space/core
|
|
9
|
+
# or
|
|
10
|
+
bun add @hypen-space/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Engine, app } from "@hypen-space/core";
|
|
17
|
+
|
|
18
|
+
// 1. Define a stateful module
|
|
19
|
+
const counter = app
|
|
20
|
+
.defineState({ count: 0 })
|
|
21
|
+
.onAction("increment", ({ state }) => state.count++)
|
|
22
|
+
.onAction("decrement", ({ state }) => state.count--)
|
|
23
|
+
.build();
|
|
24
|
+
|
|
25
|
+
// 2. Initialize the engine
|
|
26
|
+
const engine = new Engine();
|
|
27
|
+
await engine.init();
|
|
28
|
+
|
|
29
|
+
// 3. Set up render callback (you provide the renderer)
|
|
30
|
+
engine.setRenderCallback((patches) => {
|
|
31
|
+
// Apply patches to your platform (DOM, Canvas, Native, etc.)
|
|
32
|
+
myRenderer.applyPatches(patches);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 4. Register module and render
|
|
36
|
+
engine.setModule("counter", counter.actions, counter.stateKeys, counter.initialState);
|
|
37
|
+
engine.renderSource(`
|
|
38
|
+
Column {
|
|
39
|
+
Text("Count: \${state.count}")
|
|
40
|
+
Row {
|
|
41
|
+
Button(onClick: @actions.decrement) { Text("-") }
|
|
42
|
+
Button(onClick: @actions.increment) { Text("+") }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Component Discovery
|
|
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
|
|
84
|
+
|
|
85
|
+
# Sibling files
|
|
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:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { app } from "@hypen-space/core";
|
|
120
|
+
|
|
121
|
+
interface UserState {
|
|
122
|
+
user: { id: string; name: string } | null;
|
|
123
|
+
loading: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const userModule = app
|
|
127
|
+
.defineState<UserState>({ user: null, loading: false })
|
|
128
|
+
|
|
129
|
+
// Lifecycle hooks
|
|
130
|
+
.onCreated(async ({ state, next }) => {
|
|
131
|
+
state.loading = true;
|
|
132
|
+
next(); // Sync state to engine
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Action handlers
|
|
136
|
+
.onAction("loadUser", async ({ state, action, next }) => {
|
|
137
|
+
const userId = action.payload?.id;
|
|
138
|
+
state.user = await fetchUser(userId);
|
|
139
|
+
state.loading = false;
|
|
140
|
+
next();
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
.onAction("logout", ({ state, next }) => {
|
|
144
|
+
state.user = null;
|
|
145
|
+
next();
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
.onDestroyed(({ state }) => {
|
|
149
|
+
console.log("Module cleanup");
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
.build();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## State Management
|
|
156
|
+
|
|
157
|
+
Observable state with automatic change tracking:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { createObservableState, batchStateUpdates, getStateSnapshot } from "@hypen-space/core";
|
|
161
|
+
|
|
162
|
+
const state = createObservableState({ count: 0, items: [] }, {
|
|
163
|
+
onChange: (path, oldVal, newVal) => {
|
|
164
|
+
console.log(`${path.join(".")} changed: ${oldVal} -> ${newVal}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Direct mutations are tracked
|
|
169
|
+
state.count = 5;
|
|
170
|
+
state.items.push("item");
|
|
171
|
+
|
|
172
|
+
// Batch multiple updates
|
|
173
|
+
batchStateUpdates(state, () => {
|
|
174
|
+
state.count = 10;
|
|
175
|
+
state.items = ["a", "b", "c"];
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Get immutable snapshot
|
|
179
|
+
const snapshot = getStateSnapshot(state);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Routing
|
|
183
|
+
|
|
184
|
+
Built-in hash-based or pathname-based routing:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { HypenRouter } from "@hypen-space/core";
|
|
188
|
+
|
|
189
|
+
const router = new HypenRouter();
|
|
190
|
+
|
|
191
|
+
router.navigate("/products/123");
|
|
192
|
+
|
|
193
|
+
router.subscribe((state) => {
|
|
194
|
+
console.log("Route:", state.currentPath);
|
|
195
|
+
console.log("Params:", state.params);
|
|
196
|
+
console.log("Query:", state.query);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Built-in Components
|
|
201
|
+
|
|
202
|
+
Framework-provided components for routing:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Router, Route, Link } from "@hypen-space/core";
|
|
206
|
+
|
|
207
|
+
// Use in templates:
|
|
208
|
+
// Router {
|
|
209
|
+
// Route(path: "/") { HomePage }
|
|
210
|
+
// Route(path: "/products") { ProductList }
|
|
211
|
+
// }
|
|
212
|
+
// Link(to: "/products") { Text("View Products") }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Remote UI (WebSocket)
|
|
216
|
+
|
|
217
|
+
Connect to a remote Hypen server:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { RemoteEngine } from "@hypen-space/core";
|
|
221
|
+
|
|
222
|
+
const remote = new RemoteEngine("ws://localhost:3000", {
|
|
223
|
+
autoReconnect: true,
|
|
224
|
+
reconnectInterval: 3000,
|
|
225
|
+
maxReconnectAttempts: 10,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
remote
|
|
229
|
+
.onPatches((patches) => renderer.applyPatches(patches))
|
|
230
|
+
.onStateUpdate((state) => console.log("Server state:", state))
|
|
231
|
+
.onConnect(() => console.log("Connected"))
|
|
232
|
+
.onDisconnect(() => console.log("Disconnected"))
|
|
233
|
+
.onError((error) => console.error("Error:", error));
|
|
234
|
+
|
|
235
|
+
await remote.connect();
|
|
236
|
+
remote.dispatchAction("loadData", { page: 1 });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Global Context
|
|
240
|
+
|
|
241
|
+
Cross-module communication:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { HypenGlobalContext } from "@hypen-space/core";
|
|
245
|
+
|
|
246
|
+
const context = new HypenGlobalContext();
|
|
247
|
+
|
|
248
|
+
context.registerModule("auth", authModuleInstance);
|
|
249
|
+
context.registerModule("cart", cartModuleInstance);
|
|
250
|
+
|
|
251
|
+
const auth = context.getModule("auth");
|
|
252
|
+
const cartState = context.getModule("cart").getState();
|
|
253
|
+
|
|
254
|
+
// Event system
|
|
255
|
+
context.on("userLoggedIn", (user) => console.log("Logged in:", user));
|
|
256
|
+
context.emit("userLoggedIn", { id: "1", name: "Ian" });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Custom Renderer
|
|
260
|
+
|
|
261
|
+
Implement the `Renderer` interface for any platform:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { BaseRenderer, type Patch } from "@hypen-space/core";
|
|
265
|
+
|
|
266
|
+
class MyRenderer extends BaseRenderer {
|
|
267
|
+
protected onCreate(id: string, type: string, props: Record<string, any>) {
|
|
268
|
+
// Create element
|
|
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
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Exports
|
|
289
|
+
|
|
290
|
+
| Export | Description |
|
|
291
|
+
|--------|-------------|
|
|
292
|
+
| `@hypen-space/core` | Main entry - Engine, app, state, router, discovery, loader |
|
|
293
|
+
| `@hypen-space/core/engine` | Low-level WASM engine API |
|
|
294
|
+
| `@hypen-space/core/engine/browser` | Browser-optimized engine (explicit WASM init) |
|
|
295
|
+
| `@hypen-space/core/app` | Module builder and HypenModuleInstance |
|
|
296
|
+
| `@hypen-space/core/state` | Observable state utilities |
|
|
297
|
+
| `@hypen-space/core/renderer` | Abstract renderer interface |
|
|
298
|
+
| `@hypen-space/core/router` | Routing system |
|
|
299
|
+
| `@hypen-space/core/events` | Typed event emitter |
|
|
300
|
+
| `@hypen-space/core/context` | Global context |
|
|
301
|
+
| `@hypen-space/core/remote` | Remote UI protocol |
|
|
302
|
+
| `@hypen-space/core/remote/client` | WebSocket client |
|
|
303
|
+
| `@hypen-space/core/loader` | Component loader |
|
|
304
|
+
| `@hypen-space/core/discovery` | Component discovery |
|
|
305
|
+
| `@hypen-space/core/components` | Built-in Router, Route, Link |
|
|
306
|
+
|
|
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
|
+
## Requirements
|
|
322
|
+
|
|
323
|
+
- Node.js >= 18.0.0
|
|
324
|
+
- TypeScript 5+ (optional)
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
app
|
|
3
|
+
} from "../app.js";
|
|
4
|
+
import"../state.js";
|
|
5
|
+
import"../../chunk-5va59f7m.js";
|
|
6
|
+
|
|
7
|
+
// src/components/builtin.ts
|
|
8
|
+
var Router = app.defineState({
|
|
9
|
+
currentPath: "/",
|
|
10
|
+
matchedRoute: null,
|
|
11
|
+
routeParams: {}
|
|
12
|
+
}, { name: "__Router" }).onCreated((state, context) => {
|
|
13
|
+
if (!context) {
|
|
14
|
+
console.error("[Router] Requires global context");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const router = context.__router;
|
|
18
|
+
if (!router) {
|
|
19
|
+
console.error("[Router] Router not found in context");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const hypenEngine = context.__hypenEngine;
|
|
23
|
+
const updateRouteVisibility = async (currentPath) => {
|
|
24
|
+
const routeElements = document.querySelectorAll('[data-hypen-type="route"]');
|
|
25
|
+
let matchFound = false;
|
|
26
|
+
for (let index = 0;index < routeElements.length; index++) {
|
|
27
|
+
const routeEl = routeElements[index];
|
|
28
|
+
const htmlEl = routeEl;
|
|
29
|
+
const routePath = htmlEl.dataset.routePath || "/";
|
|
30
|
+
const isMatch = routePath === currentPath;
|
|
31
|
+
htmlEl.style.display = isMatch ? "flex" : "none";
|
|
32
|
+
if (isMatch) {
|
|
33
|
+
matchFound = true;
|
|
34
|
+
const componentName = htmlEl.dataset.routeComponent;
|
|
35
|
+
const isLazy = htmlEl.dataset.routeLazy === "true";
|
|
36
|
+
const hasContent = htmlEl.children.length > 0;
|
|
37
|
+
if (componentName && !hasContent && hypenEngine) {
|
|
38
|
+
try {
|
|
39
|
+
await hypenEngine.renderLazyRoute(routePath, componentName, htmlEl);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`[Router] Failed to render route ${routePath}:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!matchFound) {
|
|
47
|
+
console.warn(`[Router] No route matched path: ${currentPath}. Available routes:`, Array.from(routeElements).map((el) => el.dataset.routePath));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
updateRouteVisibility(state.currentPath);
|
|
52
|
+
}, 100);
|
|
53
|
+
router.onNavigate((routeState) => {
|
|
54
|
+
state.currentPath = routeState.currentPath;
|
|
55
|
+
state.routeParams = routeState.params;
|
|
56
|
+
updateRouteVisibility(routeState.currentPath);
|
|
57
|
+
});
|
|
58
|
+
}).build();
|
|
59
|
+
var Route = app.defineState({}, { name: "__Route" }).build();
|
|
60
|
+
var Link = app.defineState({
|
|
61
|
+
to: "/",
|
|
62
|
+
isActive: false
|
|
63
|
+
}, { name: "__Link" }).onAction("navigate", ({ state, context }) => {
|
|
64
|
+
const router = context?.__router;
|
|
65
|
+
if (!router) {
|
|
66
|
+
console.error("[Link] Requires router context");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const targetPath = state.to;
|
|
70
|
+
router.push(targetPath);
|
|
71
|
+
}).onCreated((state, context) => {
|
|
72
|
+
if (!context)
|
|
73
|
+
return;
|
|
74
|
+
const router = context.__router;
|
|
75
|
+
if (router) {
|
|
76
|
+
router.onNavigate((routeState) => {
|
|
77
|
+
state.isActive = routeState.currentPath === state.to;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}).build();
|
|
81
|
+
export {
|
|
82
|
+
Router,
|
|
83
|
+
Route,
|
|
84
|
+
Link
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { Router, Route, Link };
|
|
88
|
+
|
|
89
|
+
//# debugId=DCABC4D99E5064EE64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/components/builtin.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Built-in Hypen Components\n * Framework-provided components like Router and Route\n */\n\nimport { app } from \"../app.js\";\nimport type { HypenRouter } from \"../router.js\";\n\n/**\n * Router Component\n * Automatically matches routes and renders the appropriate child Route\n *\n * Usage in Hypen DSL:\n * Router {\n * Route(path: \"/\") { HomePage }\n * Route(path: \"/products\") { ProductList }\n * Route(path: \"/users/:id\") { UserProfile }\n * }\n */\nexport const Router = app\n .defineState(\n {\n currentPath: \"/\",\n matchedRoute: null as any,\n routeParams: {} as Record<string, string>,\n },\n { name: \"__Router\" }\n )\n .onCreated((state, context) => {\n if (!context) {\n console.error(\"[Router] Requires global context\");\n return;\n }\n\n // Subscribe to router changes\n const router = (context as any).__router as HypenRouter;\n if (!router) {\n console.error(\"[Router] Router not found in context\");\n return;\n }\n\n // Get access to the Hypen engine for lazy rendering\n const hypenEngine = (context as any).__hypenEngine;\n\n // Function to render only the matching route\n const updateRouteVisibility = async (currentPath: string) => {\n // Find all Route elements in the DOM\n const routeElements = document.querySelectorAll(\n '[data-hypen-type=\"route\"]'\n );\n\n let matchFound = false;\n\n for (let index = 0; index < routeElements.length; index++) {\n const routeEl = routeElements[index];\n const htmlEl = routeEl as HTMLElement;\n const routePath = htmlEl.dataset.routePath || \"/\";\n\n // Simple path matching (exact match for now)\n const isMatch = routePath === currentPath;\n\n // Only show the matching route, hide all others\n htmlEl.style.display = isMatch ? \"flex\" : \"none\";\n\n if (isMatch) {\n matchFound = true;\n\n // Check if this route needs to be rendered\n const componentName = htmlEl.dataset.routeComponent;\n const isLazy = htmlEl.dataset.routeLazy === \"true\";\n const hasContent = htmlEl.children.length > 0;\n\n // Render if route has no content and has a component name\n if (componentName && !hasContent && hypenEngine) {\n try {\n await hypenEngine.renderLazyRoute(\n routePath,\n componentName,\n htmlEl\n );\n } catch (err) {\n console.error(`[Router] Failed to render route ${routePath}:`, err);\n }\n }\n }\n }\n\n if (!matchFound) {\n console.warn(\n `[Router] No route matched path: ${currentPath}. Available routes:`,\n Array.from(routeElements).map(\n (el: Element) => (el as HTMLElement).dataset.routePath\n )\n );\n }\n };\n\n // Initial route visibility (after DOM is ready)\n setTimeout(() => {\n updateRouteVisibility(state.currentPath);\n }, 100);\n\n router.onNavigate((routeState) => {\n state.currentPath = routeState.currentPath;\n state.routeParams = routeState.params;\n\n // Update route visibility when path changes\n updateRouteVisibility(routeState.currentPath);\n });\n })\n .build();\n\n/**\n * Route Component\n * Defines a route pattern and its content\n * This is just a marker component - Router processes it\n *\n * Props:\n * - path: string - Route pattern (e.g., \"/\", \"/users/:id\", \"/dashboard/*\")\n *\n * Usage:\n * Route(path: \"/products\") {\n * ProductList\n * }\n */\nexport const Route = app.defineState({}, { name: \"__Route\" }).build();\n\n/**\n * Navigation Link Component\n * Navigates to a route when clicked\n *\n * Props:\n * - to: string - Target path\n *\n * Usage:\n * Link(to: \"/products\") {\n * Text(\"View Products\")\n * }\n */\nexport const Link = app\n .defineState(\n {\n to: \"/\",\n isActive: false,\n },\n { name: \"__Link\" }\n )\n .onAction(\"navigate\", ({ state, context }) => {\n const router = (context as any)?.__router as HypenRouter;\n if (!router) {\n console.error(\"[Link] Requires router context\");\n return;\n }\n\n const targetPath = state.to;\n router.push(targetPath);\n })\n .onCreated((state, context) => {\n if (!context) return;\n\n // Check if current path matches this link's target\n const router = (context as any).__router as HypenRouter;\n if (router) {\n router.onNavigate((routeState) => {\n state.isActive = routeState.currentPath === state.to;\n });\n }\n })\n .build();\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;AAmBO,IAAM,SAAS,IACnB,YACC;AAAA,EACE,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa,CAAC;AAChB,GACA,EAAE,MAAM,WAAW,CACrB,EACC,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC,SAAS;AAAA,IACZ,QAAQ,MAAM,kCAAkC;AAAA,IAChD;AAAA,EACF;AAAA,EAGA,MAAM,SAAU,QAAgB;AAAA,EAChC,IAAI,CAAC,QAAQ;AAAA,IACX,QAAQ,MAAM,sCAAsC;AAAA,IACpD;AAAA,EACF;AAAA,EAGA,MAAM,cAAe,QAAgB;AAAA,EAGrC,MAAM,wBAAwB,OAAO,gBAAwB;AAAA,IAE3D,MAAM,gBAAgB,SAAS,iBAC7B,2BACF;AAAA,IAEA,IAAI,aAAa;AAAA,IAEjB,SAAS,QAAQ,EAAG,QAAQ,cAAc,QAAQ,SAAS;AAAA,MACzD,MAAM,UAAU,cAAc;AAAA,MAC9B,MAAM,SAAS;AAAA,MACf,MAAM,YAAY,OAAO,QAAQ,aAAa;AAAA,MAG9C,MAAM,UAAU,cAAc;AAAA,MAG9B,OAAO,MAAM,UAAU,UAAU,SAAS;AAAA,MAE1C,IAAI,SAAS;AAAA,QACX,aAAa;AAAA,QAGb,MAAM,gBAAgB,OAAO,QAAQ;AAAA,QACrC,MAAM,SAAS,OAAO,QAAQ,cAAc;AAAA,QAC5C,MAAM,aAAa,OAAO,SAAS,SAAS;AAAA,QAG5C,IAAI,iBAAiB,CAAC,cAAc,aAAa;AAAA,UAC/C,IAAI;AAAA,YACF,MAAM,YAAY,gBAChB,WACA,eACA,MACF;AAAA,YACA,OAAO,KAAK;AAAA,YACZ,QAAQ,MAAM,mCAAmC,cAAc,GAAG;AAAA;AAAA,QAEtE;AAAA,MACF;AAAA,IACF;AAAA,IAEA,IAAI,CAAC,YAAY;AAAA,MACf,QAAQ,KACN,mCAAmC,kCACnC,MAAM,KAAK,aAAa,EAAE,IACxB,CAAC,OAAiB,GAAmB,QAAQ,SAC/C,CACF;AAAA,IACF;AAAA;AAAA,EAIF,WAAW,MAAM;AAAA,IACf,sBAAsB,MAAM,WAAW;AAAA,KACtC,GAAG;AAAA,EAEN,OAAO,WAAW,CAAC,eAAe;AAAA,IAChC,MAAM,cAAc,WAAW;AAAA,IAC/B,MAAM,cAAc,WAAW;AAAA,IAG/B,sBAAsB,WAAW,WAAW;AAAA,GAC7C;AAAA,CACF,EACA,MAAM;AAeF,IAAM,QAAQ,IAAI,YAAY,CAAC,GAAG,EAAE,MAAM,UAAU,CAAC,EAAE,MAAM;AAc7D,IAAM,OAAO,IACjB,YACC;AAAA,EACE,IAAI;AAAA,EACJ,UAAU;AACZ,GACA,EAAE,MAAM,SAAS,CACnB,EACC,SAAS,YAAY,GAAG,OAAO,cAAc;AAAA,EAC5C,MAAM,SAAU,SAAiB;AAAA,EACjC,IAAI,CAAC,QAAQ;AAAA,IACX,QAAQ,MAAM,gCAAgC;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,MAAM;AAAA,EACzB,OAAO,KAAK,UAAU;AAAA,CACvB,EACA,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC;AAAA,IAAS;AAAA,EAGd,MAAM,SAAU,QAAgB;AAAA,EAChC,IAAI,QAAQ;AAAA,IACV,OAAO,WAAW,CAAC,eAAe;AAAA,MAChC,MAAM,WAAW,WAAW,gBAAgB,MAAM;AAAA,KACnD;AAAA,EACH;AAAA,CACD,EACA,MAAM;",
|
|
8
|
+
"debugId": "DCABC4D99E5064EE64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require,
|
|
3
|
+
__toESM
|
|
4
|
+
} from "../chunk-5va59f7m.js";
|
|
5
|
+
|
|
6
|
+
// src/discovery.ts
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, watch } from "fs";
|
|
8
|
+
import { join, basename, resolve } from "path";
|
|
9
|
+
function removeImports(text) {
|
|
10
|
+
return text.replace(/import\s+(?:\{[^}]+\}|\w+)\s+from\s+["'][^"']+["']\s*/g, "");
|
|
11
|
+
}
|
|
12
|
+
async function discoverComponents(baseDir, options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
patterns = ["folder", "sibling", "index"],
|
|
15
|
+
recursive = false,
|
|
16
|
+
debug = false
|
|
17
|
+
} = options;
|
|
18
|
+
const log = debug ? (...args) => console.log("[discovery]", ...args) : () => {};
|
|
19
|
+
const resolvedDir = resolve(baseDir);
|
|
20
|
+
const components = [];
|
|
21
|
+
const seen = new Set;
|
|
22
|
+
log("Scanning directory:", resolvedDir);
|
|
23
|
+
log("Patterns:", patterns);
|
|
24
|
+
const addComponent = (name, hypenPath, modulePath) => {
|
|
25
|
+
if (seen.has(name)) {
|
|
26
|
+
log(`Skipping duplicate: ${name}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
seen.add(name);
|
|
30
|
+
const templateRaw = readFileSync(hypenPath, "utf-8");
|
|
31
|
+
const template = removeImports(templateRaw).trim();
|
|
32
|
+
components.push({
|
|
33
|
+
name,
|
|
34
|
+
hypenPath,
|
|
35
|
+
modulePath,
|
|
36
|
+
template,
|
|
37
|
+
hasModule: modulePath !== null
|
|
38
|
+
});
|
|
39
|
+
log(`Found: ${name} (${modulePath ? "with module" : "stateless"})`);
|
|
40
|
+
};
|
|
41
|
+
const scanForFolderComponents = (dir) => {
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
return;
|
|
44
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory())
|
|
47
|
+
continue;
|
|
48
|
+
const folderPath = join(dir, entry.name);
|
|
49
|
+
const componentName = entry.name;
|
|
50
|
+
if (patterns.includes("folder")) {
|
|
51
|
+
const hypenPath = join(folderPath, "component.hypen");
|
|
52
|
+
if (existsSync(hypenPath)) {
|
|
53
|
+
const modulePath = join(folderPath, "component.ts");
|
|
54
|
+
addComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (patterns.includes("index")) {
|
|
59
|
+
const hypenPath = join(folderPath, "index.hypen");
|
|
60
|
+
if (existsSync(hypenPath)) {
|
|
61
|
+
const modulePath = join(folderPath, "index.ts");
|
|
62
|
+
addComponent(componentName, hypenPath, existsSync(modulePath) ? modulePath : null);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (recursive) {
|
|
67
|
+
scanForFolderComponents(folderPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const scanForSiblingComponents = (dir) => {
|
|
72
|
+
if (!existsSync(dir))
|
|
73
|
+
return;
|
|
74
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (recursive) {
|
|
78
|
+
scanForSiblingComponents(join(dir, entry.name));
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!entry.name.endsWith(".hypen"))
|
|
83
|
+
continue;
|
|
84
|
+
const hypenPath = join(dir, entry.name);
|
|
85
|
+
const baseName = basename(entry.name, ".hypen");
|
|
86
|
+
if (baseName === "component" || baseName === "index")
|
|
87
|
+
continue;
|
|
88
|
+
const modulePath = join(dir, `${baseName}.ts`);
|
|
89
|
+
addComponent(baseName, hypenPath, existsSync(modulePath) ? modulePath : null);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
if (patterns.includes("folder") || patterns.includes("index")) {
|
|
93
|
+
scanForFolderComponents(resolvedDir);
|
|
94
|
+
}
|
|
95
|
+
if (patterns.includes("sibling")) {
|
|
96
|
+
scanForSiblingComponents(resolvedDir);
|
|
97
|
+
}
|
|
98
|
+
log(`Discovered ${components.length} components`);
|
|
99
|
+
return components;
|
|
100
|
+
}
|
|
101
|
+
async function loadDiscoveredComponents(components) {
|
|
102
|
+
const { app } = await import("./app.js");
|
|
103
|
+
const loaded = new Map;
|
|
104
|
+
for (const component of components) {
|
|
105
|
+
let module;
|
|
106
|
+
if (component.modulePath) {
|
|
107
|
+
const moduleExport = await import(component.modulePath);
|
|
108
|
+
module = moduleExport.default;
|
|
109
|
+
} else {
|
|
110
|
+
module = app.defineState({}).build();
|
|
111
|
+
}
|
|
112
|
+
loaded.set(component.name, {
|
|
113
|
+
name: component.name,
|
|
114
|
+
module,
|
|
115
|
+
template: component.template
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return loaded;
|
|
119
|
+
}
|
|
120
|
+
function watchComponents(baseDir, options = {}) {
|
|
121
|
+
const resolvedDir = resolve(baseDir);
|
|
122
|
+
const {
|
|
123
|
+
onChange,
|
|
124
|
+
onAdd,
|
|
125
|
+
onRemove,
|
|
126
|
+
onUpdate,
|
|
127
|
+
debug = false,
|
|
128
|
+
...discoveryOptions
|
|
129
|
+
} = options;
|
|
130
|
+
const log = debug ? (...args) => console.log("[discovery:watch]", ...args) : () => {};
|
|
131
|
+
let currentComponents = new Map;
|
|
132
|
+
let debounceTimer = null;
|
|
133
|
+
const initialScan = async () => {
|
|
134
|
+
const components = await discoverComponents(resolvedDir, discoveryOptions);
|
|
135
|
+
currentComponents = new Map(components.map((c) => [c.name, c]));
|
|
136
|
+
onChange?.(components);
|
|
137
|
+
};
|
|
138
|
+
const rescan = async () => {
|
|
139
|
+
if (debounceTimer) {
|
|
140
|
+
clearTimeout(debounceTimer);
|
|
141
|
+
}
|
|
142
|
+
debounceTimer = setTimeout(async () => {
|
|
143
|
+
log("Rescanning...");
|
|
144
|
+
const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
|
|
145
|
+
const newMap = new Map(newComponents.map((c) => [c.name, c]));
|
|
146
|
+
for (const [name, component] of newMap) {
|
|
147
|
+
const existing = currentComponents.get(name);
|
|
148
|
+
if (!existing) {
|
|
149
|
+
log("Added:", name);
|
|
150
|
+
onAdd?.(component);
|
|
151
|
+
} else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
|
|
152
|
+
log("Updated:", name);
|
|
153
|
+
onUpdate?.(component);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const name of currentComponents.keys()) {
|
|
157
|
+
if (!newMap.has(name)) {
|
|
158
|
+
log("Removed:", name);
|
|
159
|
+
onRemove?.(name);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
currentComponents = newMap;
|
|
163
|
+
onChange?.(newComponents);
|
|
164
|
+
}, 100);
|
|
165
|
+
};
|
|
166
|
+
const watcher = watch(resolvedDir, { recursive: true }, (event, filename) => {
|
|
167
|
+
if (!filename)
|
|
168
|
+
return;
|
|
169
|
+
if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
|
|
170
|
+
log("File changed:", filename);
|
|
171
|
+
rescan();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
initialScan();
|
|
175
|
+
return {
|
|
176
|
+
stop: () => {
|
|
177
|
+
watcher.close();
|
|
178
|
+
if (debounceTimer) {
|
|
179
|
+
clearTimeout(debounceTimer);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function generateComponentsCode(baseDir, options = {}) {
|
|
185
|
+
const components = await discoverComponents(baseDir, options);
|
|
186
|
+
const resolvedDir = resolve(baseDir);
|
|
187
|
+
let code = `/**
|
|
188
|
+
* Auto-generated component imports
|
|
189
|
+
* Generated by @hypen-space/core discovery
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
`;
|
|
193
|
+
for (const component of components) {
|
|
194
|
+
const relativePath = component.modulePath ? "./" + component.modulePath.replace(resolvedDir + "/", "").replace(/\.ts$/, ".js") : null;
|
|
195
|
+
if (relativePath) {
|
|
196
|
+
code += `import ${component.name}Module from "${relativePath}";
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
code += `
|
|
201
|
+
import { app } from "@hypen-space/core";
|
|
202
|
+
|
|
203
|
+
`;
|
|
204
|
+
for (const component of components) {
|
|
205
|
+
const templateJson = JSON.stringify(component.template);
|
|
206
|
+
if (component.hasModule) {
|
|
207
|
+
code += `export const ${component.name} = {
|
|
208
|
+
module: ${component.name}Module,
|
|
209
|
+
template: ${templateJson},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
`;
|
|
213
|
+
} else {
|
|
214
|
+
code += `const ${component.name}Module = app.defineState({}).build();
|
|
215
|
+
export const ${component.name} = {
|
|
216
|
+
module: ${component.name}Module,
|
|
217
|
+
template: ${templateJson},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return code;
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
watchComponents,
|
|
227
|
+
loadDiscoveredComponents,
|
|
228
|
+
generateComponentsCode,
|
|
229
|
+
discoverComponents
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export { discoverComponents, loadDiscoveredComponents, watchComponents, generateComponentsCode };
|
|
233
|
+
|
|
234
|
+
//# debugId=450977873A4D92DA64756E2164756E21
|