ttsc 0.12.3 → 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 (126) 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 +22 -9
  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/shim.go +24 -0
  94. package/shim/checker/shim.go +35 -0
  95. package/shim/core/shim.go +17 -0
  96. package/shim/diagnosticwriter/shim.go +4 -0
  97. package/shim/lsp/shim.go +3 -3
  98. package/shim/printer/shim.go +20 -0
  99. package/src/TtscCompiler.ts +7 -0
  100. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  101. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  102. package/src/compiler/internal/paths.ts +68 -0
  103. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  104. package/src/compiler/internal/project/resolveProjectConfig.ts +24 -1
  105. package/src/compiler/internal/resolveBinary.ts +17 -1
  106. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  107. package/src/compiler/internal/resolveTsgo.ts +29 -1
  108. package/src/compiler/internal/runBuild.ts +110 -55
  109. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  110. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  111. package/src/compiler/internal/spawnNative.ts +64 -0
  112. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  113. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  114. package/src/launcher/internal/prepareExecution.ts +32 -0
  115. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  116. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  117. package/src/launcher/internal/runTtsc.ts +22 -10
  118. package/src/launcher/internal/runTtscserver.ts +9 -1
  119. package/src/launcher/internal/runTtsx.ts +33 -8
  120. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  121. package/src/plugin/internal/loadProjectPlugins.ts +42 -18
  122. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  123. package/src/structures/ITtscPlugin.ts +0 -1
  124. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  125. package/tools/gen_shims/main.go +43 -8
  126. package/utility/host.go +294 -245
