rip-lang 3.6.0 → 3.6.2

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/src/types.js CHANGED
@@ -1,13 +1,17 @@
1
1
  // Type System — Optional type annotations and .d.ts emission for Rip
2
2
  //
3
- // Architecture: installTypeSupport(Lexer) adds rewriteTypes() to the lexer
4
- // prototype (sidecar pattern, same as components.js for CodeGenerator).
5
- // emitTypes(tokens) generates .d.ts from annotated tokens before parsing.
6
- // generateEnum() is the one CodeGenerator method for runtime enum output.
3
+ // Architecture:
4
+ // installTypeSupport(Lexer) adds rewriteTypes() to the lexer prototype.
5
+ // Strips type annotations from the token stream and stores them as
6
+ // metadata on surviving tokens. The parser never sees types.
7
7
  //
8
- // Design: Types are fully resolved at the token level. The parser never sees
9
- // type annotations, type aliases, or interfaces only enum crosses into the
10
- // grammar because it emits runtime JavaScript.
8
+ // emitTypes(tokens, sexpr) generates .d.ts from annotated tokens and
9
+ // the parsed s-expression tree. Called after parsing so it has access
10
+ // to both token-level annotations (variables, functions, types) and
11
+ // s-expression structures (components). One function, one output.
12
+ //
13
+ // generateEnum() — the one CodeGenerator method for runtime enum output.
14
+ // Enums cross into the grammar because they emit runtime JavaScript.
11
15
 
12
16
  // ============================================================================
13
17
  // installTypeSupport — adds rewriteTypes() to Lexer.prototype
