ttsc 0.11.0 → 0.12.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 (79) hide show
  1. package/README.md +25 -6
  2. package/cmd/ttsc/api_compile.go +101 -106
  3. package/cmd/ttsc/api_compile_branches_test.go +81 -0
  4. package/cmd/ttsc/api_transform.go +49 -54
  5. package/cmd/ttsc/api_transform_branches_test.go +41 -0
  6. package/cmd/ttsc/build.go +108 -109
  7. package/cmd/ttsc/build_branches_test.go +130 -0
  8. package/cmd/ttsc/helpers_test.go +47 -0
  9. package/cmd/ttsc/main.go +104 -103
  10. package/cmd/ttsc/main_alias_branches_test.go +29 -0
  11. package/cmd/ttscserver/main.go +104 -96
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
  15. package/cmd/utility-host/main.go +42 -0
  16. package/driver/lsp.go +60 -0
  17. package/driver/plugins.go +122 -0
  18. package/driver/program.go +346 -356
  19. package/driver/rewrite.go +336 -339
  20. package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
  21. package/{driver → internal/lspserver}/lsp_frame.go +62 -62
  22. package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
  23. package/internal/lspserver/lsp_proxy.go +391 -0
  24. package/internal/lspserver/lsp_server.go +231 -0
  25. package/lib/TtscCompiler.d.ts +3 -3
  26. package/lib/TtscCompiler.js +20 -6
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
  29. package/lib/compiler/internal/resolveTsgo.js +15 -4
  30. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  31. package/lib/compiler/internal/runBuild.js +12 -4
  32. package/lib/compiler/internal/runBuild.js.map +1 -1
  33. package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
  34. package/lib/compiler/internal/sharedHostHelpers.js +27 -70
  35. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  36. package/lib/compiler/internal/transformProjectInMemory.js +12 -5
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/index.d.ts +0 -1
  39. package/lib/index.js +0 -1
  40. package/lib/index.js.map +1 -1
  41. package/lib/launcher/internal/runTtsc.js +9 -4
  42. package/lib/launcher/internal/runTtsc.js.map +1 -1
  43. package/lib/launcher/internal/runTtscserver.d.ts +1 -0
  44. package/lib/launcher/internal/runTtscserver.js +38 -1
  45. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  46. package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
  47. package/lib/plugin/internal/buildSourcePlugin.js +444 -41
  48. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  49. package/lib/plugin/internal/loadProjectPlugins.js +153 -34
  50. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  51. package/lib/structures/ITtscCompilerContext.d.ts +2 -1
  52. package/lib/structures/ITtscPlugin.d.ts +8 -13
  53. package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
  54. package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
  55. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
  56. package/package.json +8 -8
  57. package/shim/lsp/shim.go +11 -17
  58. package/src/TtscCompiler.ts +20 -7
  59. package/src/compiler/internal/resolveTsgo.ts +18 -5
  60. package/src/compiler/internal/runBuild.ts +17 -4
  61. package/src/compiler/internal/sharedHostHelpers.ts +34 -67
  62. package/src/compiler/internal/transformProjectInMemory.ts +18 -5
  63. package/src/index.ts +0 -1
  64. package/src/launcher/internal/runTtsc.ts +9 -4
  65. package/src/launcher/internal/runTtscserver.ts +43 -1
  66. package/src/plugin/internal/buildSourcePlugin.ts +475 -41
  67. package/src/plugin/internal/loadProjectPlugins.ts +199 -39
  68. package/src/structures/ITtscCompilerContext.ts +2 -1
  69. package/src/structures/ITtscPlugin.ts +9 -13
  70. package/src/structures/ITtscPluginFactoryContext.ts +4 -5
  71. package/src/structures/ITtscProjectPluginConfig.ts +2 -2
  72. package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
  73. package/utility/host.go +267 -1296
  74. package/driver/lsp_proxy.go +0 -394
  75. package/driver/lsp_server.go +0 -197
  76. package/lib/defineTtscPlugin.d.ts +0 -18
  77. package/lib/defineTtscPlugin.js +0 -7
  78. package/lib/defineTtscPlugin.js.map +0 -1
  79. package/src/defineTtscPlugin.ts +0 -26
package/utility/host.go CHANGED
@@ -1,1337 +1,308 @@
1
1
  package utility
2
2
 
3
3
  import (
4
- "encoding/json"
5
- "flag"
6
- "fmt"
7
- "os"
8
- "os/exec"
9
- "path/filepath"
10
- "sort"
11
- "strings"
4
+ "encoding/json"
5
+ "flag"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
12
10
 
13
- shimast "github.com/microsoft/typescript-go/shim/ast"
14
- shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
15
- shimcore "github.com/microsoft/typescript-go/shim/core"
16
- shimprinter "github.com/microsoft/typescript-go/shim/printer"
11
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
12
+ shimprinter "github.com/microsoft/typescript-go/shim/printer"
17
13
 
18
- "github.com/samchon/ttsc/packages/ttsc/driver"
14
+ "github.com/samchon/ttsc/packages/ttsc/driver"
19
15
  )
20
16
 
21
- type pluginEntry struct {
22
- Config map[string]any `json:"config"`
23
- Name string `json:"name"`
24
- Stage string `json:"stage"`
25
- }
26
-
27
17
  type hostOptions struct {
28
- cwd string
29
- emit bool
30
- noEmit bool
31
- outDir string
32
- pluginsJSON string
33
- quiet bool
34
- tsconfig string
35
- verbose bool
36
- }
37
-
38
- type transformState struct {
39
- paths *pathsRewriter
40
- strip *stripRewriter
18
+ cwd string
19
+ emit bool
20
+ noEmit bool
21
+ outDir string
22
+ pluginsJSON string
23
+ quiet bool
24
+ tsconfig string
25
+ verbose bool
41
26
  }
42
27
 
43
28
  type transformResult struct {
44
- Diagnostics []any `json:"diagnostics,omitempty"`
45
- TypeScript map[string]string `json:"typescript"`
29
+ Diagnostics []any `json:"diagnostics,omitempty"`
30
+ TypeScript map[string]string `json:"typescript"`
46
31
  }
47
32
 
