@constela/runtime 0.15.1 → 0.16.0

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/index.d.ts CHANGED
@@ -50,8 +50,11 @@ interface StateStore {
50
50
  getPath(name: string, path: string | (string | number)[]): unknown;
51
51
  setPath(name: string, path: string | (string | number)[], value: unknown): void;
52
52
  subscribeToPath(name: string, path: string | (string | number)[], fn: (value: unknown) => void): () => void;
53
+ serialize(): Record<string, unknown>;
54
+ restore(snapshot: Record<string, unknown>, newDefinitions: StateDefinition[]): void;
53
55
  }
54
56
  interface StateDefinition {
57
+ name?: string;
55
58
  type: string;
56
59
  initial: unknown;
57
60
  }
@@ -316,6 +319,11 @@ interface RenderContext {
316
319
  store: LocalStateStore;
317
320
  actions: Record<string, CompiledLocalAction>;
318
321
  };
322
+ route?: {
323
+ params: Record<string, string>;
324
+ query: Record<string, string>;
325
+ path: string;
326
+ };
319
327
  }
320
328
  declare function render(node: CompiledNode, ctx: RenderContext): Node;
321
329
 
@@ -385,4 +393,194 @@ interface HydrateOptions {
385
393
  */
386
394
  declare function hydrateApp(options: HydrateOptions): AppInstance;
387
395
 
388
- export { type ActionContext, type AppInstance, type Computed, type ConnectionManager, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, type StylePreset, type TypedStateStore, type WebSocketConnection, type WebSocketHandlers, createApp, createComputed, createConnectionManager, createEffect, createSignal, createStateStore, createTypedStateStore, createWebSocketConnection, evaluate, evaluateStyle, executeAction, hydrateApp, render };
396
+ /**
397
+ * HMR Client - WebSocket client for Hot Module Replacement
398
+ *
399
+ * This module provides a client-side WebSocket connection for receiving
400
+ * HMR updates from the development server.
401
+ *
402
+ * WebSocket Protocol:
403
+ * - Server -> Client: 'connected' on connection
404
+ * - Server -> Client: 'update' when file changes (file: string, program: CompiledProgram)
405
+ * - Server -> Client: 'error' when compilation fails (file: string, errors: unknown[])
406
+ */
407
+
408
+ /**
409
+ * Options for creating an HMR client
410
+ */
411
+ interface HMRClientOptions {
412
+ /** WebSocket server URL (e.g., 'ws://localhost:3001') */
413
+ url: string;
414
+ /** Called when a file update is received */
415
+ onUpdate: (file: string, program: CompiledProgram) => void;
416
+ /** Called when a compilation error is received */
417
+ onError: (file: string, errors: unknown[]) => void;
418
+ /** Called when connection is established */
419
+ onConnect?: () => void;
420
+ /** Called when connection is lost */
421
+ onDisconnect?: () => void;
422
+ /** Initial reconnect delay in milliseconds (default: 1000) */
423
+ initialReconnectDelay?: number;
424
+ /** Maximum reconnect delay in milliseconds (default: 30000) */
425
+ maxReconnectDelay?: number;
426
+ }
427
+ /**
428
+ * HMR Client interface
429
+ */
430
+ interface HMRClient {
431
+ /** Establish WebSocket connection */
432
+ connect(): void;
433
+ /** Close WebSocket connection */
434
+ disconnect(): void;
435
+ /** Check if currently connected */
436
+ isConnected(): boolean;
437
+ }
438
+ /**
439
+ * Creates an HMR client for receiving hot updates from the development server.
440
+ *
441
+ * Features:
442
+ * - WebSocket connection management
443
+ * - JSON message parsing
444
+ * - Auto-reconnect with exponential backoff
445
+ * - Connection state tracking
446
+ *
447
+ * @param options - Client configuration options
448
+ * @returns HMRClient interface for managing the connection
449
+ */
450
+ declare function createHMRClient(options: HMRClientOptions): HMRClient;
451
+
452
+ /**
453
+ * HMR Handler - Update handler for Hot Module Replacement
454
+ *
455
+ * This module handles applying HMR updates to the running application,
456
+ * preserving state across updates.
457
+ *
458
+ * Update Flow:
459
+ * 1. Serialize current state: state.serialize()
460
+ * 2. Destroy old app: app.destroy()
461
+ * 3. Render new app with new program
462
+ * 4. Restore state: state.restore(snapshot, newDefinitions)
463
+ */
464
+
465
+ /**
466
+ * Route context for the application
467
+ */
468
+ interface RouteContext {
469
+ params: Record<string, string>;
470
+ query: Record<string, string>;
471
+ path: string;
472
+ }
473
+ /**
474
+ * Options for creating an HMR handler
475
+ */
476
+ interface HMRHandlerOptions {
477
+ /** Container element for the application */
478
+ container: HTMLElement;
479
+ /** Initial compiled program */
480
+ program: CompiledProgram;
481
+ /** Optional route context */
482
+ route?: RouteContext;
483
+ }
484
+ /**
485
+ * HMR Handler interface
486
+ */
487
+ interface HMRHandler {
488
+ /** Apply a program update while preserving state */
489
+ handleUpdate(program: CompiledProgram): void;
490
+ /** Destroy the handler and cleanup resources */
491
+ destroy(): void;
492
+ }
493
+ /**
494
+ * Creates an HMR handler for applying hot updates to the running application.
495
+ *
496
+ * Update Flow:
497
+ * 1. Serialize current state: state.serialize()
498
+ * 2. Destroy old app: app.destroy()
499
+ * 3. Create new state store from new program
500
+ * 4. Restore state: state.restore(snapshot, newDefinitions)
501
+ * 5. Render new app with restored state
502
+ *
503
+ * @param options - Handler configuration options
504
+ * @returns HMRHandler interface for managing updates
505
+ */
506
+ declare function createHMRHandler(options: HMRHandlerOptions): HMRHandler;
507
+
508
+ /**
509
+ * HMR Error Overlay - Display compilation errors during development
510
+ *
511
+ * This module provides an error overlay UI that displays compilation errors
512
+ * as an overlay on the DOM, helping developers quickly identify and fix issues.
513
+ *
514
+ * Features:
515
+ * - Display file path and error details
516
+ * - Show error code and suggestions
517
+ * - Auto-hide when error is fixed
518
+ * - Styled overlay with dark background and high z-index
519
+ *
520
+ * Usage:
521
+ * ```typescript
522
+ * const overlay = createErrorOverlay();
523
+ *
524
+ * // Show error
525
+ * overlay.show({
526
+ * file: '/src/pages/index.json',
527
+ * errors: [{ code: 'PARSE_ERROR', message: 'Unexpected token' }]
528
+ * });
529
+ *
530
+ * // Hide when fixed
531
+ * overlay.hide();
532
+ * ```
533
+ */
534
+ /**
535
+ * Error information to display in the overlay
536
+ */
537
+ interface ErrorInfo {
538
+ /** File path where the error occurred */
539
+ file: string;
540
+ /** Array of error details */
541
+ errors: Array<{
542
+ /** Error code (e.g., 'PARSE_ERROR', 'UNDEFINED_STATE') */
543
+ code?: string;
544
+ /** Human-readable error message */
545
+ message: string;
546
+ /** Path to the problematic element in the JSON (e.g., 'view.children[0].value') */
547
+ path?: string;
548
+ /** Error severity ('error' | 'warning') */
549
+ severity?: string;
550
+ /** Suggestion for fixing the error */
551
+ suggestion?: string;
552
+ }>;
553
+ }
554
+ /**
555
+ * Error overlay interface for displaying and hiding compilation errors
556
+ */
557
+ interface ErrorOverlay {
558
+ /**
559
+ * Show the error overlay with the given error information
560
+ * @param errorInfo - Error details to display
561
+ */
562
+ show(errorInfo: ErrorInfo): void;
563
+ /**
564
+ * Hide the error overlay and remove it from the DOM
565
+ */
566
+ hide(): void;
567
+ /**
568
+ * Check if the overlay is currently visible
569
+ * @returns true if the overlay is visible, false otherwise
570
+ */
571
+ isVisible(): boolean;
572
+ }
573
+ /**
574
+ * Creates an error overlay instance for displaying compilation errors.
575
+ *
576
+ * The overlay is rendered as a full-screen element with:
577
+ * - Dark semi-transparent background
578
+ * - White text for readability
579
+ * - High z-index to appear above all other content
580
+ * - File path and error details prominently displayed
581
+ *
582
+ * @returns ErrorOverlay interface for managing the overlay
583
+ */
584
+ declare function createErrorOverlay(): ErrorOverlay;
585
+
586
+ export { type ActionContext, type AppInstance, type Computed, type ConnectionManager, type ErrorInfo, type ErrorOverlay, type EvaluationContext, type HMRClient, type HMRClientOptions, type HMRHandler, type HMRHandlerOptions, type HydrateOptions, type RenderContext, type RouteContext, type Signal, type StateStore, type StylePreset, type TypedStateStore, type WebSocketConnection, type WebSocketHandlers, createApp, createComputed, createConnectionManager, createEffect, createErrorOverlay, createHMRClient, createHMRHandler, createSignal, createStateStore, createTypedStateStore, createWebSocketConnection, evaluate, evaluateStyle, executeAction, hydrateApp, render };
package/dist/index.js CHANGED
@@ -240,18 +240,18 @@ function createStateStore(definitions) {
240
240
  initialValue = cookieValue !== void 0 ? cookieValue : def.initial.default;
241
241
  } else {
242
242
  initialValue = def.initial;
243
- }
244
- if (name === "theme" && typeof window !== "undefined") {
245
- try {
246
- const stored = localStorage.getItem("theme");
247
- if (stored !== null) {
248
- try {
249
- initialValue = JSON.parse(stored);
250
- } catch {
251
- initialValue = stored;
243
+ if (name === "theme" && typeof window !== "undefined") {
244
+ try {
245
+ const stored = localStorage.getItem("theme");
246
+ if (stored !== null) {
247
+ try {
248
+ initialValue = JSON.parse(stored);
249
+ } catch {
250
+ initialValue = stored;
251
+ }
252
252
  }
253
+ } catch {
253
254
  }
254
- } catch {
255
255
  }
256
256
  }
257
257
  signals.set(name, createSignal(initialValue));
@@ -319,9 +319,54 @@ function createStateStore(definitions) {
319
319
  fn(newValue);
320
320
  }
321
321
  });
322
+ },
323
+ serialize() {
324
+ const result = {};
325
+ for (const [name, signal] of signals) {
326
+ const value = signal.get();
327
+ if (typeof value !== "function") {
328
+ result[name] = value;
329
+ }
330
+ }
331
+ return result;
332
+ },
333
+ restore(snapshot, newDefinitions) {
334
+ for (const def of newDefinitions) {
335
+ const name = def.name;
336
+ if (!name) continue;
337
+ const signal = signals.get(name);
338
+ if (!signal) continue;
339
+ if (!(name in snapshot)) {
340
+ continue;
341
+ }
342
+ const snapshotValue = snapshot[name];
343
+ const initialValue = def.initial;
344
+ if (typesMatch(snapshotValue, initialValue)) {
345
+ signal.set(snapshotValue);
346
+ } else {
347
+ console.warn(
348
+ `State field "${name}" type changed. Using new initial value.`
349
+ );
350
+ signal.set(initialValue);
351
+ }
352
+ }
322
353
  }
323
354
  };