@@ -56,7 +60,7 @@ export function installTypeSupport(Lexer) {
56
60
  let isAlias = !isDef && tokens[i + 1 + genTokens.length]?.[0] === 'TYPE_ALIAS';
57
61
  if (isDef || isAlias) {
58
62
  if (!token.data) token.data = {};
59
- token.data.typeParams = genTokens.map(t => t[1]).join('');
63
+ token.data.typeParams = buildTypeString(genTokens);
60
64
  tokens.splice(i + 1, genTokens.length);
61
65
  // After removing <T>, retag ( as CALL_START if it follows DEF IDENTIFIER
62
66
  if (isDef && tokens[i + 1]?.[0] === '(') {
@@ -272,12 +276,17 @@ function collectTypeExpression(tokens, j) {
272
276
  // Build a clean type string from collected tokens
273
277
  function buildTypeString(typeTokens) {
274
278
  if (typeTokens.length === 0) return '';
279
+ // Bare => (no params) means () => — add empty parens
280
+ if (typeTokens[0]?.[0] === '=>') typeTokens.unshift(['', '()']);
275
281
  let typeStr = typeTokens.map(t => t[1]).join(' ').replace(/\s+/g, ' ').trim();
276
282
  typeStr = typeStr
277
283
  .replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>')
278
284
  .replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
279
285
  .replace(/\s*\(\s*/g, '(').replace(/\s*\)\s*/g, ')')
280
- .replace(/\s*,\s*/g, ', ');
286
+ .replace(/\s*,\s*/g, ', ')
287
+ .replace(/\s*=>\s*/g, ' => ')
288
+ .replace(/ :: /g, ': ')
289
+ .replace(/:: /g, ': ');
281
290
  return typeStr;
282
291
  }
283
292
 
@@ -347,11 +356,17 @@ function collectStructuralType(tokens, indentIdx) {
347
356
  // Skip : separator
348
357
  if (tokens[j]?.[1] === ':') j++;
349
358
 
350
- // Collect the type (until TERMINATOR, OUTDENT, or next property)
359
+ // Collect the type (until TERMINATOR or OUTDENT at property depth)
351
360
  let propTypeTokens = [];
361
+ let typeDepth = 0;
352
362
  while (j < tokens.length) {
353
363
  let pt = tokens[j];
354
- if (pt[0] === 'TERMINATOR' || pt[0] === 'OUTDENT' || pt[0] === 'INDENT') break;
364
+ if (pt[0] === 'INDENT') { typeDepth++; j++; continue; }
365
+ if (pt[0] === 'OUTDENT') {
366
+ if (typeDepth > 0) { typeDepth--; j++; continue; }
367
+ break;
368
+ }
369
+ if (pt[0] === 'TERMINATOR' && typeDepth === 0) break;
355
370
  propTypeTokens.push(pt);
356
371
  j++;
357
372
  }
@@ -441,15 +456,17 @@ function collectBlockUnion(tokens, startIdx) {
441
456
  }
442
457
 
443
458
  // ============================================================================
444
- // emitTypes — generate .d.ts from annotated token stream
459
+ // emitTypes — generate .d.ts from annotated tokens + s-expression tree
445
460
  // ============================================================================
446
461
 
447
- export function emitTypes(tokens) {
462
+ export function emitTypes(tokens, sexpr = null) {
448
463
  let lines = [];
449
464
  let indentLevel = 0;
450
465
  let indentStr = ' ';
451
466
  let indent = () => indentStr.repeat(indentLevel);
452
467
  let inClass = false;
468
+ let usesSignal = false;
469
+ let usesComputed = false;
453
470
 
454
471
  // Format { prop; prop } into multi-line block
455
472
  let emitBlock = (prefix, body, suffix) => {
@@ -467,6 +484,110 @@ export function emitTypes(tokens) {
467
484
  lines.push(`${indent()}${prefix}${body}${suffix}`);
468
485
  };
469
486
 
487
+ // Collect function parameters (handles simple, destructured, rest, defaults)
488
+ let collectParams = (tokens, startIdx) => {
489
+ let params = [];
490
+ let j = startIdx;
491
+ let openTag = tokens[j]?.[0];
492
+ if (openTag !== 'CALL_START' && openTag !== 'PARAM_START') return { params, endIdx: j };
493
+ let closeTag = openTag === 'CALL_START' ? 'CALL_END' : 'PARAM_END';
494
+ j++;
495
+ let depth = 0;
496
+
497
+ while (j < tokens.length && !(tokens[j][0] === closeTag && depth === 0)) {
498
+ let tok = tokens[j];
499
+
500
+ // Skip commas at depth 0
501
+ if (tok[1] === ',' && depth === 0) { j++; continue; }
502
+
503
+ // Track nesting
504
+ if (tok[0] === '{' || tok[0] === '[' || tok[0] === 'CALL_START' ||
505
+ tok[0] === 'PARAM_START' || tok[0] === 'INDEX_START') depth++;
506
+ if (tok[0] === '}' || tok[0] === ']' || tok[0] === 'CALL_END' ||
507
+ tok[0] === 'PARAM_END' || tok[0] === 'INDEX_END') { depth--; j++; continue; }
508
+
509
+ // @ prefix (constructor shorthand: @name)
510
+ if (tok[0] === '@') {
511
+ j++;
512
+ if (tokens[j]?.[0] === 'PROPERTY' || tokens[j]?.[0] === 'IDENTIFIER') {
513
+ let name = tokens[j][1];
514
+ let type = tokens[j].data?.type;
515
+ params.push(type ? `${name}: ${expandSuffixes(type)}` : name);
516
+ j++;
517
+ }
518
+ continue;
519
+ }
520
+
521
+ // Rest parameter: ...name
522
+ if (tok[0] === 'SPREAD' || tok[1] === '...') {
523
+ j++;
524
+ if (tokens[j]?.[0] === 'IDENTIFIER') {
525
+ let name = tokens[j][1];
526
+ let type = tokens[j].data?.type;
527
+ params.push(type ? `...${name}: ${expandSuffixes(type)}` : `...${name}: any[]`);
528
+ j++;
529
+ }
530
+ continue;
531
+ }
532
+
533
+ // Destructured object parameter: { a, b }
534
+ if (tok[0] === '{') {
535
+ // Collect the whole destructured pattern as a string
536
+ let pattern = '{';
537
+ j++;
538
+ let d = 1;
539
+ while (j < tokens.length && d > 0) {
540
+ if (tokens[j][0] === '{') d++;
541
+ if (tokens[j][0] === '}') d--;
542
+ if (d > 0) pattern += tokens[j][1] + (tokens[j + 1]?.[0] === '}' ? '' : ', ');
543
+ j++;
544
+ }
545
+ pattern += '}';
546
+ // Check if the closing } had a type annotation
547
+ let type = tokens[j - 1]?.data?.type;
548
+ params.push(type ? `${pattern}: ${expandSuffixes(type)}` : pattern);
549
+ continue;
550
+ }
551
+
552
+ // Simple identifier parameter
553
+ if (tok[0] === 'IDENTIFIER') {
554
+ let paramName = tok[1];
555
+ let paramType = tok.data?.type;
556
+
557
+ // Check for default value (skip = and the default expression)
558
+ let hasDefault = false;
559
+ if (tokens[j + 1]?.[0] === '=') {
560
+ hasDefault = true;
561
+ }
562
+
563
+ if (paramType) {
564
+ params.push(`${paramName}${hasDefault ? '?' : ''}: ${expandSuffixes(paramType)}`);
565
+ } else {
566
+ params.push(paramName);
567
+ }
568
+ j++;
569
+
570
+ // Skip past default value expression
571
+ if (hasDefault) {
572
+ j++; // skip =
573
+ let dd = 0;
574
+ while (j < tokens.length) {
575
+ let dt = tokens[j];
576
+ if (dt[0] === '(' || dt[0] === '[' || dt[0] === '{') dd++;
577
+ if (dt[0] === ')' || dt[0] === ']' || dt[0] === '}') dd--;
578
+ if (dd === 0 && (dt[1] === ',' || dt[0] === 'CALL_END')) break;
579
+ j++;
580
+ }
581
+ }
582
+ continue;
583
+ }
584
+
585
+ j++;
586
+ }
587
+
588
+ return { params, endIdx: j };
589
+ };
590
+
470
591
  for (let i = 0; i < tokens.length; i++) {
471
592
  let t = tokens[i];
472
593
  let tag = t[0];
@@ -479,6 +600,40 @@ export function emitTypes(tokens) {
479
600
  if (i >= tokens.length) break;
480
601
  t = tokens[i];
481
602
  tag = t[0];
603
+
604
+ // Export default
605
+ if (tag === 'DEFAULT') {
606
+ i++;
607
+ if (i >= tokens.length) break;
608
+ t = tokens[i];
609
+ tag = t[0];
610
+
611
+ // export default IDENTIFIER (re-export)
612
+ if (tag === 'IDENTIFIER') {
613
+ lines.push(`${indent()}export default ${t[1]};`);
614
+ }
615
+ // export default { ... } or other expressions — skip for now
616
+ continue;
617
+ }
618
+ }
619
+
620
+ // Import statements — pass through for type references
621
+ if (tag === 'IMPORT') {
622
+ let importTokens = [];
623
+ let j = i + 1;
624
+ while (j < tokens.length && tokens[j][0] !== 'TERMINATOR') {
625
+ importTokens.push(tokens[j]);
626
+ j++;
627
+ }
628
+ // Reconstruct: join with spaces, then clean up spacing
629
+ let raw = 'import ' + importTokens.map(tk => tk[1]).join(' ');
630
+ raw = raw.replace(/\s+/g, ' ')
631
+ .replace(/\s*,\s*/g, ', ')
632
+ .replace(/\{\s*/g, '{ ').replace(/\s*\}/g, ' }')
633
+ .trim();
634
+ lines.push(`${indent()}${raw};`);
635
+ i = j;
636
+ continue;
482
637
  }
483
638
 
484
639
  // TYPE_DECL marker — emit type alias or interface
@@ -578,7 +733,7 @@ export function emitTypes(tokens) {
578
733
  continue;
579
734
  }
580
735
 
581
- // DEF — emit function declaration
736
+ // DEF — emit function or method declaration
582
737
  if (tag === 'DEF') {
583
738
  let nameToken = tokens[i + 1];
584
739
  if (!nameToken) continue;
@@ -586,24 +741,7 @@ export function emitTypes(tokens) {
586
741
  let returnType = nameToken.data?.returnType;
587
742
  let typeParams = nameToken.data?.typeParams || '';
588
743
 
589
- // Collect parameters
590
- let j = i + 2;
591
- let params = [];
592
- if (tokens[j]?.[0] === 'CALL_START') {
593
- j++;
594
- while (j < tokens.length && tokens[j][0] !== 'CALL_END') {
595
- if (tokens[j][0] === 'IDENTIFIER') {
596
- let paramName = tokens[j][1];
597
- let paramType = tokens[j].data?.type;
598
- if (paramType) {
599
- params.push(`${paramName}: ${expandSuffixes(paramType)}`);
600
- } else {
601
- params.push(paramName);
602
- }
603
- }
604
- j++;
605
- }
606
- }
744
+ let { params, endIdx } = collectParams(tokens, i + 2);
607
745
 
608
746
  // Only emit if there are type annotations
609
747
  if (returnType || params.some(p => p.includes(':'))) {
@@ -611,11 +749,71 @@ export function emitTypes(tokens) {
611
749
  let declare = inClass ? '' : (exported ? '' : 'declare ');
612
750
  let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
613
751
  let paramStr = params.join(', ');
614
- lines.push(`${indent()}${exp}${declare}function ${fnName}${typeParams}(${paramStr})${ret};`);
752
+ if (inClass) {
753
+ lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
754
+ } else {
755
+ lines.push(`${indent()}${exp}${declare}function ${fnName}${typeParams}(${paramStr})${ret};`);
756
+ }
615
757
  }
616
758
  continue;
617
759
  }
618
760
 
761
+ // Class method block: { PROPERTY ... PROPERTY ... }
762
+ // Contains one or more methods separated by TERMINATOR
763
+ if (tag === '{' && inClass) {
764
+ let j = i + 1;
765
+ let braceDepth = 1;
766
+
767
+ while (j < tokens.length && braceDepth > 0) {
768
+ let tok = tokens[j];
769
+
770
+ if (tok[0] === '{') { braceDepth++; j++; continue; }
771
+ if (tok[0] === '}') { braceDepth--; j++; continue; }
772
+ if (tok[0] === 'TERMINATOR') { j++; continue; }
773
+
774
+ // Found a method: PROPERTY "name" : PARAM_START ... PARAM_END -> body
775
+ if (tok[0] === 'PROPERTY' && braceDepth === 1) {
776
+ let methodName = tok[1];
777
+ let returnType = tok.data?.returnType;
778
+ j++;
779
+
780
+ // Skip : separator
781
+ if (tokens[j]?.[1] === ':') j++;
782
+
783
+ let params = [];
784
+ if (tokens[j]?.[0] === 'PARAM_START') {
785
+ let result = collectParams(tokens, j);
786
+ params = result.params;
787
+ j = result.endIdx + 1;
788
+ }
789
+
790
+ // Skip -> and method body (INDENT ... OUTDENT)
791
+ if (tokens[j]?.[0] === '->' || tokens[j]?.[0] === '=>') j++;
792
+ if (tokens[j]?.[0] === 'INDENT') {
793
+ let d = 1;
794
+ j++;
795
+ while (j < tokens.length && d > 0) {
796
+ if (tokens[j][0] === 'INDENT') d++;
797
+ if (tokens[j][0] === 'OUTDENT') d--;
798
+ j++;
799
+ }
800
+ }
801
+
802
+ if (returnType || params.some(p => p.includes(':'))) {
803
+ let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
804
+ let paramStr = params.join(', ');
805
+ lines.push(`${indent()}${methodName}(${paramStr})${ret};`);
806
+ }
807
+ continue;
808
+ }
809
+
810
+ j++;
811
+ }
812
+
813
+ i = j - 1;
814
+ continue;
815
+ }
816
+
619
817
  // Track INDENT/OUTDENT for class body
620
818
  if (tag === 'INDENT') {
621
819
  continue;
@@ -629,6 +827,35 @@ export function emitTypes(tokens) {
629
827
  continue;
630
828
  }
631
829
 
830
+ // Arrow function assignment: name = (params) -> body
831
+ if (tag === 'IDENTIFIER' && !inClass && tokens[i + 1]?.[0] === '=' &&
832
+ (tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
833
+ let fnName = t[1];
834
+ let j = i + 2;
835
+
836
+ let { params } = collectParams(tokens, j);
837
+
838
+ // Find the -> or => token to get return type
839
+ let k = j;
840
+ let depth = 0;
841
+ while (k < tokens.length) {
842
+ if (tokens[k][0] === 'PARAM_START' || tokens[k][0] === '(') depth++;
843
+ if (tokens[k][0] === 'PARAM_END' || tokens[k][0] === ')') depth--;
844
+ if (depth === 0 && (tokens[k][0] === '->' || tokens[k][0] === '=>')) break;
845
+ k++;
846
+ }
847
+ let returnType = tokens[k]?.data?.returnType;
848
+
849
+ if (returnType || params.some(p => p.includes(':'))) {
850
+ let exp = exported ? 'export ' : '';
851
+ let declare = exported ? '' : 'declare ';
852
+ let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
853
+ let paramStr = params.join(', ');
854
+ lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
855
+ continue;
856
+ }
857
+ }
858
+
632
859
  // Variable assignments with type annotations
633
860
  if (tag === 'IDENTIFIER' && t.data?.type) {
634
861
  let varName = t[1];
@@ -642,11 +869,35 @@ export function emitTypes(tokens) {
642
869
  if (next[0] === 'READONLY_ASSIGN') {
643
870
  lines.push(`${indent()}${exp}${declare}const ${varName}: ${type};`);
644
871
  } else if (next[0] === 'REACTIVE_ASSIGN') {
872
+ usesSignal = true;
645
873
  lines.push(`${indent()}${exp}${declare}const ${varName}: Signal<${type}>;`);
646
874
  } else if (next[0] === 'COMPUTED_ASSIGN') {
875
+ usesComputed = true;
647
876
  lines.push(`${indent()}${exp}${declare}const ${varName}: Computed<${type}>;`);
877
+ } else if (next[0] === 'REACT_ASSIGN') {
878
+ lines.push(`${indent()}${exp}${declare}const ${varName}: () => void;`);
648
879
  } else if (next[0] === '=') {
649
- if (inClass) {
880
+ // Check if RHS is an arrow function with return type
881
+ let arrowIdx = i + 2;
882
+ // Skip past PARAM_START ... PARAM_END if present
883
+ if (tokens[arrowIdx]?.[0] === 'PARAM_START') {
884
+ let d = 1, k = arrowIdx + 1;
885
+ while (k < tokens.length && d > 0) {
886
+ if (tokens[k][0] === 'PARAM_START') d++;
887
+ if (tokens[k][0] === 'PARAM_END') d--;
888
+ k++;
889
+ }
890
+ arrowIdx = k;
891
+ }
892
+ let arrowToken = tokens[arrowIdx];
893
+ if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
894
+ arrowToken.data?.returnType) {
895
+ // Typed arrow function assignment
896
+ let returnType = expandSuffixes(arrowToken.data.returnType);
897
+ let { params } = collectParams(tokens, i + 2);
898
+ let paramStr = params.join(', ');
899
+ lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
900
+ } else if (inClass) {
650
901
  lines.push(`${indent()}${varName}: ${type};`);
651
902
  } else {
652
903
  lines.push(`${indent()}${exp}let ${varName}: ${type};`);
@@ -661,8 +912,35 @@ export function emitTypes(tokens) {
661
912
  }
662
913
  }
663
914
 
915
+ // Walk s-expression tree for component declarations
916
+ let componentVars = new Set();
917
+ if (sexpr) {
918
+ emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars);
919
+
920
+ // Remove lines for variables that belong to components (emitted as class members)
921
+ if (componentVars.size > 0) {
922
+ for (let k = lines.length - 1; k >= 0; k--) {
923
+ let match = lines[k].match(/(?:declare |export )*(?:const|let) (\w+)/);
924
+ if (match && componentVars.has(match[1])) lines.splice(k, 1);
925
+ }
926
+ }
927
+ }
928
+
664
929
  if (lines.length === 0) return null;
665
- return lines.join('\n') + '\n';
930
+
931
+ // Prepend reactive type definitions if used
932
+ let preamble = [];
933
+ if (usesSignal) {
934
+ preamble.push('interface Signal<T> { value: T; read(): T; lock(): Signal<T>; free(): Signal<T>; kill(): T; }');
935
+ }
936
+ if (usesComputed) {
937
+ preamble.push('interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }');
938
+ }
939
+ if (preamble.length > 0) {
940
+ preamble.push('');
941
+ }
942
+
943
+ return preamble.concat(lines).join('\n') + '\n';
666
944
  }
667
945
 
668
946
  // ============================================================================
@@ -687,6 +965,109 @@ function expandSuffixes(typeStr) {
687
965
  return typeStr;
688
966
  }
689
967
 
968
+ // ============================================================================
969
+ // Component type emission — walk s-expression for component declarations
970
+ // ============================================================================
971
+
972
+ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars) {
973
+ if (!Array.isArray(sexpr)) return;
974
+ let head = sexpr[0]?.valueOf?.() ?? sexpr[0];
975
+
976
+ // export Name = component ... → ["export", ["=", "Name", ["component", ...members]]]
977
+ // Name = component ... → ["=", "Name", ["component", ...members]]
978
+ let exported = false;
979
+ let name = null;
980
+ let compNode = null;
981
+
982
+ if (head === 'export' && Array.isArray(sexpr[1])) {
983
+ exported = true;
984
+ let inner = sexpr[1];
985
+ let innerHead = inner[0]?.valueOf?.() ?? inner[0];
986
+ if (innerHead === '=' && Array.isArray(inner[2]) &&
987
+ (inner[2][0]?.valueOf?.() ?? inner[2][0]) === 'component') {
988
+ name = inner[1]?.valueOf?.() ?? inner[1];
989
+ compNode = inner[2];
990
+ }
991
+ } else if (head === '=' && Array.isArray(sexpr[2]) &&
992
+ (sexpr[2][0]?.valueOf?.() ?? sexpr[2][0]) === 'component') {
993
+ name = sexpr[1]?.valueOf?.() ?? sexpr[1];
994
+ compNode = sexpr[2];
995
+ }
996
+
997
+ if (name && compNode) {
998
+ let exp = exported ? 'export ' : '';
999
+
1000
+ // Component structure: ["component", parent, ["block", ...members]]
1001
+ let body = compNode[2];
1002
+ let members = (Array.isArray(body) && (body[0]?.valueOf?.() ?? body[0]) === 'block')
1003
+ ? body.slice(1) : (body ? [body] : []);
1004
+
1005
+ let props = [];
1006
+ let methods = [];
1007
+
1008
+ for (let member of members) {
1009
+ if (!Array.isArray(member)) continue;
1010
+ let mHead = member[0]?.valueOf?.() ?? member[0];
1011
+
1012
+ // Reactive state: ["state", "count", 0]
1013
+ if (mHead === 'state') {
1014
+ let propName = member[1]?.valueOf?.() ?? member[1];
1015
+ let type = member[1]?.type;
1016
+ props.push(` ${propName}: ${type ? expandSuffixes(type) : 'any'};`);
1017
+ componentVars.add(propName);
1018
+ }
1019
+ // Computed: ["computed", "doubled", expr]
1020
+ else if (mHead === 'computed') {
1021
+ let propName = member[1]?.valueOf?.() ?? member[1];
1022
+ let type = member[1]?.type;
1023
+ props.push(` readonly ${propName}: ${type ? expandSuffixes(type) : 'any'};`);
1024
+ componentVars.add(propName);
1025
+ }
1026
+ // Method object: ["object", ["methodName", ["->", params, body], ":"]]
1027
+ else if (mHead === 'object') {
1028
+ for (let j = 1; j < member.length; j++) {
1029
+ let entry = member[j];
1030
+ if (!Array.isArray(entry)) continue;
1031
+ let methodName = entry[0]?.valueOf?.() ?? entry[0];
1032
+ if (methodName === 'render') continue; // skip render
1033
+ let fn = entry[1];
1034
+ if (Array.isArray(fn)) {
1035
+ let fnHead = fn[0]?.valueOf?.() ?? fn[0];
1036
+ if (fnHead === '->' || fnHead === '=>') {
1037
+ methods.push(` ${methodName}(): void;`);
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ // Skip render blocks
1043
+ else if (mHead === 'render') {
1044
+ continue;
1045
+ }
1046
+ }
1047
+
1048
+ lines.push(`${exp}declare class ${name} {`);
1049
+ lines.push(` constructor(props?: Record<string, any>);`);
1050
+ for (let p of props) lines.push(p);
1051
+ for (let m of methods) lines.push(m);
1052
+ lines.push(` mount(target: Element | string): ${name};`);
1053
+ lines.push(` unmount(): void;`);
1054
+ lines.push(`}`);
1055
+ }
1056
+
1057
+ // Recurse into child nodes
1058
+ if (head === 'program' || head === 'block') {
1059
+ for (let i = 1; i < sexpr.length; i++) {
1060
+ if (Array.isArray(sexpr[i])) {
1061
+ emitComponentTypes(sexpr[i], lines, indent, indentLevel, componentVars);
1062
+ }
1063
+ }
1064
+ }
1065
+ // Also check inside export wrappers
1066
+ if (head === 'export' && Array.isArray(sexpr[1]) && !compNode) {
1067
+ emitComponentTypes(sexpr[1], lines, indent, indentLevel, componentVars);
1068
+ }
1069
+ }
1070
+
690
1071
  // ============================================================================
691
1072
  // generateEnum — runtime JavaScript enum object (CodeGenerator method)
692
1073
  // ============================================================================