@hypen-space/web 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 +297 -0
- package/dist/src/canvas/index.js +4 -4
- package/dist/src/canvas/index.js.map +1 -1
- package/dist/src/canvas/renderer.js +4 -4
- package/dist/src/canvas/renderer.js.map +1 -1
- package/dist/src/dom/applicators/index.js +9 -9
- package/dist/src/dom/applicators/index.js.map +1 -1
- package/dist/src/dom/components/index.js +12 -12
- package/dist/src/dom/components/index.js.map +1 -1
- package/dist/src/dom/index.js +7 -7
- package/dist/src/dom/index.js.map +1 -1
- package/dist/src/dom/renderer.js +10 -10
- package/dist/src/dom/renderer.js.map +1 -1
- package/dist/src/hypen.js +396 -0
- package/dist/src/hypen.js.map +10 -0
- package/dist/src/index.js +19 -11
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/hypen.ts +617 -0
- package/src/index.ts +8 -1
package/src/hypen.ts
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypen - High-Level API for Web Applications
|
|
3
|
+
*
|
|
4
|
+
* Simple API for rendering Hypen applications (like ReactDOM.render)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BrowserEngine as Engine,
|
|
9
|
+
HypenModuleInstance,
|
|
10
|
+
HypenRouter,
|
|
11
|
+
HypenGlobalContext,
|
|
12
|
+
componentLoader,
|
|
13
|
+
Router,
|
|
14
|
+
Route,
|
|
15
|
+
Link,
|
|
16
|
+
type RouterContext,
|
|
17
|
+
type HypenModuleDefinition,
|
|
18
|
+
} from "@hypen-space/core";
|
|
19
|
+
import { DOMRenderer } from "./dom/renderer.js";
|
|
20
|
+
import type { DebugConfig } from "./dom/debug.js";
|
|
21
|
+
|
|
22
|
+
export interface HypenConfig {
|
|
23
|
+
/** Base directory for components (default: "./src/components") */
|
|
24
|
+
componentsDir?: string;
|
|
25
|
+
/** Enable debug logging */
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
/** Custom WASM path */
|
|
28
|
+
wasmPath?: string;
|
|
29
|
+
/** Enable re-render heatmap debugging */
|
|
30
|
+
debugHeatmap?: boolean;
|
|
31
|
+
/** Heatmap increment per re-render (default: 5%) */
|
|
32
|
+
heatmapIncrement?: number;
|
|
33
|
+
/** Heatmap fade out duration in ms (default: 2000) */
|
|
34
|
+
heatmapFadeOut?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Hypen {
|
|
38
|
+
private engine: Engine | null = null;
|
|
39
|
+
private renderer: DOMRenderer | null = null;
|
|
40
|
+
private moduleInstance: HypenModuleInstance<any> | null = null;
|
|
41
|
+
private container: HTMLElement | null = null;
|
|
42
|
+
private config: HypenConfig;
|
|
43
|
+
private router: HypenRouter;
|
|
44
|
+
private globalContext: HypenGlobalContext;
|
|
45
|
+
private moduleInstances = new Map<string, HypenModuleInstance<any>>();
|
|
46
|
+
|
|
47
|
+
constructor(config: HypenConfig = {}) {
|
|
48
|
+
this.config = {
|
|
49
|
+
componentsDir: "./src/components",
|
|
50
|
+
debug: false,
|
|
51
|
+
...config,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Initialize router and global context
|
|
55
|
+
this.router = new HypenRouter();
|
|
56
|
+
this.globalContext = new HypenGlobalContext();
|
|
57
|
+
|
|
58
|
+
// Register built-in components
|
|
59
|
+
componentLoader.register("Router", Router, "");
|
|
60
|
+
componentLoader.register("Route", Route, "");
|
|
61
|
+
componentLoader.register("Link", Link, "");
|
|
62
|
+
|
|
63
|
+
// Store router and hypen instance in global context for access in components
|
|
64
|
+
(this.globalContext as any).__router = this.router;
|
|
65
|
+
(this.globalContext as any).__hypenEngine = this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the Hypen runtime
|
|
70
|
+
* Must be called before render()
|
|
71
|
+
*/
|
|
72
|
+
async init(): Promise<void> {
|
|
73
|
+
if (this.config.debug) {
|
|
74
|
+
console.log("[Hypen] Initializing...");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Initialize engine
|
|
78
|
+
this.engine = new Engine();
|
|
79
|
+
await this.engine.init(
|
|
80
|
+
this.config.wasmPath ? { wasmPath: this.config.wasmPath } : undefined
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (this.config.debug) {
|
|
84
|
+
console.log("[Hypen] Engine initialized");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load all components from the components directory
|
|
90
|
+
*/
|
|
91
|
+
async loadComponents(componentsDir?: string): Promise<void> {
|
|
92
|
+
const dir = componentsDir || this.config.componentsDir!;
|
|
93
|
+
|
|
94
|
+
if (this.config.debug) {
|
|
95
|
+
console.log(`[Hypen] Loading components from ${dir}...`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await componentLoader.loadFromComponentsDir(dir);
|
|
99
|
+
|
|
100
|
+
if (this.config.debug) {
|
|
101
|
+
console.log(
|
|
102
|
+
`[Hypen] Loaded ${componentLoader.getNames().length} components`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Render a component to a DOM container
|
|
109
|
+
*
|
|
110
|
+
* @param componentName - Name of the component to render (e.g., "HomePage")
|
|
111
|
+
* @param containerSelector - CSS selector or HTMLElement for the mount point
|
|
112
|
+
*/
|
|
113
|
+
async render(
|
|
114
|
+
componentName: string,
|
|
115
|
+
containerSelector: string | HTMLElement
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
if (!this.engine) {
|
|
118
|
+
throw new Error("[Hypen] Engine not initialized. Call init() first.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get the container element
|
|
122
|
+
if (typeof containerSelector === "string") {
|
|
123
|
+
const element = document.querySelector(containerSelector);
|
|
124
|
+
if (!element) {
|
|
125
|
+
throw new Error(`[Hypen] Container not found: ${containerSelector}`);
|
|
126
|
+
}
|
|
127
|
+
this.container = element as HTMLElement;
|
|
128
|
+
} else {
|
|
129
|
+
this.container = containerSelector;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get the component definition
|
|
133
|
+
const component = componentLoader.get(componentName);
|
|
134
|
+
if (!component) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`[Hypen] Component "${componentName}" not found. Available: ${componentLoader.getNames().join(", ")}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.config.debug) {
|
|
141
|
+
console.log(`[Hypen] Rendering ${componentName} to`, this.container);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create renderer with debug config
|
|
145
|
+
this.renderer = new DOMRenderer(this.container, this.engine, {
|
|
146
|
+
enabled: this.config.debugHeatmap || false,
|
|
147
|
+
showHeatmap: this.config.debugHeatmap || false,
|
|
148
|
+
heatmapIncrement: this.config.heatmapIncrement || 5,
|
|
149
|
+
fadeOutDuration: this.config.heatmapFadeOut || 2000,
|
|
150
|
+
maxOpacity: 0.8,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Set render callback
|
|
154
|
+
this.engine.setRenderCallback((patches) => {
|
|
155
|
+
if (this.config.debug) {
|
|
156
|
+
console.log(`[Hypen] Applying ${patches.length} patches`);
|
|
157
|
+
}
|
|
158
|
+
this.renderer!.applyPatches(patches);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Create router context
|
|
162
|
+
const routerContext: RouterContext = {
|
|
163
|
+
root: this.router,
|
|
164
|
+
current: this.router,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Set context on renderer for component composition
|
|
168
|
+
this.renderer.setContext(routerContext, this.globalContext);
|
|
169
|
+
|
|
170
|
+
// Extract module ID from component name or .id() applicator
|
|
171
|
+
const moduleId = this.extractModuleId(componentName, component.template);
|
|
172
|
+
|
|
173
|
+
// Create module instance with router and global context
|
|
174
|
+
this.moduleInstance = new HypenModuleInstance(
|
|
175
|
+
this.engine,
|
|
176
|
+
component.module,
|
|
177
|
+
routerContext,
|
|
178
|
+
this.globalContext
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Register module in global context
|
|
182
|
+
this.globalContext.registerModule(moduleId, this.moduleInstance);
|
|
183
|
+
this.moduleInstances.set(moduleId, this.moduleInstance);
|
|
184
|
+
|
|
185
|
+
// Connect module state changes to renderer
|
|
186
|
+
this.moduleInstance.onStateChange(() => {
|
|
187
|
+
const mergedState = this.getMergedState();
|
|
188
|
+
if (this.config.debug) {
|
|
189
|
+
console.log(`[Hypen] State changed, merged state:`, mergedState);
|
|
190
|
+
}
|
|
191
|
+
this.renderer!.updateState(mergedState);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Set up component resolver for dynamic component composition
|
|
195
|
+
this.setupComponentResolver();
|
|
196
|
+
|
|
197
|
+
// Create module instances for ALL components that have state
|
|
198
|
+
this.createNestedModuleInstances();
|
|
199
|
+
|
|
200
|
+
// Render the UI template
|
|
201
|
+
this.engine.renderSource(component.template);
|
|
202
|
+
|
|
203
|
+
// Update renderer with initial state
|
|
204
|
+
this.renderer!.updateState(this.getMergedState());
|
|
205
|
+
|
|
206
|
+
if (this.config.debug) {
|
|
207
|
+
console.log(`[Hypen] ${componentName} rendered successfully`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Unmount and cleanup
|
|
213
|
+
*/
|
|
214
|
+
async unmount(): Promise<void> {
|
|
215
|
+
if (this.config.debug) {
|
|
216
|
+
console.log("[Hypen] Unmounting...");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.moduleInstance) {
|
|
220
|
+
await this.moduleInstance.destroy();
|
|
221
|
+
this.moduleInstance = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (this.container) {
|
|
225
|
+
this.container.innerHTML = "";
|
|
226
|
+
this.container = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.renderer = null;
|
|
230
|
+
|
|
231
|
+
if (this.config.debug) {
|
|
232
|
+
console.log("[Hypen] Unmounted");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get the current module instance
|
|
238
|
+
*/
|
|
239
|
+
getModuleInstance(): HypenModuleInstance<any> | null {
|
|
240
|
+
return this.moduleInstance;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the current state
|
|
245
|
+
*/
|
|
246
|
+
getState(): any {
|
|
247
|
+
return this.moduleInstance?.getState() ?? null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the router instance
|
|
252
|
+
*/
|
|
253
|
+
getRouter(): HypenRouter {
|
|
254
|
+
return this.router;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the global context
|
|
259
|
+
*/
|
|
260
|
+
getGlobalContext(): HypenGlobalContext {
|
|
261
|
+
return this.globalContext;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Enable or disable debug heatmap mode
|
|
266
|
+
*/
|
|
267
|
+
setDebugHeatmap(enabled: boolean): void {
|
|
268
|
+
if (this.renderer) {
|
|
269
|
+
this.renderer.setDebugConfig({ enabled, showHeatmap: enabled });
|
|
270
|
+
if (this.config.debug) {
|
|
271
|
+
console.log(`[Hypen] Debug heatmap ${enabled ? "enabled" : "disabled"}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Reset debug tracking for all elements
|
|
278
|
+
*/
|
|
279
|
+
resetDebugTracking(): void {
|
|
280
|
+
if (this.renderer) {
|
|
281
|
+
this.renderer.resetDebugTracking();
|
|
282
|
+
if (this.config.debug) {
|
|
283
|
+
console.log(`[Hypen] Debug tracking reset`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get debug statistics
|
|
290
|
+
*/
|
|
291
|
+
getDebugStats(): {
|
|
292
|
+
totalRerenders: number;
|
|
293
|
+
elementCount: number;
|
|
294
|
+
avgRerenders: number;
|
|
295
|
+
} | null {
|
|
296
|
+
return this.renderer?.getDebugStats() || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Render a lazy route component into a specific route element
|
|
301
|
+
* This is called by the Router when a route becomes active
|
|
302
|
+
*/
|
|
303
|
+
async renderLazyRoute(
|
|
304
|
+
routePath: string,
|
|
305
|
+
componentName: string,
|
|
306
|
+
routeElement: HTMLElement
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
if (!this.engine || !this.renderer) {
|
|
309
|
+
throw new Error("Engine not initialized");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get the component from the loader
|
|
313
|
+
const component = componentLoader.get(componentName);
|
|
314
|
+
if (!component) {
|
|
315
|
+
throw new Error(`Component ${componentName} not found`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Get the component template
|
|
319
|
+
const template = component.template;
|
|
320
|
+
if (!template) {
|
|
321
|
+
throw new Error(`Component ${componentName} has no template`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create module instance for this component if it has state
|
|
325
|
+
if (component.module && !this.moduleInstances.has(componentName)) {
|
|
326
|
+
const moduleId = this.extractModuleId(componentName, template);
|
|
327
|
+
|
|
328
|
+
const routerContext: RouterContext = {
|
|
329
|
+
root: this.router,
|
|
330
|
+
current: this.router,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const moduleInstance = new HypenModuleInstance(
|
|
334
|
+
this.engine,
|
|
335
|
+
component.module,
|
|
336
|
+
routerContext,
|
|
337
|
+
this.globalContext
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
this.globalContext.registerModule(moduleId, moduleInstance);
|
|
341
|
+
this.moduleInstances.set(componentName, moduleInstance);
|
|
342
|
+
|
|
343
|
+
// Listen to state changes
|
|
344
|
+
moduleInstance.onStateChange(() => {
|
|
345
|
+
const mergedState = this.getMergedState();
|
|
346
|
+
if (this.config.debug) {
|
|
347
|
+
console.log(
|
|
348
|
+
`[Hypen] Lazy component ${componentName} state changed:`,
|
|
349
|
+
mergedState
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
this.renderer!.updateState(mergedState);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Wait a tick for module instance to initialize and fetch data
|
|
357
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
358
|
+
|
|
359
|
+
// Get the route element's node ID from the renderer
|
|
360
|
+
const routeNodeId = routeElement.dataset.hypenId;
|
|
361
|
+
if (!routeNodeId) {
|
|
362
|
+
throw new Error(`Route element is missing data-hypen-id attribute`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Get the current merged state
|
|
366
|
+
const mergedState = this.getMergedState();
|
|
367
|
+
|
|
368
|
+
// Render into the Route element
|
|
369
|
+
this.engine.renderInto(template, routeNodeId, mergedState);
|
|
370
|
+
|
|
371
|
+
// Ensure freshly created text nodes are interpolated with current state
|
|
372
|
+
this.renderer!.updateState(mergedState);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extract module ID from component name or .id() applicator
|
|
377
|
+
*/
|
|
378
|
+
private extractModuleId(componentName: string, template: string): string {
|
|
379
|
+
// Look for .id("CustomName") or .id('CustomName') in the template
|
|
380
|
+
const idMatch = template.match(/\.id\(["']([^"']+)["']\)/);
|
|
381
|
+
const matchedId = idMatch?.[1];
|
|
382
|
+
if (matchedId) {
|
|
383
|
+
return matchedId;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Default to component name
|
|
387
|
+
return componentName;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Create module instances for all components that have state
|
|
392
|
+
*/
|
|
393
|
+
private createNestedModuleInstances(): void {
|
|
394
|
+
const routerContext: RouterContext = {
|
|
395
|
+
root: this.router,
|
|
396
|
+
current: this.router,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Create built-in Router module instance
|
|
400
|
+
if (Router && !this.moduleInstances.has("Router")) {
|
|
401
|
+
const routerInstance = new HypenModuleInstance(
|
|
402
|
+
this.engine!,
|
|
403
|
+
Router,
|
|
404
|
+
routerContext,
|
|
405
|
+
this.globalContext
|
|
406
|
+
);
|
|
407
|
+
this.globalContext.registerModule("Router", routerInstance);
|
|
408
|
+
this.moduleInstances.set("Router", routerInstance);
|
|
409
|
+
routerInstance.onStateChange(() => {
|
|
410
|
+
const mergedState = this.getMergedState();
|
|
411
|
+
if (this.config.debug) {
|
|
412
|
+
console.log(`[Hypen] Router state changed:`, mergedState);
|
|
413
|
+
}
|
|
414
|
+
this.renderer!.updateState(mergedState);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Get all registered components
|
|
419
|
+
const componentNames = componentLoader.getNames();
|
|
420
|
+
|
|
421
|
+
for (const name of componentNames) {
|
|
422
|
+
// Skip if already created
|
|
423
|
+
if (this.moduleInstances.has(name)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const comp = componentLoader.get(name);
|
|
428
|
+
if (!comp || !comp.module) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Create module instance
|
|
433
|
+
if (this.config.debug) {
|
|
434
|
+
console.log(`[Hypen] Creating nested module instance for: ${name}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const moduleInstance = new HypenModuleInstance(
|
|
438
|
+
this.engine!,
|
|
439
|
+
comp.module,
|
|
440
|
+
routerContext,
|
|
441
|
+
this.globalContext
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
this.globalContext.registerModule(name, moduleInstance);
|
|
445
|
+
this.moduleInstances.set(name, moduleInstance);
|
|
446
|
+
|
|
447
|
+
// Connect state changes to renderer
|
|
448
|
+
moduleInstance.onStateChange(() => {
|
|
449
|
+
const mergedState = this.getMergedState();
|
|
450
|
+
if (this.config.debug) {
|
|
451
|
+
console.log(
|
|
452
|
+
`[Hypen] Nested component ${name} state changed:`,
|
|
453
|
+
mergedState
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
this.renderer!.updateState(mergedState);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get merged state from all module instances
|
|
463
|
+
*/
|
|
464
|
+
private getMergedState(): Record<string, any> {
|
|
465
|
+
const merged: Record<string, any> = {};
|
|
466
|
+
|
|
467
|
+
// Include main module state
|
|
468
|
+
if (this.moduleInstance) {
|
|
469
|
+
const mainState = this.moduleInstance.getState();
|
|
470
|
+
Object.assign(merged, mainState);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Include all nested component states
|
|
474
|
+
for (const [name, instance] of this.moduleInstances.entries()) {
|
|
475
|
+
const nestedState = instance.getState();
|
|
476
|
+
Object.assign(merged, nestedState);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return merged;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Set up component resolver for the engine
|
|
484
|
+
*/
|
|
485
|
+
private setupComponentResolver(): void {
|
|
486
|
+
if (!this.engine) return;
|
|
487
|
+
|
|
488
|
+
// List of built-in DOM elements that should NOT be resolved
|
|
489
|
+
const builtInElements = new Set([
|
|
490
|
+
"Column",
|
|
491
|
+
"Row",
|
|
492
|
+
"Text",
|
|
493
|
+
"Button",
|
|
494
|
+
"Image",
|
|
495
|
+
"Input",
|
|
496
|
+
"Container",
|
|
497
|
+
"Box",
|
|
498
|
+
"Center",
|
|
499
|
+
"List",
|
|
500
|
+
"Canvas",
|
|
501
|
+
"Spacer",
|
|
502
|
+
"Divider",
|
|
503
|
+
"ScrollView",
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
this.engine.setComponentResolver(
|
|
507
|
+
(componentName: string, contextPath: string | null) => {
|
|
508
|
+
// Don't try to resolve built-in DOM elements
|
|
509
|
+
if (builtInElements.has(componentName)) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (this.config.debug) {
|
|
514
|
+
console.log(
|
|
515
|
+
`[Hypen] Resolving component: ${componentName} (context: ${contextPath})`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check if component is registered
|
|
520
|
+
const componentDef = componentLoader.get(componentName);
|
|
521
|
+
if (!componentDef) {
|
|
522
|
+
if (this.config.debug) {
|
|
523
|
+
console.log(`[Hypen] Component not found: ${componentName}`);
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Router is passthrough, Route is lazy
|
|
529
|
+
const isPassthrough = componentName === "Router";
|
|
530
|
+
const isLazy = componentName === "Route";
|
|
531
|
+
|
|
532
|
+
const resolved = {
|
|
533
|
+
source: componentDef.template,
|
|
534
|
+
path: componentDef.path || componentName,
|
|
535
|
+
passthrough: isPassthrough,
|
|
536
|
+
lazy: isLazy,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
if (this.config.debug) {
|
|
540
|
+
const flags = [];
|
|
541
|
+
if (isPassthrough) flags.push("passthrough");
|
|
542
|
+
if (isLazy) flags.push("lazy");
|
|
543
|
+
const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
544
|
+
console.log(
|
|
545
|
+
`[Hypen] Resolved ${componentName} -> ${resolved.path}${flagStr}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return resolved;
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Quick render function (like ReactDOM.render)
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```typescript
|
|
560
|
+
* import { render } from "@hypen-space/web";
|
|
561
|
+
*
|
|
562
|
+
* await render("HomePage", "#app");
|
|
563
|
+
* ```
|
|
564
|
+
*/
|
|
565
|
+
export async function render(
|
|
566
|
+
componentName: string,
|
|
567
|
+
containerSelector: string | HTMLElement,
|
|
568
|
+
config?: HypenConfig
|
|
569
|
+
): Promise<Hypen> {
|
|
570
|
+
const hypen = new Hypen(config);
|
|
571
|
+
await hypen.init();
|
|
572
|
+
await hypen.loadComponents();
|
|
573
|
+
await hypen.render(componentName, containerSelector);
|
|
574
|
+
return hypen;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Render with explicit component loading
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```typescript
|
|
582
|
+
* import { renderWithComponents } from "@hypen-space/web";
|
|
583
|
+
* import HomePage from "./components/HomePage/component";
|
|
584
|
+
* import homePageTemplate from "./components/HomePage/component.hypen";
|
|
585
|
+
*
|
|
586
|
+
* await renderWithComponents(
|
|
587
|
+
* { HomePage: { module: HomePage, template: homePageTemplate } },
|
|
588
|
+
* "HomePage",
|
|
589
|
+
* "#app"
|
|
590
|
+
* );
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
export async function renderWithComponents(
|
|
594
|
+
components: Record<string, { module: HypenModuleDefinition; template: string }>,
|
|
595
|
+
componentName: string,
|
|
596
|
+
containerSelector: string | HTMLElement,
|
|
597
|
+
config?: HypenConfig
|
|
598
|
+
): Promise<Hypen> {
|
|
599
|
+
const hypen = new Hypen(config);
|
|
600
|
+
await hypen.init();
|
|
601
|
+
|
|
602
|
+
// Register components manually
|
|
603
|
+
for (const [name, { module, template }] of Object.entries(components)) {
|
|
604
|
+
let processedTemplate = template;
|
|
605
|
+
|
|
606
|
+
// If template starts with "module ComponentName {", extract just the children
|
|
607
|
+
const moduleMatch = template.match(/^\s*module\s+\w+\s*\{([\s\S]*)\}\s*$/);
|
|
608
|
+
if (moduleMatch && moduleMatch[1]) {
|
|
609
|
+
processedTemplate = moduleMatch[1].trim();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
componentLoader.register(name, module, processedTemplate);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
await hypen.render(componentName, containerSelector);
|
|
616
|
+
return hypen;
|
|
617
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,13 @@ export { RerenderTracker, type DebugConfig, defaultDebugConfig } from "./dom/deb
|
|
|
45
45
|
export { CanvasRenderer } from "./canvas/renderer.js";
|
|
46
46
|
export { canvasHandler, canvasApplicators } from "./dom/canvas/index.js";
|
|
47
47
|
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// HIGH-LEVEL API
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export { Hypen, render, renderWithComponents } from "./hypen.js";
|
|
53
|
+
export type { HypenConfig } from "./hypen.js";
|
|
54
|
+
|
|
48
55
|
// Re-export core types that web users commonly need
|
|
49
56
|
export type {
|
|
50
57
|
Patch,
|
|
@@ -53,4 +60,4 @@ export type {
|
|
|
53
60
|
RouterContext,
|
|
54
61
|
HypenModuleInstance,
|
|
55
62
|
HypenGlobalContext,
|
|
56
|
-
} from "@hypen/core";
|
|
63
|
+
} from "@hypen-space/core";
|