48
- // RunCheck validates the project and first-party utility plugin configuration
49
- // without emitting output.
33
+ // RunCheck validates the project and linked plugin configuration without
34
+ // emitting output.
50
35
  func RunCheck(args []string) int {
51
- opts, ok := parseHostOptions("check", args)
52
- if !ok {
53
- return 2
54
- }
55
- opts.noEmit = true
56
- prog, _, _, _, ok := loadUtilityProgram(opts)
57
- if !ok {
58
- return 2
59
- }
60
- defer prog.Close()
61
- return 0
62
- }
63
-
64
- // RunBuild hosts first-party utility transform plugins inside one compiler emit.
36
+ opts, ok := parseHostOptions("check", args)
37
+ if !ok {
38
+ return 2
39
+ }
40
+ opts.noEmit = true
41
+ prog, _, ok := loadUtilityProgram(opts)
42
+ if !ok {
43
+ return 2
44
+ }
45
+ defer prog.Close()
46
+ if err := prog.ApplyLinkedPlugins(); err != nil {
47
+ fmt.Fprintln(os.Stderr, err)
48
+ return 2
49
+ }
50
+ return 0
51
+ }
52
+
53
+ // RunBuild hosts linked transform packages inside one compiler emit.
65
54
  func RunBuild(args []string) int {
66
- opts, ok := parseHostOptions("build", args)
67
- if !ok {
68
- return 2
69
- }
70
- prog, entries, _, sourcePreamble, ok := loadUtilityProgram(opts)
71
- if !ok {
72
- return 2
73
- }
74
- defer prog.Close()
75
- if opts.noEmit {
76
- return 0
77
- }
78
- if opts.verbose {
79
- opts.quiet = false
80
- }
81
- if !opts.quiet {
82
- fmt.Fprintf(os.Stdout, "// ttsc utility: plugins=%d emit=%v\n", len(entries), !opts.noEmit)
83
- }
84
- var (
85
- res *shimcompiler.EmitResult
86
- eDiags []driver.Diagnostic
87
- err error
88
- )
89
- res, eDiags, err = prog.EmitAllRaw(makeSourcePreambleWriteFile(prog, sourcePreamble))
90
- if err != nil {
91
- fmt.Fprintf(os.Stderr, "ttsc utility: emit failed: %v\n", err)
92
- return 3
93
- }
94
- for _, d := range eDiags {
95
- fmt.Fprintln(os.Stderr, " -", d.String())
96
- }
97
- if driver.CountErrors(eDiags) > 0 {
98
- return 2
99
- }
100
- if res != nil && !opts.quiet {
101
- fmt.Fprintf(os.Stdout, "// ttsc utility: emitted=%d files\n", len(res.EmittedFiles))
102
- }
103
- return 0
104
- }
105
-
106
- // RunTransform returns the project TypeScript text after source mutations.
55
+ opts, ok := parseHostOptions("build", args)
56
+ if !ok {
57
+ return 2
58
+ }
59
+ prog, entries, ok := loadUtilityProgram(opts)
60
+ if !ok {
61
+ return 2
62
+ }
63
+ defer prog.Close()
64
+ if opts.noEmit {
65
+ return 0
66
+ }
67
+ if opts.verbose {
68
+ opts.quiet = false
69
+ }
70
+ if !opts.quiet {
71
+ fmt.Fprintf(os.Stdout, "// ttsc utility: plugins=%d emit=%v\n", len(entries), !opts.noEmit)
72
+ }
73
+ res, eDiags, err := prog.EmitAllRaw(makeSourcePreambleWriteFile(prog))
74
+ if err != nil {
75
+ fmt.Fprintf(os.Stderr, "ttsc utility: emit failed: %v\n", err)
76
+ return 3
77
+ }
78
+ for _, d := range eDiags {
79
+ fmt.Fprintln(os.Stderr, " -", d.String())
80
+ }
81
+ if driver.CountErrors(eDiags) > 0 {
82
+ return 2
83
+ }
84
+ if res != nil && !opts.quiet {
85
+ fmt.Fprintf(os.Stdout, "// ttsc utility: emitted=%d files\n", len(res.EmittedFiles))
86
+ }
87
+ return 0
88
+ }
89
+
90
+ // RunTransform returns the project TypeScript text after linked source
91
+ // mutations.
107
92
  func RunTransform(args []string) int {
108
- opts, ok := parseHostOptions("transform", args)
109
- if !ok {
110
- return 2
111
- }
112
- prog, _, _, sourcePreamble, ok := loadUtilityProgram(opts)
113
- if !ok {
114
- return 2
115
- }
116
- defer prog.Close()
117
- printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{}, shimprinter.PrintHandlers{}, nil)
118
- out := transformResult{TypeScript: map[string]string{}}
119
- for _, file := range prog.SourceFiles() {
120
- text := shimprinter.EmitSourceFile(printer, file)
121
- if sourcePreamble != "" && !shouldRemoveComments(prog) && !strings.Contains(text, sourcePreamble) {
122
- text = driver.ApplySourcePreamble(text, sourcePreamble)
123
- }
124
- out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = text
125
- }
126
- data, err := json.Marshal(out)
127
- if err != nil {
128
- fmt.Fprintf(os.Stderr, "ttsc utility: transform marshal failed: %v\n", err)
129
- return 3
130
- }
131
- fmt.Fprintln(os.Stdout, string(data))
132
- return 0
93
+ opts, ok := parseHostOptions("transform", args)
94
+ if !ok {
95
+ return 2
96
+ }
97
+ prog, _, ok := loadUtilityProgram(opts)
98
+ if !ok {
99
+ return 2
100
+ }
101
+ defer prog.Close()
102
+ if err := prog.ApplyLinkedPlugins(); err != nil {
103
+ fmt.Fprintln(os.Stderr, err)
104
+ return 2
105
+ }
106
+ printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{}, shimprinter.PrintHandlers{}, nil)
107
+ out := transformResult{TypeScript: map[string]string{}}
108
+ for _, file := range prog.SourceFiles() {
109
+ text := shimprinter.EmitSourceFile(printer, file)
110
+ out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = text
111
+ }
112
+ data, _ := json.Marshal(out)
113
+ fmt.Fprintln(os.Stdout, string(data))
114
+ return 0
133
115
  }
134
116
 
135
117
  func parseHostOptions(command string, args []string) (hostOptions, bool) {
136
- fs := flag.NewFlagSet(command, flag.ContinueOnError)
137
- fs.SetOutput(os.Stderr)
138
- cwd := fs.String("cwd", "", "project directory")
139
- emit := fs.Bool("emit", false, "force emit")
140
- noEmit := fs.Bool("noEmit", false, "force no emit")
141
- outDir := fs.String("outDir", "", "emit directory override")
142
- pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
143
- quiet := fs.Bool("quiet", true, "suppress summary")
144
- tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
145
- verbose := fs.Bool("verbose", false, "print summary")
146
- if err := fs.Parse(filterHostArgs(args)); err != nil {
147
- return hostOptions{}, false
148
- }
149
- if *emit && *noEmit {
150
- fmt.Fprintln(os.Stderr, "ttsc utility: --emit and --noEmit are mutually exclusive")
151
- return hostOptions{}, false
152
- }
153
- resolvedCwd := *cwd
154
- if resolvedCwd == "" {
155
- var err error
156
- resolvedCwd, err = os.Getwd()
157
- if err != nil {
158
- fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
159
- return hostOptions{}, false
160
- }
161
- }
162
- if !filepath.IsAbs(resolvedCwd) {
163
- abs, err := filepath.Abs(resolvedCwd)
164
- if err != nil {
165
- fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
166
- return hostOptions{}, false
167
- }
168
- resolvedCwd = abs
169
- }
170
- return hostOptions{
171
- cwd: filepath.Clean(resolvedCwd),
172
- emit: *emit,
173
- noEmit: *noEmit,
174
- outDir: *outDir,
175
- pluginsJSON: *pluginsJSON,
176
- quiet: *quiet,
177
- tsconfig: *tsconfig,
178
- verbose: *verbose,
179
- }, true
118
+ fs := flag.NewFlagSet(command, flag.ContinueOnError)
119
+ fs.SetOutput(os.Stderr)
120
+ cwd := fs.String("cwd", "", "project directory")
121
+ emit := fs.Bool("emit", false, "force emit")
122
+ noEmit := fs.Bool("noEmit", false, "force no emit")
123
+ outDir := fs.String("outDir", "", "emit directory override")
124
+ pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
125
+ quiet := fs.Bool("quiet", true, "suppress summary")
126
+ tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
127
+ verbose := fs.Bool("verbose", false, "print summary")
128
+ if err := fs.Parse(filterHostArgs(args)); err != nil {
129
+ return hostOptions{}, false
130
+ }
131
+ if *emit && *noEmit {
132
+ fmt.Fprintln(os.Stderr, "ttsc utility: --emit and --noEmit are mutually exclusive")
133
+ return hostOptions{}, false
134
+ }
135
+ resolvedCwd := *cwd
136
+ if resolvedCwd == "" {
137
+ var err error
138
+ resolvedCwd, err = os.Getwd()
139
+ if err != nil {
140
+ fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
141
+ return hostOptions{}, false
142
+ }
143
+ }
144
+ if !filepath.IsAbs(resolvedCwd) {
145
+ abs, err := filepath.Abs(resolvedCwd)
146
+ if err != nil {
147
+ fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
148
+ return hostOptions{}, false
149
+ }
150
+ resolvedCwd = abs
151
+ }
152
+ return hostOptions{
153
+ cwd: filepath.Clean(resolvedCwd),
154
+ emit: *emit,
155
+ noEmit: *noEmit,
156
+ outDir: *outDir,
157
+ pluginsJSON: *pluginsJSON,
158
+ quiet: *quiet,
159
+ tsconfig: *tsconfig,
160
+ verbose: *verbose,
161
+ }, true
180
162
  }