324
355
  }
356
+ function typesMatch(a, b2) {
357
+ if (a === null) {
358
+ return b2 === null || typeof b2 === "object" && !Array.isArray(b2);
359
+ }
360
+ if (b2 === null) {
361
+ return typeof a === "object" && !Array.isArray(a);
362
+ }
363
+ const aIsArray = Array.isArray(a);
364
+ const bIsArray = Array.isArray(b2);
365
+ if (aIsArray || bIsArray) {
366
+ return aIsArray && bIsArray;
367
+ }
368
+ return typeof a === typeof b2;
369
+ }
325
370
 
326
371
  // src/state/typed.ts
327
372
  function createTypedStateStore(definitions) {
@@ -13674,7 +13719,7 @@ function renderElement(node, ctx) {
13674
13719
  }
13675
13720
  } else {
13676
13721
  const cleanup = createEffect(() => {
13677
- const value = evaluate(propValue, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
13722
+ const value = evaluate(propValue, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } });
13678
13723
  applyProp(el, propName, value, useSvgNamespace);
13679
13724
  });
13680
13725
  ctx.cleanups?.push(cleanup);
@@ -13729,7 +13774,7 @@ function applyProp(el, propName, value, isSvg = false) {
13729
13774
  function renderText(node, ctx) {
13730
13775
  const textNode = document.createTextNode("");
13731
13776
  const cleanup = createEffect(() => {
13732
- const value = evaluate(node.value, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
13777
+ const value = evaluate(node.value, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } });
13733
13778
  textNode.textContent = formatValue(value);
13734
13779
  });
13735
13780
  ctx.cleanups?.push(cleanup);
@@ -13750,7 +13795,7 @@ function renderIf(node, ctx) {
13750
13795
  let currentBranch = "none";
13751
13796
  let branchCleanups = [];
13752
13797
  const effectCleanup = createEffect(() => {
13753
- const condition = evaluate(node.condition, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
13798
+ const condition = evaluate(node.condition, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } });
13754
13799
  const shouldShowThen = Boolean(condition);
13755
13800
  const newBranch = shouldShowThen ? "then" : node.else ? "else" : "none";
13756
13801
  if (newBranch !== currentBranch) {
@@ -13828,7 +13873,7 @@ function renderEach(node, ctx) {
13828
13873
  let currentNodes = [];
13829
13874
  let itemCleanups = [];
13830
13875
  const effectCleanup = createEffect(() => {
13831
- const items = evaluate(node.items, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
13876
+ const items = evaluate(node.items, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } });
13832
13877
  if (!hasKey || !node.key) {
13833
13878
  for (const cleanup of itemCleanups) {
13834
13879
  cleanup();
@@ -13977,7 +14022,7 @@ function renderMarkdown(node, ctx) {
13977
14022
  const container = document.createElement("div");
13978
14023
  container.className = "constela-markdown";
13979
14024
  const cleanup = createEffect(() => {
13980
- const content = evaluate(node.content, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
14025
+ const content = evaluate(node.content, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } });
13981
14026
  const html6 = parseMarkdown(String(content ?? ""));
13982
14027
  container.innerHTML = html6;
13983
14028
  });
@@ -13992,8 +14037,8 @@ function renderCode(node, ctx) {
13992
14037
  container.appendChild(pre);
13993
14038
  pre.appendChild(codeEl);
13994
14039
  const cleanup = createEffect(() => {
13995
- const language = String(evaluate(node.language, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } }) ?? "plaintext");
13996
- const content = String(evaluate(node.content, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } }) ?? "");
14040
+ const language = String(evaluate(node.language, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } }) ?? "plaintext");
14041
+ const content = String(evaluate(node.content, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports }, ...ctx.route && { route: ctx.route } }) ?? "");
13997
14042
  codeEl.className = `language-${language || "plaintext"}`;
13998
14043
  codeEl.dataset["language"] = language || "plaintext";
13999
14044
  container.dataset["language"] = language || "plaintext";
@@ -14113,6 +14158,12 @@ function createStateWithLocalState(globalState, localStore) {
14113
14158
  },
14114
14159
  subscribeToPath(name, path, fn) {
14115
14160
  return globalState.subscribeToPath(name, path, fn);
14161
+ },
14162
+ serialize() {
14163
+ return globalState.serialize();
14164
+ },
14165
+ restore(snapshot, newDefinitions) {
14166
+ globalState.restore(snapshot, newDefinitions);
14116
14167
  }
14117
14168
  };
14118
14169
  }
@@ -14393,6 +14444,21 @@ function hydrateElement(node, el, ctx) {
14393
14444
  hydrateChildren(node.children, el, ctx);
14394
14445
  }
14395
14446
  }
