@hypen-space/core 0.4.37 → 0.4.38

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.
Files changed (80) hide show
  1. package/README.md +13 -14
  2. package/dist/app.js +289 -227
  3. package/dist/app.js.map +5 -5
  4. package/dist/components/builtin.js +289 -227
  5. package/dist/components/builtin.js.map +5 -5
  6. package/dist/context.js +60 -64
  7. package/dist/context.js.map +2 -2
  8. package/dist/datasource.js +80 -0
  9. package/dist/datasource.js.map +10 -0
  10. package/dist/disposable.js +60 -63
  11. package/dist/disposable.js.map +2 -2
  12. package/dist/events.js +60 -63
  13. package/dist/events.js.map +2 -2
  14. package/dist/hypen.js +78 -0
  15. package/dist/hypen.js.map +10 -0
  16. package/dist/index.browser.d.ts +2 -1
  17. package/dist/index.browser.js +303 -244
  18. package/dist/index.browser.js.map +6 -7
  19. package/dist/index.d.ts +0 -3
  20. package/dist/index.js +492 -180
  21. package/dist/index.js.map +6 -6
  22. package/dist/logger.js +60 -64
  23. package/dist/logger.js.map +2 -2
  24. package/dist/remote/client.js +243 -158
  25. package/dist/remote/client.js.map +6 -6
  26. package/dist/remote/index.d.ts +6 -5
  27. package/dist/remote/index.js +241 -1986
  28. package/dist/remote/index.js.map +6 -13
  29. package/dist/remote/session.js +151 -0
  30. package/dist/remote/session.js.map +10 -0
  31. package/dist/renderer.js +60 -63
  32. package/dist/renderer.js.map +2 -2
  33. package/dist/result.js +220 -0
  34. package/dist/result.js.map +10 -0
  35. package/dist/retry.js +329 -0
  36. package/dist/retry.js.map +11 -0
  37. package/dist/router.js +62 -70
  38. package/dist/router.js.map +2 -2
  39. package/dist/state.js +3 -8
  40. package/dist/state.js.map +2 -2
  41. package/package.json +11 -56
  42. package/src/index.browser.ts +5 -4
  43. package/src/index.ts +10 -23
  44. package/src/remote/index.ts +9 -5
  45. package/dist/discovery.d.ts +0 -90
  46. package/dist/discovery.js +0 -1334
  47. package/dist/discovery.js.map +0 -15
  48. package/dist/engine.browser.d.ts +0 -116
  49. package/dist/engine.browser.js +0 -479
  50. package/dist/engine.browser.js.map +0 -12
  51. package/dist/engine.d.ts +0 -107
  52. package/dist/engine.js +0 -543
  53. package/dist/engine.js.map +0 -12
  54. package/dist/loader.d.ts +0 -51
  55. package/dist/loader.js +0 -292
  56. package/dist/loader.js.map +0 -11
  57. package/dist/plugin.d.ts +0 -39
  58. package/dist/plugin.js +0 -685
  59. package/dist/plugin.js.map +0 -12
  60. package/dist/remote/server.d.ts +0 -188
  61. package/dist/remote/server.js +0 -2270
  62. package/dist/remote/server.js.map +0 -19
  63. package/src/discovery.ts +0 -527
  64. package/src/engine.browser.ts +0 -302
  65. package/src/engine.ts +0 -282
  66. package/src/loader.ts +0 -136
  67. package/src/plugin.ts +0 -220
  68. package/src/remote/server.ts +0 -879
  69. package/wasm-browser/README.md +0 -594
  70. package/wasm-browser/hypen_engine.d.ts +0 -389
  71. package/wasm-browser/hypen_engine.js +0 -1070
  72. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  73. package/wasm-browser/hypen_engine_bg.wasm.d.ts +0 -37
  74. package/wasm-browser/package.json +0 -24
  75. package/wasm-node/README.md +0 -594
  76. package/wasm-node/hypen_engine.d.ts +0 -327
  77. package/wasm-node/hypen_engine.js +0 -979
  78. package/wasm-node/hypen_engine_bg.wasm +0 -0
  79. package/wasm-node/hypen_engine_bg.wasm.d.ts +0 -37
  80. package/wasm-node/package.json +0 -22
