ttsc 0.12.3 → 0.13.0

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 (151) hide show
  1. package/cmd/platform/main.go +9 -0
  2. package/cmd/ttsc/api_compile.go +122 -101
  3. package/cmd/ttsc/api_compile_branches_test.go +55 -57
  4. package/cmd/ttsc/api_transform.go +67 -49
  5. package/cmd/ttsc/api_transform_branches_test.go +30 -32
  6. package/cmd/ttsc/build.go +141 -108
  7. package/cmd/ttsc/build_branches_test.go +95 -97
  8. package/cmd/ttsc/filter.go +70 -0
  9. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  10. package/cmd/ttsc/flags_gen.go +30 -0
  11. package/cmd/ttsc/helpers_test.go +39 -28
  12. package/cmd/ttsc/main.go +111 -104
  13. package/cmd/ttsc/main_alias_branches_test.go +21 -23
  14. package/cmd/ttscserver/main.go +96 -90
  15. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
  16. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
  17. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
  18. package/cmd/utility-host/main.go +30 -23
  19. package/driver/lsp.go +53 -7
  20. package/driver/plugins.go +88 -72
  21. package/driver/program.go +464 -340
  22. package/driver/rewrite.go +477 -380
  23. package/internal/lspserver/lsp_envelope.go +64 -64
  24. package/internal/lspserver/lsp_frame.go +60 -60
  25. package/internal/lspserver/lsp_plugin.go +57 -57
  26. package/internal/lspserver/lsp_proxy.go +265 -251
  27. package/internal/lspserver/lsp_server.go +155 -144
  28. package/lib/TtscCompiler.d.ts +7 -0
  29. package/lib/TtscCompiler.js +7 -0
  30. package/lib/TtscCompiler.js.map +1 -1
  31. package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
  32. package/lib/compiler/internal/buildNativeCompiler.js +23 -3
  33. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  34. package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
  35. package/lib/compiler/internal/compileProjectInMemory.js +36 -21
  36. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  37. package/lib/compiler/internal/paths.d.ts +27 -0
  38. package/lib/compiler/internal/paths.js +70 -0
  39. package/lib/compiler/internal/paths.js.map +1 -0
  40. package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
  41. package/lib/compiler/internal/project/readProjectConfig.js +80 -1
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +42 -1
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/resolveBinary.d.ts +9 -1
  47. package/lib/compiler/internal/resolveBinary.js +17 -1
  48. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  49. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
  50. package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
  51. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  52. package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
  53. package/lib/compiler/internal/resolveTsgo.js +23 -1
  54. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  55. package/lib/compiler/internal/runBuild.d.ts +24 -4
  56. package/lib/compiler/internal/runBuild.js +355 -93
  57. package/lib/compiler/internal/runBuild.js.map +1 -1
  58. package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
  59. package/lib/compiler/internal/runSingleFileEmit.js +14 -1
  60. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  61. package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
  62. package/lib/compiler/internal/sharedHostHelpers.js +6 -2
  63. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  64. package/lib/compiler/internal/spawnNative.d.ts +21 -0
  65. package/lib/compiler/internal/spawnNative.js +59 -0
  66. package/lib/compiler/internal/spawnNative.js.map +1 -0
  67. package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
  68. package/lib/compiler/internal/transformProjectInMemory.js +57 -61
  69. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  70. package/lib/flags/parser.d.ts +63 -0
  71. package/lib/flags/parser.js +254 -0
  72. package/lib/flags/parser.js.map +1 -0
  73. package/lib/flags/schema.d.ts +147 -0
  74. package/lib/flags/schema.js +405 -0
  75. package/lib/flags/schema.js.map +1 -0
  76. package/lib/launcher/internal/getCompilerVersionText.js +7 -0
  77. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  78. package/lib/launcher/internal/prepareExecution.js +35 -0
  79. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  80. package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
  81. package/lib/launcher/internal/resolveCacheDir.js +22 -0
  82. package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
  83. package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
  84. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
  85. package/lib/launcher/internal/runTtsc.d.ts +10 -0
  86. package/lib/launcher/internal/runTtsc.js +90 -153
  87. package/lib/launcher/internal/runTtsc.js.map +1 -1
  88. package/lib/launcher/internal/runTtscserver.js +10 -35
  89. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  90. package/lib/launcher/internal/runTtsx.d.ts +8 -0
  91. package/lib/launcher/internal/runTtsx.js +137 -79
  92. package/lib/launcher/internal/runTtsx.js.map +1 -1
  93. package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
  94. package/lib/plugin/internal/buildSourcePlugin.js +40 -17
  95. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  96. package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
  97. package/lib/plugin/internal/loadProjectPlugins.js +90 -41
  98. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  99. package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
  100. package/lib/structures/ITtscPlugin.d.ts +24 -0
  101. package/lib/structures/ITtscPluginCapabilities.d.ts +29 -0
  102. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  103. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  104. package/lib/structures/index.d.ts +1 -0
  105. package/lib/structures/index.js +1 -0
  106. package/lib/structures/index.js.map +1 -1
  107. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  108. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  109. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
  110. package/package.json +9 -9
  111. package/shim/ast/shim.go +24 -0
  112. package/shim/checker/shim.go +35 -0
  113. package/shim/core/shim.go +17 -0
  114. package/shim/diagnosticwriter/shim.go +4 -0
  115. package/shim/lsp/shim.go +3 -3
  116. package/shim/printer/shim.go +20 -0
  117. package/src/TtscCompiler.ts +7 -0
  118. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  119. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  120. package/src/compiler/internal/paths.ts +68 -0
  121. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  122. package/src/compiler/internal/project/resolveProjectConfig.ts +44 -1
  123. package/src/compiler/internal/resolveBinary.ts +17 -1
  124. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  125. package/src/compiler/internal/resolveTsgo.ts +29 -1
  126. package/src/compiler/internal/runBuild.ts +398 -112
  127. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  128. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  129. package/src/compiler/internal/spawnNative.ts +64 -0
  130. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  131. package/src/flags/parser.ts +367 -0
  132. package/src/flags/schema.ts +570 -0
  133. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  134. package/src/launcher/internal/prepareExecution.ts +35 -0
  135. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  136. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  137. package/src/launcher/internal/runTtsc.ts +97 -143
  138. package/src/launcher/internal/runTtscserver.ts +9 -1
  139. package/src/launcher/internal/runTtsx.ts +149 -82
  140. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  141. package/src/plugin/internal/loadProjectPlugins.ts +116 -45
  142. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  143. package/src/structures/ITtscPlugin.ts +26 -1
  144. package/src/structures/ITtscPluginCapabilities.ts +29 -0
  145. package/src/structures/index.ts +1 -0
  146. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  147. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  148. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  149. package/tools/gen_shims/main.go +43 -8
  150. package/utility/flags_gen.go +30 -0
  151. package/utility/host.go +304 -245
