ttsc 0.7.3 → 0.8.0-dev.20260506
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.
- package/README.md +3 -4
- package/cmd/ttsc/main.go +1 -1
- package/driver/program.go +56 -3
- package/driver/rewrite.go +22 -2
- package/lib/compiler/internal/runBuild.js +46 -49
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +43 -4
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +20 -2
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +6 -0
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +12 -6
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +4 -3
- package/lib/structures/ITtscPlugin.js +4 -0
- package/lib/structures/ITtscPlugin.js.map +1 -1
- package/lib/structures/TtscPluginStage.d.ts +3 -11
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +2 -0
- package/package.json +11 -10
- package/shim/ast/lint.go +1 -0
- package/shim/ast/shim.go +5 -4
- package/shim/core/shim.go +3 -0
- package/src/compiler/internal/runBuild.ts +53 -85
- package/src/compiler/internal/transformProjectInMemory.ts +50 -6
- package/src/launcher/internal/runTtsc.ts +18 -2
- package/src/plugin/internal/buildSourcePlugin.ts +23 -0
- package/src/plugin/internal/loadProjectPlugins.ts +17 -6
- package/src/structures/ITtscPlugin.ts +12 -3
- package/src/structures/TtscPluginStage.ts +3 -11
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +2 -0
- package/test/tsgo.test.ts +9 -6
- package/utility/host.go +735 -0
package/utility/host.go
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
package utility
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"flag"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"sort"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
14
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
15
|
+
|
|
16
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type pluginEntry struct {
|
|
20
|
+
Config map[string]any `json:"config"`
|
|
21
|
+
Name string `json:"name"`
|
|
22
|
+
Stage string `json:"stage"`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type hostOptions struct {
|
|
26
|
+
cwd string
|
|
27
|
+
emit bool
|
|
28
|
+
noEmit bool
|
|
29
|
+
outDir string
|
|
30
|
+
pluginsJSON string
|
|
31
|
+
quiet bool
|
|
32
|
+
tsconfig string
|
|
33
|
+
verbose bool
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type transformState struct {
|
|
37
|
+
paths *pathsRewriter
|
|
38
|
+
strip *stripRewriter
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type transformResult struct {
|
|
42
|
+
Diagnostics []any `json:"diagnostics,omitempty"`
|
|
43
|
+
TypeScript map[string]string `json:"typescript"`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// RunBuild hosts first-party utility transform plugins inside one compiler emit.
|
|
47
|
+
func RunBuild(args []string) int {
|
|
48
|
+
opts, ok := parseHostOptions("build", args)
|
|
49
|
+
if !ok {
|
|
50
|
+
return 2
|
|
51
|
+
}
|
|
52
|
+
prog, entries, _, sourcePreamble, ok := loadUtilityProgram(opts)
|
|
53
|
+
if !ok {
|
|
54
|
+
return 2
|
|
55
|
+
}
|
|
56
|
+
defer prog.Close()
|
|
57
|
+
if opts.noEmit {
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
if opts.verbose {
|
|
61
|
+
opts.quiet = false
|
|
62
|
+
}
|
|
63
|
+
if !opts.quiet {
|
|
64
|
+
fmt.Fprintf(os.Stdout, "// ttsc utility: plugins=%d emit=%v\n", len(entries), !opts.noEmit)
|
|
65
|
+
}
|
|
66
|
+
var (
|
|
67
|
+
res *shimcompiler.EmitResult
|
|
68
|
+
eDiags []driver.Diagnostic
|
|
69
|
+
err error
|
|
70
|
+
)
|
|
71
|
+
res, eDiags, err = prog.EmitAllRaw(makeSourcePreambleWriteFile(prog, sourcePreamble))
|
|
72
|
+
if err != nil {
|
|
73
|
+
fmt.Fprintf(os.Stderr, "ttsc utility: emit failed: %v\n", err)
|
|
74
|
+
return 3
|
|
75
|
+
}
|
|
76
|
+
for _, d := range eDiags {
|
|
77
|
+
fmt.Fprintln(os.Stderr, " -", d.String())
|
|
78
|
+
}
|
|
79
|
+
if res != nil && !opts.quiet {
|
|
80
|
+
fmt.Fprintf(os.Stdout, "// ttsc utility: emitted=%d files\n", len(res.EmittedFiles))
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// RunTransform returns the project TypeScript text after source mutations.
|
|
86
|
+
func RunTransform(args []string) int {
|
|
87
|
+
opts, ok := parseHostOptions("transform", args)
|
|
88
|
+
if !ok {
|
|
89
|
+
return 2
|
|
90
|
+
}
|
|
91
|
+
prog, _, _, _, ok := loadUtilityProgram(opts)
|
|
92
|
+
if !ok {
|
|
93
|
+
return 2
|
|
94
|
+
}
|
|
95
|
+
defer prog.Close()
|
|
96
|
+
out := transformResult{TypeScript: map[string]string{}}
|
|
97
|
+
for _, file := range prog.SourceFiles() {
|
|
98
|
+
out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = file.Text()
|
|
99
|
+
}
|
|
100
|
+
data, err := json.Marshal(out)
|
|
101
|
+
if err != nil {
|
|
102
|
+
fmt.Fprintf(os.Stderr, "ttsc utility: transform marshal failed: %v\n", err)
|
|
103
|
+
return 3
|
|
104
|
+
}
|
|
105
|
+
fmt.Fprintln(os.Stdout, string(data))
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func parseHostOptions(command string, args []string) (hostOptions, bool) {
|
|
110
|
+
fs := flag.NewFlagSet(command, flag.ContinueOnError)
|
|
111
|
+
fs.SetOutput(os.Stderr)
|
|
112
|
+
cwd := fs.String("cwd", "", "project directory")
|
|
113
|
+
emit := fs.Bool("emit", false, "force emit")
|
|
114
|
+
noEmit := fs.Bool("noEmit", false, "force no emit")
|
|
115
|
+
outDir := fs.String("outDir", "", "emit directory override")
|
|
116
|
+
pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
|
|
117
|
+
quiet := fs.Bool("quiet", true, "suppress summary")
|
|
118
|
+
tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
|
|
119
|
+
verbose := fs.Bool("verbose", false, "print summary")
|
|
120
|
+
if err := fs.Parse(args); err != nil {
|
|
121
|
+
return hostOptions{}, false
|
|
122
|
+
}
|
|
123
|
+
resolvedCwd := *cwd
|
|
124
|
+
if resolvedCwd == "" {
|
|
125
|
+
var err error
|
|
126
|
+
resolvedCwd, err = os.Getwd()
|
|
127
|
+
if err != nil {
|
|
128
|
+
fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
|
|
129
|
+
return hostOptions{}, false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if !filepath.IsAbs(resolvedCwd) {
|
|
133
|
+
abs, err := filepath.Abs(resolvedCwd)
|
|
134
|
+
if err != nil {
|
|
135
|
+
fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
|
|
136
|
+
return hostOptions{}, false
|
|
137
|
+
}
|
|
138
|
+
resolvedCwd = abs
|
|
139
|
+
}
|
|
140
|
+
return hostOptions{
|
|
141
|
+
cwd: filepath.Clean(resolvedCwd),
|
|
142
|
+
emit: *emit,
|
|
143
|
+
noEmit: *noEmit,
|
|
144
|
+
outDir: *outDir,
|
|
145
|
+
pluginsJSON: *pluginsJSON,
|
|
146
|
+
quiet: *quiet,
|
|
147
|
+
tsconfig: *tsconfig,
|
|
148
|
+
verbose: *verbose,
|
|
149
|
+
}, true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, string, bool) {
|
|
153
|
+
entries, err := parsePluginEntries(opts.pluginsJSON)
|
|
154
|
+
if err != nil {
|
|
155
|
+
fmt.Fprintln(os.Stderr, err)
|
|
156
|
+
return nil, nil, transformState{}, "", false
|
|
157
|
+
}
|
|
158
|
+
sourcePreamble, err := prepareSourcePreamble(entries)
|
|
159
|
+
if err != nil {
|
|
160
|
+
fmt.Fprintln(os.Stderr, err)
|
|
161
|
+
return nil, nil, transformState{}, "", false
|
|
162
|
+
}
|
|
163
|
+
prog, diags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
|
|
164
|
+
ForceEmit: opts.emit,
|
|
165
|
+
ForceNoEmit: opts.noEmit,
|
|
166
|
+
OutDir: opts.outDir,
|
|
167
|
+
SourcePreamble: sourcePreamble,
|
|
168
|
+
})
|
|
169
|
+
if err != nil {
|
|
170
|
+
fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
|
|
171
|
+
return nil, nil, transformState{}, "", false
|
|
172
|
+
}
|
|
173
|
+
if len(diags) > 0 {
|
|
174
|
+
driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
|
|
175
|
+
return nil, nil, transformState{}, "", false
|
|
176
|
+
}
|
|
177
|
+
if diags := prog.Diagnostics(); len(diags) > 0 {
|
|
178
|
+
driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
|
|
179
|
+
return nil, nil, transformState{}, "", false
|
|
180
|
+
}
|
|
181
|
+
state, err := prepareTransforms(prog, entries)
|
|
182
|
+
if err != nil {
|
|
183
|
+
fmt.Fprintln(os.Stderr, err)
|
|
184
|
+
return nil, nil, transformState{}, "", false
|
|
185
|
+
}
|
|
186
|
+
if err := applySourceTransforms(prog, state); err != nil {
|
|
187
|
+
fmt.Fprintln(os.Stderr, err)
|
|
188
|
+
return nil, nil, transformState{}, "", false
|
|
189
|
+
}
|
|
190
|
+
return prog, entries, state, sourcePreamble, true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func parsePluginEntries(input string) ([]pluginEntry, error) {
|
|
194
|
+
if strings.TrimSpace(input) == "" {
|
|
195
|
+
return nil, nil
|
|
196
|
+
}
|
|
197
|
+
var entries []pluginEntry
|
|
198
|
+
if err := json.Unmarshal([]byte(input), &entries); err != nil {
|
|
199
|
+
return nil, fmt.Errorf("ttsc utility: invalid --plugins-json: %w", err)
|
|
200
|
+
}
|
|
201
|
+
return entries, nil
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformState, error) {
|
|
205
|
+
state := transformState{}
|
|
206
|
+
for _, entry := range entries {
|
|
207
|
+
switch entry.Name {
|
|
208
|
+
case "@ttsc/paths":
|
|
209
|
+
state.paths = newPathsRewriter(prog)
|
|
210
|
+
case "@ttsc/strip":
|
|
211
|
+
strip, err := parseStrip(entry.Config)
|
|
212
|
+
if err != nil {
|
|
213
|
+
return state, err
|
|
214
|
+
}
|
|
215
|
+
state.strip = strip
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return state, nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func prepareSourcePreamble(entries []pluginEntry) (string, error) {
|
|
222
|
+
var preamble strings.Builder
|
|
223
|
+
for _, entry := range entries {
|
|
224
|
+
if entry.Name != "@ttsc/banner" {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
banner, err := parseBanner(entry.Config)
|
|
228
|
+
if err != nil {
|
|
229
|
+
return "", err
|
|
230
|
+
}
|
|
231
|
+
preamble.WriteString(banner)
|
|
232
|
+
}
|
|
233
|
+
return preamble.String(), nil
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func makeSourcePreambleWriteFile(prog *driver.Program, sourcePreamble string) shimcompiler.WriteFile {
|
|
237
|
+
if sourcePreamble == "" || shouldRemoveComments(prog) {
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
return func(fileName, text string, _ *shimcompiler.WriteFileData) error {
|
|
241
|
+
if shouldEnsureSourcePreamble(fileName, text, sourcePreamble) {
|
|
242
|
+
text = sourcePreamble + text
|
|
243
|
+
}
|
|
244
|
+
return driver.DefaultWriteFile(fileName, text)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func shouldRemoveComments(prog *driver.Program) bool {
|
|
249
|
+
if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
return prog.ParsedConfig.ParsedConfig.CompilerOptions.RemoveComments.IsTrue()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func shouldEnsureSourcePreamble(fileName, text, sourcePreamble string) bool {
|
|
256
|
+
return isSourcePreambleOutputTarget(fileName) && !strings.Contains(text, sourcePreamble)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func isSourcePreambleOutputTarget(fileName string) bool {
|
|
260
|
+
lower := strings.ToLower(filepath.ToSlash(fileName))
|
|
261
|
+
for _, suffix := range []string{".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
|
|
262
|
+
if strings.HasSuffix(lower, suffix) {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func applySourceTransforms(prog *driver.Program, state transformState) error {
|
|
270
|
+
for _, file := range prog.SourceFiles() {
|
|
271
|
+
if state.paths != nil {
|
|
272
|
+
state.paths.apply(file)
|
|
273
|
+
}
|
|
274
|
+
if state.strip != nil {
|
|
275
|
+
state.strip.apply(file)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return nil
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func apiOutputKey(cwd, fileName string) string {
|
|
282
|
+
rel, err := filepath.Rel(cwd, fileName)
|
|
283
|
+
if err != nil || strings.HasPrefix(rel, "..") {
|
|
284
|
+
return filepath.ToSlash(fileName)
|
|
285
|
+
}
|
|
286
|
+
return filepath.ToSlash(rel)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func parseBanner(config map[string]any) (string, error) {
|
|
290
|
+
raw, ok := config["banner"]
|
|
291
|
+
if !ok {
|
|
292
|
+
return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
|
|
293
|
+
}
|
|
294
|
+
text, ok := raw.(string)
|
|
295
|
+
if !ok || strings.TrimSpace(text) == "" {
|
|
296
|
+
return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
|
|
297
|
+
}
|
|
298
|
+
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
|
|
299
|
+
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
|
|
300
|
+
lines = lines[:len(lines)-1]
|
|
301
|
+
}
|
|
302
|
+
var b strings.Builder
|
|
303
|
+
sep := strings.Repeat("-", 64)
|
|
304
|
+
b.WriteString("/**\n")
|
|
305
|
+
b.WriteString(" * ")
|
|
306
|
+
b.WriteString(sep)
|
|
307
|
+
b.WriteByte('\n')
|
|
308
|
+
for _, line := range lines {
|
|
309
|
+
b.WriteString(" * ")
|
|
310
|
+
b.WriteString(sanitizeJSDocLine(line))
|
|
311
|
+
b.WriteByte('\n')
|
|
312
|
+
}
|
|
313
|
+
b.WriteString(" *\n")
|
|
314
|
+
b.WriteString(" * @packageDocumentation\n ")
|
|
315
|
+
b.WriteString("*/\n")
|
|
316
|
+
return b.String(), nil
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func sanitizeJSDocLine(line string) string {
|
|
320
|
+
return strings.ReplaceAll(line, "*/", "* /")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
type pathsRewriter struct {
|
|
324
|
+
basePath string
|
|
325
|
+
outDir string
|
|
326
|
+
patterns []pathsPattern
|
|
327
|
+
rootDir string
|
|
328
|
+
sourceFiles map[string]string
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type pathsPattern struct {
|
|
332
|
+
pattern string
|
|
333
|
+
targets []string
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func newPathsRewriter(prog *driver.Program) *pathsRewriter {
|
|
337
|
+
out := &pathsRewriter{sourceFiles: map[string]string{}}
|
|
338
|
+
if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
|
|
339
|
+
return out
|
|
340
|
+
}
|
|
341
|
+
options := prog.ParsedConfig.ParsedConfig.CompilerOptions
|
|
342
|
+
out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
|
|
343
|
+
out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
|
|
344
|
+
out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
|
|
345
|
+
files := prog.SourceFiles()
|
|
346
|
+
if out.rootDir == "" {
|
|
347
|
+
out.rootDir = commonSourceDir(files)
|
|
348
|
+
}
|
|
349
|
+
for _, file := range files {
|
|
350
|
+
name := normalizePath(file.FileName())
|
|
351
|
+
out.sourceFiles[name] = name
|
|
352
|
+
out.sourceFiles[stripKnownSourceExtension(name)] = name
|
|
353
|
+
}
|
|
354
|
+
if options.Paths != nil {
|
|
355
|
+
for pattern, targets := range options.Paths.Entries() {
|
|
356
|
+
out.patterns = append(out.patterns, pathsPattern{
|
|
357
|
+
pattern: pattern,
|
|
358
|
+
targets: append([]string(nil), targets...),
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
sort.SliceStable(out.patterns, func(i, j int) bool {
|
|
363
|
+
return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
|
|
364
|
+
})
|
|
365
|
+
return out
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func (r *pathsRewriter) apply(file *shimast.SourceFile) {
|
|
369
|
+
if r == nil || file == nil || len(r.patterns) == 0 {
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
|
|
373
|
+
if lit == nil || lit.Kind != shimast.KindStringLiteral {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
spec := lit.Text()
|
|
377
|
+
rewritten, ok := r.rewrite(file.FileName(), spec)
|
|
378
|
+
if ok && rewritten != spec {
|
|
379
|
+
lit.AsStringLiteral().Text = rewritten
|
|
380
|
+
lit.Flags |= shimast.NodeFlagsSynthesized
|
|
381
|
+
lit.Loc = shimcore.UndefinedTextRange()
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
|
|
387
|
+
if node == nil {
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
switch node.Kind {
|
|
391
|
+
case shimast.KindImportDeclaration:
|
|
392
|
+
visit(node.AsImportDeclaration().ModuleSpecifier)
|
|
393
|
+
case shimast.KindExportDeclaration:
|
|
394
|
+
visit(node.AsExportDeclaration().ModuleSpecifier)
|
|
395
|
+
case shimast.KindImportEqualsDeclaration:
|
|
396
|
+
ref := node.AsImportEqualsDeclaration().ModuleReference
|
|
397
|
+
if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
|
|
398
|
+
visit(ref.AsExternalModuleReference().Expression)
|
|
399
|
+
}
|
|
400
|
+
case shimast.KindImportType:
|
|
401
|
+
arg := node.AsImportTypeNode().Argument
|
|
402
|
+
if arg != nil && arg.Kind == shimast.KindLiteralType {
|
|
403
|
+
visit(arg.AsLiteralTypeNode().Literal)
|
|
404
|
+
}
|
|
405
|
+
case shimast.KindCallExpression:
|
|
406
|
+
call := node.AsCallExpression()
|
|
407
|
+
if call.Expression != nil && call.Expression.Kind == shimast.KindImportKeyword && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
|
|
408
|
+
visit(call.Arguments.Nodes[0])
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
412
|
+
visitModuleSpecifiers(child, visit)
|
|
413
|
+
return false
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
func (r *pathsRewriter) rewrite(fromSource string, specifier string) (string, bool) {
|
|
418
|
+
if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
|
|
419
|
+
return specifier, false
|
|
420
|
+
}
|
|
421
|
+
targetSource, ok := r.resolveSource(specifier)
|
|
422
|
+
if !ok {
|
|
423
|
+
return specifier, false
|
|
424
|
+
}
|
|
425
|
+
fromOut := r.outputPathForSource(fromSource)
|
|
426
|
+
targetOut := r.outputPathForSource(targetSource)
|
|
427
|
+
if fromOut == "" || targetOut == "" {
|
|
428
|
+
return specifier, false
|
|
429
|
+
}
|
|
430
|
+
rel, err := filepath.Rel(filepath.Dir(fromOut), targetOut)
|
|
431
|
+
if err != nil {
|
|
432
|
+
return specifier, false
|
|
433
|
+
}
|
|
434
|
+
rel = filepath.ToSlash(rel)
|
|
435
|
+
if !strings.HasPrefix(rel, ".") {
|
|
436
|
+
rel = "./" + rel
|
|
437
|
+
}
|
|
438
|
+
return rel, true
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func (r *pathsRewriter) resolveSource(specifier string) (string, bool) {
|
|
442
|
+
for _, pattern := range r.patterns {
|
|
443
|
+
star, ok := matchPattern(pattern.pattern, specifier)
|
|
444
|
+
if !ok {
|
|
445
|
+
continue
|
|
446
|
+
}
|
|
447
|
+
for _, target := range pattern.targets {
|
|
448
|
+
candidate := strings.Replace(target, "*", star, 1)
|
|
449
|
+
resolved := normalizePath(filepath.Join(r.basePath, candidate))
|
|
450
|
+
if source, ok := r.lookupSource(resolved); ok {
|
|
451
|
+
return source, true
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return "", false
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func (r *pathsRewriter) lookupSource(candidate string) (string, bool) {
|
|
459
|
+
if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
|
|
460
|
+
return source, true
|
|
461
|
+
}
|
|
462
|
+
stem := stripKnownSourceExtension(normalizePath(candidate))
|
|
463
|
+
if source, ok := r.sourceFiles[stem]; ok {
|
|
464
|
+
return source, true
|
|
465
|
+
}
|
|
466
|
+
for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
|
|
467
|
+
if source, ok := r.sourceFiles[stem+ext]; ok {
|
|
468
|
+
return source, true
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return "", false
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func (r *pathsRewriter) outputPathForSource(source string) string {
|
|
475
|
+
if r.outDir == "" || r.rootDir == "" {
|
|
476
|
+
return ""
|
|
477
|
+
}
|
|
478
|
+
rel, err := filepath.Rel(r.rootDir, source)
|
|
479
|
+
if err != nil || strings.HasPrefix(rel, "..") {
|
|
480
|
+
return ""
|
|
481
|
+
}
|
|
482
|
+
return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, ".js")))
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func matchPattern(pattern string, specifier string) (string, bool) {
|
|
486
|
+
if !strings.Contains(pattern, "*") {
|
|
487
|
+
return "", pattern == specifier
|
|
488
|
+
}
|
|
489
|
+
parts := strings.SplitN(pattern, "*", 2)
|
|
490
|
+
if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
|
|
491
|
+
return "", false
|
|
492
|
+
}
|
|
493
|
+
return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
func patternRank(pattern string) int {
|
|
497
|
+
return len(strings.ReplaceAll(pattern, "*", ""))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
func optionalPath(value string, cwd string) string {
|
|
501
|
+
if value == "" {
|
|
502
|
+
return ""
|
|
503
|
+
}
|
|
504
|
+
if filepath.IsAbs(value) {
|
|
505
|
+
return normalizePath(value)
|
|
506
|
+
}
|
|
507
|
+
return normalizePath(filepath.Join(cwd, value))
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
func commonSourceDir(files []*shimast.SourceFile) string {
|
|
511
|
+
if len(files) == 0 {
|
|
512
|
+
return ""
|
|
513
|
+
}
|
|
514
|
+
common := normalizePath(filepath.Dir(files[0].FileName()))
|
|
515
|
+
for _, file := range files[1:] {
|
|
516
|
+
dir := normalizePath(filepath.Dir(file.FileName()))
|
|
517
|
+
for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
|
|
518
|
+
next := filepath.Dir(common)
|
|
519
|
+
if next == common {
|
|
520
|
+
return common
|
|
521
|
+
}
|
|
522
|
+
common = normalizePath(next)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return common
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
func normalizePath(value string) string {
|
|
529
|
+
if value == "" {
|
|
530
|
+
return ""
|
|
531
|
+
}
|
|
532
|
+
return filepath.ToSlash(filepath.Clean(value))
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
func stripKnownSourceExtension(value string) string {
|
|
536
|
+
lower := strings.ToLower(value)
|
|
537
|
+
for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
|
|
538
|
+
if strings.HasSuffix(lower, ext) {
|
|
539
|
+
return value[:len(value)-len(ext)]
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return strings.TrimSuffix(value, filepath.Ext(value))
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
func replaceSourceExtension(value string, ext string) string {
|
|
546
|
+
return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
type stripRewriter struct {
|
|
550
|
+
calls []callPattern
|
|
551
|
+
stripDebugger bool
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
type callPattern struct {
|
|
555
|
+
parts []string
|
|
556
|
+
wildcard bool
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
func parseStrip(config map[string]any) (*stripRewriter, error) {
|
|
560
|
+
calls, err := stringArrayConfig(config, "calls")
|
|
561
|
+
if err != nil {
|
|
562
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
563
|
+
}
|
|
564
|
+
statements, err := stringArrayConfig(config, "statements")
|
|
565
|
+
if err != nil {
|
|
566
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
567
|
+
}
|
|
568
|
+
out := &stripRewriter{}
|
|
569
|
+
for _, call := range calls {
|
|
570
|
+
pattern, err := parseCallPattern(call)
|
|
571
|
+
if err != nil {
|
|
572
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
573
|
+
}
|
|
574
|
+
out.calls = append(out.calls, pattern)
|
|
575
|
+
}
|
|
576
|
+
for _, statement := range statements {
|
|
577
|
+
switch statement {
|
|
578
|
+
case "debugger":
|
|
579
|
+
out.stripDebugger = true
|
|
580
|
+
default:
|
|
581
|
+
return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return out, nil
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func (s *stripRewriter) apply(file *shimast.SourceFile) {
|
|
588
|
+
if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
filterStatements(file.Statements, s)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
|
|
595
|
+
if list == nil || len(list.Nodes) == 0 {
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
out := list.Nodes[:0]
|
|
599
|
+
for _, stmt := range list.Nodes {
|
|
600
|
+
if shouldStripStatement(stmt, strip) {
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
filterChildStatements(stmt, strip)
|
|
604
|
+
out = append(out, stmt)
|
|
605
|
+
}
|
|
606
|
+
list.Nodes = out
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
|
|
610
|
+
if node == nil {
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
if node.CanHaveStatements() {
|
|
614
|
+
filterStatements(node.StatementList(), strip)
|
|
615
|
+
}
|
|
616
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
617
|
+
filterChildStatements(child, strip)
|
|
618
|
+
return false
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
|
|
623
|
+
if node == nil {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
switch node.Kind {
|
|
627
|
+
case shimast.KindDebuggerStatement:
|
|
628
|
+
return strip.stripDebugger
|
|
629
|
+
case shimast.KindExpressionStatement:
|
|
630
|
+
expr := node.AsExpressionStatement().Expression
|
|
631
|
+
name, ok := callExpressionName(expr)
|
|
632
|
+
return ok && strip.matchesCall(name)
|
|
633
|
+
default:
|
|
634
|
+
return false
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
func (s *stripRewriter) matchesCall(name string) bool {
|
|
639
|
+
for _, pattern := range s.calls {
|
|
640
|
+
if pattern.matches(name) {
|
|
641
|
+
return true
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return false
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
func parseCallPattern(text string) (callPattern, error) {
|
|
648
|
+
parts := strings.Split(text, ".")
|
|
649
|
+
if len(parts) == 0 {
|
|
650
|
+
return callPattern{}, fmt.Errorf("empty call pattern")
|
|
651
|
+
}
|
|
652
|
+
for i, part := range parts {
|
|
653
|
+
if part == "" {
|
|
654
|
+
return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
|
|
655
|
+
}
|
|
656
|
+
if part == "*" && i != len(parts)-1 {
|
|
657
|
+
return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
wildcard := parts[len(parts)-1] == "*"
|
|
661
|
+
if wildcard {
|
|
662
|
+
parts = parts[:len(parts)-1]
|
|
663
|
+
}
|
|
664
|
+
return callPattern{parts: parts, wildcard: wildcard}, nil
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
func (p callPattern) matches(name string) bool {
|
|
668
|
+
parts := strings.Split(name, ".")
|
|
669
|
+
if p.wildcard {
|
|
670
|
+
if len(parts) <= len(p.parts) {
|
|
671
|
+
return false
|
|
672
|
+
}
|
|
673
|
+
return equalStringSlices(parts[:len(p.parts)], p.parts)
|
|
674
|
+
}
|
|
675
|
+
return equalStringSlices(parts, p.parts)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
func callExpressionName(expr *shimast.Node) (string, bool) {
|
|
679
|
+
if expr == nil || expr.Kind != shimast.KindCallExpression {
|
|
680
|
+
return "", false
|
|
681
|
+
}
|
|
682
|
+
call := expr.AsCallExpression()
|
|
683
|
+
return dottedName(call.Expression)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
func dottedName(expr *shimast.Node) (string, bool) {
|
|
687
|
+
if expr == nil {
|
|
688
|
+
return "", false
|
|
689
|
+
}
|
|
690
|
+
switch expr.Kind {
|
|
691
|
+
case shimast.KindIdentifier:
|
|
692
|
+
return expr.Text(), true
|
|
693
|
+
case shimast.KindPropertyAccessExpression:
|
|
694
|
+
prop := expr.AsPropertyAccessExpression()
|
|
695
|
+
left, ok := dottedName(prop.Expression)
|
|
696
|
+
if !ok || prop.Name() == nil {
|
|
697
|
+
return "", false
|
|
698
|
+
}
|
|
699
|
+
return left + "." + prop.Name().Text(), true
|
|
700
|
+
default:
|
|
701
|
+
return "", false
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
func stringArrayConfig(config map[string]any, key string) ([]string, error) {
|
|
706
|
+
raw, ok := config[key]
|
|
707
|
+
if !ok || raw == nil {
|
|
708
|
+
return nil, nil
|
|
709
|
+
}
|
|
710
|
+
values, ok := raw.([]any)
|
|
711
|
+
if !ok {
|
|
712
|
+
return nil, fmt.Errorf("%q must be an array of strings", key)
|
|
713
|
+
}
|
|
714
|
+
out := make([]string, 0, len(values))
|
|
715
|
+
for i, value := range values {
|
|
716
|
+
text, ok := value.(string)
|
|
717
|
+
if !ok || strings.TrimSpace(text) == "" {
|
|
718
|
+
return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
|
|
719
|
+
}
|
|
720
|
+
out = append(out, text)
|
|
721
|
+
}
|
|
722
|
+
return out, nil
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
func equalStringSlices(left, right []string) bool {
|
|
726
|
+
if len(left) != len(right) {
|
|
727
|
+
return false
|
|
728
|
+
}
|
|
729
|
+
for i := range left {
|
|
730
|
+
if left[i] != right[i] {
|
|
731
|
+
return false
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return true
|
|
735
|
+
}
|