ttsc 0.3.0 → 0.4.0-dev.20260424

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 (112) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +216 -302
  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 +17 -0
  19. package/lib/index.js +34 -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/package.json +70 -63
  34. package/shim/ast/extra-shim.json +4 -0
  35. package/shim/ast/go.mod +5 -0
  36. package/shim/ast/shim.go +117 -0
  37. package/shim/bundled/extra-shim.json +1 -0
  38. package/shim/bundled/go.mod +5 -0
  39. package/shim/bundled/shim.go +19 -0
  40. package/shim/checker/extra-shim.json +31 -0
  41. package/shim/checker/go.mod +5 -0
  42. package/shim/checker/shim.go +146 -0
  43. package/shim/compiler/extra-shim.json +4 -0
  44. package/shim/compiler/go.mod +5 -0
  45. package/shim/compiler/shim.go +50 -0
  46. package/shim/core/extra-shim.json +4 -0
  47. package/shim/core/go.mod +5 -0
  48. package/shim/core/shim.go +11 -0
  49. package/shim/diagnosticwriter/go.mod +5 -0
  50. package/shim/diagnosticwriter/shim.go +29 -0
  51. package/shim/parser/extra-shim.json +4 -0
  52. package/shim/parser/go.mod +5 -0
  53. package/shim/parser/shim.go +55 -0
  54. package/shim/scanner/extra-shim.json +4 -0
  55. package/shim/scanner/go.mod +5 -0
  56. package/shim/scanner/shim.go +96 -0
  57. package/shim/tsoptions/extra-shim.json +4 -0
  58. package/shim/tsoptions/go.mod +5 -0
  59. package/shim/tsoptions/shim.go +123 -0
  60. package/shim/tspath/extra-shim.json +4 -0
  61. package/shim/tspath/go.mod +5 -0
  62. package/shim/tspath/shim.go +165 -0
  63. package/shim/vfs/cachedvfs/extra-shim.json +4 -0
  64. package/shim/vfs/cachedvfs/go.mod +5 -0
  65. package/shim/vfs/cachedvfs/shim.go +12 -0
  66. package/shim/vfs/extra-shim.json +4 -0
  67. package/shim/vfs/go.mod +5 -0
  68. package/shim/vfs/osvfs/extra-shim.json +4 -0
  69. package/shim/vfs/osvfs/go.mod +5 -0
  70. package/shim/vfs/osvfs/shim.go +13 -0
  71. package/shim/vfs/shim.go +19 -0
  72. package/src/api.ts +416 -0
  73. package/src/cli.ts +492 -0
  74. package/src/index.ts +18 -0
  75. package/src/launcher/ttsc-dev.js +42 -0
  76. package/src/launcher/ttsc.js +19 -0
  77. package/src/native.ts +60 -0
  78. package/src/platform.ts +163 -0
  79. package/src/plugin.ts +241 -0
  80. package/src/project.ts +330 -0
  81. package/test/native.test.ts +60 -0
  82. package/test/package.test.ts +123 -0
  83. package/test/platform.test.ts +58 -0
  84. package/test/project.test.ts +195 -0
  85. package/tools/gen_shims/go.mod +14 -0
  86. package/tools/gen_shims/go.sum +12 -0
  87. package/tools/gen_shims/main.go +446 -0
  88. package/CHANGELOG.md +0 -2
  89. package/bin/tsc +0 -2
  90. package/bin/tsserver +0 -2
  91. package/lib/PluginCreator.d.ts +0 -67
  92. package/lib/PluginCreator.js +0 -190
  93. package/lib/cancelationToken.d.ts +0 -0
  94. package/lib/cancelationToken.js +0 -2
  95. package/lib/loadTypescript.d.ts +0 -7
  96. package/lib/loadTypescript.js +0 -56
  97. package/lib/patchCreateProgram.d.ts +0 -21
  98. package/lib/patchCreateProgram.js +0 -184
  99. package/lib/tsc.d.ts +0 -1
  100. package/lib/tsc.js +0 -24
  101. package/lib/tsserver.d.ts +0 -0
  102. package/lib/tsserver.js +0 -2
  103. package/lib/tsserverlibrary.d.ts +0 -3
  104. package/lib/tsserverlibrary.js +0 -4
  105. package/lib/typescript.d.ts +0 -3
  106. package/lib/typescript.js +0 -4
  107. package/lib/typescriptServices.d.ts +0 -3
  108. package/lib/typescriptServices.js +0 -4
  109. package/lib/typingsInstaller.d.ts +0 -0
  110. package/lib/typingsInstaller.js +0 -2
  111. package/lib/watchGuard.d.ts +0 -0
  112. package/lib/watchGuard.js +0 -2