181
163
 
182
164
  func filterHostArgs(args []string) []string {
183
- known := map[string]bool{
184
- "cwd": true,
185
- "emit": false,
186
- "noEmit": false,
187
- "outDir": true,
188
- "plugins-json": true,
189
- "quiet": false,
190
- "tsconfig": true,
191
- "verbose": false,
192
- }
193
- filtered := make([]string, 0, len(args))
194
- for i := 0; i < len(args); i++ {
195
- current := args[i]
196
- if current == "--" {
197
- break
198
- }
199
- if !strings.HasPrefix(current, "--") {
200
- filtered = append(filtered, current)
201
- continue
202
- }
203
- name, hasInlineValue := flagName(current)
204
- takesValue, ok := known[name]
205
- if ok {
206
- filtered = append(filtered, current)
207
- if takesValue && !hasInlineValue && i+1 < len(args) {
208
- i++
209
- filtered = append(filtered, args[i])
210
- }
211
- continue
212
- }
213
- if !hasInlineValue && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
214
- i++
215
- }
216
- }
217
- return filtered
165
+ known := map[string]bool{
166
+ "cwd": true,
167
+ "emit": false,
168
+ "noEmit": false,
169
+ "outDir": true,
170
+ "plugins-json": true,
171
+ "quiet": false,
172
+ "tsconfig": true,
173
+ "verbose": false,
174
+ }
175
+ filtered := make([]string, 0, len(args))
176
+ for i := 0; i < len(args); i++ {
177
+ current := args[i]
178
+ if current == "--" {
179
+ break
180
+ }
181
+ if !strings.HasPrefix(current, "--") {
182
+ filtered = append(filtered, current)
183
+ continue
184
+ }
185
+ name, hasInlineValue := flagName(current)
186
+ takesValue, ok := known[name]
187
+ if ok {
188
+ filtered = append(filtered, current)
189
+ if takesValue && !hasInlineValue && i+1 < len(args) {
190
+ i++
191
+ filtered = append(filtered, args[i])
192
+ }
193
+ continue
194
+ }
195
+ if !hasInlineValue && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
196
+ i++
197
+ }
198
+ }
199
+ return filtered
218
200
  }
219
201
 
220
202
  func flagName(arg string) (string, bool) {
221
- name := strings.TrimPrefix(arg, "--")
222
- before, _, found := strings.Cut(name, "=")
223
- return before, found
224
- }
225
-
226
- func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, string, bool) {
227
- entries, err := parsePluginEntries(opts.pluginsJSON)
228
- if err != nil {
229
- fmt.Fprintln(os.Stderr, err)
230
- return nil, nil, transformState{}, "", false
231
- }
232
- sourcePreamble, err := prepareSourcePreamble(entries, opts.cwd, opts.tsconfig)
233
- if err != nil {
234
- fmt.Fprintln(os.Stderr, err)
235
- return nil, nil, transformState{}, "", false
236
- }
237
- prog, diags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
238
- ForceEmit: opts.emit,
239
- ForceNoEmit: opts.noEmit,
240
- OutDir: opts.outDir,
241
- SourcePreamble: sourcePreamble,
242
- })
243
- if err != nil {
244
- fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
245
- return nil, nil, transformState{}, "", false
246
- }
247
- if len(diags) > 0 {
248
- driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
249
- if prog != nil {
250
- _ = prog.Close()
251
- }
252
- return nil, nil, transformState{}, "", false
253
- }
254
- if diags := prog.Diagnostics(); len(diags) > 0 {
255
- driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
256
- _ = prog.Close()
257
- return nil, nil, transformState{}, "", false
258
- }
259
- state, err := prepareTransforms(prog, entries)
260
- if err != nil {
261
- fmt.Fprintln(os.Stderr, err)
262
- _ = prog.Close()
263
- return nil, nil, transformState{}, "", false
264
- }
265
- if err := applySourceTransforms(prog, state); err != nil {
266
- fmt.Fprintln(os.Stderr, err)
267
- _ = prog.Close()
268
- return nil, nil, transformState{}, "", false
269
- }
270
- return prog, entries, state, sourcePreamble, true
271
- }
272
-
273
- func parsePluginEntries(input string) ([]pluginEntry, error) {
274
- if strings.TrimSpace(input) == "" {
275
- return nil, nil
276
- }
277
- var entries []pluginEntry
278
- if err := json.Unmarshal([]byte(input), &entries); err != nil {
279
- return nil, fmt.Errorf("ttsc utility: invalid --plugins-json: %w", err)
280
- }
281
- return entries, nil
282
- }
283
-
284
- func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformState, error) {
285
- state := transformState{}
286
- for _, entry := range entries {
287
- switch entry.Name {
288
- case "@ttsc/paths":
289
- state.paths = newPathsRewriter(prog)
290
- case "@ttsc/strip":
291
- strip, err := parseStrip(entry.Config)
292
- if err != nil {
293
- return state, err
294
- }
295
- state.strip = strip
296
- }
297
- }
298
- return state, nil
299
- }
300
-
301
- func prepareSourcePreamble(entries []pluginEntry, cwd, tsconfigPath string) (string, error) {
302
- var preamble strings.Builder
303
- for _, entry := range entries {
304
- if entry.Name != "@ttsc/banner" {
305
- continue
306
- }
307
- banner, err := parseBanner(entry.Config, cwd, tsconfigPath)
308
- if err != nil {
309
- return "", err
310
- }
311
- preamble.WriteString(banner)
312
- }
313
- return preamble.String(), nil
314
- }
315
-
316
- func makeSourcePreambleWriteFile(prog *driver.Program, sourcePreamble string) shimcompiler.WriteFile {
317
- if sourcePreamble == "" || shouldRemoveComments(prog) {
318
- return nil
319
- }
320
- return func(fileName, text string, _ *shimcompiler.WriteFileData) error {
321
- if shouldEnsureSourcePreamble(fileName, text, sourcePreamble) {
322
- text = driver.ApplySourcePreamble(text, sourcePreamble)
323
- }
324
- return driver.DefaultWriteFile(fileName, text)
325
- }
203
+ name := strings.TrimPrefix(arg, "--")
204
+ before, _, found := strings.Cut(name, "=")
205
+ return before, found
206
+ }
207
+
208
+ func loadUtilityProgram(opts hostOptions) (*driver.Program, []driver.PluginEntry, bool) {
209
+ entries, err := parsePluginEntries(opts.pluginsJSON)
210
+ if err != nil {
211
+ fmt.Fprintln(os.Stderr, err)
212
+ return nil, nil, false
213
+ }
214
+ restoreEnv := setLinkedPluginManifest(opts.pluginsJSON)
215
+ defer restoreEnv()
216
+
217
+ prog, diags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
218
+ ForceEmit: opts.emit,
219
+ ForceNoEmit: opts.noEmit,
220
+ OutDir: opts.outDir,
221
+ })
222
+ if err != nil {
223
+ fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
224
+ return nil, nil, false
225
+ }
226
+ if len(diags) > 0 {
227
+ driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
228
+ return nil, nil, false
229
+ }
230
+ if diags := prog.Diagnostics(); len(diags) > 0 {
231
+ driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
232
+ _ = prog.Close()
233
+ return nil, nil, false
234
+ }
235
+ return prog, entries, true
236
+ }
237
+
238
+ func parsePluginEntries(input string) ([]driver.PluginEntry, error) {
239
+ if strings.TrimSpace(input) == "" {
240
+ return nil, nil
241
+ }
242
+ var entries []driver.PluginEntry
243
+ if err := json.Unmarshal([]byte(input), &entries); err != nil {
244
+ return nil, fmt.Errorf("ttsc utility: invalid --plugins-json: %w", err)
245
+ }
246
+ return entries, nil
247
+ }
248
+
249
+ func setLinkedPluginManifest(input string) func() {
250
+ previous, existed := os.LookupEnv(driver.LinkedPluginsEnv)
251
+ if strings.TrimSpace(input) == "" {
252
+ _ = os.Unsetenv(driver.LinkedPluginsEnv)
253
+ } else {
254
+ _ = os.Setenv(driver.LinkedPluginsEnv, input)
255
+ }
256
+ return func() {
257
+ if existed {
258
+ _ = os.Setenv(driver.LinkedPluginsEnv, previous)
259
+ } else {
260
+ _ = os.Unsetenv(driver.LinkedPluginsEnv)
261
+ }
262
+ }
263
+ }
264
+
265
+ func makeSourcePreambleWriteFile(prog *driver.Program) shimcompiler.WriteFile {
266
+ if prog == nil || prog.SourcePreamble == "" || shouldRemoveComments(prog) {
267
+ return nil
268
+ }
269
+ return func(fileName, text string, _ *shimcompiler.WriteFileData) error {
270
+ if shouldEnsureSourcePreamble(fileName, text, prog.SourcePreamble) {
271
+ text = driver.ApplySourcePreamble(text, prog.SourcePreamble)
272
+ }
273
+ return driver.DefaultWriteFile(fileName, text)
274
+ }
326
275
  }