@@ -0,0 +1,64 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+
4
+ /**
5
+ * Spawn a native binary (or a Node.js script when the path has a JS/TS
6
+ * extension) with the given args and return the `spawnSync` result. Sets a
7
+ * generous 256 MiB output buffer so large projects do not truncate.
8
+ *
9
+ * When `options.encoding` is omitted it defaults to `"utf8"`. Pass an explicit
10
+ * value when the caller needs a different encoding or raw `Buffer` output.
11
+ */
12
+ export function spawnNative(
13
+ binary: string,
14
+ args: readonly string[],
15
+ options: {
16
+ cwd?: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ encoding?: BufferEncoding;
19
+ },
20
+ ) {
21
+ const viaNode = /\.(?:[cm]?js|ts)$/i.test(binary);
22
+ if (!viaNode) {
23
+ ensureExecutable(binary);
24
+ }
25
+ return spawnSync(
26
+ viaNode ? process.execPath : binary,
27
+ viaNode ? [binary, ...args] : [...args],
28
+ {
29
+ cwd: options.cwd,
30
+ encoding: options.encoding ?? "utf8",
31
+ env: options.env,
32
+ maxBuffer: 1024 * 1024 * 256,
33
+ windowsHide: true,
34
+ },
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Ensure the binary has the executable bit set on POSIX systems. Silently skips
40
+ * on Windows and swallows `chmod` errors to let the original spawn error
41
+ * surface instead of masking it with a permission error.
42
+ */
43
+ export function ensureExecutable(binary: string): void {
44
+ if (process.platform === "win32") {
45
+ return;
46
+ }
47
+ try {
48
+ const mode = fs.statSync(binary).mode & 0o777;
49
+ if ((mode & 0o111) !== 0) {
50
+ return;
51
+ }
52
+ fs.chmodSync(binary, mode | 0o755);
53
+ } catch {
54
+ /* keep the original spawn error path */
55
+ }
56
+ }
57
+
58
+ /** Coerce a `spawnSync` output value to a plain string, defaulting to `""`. */
59
+ export function outputText(value: string | Buffer | null | undefined): string {
60
+ if (value == null) {
61
+ return "";
62
+ }
63
+ return typeof value === "string" ? value : value.toString("utf8");
64
+ }
@@ -1,5 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
1
  import path from "node:path";
4
2
 
5
3
  import {
@@ -12,6 +10,7 @@ import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoa
12
10
  import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
13
11
  import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
14
12
  import { buildNativeCompiler } from "./buildNativeCompiler";
13
+ import { packageRootDir } from "./paths";
15
14
  import { readProjectConfig } from "./project/readProjectConfig";
16
15
  import { resolveBinary } from "./resolveBinary";
17
16
  import { resolveTsgo } from "./resolveTsgo";
@@ -21,8 +20,23 @@ import {
21
20
  linkedTransformPlugins,
22
21
  selectSharedHostPlugin,
23
22
  } from "./sharedHostHelpers";
23
+ import { outputText, spawnNative } from "./spawnNative";
24
24
 
25
- /** Transform a project and capture TypeScript source output in memory. */
25
+ /**
26
+ * Transform a project and capture TypeScript source output in memory.
27
+ *
28
+ * When no plugins are configured the fast path spawns the native ttsc compiler
29
+ * host (`cmd/ttsc api-transform`) which returns a JSON map of transformed
30
+ * TypeScript sources. When plugins are present:
31
+ *
32
+ * 1. Check-stage plugins run first and abort on failure.
33
+ * 2. If there are no transform-stage plugins the host is used as the transformer.
34
+ * 3. If transform plugins exist they are dispatched through the shared-host binary
35
+ * with linked plugins passed via `TTSC_LINKED_PLUGINS_JSON`.
36
+ *
37
+ * @returns A `{ result, typescript }` pair where `typescript` maps output paths
38
+ * to their transformed TypeScript source text.
39
+ */
26
40
  export function transformProjectInMemory(options: ITtscCompilerContext): {
27
41
  result: TtscBuildResult;
28
42
  typescript: Record<string, string>;
@@ -39,6 +53,7 @@ export function transformProjectInMemory(options: ITtscCompilerContext): {
39
53
  return transformProjectWithNativeHost(options, project);
40
54
  }
41
55
 
56
+ /** Return true when the project or the call-level options declare any plugins. */
42
57
  function hasConfiguredPlugins(
43
58
  options: ITtscCompilerContext,
44
59
  project: ITtscParsedProjectConfig,
@@ -46,6 +61,11 @@ function hasConfiguredPlugins(
46
61
  return hasProjectPluginEntries(project, options.plugins);
47
62
  }
48
63
 
64
+ /**
65
+ * Transform via the built-in native compiler host (`cmd/ttsc api-transform`).
66
+ * Used when no user plugins are configured, or as the fallback transformer when
67
+ * check-stage plugins pass and no transform-stage plugins are declared.
68
+ */
49
69
  function transformProjectWithNativeHost(
50
70
  options: ITtscCompilerContext,
51
71
  project: ITtscParsedProjectConfig,
@@ -160,6 +180,10 @@ function transformProjectWithPlugins(
160
180
  };
161
181
  }
162
182
 
183
+ /**
184
+ * Run every check-stage plugin in sequence, short-circuiting on the first
185
+ * failure. Returns the aggregated `TtscBuildResult` (status 0 when all pass).
186
+ */
163
187
  function runNativeChecks(
164
188
  options: ITtscCompilerContext,
165
189
  project: ITtscParsedProjectConfig,
@@ -204,6 +228,7 @@ function runNativeChecks(
204
228
  return result;
205
229
  }
206
230
 
231
+ /** Build the CLI argument list for the `transform` subcommand. */
207
232
  function createNativeTransformArgs(
208
233
  project: ITtscParsedProjectConfig,
209
234
  plugins: readonly ITtscLoadedNativePlugin[],
@@ -216,6 +241,7 @@ function createNativeTransformArgs(
216
241
  ];
217
242
  }
218
243
 
244
+ /** Build the CLI argument list for the `check` subcommand. */
219
245
  function createNativeCheckArgs(
220
246
  project: ITtscParsedProjectConfig,
221
247
  plugins: readonly ITtscLoadedNativePlugin[],
@@ -228,6 +254,10 @@ function createNativeCheckArgs(
228
254
  ];
229
255
  }
230
256
 
257
+ /**
258
+ * Serialize the plugin list to a JSON string for `--plugins-json=`. Only the
259
+ * fields the native binary needs are included to keep the arg short.
260
+ */
231
261
  function serializeNativePlugins(
232
262
  plugins: readonly ITtscLoadedNativePlugin[],
233
263
  ): string {
@@ -240,6 +270,12 @@ function serializeNativePlugins(
240
270
  );
241
271
  }
242
272
 
273
+ /**
274
+ * Build the environment for a native plugin spawn. Injects `TTSC_NODE_BINARY`,
275
+ * `TTSC_TSGO_BINARY`, and `TTSC_TTSX_BINARY` so the sidecar can re-invoke
276
+ * Node.js or tsgo without searching PATH. For transform plugins, also passes
277
+ * `TTSC_LINKED_PLUGINS_JSON` when linked sources are present.
278
+ */
243
279
  function nativePluginEnv(
244
280
  options: ITtscCompilerContext,
245
281
  projectRoot: string,
@@ -265,53 +301,13 @@ function nativePluginEnv(
265
301
  return env;
266
302
  }
267
303
 
268
- function spawnNative(
269
- binary: string,
270
- args: readonly string[],
271
- options: {
272
- cwd?: string;
273
- env?: NodeJS.ProcessEnv;
274
- },
275
- ) {
276
- const viaNode = /\.(?:[cm]?js|ts)$/i.test(binary);
277
- if (!viaNode) {
278
- ensureExecutable(binary);
279
- }
280
- return spawnSync(
281
- viaNode ? process.execPath : binary,
282
- viaNode ? [binary, ...args] : [...args],
283
- {
284
- cwd: options.cwd,
285
- encoding: "utf8",
286
- env: options.env,
287
- maxBuffer: 1024 * 1024 * 256,
288
- windowsHide: true,
289
- },
290
- );
291
- }
292
-
293
- function ensureExecutable(binary: string): void {
294
- if (process.platform === "win32") {
295
- return;
296
- }
297
- try {
298
- const mode = fs.statSync(binary).mode & 0o777;
299
- if ((mode & 0o111) !== 0) {
300
- return;
301
- }
302
- fs.chmodSync(binary, mode | 0o755);
303
- } catch {
304
- /* keep the original spawn error path */
305
- }
306
- }
307
-
308
- function outputText(value: string | Buffer | null | undefined): string {
309
- if (value == null) {
310
- return "";
311
- }
312
- return typeof value === "string" ? value : value.toString("utf8");
313
- }
314
-
304
+ /**
305
+ * Parse the JSON envelope written by the native transform host to stdout.
306
+ *
307
+ * The `typescript` field must be a `Record<string, string>`. Any other shape is
308
+ * treated as a protocol error and throws with the stderr/stdout context. JSON
309
+ * parse errors are also wrapped with the same context message.
310
+ */
315
311
  function parseNativeTransformOutput(
316
312
  stdout: string,
317
313
  stderr: string,
@@ -344,6 +340,7 @@ function parseNativeTransformOutput(
344
340
  }
345
341
  }
346
342
 
343
+ /** Type guard: true when `value` is a non-null, non-array object of strings. */
347
344
  function isTextRecord(value: unknown): value is Record<string, string> {
348
345
  return (
349
346
  typeof value === "object" &&
@@ -352,20 +349,3 @@ function isTextRecord(value: unknown): value is Record<string, string> {
352
349
  Object.values(value).every((entry) => typeof entry === "string")
353
350
  );
354
351
  }
355
-
356
- function packageRootDir(): string {
357
- let current = path.resolve(__dirname);
358
- while (true) {
359
- if (
360
- fs.existsSync(path.join(current, "package.json")) &&
361
- fs.existsSync(path.join(current, "go.mod"))
362
- ) {
363
- return fs.realpathSync.native?.(current) ?? fs.realpathSync(current);
364
- }
365
- const parent = path.dirname(current);
366
- if (parent === current) {
367
- throw new Error("ttsc: package root not found for native compiler build");
368
- }
369
- current = parent;
370
- }
371
- }
@@ -23,6 +23,7 @@ export function getCompilerVersionText(
23
23
  return `ttsc ${readOwnPackageVersion()} (${outputText(res.stdout).trim()})`;
24
24
  }
25
25
 
26
+ /** Coerce a spawnSync stdio field (string | Buffer | null) to a plain string. */
26
27
  function outputText(value: string | Buffer | null | undefined): string {
27
28
  if (value == null) {
28
29
  return "";
@@ -30,6 +31,12 @@ function outputText(value: string | Buffer | null | undefined): string {
30
31
  return typeof value === "string" ? value : value.toString("utf8");
31
32
  }
32
33
 
34
+ /**
35
+ * Read the `version` field from the `@ttsc/ttsc` package.json that sits three
36
+ * directories above the compiled launcher output (`lib/launcher/internal/`).
37
+ * Returns `"0.0.0"` on any I/O or parse failure so the banner is always safe to
38
+ * display.
39
+ */
33
40
  function readOwnPackageVersion(): string {
34
41
  try {
35
42
  const file = path.resolve(__dirname, "..", "..", "..", "package.json");
@@ -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
  }
@@ -11,7 +11,18 @@ import { runSingleFileEmit } from "../../compiler/internal/runSingleFileEmit";
11
11
  import { defaultPluginCacheCleanTargets } from "../../plugin/internal/buildSourcePlugin";
12
12
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
13
13
  import { getCompilerVersionText } from "./getCompilerVersionText";
14
-
14
+ import { resolveCacheDir } from "./resolveCacheDir";
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
+ */
15
26
  export function runTtsc(
16
27
  argv: readonly string[] = process.argv.slice(2),
17
28
  ): number {
@@ -67,6 +78,12 @@ export function runTtsc(
67
78
  }
68
79
  }
69
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
+ */
70
87
  function isBuildAlias(command: string): boolean {
71
88
  if (command.startsWith("-")) return true;
72
89
  return [".json", ".ts", ".tsx", ".mts", ".cts"].some((ext) =>
@@ -362,6 +379,8 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
362
379
  } else if (current.startsWith("--outDir=")) {
363
380
  outDir = current.slice("--outDir=".length);
364
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.
365
384
  watch = true;
366
385
  } else if (current.startsWith("--tsconfig=")) {
367
386
  tsconfig = current.slice("--tsconfig=".length);
@@ -375,6 +394,7 @@ function parseBuildArgs(argv: readonly string[], checkOnly: boolean) {
375
394
  } else if (current.startsWith("--cache-dir=")) {
376
395
  cacheDir = current.slice("--cache-dir=".length);
377
396
  } else if (current === "--verbose") {
397
+ // Unreachable: `--verbose` is handled by the switch case above.
378
398
  quiet = false;
379
399
  } else if (current.startsWith("-")) {
380
400
  throw new Error(`ttsc: unknown option ${current}`);
@@ -588,8 +608,7 @@ function runWatch(
588
608
  tsconfig: options.tsconfig,
589
609
  }),
590
610
  );
591
- const watchRoot = root;
592
- const directories = collectWatchDirectories(watchRoot);
611
+ const directories = collectWatchDirectories(root);
593
612
  const watchers = directories.map((dir) =>
594
613
  fs.watch(dir, { persistent: true }, () => trigger()),
595
614
  );
@@ -652,13 +671,6 @@ function runWatch(
652
671
  return 0;
653
672
  }
654
673
 
655
- function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
656
- if (!cacheDir) {
657
- return undefined;
658
- }
659
- return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
660
- }
661
-
662
674
  function collectWatchDirectories(root: string): string[] {
663
675
  const out: string[] = [];
664
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;