ttsc 0.12.2 → 0.12.4

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.
Files changed (134) hide show
  1. package/cmd/platform/main.go +9 -0
  2. package/cmd/ttsc/api_compile.go +109 -101
  3. package/cmd/ttsc/api_compile_branches_test.go +55 -57
  4. package/cmd/ttsc/api_transform.go +54 -49
  5. package/cmd/ttsc/api_transform_branches_test.go +30 -32
  6. package/cmd/ttsc/build.go +111 -108
  7. package/cmd/ttsc/build_branches_test.go +95 -97
  8. package/cmd/ttsc/helpers_test.go +39 -28
  9. package/cmd/ttsc/main.go +111 -104
  10. package/cmd/ttsc/main_alias_branches_test.go +21 -23
  11. package/cmd/ttscserver/main.go +96 -90
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
  15. package/cmd/utility-host/main.go +30 -23
  16. package/driver/lsp.go +53 -7
  17. package/driver/plugins.go +88 -72
  18. package/driver/program.go +362 -339
  19. package/driver/rewrite.go +442 -380
  20. package/internal/lspserver/lsp_envelope.go +64 -64
  21. package/internal/lspserver/lsp_frame.go +60 -60
  22. package/internal/lspserver/lsp_plugin.go +57 -57
  23. package/internal/lspserver/lsp_proxy.go +265 -251
  24. package/internal/lspserver/lsp_server.go +155 -144
  25. package/lib/TtscCompiler.d.ts +7 -0
  26. package/lib/TtscCompiler.js +7 -0
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
  29. package/lib/compiler/internal/buildNativeCompiler.js +23 -3
  30. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  31. package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
  32. package/lib/compiler/internal/compileProjectInMemory.js +36 -21
  33. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  34. package/lib/compiler/internal/paths.d.ts +27 -0
  35. package/lib/compiler/internal/paths.js +70 -0
  36. package/lib/compiler/internal/paths.js.map +1 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
  38. package/lib/compiler/internal/project/readProjectConfig.js +80 -1
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
  41. package/lib/compiler/internal/project/resolveProjectConfig.js +24 -1
  42. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/resolveBinary.d.ts +9 -1
  44. package/lib/compiler/internal/resolveBinary.js +17 -1
  45. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  46. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
  47. package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
  48. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  49. package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
  50. package/lib/compiler/internal/resolveTsgo.js +23 -1
  51. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  52. package/lib/compiler/internal/runBuild.d.ts +19 -0
  53. package/lib/compiler/internal/runBuild.js +112 -46
  54. package/lib/compiler/internal/runBuild.js.map +1 -1
  55. package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
  56. package/lib/compiler/internal/runSingleFileEmit.js +14 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  58. package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
  59. package/lib/compiler/internal/sharedHostHelpers.js +6 -2
  60. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  61. package/lib/compiler/internal/spawnNative.d.ts +21 -0
  62. package/lib/compiler/internal/spawnNative.js +59 -0
  63. package/lib/compiler/internal/spawnNative.js.map +1 -0
  64. package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
  65. package/lib/compiler/internal/transformProjectInMemory.js +57 -61
  66. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  67. package/lib/launcher/internal/getCompilerVersionText.js +7 -0
  68. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  69. package/lib/launcher/internal/prepareExecution.js +32 -0
  70. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  71. package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
  72. package/lib/launcher/internal/resolveCacheDir.js +22 -0
  73. package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
  74. package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
  75. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
  76. package/lib/launcher/internal/runTtsc.d.ts +10 -0
  77. package/lib/launcher/internal/runTtsc.js +101 -16
  78. package/lib/launcher/internal/runTtsc.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +10 -35
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.d.ts +8 -0
  82. package/lib/launcher/internal/runTtsx.js +33 -7
  83. package/lib/launcher/internal/runTtsx.js.map +1 -1
  84. package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
  85. package/lib/plugin/internal/buildSourcePlugin.js +40 -17
  86. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  87. package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
  88. package/lib/plugin/internal/loadProjectPlugins.js +32 -18
  89. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  90. package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
  91. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
  92. package/package.json +9 -9
  93. package/shim/ast/go.mod +9 -0
  94. package/shim/ast/go.sum +20 -0
  95. package/shim/ast/safe_text.go +86 -0
  96. package/shim/ast/shim.go +24 -0
  97. package/shim/ast/test/node_text_joins_multi_hop_qualified_name_test.go +32 -0
  98. package/shim/ast/test/node_text_joins_one_hop_qualified_name_test.go +31 -0
  99. package/shim/ast/test/node_text_returns_empty_for_nil_test.go +23 -0
  100. package/shim/ast/test/node_text_returns_identifier_text_test.go +27 -0
  101. package/shim/ast/test/node_text_skips_empty_qualified_left_test.go +30 -0
  102. package/shim/checker/shim.go +35 -0
  103. package/shim/core/shim.go +17 -0
  104. package/shim/diagnosticwriter/shim.go +4 -0
  105. package/shim/lsp/shim.go +3 -3
  106. package/shim/printer/shim.go +20 -0
  107. package/src/TtscCompiler.ts +7 -0
  108. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  109. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  110. package/src/compiler/internal/paths.ts +68 -0
  111. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  112. package/src/compiler/internal/project/resolveProjectConfig.ts +24 -1
  113. package/src/compiler/internal/resolveBinary.ts +17 -1
  114. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  115. package/src/compiler/internal/resolveTsgo.ts +29 -1
  116. package/src/compiler/internal/runBuild.ts +110 -55
  117. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  118. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  119. package/src/compiler/internal/spawnNative.ts +64 -0
  120. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  121. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  122. package/src/launcher/internal/prepareExecution.ts +32 -0
  123. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  124. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  125. package/src/launcher/internal/runTtsc.ts +120 -20
  126. package/src/launcher/internal/runTtscserver.ts +9 -1
  127. package/src/launcher/internal/runTtsx.ts +33 -8
  128. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  129. package/src/plugin/internal/loadProjectPlugins.ts +42 -18
  130. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  131. package/src/structures/ITtscPlugin.ts +0 -1
  132. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  133. package/tools/gen_shims/main.go +43 -8
  134. package/utility/host.go +294 -245
