tova 0.3.4 → 0.3.6
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/bin/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
|
@@ -9,11 +9,21 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
9
9
|
this.componentNames = new Set(); // Track component names for JSX
|
|
10
10
|
this.storeNames = new Set(); // Track store names
|
|
11
11
|
this._asyncContext = false; // When true, server.xxx() calls emit `await`
|
|
12
|
+
this._rpcCache = new WeakMap(); // Memoize _containsRPC() results
|
|
13
|
+
this._signalCache = new WeakMap(); // Memoize _exprReadsSignal() results
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
// AST-walk to check if a subtree contains server.xxx() RPC calls
|
|
16
|
+
// AST-walk to check if a subtree contains server.xxx() RPC calls (memoized)
|
|
15
17
|
_containsRPC(node) {
|
|
16
18
|
if (!node) return false;
|
|
19
|
+
const cached = this._rpcCache.get(node);
|
|
20
|
+
if (cached !== undefined) return cached;
|
|
21
|
+
const result = this._containsRPCImpl(node);
|
|
22
|
+
this._rpcCache.set(node, result);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_containsRPCImpl(node) {
|
|
17
27
|
if (node.type === 'CallExpression' && this._isRPCCall(node)) return true;
|
|
18
28
|
if (node.type === 'BlockStatement') return node.body.some(s => this._containsRPC(s));
|
|
19
29
|
if (node.type === 'ExpressionStatement') return this._containsRPC(node.expression);
|
|
@@ -179,7 +189,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
179
189
|
const lines = [];
|
|
180
190
|
|
|
181
191
|
// Runtime imports
|
|
182
|
-
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_transition, tova_inject_css, batch, onMount, onUnmount, onCleanup, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, ErrorInfo, createRoot, watch, untrack, Dynamic, Portal, lazy } from './runtime/reactivity.js';`);
|
|
192
|
+
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_transition, tova_inject_css, batch, onMount, onUnmount, onCleanup, onBeforeUpdate, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, ErrorInfo, createRoot, watch, untrack, Dynamic, Portal, lazy, Suspense, __tova_action } from './runtime/reactivity.js';`);
|
|
183
193
|
lines.push(`import { rpc } from './runtime/rpc.js';`);
|
|
184
194
|
|
|
185
195
|
// Hoist import lines from shared code to the top of the module
|
|
@@ -395,25 +405,118 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
395
405
|
}
|
|
396
406
|
|
|
397
407
|
// Scope CSS selectors by appending [data-tova-HASH] to each selector
|
|
408
|
+
// Uses a lightweight tokenizer to properly handle:
|
|
409
|
+
// - @media, @keyframes, @layer blocks (don't scope their content selectors)
|
|
410
|
+
// - :is(), :where(), :has() pseudo-functions
|
|
411
|
+
// - :global() escape hatch (strip wrapper, don't scope)
|
|
412
|
+
// - CSS comments /* */
|
|
413
|
+
// - Nested CSS
|
|
414
|
+
// - Multiple rules in sequence
|
|
398
415
|
_scopeCSS(css, scopeAttr) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
416
|
+
const result = [];
|
|
417
|
+
let i = 0;
|
|
418
|
+
let depth = 0;
|
|
419
|
+
let buf = '';
|
|
420
|
+
const noScopeDepths = new Set(); // Depths where we DON'T scope (property decls, @keyframes, @font-face)
|
|
421
|
+
|
|
422
|
+
while (i < css.length) {
|
|
423
|
+
// Skip CSS comments
|
|
424
|
+
if (css[i] === '/' && css[i + 1] === '*') {
|
|
425
|
+
const end = css.indexOf('*/', i + 2);
|
|
426
|
+
if (end === -1) { buf += css.slice(i); break; }
|
|
427
|
+
buf += css.slice(i, end + 2);
|
|
428
|
+
i = end + 2;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Skip quoted strings
|
|
433
|
+
if (css[i] === '"' || css[i] === "'") {
|
|
434
|
+
const q = css[i];
|
|
435
|
+
buf += css[i++];
|
|
436
|
+
while (i < css.length && css[i] !== q) {
|
|
437
|
+
if (css[i] === '\\') buf += css[i++];
|
|
438
|
+
buf += css[i++];
|
|
407
439
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
440
|
+
if (i < css.length) buf += css[i++];
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Opening brace — process accumulated buf as selector or pass through
|
|
445
|
+
if (css[i] === '{') {
|
|
446
|
+
const trimmed = buf.trim();
|
|
447
|
+
|
|
448
|
+
if (noScopeDepths.has(depth)) {
|
|
449
|
+
// Inside a no-scope context (property declarations, @keyframes) — pass through
|
|
450
|
+
result.push(buf + '{');
|
|
451
|
+
} else if (trimmed.startsWith('@')) {
|
|
452
|
+
// @keyframes, @font-face: mark inner as no-scope
|
|
453
|
+
if (/^@keyframes\s/.test(trimmed) || /^@font-face/.test(trimmed)) {
|
|
454
|
+
noScopeDepths.add(depth + 1);
|
|
455
|
+
}
|
|
456
|
+
// @media, @supports, @layer: keep scoping inside (don't mark)
|
|
457
|
+
result.push(buf + '{');
|
|
458
|
+
} else {
|
|
459
|
+
// Regular selector — scope it and mark inner depth as no-scope (property declarations)
|
|
460
|
+
const scopedSelectors = buf.split(',').map(s => {
|
|
461
|
+
s = s.trim();
|
|
462
|
+
if (!s || s === 'from' || s === 'to' || /^\d+%$/.test(s)) return s;
|
|
463
|
+
return this._scopeSelector(s, scopeAttr);
|
|
464
|
+
}).join(', ');
|
|
465
|
+
result.push(scopedSelectors + '{');
|
|
466
|
+
noScopeDepths.add(depth + 1);
|
|
412
467
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
468
|
+
|
|
469
|
+
depth++;
|
|
470
|
+
buf = '';
|
|
471
|
+
i++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Closing brace
|
|
476
|
+
if (css[i] === '}') {
|
|
477
|
+
result.push(buf + '}');
|
|
478
|
+
buf = '';
|
|
479
|
+
noScopeDepths.delete(depth);
|
|
480
|
+
depth--;
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Accumulate character
|
|
486
|
+
buf += css[i];
|
|
487
|
+
i++;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (buf) result.push(buf);
|
|
491
|
+
return result.join('');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Scope a single CSS selector
|
|
495
|
+
_scopeSelector(selector, scopeAttr) {
|
|
496
|
+
let s = selector.trim();
|
|
497
|
+
|
|
498
|
+
// :global() escape hatch — strip wrapper, don't scope
|
|
499
|
+
if (s.startsWith(':global(') && s.endsWith(')')) {
|
|
500
|
+
return s.slice(8, -1);
|
|
501
|
+
}
|
|
502
|
+
// Inline :global() in the middle of a selector
|
|
503
|
+
s = s.replace(/:global\(([^)]+)\)/g, '$1');
|
|
504
|
+
|
|
505
|
+
// Handle pseudo-elements (::before, ::after, ::placeholder, etc.)
|
|
506
|
+
const pseudoElMatch = s.match(/(::[\w-]+(?:\([^)]*\))?)$/);
|
|
507
|
+
if (pseudoElMatch) {
|
|
508
|
+
return s.slice(0, -pseudoElMatch[0].length) + scopeAttr + pseudoElMatch[0];
|
|
509
|
+
}
|
|
510
|
+
// Handle pseudo-classes with functions (:is(), :where(), :has(), :not(), :hover, etc.)
|
|
511
|
+
const pseudoClsMatch = s.match(/((?::[\w-]+(?:\([^)]*\))?)+)$/);
|
|
512
|
+
if (pseudoClsMatch) {
|
|
513
|
+
const pseudoPart = pseudoClsMatch[0];
|
|
514
|
+
const basePart = s.slice(0, -pseudoPart.length);
|
|
515
|
+
if (basePart.trim()) {
|
|
516
|
+
return basePart + scopeAttr + pseudoPart;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return s + scopeAttr;
|
|
417
520
|
}
|
|
418
521
|
|
|
419
522
|
generateComponent(comp) {
|
|
@@ -580,10 +683,19 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
580
683
|
return p.join('');
|
|
581
684
|
}
|
|
582
685
|
|
|
583
|
-
// Check if an AST expression references any signal/computed name
|
|
686
|
+
// Check if an AST expression references any signal/computed name (memoized)
|
|
584
687
|
_exprReadsSignal(node) {
|
|
585
688
|
if (!node) return false;
|
|
689
|
+
// Cannot cache Identifier lookups — result depends on current stateNames/computedNames
|
|
586
690
|
if (node.type === 'Identifier') return this.stateNames.has(node.name) || this.computedNames.has(node.name);
|
|
691
|
+
const cached = this._signalCache.get(node);
|
|
692
|
+
if (cached !== undefined) return cached;
|
|
693
|
+
const result = this._exprReadsSignalImpl(node);
|
|
694
|
+
this._signalCache.set(node, result);
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
_exprReadsSignalImpl(node) {
|
|
587
699
|
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
|
|
588
700
|
return this._exprReadsSignal(node.left) || this._exprReadsSignal(node.right);
|
|
589
701
|
}
|
|
@@ -649,6 +761,30 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
649
761
|
}
|
|
650
762
|
|
|
651
763
|
genJSXElement(node) {
|
|
764
|
+
// <slot /> or <slot name="header" /> — render children passed from parent
|
|
765
|
+
if (node.tag === 'slot') {
|
|
766
|
+
const nameAttr = node.attributes.find(a => a.name === 'name');
|
|
767
|
+
const slotProps = node.attributes.filter(a => a.name !== 'name');
|
|
768
|
+
|
|
769
|
+
if (nameAttr && nameAttr.value.type === 'StringLiteral') {
|
|
770
|
+
// Named slot: <slot name="header" />
|
|
771
|
+
const slotName = nameAttr.value.value;
|
|
772
|
+
return `(__props.${slotName} || '')`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (slotProps.length > 0) {
|
|
776
|
+
// Scoped slot: <slot count={count()} /> — pass props to render function
|
|
777
|
+
const propParts = slotProps.map(a => {
|
|
778
|
+
const val = this.genExpression(a.value);
|
|
779
|
+
return `${a.name}: ${val}`;
|
|
780
|
+
});
|
|
781
|
+
return `(typeof __props.children === 'function' ? __props.children({${propParts.join(', ')}}) : (__props.children || ''))`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Default slot: <slot />
|
|
785
|
+
return `(__props.children || '')`;
|
|
786
|
+
}
|
|
787
|
+
|
|
652
788
|
const isComponent = node.tag[0] === node.tag[0].toUpperCase() && /^[A-Z]/.test(node.tag);
|
|
653
789
|
|
|
654
790
|
// Attributes
|
|
@@ -728,16 +864,82 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
728
864
|
// Conditional class: class:active={cond}
|
|
729
865
|
const className = attr.name.slice(6);
|
|
730
866
|
classDirectives.push({ className, condition: this.genExpression(attr.value), node: attr.value });
|
|
867
|
+
} else if (attr.name.startsWith('use:')) {
|
|
868
|
+
// use:action directive: use:tooltip={params}
|
|
869
|
+
const actionName = attr.name.slice(4);
|
|
870
|
+
const param = attr.value.type === 'BooleanLiteral' ? 'undefined' : this.genExpression(attr.value);
|
|
871
|
+
const reactive = attr.value.type !== 'BooleanLiteral' && this._exprReadsSignal(attr.value);
|
|
872
|
+
if (!node._actions) node._actions = [];
|
|
873
|
+
node._actions.push({ name: actionName, param, reactive });
|
|
874
|
+
} else if (attr.name.startsWith('in:')) {
|
|
875
|
+
// in:fade — enter-only transition
|
|
876
|
+
const transName = attr.name.slice(3);
|
|
877
|
+
const config = attr.value.type === 'BooleanLiteral' ? '{}' : this.genExpression(attr.value);
|
|
878
|
+
node._inTransition = { name: transName, config };
|
|
879
|
+
} else if (attr.name.startsWith('out:')) {
|
|
880
|
+
// out:slide — leave-only transition
|
|
881
|
+
const transName = attr.name.slice(4);
|
|
882
|
+
const config = attr.value.type === 'BooleanLiteral' ? '{}' : this.genExpression(attr.value);
|
|
883
|
+
node._outTransition = { name: transName, config };
|
|
731
884
|
} else if (attr.name.startsWith('transition:')) {
|
|
732
885
|
// transition:fade, transition:slide={duration: 300}, etc.
|
|
733
886
|
const transName = attr.name.slice(11); // 'fade', 'slide', 'scale', 'fly'
|
|
887
|
+
const builtins = new Set(['fade', 'slide', 'scale', 'fly']);
|
|
734
888
|
const config = attr.value.type === 'BooleanLiteral' ? '{}' : this.genExpression(attr.value);
|
|
735
889
|
// Store transition info for element wrapping
|
|
736
890
|
if (!node._transitions) node._transitions = [];
|
|
737
|
-
node._transitions.push({ name: transName, config });
|
|
891
|
+
node._transitions.push({ name: transName, config, custom: !builtins.has(transName) });
|
|
892
|
+
} else if (attr.name === 'bind:this') {
|
|
893
|
+
// bind:this={ref} → ref: refValue (works with both ref objects and functions)
|
|
894
|
+
attrs.ref = this.genExpression(attr.value);
|
|
738
895
|
} else if (attr.name.startsWith('on:')) {
|
|
739
|
-
const
|
|
740
|
-
|
|
896
|
+
const fullName = attr.name.slice(3); // e.g. "click.stop.prevent"
|
|
897
|
+
const parts = fullName.split('.');
|
|
898
|
+
const eventName = parts[0];
|
|
899
|
+
const modifiers = parts.slice(1);
|
|
900
|
+
let handler = this.genExpression(attr.value);
|
|
901
|
+
|
|
902
|
+
if (modifiers.length > 0) {
|
|
903
|
+
const guards = [];
|
|
904
|
+
let useCapture = false;
|
|
905
|
+
let useOnce = false;
|
|
906
|
+
|
|
907
|
+
// Key modifier map for keydown/keyup events
|
|
908
|
+
const keyMap = {
|
|
909
|
+
enter: '"Enter"', escape: '"Escape"', tab: '"Tab"', space: '" "',
|
|
910
|
+
up: '"ArrowUp"', down: '"ArrowDown"', left: '"ArrowLeft"', right: '"ArrowRight"',
|
|
911
|
+
delete: '"Delete"', backspace: '"Backspace"',
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
for (const mod of modifiers) {
|
|
915
|
+
if (mod === 'prevent') {
|
|
916
|
+
guards.push('e.preventDefault()');
|
|
917
|
+
} else if (mod === 'stop') {
|
|
918
|
+
guards.push('e.stopPropagation()');
|
|
919
|
+
} else if (mod === 'self') {
|
|
920
|
+
guards.push('if (e.target !== e.currentTarget) return');
|
|
921
|
+
} else if (mod === 'capture') {
|
|
922
|
+
useCapture = true;
|
|
923
|
+
} else if (mod === 'once') {
|
|
924
|
+
useOnce = true;
|
|
925
|
+
} else if (keyMap[mod]) {
|
|
926
|
+
guards.push(`if (e.key !== ${keyMap[mod]}) return`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (guards.length > 0) {
|
|
931
|
+
handler = `(e) => { ${guards.join('; ')}; (${handler})(e); }`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (useCapture || useOnce) {
|
|
935
|
+
const opts = [];
|
|
936
|
+
if (useCapture) opts.push('capture: true');
|
|
937
|
+
if (useOnce) opts.push('once: true');
|
|
938
|
+
handler = `{ handler: ${handler}, options: { ${opts.join(', ')} } }`;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
events[eventName] = handler;
|
|
741
943
|
} else {
|
|
742
944
|
const attrName = attr.name === 'class' ? 'className' : attr.name;
|
|
743
945
|
const expr = this.genExpression(attr.value);
|
|
@@ -784,12 +986,22 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
784
986
|
}
|
|
785
987
|
|
|
786
988
|
const propParts = [];
|
|
989
|
+
const memoizedProps = []; // Computed memoization for complex expressions
|
|
787
990
|
for (const [key, val] of Object.entries(attrs)) {
|
|
788
991
|
// For component props, convert reactive () => wrappers to JS getter syntax
|
|
789
992
|
// so the prop stays reactive through the __props access pattern
|
|
790
993
|
if (isComponent && spreads.length === 0 && typeof val === 'string' && val.startsWith('() => ')) {
|
|
791
994
|
const rawExpr = val.slice(6);
|
|
792
|
-
|
|
995
|
+
// Simple signal read: just use a getter (no overhead)
|
|
996
|
+
// Complex expressions: memoize with createComputed
|
|
997
|
+
const isSimple = /^[a-zA-Z_$]\w*\(\)$/.test(rawExpr);
|
|
998
|
+
if (isSimple) {
|
|
999
|
+
propParts.push(`get ${key}() { return ${rawExpr}; }`);
|
|
1000
|
+
} else {
|
|
1001
|
+
const memoName = `__memo_${key}`;
|
|
1002
|
+
memoizedProps.push(`const ${memoName} = createComputed(() => ${rawExpr})`);
|
|
1003
|
+
propParts.push(`get ${key}() { return ${memoName}(); }`);
|
|
1004
|
+
}
|
|
793
1005
|
} else {
|
|
794
1006
|
propParts.push(`${key}: ${val}`);
|
|
795
1007
|
}
|
|
@@ -844,7 +1056,10 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
844
1056
|
propsStr = `{${propParts.join(', ')}}`;
|
|
845
1057
|
}
|
|
846
1058
|
}
|
|
847
|
-
|
|
1059
|
+
if (memoizedProps.length > 0) {
|
|
1060
|
+
return `(() => { ${memoizedProps.join('; ')}; const __v = ${node.tag}(${propsStr}); if (__v && __v.__tova) __v._componentName = "${node.tag}"; return __v; })()`;
|
|
1061
|
+
}
|
|
1062
|
+
return `((__tova_v) => (__tova_v && __tova_v.__tova && (__tova_v._componentName = "${node.tag}"), __tova_v))(${node.tag}(${propsStr}))`;
|
|
848
1063
|
}
|
|
849
1064
|
|
|
850
1065
|
const tag = JSON.stringify(node.tag);
|
|
@@ -860,7 +1075,30 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
860
1075
|
// Wrap with transition directives if present
|
|
861
1076
|
if (node._transitions && node._transitions.length > 0) {
|
|
862
1077
|
for (const t of node._transitions) {
|
|
863
|
-
|
|
1078
|
+
if (t.custom) {
|
|
1079
|
+
result = `tova_transition(${result}, ${t.name}, ${t.config})`;
|
|
1080
|
+
} else {
|
|
1081
|
+
result = `tova_transition(${result}, "${t.name}", ${t.config})`;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Wrap with directional transitions if present
|
|
1087
|
+
if (node._inTransition || node._outTransition) {
|
|
1088
|
+
const inPart = node._inTransition ? `in: { name: "${node._inTransition.name}", config: ${node._inTransition.config} }` : '';
|
|
1089
|
+
const outPart = node._outTransition ? `out: { name: "${node._outTransition.name}", config: ${node._outTransition.config} }` : '';
|
|
1090
|
+
const parts = [inPart, outPart].filter(Boolean).join(', ');
|
|
1091
|
+
result = `tova_transition(${result}, { ${parts} })`;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Wrap with use: action directives if present
|
|
1095
|
+
if (node._actions && node._actions.length > 0) {
|
|
1096
|
+
for (const a of node._actions) {
|
|
1097
|
+
if (a.reactive) {
|
|
1098
|
+
result = `__tova_action(${result}, ${a.name}, () => ${a.param})`;
|
|
1099
|
+
} else {
|
|
1100
|
+
result = `__tova_action(${result}, ${a.name}, ${a.param})`;
|
|
1101
|
+
}
|
|
864
1102
|
}
|
|
865
1103
|
}
|
|
866
1104
|
|
|
@@ -882,25 +1120,36 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
882
1120
|
return this.genExpression(node.value);
|
|
883
1121
|
}
|
|
884
1122
|
|
|
1123
|
+
_genJSXForVar(variable) {
|
|
1124
|
+
if (typeof variable === 'string') return variable;
|
|
1125
|
+
if (variable.type === 'ArrayPattern') {
|
|
1126
|
+
return `[${variable.elements.join(', ')}]`;
|
|
1127
|
+
}
|
|
1128
|
+
if (variable.type === 'ObjectPattern') {
|
|
1129
|
+
return `{${variable.properties.map(p => p.value ? `${p.key}: ${p.value}` : p.key).join(', ')}}`;
|
|
1130
|
+
}
|
|
1131
|
+
return String(variable);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
885
1134
|
genJSXFor(node) {
|
|
886
|
-
const varName = node.variable;
|
|
1135
|
+
const varName = this._genJSXForVar(node.variable);
|
|
887
1136
|
const iterable = this.genExpression(node.iterable);
|
|
888
1137
|
const children = node.body.map(c => this.genJSX(c));
|
|
1138
|
+
const needsReactive = this._exprReadsSignal(node.iterable);
|
|
1139
|
+
const wrap = needsReactive ? '() => ' : '';
|
|
889
1140
|
|
|
890
|
-
// Wrap in reactive closure so the runtime creates a dynamic block that
|
|
891
|
-
// re-evaluates when the iterable signal changes
|
|
892
1141
|
if (node.keyExpr) {
|
|
893
1142
|
const keyExpr = this.genExpression(node.keyExpr);
|
|
894
1143
|
if (children.length === 1) {
|
|
895
|
-
return
|
|
1144
|
+
return `${wrap}${iterable}.map((${varName}) => tova_keyed(${keyExpr}, ${children[0]}))`;
|
|
896
1145
|
}
|
|
897
|
-
return
|
|
1146
|
+
return `${wrap}${iterable}.map((${varName}) => tova_keyed(${keyExpr}, tova_fragment([${children.join(', ')}])))`;
|
|
898
1147
|
}
|
|
899
1148
|
|
|
900
1149
|
if (children.length === 1) {
|
|
901
|
-
return
|
|
1150
|
+
return `${wrap}${iterable}.map((${varName}) => ${children[0]})`;
|
|
902
1151
|
}
|
|
903
|
-
return
|
|
1152
|
+
return `${wrap}${iterable}.map((${varName}) => tova_fragment([${children.join(', ')}]))`;
|
|
904
1153
|
}
|
|
905
1154
|
|
|
906
1155
|
genJSXIf(node) {
|
|
@@ -929,8 +1178,13 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
929
1178
|
result += ` : null`;
|
|
930
1179
|
}
|
|
931
1180
|
|
|
932
|
-
//
|
|
933
|
-
|
|
1181
|
+
// Only wrap in reactive closure if the condition reads signals
|
|
1182
|
+
const needsReactive = this._exprReadsSignal(node.condition) ||
|
|
1183
|
+
(node.alternates && node.alternates.some(a => this._exprReadsSignal(a.condition)));
|
|
1184
|
+
if (needsReactive) {
|
|
1185
|
+
return `() => ${result}`;
|
|
1186
|
+
}
|
|
1187
|
+
return result;
|
|
934
1188
|
}
|
|
935
1189
|
|
|
936
1190
|
genJSXMatch(node) {
|
|
@@ -962,8 +1216,11 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
962
1216
|
}
|
|
963
1217
|
|
|
964
1218
|
p.push(`})(${subject})`);
|
|
965
|
-
//
|
|
966
|
-
|
|
1219
|
+
// Only wrap in reactive closure if the subject reads signals
|
|
1220
|
+
if (this._exprReadsSignal(node.subject)) {
|
|
1221
|
+
return `() => ${p.join('')}`;
|
|
1222
|
+
}
|
|
1223
|
+
return p.join('');
|
|
967
1224
|
}
|
|
968
1225
|
|
|
969
1226
|
genJSXFragment(node) {
|
package/src/codegen/codegen.js
CHANGED
|
@@ -4,31 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { SharedCodegen } from './shared-codegen.js';
|
|
6
6
|
import { BUILTIN_NAMES } from '../stdlib/inline.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let _ServerCodegen = null;
|
|
10
|
-
let _ClientCodegen = null;
|
|
7
|
+
import { ServerCodegen } from './server-codegen.js';
|
|
8
|
+
import { ClientCodegen } from './client-codegen.js';
|
|
11
9
|
|
|
12
10
|
function getServerCodegen() {
|
|
13
|
-
|
|
14
|
-
// Dynamic require avoids loading server-codegen.js for client-only builds
|
|
15
|
-
_ServerCodegen = import.meta.require('./server-codegen.js').ServerCodegen;
|
|
16
|
-
}
|
|
17
|
-
return _ServerCodegen;
|
|
11
|
+
return ServerCodegen;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
14
|
function getClientCodegen() {
|
|
21
|
-
|
|
22
|
-
// Dynamic require avoids loading client-codegen.js for server-only builds
|
|
23
|
-
_ClientCodegen = import.meta.require('./client-codegen.js').ClientCodegen;
|
|
24
|
-
}
|
|
25
|
-
return _ClientCodegen;
|
|
15
|
+
return ClientCodegen;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
export class CodeGenerator {
|
|
29
|
-
constructor(ast, filename = '<stdin>') {
|
|
19
|
+
constructor(ast, filename = '<stdin>', options = {}) {
|
|
30
20
|
this.ast = ast;
|
|
31
21
|
this.filename = filename;
|
|
22
|
+
this._sourceMaps = options.sourceMaps !== false; // default true; pass false for REPL/check
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
// Group blocks by name (null name = "default")
|
|
@@ -72,6 +63,7 @@ export class CodeGenerator {
|
|
|
72
63
|
|
|
73
64
|
if (isModule) {
|
|
74
65
|
const moduleGen = new SharedCodegen();
|
|
66
|
+
moduleGen._sourceMapsEnabled = this._sourceMaps;
|
|
75
67
|
moduleGen.setSourceFile(this.filename);
|
|
76
68
|
const moduleCode = topLevel.map(s => moduleGen.generateStatement(s)).join('\n');
|
|
77
69
|
const helpers = moduleGen.generateHelpers();
|
|
@@ -87,6 +79,7 @@ export class CodeGenerator {
|
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
const sharedGen = new SharedCodegen();
|
|
82
|
+
sharedGen._sourceMapsEnabled = this._sourceMaps;
|
|
90
83
|
sharedGen.setSourceFile(this.filename);
|
|
91
84
|
|
|
92
85
|
// All shared blocks (regardless of name) are merged into one shared output
|
|
@@ -134,6 +127,7 @@ export class CodeGenerator {
|
|
|
134
127
|
const servers = {};
|
|
135
128
|
for (const [name, blocks] of serverGroups) {
|
|
136
129
|
const gen = new (getServerCodegen())();
|
|
130
|
+
gen._sourceMapsEnabled = this._sourceMaps;
|
|
137
131
|
const key = name || 'default';
|
|
138
132
|
// Build peer blocks map (all named blocks except self)
|
|
139
133
|
let peerBlocks = null;
|
|
@@ -152,6 +146,7 @@ export class CodeGenerator {
|
|
|
152
146
|
const clients = {};
|
|
153
147
|
for (const [name, blocks] of clientGroups) {
|
|
154
148
|
const gen = new (getClientCodegen())();
|
|
149
|
+
gen._sourceMapsEnabled = this._sourceMaps;
|
|
155
150
|
const key = name || 'default';
|
|
156
151
|
clients[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins);
|
|
157
152
|
}
|