@hypen-space/core 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Hypen App Builder API
3
+ * Implements the stateful module system from RFC-0001
4
+ */
5
+ import type { Action } from "./types.js";
6
+ import type { Session } from "./remote/types.js";
7
+ import { HypenError } from "./result.js";
8
+ export interface IEngine {
9
+ setModule(name: string, actions: string[], stateKeys: string[], initialState: unknown): void;
10
+ onAction(actionName: string, handler: (action: Action) => void | Promise<void>): void;
11
+ notifyStateChange(paths: string[], changedValues: Record<string, unknown>): void;
12
+ }
13
+ import type { HypenRouter } from "./router.js";
14
+ import type { HypenGlobalContext, ModuleReference } from "./context.js";
15
+ export type RouterContext = {
16
+ root: HypenRouter;
17
+ current?: HypenRouter;
18
+ parent?: HypenRouter;
19
+ };
20
+ export type ActionContext = {
21
+ name: string;
22
+ payload?: unknown;
23
+ sender?: string;
24
+ };
25
+ export type ActionNext = {
26
+ router: HypenRouter;
27
+ };
28
+ export type GlobalContext = {
29
+ getModule: <T = unknown>(id: string) => ModuleReference<T>;
30
+ hasModule: (id: string) => boolean;
31
+ getModuleIds: () => string[];
32
+ getGlobalState: () => Record<string, unknown>;
33
+ emit: (event: string, payload?: unknown) => void;
34
+ on: (event: string, handler: (payload?: unknown) => void) => () => void;
35
+ __router?: unknown;
36
+ };
37
+ export type LifecycleHandler<T> = (state: T, context?: GlobalContext) => void | Promise<void>;
38
+ /**
39
+ * Action handler context - all parameters available explicitly
40
+ */
41
+ export interface ActionHandlerContext<T> {
42
+ action: ActionContext;
43
+ state: T;
44
+ next: ActionNext;
45
+ context: GlobalContext;
46
+ }
47
+ /**
48
+ * Action handler - receives all context in a single object
49
+ */
50
+ export type ActionHandler<T> = (ctx: ActionHandlerContext<T>) => void | Promise<void>;
51
+ /**
52
+ * Context passed to onDisconnect handler
53
+ */
54
+ export interface DisconnectContext<T> {
55
+ /** Current state snapshot (ready to serialize and save) */
56
+ state: T;
57
+ /** Session information */
58
+ session: Session;
59
+ }
60
+ /**
61
+ * Context passed to onReconnect handler
62
+ */
63
+ export interface ReconnectContext<T> {
64
+ /** Session information */
65
+ session: Session;
66
+ /** Call this with saved state to restore it */
67
+ restore: (savedState: T) => void;
68
+ }
69
+ /**
70
+ * Context passed to onExpire handler
71
+ */
72
+ export interface ExpireContext {
73
+ /** Session information */
74
+ session: Session;
75
+ }
76
+ /**
77
+ * Handler called when client disconnects (session still alive for TTL)
78
+ */
79
+ export type DisconnectHandler<T> = (ctx: DisconnectContext<T>) => void | Promise<void>;
80
+ /**
81
+ * Handler called when client reconnects with existing session
82
+ */
83
+ export type ReconnectHandler<T> = (ctx: ReconnectContext<T>) => void | Promise<void>;
84
+ /**
85
+ * Handler called when session TTL expires (client never reconnected)
86
+ */
87
+ export type ExpireHandler = (ctx: ExpireContext) => void | Promise<void>;
88
+ /**
89
+ * Context passed to onError handler
90
+ */
91
+ export interface ErrorContext<T> {
92
+ /** The error that occurred */
93
+ error: HypenError;
94
+ /** Current state (for inspection, not mutation during error handling) */
95
+ state: T;
96
+ /** The action name if error occurred in an action handler */
97
+ actionName?: string;
98
+ /** The lifecycle phase if error occurred in a lifecycle handler */
99
+ lifecycle?: "created" | "destroyed" | "disconnect" | "reconnect" | "expire";
100
+ }
101
+ /**
102
+ * Error handler return type - controls error propagation
103
+ */
104
+ export type ErrorHandlerResult = void | {
105
+ handled: true;
106
+ } | {
107
+ retry: true;
108
+ } | {
109
+ rethrow: true;
110
+ };
111
+ /**
112
+ * Handler called when an error occurs in the module
113
+ */
114
+ export type ErrorHandler<T> = (ctx: ErrorContext<T>) => ErrorHandlerResult | Promise<ErrorHandlerResult>;
115
+ export interface HypenModuleDefinition<T = unknown> {
116
+ name?: string;
117
+ actions: string[];
118
+ stateKeys: string[];
119
+ persist?: boolean;
120
+ version?: number;
121
+ initialState: T;
122
+ /**
123
+ * Inline UI template for single-file components.
124
+ * Set via the `.ui(hypen`...`)` method.
125
+ */
126
+ template?: string;
127
+ handlers: {
128
+ onCreated?: LifecycleHandler<T>;
129
+ onAction: Map<string, ActionHandler<T>>;
130
+ onDestroyed?: LifecycleHandler<T>;
131
+ /** Called when client disconnects (session persists for TTL) */
132
+ onDisconnect?: DisconnectHandler<T>;
133
+ /** Called when client reconnects with existing session */
134
+ onReconnect?: ReconnectHandler<T>;
135
+ /** Called when session TTL expires */
136
+ onExpire?: ExpireHandler;
137
+ /** Called when any error occurs in the module */
138
+ onError?: ErrorHandler<T>;
139
+ };
140
+ }
141
+ /**
142
+ * Alias for HypenModuleDefinition for backward compatibility
143
+ */
144
+ export type HypenModule<T = unknown> = HypenModuleDefinition<T>;
145
+ /**
146
+ * Builder for creating Hypen app modules
147
+ */
148
+ export declare class HypenAppBuilder<T> {
149
+ private initialState;
150
+ private options;
151
+ private createdHandler?;
152
+ private actionHandlers;
153
+ private destroyedHandler?;
154
+ private disconnectHandler?;
155
+ private reconnectHandler?;
156
+ private expireHandler?;
157
+ private errorHandler?;
158
+ private template?;
159
+ constructor(initialState: T, options?: {
160
+ persist?: boolean;
161
+ version?: number;
162
+ name?: string;
163
+ });
164
+ /**
165
+ * Register a handler for module creation
166
+ */
167
+ onCreated(fn: LifecycleHandler<T>): this;
168
+ /**
169
+ * Register a handler for a specific action
170
+ */
171
+ onAction(name: string, fn: ActionHandler<T>): this;
172
+ /**
173
+ * Register a handler for module destruction
174
+ */
175
+ onDestroyed(fn: LifecycleHandler<T>): this;
176
+ /**
177
+ * Register a handler for client disconnection
178
+ * Called when client disconnects; session persists for TTL
179
+ * Use this to save state for later restoration
180
+ */
181
+ onDisconnect(fn: DisconnectHandler<T>): this;
182
+ /**
183
+ * Register a handler for session reconnection
184
+ * Called when client reconnects with an existing session ID
185
+ * Use restore() to hydrate state from saved data
186
+ */
187
+ onReconnect(fn: ReconnectHandler<T>): this;
188
+ /**
189
+ * Register a handler for session expiration
190
+ * Called when TTL expires and client never reconnected
191
+ * Use this to clean up stored state
192
+ */
193
+ onExpire(fn: ExpireHandler): this;
194
+ /**
195
+ * Register an error handler for the module
196
+ * Called when any error occurs in action handlers or lifecycle hooks
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * app
201
+ * .defineState({ count: 0 })
202
+ * .onAction("increment", ({ state }) => {
203
+ * if (state.count > 100) throw new Error("Count too high");
204
+ * state.count += 1;
205
+ * })
206
+ * .onError(({ error, actionName, state }) => {
207
+ * console.error(`Error in ${actionName}:`, error.message);
208
+ * // Optionally recover
209
+ * if (error.message.includes("too high")) {
210
+ * return { handled: true }; // Don't propagate
211
+ * }
212
+ * // Or retry
213
+ * // return { retry: true };
214
+ * })
215
+ * .build();
216
+ * ```
217
+ *
218
+ * @param fn - Error handler function
219
+ * @returns Builder for chaining
220
+ */
221
+ onError(fn: ErrorHandler<T>): this;
222
+ /**
223
+ * Define the component's UI template inline (single-file component).
224
+ *
225
+ * Use with the `hypen` tagged template literal and binding proxies:
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * import { app, hypen, state } from "@hypen-space/core";
230
+ *
231
+ * export default app
232
+ * .defineState({ count: 0 })
233
+ * .onAction("increment", ({ state }) => {
234
+ * state.count += 1;
235
+ * })
236
+ * .ui(hypen`
237
+ * Column {
238
+ * Text("Count: ${state.count}")
239
+ * Button { Text("+") }
240
+ * .onClick("@actions.increment")
241
+ * }
242
+ * `);
243
+ * ```
244
+ *
245
+ * @param template - The Hypen DSL template string
246
+ * @returns The built module definition (calls build() internally)
247
+ */
248
+ ui(template: string): HypenModuleDefinition<T>;
249
+ /**
250
+ * Build the module definition
251
+ */
252
+ build(): HypenModuleDefinition<T>;
253
+ }
254
+ /**
255
+ * Hypen App API
256
+ */
257
+ export declare class HypenApp {
258
+ /**
259
+ * Define the initial state for a module
260
+ */
261
+ defineState<T>(initial: T, options?: {
262
+ persist?: boolean;
263
+ version?: number;
264
+ name?: string;
265
+ }): HypenAppBuilder<T>;
266
+ }
267
+ /**
268
+ * The main app instance for creating modules
269
+ */
270
+ export declare const app: HypenApp;
271
+ /**
272
+ * Module Instance - manages a running module with typed state
273
+ */
274
+ export declare class HypenModuleInstance<T extends object = any> {
275
+ private engine;
276
+ private definition;
277
+ private state;
278
+ private isDestroyed;
279
+ private routerContext?;
280
+ private globalContext?;
281
+ private stateChangeCallbacks;
282
+ constructor(engine: IEngine, definition: HypenModuleDefinition<T>, routerContext?: RouterContext, globalContext?: HypenGlobalContext);
283
+ /**
284
+ * Create the global context API for this module
285
+ */
286
+ private createGlobalContextAPI;
287
+ /**
288
+ * Execute an action handler with Result-based error handling
289
+ * Handles both synchronous throws and async rejections
290
+ */
291
+ private executeAction;
292
+ /**
293
+ * Handle an error with module-level error handler support
294
+ * Falls back to default behavior (emit + log) if no handler or handler doesn't suppress
295
+ * @returns true if the error should be rethrown
296
+ */
297
+ private handleError;
298
+ /**
299
+ * Call the onCreated handler
300
+ */
301
+ private callCreatedHandler;
302
+ /**
303
+ * Register a callback to be notified when state changes
304
+ */
305
+ onStateChange(callback: () => void): void;
306
+ /**
307
+ * Destroy the module instance
308
+ */
309
+ destroy(): Promise<void>;
310
+ /**
311
+ * Get the current state (returns a snapshot)
312
+ */
313
+ getState(): T;
314
+ /**
315
+ * Get the live observable state
316
+ */
317
+ getLiveState(): T;
318
+ /**
319
+ * Update state directly (merges with existing state)
320
+ */
321
+ updateState(patch: Partial<T>): void;
322
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Built-in Hypen Components
3
+ * Framework-provided components like Router and Route
4
+ */
5
+ /**
6
+ * Router Component
7
+ * Automatically matches routes and renders the appropriate child Route
8
+ *
9
+ * Usage in Hypen DSL:
10
+ * Router {
11
+ * Route(path: "/") { HomePage }
12
+ * Route(path: "/products") { ProductList }
13
+ * Route(path: "/users/:id") { UserProfile }
14
+ * }
15
+ */
16
+ export declare const Router: import("../app.js").HypenModuleDefinition<{
17
+ currentPath: string;
18
+ matchedRoute: any;
19
+ routeParams: Record<string, string>;
20
+ }>;
21
+ /**
22
+ * Route Component
23
+ * Defines a route pattern and its content
24
+ * This is just a marker component - Router processes it
25
+ *
26
+ * Props:
27
+ * - path: string - Route pattern (e.g., "/", "/users/:id", "/dashboard/*")
28
+ *
29
+ * Usage:
30
+ * Route(path: "/products") {
31
+ * ProductList
32
+ * }
33
+ */
34
+ export declare const Route: import("../app.js").HypenModuleDefinition<{}>;
35
+ /**
36
+ * Navigation Link Component
37
+ * Navigates to a route when clicked
38
+ *
39
+ * Props:
40
+ * - to: string - Target path
41
+ *
42
+ * Usage:
43
+ * Link(to: "/products") {
44
+ * Text("View Products")
45
+ * }
46
+ */
47
+ export declare const Link: import("../app.js").HypenModuleDefinition<{
48
+ to: string;
49
+ isActive: boolean;
50
+ }>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Global Context - Cross-module communication and state access
3
+ */
4
+ import type { HypenModuleInstance } from "./app.js";
5
+ import { TypedEventEmitter, type HypenFrameworkEvents } from "./events.js";
6
+ export type ModuleReference<T = unknown> = {
7
+ state: T;
8
+ setState: (patch: Partial<T>) => void;
9
+ getState: () => T;
10
+ };
11
+ /**
12
+ * @deprecated Use TypedEventEmitter from events.ts for type-safe events
13
+ */
14
+ export type EventHandler = (payload?: unknown) => void;
15
+ /**
16
+ * Global Context - Provides access to all modules and cross-module communication
17
+ *
18
+ * @template TEvents - Custom event map (extends HypenFrameworkEvents)
19
+ */
20
+ export declare class HypenGlobalContext<TEvents extends Record<string, unknown> = HypenFrameworkEvents> {
21
+ private modules;
22
+ private typedEvents;
23
+ /**
24
+ * @deprecated Legacy event bus for backward compatibility
25
+ */
26
+ private legacyEventBus;
27
+ constructor();
28
+ /**
29
+ * Get the typed event emitter for type-safe event handling
30
+ */
31
+ get events(): TypedEventEmitter<TEvents>;
32
+ /**
33
+ * Register a module instance with an ID
34
+ */
35
+ registerModule(id: string, instance: HypenModuleInstance): void;
36
+ /**
37
+ * Unregister a module
38
+ */
39
+ unregisterModule(id: string): void;
40
+ /**
41
+ * Get a module by ID with type safety
42
+ */
43
+ getModule<T = unknown>(id: string): ModuleReference<T>;
44
+ /**
45
+ * Check if a module exists
46
+ */
47
+ hasModule(id: string): boolean;
48
+ /**
49
+ * Get all registered module IDs
50
+ */
51
+ getModuleIds(): string[];
52
+ /**
53
+ * Get the entire app state tree (snapshot)
54
+ */
55
+ getGlobalState(): Record<string, unknown>;
56
+ /**
57
+ * Emit an event to the event bus (legacy API for backward compatibility)
58
+ * @deprecated Use `context.events.emit()` for type-safe events
59
+ */
60
+ emit(event: string, payload?: unknown): void;
61
+ /**
62
+ * Subscribe to an event (legacy API for backward compatibility)
63
+ * @deprecated Use `context.events.on()` for type-safe events
64
+ */
65
+ on(event: string, handler: EventHandler): () => void;
66
+ /**
67
+ * Unsubscribe from an event (legacy API)
68
+ * @deprecated Use the unsubscribe function returned by `context.events.on()`
69
+ */
70
+ off(event: string, handler: EventHandler): void;
71
+ /**
72
+ * Clear all event listeners for an event (legacy API)
73
+ * @deprecated Use `context.events.removeAllListeners()`
74
+ */
75
+ clearEvent(event: string): void;
76
+ /**
77
+ * Clear all event listeners (legacy API)
78
+ * @deprecated Use `context.events.clearAll()`
79
+ */
80
+ clearAllEvents(): void;
81
+ /**
82
+ * Get debug info about the context
83
+ */
84
+ debug(): {
85
+ modules: string[];
86
+ events: string[];
87
+ typedEvents: Array<keyof TEvents>;
88
+ state: Record<string, unknown>;
89
+ };
90
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Component Discovery
3
+ *
4
+ * Automatically discovers Hypen components from the filesystem.
5
+ *
6
+ * Supports multiple conventions:
7
+ * 1. Single-file: ComponentName.ts with .ui(hypen`...`) inline template
8
+ * 2. Folder-based: ComponentName/component.ts + ComponentName/component.hypen
9
+ * 3. Sibling files: ComponentName.ts + ComponentName.hypen
10
+ * 4. Index-based: ComponentName/index.ts + ComponentName/index.hypen
11
+ */
12
+ import type { HypenModuleDefinition } from "./app.js";
13
+ export interface DiscoveredComponent {
14
+ name: string;
15
+ /** Path to .hypen file (null for single-file components with inline templates) */
16
+ hypenPath: string | null;
17
+ /** Path to .ts module file */
18
+ modulePath: string | null;
19
+ /** The UI template (from .hypen file or inline) */
20
+ template: string;
21
+ /** Whether this component has a module (state/actions) */
22
+ hasModule: boolean;
23
+ /** Whether this is a single-file component with inline template */
24
+ isSingleFile: boolean;
25
+ }
26
+ export interface DiscoveryOptions {
27
+ /**
28
+ * Which naming patterns to look for
29
+ * Default: ["single-file", "folder", "sibling", "index"]
30
+ *
31
+ * - single-file: ComponentName.ts with .ui(hypen`...`) inline template
32
+ * - folder: ComponentName/component.ts + ComponentName/component.hypen
33
+ * - sibling: ComponentName.ts + ComponentName.hypen
34
+ * - index: ComponentName/index.ts + ComponentName/index.hypen
35
+ */
36
+ patterns?: ("single-file" | "folder" | "sibling" | "index")[];
37
+ /**
38
+ * Recursively scan subdirectories
39
+ * Default: false
40
+ */
41
+ recursive?: boolean;
42
+ /**
43
+ * Enable debug logging
44
+ * Default: false
45
+ */
46
+ debug?: boolean;
47
+ }
48
+ export interface WatchOptions extends DiscoveryOptions {
49
+ /**
50
+ * Callback when components change
51
+ */
52
+ onChange?: (components: DiscoveredComponent[]) => void;
53
+ /**
54
+ * Callback when a component is added
55
+ */
56
+ onAdd?: (component: DiscoveredComponent) => void;
57
+ /**
58
+ * Callback when a component is removed
59
+ */
60
+ onRemove?: (name: string) => void;
61
+ /**
62
+ * Callback when a component is updated
63
+ */
64
+ onUpdate?: (component: DiscoveredComponent) => void;
65
+ }
66
+ /**
67
+ * Discover all Hypen components in a directory
68
+ */
69
+ export declare function discoverComponents(baseDir: string, options?: DiscoveryOptions): Promise<DiscoveredComponent[]>;
70
+ /**
71
+ * Load discovered components into a map for use with Hypen
72
+ */
73
+ export declare function loadDiscoveredComponents(components: DiscoveredComponent[]): Promise<Map<string, {
74
+ name: string;
75
+ module: HypenModuleDefinition<any>;
76
+ template: string;
77
+ }>>;
78
+ /**
79
+ * Watch a directory for component changes
80
+ */
81
+ export declare function watchComponents(baseDir: string, options?: WatchOptions): {
82
+ stop: () => void;
83
+ };
84
+ /**
85
+ * Generate a components object from discovered components
86
+ * Useful for static imports / code generation
87
+ */
88
+ export declare function generateComponentsCode(baseDir: string, options?: DiscoveryOptions & {
89
+ outputDir?: string;
90
+ }): Promise<string>;
package/dist/discovery.js CHANGED
@@ -854,7 +854,7 @@ var init_app = __esm(() => {
854
854
  // src/discovery.ts
855
855
  init_logger();
856
856
  import { existsSync, readdirSync, readFileSync, watch } from "fs";
857
- import { join, basename, resolve } from "path";
857
+ import { join, basename, resolve, relative } from "path";
858
858
  async function discoverComponents(baseDir, options = {}) {
859
859
  const {
860
860
  patterns = ["single-file", "folder", "sibling", "index"],
@@ -1090,6 +1090,7 @@ function watchComponents(baseDir, options = {}) {
1090
1090
  async function generateComponentsCode(baseDir, options = {}) {
1091
1091
  const components = await discoverComponents(baseDir, options);
1092
1092
  const resolvedDir = resolve(baseDir);
1093
+ const outputBase = options.outputDir ? resolve(options.outputDir) : resolvedDir;
1093
1094
  let code = `/**
1094
1095
  * Auto-generated component imports
1095
1096
  * Generated by @hypen-space/core discovery
@@ -1097,9 +1098,13 @@ async function generateComponentsCode(baseDir, options = {}) {
1097
1098
 
1098
1099
  `;
1099
1100
  for (const component of components) {
1100
- const relativePath = component.modulePath ? "./" + component.modulePath.replace(resolvedDir + "/", "").replace(/\.ts$/, ".js") : null;
1101
- if (relativePath) {
1102
- code += `import ${component.name}Module from "${relativePath}";
1101
+ let importPath = null;
1102
+ if (component.modulePath) {
1103
+ const rel = relative(outputBase, component.modulePath).replace(/\.ts$/, ".js");
1104
+ importPath = rel.startsWith(".") ? rel : "./" + rel;
1105
+ }
1106
+ if (importPath) {
1107
+ code += `import ${component.name}Module from "${importPath}";
1103
1108
  `;
1104
1109
  }
1105
1110
  }
@@ -1143,4 +1148,4 @@ export {
1143
1148
  discoverComponents
1144
1149
  };
1145
1150
 
1146
- //# debugId=DEF834620FC1CE5864756E2164756E21
1151
+ //# debugId=85977D1E65CCBA9B64756E2164756E21