package/cmd/ttsc/build.go CHANGED
@@ -8,15 +8,15 @@
8
8
  package main
9
9
 
10
10
  import (
11
- "encoding/json"
12
- "flag"
13
- "fmt"
14
- "os"
15
- "path/filepath"
11
+ "encoding/json"
12
+ "flag"
13
+ "fmt"
14
+ "os"
15
+ "path/filepath"
16
16
 
17
- shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
17
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
18
18
 
19
- "github.com/samchon/ttsc/packages/ttsc/driver"
19
+ "github.com/samchon/ttsc/packages/ttsc/driver"
20
20
  )
21
21
 
22
22
  // runBuild implements the `ttsc` project-build lane. Two modes:
@@ -25,110 +25,143 @@ import (
25
25
  // - --emit: also run tsgo's emit pipeline, patching every recognized native
26
26
  // consumer call in the resulting .js files.
27
27
  func runBuild(args []string) int {
28
- fs := flag.NewFlagSet("build", flag.ContinueOnError)
29
- fs.SetOutput(stderr)
30
- tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
31
- cwdOverride := fs.String("cwd", "", "override the working directory (defaults to process cwd)")
32
- quiet := fs.Bool("quiet", true, "suppress the per-call diagnostic summary")
33
- verbose := fs.Bool("verbose", false, "print the per-call diagnostic summary")
34
- emit := fs.Bool("emit", false, "force emitted .js files (runs tsgo + ttsc rewrite)")
35
- noEmit := fs.Bool("noEmit", false, "force analysis-only run with no file writes")
36
- outDir := fs.String("outDir", "", "override compilerOptions.outDir for this build")
37
- manifestPath := fs.String("manifest", "", "write emitted file list as JSON to this path")
38
- if err := fs.Parse(args); err != nil {
39
- return 2
40
- }
41
- if *emit && *noEmit {
42
- fmt.Fprintln(stderr, "ttsc: --emit and --noEmit are mutually exclusive")
43
- return 2
44
- }
45
- if *verbose {
46
- *quiet = false
47
- }
28
+ fs := flag.NewFlagSet("build", flag.ContinueOnError)
29
+ fs.SetOutput(stderr)
30
+ tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
31
+ cwdOverride := fs.String("cwd", "", "override the working directory (defaults to process cwd)")
32
+ quiet := fs.Bool("quiet", true, "suppress the per-call diagnostic summary")
33
+ verbose := fs.Bool("verbose", false, "print the per-call diagnostic summary")
34
+ emit := fs.Bool("emit", false, "force emitted .js files (runs tsgo + ttsc rewrite)")
35
+ noEmit := fs.Bool("noEmit", false, "force analysis-only run with no file writes")
36
+ outDir := fs.String("outDir", "", "override compilerOptions.outDir for this build")
37
+ manifestPath := fs.String("manifest", "", "write emitted file list as JSON to this path")
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)
51
+ return 2
52
+ }
53
+ if *emit && *noEmit {
54
+ fmt.Fprintln(stderr, "ttsc: --emit and --noEmit are mutually exclusive")
55
+ return 2
56
+ }
57
+ if *verbose {
58
+ *quiet = false
59
+ }
48
60
 