@@ -1,302 +0,0 @@
1
- /**
2
- * Browser-compatible wrapper around the WASM engine
3
- * Uses web target for browser environments
4
- */
5
-
6
- import { frameworkLoggers } from "./logger.js";
7
- import { classifyEngineError } from "./result.js";
8
- import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
9
-
10
- // Re-export types so existing consumers of "./engine.browser.js" still work
11
- export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
12
-
13
- const log = frameworkLoggers.engine;
14
-
15
- // Dynamic import path - will be configured at build time
16
- // For browser, the WASM needs to be served and initialized explicitly
17
- let wasmInit: ((path?: string) => Promise<void>) | null = null;
18
- let WasmEngineClass: any = null;
19
-
20
- export interface EngineInitOptions {
21
- /**
22
- * URL to the WASM binary file.
23
- * Default: loads from unpkg CDN
24
- * For production, consider serving from your own domain for better performance.
25
- */
26
- wasmUrl?: string;
27
-
28
- /**
29
- * URL to the WASM JS glue code.
30
- * Default: loads from unpkg CDN
31
- * For self-hosting, point to your own copy of hypen_engine.js
32
- */
33
- jsUrl?: string;
34
- }
35
-
36
- /**
37
- * Recursively convert Maps and nested structures to plain objects
38
- */
39
- function mapToObject(value: any): any {
40
- if (value instanceof Map) {
41
- const obj: Record<string, any> = {};
42
- for (const [key, val] of value.entries()) {
43
- obj[key] = mapToObject(val);
44
- }
45
- return obj;
46
- } else if (Array.isArray(value)) {
47
- return value.map(mapToObject);
48
- } else if (value && typeof value === 'object' && value.constructor === Object) {
49
- const obj: Record<string, any> = {};
50
- for (const [key, val] of Object.entries(value)) {
51
- obj[key] = mapToObject(val);
52
- }
53
- return obj;
54
- }
55
- return value;
56
- }
57
-
58
- /**
59
- * Engine wraps the WASM engine and provides a TypeScript-friendly API
60
- * Browser version with explicit WASM initialization
61
- */
62
- export class Engine {
63
- private wasmEngine: any = null;
64
- private initialized = false;
65
-
66
- /**
67
- * Initialize the WASM module
68
- * @param options - Initialization options including wasmUrl and jsUrl
69
- */
70
- async init(options: EngineInitOptions = {}): Promise<void> {
71
- if (this.initialized) return;
72
-
73
- // Default to CDN for zero-config experience
74
- const cdnBase = "https://unpkg.com/@hypen-space/core@latest/wasm-browser";
75
- const jsUrl = options.jsUrl ?? `${cdnBase}/hypen_engine.js`;
76
- const wasmUrl = options.wasmUrl ?? `${cdnBase}/hypen_engine_bg.wasm`;
77
-
78
- // Dynamically import the WASM JS glue code from CDN (or custom URL)
79
- // Using dynamic import with variable URL to avoid bundler resolution
80
- try {
81
- const wasmModule: any = await import(/* @vite-ignore */ jsUrl);
82
- wasmInit = wasmModule.default;
83
- WasmEngineClass = wasmModule.WasmEngine;
84
-
85
- // Initialize WASM with explicit path
86
- await wasmInit!(wasmUrl);
87
-
88
- this.wasmEngine = new WasmEngineClass();
89
- this.initialized = true;
90
- } catch (error) {
91
- log.error("Failed to initialize WASM engine:", error);
92
- throw error;
93
- }
94
- }
95
-
96
- /**
97
- * Ensure the engine is initialized before operations
98
- */
99
- private ensureInitialized(): any {
100
- if (!this.wasmEngine) {
101
- throw new Error("Engine not initialized. Call init() first.");
102
- }
103
- return this.wasmEngine;
104
- }
105
-
106
- /**
107
- * Set the render callback that receives patches
108
- */
109
- setRenderCallback(callback: RenderCallback): void {
110
- const engine = this.ensureInitialized();
111
- engine.setRenderCallback((patches: Patch[]) => {
112
- callback(patches);
113
- });
114
- }
115
-
116
- /**
117
- * Set the component resolver for dynamic component composition
118
- */
119
- setComponentResolver(resolver: ComponentResolver): void {
120
- const engine = this.ensureInitialized();
121
- engine.setComponentResolver((componentName: string, contextPath: string | null) => {
122
- const result = resolver(componentName, contextPath);
123
- return result;
124
- });
125
- }
126
-
127
- /**
128
- * Parse and render Hypen DSL source code
129
- * @throws {ParseError} if the source fails to parse
130
- * @throws {RenderError} if rendering fails
131
- */
132
- renderSource(source: string): void {
133
- const engine = this.ensureInitialized();
134
- try {
135
- engine.renderSource(source);
136
- } catch (err) {
137
- throw classifyEngineError(err);
138
- }
139
- }
140
-
141
- /**
142
- * Render a lazy component (for lazy route loading)
143
- */
144
- renderLazyComponent(source: string): void {
145
- const engine = this.ensureInitialized();
146
- engine.renderLazyComponent(source);
147
- }
148
-
149
- /**
150
- * Render a component into a specific parent node (subtree rendering)
151
- * @throws {ParseError} if the source fails to parse
152
- * @throws {RenderError} if the parent node is not found or rendering fails
153
- */
154
- renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
155
- const engine = this.ensureInitialized();
156
- const safeState = JSON.parse(JSON.stringify(state));
157
- try {
158
- engine.renderInto(source, parentNodeId, safeState);
159
- } catch (err) {
160
- throw classifyEngineError(err);
161
- }
162
- }
163
-
164
- /**
165
- * Notify the engine of state changes using sparse updates
166
- * @throws {StateError} if the state patch is invalid
167
- */
168
- notifyStateChange(paths: string[], values: Record<string, any>): void {
169
- const engine = this.ensureInitialized();
170
-
171
- if (paths.length === 0) {
172
- return;
173
- }
174
-
175
- const plainValues = JSON.parse(JSON.stringify(values));
176
- try {
177
- engine.updateStateSparse(paths, plainValues);
178
- } catch (err) {
179
- throw classifyEngineError(err);
180
- }
181
- log.debug("State changed (sparse):", paths);
182
- }
183
-
184
- /**
185
- * Notify the engine of state changes using full state replacement
186
- * @deprecated Use notifyStateChange with sparse values instead
187
- */
188
- notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
189
- const engine = this.ensureInitialized();
190
- const plainObject = JSON.parse(JSON.stringify(currentState));
191
- engine.updateState(plainObject);
192
-
193
- if (paths.length > 0) {
194
- log.debug("State changed (full):", paths);
195
- }
196
- }
197
-
198
- /**
199
- * Update state (triggers re-render of affected nodes)
200
- * @deprecated Use notifyStateChange instead
201
- */
202
- updateState(statePatch: Record<string, any>): void {
203
- const engine = this.ensureInitialized();
204
- const plainObject = JSON.parse(JSON.stringify(statePatch));
205
- engine.updateState(plainObject);
206
- }
207
-
208
- /**
209
- * Dispatch an action
210
- * @throws {HypenError} if the action dispatch fails
211
- */
212
- dispatchAction(name: string, payload?: any): void {
213
- const engine = this.ensureInitialized();
214
- log.debug(`Action dispatched: ${name}`);
215
- try {
216
- engine.dispatchAction(name, payload ?? null);
217
- } catch (err) {
218
- throw classifyEngineError(err);
219
- }
220
- }
221
-
222
- /**
223
- * Register an action handler
224
- */
225
- onAction(actionName: string, handler: ActionHandler): void {
226
- const engine = this.ensureInitialized();
227
- engine.onAction(actionName, (action: Action) => {
228
- const normalizedAction: Action = {
229
- ...action,
230
- payload: action.payload ? mapToObject(action.payload) : action.payload,
231
- };
232
- Promise.resolve(handler(normalizedAction)).catch((err) => log.error("Action handler error:", err));
233
- });
234
- }
235
-
236
- /**
237
- * Initialize a module
238
- */
239
- setModule(
240
- name: string,
241
- actions: string[],
242
- stateKeys: string[],
243
- initialState: Record<string, any>
244
- ): void {
245
- const engine = this.ensureInitialized();
246
- engine.setModule(name, actions, stateKeys, initialState);
247
- }
248
-
249
- /**
250
- * Get the current revision number
251
- */
252
- getRevision(): bigint {
253
- const engine = this.ensureInitialized();
254
- return engine.getRevision();
255
- }
256
-
257
- /**
258
- * Clear the engine tree
259
- */
260
- clearTree(): void {
261
- const engine = this.ensureInitialized();
262
- engine.clearTree();
263
- }
264
-
265
- /**
266
- * Debug method to inspect parsed components
267
- */
268
- debugParseComponent(source: string): string {
269
- const engine = this.ensureInitialized();
270
- return engine.debugParseComponent(source);
271
- }
272
-
273
- // ── Data Source Context ────────────────────────────────────────
274
-
275
- /**
276
- * Set (or replace) a named data source context.
277
- *
278
- * Registers the provider in the dependency graph, stores the data,
279
- * and re-renders every node bound to `$name.*`.
280
- *
281
- * @param name - Provider name (e.g., "spacetime", "firebase")
282
- * @param data - Full state object for this provider
283
- */
284
- setContext(name: string, data: Record<string, unknown>): void {
285
- const engine = this.ensureInitialized();
286
- const plainData = JSON.parse(JSON.stringify(data));
287
- try {
288
- engine.setContext(name, plainData);
289
- } catch (err) {
290
- throw classifyEngineError(err);
291
- }
292
- }
293
-
294
- /**
295
- * Remove a data source context entirely.
296
- * Drops the provider's state and re-renders bound nodes (they resolve to null).
297
- */
298
- removeContext(name: string): void {
299
- const engine = this.ensureInitialized();
300
- engine.removeContext(name);
301
- }
302
- }
package/src/engine.ts DELETED
@@ -1,282 +0,0 @@
1
- /**
2
- * Low-level wrapper around the WASM engine
3
- * Node.js / Bundler target
4
- */
5
-
6
- // WASM module types
7
- import { WasmEngine } from "../wasm-node/hypen_engine.js";
8
- import { frameworkLoggers } from "./logger.js";
9
- import { classifyEngineError } from "./result.js";
10
- import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
11
-
12
- // Re-export types so existing consumers of "./engine.js" still work
13
- export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
14
-
15
- const log = frameworkLoggers.engine;
16
-
17
- /**
18
- * Unwrap proxy objects to plain values for WASM serialization.
19
- * Uses structuredClone when available (Node 17+, Bun, modern browsers),
20
- * falls back to JSON round-trip for proxy objects or older environments.
21
- */
22
- function unwrapForWasm<T>(value: T): T {
23
- // Fast path: primitives don't need cloning
24
- if (value === null || typeof value !== 'object') {
25
- return value;
26
- }
27
-
28
- // Check if the object has a snapshot method (our proxy convention)
29
- if (typeof (value as any).__getSnapshot === 'function') {
30
- return (value as any).__getSnapshot() as T;
31
- }
32
-
33
- // Try structuredClone first (fastest for plain objects)
34
- try {
35
- return structuredClone(value);
36
- } catch {
37
- // Fallback for proxy objects or unsupported types
38
- return JSON.parse(JSON.stringify(value));
39
- }
40
- }
41
-
42
- /**
43
- * Engine wraps the WASM engine and provides a TypeScript-friendly API
44
- */
45
- export class Engine {
46
- private wasmEngine: WasmEngine | null = null;
47
- private initialized = false;
48
-
49
- /**
50
- * Initialize the WASM module
51
- */
52
- async init(): Promise<void> {
53
- if (this.initialized) return;
54
-
55
- // For bundler target, WASM is auto-initialized
56
- this.wasmEngine = new WasmEngine();
57
- this.initialized = true;
58
- }
59
-
60
- /**
61
- * Ensure the engine is initialized before operations
62
- */
63
- private ensureInitialized(): WasmEngine {
64
- if (!this.wasmEngine) {
65
- throw new Error("Engine not initialized. Call init() first.");
66
- }
67
- return this.wasmEngine;
68
- }
69
-
70
- /**
71
- * Set the render callback that receives patches
72
- */
73
- setRenderCallback(callback: RenderCallback): void {
74
- const engine = this.ensureInitialized();
75
- engine.setRenderCallback((patches: Patch[]) => {
76
- callback(patches);
77
- });
78
- }
79
-
80
- /**
81
- * Set the component resolver for dynamic component composition
82
- */
83
- setComponentResolver(resolver: ComponentResolver): void {
84
- const engine = this.ensureInitialized();
85
- engine.setComponentResolver((componentName: string, contextPath: string | null) => {
86
- const result = resolver(componentName, contextPath);
87
- return result;
88
- });
89
- }
90
-
91
- /**
92
- * Parse and render Hypen DSL source code
93
- * @throws {ParseError} if the source fails to parse
94
- * @throws {RenderError} if rendering fails
95
- */
96
- renderSource(source: string): void {
97
- const engine = this.ensureInitialized();
98
- try {
99
- engine.renderSource(source);
100
- } catch (err) {
101
- throw classifyEngineError(err);
102
- }
103
- }
104
-
105
- /**
106
- * Render a lazy component (for lazy route loading)
107
- */
108
- renderLazyComponent(source: string): void {
109
- const engine = this.ensureInitialized();
110
- engine.renderLazyComponent(source);
111
- }
112
-
113
- /**
114
- * Render a component into a specific parent node (subtree rendering)
115
- * @throws {ParseError} if the source fails to parse
116
- * @throws {RenderError} if the parent node is not found or rendering fails
117
- */
118
- renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
119
- const engine = this.ensureInitialized();
120
- try {
121
- engine.renderInto(source, parentNodeId, unwrapForWasm(state));
122
- } catch (err) {
123
- throw classifyEngineError(err);
124
- }
125
- }
126
-
127
- /**
128
- * Notify the engine of state changes using sparse updates
129
- * @throws {StateError} if the state patch is invalid
130
- */
131
- notifyStateChange(paths: string[], values: Record<string, any>): void {
132
- const engine = this.ensureInitialized();
133
-
134
- if (paths.length === 0) {
135
- return;
136
- }
137
-
138
- try {
139
- engine.updateStateSparse(paths, unwrapForWasm(values));
140
- } catch (err) {
141
- throw classifyEngineError(err);
142
- }
143
- log.debug("State changed (sparse):", paths);
144
- }
145
-
146
- /**
147
- * Notify the engine of state changes using full state replacement
148
- * @deprecated Use notifyStateChange with sparse values instead
149
- */
150
- notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
151
- const engine = this.ensureInitialized();
152
- engine.updateState(unwrapForWasm(currentState));
153
-
154
- if (paths.length > 0) {
155
- log.debug("State changed (full):", paths);
156
- }
157
- }
158
-
159
- /**
160
- * Update state (triggers re-render of affected nodes)
161
- * @deprecated Use notifyStateChange instead
162
- */
163
- updateState(statePatch: Record<string, any>): void {
164
- const engine = this.ensureInitialized();
165
- engine.updateState(unwrapForWasm(statePatch));
166
- }
167
-
168
- /**
169
- * Dispatch an action
170
- * @throws {HypenError} if the action dispatch fails
171
- */
172
- dispatchAction(name: string, payload?: any): void {
173
- const engine = this.ensureInitialized();
174
- try {
175
- engine.dispatchAction(name, payload ?? null);
176
- } catch (err) {
177
- throw classifyEngineError(err);
178
- }
179
- }
180
-
181
- /**
182
- * Register an action handler
183
- */
184
- private _onErrorHandler?: (error: Error) => void;
185
-
186
- /**
187
- * Set error handler for action errors
188
- */
189
- setOnError(handler: (error: Error) => void): void {
190
- this._onErrorHandler = handler;
191
- }
192
-
193
- onAction(actionName: string, handler: ActionHandler): void {
194
- const engine = this.ensureInitialized();
195
- const handleError = (err: unknown) => {
196
- log.error("Action handler error:", err);
197
- if (this._onErrorHandler) {
198
- this._onErrorHandler(err instanceof Error ? err : new Error(String(err)));
199
- }
200
- };
201
- engine.onAction(actionName, (action: Action) => {
202
- try {
203
- const result = handler(action);
204
- // If handler returns a promise, catch async errors too
205
- if (result && typeof (result as any).catch === "function") {
206
- (result as any).catch(handleError);
207
- }
208
- } catch (err) {
209
- handleError(err);
210
- }
211
- });
212
- }
213
-
214
- /**
215
- * Initialize a module
216
- */
217
- setModule(
218
- name: string,
219
- actions: string[],
220
- stateKeys: string[],
221
- initialState: Record<string, any>
222
- ): void {
223
- const engine = this.ensureInitialized();
224
- engine.setModule(name, actions, stateKeys, initialState);
225
- }
226
-
227
- /**
228
- * Get the current revision number
229
- */
230
- getRevision(): number {
231
- const engine = this.ensureInitialized();
232
- return Number(engine.getRevision());
233
- }
234
-
235
- /**
236
- * Clear the engine tree
237
- */
238
- clearTree(): void {
239
- const engine = this.ensureInitialized();
240
- engine.clearTree();
241
- }
242
-
243
- /**
244
- * Debug method to inspect parsed components
245
- */
246
- debugParseComponent(source: string): string {
247
- const engine = this.ensureInitialized();
248
- return engine.debugParseComponent(source);
249
- }
250
-
251
- // ── Data Source Context ────────────────────────────────────────
252
-
253
- /**
254
- * Set (or replace) a named data source context.
255
- *
256
- * Registers the provider in the dependency graph, stores the data,
257
- * and re-renders every node bound to `$name.*`.
258
- * Sparse merging (if needed) should happen at the SDK layer before
259
- * calling this method with the merged object.
260
- *
261
- * @param name - Provider name (e.g., "spacetime", "firebase")
262
- * @param data - Full state object for this provider
263
- */
264
- setContext(name: string, data: Record<string, unknown>): void {
265
- const engine = this.ensureInitialized();
266
- try {
267
- engine.setContext(name, unwrapForWasm(data));
268
- } catch (err) {
269
- throw classifyEngineError(err);
270
- }
271
- log.debug(`Data source "${name}" context set`);
272
- }
273
-
274
- /**
275
- * Remove a data source context entirely.
276
- * Drops the provider's state and re-renders bound nodes (they resolve to null).
277
- */
278
- removeContext(name: string): void {
279
- const engine = this.ensureInitialized();
280
- engine.removeContext(name);
281
- }
282
- }
package/src/loader.ts DELETED
@@ -1,136 +0,0 @@
1
- /**
2
- * Component Loader
3
- *
4
- * Loads and registers Hypen components from file system.
5
- * Works in both Node.js and Bun environments.
6
- */
7
-
8
- import { existsSync, readdirSync, readFileSync } from "fs";
9
- import { join } from "path";
10
- import type { HypenModuleDefinition } from "./app.js";
11
- import { frameworkLoggers } from "./logger.js";
12
-
13
- const log = frameworkLoggers.loader;
14
-
15
- export interface ComponentDefinition {
16
- name: string;
17
- module: HypenModuleDefinition<any>;
18
- template: string; // The .hypen file content
19
- path: string;
20
- }
21
-
22
- export class ComponentLoader {
23
- private components = new Map<string, ComponentDefinition>();
24
-
25
- /**
26
- * Register a component with its module and template
27
- */
28
- register(
29
- name: string,
30
- module: HypenModuleDefinition<any>,
31
- template: string,
32
- path?: string
33
- ): void {
34
- this.components.set(name, {
35
- name,
36
- module,
37
- template,
38
- path: path || name,
39
- });
40
- }
41
-
42
- /**
43
- * Get a registered component by name
44
- */
45
- get(name: string): ComponentDefinition | undefined {
46
- return this.components.get(name);
47
- }
48
-
49
- /**
50
- * Check if a component is registered
51
- */
52
- has(name: string): boolean {
53
- return this.components.has(name);
54
- }
55
-
56
- /**
57
- * Get all registered component names
58
- */
59
- getNames(): string[] {
60
- return Array.from(this.components.keys());
61
- }
62
-
63
- /**
64
- * Get all registered components
65
- */
66
- getAll(): ComponentDefinition[] {
67
- return Array.from(this.components.values());
68
- }
69
-
70
- /**
71
- * Clear all registered components
72
- */
73
- clear(): void {
74
- this.components.clear();
75
- }
76
-
77
- /**
78
- * Load a component from a directory
79
- * Expects: component.ts and component.hypen in the same directory
80
- */
81
- async loadFromDirectory(name: string, dirPath: string): Promise<void> {
82
- try {
83
- // Dynamic import of the module
84
- const modulePath = join(dirPath, "component.ts");
85
- const moduleExport = await import(modulePath);
86
- const module = moduleExport.default as HypenModuleDefinition<any>;
87
-
88
- // Read the .hypen template file
89
- const templatePath = join(dirPath, "component.hypen");
90
- const template = readFileSync(templatePath, "utf-8");
91
-
92
- // Register the component
93
- this.register(name, module, template, dirPath);
94
-
95
- log.debug(`Loaded component: ${name} from ${dirPath}`);
96
- } catch (error) {
97
- log.error(`Failed to load component ${name} from ${dirPath}:`, error);
98
- throw error;
99
- }
100
- }
101
-
102
- /**
103
- * Auto-load all components from a directory
104
- * Scans for subdirectories containing component.ts and component.hypen
105
- */
106
- async loadFromComponentsDir(baseDir: string): Promise<void> {
107
- try {
108
- if (!existsSync(baseDir)) {
109
- log.warn(`Components directory not found: ${baseDir}`);
110
- return;
111
- }
112
-
113
- const entries = readdirSync(baseDir, { withFileTypes: true });
114
-
115
- for (const entry of entries) {
116
- if (!entry.isDirectory()) continue;
117
-
118
- const componentDir = join(baseDir, entry.name);
119
- const hypenPath = join(componentDir, "component.hypen");
120
-
121
- // Only load if component.hypen exists
122
- if (existsSync(hypenPath)) {
123
- await this.loadFromDirectory(entry.name, componentDir);
124
- }
125
- }
126
-
127
- log.debug(`Loaded ${this.components.size} components from ${baseDir}`);
128
- } catch (error) {
129
- log.error(`Failed to load components from ${baseDir}:`, error);
130
- throw error;
131
- }
132
- }
133
- }
134
-
135
- // Global component loader instance
136
- export const componentLoader = new ComponentLoader();