@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 +199 -1
- package/dist/index.js +435 -43
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
|
14498
|
+
const clientBranch = Boolean(clientCondition) ? "then" : ifNode.else ? "else" : "none";
|
|
14433
14499
|
const domChild = domChildren[domIndex];
|
|
14434
|
-
|
|
14435
|
-
|
|
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
|
-
|
|
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
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
32
|
+
"@constela/server": "8.0.1"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|