typenative 0.1.1 → 0.1.3

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/transpiler.js CHANGED
@@ -263,6 +263,7 @@ const importedPackages = new Set();
263
263
  let outsideNodes = [];
264
264
  const classNames = new Set();
265
265
  let promiseResolveName = '';
266
+ let promiseRejectName = '';
266
267
  // Go keywords that cannot be used as identifiers
267
268
  const dangerousNames = new Set([
268
269
  'main',
@@ -337,6 +338,8 @@ const interfacePropertyTypes = new Map();
337
338
  const typeAliases = new Map();
338
339
  const enumNames = new Set();
339
340
  const enumBaseTypes = new Map();
341
+ // Member types of heterogeneous enums ("E.A" → float64 / string)
342
+ const enumMemberGoTypes = new Map();
340
343
  // Maps local TS name → Go qualified name (e.g. 'Println' → 'fmt.Println', 'myFmt' → 'fmt')
341
344
  const importAliases = new Map();
342
345
  // Tracks static methods per class: Set of "ClassName.methodName" strings
@@ -389,13 +392,22 @@ const OPERATOR_TEXT = {
389
392
  function operatorTokenText(token) {
390
393
  return OPERATOR_TEXT[token?.kind] ?? token?.kind ?? '';
391
394
  }
392
- function defaultParseFunction(_code) {
395
+ function defaultParseFunction(_code, _tsx) {
393
396
  throw new Error('transpileToNative: no parse function injected (options.parse)');
394
397
  }
395
398
  // Parses via the injected parser and links each node to its parent: the JSON
396
399
  // AST carries no parent pointers, but the transpiler walks up via node.parent.
397
400
  function parseSource(code) {
398
- const sourceFile = parseFunction(code);
401
+ const sourceFile = parseFunction(code, currentFileIsTsx);
402
+ stripThisParameters(sourceFile);
403
+ lowerIndexSignatures(sourceFile);
404
+ lowerAmbientDeclarations(sourceFile);
405
+ lowerModuleForms(sourceFile);
406
+ lowerDisposeSymbols(sourceFile);
407
+ lowerNamespaces(sourceFile);
408
+ lowerClassExpressions(sourceFile);
409
+ lowerWeakCollections(sourceFile);
410
+ lowerGlobalThis(sourceFile);
399
411
  linkParents(sourceFile, undefined);
400
412
  for (const stmt of sourceFile.statements ?? []) {
401
413
  if (!stmt.name || !isIdentifier(stmt.name))
@@ -415,6 +427,289 @@ function parseSource(code) {
415
427
  }
416
428
  return sourceFile;
417
429
  }
430
+ // `this: T` parameters only type `this`; they are not real parameters
431
+ function stripThisParameters(node) {
432
+ if (Array.isArray(node.parameters)) {
433
+ node.parameters = node.parameters.filter((p) => !(isIdentifier(p.name) && p.name.text === 'this'));
434
+ }
435
+ for (const child of childNodes(node))
436
+ stripThisParameters(child);
437
+ }
438
+ // { [k: K]: V } and { [K in string]: V } → Record<K, V>; an interface that is
439
+ // only an index signature becomes an alias of that Record
440
+ function lowerIndexSignatures(node) {
441
+ for (const child of childNodes(node))
442
+ lowerIndexSignatures(child);
443
+ const keyKind = node.typeParameter?.constraint?.kind;
444
+ if (node.kind === 'MappedType' && (keyKind === 'StringKeyword' || keyKind === 'NumberKeyword')) {
445
+ const keyType = { kind: keyKind };
446
+ makeRecordReference(node, keyType, node.type);
447
+ return;
448
+ }
449
+ const members = node.members ?? [];
450
+ // { (a: A): R } / { new (a: A): C } → (a: A) => R / (a: A) => C
451
+ const isSignature = (m) => m.kind === 'CallSignature' || m.kind === 'ConstructSignature';
452
+ if (members.length === 1 && isSignature(members[0]) && (node.kind === 'TypeLiteral' || node.kind === 'InterfaceDeclaration')) {
453
+ const fnType = { kind: 'FunctionType', parameters: members[0].parameters ?? [], type: members[0].type };
454
+ if (node.kind === 'TypeLiteral') {
455
+ for (const key of Object.keys(node))
456
+ if (key !== 'parent')
457
+ delete node[key];
458
+ node.kind = 'FunctionType';
459
+ node.parameters = fnType.parameters;
460
+ node.type = fnType.type;
461
+ }
462
+ else {
463
+ delete node.members;
464
+ delete node.heritageClauses;
465
+ node.kind = 'TypeAliasDeclaration';
466
+ node.type = fnType;
467
+ }
468
+ return;
469
+ }
470
+ if (members.length !== 1 || members[0].kind !== 'IndexSignature')
471
+ return;
472
+ const signature = members[0];
473
+ if (node.kind === 'TypeLiteral')
474
+ makeRecordReference(node, signature.parameters[0].type, signature.type);
475
+ if (node.kind === 'InterfaceDeclaration') {
476
+ const record = { kind: 'TypeLiteral' };
477
+ makeRecordReference(record, signature.parameters[0].type, signature.type);
478
+ delete node.members;
479
+ delete node.heritageClauses;
480
+ node.kind = 'TypeAliasDeclaration';
481
+ node.type = record;
482
+ }
483
+ }
484
+ // Turns node into the type reference Record<keyType, valueType>
485
+ function makeRecordReference(node, keyType, valueType) {
486
+ for (const key of Object.keys(node))
487
+ if (key !== 'parent')
488
+ delete node[key];
489
+ const typeName = { kind: 'Identifier', text: 'Record' };
490
+ node.kind = 'TypeReference';
491
+ node.typeName = typeName;
492
+ node.typeArguments = [keyType, valueType];
493
+ }
494
+ function lowerAmbientDeclarations(node) {
495
+ for (const child of childNodes(node))
496
+ lowerAmbientDeclarations(child);
497
+ if (!Array.isArray(node.statements))
498
+ return;
499
+ const isDeclare = (m) => m.kind === 'DeclareKeyword';
500
+ node.statements = node.statements.filter((statement) => {
501
+ if (statement.kind === 'VariableStatement' && statement.modifiers) {
502
+ // declare / export on variables don't change the emitted Go
503
+ statement.modifiers = statement.modifiers.filter((m) => !isDeclare(m) && m.kind !== 'ExportKeyword');
504
+ return true;
505
+ }
506
+ return !(statement.modifiers ?? []).some(isDeclare);
507
+ });
508
+ }
509
+ // Every module form becomes an import declaration (all files share one Go package):
510
+ // export { a, b as c } from './m' → import { a, b as c } from './m'
511
+ // export * from './m' → import './m'
512
+ // import x = require('./m') → import * as x from './m'
513
+ // const x = await import('./m') → import * as x from './m'
514
+ function lowerModuleForms(node) {
515
+ for (const child of childNodes(node))
516
+ lowerModuleForms(child);
517
+ if (!Array.isArray(node.statements))
518
+ return;
519
+ node.statements = node.statements.map((statement) => {
520
+ const namespaceImport = (name, specifier) => ({
521
+ kind: 'ImportDeclaration',
522
+ moduleSpecifier: specifier,
523
+ importClause: { kind: 'ImportClause', namedBindings: { kind: 'NamespaceImport', name } }
524
+ });
525
+ if (statement.kind === 'ExportDeclaration' && statement.moduleSpecifier && !statement.isTypeOnly) {
526
+ const clause = statement.exportClause;
527
+ if (clause?.kind === 'NamespaceExport')
528
+ return namespaceImport(clause.name, statement.moduleSpecifier);
529
+ const importClause = clause
530
+ ? { kind: 'ImportClause', namedBindings: { kind: 'NamedImports', elements: clause.elements ?? [] } }
531
+ : undefined;
532
+ return { kind: 'ImportDeclaration', moduleSpecifier: statement.moduleSpecifier, importClause };
533
+ }
534
+ if (statement.kind === 'ImportEqualsDeclaration' && statement.moduleReference?.kind === 'ExternalModuleReference') {
535
+ return namespaceImport(statement.name, statement.moduleReference.expression);
536
+ }
537
+ const declarations = statement.kind === 'VariableStatement' ? (statement.declarationList?.declarations ?? []) : [];
538
+ const init = declarations.length === 1 ? declarations[0].initializer : undefined;
539
+ const call = init?.kind === 'AwaitExpression' ? init.expression : undefined;
540
+ if (call?.kind === 'CallExpression' && call.expression?.kind === 'ImportKeyword' && isStringLiteral(call.arguments?.[0]) && isIdentifier(declarations[0].name)) {
541
+ return namespaceImport(declarations[0].name, call.arguments[0]);
542
+ }
543
+ return statement;
544
+ });
545
+ }
546
+ // Names whose identifier is a property / member name, not a reference
547
+ const MEMBER_NAME_PARENTS = new Set([
548
+ 'PropertyAccessExpression', 'PropertyAssignment', 'PropertySignature', 'MethodSignature',
549
+ 'PropertyDeclaration', 'MethodDeclaration', 'GetAccessor', 'SetAccessor', 'EnumMember'
550
+ ]);
551
+ // Top-level names a statement list declares
552
+ function getDeclaredNames(statements) {
553
+ const names = [];
554
+ for (const statement of statements) {
555
+ if (statement.kind === 'VariableStatement') {
556
+ for (const d of statement.declarationList?.declarations ?? [])
557
+ if (isIdentifier(d.name))
558
+ names.push(d.name.text);
559
+ }
560
+ else if (isIdentifier(statement.name)) {
561
+ names.push(statement.name.text);
562
+ }
563
+ }
564
+ return names;
565
+ }
566
+ // Renames references to the given names (not property/member names) to prefix_name
567
+ function renameIdentifiers(node, names, prefix) {
568
+ for (const key of Object.keys(node)) {
569
+ if (key === 'parent')
570
+ continue;
571
+ const value = node[key];
572
+ const children = Array.isArray(value) ? value : value && typeof value === 'object' ? [value] : [];
573
+ for (const child of children) {
574
+ if (!child || typeof child.kind !== 'string')
575
+ continue;
576
+ if (isIdentifier(child) && names.has(child.text)) {
577
+ if (key === 'name' && MEMBER_NAME_PARENTS.has(node.kind))
578
+ continue;
579
+ if (key === 'right' && node.kind === 'QualifiedName')
580
+ continue;
581
+ if (node.kind === 'ShorthandPropertyAssignment') {
582
+ // { unit } keeps its key: { unit: N_unit }
583
+ node.kind = 'PropertyAssignment';
584
+ const initializer = { kind: 'Identifier', text: `${prefix}_${child.text}` };
585
+ node.initializer = initializer;
586
+ continue;
587
+ }
588
+ child.text = `${prefix}_${child.text}`;
589
+ }
590
+ else {
591
+ renameIdentifiers(child, names, prefix);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ // Flattens the namespaces in a statement list; returns the namespace names found
597
+ // (nested ones as Outer_Inner)
598
+ function flattenNamespaces(statements) {
599
+ const out = [];
600
+ const namespaces = [];
601
+ for (const statement of statements) {
602
+ const isDeclare = (statement.modifiers ?? []).some((m) => m.kind === 'DeclareKeyword');
603
+ if (statement.kind !== 'ModuleDeclaration' || !isIdentifier(statement.name) || isDeclare) {
604
+ out.push(statement);
605
+ continue;
606
+ }
607
+ const prefix = statement.name.text;
608
+ // namespace A.B { } nests a ModuleDeclaration as the body
609
+ const body = statement.body?.kind === 'ModuleDeclaration' ? [statement.body] : (statement.body?.statements ?? []);
610
+ const inner = flattenNamespaces(body);
611
+ rewriteNamespaceAccess(inner.statements, new Set(inner.namespaces));
612
+ const names = new Set([...getDeclaredNames(inner.statements), ...inner.namespaces]);
613
+ for (const child of inner.statements) {
614
+ const wrapper = { kind: 'Block', statements: [child] };
615
+ renameIdentifiers(wrapper, names, prefix);
616
+ }
617
+ namespaces.push(prefix);
618
+ for (const n of inner.namespaces)
619
+ namespaces.push(`${prefix}_${n}`);
620
+ out.push(...inner.statements);
621
+ }
622
+ return { statements: out, namespaces };
623
+ }
624
+ // N.x / N.Inner.x (expressions) and N.T (types) → the flattened identifiers
625
+ function rewriteNamespaceAccess(nodes, namespaces) {
626
+ const rewrite = (node) => {
627
+ for (const key of Object.keys(node)) {
628
+ if (key === 'parent')
629
+ continue;
630
+ const value = node[key];
631
+ if (Array.isArray(value))
632
+ node[key] = value.map((v) => (v && typeof v.kind === 'string' ? rewrite(v) : v));
633
+ else if (value && typeof value.kind === 'string')
634
+ node[key] = rewrite(value);
635
+ }
636
+ const left = node.kind === 'PropertyAccessExpression' ? node.expression : node.kind === 'QualifiedName' ? node.left : undefined;
637
+ const right = node.kind === 'PropertyAccessExpression' ? node.name : node.kind === 'QualifiedName' ? node.right : undefined;
638
+ if (isIdentifier(left) && namespaces.has(left.text) && isIdentifier(right)) {
639
+ const flattened = { kind: 'Identifier', text: `${left.text}_${right.text}` };
640
+ return flattened;
641
+ }
642
+ return node;
643
+ };
644
+ for (let i = 0; i < nodes.length; i++)
645
+ nodes[i] = rewrite(nodes[i]);
646
+ }
647
+ function lowerNamespaces(sourceFile) {
648
+ const flat = flattenNamespaces(sourceFile.statements ?? []);
649
+ sourceFile.statements = flat.statements;
650
+ if (flat.namespaces.length > 0)
651
+ rewriteNamespaceAccess(sourceFile.statements, new Set(flat.namespaces));
652
+ }
653
+ const DISPOSE_SYMBOLS = { dispose: '__dispose', asyncDispose: '__asyncDispose' };
654
+ function getDisposeSymbolName(expr) {
655
+ if (!isPropertyAccessExpression(expr) || !isIdentifier(expr.expression) || expr.expression.text !== 'Symbol')
656
+ return undefined;
657
+ return DISPOSE_SYMBOLS[expr.name.text];
658
+ }
659
+ // [Symbol.dispose]() { … } → __dispose() { … }; x[Symbol.dispose]() → x.__dispose()
660
+ function lowerDisposeSymbols(node) {
661
+ for (const child of childNodes(node))
662
+ lowerDisposeSymbols(child);
663
+ if (node.name?.kind === 'ComputedPropertyName') {
664
+ const name = getDisposeSymbolName(node.name.expression);
665
+ if (name) {
666
+ const identifier = { kind: 'Identifier', text: name };
667
+ node.name = identifier;
668
+ }
669
+ }
670
+ if (node.kind === 'ElementAccessExpression') {
671
+ const name = getDisposeSymbolName(node.argumentExpression);
672
+ if (name) {
673
+ const identifier = { kind: 'Identifier', text: name };
674
+ node.kind = 'PropertyAccessExpression';
675
+ node.name = identifier;
676
+ delete node.argumentExpression;
677
+ }
678
+ }
679
+ }
680
+ // const Point = class { … } → class Point { … } (the variable names the class)
681
+ function lowerClassExpressions(node) {
682
+ for (const child of childNodes(node))
683
+ lowerClassExpressions(child);
684
+ if (!Array.isArray(node.statements))
685
+ return;
686
+ node.statements = node.statements.map((statement) => {
687
+ const declarations = statement.kind === 'VariableStatement' ? (statement.declarationList?.declarations ?? []) : [];
688
+ const initializer = declarations.length === 1 ? declarations[0].initializer : undefined;
689
+ if (initializer?.kind !== 'ClassExpression' || !isIdentifier(declarations[0].name))
690
+ return statement;
691
+ return { ...initializer, kind: 'ClassDeclaration', name: declarations[0].name, modifiers: statement.modifiers };
692
+ });
693
+ }
694
+ // WeakMap / WeakSet → Map / Set: same API; weakness is not observable
695
+ function lowerWeakCollections(node) {
696
+ for (const child of childNodes(node))
697
+ lowerWeakCollections(child);
698
+ const name = node.kind === 'NewExpression' ? node.expression : node.kind === 'TypeReference' ? node.typeName : undefined;
699
+ if (isIdentifier(name) && (name.text === 'WeakMap' || name.text === 'WeakSet'))
700
+ name.text = name.text.slice(4);
701
+ }
702
+ // globalThis.x (typed) is the global x itself; (globalThis as any) stays a dynamic object
703
+ function lowerGlobalThis(node) {
704
+ for (const key of Object.keys(node)) {
705
+ const child = node[key];
706
+ if (isPropertyAccessExpression(child) && isIdentifier(child.expression) && child.expression.text === 'globalThis') {
707
+ node[key] = child.name;
708
+ }
709
+ }
710
+ for (const child of childNodes(node))
711
+ lowerGlobalThis(child);
712
+ }
418
713
  function linkParents(node, parent) {
419
714
  node.parent = parent;
420
715
  for (const key of Object.keys(node)) {
@@ -494,9 +789,12 @@ const usedHelpers = new Set();
494
789
  // per-file import lists can exclude them (helpers are emitted once, in the
495
790
  // main file, which carries these imports instead).
496
791
  const helperProvidedPackages = new Set();
792
+ // Whether the file being parsed is .tsx
793
+ let currentFileIsTsx = false;
497
794
  export function transpileToNative(code, options) {
498
795
  fileResolver = options?.readFile ?? null;
499
796
  parseFunction = options?.parse ?? defaultParseFunction;
797
+ currentFileIsTsx = !!options?.tsx;
500
798
  declaredFunctions.clear();
501
799
  declaredInterfaces.clear();
502
800
  declaredTypeAliases.clear();
@@ -542,6 +840,8 @@ export function transpileToNative(code, options) {
542
840
  const drains = [];
543
841
  if (usedHelpers.has('asyncWait'))
544
842
  drains.push('__tnWaitGroup.Wait()');
843
+ if (usedHelpers.has('timerWait'))
844
+ drains.unshift('TnRunMicrotasks()');
545
845
  if (usedHelpers.has('timerWait'))
546
846
  drains.push('__tnTimers.Wait()');
547
847
  const drainCode = drains.length > 0 ? `\n\t${drains.join('\n\t')}` : '';
@@ -600,6 +900,13 @@ function visit(node, options = {}) {
600
900
  .filter((n) => !!n)
601
901
  .join(options.inline ? '' : '\n\t');
602
902
  }
903
+ else if (isJsxNode(node)) {
904
+ return visitJsx(node);
905
+ }
906
+ else if (isIdentifier(node) && node.text === 'globalThis') {
907
+ useHelper('globalThis');
908
+ return 'TnGlobalThis';
909
+ }
603
910
  else if (isIdentifier(node)) {
604
911
  if (isFieldNameIdentifier(node))
605
912
  return goFieldName(node.text);
@@ -619,6 +926,9 @@ function visit(node, options = {}) {
619
926
  const goAlias = importAliases.get(node.text);
620
927
  if (goAlias)
621
928
  return goAlias;
929
+ // A class passed as a value (make(Box)) is its constructor function
930
+ if (classNames.has(node.text) && isValuePosition(node))
931
+ return `New${getSafeName(node.text)}`;
622
932
  // Array references: reads go through the shared slice-header pointer
623
933
  if (referenceArrays.has(node.text)) {
624
934
  return `(*${getSafeName(node.text)})`;
@@ -640,6 +950,18 @@ function visit(node, options = {}) {
640
950
  else if (isTemplateExpression(node)) {
641
951
  return visitTemplateExpression(node);
642
952
  }
953
+ else if (node.kind === 'TaggedTemplateExpression' && isStringRawTag(node.tag)) {
954
+ // String.raw`a\n${x}`: the raw source text with the values interpolated
955
+ const template = node.template;
956
+ const raw = (part) => toGoStringLiteral(part.rawText ?? part.text);
957
+ if (template.kind === 'NoSubstitutionTemplateLiteral')
958
+ return raw(template);
959
+ const pieces = [raw(template.head)];
960
+ for (const span of template.templateSpans ?? []) {
961
+ pieces.push(jsStringOf(visit(span.expression), inferExpressionType(span.expression)), raw(span.literal));
962
+ }
963
+ return `(${pieces.join(' + ')})`;
964
+ }
643
965
  else if (node.kind === 'TaggedTemplateExpression') {
644
966
  // tag`x${2}` → tag(["x", ""], 2)
645
967
  const tag = visit(node.tag);
@@ -684,7 +1006,7 @@ function visit(node, options = {}) {
684
1006
  // backtracking engine — both live in the TnRegex wrapper
685
1007
  if (needsStatefulRegex(pattern, flags)) {
686
1008
  useHelper('fancyRegex');
687
- return `TnRegexCompile("${goPattern}", ${flags.includes('g') ? 'true' : 'false'})`;
1009
+ return `TnRegexCompile("${goPattern}", "${flags}", ${toGoStringLiteral(pattern)})`;
688
1010
  }
689
1011
  importedPackages.add('regexp');
690
1012
  const goFlags = jsRegexFlagsToGo(flags);
@@ -735,16 +1057,21 @@ function visit(node, options = {}) {
735
1057
  return `${visit(node.expression)}[int(${visit(node.argumentExpression)})]`;
736
1058
  }
737
1059
  else if (isPropertyAccessExpression(node)) {
1060
+ const regexProperty = visitRegexProperty(node);
1061
+ if (regexProperty)
1062
+ return regexProperty;
738
1063
  // import.meta.url — the file URL of the running program
1064
+ if (isNewTargetNode(node.expression) && node.name.text === 'name')
1065
+ return '__newTarget';
739
1066
  if (node.expression.kind === 'MetaProperty' && node.name.text === 'url') {
740
1067
  useHelper('moduleUrl');
741
1068
  return 'TnModuleURL()';
742
1069
  }
743
- if (isDynamicValue(node.expression) && node.name.text === 'length' && !isCallee(node)) {
1070
+ if (hasDynamicProperties(node.expression) && node.name.text === 'length' && !isCallee(node)) {
744
1071
  useHelper('dynamic');
745
1072
  return `TnLength(${visit(node.expression)})`;
746
1073
  }
747
- if (isDynamicValue(node.expression) && !isCallee(node) && !isProcessEnv(node.expression)) {
1074
+ if (hasDynamicProperties(node.expression) && !isCallee(node) && !isProcessEnv(node.expression)) {
748
1075
  useHelper('dynamic');
749
1076
  return `TnGet(${visit(node.expression)}, "${node.name.text}")`;
750
1077
  }
@@ -953,6 +1280,11 @@ function visit(node, options = {}) {
953
1280
  if (referenceArrays.has(node.name.text) && type.startsWith('[]')) {
954
1281
  type = variableGoTypes.get(node.name.text);
955
1282
  }
1283
+ // const v = undefined / void 0: Go cannot infer a type from nil
1284
+ if (type === ':' && inferredType === 'nil') {
1285
+ type = 'interface{}';
1286
+ setVarGoType(node.name.text, 'interface{}');
1287
+ }
956
1288
  }
957
1289
  let initializer = node.initializer ? `= ${visit(node.initializer)}` : '';
958
1290
  // Wrap non-nil values assigned to nullable primitive pointer types
@@ -981,6 +1313,13 @@ function visit(node, options = {}) {
981
1313
  return initializer ? `${name} ${initializer}` : '';
982
1314
  }
983
1315
  const declName = isIdentifier(node.name) ? getSafeName(node.name.text) : visit(node.name);
1316
+ // const f = (…) => … f(…) … / const id = setInterval(() => clearInterval(id)):
1317
+ // a Go closure can't see the variable in its own :=, so declare first, then assign
1318
+ if (type === ':' &&
1319
+ isIdentifier(node.name) &&
1320
+ containsNode(node.initializer, (n) => isIdentifier(n) && n.text === node.name.text && !isFieldNameIdentifier(n))) {
1321
+ return `var ${declName} ${inferExpressionType(node.initializer)}\n\t\t${declName} ${initializer}`;
1322
+ }
984
1323
  return `${type === ':' ? '' : 'var '}${declName} ${type}${type === ':' ? '' : ' '}${initializer}`;
985
1324
  }
986
1325
  else if (isCallExpression(node)) {
@@ -994,6 +1333,9 @@ function visit(node, options = {}) {
994
1333
  return visitNullablePrimitiveOptionalCall(node, visit(node.expression.expression), receiverType);
995
1334
  }
996
1335
  }
1336
+ const recordCall = visitRecordFunctionCall(node);
1337
+ if (recordCall)
1338
+ return recordCall;
997
1339
  const dynamicCall = visitDynamicMethodCall(node);
998
1340
  if (dynamicCall)
999
1341
  return dynamicCall;
@@ -1020,8 +1362,8 @@ function visit(node, options = {}) {
1020
1362
  const callback = visit((node.arguments ?? [])[0]);
1021
1363
  const delayNode = (node.arguments ?? [])[1];
1022
1364
  const delay = isNumericLiteral(delayNode) ? delayNode.text : visit(delayNode);
1023
- // Registered on the wait group so the process lives until it fires
1024
- return `__tnTimers.Add(1)\n\t\ttime.AfterFunc(${delay} * time.Millisecond, func() {\n\t\t\tdefer __tnTimers.Done()\n\t\t\t${callback.trim()}()\n\t\t})`;
1365
+ // Registered on the wait group so the process lives until it fires (or is cleared)
1366
+ return `TnSetTimeout(${delay ?? '0'}, func() { (${callback.trim()})() })`;
1025
1367
  }
1026
1368
  // p.then(cb) on a channel-based Promise: wait for the value, run the callback
1027
1369
  if (isPropertyAccessExpression(node.expression) &&
@@ -1075,6 +1417,15 @@ function visit(node, options = {}) {
1075
1417
  if (arrayHigherOrderCall) {
1076
1418
  return arrayHigherOrderCall;
1077
1419
  }
1420
+ const promiseCall = visitPromiseCall(node);
1421
+ if (promiseCall)
1422
+ return promiseCall;
1423
+ const functionCall = visitFunctionCallApply(node);
1424
+ if (functionCall)
1425
+ return functionCall;
1426
+ const objectCall = visitObjectCall(node);
1427
+ if (objectCall)
1428
+ return objectCall;
1078
1429
  const caller = visit(node.expression);
1079
1430
  const safeCaller = getSafeName(caller);
1080
1431
  const typeArgs = getTypeArguments((node.typeArguments ?? []));
@@ -1086,6 +1437,7 @@ function visit(node, options = {}) {
1086
1437
  if (isPropertyAccessExpression(node.expression)) {
1087
1438
  objectType = resolveExpressionType(node.expression.expression);
1088
1439
  }
1440
+ currentCallArgTypes = (node.arguments ?? []).map((a) => inferExpressionType(a));
1089
1441
  currentReceiverGoType = isPropertyAccessExpression(node.expression)
1090
1442
  ? inferExpressionType(node.expression.expression)
1091
1443
  : undefined;
@@ -1106,6 +1458,15 @@ function visit(node, options = {}) {
1106
1458
  else if (isPostfixUnaryExpression(node)) {
1107
1459
  return visitIncrementOrDecrement(node, false);
1108
1460
  }
1461
+ else if (node.kind === 'VoidExpression') {
1462
+ // void expr evaluates expr and gives undefined
1463
+ if (isNumericLiteral(node.expression))
1464
+ return 'nil';
1465
+ return `func() interface{} { _ = ${visit(node.expression)}; return nil }()`;
1466
+ }
1467
+ else if (node.kind === 'SatisfiesExpression') {
1468
+ return visit(node.expression);
1469
+ }
1109
1470
  else if (node.kind === 'DeleteExpression') {
1110
1471
  return visitDelete(node);
1111
1472
  }
@@ -1254,6 +1615,16 @@ function visit(node, options = {}) {
1254
1615
  return op === '==' ? eq : `!(${eq})`;
1255
1616
  }
1256
1617
  }
1618
+ // new.target === C → the target class name is C's
1619
+ if ((op === '==' || op === '!=') && (isNewTargetNode(node.left) || isNewTargetNode(node.right))) {
1620
+ const other = isNewTargetNode(node.left) ? node.right : node.left;
1621
+ return `__newTarget ${op} ${isIdentifier(other) ? toGoStringLiteral(other.text) : '""'}`;
1622
+ }
1623
+ // obj[key] = v on any
1624
+ if (op === '=' && isElementAccessExpression(node.left) && isDynamicValue(node.left.expression)) {
1625
+ useHelper('dynamic');
1626
+ return `TnSetIndex(${visit(node.left.expression)}, ${visit(node.left.argumentExpression)}, ${visit(node.right)})`;
1627
+ }
1257
1628
  if (op === '=' && isPropertyAccessExpression(node.left) && isDynamicValue(node.left.expression)) {
1258
1629
  useHelper('dynamic');
1259
1630
  return `TnSet(${visit(node.left.expression)}, "${node.left.name.text}", ${visit(node.right)})`;
@@ -1328,6 +1699,10 @@ function visit(node, options = {}) {
1328
1699
  return '';
1329
1700
  }
1330
1701
  else if (isAwaitExpression(node)) {
1702
+ if (inferExpressionType(node.expression)?.startsWith('chan ')) {
1703
+ useHelper('promise');
1704
+ return `TnAwait(${visit(node.expression)})`;
1705
+ }
1331
1706
  return `<-${visit(node.expression)}`;
1332
1707
  }
1333
1708
  else if (isVariableDeclarationList(node)) {
@@ -1342,11 +1717,15 @@ function visit(node, options = {}) {
1342
1717
  asyncFunctionNames.has(exprStmt.expression.text) &&
1343
1718
  !emittingModuleFile) {
1344
1719
  useHelper('asyncWait');
1345
- return `__tnWaitGroup.Add(1)\n\t\tgo func() {\n\t\t\tdefer __tnWaitGroup.Done()\n\t\t\t<-${getSafeName(exprStmt.expression.text)}()\n\t\t}();\n\t`;
1720
+ useHelper('promise');
1721
+ return `__tnWaitGroup.Add(1)\n\t\tgo func() {\n\t\t\tdefer __tnWaitGroup.Done()\n\t\t\tTnAwait(${getSafeName(exprStmt.expression.text)}())\n\t\t}();\n\t`;
1346
1722
  }
1347
1723
  return visit(node.expression) + (options.inline ? '' : ';\n\t');
1348
1724
  }
1349
1725
  else if (isForStatement(node)) {
1726
+ const multi = visitMultiForHeader(node);
1727
+ if (multi)
1728
+ return multi;
1350
1729
  return `for ${visit(node.initializer, { inline: true })}; ${node.condition ? toGoCondition(node.condition) : ''}; ${visit(node.incrementor, { inline: true })}${visitLoopBody(node.statement)}`;
1351
1730
  }
1352
1731
  else if (isForInStatement(node)) {
@@ -1489,6 +1868,9 @@ function visit(node, options = {}) {
1489
1868
  return `return ${value}` + terminator;
1490
1869
  }
1491
1870
  else if (isFunctionDeclaration(node) || isFunctionExpression(node)) {
1871
+ // overload signatures have no body: only the implementation is emitted
1872
+ if (!node.body)
1873
+ return '';
1492
1874
  if (options.addFunctionOutside) {
1493
1875
  outsideNodes.push(node);
1494
1876
  return '';
@@ -1503,7 +1885,7 @@ function visit(node, options = {}) {
1503
1885
  if (options.isOutside) {
1504
1886
  // async functions run their body in a goroutine sending over a channel
1505
1887
  const isAsync = (node.modifiers ?? []).some((m) => m.kind === 'AsyncKeyword');
1506
- if (isAsync && node.name) {
1888
+ if (isAsync && node.name && !node.asteriskToken) {
1507
1889
  return emitChannelFunction(node, true);
1508
1890
  }
1509
1891
  if (node.asteriskToken && node.name) {
@@ -1535,7 +1917,7 @@ function visit(node, options = {}) {
1535
1917
  }
1536
1918
  if (!isBlock(node.body)) {
1537
1919
  // void-style calls (console.*, assert) emit statements, not values
1538
- if (isVoidStatementCall(node.body)) {
1920
+ if (isVoidStatementCall(node.body) || !inferredRetType) {
1539
1921
  return `func(${parameterInfo.signature})${returnType} { ${visit(node.body)}; }`;
1540
1922
  }
1541
1923
  return `func(${parameterInfo.signature})${returnType} { return ${visit(node.body)}; }`;
@@ -1547,6 +1929,10 @@ function visit(node, options = {}) {
1547
1929
  else if (node.kind === 'ThisKeyword') {
1548
1930
  if (thisOverrideName && getEnclosingFunction(node) === thisOverrideFn)
1549
1931
  return thisOverrideName;
1932
+ if (isThisOfPlainFunction(node)) {
1933
+ useHelper('decorators');
1934
+ return 'TnThis';
1935
+ }
1550
1936
  return 'self';
1551
1937
  }
1552
1938
  else if (node.kind === 'PrivateIdentifier') {
@@ -1558,6 +1944,8 @@ function visit(node, options = {}) {
1558
1944
  enumNames.add(enumName);
1559
1945
  enumBaseTypes.set(enumName, getEnumBaseType(node));
1560
1946
  if (options.addFunctionOutside) {
1947
+ if (enumHasComputedMember(node))
1948
+ return visitEnumDeclaration(node, true);
1561
1949
  outsideNodes.push(node);
1562
1950
  return '';
1563
1951
  }
@@ -1645,7 +2033,7 @@ function visit(node, options = {}) {
1645
2033
  return `type ${name}${typeParams} struct {\n${fields.join('\n')}\n}`;
1646
2034
  }
1647
2035
  const members = [...extendedInterfaces.map((e) => `\t${e}`), ...methods];
1648
- return `type ${name}${typeParams} interface {\n${members.join('\n')}\n}`;
2036
+ return `type ${name}${typeParams} interface {\n${members.join('\n')}\n}${extendedInterfaces.length === 0 ? emitInterfaceAdapter(node) : ''}`;
1649
2037
  }
1650
2038
  else if (isClassDeclaration(node)) {
1651
2039
  if (options.addFunctionOutside) {
@@ -1729,7 +2117,7 @@ function visit(node, options = {}) {
1729
2117
  }
1730
2118
  classPropertyTypes.set(className, properties);
1731
2119
  classMethodReturnTypes.set(className, methods);
1732
- return '';
2120
+ return visitClassDecorators(node);
1733
2121
  }
1734
2122
  const name = visit(node.name);
1735
2123
  const typeParams = getTypeParameters(node.typeParameters);
@@ -1785,7 +2173,7 @@ function visit(node, options = {}) {
1785
2173
  const args = (s.expression.arguments ?? []).map((a) => visit(a));
1786
2174
  if (extendsError)
1787
2175
  return `self.message = ${args[0] ?? '""'}\n\t\t`;
1788
- return `self.${goFieldName(parentClass)} = *New${parentClass}(${args.join(', ')})\n\t\t`;
2176
+ return `self.${goFieldName(parentClass)} = *${parentConstructorCall(parentClass, args)}\n\t\t`;
1789
2177
  }
1790
2178
  return visit(s);
1791
2179
  })
@@ -1794,17 +2182,19 @@ function visit(node, options = {}) {
1794
2182
  .map((p) => `self.${goFieldName(p.name.text)} = ${visit(p.name)}\n\t\t`)
1795
2183
  .join('');
1796
2184
  const parentInit = parentClass && !extendsError && !superCalledInCtor
1797
- ? `self.${goFieldName(parentClass)} = *New${parentClass}()\n\t\t`
2185
+ ? `self.${goFieldName(parentClass)} = *${parentConstructorCall(parentClass, [])}\n\t\t`
1798
2186
  : '';
1799
- result += `func New${name}${typeParams}(${ctorParameterInfo.signature}) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${parentInit}${fieldInitializers(node)}${parameterAssignments}${bodyStatements}return self;\n\t}\n\n`;
2187
+ const ctorBody = `{\n\t\tself := &${name}${typeParamNames}{}\n\t\t${ctorParameterInfo.prefixBlockContent}${parentInit}${fieldInitializers(node)}${parameterAssignments}${bodyStatements}return self;\n\t}`;
2188
+ result += emitConstructor(name, typeParams, typeParamNames, ctorParameterInfo.signature, ctorBody);
1800
2189
  }
1801
2190
  else if (extendsError) {
1802
2191
  // class X extends Error {} — the constructor takes the message
1803
2192
  result += `func New${name}${typeParams}(message string) *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\tself.message = message\n\t\t${fieldInitializers(node)}return self\n\t}\n\n`;
1804
2193
  }
1805
2194
  else {
1806
- const parentInit = parentClass && !extendsError ? `self.${goFieldName(parentClass)} = *New${parentClass}()\n\t\t` : '';
1807
- result += `func New${name}${typeParams}() *${name}${typeParamNames} {\n\t\tself := &${name}${typeParamNames}{}\n\t\t${parentInit}${fieldInitializers(node)}return self\n\t}\n\n`;
2195
+ const parentInit = parentClass && !extendsError ? `self.${goFieldName(parentClass)} = *${parentConstructorCall(parentClass, [])}\n\t\t` : '';
2196
+ const ctorBody = `{\n\t\tself := &${name}${typeParamNames}{}\n\t\t${parentInit}${fieldInitializers(node)}return self\n\t}`;
2197
+ result += emitConstructor(name, typeParams, typeParamNames, '', ctorBody);
1808
2198
  }
1809
2199
  // Inherited concrete methods are copied onto the subclass: Go embedding
1810
2200
  // promotes the parent's receiver, which would break `this` dispatch
@@ -1856,6 +2246,10 @@ function visit(node, options = {}) {
1856
2246
  if (!methodName || ownMethodNames.has(methodName))
1857
2247
  continue;
1858
2248
  ownMethodNames.add(methodName);
2249
+ if (getDecorators(member).length > 0) {
2250
+ copiedMethods.push(emitDecoratedMethod(name, typeParamNames, member, visit(ancestor.name)));
2251
+ continue;
2252
+ }
1859
2253
  const methodParameterInfo = getFunctionParametersInfo(member.parameters ?? []);
1860
2254
  const returnType = member.type ? ` ${getType(member.type)}` : '';
1861
2255
  copiedMethods.push(`func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n`);
@@ -1902,7 +2296,10 @@ function visit(node, options = {}) {
1902
2296
  continue; // abstract method — implemented by subclasses
1903
2297
  if (isAbstractClass && !isStatic)
1904
2298
  continue; // only reachable via subclasses
1905
- if (isStatic) {
2299
+ if (getDecorators(member).length > 0) {
2300
+ result += emitDecoratedMethod(name, typeParamNames, member, name);
2301
+ }
2302
+ else if (isStatic) {
1906
2303
  // Static methods become package-level functions named ClassName_methodName
1907
2304
  result += `func ${name}_${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
1908
2305
  }
@@ -1936,6 +2333,12 @@ function visit(node, options = {}) {
1936
2333
  result += `var ${name}_${fieldName} ${fieldType}${initializer}\n\n`;
1937
2334
  }
1938
2335
  }
2336
+ // static { … } blocks run once at program start, after the static fields
2337
+ for (const member of node.members ?? []) {
2338
+ if (member.kind === 'ClassStaticBlockDeclaration' && member.body) {
2339
+ result += `func init() ${visit(member.body).trimEnd()}\n\n`;
2340
+ }
2341
+ }
1939
2342
  return result.trim();
1940
2343
  }
1941
2344
  else if (isNewExpression(node)) {
@@ -1983,6 +2386,10 @@ function visit(node, options = {}) {
1983
2386
  }
1984
2387
  const typeArgs = getTypeArguments((node.typeArguments ?? []));
1985
2388
  const args = node.arguments ? (node.arguments ?? []).map((a) => visit(a)) : [];
2389
+ // new F(…) where F holds a constructor function (construct signature)
2390
+ if (!classNames.has(className) && inferExpressionType(node.expression)?.startsWith('func(')) {
2391
+ return `${className}(${args.join(', ')})`;
2392
+ }
1986
2393
  return `New${className}${typeArgs}(${args.join(', ')})`;
1987
2394
  }
1988
2395
  else if (isObjectLiteralExpression(node)) {
@@ -2018,8 +2425,14 @@ function visit(node, options = {}) {
2018
2425
  if (!typeName || typeName === 'interface{}') {
2019
2426
  return visitAnonymousStructLiteral(node);
2020
2427
  }
2021
- // Objects are pointers: the struct type is typeName without its *
2022
- const structName = typeName.replace(/^\*/, '');
2428
+ // Objects are pointers: the struct type is typeName without its *; generic
2429
+ // types are instantiated with the contextual type arguments (Box<number> → Box[float64])
2430
+ const typeArgs = isTypeReferenceNode(contextualType) && contextualType.typeName.text === typeName.replace(/^\*/, '')
2431
+ ? getTypeArguments(contextualType.typeArguments)
2432
+ : '';
2433
+ const structName = typeName.replace(/^\*/, '') + (typeName.endsWith(']') ? '' : typeArgs);
2434
+ if (isMethodInterfaceName(typeName.replace(/^\*/, '')))
2435
+ return visitInterfaceLiteral(node, typeName.replace(/^\*/, ''), typeArgs);
2023
2436
  // { ...base, x: 1 } → a copy of *base with fields set
2024
2437
  if ((node.properties ?? []).some((p) => p.kind === 'SpreadAssignment')) {
2025
2438
  const steps = (node.properties ?? []).map((p) => {
@@ -2035,7 +2448,7 @@ function visit(node, options = {}) {
2035
2448
  });
2036
2449
  return `func() *${structName} { var __obj ${structName}; ${steps.join('; ')}; return &__obj }()`;
2037
2450
  }
2038
- const fieldTypes = interfacePropertyTypes.get(structName) ?? classPropertyTypes.get(structName);
2451
+ const fieldTypes = typeArgs ? undefined : (interfacePropertyTypes.get(structName) ?? classPropertyTypes.get(structName));
2039
2452
  const properties = (node.properties ?? [])
2040
2453
  .map((p) => {
2041
2454
  if (isPropertyAssignment(p)) {
@@ -2084,6 +2497,8 @@ function visit(node, options = {}) {
2084
2497
  // Visits a value going into a slot of Go type `goType`; non-nil values for
2085
2498
  // nullable primitives (*string/*float64/*bool) are boxed into pointers
2086
2499
  function toGoValueOfType(expr, goType) {
2500
+ if (goType === 'string' && inferExpressionType(expr) === 'TnJsxNode')
2501
+ return `string(${visit(expr)})`;
2087
2502
  if (goType &&
2088
2503
  ((isObjectLiteralExpression(expr) && (expr.properties ?? []).length === 0 && goType.startsWith('*TnMap[')) ||
2089
2504
  (isArrayLiteralExpression(expr) && (expr.elements ?? []).length === 0 && goType.startsWith('[]')))) {
@@ -2137,7 +2552,7 @@ function toGoValueOfType(expr, goType) {
2137
2552
  return code;
2138
2553
  if (isBinaryExpression(expr) &&
2139
2554
  expr.operatorToken.kind === 'QuestionQuestionToken' &&
2140
- inferExpressionType(expr.left)?.startsWith('*')) {
2555
+ getNullishEmitType(expr) === goType) {
2141
2556
  return code;
2142
2557
  }
2143
2558
  if (inferExpressionType(expr) !== goType.slice(1))
@@ -2270,7 +2685,8 @@ function isNarrowableReference(node) {
2270
2685
  return false;
2271
2686
  if (isPropertyAccessExpression(parent) && parent.name === node)
2272
2687
  return false;
2273
- if (parent.name === node)
2688
+ // { x } reads the variable x
2689
+ if (parent.name === node && !isShorthandPropertyAssignment(parent))
2274
2690
  return false;
2275
2691
  if (isBinaryExpression(parent) && parent.left === node) {
2276
2692
  const op = parent.operatorToken.kind;
@@ -2296,7 +2712,12 @@ function alwaysExits(statement) {
2296
2712
  function visitBlockStatements(statements) {
2297
2713
  const added = [];
2298
2714
  const parts = [];
2299
- for (const statement of statements) {
2715
+ for (let index = 0; index < statements.length; index++) {
2716
+ const statement = statements[index];
2717
+ if (isUsingDeclaration(statement)) {
2718
+ parts.push(visitUsingDeclaration(statement, statements.slice(index + 1)));
2719
+ break;
2720
+ }
2300
2721
  parts.push(visit(statement));
2301
2722
  if (isIfStatement(statement) && !statement.elseStatement && alwaysExits(statement.thenStatement)) {
2302
2723
  for (const name of getNarrowedNames(statement.expression, false)) {
@@ -2311,6 +2732,39 @@ function visitBlockStatements(statements) {
2311
2732
  narrowedVariables.delete(name);
2312
2733
  return parts.join('\t');
2313
2734
  }
2735
+ function isUsingDeclaration(statement) {
2736
+ const kind = statement.kind === 'VariableStatement' ? statement.declarationList?.declarationKind : undefined;
2737
+ return kind === 'using' || kind === 'await using';
2738
+ }
2739
+ // using x = r; …rest → x := r; defer dispose(x); …rest. In a function body Go's
2740
+ // defer is the block end; nested blocks run the rest in a closure so disposal
2741
+ // happens when the block ends (return/break/continue there keep the defer form)
2742
+ function visitUsingDeclaration(statement, rest) {
2743
+ const isAwait = statement.declarationList.declarationKind === 'await using';
2744
+ const disposals = (statement.declarationList.declarations ?? [])
2745
+ .filter((d) => isIdentifier(d.name))
2746
+ .map((d) => {
2747
+ const name = visit(d.name);
2748
+ const goType = inferExpressionType(d.initializer) ?? '';
2749
+ const className = goType.replace(/^\*/, '').replace(/\[.*$/, '');
2750
+ const classNode = declaredClasses.get(className);
2751
+ const hasAsync = (classNode?.members ?? []).some((m) => m.name?.text === '__asyncDispose');
2752
+ if (isAwait && hasAsync)
2753
+ useHelper('promise');
2754
+ const call = isAwait && hasAsync ? `TnAwait(${name}.__asyncDispose())` : `${name}.__dispose()`;
2755
+ return `defer func() { if ${name} != nil { ${call} } }()`;
2756
+ })
2757
+ .reverse();
2758
+ const declaration = { ...statement, declarationList: { ...statement.declarationList, declarationKind: 'const' } };
2759
+ const head = `${visit(declaration)}${disposals.join('\n\t\t')}\n\t\t`;
2760
+ const block = statement.parent;
2761
+ const isFunctionBody = !!block?.parent && block.parent.body === block;
2762
+ const restBlock = { kind: 'Block', statements: rest };
2763
+ const exits = containsNode(restBlock, (n) => n.kind === 'ReturnStatement' || n.kind === 'BreakStatement' || n.kind === 'ContinueStatement');
2764
+ if (isFunctionBody || exits)
2765
+ return head + visitBlockStatements(rest);
2766
+ return `func() {\n\t\t${head}${visitBlockStatements(rest)}}()\n\t`;
2767
+ }
2314
2768
  function visitDynamicArithmetic(node, op) {
2315
2769
  const leftDynamic = isDynamicValue(node.left);
2316
2770
  const rightDynamic = isDynamicValue(node.right);
@@ -2515,6 +2969,66 @@ function visitSpreadCallArguments(node) {
2515
2969
  return result;
2516
2970
  }
2517
2971
  // self.field = initializer for each instance field that has one
2972
+ // Whether a class's constructor chain (its own or an ancestor's) reads new.target
2973
+ function usesNewTarget(className) {
2974
+ let current = className;
2975
+ for (let depth = 0; depth < 50; depth++) {
2976
+ const classNode = declaredClasses.get(current);
2977
+ if (!classNode)
2978
+ return false;
2979
+ const ctor = (classNode.members ?? []).find((m) => isConstructorDeclaration(m));
2980
+ if (ctor?.body && containsNode(ctor.body, (n) => isNewTargetNode(n)))
2981
+ return true;
2982
+ const extendsClause = (classNode.heritageClauses ?? []).find((c) => c.token === 'ExtendsKeyword');
2983
+ if (!extendsClause)
2984
+ return false;
2985
+ current = visit(extendsClause.types[0].expression);
2986
+ }
2987
+ return false;
2988
+ }
2989
+ function isNewTargetNode(node) {
2990
+ return node?.kind === 'MetaProperty' && node.name?.text === 'target';
2991
+ }
2992
+ // The parent part of a subclass constructor: new.target-aware chains pass the target on
2993
+ function parentConstructorCall(parentClass, args) {
2994
+ if (!usesNewTarget(parentClass))
2995
+ return `New${parentClass}(${args.join(', ')})`;
2996
+ return `new${parentClass}As(${['__newTarget', ...args].join(', ')})`;
2997
+ }
2998
+ // Splits a Go parameter list at top-level commas
2999
+ function splitGoParameters(signature) {
3000
+ const parts = [];
3001
+ let depth = 0;
3002
+ let current = '';
3003
+ for (const ch of signature) {
3004
+ if (ch === '(' || ch === '[' || ch === '{')
3005
+ depth++;
3006
+ if (ch === ')' || ch === ']' || ch === '}')
3007
+ depth--;
3008
+ if (ch === ',' && depth === 0) {
3009
+ parts.push(current.trim());
3010
+ current = '';
3011
+ }
3012
+ else
3013
+ current += ch;
3014
+ }
3015
+ if (current.trim())
3016
+ parts.push(current.trim());
3017
+ return parts;
3018
+ }
3019
+ // NewX(…) — or, for new.target-aware classes, newXAs(__newTarget string, …) plus a
3020
+ // NewX(…) that calls it with "X"
3021
+ function emitConstructor(name, typeParams, typeParamNames, signature, body) {
3022
+ if (!usesNewTarget(name))
3023
+ return `func New${name}${typeParams}(${signature}) *${name}${typeParamNames} ${body}\n\n`;
3024
+ const forward = splitGoParameters(signature).map((p) => {
3025
+ const [paramName, paramType] = [p.slice(0, p.indexOf(' ')), p.slice(p.indexOf(' ') + 1)];
3026
+ return paramType.startsWith('...') ? `${paramName}...` : paramName;
3027
+ });
3028
+ const internal = ['__newTarget string', signature].filter((part) => part).join(', ');
3029
+ return (`func new${name}As${typeParams}(${internal}) *${name}${typeParamNames} ${body}\n\n` +
3030
+ `func New${name}${typeParams}(${signature}) *${name}${typeParamNames} {\n\t\treturn new${name}As${typeParamNames}(${[toGoStringLiteral(name), ...forward].join(', ')})\n\t}\n\n`);
3031
+ }
2518
3032
  function fieldInitializers(classNode) {
2519
3033
  return (classNode.members ?? [])
2520
3034
  .filter((m) => isPropertyDeclaration(m) &&
@@ -2533,6 +3047,71 @@ function getPropertyDeclarationType(member) {
2533
3047
  return getOptionalNodeType(member.type, !!member.questionToken);
2534
3048
  return toFieldGoType(inferExpressionType(member.initializer));
2535
3049
  }
3050
+ // Sorts in place and returns the array; without a comparator, compares as strings (like JS)
3051
+ function sortArray(obj, args) {
3052
+ importedPackages.add('sort');
3053
+ const arrayType = currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType : '[]interface{}';
3054
+ let less = `(${args[0]})(__s[i], __s[j]) < 0`;
3055
+ if (!args[0]) {
3056
+ importedPackages.add('fmt');
3057
+ less = `fmt.Sprintf("%v", __s[i]) < fmt.Sprintf("%v", __s[j])`;
3058
+ }
3059
+ return `func() ${arrayType} { __s := ${obj}; sort.SliceStable(__s, func(i, j int) bool { return ${less} }); return __s }()`;
3060
+ }
3061
+ function mathCall(fn, args) {
3062
+ importedPackages.add('math');
3063
+ return `math.${fn}(${args.join(', ')})`;
3064
+ }
3065
+ // Go types of the arguments of the call being emitted (for call handlers)
3066
+ let currentCallArgTypes = [];
3067
+ // for headers with several declarations / updates: Go's parallel forms
3068
+ // (i, j := 0, 10 and i, j = i+1, j-1)
3069
+ function visitMultiForHeader(node) {
3070
+ const declarations = isVariableDeclarationList(node.initializer) ? (node.initializer.declarations ?? []) : [];
3071
+ const updates = [];
3072
+ const collect = (expr) => {
3073
+ if (!expr)
3074
+ return;
3075
+ if (isBinaryExpression(expr) && expr.operatorToken.kind === 'CommaToken') {
3076
+ collect(expr.left);
3077
+ collect(expr.right);
3078
+ }
3079
+ else
3080
+ updates.push(expr);
3081
+ };
3082
+ collect(node.incrementor);
3083
+ if (declarations.length < 2 && updates.length < 2)
3084
+ return undefined;
3085
+ let init = visit(node.initializer, { inline: true });
3086
+ if (declarations.length >= 2) {
3087
+ for (const d of declarations) {
3088
+ if (isIdentifier(d.name) && d.initializer)
3089
+ registerLocalVariable(d.name.text, inferExpressionType(d.initializer));
3090
+ }
3091
+ init = `${declarations.map((d) => visit(d.name)).join(', ')} := ${declarations.map((d) => (d.initializer ? visit(d.initializer) : 'nil')).join(', ')}`;
3092
+ }
3093
+ let post = visit(node.incrementor, { inline: true });
3094
+ if (updates.length >= 2) {
3095
+ const targets = [];
3096
+ const values = [];
3097
+ for (const u of updates) {
3098
+ if ((isPrefixUnaryExpression(u) || isPostfixUnaryExpression(u)) && isIncrementOrDecrement(u)) {
3099
+ const target = visit(u.operand);
3100
+ targets.push(target);
3101
+ values.push(`${target} ${u.operator === 'PlusPlusToken' ? '+' : '-'} 1`);
3102
+ }
3103
+ else if (isBinaryExpression(u) && u.operatorToken.kind === 'EqualsToken') {
3104
+ targets.push(visit(u.left));
3105
+ values.push(visit(u.right));
3106
+ }
3107
+ else
3108
+ return undefined;
3109
+ }
3110
+ post = `${targets.join(', ')} = ${values.join(', ')}`;
3111
+ }
3112
+ const condition = node.condition ? toGoCondition(node.condition) : '';
3113
+ return `for ${init}; ${condition}; ${post}${visitLoopBody(node.statement)}`;
3114
+ }
2536
3115
  function isIncrementOrDecrement(node) {
2537
3116
  return node.operator === 'PlusPlusToken' || node.operator === 'MinusMinusToken';
2538
3117
  }
@@ -2598,6 +3177,15 @@ function getDictionaryValueType(node) {
2598
3177
  function isDynamicObjectLiteral(node) {
2599
3178
  if (isAnyContext(getContextualTypeNode(node)))
2600
3179
  return true;
3180
+ // { name: { … } } nested in a dynamic object (with no declared member type) is dynamic too
3181
+ const parent = node.parent;
3182
+ if (isPropertyAssignment(parent) &&
3183
+ parent.initializer === node &&
3184
+ isObjectLiteralExpression(parent.parent) &&
3185
+ !getContextualTypeNode(node) &&
3186
+ isDynamicObjectLiteral(parent.parent)) {
3187
+ return true;
3188
+ }
2601
3189
  return (node.properties ?? []).some((p) => p.kind === 'SpreadAssignment' && isDynamicValue(p.expression));
2602
3190
  }
2603
3191
  function getObjectLiteralGoType(node) {
@@ -2638,6 +3226,14 @@ function getObjectLiteralGoType(node) {
2638
3226
  function isDynamicValue(expr) {
2639
3227
  return inferExpressionType(expr) === 'interface{}';
2640
3228
  }
3229
+ // A value whose type is a type parameter (a: T in f<T>): its properties are read dynamically
3230
+ function isTypeParameterValue(expr) {
3231
+ const type = inferExpressionType(expr);
3232
+ return !!type && isUnboundGoType(type) && isTypeParameterInScope(type, expr);
3233
+ }
3234
+ function hasDynamicProperties(expr) {
3235
+ return isDynamicValue(expr) || isTypeParameterValue(expr);
3236
+ }
2641
3237
  // Whether an expression is written to (x = …, x += …, x++)
2642
3238
  function isAssignmentTarget(node) {
2643
3239
  const parent = node.parent;
@@ -2680,6 +3276,30 @@ function isAnyContext(typeNode) {
2680
3276
  }
2681
3277
  return false;
2682
3278
  }
3279
+ const JS_MAP_METHODS = new Set(['get', 'set', 'has', 'delete', 'clear', 'keys', 'values', 'entries', 'forEach']);
3280
+ // rec.fn(args) where rec is a Record/dictionary (an ordered map) holding functions.
3281
+ // Names of JS Map methods (get, set, …) are Map methods unless rec is typed as a Record.
3282
+ function getRecordFunctionCall(node) {
3283
+ if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression))
3284
+ return undefined;
3285
+ const receiver = node.expression.expression;
3286
+ const mapType = inferExpressionType(receiver);
3287
+ if (!mapType?.startsWith('*TnMap['))
3288
+ return undefined;
3289
+ if (!extractMapValueType(mapType).startsWith('func('))
3290
+ return undefined;
3291
+ if (JS_MAP_METHODS.has(node.expression.name.text) && !isRecordTypeNode(resolveTypeNode(getExpressionTypeNode(receiver)))) {
3292
+ return undefined;
3293
+ }
3294
+ return { receiver, mapType };
3295
+ }
3296
+ function visitRecordFunctionCall(node) {
3297
+ const call = getRecordFunctionCall(node);
3298
+ if (!call)
3299
+ return undefined;
3300
+ const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
3301
+ return `${visit(call.receiver)}.Get(${toGoStringLiteral(node.expression.name.text)})(${args})`;
3302
+ }
2683
3303
  function isRecordTypeNode(typeNode) {
2684
3304
  return (isTypeReferenceNode(typeNode) &&
2685
3305
  isIdentifier(typeNode.typeName) &&
@@ -2981,8 +3601,9 @@ function getContextualTypeNode(node) {
2981
3601
  return getExpressionTypeNode(parent.left);
2982
3602
  }
2983
3603
  }
2984
- if (isAsExpression(parent) || isTypeAssertionExpression(parent))
3604
+ if (isAsExpression(parent) || isTypeAssertionExpression(parent) || parent.kind === 'SatisfiesExpression') {
2985
3605
  return parent.type;
3606
+ }
2986
3607
  if ((isVariableDeclaration(parent) || isPropertyDeclaration(parent) || parent.kind === 'Parameter') &&
2987
3608
  parent.initializer === node) {
2988
3609
  return parent.type;
@@ -3012,6 +3633,14 @@ function getContextualTypeNode(node) {
3012
3633
  }
3013
3634
  return undefined;
3014
3635
  }
3636
+ // A type parameter of an enclosing function or class (T inside function f<T>)
3637
+ function isTypeParameterInScope(name, node) {
3638
+ for (let n = node; n; n = n.parent) {
3639
+ if ((n.typeParameters ?? []).some((tp) => tp.name?.text === name))
3640
+ return true;
3641
+ }
3642
+ return false;
3643
+ }
3015
3644
  // Element type of an array literal: from its contextual type (declared
3016
3645
  // variable/parameter/return type), else inferred from its first typed element
3017
3646
  function getArrayLiteralElementType(node) {
@@ -3019,7 +3648,7 @@ function getArrayLiteralElementType(node) {
3019
3648
  if (isArrayTypeNode(contextual)) {
3020
3649
  const elem = getType(contextual.elementType);
3021
3650
  // An unbound type parameter (call-site []T) resolves from the elements
3022
- if (!isUnboundGoType(elem))
3651
+ if (!isUnboundGoType(elem) || isTypeParameterInScope(elem, node))
3023
3652
  return elem;
3024
3653
  }
3025
3654
  if (contextual?.kind === 'TupleType')
@@ -3032,6 +3661,7 @@ function getArrayLiteralElementType(node) {
3032
3661
  if (!isUnboundGoType(elem))
3033
3662
  return elem;
3034
3663
  }
3664
+ let firstElementType;
3035
3665
  for (const element of node.elements ?? []) {
3036
3666
  if (isSpreadElement(element)) {
3037
3667
  const spreadType = inferExpressionType(element.expression);
@@ -3044,11 +3674,16 @@ function getArrayLiteralElementType(node) {
3044
3674
  }
3045
3675
  continue;
3046
3676
  }
3677
+ // mixed element types ([['a', 1]]) make an interface{} array
3047
3678
  const elementType = inferExpressionType(element);
3048
- if (elementType && elementType !== 'nil' && !isUnboundGoType(elementType))
3049
- return elementType;
3679
+ if (elementType && elementType !== 'nil' && !isUnboundGoType(elementType)) {
3680
+ if (firstElementType && firstElementType !== elementType)
3681
+ return 'interface{}';
3682
+ if (!firstElementType)
3683
+ firstElementType = elementType;
3684
+ }
3050
3685
  }
3051
- return 'interface{}';
3686
+ return firstElementType ?? 'interface{}';
3052
3687
  }
3053
3688
  // A Go type that is a generic type parameter of the enclosing function (T)
3054
3689
  // rather than a concrete type: unresolvable at the call site
@@ -3111,6 +3746,13 @@ function getTupleElementType(tuple) {
3111
3746
  function getTypeText(typeNode) {
3112
3747
  if (!typeNode)
3113
3748
  return ':';
3749
+ // Partial<T> etc. construct T
3750
+ if (isTypeReferenceNode(typeNode) &&
3751
+ isIdentifier(typeNode.typeName) &&
3752
+ UTILITY_TYPES.has(typeNode.typeName.text) &&
3753
+ typeNode.typeArguments?.length) {
3754
+ return getTypeText(typeNode.typeArguments[0]);
3755
+ }
3114
3756
  if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
3115
3757
  return typeNode.typeName.text;
3116
3758
  }
@@ -3187,8 +3829,10 @@ function visitTemplateExpression(node) {
3187
3829
  function isVoidStatementCall(expr) {
3188
3830
  if (!isCallExpression(expr))
3189
3831
  return false;
3190
- if (isIdentifier(expr.expression))
3191
- return expr.expression.text === 'assert';
3832
+ if (isIdentifier(expr.expression)) {
3833
+ const name = expr.expression.text;
3834
+ return name === 'assert' || (!!promiseResolveName && name === promiseResolveName) || (!!promiseRejectName && name === promiseRejectName);
3835
+ }
3192
3836
  return (isPropertyAccessExpression(expr.expression) &&
3193
3837
  isIdentifier(expr.expression.expression) &&
3194
3838
  expr.expression.expression.text === 'console');
@@ -3298,10 +3942,18 @@ function inferExpressionType(expr) {
3298
3942
  }
3299
3943
  if (isNumericLiteral(expr))
3300
3944
  return 'float64';
3945
+ if (expr.kind === 'VoidExpression')
3946
+ return 'nil';
3947
+ if (expr.kind === 'SatisfiesExpression')
3948
+ return inferExpressionType(expr.expression);
3301
3949
  if (expr.kind === 'BigIntLiteral')
3302
3950
  return '*tnbig.Int';
3303
3951
  if (expr.kind === 'TypeOfExpression')
3304
3952
  return 'string';
3953
+ if (isJsxNode(expr))
3954
+ return 'TnJsxNode';
3955
+ if (expr.kind === 'TaggedTemplateExpression' && isStringRawTag(expr.tag))
3956
+ return 'string';
3305
3957
  if (isRegularExpressionLiteral(expr)) {
3306
3958
  const lastSlash = expr.text.lastIndexOf('/');
3307
3959
  if (needsStatefulRegex(expr.text.substring(1, lastSlash), expr.text.substring(lastSlash + 1))) {
@@ -3375,6 +4027,8 @@ function inferExpressionType(expr) {
3375
4027
  return '[]string';
3376
4028
  }
3377
4029
  // import.meta.url is a string
4030
+ if (isPropertyAccessExpression(expr) && isNewTargetNode(expr.expression) && expr.name.text === 'name')
4031
+ return 'string';
3378
4032
  if (isPropertyAccessExpression(expr) && expr.expression.kind === 'MetaProperty' && expr.name.text === 'url') {
3379
4033
  return 'string';
3380
4034
  }
@@ -3396,6 +4050,15 @@ function inferExpressionType(expr) {
3396
4050
  : isPropertyAccessExpression(expr.expression) && isIdentifier(expr.expression.expression)
3397
4051
  ? `${expr.expression.expression.text}.${expr.expression.name.text}`
3398
4052
  : undefined;
4053
+ const promiseType = inferPromiseCallType(expr);
4054
+ if (promiseType)
4055
+ return promiseType;
4056
+ const objectCallType = inferObjectCallType(expr);
4057
+ if (objectCallType)
4058
+ return objectCallType;
4059
+ if (isIdentifier(expr.expression) && expr.expression.text === 'structuredClone') {
4060
+ return inferExpressionType(expr.arguments?.[0]);
4061
+ }
3399
4062
  // Promise.all([...]) resolves to a slice over a channel
3400
4063
  if (isPropertyAccessExpression(expr.expression) &&
3401
4064
  isIdentifier(expr.expression.expression) &&
@@ -3441,12 +4104,21 @@ function inferExpressionType(expr) {
3441
4104
  if (builtinType && !declaredFunctions.has(expr.expression.text))
3442
4105
  return builtinType;
3443
4106
  }
3444
- if (isPropertyAccessExpression(expr) && expr.name.text === 'length' && isDynamicValue(expr.expression)) {
4107
+ const regexPropertyType = inferRegexPropertyType(expr);
4108
+ if (regexPropertyType)
4109
+ return regexPropertyType;
4110
+ if (isPropertyAccessExpression(expr) && inferExpressionType(expr.expression) === '*TnDecoratorContext') {
4111
+ return ['static', 'private'].includes(expr.name.text) ? 'bool' : 'string';
4112
+ }
4113
+ if (isPropertyAccessExpression(expr) && expr.name.text === 'length' && hasDynamicProperties(expr.expression)) {
3445
4114
  return 'float64';
3446
4115
  }
3447
4116
  if ((isPropertyAccessExpression(expr) || isElementAccessExpression(expr)) && isDynamicValue(expr.expression)) {
3448
4117
  return 'interface{}';
3449
4118
  }
4119
+ if (isPropertyAccessExpression(expr) && !isCallee(expr) && isTypeParameterValue(expr.expression)) {
4120
+ return 'interface{}';
4121
+ }
3450
4122
  if (isCallExpression(expr) &&
3451
4123
  isPropertyAccessExpression(expr.expression) &&
3452
4124
  isIdentifier(expr.expression.expression) &&
@@ -3456,6 +4128,15 @@ function inferExpressionType(expr) {
3456
4128
  }
3457
4129
  if (isCallExpression(expr) && isObjectKeysOfDynamic(expr))
3458
4130
  return '[]string';
4131
+ // f?.() is undefined when f is
4132
+ if (isCallExpression(expr) && hasQuestionDot(expr)) {
4133
+ const fnType = inferExpressionType(expr.expression) ?? '';
4134
+ const returnType = fnType.startsWith('func(') ? goFuncReturnType(fnType) : undefined;
4135
+ return returnType ? makeNullableType(returnType) : undefined;
4136
+ }
4137
+ const recordCall = isCallExpression(expr) ? getRecordFunctionCall(expr) : undefined;
4138
+ if (recordCall)
4139
+ return goFuncReturnType(extractMapValueType(recordCall.mapType));
3459
4140
  if (isCallExpression(expr) &&
3460
4141
  isPropertyAccessExpression(expr.expression) &&
3461
4142
  isIdentifier(expr.expression.expression) &&
@@ -3491,6 +4172,9 @@ function inferExpressionType(expr) {
3491
4172
  // Numeric enums are float64 constants; string enums keep their type
3492
4173
  if (enumBaseTypes.get(expr.expression.text) === 'float64')
3493
4174
  return 'float64';
4175
+ if (enumBaseTypes.get(expr.expression.text) === 'interface{}') {
4176
+ return enumMemberGoTypes.get(`${expr.expression.text}.${getEnumMemberName(expr.name)}`) ?? 'interface{}';
4177
+ }
3494
4178
  const enumType = getSafeName(expr.expression.text);
3495
4179
  return enumType;
3496
4180
  }
@@ -3570,15 +4254,17 @@ function inferExpressionType(expr) {
3570
4254
  if (methodName === 'getTime' || methodName === 'valueOf' || methodName === 'getMilliseconds' ||
3571
4255
  methodName === 'getSeconds' || methodName === 'getMinutes' || methodName === 'getHours' ||
3572
4256
  methodName === 'getDate' || methodName === 'getDay' || methodName === 'getMonth' ||
3573
- methodName === 'getFullYear')
4257
+ methodName === 'getFullYear' || methodName.startsWith('getUTC'))
3574
4258
  return 'float64';
3575
4259
  if (methodName === 'toISOString' || methodName === 'toJSON' || methodName === 'toString' ||
3576
4260
  methodName === 'toDateString' || methodName === 'toTimeString')
3577
4261
  return 'string';
3578
4262
  }
3579
4263
  if (ownerType && ownerType.startsWith('*TnMap[')) {
3580
- if (methodName === 'has')
4264
+ if (['has', 'isSubsetOf', 'isSupersetOf', 'isDisjointFrom'].includes(methodName))
3581
4265
  return 'bool';
4266
+ if (['union', 'intersection', 'difference', 'symmetricDifference'].includes(methodName))
4267
+ return ownerType;
3582
4268
  if (methodName === 'get') {
3583
4269
  const valueType = extractMapValueType(ownerType);
3584
4270
  return isStructGoType(valueType) ? `*${valueType}` : valueType;
@@ -3602,6 +4288,10 @@ function inferExpressionType(expr) {
3602
4288
  return 'float64';
3603
4289
  if (['pop', 'shift', 'at'].includes(methodName))
3604
4290
  return elementType;
4291
+ if (['concat', 'toSorted', 'toReversed', 'with', 'copyWithin', 'toSpliced'].includes(methodName))
4292
+ return ownerType;
4293
+ if (methodName === 'reduceRight' && expr.arguments?.[1])
4294
+ return inferExpressionType(expr.arguments[1]);
3605
4295
  if (methodName === 'flat' && elementType.startsWith('[]'))
3606
4296
  return elementType;
3607
4297
  if (methodName === 'flat' || methodName === 'flatMap')
@@ -3698,6 +4388,11 @@ function inferExpressionType(expr) {
3698
4388
  return undefined;
3699
4389
  }
3700
4390
  const BUILTIN_CALL_TYPES = {
4391
+ isFinite: 'bool',
4392
+ encodeURIComponent: 'string',
4393
+ decodeURIComponent: 'string',
4394
+ 'Object.fromEntries': 'interface{}',
4395
+ 'Date.UTC': 'float64',
3701
4396
  'Number.isNaN': 'bool',
3702
4397
  'Number.isFinite': 'bool',
3703
4398
  'Number.isInteger': 'bool',
@@ -3705,6 +4400,156 @@ const BUILTIN_CALL_TYPES = {
3705
4400
  'Object.assign': '*TnMap[string, interface{}]',
3706
4401
  'Date.now': 'float64'
3707
4402
  };
4403
+ // Object.freeze & co. record the integrity level (1 preventExtensions, 2 seal,
4404
+ // 3 freeze) by identity; TypeScript's Readonly<T> result types reject writes
4405
+ const OBJECT_INTEGRITY_LEVELS = { preventExtensions: 1, seal: 2, freeze: 3 };
4406
+ const OBJECT_INTEGRITY_CHECKS = { isExtensible: 1, isSealed: 2, isFrozen: 3 };
4407
+ function getObjectCallName(node) {
4408
+ if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression))
4409
+ return undefined;
4410
+ if (!isIdentifier(node.expression.expression) || node.expression.expression.text !== 'Object')
4411
+ return undefined;
4412
+ return node.expression.name.text;
4413
+ }
4414
+ // The Go type of a property of an object type (anonymous struct, interface or class)
4415
+ function getObjectFieldGoType(objectType, field) {
4416
+ const base = objectType.replace(/^\*/, '').replace(/\[.*$/, '');
4417
+ return (getStructFieldGoType(objectType, field) ??
4418
+ interfacePropertyTypes.get(base)?.get(field) ??
4419
+ classPropertyTypes.get(base)?.get(field));
4420
+ }
4421
+ function inferObjectCallType(expr) {
4422
+ const name = getObjectCallName(expr);
4423
+ if (!name)
4424
+ return undefined;
4425
+ if (OBJECT_INTEGRITY_LEVELS[name] || name === 'defineProperty')
4426
+ return inferExpressionType(expr.arguments?.[0]);
4427
+ if (OBJECT_INTEGRITY_CHECKS[name] || name === 'hasOwn')
4428
+ return 'bool';
4429
+ if (name === 'getOwnPropertyNames')
4430
+ return inferExpressionType({ ...expr, expression: { ...expr.expression, name: { kind: 'Identifier', text: 'keys' } } });
4431
+ return undefined;
4432
+ }
4433
+ function visitObjectCall(node) {
4434
+ const name = getObjectCallName(node);
4435
+ if (!name)
4436
+ return undefined;
4437
+ const args = node.arguments ?? [];
4438
+ if (OBJECT_INTEGRITY_LEVELS[name]) {
4439
+ useHelper('objectIntegrity');
4440
+ return `TnSetIntegrity(${visit(args[0])}, ${OBJECT_INTEGRITY_LEVELS[name]})`;
4441
+ }
4442
+ if (OBJECT_INTEGRITY_CHECKS[name]) {
4443
+ useHelper('objectIntegrity');
4444
+ const check = `TnHasIntegrity(${visit(args[0])}, ${OBJECT_INTEGRITY_CHECKS[name]})`;
4445
+ return name === 'isExtensible' ? `!${check}` : check;
4446
+ }
4447
+ if (name === 'getOwnPropertyNames') {
4448
+ return visit({ ...node, expression: { ...node.expression, name: { kind: 'Identifier', text: 'keys' } } });
4449
+ }
4450
+ const objectType = inferExpressionType(args[0]) ?? '';
4451
+ const key = args[1];
4452
+ if (name === 'hasOwn') {
4453
+ if (isDynamicValue(args[0])) {
4454
+ useHelper('dynamic');
4455
+ return `TnHas(${visit(args[0])}, ${visit(key)})`;
4456
+ }
4457
+ if (objectType.startsWith('*TnMap['))
4458
+ return `${visit(args[0])}.Has(${visit(key)})`;
4459
+ if (!isStringLiteral(key))
4460
+ throw new Error('Object.hasOwn on a typed object needs a literal key');
4461
+ const fieldType = getObjectFieldGoType(objectType, key.text);
4462
+ if (!fieldType)
4463
+ return 'false';
4464
+ // an absent optional primitive is nil
4465
+ return /^\*(float64|string|bool)$/.test(fieldType) ? `(${visit(args[0])}.${goFieldName(key.text)} != nil)` : 'true';
4466
+ }
4467
+ if (name === 'defineProperty') {
4468
+ const descriptor = args[2];
4469
+ const valueProp = isObjectLiteralExpression(descriptor)
4470
+ ? (descriptor.properties ?? []).find((p) => isPropertyAssignment(p) && isIdentifier(p.name) && p.name.text === 'value')
4471
+ : undefined;
4472
+ if (!valueProp)
4473
+ throw new Error('Object.defineProperty needs a { value } descriptor literal');
4474
+ const target = visit(args[0]);
4475
+ if (isDynamicValue(args[0])) {
4476
+ useHelper('dynamic');
4477
+ return `TnSet(${target}, ${visit(key)}, ${visit(valueProp.initializer)})`;
4478
+ }
4479
+ if (objectType.startsWith('*TnMap[')) {
4480
+ return `${target}.Set(${visit(key)}, ${toGoValueOfType(valueProp.initializer, extractMapValueType(objectType))})`;
4481
+ }
4482
+ if (!isStringLiteral(key))
4483
+ throw new Error('Object.defineProperty on a typed object needs a literal key');
4484
+ return `${target}.${goFieldName(key.text)} = ${toGoValueOfType(valueProp.initializer, getObjectFieldGoType(objectType, key.text))}`;
4485
+ }
4486
+ return undefined;
4487
+ }
4488
+ const PROMISE_COMBINATORS = new Set(['race', 'allSettled', 'any']);
4489
+ function isPromiseStaticCall(node, names) {
4490
+ return (isCallExpression(node) &&
4491
+ isPropertyAccessExpression(node.expression) &&
4492
+ isIdentifier(node.expression.expression) &&
4493
+ node.expression.expression.text === 'Promise' &&
4494
+ names.has(node.expression.name.text));
4495
+ }
4496
+ // p.catch(cb) / p.finally(cb) on a promise channel
4497
+ function isPromiseMethodCall(node) {
4498
+ return (isCallExpression(node) &&
4499
+ isPropertyAccessExpression(node.expression) &&
4500
+ ['catch', 'finally'].includes(node.expression.name.text) &&
4501
+ !!inferExpressionType(node.expression.expression)?.startsWith('chan '));
4502
+ }
4503
+ // The value type of Promise.reject(…): from sibling promises in an array, else the
4504
+ // enclosing function's Promise<T>
4505
+ function getRejectedPromiseType(node) {
4506
+ const parent = node.parent;
4507
+ if (isArrayLiteralExpression(parent)) {
4508
+ for (const sibling of parent.elements ?? []) {
4509
+ if (sibling === node || isPromiseStaticCall(sibling, new Set(['reject'])))
4510
+ continue;
4511
+ const type = inferExpressionType(sibling);
4512
+ if (type?.startsWith('chan '))
4513
+ return type.slice(5);
4514
+ }
4515
+ }
4516
+ return getPromiseChannelType(node);
4517
+ }
4518
+ function inferPromiseCallType(expr) {
4519
+ if (isPromiseStaticCall(expr, new Set(['reject'])))
4520
+ return `chan ${getRejectedPromiseType(expr)}`;
4521
+ if (isPromiseStaticCall(expr, PROMISE_COMBINATORS)) {
4522
+ const elem = promiseAllElementType(expr) ?? 'interface{}';
4523
+ return expr.expression.name.text === 'allSettled' ? `chan []*TnSettled[${elem}]` : `chan ${elem}`;
4524
+ }
4525
+ if (isPromiseMethodCall(expr))
4526
+ return inferExpressionType(expr.expression.expression);
4527
+ return undefined;
4528
+ }
4529
+ function visitPromiseCall(node) {
4530
+ const args = node.arguments ?? [];
4531
+ if (isPromiseStaticCall(node, new Set(['reject']))) {
4532
+ useHelper('promise');
4533
+ return `TnPromiseReject[${getRejectedPromiseType(node)}](${args[0] ? visit(args[0]) : 'nil'})`;
4534
+ }
4535
+ if (isPromiseStaticCall(node, PROMISE_COMBINATORS)) {
4536
+ useHelper('promise');
4537
+ const name = node.expression.name.text;
4538
+ return `TnPromise${name[0].toUpperCase()}${name.slice(1)}(${args[0] ? visit(args[0]) : '[]chan interface{}{}'})`;
4539
+ }
4540
+ if (!isPromiseMethodCall(node))
4541
+ return undefined;
4542
+ useHelper('promise');
4543
+ const receiver = visit(node.expression.expression);
4544
+ const callback = args[0];
4545
+ const cb = callback ? visit(callback) : 'func() {}';
4546
+ if (node.expression.name.text === 'finally')
4547
+ return `TnPromiseFinally(${receiver}, ${cb})`;
4548
+ // .catch(() => …) ignores the reason; .catch((e) => value) settles with the value
4549
+ const handler = callback && (callback.parameters ?? []).length === 0 ? `func(_ interface{}) { (${cb})() }` : cb;
4550
+ const returnsValue = callback && !/\)$/.test(inferExpressionType(callback) ?? ')');
4551
+ return `TnPromiseCatch${returnsValue ? 'Value' : ''}(${receiver}, ${handler})`;
4552
+ }
3708
4553
  // Element type of `await Promise.all([...])` from the first promise channel
3709
4554
  function promiseAllElementType(expr) {
3710
4555
  const arrayNode = (expr.arguments ?? [])[0];
@@ -3716,11 +4561,14 @@ const BUILTIN_FUNCTION_TYPES = {
3716
4561
  parseFloat: 'float64',
3717
4562
  parseInt: 'float64',
3718
4563
  Number: 'float64',
3719
- String: 'string'
4564
+ String: 'string',
4565
+ setTimeout: '*time.Timer',
4566
+ setInterval: '*TnInterval'
3720
4567
  };
3721
4568
  const STRING_METHOD_RETURN_TYPES = {
3722
4569
  trim: 'string',
3723
4570
  trimStart: 'string',
4571
+ normalize: 'string',
3724
4572
  trimEnd: 'string',
3725
4573
  toUpperCase: 'string',
3726
4574
  toLowerCase: 'string',
@@ -3875,6 +4723,46 @@ function makeNullableType(typeName) {
3875
4723
  // property-only interfaces / object type aliases
3876
4724
  // Object types that compile to Go structs: property-only interfaces and
3877
4725
  // object type aliases (values of these types are *Name, like JS references)
4726
+ // A declared interface with method signatures (a Go interface type)
4727
+ function isMethodInterfaceName(name) {
4728
+ return (declaredInterfaces.get(name)?.members ?? []).some((m) => isMethodSignature(m));
4729
+ }
4730
+ // type P_obj struct { get_fn func() T; … } + func (o *P_obj) get() T { return o.get_fn() }
4731
+ function emitInterfaceAdapter(node) {
4732
+ const name = visit(node.name);
4733
+ const typeParams = getTypeParameters(node.typeParameters);
4734
+ const typeParamNames = getTypeParameterNames(node.typeParameters);
4735
+ const fields = [];
4736
+ const methods = [];
4737
+ for (const member of node.members ?? []) {
4738
+ if (!isMethodSignature(member))
4739
+ continue;
4740
+ const methodName = visit(member.name);
4741
+ const params = (member.parameters ?? []).map((p) => `${visit(p.name)} ${getType(p.type)}`);
4742
+ const args = (member.parameters ?? []).map((p) => visit(p.name));
4743
+ const returnType = member.type ? getType(member.type) : '';
4744
+ const signature = `func(${params.join(', ')})${returnType ? ` ${returnType}` : ''}`;
4745
+ fields.push(`\t${methodName}_fn ${signature}`);
4746
+ methods.push(`func (o *${name}_obj${typeParamNames}) ${methodName}(${params.join(', ')})${returnType ? ` ${returnType}` : ''} { ${returnType ? 'return ' : ''}o.${methodName}_fn(${args.join(', ')}) }`);
4747
+ }
4748
+ return `\n\ntype ${name}_obj${typeParams} struct {\n${fields.join('\n')}\n}\n\n${methods.join('\n\n')}`;
4749
+ }
4750
+ // { get: () => 4, set(v) { … } } typed as a method interface → &P_obj{get_fn: …, set_fn: …}
4751
+ function visitInterfaceLiteral(node, interfaceName, typeArgs) {
4752
+ const fields = (node.properties ?? []).map((p) => {
4753
+ const fieldName = `${visit(p.name)}_fn`;
4754
+ if (isPropertyAssignment(p))
4755
+ return `${fieldName}: ${visit(p.initializer)}`;
4756
+ if (isShorthandPropertyAssignment(p))
4757
+ return `${fieldName}: ${visit(p.name)}`;
4758
+ if (isMethodDeclaration(p)) {
4759
+ const fn = { kind: 'FunctionExpression', parameters: p.parameters, type: p.type, body: p.body, parent: p.parent };
4760
+ return `${fieldName}: ${visit(fn)}`;
4761
+ }
4762
+ return '';
4763
+ });
4764
+ return `&${interfaceName}_obj${typeArgs}${compositeBody(fields.filter((f) => f))}`;
4765
+ }
3878
4766
  function isObjectTypeName(name) {
3879
4767
  const alias = declaredTypeAliases.get(name) ?? typeAliases.get(name);
3880
4768
  if (alias)
@@ -3886,6 +4774,43 @@ function isObjectTypeName(name) {
3886
4774
  }
3887
4775
  return interfacePropertyTypes.has(name);
3888
4776
  }
4777
+ // The keys a mapped type iterates: string literals of a union, or keyof an object type
4778
+ function getMappedTypeKeys(constraint) {
4779
+ const keys = resolveTypeNode(constraint);
4780
+ if (!keys)
4781
+ return undefined;
4782
+ if (keys.kind === 'TypeOperator' && keys.operator === 'KeyOfKeyword') {
4783
+ const fields = getObjectTypeFields(keys.type);
4784
+ return fields.length > 0 ? fields.map((f) => f.split(' ')[0]) : undefined;
4785
+ }
4786
+ const types = isUnionTypeNode(keys) ? (keys.types ?? []) : [keys];
4787
+ if (!types.every((t) => isLiteralTypeNode(t) && isStringLiteral(t.literal)))
4788
+ return undefined;
4789
+ return types.map((t) => goFieldName(t.literal.text));
4790
+ }
4791
+ function getMappedType(typeNode) {
4792
+ const valueType = typeNode.type ? getOptionalNodeType(typeNode.type, !!typeNode.questionToken) : 'interface{}';
4793
+ const keys = getMappedTypeKeys(typeNode.typeParameter?.constraint);
4794
+ if (keys)
4795
+ return `*struct{ ${keys.map((k) => `${k} ${valueType}`).join('; ')} }`;
4796
+ return 'interface{}';
4797
+ }
4798
+ const UTILITY_TYPES = new Set(['Partial', 'Required', 'Readonly', 'Pick', 'Omit', 'NonNullable']);
4799
+ // "name T" field declarations of an object type (type literal, alias or interface)
4800
+ function getObjectTypeFields(typeNode) {
4801
+ let members = [];
4802
+ const resolved = typeNode.kind === 'TypeLiteral' ? typeNode : undefined;
4803
+ if (resolved)
4804
+ members = resolved.members ?? [];
4805
+ else if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
4806
+ const name = typeNode.typeName.text;
4807
+ const alias = declaredTypeAliases.get(name) ?? typeAliases.get(name);
4808
+ members = alias?.kind === 'TypeLiteral' ? (alias.members ?? []) : (declaredInterfaces.get(name)?.members ?? []);
4809
+ }
4810
+ return members
4811
+ .filter((m) => isPropertySignature(m) && isIdentifier(m.name))
4812
+ .map((m) => `${goFieldName(m.name.text)} ${getOptionalNodeType(m.type, !!m.questionToken)}`);
4813
+ }
3889
4814
  function isStructGoType(goType) {
3890
4815
  return goType.startsWith('struct{') || interfacePropertyTypes.has(goType);
3891
4816
  }
@@ -4071,6 +4996,18 @@ function withNarrowingType(names, infer) {
4071
4996
  return type;
4072
4997
  }
4073
4998
  function visitOptionalCall(node) {
4999
+ // f?.(): call only when the function value is not nil
5000
+ if (hasQuestionDot(node)) {
5001
+ const fnType = inferExpressionType(node.expression) ?? '';
5002
+ const returnType = fnType.startsWith('func(') ? goFuncReturnType(fnType) : undefined;
5003
+ const fn = getTempName('fn');
5004
+ const args = (node.arguments ?? []).map((a) => visit(a)).join(', ');
5005
+ if (!returnType)
5006
+ return `func() { if ${fn} := ${visit(node.expression)}; ${fn} != nil { ${fn}(${args}) } }()`;
5007
+ const resultType = makeNullableType(returnType);
5008
+ const value = resultType === returnType ? `${fn}(${args})` : `func() ${resultType} { v := ${fn}(${args}); return &v }()`;
5009
+ return `func() ${resultType} { ${fn} := ${visit(node.expression)}; if ${fn} == nil { return nil }; return ${value} }()`;
5010
+ }
4074
5011
  if (!isPropertyAccessExpression(node.expression)) {
4075
5012
  return `${visit(node.expression)}(${(node.arguments ?? []).map((a) => visit(a)).join(', ')})`;
4076
5013
  }
@@ -4189,7 +5126,7 @@ function visitArrayHigherOrderCall(node) {
4189
5126
  if (!isPropertyAccessExpression(node.expression))
4190
5127
  return undefined;
4191
5128
  const methodName = node.expression.name.text;
4192
- if (!['map', 'filter', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'forEach', 'reduce', 'join', 'entries', 'flatMap'].includes(methodName)) {
5129
+ if (!['map', 'filter', 'some', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'forEach', 'reduce', 'reduceRight', 'join', 'entries', 'flatMap'].includes(methodName)) {
4193
5130
  return undefined;
4194
5131
  }
4195
5132
  const arrayExprNode = node.expression.expression;
@@ -4294,6 +5231,17 @@ function visitArrayHigherOrderCall(node) {
4294
5231
  const callbackCall = buildArrayCallbackInvocation(callbackInfo, itemVar, idxVar, arrVar);
4295
5232
  return `func() { ${arrVar} := ${arrayExpr}; for ${rangeIndexVar}, ${itemVar} := range ${arrVar} { ${callbackCall} } }()`;
4296
5233
  }
5234
+ if (methodName === 'reduceRight') {
5235
+ importedPackages.add('slices');
5236
+ const reversed = { kind: 'Identifier', text: `func() []${elementType} { __s := slices.Clone(${arrayExpr}); slices.Reverse(__s); return __s }()` };
5237
+ variableGoTypes.set(reversed.text, ownerType ?? '[]interface{}');
5238
+ const access = { ...node.expression, name: { kind: 'Identifier', text: 'reduce' }, expression: reversed };
5239
+ const call = { ...node, expression: access };
5240
+ reversed.parent = access;
5241
+ access.parent = call;
5242
+ access.name.parent = access;
5243
+ return visitArrayHigherOrderCall(call);
5244
+ }
4297
5245
  if (methodName === 'reduce') {
4298
5246
  const reduceCallback = (node.arguments ?? [])[0];
4299
5247
  if (!reduceCallback)
@@ -4359,16 +5307,23 @@ function getEnumMemberName(name) {
4359
5307
  }
4360
5308
  return 'Member';
4361
5309
  }
5310
+ // string enums, numeric enums, or interface{} for heterogeneous ones (each member keeps its own type)
4362
5311
  function getEnumBaseType(node) {
5312
+ let hasString = false;
5313
+ let hasNumber = false;
4363
5314
  for (const member of (node.members ?? [])) {
4364
5315
  const initializer = member.initializer;
4365
- if (!initializer)
4366
- continue;
4367
- if (isStringLiteral(initializer) || isNoSubstitutionTemplateLiteral(initializer)) {
4368
- return 'string';
5316
+ if (initializer && (isStringLiteral(initializer) || isNoSubstitutionTemplateLiteral(initializer))) {
5317
+ hasString = true;
5318
+ }
5319
+ else {
5320
+ hasNumber = true;
4369
5321
  }
5322
+ enumMemberGoTypes.set(`${node.name.text}.${getEnumMemberName(member.name)}`, initializer && (isStringLiteral(initializer) || isNoSubstitutionTemplateLiteral(initializer)) ? 'string' : 'float64');
4370
5323
  }
4371
- return 'float64';
5324
+ if (hasString && hasNumber)
5325
+ return 'interface{}';
5326
+ return hasString ? 'string' : 'float64';
4372
5327
  }
4373
5328
  function readNumericEnumInitializer(initializer) {
4374
5329
  if (isNumericLiteral(initializer)) {
@@ -4381,18 +5336,28 @@ function readNumericEnumInitializer(initializer) {
4381
5336
  }
4382
5337
  return undefined;
4383
5338
  }
4384
- function visitEnumDeclaration(node) {
5339
+ function enumHasComputedMember(node) {
5340
+ return (node.members ?? []).some((m) => m.initializer &&
5341
+ !isStringLiteral(m.initializer) &&
5342
+ !isNoSubstitutionTemplateLiteral(m.initializer) &&
5343
+ readNumericEnumInitializer(m.initializer) === undefined);
5344
+ }
5345
+ function visitEnumDeclaration(node, inPlace = false) {
4385
5346
  const enumName = getSafeName(node.name.text);
4386
5347
  const baseType = enumBaseTypes.get(node.name.text) ?? getEnumBaseType(node);
4387
5348
  let nextNumericValue = 0;
4388
5349
  let canAutoIncrement = true;
5350
+ let hasComputedMember = false;
4389
5351
  const members = [];
4390
5352
  for (const member of (node.members ?? [])) {
4391
5353
  const memberName = getEnumMemberName(member.name);
4392
5354
  const symbolName = `${enumName}_${memberName}`;
4393
5355
  let valueExpr;
4394
5356
  if (member.initializer) {
4395
- if (baseType === 'float64') {
5357
+ if (baseType === 'interface{}' && (isStringLiteral(member.initializer) || isNoSubstitutionTemplateLiteral(member.initializer))) {
5358
+ valueExpr = visit(member.initializer);
5359
+ }
5360
+ else if (baseType !== 'string') {
4396
5361
  const numericValue = readNumericEnumInitializer(member.initializer);
4397
5362
  if (numericValue !== undefined) {
4398
5363
  valueExpr = `${numericValue}`;
@@ -4402,13 +5367,14 @@ function visitEnumDeclaration(node) {
4402
5367
  else {
4403
5368
  valueExpr = `float64(${visit(member.initializer)})`;
4404
5369
  canAutoIncrement = false;
5370
+ hasComputedMember = true;
4405
5371
  }
4406
5372
  }
4407
5373
  else {
4408
5374
  valueExpr = visit(member.initializer);
4409
5375
  }
4410
5376
  }
4411
- else if (baseType === 'float64') {
5377
+ else if (baseType !== 'string') {
4412
5378
  const currentValue = canAutoIncrement ? nextNumericValue : 0;
4413
5379
  valueExpr = `${currentValue}`;
4414
5380
  nextNumericValue = currentValue + 1;
@@ -4418,10 +5384,37 @@ function visitEnumDeclaration(node) {
4418
5384
  }
4419
5385
  members.push(`\t${symbolName} ${enumName} = ${enumName}(${valueExpr})`);
4420
5386
  }
4421
- // Numeric enums are plain float64 constants so they compare against numbers;
4422
- // string enums keep a named type
5387
+ // Assigned in place: package variables declared, values set in source order
5388
+ if (inPlace) {
5389
+ return members
5390
+ .map((m) => {
5391
+ const name = m.slice(m.indexOf('\t') + 1, m.indexOf(' '));
5392
+ const raw = m.slice(m.indexOf(`= ${enumName}(`) + enumName.length + 3, -1);
5393
+ const value = /^-?[0-9.]+$/.test(raw) ? `float64(${raw})` : raw;
5394
+ const memberType = baseType === 'interface{}'
5395
+ ? (enumMemberGoTypes.get(`${node.name.text}.${name.slice(enumName.length + 1)}`) ?? 'interface{}')
5396
+ : baseType === 'float64'
5397
+ ? 'float64'
5398
+ : enumName;
5399
+ mainPackageVariables.push(`var ${name} ${memberType}`);
5400
+ return `${name} = ${memberType === enumName ? `${enumName}(${raw})` : value};\n\t`;
5401
+ })
5402
+ .join('');
5403
+ }
5404
+ // Heterogeneous enums: plain variables of each member's own type
5405
+ if (baseType === 'interface{}') {
5406
+ return `var (\n${members
5407
+ .map((m) => {
5408
+ const name = m.slice(m.indexOf('\t') + 1, m.indexOf(' '));
5409
+ const value = m.slice(m.indexOf(`= ${enumName}(`) + enumName.length + 3, -1);
5410
+ return `\t${name} = ${/^-?[0-9.]+$/.test(value) ? `float64(${value})` : value}`;
5411
+ })
5412
+ .join('\n')}\n)`;
5413
+ }
5414
+ // Numeric enums are plain float64 constants so they compare against numbers
5415
+ // (variables when a member is computed); string enums keep a named type
4423
5416
  if (baseType === 'float64') {
4424
- return `const (\n${members
5417
+ return `${hasComputedMember ? 'var' : 'const'} (\n${members
4425
5418
  .map((m) => {
4426
5419
  const name = m.slice(m.indexOf('\t') + 1, m.indexOf(' '));
4427
5420
  const value = m.slice(m.lastIndexOf('=') + 1).trim();
@@ -4444,6 +5437,35 @@ function getType(typeNode, getArrayType = false) {
4444
5437
  const elementType = getType(typeNode.elementType);
4445
5438
  return getArrayType ? elementType : `[]${elementType}`;
4446
5439
  }
5440
+ if (typeNode.kind === 'MappedType')
5441
+ return getMappedType(typeNode);
5442
+ if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName) && /DecoratorContext$/.test(typeNode.typeName.text)) {
5443
+ useHelper('decorators');
5444
+ return '*TnDecoratorContext';
5445
+ }
5446
+ if (isTypeReferenceNode(typeNode) &&
5447
+ typeNode.typeName?.kind === 'QualifiedName' &&
5448
+ typeNode.typeName.left?.text === 'JSX' &&
5449
+ typeNode.typeName.right?.text === 'Element') {
5450
+ useHelper('jsx');
5451
+ return 'TnJsxNode';
5452
+ }
5453
+ // readonly T[] → T[]; keyof T → string
5454
+ if (typeNode.kind === 'TypeOperator') {
5455
+ return typeNode.operator === 'KeyOfKeyword' ? 'string' : getType(typeNode.type, getArrayType);
5456
+ }
5457
+ // typeof x → x's type
5458
+ if (typeNode.kind === 'TypeQuery' && isIdentifier(typeNode.exprName)) {
5459
+ return variableGoTypes.get(typeNode.exprName.text) ?? 'interface{}';
5460
+ }
5461
+ // asserts x is T → no value; x is T → bool
5462
+ if (typeNode.kind === 'TypePredicate')
5463
+ return typeNode.assertsModifier ? '' : 'bool';
5464
+ // A & B of object types → one struct with all their fields
5465
+ if (typeNode.kind === 'IntersectionType') {
5466
+ const fields = (typeNode.types ?? []).flatMap((t) => getObjectTypeFields(t));
5467
+ return fields.length > 0 ? `*struct{ ${fields.join('; ')} }` : getType(typeNode.types[0]);
5468
+ }
4447
5469
  // [A, B] tuples are slices: []A when all elements share a type, else []interface{}
4448
5470
  if (typeNode.kind === 'TupleType') {
4449
5471
  const elementType = getTupleElementType(typeNode);
@@ -4467,8 +5489,12 @@ function getType(typeNode, getArrayType = false) {
4467
5489
  return 'interface{}';
4468
5490
  }
4469
5491
  if (isFunctionTypeNode(typeNode)) {
5492
+ // (...args: T[]) => R is variadic: func(...T) R
4470
5493
  const params = (typeNode.parameters ?? [])
4471
- .map((p) => (p.type ? getType(p.type) : 'interface{}'))
5494
+ .map((p) => {
5495
+ const paramType = p.type ? getType(p.type) : 'interface{}';
5496
+ return p.dotDotDotToken && paramType.startsWith('[]') ? `...${paramType.slice(2)}` : paramType;
5497
+ })
4472
5498
  .join(', ');
4473
5499
  const ret = typeNode.type ? ` ${getType(typeNode.type)}` : '';
4474
5500
  return `func(${params})${ret}`;
@@ -4488,6 +5514,8 @@ function getType(typeNode, getArrayType = false) {
4488
5514
  // Numeric enums lowered to float64 constants have no named type
4489
5515
  if (enumBaseTypes.get(name) === 'float64')
4490
5516
  return 'float64';
5517
+ if (enumBaseTypes.get(name) === 'interface{}')
5518
+ return 'interface{}';
4491
5519
  return getSafeName(name);
4492
5520
  }
4493
5521
  const aliasType = getAliasType(name);
@@ -4518,6 +5546,10 @@ function getType(typeNode, getArrayType = false) {
4518
5546
  if (name === 'Set' && typeNode.typeArguments && typeNode.typeArguments.length === 1) {
4519
5547
  return mapGoType(getType(typeNode.typeArguments[0]), 'struct{}');
4520
5548
  }
5549
+ // Partial / Required / Readonly / Pick / Omit / NonNullable<T, ...> → T's Go type
5550
+ if (UTILITY_TYPES.has(name) && typeNode.typeArguments?.length) {
5551
+ return getType(typeNode.typeArguments[0], getArrayType);
5552
+ }
4521
5553
  const typeArgs = getTypeArguments(typeNode.typeArguments);
4522
5554
  if (classNames.has(name) || isObjectTypeName(name)) {
4523
5555
  return `*${name}${typeArgs}`;
@@ -4550,7 +5582,10 @@ function getType(typeNode, getArrayType = false) {
4550
5582
  case 'any':
4551
5583
  return 'interface{}';
4552
5584
  case 'void':
5585
+ case 'never':
4553
5586
  return '';
5587
+ case 'object':
5588
+ return 'interface{}';
4554
5589
  default:
4555
5590
  return typeName;
4556
5591
  }
@@ -4661,6 +5696,22 @@ function getAcessString(leftSide, rightSide, objectType) {
4661
5696
  return 'process.env';
4662
5697
  }
4663
5698
  }
5699
+ if (leftSide === 'Math') {
5700
+ const mathConstants = {
5701
+ PI: 'math.Pi',
5702
+ E: 'math.E',
5703
+ LN2: 'math.Ln2',
5704
+ LN10: 'math.Ln10',
5705
+ LOG2E: 'math.Log2E',
5706
+ LOG10E: 'math.Log10E',
5707
+ SQRT2: 'math.Sqrt2',
5708
+ SQRT1_2: '(1 / math.Sqrt2)'
5709
+ };
5710
+ if (mathConstants[rightSide]) {
5711
+ importedPackages.add('math');
5712
+ return `float64(${mathConstants[rightSide]})`;
5713
+ }
5714
+ }
4664
5715
  if (leftSide === 'Number') {
4665
5716
  const constants = {
4666
5717
  MAX_SAFE_INTEGER: 'float64(9007199254740991)',
@@ -4701,6 +5752,63 @@ const callHandlers = {
4701
5752
  importedPackages.add('fmt');
4702
5753
  return `fmt.Println("Elapsed time:", time.Since(${getTimerName(args[0])}))`;
4703
5754
  },
5755
+ 'Math.hypot': (_caller, args) => mathCall('Hypot', args),
5756
+ 'Math.cbrt': (_caller, args) => mathCall('Cbrt', args),
5757
+ 'Math.exp': (_caller, args) => mathCall('Exp', args),
5758
+ 'Math.expm1': (_caller, args) => mathCall('Expm1', args),
5759
+ 'Math.log1p': (_caller, args) => mathCall('Log1p', args),
5760
+ 'Math.asin': (_caller, args) => mathCall('Asin', args),
5761
+ 'Math.acos': (_caller, args) => mathCall('Acos', args),
5762
+ 'Math.atan': (_caller, args) => mathCall('Atan', args),
5763
+ 'Math.atan2': (_caller, args) => mathCall('Atan2', args),
5764
+ 'Math.sinh': (_caller, args) => mathCall('Sinh', args),
5765
+ 'Math.cosh': (_caller, args) => mathCall('Cosh', args),
5766
+ 'Math.tanh': (_caller, args) => mathCall('Tanh', args),
5767
+ isFinite: (_caller, args) => {
5768
+ importedPackages.add('math');
5769
+ return `(!math.IsInf(${args[0]}, 0) && !math.IsNaN(${args[0]}))`;
5770
+ },
5771
+ encodeURIComponent: (_caller, args) => {
5772
+ useHelper('uri');
5773
+ return `TnEncodeURIComponent(${args[0]})`;
5774
+ },
5775
+ decodeURIComponent: (_caller, args) => {
5776
+ useHelper('uri');
5777
+ return `TnDecodeURIComponent(${args[0]})`;
5778
+ },
5779
+ 'Object.fromEntries': (_caller, args) => {
5780
+ useHelper('dynamic');
5781
+ return `TnFromEntries(${args[0]})`;
5782
+ },
5783
+ 'Date.UTC': (_caller, args) => {
5784
+ importedPackages.add('time');
5785
+ const part = (i, fallback) => (args[i] ? `int(${args[i]})` : fallback);
5786
+ return `float64(time.Date(${part(0, '1970')}, time.Month(${part(1, '0')}+1), ${part(2, '1')}, ${part(3, '0')}, ${part(4, '0')}, ${part(5, '0')}, ${part(6, '0')}*1000000, time.UTC).UnixMilli())`;
5787
+ },
5788
+ 'Promise.resolve': (_caller, args) => {
5789
+ const valueType = currentCallArgTypes[0] ?? 'interface{}';
5790
+ return `func() chan ${valueType} { __ch := make(chan ${valueType}, 1); __ch <- ${args[0] ?? 'nil'}; return __ch }()`;
5791
+ },
5792
+ structuredClone: (_caller, args) => {
5793
+ useHelper('structuredClone');
5794
+ return `TnStructuredClone(${args[0]})`;
5795
+ },
5796
+ setInterval: (_caller, args) => {
5797
+ useHelper('timerWait');
5798
+ return `TnSetInterval(${args[1] ?? '0'}, ${args[0]})`;
5799
+ },
5800
+ clearInterval: (_caller, args) => {
5801
+ useHelper('timerWait');
5802
+ return `TnClearInterval(${args[0]})`;
5803
+ },
5804
+ queueMicrotask: (_caller, args) => {
5805
+ useHelper('timerWait');
5806
+ return `TnQueueMicrotask(${args[0]})`;
5807
+ },
5808
+ clearTimeout: (_caller, args) => {
5809
+ useHelper('timerWait');
5810
+ return `TnClearTimeout(${args[0]})`;
5811
+ },
4704
5812
  'Math.random': () => {
4705
5813
  importedPackages.add('math/rand');
4706
5814
  return 'rand.Float64()';
@@ -4833,10 +5941,7 @@ const callHandlers = {
4833
5941
  importedPackages.add('reflect');
4834
5942
  return `(reflect.ValueOf(${args[0]}).Kind() == reflect.Slice)`;
4835
5943
  },
4836
- 'String': (_caller, args) => {
4837
- importedPackages.add('fmt');
4838
- return `fmt.Sprintf("%v", ${args[0]})`;
4839
- },
5944
+ 'String': (_caller, args) => jsStringOf(args[0] ?? '""', currentCallArgTypes[0]),
4840
5945
  'String.fromCharCode': (_caller, args) => `string(rune(int(${args[0] ?? '0'})))`,
4841
5946
  // Symbol(description) — unique values for record keys
4842
5947
  Symbol: (_caller, args) => {
@@ -4863,9 +5968,7 @@ const callHandlers = {
4863
5968
  importedPackages.add('math');
4864
5969
  return `(${args[0]} == math.Trunc(${args[0]}) && !math.IsInf(${args[0]}, 0))`;
4865
5970
  },
4866
- 'Boolean': (_caller, args) => {
4867
- return `(${args[0]} != nil && ${args[0]} != false && ${args[0]} != 0 && ${args[0]} != "")`;
4868
- },
5971
+ 'Boolean': (_caller, args) => `(${truthinessCheck(args[0] ?? 'false', currentCallArgTypes[0] ?? 'interface{}')})`,
4869
5972
  };
4870
5973
  const stringMethodHandlers = {
4871
5974
  split: (obj, args) => {
@@ -4943,6 +6046,15 @@ const stringMethodHandlers = {
4943
6046
  return `${obj}[${sliceIndex(obj, args[0])}:]`;
4944
6047
  },
4945
6048
  concat: (obj, args) => `${obj} + ${args.join(' + ')}`,
6049
+ // normalize('NFC' | 'NFD' | 'NFKC' | 'NFKD')
6050
+ normalize: (obj, args) => {
6051
+ importedPackages.add('golang.org/x/text/unicode/norm');
6052
+ const form = args[0] ?? '"NFC"';
6053
+ const literal = /^"(NFC|NFD|NFKC|NFKD)"$/.exec(form);
6054
+ if (literal)
6055
+ return `tnnorm.${literal[1]}.String(${obj})`;
6056
+ return `func() string { switch ${form} { case "NFD": return tnnorm.NFD.String(${obj}); case "NFKC": return tnnorm.NFKC.String(${obj}); case "NFKD": return tnnorm.NFKD.String(${obj}) }; return tnnorm.NFC.String(${obj}) }()`;
6057
+ },
4946
6058
  padStart: (obj, args) => {
4947
6059
  importedPackages.add('strings');
4948
6060
  const pad = args[1] ?? '" "';
@@ -5011,8 +6123,16 @@ const dateMethodHandlers = {
5011
6123
  getDay: (obj) => `float64(int((${obj}).Weekday()))`,
5012
6124
  getMonth: (obj) => `float64(int((${obj}).Month()) - 1)`,
5013
6125
  getFullYear: (obj) => `float64((${obj}).Year())`,
5014
- toISOString: (obj) => `(${obj}).UTC().Format("2006-01-01T15:04:05.000Z07:00")`,
5015
- toJSON: (obj) => `(${obj}).UTC().Format("2006-01-01T15:04:05.000Z07:00")`,
6126
+ toISOString: (obj) => `(${obj}).UTC().Format("2006-01-02T15:04:05.000Z07:00")`,
6127
+ toJSON: (obj) => `(${obj}).UTC().Format("2006-01-02T15:04:05.000Z07:00")`,
6128
+ getUTCMilliseconds: (obj) => `float64((${obj}).UTC().Nanosecond() / 1000000)`,
6129
+ getUTCSeconds: (obj) => `float64((${obj}).UTC().Second())`,
6130
+ getUTCMinutes: (obj) => `float64((${obj}).UTC().Minute())`,
6131
+ getUTCHours: (obj) => `float64((${obj}).UTC().Hour())`,
6132
+ getUTCDate: (obj) => `float64((${obj}).UTC().Day())`,
6133
+ getUTCDay: (obj) => `float64(int((${obj}).UTC().Weekday()))`,
6134
+ getUTCMonth: (obj) => `float64(int((${obj}).UTC().Month()) - 1)`,
6135
+ getUTCFullYear: (obj) => `float64((${obj}).UTC().Year())`,
5016
6136
  toString: (obj) => `(${obj}).UTC().Format("Mon Jan 02 2006 15:04:05 GMT+0000 (UTC)")`,
5017
6137
  toDateString: (obj) => `(${obj}).UTC().Format("Mon Jan 02 2006")`,
5018
6138
  toTimeString: (obj) => `(${obj}).UTC().Format("15:04:05 GMT+0000 (UTC)")`,
@@ -5071,19 +6191,38 @@ const arrayMethodHandlers = {
5071
6191
  return `func() float64 { __x := ${args[0]}; for __i := len(${obj}) - 1; __i >= 0; __i-- { if ${elementEqualsFor(elementType, `${obj}[__i]`, '__x')} { return float64(__i) } }; return float64(-1) }()`;
5072
6192
  },
5073
6193
  // Sorts in place and returns the array; without a comparator, compares as strings (like JS)
5074
- sort: (obj, args) => {
5075
- importedPackages.add('sort');
5076
- const arrayType = currentReceiverGoType?.startsWith('[]') ? currentReceiverGoType : '[]interface{}';
5077
- let less = `(${args[0]})(__s[i], __s[j]) < 0`;
5078
- if (!args[0]) {
5079
- importedPackages.add('fmt');
5080
- less = `fmt.Sprintf("%v", __s[i]) < fmt.Sprintf("%v", __s[j])`;
5081
- }
5082
- return `func() ${arrayType} { __s := ${obj}; sort.SliceStable(__s, func(i, j int) bool { return ${less} }); return __s }()`;
5083
- },
6194
+ sort: (obj, args) => sortArray(obj, args),
5084
6195
  indexOf: (obj, args) => `func() float64 { __x := ${args[0]}; for __i, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return float64(__i) } }; return float64(-1) }()`,
5085
6196
  includes: (obj, args) => `func() bool { __x := ${args[0]}; for _, __v := range ${obj} { if ${elementEquals('__v', '__x')} { return true } }; return false }()`,
5086
- concat: (obj, args) => `append(${obj}, ${args.join(', ')}...)`,
6197
+ // concat(a, [b, c], d): arrays are flattened one level, other values appended
6198
+ concat: (obj, args) => {
6199
+ const parts = args.map((arg, i) => currentCallArgTypes[i]?.startsWith('[]') ? `__r = append(__r, ${arg}...)` : `__r = append(__r, ${arg})`);
6200
+ return `func() []${receiverElementType()} { __r := append([]${receiverElementType()}{}, ${obj}...); ${parts.join('; ')}; return __r }()`;
6201
+ },
6202
+ toSorted: (obj, args) => {
6203
+ importedPackages.add('slices');
6204
+ return sortArray(`slices.Clone(${obj})`, args);
6205
+ },
6206
+ // copyWithin(target, start, end?): copies in place (overlap-safe) and returns the array
6207
+ copyWithin: (obj, args) => {
6208
+ const end = args[2] ? sliceIndex('__s', args[2]) : 'len(__s)';
6209
+ return `func() []${receiverElementType()} { __s := ${obj}; __t := ${sliceIndex('__s', args[0] ?? '0')}; __start := ${sliceIndex('__s', args[1] ?? '0')}; __end := ${end}; if __start < __end { copy(__s[__t:], __s[__start:__end]) }; return __s }()`;
6210
+ },
6211
+ // toSpliced(start, deleteCount?, ...items): a spliced copy
6212
+ toSpliced: (obj, args) => {
6213
+ importedPackages.add('slices');
6214
+ const elementType = receiverElementType();
6215
+ const end = args.length >= 2 ? `min(__start+max(int(${args[1]}), 0), len(__s))` : 'len(__s)';
6216
+ return `func() []${elementType} { __s := ${obj}; __start := ${sliceIndex('__s', args[0] ?? '0')}; return slices.Concat(__s[:__start:__start], []${elementType}{${args.slice(2).join(', ')}}, __s[${end}:]) }()`;
6217
+ },
6218
+ toReversed: (obj) => {
6219
+ importedPackages.add('slices');
6220
+ return `func() []${receiverElementType()} { __s := slices.Clone(${obj}); slices.Reverse(__s); return __s }()`;
6221
+ },
6222
+ with: (obj, args) => {
6223
+ importedPackages.add('slices');
6224
+ return `func() []${receiverElementType()} { __s := slices.Clone(${obj}); __s[${sliceIndex('__s', args[0])}] = ${args[1]}; return __s }()`;
6225
+ },
5087
6226
  // [][]T → []T (one level, like JS's default depth)
5088
6227
  flat: (obj) => {
5089
6228
  const elementType = receiverElementType();
@@ -5148,12 +6287,25 @@ const setMethodHandlers = {
5148
6287
  has: (obj, args) => `${obj}.Has(${args[0]})`,
5149
6288
  delete: (obj, args) => `${obj}.Delete(${args[0]})`,
5150
6289
  clear: (obj) => `${obj}.Clear()`,
5151
- values: (obj) => `${obj}.Keys()`
6290
+ values: (obj) => `${obj}.Keys()`,
6291
+ union: (obj, args) => `${obj}.Union(${args[0]})`,
6292
+ intersection: (obj, args) => `${obj}.Intersection(${args[0]})`,
6293
+ difference: (obj, args) => `${obj}.Difference(${args[0]})`,
6294
+ symmetricDifference: (obj, args) => `${obj}.SymmetricDifference(${args[0]})`,
6295
+ isSubsetOf: (obj, args) => `${obj}.IsSubsetOf(${args[0]})`,
6296
+ isSupersetOf: (obj, args) => `${args[0]}.IsSubsetOf(${obj})`,
6297
+ isDisjointFrom: (obj, args) => `(${obj}.Intersection(${args[0]}).Len() == 0)`
5152
6298
  };
5153
6299
  function getDynamicCallHandler(caller, objectType) {
5154
6300
  if (promiseResolveName && caller === promiseResolveName) {
5155
6301
  return (_caller, args) => `ch <- ${args[0]}`;
5156
6302
  }
6303
+ if (promiseRejectName && caller === promiseRejectName) {
6304
+ return (_caller, args) => {
6305
+ useHelper('promise');
6306
+ return `TnReject(ch, ${args[0] ?? 'nil'})`;
6307
+ };
6308
+ }
5157
6309
  const dotIndex = caller.lastIndexOf('.');
5158
6310
  if (dotIndex !== -1) {
5159
6311
  const methodName = caller.substring(dotIndex + 1);
@@ -5168,6 +6320,12 @@ function getDynamicCallHandler(caller, objectType) {
5168
6320
  return jsStringOf(obj, objectType === 'number' || objectType === 'float64' ? 'float64' : undefined);
5169
6321
  };
5170
6322
  }
6323
+ if (methodName === 'toPrecision') {
6324
+ return (c, args) => {
6325
+ importedPackages.add('strconv');
6326
+ return `strconv.FormatFloat(${c.substring(0, dotIndex)}, 'g', int(${args[0] ?? '-1'}), 64)`;
6327
+ };
6328
+ }
5171
6329
  // number.toFixed(digits)
5172
6330
  if (methodName === 'toFixed') {
5173
6331
  return (c, args) => {
@@ -5289,7 +6447,9 @@ function isObjectKeysOfDynamic(node) {
5289
6447
  isDynamicValue(node.arguments[0]));
5290
6448
  }
5291
6449
  const ARRAY_ONLY_METHODS = new Set([
5292
- 'map', 'filter', 'some', 'every', 'find', 'findIndex', 'forEach', 'reduce', 'join', 'flat', 'sort', 'reverse'
6450
+ 'map', 'filter', 'some', 'every', 'find', 'findIndex', 'findLast', 'findLastIndex', 'forEach', 'reduce',
6451
+ 'reduceRight', 'join', 'flat', 'flatMap', 'sort', 'reverse', 'toSorted', 'toReversed', 'fill', 'splice',
6452
+ 'copyWithin', 'toSpliced'
5293
6453
  ]);
5294
6454
  const STRING_ONLY_METHODS = new Set([
5295
6455
  'startsWith', 'endsWith', 'trim', 'trimStart', 'trimEnd', 'toUpperCase', 'toLowerCase', 'split',
@@ -5341,7 +6501,7 @@ function getSortComparatorElementType(fn) {
5341
6501
  const call = fn.parent;
5342
6502
  if (!isCallExpression(call) || call.arguments?.[0] !== fn)
5343
6503
  return undefined;
5344
- if (!isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'sort') {
6504
+ if (!isPropertyAccessExpression(call.expression) || !['sort', 'toSorted'].includes(call.expression.name.text)) {
5345
6505
  return undefined;
5346
6506
  }
5347
6507
  const arrayType = inferExpressionType(call.expression.expression);
@@ -5441,6 +6601,9 @@ function jsRegexFlagsToGo(flags) {
5441
6601
  // groups to RE2 syntax: (?<name> → (?P<name>
5442
6602
  function translateJsRegexPattern(pattern) {
5443
6603
  return pattern
6604
+ // \u{1F600} / \u00e9 → RE2's \x{…}
6605
+ .replace(/\\u\{([0-9A-Fa-f]+)\}/g, '\\x{$1}')
6606
+ .replace(/\\u([0-9A-Fa-f]{4})/g, '\\x{$1}')
5444
6607
  .replace(/\\/g, '\\\\')
5445
6608
  .replace(/"/g, '\\"')
5446
6609
  .replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, '(?P<$1>');
@@ -5449,12 +6612,267 @@ function translateJsRegexPattern(pattern) {
5449
6612
  // Named groups (?<name> are fine (translated to (?P<name>), so strip them
5450
6613
  // before testing for lookarounds (?= (?! (?<= (?<!
5451
6614
  function needsStatefulRegex(pattern, flags) {
5452
- if (flags.includes('g'))
6615
+ // /g and /y keep lastIndex state; /u keeps its flag for .flags
6616
+ if (flags.includes('g') || flags.includes('y') || flags.includes('u'))
5453
6617
  return true;
5454
6618
  const stripped = pattern.replace(/\(\?<[A-Za-z_][A-Za-z0-9_]*>/g, '');
5455
6619
  // (?= (?! and any remaining (?< (lookbehind) need the backtracker
5456
6620
  return /\\[1-9]|\(\?[=!<]/.test(stripped);
5457
6621
  }
6622
+ const REGEX_FLAG_PROPERTIES = {
6623
+ global: 'g', ignoreCase: 'i', multiline: 'm', dotAll: 's', unicode: 'u', sticky: 'y'
6624
+ };
6625
+ function inferRegexPropertyType(expr) {
6626
+ if (!isPropertyAccessExpression(expr))
6627
+ return undefined;
6628
+ const receiverType = inferExpressionType(expr.expression);
6629
+ if (receiverType !== '*TnRegex' && receiverType !== '*regexp.Regexp')
6630
+ return undefined;
6631
+ const name = expr.name.text;
6632
+ if (name === 'lastIndex')
6633
+ return 'float64';
6634
+ if (name === 'source' || name === 'flags')
6635
+ return 'string';
6636
+ if (REGEX_FLAG_PROPERTIES[name])
6637
+ return 'bool';
6638
+ return undefined;
6639
+ }
6640
+ function visitRegexProperty(node) {
6641
+ const type = inferRegexPropertyType(node);
6642
+ if (!type || isCallee(node))
6643
+ return undefined;
6644
+ const name = node.name.text;
6645
+ const receiver = visit(node.expression);
6646
+ useHelper('fancyRegex');
6647
+ if (inferExpressionType(node.expression) === '*TnRegex') {
6648
+ if (name === 'lastIndex' || name === 'source' || name === 'flags')
6649
+ return `${receiver}.${name}`;
6650
+ importedPackages.add('strings');
6651
+ return `strings.Contains(${receiver}.flags, "${REGEX_FLAG_PROPERTIES[name]}")`;
6652
+ }
6653
+ // plain RE2 regexes have no state; their flags (i/m/s) lead the pattern
6654
+ if (name === 'lastIndex')
6655
+ return 'float64(0)';
6656
+ if (name === 'source')
6657
+ return `TnRegexpSource(${receiver})`;
6658
+ if (name === 'flags')
6659
+ return `TnRegexpFlags(${receiver})`;
6660
+ importedPackages.add('strings');
6661
+ return `strings.Contains(TnRegexpFlags(${receiver}), "${REGEX_FLAG_PROPERTIES[name]}")`;
6662
+ }
6663
+ // Whether an expression is used as a value (argument, initializer, element, assigned or returned)
6664
+ function isValuePosition(node) {
6665
+ const parent = node.parent;
6666
+ if (!parent)
6667
+ return false;
6668
+ if (isCallExpression(parent))
6669
+ return (parent.arguments ?? []).includes(node);
6670
+ if (isVariableDeclaration(parent) || isPropertyAssignment(parent))
6671
+ return parent.initializer === node;
6672
+ if (isArrayLiteralExpression(parent))
6673
+ return true;
6674
+ if (isReturnStatement(parent))
6675
+ return true;
6676
+ return isBinaryExpression(parent) && parent.operatorToken.kind === 'EqualsToken' && parent.right === node;
6677
+ }
6678
+ function isJsxNode(node) {
6679
+ return node?.kind === 'JsxElement' || node?.kind === 'JsxSelfClosingElement' || node?.kind === 'JsxFragment';
6680
+ }
6681
+ // JSX text: lines trimmed at their inner edges, blank lines dropped, joined by a space
6682
+ function cleanJsxText(text) {
6683
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
6684
+ const kept = [];
6685
+ for (let i = 0; i < lines.length; i++) {
6686
+ let line = lines[i].replace(/\t/g, ' ');
6687
+ if (i > 0)
6688
+ line = line.replace(/^ +/, '');
6689
+ if (i < lines.length - 1)
6690
+ line = line.replace(/ +$/, '');
6691
+ if (line)
6692
+ kept.push(line);
6693
+ }
6694
+ return kept.join(' ');
6695
+ }
6696
+ // The children of an element as []interface{} (text, {expressions}, nested elements)
6697
+ function visitJsxChildren(children) {
6698
+ const parts = [];
6699
+ for (const child of children) {
6700
+ if (child.kind === 'JsxText') {
6701
+ const text = cleanJsxText(child.text ?? '');
6702
+ if (text)
6703
+ parts.push(toGoStringLiteral(text));
6704
+ }
6705
+ else if (child.kind === 'JsxExpression') {
6706
+ if (child.expression)
6707
+ parts.push(visit(child.expression));
6708
+ }
6709
+ else {
6710
+ parts.push(visit(child));
6711
+ }
6712
+ }
6713
+ return `[]interface{}{${parts.join(', ')}}`;
6714
+ }
6715
+ // An attribute's value: name="x", name={expr}, or a bare name (true)
6716
+ function getJsxAttributeValue(attribute) {
6717
+ if (!attribute.initializer) {
6718
+ const trueNode = { kind: 'TrueKeyword', parent: attribute };
6719
+ return trueNode;
6720
+ }
6721
+ return attribute.initializer.kind === 'JsxExpression' ? attribute.initializer.expression : attribute.initializer;
6722
+ }
6723
+ function visitJsx(node) {
6724
+ useHelper('jsx');
6725
+ if (node.kind === 'JsxFragment')
6726
+ return `TnJsxChildren(${visitJsxChildren(node.children ?? [])})`;
6727
+ const opening = node.kind === 'JsxElement' ? node.openingElement : node;
6728
+ const children = node.kind === 'JsxElement' ? (node.children ?? []) : [];
6729
+ const tag = opening.tagName;
6730
+ const attributes = (opening.attributes?.properties ?? []).filter((a) => a.kind === 'JsxAttribute');
6731
+ // <div …> / <my-element …>: an HTML element
6732
+ if (isIdentifier(tag) && (/^[a-z]/.test(tag.text) || tag.text.includes('-'))) {
6733
+ const attrs = attributes.map((a) => `{${toGoStringLiteral(a.name.text)}, ${visit(getJsxAttributeValue(a))}}`);
6734
+ return `TnJsxElement(${toGoStringLiteral(tag.text)}, []TnJsxAttr{${attrs.join(', ')}}, ${visitJsxChildren(children)})`;
6735
+ }
6736
+ // <Card title="x">…</Card> → Card({ title: "x", children: … })
6737
+ const call = { kind: 'CallExpression', expression: tag, arguments: [], parent: node.parent };
6738
+ tag.parent = call;
6739
+ const properties = [];
6740
+ for (const a of attributes) {
6741
+ const name = { kind: 'Identifier', text: a.name.text };
6742
+ const property = { kind: 'PropertyAssignment', name, initializer: getJsxAttributeValue(a) };
6743
+ property.initializer.parent = property;
6744
+ name.parent = property;
6745
+ properties.push(property);
6746
+ }
6747
+ const meaningful = children.filter((c) => c.kind !== 'JsxText' || cleanJsxText(c.text ?? ''));
6748
+ if (meaningful.length > 0) {
6749
+ const name = { kind: 'Identifier', text: 'children' };
6750
+ const fragment = { kind: 'JsxFragment', children };
6751
+ const property = { kind: 'PropertyAssignment', name, initializer: fragment };
6752
+ fragment.parent = property;
6753
+ name.parent = property;
6754
+ properties.push(property);
6755
+ }
6756
+ const props = { kind: 'ObjectLiteralExpression', properties, parent: call };
6757
+ for (const property of properties)
6758
+ property.parent = props;
6759
+ const declared = isIdentifier(tag) ? declaredFunctions.get(tag.text) : undefined;
6760
+ if (properties.length > 0 || (declared?.parameters ?? []).length > 0) {
6761
+ const args = [props];
6762
+ call.arguments = args;
6763
+ }
6764
+ return visit(call);
6765
+ }
6766
+ function getDecorators(node) {
6767
+ return (node.modifiers ?? []).filter((m) => m.kind === 'Decorator');
6768
+ }
6769
+ // `this` inside a function / function expression (not a class member, not an arrow's)
6770
+ function isThisOfPlainFunction(node) {
6771
+ for (let n = node.parent; n; n = n.parent) {
6772
+ if (isArrowFunction(n))
6773
+ continue;
6774
+ if (isFunctionExpression(n) || isFunctionDeclaration(n))
6775
+ return !isPropertyAssignment(n.parent);
6776
+ if (isMethodDeclaration(n) || isConstructorDeclaration(n) || isPropertyDeclaration(n) || isGetAccessor(n) || isSetAccessor(n) || isClassDeclaration(n))
6777
+ return false;
6778
+ }
6779
+ return false;
6780
+ }
6781
+ // f.call(thisArg, a, b) → f(a, b); f.apply(thisArg, args) → f(args...) (this is TnThis)
6782
+ function visitFunctionCallApply(node) {
6783
+ if (!isPropertyAccessExpression(node.expression))
6784
+ return undefined;
6785
+ const method = node.expression.name.text;
6786
+ if (method !== 'call' && method !== 'apply')
6787
+ return undefined;
6788
+ if (!inferExpressionType(node.expression.expression)?.startsWith('func('))
6789
+ return undefined;
6790
+ const fn = visit(node.expression.expression);
6791
+ const args = (node.arguments ?? []).slice(1);
6792
+ if (method === 'apply')
6793
+ return `${fn}(${args[0] ? `${visit(args[0])}...` : ''})`;
6794
+ return `${fn}(${args.map((a) => (isSpreadElement(a) ? `${visit(a.expression)}...` : visit(a))).join(', ')})`;
6795
+ }
6796
+ // Whether a decorator call returns a value (a replacement), or only has effects
6797
+ function decoratorReturnsValue(decorator) {
6798
+ let fnType = inferExpressionType(decorator.expression);
6799
+ // @factory(args): the decorator is what the factory returns
6800
+ const factoryCall = decorator.expression;
6801
+ if (isCallExpression(factoryCall) && isIdentifier(factoryCall.expression)) {
6802
+ const factory = declaredFunctions.get(factoryCall.expression.text);
6803
+ if (factory)
6804
+ fnType = factory.type ? getType(factory.type) : inferFunctionBodyReturnType(factory);
6805
+ }
6806
+ if (fnType?.startsWith('func('))
6807
+ return !!goFuncReturnType(fnType);
6808
+ const call = { kind: 'CallExpression', expression: decorator.expression, arguments: [], parent: decorator };
6809
+ const returnType = inferExpressionType(call);
6810
+ return !!returnType && returnType !== 'struct{}';
6811
+ }
6812
+ // The decorated method m: m__undecorated holds the body; m calls the decorated function
6813
+ // (Owner_m__decorated) with TnThis set to the receiver
6814
+ function emitDecoratedMethod(className, typeParamNames, member, ownerClass) {
6815
+ useHelper('decorators');
6816
+ useHelper('dynamic');
6817
+ const methodName = visit(member.name);
6818
+ const info = getFunctionParametersInfo(member.parameters ?? []);
6819
+ if (info.signature.includes('...'))
6820
+ throw new Error(`decorated method ${methodName}: rest/default parameters are not supported`);
6821
+ const names = splitGoParameters(info.signature).map((p) => p.slice(0, p.indexOf(' ')));
6822
+ const returnType = member.type ? getType(member.type) : '';
6823
+ const result = returnType ? ` ${returnType}` : '';
6824
+ const body = visit(member.body, { prefixBlockContent: info.prefixBlockContent });
6825
+ const call = `${ownerClass}_${methodName}__decorated(${names.join(', ')})`;
6826
+ const invoke = returnType ? `return TnAs[${returnType}](${call})` : call;
6827
+ if ((member.modifiers ?? []).some((m) => m.kind === 'StaticKeyword')) {
6828
+ return `func ${className}_${methodName}__undecorated(${info.signature})${result} ${body}\n\nfunc ${className}_${methodName}(${info.signature})${result} {\n\t\t${invoke}\n\t}\n\n`;
6829
+ }
6830
+ return (`func (self *${className}${typeParamNames}) ${methodName}__undecorated(${info.signature})${result} ${body}\n\n` +
6831
+ `func (self *${className}${typeParamNames}) ${methodName}(${info.signature})${result} {\n\t\t__prevThis := TnThis\n\t\tTnThis = self\n\t\tdefer func() { TnThis = __prevThis }()\n\t\t${invoke}\n\t}\n\n`);
6832
+ }
6833
+ // Applies a class's decorators where it is defined (in source order): each decorated
6834
+ // method's function is wrapped bottom-up, then class decorators run
6835
+ function visitClassDecorators(node) {
6836
+ const className = visit(node.name);
6837
+ const statements = [];
6838
+ for (const member of node.members ?? []) {
6839
+ const decorators = getDecorators(member);
6840
+ if (decorators.length === 0)
6841
+ continue;
6842
+ if (!isMethodDeclaration(member) || !member.body)
6843
+ throw new Error(`${className}: only method and class decorators are supported`);
6844
+ useHelper('decorators');
6845
+ useHelper('dynamic');
6846
+ const methodName = visit(member.name);
6847
+ const isStatic = (member.modifiers ?? []).some((m) => m.kind === 'StaticKeyword');
6848
+ const isPrivate = member.name?.kind === 'PrivateIdentifier';
6849
+ const params = splitGoParameters(getFunctionParametersInfo(member.parameters ?? []).signature);
6850
+ const paramTypes = params.map((p) => p.slice(p.indexOf(' ') + 1));
6851
+ const returnType = member.type ? getType(member.type) : '';
6852
+ const varName = `${className}_${methodName}__decorated`;
6853
+ mainPackageVariables.push(`var ${varName} func(...interface{}) interface{}`);
6854
+ const args = paramTypes.map((t, i) => `TnAs[${t}](__args[${i}])`).join(', ');
6855
+ const target = isStatic
6856
+ ? `${className}_${methodName}__undecorated(${args})`
6857
+ : `TnThis.(interface{ ${methodName}__undecorated(${paramTypes.join(', ')})${returnType ? ` ${returnType}` : ''} }).${methodName}__undecorated(${args})`;
6858
+ statements.push(`${varName} = func(__args ...interface{}) interface{} { ${returnType ? `return ${target}` : `${target}; return nil`} }`);
6859
+ for (const decorator of [...decorators].reverse()) {
6860
+ const context = `&TnDecoratorContext{kind: "method", name: ${toGoStringLiteral(isPrivate ? member.name.text : methodName)}, static: ${isStatic}, private: ${isPrivate}}`;
6861
+ const call = `(${visit(decorator.expression)})(${varName}, ${context})`;
6862
+ statements.push(decoratorReturnsValue(decorator) ? `${varName} = TnDecorate(${varName}, ${call})` : call);
6863
+ }
6864
+ }
6865
+ for (const decorator of [...getDecorators(node)].reverse()) {
6866
+ useHelper('decorators');
6867
+ const context = `&TnDecoratorContext{kind: "class", name: ${toGoStringLiteral(className)}}`;
6868
+ const call = `(${visit(decorator.expression)})(New${className}, ${context})`;
6869
+ statements.push(decoratorReturnsValue(decorator) ? `TnDecorateClass(${call})` : call);
6870
+ }
6871
+ return statements.map((statement) => `${statement};\n\t`).join('');
6872
+ }
6873
+ function isStringRawTag(tag) {
6874
+ return isPropertyAccessExpression(tag) && isIdentifier(tag.expression) && tag.expression.text === 'String' && tag.name.text === 'raw';
6875
+ }
5458
6876
  // Whether a visited regex argument is the stateful wrapper type
5459
6877
  function isTnRegexArg(arg) {
5460
6878
  return arg.startsWith('TnRegexCompile(') || variableGoTypes.get(arg) === '*TnRegex';
@@ -5465,7 +6883,8 @@ function getTypeParameters(typeParameters) {
5465
6883
  const params = typeParameters.map((tp) => {
5466
6884
  const name = visit(tp.name);
5467
6885
  const constraint = tp.constraint ? getType(tp.constraint) : 'any';
5468
- return `${name} ${constraint}`;
6886
+ // Go constraints cannot require fields: object-type bounds become any
6887
+ return `${name} ${constraint.startsWith('*') ? 'any' : constraint}`;
5469
6888
  });
5470
6889
  return `[${params.join(', ')}]`;
5471
6890
  }
@@ -5626,6 +7045,14 @@ function visitCallArguments(node) {
5626
7045
  return result;
5627
7046
  }
5628
7047
  function withContextualParameters(fn, info) {
7048
+ if (usesArgumentsObject(fn) && !info.signature.includes('...')) {
7049
+ const names = (fn.parameters ?? []).map((p) => getParameterName(p, fn.parameters));
7050
+ registerLocalVariable('arguments', '[]interface{}');
7051
+ return {
7052
+ signature: [info.signature, '__arguments ...interface{}'].filter((part) => part).join(', '),
7053
+ prefixBlockContent: `${info.prefixBlockContent}arguments := append([]interface{}{${names.join(', ')}}, __arguments...)\n\t\t_ = arguments\n\t\t`
7054
+ };
7055
+ }
5629
7056
  const contextualFn = getContextualFunctionType(fn);
5630
7057
  const declaredCount = (fn.parameters ?? []).length;
5631
7058
  const contextualParams = contextualFn?.parameters ?? [];
@@ -5637,6 +7064,16 @@ function withContextualParameters(fn, info) {
5637
7064
  const signature = [info.signature, ...extra].filter((part) => part).join(', ');
5638
7065
  return { ...info, signature };
5639
7066
  }
7067
+ // A non-arrow function whose own body reads `arguments` (arrows see their parent's)
7068
+ function usesArgumentsObject(fn) {
7069
+ if (isArrowFunction(fn) || !fn.body)
7070
+ return false;
7071
+ return containsNode(fn.body, (n) => {
7072
+ if (n !== fn.body && (isFunctionDeclaration(n) || isFunctionExpression(n) || isMethodDeclaration(n)))
7073
+ return false;
7074
+ return isIdentifier(n) && n.text === 'arguments' && !isFieldNameIdentifier(n);
7075
+ });
7076
+ }
5640
7077
  // Destructured parameters ({ a, b }: T / [x, y]: T[]) get a temporary name
5641
7078
  function getParameterName(param, parameters) {
5642
7079
  if (isIdentifier(param.name))
@@ -5737,7 +7174,8 @@ function getPromiseChannelType(node) {
5737
7174
  while (parent) {
5738
7175
  if (isFunctionDeclaration(parent) ||
5739
7176
  isMethodDeclaration(parent) ||
5740
- isFunctionExpression(parent)) {
7177
+ isFunctionExpression(parent) ||
7178
+ isArrowFunction(parent)) {
5741
7179
  if (parent.type &&
5742
7180
  isTypeReferenceNode(parent.type) &&
5743
7181
  isIdentifier(parent.type.typeName)) {
@@ -5764,6 +7202,9 @@ function emitChannelFunction(node, isAsync) {
5764
7202
  }
5765
7203
  const chanType = isAsync ? getPromiseChannelType(node) : getGeneratorYieldType(node);
5766
7204
  const channelVar = '__ch';
7205
+ if (isAsync)
7206
+ useHelper('promise');
7207
+ const rejectOnPanic = isAsync ? `\n\t\tdefer TnRejectOnPanic(${channelVar})` : '';
5767
7208
  const prevChannel = asyncChannelVar;
5768
7209
  const prevFn = asyncFunctionNode;
5769
7210
  asyncChannelVar = channelVar;
@@ -5771,7 +7212,7 @@ function emitChannelFunction(node, isAsync) {
5771
7212
  const body = visit(node.body, { prefixBlockContent: parameterInfo.prefixBlockContent });
5772
7213
  asyncChannelVar = prevChannel;
5773
7214
  asyncFunctionNode = prevFn;
5774
- return (`func ${name}${typeParams}(${parameterInfo.signature}) chan ${chanType} {\n\t${channelVar} := make(chan ${chanType})\n\tgo func() {\n\t\tdefer close(${channelVar})\n\t\t${body.trim()}\n\t}()\n\treturn ${channelVar}\n}`);
7215
+ return (`func ${name}${typeParams}(${parameterInfo.signature}) chan ${chanType} {\n\t${channelVar} := make(chan ${chanType})\n\tgo func() {\n\t\tdefer close(${channelVar})${rejectOnPanic}\n\t\t${body.trim()}\n\t}()\n\treturn ${channelVar}\n}`);
5775
7216
  }
5776
7217
  // The type a generator yields: from its first yield expression's operand
5777
7218
  function getGeneratorYieldType(fn) {
@@ -5793,10 +7234,14 @@ function visitPromiseReturn(node, options) {
5793
7234
  const channelType = getPromiseChannelType(node);
5794
7235
  const resolveParam = callback.parameters[0];
5795
7236
  const resolveName = resolveParam ? visit(resolveParam.name) : '';
7237
+ const rejectParam = callback.parameters[1];
5796
7238
  const prevResolveName = promiseResolveName;
7239
+ const prevRejectName = promiseRejectName;
5797
7240
  promiseResolveName = resolveName;
7241
+ promiseRejectName = rejectParam ? visit(rejectParam.name) : '';
5798
7242
  const body = isBlock(callback.body) ? visit(callback.body) : `{ ${visit(callback.body)} }`;
5799
7243
  promiseResolveName = prevResolveName;
7244
+ promiseRejectName = prevRejectName;
5800
7245
  return (`ch := make(chan ${channelType})\n\t\tgo func() ${body.trimEnd()}()\n\t\treturn ch` +
5801
7246
  (options.inline ? '' : ';\n\t'));
5802
7247
  }
@@ -5808,10 +7253,14 @@ function visitNewPromise(node) {
5808
7253
  const channelType = getPromiseChannelType(node);
5809
7254
  const resolveParam = callback.parameters[0];
5810
7255
  const resolveName = resolveParam ? visit(resolveParam.name) : '';
7256
+ const rejectParam = callback.parameters[1];
5811
7257
  const prevResolveName = promiseResolveName;
7258
+ const prevRejectName = promiseRejectName;
5812
7259
  promiseResolveName = resolveName;
7260
+ promiseRejectName = rejectParam ? visit(rejectParam.name) : '';
5813
7261
  const body = isBlock(callback.body) ? visit(callback.body) : `{ ${visit(callback.body)} }`;
5814
7262
  promiseResolveName = prevResolveName;
7263
+ promiseRejectName = prevRejectName;
5815
7264
  return `func() chan ${channelType} {\n\t\tch := make(chan ${channelType})\n\t\tgo func() ${body.trimEnd()}()\n\t\treturn ch;\n\t}()`;
5816
7265
  }
5817
7266
  function extractMapValueType(mapType) {
@@ -5865,8 +7314,8 @@ function visitNewSet(node) {
5865
7314
  elementType = getArrayLiteralElementType(node.arguments[0]);
5866
7315
  }
5867
7316
  const args = node.arguments ?? [];
5868
- // new Set(otherSet/values()) — copy at runtime
5869
- if (args.length > 0 && !isArrayLiteralExpression(args[0])) {
7317
+ // new Set(otherSet/values()) / new Set([...a, ...b]) — copy at runtime
7318
+ if (args.length > 0 && (!isArrayLiteralExpression(args[0]) || (args[0].elements ?? []).some((e) => isSpreadElement(e)))) {
5870
7319
  useHelper('orderedMap');
5871
7320
  const src = getTempName('setsrc');
5872
7321
  const item = getTempName('item');
@@ -5901,9 +7350,11 @@ function normalizeCjsToEsm(code) {
5901
7350
  code = code.replace(/(?:module\.exports|exports)\.(\w+)\s*=\s*function\s*\w*\s*\(/g, 'export function $1(');
5902
7351
  return code;
5903
7352
  }
5904
- function includeLocalImport(code, dir, goFileName) {
7353
+ function includeLocalImport(code, dir, goFileName, tsx = false) {
5905
7354
  const prevDir = currentFileDir;
7355
+ const prevTsx = currentFileIsTsx;
5906
7356
  currentFileDir = dir;
7357
+ currentFileIsTsx = tsx;
5907
7358
  // Normalize CommonJS modules to ES module syntax before parsing
5908
7359
  if (!code.includes('export ') && (code.includes('module.exports') || code.includes('exports.'))) {
5909
7360
  code = normalizeCjsToEsm(code);
@@ -5915,6 +7366,7 @@ function includeLocalImport(code, dir, goFileName) {
5915
7366
  visit(stmt, { addFunctionOutside: true });
5916
7367
  }
5917
7368
  currentFileDir = prevDir;
7369
+ currentFileIsTsx = prevTsx;
5918
7370
  return;
5919
7371
  }
5920
7372
  // Separate-file mode (local TS imports): transpile to its own Go file.
@@ -5936,7 +7388,13 @@ function includeLocalImport(code, dir, goFileName) {
5936
7388
  }
5937
7389
  emittingModuleFile = prevEmittingModuleFile;
5938
7390
  const fileOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
5939
- const fileInline = inlineLines.join('\n');
7391
+ const isDeclarationLine = (line) => /^(var|const|type|func|import)\b/.test(line.trim());
7392
+ const declarationLines = inlineLines.filter(isDeclarationLine);
7393
+ const statementLines = inlineLines.filter((line) => !isDeclarationLine(line));
7394
+ const fileInline = [
7395
+ ...declarationLines,
7396
+ ...(statementLines.length > 0 ? [`func init() {\n\t${statementLines.join('\n\t')}\n}`] : [])
7397
+ ].join('\n');
5940
7398
  // Computed after all visits: function bodies (outside nodes) register
5941
7399
  // packages too, and imports must be captured after those visits.
5942
7400
  // Helper-provided packages are only imported here if the file's own code
@@ -5967,6 +7425,7 @@ function includeLocalImport(code, dir, goFileName) {
5967
7425
  importedPackages.clear();
5968
7426
  for (const p of savedPackages)
5969
7427
  importedPackages.add(p);
7428
+ currentFileIsTsx = prevTsx;
5970
7429
  for (const p of helperProvidedPackages)
5971
7430
  importedPackages.add(p);
5972
7431
  currentFileDir = prevDir;
@@ -6029,7 +7488,13 @@ function needPkg(pkg) {
6029
7488
  // since the import list is serialized before helpers are emitted.
6030
7489
  const helperRequires = {
6031
7490
  objectAssign: ['orderedMap'],
6032
- promiseAll: ['orderedMap', 'asyncWait'],
7491
+ promiseAll: ['orderedMap', 'asyncWait', 'promise'],
7492
+ promise: ['error'],
7493
+ decorators: [],
7494
+ jsx: [],
7495
+ objectIntegrity: [],
7496
+ structuredClone: [],
7497
+ globalThis: ['dynamic'],
6033
7498
  namedGroups: ['orderedMap'],
6034
7499
  fancyRegex: ['orderedMap'],
6035
7500
  dynamic: ['error'],
@@ -6205,7 +7670,7 @@ const helperPackages = {
6205
7670
  jsonStringify: ['encoding/json', 'bytes', 'strings', 'unsafe'],
6206
7671
  objectAssign: ['reflect', 'unsafe'],
6207
7672
  typeOf: ['reflect'],
6208
- dynamic: ['math', 'strings', 'fmt', 'sort', 'reflect', 'strconv', 'regexp'],
7673
+ dynamic: ['math', 'strings', 'fmt', 'sort', 'reflect', 'strconv', 'regexp', 'unsafe'],
6209
7674
  regexReplaceFunc: ['regexp'],
6210
7675
  readFile: ['os'],
6211
7676
  writeFile: ['os'],
@@ -6225,7 +7690,14 @@ const helperPackages = {
6225
7690
  moduleUrl: ['os'],
6226
7691
  asyncWait: ['sync'],
6227
7692
  timerWait: ['sync', 'time'],
7693
+ uri: ['strings', 'fmt', 'net/url'],
6228
7694
  promiseAll: [],
7695
+ promise: ['sync'],
7696
+ decorators: [],
7697
+ jsx: ['strings', 'html', 'reflect', 'strconv', 'fmt'],
7698
+ objectIntegrity: ['sync', 'reflect'],
7699
+ structuredClone: ['reflect', 'unsafe'],
7700
+ globalThis: [],
6229
7701
  exec: ['os/exec', 'bytes', 'runtime'],
6230
7702
  execSync: ['os/exec', 'bytes', 'runtime'],
6231
7703
  runInherit: ['os/exec', 'os'],
@@ -6256,6 +7728,18 @@ func TnGet(obj interface{}, key string) interface{} {
6256
7728
  return o.message
6257
7729
  }
6258
7730
  }
7731
+ rv := reflect.ValueOf(obj)
7732
+ if rv.Kind() == reflect.Pointer && !rv.IsNil() && rv.Elem().Kind() == reflect.Struct {
7733
+ for _, name := range []string{key, key + "_"} {
7734
+ if f := rv.Elem().FieldByName(name); f.IsValid() {
7735
+ // Unexported field: read it through a pointer to bypass the flag
7736
+ return reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem().Interface()
7737
+ }
7738
+ }
7739
+ }
7740
+ if key == "length" {
7741
+ return TnLength(obj)
7742
+ }
6259
7743
  return nil
6260
7744
  }
6261
7745
 
@@ -6267,6 +7751,22 @@ func TnSet(obj interface{}, key string, value interface{}) {
6267
7751
  panic("TnSet: cannot set property " + key)
6268
7752
  }
6269
7753
 
7754
+ func TnSetIndex(obj interface{}, key interface{}, value interface{}) {
7755
+ switch o := obj.(type) {
7756
+ case map[string]interface{}:
7757
+ if k, ok := key.(string); ok {
7758
+ o[k] = value
7759
+ return
7760
+ }
7761
+ case []interface{}:
7762
+ if i, ok := key.(float64); ok && i >= 0 && int(i) < len(o) {
7763
+ o[int(i)] = value
7764
+ return
7765
+ }
7766
+ }
7767
+ panic(fmt.Sprintf("TnSetIndex: cannot set property %v", key))
7768
+ }
7769
+
6270
7770
  func TnIndex(obj interface{}, key interface{}) interface{} {
6271
7771
  switch o := obj.(type) {
6272
7772
  case map[string]interface{}:
@@ -6324,6 +7824,9 @@ func TnLength(v interface{}) float64 {
6324
7824
  case []interface{}:
6325
7825
  return float64(len(t))
6326
7826
  }
7827
+ if rv := reflect.ValueOf(v); rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map {
7828
+ return float64(rv.Len())
7829
+ }
6327
7830
  return 0
6328
7831
  }
6329
7832
 
@@ -6466,6 +7969,19 @@ func TnDelete(obj interface{}, key interface{}) bool {
6466
7969
  return true
6467
7970
  }
6468
7971
 
7972
+ // Object.fromEntries: [key, value] pairs → a dynamic object
7973
+ func TnFromEntries(entries interface{}) interface{} {
7974
+ result := map[string]interface{}{}
7975
+ list := reflect.ValueOf(entries)
7976
+ for i := 0; i < list.Len(); i++ {
7977
+ pair := reflect.ValueOf(list.Index(i).Interface())
7978
+ if pair.Kind() == reflect.Slice && pair.Len() >= 2 {
7979
+ result[fmt.Sprint(pair.Index(0).Interface())] = pair.Index(1).Interface()
7980
+ }
7981
+ }
7982
+ return result
7983
+ }
7984
+
6469
7985
  func TnHas(obj interface{}, key interface{}) bool {
6470
7986
  if o, ok := obj.(map[string]interface{}); ok {
6471
7987
  if k, ok := key.(string); ok {
@@ -6620,14 +8136,30 @@ func TnIsNil(v interface{}) bool {
6620
8136
  return false
6621
8137
  }`,
6622
8138
  typeOf: `func TnTypeOf(v interface{}) string {
6623
- switch v.(type) {
8139
+ // an unset nullable primitive (*T nil) is undefined
8140
+ switch t := v.(type) {
6624
8141
  case nil:
6625
8142
  return "undefined"
6626
- case string, *string:
8143
+ case string:
8144
+ return "string"
8145
+ case *string:
8146
+ if t == nil {
8147
+ return "undefined"
8148
+ }
6627
8149
  return "string"
6628
- case float64, *float64:
8150
+ case float64:
6629
8151
  return "number"
6630
- case bool, *bool:
8152
+ case *float64:
8153
+ if t == nil {
8154
+ return "undefined"
8155
+ }
8156
+ return "number"
8157
+ case bool:
8158
+ return "boolean"
8159
+ case *bool:
8160
+ if t == nil {
8161
+ return "undefined"
8162
+ }
6631
8163
  return "boolean"
6632
8164
  }
6633
8165
  if reflect.ValueOf(v).Kind() == reflect.Func {
@@ -6673,7 +8205,7 @@ func tnJSONWrite(buf *bytes.Buffer, rv reflect.Value, indent string, depth int)
6673
8205
  case reflect.Float32, reflect.Float64:
6674
8206
  buf.WriteString(TnNumStr(rv.Float()))
6675
8207
  case reflect.String:
6676
- data, _ := json.Marshal(rv.String())
8208
+ data, _ := tnJSONMarshal(rv.String())
6677
8209
  buf.Write(data)
6678
8210
  case reflect.Slice, reflect.Array:
6679
8211
  if rv.Kind() == reflect.Slice && rv.IsNil() {
@@ -6704,7 +8236,7 @@ func tnJSONWrite(buf *bytes.Buffer, rv reflect.Value, indent string, depth int)
6704
8236
  buf.WriteByte(',')
6705
8237
  tnJSONNewline(buf, indent, depth+1)
6706
8238
  }
6707
- kd, _ := json.Marshal(ks)
8239
+ kd, _ := tnJSONMarshal(ks)
6708
8240
  buf.Write(kd)
6709
8241
  buf.WriteByte(':')
6710
8242
  tnJSONWrite(buf, byKey[ks], indent, depth+1)
@@ -6739,20 +8271,31 @@ func tnJSONWrite(buf *bytes.Buffer, rv reflect.Value, indent string, depth int)
6739
8271
  tnJSONNewline(buf, indent, depth+1)
6740
8272
  }
6741
8273
  first = false
6742
- kd, _ := json.Marshal(f.Name)
8274
+ kd, _ := tnJSONMarshal(f.Name)
6743
8275
  buf.Write(kd)
6744
8276
  buf.WriteByte(':')
6745
8277
  tnJSONWrite(buf, value, indent, depth+1)
6746
8278
  }
6747
8279
  buf.WriteByte('}')
6748
8280
  default:
6749
- data, err := json.Marshal(rv.Interface())
8281
+ data, err := tnJSONMarshal(rv.Interface())
6750
8282
  if err != nil {
6751
8283
  buf.WriteString("null")
6752
8284
  return
6753
8285
  }
6754
8286
  buf.Write(data)
6755
8287
  }
8288
+ }
8289
+
8290
+ // json.Marshal without HTML escaping of <, > and & (JSON.stringify does not escape them)
8291
+ func tnJSONMarshal(v interface{}) ([]byte, error) {
8292
+ var buf bytes.Buffer
8293
+ enc := json.NewEncoder(&buf)
8294
+ enc.SetEscapeHTML(false)
8295
+ if err := enc.Encode(v); err != nil {
8296
+ return nil, err
8297
+ }
8298
+ return bytes.TrimSuffix(buf.Bytes(), []byte("\\n")), nil
6756
8299
  }`,
6757
8300
  orderedMap: `// TnMap: JS Map/Set/object semantics — keys iterate in insertion order
6758
8301
  type TnMap[K comparable, V any] struct {
@@ -6811,6 +8354,51 @@ func (m *TnMap[K, V]) Delete(k K) bool {
6811
8354
  return true
6812
8355
  }
6813
8356
 
8357
+ // Set operations (ES2025), in the receiver's insertion order then the argument's
8358
+ func (m *TnMap[K, V]) Union(o *TnMap[K, V]) *TnMap[K, V] {
8359
+ r := TnNewMap[K, V]()
8360
+ for _, k := range m.keys {
8361
+ r.Set(k, m.values[k])
8362
+ }
8363
+ for _, k := range o.keys {
8364
+ r.Set(k, o.values[k])
8365
+ }
8366
+ return r
8367
+ }
8368
+
8369
+ func (m *TnMap[K, V]) Intersection(o *TnMap[K, V]) *TnMap[K, V] {
8370
+ r := TnNewMap[K, V]()
8371
+ for _, k := range m.keys {
8372
+ if o.Has(k) {
8373
+ r.Set(k, m.values[k])
8374
+ }
8375
+ }
8376
+ return r
8377
+ }
8378
+
8379
+ func (m *TnMap[K, V]) Difference(o *TnMap[K, V]) *TnMap[K, V] {
8380
+ r := TnNewMap[K, V]()
8381
+ for _, k := range m.keys {
8382
+ if !o.Has(k) {
8383
+ r.Set(k, m.values[k])
8384
+ }
8385
+ }
8386
+ return r
8387
+ }
8388
+
8389
+ func (m *TnMap[K, V]) SymmetricDifference(o *TnMap[K, V]) *TnMap[K, V] {
8390
+ return m.Difference(o).Union(o.Difference(m))
8391
+ }
8392
+
8393
+ func (m *TnMap[K, V]) IsSubsetOf(o *TnMap[K, V]) bool {
8394
+ for _, k := range m.keys {
8395
+ if !o.Has(k) {
8396
+ return false
8397
+ }
8398
+ }
8399
+ return true
8400
+ }
8401
+
6814
8402
  func (m *TnMap[K, V]) Clear() {
6815
8403
  m.keys = nil
6816
8404
  m.values = map[K]V{}
@@ -6910,15 +8498,44 @@ func TnRegexExecNamed(re *regexp.Regexp, s string) *TnMatch {
6910
8498
  type TnRegex struct {
6911
8499
  re *regexp.Regexp
6912
8500
  bt *tnBt
6913
- lastIndex int
8501
+ lastIndex float64
6914
8502
  global bool
8503
+ sticky bool
8504
+ source string
8505
+ flags string
8506
+ }
8507
+
8508
+ func TnRegexCompile(pattern string, flags string, source string) *TnRegex {
8509
+ r := &TnRegex{global: strings.Contains(flags, "g"), sticky: strings.Contains(flags, "y"), source: source, flags: flags}
8510
+ prefix := ""
8511
+ for _, f := range "ims" {
8512
+ if strings.ContainsRune(flags, f) {
8513
+ prefix += string(f)
8514
+ }
8515
+ }
8516
+ if prefix != "" {
8517
+ prefix = "(?" + prefix + ")"
8518
+ }
8519
+ if re, err := regexp.Compile(prefix + pattern); err == nil {
8520
+ r.re = re
8521
+ } else {
8522
+ r.bt = tnBtCompile(pattern)
8523
+ }
8524
+ return r
8525
+ }
8526
+
8527
+ // .source / .flags of a plain RE2 regex: flags lead the pattern as (?ims)
8528
+ var tnRegexpFlagPrefix = regexp.MustCompile("^\\\\(\\\\?([ims]+)\\\\)")
8529
+
8530
+ func TnRegexpSource(re *regexp.Regexp) string {
8531
+ return strings.ReplaceAll(tnRegexpFlagPrefix.ReplaceAllString(re.String(), ""), "(?P<", "(?<")
6915
8532
  }
6916
8533
 
6917
- func TnRegexCompile(pattern string, global bool) *TnRegex {
6918
- if re, err := regexp.Compile(pattern); err == nil {
6919
- return &TnRegex{re: re, global: global}
8534
+ func TnRegexpFlags(re *regexp.Regexp) string {
8535
+ if m := tnRegexpFlagPrefix.FindStringSubmatch(re.String()); m != nil {
8536
+ return m[1]
6920
8537
  }
6921
- return &TnRegex{bt: tnBtCompile(pattern), global: global}
8538
+ return ""
6922
8539
  }
6923
8540
 
6924
8541
  func (r *TnRegex) Test(s string) bool {
@@ -7065,15 +8682,15 @@ func (r *TnRegex) find(s string) ([]string, []int) {
7065
8682
  runs := []rune(s)
7066
8683
  if r.bt != nil {
7067
8684
  start := 0
7068
- if r.global {
7069
- if r.lastIndex > len(runs) {
8685
+ if r.global || r.sticky {
8686
+ if int(r.lastIndex) > len(runs) {
7070
8687
  r.lastIndex = 0
7071
8688
  return nil, nil
7072
8689
  }
7073
- start = r.lastIndex
8690
+ start = int(r.lastIndex)
7074
8691
  }
7075
8692
  offs := tnByteOffsets(runs)
7076
- for pos := start; pos <= len(runs); pos++ {
8693
+ for pos := start; pos <= len(runs) && (!r.sticky || pos == start); pos++ {
7077
8694
  caps := make([]int, (r.bt.ngroups+1)*2)
7078
8695
  for i := range caps {
7079
8696
  caps[i] = -1
@@ -7093,22 +8710,22 @@ func (r *TnRegex) find(s string) ([]string, []int) {
7093
8710
  loc[2*i], loc[2*i+1] = -1, -1
7094
8711
  }
7095
8712
  }
7096
- if r.global {
8713
+ if r.global || r.sticky {
7097
8714
  next := end
7098
8715
  if next == pos {
7099
8716
  next++
7100
8717
  }
7101
- r.lastIndex = next
8718
+ r.lastIndex = float64(next)
7102
8719
  }
7103
8720
  return groups, loc
7104
8721
  }
7105
8722
  }
7106
- if r.global {
8723
+ if r.global || r.sticky {
7107
8724
  r.lastIndex = 0
7108
8725
  }
7109
8726
  return nil, nil
7110
8727
  }
7111
- if !r.global {
8728
+ if !r.global && !r.sticky {
7112
8729
  loc := r.re.FindStringSubmatchIndex(s)
7113
8730
  if loc == nil {
7114
8731
  return nil, nil
@@ -7121,16 +8738,16 @@ func (r *TnRegex) find(s string) ([]string, []int) {
7121
8738
  }
7122
8739
  return groups, loc
7123
8740
  }
7124
- if r.lastIndex >= len(runs) {
8741
+ if int(r.lastIndex) >= len(runs) {
7125
8742
  r.lastIndex = 0
7126
8743
  return nil, nil
7127
8744
  }
7128
8745
  byteStart := 0
7129
- for i := 0; i < r.lastIndex; i++ {
8746
+ for i := 0; i < int(r.lastIndex); i++ {
7130
8747
  byteStart += len(string(runs[i]))
7131
8748
  }
7132
8749
  loc := r.re.FindStringSubmatchIndex(s[byteStart:])
7133
- if loc == nil {
8750
+ if loc == nil || (r.sticky && loc[0] != 0) {
7134
8751
  r.lastIndex = 0
7135
8752
  return nil, nil
7136
8753
  }
@@ -7145,16 +8762,16 @@ func (r *TnRegex) find(s string) ([]string, []int) {
7145
8762
  groups[i] = s[loc[2*i]:loc[2*i+1]]
7146
8763
  }
7147
8764
  }
7148
- endRune := r.lastIndex
8765
+ endRune := int(r.lastIndex)
7149
8766
  bo := byteStart
7150
- for ri := r.lastIndex; ri < len(runs) && bo < loc[1]; ri++ {
8767
+ for ri := int(r.lastIndex); ri < len(runs) && bo < loc[1]; ri++ {
7151
8768
  bo += len(string(runs[ri]))
7152
8769
  endRune = ri + 1
7153
8770
  }
7154
- if endRune == r.lastIndex {
8771
+ if endRune == int(r.lastIndex) {
7155
8772
  endRune++
7156
8773
  }
7157
- r.lastIndex = endRune
8774
+ r.lastIndex = float64(endRune)
7158
8775
  return groups, loc
7159
8776
  }
7160
8777
 
@@ -7926,14 +9543,480 @@ func TnNewSymbol(description string) *TnSymbol {
7926
9543
  return &TnSymbol{description: description}
7927
9544
  }`,
7928
9545
  timerWait: `// Pending setTimeout timers are waited on before the process exits
7929
- var __tnTimers sync.WaitGroup`,
9546
+ var __tnTimers sync.WaitGroup
9547
+
9548
+ func TnSetTimeout(ms float64, fn func()) *time.Timer {
9549
+ __tnTimers.Add(1)
9550
+ return time.AfterFunc(time.Duration(ms*float64(time.Millisecond)), func() {
9551
+ defer __tnTimers.Done()
9552
+ fn()
9553
+ TnRunMicrotasks()
9554
+ })
9555
+ }
9556
+
9557
+ // setInterval: fn every ms until cleared (the process lives until then)
9558
+ type TnInterval struct {
9559
+ stop chan struct{}
9560
+ once sync.Once
9561
+ }
9562
+
9563
+ func TnSetInterval(ms float64, fn func()) *TnInterval {
9564
+ __tnTimers.Add(1)
9565
+ t := &TnInterval{stop: make(chan struct{})}
9566
+ go func() {
9567
+ ticker := time.NewTicker(max(time.Duration(ms*float64(time.Millisecond)), time.Millisecond))
9568
+ defer ticker.Stop()
9569
+ for {
9570
+ select {
9571
+ case <-t.stop:
9572
+ return
9573
+ case <-ticker.C:
9574
+ fn()
9575
+ TnRunMicrotasks()
9576
+ }
9577
+ }
9578
+ }()
9579
+ return t
9580
+ }
9581
+
9582
+ func TnClearInterval(t *TnInterval) {
9583
+ if t != nil {
9584
+ t.once.Do(func() {
9585
+ close(t.stop)
9586
+ __tnTimers.Done()
9587
+ })
9588
+ }
9589
+ }
9590
+
9591
+ // queueMicrotask: run after the current synchronous code (end of main or of a timer callback)
9592
+ var tnMicrotasks []func()
9593
+ var tnMicrotasksMu sync.Mutex
9594
+
9595
+ func TnQueueMicrotask(fn func()) {
9596
+ tnMicrotasksMu.Lock()
9597
+ tnMicrotasks = append(tnMicrotasks, fn)
9598
+ tnMicrotasksMu.Unlock()
9599
+ }
9600
+
9601
+ func TnRunMicrotasks() {
9602
+ for {
9603
+ tnMicrotasksMu.Lock()
9604
+ if len(tnMicrotasks) == 0 {
9605
+ tnMicrotasksMu.Unlock()
9606
+ return
9607
+ }
9608
+ fn := tnMicrotasks[0]
9609
+ tnMicrotasks = tnMicrotasks[1:]
9610
+ tnMicrotasksMu.Unlock()
9611
+ fn()
9612
+ }
9613
+ }
9614
+
9615
+ func TnClearTimeout(t *time.Timer) {
9616
+ if t != nil && t.Stop() {
9617
+ __tnTimers.Done()
9618
+ }
9619
+ }`,
9620
+ uri: `// encodeURIComponent: escapes everything except A-Z a-z 0-9 - _ . ! ~ * ' ( )
9621
+ func TnEncodeURIComponent(s string) string {
9622
+ var b strings.Builder
9623
+ for _, c := range []byte(s) {
9624
+ if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || strings.IndexByte("-_.!~*'()", c) >= 0 {
9625
+ b.WriteByte(c)
9626
+ } else {
9627
+ fmt.Fprintf(&b, "%%%02X", c)
9628
+ }
9629
+ }
9630
+ return b.String()
9631
+ }
9632
+
9633
+ func TnDecodeURIComponent(s string) string {
9634
+ decoded, err := tnurl.PathUnescape(s)
9635
+ if err != nil {
9636
+ panic(err)
9637
+ }
9638
+ return decoded
9639
+ }`,
9640
+ objectIntegrity: `// Object.freeze / seal / preventExtensions levels, by object identity
9641
+ var tnIntegrity sync.Map
9642
+
9643
+ func tnIdentity(v interface{}) (uintptr, bool) {
9644
+ rv := reflect.ValueOf(v)
9645
+ switch rv.Kind() {
9646
+ case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan:
9647
+ return rv.Pointer(), true
9648
+ }
9649
+ return 0, false
9650
+ }
9651
+
9652
+ func TnSetIntegrity[T any](v T, level int) T {
9653
+ if id, ok := tnIdentity(v); ok {
9654
+ if prev, had := tnIntegrity.Load(id); !had || prev.(int) < level {
9655
+ tnIntegrity.Store(id, level)
9656
+ }
9657
+ }
9658
+ return v
9659
+ }
9660
+
9661
+ // Primitives are always frozen
9662
+ func TnHasIntegrity(v interface{}, level int) bool {
9663
+ id, ok := tnIdentity(v)
9664
+ if !ok {
9665
+ return true
9666
+ }
9667
+ l, had := tnIntegrity.Load(id)
9668
+ return had && l.(int) >= level
9669
+ }`,
9670
+ structuredClone: `// structuredClone: a deep copy (pointers, structs, slices, maps)
9671
+ func TnStructuredClone[T any](v T) T {
9672
+ return tnCloneValue(reflect.ValueOf(&v).Elem()).Interface().(T)
9673
+ }
9674
+
9675
+ func tnCloneValue(v reflect.Value) reflect.Value {
9676
+ switch v.Kind() {
9677
+ case reflect.Pointer:
9678
+ if v.IsNil() {
9679
+ return v
9680
+ }
9681
+ c := reflect.New(v.Elem().Type())
9682
+ c.Elem().Set(tnCloneValue(v.Elem()))
9683
+ return c
9684
+ case reflect.Struct:
9685
+ if !v.CanAddr() {
9686
+ tmp := reflect.New(v.Type()).Elem()
9687
+ tmp.Set(v)
9688
+ v = tmp
9689
+ }
9690
+ c := reflect.New(v.Type()).Elem()
9691
+ for i := 0; i < v.NumField(); i++ {
9692
+ // unexported fields are read and written through their addresses
9693
+ src := reflect.NewAt(v.Field(i).Type(), unsafe.Pointer(v.Field(i).UnsafeAddr())).Elem()
9694
+ dst := reflect.NewAt(c.Field(i).Type(), unsafe.Pointer(c.Field(i).UnsafeAddr())).Elem()
9695
+ dst.Set(tnCloneValue(src))
9696
+ }
9697
+ return c
9698
+ case reflect.Slice:
9699
+ if v.IsNil() {
9700
+ return v
9701
+ }
9702
+ c := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
9703
+ for i := 0; i < v.Len(); i++ {
9704
+ c.Index(i).Set(tnCloneValue(v.Index(i)))
9705
+ }
9706
+ return c
9707
+ case reflect.Map:
9708
+ if v.IsNil() {
9709
+ return v
9710
+ }
9711
+ c := reflect.MakeMapWithSize(v.Type(), v.Len())
9712
+ iter := v.MapRange()
9713
+ for iter.Next() {
9714
+ c.SetMapIndex(iter.Key(), tnCloneValue(iter.Value()))
9715
+ }
9716
+ return c
9717
+ case reflect.Interface:
9718
+ if v.IsNil() {
9719
+ return v
9720
+ }
9721
+ c := reflect.New(v.Type()).Elem()
9722
+ c.Set(tnCloneValue(v.Elem()))
9723
+ return c
9724
+ }
9725
+ return v
9726
+ }`,
9727
+ globalThis: `// (globalThis as any).x: properties set on the global object at runtime
9728
+ var TnGlobalThis = map[string]interface{}{}`,
9729
+ jsx: `// JSX renders to HTML: TnJsxNode is markup, plain strings are escaped text
9730
+ type TnJsxNode string
9731
+
9732
+ type TnJsxAttr struct {
9733
+ name string
9734
+ value interface{}
9735
+ }
9736
+
9737
+ var tnJsxVoidTags = map[string]bool{"area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true, "input": true, "link": true, "meta": true, "source": true, "track": true, "wbr": true}
9738
+
9739
+ func TnJsxElement(tag string, attrs []TnJsxAttr, children []interface{}) TnJsxNode {
9740
+ var b strings.Builder
9741
+ b.WriteString("<" + tag)
9742
+ for _, a := range attrs {
9743
+ name := a.name
9744
+ switch name {
9745
+ case "key", "ref":
9746
+ continue
9747
+ case "className":
9748
+ name = "class"
9749
+ case "htmlFor":
9750
+ name = "for"
9751
+ }
9752
+ if a.value == nil {
9753
+ continue
9754
+ }
9755
+ if v, ok := a.value.(bool); ok {
9756
+ if v {
9757
+ b.WriteString(" " + name)
9758
+ }
9759
+ continue
9760
+ }
9761
+ if reflect.ValueOf(a.value).Kind() == reflect.Func {
9762
+ continue
9763
+ }
9764
+ b.WriteString(" " + name + "=\\"" + html.EscapeString(tnJsxString(a.value)) + "\\"")
9765
+ }
9766
+ if tnJsxVoidTags[tag] {
9767
+ b.WriteString("/>")
9768
+ return TnJsxNode(b.String())
9769
+ }
9770
+ b.WriteString(">")
9771
+ b.WriteString(string(TnJsxChildren(children)))
9772
+ b.WriteString("</" + tag + ">")
9773
+ return TnJsxNode(b.String())
9774
+ }
9775
+
9776
+ func TnJsxChildren(children []interface{}) TnJsxNode {
9777
+ var b strings.Builder
9778
+ for _, c := range children {
9779
+ tnJsxWriteChild(&b, c)
9780
+ }
9781
+ return TnJsxNode(b.String())
9782
+ }
9783
+
9784
+ // null / booleans render nothing; arrays render each item
9785
+ func tnJsxWriteChild(b *strings.Builder, c interface{}) {
9786
+ switch v := c.(type) {
9787
+ case nil, bool:
9788
+ return
9789
+ case TnJsxNode:
9790
+ b.WriteString(string(v))
9791
+ return
9792
+ case string:
9793
+ b.WriteString(html.EscapeString(v))
9794
+ return
9795
+ }
9796
+ rv := reflect.ValueOf(c)
9797
+ switch rv.Kind() {
9798
+ case reflect.Slice:
9799
+ for i := 0; i < rv.Len(); i++ {
9800
+ tnJsxWriteChild(b, rv.Index(i).Interface())
9801
+ }
9802
+ return
9803
+ case reflect.Pointer:
9804
+ if !rv.IsNil() {
9805
+ tnJsxWriteChild(b, rv.Elem().Interface())
9806
+ }
9807
+ return
9808
+ }
9809
+ b.WriteString(html.EscapeString(tnJsxString(c)))
9810
+ }
9811
+
9812
+ func tnJsxString(v interface{}) string {
9813
+ switch t := v.(type) {
9814
+ case string:
9815
+ return t
9816
+ case TnJsxNode:
9817
+ return string(t)
9818
+ case float64:
9819
+ return strconv.FormatFloat(t, 'f', -1, 64)
9820
+ }
9821
+ return fmt.Sprint(v)
9822
+ }`,
9823
+ decorators: `// this inside plain functions: the receiver of the decorated method being called
9824
+ var TnThis interface{}
9825
+
9826
+ // The context a decorator receives (kind is "method" or "class")
9827
+ type TnDecoratorContext struct {
9828
+ kind string
9829
+ name string
9830
+ static bool
9831
+ private bool
9832
+ }
9833
+
9834
+ // A method decorator's result replaces the method; undefined keeps it
9835
+ func TnDecorate(prev func(...interface{}) interface{}, result interface{}) func(...interface{}) interface{} {
9836
+ if result == nil {
9837
+ return prev
9838
+ }
9839
+ if f, ok := result.(func(...interface{}) interface{}); ok && f != nil {
9840
+ return f
9841
+ }
9842
+ return prev
9843
+ }
9844
+
9845
+ // Class decorators may not replace a class (a Go type)
9846
+ func TnDecorateClass(result interface{}) {
9847
+ if result != nil {
9848
+ panic("class decorators cannot replace the class")
9849
+ }
9850
+ }`,
9851
+ promise: `// Promises are channels; a rejected promise is a closed channel with a reason
9852
+ var tnRejections sync.Map
9853
+
9854
+ func TnReject[T any](ch chan T, reason interface{}) {
9855
+ tnRejections.Store(ch, reason)
9856
+ close(ch)
9857
+ }
9858
+
9859
+ // Deferred in async functions: a throw rejects the function's promise
9860
+ func TnRejectOnPanic[T any](ch chan T) {
9861
+ if r := recover(); r != nil {
9862
+ tnRejections.Store(ch, r)
9863
+ }
9864
+ }
9865
+
9866
+ // Waits for a promise: its value, or its rejection reason
9867
+ func tnSettle[T any](ch chan T) (T, interface{}, bool) {
9868
+ v, ok := <-ch
9869
+ if !ok {
9870
+ if reason, rejected := tnRejections.Load(ch); rejected {
9871
+ return v, reason, true
9872
+ }
9873
+ }
9874
+ return v, nil, false
9875
+ }
9876
+
9877
+ // await: the value, or the rejection rethrown
9878
+ func TnAwait[T any](ch chan T) T {
9879
+ v, reason, rejected := tnSettle(ch)
9880
+ if rejected {
9881
+ panic(reason)
9882
+ }
9883
+ return v
9884
+ }
9885
+
9886
+ func TnPromiseReject[T any](reason interface{}) chan T {
9887
+ ch := make(chan T)
9888
+ TnReject(ch, reason)
9889
+ return ch
9890
+ }
9891
+
9892
+ func TnPromiseCatch[T any, E any](p chan T, onRejected func(E)) chan T {
9893
+ out := make(chan T, 1)
9894
+ go func() {
9895
+ v, reason, rejected := tnSettle(p)
9896
+ if rejected {
9897
+ e, _ := reason.(E)
9898
+ onRejected(e)
9899
+ }
9900
+ out <- v
9901
+ close(out)
9902
+ }()
9903
+ return out
9904
+ }
9905
+
9906
+ func TnPromiseCatchValue[T any, E any](p chan T, onRejected func(E) T) chan T {
9907
+ out := make(chan T, 1)
9908
+ go func() {
9909
+ v, reason, rejected := tnSettle(p)
9910
+ if rejected {
9911
+ e, _ := reason.(E)
9912
+ v = onRejected(e)
9913
+ }
9914
+ out <- v
9915
+ close(out)
9916
+ }()
9917
+ return out
9918
+ }
9919
+
9920
+ func TnPromiseFinally[T any](p chan T, onFinally func()) chan T {
9921
+ out := make(chan T, 1)
9922
+ go func() {
9923
+ v, reason, rejected := tnSettle(p)
9924
+ onFinally()
9925
+ if rejected {
9926
+ TnReject(out, reason)
9927
+ return
9928
+ }
9929
+ out <- v
9930
+ close(out)
9931
+ }()
9932
+ return out
9933
+ }
9934
+
9935
+ // Promise.race: the first promise to settle, fulfilled or rejected
9936
+ func TnPromiseRace[T any](chs []chan T) chan T {
9937
+ out := make(chan T, 1)
9938
+ var once sync.Once
9939
+ for _, ch := range chs {
9940
+ go func(ch chan T) {
9941
+ v, reason, rejected := tnSettle(ch)
9942
+ once.Do(func() {
9943
+ if rejected {
9944
+ TnReject(out, reason)
9945
+ return
9946
+ }
9947
+ out <- v
9948
+ close(out)
9949
+ })
9950
+ }(ch)
9951
+ }
9952
+ return out
9953
+ }
9954
+
9955
+ type TnSettled[T any] struct {
9956
+ status string
9957
+ value T
9958
+ reason interface{}
9959
+ }
9960
+
9961
+ func TnPromiseAllSettled[T any](chs []chan T) chan []*TnSettled[T] {
9962
+ out := make(chan []*TnSettled[T], 1)
9963
+ go func() {
9964
+ results := make([]*TnSettled[T], len(chs))
9965
+ for i, ch := range chs {
9966
+ v, reason, rejected := tnSettle(ch)
9967
+ if rejected {
9968
+ results[i] = &TnSettled[T]{status: "rejected", reason: reason}
9969
+ } else {
9970
+ results[i] = &TnSettled[T]{status: "fulfilled", value: v}
9971
+ }
9972
+ }
9973
+ out <- results
9974
+ close(out)
9975
+ }()
9976
+ return out
9977
+ }
9978
+
9979
+ // Promise.any: the first promise to fulfil; rejected when all of them reject
9980
+ func TnPromiseAny[T any](chs []chan T) chan T {
9981
+ out := make(chan T, 1)
9982
+ go func() {
9983
+ values := make(chan T, len(chs))
9984
+ failures := make(chan interface{}, len(chs))
9985
+ for _, ch := range chs {
9986
+ go func(ch chan T) {
9987
+ v, reason, rejected := tnSettle(ch)
9988
+ if rejected {
9989
+ failures <- reason
9990
+ } else {
9991
+ values <- v
9992
+ }
9993
+ }(ch)
9994
+ }
9995
+ for range chs {
9996
+ select {
9997
+ case v := <-values:
9998
+ out <- v
9999
+ close(out)
10000
+ return
10001
+ case <-failures:
10002
+ }
10003
+ }
10004
+ TnReject(out, TnNewError("All promises were rejected"))
10005
+ }()
10006
+ return out
10007
+ }`,
7930
10008
  promiseAll: `// Promise.all: every channel drained in order, results collected
7931
10009
  func TnPromiseAll[T any](chs []chan T) chan []T {
7932
10010
  out := make(chan []T)
7933
10011
  go func() {
7934
10012
  results := make([]T, len(chs))
7935
10013
  for i, ch := range chs {
7936
- results[i] = <-ch
10014
+ v, reason, rejected := tnSettle(ch)
10015
+ if rejected {
10016
+ TnReject(out, reason)
10017
+ return
10018
+ }
10019
+ results[i] = v
7937
10020
  }
7938
10021
  out <- results
7939
10022
  close(out)
@@ -8118,7 +10201,21 @@ function visitImportDeclaration(node) {
8118
10201
  const resolved = fileResolver(moduleSpec, currentFileDir);
8119
10202
  if (resolved) {
8120
10203
  const goFileName = specifierToGoFileName(moduleSpec);
8121
- includeLocalImport(resolved.content, resolved.dir, goFileName);
10204
+ includeLocalImport(resolved.content, resolved.dir, goFileName, !!resolved.tsx);
10205
+ }
10206
+ }
10207
+ // Renamed bindings refer to the original declaration (one Go package)
10208
+ const bindings = node.importClause?.namedBindings;
10209
+ if (bindings && isNamespaceImport(bindings))
10210
+ defaultImportNamespaces.add(bindings.name.text);
10211
+ if (bindings && isNamedImports(bindings)) {
10212
+ for (const el of bindings.elements ?? []) {
10213
+ if (!el.propertyName)
10214
+ continue;
10215
+ importAliases.set(el.name.text, getSafeName(el.propertyName.text));
10216
+ const fn = declaredFunctions.get(el.propertyName.text);
10217
+ if (fn)
10218
+ declaredFunctions.set(el.name.text, fn);
8122
10219
  }
8123
10220
  }
8124
10221
  return '';
@@ -8166,7 +10263,8 @@ function visitImportDeclaration(node) {
8166
10263
  // variables (a variable named `big` would shadow the package name)
8167
10264
  const goImportAliases = {
8168
10265
  'math/big': 'tnbig',
8169
- 'net/url': 'tnurl'
10266
+ 'net/url': 'tnurl',
10267
+ 'golang.org/x/text/unicode/norm': 'tnnorm'
8170
10268
  };
8171
10269
  function goImportLine(pkg) {
8172
10270
  const alias = goImportAliases[pkg];