@ttsc/paths 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 +11 -13
- package/go.mod +1 -12
- package/package.json +1 -1
- package/plugin/main.go +7 -9
- package/plugin/paths.go +1 -660
- package/src/index.cjs +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/samchon/ttsc/tree/master/docs)
|
|
10
10
|
[](https://discord.gg/E94XhzrUCZ)
|
|
11
11
|
|
|
12
|
-
`@ttsc/paths` rewrites
|
|
12
|
+
`@ttsc/paths` rewrites source module specifiers that match `compilerOptions.paths` into relative JavaScript paths. TypeScript-Go carries those source AST changes into declaration emit.
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
@@ -26,14 +26,12 @@ Open your project's `tsconfig.json`, then configure `paths`, `rootDir`, `outDir`
|
|
|
26
26
|
"compilerOptions": {
|
|
27
27
|
"paths": {
|
|
28
28
|
"@/*": ["./src/*"],
|
|
29
|
-
"@lib/*": ["./src/modules/*"]
|
|
29
|
+
"@lib/*": ["./src/modules/*"],
|
|
30
30
|
},
|
|
31
31
|
"rootDir": "src",
|
|
32
32
|
"outDir": "dist",
|
|
33
|
-
"plugins": [
|
|
34
|
-
|
|
35
|
-
]
|
|
36
|
-
}
|
|
33
|
+
"plugins": [{ "transform": "@ttsc/paths" }],
|
|
34
|
+
},
|
|
37
35
|
}
|
|
38
36
|
```
|
|
39
37
|
|
|
@@ -43,7 +41,7 @@ Run your normal `ttsc` command:
|
|
|
43
41
|
npx ttsc
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
An
|
|
44
|
+
An import such as `import { value } from "@lib/value"` becomes a relative JavaScript import such as `import { value } from "./modules/value.js"`. Declaration output follows the same source rewrite.
|
|
47
45
|
|
|
48
46
|
## Notes
|
|
49
47
|
|
|
@@ -53,7 +51,7 @@ No separate plugin options are required. `@ttsc/paths` reads the same `compilerO
|
|
|
53
51
|
{
|
|
54
52
|
"compilerOptions": {
|
|
55
53
|
"paths": {
|
|
56
|
-
"@lib/*": ["./src/modules/*"]
|
|
54
|
+
"@lib/*": ["./src/modules/*"],
|
|
57
55
|
},
|
|
58
56
|
"rootDir": "src",
|
|
59
57
|
"outDir": "dist",
|
|
@@ -61,11 +59,11 @@ No separate plugin options are required. `@ttsc/paths` reads the same `compilerO
|
|
|
61
59
|
// Keep lint first.
|
|
62
60
|
{ "transform": "@ttsc/lint", "config": { "no-var": "error" } },
|
|
63
61
|
|
|
64
|
-
//
|
|
65
|
-
{ "transform": "@ttsc/banner", "banner": "
|
|
62
|
+
// First-party utilities use their documented transform order.
|
|
63
|
+
{ "transform": "@ttsc/banner", "banner": "License MIT" },
|
|
66
64
|
{ "transform": "@ttsc/paths" },
|
|
67
|
-
{ "transform": "@ttsc/strip", "calls": ["console.log"] }
|
|
68
|
-
]
|
|
69
|
-
}
|
|
65
|
+
{ "transform": "@ttsc/strip", "calls": ["console.log"] },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
70
68
|
}
|
|
71
69
|
```
|
package/go.mod
CHANGED
|
@@ -2,18 +2,7 @@ module github.com/samchon/ttsc/packages/paths
|
|
|
2
2
|
|
|
3
3
|
go 1.26
|
|
4
4
|
|
|
5
|
-
require
|
|
6
|
-
github.com/microsoft/typescript-go/shim/ast v0.0.0
|
|
7
|
-
github.com/microsoft/typescript-go/shim/bundled v0.0.0
|
|
8
|
-
github.com/microsoft/typescript-go/shim/compiler v0.0.0
|
|
9
|
-
github.com/microsoft/typescript-go/shim/core v0.0.0
|
|
10
|
-
github.com/microsoft/typescript-go/shim/diagnosticwriter v0.0.0
|
|
11
|
-
github.com/microsoft/typescript-go/shim/parser v0.0.0
|
|
12
|
-
github.com/microsoft/typescript-go/shim/tsoptions v0.0.0
|
|
13
|
-
github.com/microsoft/typescript-go/shim/tspath v0.0.0
|
|
14
|
-
github.com/microsoft/typescript-go/shim/vfs/cachedvfs v0.0.0
|
|
15
|
-
github.com/microsoft/typescript-go/shim/vfs/osvfs v0.0.0
|
|
16
|
-
)
|
|
5
|
+
require github.com/samchon/ttsc/packages/ttsc v0.0.0
|
|
17
6
|
|
|
18
7
|
require (
|
|
19
8
|
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
package/package.json
CHANGED
package/plugin/main.go
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// Native sidecar entrypoint for `@ttsc/paths`.
|
|
2
|
-
//
|
|
3
|
-
// The sidecar implements an output-stage transform. It reads emitted JS or
|
|
4
|
-
// declaration text, resolves bare specifiers covered by tsconfig `paths`, and
|
|
5
|
-
// rewrites them to relative runtime paths.
|
|
6
2
|
package main
|
|
7
3
|
|
|
8
4
|
import (
|
|
9
5
|
"fmt"
|
|
10
6
|
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/utility"
|
|
11
9
|
)
|
|
12
10
|
|
|
13
11
|
const version = "0.0.1"
|
|
@@ -18,19 +16,19 @@ func main() {
|
|
|
18
16
|
|
|
19
17
|
func run(args []string) int {
|
|
20
18
|
if len(args) == 0 {
|
|
21
|
-
fmt.Fprintln(os.Stderr, "@ttsc/paths: command required (expected
|
|
19
|
+
fmt.Fprintln(os.Stderr, "@ttsc/paths: command required (expected build|transform|check|version)")
|
|
22
20
|
return 2
|
|
23
21
|
}
|
|
24
22
|
switch args[0] {
|
|
25
23
|
case "-v", "--version", "version":
|
|
26
24
|
fmt.Fprintf(os.Stdout, "@ttsc/paths %s\n", version)
|
|
27
25
|
return 0
|
|
26
|
+
case "build":
|
|
27
|
+
return utility.RunBuild(args[1:])
|
|
28
|
+
case "transform":
|
|
29
|
+
return utility.RunTransform(args[1:])
|
|
28
30
|
case "check":
|
|
29
|
-
// Path rewriting depends on emitted output, so check has no standalone
|
|
30
|
-
// diagnostics beyond command availability.
|
|
31
31
|
return 0
|
|
32
|
-
case "output":
|
|
33
|
-
return RunOutput(args[1:])
|
|
34
32
|
default:
|
|
35
33
|
fmt.Fprintf(os.Stderr, "@ttsc/paths: unknown command %q\n", args[0])
|
|
36
34
|
return 2
|
package/plugin/paths.go
CHANGED
|
@@ -1,662 +1,3 @@
|
|
|
1
|
-
// Output transformer for tsconfig `paths` rewrites.
|
|
2
|
-
//
|
|
3
|
-
// TypeScript's `paths` option is a compile-time resolver hint. Runtime output
|
|
4
|
-
// still contains the original bare specifier unless a bundler handles it.
|
|
5
|
-
// `@ttsc/paths` rewrites those specifiers after emit, using TypeScript-Go's
|
|
6
|
-
// parser and parsed compiler options rather than ad hoc text replacement.
|
|
7
1
|
package main
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
"encoding/json"
|
|
11
|
-
"errors"
|
|
12
|
-
"flag"
|
|
13
|
-
"fmt"
|
|
14
|
-
"os"
|
|
15
|
-
"path/filepath"
|
|
16
|
-
"sort"
|
|
17
|
-
"strings"
|
|
18
|
-
|
|
19
|
-
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
20
|
-
"github.com/microsoft/typescript-go/shim/bundled"
|
|
21
|
-
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
22
|
-
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
23
|
-
shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
|
|
24
|
-
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
25
|
-
"github.com/microsoft/typescript-go/shim/tsoptions"
|
|
26
|
-
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
27
|
-
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
|
|
28
|
-
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
type pluginEntry struct {
|
|
32
|
-
// The host sends every plugin descriptor so ordered sidecars see the same
|
|
33
|
-
// manifest. This plugin needs only the presence of its own descriptor.
|
|
34
|
-
Config map[string]any `json:"config"`
|
|
35
|
-
Name string `json:"name"`
|
|
36
|
-
Stage string `json:"stage"`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type program struct {
|
|
40
|
-
// parsed owns the compilerOptions used for paths/baseUrl/rootDir/outDir.
|
|
41
|
-
cwd string
|
|
42
|
-
parsed *tsoptions.ParsedCommandLine
|
|
43
|
-
tsProgram *shimcompiler.Program
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type pathsResolver struct {
|
|
47
|
-
// basePath is TypeScript's resolved base for paths entries. outDir/rootDir
|
|
48
|
-
// are normalized absolute paths so output-relative specifiers are stable.
|
|
49
|
-
basePath string
|
|
50
|
-
outDir string
|
|
51
|
-
patterns []pathsPattern
|
|
52
|
-
rootDir string
|
|
53
|
-
sourceFiles map[string]string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type pathsPattern struct {
|
|
57
|
-
// pattern is the user-facing key from compilerOptions.paths, targets are the
|
|
58
|
-
// corresponding target patterns in declaration order.
|
|
59
|
-
pattern string
|
|
60
|
-
targets []string
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type textEdit struct {
|
|
64
|
-
start int
|
|
65
|
-
end int
|
|
66
|
-
text string
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func RunOutput(args []string) int {
|
|
70
|
-
fs := flag.NewFlagSet("output", flag.ContinueOnError)
|
|
71
|
-
fs.SetOutput(os.Stderr)
|
|
72
|
-
file := fs.String("file", "", "emitted file to transform")
|
|
73
|
-
out := fs.String("out", "", "write transformed text to this file instead of updating --file")
|
|
74
|
-
cwd := fs.String("cwd", "", "project directory")
|
|
75
|
-
outDir := fs.String("outDir", "", "emit directory override")
|
|
76
|
-
pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
|
|
77
|
-
tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
|
|
78
|
-
if err := fs.Parse(args); err != nil {
|
|
79
|
-
return 2
|
|
80
|
-
}
|
|
81
|
-
if *file == "" {
|
|
82
|
-
fmt.Fprintln(os.Stderr, "@ttsc/paths: output requires --file")
|
|
83
|
-
return 2
|
|
84
|
-
}
|
|
85
|
-
if err := requireConfig(*pluginsJSON); err != nil {
|
|
86
|
-
fmt.Fprintln(os.Stderr, err)
|
|
87
|
-
return 2
|
|
88
|
-
}
|
|
89
|
-
resolvedCwd, err := resolveCwd(*cwd)
|
|
90
|
-
if err != nil {
|
|
91
|
-
fmt.Fprintln(os.Stderr, err)
|
|
92
|
-
return 2
|
|
93
|
-
}
|
|
94
|
-
prog, parseDiags, err := loadProgram(resolvedCwd, *tsconfig, *outDir)
|
|
95
|
-
if err != nil {
|
|
96
|
-
fmt.Fprintf(os.Stderr, "@ttsc/paths: %v\n", err)
|
|
97
|
-
return 2
|
|
98
|
-
}
|
|
99
|
-
if len(parseDiags) > 0 {
|
|
100
|
-
shimdw.FormatASTDiagnosticsWithColorAndContext(os.Stderr, parseDiags, resolvedCwd)
|
|
101
|
-
return 2
|
|
102
|
-
}
|
|
103
|
-
text, err := os.ReadFile(*file)
|
|
104
|
-
if err != nil {
|
|
105
|
-
fmt.Fprintf(os.Stderr, "@ttsc/paths: read %s: %v\n", *file, err)
|
|
106
|
-
return 2
|
|
107
|
-
}
|
|
108
|
-
patched, err := Apply(prog, *file, string(text))
|
|
109
|
-
if err != nil {
|
|
110
|
-
fmt.Fprintln(os.Stderr, err)
|
|
111
|
-
return 2
|
|
112
|
-
}
|
|
113
|
-
target := *file
|
|
114
|
-
if *out != "" {
|
|
115
|
-
target = *out
|
|
116
|
-
}
|
|
117
|
-
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
118
|
-
fmt.Fprintf(os.Stderr, "@ttsc/paths: mkdir: %v\n", err)
|
|
119
|
-
return 2
|
|
120
|
-
}
|
|
121
|
-
if err := os.WriteFile(target, []byte(patched), 0o644); err != nil {
|
|
122
|
-
fmt.Fprintf(os.Stderr, "@ttsc/paths: write %s: %v\n", target, err)
|
|
123
|
-
return 2
|
|
124
|
-
}
|
|
125
|
-
return 0
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
func Apply(prog *program, fileName string, text string) (string, error) {
|
|
129
|
-
// Apply is pure with respect to the file system. The caller provides the
|
|
130
|
-
// already-loaded program and output text; all writes happen in RunOutput.
|
|
131
|
-
resolver := newPathsResolver(prog)
|
|
132
|
-
return resolver.apply(fileName, text)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
func loadProgram(cwd, tsconfigPath string, outDir string) (*program, []*shimast.Diagnostic, error) {
|
|
136
|
-
if !filepath.IsAbs(cwd) {
|
|
137
|
-
abs, err := filepath.Abs(cwd)
|
|
138
|
-
if err != nil {
|
|
139
|
-
return nil, nil, fmt.Errorf("cwd: %w", err)
|
|
140
|
-
}
|
|
141
|
-
cwd = abs
|
|
142
|
-
}
|
|
143
|
-
resolved := tsconfigPath
|
|
144
|
-
if !filepath.IsAbs(resolved) {
|
|
145
|
-
resolved = filepath.Join(cwd, resolved)
|
|
146
|
-
}
|
|
147
|
-
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
148
|
-
host := shimcompiler.NewCompilerHost(cwd, fs, bundled.LibPath(), nil, nil)
|
|
149
|
-
parsed, parseDiags := tsoptions.GetParsedCommandLineOfConfigFile(
|
|
150
|
-
resolved,
|
|
151
|
-
&shimcore.CompilerOptions{},
|
|
152
|
-
nil,
|
|
153
|
-
host,
|
|
154
|
-
nil,
|
|
155
|
-
)
|
|
156
|
-
if parsed == nil {
|
|
157
|
-
return nil, nil, fmt.Errorf("tsoptions: parsed command line was nil for %s", resolved)
|
|
158
|
-
}
|
|
159
|
-
if len(parseDiags) > 0 {
|
|
160
|
-
return nil, parseDiags, nil
|
|
161
|
-
}
|
|
162
|
-
if len(parsed.Errors) > 0 {
|
|
163
|
-
return nil, parsed.Errors, nil
|
|
164
|
-
}
|
|
165
|
-
if outDir != "" {
|
|
166
|
-
// The host may override outDir for runtime/cache builds. The resolver must
|
|
167
|
-
// observe the same effective outDir that produced the file being rewritten.
|
|
168
|
-
overrideOutDir(cwd, parsed, outDir)
|
|
169
|
-
}
|
|
170
|
-
tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
|
|
171
|
-
Config: parsed,
|
|
172
|
-
SingleThreaded: shimcore.TSTrue,
|
|
173
|
-
Host: host,
|
|
174
|
-
UseSourceOfProjectReference: true,
|
|
175
|
-
})
|
|
176
|
-
if tsProgram == nil {
|
|
177
|
-
return nil, nil, errors.New("compiler.NewProgram returned nil")
|
|
178
|
-
}
|
|
179
|
-
return &program{cwd: cwd, parsed: parsed, tsProgram: tsProgram}, nil, nil
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
func (p *program) userSourceFiles() []*shimast.SourceFile {
|
|
183
|
-
out := make([]*shimast.SourceFile, 0)
|
|
184
|
-
for _, f := range p.tsProgram.SourceFiles() {
|
|
185
|
-
if f == nil || f.IsDeclarationFile {
|
|
186
|
-
continue
|
|
187
|
-
}
|
|
188
|
-
out = append(out, f)
|
|
189
|
-
}
|
|
190
|
-
return out
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
func newPathsResolver(prog *program) *pathsResolver {
|
|
194
|
-
resolver := &pathsResolver{sourceFiles: map[string]string{}}
|
|
195
|
-
if prog == nil || prog.parsed == nil || prog.parsed.ParsedConfig == nil || prog.parsed.ParsedConfig.CompilerOptions == nil {
|
|
196
|
-
return resolver
|
|
197
|
-
}
|
|
198
|
-
options := prog.parsed.ParsedConfig.CompilerOptions
|
|
199
|
-
resolver.basePath = options.GetPathsBasePath(prog.cwd)
|
|
200
|
-
resolver.outDir = normalizeOptionalPath(options.OutDir, prog.cwd)
|
|
201
|
-
resolver.rootDir = normalizeOptionalPath(options.RootDir, prog.cwd)
|
|
202
|
-
files := prog.userSourceFiles()
|
|
203
|
-
if resolver.rootDir == "" {
|
|
204
|
-
// TypeScript derives a common source directory when rootDir is absent.
|
|
205
|
-
// Matching that rule keeps rewritten output paths aligned with tsgo emit.
|
|
206
|
-
resolver.rootDir = commonSourceDir(files)
|
|
207
|
-
}
|
|
208
|
-
for _, file := range files {
|
|
209
|
-
if file == nil {
|
|
210
|
-
continue
|
|
211
|
-
}
|
|
212
|
-
name := normalizePath(file.FileName())
|
|
213
|
-
resolver.sourceFiles[name] = name
|
|
214
|
-
resolver.sourceFiles[stripKnownSourceExtension(name)] = name
|
|
215
|
-
}
|
|
216
|
-
if options.Paths != nil {
|
|
217
|
-
for pattern, targets := range options.Paths.Entries() {
|
|
218
|
-
resolver.patterns = append(resolver.patterns, pathsPattern{
|
|
219
|
-
pattern: pattern,
|
|
220
|
-
targets: append([]string(nil), targets...),
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
sort.SliceStable(resolver.patterns, func(i, j int) bool {
|
|
225
|
-
// Specific patterns win over broad wildcard patterns, mirroring the usual
|
|
226
|
-
// TypeScript path matching expectation.
|
|
227
|
-
return pathsPatternRank(resolver.patterns[i].pattern) > pathsPatternRank(resolver.patterns[j].pattern)
|
|
228
|
-
})
|
|
229
|
-
return resolver
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
func (r *pathsResolver) apply(fileName string, text string) (string, error) {
|
|
233
|
-
if r == nil || len(r.patterns) == 0 || !isPathsOutput(fileName) {
|
|
234
|
-
return text, nil
|
|
235
|
-
}
|
|
236
|
-
file := parseModuleSpecifierFile(fileName, text)
|
|
237
|
-
if file == nil {
|
|
238
|
-
return text, nil
|
|
239
|
-
}
|
|
240
|
-
edits := make([]textEdit, 0)
|
|
241
|
-
addEdit := func(lit *shimast.Node) {
|
|
242
|
-
if lit == nil || lit.Kind != shimast.KindStringLiteral {
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
specifier := lit.Text()
|
|
246
|
-
rewritten, ok := r.rewriteSpecifier(fileName, specifier)
|
|
247
|
-
if !ok || rewritten == specifier {
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
start, end, quote, ok := stringLiteralRange(text, lit)
|
|
251
|
-
if !ok {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
edits = append(edits, textEdit{
|
|
255
|
-
start: start,
|
|
256
|
-
end: end,
|
|
257
|
-
text: quoteJSString(quote, rewritten),
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
// Walk only syntactic module-specifier positions. Ordinary string literals
|
|
261
|
-
// are deliberately ignored, even if their text happens to match a path alias.
|
|
262
|
-
var walk func(*shimast.Node)
|
|
263
|
-
walk = func(node *shimast.Node) {
|
|
264
|
-
if node == nil {
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
switch node.Kind {
|
|
268
|
-
case shimast.KindImportDeclaration:
|
|
269
|
-
addEdit(node.AsImportDeclaration().ModuleSpecifier)
|
|
270
|
-
case shimast.KindExportDeclaration:
|
|
271
|
-
addEdit(node.AsExportDeclaration().ModuleSpecifier)
|
|
272
|
-
case shimast.KindImportEqualsDeclaration:
|
|
273
|
-
ref := node.AsImportEqualsDeclaration().ModuleReference
|
|
274
|
-
if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
|
|
275
|
-
addEdit(ref.AsExternalModuleReference().Expression)
|
|
276
|
-
}
|
|
277
|
-
case shimast.KindImportType:
|
|
278
|
-
arg := node.AsImportTypeNode().Argument
|
|
279
|
-
if arg != nil && arg.Kind == shimast.KindLiteralType {
|
|
280
|
-
addEdit(arg.AsLiteralTypeNode().Literal)
|
|
281
|
-
}
|
|
282
|
-
case shimast.KindModuleDeclaration:
|
|
283
|
-
addEdit(node.AsModuleDeclaration().Name())
|
|
284
|
-
case shimast.KindCallExpression:
|
|
285
|
-
call := node.AsCallExpression()
|
|
286
|
-
if call != nil && (isRequireCall(call) || isDynamicImportCall(call)) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
|
|
287
|
-
addEdit(call.Arguments.Nodes[0])
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
node.ForEachChild(func(child *shimast.Node) bool {
|
|
291
|
-
walk(child)
|
|
292
|
-
return false
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
for _, stmt := range file.Statements.Nodes {
|
|
296
|
-
walk(stmt)
|
|
297
|
-
}
|
|
298
|
-
return applyTextEdits(text, edits), nil
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
func (r *pathsResolver) rewriteSpecifier(outputFile string, specifier string) (string, bool) {
|
|
302
|
-
// Relative and absolute specifiers are already runtime-addressable.
|
|
303
|
-
if isExternalModuleNameRelative(specifier) || strings.HasPrefix(specifier, "/") {
|
|
304
|
-
return specifier, false
|
|
305
|
-
}
|
|
306
|
-
for _, pattern := range r.patterns {
|
|
307
|
-
capture, ok := matchPathPattern(pattern.pattern, specifier)
|
|
308
|
-
if !ok || len(pattern.targets) == 0 {
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
311
|
-
for _, targetPattern := range pattern.targets {
|
|
312
|
-
target := strings.ReplaceAll(targetPattern, "*", capture)
|
|
313
|
-
source, ok := r.resolveTargetSource(target)
|
|
314
|
-
if !ok {
|
|
315
|
-
continue
|
|
316
|
-
}
|
|
317
|
-
targetOutput := r.outputPathForSource(source)
|
|
318
|
-
// The emitted file imports another emitted file, not the source file.
|
|
319
|
-
// Relative paths are therefore calculated from outputFile to targetOutput.
|
|
320
|
-
relative, err := filepath.Rel(filepath.Dir(normalizePath(outputFile)), targetOutput)
|
|
321
|
-
if err != nil {
|
|
322
|
-
return specifier, false
|
|
323
|
-
}
|
|
324
|
-
relative = filepath.ToSlash(relative)
|
|
325
|
-
if relative == "." {
|
|
326
|
-
relative = "./" + filepath.Base(targetOutput)
|
|
327
|
-
}
|
|
328
|
-
if !strings.HasPrefix(relative, ".") {
|
|
329
|
-
relative = "./" + relative
|
|
330
|
-
}
|
|
331
|
-
return relative, true
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return specifier, false
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
func (r *pathsResolver) resolveTargetSource(target string) (string, bool) {
|
|
338
|
-
base := r.basePath
|
|
339
|
-
if base == "" {
|
|
340
|
-
base = "."
|
|
341
|
-
}
|
|
342
|
-
raw := normalizePath(filepath.Join(base, target))
|
|
343
|
-
candidates := []string{raw}
|
|
344
|
-
if stripKnownSourceExtension(raw) == raw {
|
|
345
|
-
// Match Node/TypeScript-style extension and index resolution against the
|
|
346
|
-
// program source-file set; no direct filesystem probe is needed.
|
|
347
|
-
for _, ext := range knownResolvableExtensions() {
|
|
348
|
-
candidates = append(candidates, raw+ext)
|
|
349
|
-
}
|
|
350
|
-
for _, ext := range knownResolvableExtensions() {
|
|
351
|
-
candidates = append(candidates, filepath.ToSlash(filepath.Join(raw, "index"+ext)))
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
for _, candidate := range candidates {
|
|
355
|
-
normalized := normalizePath(candidate)
|
|
356
|
-
if source, ok := r.sourceFiles[normalized]; ok {
|
|
357
|
-
return source, true
|
|
358
|
-
}
|
|
359
|
-
if source, ok := r.sourceFiles[stripKnownSourceExtension(normalized)]; ok {
|
|
360
|
-
return source, true
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return "", false
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
func (r *pathsResolver) outputPathForSource(source string) string {
|
|
367
|
-
outputExt := outputExtensionForSource(source)
|
|
368
|
-
if r.outDir == "" {
|
|
369
|
-
return changeExtension(source, outputExt)
|
|
370
|
-
}
|
|
371
|
-
if r.rootDir != "" {
|
|
372
|
-
if rel, err := filepath.Rel(r.rootDir, source); err == nil && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel) {
|
|
373
|
-
return normalizePath(filepath.Join(r.outDir, changeExtension(rel, outputExt)))
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return normalizePath(filepath.Join(r.outDir, filepath.Base(changeExtension(source, outputExt))))
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
func requireConfig(pluginsJSON string) error {
|
|
380
|
-
if strings.TrimSpace(pluginsJSON) == "" {
|
|
381
|
-
return fmt.Errorf("@ttsc/paths: missing --plugins-json")
|
|
382
|
-
}
|
|
383
|
-
var entries []pluginEntry
|
|
384
|
-
if err := json.Unmarshal([]byte(pluginsJSON), &entries); err != nil {
|
|
385
|
-
return fmt.Errorf("@ttsc/paths: invalid --plugins-json: %w", err)
|
|
386
|
-
}
|
|
387
|
-
for _, entry := range entries {
|
|
388
|
-
if entry.Name == "@ttsc/paths" {
|
|
389
|
-
return nil
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return fmt.Errorf("@ttsc/paths: plugin entry not found")
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
func resolveCwd(override string) (string, error) {
|
|
396
|
-
if override != "" {
|
|
397
|
-
abs, err := filepath.Abs(override)
|
|
398
|
-
if err != nil {
|
|
399
|
-
return "", fmt.Errorf("@ttsc/paths: --cwd: %w", err)
|
|
400
|
-
}
|
|
401
|
-
return abs, nil
|
|
402
|
-
}
|
|
403
|
-
wd, err := os.Getwd()
|
|
404
|
-
if err != nil {
|
|
405
|
-
return "", fmt.Errorf("@ttsc/paths: cwd: %w", err)
|
|
406
|
-
}
|
|
407
|
-
return wd, nil
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir string) {
|
|
411
|
-
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
if filepath.IsAbs(outDir) {
|
|
415
|
-
parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(outDir)
|
|
416
|
-
return
|
|
417
|
-
}
|
|
418
|
-
parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(filepath.Join(cwd, outDir))
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
func parseModuleSpecifierFile(fileName string, text string) *shimast.SourceFile {
|
|
422
|
-
normalized := normalizePath(fileName)
|
|
423
|
-
if !filepath.IsAbs(normalized) {
|
|
424
|
-
if abs, err := filepath.Abs(normalized); err == nil {
|
|
425
|
-
normalized = normalizePath(abs)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
opts := shimast.SourceFileParseOptions{FileName: normalized}
|
|
429
|
-
kind := shimcore.ScriptKindJS
|
|
430
|
-
if isDeclarationOutput(fileName) {
|
|
431
|
-
kind = shimcore.ScriptKindTS
|
|
432
|
-
}
|
|
433
|
-
return shimparser.ParseSourceFile(opts, text, kind)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
func isRequireCall(call *shimast.CallExpression) bool {
|
|
437
|
-
if call == nil || call.Expression == nil || call.Expression.Kind != shimast.KindIdentifier {
|
|
438
|
-
return false
|
|
439
|
-
}
|
|
440
|
-
return call.Expression.Text() == "require"
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
func isDynamicImportCall(call *shimast.CallExpression) bool {
|
|
444
|
-
if call == nil || call.Expression == nil || call.Expression.Kind != shimast.KindImportKeyword {
|
|
445
|
-
return false
|
|
446
|
-
}
|
|
447
|
-
return call.Arguments != nil && len(call.Arguments.Nodes) == 1
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
func stringLiteralRange(text string, node *shimast.Node) (int, int, byte, bool) {
|
|
451
|
-
start := clamp(node.Pos(), 0, len(text))
|
|
452
|
-
end := clamp(node.End(), start, len(text))
|
|
453
|
-
for start < end && text[start] != '"' && text[start] != '\'' {
|
|
454
|
-
start++
|
|
455
|
-
}
|
|
456
|
-
if start >= end {
|
|
457
|
-
return 0, 0, 0, false
|
|
458
|
-
}
|
|
459
|
-
quote := text[start]
|
|
460
|
-
for end > start+1 && text[end-1] != quote {
|
|
461
|
-
end--
|
|
462
|
-
}
|
|
463
|
-
if end <= start+1 {
|
|
464
|
-
return 0, 0, 0, false
|
|
465
|
-
}
|
|
466
|
-
return start, end, quote, true
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
func quoteJSString(quote byte, value string) string {
|
|
470
|
-
var b strings.Builder
|
|
471
|
-
b.WriteByte(quote)
|
|
472
|
-
for _, r := range value {
|
|
473
|
-
switch r {
|
|
474
|
-
case '\\':
|
|
475
|
-
b.WriteString(`\\`)
|
|
476
|
-
case '\n':
|
|
477
|
-
b.WriteString(`\n`)
|
|
478
|
-
case '\r':
|
|
479
|
-
b.WriteString(`\r`)
|
|
480
|
-
case '\t':
|
|
481
|
-
b.WriteString(`\t`)
|
|
482
|
-
default:
|
|
483
|
-
if byte(r) == quote && r < utf8RuneSelf {
|
|
484
|
-
b.WriteByte('\\')
|
|
485
|
-
b.WriteByte(byte(r))
|
|
486
|
-
} else {
|
|
487
|
-
b.WriteRune(r)
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
b.WriteByte(quote)
|
|
492
|
-
return b.String()
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const utf8RuneSelf = 0x80
|
|
496
|
-
|
|
497
|
-
func applyTextEdits(text string, edits []textEdit) string {
|
|
498
|
-
if len(edits) == 0 {
|
|
499
|
-
return text
|
|
500
|
-
}
|
|
501
|
-
sort.SliceStable(edits, func(i, j int) bool {
|
|
502
|
-
if edits[i].start == edits[j].start {
|
|
503
|
-
return edits[i].end > edits[j].end
|
|
504
|
-
}
|
|
505
|
-
return edits[i].start > edits[j].start
|
|
506
|
-
})
|
|
507
|
-
out := text
|
|
508
|
-
lastStart := len(text) + 1
|
|
509
|
-
for _, edit := range edits {
|
|
510
|
-
if edit.start < 0 || edit.end < edit.start || edit.start > len(out) {
|
|
511
|
-
continue
|
|
512
|
-
}
|
|
513
|
-
if edit.end > lastStart {
|
|
514
|
-
// Overlapping edits can happen only if TypeScript-Go exposes nested
|
|
515
|
-
// specifier nodes. The outer edit wins by trimming the later range.
|
|
516
|
-
edit.end = lastStart
|
|
517
|
-
}
|
|
518
|
-
if edit.end > len(out) {
|
|
519
|
-
edit.end = len(out)
|
|
520
|
-
}
|
|
521
|
-
out = out[:edit.start] + edit.text + out[edit.end:]
|
|
522
|
-
lastStart = edit.start
|
|
523
|
-
}
|
|
524
|
-
return out
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
func matchPathPattern(pattern string, specifier string) (string, bool) {
|
|
528
|
-
star := strings.Index(pattern, "*")
|
|
529
|
-
if star < 0 {
|
|
530
|
-
return "", pattern == specifier
|
|
531
|
-
}
|
|
532
|
-
prefix := pattern[:star]
|
|
533
|
-
suffix := pattern[star+1:]
|
|
534
|
-
if !strings.HasPrefix(specifier, prefix) || !strings.HasSuffix(specifier, suffix) {
|
|
535
|
-
return "", false
|
|
536
|
-
}
|
|
537
|
-
return specifier[len(prefix) : len(specifier)-len(suffix)], true
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
func commonSourceDir(files []*shimast.SourceFile) string {
|
|
541
|
-
var common string
|
|
542
|
-
for _, file := range files {
|
|
543
|
-
if file == nil {
|
|
544
|
-
continue
|
|
545
|
-
}
|
|
546
|
-
dir := filepath.Dir(normalizePath(file.FileName()))
|
|
547
|
-
if common == "" {
|
|
548
|
-
common = dir
|
|
549
|
-
continue
|
|
550
|
-
}
|
|
551
|
-
common = commonPathPrefix(common, dir)
|
|
552
|
-
}
|
|
553
|
-
return common
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
func commonPathPrefix(a string, b string) string {
|
|
557
|
-
aParts := strings.Split(normalizePath(a), "/")
|
|
558
|
-
bParts := strings.Split(normalizePath(b), "/")
|
|
559
|
-
n := len(aParts)
|
|
560
|
-
if len(bParts) < n {
|
|
561
|
-
n = len(bParts)
|
|
562
|
-
}
|
|
563
|
-
i := 0
|
|
564
|
-
for i < n && aParts[i] == bParts[i] {
|
|
565
|
-
i++
|
|
566
|
-
}
|
|
567
|
-
if i == 0 {
|
|
568
|
-
return ""
|
|
569
|
-
}
|
|
570
|
-
return strings.Join(aParts[:i], "/")
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
func normalizeOptionalPath(value string, cwd string) string {
|
|
574
|
-
if value == "" {
|
|
575
|
-
return ""
|
|
576
|
-
}
|
|
577
|
-
if filepath.IsAbs(value) {
|
|
578
|
-
return normalizePath(value)
|
|
579
|
-
}
|
|
580
|
-
return normalizePath(filepath.Join(cwd, value))
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
func normalizePath(value string) string {
|
|
584
|
-
return filepath.ToSlash(shimtspath.NormalizePath(value))
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
func stripKnownSourceExtension(value string) string {
|
|
588
|
-
for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".tsx", ".ts", ".mts", ".cts", ".jsx", ".js", ".json"} {
|
|
589
|
-
if strings.HasSuffix(value, ext) {
|
|
590
|
-
return strings.TrimSuffix(value, ext)
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return value
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
func outputExtensionForSource(source string) string {
|
|
597
|
-
switch strings.ToLower(filepath.Ext(source)) {
|
|
598
|
-
case ".mts":
|
|
599
|
-
return ".mjs"
|
|
600
|
-
case ".cts":
|
|
601
|
-
return ".cjs"
|
|
602
|
-
case ".json":
|
|
603
|
-
return ".json"
|
|
604
|
-
default:
|
|
605
|
-
return ".js"
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
func changeExtension(value string, ext string) string {
|
|
610
|
-
return strings.TrimSuffix(value, filepath.Ext(value)) + ext
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
func isExternalModuleNameRelative(specifier string) bool {
|
|
614
|
-
return strings.HasPrefix(specifier, "./") ||
|
|
615
|
-
strings.HasPrefix(specifier, "../") ||
|
|
616
|
-
specifier == "." ||
|
|
617
|
-
specifier == ".."
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
func isPathsOutput(fileName string) bool {
|
|
621
|
-
return isJavaScriptOutput(fileName) || isDeclarationOutput(fileName)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
func isJavaScriptOutput(fileName string) bool {
|
|
625
|
-
switch strings.ToLower(filepath.Ext(fileName)) {
|
|
626
|
-
case ".js", ".mjs", ".cjs":
|
|
627
|
-
return true
|
|
628
|
-
default:
|
|
629
|
-
return false
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
func isDeclarationOutput(fileName string) bool {
|
|
634
|
-
lower := strings.ToLower(fileName)
|
|
635
|
-
return strings.HasSuffix(lower, ".d.ts") ||
|
|
636
|
-
strings.HasSuffix(lower, ".d.mts") ||
|
|
637
|
-
strings.HasSuffix(lower, ".d.cts")
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
func knownResolvableExtensions() []string {
|
|
641
|
-
return []string{".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".json"}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
func pathsPatternRank(pattern string) int {
|
|
645
|
-
star := strings.Index(pattern, "*")
|
|
646
|
-
if star < 0 {
|
|
647
|
-
return 1_000_000 + len(pattern)
|
|
648
|
-
}
|
|
649
|
-
prefix := len(pattern[:star])
|
|
650
|
-
suffix := len(pattern[star+1:])
|
|
651
|
-
return prefix*1_000 + suffix*10 + len(pattern)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
func clamp(value int, min int, max int) int {
|
|
655
|
-
if value < min {
|
|
656
|
-
return min
|
|
657
|
-
}
|
|
658
|
-
if value > max {
|
|
659
|
-
return max
|
|
660
|
-
}
|
|
661
|
-
return value
|
|
662
|
-
}
|
|
3
|
+
// Paths transform logic lives in github.com/samchon/ttsc/packages/ttsc/utility.
|