@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 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