49
- cwd := *cwdOverride
50
- if cwd == "" {
51
- var err error
52
- cwd, err = getwd()
53
- if err != nil {
54
- fmt.Fprintf(stderr, "ttsc: could not get working directory: %v\n", err)
55
- return 2
56
- }
57
- }
61
+ cwd := *cwdOverride
62
+ if cwd == "" {
63
+ var err error
64
+ cwd, err = getwd()
65
+ if err != nil {
66
+ fmt.Fprintf(stderr, "ttsc: could not get working directory: %v\n", err)
67
+ return 2
68
+ }
69
+ }
58
70
 
59
- prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
60
- ForceEmit: *emit,
61
- ForceNoEmit: *noEmit,
62
- OutDir: *outDir,
63
- })
64
- if err != nil {
65
- fmt.Fprintf(stderr, "ttsc: %v\n", err)
66
- return 2
67
- }
68
- if len(diags) > 0 {
69
- driver.WritePrettyDiagnostics(stderr, diags, cwd)
70
- return 2
71
- }
72
- defer prog.Close()
73
- if diags := prog.Diagnostics(); len(diags) > 0 {
74
- driver.WritePrettyDiagnostics(stderr, diags, cwd)
75
- return 2
76
- }
71
+ prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
72
+ ForceEmit: *emit,
73
+ ForceNoEmit: *noEmit,
74
+ OutDir: *outDir,
75
+ SingleThreaded: *singleThreaded,
76
+ Checkers: *checkers,
77
+ TsgoArgs: tsgoArgs,
78
+ })
79
+ if err != nil {
80
+ fmt.Fprintf(stderr, "ttsc: %v\n", err)
81
+ return 2
82
+ }
83
+ if len(diags) > 0 {
84
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
85
+ return 2
86
+ }
87
+ defer prog.Close()
88
+ if diags := prog.Diagnostics(); len(diags) > 0 {
89
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
90
+ return 2
91
+ }
77
92
 
