tova 0.2.9 → 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.
- package/bin/tova.js +1404 -114
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +882 -695
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +473 -111
- package/src/codegen/client-codegen.js +109 -46
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +297 -38
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +306 -64
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +935 -53
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +492 -1056
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +239 -42
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +556 -13
- package/src/version.js +1 -1
|
@@ -27,6 +27,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
27
27
|
}
|
|
28
28
|
if (node.type === 'IfExpression') {
|
|
29
29
|
return this._containsRPC(node.condition) || this._containsRPC(node.consequent) ||
|
|
30
|
+
(node.alternates && node.alternates.some(a => this._containsRPC(a.condition) || this._containsRPC(a.body))) ||
|
|
30
31
|
this._containsRPC(node.elseBody);
|
|
31
32
|
}
|
|
32
33
|
if (node.type === 'ForStatement') return this._containsRPC(node.iterable) || this._containsRPC(node.body);
|
|
@@ -45,14 +46,15 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
45
46
|
return this._containsRPC(node.subject) || node.arms.some(a => this._containsRPC(a.body));
|
|
46
47
|
}
|
|
47
48
|
if (node.type === 'TryCatchStatement') {
|
|
48
|
-
return
|
|
49
|
-
this._containsRPC(
|
|
49
|
+
return (node.tryBody && node.tryBody.some(s => this._containsRPC(s))) ||
|
|
50
|
+
(node.catchBody && node.catchBody.some(s => this._containsRPC(s))) ||
|
|
51
|
+
(node.finallyBody && node.finallyBody.some(s => this._containsRPC(s)));
|
|
50
52
|
}
|
|
51
53
|
if (node.type === 'PipeExpression') {
|
|
52
54
|
return this._containsRPC(node.left) || this._containsRPC(node.right);
|
|
53
55
|
}
|
|
54
56
|
if (node.type === 'GuardStatement') {
|
|
55
|
-
return this._containsRPC(node.condition) || this._containsRPC(node.
|
|
57
|
+
return this._containsRPC(node.condition) || this._containsRPC(node.elseBody);
|
|
56
58
|
}
|
|
57
59
|
if (node.type === 'LetDestructure') return this._containsRPC(node.value);
|
|
58
60
|
if (node.type === 'ArrayLiteral') return node.elements.some(e => this._containsRPC(e));
|
|
@@ -61,7 +63,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
61
63
|
if (node.type === 'AwaitExpression') return this._containsRPC(node.argument);
|
|
62
64
|
if (node.type === 'PropagateExpression') return this._containsRPC(node.expression);
|
|
63
65
|
if (node.type === 'UnaryExpression') return this._containsRPC(node.operand);
|
|
64
|
-
if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.
|
|
66
|
+
if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.value));
|
|
65
67
|
if (node.type === 'ChainedComparison') return node.operands.some(o => this._containsRPC(o));
|
|
66
68
|
if (node.type === 'RangeExpression') return this._containsRPC(node.start) || this._containsRPC(node.end);
|
|
67
69
|
if (node.type === 'SliceExpression') return this._containsRPC(node.object) || this._containsRPC(node.start) || this._containsRPC(node.end) || this._containsRPC(node.step);
|
|
@@ -177,7 +179,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
177
179
|
const lines = [];
|
|
178
180
|
|
|
179
181
|
// Runtime imports
|
|
180
|
-
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';`);
|
|
181
183
|
lines.push(`import { rpc } from './runtime/rpc.js';`);
|
|
182
184
|
|
|
183
185
|
// Hoist import lines from shared code to the top of the module
|
|
@@ -360,26 +362,26 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
360
362
|
|
|
361
363
|
_generateEffect(body) {
|
|
362
364
|
const hasRPC = this._containsRPC(body);
|
|
363
|
-
|
|
365
|
+
const p = [];
|
|
364
366
|
if (hasRPC) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
p.push(`createEffect(() => {\n`);
|
|
368
|
+
p.push(`${this.i()} (async () => {\n`);
|
|
367
369
|
this.indent += 2;
|
|
368
370
|
const prevAsync = this._asyncContext;
|
|
369
371
|
this._asyncContext = true;
|
|
370
|
-
|
|
372
|
+
p.push(this.genBlockStatements(body));
|
|
371
373
|
this._asyncContext = prevAsync;
|
|
372
374
|
this.indent -= 2;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
+
p.push(`\n${this.i()} })();\n`);
|
|
376
|
+
p.push(`${this.i()}});`);
|
|
375
377
|
} else {
|
|
376
|
-
|
|
378
|
+
p.push(`createEffect(() => {\n`);
|
|
377
379
|
this.indent++;
|
|
378
|
-
|
|
380
|
+
p.push(this.genBlockStatements(body));
|
|
379
381
|
this.indent--;
|
|
380
|
-
|
|
382
|
+
p.push(`\n${this.i()}});`);
|
|
381
383
|
}
|
|
382
|
-
return
|
|
384
|
+
return p.join('');
|
|
383
385
|
}
|
|
384
386
|
|
|
385
387
|
// Generate a short hash from component name + CSS content (for CSS scoping)
|
|
@@ -422,20 +424,21 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
422
424
|
const savedState = new Set(this.stateNames);
|
|
423
425
|
const savedComputed = new Set(this.computedNames);
|
|
424
426
|
|
|
425
|
-
|
|
427
|
+
const p = [];
|
|
428
|
+
p.push(`function ${comp.name}(${paramStr}) {\n`);
|
|
426
429
|
this.indent++;
|
|
427
430
|
|
|
428
431
|
// Generate reactive prop accessors — each prop is accessed through __props getter
|
|
429
432
|
// This ensures parent signal changes propagate reactively to the child
|
|
430
433
|
if (hasParams) {
|
|
431
|
-
for (const
|
|
432
|
-
this.computedNames.add(
|
|
433
|
-
const def =
|
|
434
|
+
for (const param of comp.params) {
|
|
435
|
+
this.computedNames.add(param.name);
|
|
436
|
+
const def = param.default || param.defaultValue;
|
|
434
437
|
if (def) {
|
|
435
438
|
const defaultExpr = this.genExpression(def);
|
|
436
|
-
|
|
439
|
+
p.push(`${this.i()}const ${param.name} = () => __props.${param.name} !== undefined ? __props.${param.name} : ${defaultExpr};\n`);
|
|
437
440
|
} else {
|
|
438
|
-
|
|
441
|
+
p.push(`${this.i()}const ${param.name} = () => __props.${param.name};\n`);
|
|
439
442
|
}
|
|
440
443
|
}
|
|
441
444
|
}
|
|
@@ -446,7 +449,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
446
449
|
const bodyItems = [];
|
|
447
450
|
|
|
448
451
|
for (const node of comp.body) {
|
|
449
|
-
if (node.type === 'JSXElement' || node.type === 'JSXFor' || node.type === 'JSXIf') {
|
|
452
|
+
if (node.type === 'JSXElement' || node.type === 'JSXFragment' || node.type === 'JSXFor' || node.type === 'JSXIf') {
|
|
450
453
|
jsxElements.push(node);
|
|
451
454
|
} else if (node.type === 'ComponentStyleBlock') {
|
|
452
455
|
styleBlocks.push(node);
|
|
@@ -462,7 +465,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
462
465
|
const scopeId = this._genScopeId(comp.name, rawCSS);
|
|
463
466
|
this._currentScopeId = scopeId;
|
|
464
467
|
const scopedCSS = this._scopeCSS(rawCSS, `[data-tova-${scopeId}]`);
|
|
465
|
-
|
|
468
|
+
p.push(`${this.i()}tova_inject_css(${JSON.stringify(scopeId)}, ${JSON.stringify(scopedCSS)});\n`);
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
// Generate body items in order (state, computed, effect, other statements)
|
|
@@ -470,38 +473,38 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
470
473
|
if (node.type === 'StateDeclaration') {
|
|
471
474
|
this.stateNames.add(node.name);
|
|
472
475
|
const init = this.genExpression(node.initialValue);
|
|
473
|
-
|
|
476
|
+
p.push(`${this.i()}const [${node.name}, set${capitalize(node.name)}] = createSignal(${init});\n`);
|
|
474
477
|
} else if (node.type === 'ComputedDeclaration') {
|
|
475
478
|
this.computedNames.add(node.name);
|
|
476
479
|
const expr = this.genExpression(node.expression);
|
|
477
|
-
|
|
480
|
+
p.push(`${this.i()}const ${node.name} = createComputed(() => ${expr});\n`);
|
|
478
481
|
} else if (node.type === 'EffectDeclaration') {
|
|
479
482
|
this.indent++;
|
|
480
483
|
const effectCode = this._generateEffect(node.body);
|
|
481
484
|
this.indent--;
|
|
482
|
-
|
|
485
|
+
p.push(`${this.i()}${effectCode}\n`);
|
|
483
486
|
} else {
|
|
484
|
-
|
|
487
|
+
p.push(this.generateStatement(node) + '\n');
|
|
485
488
|
}
|
|
486
489
|
}
|
|
487
490
|
|
|
488
491
|
// Generate JSX return
|
|
489
492
|
if (jsxElements.length === 1) {
|
|
490
|
-
|
|
493
|
+
p.push(`${this.i()}return ${this.genJSX(jsxElements[0])};\n`);
|
|
491
494
|
} else if (jsxElements.length > 1) {
|
|
492
495
|
const children = jsxElements.map(el => this.genJSX(el)).join(', ');
|
|
493
|
-
|
|
496
|
+
p.push(`${this.i()}return tova_fragment([${children}]);\n`);
|
|
494
497
|
}
|
|
495
498
|
|
|
496
499
|
this.indent--;
|
|
497
|
-
|
|
500
|
+
p.push(`}`);
|
|
498
501
|
|
|
499
502
|
// Restore scoped names and scope id
|
|
500
503
|
this.stateNames = savedState;
|
|
501
504
|
this.computedNames = savedComputed;
|
|
502
505
|
this._currentScopeId = savedScopeId;
|
|
503
506
|
|
|
504
|
-
return
|
|
507
|
+
return p.join('');
|
|
505
508
|
}
|
|
506
509
|
|
|
507
510
|
generateStore(store) {
|
|
@@ -526,54 +529,55 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
526
529
|
}
|
|
527
530
|
}
|
|
528
531
|
|
|
529
|
-
|
|
532
|
+
const p = [];
|
|
533
|
+
p.push(`const ${store.name} = (() => {\n`);
|
|
530
534
|
this.indent++;
|
|
531
535
|
|
|
532
536
|
// Generate state signals
|
|
533
537
|
for (const s of storeStates) {
|
|
534
538
|
const init = this.genExpression(s.initialValue);
|
|
535
|
-
|
|
539
|
+
p.push(`${this.i()}const [${s.name}, set${capitalize(s.name)}] = createSignal(${init});\n`);
|
|
536
540
|
}
|
|
537
541
|
|
|
538
542
|
// Generate computed values
|
|
539
543
|
for (const c of storeComputeds) {
|
|
540
544
|
const expr = this.genExpression(c.expression);
|
|
541
|
-
|
|
545
|
+
p.push(`${this.i()}const ${c.name} = createComputed(() => ${expr});\n`);
|
|
542
546
|
}
|
|
543
547
|
|
|
544
548
|
// Generate functions
|
|
545
549
|
for (const fn of storeFunctions) {
|
|
546
|
-
|
|
550
|
+
p.push(this.genFunctionDeclaration(fn) + '\n');
|
|
547
551
|
}
|
|
548
552
|
|
|
549
553
|
// Build return object with getters/setters
|
|
550
|
-
|
|
554
|
+
p.push(`${this.i()}return {\n`);
|
|
551
555
|
this.indent++;
|
|
552
556
|
|
|
553
557
|
for (const s of storeStates) {
|
|
554
|
-
|
|
555
|
-
|
|
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`);
|
|
556
560
|
}
|
|
557
561
|
|
|
558
562
|
for (const c of storeComputeds) {
|
|
559
|
-
|
|
563
|
+
p.push(`${this.i()}get ${c.name}() { return ${c.name}(); },\n`);
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
for (const fn of storeFunctions) {
|
|
563
|
-
|
|
567
|
+
p.push(`${this.i()}${fn.name},\n`);
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
this.indent--;
|
|
567
|
-
|
|
571
|
+
p.push(`${this.i()}};\n`);
|
|
568
572
|
|
|
569
573
|
this.indent--;
|
|
570
|
-
|
|
574
|
+
p.push(`${this.i()}})();`);
|
|
571
575
|
|
|
572
576
|
// Restore state/computed names
|
|
573
577
|
this.stateNames = savedState;
|
|
574
578
|
this.computedNames = savedComputed;
|
|
575
579
|
|
|
576
|
-
return
|
|
580
|
+
return p.join('');
|
|
577
581
|
}
|
|
578
582
|
|
|
579
583
|
// Check if an AST expression references any signal/computed name
|
|
@@ -602,6 +606,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
602
606
|
if (node.type === 'ObjectLiteral') return node.properties.some(p => this._exprReadsSignal(p.value));
|
|
603
607
|
if (node.type === 'IfExpression') {
|
|
604
608
|
return this._exprReadsSignal(node.condition) || this._exprReadsSignal(node.consequent) ||
|
|
609
|
+
(node.alternates && node.alternates.some(a => this._exprReadsSignal(a.condition) || this._exprReadsSignal(a.body))) ||
|
|
605
610
|
this._exprReadsSignal(node.elseBody);
|
|
606
611
|
}
|
|
607
612
|
if (node.type === 'MatchExpression') {
|
|
@@ -623,6 +628,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
623
628
|
|
|
624
629
|
switch (node.type) {
|
|
625
630
|
case 'JSXElement': return this.genJSXElement(node);
|
|
631
|
+
case 'JSXFragment': return this.genJSXFragment(node);
|
|
626
632
|
case 'JSXText': return this.genJSXText(node);
|
|
627
633
|
case 'JSXExpression': {
|
|
628
634
|
// If expression reads a signal, wrap as () => expr for fine-grained reactivity
|
|
@@ -702,10 +708,24 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
702
708
|
events.change = `(e) => { set${capitalize(exprName)}(${valueExpr}); }`;
|
|
703
709
|
}
|
|
704
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 };
|
|
705
718
|
} else if (attr.name.startsWith('class:')) {
|
|
706
719
|
// Conditional class: class:active={cond}
|
|
707
720
|
const className = attr.name.slice(6);
|
|
708
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 });
|
|
709
729
|
} else if (attr.name.startsWith('on:')) {
|
|
710
730
|
const eventName = attr.name.slice(3);
|
|
711
731
|
events[eventName] = this.genExpression(attr.value);
|
|
@@ -731,6 +751,24 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
731
751
|
attrs.className = isReactive ? `() => ${classExpr}` : classExpr;
|
|
732
752
|
}
|
|
733
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
|
+
|
|
734
772
|
// Add scoped CSS attribute to HTML elements (not components)
|
|
735
773
|
if (this._currentScopeId && !isComponent) {
|
|
736
774
|
attrs[`"data-tova-${this._currentScopeId}"`] = '""';
|
|
@@ -802,12 +840,22 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
802
840
|
|
|
803
841
|
const tag = JSON.stringify(node.tag);
|
|
804
842
|
|
|
843
|
+
let result;
|
|
805
844
|
if (node.selfClosing || node.children.length === 0) {
|
|
806
|
-
|
|
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}])`;
|
|
807
849
|
}
|
|
808
850
|
|
|
809
|
-
|
|
810
|
-
|
|
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;
|
|
811
859
|
}
|
|
812
860
|
|
|
813
861
|
genJSXText(node) {
|
|
@@ -876,6 +924,21 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
876
924
|
return `() => ${result}`;
|
|
877
925
|
}
|
|
878
926
|
|
|
927
|
+
genJSXFragment(node) {
|
|
928
|
+
const children = node.children.map(c => this.genJSX(c)).join(', ');
|
|
929
|
+
return `tova_fragment([${children}])`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Override to add await for piped RPC calls
|
|
933
|
+
genPipeExpression(node) {
|
|
934
|
+
const result = super.genPipeExpression(node);
|
|
935
|
+
// If the pipe target is an RPC call and we're in async context, wrap with await
|
|
936
|
+
if (this._asyncContext && this._containsRPC(node.right)) {
|
|
937
|
+
return `await ${result}`;
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
|
|
879
942
|
// Override function declaration to make async if it contains server.* calls
|
|
880
943
|
genFunctionDeclaration(node) {
|
|
881
944
|
const hasRPC = this._containsRPC(node.body);
|
package/src/codegen/codegen.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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];
|