typenative 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/transpiler.js CHANGED
@@ -285,10 +285,16 @@ const SyntaxKind = {
285
285
  // Local replacement for nanoid's customAlphabet — must stay transpilable by
286
286
  // TypeNative itself (no dependencies), only using mapped String methods.
287
287
  const GO_SAFE_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
288
+ // Ids come from a Park–Miller generator reset for every transpilation, so the
289
+ // same source always gives the same Go (reproducible builds; the self-hosted
290
+ // compiler's output can be compared with the Node CLI's). The products stay
291
+ // below 2^46, exact in a float64 in both runtimes.
292
+ let goSafeIdSeed = 1;
288
293
  function goSafeId() {
289
294
  let id = '';
290
295
  for (let i = 0; i < 8; i++) {
291
- id += GO_SAFE_ALPHABET.charAt(Math.floor(Math.random() * GO_SAFE_ALPHABET.length));
296
+ goSafeIdSeed = (goSafeIdSeed * 16807) % 2147483647;
297
+ id += GO_SAFE_ALPHABET.charAt(goSafeIdSeed % GO_SAFE_ALPHABET.length);
292
298
  }
293
299
  return id;
294
300
  }
@@ -398,6 +404,13 @@ const classStaticProps = new Set();
398
404
  let fileResolver = null;
399
405
  // Directory of the file currently being processed (null = main entry file)
400
406
  let currentFileDir = null;
407
+ // Name of the file currently being parsed, as diagnostics and line directives show it
408
+ let currentFileName = '';
409
+ // Names of the parsed source files, indexed by the fileIndex stamped on each
410
+ // SourceFile node (line markers and diagnostics refer to them)
411
+ const sourceFileNames = [];
412
+ // Errors found while transpiling (unsupported syntax, unresolved imports)
413
+ const diagnostics = [];
401
414
  // Tracks already-included files by a stable key to prevent duplicates/cycles
402
415
  const includedLocalImports = new Set();
403
416
  // Collects Go source files generated from local TS imports (filename → content)
@@ -454,6 +467,8 @@ function parseSource(code) {
454
467
  lowerWeakCollections(sourceFile);
455
468
  lowerGlobalThis(sourceFile);
456
469
  linkParents(sourceFile, undefined);
470
+ sourceFile.fileIndex = sourceFileNames.length;
471
+ sourceFileNames.push(currentFileName);
457
472
  for (const stmt of sourceFile.statements ?? []) {
458
473
  if (!stmt.name || !isIdentifier(stmt.name))
459
474
  continue;
@@ -472,6 +487,115 @@ function parseSource(code) {
472
487
  }
473
488
  return sourceFile;
474
489
  }
490
+ // Index (into sourceFileNames) of the file a node was parsed from, or -1
491
+ function sourceFileIndexOf(node) {
492
+ let current = node;
493
+ while (current && current.kind !== 'SourceFile')
494
+ current = current.parent;
495
+ return current && typeof current.fileIndex === 'number' ? current.fileIndex : -1;
496
+ }
497
+ // Line markers: generated statements and declarations start with
498
+ // /*tn@<file>:<line>:<column>*/, which applyLineDirectives turns into Go
499
+ // /*line file.ts:line:column*/ directives once the code is complete, so Go
500
+ // compile errors and stack traces point at the TypeScript source. The marker
501
+ // carries no file name until then: names like strings.ts would otherwise look
502
+ // like uses of a Go package to the import pruning.
503
+ // A line holding only this sentinel becomes //line <go file>:<next line>,
504
+ // which returns the following generated code (helpers) to its real Go position
505
+ const LINE_RESET = '/*tn@' + 'reset*/';
506
+ function lineMarker(node) {
507
+ if (!node?.line)
508
+ return '';
509
+ const fileIndex = sourceFileIndexOf(node);
510
+ if (fileIndex < 0)
511
+ return '';
512
+ return `/*tn@${fileIndex}:${node.line}:${node.column}*/`;
513
+ }
514
+ // Prefixes generated code with its node's line marker (after any leading
515
+ // whitespace, so the position applies to the code's first token)
516
+ function withLineMarker(node, code) {
517
+ if (!code.trim())
518
+ return code;
519
+ const marker = lineMarker(node);
520
+ if (!marker)
521
+ return code;
522
+ const indent = code.length - code.trimStart().length;
523
+ return code.slice(0, indent) + marker + code.slice(indent);
524
+ }
525
+ // Replaces line markers with Go line directives and resets with //line
526
+ // directives back to the generated file (goFile, as Go should report it).
527
+ // One statement often spans several Go lines (assert → if + panic): every
528
+ // unmarked line after a marker repeats its directive, so those lines report
529
+ // the statement's line instead of counting on from it.
530
+ function applyLineDirectives(code, goFile) {
531
+ const lines = code.split('\n');
532
+ let pin = '';
533
+ // Inside a multi-line raw string or block comment, where nothing may be inserted
534
+ let state = '';
535
+ for (let index = 0; index < lines.length; index++) {
536
+ const text = lines[index];
537
+ if (text === LINE_RESET) {
538
+ lines[index] = `//line ${goFile}:${index + 2}`;
539
+ pin = '';
540
+ continue;
541
+ }
542
+ const startState = state;
543
+ if (text.includes('`') || text.includes('/*') || text.includes('*/'))
544
+ state = goLineEndState(text, state);
545
+ if (text.includes('/*tn@')) {
546
+ // (regex literals inline: a /g regex held in a variable compiles to a
547
+ // replace-first in the self-hosted compiler). Later lines repeat the
548
+ // line's last directive; text like "/*tn@" in a string matches nothing
549
+ lines[index] = text.replace(/\/\*tn@(\d+):(\d+):(\d+)\*\//g, (_match, file, line, column) => {
550
+ pin = `/*line ${sourceFileNames[Number(file)]}:${line}:${column}*/`;
551
+ return pin;
552
+ });
553
+ }
554
+ else if (pin && startState === '' && text.trim()) {
555
+ const indent = text.length - text.trimStart().length;
556
+ lines[index] = text.slice(0, indent) + pin + text.slice(indent);
557
+ }
558
+ }
559
+ return lines.join('\n');
560
+ }
561
+ // The multi-line context after a Go line that starts in state: '' (code),
562
+ // '`' (inside a raw string) or '*' (inside a block comment)
563
+ function goLineEndState(line, state) {
564
+ let rest = line;
565
+ if (state !== '') {
566
+ const close = state === '`' ? '`' : '*/';
567
+ const end = rest.indexOf(close);
568
+ if (end < 0)
569
+ return state;
570
+ rest = rest.slice(end + close.length);
571
+ }
572
+ // Go literals and comments that end on this line, matched leftmost-first
573
+ // like Go's scanner (a quote inside a raw string is not a string): what's
574
+ // left after them is an unclosed raw string or comment
575
+ rest = rest.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`]*`|\/\*.*?\*\/|\/\/.*/g, ' ');
576
+ const rawString = rest.indexOf('`');
577
+ const comment = rest.indexOf('/*');
578
+ if (rawString < 0 && comment < 0)
579
+ return '';
580
+ if (comment < 0 || (rawString >= 0 && rawString < comment))
581
+ return '`';
582
+ return '*';
583
+ }
584
+ // Records a transpile error at the node (or its nearest positioned ancestor)
585
+ function reportError(node, message) {
586
+ let positioned = node;
587
+ while (positioned && !positioned.line)
588
+ positioned = positioned.parent;
589
+ const fileIndex = sourceFileIndexOf(node);
590
+ const line = positioned ? positioned.line : 0;
591
+ const column = positioned ? positioned.column : 0;
592
+ diagnostics.push({
593
+ file: fileIndex >= 0 ? sourceFileNames[fileIndex] : currentFileName,
594
+ line: line,
595
+ column: column,
596
+ message: message
597
+ });
598
+ }
475
599
  // `this: T` parameters only type `this`; they are not real parameters
