ttsc 0.3.1 → 0.4.0-dev.20260425

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 (121) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +93 -362
  3. package/cmd/ttsc/build.go +128 -0
  4. package/cmd/ttsc/main.go +222 -0
  5. package/cmd/ttsc/main_test.go +288 -0
  6. package/cmd/ttsc/transform.go +166 -0
  7. package/driver/host.go +31 -0
  8. package/driver/program.go +298 -0
  9. package/driver/rewrite.go +315 -0
  10. package/go.mod +48 -0
  11. package/go.sum +22 -0
  12. package/lib/api.d.ts +91 -0
  13. package/lib/api.js +325 -0
  14. package/lib/api.js.map +1 -0
  15. package/lib/cli.d.ts +1 -0
  16. package/lib/cli.js +481 -0
  17. package/lib/cli.js.map +1 -0
  18. package/lib/index.d.ts +18 -0
  19. package/lib/index.js +35 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/native.d.ts +15 -0
  22. package/lib/native.js +34 -0
  23. package/lib/native.js.map +1 -0
  24. package/lib/platform.d.ts +66 -0
  25. package/lib/platform.js +170 -0
  26. package/lib/platform.js.map +1 -0
  27. package/lib/plugin.d.ts +47 -0
  28. package/lib/plugin.js +150 -0
  29. package/lib/plugin.js.map +1 -0
  30. package/lib/project.d.ts +32 -0
  31. package/lib/project.js +303 -0
  32. package/lib/project.js.map +1 -0
  33. package/lib/runner/cli.d.ts +1 -0
  34. package/lib/runner/cli.js +284 -0
  35. package/lib/runner/cli.js.map +1 -0
  36. package/lib/runner/register.d.ts +13 -0
  37. package/lib/runner/register.js +424 -0
  38. package/lib/runner/register.js.map +1 -0
  39. package/package.json +72 -63
  40. package/shim/ast/extra-shim.json +4 -0
  41. package/shim/ast/go.mod +5 -0
  42. package/shim/ast/shim.go +117 -0
  43. package/shim/bundled/extra-shim.json +1 -0
  44. package/shim/bundled/go.mod +5 -0
  45. package/shim/bundled/shim.go +19 -0
  46. package/shim/checker/extra-shim.json +31 -0
  47. package/shim/checker/go.mod +5 -0
  48. package/shim/checker/shim.go +146 -0
  49. package/shim/compiler/extra-shim.json +4 -0
  50. package/shim/compiler/go.mod +5 -0
  51. package/shim/compiler/shim.go +50 -0
  52. package/shim/core/extra-shim.json +4 -0
  53. package/shim/core/go.mod +5 -0
  54. package/shim/core/shim.go +11 -0
  55. package/shim/diagnosticwriter/go.mod +5 -0
  56. package/shim/diagnosticwriter/shim.go +29 -0
  57. package/shim/parser/extra-shim.json +4 -0
  58. package/shim/parser/go.mod +5 -0
  59. package/shim/parser/shim.go +55 -0
  60. package/shim/scanner/extra-shim.json +4 -0
  61. package/shim/scanner/go.mod +5 -0
  62. package/shim/scanner/shim.go +96 -0
  63. package/shim/tsoptions/extra-shim.json +4 -0
  64. package/shim/tsoptions/go.mod +5 -0
  65. package/shim/tsoptions/shim.go +123 -0
  66. package/shim/tspath/extra-shim.json +4 -0
  67. package/shim/tspath/go.mod +5 -0
  68. package/shim/tspath/shim.go +165 -0
  69. package/shim/vfs/cachedvfs/extra-shim.json +4 -0
  70. package/shim/vfs/cachedvfs/go.mod +5 -0
  71. package/shim/vfs/cachedvfs/shim.go +12 -0
  72. package/shim/vfs/extra-shim.json +4 -0
  73. package/shim/vfs/go.mod +5 -0
  74. package/shim/vfs/osvfs/extra-shim.json +4 -0
  75. package/shim/vfs/osvfs/go.mod +5 -0
  76. package/shim/vfs/osvfs/shim.go +13 -0
  77. package/shim/vfs/shim.go +19 -0
  78. package/src/api.ts +416 -0
  79. package/src/cli.ts +492 -0
  80. package/src/index.ts +19 -0
  81. package/src/launcher/ttsc-dev.js +42 -0
  82. package/src/launcher/ttsc.js +19 -0
  83. package/src/launcher/ttsx.js +19 -0
  84. package/src/native.ts +60 -0
  85. package/src/platform.ts +163 -0
  86. package/src/plugin.ts +241 -0
  87. package/src/project.ts +330 -0
  88. package/src/runner/cli.ts +286 -0
  89. package/src/runner/register.ts +487 -0
  90. package/test/native.test.ts +60 -0
  91. package/test/package.test.ts +131 -0
  92. package/test/platform.test.ts +58 -0
  93. package/test/project.test.ts +195 -0
  94. package/tools/gen_shims/go.mod +14 -0
  95. package/tools/gen_shims/go.sum +12 -0
  96. package/tools/gen_shims/main.go +446 -0
  97. package/CHANGELOG.md +0 -2
  98. package/bin/tsc +0 -2
  99. package/bin/tsserver +0 -2
  100. package/lib/PluginCreator.d.ts +0 -67
  101. package/lib/PluginCreator.js +0 -190
  102. package/lib/cancelationToken.d.ts +0 -0
  103. package/lib/cancelationToken.js +0 -2
  104. package/lib/loadTypescript.d.ts +0 -7
  105. package/lib/loadTypescript.js +0 -56
  106. package/lib/patchCreateProgram.d.ts +0 -21
  107. package/lib/patchCreateProgram.js +0 -190
  108. package/lib/tsc.d.ts +0 -1
  109. package/lib/tsc.js +0 -24
  110. package/lib/tsserver.d.ts +0 -0
  111. package/lib/tsserver.js +0 -2
  112. package/lib/tsserverlibrary.d.ts +0 -3
  113. package/lib/tsserverlibrary.js +0 -4
  114. package/lib/typescript.d.ts +0 -3
  115. package/lib/typescript.js +0 -4
  116. package/lib/typescriptServices.d.ts +0 -3
  117. package/lib/typescriptServices.js +0 -4
  118. package/lib/typingsInstaller.d.ts +0 -0
  119. package/lib/typingsInstaller.js +0 -2
  120. package/lib/watchGuard.d.ts +0 -0
  121. package/lib/watchGuard.js +0 -2
