tova 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -179,7 +179,7 @@ export class ClientCodegen extends BaseCodegen {
179
179
  const lines = [];
180
180
 
181
181
  // Runtime imports
182
- lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_inject_css, batch, onMount, onUnmount, onCleanup, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, createRoot, watch, untrack, Dynamic, Portal, lazy } from './runtime/reactivity.js';`);
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';`);
183
183
  lines.push(`import { rpc } from './runtime/rpc.js';`);
184
184
 
185
185
  // Hoist import lines from shared code to the top of the module
@@ -362,26 +362,26 @@ export class ClientCodegen extends BaseCodegen {
362
362
 
363
363
  _generateEffect(body) {
364
364
  const hasRPC = this._containsRPC(body);
365
- let code;
365
+ const p = [];
366
366
  if (hasRPC) {
367
- code = `createEffect(() => {\n`;
368
- code += `${this.i()} (async () => {\n`;
367
+ p.push(`createEffect(() => {\n`);
368
+ p.push(`${this.i()} (async () => {\n`);
369
369
  this.indent += 2;
370
370
  const prevAsync = this._asyncContext;
371
371
  this._asyncContext = true;
372
- code += this.genBlockStatements(body);
372
+ p.push(this.genBlockStatements(body));
373
373
  this._asyncContext = prevAsync;
374
374
  this.indent -= 2;
375
- code += `\n${this.i()} })();\n`;
376
- code += `${this.i()}});`;
375
+ p.push(`\n${this.i()} })();\n`);
376
+ p.push(`${this.i()}});`);
377
377
  } else {
378
- code = `createEffect(() => {\n`;
378
+ p.push(`createEffect(() => {\n`);
379
379
  this.indent++;
380
- code += this.genBlockStatements(body);
380
+ p.push(this.genBlockStatements(body));
381
381
  this.indent--;
382
- code += `\n${this.i()}});`;
382
+ p.push(`\n${this.i()}});`);
383
383
  }
384
- return code;
384
+ return p.join('');
385
385
  }
386
386
 
387
387
  // Generate a short hash from component name + CSS content (for CSS scoping)
@@ -424,20 +424,21 @@ export class ClientCodegen extends BaseCodegen {
424
424
  const savedState = new Set(this.stateNames);
425
425
  const savedComputed = new Set(this.computedNames);
426
426
 
427
- let code = `function ${comp.name}(${paramStr}) {\n`;
427
+ const p = [];
428
+ p.push(`function ${comp.name}(${paramStr}) {\n`);
428
429
  this.indent++;
429
430
 
430
431
  // Generate reactive prop accessors — each prop is accessed through __props getter
431
432
  // This ensures parent signal changes propagate reactively to the child
432
433
  if (hasParams) {
433
- for (const p of comp.params) {
434
- this.computedNames.add(p.name);
435
- const def = p.default || p.defaultValue;
434
+ for (const param of comp.params) {
435
+ this.computedNames.add(param.name);
436
+ const def = param.default || param.defaultValue;
436
437
  if (def) {
437
438
  const defaultExpr = this.genExpression(def);
438
- code += `${this.i()}const ${p.name} = () => __props.${p.name} !== undefined ? __props.${p.name} : ${defaultExpr};\n`;
439
+ p.push(`${this.i()}const ${param.name} = () => __props.${param.name} !== undefined ? __props.${param.name} : ${defaultExpr};\n`);
439
440
  } else {
440
- code += `${this.i()}const ${p.name} = () => __props.${p.name};\n`;
441
+ p.push(`${this.i()}const ${param.name} = () => __props.${param.name};\n`);
441
442
  }
442
443
  }
443
444
  }
@@ -448,7 +449,7 @@ export class ClientCodegen extends BaseCodegen {
448
449
  const bodyItems = [];
449
450
 
450
451
  for (const node of comp.body) {
451
- if (node.type === 'JSXElement' || node.type === 'JSXFor' || node.type === 'JSXIf') {
452
+ if (node.type === 'JSXElement' || node.type === 'JSXFragment' || node.type === 'JSXFor' || node.type === 'JSXIf') {
452
453
  jsxElements.push(node);
453
454
  } else if (node.type === 'ComponentStyleBlock') {
454
455
  styleBlocks.push(node);
@@ -464,7 +465,7 @@ export class ClientCodegen extends BaseCodegen {
464
465
  const scopeId = this._genScopeId(comp.name, rawCSS);
465
466
  this._currentScopeId = scopeId;
466
467
  const scopedCSS = this._scopeCSS(rawCSS, `[data-tova-${scopeId}]`);
467
- code += `${this.i()}tova_inject_css(${JSON.stringify(scopeId)}, ${JSON.stringify(scopedCSS)});\n`;
468
+ p.push(`${this.i()}tova_inject_css(${JSON.stringify(scopeId)}, ${JSON.stringify(scopedCSS)});\n`);
468
469
  }
469
470
 
470
471
  // Generate body items in order (state, computed, effect, other statements)
@@ -472,38 +473,38 @@ export class ClientCodegen extends BaseCodegen {
472
473
  if (node.type === 'StateDeclaration') {
473
474
  this.stateNames.add(node.name);
474
475
  const init = this.genExpression(node.initialValue);
475
- code += `${this.i()}const [${node.name}, set${capitalize(node.name)}] = createSignal(${init});\n`;
476
+ p.push(`${this.i()}const [${node.name}, set${capitalize(node.name)}] = createSignal(${init});\n`);
476
477
  } else if (node.type === 'ComputedDeclaration') {
477
478
  this.computedNames.add(node.name);
478
479
  const expr = this.genExpression(node.expression);
479
- code += `${this.i()}const ${node.name} = createComputed(() => ${expr});\n`;
480
+ p.push(`${this.i()}const ${node.name} = createComputed(() => ${expr});\n`);
480
481
  } else if (node.type === 'EffectDeclaration') {
481
482
  this.indent++;
482
483
  const effectCode = this._generateEffect(node.body);
483
484
  this.indent--;
484
- code += `${this.i()}${effectCode}\n`;
485
+ p.push(`${this.i()}${effectCode}\n`);
485
486
  } else {
486
- code += this.generateStatement(node) + '\n';
487
+ p.push(this.generateStatement(node) + '\n');
487
488
  }
488
489
  }
489
490
 
490
491
  // Generate JSX return
491
492
  if (jsxElements.length === 1) {
492
- code += `${this.i()}return ${this.genJSX(jsxElements[0])};\n`;
493
+ p.push(`${this.i()}return ${this.genJSX(jsxElements[0])};\n`);
493
494
  } else if (jsxElements.length > 1) {
494
495
  const children = jsxElements.map(el => this.genJSX(el)).join(', ');
495
- code += `${this.i()}return tova_fragment([${children}]);\n`;
496
+ p.push(`${this.i()}return tova_fragment([${children}]);\n`);
496
497
  }
497
498
 
498
499
  this.indent--;
499
- code += `}`;
500
+ p.push(`}`);
500
501
 
501
502
  // Restore scoped names and scope id
502
503
  this.stateNames = savedState;
503
504
  this.computedNames = savedComputed;
504
505
  this._currentScopeId = savedScopeId;
505
506
 
506
- return code;
507
+ return p.join('');
507
508
  }
508
509
 
509
510
  generateStore(store) {
@@ -528,54 +529,55 @@ export class ClientCodegen extends BaseCodegen {
528
529
  }
529
530
  }
530
531
 
531
- let code = `const ${store.name} = (() => {\n`;
532
+ const p = [];
533
+ p.push(`const ${store.name} = (() => {\n`);
532
534
  this.indent++;
533
535
 
534
536
  // Generate state signals
535
537
  for (const s of storeStates) {
536
538
  const init = this.genExpression(s.initialValue);
537
- code += `${this.i()}const [${s.name}, set${capitalize(s.name)}] = createSignal(${init});\n`;
539
+ p.push(`${this.i()}const [${s.name}, set${capitalize(s.name)}] = createSignal(${init});\n`);
538
540
  }
539
541
 
540
542
  // Generate computed values
541
543
  for (const c of storeComputeds) {
542
544
  const expr = this.genExpression(c.expression);
543
- code += `${this.i()}const ${c.name} = createComputed(() => ${expr});\n`;
545
+ p.push(`${this.i()}const ${c.name} = createComputed(() => ${expr});\n`);
544
546
  }
545
547
 
546
548
  // Generate functions
547
549
  for (const fn of storeFunctions) {
548
- code += this.genFunctionDeclaration(fn) + '\n';
550
+ p.push(this.genFunctionDeclaration(fn) + '\n');
549
551
  }
550
552
 
551
553
  // Build return object with getters/setters
552
- code += `${this.i()}return {\n`;
554
+ p.push(`${this.i()}return {\n`);
553
555
  this.indent++;
554
556
 
555
557
  for (const s of storeStates) {
556
- code += `${this.i()}get ${s.name}() { return ${s.name}(); },\n`;
557
- code += `${this.i()}set ${s.name}(v) { set${capitalize(s.name)}(v); },\n`;
558
+ p.push(`${this.i()}get ${s.name}() { return ${s.name}(); },\n`);
559
+ p.push(`${this.i()}set ${s.name}(v) { set${capitalize(s.name)}(v); },\n`);
558
560
  }
559
561
 
560
562
  for (const c of storeComputeds) {
561
- code += `${this.i()}get ${c.name}() { return ${c.name}(); },\n`;
563
+ p.push(`${this.i()}get ${c.name}() { return ${c.name}(); },\n`);
562
564
  }
563
565
 
564
566
  for (const fn of storeFunctions) {
565
- code += `${this.i()}${fn.name},\n`;
567
+ p.push(`${this.i()}${fn.name},\n`);
566
568
  }
567
569
 
568
570
  this.indent--;
569
- code += `${this.i()}};\n`;
571
+ p.push(`${this.i()}};\n`);
570
572
 
571
573
  this.indent--;
572
- code += `${this.i()}})();`;
574
+ p.push(`${this.i()}})();`);
573
575
 
574
576
  // Restore state/computed names
575
577
  this.stateNames = savedState;
576
578
  this.computedNames = savedComputed;
577
579
 
578
- return code;
580
+ return p.join('');
579
581
  }
580
582
 
581
583
  // Check if an AST expression references any signal/computed name
@@ -626,6 +628,7 @@ export class ClientCodegen extends BaseCodegen {
626
628
 
627
629
  switch (node.type) {
628
630
  case 'JSXElement': return this.genJSXElement(node);
631
+ case 'JSXFragment': return this.genJSXFragment(node);
629
632
  case 'JSXText': return this.genJSXText(node);
630
633
  case 'JSXExpression': {
631
634
  // If expression reads a signal, wrap as () => expr for fine-grained reactivity
@@ -705,10 +708,24 @@ export class ClientCodegen extends BaseCodegen {
705
708
  events.change = `(e) => { set${capitalize(exprName)}(${valueExpr}); }`;
706
709
  }
707
710
  }
711
+ } else if (attr.name === 'show') {
712
+ // show={condition} → toggles display:none instead of removing from DOM
713
+ const expr = this.genExpression(attr.value);
714
+ const reactive = this._exprReadsSignal(attr.value);
715
+ const displayExpr = `(${expr}) ? "" : "none"`;
716
+ // Store show directive to merge with style later
717
+ node._showDirective = { expr: displayExpr, reactive };
708
718
  } else if (attr.name.startsWith('class:')) {
709
719
  // Conditional class: class:active={cond}
710
720
  const className = attr.name.slice(6);
711
721
  classDirectives.push({ className, condition: this.genExpression(attr.value), node: attr.value });
722
+ } else if (attr.name.startsWith('transition:')) {
723
+ // transition:fade, transition:slide={duration: 300}, etc.
724
+ const transName = attr.name.slice(11); // 'fade', 'slide', 'scale', 'fly'
725
+ const config = attr.value.type === 'BooleanLiteral' ? '{}' : this.genExpression(attr.value);
726
+ // Store transition info for element wrapping
727
+ if (!node._transitions) node._transitions = [];
728
+ node._transitions.push({ name: transName, config });
712
729
  } else if (attr.name.startsWith('on:')) {
713
730
  const eventName = attr.name.slice(3);
714
731
  events[eventName] = this.genExpression(attr.value);
@@ -734,6 +751,24 @@ export class ClientCodegen extends BaseCodegen {
734
751
  attrs.className = isReactive ? `() => ${classExpr}` : classExpr;
735
752
  }
736
753
 
754
+ // Merge show directive with style (show toggles display:none)
755
+ if (node._showDirective) {
756
+ const { expr: displayExpr, reactive } = node._showDirective;
757
+ if (attrs.style) {
758
+ // Merge with existing style object
759
+ const existing = attrs.style;
760
+ if (reactive) {
761
+ attrs.style = `() => Object.assign({}, ${existing}, { display: ${displayExpr} })`;
762
+ } else {
763
+ attrs.style = `Object.assign({}, ${existing}, { display: ${displayExpr} })`;
764
+ }
765
+ } else {
766
+ attrs.style = reactive
767
+ ? `() => ({ display: ${displayExpr} })`
768
+ : `{ display: ${displayExpr} }`;
769
+ }
770
+ }
771
+
737
772
  // Add scoped CSS attribute to HTML elements (not components)
738
773
  if (this._currentScopeId && !isComponent) {
739
774
  attrs[`"data-tova-${this._currentScopeId}"`] = '""';
@@ -805,12 +840,22 @@ export class ClientCodegen extends BaseCodegen {
805
840
 
806
841
  const tag = JSON.stringify(node.tag);
807
842
 
843
+ let result;
808
844
  if (node.selfClosing || node.children.length === 0) {
809
- return `tova_el(${tag}, ${propsStr})`;
845
+ result = `tova_el(${tag}, ${propsStr})`;
846
+ } else {
847
+ const children = node.children.map(c => this.genJSX(c)).join(', ');
848
+ result = `tova_el(${tag}, ${propsStr}, [${children}])`;
810
849
  }
811
850
 
812
- const children = node.children.map(c => this.genJSX(c)).join(', ');
813
- return `tova_el(${tag}, ${propsStr}, [${children}])`;
851
+ // Wrap with transition directives if present
852
+ if (node._transitions && node._transitions.length > 0) {
853
+ for (const t of node._transitions) {
854
+ result = `tova_transition(${result}, "${t.name}", ${t.config})`;
855
+ }
856
+ }
857
+
858
+ return result;
814
859
  }
815
860
 
816
861
  genJSXText(node) {
@@ -879,6 +924,11 @@ export class ClientCodegen extends BaseCodegen {
879
924
  return `() => ${result}`;
880
925
  }
881
926
 
927
+ genJSXFragment(node) {
928
+ const children = node.children.map(c => this.genJSX(c)).join(', ');
929
+ return `tova_fragment([${children}])`;
930
+ }
931
+
882
932
  // Override to add await for piped RPC calls
883
933
  genPipeExpression(node) {
884
934
  const result = super.genPipeExpression(node);
@@ -3,10 +3,28 @@
3
3
  // Blocks with the same name are merged; different names produce separate output files.
4
4
 
5
5
  import { SharedCodegen } from './shared-codegen.js';
6
- import { ServerCodegen } from './server-codegen.js';
7
- import { ClientCodegen } from './client-codegen.js';
8
6
  import { BUILTIN_NAMES } from '../stdlib/inline.js';
9
7
 
8
+ // Lazy-loaded codegen modules — only imported when server/client blocks exist
9
+ let _ServerCodegen = null;
10
+ let _ClientCodegen = null;
11
+
12
+ function getServerCodegen() {
13
+ if (!_ServerCodegen) {
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;
18
+ }
19
+
20
+ function getClientCodegen() {
21
+ if (!_ClientCodegen) {
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;
26
+ }
27
+
10
28
  export class CodeGenerator {
11
29
  constructor(ast, filename = '<stdin>') {
12
30
  this.ast = ast;
@@ -31,6 +49,7 @@ export class CodeGenerator {
31
49
  const topLevel = [];
32
50
 
33
51
  const testBlocks = [];
52
+ const benchBlocks = [];
34
53
  const dataBlocks = [];
35
54
 
36
55
  for (const node of this.ast.body) {
@@ -39,12 +58,36 @@ export class CodeGenerator {
39
58
  case 'ServerBlock': serverBlocks.push(node); break;
40
59
  case 'ClientBlock': clientBlocks.push(node); break;
41
60
  case 'TestBlock': testBlocks.push(node); break;
61
+ case 'BenchBlock': benchBlocks.push(node); break;
42
62
  case 'DataBlock': dataBlocks.push(node); break;
43
63
  default: topLevel.push(node); break;
44
64
  }
45
65
  }
46
66
 
67
+ // Detect module mode: no blocks, only top-level statements
68
+ const isModule = sharedBlocks.length === 0 && serverBlocks.length === 0
69
+ && clientBlocks.length === 0 && testBlocks.length === 0
70
+ && benchBlocks.length === 0 && dataBlocks.length === 0
71
+ && topLevel.length > 0;
72
+
73
+ if (isModule) {
74
+ const moduleGen = new SharedCodegen();
75
+ moduleGen.setSourceFile(this.filename);
76
+ const moduleCode = topLevel.map(s => moduleGen.generateStatement(s)).join('\n');
77
+ const helpers = moduleGen.generateHelpers();
78
+ const combined = [helpers, moduleCode].filter(s => s.trim()).join('\n').trim();
79
+ return {
80
+ shared: combined,
81
+ server: '',
82
+ client: '',
83
+ isModule: true,
84
+ sourceMappings: moduleGen.getSourceMappings(),
85
+ _sourceFile: this.filename,
86
+ };
87
+ }
88
+
47
89
  const sharedGen = new SharedCodegen();
90
+ sharedGen.setSourceFile(this.filename);
48
91
 
49
92
  // All shared blocks (regardless of name) are merged into one shared output
50
93
  const sharedCode = sharedBlocks.map(b => sharedGen.generate(b)).join('\n');
@@ -90,7 +133,7 @@ export class CodeGenerator {
90
133
  // Generate server outputs (one per named group)
91
134
  const servers = {};
92
135
  for (const [name, blocks] of serverGroups) {
93
- const gen = new ServerCodegen();
136
+ const gen = new (getServerCodegen())();
94
137
  const key = name || 'default';
95
138
  // Build peer blocks map (all named blocks except self)
96
139
  let peerBlocks = null;
@@ -108,7 +151,7 @@ export class CodeGenerator {
108
151
  // Generate client outputs (one per named group)
109
152
  const clients = {};
110
153
  for (const [name, blocks] of clientGroups) {
111
- const gen = new ClientCodegen();
154
+ const gen = new (getClientCodegen())();
112
155
  const key = name || 'default';
113
156
  clients[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins);
114
157
  }
@@ -116,7 +159,7 @@ export class CodeGenerator {
116
159
  // Generate tests if test blocks exist
117
160
  let testCode = '';
118
161
  if (testBlocks.length > 0) {
119
- const testGen = new ServerCodegen();
162
+ const testGen = new (getServerCodegen())();
120
163
  testCode = testGen.generateTests(testBlocks);
121
164
 
122
165
  // Add __handleRequest export to server code
@@ -126,6 +169,13 @@ export class CodeGenerator {
126
169
  }
127
170
  }
128
171
 
172
+ // Generate benchmarks if bench blocks exist
173
+ let benchCode = '';
174
+ if (benchBlocks.length > 0) {
175
+ const benchGen = new (getServerCodegen())();
176
+ benchCode = benchGen.generateBench(benchBlocks);
177
+ }
178
+
129
179
  // Backward-compatible: if only unnamed blocks, return flat structure
130
180
  const hasNamedBlocks = [...serverGroups.keys(), ...clientGroups.keys()].some(k => k !== null);
131
181
 
@@ -138,8 +188,10 @@ export class CodeGenerator {
138
188
  server: servers['default'] || '',
139
189
  client: clients['default'] || '',
140
190
  sourceMappings,
191
+ _sourceFile: this.filename,
141
192
  };
142
193
  if (testCode) result.test = testCode;
194
+ if (benchCode) result.bench = benchCode;
143
195
  return result;
144
196
  }
145
197
 
@@ -152,8 +204,10 @@ export class CodeGenerator {
152
204
  clients, // { "admin": code, "dashboard": code, ... }
153
205
  multiBlock: true,
154
206
  sourceMappings,
207
+ _sourceFile: this.filename,
155
208
  };
156
209
  if (testCode) result.test = testCode;
210
+ if (benchCode) result.bench = benchCode;
157
211
  return result;
158
212
  }
159
213
 
@@ -167,6 +221,12 @@ export class CodeGenerator {
167
221
  if (node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && BUILTIN_NAMES.has(node.callee.name)) {
168
222
  targetSet.add(node.callee.name);
169
223
  }
224
+ // Track namespace builtin usage: math.sin() or math.PI
225
+ if (node.type === 'MemberExpression' &&
226
+ node.object.type === 'Identifier' &&
227
+ BUILTIN_NAMES.has(node.object.name)) {
228
+ targetSet.add(node.object.name);
229
+ }
170
230
  for (const key of Object.keys(node)) {
171
231
  if (key === 'loc' || key === 'type') continue;
172
232
  const val = node[key];