78
- rewrites := driver.NewRewriteSet()
79
- shouldEmit := !prog.ParsedConfig.ParsedConfig.CompilerOptions.NoEmit.IsTrue()
80
- if !*quiet {
81
- fmt.Fprintf(stdout, "// ttsc: tsconfig=%s cwd=%s sites=%d emit=%v\n", *tsconfigPath, cwd, 0, shouldEmit)
82
- }
93
+ rewrites := driver.NewRewriteSet()
94
+ // shouldEmit reflects the resolved tsconfig noEmit flag. The flag lives
95
+ // three levels deep in the parsed config because TypeScript-Go mirrors the
96
+ // tsconfig object structure verbatim, with a tri-state bool per option.
97
+ shouldEmit := !prog.ParsedConfig.ParsedConfig.CompilerOptions.NoEmit.IsTrue()
98
+ if !*quiet {
99
+ fmt.Fprintf(stdout, "// ttsc: tsconfig=%s cwd=%s sites=%d emit=%v\n", *tsconfigPath, cwd, 0, shouldEmit)
100
+ }
83
101
 
84
- if shouldEmit {
85
- // Emit is callback-driven in TypeScript-Go. ttsc keeps that shape and
86
- // wraps only the final WriteFile step so native rewrites and custom output
87
- // capture share the same path.
88
- writeFile := shimcompiler.WriteFile(
89
- func(fileName, text string, _ *shimcompiler.WriteFileData) error {
90
- return driver.DefaultWriteFile(fileName, text)
91
- },
92
- )
93
- res, eDiags, err := prog.EmitAll(rewrites, writeFile)
94
- if err != nil {
95
- fmt.Fprintf(stderr, "ttsc: emit failed: %v\n", err)
96
- return 3
97
- }
98
- for _, d := range eDiags {
99
- fmt.Fprintln(stderr, " -", d.String())
100
- }
101
- if driver.CountErrors(eDiags) > 0 {
102
- return 2
103
- }
104
- if !*quiet {
105
- fmt.Fprintf(stdout, "// ttsc: emitted=%d files\n", len(res.EmittedFiles))
106
- for _, f := range res.EmittedFiles {
107
- rel := f
108
- if abs, err := filepath.Rel(cwd, f); err == nil {
109
- rel = abs
110
- }
111
- fmt.Fprintln(stdout, " +", rel)
112
- }
113
- }
114
- if *manifestPath != "" {
115
- // The manifest is intentionally just the emitted file list. Higher
116
- // layers already know the project and tsconfig, and tests compare this
117
- // array as the build contract.
118
- data, _ := json.Marshal(res.EmittedFiles)
119
- if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
120
- fmt.Fprintf(stderr, "ttsc: manifest mkdir failed: %v\n", err)
121
- return 3
122
- }
123
- if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
124
- fmt.Fprintf(stderr, "ttsc: manifest write failed: %v\n", err)
125
- return 3
126
- }
127
- }
128
- }
102
+ if shouldEmit {
103
+ // Emit is callback-driven in TypeScript-Go. ttsc keeps that shape and
104
+ // wraps only the final WriteFile step so native rewrites and custom output
105
+ // capture share the same path.
106
+ writeFile := shimcompiler.WriteFile(
107
+ func(fileName, text string, _ *shimcompiler.WriteFileData) error {
108
+ return driver.DefaultWriteFile(fileName, text)
109
+ },
110
+ )
111
+ res, eDiags, err := prog.EmitAll(rewrites, writeFile)
112
+ if err != nil {
113
+ fmt.Fprintf(stderr, "ttsc: emit failed: %v\n", err)
114
+ return 3
115
+ }
116
+ for _, d := range eDiags {
117
+ fmt.Fprintln(stderr, " -", d.String())
118
+ }
119
+ if driver.CountErrors(eDiags) > 0 {
120
+ return 2
121
+ }
122
+ if !*quiet {
123
+ fmt.Fprintf(stdout, "// ttsc: emitted=%d files\n", len(res.EmittedFiles))
124
+ for _, f := range res.EmittedFiles {
125
+ rel := f
126
+ if abs, err := filepath.Rel(cwd, f); err == nil {
127
+ rel = abs
128
+ }
129
+ fmt.Fprintln(stdout, " +", rel)
130
+ }
131
+ }
132
+ if *manifestPath != "" {
133
+ // The manifest is intentionally just the emitted file list. Higher
134
+ // layers already know the project and tsconfig, and tests compare this
135
+ // array as the build contract.
136
+ data, _ := json.Marshal(res.EmittedFiles)
137
+ if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
138
+ fmt.Fprintf(stderr, "ttsc: manifest mkdir failed: %v\n", err)
139
+ return 3
140
+ }
141
+ if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
142
+ fmt.Fprintf(stderr, "ttsc: manifest write failed: %v\n", err)
143
+ return 3
144
+ }
145
+ }
146
+ }
129
147
 