package/src/api.ts ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Programmatic API for ttsc.
3
+ *
4
+ * This module is the TypeScript surface bundler adapters (unplugin, vite,
5
+ * webpack, rollup, esbuild, rspack, farm, next/swc, bun) consume. Everything
6
+ * here is a thin wrapper around the Go binary resolved by `platform.ts` —
7
+ * adapters never have to spawn the process themselves.
8
+ *
9
+ * Contract:
10
+ *
11
+ * - `transform()` emits one file's rewritten JS, returning the text.
12
+ * - `build()` runs the whole project (tsgo + ttsc rewrite + --emit).
13
+ * - `check()` runs the analysis pass without emitting (CI gate use).
14
+ * - `version()` returns the binary's version banner for user-agent logs.
15
+ *
16
+ * All helpers accept a `binary` override so tests can point at a specific
17
+ * executable without touching PATH or node_modules.
18
+ */
19
+ import { spawnSync } from "node:child_process";
20
+ import * as fs from "node:fs";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+
24
+ import { type ResolveOptions, installHint, resolveBinary } from "./platform";
25
+ import {
26
+ type TtscPlugin,
27
+ applyPluginTransforms,
28
+ loadProjectPlugins,
29
+ } from "./plugin";
30
+ import {
31
+ type ProjectPluginConfig,
32
+ resolveProjectConfig,
33
+ resolveProjectRoot,
34
+ } from "./project";
35
+
36
+ /**
37
+ * Options shared by every API call. `binary` takes precedence over platform
38
+ * resolution; `cwd` defaults to `process.cwd()`; `env` layers on top of the
39
+ * current process env.
40
+ */
41
+ export interface CommonOptions extends ResolveOptions {
42
+ /** Absolute path to an already-resolved ttsc binary. Skips resolution. */
43
+ binary?: string;
44
+ /** Working directory passed to the child process. */
45
+ cwd?: string;
46
+ /** Extra environment variables; merged onto `process.env`. */
47
+ env?: NodeJS.ProcessEnv;
48
+ /**
49
+ * Override project plugin loading. `false` disables tsconfig plugins; an
50
+ * array replaces the tsconfig `compilerOptions.plugins` list.
51
+ */
52
+ plugins?: readonly ProjectPluginConfig[] | false;
53
+ /**
54
+ * Override the native rewrite backend. Defaults to the loaded plugin mode.
55
+ *
56
+ * @deprecated Prefer plugin-declared `native.mode`; this override is for
57
+ * low-level tests and migration probes.
58
+ */
59
+ rewriteMode?: string;
60
+ }
61
+
62
+ /** Options for `transform()`. */
63
+ export interface TransformOptions extends CommonOptions {
64
+ /** Path to the .ts file to transform. Absolute or `cwd`-relative. */
65
+ file: string;
66
+ /** Path to the tsconfig owning `file`. Default: `tsconfig.json`. */
67
+ tsconfig?: string;
68
+ /**
69
+ * When provided, the binary writes JS directly to this path instead of piping
70
+ * stdout. Useful when the emitted text is large.
71
+ */
72
+ out?: string;
73
+ }
74
+
75
+ /** Options for `build()`. */
76
+ export interface BuildOptions extends CommonOptions {
77
+ /** Path to tsconfig.json. Default: `tsconfig.json`. */
78
+ tsconfig?: string;
79
+ /**
80
+ * Emit override. `true` forces emit, `false` forces noEmit, `undefined`
81
+ * follows tsconfig.
82
+ */
83
+ emit?: boolean;
84
+ /** Override compilerOptions.outDir for this invocation. */
85
+ outDir?: string;
86
+ /** Suppress the per-call summary banner. Default: `true`. */
87
+ quiet?: boolean;
88
+ }
89
+
90
+ /** Options for `check()`. */
91
+ export type CheckOptions = Omit<BuildOptions, "emit">;
92
+
93
+ /**
94
+ * Resolve the binary or throw a user-friendly error. Extracted so every API
95
+ * helper shares the same failure mode.
96
+ */
97
+ function resolveOrThrow(opts: CommonOptions): string {
98
+ if (
99
+ opts.binary &&
100
+ path.isAbsolute(opts.binary) &&
101
+ fs.existsSync(opts.binary)
102
+ ) {
103
+ return opts.binary;
104
+ }
105
+ const bin = resolveBinary(opts);
106
+ if (!bin) {
107
+ throw new Error(installHint(opts));
108
+ }
109
+ return bin;
110
+ }
111
+
112
+ /** Merge spawn env without clobbering unrelated vars. */
113
+ function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
114
+ if (!extra) return process.env;
115
+ return { ...process.env, ...extra };
116
+ }
117
+
118
+ function spawnBinary(
119
+ binary: string,
120
+ args: readonly string[],
121
+ options: {
122
+ cwd?: string;
123
+ env?: NodeJS.ProcessEnv;
124
+ encoding?: BufferEncoding;
125
+ },
126
+ ) {
127
+ const viaNode = shouldSpawnViaNode(binary);
128
+ if (!viaNode) {
129
+ ensureExecutable(binary);
130
+ }
131
+ return spawnSync(
132
+ viaNode ? process.execPath : binary,
133
+ viaNode ? [binary, ...args] : [...args],
134
+ {
135
+ cwd: options.cwd,
136
+ env: options.env,
137
+ encoding: options.encoding,
138
+ maxBuffer: 1024 * 1024 * 256,
139
+ windowsHide: true,
140
+ },
141
+ );
142
+ }
143
+
144
+ function shouldSpawnViaNode(binary: string): boolean {
145
+ return /\.(?:[cm]?js|ts)$/i.test(binary);
146
+ }
147
+
148
+ function ensureExecutable(binary: string): void {
149
+ if (process.platform === "win32") {
150
+ return;
151
+ }
152
+ try {
153
+ const mode = fs.statSync(binary).mode & 0o777;
154
+ if ((mode & 0o111) !== 0) {
155
+ return;
156
+ }
157
+ fs.chmodSync(binary, mode | 0o755);
158
+ } catch {
159
+ /* keep the original spawn error path */
160
+ }
161
+ }
162
+
163
+ function outputText(value: string | Buffer | null | undefined): string {
164
+ if (value == null) {
165
+ return "";
166
+ }
167
+ return typeof value === "string" ? value : value.toString("utf8");
168
+ }
169
+
170
+ /**
171
+ * Transform a single .ts file and return the rewritten JS as a string.
172
+ *
173
+ * Intended for bundler per-file transforms (unplugin `transform()`). The caller
174
+ * passes the absolute path; ttsc loads the enclosing tsconfig, compiles with
175
+ * tsgo, and returns the rewritten JS.
176
+ *
177
+ * Throws when the binary exits non-zero — the error includes stderr so bundler
178
+ * error overlays surface the real cause.
179
+ */
180
+ export function transform(options: TransformOptions): string {
181
+ const execution = resolveExecutionContext(options);
182
+ const sourceFile = realpathIfExists(
183
+ path.isAbsolute(options.file)
184
+ ? options.file
185
+ : path.resolve(options.cwd ?? process.cwd(), options.file),
186
+ );
187
+ const args = [
188
+ "transform",
189
+ "--file=" + sourceFile,
190
+ "--tsconfig=" + execution.tsconfig,
191
+ "--rewrite-mode=" + execution.nativeMode,
192
+ ];
193
+
194
+ const res = spawnBinary(execution.bin, args, {
195
+ cwd: options.cwd,
196
+ env: mergeEnv(options.env),
197
+ encoding: "utf8",
198
+ });
199
+ if (res.error) {
200
+ throw new Error(
201
+ "ttsc.transform: failed to spawn " +
202
+ execution.bin +
203
+ ": " +
204
+ res.error.message,
205
+ );
206
+ }
207
+ if (res.status !== 0) {
208
+ throw new Error(
209
+ "ttsc.transform exited " + res.status + "\n" + (res.stderr || ""),
210
+ );
211
+ }
212
+ const transformed = finalizeTransformText(
213
+ execution.plugins,
214
+ {
215
+ command: "transform",
216
+ cwd: execution.cwd,
217
+ projectRoot: execution.projectRoot,
218
+ sourceFile,
219
+ tsconfig: execution.tsconfig,
220
+ },
221
+ outputText(res.stdout),
222
+ );
223
+ if (options.out) {
224
+ fs.mkdirSync(path.dirname(options.out), { recursive: true });
225
+ fs.writeFileSync(options.out, transformed, "utf8");
226
+ }
227
+ return transformed;
228
+ }
229
+
230
+ function realpathIfExists(file: string): string {
231
+ try {
232
+ return fs.realpathSync(file);
233
+ } catch {
234
+ return file;
235
+ }
236
+ }
237
+
238
+ /** Result of `build()`. Non-zero `status` means the build failed. */
239
+ export interface BuildResult {
240
+ status: number;
241
+ stdout: string;
242
+ stderr: string;
243
+ }
244
+
245
+ /**
246
+ * Run `ttsc` against a tsconfig. Returns once the binary exits so the caller
247
+ * can decide how to surface diagnostics. Does not throw on non-zero exit —
248
+ * bundler pipelines often want to continue and collect errors.
249
+ */
250
+ export function build(options: BuildOptions = {}): BuildResult {
251
+ const execution = resolveExecutionContext(options);
252
+ const args = [
253
+ "build",
254
+ "--tsconfig=" + execution.tsconfig,
255
+ "--rewrite-mode=" + execution.nativeMode,
256
+ ];
257
+ if (options.emit === true) args.push("--emit");
258
+ else if (options.emit === false) args.push("--noEmit");
259
+ if (options.outDir) args.push("--outDir=" + options.outDir);
260
+ if (options.quiet === false) args.push("--verbose");
261
+ else if (options.quiet === true) args.push("--quiet");
262
+ const needsManifest = options.emit !== false && execution.plugins.length > 0;
263
+ const manifest = needsManifest ? createTempManifestPath() : null;
264
+ if (manifest) args.push("--manifest=" + manifest);
265
+
266
+ const res = spawnBinary(execution.bin, args, {
267
+ cwd: options.cwd,
268
+ env: mergeEnv(options.env),
269
+ encoding: "utf8",
270
+ });
271
+ if (res.error) {
272
+ throw new Error(
273
+ "ttsc.build: failed to spawn " + execution.bin + ": " + res.error.message,
274
+ );
275
+ }
276
+ if ((res.status ?? 1) === 0 && manifest) {
277
+ try {
278
+ const emittedFiles = JSON.parse(
279
+ fs.readFileSync(manifest, "utf8"),
280
+ ) as string[];
281
+ applyBuildPlugins(execution.plugins, execution, emittedFiles);
282
+ } finally {
283
+ fs.rmSync(path.dirname(manifest), { recursive: true, force: true });
284
+ }
285
+ }
286
+ return {
287
+ status: res.status ?? 1,
288
+ stdout: outputText(res.stdout),
289
+ stderr: outputText(res.stderr),
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Run `ttsc check` (build without emit) — CI gate / pre-commit hook use.
295
+ * Resolves with an exit-code record; does not throw.
296
+ */
297
+ export function check(options: CheckOptions = {}): BuildResult {
298
+ return build({ ...options, emit: false });
299
+ }
300
+
301
+ /** Ask the binary for its version banner. Handy for user-agent strings. */
302
+ export function version(options: CommonOptions = {}): string {
303
+ const bin = resolveOrThrow(options);
304
+ const res = spawnBinary(bin, ["version"], {
305
+ encoding: "utf8",
306
+ });
307
+ if (res.error || res.status !== 0) {
308
+ throw new Error(
309
+ "ttsc.version: failed: " + (outputText(res.stderr) || res.error?.message),
310
+ );
311
+ }
312
+ return outputText(res.stdout).trim();
313
+ }
314
+
315
+ /**
316
+ * Promise-facing variant of `transform()`. The plugin host keeps the transform
317
+ * pipeline synchronous so plugin modules can stay dependency-free, but many
318
+ * adapter surfaces still prefer a Promise-returning function.
319
+ */
320
+ export function transformAsync(options: TransformOptions): Promise<string> {
321
+ return Promise.resolve().then(() => transform(options));
322
+ }
323
+
324
+ interface ExecutionContext {
325
+ bin: string;
326
+ cwd: string;
327
+ nativeMode: string;
328
+ plugins: readonly TtscPlugin[];
329
+ projectRoot: string;
330
+ tsconfig: string;
331
+ }
332
+
333
+ function resolveExecutionContext(
334
+ options: CommonOptions & { tsconfig?: string },
335
+ ): ExecutionContext {
336
+ const cwd = path.resolve(options.cwd ?? process.cwd());
337
+ const fallbackBinary =
338
+ options.binary &&
339
+ path.isAbsolute(options.binary) &&
340
+ fs.existsSync(options.binary)
341
+ ? options.binary
342
+ : resolveBinary(options);
343
+ const tsconfig = resolveProjectConfig({
344
+ cwd,
345
+ tsconfig: options.tsconfig,
346
+ });
347
+ const projectRoot = resolveProjectRoot({ cwd, tsconfig });
348
+ const loaded = loadProjectPlugins({
349
+ binary: fallbackBinary ?? "",
350
+ cwd,
351
+ entries: options.plugins,
352
+ tsconfig,
353
+ });
354
+ const bin = options.binary ?? loaded.nativeBinary ?? fallbackBinary;
355
+ if (!bin) {
356
+ throw new Error(installHint(options));
357
+ }
358
+ return {
359
+ bin,
360
+ cwd,
361
+ nativeMode: options.rewriteMode ?? loaded.nativeMode,
362
+ plugins: loaded.plugins.filter((plugin) => plugin.transformOutput),
363
+ projectRoot,
364
+ tsconfig,
365
+ };
366
+ }
367
+
368
+ function finalizeTransformText(
369
+ plugins: readonly TtscPlugin[],
370
+ context: Omit<Parameters<typeof applyPluginTransforms>[1], "code">,
371
+ text: string,
372
+ ): string {
373
+ if (plugins.length === 0) {
374
+ return text;
375
+ }
376
+ return applyPluginTransforms(plugins, {
377
+ ...context,
378
+ code: text,
379
+ });
380
+ }
381
+
382
+ function applyBuildPlugins(
383
+ plugins: readonly TtscPlugin[],
384
+ execution: ExecutionContext,
385
+ emittedFiles: readonly string[],
386
+ ): void {
387
+ if (plugins.length === 0) {
388
+ return;
389
+ }
390
+ for (const file of emittedFiles) {
391
+ if (!isJavaScriptOutput(file) || !fs.existsSync(file)) {
392
+ continue;
393
+ }
394
+ const current = fs.readFileSync(file, "utf8");
395
+ const next = applyPluginTransforms(plugins, {
396
+ code: current,
397
+ command: "build",
398
+ cwd: execution.cwd,
399
+ outputFile: file,
400
+ projectRoot: execution.projectRoot,
401
+ tsconfig: execution.tsconfig,
402
+ });
403
+ if (next !== current) {
404
+ fs.writeFileSync(file, next, "utf8");
405
+ }
406
+ }
407
+ }
408
+
409
+ function isJavaScriptOutput(file: string): boolean {
410
+ return /\.(?:[cm]?js)$/i.test(file);
411
+ }
412
+
413
+ function createTempManifestPath(): string {
414
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-build-"));
415
+ return path.join(dir, "manifest.json");
416
+ }