476
600
  function stripThisParameters(node) {
477
601
  if (Array.isArray(node.parameters)) {
@@ -834,12 +958,20 @@ const usedHelpers = new Set();
834
958
  // per-file import lists can exclude them (helpers are emitted once, in the
835
959
  // main file, which carries these imports instead).
836
960
  const helperProvidedPackages = new Set();
961
+ // GOOS of the current transpilation (see platformHelper)
962
+ let targetGoos = 'linux';
837
963
  // Whether the file being parsed is .tsx
838
964
  let currentFileIsTsx = false;
839
965
  export function transpileToNative(code, options) {
840
966
  fileResolver = options?.readFile ?? null;
841
967
  parseFunction = options?.parse ?? defaultParseFunction;
842
968
  currentFileIsTsx = !!options?.tsx;
969
+ currentFileName = options?.fileName ?? (currentFileIsTsx ? 'main.tsx' : 'main.ts');
970
+ const goFile = options?.goFile ?? 'code.go';
971
+ targetGoos = options?.goos ?? 'linux';
972
+ sourceFileNames.length = 0;
973
+ diagnostics.length = 0;
974
+ goSafeIdSeed = 1;
843
975
  consoleGroupsUsed = code.includes('console.group');
844
976
  declaredFunctions.clear();
845
977
  declaredInterfaces.clear();
@@ -890,7 +1022,7 @@ export function transpileToNative(code, options) {
890
1022
  helperProvidedPackages.clear();
891
1023
  mainPackageVariables.length = 0;
892
1024
  const transpiledCode = visit(sourceFile, { addFunctionOutside: true });
893
- const transpiledCodeOutside = outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n');
1025
+ const transpiledCodeOutside = outsideNodes.map((n) => withLineMarker(n, visit(n, { isOutside: true }))).join('\n');
894
1026
  // Keep the process alive until detached async work and timers settle
895
1027
  const drains = [];
896
1028
  // The event loop runs until no timers, async work or open handles remain
@@ -899,7 +1031,7 @@ export function transpileToNative(code, options) {
899
1031
  // process.exitCode
900
1032
  if (usedHelpers.has('nodeProcess'))
901
1033
  drains.push('TnProcessExit()');
902
- const drainCode = drains.length > 0 ? `\n\t${drains.join('\n\t')}` : '';
1034
+ const drainCode = drains.length > 0 ? `\n${LINE_RESET}\n\t${drains.join('\n\t')}` : '';
903
1035
  // Go bridge types are qualified by path internally (net/http.Cookie)
904
1036
  const mainCode = localizeGoPaths(transpiledCode);
905
1037
  const outsideCode = localizeGoPaths(transpiledCodeOutside);
@@ -929,14 +1061,19 @@ export function transpileToNative(code, options) {
929
1061
  ${mainImports.sort().map((pkg) => importLine(pkg)).join('\n')}
930
1062
 
931
1063
  ${packageVariables.join('\n')}
932
-
1064
+ ${LINE_RESET}
933
1065
  func main() {
934
1066
  ${body}${drainCode}
935
1067
  }
936
1068
 
937
1069
  ${outsideCode.trim()}
1070
+ ${LINE_RESET}
938
1071
  ${helpersCode}`.trimEnd();
939
- return { main, files: localImportFiles };
1072
+ return {
1073
+ main: applyLineDirectives(main, goFile),
1074
+ files: localImportFiles,
1075
+ diagnostics: diagnostics.slice()
1076
+ };
940
1077
  }
941
1078
  export // Property map of an interface, including inherited properties (flattened —
942
1079
  function collectInterfaceProperties(iface, into, seen = new Set()) {
@@ -975,7 +1112,7 @@ function visit(node, options = {}) {
975
1112
  }
976
1113
  if (isSourceFile(node)) {
977
1114
  return (node.statements ?? [])
978
- .map((n) => visit(n, { addFunctionOutside: true }))
1115
+ .map((n) => withLineMarker(n, visit(n, { addFunctionOutside: true })))
979
1116
  .filter((n) => !!n)
980
1117
  .join(options.inline ? '' : '\n\t');
981
1118
  }
@@ -2038,13 +2175,13 @@ function visit(node, options = {}) {
2038
2175
  const thenTerm = node.elseStatement ? '' : ';';
2039
2176
  const thenCode = withNarrowing(getNarrowedNames(node.expression, true), () => isBlock(node.thenStatement)
2040
2177
  ? visit(node.thenStatement, { inline: !!node.elseStatement })
2041
- : `{\n${visit(node.thenStatement)}\n}${thenTerm}`);
2178
+ : `{\n${withLineMarker(node.thenStatement, visit(node.thenStatement))}\n}${thenTerm}`);
2042
2179
  const condition = `if ${toGoCondition(node.expression)} ${thenCode}`;
2043
2180
  if (node.elseStatement) {
2044
2181
  // else-if chains stay chained; other single statements get a block
2045
2182
  const elseCode = withNarrowing(getNarrowedNames(node.expression, false), () => isBlock(node.elseStatement) || isIfStatement(node.elseStatement)
2046
2183
  ? visit(node.elseStatement)
2047
- : `{\n${visit(node.elseStatement)}\n};\n\t`);
2184
+ : `{\n${withLineMarker(node.elseStatement, visit(node.elseStatement))}\n};\n\t`);
2048
2185
  return `${condition} else ${elseCode}`;
2049
2186
  }
2050
2187
  return condition;
@@ -2512,7 +2649,7 @@ function visit(node, options = {}) {
2512
2649
  }
2513
2650
  const methodParameterInfo = getFunctionParametersInfo(member.parameters ?? []);
2514
2651
  const returnType = member.type ? ` ${getType(member.type)}` : '';
2515
- copiedMethods.push(`func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n`);
2652
+ copiedMethods.push(`${lineMarker(member)}func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n`);
2516
2653
  }
2517
2654
  ancestorIndex--;
2518
2655
  }
@@ -2543,7 +2680,7 @@ function visit(node, options = {}) {
2543
2680
  const genBody = visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent });
2544
2681
  asyncChannelVar = prevChannel;
2545
2682
  asyncFunctionNode = prevFn;
2546
- result += `func (self *${name}${typeParamNames}) ${genName}(${methodParameterInfo.signature}) chan ${yieldType} {\n\t__ch := make(chan ${yieldType})\n\tgo func() {\n\t\tdefer close(__ch)\n\t\t${genBody.trim()}\n\t}()\n\treturn __ch\n}\n\n`;
2683
+ result += `${lineMarker(member)}func (self *${name}${typeParamNames}) ${genName}(${methodParameterInfo.signature}) chan ${yieldType} {\n\t__ch := make(chan ${yieldType})\n\tgo func() {\n\t\tdefer close(__ch)\n\t\t${genBody.trim()}\n\t}()\n\treturn __ch\n}\n\n`;
2547
2684
  }
2548
2685
  continue;
2549
2686
  }
@@ -2561,23 +2698,23 @@ function visit(node, options = {}) {
2561
2698
  }
2562
2699
  else if (isStatic) {
2563
2700
  // Static methods become package-level functions named ClassName_methodName
2564
- result += `func ${name}_${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
2701
+ result += `${lineMarker(member)}func ${name}_${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
2565
2702
  }
2566
2703
  else {
2567
- result += `func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
2704
+ result += `${lineMarker(member)}func (self *${name}${typeParamNames}) ${methodName}(${methodParameterInfo.signature})${returnType} ${visit(member.body, { prefixBlockContent: methodParameterInfo.prefixBlockContent })}\n\n`;
2568
2705
  }
2569
2706
  }
2570
2707
  else if (isGetAccessor(member)) {
2571
2708
  // Getter: get prop() { ... } → func (self *T) Get_prop() RetType { ... }
2572
2709
  const getterName = visit(member.name);
2573
2710
  const returnType = member.type ? ` ${getType(member.type)}` : ' interface{}';
2574
- result += `func (self *${name}${typeParamNames}) Get_${getterName}()${returnType} ${visit(member.body)}\n\n`;
2711
+ result += `${lineMarker(member)}func (self *${name}${typeParamNames}) Get_${getterName}()${returnType} ${visit(member.body)}\n\n`;
2575
2712
  }
2576
2713
  else if (isSetAccessor(member)) {
2577
2714
  // Setter: set prop(val) { ... } → func (self *T) Set_prop(val ValType) { ... }
2578
2715
  const setterName = visit(member.name);
2579
2716
  const parameterInfo = getFunctionParametersInfo(member.parameters ?? []);
2580
- result += `func (self *${name}${typeParamNames}) Set_${setterName}(${parameterInfo.signature}) ${visit(member.body, { prefixBlockContent: parameterInfo.prefixBlockContent })}\n\n`;
2717
+ result += `${lineMarker(member)}func (self *${name}${typeParamNames}) Set_${setterName}(${parameterInfo.signature}) ${visit(member.body, { prefixBlockContent: parameterInfo.prefixBlockContent })}\n\n`;
2581
2718
  }
2582
2719
  }
2583
2720
  result += copiedMethods.join('\n');
@@ -2760,10 +2897,16 @@ function visit(node, options = {}) {
2760
2897
  else if (isExportDeclaration(node) || isExportAssignment(node)) {
2761
2898
  return '';
2762
2899
  }
2900
+ else if (node.kind === 'EmptyStatement' || node.kind === 'DebuggerStatement') {
2901
+ // `;` and `debugger` do nothing in a native binary
2902
+ return '';
2903
+ }
2763
2904
  const syntaxKind = node.kind;
2764
2905
  if (!['VariableStatement', 'EndOfFileToken'].includes(syntaxKind)) {
2765
- const snippet = String(node.text ?? node.kind).substring(0, 60).replace(/\n/g, ' ');
2766
- console.warn(`[TypeNative] Unsupported syntax: ${syntaxKind} — "${snippet}"`);
2906
+ // The Go output would be incomplete: an error, not a guess (its children
2907
+ // are not visited, which would only report follow-on errors)
2908
+ reportError(node, `Unsupported syntax: ${syntaxKind}`);
2909
+ return '';
2767
2910
  }
2768
2911
  for (const child of childNodes(node)) {
2769
2912
  code += visit(child);
@@ -3006,10 +3149,10 @@ function visitBlockStatements(statements) {
3006
3149
  for (let index = 0; index < statements.length; index++) {
3007
3150
  const statement = statements[index];
3008
3151
  if (isUsingDeclaration(statement)) {
3009
- parts.push(visitUsingDeclaration(statement, statements.slice(index + 1)));
3152
+ parts.push(withLineMarker(statement, visitUsingDeclaration(statement, statements.slice(index + 1))));
3010
3153
  break;
3011
3154
  }
3012
- parts.push(visit(statement));
3155
+ parts.push(withLineMarker(statement, visit(statement)));
3013
3156
  if (isIfStatement(statement) && !statement.elseStatement && alwaysExits(statement.thenStatement)) {
3014
3157
  for (const name of getNarrowedNames(statement.expression, false)) {
3015
3158
  if (!narrowedVariables.has(name)) {
@@ -8057,11 +8200,13 @@ function normalizeCjsToEsm(code) {
8057
8200
  code = code.replace(/(?:module\.exports|exports)\.(\w+)\s*=\s*function\s*\w*\s*\(/g, 'export function $1(');
8058
8201
  return code;
8059
8202
  }
8060
- function includeLocalImport(code, dir, goFileName, tsx = false) {
8203
+ function includeLocalImport(code, dir, goFileName, tsx = false, fileName = '') {
8061
8204
  const prevDir = currentFileDir;
8062
8205
  const prevTsx = currentFileIsTsx;
8206
+ const prevFileName = currentFileName;
8063
8207
  currentFileDir = dir;
8064
8208
  currentFileIsTsx = tsx;
8209
+ currentFileName = fileName;
8065
8210
  // Normalize CommonJS modules to ES module syntax before parsing
8066
8211
  if (!code.includes('export ') && (code.includes('module.exports') || code.includes('exports.'))) {
8067
8212
  code = normalizeCjsToEsm(code);
@@ -8074,6 +8219,7 @@ function includeLocalImport(code, dir, goFileName, tsx = false) {
8074
8219
  }
8075
8220
  currentFileDir = prevDir;
8076
8221
  currentFileIsTsx = prevTsx;
8222
+ currentFileName = prevFileName;
8077
8223
  return;
8078
8224
  }
8079
8225
  // Separate-file mode (local TS imports): transpile to its own Go file.
@@ -8088,19 +8234,23 @@ function includeLocalImport(code, dir, goFileName, tsx = false) {
8088
8234
  importedPackages.clear();
8089
8235
  helperProvidedPackages.clear();
8090
8236
  const sf = parseSource(code);
8091
- const inlineLines = [];
8237
+ const isDeclarationLine = (line) => /^(var|const|type|func|import)\b/.test(line.trim());
8238
+ const declarationLines = [];
8239
+ const statementLines = [];
8092
8240
  const prevEmittingModuleFile = emittingModuleFile;
8093
8241
  emittingModuleFile = true;
8094
8242
  for (const stmt of sf.statements) {
8095
8243
  const result = visit(stmt, { addFunctionOutside: true });
8096
- if (result.trim())
8097
- inlineLines.push(localizeGoPaths(result));
8244
+ if (!result.trim())
8245
+ continue;
8246
+ const line = withLineMarker(stmt, localizeGoPaths(result));
8247
+ if (isDeclarationLine(result))
8248
+ declarationLines.push(line);
8249
+ else
8250
+ statementLines.push(line);
8098
8251
  }
8099
8252
  emittingModuleFile = prevEmittingModuleFile;
8100
- const fileOutside = localizeGoPaths(outsideNodes.map((n) => visit(n, { isOutside: true })).join('\n'));
8101
- const isDeclarationLine = (line) => /^(var|const|type|func|import)\b/.test(line.trim());
8102
- const declarationLines = inlineLines.filter(isDeclarationLine);
8103
- const statementLines = inlineLines.filter((line) => !isDeclarationLine(line));
8253
+ const fileOutside = localizeGoPaths(outsideNodes.map((n) => withLineMarker(n, visit(n, { isOutside: true }))).join('\n'));
8104
8254
  const fileInline = [
8105
8255
  ...declarationLines,
8106
8256
  ...(statementLines.length > 0 ? [`func init() {\n\t${statementLines.join('\n\t')}\n}`] : [])
@@ -8128,7 +8278,7 @@ function includeLocalImport(code, dir, goFileName, tsx = false) {
8128
8278
  parts.push(fileInline.trim());
8129
8279
  if (fileOutside.trim())
8130
8280
  parts.push(fileOutside.trim());
8131
- localImportFiles.set(goFileName, parts.join('\n\n'));
8281
+ localImportFiles.set(goFileName, applyLineDirectives(parts.join('\n\n'), goFileName));
8132
8282
  // Restore main-file state; helper-provided packages carry over so the main
8133
8283
  // file (which carries the helper sources) imports them.
8134
8284
  outsideNodes = savedOutsideNodes;
@@ -8141,6 +8291,7 @@ function includeLocalImport(code, dir, goFileName, tsx = false) {
8141
8291
  for (const p of helperProvidedPackages)
8142
8292
  importedPackages.add(p);
8143
8293
  currentFileDir = prevDir;
8294
+ currentFileName = prevFileName;
8144
8295
  }
8145
8296
  // Go source without its string literals (for detecting real package use)
8146
8297
  function stripGoStrings(code) {
@@ -8263,6 +8414,16 @@ const helperRequires = {
8263
8414
  asyncWait: ['eventLoop'],
8264
8415
  eventLoop: []
8265
8416
  };
8417
+ // Helpers with OS-specific variants (Windows APIs that would not compile
8418
+ // elsewhere) are required as '<id>@os': the target OS's variant
8419
+ // ('<id>@windows') when there is one, else '<id>@other'
8420
+ function platformHelper(id) {
8421
+ if (!id.endsWith('@os'))
8422
+ return id;
8423
+ const base = id.slice(0, id.length - 3);
8424
+ const specific = `${base}@${targetGoos}`;
8425
+ return specific in goHelpers || specific in nodeRegistry.goHelpers ? specific : `${base}@other`;
8426
+ }
8266
8427
  function useHelper(id) {
8267
8428
  if (usedHelpers.has(id))
8268
8429
  return;
@@ -8272,7 +8433,7 @@ function useHelper(id) {
8272
8433
  interfacePropertyTypes.set('TnMatch', new Map([['groups', '*TnMap[string, string]']]));
8273
8434
  }
8274
8435
  for (const dep of helperRequires[id] ?? nodeRegistry.helperRequires[id] ?? [])
8275
- useHelper(dep);
8436
+ useHelper(platformHelper(dep));
8276
8437
  for (const pkg of helperPackages[id] ?? nodeRegistry.helperPackages[id] ?? []) {
8277
8438
  importedPackages.add(pkg);
8278
8439
  helperProvidedPackages.add(pkg);
@@ -8368,7 +8529,7 @@ const helperPackages = {
8368
8529
  namedGroups: ['regexp'],
8369
8530
  moduleUrl: ['os'],
8370
8531
  asyncWait: [],
8371
- timerWait: ['sync', 'sync/atomic', 'time'],
8532
+ timerWait: ['sort', 'sync', 'sync/atomic', 'time'],
8372
8533
  clock: ['time'],
8373
8534
  eventLoop: ['sync', 'sync/atomic', 'time'],
8374
8535
  uri: ['strings', 'fmt', 'net/url'],
@@ -10614,7 +10775,46 @@ type TnTimeout struct {
10614
10775
  fn func()
10615
10776
  refd bool
10616
10777
  active bool
10617
- gen int
10778
+ due time.Time
10779
+ seq int
10780
+ }
10781
+
10782
+ // Armed timers, owned by the baton holder. An expired timer's goroutine only
10783
+ // wakes the loop, which then runs every due timer in deadline order: timer
10784
+ // goroutines that expire together (a starved loop, a coarse OS clock) may post
10785
+ // in any order, but Node fires timers in deadline order.
10786
+ var __tnPendingTimers = map[*TnTimeout]struct{}{}
10787
+ var __tnTimerSeq int
10788
+
10789
+ func tnRunDueTimers() {
10790
+ now := time.Now()
10791
+ var due []*TnTimeout
10792
+ for t := range __tnPendingTimers {
10793
+ if !t.active {
10794
+ delete(__tnPendingTimers, t)
10795
+ } else if !t.due.After(now) {
10796
+ due = append(due, t)
10797
+ }
10798
+ }
10799
+ sort.Slice(due, func(i, j int) bool {
10800
+ if !due[i].due.Equal(due[j].due) {
10801
+ return due[i].due.Before(due[j].due)
10802
+ }
10803
+ return due[i].seq < due[j].seq
10804
+ })
10805
+ for _, t := range due {
10806
+ // An earlier callback may have cleared or refreshed it
10807
+ if !t.active || t.due.After(now) {
10808
+ continue
10809
+ }
10810
+ delete(__tnPendingTimers, t)
10811
+ t.active = false
10812
+ if t.refd {
10813
+ t.refd = false
10814
+ __tnTimers.Done()
10815
+ }
10816
+ t.fn()
10817
+ }
10618
10818
  }
10619
10819
 
10620
10820
  // node:test mock timers: when set (and the API is mocked) it takes over a timer;
@@ -10643,21 +10843,12 @@ func tnSetTimeout(kind string, ms float64, fn func()) *TnTimeout {
10643
10843
  }
10644
10844
 
10645
10845
  func (t *TnTimeout) arm() {
10646
- t.gen++
10647
- gen := t.gen
10648
- t.timer = time.AfterFunc(time.Duration(t.ms*float64(time.Millisecond)), func() {
10649
- TnLoopPost(func() {
10650
- if !t.active || gen != t.gen {
10651
- return
10652
- }
10653
- t.active = false
10654
- if t.refd {
10655
- t.refd = false
10656
- __tnTimers.Done()
10657
- }
10658
- t.fn()
10659
- })
10660
- })
10846
+ __tnTimerSeq++
10847
+ t.seq = __tnTimerSeq
10848
+ d := time.Duration(t.ms * float64(time.Millisecond))
10849
+ t.due = time.Now().Add(d)
10850
+ __tnPendingTimers[t] = struct{}{}
10851
+ t.timer = time.AfterFunc(d, func() { TnLoopPost(tnRunDueTimers) })
10661
10852
  }
10662
10853
 
10663
10854
  func (t *TnTimeout) ref() *TnTimeout {
@@ -10797,6 +10988,7 @@ func (t *TnInterval) close() *TnInterval {
10797
10988
  func TnClearTimeout(t *TnTimeout) {
10798
10989
  if t != nil && t.active {
10799
10990
  t.active = false
10991
+ delete(__tnPendingTimers, t)
10800
10992
  if t.timer != nil {
10801
10993
  t.timer.Stop()
10802
10994
  }
@@ -11547,8 +11739,6 @@ function nodeModuleName(spec) {
11547
11739
  name = nodeRegistry.aliases[name];
11548
11740
  if (nodeRegistry.modules[name])
11549
11741
  return name;
11550
- if (prefixed)
11551
- console.warn(`[TypeNative] Unsupported Node.js module: ${spec}`);
11552
11742
  return '';
11553
11743
  }
11554
11744
  // Binds the import's local names to registry keys
@@ -12117,7 +12307,7 @@ function visitImportDeclaration(node) {
12117
12307
  if (resolved && !includedLocalImports.has(fileKey)) {
12118
12308
  includedLocalImports.add(fileKey);
12119
12309
  const goFileName = specifierToGoFileName(moduleSpec);
12120
- includeLocalImport(resolved.content, resolved.dir, goFileName, !!resolved.tsx);
12310
+ includeLocalImport(resolved.content, resolved.dir, goFileName, !!resolved.tsx, resolved.path ?? moduleSpec);
12121
12311
  }
12122
12312
  }
12123
12313
  // Renamed bindings refer to the original declaration (one Go package)
@@ -12143,11 +12333,13 @@ function visitImportDeclaration(node) {
12143
12333
  setupNodeModuleImport(node, nodeModule);
12144
12334
  return '';
12145
12335
  }
12146
- if (moduleSpec.startsWith('node:'))
12147
- return '';
12148
12336
  // Type-only imports contribute nothing to runtime
12149
12337
  if (node.importClause?.isTypeOnly)
12150
12338
  return '';
12339
+ if (moduleSpec.startsWith('node:')) {
12340
+ reportError(node, `Unsupported Node.js module: ${moduleSpec} (see conformance.md#node-builtins-node)`);
12341
+ return '';
12342
+ }
12151
12343
  // Go standard library: `import { Println } from 'go:fmt'` → import "fmt"
12152
12344
  if (moduleSpec.startsWith('go:')) {
12153
12345
  const goPkg = moduleSpec.slice(3);
@@ -12172,7 +12364,7 @@ function visitImportDeclaration(node) {
12172
12364
  const resolved = fileResolver(moduleSpec, currentFileDir);
12173
12365
  if (resolved) {
12174
12366
  const goFileName = specifierToGoFileName(moduleSpec);
12175
- includeLocalImport(resolved.content, resolved.dir, goFileName);
12367
+ includeLocalImport(resolved.content, resolved.dir, goFileName, !!resolved.tsx, resolved.path ?? moduleSpec);
12176
12368
  }
12177
12369
  }
12178
12370
  // Register default/namespace import name as a "stripped" namespace
@@ -0,0 +1,78 @@
1
+ // Node-side type-checking: builds a TypeScript program for the entry file with
2
+ // the `typescript` package and returns its errors. The self-hosted binary runs
3
+ // the same check with typescript-go (`tsparser --check`, tsparser/check.go).
4
+ //
5
+ // Node-only module: injected into run() by the Node CLI wrapper (src/cli.ts).
6
+ import ts from 'typescript';
7
+ import path from 'node:path';
8
+ // The bundled declarations that replace the default lib (types/ in the package)
9
+ const TYPENATIVE_TYPES = ['typenative.d.ts', 'typenative-go.d.ts', 'typenative-npm.d.ts'];
10
+ // TS18003: "No inputs were found in config file" (only the entry is checked)
11
+ const NO_INPUTS_FOUND = 18003;
12
+ // Type-checks entry with the nearest tsconfig.json's options (or TypeNative's
13
+ // defaults, typed by typesDir). The program's roots are the entry plus the
14
+ // config's .d.ts files, so other entry points in the same project don't
15
+ // redeclare each other's globals.
16
+ export function typeCheckNode(entry, typesDir) {
17
+ const entryPath = path.resolve(entry);
18
+ const roots = [entryPath];
19
+ const out = [];
20
+ let options;
21
+ const configPath = ts.findConfigFile(path.dirname(entryPath), ts.sys.fileExists);
22
+ if (configPath) {
23
+ const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
24
+ ...ts.sys,
25
+ onUnRecoverableConfigFileDiagnostic: (d) => out.push(toDiagnostic(d))
26
+ });
27
+ if (!parsed)
28
+ return out;
29
+ for (const d of parsed.errors) {
30
+ if (d.code !== NO_INPUTS_FOUND)
31
+ out.push(toDiagnostic(d));
32
+ }
33
+ options = parsed.options;
34
+ for (const name of parsed.fileNames) {
35
+ if (name.endsWith('.d.ts'))
36
+ roots.push(name);
37
+ }
38
+ }
39
+ else {
40
+ options = {
41
+ target: ts.ScriptTarget.ES2020,
42
+ module: ts.ModuleKind.ESNext,
43
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
44
+ lib: [],
45
+ types: [],
46
+ strict: true,
47
+ noImplicitAny: true,
48
+ allowSyntheticDefaultImports: true,
49
+ allowImportingTsExtensions: true,
50
+ jsx: ts.JsxEmit.Preserve,
51
+ skipLibCheck: true
52
+ };
53
+ for (const name of TYPENATIVE_TYPES)
54
+ roots.push(path.join(typesDir, name));
55
+ }
56
+ const program = ts.createProgram(roots, { ...options, noEmit: true });
57
+ for (const d of ts.getPreEmitDiagnostics(program)) {
58
+ if (d.category === ts.DiagnosticCategory.Error)
59
+ out.push(toDiagnostic(d));
60
+ }
61
+ return out;
62
+ }
63
+ function toDiagnostic(d) {
64
+ const out = {
65
+ file: '',
66
+ line: 0,
67
+ column: 0,
68
+ code: d.code,
69
+ message: ts.flattenDiagnosticMessageText(d.messageText, '\n')
70
+ };
71
+ if (d.file && d.start !== undefined) {
72
+ const start = d.file.getLineAndCharacterOfPosition(d.start);
73
+ out.file = d.file.fileName;
74
+ out.line = start.line + 1;
75
+ out.column = start.character + 1;
76
+ }
77
+ return out;
78
+ }
package/bin/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/gen-version.mjs — do not edit.
2
- export const VERSION = '0.2.0';
2
+ export const VERSION = '0.3.1';
package/conformance.md CHANGED
@@ -537,7 +537,7 @@ async function runs synchronously until its first `await`.
537
537
  | -------------------------------------------------------------------------------------------- | :----: | -------------------------------------------- | ----------------------------------------------------------------------- |
538
538
  | `platform` / `arch` / `type` / `machine` / `EOL` / `devNull` / `endianness` | ✅ | [lib-node-os.ts](conformance/lib-node-os.ts) | Node's names (`win32`, `x64`, `Windows_NT`, `x86_64`) |
539
539
  | `hostname` / `homedir` / `tmpdir` / `userInfo` / `cpus` / `availableParallelism` / `networkInterfaces` / `constants.signals` | ✅ | [lib-node-os.ts](conformance/lib-node-os.ts) | `cpus()` has one entry per logical CPU (model / speed / times are zero) |
540
- | `totalmem` / `freemem` / `uptime` / `loadavg` / `release` / `version` | ✅ | [lib-node-os-mem.ts](conformance/lib-node-os-mem.ts) | `/proc` on Linux, `sysctl` on macOS / BSD; zeros where neither exists |
540
+ | `totalmem` / `freemem` / `uptime` / `loadavg` / `release` / `version` | ✅ | [lib-node-os-mem.ts](conformance/lib-node-os-mem.ts) | `/proc` on Linux, `sysctl` on macOS / BSD, `GlobalMemoryStatusEx` / `GetTickCount64` on Windows (`loadavg` is zeros there, as in Node) |
541
541
 
542
542
  #### `node:process`
543
543
 
@@ -569,6 +569,7 @@ async function runs synchronously until its first `await`.
569
569
  | `promisify` / `callbackify` | ✅ | [lib-node-util-promisify.ts](conformance/lib-node-util-promisify.ts) | Node functions map to their promise versions at compile time (`fs.readFile`, `setTimeout`, `exec`); other functions are wrapped |
570
570
  | `parseArgs` (short / long / grouped flags, `multiple`, `default`, `strict`, `allowPositionals`, `allowNegative`, `tokens`, Node's errors), `getSystemErrorName` / `getSystemErrorMessage` / `getSystemErrorMap`, `MIMEType` / `MIMEParams`, `parseEnv` (+ `process.loadEnvFile`), `aborted`, `debuglog` (`NODE_DEBUG`), `toUSVString` | ✅ | [lib-node-util-parse-args.ts](conformance/lib-node-util-parse-args.ts) | |
571
571
  | `inherits` (legacy), `inspect.custom` (symbols), `getCallSites` (V8 stack frames) | ❌ | — | Legacy or engine-specific |
572
+ | Node's errno numbers on Windows (libuv's: `ENOENT` is `-4058`) | ❌ | — | errno values are the OS's (Go's `syscall` numbers): the same as Node's on Linux and macOS, but `ENOENT` is `-2` on Windows. `getSystemErrorName(err.errno)` still names any error TypeNative raises |
572
573
 
573
574
  #### `node:assert`
574
575
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typenative",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Build native applications using Typescript.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "check:go-types": "tsc -p conformance/tsconfig.go-check.json",
19
19
  "conformance": "node scripts/conformance.mjs",
20
20
  "conformance:parallel": "node scripts/conformance-parallel.mjs",
21
+ "bootstrap:check": "node scripts/bootstrap-check.mjs",
21
22
  "test": "npm run conformance",
22
23
  "release": "npm publish"
23
24
  },