327
276
 
328
277
  func shouldRemoveComments(prog *driver.Program) bool {
329
- if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
330
- return false
331
- }
332
- return prog.ParsedConfig.ParsedConfig.CompilerOptions.RemoveComments.IsTrue()
278
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
279
+ return false
280
+ }
281
+ return prog.ParsedConfig.ParsedConfig.CompilerOptions.RemoveComments.IsTrue()
333
282
  }
334
283
 
335
284
  func shouldEnsureSourcePreamble(fileName, text, sourcePreamble string) bool {
336
- return isSourcePreambleOutputTarget(fileName) && !strings.Contains(text, sourcePreamble)
285
+ return isSourcePreambleOutputTarget(fileName) && !strings.Contains(text, sourcePreamble)
337
286
  }
338
287
 
339
288
  func isSourcePreambleOutputTarget(fileName string) bool {
340
- lower := strings.ToLower(filepath.ToSlash(fileName))
341
- for _, suffix := range []string{".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
342
- if strings.HasSuffix(lower, suffix) {
343
- return true
344
- }
345
- }
346
- return false
347
- }
348
-
349
- func applySourceTransforms(prog *driver.Program, state transformState) error {
350
- for _, file := range prog.SourceFiles() {
351
- if state.paths != nil {
352
- state.paths.apply(file)
353
- }
354
- if state.strip != nil {
355
- state.strip.apply(file)
356
- }
357
- }
358
- return nil
289
+ lower := strings.ToLower(filepath.ToSlash(fileName))
290
+ for _, suffix := range []string{".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
291
+ if strings.HasSuffix(lower, suffix) {
292
+ return true
293
+ }
294
+ }
295
+ return false
359
296
  }
360
297
 
361
298
  func apiOutputKey(cwd, fileName string) string {
362
- rel, err := filepath.Rel(cwd, fileName)
363
- if err != nil || isOutsideRelativePath(rel) {
364
- return filepath.ToSlash(fileName)
365
- }
366
- return filepath.ToSlash(rel)
299
+ rel, err := filepath.Rel(cwd, fileName)
300
+ if err != nil || isOutsideRelativePath(rel) {
301
+ return filepath.ToSlash(fileName)
302
+ }
303
+ return filepath.ToSlash(rel)
367
304
  }
368
305
 