@@ -7,7 +7,14 @@ import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmitted
7
7
  import { runBuild } from "../../compiler/internal/runBuild";
8
8
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
9
9
 
10
+ /** Subdirectory name that isolates concurrent ttsx processes by PID. */
10
11
  const PROCESS_CACHE_KEY = String(process.pid);
12
+ /**
13
+ * Maximum number of ancestor directories above the project root that the
14
+ * virtual filesystem overlay mirrors. Three levels covers the common monorepo
15
+ * layout (workspace-root → packages → package-root) so `node_modules` symlinks
16
+ * resolve correctly without reaching an unsafe boundary.
17
+ */
11
18
  const MAX_VIRTUAL_PARENT_DEPTH = 3;
12
19
 
13
20
  /** Build the owning project and locate the emitted JavaScript entry for `ttsx`. */
@@ -162,6 +169,7 @@ function linkVirtualEntry(
162
169
  entry: fs.Dirent,
163
170
  ): void {
164
171
  if (entry.isDirectory()) {
172
+ // Use junction points on Windows; plain symlinks elsewhere.
165
173
  fs.symlinkSync(
166
174
  realEntry,
167
175
  virtualEntry,
@@ -171,15 +179,25 @@ function linkVirtualEntry(
171
179
  }
172
180
  if (entry.isFile()) {
173
181
  try {
182
+ // Hard-link first: cheap, preserves inode, no extra disk usage.
174
183
  fs.linkSync(realEntry, virtualEntry);
175
184
  } catch {
185
+ // Cross-device or unsupported filesystem: fall back to a full copy.
176
186
  fs.copyFileSync(realEntry, virtualEntry);
177
187
  }
178
188
  return;
179
189
  }
190
+ // Symlinks (and other special entries) are re-symlinked as-is.
180
191
  fs.symlinkSync(realEntry, virtualEntry);
181
192
  }
182
193
 
194
+ /**
195
+ * Walk from `projectRoot` upward (up to `MAX_VIRTUAL_PARENT_DEPTH` steps),
196
+ * stopping early at a workspace root (`pnpm-workspace.yaml` or `.git`). The
197
+ * collected directories are reversed so callers can iterate outermost-first,
198
+ * which lets inner symlinks override outer ones without conflicting mkdir
199
+ * calls.
200
+ */
183
201
  function collectLinkDirectories(projectRoot: string): string[] {
184
202
  const out: string[] = [];
185
203
  let current = projectRoot;
@@ -207,6 +225,14 @@ function isUnsafeVirtualParent(directory: string): boolean {
207
225
  return resolved === root || resolved === path.resolve(os.tmpdir());
208
226
  }
209
227
 
228
+ /**
229
+ * Map an absolute path into a stable, filesystem-safe subtree under `root`.
230
+ *
231
+ * On POSIX the root is always `/`, so every path shares the same prefix —
232
+ * represented here as `"posix"`. On Windows, drive letters and UNC roots each
233
+ * get a sanitized label (e.g. `"C_"` for `C:\`), preventing collisions between
234
+ * paths from different drives inside the same virtual root.
235
+ */
210
236
  function virtualPath(root: string, absolute: string): string {
211
237
  const parsed = path.parse(path.resolve(absolute));
212
238
  const label =
@@ -218,6 +244,12 @@ function virtualPath(root: string, absolute: string): string {
218
244
  return path.join(root, label, relative);
219
245
  }
220
246
 
247
+ /**
248
+ * Heuristic: classify emitted JS as ESM when it contains top-level `import` or
249
+ * `export` statements but none of the well-known CJS patterns. The CJS checks
250
+ * run first so that re-exported CJS bundles with both `require` calls and an
251
+ * `export` declaration are conservatively treated as CJS.
252
+ */
221
253
  function looksLikeESM(output: string): boolean {
222
254
  if (
223
255
  /\bObject\.defineProperty\(exports\b/.test(output) ||
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * Resolve a user-supplied `--cache-dir` value into an absolute path.
5
+ *
6
+ * When `cacheDir` is absent or empty, returns `undefined` so callers fall
7
+ * through to the environment variable and global-cache defaults handled inside
8
+ * `buildSourcePlugin`. When `cacheDir` is already absolute it is returned
9
+ * unchanged; otherwise it is resolved relative to `cwd`.
10
+ */
11
+ export function resolveCacheDir(
12
+ cwd: string,
13
+ cacheDir?: string,
14
+ ): string | undefined {
15
+ if (!cacheDir) {
16
+ return undefined;
17
+ }
18
+ return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
19
+ }
@@ -47,5 +47,7 @@ function defaultLocalBinaryPath(): string | null {
47
47
 
48
48
  function packageRootDir(): string {
49
49
  const moduleDir = path.resolve(__dirname, "..", "..");
50
+ // Prefer the faster native variant; fall back to the JS implementation on
51
+ // platforms (or environments) where `realpathSync.native` is unavailable.
50
52
  return fs.realpathSync.native?.(moduleDir) ?? fs.realpathSync(moduleDir);
51
53
  }
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { TtscCompiler } from "../../TtscCompiler";
6
+ import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
6
7
  import { resolveProjectConfig } from "../../compiler/internal/project/resolveProjectConfig";
7
8
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
8
9
  import { runBuild } from "../../compiler/internal/runBuild";
@@ -10,7 +11,18 @@ import { runSingleFileEmit } from "../../compiler/internal/runSingleFileEmit";
10
11
  import { defaultPluginCacheCleanTargets } from "../../plugin/internal/buildSourcePlugin";
11
12
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
12
13
  import { getCompilerVersionText } from "./getCompilerVersionText";
14
+ import { resolveCacheDir } from "./resolveCacheDir";
13
15
 
16
+ /**
17
+ * CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane
18
+ * (build, check, fix, format, prepare, clean, or native-delegate) and returns
19
+ * an exit code. Errors thrown by any lane are caught here and written to stderr
20
+ * so the process can exit cleanly.
21
+ *
22
+ * @param argv - Command-line arguments (defaults to `process.argv.slice(2)`).
23
+ * @returns The exit code: `0` on success, `1` on binary-not-found, `2` on user
24
+ * error or build failure.
25
+ */
14
26
  export function runTtsc(
15
27
  argv: readonly string[] = process.argv.slice(2),
16
28
  ): number {
@@ -66,6 +78,12 @@ export function runTtsc(
66
78
  }
67
79
  }
68
80
 
81
+ /**
82
+ * Return `true` when `command` looks like a build argument rather than a
83
+ * subcommand name — a flag (`-p`, `--watch`, …) or a TypeScript/config file
84
+ * path. These are forwarded to the build lane unchanged so users can write
85
+ * `ttsc -p tsconfig.json` without an explicit `build` subcommand.
86
+ */
69
87
  function isBuildAlias(command: string): boolean {
70
88
  if (command.startsWith("-")) return true;
71
89
  return [".json", ".ts", ".tsx", ".mts", ".cts"].some((ext) =>
@@ -361,6 +379,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
361
379
  } else if (current.startsWith("--outDir=")) {
362
380
  outDir = current.slice("--outDir=".length);
363
381
  } else if (current === "-w") {
382
+ // Unreachable: `-w` is handled by the switch case above. Kept here
383
+ // as a guard in case the switch is refactored in the future.
364
384
  watch = true;
365
385
  } else if (current.startsWith("--tsconfig=")) {
366
386
  tsconfig = current.slice("--tsconfig=".length);
@@ -374,6 +394,7 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
374
394
  } else if (current.startsWith("--cache-dir=")) {
375
395
  cacheDir = current.slice("--cache-dir=".length);
376
396
  } else if (current === "--verbose") {
397
+ // Unreachable: `--verbose` is handled by the switch case above.
377
398
  quiet = false;
378
399
  } else if (current.startsWith("-")) {
379
400
  throw new Error(`ttsc: unknown option ${current}`);
@@ -455,7 +476,12 @@ function runSingleFile(options: ReturnType<typeof parseBuildArgs>): number {
455
476
  }
456
477
  const cwd = path.resolve(options.cwd ?? process.cwd());
457
478
  const file = path.resolve(cwd, options.files[0]!);
458
- const out = resolveSingleFileOut(file, cwd, options.outDir);
479
+ const out = resolveSingleFileOut({
480
+ cliOutDir: options.outDir,
481
+ cwd,
482
+ file,
483
+ tsconfig: options.tsconfig,
484
+ });
459
485
  const text = runSingleFileEmit({
460
486
  binary: options.binary,
461
487
  cwd,
@@ -471,17 +497,99 @@ function runSingleFile(options: ReturnType<typeof parseBuildArgs>): number {
471
497
  return 0;
472
498
  }
473
499
 
474
- function resolveSingleFileOut(
475
- file: string,
476
- cwd: string,
477
- outDir?: string,
478
- ): string {
479
- const relative = path.relative(cwd, file);
480
- const jsRelative = relative.replace(/\.[cm]?tsx?$/i, ".js");
481
- if (outDir) {
482
- return path.resolve(cwd, outDir, jsRelative);
500
+ function resolveSingleFileOut(opts: {
501
+ cliOutDir?: string;
502
+ cwd: string;
503
+ file: string;
504
+ tsconfig?: string;
505
+ }): string {
506
+ const jsBasename =
507
+ path.basename(opts.file).replace(/\.[cm]?tsx?$/i, "") +
508
+ singleFileJsExtension(opts.file);
509
+
510
+ // Explicit CLI --outDir wins. Mirrors the CWD-relative source layout under
511
+ // the requested directory so existing single-file invocations don't shift.
512
+ if (opts.cliOutDir) {
513
+ const relative = path.relative(opts.cwd, opts.file);
514
+ const jsRelative =
515
+ relative.slice(0, relative.length - path.extname(relative).length) +
516
+ singleFileJsExtension(opts.file);
517
+ return path.resolve(opts.cwd, opts.cliOutDir, jsRelative);
518
+ }
519
+
520
+ // No CLI override: honor tsconfig's outDir so `ttsc src/foo.ts` lands the
521
+ // emitted JS at `<outDir>/<relative-from-rootDir>.js` instead of dropping
522
+ // it next to the source file. This matches how project mode emits and how
523
+ // `tsc <file>` would behave with the same tsconfig.
524
+ const projectOutDir = readProjectOutDir({
525
+ cwd: opts.cwd,
526
+ file: opts.file,
527
+ tsconfig: opts.tsconfig,
528
+ });
529
+ if (projectOutDir !== null) {
530
+ const fromRoot = path.relative(projectOutDir.rootDir, opts.file);
531
+ if (fromRoot !== "" && !isOutsideSingleFileLayout(fromRoot)) {
532
+ const jsRelative =
533
+ fromRoot.slice(0, fromRoot.length - path.extname(fromRoot).length) +
534
+ singleFileJsExtension(opts.file);
535
+ return path.resolve(projectOutDir.outDir, jsRelative);
536
+ }
537
+ return path.resolve(projectOutDir.outDir, jsBasename);
538
+ }
539
+
540
+ // Last resort (no tsconfig outDir at all): emit next to the source. This
541
+ // preserves the legacy `ttsc <file.ts>` → `<file.js>` behavior for projects
542
+ // that intentionally don't configure outDir.
543
+ return opts.file.replace(/\.[cm]?tsx?$/i, singleFileJsExtension(opts.file));
544
+ }
545
+
546
+ function readProjectOutDir(opts: {
547
+ cwd: string;
548
+ file: string;
549
+ tsconfig?: string;
550
+ }): { outDir: string; rootDir: string } | null {
551
+ try {
552
+ const project = readProjectConfig({
553
+ cwd: opts.cwd,
554
+ file: opts.file,
555
+ tsconfig: opts.tsconfig,
556
+ });
557
+ const outDir = project.compilerOptions.outDir;
558
+ if (typeof outDir !== "string" || outDir.length === 0) {
559
+ return null;
560
+ }
561
+ const rawRoot = project.compilerOptions.rootDir;
562
+ const rootDir =
563
+ typeof rawRoot === "string" && rawRoot.length !== 0
564
+ ? path.isAbsolute(rawRoot)
565
+ ? rawRoot
566
+ : path.resolve(project.root, rawRoot)
567
+ : project.root;
568
+ return { outDir, rootDir };
569
+ } catch {
570
+ // Missing or unreadable tsconfig: fall back to the legacy behavior so
571
+ // `ttsc <file>` still works outside a configured project.
572
+ return null;
573
+ }
574
+ }
575
+
576
+ function isOutsideSingleFileLayout(relative: string): boolean {
577
+ return (
578
+ relative === ".." ||
579
+ relative.startsWith(`..${path.sep}`) ||
580
+ path.isAbsolute(relative)
581
+ );
582
+ }
583
+
584
+ function singleFileJsExtension(file: string): string {
585
+ switch (path.extname(file).toLowerCase()) {
586
+ case ".mts":
587
+ return ".mjs";
588
+ case ".cts":
589
+ return ".cjs";
590
+ default:
591
+ return ".js";
483
592
  }
484
- return file.replace(/\.[cm]?tsx?$/i, ".js");
485
593
  }
486
594
 
487
595
  function runWatch(
@@ -500,8 +608,7 @@ function runWatch(
500
608
  tsconfig: options.tsconfig,
501
609
  }),
502
610
  );
503
- const watchRoot = root;
504
- const directories = collectWatchDirectories(watchRoot);
611
+ const directories = collectWatchDirectories(root);
505
612
  const watchers = directories.map((dir) =>
506
613
  fs.watch(dir, { persistent: true }, () => trigger()),
507
614
  );
@@ -564,13 +671,6 @@ function runWatch(
564
671
  return 0;
565
672
  }
566
673
 
567
- function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
568
- if (!cacheDir) {
569
- return undefined;
570
- }
571
- return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
572
- }
573
-
574
674
  function collectWatchDirectories(root: string): string[] {
575
675
  const out: string[] = [];
576
676
  const stack: string[] = [root];
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
- import * as os from "node:os";
3
+ import os from "node:os";
4
4
 
5
5
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
6
6
  import { resolveTtscserverBinary } from "./resolveTtscserverBinary";
@@ -66,8 +66,16 @@ export function runTtscserver(
66
66
  return result.status ?? 1;
67
67
  }
68
68
 
69
+ /**
70
+ * Build the environment for the native binary. When running in `--stdio` (LSP)
71
+ * mode the Go binary needs to know which tsgo binary to wrap; inject
72
+ * `TTSC_TSGO_BINARY` so the native host does not have to re-resolve it from
73
+ * inside a potentially different working directory. Skip injection when the
74
+ * caller already provided the variable or passed an explicit `--tsgo` option.
75
+ */
69
76
  function resolveTtscserverEnv(argv: readonly string[]): NodeJS.ProcessEnv {
70
77
  if (!argv.includes("--stdio")) {
78
+ // Non-LSP invocations (--version, --help) do not shell out to tsgo.
71
79
  return process.env;
72
80
  }
73
81
  if (process.env.TTSC_TSGO_BINARY || hasTsgoOption(argv)) {
@@ -4,7 +4,16 @@ import path from "node:path";
4
4
 
5
5
  import { getCompilerVersionText } from "./getCompilerVersionText";
6
6
  import { prepareExecution } from "./prepareExecution";
7
-
7
+ import { resolveCacheDir } from "./resolveCacheDir";
8
+
9
+ /**
10
+ * CLI entry point for `ttsx`. Type-checks the owning project via tsgo, emits
11
+ * JavaScript to a PID-isolated temp directory, rewrites ESM specifiers when
12
+ * needed, and executes the compiled entry with the current Node.js runtime.
13
+ *
14
+ * @param argv - Command-line arguments (defaults to `process.argv.slice(2)`).
15
+ * @returns The child-process exit code, or `2` on a ttsx-level error.
16
+ */
8
17
  export function runTtsx(
9
18
  argv: readonly string[] = process.argv.slice(2),
10
19
  ): number {
@@ -172,13 +181,6 @@ function isRelativeSpecifier(specifier: string): boolean {
172
181
  );
173
182
  }
174
183
 
175
- function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
176
- if (!cacheDir) {
177
- return undefined;
178
- }
179
- return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
180
- }
181
-
182
184
  function takeValue(flag: string, rest: string[]): string {
183
185
  const value = rest.shift();
184
186
  if (!value) {
@@ -222,6 +224,12 @@ function runPreparedEntry(
222
224
  return result.status ?? 1;
223
225
  }
224
226
 
227
+ /**
228
+ * Walk every `.js`/`.mjs`/`.cjs` file in `root` and rewrite bare relative
229
+ * specifiers (e.g. `"./foo"`) to include the resolved file extension so Node
230
+ * can load them with `--input-type=module`. Files that did not change are not
231
+ * written back.
232
+ */
225
233
  function rewriteEsmSpecifiers(root: string): void {
226
234
  const stack = [root];
227
235
  while (stack.length !== 0) {
@@ -431,26 +439,37 @@ function findTemplateExpressionEnd(
431
439
  return null;
432
440
  }
433
441
 
442
+ /**
443
+ * Append or fix a file extension on a relative specifier so that Node's ESM
444
+ * loader can resolve it. The resolution order mirrors Node's own algorithm:
445
+ * exact file match → directory index → `.js` fallback. Non-relative specifiers
446
+ * and already-extensioned paths are returned unchanged.
447
+ */
434
448
  function withResolvableExtension(fromFile: string, specifier: string): string {
435
449
  if (!specifier.startsWith(".")) {
450
+ // Bare specifiers (packages, builtins) need no rewriting.
436
451
  return specifier;
437
452
  }
438
453
  if (/\.(?:[cm]?js|json|node)$/i.test(specifier)) {
454
+ // Already has a concrete extension the loader understands.
439
455
  return specifier;
440
456
  }
441
457
  const [pathname, suffix = ""] = splitSpecifierSuffix(specifier);
442
458
  const fromDir = path.dirname(fromFile);
459
+ // 1. Try the specifier as a file path with each JS extension.
443
460
  for (const extension of [".js", ".mjs", ".cjs"]) {
444
461
  if (fs.existsSync(path.resolve(fromDir, pathname + extension))) {
445
462
  return pathname + extension + suffix;
446
463
  }
447
464
  }
465
+ // 2. Try interpreting it as a directory with an index file.
448
466
  const directory = pathname.replace(/\/+$/, "");
449
467
  for (const extension of [".js", ".mjs", ".cjs"]) {
450
468
  if (fs.existsSync(path.resolve(fromDir, directory, "index" + extension))) {
451
469
  return `${directory}/index${extension}${suffix}`;
452
470
  }
453
471
  }
472
+ // 3. Last resort: append `.js` and let Node surface the error if it's wrong.
454
473
  return `${specifier}.js`;
455
474
  }
456
475
 
@@ -553,6 +572,12 @@ function skipRegex(input: string, start: number): number {
553
572
  return input.length;
554
573
  }
555
574
 
575
+ /**
576
+ * Determine whether the `/` at `start` opens a regex literal rather than a
577
+ * division operator. This is a conservative approximation: check the preceding
578
+ * non-whitespace token — if it is an operator character or a keyword that must
579
+ * be followed by an expression, we assume regex.
580
+ */
556
581
  function looksLikeRegexStart(input: string, start: number): boolean {
557
582
  let i = start - 1;
558
583
  while (i >= 0 && /\s/.test(input[i]!)) {
@@ -5,6 +5,8 @@ import { createRequire } from "node:module";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
 
8
+ import { findNearestGoMod } from "../../compiler/internal/paths";
9
+
8
10
  const GO_MOD_SEARCH_MAX_DEPTH = 3;
9
11
  const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
10
12
  const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
@@ -397,20 +399,6 @@ function resolveSourceBuildTarget(opts: {
397
399
  };
398
400
  }
399
401
 
400
- function findNearestGoMod(from: string, maxDepth: number): string | null {
401
- let current = path.resolve(from);
402
- let depth = 0;
403
- while (true) {
404
- const candidate = path.join(current, "go.mod");
405
- if (fs.existsSync(candidate)) return candidate;
406
- if (depth >= maxDepth) return null;
407
- const parent = path.dirname(current);
408
- if (parent === current) return null;
409
- current = parent;
410
- depth += 1;
411
- }
412
- }
413
-
414
402
  function materializeScratchDir(source: string, scratch: string): void {
415
403
  fs.mkdirSync(scratch, { recursive: true });
416
404
  fs.cpSync(source, scratch, {
@@ -768,6 +756,19 @@ function walkForGoMod(dir: string, out: string[]): void {
768
756
  }
769
757
  }
770
758
 
759
+ /**
760
+ * Resolve the directory where compiled plugin binaries are cached.
761
+ *
762
+ * Priority order:
763
+ *
764
+ * 1. `cacheDir` option (resolved relative to `projectRoot`).
765
+ * 2. `TTSC_CACHE_DIR` environment variable.
766
+ * 3. Platform-specific global user cache (XDG / AppData / Library/Caches).
767
+ *
768
+ * When falling back to the global cache, a GC pass is triggered
769
+ * opportunistically to evict old or oversized entries before the cache is
770
+ * used.
771
+ */
771
772
  export function resolvePluginCacheRoot(
772
773
  projectRoot: string,
773
774
  cacheDir?: string,
@@ -783,6 +784,13 @@ export function resolvePluginCacheRoot(
783
784
  return root;
784
785
  }
785
786
 
787
+ /**
788
+ * Return the absolute path to the global plugin binary cache root.
789
+ *
790
+ * The path follows platform conventions (XDG on Linux, AppData on Windows,
791
+ * Library/Caches on macOS) and does not depend on project-local config. Used by
792
+ * `ttsc cache clean` to enumerate cache locations.
793
+ */
786
794
  export function resolveGlobalPluginCacheRoot(): string {
787
795
  return path.join(
788
796
  resolveUserCacheRoot(),
@@ -791,6 +799,13 @@ export function resolveGlobalPluginCacheRoot(): string {
791
799
  );
792
800
  }
793
801
 
802
+ /**
803
+ * Return all directories that `ttsc cache clean` should wipe for a project.
804
+ *
805
+ * Covers three locations where plugin binaries may accumulate: the global user
806
+ * cache, the project-local `node_modules/.ttsc` cache (legacy), and the
807
+ * project-local `.ttsc` cache.
808
+ */
794
809
  export function defaultPluginCacheCleanTargets(projectRoot: string): string[] {
795
810
  return [
796
811
  resolveGlobalPluginCacheRoot(),
@@ -878,6 +893,17 @@ function resolveGoCompiler(): string {
878
893
  return "go";
879
894
  }
880
895
 
896
+ /**
897
+ * Compute a deterministic SHA-256 cache key for a plugin build.
898
+ *
899
+ * The key covers every input that can produce a different binary: ttsc/tsgo
900
+ * versions, platform, entry package, Go compiler identity, Go build environment
901
+ * variables, overlay module sources, plugin source files, and contributor
902
+ * source files. Contributors are sorted by name so declaration order does not
903
+ * affect the key.
904
+ *
905
+ * Exposed for testing and for the `ttsc cache` CLI command.
906
+ */
881
907
  export function computeCacheKey(inputs: {
882
908
  contributors?: readonly ITtscBuildContributor[];
883
909
  dir: string;
@@ -2,9 +2,10 @@ import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
 
5
+ import { findNearestGoMod } from "../../compiler/internal/paths";
5
6
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
6
- import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
7
7
  import type { ITtscPlugin } from "../../structures/ITtscPlugin";
8
+ import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
8
9
  import type { ITtscPluginFactoryContext } from "../../structures/ITtscPluginFactoryContext";
9
10
  import type { ITtscProjectPluginConfig } from "../../structures/ITtscProjectPluginConfig";
10
11
  import type { TtscPluginStage } from "../../structures/TtscPluginStage";
@@ -30,6 +31,24 @@ type PackageManifest = {
30
31
  ttsc?: unknown;
31
32
  };
32
33
 
34
+ /**
35
+ * Resolve, load, and build all native plugin sidecars for a TypeScript project.
36
+ *
37
+ * Reads the project config, discovers plugin entries (from tsconfig and package
38
+ * auto-discovery), validates and composes their descriptors, then invokes
39
+ * `buildSourcePlugin` to compile each Go source package into a cached binary.
40
+ * Returns the ordered set of loaded native plugins alongside the parsed project
41
+ * config.
42
+ *
43
+ * @param options.binary - Absolute path to the ttsc native helper binary.
44
+ * @param options.cacheDir - Override the plugin binary cache directory.
45
+ * @param options.cwd - Working directory for resolving relative paths.
46
+ * @param options.entries - Explicit plugin entries; `false` disables all
47
+ * plugins (skips both tsconfig entries and package auto-discovery).
48
+ * @param options.file - Path to the tsconfig/jsconfig file.
49
+ * @param options.projectRoot - Override the project root directory.
50
+ * @param options.tsconfig - Alias for `file`.
51
+ */
33
52
  export function loadProjectPlugins(options: {
34
53
  binary: string;
35
54
  cacheDir?: string;
@@ -85,14 +104,21 @@ export function loadProjectPlugins(options: {
85
104
  validatePluginSource(plugin);
86
105
  const contributors = validatePluginContributors(plugin);
87
106
  const source = resolvePluginSource(plugin.source, context.projectRoot);
88
- const kind = resolveNativeSourceKind(source, plugin, entries[index]!.config, index);
107
+ const kind = resolveNativeSourceKind(
108
+ source,
109
+ plugin,
110
+ entries[index]!.config,
111
+ index,
112
+ );
89
113
  if (kind === "linked" && stage !== "transform") {
90
114
  throw new Error(
91
115
  `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
92
116
  );
93
117
  }
94
118
  const linkedContributorName =
95
- kind === "linked" ? `linked_${String(index).padStart(6, "0")}` : undefined;
119
+ kind === "linked"
120
+ ? `linked_${String(index).padStart(6, "0")}`
121
+ : undefined;
96
122
  return {
97
123
  contributors,
98
124
  config: entries[index]!.config,
@@ -261,6 +287,7 @@ function composePluginSources(
261
287
  return {
262
288
  ...plugin,
263
289
  source: aggregate.plugin.source,
290
+ contributors: aggregate.plugin.contributors,
264
291
  };
265
292
  });
266
293
  }
@@ -276,6 +303,14 @@ function matchesPluginAlias(
276
303
  );
277
304
  }
278
305
 
306
+ /**
307
+ * Return `true` when the project has at least one enabled plugin entry.
308
+ *
309
+ * Used by callers that need to skip plugin-specific work when no plugins are
310
+ * configured, without paying the full cost of `loadProjectPlugins`.
311
+ *
312
+ * @param entries - Explicit entries; `false` always returns `false`.
313
+ */
279
314
  export function hasProjectPluginEntries(
280
315
  project: ITtscParsedProjectConfig,
281
316
  entries?: readonly ITtscProjectPluginConfig[] | false,
@@ -610,7 +645,10 @@ function resolveNativeSourceKind(
610
645
  config: ITtscProjectPluginConfig,
611
646
  index: number,
612
647
  ): "executable" | "linked" {
613
- const packageDir = resolveGoPackageDir(source, pluginLabel(plugin, config, index));
648
+ const packageDir = resolveGoPackageDir(
649
+ source,
650
+ pluginLabel(plugin, config, index),
651
+ );
614
652
  if (findNearestGoMod(packageDir, GO_MOD_SEARCH_MAX_DEPTH) === null) {
615
653
  throw new Error(
616
654
  `ttsc: plugin "${pluginLabel(plugin, config, index)}" source must be inside a Go module with go.mod within ${GO_MOD_SEARCH_MAX_DEPTH} parent directories: ${source}`,
@@ -625,20 +663,6 @@ function resolveNativeSourceKind(
625
663
  return packageName === "main" ? "executable" : "linked";
626
664
  }
627
665
 
628
- function findNearestGoMod(from: string, maxDepth: number): string | null {
629
- let current = path.resolve(from);
630
- let depth = 0;
631
- while (true) {
632
- const candidate = path.join(current, "go.mod");
633
- if (fs.existsSync(candidate)) return candidate;
634
- if (depth >= maxDepth) return null;
635
- const parent = path.dirname(current);
636
- if (parent === current) return null;
637
- current = parent;
638
- depth += 1;
639
- }
640
- }
641
-
642
666
  function resolveGoPackageDir(source: string, label: string): string {
643
667
  if (!fs.existsSync(source)) {
644
668
  throw new Error(`ttsc: plugin "${label}" source does not exist: ${source}`);
@@ -22,7 +22,14 @@ export namespace ITtscCompilerTransformation {
22
22
  /** Non-fatal diagnostics reported during transformation. */
23
23
  diagnostics?: ITtscCompilerDiagnostic[];
24
24
 
25
- /** Transformed TypeScript source text keyed by project file path. */
25
+ /**
26
+ * Transformed TypeScript source text keyed by project-relative file path.
27
+ *
28
+ * Values are TypeScript source text, never JavaScript, declaration files,
29
+ * or source maps. When no transform sidecar is configured, this map
30
+ * contains the unmodified TypeScript files loaded by the TypeScript-Go
31
+ * Program.
32
+ */
26
33
  typescript: Record<string, string>;
27
34
  }
28
35
 
@@ -31,7 +38,13 @@ export namespace ITtscCompilerTransformation {
31
38
  /** Indicates that transformation completed with diagnostics. */
32
39
  type: "failure";
33
40
 
34
- /** Transformed or partially transformed TypeScript source text. */
41
+ /**
42
+ * Transformed or partially transformed TypeScript source text keyed by
43
+ * project-relative file path.
44
+ *
45
+ * May be empty or partial when diagnostics prevented the transform sidecar
46
+ * from completing its pass.
47
+ */
35
48
  typescript: Record<string, string>;
36
49
 
37
50
  /** Diagnostics reported during transformation. */
@@ -96,5 +96,4 @@ export interface ITtscPlugin {
96
96
  * unique within a single plugin build.
97
97
  */
98
98
  contributors?: ITtscPluginContributor[];
99
-
100
99
  }
@@ -10,6 +10,4 @@ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
10
10
  out?: string;
11
11
  /** Suppress summary banners from ttsc/native sidecars. Defaults to `true`. */
12
12
  quiet?: boolean;
13
- /** Normalize compiler output so diagnostics can be parsed structurally. */
14
- structuredDiagnostics?: boolean;
15
13
  }