130
- if !*quiet {
131
- fmt.Fprintf(stdout, "// ttsc: recognized=%d total=%d rewrites=%d\n", 0, 0, rewrites.Len())
132
- }
133
- return 0
148
+ if !*quiet {
149
+ fmt.Fprintf(stdout, "// ttsc: recognized=%d total=%d rewrites=%d\n", 0, 0, rewrites.Len())
150
+ }
151
+ return 0
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
134
167
  }
@@ -1,130 +1,128 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "errors"
5
- "path/filepath"
6
- "strings"
7
- "testing"
4
+ "errors"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
8
 
9
- "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ "github.com/samchon/ttsc/packages/ttsc/driver"
10
10
  )
11
11
 
12
12
  type buildApplyErrorPlugin struct{}
13
13
 
14
14
  func (buildApplyErrorPlugin) ApplyProgram(*driver.Program, driver.PluginContext) error {
15
- return errors.New("build apply boom")
15
+ return errors.New("build apply boom")
16
16
  }
17
17
 
18
- /**
19
- * Verifies build command branches report setup, emit, and manifest failures.
20
- *
21
- * The native build command is the fallback compiler host behind ttsc. It must
22
- * fail before writing partial output when flags, cwd, tsconfig, disk emit, or
23
- * manifest paths are invalid.
24
- *
25
- * 1. Exercise bad flags, cwd failure, and project-load failures.
26
- * 2. Force emit through an invalid outDir.
27
- * 3. Force manifest mkdir and write failures after a valid emit.
28
- */
18
+ // TestBuildBranches verifies build command branches report setup, emit, and manifest failures.
19
+ //
20
+ // The native build command is the fallback compiler host behind ttsc. It must
21
+ // fail before writing partial output when flags, cwd, tsconfig, disk emit, or
22
+ // manifest paths are invalid.
23
+ //
24
+ // 1. Exercise bad flags, cwd failure, and project-load failures.
25
+ // 2. Force emit through an invalid outDir.
26
+ // 3. Force manifest mkdir and write failures after a valid emit.
29
27
  func TestBuildBranches(t *testing.T) {
30
- code, _, _ := captureCommand(t, func() int {
31
- return runBuild([]string{"--cwd"})
32
- })
33
- if code != 2 {
34
- t.Fatalf("bad flag status mismatch: %d", code)
35
- }
36
- code, _, errText := captureCommand(t, func() int {
37
- getwd = failGetwd
38
- return runBuild(nil)
39
- })
40
- if code != 2 || !strings.Contains(errText, "cwd boom") {
41
- t.Fatalf("cwd error mismatch: code=%d stderr=%q", code, errText)
42
- }
43
- root := t.TempDir()
44
- code, _, errText = captureCommand(t, func() int {
45
- return runBuild([]string{"--cwd", root, "--tsconfig", "missing.json"})
46
- })
47
- if code != 2 || !strings.Contains(errText, "tsconfig not found") {
48
- t.Fatalf("missing config mismatch: code=%d stderr=%q", code, errText)
49
- }
28
+ code, _, _ := captureCommand(t, func() int {
29
+ return runBuild([]string{"--cwd"})
30
+ })
31
+ if code != 2 {
32
+ t.Fatalf("bad flag status mismatch: %d", code)
33
+ }
34
+ code, _, errText := captureCommand(t, func() int {
35
+ getwd = failGetwd
36
+ return runBuild(nil)
37
+ })
38
+ if code != 2 || !strings.Contains(errText, "cwd boom") {
39
+ t.Fatalf("cwd error mismatch: code=%d stderr=%q", code, errText)
40
+ }
41
+ root := t.TempDir()
42
+ code, _, errText = captureCommand(t, func() int {
43
+ return runBuild([]string{"--cwd", root, "--tsconfig", "missing.json"})
44
+ })
45
+ if code != 2 || !strings.Contains(errText, "tsconfig not found") {
46
+ t.Fatalf("missing config mismatch: code=%d stderr=%q", code, errText)
47
+ }
50
48
 
51
- writeCommandProjectFile(t, root, "tsconfig.json", `{
49
+ writeCommandProjectFile(t, root, "tsconfig.json", `{
52
50
  "compilerOptions": { "module": "commonjs", "target": "es2020", "strict": true },
53
51
  "files": ["index.ts"]
54
52
  }
55
53
  `)
56
- writeCommandProjectFile(t, root, "index.ts", `const value: number = "text";
54
+ writeCommandProjectFile(t, root, "index.ts", `const value: number = "text";
57
55
  export { value };
58
56
  `)
59
- code, _, errText = captureCommand(t, func() int {
60
- return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json"})
61
- })
62
- if code != 2 || !strings.Contains(errText, "number") {
63
- t.Fatalf("semantic diagnostics mismatch: code=%d stderr=%q", code, errText)
64
- }
57
+ code, _, errText = captureCommand(t, func() int {
58
+ return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json"})
59
+ })
60
+ if code != 2 || !strings.Contains(errText, "number") {
61
+ t.Fatalf("semantic diagnostics mismatch: code=%d stderr=%q", code, errText)
62
+ }
65
63
 
66
- writeCommandProjectFile(t, root, "tsconfig.json", `{
64
+ writeCommandProjectFile(t, root, "tsconfig.json", `{
67
65
  "compilerOptions": { "module": "not-a-module-kind", "target": "es2020" },
68
66
  "files": ["index.ts"]
69
67
  }
70
68
  `)
71
- code, _, errText = captureCommand(t, func() int {
72
- return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json"})
73
- })
74
- if code != 2 || !strings.Contains(errText, "not-a-module-kind") {
75
- t.Fatalf("invalid config mismatch: code=%d stderr=%q", code, errText)
76
- }
69
+ code, _, errText = captureCommand(t, func() int {
70
+ return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json"})
71
+ })
72
+ if code != 2 || !strings.Contains(errText, "not-a-module-kind") {
73
+ t.Fatalf("invalid config mismatch: code=%d stderr=%q", code, errText)
74
+ }
77
75
 
78
- writeCommandProjectFile(t, root, "tsconfig.json", `{
76
+ writeCommandProjectFile(t, root, "tsconfig.json", `{
79
77
  "compilerOptions": { "module": "commonjs", "target": "es2020", "strict": true },
80
78
  "files": ["index.ts"]
81
79
  }
82
80
  `)
83
- writeCommandProjectFile(t, root, "index.ts", `export const value = 1;
81
+ writeCommandProjectFile(t, root, "index.ts", `export const value = 1;
84
82
  `)
85
- writeCommandProjectFile(t, root, "blocked", `not a directory`)
86
- code, _, errText = captureCommand(t, func() int {
87
- return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json", "--emit", "--outDir", "blocked"})
88
- })
89
- if code != 2 || !strings.Contains(errText, "not a directory") {
90
- t.Fatalf("emit failure mismatch: code=%d stderr=%q", code, errText)
91
- }
83
+ writeCommandProjectFile(t, root, "blocked", `not a directory`)
84
+ code, _, errText = captureCommand(t, func() int {
85
+ return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json", "--emit", "--outDir", "blocked"})
86
+ })
87
+ if code != 2 || !strings.Contains(errText, "not a directory") {
88
+ t.Fatalf("emit failure mismatch: code=%d stderr=%q", code, errText)
89
+ }
92
90
 
93
- resetCommandLinkedPluginRegistry()
94
- driver.RegisterPlugin(buildApplyErrorPlugin{})
95
- t.Cleanup(resetCommandLinkedPluginRegistry)
96
- t.Setenv(driver.LinkedPluginsEnv, `[{"name":"error","stage":"transform","config":{}}]`)
97
- code, _, errText = captureCommand(t, func() int {
98
- return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json", "--emit", "--outDir", "dist"})
99
- })
100
- if code != 3 || !strings.Contains(errText, "build apply boom") {
101
- t.Fatalf("linked apply failure mismatch: code=%d stderr=%q", code, errText)
102
- }
103
- resetCommandLinkedPluginRegistry()
104
- t.Setenv(driver.LinkedPluginsEnv, "")
91
+ resetCommandLinkedPluginRegistry()
92
+ driver.RegisterPlugin(buildApplyErrorPlugin{})
93
+ t.Cleanup(resetCommandLinkedPluginRegistry)
94
+ t.Setenv(driver.LinkedPluginsEnv, `[{"name":"error","stage":"transform","config":{}}]`)
95
+ code, _, errText = captureCommand(t, func() int {
96
+ return runBuild([]string{"--cwd", root, "--tsconfig", "tsconfig.json", "--emit", "--outDir", "dist"})
97
+ })
98
+ if code != 3 || !strings.Contains(errText, "build apply boom") {
99
+ t.Fatalf("linked apply failure mismatch: code=%d stderr=%q", code, errText)
100
+ }
101
+ resetCommandLinkedPluginRegistry()
102
+ t.Setenv(driver.LinkedPluginsEnv, "")
105
103
 
106
- code, _, errText = captureCommand(t, func() int {
107
- return runBuild([]string{
108
- "--cwd", root,
109
- "--tsconfig", "tsconfig.json",
110
- "--emit",
111
- "--outDir", "dist",
112
- "--manifest", filepath.Join(root, "index.ts", "manifest.json"),
113
- })
114
- })
115
- if code != 3 || !strings.Contains(errText, "manifest mkdir failed") {
116
- t.Fatalf("manifest mkdir mismatch: code=%d stderr=%q", code, errText)
117
- }
118
- code, _, errText = captureCommand(t, func() int {
119
- return runBuild([]string{
120
- "--cwd", root,
121
- "--tsconfig", "tsconfig.json",
122
- "--emit",
123
- "--outDir", "dist",
124
- "--manifest", root,
125
- })
126
- })
127
- if code != 3 || !strings.Contains(errText, "manifest write failed") {
128
- t.Fatalf("manifest write mismatch: code=%d stderr=%q", code, errText)
129
- }
104
+ code, _, errText = captureCommand(t, func() int {
105
+ return runBuild([]string{
106
+ "--cwd", root,
107
+ "--tsconfig", "tsconfig.json",
108
+ "--emit",
109
+ "--outDir", "dist",
110
+ "--manifest", filepath.Join(root, "index.ts", "manifest.json"),
111
+ })
112
+ })
113
+ if code != 3 || !strings.Contains(errText, "manifest mkdir failed") {
114
+ t.Fatalf("manifest mkdir mismatch: code=%d stderr=%q", code, errText)
115
+ }
116
+ code, _, errText = captureCommand(t, func() int {
117
+ return runBuild([]string{
118
+ "--cwd", root,
119
+ "--tsconfig", "tsconfig.json",
120
+ "--emit",
121
+ "--outDir", "dist",
122
+ "--manifest", root,
123
+ })
124
+ })
125
+ if code != 3 || !strings.Contains(errText, "manifest write failed") {
126
+ t.Fatalf("manifest write mismatch: code=%d stderr=%q", code, errText)
127
+ }
130
128
  }
@@ -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
+ }