369
306
  func isOutsideRelativePath(rel string) bool {
370
- return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
371
- }
372
-
373
- func parseBanner(config map[string]any, cwd, tsconfigPath string) (string, error) {
374
- text, err := resolveBannerText(config, cwd, tsconfigPath)
375
- if err != nil {
376
- return "", err
377
- }
378
- lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
379
- for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
380
- lines = lines[:len(lines)-1]
381
- }
382
- var b strings.Builder
383
- sep := strings.Repeat("-", 64)
384
- b.WriteString("/**\n")
385
- b.WriteString(" * ")
386
- b.WriteString(sep)
387
- b.WriteByte('\n')
388
- for _, line := range lines {
389
- b.WriteString(" * ")
390
- b.WriteString(sanitizeJSDocLine(line))
391
- b.WriteByte('\n')
392
- }
393
- b.WriteString(" *\n")
394
- b.WriteString(" * @packageDocumentation\n ")
395
- b.WriteString("*/\n")
396
- return b.String(), nil
397
- }
398
-
399
- func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string, error) {
400
- if text, ok, err := bannerTextFromConfigValue(config["text"], `"text"`); ok || err != nil {
401
- return text, err
402
- }
403
- if rawConfigPath, ok := config["config"]; ok {
404
- configPath, ok := rawConfigPath.(string)
405
- if !ok || strings.TrimSpace(configPath) == "" {
406
- return "", fmt.Errorf("@ttsc/banner: \"config\" must be a non-empty string path")
407
- }
408
- location := resolveBannerConfigPath(configPath, cwd, tsconfigPath)
409
- raw, err := loadBannerConfigFile(location)
410
- if err != nil {
411
- return "", err
412
- }
413
- text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
414
- if err != nil {
415
- return "", err
416
- }
417
- if !ok {
418
- return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
419
- }
420
- return text, nil
421
- }
422
- location, err := findBannerConfigFile(cwd, tsconfigPath)
423
- if err != nil {
424
- return "", err
425
- }
426
- if location == "" {
427
- return "", fmt.Errorf("@ttsc/banner: \"text\" must be a non-empty string or a banner.config.{js,cjs,mjs,ts,mts,cts} file must exist")
428
- }
429
- raw, err := loadBannerConfigFile(location)
430
- if err != nil {
431
- return "", err
432
- }
433
- text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
434
- if err != nil {
435
- return "", err
436
- }
437
- if !ok {
438
- return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
439
- }
440
- return text, nil
441
- }
442
-
443
- func bannerTextFromConfigValue(raw any, label string) (string, bool, error) {
444
- if raw == nil {
445
- return "", false, nil
446
- }
447
- text, ok := raw.(string)
448
- if ok {
449
- if strings.TrimSpace(text) == "" {
450
- return "", true, fmt.Errorf("@ttsc/banner: %s must be a non-empty string", label)
451
- }
452
- return text, true, nil
453
- }
454
- object, ok := raw.(map[string]any)
455
- if !ok {
456
- return "", true, fmt.Errorf("@ttsc/banner: %s must be a string or an object with a non-empty \"text\" string", label)
457
- }
458
- rawText, ok := object["text"]
459
- if !ok {
460
- return "", false, nil
461
- }
462
- text, ok = rawText.(string)
463
- if !ok || strings.TrimSpace(text) == "" {
464
- return "", true, fmt.Errorf("@ttsc/banner: %s.text must be a non-empty string", label)
465
- }
466
- return text, true, nil
467
- }
468
-
469
- func findBannerConfigFile(cwd, tsconfigPath string) (string, error) {
470
- dir := discoveryConfigBaseDir(cwd, tsconfigPath)
471
- for {
472
- matches := make([]string, 0, 1)
473
- for _, name := range []string{
474
- "banner.config.js",
475
- "banner.config.cjs",
476
- "banner.config.mjs",
477
- "banner.config.ts",
478
- "banner.config.cts",
479
- "banner.config.mts",
480
- } {
481
- candidate := filepath.Join(dir, name)
482
- if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
483
- matches = append(matches, candidate)
484
- }
485
- }
486
- if len(matches) > 1 {
487
- return "", fmt.Errorf("@ttsc/banner: multiple banner.config.* files found in %s", dir)
488
- }
489
- if len(matches) == 1 {
490
- return matches[0], nil
491
- }
492
- parent := filepath.Dir(dir)
493
- if parent == dir {
494
- return "", nil
495
- }
496
- dir = parent
497
- }
498
- }
499
-
500
- func resolveBannerConfigPath(configPath, cwd, tsconfigPath string) string {
501
- if filepath.IsAbs(configPath) {
502
- return configPath
503
- }
504
- return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
505
- }
506
-
507
- func tsconfigBaseDir(cwd, tsconfigPath string) string {
508
- if tsconfigPath == "" {
509
- return cwd
510
- }
511
- resolvedTsconfig := tsconfigPath
512
- if !filepath.IsAbs(resolvedTsconfig) {
513
- resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
514
- }
515
- return filepath.Dir(resolvedTsconfig)
516
- }
517
-
518
- func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
519
- if tsconfigPath != "" {
520
- resolvedTsconfig := tsconfigPath
521
- if !filepath.IsAbs(resolvedTsconfig) {
522
- resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
523
- }
524
- return filepath.Dir(resolvedTsconfig)
525
- }
526
- return cwd
527
- }
528
-
529
- func loadBannerConfigFile(location string) (any, error) {
530
- if !isBannerConfigFileName(filepath.Base(location)) {
531
- return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{js,cjs,mjs,ts,mts,cts}: %s", location)
532
- }
533
- ext := strings.ToLower(filepath.Ext(location))
534
- switch ext {
535
- case ".js", ".cjs", ".mjs":
536
- return loadBannerScriptConfigFile(location)
537
- case ".ts", ".cts", ".mts":
538
- return loadBannerTypeScriptConfigFile(location)
539
- default:
540
- return nil, fmt.Errorf("@ttsc/banner: unsupported config file extension %q for %s", ext, location)
541
- }
542
- }
543
-
544
- func isBannerConfigFileName(name string) bool {
545
- switch name {
546
- case "banner.config.js",
547
- "banner.config.cjs",
548
- "banner.config.mjs",
549
- "banner.config.ts",
550
- "banner.config.cts",
551
- "banner.config.mts":
552
- return true
553
- default:
554
- return false
555
- }
556
- }
557
-
558
- func loadBannerScriptConfigFile(location string) (any, error) {
559
- const script = `
560
- const { pathToFileURL } = require("node:url");
561
-
562
- (async () => {
563
- const mod = await import(pathToFileURL(process.argv[1]).href);
564
- const candidate = Object.prototype.hasOwnProperty.call(mod, "default") ? mod.default : mod;
565
- const value = typeof candidate === "function" ? await candidate() : candidate;
566
- process.stdout.write(JSON.stringify(toSerializableBanner(value)));
567
- })().catch((error) => {
568
- process.stderr.write(error && error.stack ? error.stack : String(error));
569
- process.exit(1);
570
- });
571
-
572
- function toSerializableBanner(value) {
573
- if (typeof value === "string") {
574
- return value;
575
- }
576
- if (value !== null && typeof value === "object" && typeof value.text === "string") {
577
- return { text: value.text };
578
- }
579
- throw new Error("config file must export a string or an object with a text string");
580
- }
581
- `
582
- node := os.Getenv("TTSC_NODE_BINARY")
583
- if node == "" {
584
- node = "node"
585
- }
586
- cmd := exec.Command(node, "-e", script, location)
587
- cmd.Env = nodeConfigLoaderEnv(location)
588
- output, err := cmd.Output()
589
- if err != nil {
590
- stderr := ""
591
- if exit, ok := err.(*exec.ExitError); ok {
592
- stderr = strings.TrimSpace(string(exit.Stderr))
593
- }
594
- if stderr != "" {
595
- return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
596
- }
597
- return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
598
- }
599
- var out any
600
- if err := json.Unmarshal(output, &out); err != nil {
601
- return nil, fmt.Errorf("@ttsc/banner: parse config file %s output: %w", location, err)
602
- }
603
- return out, nil
604
- }
605
-
606
- func loadBannerTypeScriptConfigFile(location string) (any, error) {
607
- tempDir, err := os.MkdirTemp("", "ttsc-banner-config-")
608
- if err != nil {
609
- return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
610
- }
611
- defer os.RemoveAll(tempDir)
612
-
613
- if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
614
- return nil, err
615
- }
616
-
617
- loader := filepath.Join(tempDir, "loader.mts")
618
- tsconfig := filepath.Join(tempDir, "tsconfig.json")
619
- importSpecifier, err := relativeImportSpecifier(tempDir, location)
620
- if err != nil {
621
- return nil, err
622
- }
623
- importLiteral, err := json.Marshal(importSpecifier)
624
- if err != nil {
625
- return nil, fmt.Errorf("@ttsc/banner: encode config import %s: %w", location, err)
626
- }
627
- if err := os.WriteFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
628
- return nil, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
629
- }
630
- if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
631
- return nil, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
632
- }
633
-
634
- args := []string{
635
- "--project", tsconfig,
636
- "--cwd", tempDir,
637
- "--cache-dir", filepath.Join(tempDir, "cache"),
638
- }
639
- if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
640
- args = append(args, "--binary", tsgo)
641
- }
642
- args = append(args, loader)
643
-
644
- cmd := ttsxCommand(args...)
645
- cmd.Env = nodeConfigLoaderEnv(location)
646
- output, err := cmd.Output()
647
- if err != nil {
648
- stderr := ""
649
- if exit, ok := err.(*exec.ExitError); ok {
650
- stderr = strings.TrimSpace(string(exit.Stderr))
651
- }
652
- if stderr != "" {
653
- return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
654
- }
655
- return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
656
- }
657
- var out any
658
- if err := json.Unmarshal(output, &out); err != nil {
659
- return nil, fmt.Errorf("@ttsc/banner: parse TypeScript config file %s output: %w", location, err)
660
- }
661
- return out, nil
662
- }
663
-
664
- func relativeImportSpecifier(fromDir, location string) (string, error) {
665
- relative, err := filepath.Rel(fromDir, location)
666
- if err != nil {
667
- return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
668
- }
669
- relative = filepath.ToSlash(relative)
670
- if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
671
- return relative, nil
672
- }
673
- return "./" + relative, nil
674
- }
675
-
676
- func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
677
- return fmt.Sprintf(`import * as importedConfig from %s;
678
-
679
- declare const process: {
680
- stdout: { write(value: string): void };
681
- stderr: { write(value: string): void };
682
- exit(code?: number): never;
683
- };
684
-
685
- try {
686
- const value = await resolveConfig(importedConfig);
687
- process.stdout.write(JSON.stringify(toSerializableBanner(value)));
688
- } catch (error) {
689
- process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
690
- process.exit(1);
691
- }
692
-
693
- async function resolveConfig(value: unknown): Promise<unknown> {
694
- let current = value;
695
- for (let i = 0; i < 8; i++) {
696
- if (isObject(current) && hasOwn(current, "default")) {
697
- current = current.default;
698
- continue;
699
- }
700
- break;
701
- }
702
- if (typeof current === "function") {
703
- return await (current as () => unknown | Promise<unknown>)();
704
- }
705
- return current;
706
- }
707
-
708
- function isObject(value: unknown): value is Record<string, unknown> {
709
- return value !== null && typeof value === "object";
710
- }
711
-
712
- function hasOwn(value: Record<string, unknown>, key: string): boolean {
713
- return Object.prototype.hasOwnProperty.call(value, key);
714
- }
715
-
716
- function toSerializableBanner(value: unknown): unknown {
717
- if (typeof value === "string") {
718
- return value;
719
- }
720
- if (isObject(value) && typeof value.text === "string") {
721
- return { text: value.text };
722
- }
723
- throw new Error("config file must export a string or an object with a text string");
724
- }
725
- `, importLiteral)
726
- }
727
-
728
- func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
729
- content := map[string]any{
730
- "compilerOptions": map[string]any{
731
- "allowImportingTsExtensions": true,
732
- "module": "ESNext",
733
- "moduleResolution": "bundler",
734
- "outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
735
- "rewriteRelativeImportExtensions": true,
736
- "rootDir": "/",
737
- "skipLibCheck": true,
738
- "strict": true,
739
- "target": "ES2022",
740
- },
741
- "files": []string{
742
- filepath.ToSlash(loader),
743
- filepath.ToSlash(location),
744
- },
745
- }
746
- body, err := json.MarshalIndent(content, "", " ")
747
- if err != nil {
748
- panic(err)
749
- }
750
- return string(body)
751
- }
752
-
753
- func ttsxCommand(args ...string) *exec.Cmd {
754
- ttsx := os.Getenv("TTSC_TTSX_BINARY")
755
- if ttsx == "" {
756
- ttsx = "ttsx"
757
- }
758
- if shouldRunTtsxThroughNode(ttsx) {
759
- node := os.Getenv("TTSC_NODE_BINARY")
760
- if node == "" {
761
- node = "node"
762
- }
763
- return exec.Command(node, append([]string{ttsx}, args...)...)
764
- }
765
- return exec.Command(ttsx, args...)
766
- }
767
-
768
- func shouldRunTtsxThroughNode(binary string) bool {
769
- switch strings.ToLower(filepath.Ext(binary)) {
770
- case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
771
- return true
772
- default:
773
- return false
774
- }
775
- }
776
-
777
- func nodeConfigLoaderEnv(location string) []string {
778
- env := os.Environ()
779
- parts := make([]string, 0, 2)
780
- if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
781
- parts = append(parts, nodeModules)
782
- }
783
- if existing := os.Getenv("NODE_PATH"); existing != "" {
784
- parts = append(parts, existing)
785
- }
786
- if len(parts) == 0 {
787
- return env
788
- }
789
- return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
790
- }
791
-
792
- func linkNearestNodeModules(tempDir, sourceDir string) error {
793
- nodeModules := findNearestNodeModules(sourceDir)
794
- if nodeModules == "" {
795
- return nil
796
- }
797
- link := filepath.Join(tempDir, "node_modules")
798
- if err := os.Symlink(nodeModules, link); err != nil {
799
- return fmt.Errorf("@ttsc/banner: link config node_modules %s: %w", nodeModules, err)
800
- }
801
- return nil
802
- }
803
-
804
- func findNearestNodeModules(start string) string {
805
- dir := filepath.Clean(start)
806
- for {
807
- candidate := filepath.Join(dir, "node_modules")
808
- if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
809
- return candidate
810
- }
811
- parent := filepath.Dir(dir)
812
- if parent == dir {
813
- return ""
814
- }
815
- dir = parent
816
- }
817
- }
818
-
819
- func setEnv(env []string, key, value string) []string {
820
- prefix := key + "="
821
- for i, entry := range env {
822
- if strings.HasPrefix(entry, prefix) {
823
- env[i] = prefix + value
824
- return env
825
- }
826
- }
827
- return append(env, prefix+value)
828
- }
829
-
830
- func sanitizeJSDocLine(line string) string {
831
- return strings.ReplaceAll(line, "*/", "* /")
832
- }
833
-
834
- type pathsRewriter struct {
835
- basePath string
836
- outDir string
837
- patterns []pathsPattern
838
- rootDir string
839
- sourceFiles map[string]string
840
- }
841
-
842
- type pathsPattern struct {
843
- pattern string
844
- targets []string
845
- }
846
-
847
- func newPathsRewriter(prog *driver.Program) *pathsRewriter {
848
- out := &pathsRewriter{sourceFiles: map[string]string{}}
849
- if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
850
- return out
851
- }
852
- options := prog.ParsedConfig.ParsedConfig.CompilerOptions
853
- out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
854
- out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
855
- out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
856
- files := prog.SourceFiles()
857
- if out.rootDir == "" {
858
- out.rootDir = commonSourceDir(files)
859
- }
860
- for _, file := range files {
861
- name := normalizePath(file.FileName())
862
- out.sourceFiles[name] = name
863
- out.sourceFiles[stripKnownSourceExtension(name)] = name
864
- }
865
- if options.Paths != nil {
866
- for pattern, targets := range options.Paths.Entries() {
867
- out.patterns = append(out.patterns, pathsPattern{
868
- pattern: pattern,
869
- targets: append([]string(nil), targets...),
870
- })
871
- }
872
- }
873
- sort.SliceStable(out.patterns, func(i, j int) bool {
874
- return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
875
- })
876
- return out
877
- }
878
-
879
- func (r *pathsRewriter) apply(file *shimast.SourceFile) {
880
- if r == nil || file == nil || len(r.patterns) == 0 {
881
- return
882
- }
883
- visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
884
- if lit == nil || lit.Kind != shimast.KindStringLiteral {
885
- return
886
- }
887
- spec := lit.Text()
888
- rewritten, ok := r.rewrite(file.FileName(), spec)
889
- if ok && rewritten != spec {
890
- lit.AsStringLiteral().Text = rewritten
891
- lit.Flags |= shimast.NodeFlagsSynthesized
892
- lit.Loc = shimcore.UndefinedTextRange()
893
- }
894
- })
895
- }
896
-
897
- func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
898
- if node == nil {
899
- return
900
- }
901
- switch node.Kind {
902
- case shimast.KindImportDeclaration:
903
- visit(node.AsImportDeclaration().ModuleSpecifier)
904
- case shimast.KindExportDeclaration:
905
- visit(node.AsExportDeclaration().ModuleSpecifier)
906
- case shimast.KindImportEqualsDeclaration:
907
- ref := node.AsImportEqualsDeclaration().ModuleReference
908
- if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
909
- visit(ref.AsExternalModuleReference().Expression)
910
- }
911
- case shimast.KindImportType:
912
- arg := node.AsImportTypeNode().Argument
913
- if arg != nil && arg.Kind == shimast.KindLiteralType {
914
- visit(arg.AsLiteralTypeNode().Literal)
915
- }
916
- case shimast.KindModuleDeclaration:
917
- decl := node.AsModuleDeclaration()
918
- if decl != nil {
919
- visit(decl.Name())
920
- }
921
- case shimast.KindCallExpression:
922
- call := node.AsCallExpression()
923
- if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
924
- visit(call.Arguments.Nodes[0])
925
- }
926
- }
927
- node.ForEachChild(func(child *shimast.Node) bool {
928
- visitModuleSpecifiers(child, visit)
929
- return false
930
- })
931
- }
932
-
933
- func isModuleSpecifierCall(call *shimast.CallExpression) bool {
934
- if call == nil || call.Expression == nil {
935
- return false
936
- }
937
- switch call.Expression.Kind {
938
- case shimast.KindImportKeyword:
939
- return true
940
- case shimast.KindIdentifier:
941
- return call.Expression.Text() == "require"
942
- default:
943
- return false
944
- }
945
- }
946
-
947
- func (r *pathsRewriter) rewrite(fromSource string, specifier string) (string, bool) {
948
- if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
949
- return specifier, false
950
- }
951
- targetSource, ok := r.resolveSource(specifier)
952
- if !ok {
953
- return specifier, false
954
- }
955
- fromOut := r.outputPathForSource(fromSource)
956
- targetOut := r.outputPathForSource(targetSource)
957
- if fromOut == "" || targetOut == "" {
958
- return specifier, false
959
- }
960
- rel, err := filepath.Rel(filepath.Dir(fromOut), targetOut)
961
- if err != nil {
962
- return specifier, false
963
- }
964
- rel = filepath.ToSlash(rel)
965
- if !strings.HasPrefix(rel, ".") {
966
- rel = "./" + rel
967
- }
968
- return rel, true
969
- }
970
-
971
- func (r *pathsRewriter) resolveSource(specifier string) (string, bool) {
972
- for _, pattern := range r.patterns {
973
- star, ok := matchPattern(pattern.pattern, specifier)
974
- if !ok {
975
- continue
976
- }
977
- for _, target := range pattern.targets {
978
- candidate := strings.Replace(target, "*", star, 1)
979
- resolved := normalizePath(filepath.Join(r.basePath, candidate))
980
- if source, ok := r.lookupSource(resolved); ok {
981
- return source, true
982
- }
983
- }
984
- }
985
- return "", false
986
- }
987
-
988
- func (r *pathsRewriter) lookupSource(candidate string) (string, bool) {
989
- if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
990
- return source, true
991
- }
992
- stem := stripKnownSourceExtension(normalizePath(candidate))
993
- if source, ok := r.sourceFiles[stem]; ok {
994
- return source, true
995
- }
996
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
997
- if source, ok := r.sourceFiles[stem+ext]; ok {
998
- return source, true
999
- }
1000
- }
1001
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
1002
- if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
1003
- return source, true
1004
- }
1005
- }
1006
- return "", false
1007
- }
1008
-
1009
- func (r *pathsRewriter) outputPathForSource(source string) string {
1010
- if r.outDir == "" || r.rootDir == "" {
1011
- return ""
1012
- }
1013
- rel, err := filepath.Rel(r.rootDir, source)
1014
- if err != nil || isOutsideRelativePath(rel) {
1015
- return ""
1016
- }
1017
- return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
1018
- }
1019
-
1020
- func emittedJavaScriptExtension(source string) string {
1021
- switch strings.ToLower(filepath.Ext(source)) {
1022
- case ".mts":
1023
- return ".mjs"
1024
- case ".cts":
1025
- return ".cjs"
1026
- default:
1027
- return ".js"
1028
- }
1029
- }
1030
-
1031
- func matchPattern(pattern string, specifier string) (string, bool) {
1032
- if !strings.Contains(pattern, "*") {
1033
- return "", pattern == specifier
1034
- }
1035
- parts := strings.SplitN(pattern, "*", 2)
1036
- if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
1037
- return "", false
1038
- }
1039
- return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
1040
- }
1041
-
1042
- func patternRank(pattern string) int {
1043
- return len(strings.ReplaceAll(pattern, "*", ""))
1044
- }
1045
-
1046
- func optionalPath(value string, cwd string) string {
1047
- if value == "" {
1048
- return ""
1049
- }
1050
- if filepath.IsAbs(value) {
1051
- return normalizePath(value)
1052
- }
1053
- return normalizePath(filepath.Join(cwd, value))
1054
- }
1055
-
1056
- func commonSourceDir(files []*shimast.SourceFile) string {
1057
- if len(files) == 0 {
1058
- return ""
1059
- }
1060
- common := normalizePath(filepath.Dir(files[0].FileName()))
1061
- for _, file := range files[1:] {
1062
- dir := normalizePath(filepath.Dir(file.FileName()))
1063
- for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
1064
- next := filepath.Dir(common)
1065
- if next == common {
1066
- return common
1067
- }
1068
- common = normalizePath(next)
1069
- }
1070
- }
1071
- return common
1072
- }
1073
-
1074
- func normalizePath(value string) string {
1075
- if value == "" {
1076
- return ""
1077
- }
1078
- return filepath.ToSlash(filepath.Clean(value))
1079
- }
1080
-
1081
- func stripKnownSourceExtension(value string) string {
1082
- lower := strings.ToLower(value)
1083
- for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
1084
- if strings.HasSuffix(lower, ext) {
1085
- return value[:len(value)-len(ext)]
1086
- }
1087
- }
1088
- return strings.TrimSuffix(value, filepath.Ext(value))
1089
- }
1090
-
1091
- func replaceSourceExtension(value string, ext string) string {
1092
- return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
1093
- }
1094
-
1095
- type stripRewriter struct {
1096
- calls []callPattern
1097
- stripDebugger bool
1098
- }
1099
-
1100
- type callPattern struct {
1101
- parts []string
1102
- wildcard bool
1103
- }
1104
-
1105
- func parseStrip(config map[string]any) (*stripRewriter, error) {
1106
- _, hasCalls := config["calls"]
1107
- _, hasStatements := config["statements"]
1108
- if !hasCalls && !hasStatements {
1109
- config = map[string]any{
1110
- "calls": []any{"console.log", "console.debug", "assert.*"},
1111
- "statements": []any{"debugger"},
1112
- }
1113
- }
1114
- calls, err := stringArrayConfig(config, "calls")
1115
- if err != nil {
1116
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
1117
- }
1118
- statements, err := stringArrayConfig(config, "statements")
1119
- if err != nil {
1120
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
1121
- }
1122
- out := &stripRewriter{}
1123
- for _, call := range calls {
1124
- pattern, err := parseCallPattern(call)
1125
- if err != nil {
1126
- return nil, fmt.Errorf("@ttsc/strip: %w", err)
1127
- }
1128
- out.calls = append(out.calls, pattern)
1129
- }
1130
- for _, statement := range statements {
1131
- switch statement {
1132
- case "debugger":
1133
- out.stripDebugger = true
1134
- default:
1135
- return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
1136
- }
1137
- }
1138
- return out, nil
1139
- }
1140
-
1141
- func (s *stripRewriter) apply(file *shimast.SourceFile) {
1142
- if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
1143
- return
1144
- }
1145
- filterStatements(file.Statements, s)
1146
- }
1147
-
1148
- func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
1149
- if list == nil || len(list.Nodes) == 0 {
1150
- return
1151
- }
1152
- out := make([]*shimast.Node, 0, len(list.Nodes))
1153
- for _, stmt := range list.Nodes {
1154
- if shouldStripStatement(stmt, strip) {
1155
- continue
1156
- }
1157
- filterChildStatements(stmt, strip)
1158
- out = append(out, stmt)
1159
- }
1160
- list.Nodes = out
1161
- }
1162
-
1163
- func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
1164
- if node == nil {
1165
- return
1166
- }
1167
- filterEmbeddedStatements(node, strip)
1168
- if node.CanHaveStatements() {
1169
- filterStatements(node.StatementList(), strip)
1170
- }
1171
- node.ForEachChild(func(child *shimast.Node) bool {
1172
- filterChildStatements(child, strip)
1173
- return false
1174
- })
1175
- }
1176
-
1177
- func filterEmbeddedStatements(node *shimast.Node, strip *stripRewriter) {
1178
- switch node.Kind {
1179
- case shimast.KindIfStatement:
1180
- stmt := node.AsIfStatement()
1181
- stmt.ThenStatement = filterEmbeddedStatement(stmt.ThenStatement, strip)
1182
- stmt.ElseStatement = filterEmbeddedStatement(stmt.ElseStatement, strip)
1183
- case shimast.KindDoStatement:
1184
- stmt := node.AsDoStatement()
1185
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1186
- case shimast.KindWhileStatement:
1187
- stmt := node.AsWhileStatement()
1188
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1189
- case shimast.KindForStatement:
1190
- stmt := node.AsForStatement()
1191
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1192
- case shimast.KindForInStatement, shimast.KindForOfStatement:
1193
- stmt := node.AsForInOrOfStatement()
1194
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1195
- case shimast.KindWithStatement:
1196
- stmt := node.AsWithStatement()
1197
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1198
- case shimast.KindLabeledStatement:
1199
- stmt := node.AsLabeledStatement()
1200
- stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1201
- }
1202
- }
1203
-
1204
- func filterEmbeddedStatement(stmt *shimast.Statement, strip *stripRewriter) *shimast.Statement {
1205
- if stmt == nil {
1206
- return nil
1207
- }
1208
- if shouldStripStatement(stmt, strip) {
1209
- return emptyStatement(stmt)
1210
- }
1211
- filterChildStatements(stmt, strip)
1212
- return stmt
1213
- }
1214
-
1215
- func emptyStatement(original *shimast.Node) *shimast.Statement {
1216
- empty := shimast.NewNodeFactory(shimast.NodeFactoryHooks{}).NewEmptyStatement()
1217
- empty.Flags |= shimast.NodeFlagsSynthesized
1218
- if original != nil {
1219
- empty.Loc = original.Loc
1220
- }
1221
- return empty
1222
- }
1223
-
1224
- func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
1225
- if node == nil {
1226
- return false
1227
- }
1228
- switch node.Kind {
1229
- case shimast.KindDebuggerStatement:
1230
- return strip.stripDebugger
1231
- case shimast.KindExpressionStatement:
1232
- expr := node.AsExpressionStatement().Expression
1233
- name, ok := callExpressionName(expr)
1234
- return ok && strip.matchesCall(name)
1235
- default:
1236
- return false
1237
- }
1238
- }
1239
-
1240
- func (s *stripRewriter) matchesCall(name string) bool {
1241
- for _, pattern := range s.calls {
1242
- if pattern.matches(name) {
1243
- return true
1244
- }
1245
- }
1246
- return false
1247
- }
1248
-
1249
- func parseCallPattern(text string) (callPattern, error) {
1250
- parts := strings.Split(text, ".")
1251
- if len(parts) == 0 {
1252
- return callPattern{}, fmt.Errorf("empty call pattern")
1253
- }
1254
- for i, part := range parts {
1255
- if part == "" {
1256
- return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
1257
- }
1258
- if part == "*" && i != len(parts)-1 {
1259
- return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
1260
- }
1261
- }
1262
- wildcard := parts[len(parts)-1] == "*"
1263
- if wildcard {
1264
- parts = parts[:len(parts)-1]
1265
- }
1266
- return callPattern{parts: parts, wildcard: wildcard}, nil
1267
- }
1268
-
1269
- func (p callPattern) matches(name string) bool {
1270
- parts := strings.Split(name, ".")
1271
- if p.wildcard {
1272
- if len(parts) <= len(p.parts) {
1273
- return false
1274
- }
1275
- return equalStringSlices(parts[:len(p.parts)], p.parts)
1276
- }
1277
- return equalStringSlices(parts, p.parts)
1278
- }
1279
-
1280
- func callExpressionName(expr *shimast.Node) (string, bool) {
1281
- if expr == nil || expr.Kind != shimast.KindCallExpression {
1282
- return "", false
1283
- }
1284
- call := expr.AsCallExpression()
1285
- return dottedName(call.Expression)
1286
- }
1287
-
1288
- func dottedName(expr *shimast.Node) (string, bool) {
1289
- if expr == nil {
1290
- return "", false
1291
- }
1292
- switch expr.Kind {
1293
- case shimast.KindIdentifier:
1294
- return expr.Text(), true
1295
- case shimast.KindPropertyAccessExpression:
1296
- prop := expr.AsPropertyAccessExpression()
1297
- left, ok := dottedName(prop.Expression)
1298
- if !ok || prop.Name() == nil {
1299
- return "", false
1300
- }
1301
- return left + "." + prop.Name().Text(), true
1302
- default:
1303
- return "", false
1304
- }
1305
- }
1306
-
1307
- func stringArrayConfig(config map[string]any, key string) ([]string, error) {
1308
- raw, ok := config[key]
1309
- if !ok || raw == nil {
1310
- return nil, nil
1311
- }
1312
- values, ok := raw.([]any)
1313
- if !ok {
1314
- return nil, fmt.Errorf("%q must be an array of strings", key)
1315
- }
1316
- out := make([]string, 0, len(values))
1317
- for i, value := range values {
1318
- text, ok := value.(string)
1319
- if !ok || strings.TrimSpace(text) == "" {
1320
- return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
1321
- }
1322
- out = append(out, text)
1323
- }
1324
- return out, nil
1325
- }
1326
-
1327
- func equalStringSlices(left, right []string) bool {
1328
- if len(left) != len(right) {
1329
- return false
1330
- }
1331
- for i := range left {
1332
- if left[i] != right[i] {
1333
- return false
1334
- }
1335
- }
1336
- return true
307
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
1337
308
  }