ttsc 0.12.4 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +3 -0
  2. package/cmd/ttsc/api_compile.go +15 -2
  3. package/cmd/ttsc/api_transform.go +15 -2
  4. package/cmd/ttsc/build.go +34 -4
  5. package/cmd/ttsc/filter.go +70 -0
  6. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  7. package/cmd/ttsc/flags_gen.go +30 -0
  8. package/driver/program.go +105 -4
  9. package/driver/rewrite.go +39 -4
  10. package/lib/compiler/internal/project/resolveProjectConfig.js +18 -0
  11. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  12. package/lib/compiler/internal/runBuild.d.ts +5 -4
  13. package/lib/compiler/internal/runBuild.js +358 -51
  14. package/lib/compiler/internal/runBuild.js.map +1 -1
  15. package/lib/flags/parser.d.ts +63 -0
  16. package/lib/flags/parser.js +254 -0
  17. package/lib/flags/parser.js.map +1 -0
  18. package/lib/flags/schema.d.ts +146 -0
  19. package/lib/flags/schema.js +423 -0
  20. package/lib/flags/schema.js.map +1 -0
  21. package/lib/launcher/internal/prepareExecution.js +3 -0
  22. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  23. package/lib/launcher/internal/runTtsc.js +71 -147
  24. package/lib/launcher/internal/runTtsc.js.map +1 -1
  25. package/lib/launcher/internal/runTtsx.js +104 -72
  26. package/lib/launcher/internal/runTtsx.js.map +1 -1
  27. package/lib/plugin/internal/loadProjectPlugins.js +58 -23
  28. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  29. package/lib/structures/ITtscPlugin.d.ts +24 -0
  30. package/lib/structures/ITtscPluginCapabilities.d.ts +40 -0
  31. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  32. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  33. package/lib/structures/index.d.ts +1 -0
  34. package/lib/structures/index.js +1 -0
  35. package/lib/structures/index.js.map +1 -1
  36. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  37. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  38. package/package.json +9 -9
  39. package/shim/ast/lint.go +6 -0
  40. package/src/compiler/internal/project/resolveProjectConfig.ts +20 -0
  41. package/src/compiler/internal/runBuild.ts +463 -56
  42. package/src/flags/parser.ts +367 -0
  43. package/src/flags/schema.ts +589 -0
  44. package/src/launcher/internal/prepareExecution.ts +3 -0
  45. package/src/launcher/internal/runTtsc.ts +78 -136
  46. package/src/launcher/internal/runTtsx.ts +116 -74
  47. package/src/plugin/internal/loadProjectPlugins.ts +74 -27
  48. package/src/structures/ITtscPlugin.ts +26 -0
  49. package/src/structures/ITtscPluginCapabilities.ts +41 -0
  50. package/src/structures/index.ts +1 -0
  51. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  52. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  53. package/utility/flags_gen.go +30 -0
  54. package/utility/host.go +43 -33
package/README.md CHANGED
@@ -11,6 +11,9 @@
11
11
 
12
12
  A `typescript-go` toolchain for compiler-powered plugins and type-safe execution.
13
13
 
14
+ Benchmarked on the VS Code fixture: up to **10x** faster type checks and a
15
+ **500x** faster measured lint pass.
16
+
14
17
  - **`ttsc`**: build, check, and transform.
15
18
  - **`ttsx`**: execute TypeScript with type checking.
16
19
  - native TypeScript-Go execution instead of transpile-only runners.
@@ -47,7 +47,11 @@ func runAPICompile(args []string) int {
47
47
  fs.SetOutput(stderr)
48
48
  tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
49
49
  cwdOverride := fs.String("cwd", "", "override the working directory")
50
- if err := fs.Parse(args); err != nil {
50
+ singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
51
+ checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
52
+ tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
53
+ // See cmd/ttsc/build.go's filterHostArgs call for the rationale.
54
+ if err := fs.Parse(filterHostArgs(args)); err != nil {
51
55
  return 2
52
56
  }
53
57
 
@@ -57,8 +61,17 @@ func runAPICompile(args []string) int {
57
61
  return 2
58
62
  }
59
63
 
64
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
65
+ if err != nil {
66
+ fmt.Fprintf(stderr, "ttsc: %v\n", err)
67
+ return 2
68
+ }
69
+
60
70
  prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
61
- ForceEmit: true,
71
+ ForceEmit: true,
72
+ SingleThreaded: *singleThreaded,
73
+ Checkers: *checkers,
74
+ TsgoArgs: tsgoArgs,
62
75
  })