@@ -0,0 +1,128 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "flag"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+
10
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
11
+
12
+ "github.com/samchon/ttsc/packages/ttsc/driver"
13
+ )
14
+
15
+ // runBuild implements the `ttsc` project-build lane. Two modes:
16
+ //
17
+ // - default (dry run): analyze quietly, surfacing only diagnostics/errors.
18
+ // - --emit: also run tsgo's emit pipeline, patching every recognized native
19
+ // consumer call in the resulting .js files.
20
+ func runBuild(args []string) int {
21
+ fs := flag.NewFlagSet("build", flag.ContinueOnError)
22
+ fs.SetOutput(stderr)
23
+ tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
24
+ cwdOverride := fs.String("cwd", "", "override the working directory (defaults to process cwd)")
25
+ quiet := fs.Bool("quiet", true, "suppress the per-call diagnostic summary")
26
+ verbose := fs.Bool("verbose", false, "print the per-call diagnostic summary")
27
+ emit := fs.Bool("emit", false, "force emitted .js files (runs tsgo + ttsc rewrite)")
28
+ noEmit := fs.Bool("noEmit", false, "force analysis-only run with no file writes")
29
+ outDir := fs.String("outDir", "", "override compilerOptions.outDir for this build")
30
+ manifestPath := fs.String("manifest", "", "write emitted file list as JSON to this path")
31
+ rewriteMode := fs.String("rewrite-mode", "none", "native rewrite backend id")
32
+ if err := fs.Parse(args); err != nil {
33
+ return 2
34
+ }
35
+ if *emit && *noEmit {
36
+ fmt.Fprintln(stderr, "ttsc: --emit and --noEmit are mutually exclusive")
37
+ return 2
38
+ }
39
+ if *verbose {
40
+ *quiet = false
41
+ }
42
+
43
+ cwd := *cwdOverride
44
+ if cwd == "" {
45
+ var err error
46
+ cwd, err = os.Getwd()
47
+ if err != nil {
48
+ fmt.Fprintf(stderr, "ttsc: could not get working directory: %v\n", err)
49
+ return 2
50
+ }
51
+ }
52
+
53
+ prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
54
+ ForceEmit: *emit,
55
+ ForceNoEmit: *noEmit,
56
+ OutDir: *outDir,
57
+ })
58
+ if err != nil {
59
+ fmt.Fprintf(stderr, "ttsc: %v\n", err)
60
+ return 2
61
+ }
62
+ if len(diags) > 0 {
63
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
64
+ return 2
65
+ }
66
+ defer prog.Close()
67
+ if diags := prog.Diagnostics(); len(diags) > 0 {
68
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
69
+ return 2
70
+ }
71
+
72
+ rewrites := driver.NewRewriteSet()
73
+ shouldEmit := !prog.ParsedConfig.ParsedConfig.CompilerOptions.NoEmit.IsTrue()
74
+ if *rewriteMode != "none" {
75
+ fmt.Fprintf(stderr, "ttsc: rewrite backend %q is not built into the standalone ttsc binary\n", *rewriteMode)
76
+ fmt.Fprintln(stderr, "ttsc: run through the JS host with the matching compiler plugin so it can select the consumer-provided native binary")
77
+ return 2
78
+ }
79
+ if !*quiet {
80
+ fmt.Fprintf(stdout, "// ttsc: tsconfig=%s cwd=%s sites=%d emit=%v rewrite=%s\n", *tsconfigPath, cwd, 0, shouldEmit, *rewriteMode)
81
+ }
82
+
83
+ if shouldEmit {
84
+ writeFile := shimcompiler.WriteFile(
85
+ func(fileName, text string, _ *shimcompiler.WriteFileData) error {
86
+ return driver.DefaultWriteFile(fileName, text)
87
+ },
88
+ )
89
+ res, eDiags, err := prog.EmitAll(rewrites, writeFile)
90
+ if err != nil {
91
+ fmt.Fprintf(stderr, "ttsc: emit failed: %v\n", err)
92
+ return 3
93
+ }
94
+ for _, d := range eDiags {
95
+ fmt.Fprintln(stderr, " -", d.String())
96
+ }
97
+ if !*quiet {
98
+ fmt.Fprintf(stdout, "// ttsc: emitted=%d files\n", len(res.EmittedFiles))
99
+ for _, f := range res.EmittedFiles {
100
+ rel := f
101
+ if abs, err := filepath.Rel(cwd, f); err == nil {
102
+ rel = abs
103
+ }
104
+ fmt.Fprintln(stdout, " +", rel)
105
+ }
106
+ }
107
+ if *manifestPath != "" {
108
+ data, err := json.Marshal(res.EmittedFiles)
109
+ if err != nil {
110
+ fmt.Fprintf(stderr, "ttsc: manifest marshal failed: %v\n", err)
111
+ return 3
112
+ }
113
+ if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
114
+ fmt.Fprintf(stderr, "ttsc: manifest mkdir failed: %v\n", err)
115
+ return 3
116
+ }
117
+ if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
118
+ fmt.Fprintf(stderr, "ttsc: manifest write failed: %v\n", err)
119
+ return 3
120
+ }
121
+ }
122
+ }
123
+
124
+ if !*quiet {
125
+ fmt.Fprintf(stdout, "// ttsc: recognized=%d total=%d rewrites=%d\n", 0, 0, rewrites.Len())
126
+ }
127
+ return 0
128
+ }
@@ -0,0 +1,222 @@
1
+ // Command ttsc is the Go CLI entrypoint for ttsc.
2
+ //
3
+ // Current responsibilities:
4
+ // - Report version and platform (`ttsc --version`).
5
+ // - Exercise a native rewrite backend end-to-end (`ttsc demo --type=string`)
6
+ // so smoke tests can verify the Go binary really runs.
7
+ // - Act as a placeholder for the eventual `build` / `dev` / `check` / `setup`
8
+ // subcommands that will land in Week 3+.
9
+ //
10
+ // Everything below is deliberately dependency-free so that the standalone binary
11
+ // stays small and its behavior is trivial to audit.
12
+ package main
13
+
14
+ import (
15
+ "flag"
16
+ "fmt"
17
+ "io"
18
+ "os"
19
+ "runtime"
20
+ "strings"
21
+ )
22
+
23
+ // These are overridden via `-ldflags "-X main.version=... -X main.commit=..."`
24
+ // in CI. Sensible defaults keep local `go run ./cmd/ttsc` usable.
25
+ var (
26
+ version = "0.0.0-dev"
27
+ commit = "dev"
28
+ date = "unknown"
29
+ )
30
+
31
+ // stdout / stderr are package-level to keep the CLI testable.
32
+ var (
33
+ stdout io.Writer = os.Stdout
34
+ stderr io.Writer = os.Stderr
35
+ )
36
+
37
+ func main() {
38
+ os.Exit(run(os.Args[1:]))
39
+ }
40
+
41
+ func run(args []string) int {
42
+ if len(args) == 0 {
43
+ return runBuild(nil)
44
+ }
45
+
46
+ switch args[0] {
47
+ case "-h", "--help", "help":
48
+ printHelp(stdout)
49
+ return 0
50
+ case "-v", "--version", "version":
51
+ printVersion(stdout)
52
+ return 0
53
+ case "demo":
54
+ return runDemo(args[1:])
55
+ case "build":
56
+ return runBuild(args[1:])
57
+ case "check":
58
+ // `ttsc check` runs the analyze pipeline without emitting JS — useful
59
+ // in CI and pre-commit hooks that only need schema validation.
60
+ return runBuild(append([]string{"--noEmit"}, args[1:]...))
61
+ case "transform":
62
+ // Single-file transform for bundler plugin integration (unplugin,
63
+ // vite, esbuild, webpack, …). Emits only the requested file's JS to
64
+ // stdout or a chosen path — no project-wide write.
65
+ return runTransform(args[1:])
66
+ case "-p", "--project":
67
+ if len(args) < 2 {
68
+ fmt.Fprintln(stderr, "ttsc: -p/--project requires a path argument")
69
+ return 2
70
+ }
71
+ return runBuild(append([]string{"--tsconfig=" + args[1]}, args[2:]...))
72
+ default:
73
+ if isBuildAlias(args[0]) {
74
+ return runBuild(args)
75
+ }
76
+ fmt.Fprintf(stderr, "ttsc: unknown command %q\n", args[0])
77
+ fmt.Fprintln(stderr, `ttsc: run "ttsc --help" to see supported commands`)
78
+ return 2
79
+ }
80
+ }
81
+
82
+ func isBuildAlias(arg string) bool {
83
+ if strings.HasPrefix(arg, "-") {
84
+ return true
85
+ }
86
+ switch {
87
+ case strings.HasSuffix(arg, ".json"),
88
+ strings.HasSuffix(arg, ".ts"),
89
+ strings.HasSuffix(arg, ".tsx"),
90
+ strings.HasSuffix(arg, ".mts"),
91
+ strings.HasSuffix(arg, ".cts"):
92
+ return true
93
+ default:
94
+ return false
95
+ }
96
+ }
97
+
98
+ // appendUnique appends `extra` to `args` only when it's not already present.
99
+ func appendUnique(args []string, extra ...string) []string {
100
+ out := append([]string{}, args...)
101
+ for _, e := range extra {
102
+ seen := false
103
+ for _, a := range out {
104
+ if a == e {
105
+ seen = true
106
+ break
107
+ }
108
+ }
109
+ if !seen {
110
+ out = append(out, e)
111
+ }
112
+ }
113
+ return out
114
+ }
115
+
116
+ func printVersion(w io.Writer) {
117
+ fmt.Fprintf(
118
+ w,
119
+ "ttsc %s (commit %s, built %s, %s/%s, go %s)\n",
120
+ version,
121
+ commit,
122
+ date,
123
+ runtime.GOOS,
124
+ runtime.GOARCH,
125
+ runtime.Version(),
126
+ )
127
+ }
128
+
129
+ func printHelp(w io.Writer) {
130
+ fmt.Fprintln(w, strings.TrimSpace(`
131
+ ttsc — standalone typescript-go host.
132
+
133
+ Usage:
134
+ ttsc
135
+ ttsc -p tsconfig.json
136
+ ttsc --watch
137
+ ttsc --noEmit
138
+ ttsc transform --file=src/main.ts
139
+
140
+ Project build:
141
+ ttsc compiles the current tsconfig.json, matching the tsc/tsgo shape.
142
+ build Compatibility alias for the same project build lane.
143
+ check Compatibility alias for --noEmit validation.
144
+ transform Emit a single file's rewritten JS (bundler plugin hook).
145
+ demo Run a native backend smoke pipeline with synthetic input.
146
+ version Print version, build info, and platform.
147
+ help Show this help.
148
+
149
+ Build options:
150
+ --tsconfig=FILE Path to tsconfig.json (default: tsconfig.json).
151
+ --cwd=DIR Override working directory.
152
+ --emit Force emitted .js files even when tsconfig has noEmit.
153
+ --noEmit Force analysis-only run even when tsconfig would emit.
154
+ --quiet Suppress the per-call summary banner (default).
155
+ --verbose Print the per-call summary banner and emitted file list.
156
+ --rewrite-mode=M Native rewrite backend id.
157
+ --manifest=FILE Write emitted file paths as JSON to FILE after build --emit.
158
+
159
+ Transform options:
160
+ --file=PATH Absolute or cwd-relative path of the .ts file to transform.
161
+ --tsconfig=FILE tsconfig.json owning --file (default: tsconfig.json).
162
+ --out=PATH Write output JS to PATH. Default: stdout.
163
+ --rewrite-mode=M Native rewrite backend id.
164
+
165
+ Demo options:
166
+ --type=T Atomic TypeScript type to simulate. One of:
167
+ string, number, boolean, bigint, any (default: string).
168
+
169
+ Examples:
170
+ ttsc --version
171
+ ttsc
172
+ ttsc -p ./tsconfig.json
173
+ ttsc --noEmit
174
+ ttsc transform --file=src/main.ts
175
+ ttsc demo --type=number
176
+
177
+ Integration guide (bundlers):
178
+ - vite / webpack / rollup / esbuild: shell out to "ttsc transform" per file,
179
+ forward stdout as the bundler transform result. A bundler adapter can wrap
180
+ this command without depending on the Go internals.
181
+ - Next.js / Nuxt / Bun: "ttsc" in your pipeline replaces tsc and
182
+ the rewritten .js feeds the runtime directly.
183
+ - Monorepo / pnpm workspace: share one ttsc binary via a root script;
184
+ per-package tsconfig.json references work unchanged.
185
+ `))
186
+ }
187
+
188
+ func runDemo(args []string) int {
189
+ fs := flag.NewFlagSet("demo", flag.ContinueOnError)
190
+ fs.SetOutput(stderr)
191
+ typ := fs.String("type", "string", "atomic type to simulate")
192
+ if err := fs.Parse(args); err != nil {
193
+ return 2
194
+ }
195
+
196
+ arrow, err := demoArrow(*typ)
197
+ if err != nil {
198
+ fmt.Fprintf(stderr, "ttsc demo: %v\n", err)
199
+ return 2
200
+ }
201
+
202
+ fmt.Fprintf(stdout, "// demo<%s> -> emitted by ttsc %s\n", *typ, version)
203
+ fmt.Fprintln(stdout, arrow)
204
+ return 0
205
+ }
206
+
207
+ func demoArrow(name string) (string, error) {
208
+ switch strings.ToLower(name) {
209
+ case "any":
210
+ return "(input) => true", nil
211
+ case "boolean":
212
+ return `(input) => "boolean" === typeof input`, nil
213
+ case "number":
214
+ return `(input) => "number" === typeof input`, nil
215
+ case "bigint":
216
+ return `(input) => "bigint" === typeof input`, nil
217
+ case "string", "":
218
+ return `(input) => "string" === typeof input`, nil
219
+ default:
220
+ return "", fmt.Errorf("unknown --type value %q (want: string|number|boolean|bigint|any)", name)
221
+ }
222
+ }
@@ -0,0 +1,288 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ // withCapture replaces stdout/stderr with buffers for the duration of fn.
12
+ // Keeps each test hermetic.
13
+ func withCapture(fn func()) (out, errOut string) {
14
+ var outBuf, errBuf bytes.Buffer
15
+ prevOut, prevErr := stdout, stderr
16
+ stdout = &outBuf
17
+ stderr = &errBuf
18
+ defer func() {
19
+ stdout = prevOut
20
+ stderr = prevErr
21
+ }()
22
+ fn()
23
+ return outBuf.String(), errBuf.String()
24
+ }
25
+
26
+ func TestRunNoArgsAttemptsProjectBuild(t *testing.T) {
27
+ out, errOut := withCapture(func() {
28
+ if code := run(nil); code == 0 {
29
+ t.Errorf("expected no-args run without a local tsconfig to fail")
30
+ }
31
+ })
32
+ if strings.Contains(out, "Usage:") {
33
+ t.Errorf("no-args ttsc must build the project, not print help; got %q", out)
34
+ }
35
+ if !strings.Contains(errOut, "tsconfig") {
36
+ t.Errorf("expected no-args build to look for tsconfig, got stderr %q", errOut)
37
+ }
38
+ }
39
+
40
+ func TestRunHelpVariants(t *testing.T) {
41
+ for _, flag := range []string{"-h", "--help", "help"} {
42
+ t.Run(flag, func(t *testing.T) {
43
+ out, _ := withCapture(func() {
44
+ if code := run([]string{flag}); code != 0 {
45
+ t.Errorf("expected exit 0, got %d", code)
46
+ }
47
+ })
48
+ if !strings.Contains(out, "demo") {
49
+ t.Errorf("help text should list demo subcommand, got %q", out)
50
+ }
51
+ })
52
+ }
53
+ }
54
+
55
+ func TestRunVersion(t *testing.T) {
56
+ for _, flag := range []string{"-v", "--version", "version"} {
57
+ t.Run(flag, func(t *testing.T) {
58
+ out, _ := withCapture(func() {
59
+ if code := run([]string{flag}); code != 0 {
60
+ t.Errorf("expected exit 0, got %d", code)
61
+ }
62
+ })
63
+ if !strings.Contains(out, "ttsc ") {
64
+ t.Errorf("expected version banner, got %q", out)
65
+ }
66
+ // Include useful debug bits
67
+ for _, tag := range []string{"commit", "go"} {
68
+ if !strings.Contains(out, tag) {
69
+ t.Errorf("expected %q substring in version output, got %q", tag, out)
70
+ }
71
+ }
72
+ })
73
+ }
74
+ }
75
+
76
+ func TestRunUnknownCommandExits2(t *testing.T) {
77
+ _, errOut := withCapture(func() {
78
+ if code := run([]string{"fly-to-mars"}); code != 2 {
79
+ t.Errorf("expected exit 2, got %d", code)
80
+ }
81
+ })
82
+ if !strings.Contains(errOut, "unknown command") {
83
+ t.Errorf("expected error text, got %q", errOut)
84
+ }
85
+ }
86
+
87
+ func TestRunBuildBlocksSemanticDiagnosticsBeforeEmit(t *testing.T) {
88
+ root := t.TempDir()
89
+ writeProjectFile(t, root, "tsconfig.json", `{
90
+ "compilerOptions": {
91
+ "module": "commonjs",
92
+ "target": "es2020",
93
+ "outDir": "bin",
94
+ "strict": true
95
+ }
96
+ }
97
+ `)
98
+ writeProjectFile(t, root, "index.ts", `const value: string = 123;
99
+ console.log(value);
100
+ `)
101
+
102
+ _, errOut := withCapture(func() {
103
+ if code := runBuild([]string{"--cwd", root, "--emit"}); code != 2 {
104
+ t.Errorf("expected exit 2, got %d", code)
105
+ }
106
+ })
107
+ if !strings.Contains(errOut, "Type 'number' is not assignable to type 'string'") {
108
+ t.Errorf("expected semantic diagnostic, got %q", errOut)
109
+ }
110
+ if !strings.Contains(errOut, "\x1b[91merror\x1b[0m") || !strings.Contains(errOut, "TS2322") {
111
+ t.Errorf("expected colored TypeScript-style diagnostic header, got %q", errOut)
112
+ }
113
+ if !strings.Contains(errOut, "\x1b[7m1\x1b[0m") || !strings.Contains(errOut, "~~~~~") {
114
+ t.Errorf("expected source context with highlighted span, got %q", errOut)
115
+ }
116
+ if _, err := os.Stat(filepath.Join(root, "bin", "index.js")); !os.IsNotExist(err) {
117
+ t.Errorf("expected no emitted JS, stat err=%v", err)
118
+ }
119
+ }
120
+
121
+ func TestRunBuildAllowsUnusedTypeParametersOnOverloadSignatures(t *testing.T) {
122
+ root := t.TempDir()
123
+ writeProjectFile(t, root, "tsconfig.json", `{
124
+ "compilerOptions": {
125
+ "module": "commonjs",
126
+ "target": "es2020",
127
+ "outDir": "bin",
128
+ "strict": true,
129
+ "noUnusedLocals": true,
130
+ "noUnusedParameters": true
131
+ }
132
+ }
133
+ `)
134
+ writeProjectFile(t, root, "index.ts", `export function marker<T>(input: unknown): string;
135
+ export function marker(input: unknown): string {
136
+ return String(input);
137
+ }
138
+ `)
139
+
140
+ _, errOut := withCapture(func() {
141
+ if code := runBuild([]string{"--cwd", root, "--emit", "--quiet"}); code != 0 {
142
+ t.Errorf("expected exit 0, got %d", code)
143
+ }
144
+ })
145
+ if strings.Contains(errOut, "declared but never used") || strings.Contains(errOut, "All type parameters are unused") {
146
+ t.Errorf("overload signature type parameters must not fail noUnused checks, got %q", errOut)
147
+ }
148
+ if _, err := os.Stat(filepath.Join(root, "bin", "index.js")); err != nil {
149
+ t.Errorf("expected emitted JS, stat err=%v", err)
150
+ }
151
+ }
152
+
153
+ func TestRunBuildBlocksUnusedParametersBeforeEmit(t *testing.T) {
154
+ root := t.TempDir()
155
+ writeProjectFile(t, root, "tsconfig.json", `{
156
+ "compilerOptions": {
157
+ "module": "commonjs",
158
+ "target": "es2020",
159
+ "outDir": "bin",
160
+ "strict": true,
161
+ "noUnusedParameters": true
162
+ }
163
+ }
164
+ `)
165
+ writeProjectFile(t, root, "index.ts", `export function run(unused: string): number {
166
+ return 1;
167
+ }
168
+ `)
169
+
170
+ _, errOut := withCapture(func() {
171
+ if code := runBuild([]string{"--cwd", root, "--emit"}); code != 2 {
172
+ t.Errorf("expected exit 2, got %d", code)
173
+ }
174
+ })
175
+ if !strings.Contains(errOut, "'unused' is declared but its value is never read") &&
176
+ !strings.Contains(errOut, "'unused' is declared but never used") {
177
+ t.Errorf("expected unused parameter diagnostic, got %q", errOut)
178
+ }
179
+ if _, err := os.Stat(filepath.Join(root, "bin", "index.js")); !os.IsNotExist(err) {
180
+ t.Errorf("expected no emitted JS, stat err=%v", err)
181
+ }
182
+ }
183
+
184
+ func TestRunBuildBlocksSyntacticDiagnosticsBeforeEmit(t *testing.T) {
185
+ root := t.TempDir()
186
+ writeProjectFile(t, root, "tsconfig.json", `{
187
+ "compilerOptions": {
188
+ "module": "commonjs",
189
+ "target": "es2020",
190
+ "outDir": "bin"
191
+ }
192
+ }
193
+ `)
194
+ writeProjectFile(t, root, "index.ts", `console.log("before");
195
+ const = ;
196
+ `)
197
+
198
+ _, errOut := withCapture(func() {
199
+ if code := runBuild([]string{"--cwd", root, "--emit"}); code != 2 {
200
+ t.Errorf("expected exit 2, got %d", code)
201
+ }
202
+ })
203
+ if !strings.Contains(errOut, "Variable declaration expected") {
204
+ t.Errorf("expected syntactic diagnostic, got %q", errOut)
205
+ }
206
+ if _, err := os.Stat(filepath.Join(root, "bin", "index.js")); !os.IsNotExist(err) {
207
+ t.Errorf("expected no emitted JS, stat err=%v", err)
208
+ }
209
+ }
210
+
211
+ func writeProjectFile(t *testing.T, root string, name string, contents string) {
212
+ t.Helper()
213
+ file := filepath.Join(root, name)
214
+ if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
215
+ t.Fatal(err)
216
+ }
217
+ if err := os.WriteFile(file, []byte(contents), 0o644); err != nil {
218
+ t.Fatal(err)
219
+ }
220
+ }
221
+
222
+ func TestRunDemoString(t *testing.T) {
223
+ out, _ := withCapture(func() {
224
+ if code := run([]string{"demo", "--type=string"}); code != 0 {
225
+ t.Errorf("expected exit 0, got %d", code)
226
+ }
227
+ })
228
+ want := `(input) => "string" === typeof input`
229
+ if !strings.Contains(out, want) {
230
+ t.Errorf("expected demo output to contain %q, got:\n%s", want, out)
231
+ }
232
+ if !strings.Contains(out, "demo<string>") {
233
+ t.Errorf("expected comment prefix in demo output, got:\n%s", out)
234
+ }
235
+ }
236
+
237
+ func TestRunDemoEveryAtomic(t *testing.T) {
238
+ cases := map[string]string{
239
+ "boolean": `"boolean" === typeof input`,
240
+ "number": `"number" === typeof input`,
241
+ "bigint": `"bigint" === typeof input`,
242
+ "any": `(input) => true`,
243
+ }
244
+ for typ, expected := range cases {
245
+ t.Run(typ, func(t *testing.T) {
246
+ out, _ := withCapture(func() {
247
+ if code := run([]string{"demo", "--type=" + typ}); code != 0 {
248
+ t.Errorf("expected exit 0 for %s, got %d", typ, code)
249
+ }
250
+ })
251
+ if !strings.Contains(out, expected) {
252
+ t.Errorf("for --type=%s, want %q in output:\n%s", typ, expected, out)
253
+ }
254
+ })
255
+ }
256
+ }
257
+
258
+ func TestRunDemoUnknownTypeExits2(t *testing.T) {
259
+ _, errOut := withCapture(func() {
260
+ if code := run([]string{"demo", "--type=object"}); code != 2 {
261
+ t.Errorf("expected exit 2 for unknown type, got %d", code)
262
+ }
263
+ })
264
+ if !strings.Contains(errOut, "unknown --type") {
265
+ t.Errorf("expected diagnostic text, got %q", errOut)
266
+ }
267
+ }
268
+
269
+ func TestDemoArrowEveryAtomic(t *testing.T) {
270
+ cases := map[string]string{
271
+ "string": `"string" === typeof input`,
272
+ "number": `"number" === typeof input`,
273
+ "boolean": `"boolean" === typeof input`,
274
+ "bigint": `"bigint" === typeof input`,
275
+ "any": `(input) => true`,
276
+ }
277
+ for name, want := range cases {
278
+ t.Run(name, func(t *testing.T) {
279
+ arrow, err := demoArrow(name)
280
+ if err != nil {
281
+ t.Fatalf("unexpected err: %v", err)
282
+ }
283
+ if !strings.Contains(arrow, want) {
284
+ t.Errorf("for %s want %q, got %q", name, want, arrow)
285
+ }
286
+ })
287
+ }
288
+ }