14447
+ function findSsrIfBranchMarker(parent, beforeNode) {
14448
+ let current = beforeNode ? beforeNode.previousSibling : parent.lastChild;
14449
+ while (current) {
14450
+ if (current.nodeType === Node.COMMENT_NODE) {
14451
+ const comment2 = current;
14452
+ const text3 = comment2.textContent;
14453
+ if (text3 === "if:then") return { branch: "then", marker: comment2 };
14454
+ if (text3 === "if:else") return { branch: "else", marker: comment2 };
14455
+ if (text3 === "if:none") return { branch: "none", marker: comment2 };
14456
+ }
14457
+ if (current.nodeType === Node.ELEMENT_NODE) break;
14458
+ current = current.previousSibling;
14459
+ }
14460
+ return null;
14461
+ }
14396
14462
  function hydrateChildren(children, parent, ctx) {
14397
14463
  const domChildren = [];
14398
14464
  for (let i = 0; i < parent.childNodes.length; i++) {
@@ -14423,19 +14489,35 @@ function hydrateChildren(children, parent, ctx) {
14423
14489
  }
14424
14490
  } else if (childNode.kind === "if") {
14425
14491
  const ifNode = childNode;
14426
- const initialCondition = evaluate(ifNode.condition, {
14492
+ const clientCondition = evaluate(ifNode.condition, {
14427
14493
  state: ctx.state,
14428
14494
  locals: ctx.locals,
14429
14495
  ...ctx.imports && { imports: ctx.imports },
14430
14496
  ...ctx.route && { route: ctx.route }
14431
14497
  });
14432
- const hasDomForIf = Boolean(initialCondition) || Boolean(ifNode.else);
14498
+ const clientBranch = Boolean(clientCondition) ? "then" : ifNode.else ? "else" : "none";
14433
14499
  const domChild = domChildren[domIndex];
14434
- if (hasDomForIf && domChild) {
14435
- hydrate(childNode, domChild, ctx);
14500
+ const ssrInfo = findSsrIfBranchMarker(parent, domChild || null);
14501
+ const ssrBranch = ssrInfo?.branch ?? null;
14502
+ const ssrHasDom = ssrBranch === "then" || ssrBranch === "else";
14503
+ if (ssrInfo?.marker) {
14504
+ ssrInfo.marker.remove();
14505
+ }
14506
+ if (ssrHasDom && domChild) {
14507
+ hydrateIf(ifNode, domChild, ctx, { ssrBranch, clientBranch });
14436
14508
  domIndex++;
14509
+ } else if (ssrBranch === "none") {
14510
+ hydrateIfWithoutDom(ifNode, parent, domChildren[domIndex] || null, ctx, {
14511
+ clientBranch
14512
+ });
14437
14513
  } else {
14438
- hydrateIfWithoutDom(ifNode, parent, domChildren[domIndex] || null, ctx);
14514
+ const hasDomForIf = Boolean(clientCondition) || Boolean(ifNode.else);
14515
+ if (hasDomForIf && domChild) {
14516
+ hydrate(childNode, domChild, ctx);
14517
+ domIndex++;
14518
+ } else {
14519
+ hydrateIfWithoutDom(ifNode, parent, domChildren[domIndex] || null, ctx);
14520
+ }
14439
14521
  }
14440
14522
  } else if (childNode.kind === "each") {
14441
14523
  const items = evaluate(childNode.items, {
@@ -14525,32 +14607,37 @@ function formatValue2(value) {
14525
14607
  }
14526
14608
  return String(value);
14527
14609
  }
14528
- function hydrateIf(node, initialDomNode, ctx) {
14610
+ function hydrateIf(node, initialDomNode, ctx, branchInfo) {
14529
14611
  const anchor = document.createComment("if");
14530
14612
  const parent = initialDomNode.parentNode;
14531
14613
  if (!parent) return;
14532
14614
  parent.insertBefore(anchor, initialDomNode);
14533
14615
  let currentNode = initialDomNode;
14534
- let currentBranch = "none";
14535
14616
  let branchCleanups = [];
14536
14617
  let isFirstRun = true;
14537
- const initialCondition = evaluate(node.condition, {
14538
- state: ctx.state,
14539
- locals: ctx.locals,
14540
- ...ctx.imports && { imports: ctx.imports },
14541
- ...ctx.route && { route: ctx.route }
14542
- });
14543
- currentBranch = Boolean(initialCondition) ? "then" : node.else ? "else" : "none";
14544
- if (currentBranch === "then") {
14545
- const localCleanups = [];
14546
- const branchCtx = { ...ctx, cleanups: localCleanups };
14547
- hydrate(node.then, currentNode, branchCtx);
14548
- branchCleanups = localCleanups;
14549
- } else if (currentBranch === "else" && node.else) {
14550
- const localCleanups = [];
14551
- const branchCtx = { ...ctx, cleanups: localCleanups };
14552
- hydrate(node.else, currentNode, branchCtx);
14553
- branchCleanups = localCleanups;
14618
+ const hasMismatch = branchInfo && branchInfo.ssrBranch !== branchInfo.clientBranch;
14619
+ let currentBranch = branchInfo?.ssrBranch ?? "none";
14620
+ if (!branchInfo) {
14621
+ const initialCondition = evaluate(node.condition, {
14622
+ state: ctx.state,
14623
+ locals: ctx.locals,
14624
+ ...ctx.imports && { imports: ctx.imports },
14625
+ ...ctx.route && { route: ctx.route }
14626
+ });
14627
+ currentBranch = Boolean(initialCondition) ? "then" : node.else ? "else" : "none";
14628
+ }
14629
+ if (!hasMismatch) {
14630
+ if (currentBranch === "then") {
14631
+ const localCleanups = [];
14632
+ const branchCtx = { ...ctx, cleanups: localCleanups };
14633
+ hydrate(node.then, currentNode, branchCtx);
14634
+ branchCleanups = localCleanups;
14635
+ } else if (currentBranch === "else" && node.else) {
14636
+ const localCleanups = [];
14637
+ const branchCtx = { ...ctx, cleanups: localCleanups };
14638
+ hydrate(node.else, currentNode, branchCtx);
14639
+ branchCleanups = localCleanups;
14640
+ }
14554
14641
  }
14555
14642
  const effectCleanup = createEffect(() => {
14556
14643
  const condition = evaluate(node.condition, {
@@ -14563,7 +14650,10 @@ function hydrateIf(node, initialDomNode, ctx) {
14563
14650
  const newBranch = shouldShowThen ? "then" : node.else ? "else" : "none";
14564
14651
  if (isFirstRun) {
14565
14652
  isFirstRun = false;
14566
- return;
14653
+ if (hasMismatch) {
14654
+ } else {
14655
+ return;
14656
+ }
14567
14657
  }
14568
14658
  if (newBranch !== currentBranch) {
14569
14659
  for (const cleanup of branchCleanups) {
@@ -14603,7 +14693,7 @@ function hydrateIf(node, initialDomNode, ctx) {
14603
14693
  }
14604
14694
  });
14605
14695
  }
14606
- function hydrateIfWithoutDom(node, parent, nextSibling, ctx) {
14696
+ function hydrateIfWithoutDom(node, parent, nextSibling, ctx, branchInfo) {
14607
14697
  const anchor = document.createComment("if");
14608
14698
  if (nextSibling) {
14609
14699
  parent.insertBefore(anchor, nextSibling);
@@ -14613,6 +14703,8 @@ function hydrateIfWithoutDom(node, parent, nextSibling, ctx) {
14613
14703
  let currentNode = null;
14614
14704
  let currentBranch = "none";
14615
14705
  let branchCleanups = [];
14706
+ const needsImmediateRender = branchInfo && branchInfo.clientBranch !== "none";
14707
+ let isFirstRun = true;
14616
14708
  const effectCleanup = createEffect(() => {
14617
14709
  const condition = evaluate(node.condition, {
14618
14710
  state: ctx.state,
@@ -14622,6 +14714,13 @@ function hydrateIfWithoutDom(node, parent, nextSibling, ctx) {
14622
14714
  });
14623
14715
  const shouldShowThen = Boolean(condition);
14624
14716
  const newBranch = shouldShowThen ? "then" : node.else ? "else" : "none";
14717
+ if (isFirstRun) {
14718
+ isFirstRun = false;
14719
+ if (needsImmediateRender) {
14720
+ } else if (newBranch === "none") {
14721
+ return;
14722
+ }
14723
+ }
14625
14724
  if (newBranch !== currentBranch) {
14626
14725
  for (const cleanup of branchCleanups) {
14627
14726
  cleanup();
@@ -15023,11 +15122,304 @@ function createConnectionManager() {
15023
15122
  }
15024
15123
  };
15025
15124
  }
15125
+
15126
+ // src/hmr/client.ts
15127
+ var DEFAULT_INITIAL_RECONNECT_DELAY = 1e3;
15128
+ var DEFAULT_MAX_RECONNECT_DELAY = 3e4;
15129
+ function createHMRClient(options) {
15130
+ const {
15131
+ url,
15132
+ onUpdate,
15133
+ onError,
15134
+ onConnect,
15135
+ onDisconnect,
15136
+ initialReconnectDelay = DEFAULT_INITIAL_RECONNECT_DELAY,
15137
+ maxReconnectDelay = DEFAULT_MAX_RECONNECT_DELAY
15138
+ } = options;
15139
+ let ws = null;
15140
+ let isManualDisconnect = false;
15141
+ let reconnectDelay = initialReconnectDelay;
15142
+ let reconnectTimeoutId = null;
15143
+ function handleMessage(event) {
15144
+ try {
15145
+ const message = JSON.parse(event.data);
15146
+ switch (message.type) {
15147
+ case "connected":
15148
+ break;
15149
+ case "update":
15150
+ if (message.file && message.program) {
15151
+ onUpdate(message.file, message.program);
15152
+ }
15153
+ break;
15154
+ case "error":
15155
+ if (message.file && message.errors) {
15156
+ onError(message.file, message.errors);
15157
+ }
15158
+ break;
15159
+ default:
15160
+ break;
15161
+ }
15162
+ } catch {
15163
+ }
15164
+ }
15165
+ function handleOpen() {
15166
+ reconnectDelay = initialReconnectDelay;
15167
+ onConnect?.();
15168
+ }
15169
+ function handleClose() {
15170
+ onDisconnect?.();
15171
+ if (!isManualDisconnect) {
15172
+ scheduleReconnect();
15173
+ }
15174
+ }
15175
+ function handleError() {
15176
+ }
15177
+ function scheduleReconnect() {
15178
+ if (reconnectTimeoutId !== null) {
15179
+ return;
15180
+ }
15181
+ reconnectTimeoutId = setTimeout(() => {
15182
+ reconnectTimeoutId = null;
15183
+ if (!isManualDisconnect) {
15184
+ ws = null;
15185
+ createConnection();
15186
+ reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
15187
+ }
15188
+ }, reconnectDelay);
15189
+ }
15190
+ function createConnection() {
15191
+ ws = new WebSocket(url);
15192
+ ws.onopen = handleOpen;
15193
+ ws.onclose = handleClose;
15194
+ ws.onerror = handleError;
15195
+ ws.onmessage = handleMessage;
15196
+ }
15197
+ return {
15198
+ connect() {
15199
+ if (ws && ws.readyState === WebSocket.OPEN) {
15200
+ return;
15201
+ }
15202
+ isManualDisconnect = false;
15203
+ if (reconnectTimeoutId !== null) {
15204
+ clearTimeout(reconnectTimeoutId);
15205
+ reconnectTimeoutId = null;
15206
+ }
15207
+ createConnection();
15208
+ },
15209
+ disconnect() {
15210
+ isManualDisconnect = true;
15211
+ if (reconnectTimeoutId !== null) {
15212
+ clearTimeout(reconnectTimeoutId);
15213
+ reconnectTimeoutId = null;
15214
+ }
15215
+ if (ws) {
15216
+ ws.close();
15217
+ ws = null;
15218
+ }
15219
+ },
15220
+ isConnected() {
15221
+ return ws !== null && ws.readyState === WebSocket.OPEN;
15222
+ }
15223
+ };
15224
+ }
15225
+
15226
+ // src/hmr/handler.ts
15227
+ function createHMRApp(program, container, route, existingStateStore) {
15228
+ const state = existingStateStore ?? createStateStore(program.state);
15229
+ let actions;
15230
+ if (program.actions instanceof Map) {
15231
+ actions = {};
15232
+ program.actions.forEach((action, name) => {
15233
+ actions[name] = action;
15234
+ });
15235
+ } else {
15236
+ actions = program.actions;
15237
+ }
15238
+ const cleanups = [];
15239
+ const refs = {};
15240
+ const ctx = {
15241
+ state,
15242
+ actions,
15243
+ locals: {},
15244
+ cleanups,
15245
+ refs,
15246
+ ...route && { route },
15247
+ ...program.importData && { imports: program.importData }
15248
+ };
15249
+ const rootNode = render(program.view, ctx);
15250
+ container.appendChild(rootNode);
15251
+ const actionCtx = {
15252
+ state,
15253
+ actions,
15254
+ locals: {},
15255
+ refs,
15256
+ ...route && { route },
15257
+ ...program.importData && { imports: program.importData }
15258
+ };
15259
+ if (program.lifecycle?.onMount) {
15260
+ const onMountAction = actions[program.lifecycle.onMount];
15261
+ if (onMountAction) {
15262
+ void executeAction(onMountAction, actionCtx);
15263
+ }
15264
+ }
15265
+ let destroyed = false;
15266
+ return {
15267
+ stateStore: state,
15268
+ destroy() {
15269
+ if (destroyed) return;
15270
+ destroyed = true;
15271
+ if (program.lifecycle?.onUnmount) {
15272
+ const onUnmountAction = actions[program.lifecycle.onUnmount];
15273
+ if (onUnmountAction) {
15274
+ void executeAction(onUnmountAction, actionCtx);
15275
+ }
15276
+ }
15277
+ for (const cleanup of cleanups) {
15278
+ cleanup();
15279
+ }
15280
+ while (container.firstChild) {
15281
+ container.removeChild(container.firstChild);
15282
+ }
15283
+ },
15284
+ setState(name, value) {
15285
+ if (destroyed) return;
15286
+ state.set(name, value);
15287
+ },
15288
+ getState(name) {
15289
+ return state.get(name);
15290
+ },
15291
+ subscribe(name, fn) {
15292
+ if (destroyed) return () => {
15293
+ };
15294
+ return state.subscribe(name, fn);
15295
+ }
15296
+ };
15297
+ }
15298
+ function createHMRHandler(options) {
15299
+ const { container, program, route } = options;
15300
+ let currentApp = null;
15301
+ let destroyed = false;
15302
+ currentApp = createHMRApp(program, container, route);
15303
+ return {
15304
+ handleUpdate(newProgram) {
15305
+ if (destroyed) {
15306
+ return;
15307
+ }
15308
+ let stateSnapshot = {};
15309
+ if (currentApp) {
15310
+ stateSnapshot = currentApp.stateStore.serialize();
15311
+ currentApp.destroy();
15312
+ currentApp = null;
15313
+ }
15314
+ const newStateStore = createStateStore(newProgram.state);
15315
+ const newDefinitions = Object.entries(newProgram.state).map(
15316
+ ([name, def]) => ({
15317
+ name,
15318
+ ...def
15319
+ })
15320
+ );
15321
+ newStateStore.restore(stateSnapshot, newDefinitions);
15322
+ currentApp = createHMRApp(newProgram, container, route, newStateStore);
15323
+ },
15324
+ destroy() {
15325
+ if (destroyed) {
15326
+ return;
15327
+ }
15328
+ destroyed = true;
15329
+ if (currentApp) {
15330
+ currentApp.destroy();
15331
+ currentApp = null;
15332
+ }
15333
+ }
15334
+ };
15335
+ }
15336
+
15337
+ // src/utils/escape.ts
15338
+ function escapeHtml(text3) {
15339
+ const div = document.createElement("div");
15340
+ div.textContent = text3;
15341
+ return div.innerHTML;
15342
+ }
15343
+
15344
+ // src/hmr/overlay.ts
15345
+ var OVERLAY_ATTRIBUTE = "data-constela-error-overlay";
15346
+ function createOverlayElement() {
15347
+ const element2 = document.createElement("div");
15348
+ element2.setAttribute(OVERLAY_ATTRIBUTE, "");
15349
+ Object.assign(element2.style, {
15350
+ position: "fixed",
15351
+ top: "0",
15352
+ left: "0",
15353
+ right: "0",
15354
+ bottom: "0",
15355
+ width: "100%",
15356
+ height: "100%",
15357
+ backgroundColor: "rgba(0, 0, 0, 0.9)",
15358
+ color: "#ffffff",
15359
+ zIndex: "9999",
15360
+ overflow: "auto",
15361
+ padding: "24px",
15362
+ boxSizing: "border-box",
15363
+ fontFamily: "monospace",
15364
+ fontSize: "14px",
15365
+ lineHeight: "1.5"
15366
+ });
15367
+ return element2;
15368
+ }
15369
+ function renderErrorContent(errorInfo) {
15370
+ const errorItems = errorInfo.errors.map((error) => {
15371
+ const parts = [];
15372
+ if (error.code) {
15373
+ parts.push(`<span style="color: #ff6b6b; font-weight: bold;">${escapeHtml(error.code)}</span>`);
15374
+ }
15375
+ parts.push(`<div style="margin-top: 4px;">${escapeHtml(error.message)}</div>`);
15376
+ if (error.path) {
15377
+ parts.push(`<div style="color: #888; margin-top: 4px;">at ${escapeHtml(error.path)}</div>`);
15378
+ }
15379
+ if (error.suggestion) {
15380
+ parts.push(`<div style="color: #4ecdc4; margin-top: 8px;">${escapeHtml(error.suggestion)}</div>`);
15381
+ }
15382
+ return `<div style="background: rgba(255, 255, 255, 0.05); padding: 16px; border-radius: 4px; margin-bottom: 12px;">${parts.join("")}</div>`;
15383
+ }).join("");
15384
+ return `
15385
+ <div style="margin-bottom: 24px;">
15386
+ <div style="color: #ff6b6b; font-size: 18px; font-weight: bold; margin-bottom: 8px;">Compilation Error</div>
15387
+ <div style="color: #aaa; word-break: break-all;">${escapeHtml(errorInfo.file)}</div>
15388
+ </div>
15389
+ <div>${errorItems}</div>
15390
+ `;
15391
+ }
15392
+ function createErrorOverlay() {
15393
+ let overlayElement = null;
15394
+ return {
15395
+ show(errorInfo) {
15396
+ if (overlayElement && document.body.contains(overlayElement)) {
15397
+ overlayElement.innerHTML = renderErrorContent(errorInfo);
15398
+ return;
15399
+ }
15400
+ overlayElement = createOverlayElement();
15401
+ overlayElement.innerHTML = renderErrorContent(errorInfo);
15402
+ document.body.appendChild(overlayElement);
15403
+ },
15404
+ hide() {
15405
+ if (overlayElement && document.body.contains(overlayElement)) {
15406
+ document.body.removeChild(overlayElement);
15407
+ }
15408
+ overlayElement = null;
15409
+ },
15410
+ isVisible() {
15411
+ return overlayElement !== null && document.body.contains(overlayElement);
15412
+ }
15413
+ };
15414
+ }
15026
15415
  export {
15027
15416
  createApp,
15028
15417
  createComputed,
15029
15418
  createConnectionManager,
15030
15419
  createEffect,
15420
+ createErrorOverlay,
15421
+ createHMRClient,
15422
+ createHMRHandler,
15031
15423
  createSignal,
15032
15424
  createStateStore,
15033
15425
  createTypedStateStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Runtime DOM renderer for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.3.0",
31
31
  "vitest": "^2.0.0",
32
- "@constela/server": "8.0.0"
32
+ "@constela/server": "8.0.1"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"