63
76
  if err != nil {
64
77
  fmt.Fprintf(stderr, "ttsc api-compile: %v\n", err)
@@ -32,7 +32,11 @@ func runAPITransform(args []string) int {
32
32
  fs.SetOutput(stderr)
33
33
  tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
34
34
  cwdOverride := fs.String("cwd", "", "override the working directory")
35
- if err := fs.Parse(args); err != nil {
35
+ singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
36
+ checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
37
+ tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
38
+ // See cmd/ttsc/build.go's filterHostArgs call for the rationale.
39
+ if err := fs.Parse(filterHostArgs(args)); err != nil {
36
40
  return 2
37
41
  }
38
42
 
@@ -42,8 +46,17 @@ func runAPITransform(args []string) int {
42
46
  return 2
43
47
  }
44
48
 
49
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
50
+ if err != nil {
51
+ fmt.Fprintf(stderr, "ttsc: %v\n", err)
52
+ return 2
53
+ }
54
+
45
55
  prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
46
- ForceNoEmit: true,
56
+ ForceNoEmit: true,
57
+ SingleThreaded: *singleThreaded,
58
+ Checkers: *checkers,
59
+ TsgoArgs: tsgoArgs,
47
60
  })
48
61
  if err != nil {
49
62
  fmt.Fprintf(stderr, "ttsc api-transform: %v\n", err)
package/cmd/ttsc/build.go CHANGED
@@ -35,7 +35,19 @@ func runBuild(args []string) int {
35
35
  noEmit := fs.Bool("noEmit", false, "force analysis-only run with no file writes")
36
36
  outDir := fs.String("outDir", "", "override compilerOptions.outDir for this build")
37
37
  manifestPath := fs.String("manifest", "", "write emitted file list as JSON to this path")
38
- if err := fs.Parse(args); err != nil {
38
+ singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
39
+ checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
40
+ tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
41
+ // Strip unknown forwarded tsgo options before fs.Parse so a flag like
42
+ // `--strict` (which tsgo accepts but cmd/ttsc's FlagSet does not declare)
43
+ // does not exit 2 before reaching the tsgo lane via `--tsgo-args=<JSON>`.
44
+ // See packages/ttsc/cmd/ttsc/filter.go for the allow-list source.
45
+ if err := fs.Parse(filterHostArgs(args)); err != nil {
46
+ return 2
47
+ }
48
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
49
+ if err != nil {
50
+ fmt.Fprintf(stderr, "ttsc: %v\n", err)
39
51
  return 2
40
52
  }
41
53
  if *emit && *noEmit {
@@ -57,9 +69,12 @@ func runBuild(args []string) int {
57
69
  }
58
70
 
59
71
  prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
60
- ForceEmit: *emit,
61
- ForceNoEmit: *noEmit,
62
- OutDir: *outDir,
72
+ ForceEmit: *emit,
73
+ ForceNoEmit: *noEmit,
74
+ OutDir: *outDir,
75
+ SingleThreaded: *singleThreaded,
76
+ Checkers: *checkers,
77
+ TsgoArgs: tsgoArgs,
63
78
  })
64
79
  if err != nil {
65
80
  fmt.Fprintf(stderr, "ttsc: %v\n", err)
@@ -135,3 +150,18 @@ func runBuild(args []string) int {
135
150
  }
136
151
  return 0
137
152
  }
153
+
154
+ // decodeTsgoArgs decodes the JSON-array value of the `--tsgo-args` flag — the
155
+ // tsgo CLI flags the `ttsc` launcher forwarded — into a string slice. An empty
156
+ // flag yields a nil slice. Shared by the build / api-compile / api-transform
157
+ // subcommands, which all hand the result to driver.LoadProgram.
158
+ func decodeTsgoArgs(raw string) ([]string, error) {
159
+ if raw == "" {
160
+ return nil, nil
161
+ }
162
+ var args []string
163
+ if err := json.Unmarshal([]byte(raw), &args); err != nil {
164
+ return nil, fmt.Errorf("invalid --tsgo-args: %w", err)
165
+ }
166
+ return args, nil
167
+ }
@@ -0,0 +1,70 @@
1
+ // Argument filtering for cmd/ttsc subcommand parsers.
2
+ //
3
+ // `flag.NewFlagSet(..., flag.ContinueOnError).Parse` exits with non-zero on
4
+ // the first unknown flag it sees, which is exactly the failure mode RCA
5
+ // #4 in `.discussions/cli-parser-rca/report.md` flagged: a forwarded tsgo
6
+ // option from the JS launcher would crash the build before tsgo's own
7
+ // option table got a chance to consume it. Mirror the strategy that
8
+ // `packages/ttsc/utility/host.go` already uses for the utility host —
9
+ // strip flags that the local FlagSet does not declare, swallowing the
10
+ // next token when the unknown flag clearly takes one. The allow-list is
11
+ // the same generated `HostFlagAllowList` shared by both consumers
12
+ // (`packages/ttsc/cmd/ttsc/flags_gen.go` and
13
+ // `packages/ttsc/utility/flags_gen.go`).
14
+
15
+ package main
16
+
17
+ import "strings"
18
+
19
+ // filterHostArgs strips flags that the cmd/ttsc FlagSet does not declare,
20
+ // so a forwarded tsgo option from the JS launcher (e.g. `--strict`) does
21
+ // not make `fs.Parse` exit 2 before the build can hand it to tsgo via
22
+ // `--tsgo-args=&lt;JSON&gt;`. Flags absent from `HostFlagAllowList` are
23
+ // dropped together with their value token when they clearly take one
24
+ // (no inline `=` and the next token does not start with `-`).
25
+ //
26
+ // The allow-list itself is generated from
27
+ // `packages/ttsc/src/flags/schema.ts` (see `flags_gen.go`); editing it
28
+ // means editing the schema and re-running `pnpm format`, not patching
29
+ // this file.
30
+ func filterHostArgs(args []string) []string {
31
+ filtered := make([]string, 0, len(args))
32
+ for i := 0; i < len(args); i++ {
33
+ current := args[i]
34
+ if current == "--" {
35
+ // Forward `--` and everything after verbatim. The Go flag parser
36
+ // treats `--` as "end of options" and stops there; mirroring that
37
+ // behavior here keeps trailing positional tokens intact.
38
+ filtered = append(filtered, args[i:]...)
39
+ break
40
+ }
41
+ if !strings.HasPrefix(current, "--") {
42
+ filtered = append(filtered, current)
43
+ continue
44
+ }
45
+ name, hasInlineValue := splitFlagName(current)
46
+ takesValue, ok := HostFlagAllowList[name]
47
+ if ok {
48
+ filtered = append(filtered, current)
49
+ if takesValue && !hasInlineValue && i+1 < len(args) {
50
+ i++
51
+ filtered = append(filtered, args[i])
52
+ }
53
+ continue
54
+ }
55
+ if !hasInlineValue && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
56
+ i++
57
+ }
58
+ }
59
+ return filtered
60
+ }
61
+
62
+ // splitFlagName strips the leading `--` from a flag argument and reports
63
+ // whether the argument carries an inline value (`--foo=value`).
64
+ func splitFlagName(arg string) (string, bool) {
65
+ name := strings.TrimPrefix(arg, "--")
66
+ if i := strings.IndexByte(name, '='); i != -1 {
67
+ return name[:i], true
68
+ }
69
+ return name, false
70
+ }
@@ -0,0 +1,61 @@
1
+ package main
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+ )
7
+
8
+ // TestFilterHostArgsStripsUnknownForwardedFlag verifies cmd/ttsc's
9
+ // filterHostArgs drops forwarded tsgo options before fs.Parse so the
10
+ // FlagSet does not exit 2.
11
+ //
12
+ // `cmd/ttsc/{build,api_compile,api_transform}.go` use
13
+ // `flag.NewFlagSet(..., flag.ContinueOnError)` which exits non-zero on
14
+ // the first unknown flag it sees. The schema-generated allow-list in
15
+ // `flags_gen.go` (`HostFlagAllowList`) feeds this filter so a forwarded
16
+ // `--strict` from the JS launcher reaches the tsgo lane via
17
+ // `--tsgo-args=<JSON>` instead of crashing the build. This pins the
18
+ // behavior RCA #4 flagged as "next likely bug — `cmd/ttsc/build.go:42`
19
+ // uses bare `flag.FlagSet` with no filter and will exit".
20
+ //
21
+ // 1. Known flags + values survive (long form and `=` form).
22
+ // 2. Unknown flag with separate value (`--strict true`) is dropped
23
+ // together with its value.
24
+ // 3. Unknown flag with inline value (`--target=ES2022`) is dropped.
25
+ // 4. `--` separator and everything after survives verbatim.
26
+ func TestFilterHostArgsStripsUnknownForwardedFlag(t *testing.T) {
27
+ cases := []struct {
28
+ name string
29
+ in []string
30
+ want []string
31
+ }{
32
+ {
33
+ name: "keeps known flags including values",
34
+ in: []string{"--tsconfig", "tsconfig.json", "--cwd=.", "--emit"},
35
+ want: []string{"--tsconfig", "tsconfig.json", "--cwd=.", "--emit"},
36
+ },
37
+ {
38
+ name: "drops unknown flag with separate value",
39
+ in: []string{"--tsconfig", "tsconfig.json", "--strict", "true", "--emit"},
40
+ want: []string{"--tsconfig", "tsconfig.json", "--emit"},
41
+ },
42
+ {
43
+ name: "drops unknown flag with inline value",
44
+ in: []string{"--tsconfig=tsconfig.json", "--target=ES2022"},
45
+ want: []string{"--tsconfig=tsconfig.json"},
46
+ },
47
+ {
48
+ name: "double dash preserves trailing tokens",
49
+ in: []string{"--tsconfig=tsconfig.json", "--", "--anything", "src/main.ts"},
50
+ want: []string{"--tsconfig=tsconfig.json", "--", "--anything", "src/main.ts"},
51
+ },
52
+ }
53
+ for _, tc := range cases {
54
+ t.Run(tc.name, func(t *testing.T) {
55
+ got := filterHostArgs(tc.in)
56
+ if !reflect.DeepEqual(got, tc.want) {
57
+ t.Fatalf("filterHostArgs(%v):\n want %v\n got %v", tc.in, tc.want, got)
58
+ }
59
+ })
60
+ }
61
+ }
@@ -0,0 +1,30 @@
1
+ // Code generated by packages/ttsc/scripts/gen-flags.mts. DO NOT EDIT.
2
+ //
3
+ // Source of truth: packages/ttsc/src/flags/schema.ts.
4
+ // Regenerate with: pnpm format
5
+ // Verify in CI with: node packages/ttsc/scripts/check-flags.cjs
6
+
7
+ package main
8
+
9
+ // HostFlagAllowList is the allow-list of CLI flags this Go layer accepts. The map's
10
+ // value is true when the flag carries a separate value token (--flag VALUE
11
+ // or --flag=VALUE) and false when the flag is boolean (--flag).
12
+ //
13
+ // Generated from packages/ttsc/src/flags/schema.ts. Edit the schema, not
14
+ // this file.
15
+ var HostFlagAllowList = map[string]bool{
16
+ "checkers": true,
17
+ "cwd": true,
18
+ "emit": false,
19
+ "manifest": true,
20
+ "noEmit": false,
21
+ "outDir": true,
22
+ "p": true,
23
+ "plugins-json": true,
24
+ "project": true,
25
+ "quiet": false,
26
+ "singleThreaded": false,
27
+ "tsconfig": true,
28
+ "tsgo-args": true,
29
+ "verbose": false,
30
+ }
package/driver/program.go CHANGED
@@ -204,6 +204,17 @@ type LoadProgramOptions struct {
204
204
  ForceNoEmit bool
205
205
  OutDir string
206
206
  SourcePreamble string
207
+ // SingleThreaded forces TypeScript-Go's single-threaded mode (one checker,
208
+ // serial parse/check/emit), mirroring `tsgo --singleThreaded`.
209
+ SingleThreaded bool
210
+ // Checkers overrides the type-checker pool size, mirroring `tsgo --checkers`.
211
+ // Zero leaves TypeScript-Go's default; ignored when SingleThreaded is set.
212
+ Checkers int
213
+ // TsgoArgs carries tsgo CLI flags the `ttsc` launcher did not recognize as
214
+ // its own (`--strict`, `--target es2020`, …). They are parsed through
215
+ // TypeScript-Go's own command-line parser into a CompilerOptions overlay
216
+ // that wins over the tsconfig, exactly as `tsgo`'s CLI merges them.
217
+ TsgoArgs []string
207
218
  }
208
219
 
209
220
  // Close releases the checker pool lease acquired by LoadProgram.
@@ -221,12 +232,19 @@ func (p *Program) Close() error {
221
232
  // The absolute path is resolved against cwd before any VFS lookups because
222
233
  // tsgo's filesystem APIs require absolute paths — mirrors what tsc does when
223
234
  // you pass a relative `--project` flag.
224
- func ParseTSConfig(fs vfs.FS, cwd, tsconfigPath string, host shimcompiler.CompilerHost) (*tsoptions.ParsedCommandLine, []Diagnostic, error) {
235
+ //
236
+ // cliOptions is a CompilerOptions overlay (from forwarded `tsgo` CLI flags);
237
+ // TypeScript-Go merges its non-zero fields over the tsconfig so the CLI wins,
238
+ // the same precedence tsgo's own command line uses. Pass nil for none.
239
+ func ParseTSConfig(fs vfs.FS, cwd, tsconfigPath string, host shimcompiler.CompilerHost, cliOptions *core.CompilerOptions) (*tsoptions.ParsedCommandLine, []Diagnostic, error) {
225
240
  resolved := tspath.ResolvePath(cwd, tsconfigPath)
226
241
  if !fs.FileExists(resolved) {
227
242
  return nil, nil, fmt.Errorf("tsconfig not found: %s", resolved)
228
243
  }
229
- parsed, diags := tsoptions.GetParsedCommandLineOfConfigFile(resolved, &core.CompilerOptions{}, nil, host, nil)
244
+ if cliOptions == nil {
245
+ cliOptions = &core.CompilerOptions{}
246
+ }
247
+ parsed, diags := tsoptions.GetParsedCommandLineOfConfigFile(resolved, cliOptions, nil, host, nil)
230
248
  allDiags := append(diags, parsed.Errors...)
231
249
  if len(allDiags) > 0 {
232
250
  return nil, convertDiagnostics(allDiags), nil
@@ -234,14 +252,48 @@ func ParseTSConfig(fs vfs.FS, cwd, tsconfigPath string, host shimcompiler.Compil
234
252
  return parsed, nil, nil
235
253
  }
236
254
 
255
+ // parseTsgoArgs runs forwarded tsgo CLI flags through TypeScript-Go's own
256
+ // command-line parser, yielding a CompilerOptions overlay ParseTSConfig merges
257
+ // over the tsconfig. This is how a plugin build — which constructs its Program
258
+ // in-process rather than shelling out to `tsgo` — still honors flags like
259
+ // `ttsc --strict`. Returns (nil, nil, nil) when there are no forwarded flags.
260
+ func parseTsgoArgs(args []string, host shimcompiler.CompilerHost) (*core.CompilerOptions, []Diagnostic, error) {
261
+ if len(args) == 0 {
262
+ return nil, nil, nil
263
+ }
264
+ cli := tsoptions.ParseCommandLine(args, host)
265
+ if cli == nil {
266
+ return nil, nil, fmt.Errorf("driver: tsgo argument parser returned nil")
267
+ }
268
+ if len(cli.Errors) > 0 {
269
+ return nil, convertDiagnostics(cli.Errors), nil
270
+ }
271
+ return cli.CompilerOptions(), nil, nil
272
+ }
273
+
237
274
  // CreateProgramFromConfig builds a tsgo Program from the parsed config.
275
+ //
276
+ // SingleThreaded is intentionally left unset so the program keeps
277
+ // TypeScript-Go's parallel source parsing and parallel emit. The checker
278
+ // pool, however, is pinned to a single checker (see forceSingleChecker):
279
+ // every phase ttsc layers on top — plugin transforms and the output
280
+ // rewriter — walks the program serially against the one checker returned by
281
+ // Program.GetTypeChecker, and then asks that checker to resolve types in
282
+ // nodes drawn from *every* source file. TypeScript-Go's multi-checker pool
283
+ // affinitizes each file to a different checker and forbids mixing types
284
+ // across them; a circular type whose declarations span files on different
285
+ // checkers resolves to `any` on the borrowed checker. Pinning the pool to
286
+ // one checker keeps prog.Checker consistent with how every file was checked
287
+ // while leaving parse and emit parallel. Both EmitAll and EmitAllRaw
288
+ // serialize the WriteFile callback under a mutex so the emit-stage rewriter
289
+ // never observes the parallel emit either.
238
290
  func CreateProgramFromConfig(parsed *tsoptions.ParsedCommandLine, host shimcompiler.CompilerHost) (*shimcompiler.Program, []Diagnostic, error) {
239
291
  if parsed == nil {
240
292
  return nil, nil, fmt.Errorf("driver: nil parsed command line")
241
293
  }
294
+ forceSingleChecker(parsed)
242
295
  opts := shimcompiler.ProgramOptions{
243
296
  Config: parsed,
244
- SingleThreaded: core.TSTrue,
245
297
  Host: host,
246
298
  UseSourceOfProjectReference: true,
247
299
  }
@@ -249,6 +301,24 @@ func CreateProgramFromConfig(parsed *tsoptions.ParsedCommandLine, host shimcompi
249
301
  return p, nil, nil
250
302
  }
251
303
 
304
+ // forceSingleChecker pins the TypeScript-Go checker pool to a single checker.
305
+ //
306
+ // ttsc's transform and rewrite phases run serially and obtain types through
307
+ // the single checker that Program.GetTypeChecker hands back. Those phases
308
+ // query types on nodes from arbitrary source files, so the checker must be
309
+ // the same one that checked every file. A pool of size > 1 affinitizes files
310
+ // to distinct checkers; resolving a type whose declarations cross that
311
+ // boundary (e.g. a circular indexed-access alias) yields `any`. Parallel
312
+ // parsing and emit are unaffected — they do not consult the checker count.
313
+ func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
314
+ options := parsed.ParsedConfig.CompilerOptions
315
+ if options.SingleThreaded == core.TSTrue {
316
+ return
317
+ }
318
+ one := 1
319
+ options.Checkers = &one
320
+ }
321
+
252
322
  // LoadProgram is the one-shot convenience used by `ttsc`.
253
323
  // It parses the tsconfig, creates a program and a type-checker, and returns
254
324
  // the wrapped facade.
@@ -281,7 +351,15 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
281
351
  }
282
352
  host := DefaultHost(cwd, fs)
283
353
 
284
- parsed, diags, err := ParseTSConfig(fs, cwd, tsconfigPath, host)
354
+ cliOptions, cliDiags, err := parseTsgoArgs(options.TsgoArgs, host)
355
+ if err != nil {
356
+ return nil, nil, err
357
+ }
358
+ if len(cliDiags) > 0 {
359
+ return nil, cliDiags, nil
360
+ }
361
+
362
+ parsed, diags, err := ParseTSConfig(fs, cwd, tsconfigPath, host, cliOptions)
285
363
  if err != nil {
286
364
  return nil, nil, err
287
365
  }
@@ -297,6 +375,7 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
297
375
  if options.OutDir != "" {
298
376
  overrideOutDir(cwd, parsed, options.OutDir)
299
377
  }
378
+ applyThreadingOptions(parsed, options.SingleThreaded, options.Checkers)
300
379
 
301
380
  tsProgram, _, _ := CreateProgramFromConfig(parsed, host)
302
381
 
@@ -333,6 +412,28 @@ func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir stri
333
412
  parsed.ParsedConfig.CompilerOptions.OutDir = tspath.ResolvePath(cwd, outDir)
334
413
  }
335
414
 
415
+ // applyThreadingOptions forwards the CLI threading knobs onto the parsed
416
+ // compiler options. ttsc mirrors tsgo here: `--singleThreaded` / `--checkers`
417
+ // land in CompilerOptions, and both Program.SingleThreaded() and the checker
418
+ // pool read them from there — ProgramOptions is left untouched, exactly as
419
+ // tsgo's own CLI does. SingleThreaded wins over Checkers, matching the pool's
420
+ // own precedence.
421
+ //
422
+ // Note that CreateProgramFromConfig calls forceSingleChecker afterwards, so a
423
+ // `--checkers N` greater than 1 is recorded here but then clamped back to a
424
+ // single checker: ttsc's serial transform/rewrite phases require one checker
425
+ // (see forceSingleChecker). `--singleThreaded` still takes full effect.
426
+ func applyThreadingOptions(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, checkers int) {
427
+ options := parsed.ParsedConfig.CompilerOptions
428
+ if singleThreaded {
429
+ options.SingleThreaded = core.TSTrue
430
+ }
431
+ if checkers > 0 {
432
+ n := checkers
433
+ options.Checkers = &n
434
+ }
435
+ }
436
+
336
437
  // sourcePreambleFS wraps a vfs.FS and prepends the preamble string to every
337
438
  // source file read by tsgo's parser. Declaration files (.d.ts etc.) are
338
439
  // excluded so injected code never appears in type definitions.
package/driver/rewrite.go CHANGED
@@ -73,11 +73,24 @@ const RewriteSentinel = "/* @ttsc-rewritten */"
73
73
  // the output. Returns the tsgo diagnostics and any patch-time error. When
74
74
  // `writeFile` is nil, the patched JS is written to disk via the standard
75
75
  // tsgo WriteFile.
76
+ //
77
+ // `writeFile` does not need to be concurrency-safe: emit() funnels every
78
+ // invocation through one mutex, so the callback never runs on two goroutines
79
+ // at once even though TypeScript-Go emits files in parallel.
76
80
  func (p *Program) EmitAll(rs *RewriteSet, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
77
81
  return p.emit(rs, nil, writeFile)
78
82
  }
79
83
 
80
84
  // EmitAllRaw runs TypeScript-Go emit without ttsc output-text rewrites.
85
+ //
86
+ // `writeFile` does not need to be concurrency-safe: like EmitAll, EmitAllRaw
87
+ // funnels every invocation through one mutex, so the callback never runs on
88
+ // two goroutines at once even though TypeScript-Go emits files in parallel.
89
+ // This is the contract a plugin's output rewriter relies on — it is the
90
+ // emit-stage phase ttsc guarantees runs single-threaded (a plugin's WriteFile
91
+ // is the standard place to carry per-file cursors or an output map), so ttsc
92
+ // owns the serialization rather than pushing goroutine-safety onto every
93
+ // plugin author. See the emit-concurrency contract in the plugin docs.
81
94
  func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
82
95
  if p == nil || p.TSProgram == nil {
83
96
  return nil, nil, errors.New("driver: nil program")
@@ -85,11 +98,22 @@ func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.Em
85
98
  if err := p.ApplyLinkedPlugins(); err != nil {
86
99
  return nil, nil, err
87
100
  }
88
- wf := writeFile
89
- if wf == nil {
90
- wf = func(fileName, text string, data *shimcompiler.WriteFileData) error {
91
- return DefaultWriteFile(fileName, text)
101
+ // TypeScript-Go's parallel emit invokes WriteFile once per emitted file,
102
+ // concurrently — one goroutine per source file. Serialize the whole callback
103
+ // under wfMu so a plugin's output rewriter sees one writer at a time: a
104
+ // callback that mutates shared state (e.g. @nestia/core's per-file rewrite
105
+ // cursors and runtime-alias cache) would otherwise trip `fatal error:
106
+ // concurrent map read and map write`. The callback is cheap I/O, so
107
+ // serializing it costs ~nothing while parse/check/emit-text still parallelize
108
+ // — the same trade EmitAll makes for its own WriteFile.
109
+ var wfMu sync.Mutex
110
+ wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
111
+ wfMu.Lock()
112
+ defer wfMu.Unlock()
113
+ if writeFile != nil {
114
+ return writeFile(fileName, text, data)
92
115
  }
116
+ return DefaultWriteFile(fileName, text)
93
117
  }
94
118
  result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
95
119
  WriteFile: wf,
@@ -114,7 +138,18 @@ func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcom
114
138
  rs = NewRewriteSet()
115
139
  }
116
140
  cursors := map[string]int{}
141
+ // TypeScript-Go's parallel emit invokes this WriteFile callback once per
142
+ // emitted file, concurrently — one goroutine per source file. Serialize the
143
+ // whole callback body under wfMu: the `cursors` map would otherwise trip
144
+ // `fatal error: concurrent map writes`, and the wrapped `writeFile` (which a
145
+ // caller may back with its own non-thread-safe state, e.g. api-compile's
146
+ // output map) must likewise see one writer at a time. The patch work here is
147
+ // cheap, so serializing only the callback costs ~nothing while parsing,
148
+ // checking, and emit-text generation still parallelize.
149
+ var wfMu sync.Mutex
117
150
  wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
151
+ wfMu.Lock()
152
+ defer wfMu.Unlock()
118
153
  // A patched file is idempotent: once the sentinel exists, the emitted text
119
154
  // is passed through unchanged. This matters for watch/rebuild loops and
120
155
  // tests that re-run emit over the same output directory.
@@ -26,6 +26,24 @@ function resolveProjectConfig(opts = {}) {
26
26
  if (!node_fs_1.default.existsSync(resolved)) {
27
27
  throw new Error(`ttsc: tsconfig not found: ${resolved}`);
28
28
  }
29
+ // `-p <directory>` is the documented tsgo shorthand for the
30
+ // directory that contains a `tsconfig.json`. Mirror that — without
31
+ // this branch a forwarded `--tsconfig=sub` would feed the directory
32
+ // path into `readResolvedCompilerOptions`, which calls
33
+ // `fs.readFileSync` and throws `EISDIR` (the RCA's predicted
34
+ // RC-3 §5 #2 bug, pinned by
35
+ // `test_ttsc_dash_p_directory_path_is_accepted`).
36
+ if (isDirectory(resolved)) {
37
+ const tsconfigInDir = node_path_1.default.join(resolved, "tsconfig.json");
38
+ if (node_fs_1.default.existsSync(tsconfigInDir)) {
39
+ return resolveRealPath(tsconfigInDir);
40
+ }
41
+ const jsconfigInDir = node_path_1.default.join(resolved, "jsconfig.json");
42
+ if (node_fs_1.default.existsSync(jsconfigInDir)) {
43
+ return resolveRealPath(jsconfigInDir);
44
+ }
45
+ throw new Error(`ttsc: directory has no tsconfig.json / jsconfig.json: ${resolved}`);
46
+ }
29
47
  return resolveRealPath(resolved);
30
48
  }
31
49
  const start = opts.file
@@ -1 +1 @@
1
- {"version":3,"file":"resolveProjectConfig.js","sourceRoot":"","sources":["../../../../src/compiler/internal/project/resolveProjectConfig.ts"],"names":[],"mappings":";;;;;;AAAA,sDAAyB;AACzB,0DAA6B;AAI7B;;;;;;;;;;;;GAYG;AACH,8BACE,IAAI,GAA+B,EAAE;IAErC,MAAM,GAAG,GAAG,mBAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC,iBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;QACrB,CAAC,CAAC,eAAe,CAAC,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC,CAAC,GAAG,CAAC;IACR,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qEAAqE,IAAI,EAAE,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,sEAAsE;AACtE,SAAS,mBAAmB,CAAC,GAAW,EAAE,MAAc;IACtD,OAAO,mBAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,CAAC;QACH,OAAO,iBAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,MAAM,CAAC,IAAY,EAAE,KAAwB;IACpD,IAAI,OAAO,GAAG,mBAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,IAAI,EAAE,CAAC;QACZ,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,SAAS,GAAG,mBAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC3C,IAAI,iBAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC7B,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,mBAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;AACH,CAAC;AAED,6DAA6D;AAC7D,SAAS,WAAW,CAAC,QAAgB;IACnC,IAAI,CAAC;QACH,OAAO,iBAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"resolveProjectConfig.js","sourceRoot":"","sources":["../../../../src/compiler/internal/project/resolveProjectConfig.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,15 @@
1
1
  import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
2
2
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
3
3
  import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
4
+ type RunBuildOptions = TtscBuildOptions & {
5
+ skipDiagnosticsCheck?: boolean;
6
+ forceListEmittedFiles?: boolean;
7
+ };
4
8
  /**
5
9
  * Run `ttsc` against a tsconfig. Returns once the binary exits so the CLI can
6
10
  * decide how to surface diagnostics. Does not throw on non-zero exit.
7
11
  */
8
- export declare function runBuild(options?: TtscBuildOptions & {
9
- skipDiagnosticsCheck?: boolean;
10
- forceListEmittedFiles?: boolean;
11
- }): TtscBuildResult;
12
+ export declare function runBuild(options?: RunBuildOptions): TtscBuildResult;
12
13
  /**
13
14
  * Merge two `TtscBuildResult` values into one.
14
15
  *