@ttsc/paths 0.12.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver/paths.go +269 -217
- package/package.json +1 -1
package/driver/paths.go
CHANGED
|
@@ -1,288 +1,340 @@
|
|
|
1
1
|
package paths
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
9
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
func init() {
|
|
15
|
-
|
|
15
|
+
driver.RegisterPlugin(plugin{})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// plugin implements driver.ProgramPlugin for @ttsc/paths.
|
|
18
19
|
type plugin struct{}
|
|
19
20
|
|
|
21
|
+
// ApplyProgram rewrites tsconfig paths aliases to relative import specifiers
|
|
22
|
+
// across every source file in the program.
|
|
20
23
|
func (plugin) ApplyProgram(prog *driver.Program, _ driver.PluginContext) error {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
rewriter := newRewriter(prog)
|
|
25
|
+
for _, file := range prog.SourceFiles() {
|
|
26
|
+
rewriter.apply(file)
|
|
27
|
+
}
|
|
28
|
+
return nil
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
// rewriter holds the resolved tsconfig paths configuration used to rewrite
|
|
32
|
+
// module specifiers across an entire program.
|
|
28
33
|
type rewriter struct {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
basePath string
|
|
35
|
+
outDir string
|
|
36
|
+
patterns []pathPattern
|
|
37
|
+
rootDir string
|
|
38
|
+
sourceFiles map[string]string // normalized source path → same path (used as a set)
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
// pathPattern is a single tsconfig paths entry with its wildcard pattern and
|
|
42
|
+
// ordered list of substitution targets.
|
|
36
43
|
type pathPattern struct {
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
pattern string
|
|
45
|
+
targets []string
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
// newRewriter builds a rewriter from the program's compiler options.
|
|
49
|
+
// Patterns are sorted by decreasing specificity (longer literal prefix first)
|
|
50
|
+
// so the most-specific match wins on overlapping patterns.
|
|
41
51
|
func newRewriter(prog *driver.Program) *rewriter {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
out := &rewriter{sourceFiles: map[string]string{}}
|
|
53
|
+
if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
options := prog.ParsedConfig.ParsedConfig.CompilerOptions
|
|
57
|
+
out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
|
|
58
|
+
out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
|
|
59
|
+
out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
|
|
60
|
+
files := prog.SourceFiles()
|
|
61
|
+
if out.rootDir == "" {
|
|
62
|
+
out.rootDir = commonSourceDir(files)
|
|
63
|
+
}
|
|
64
|
+
for _, file := range files {
|
|
65
|
+
name := normalizePath(file.FileName())
|
|
66
|
+
out.sourceFiles[name] = name
|
|
67
|
+
out.sourceFiles[stripKnownSourceExtension(name)] = name
|
|
68
|
+
}
|
|
69
|
+
if options.Paths != nil {
|
|
70
|
+
for key, targets := range options.Paths.Entries() {
|
|
71
|
+
out.patterns = append(out.patterns, pathPattern{
|
|
72
|
+
pattern: key,
|
|
73
|
+
targets: append([]string(nil), targets...),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
sort.SliceStable(out.patterns, func(i, j int) bool {
|
|
78
|
+
return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
|
|
79
|
+
})
|
|
80
|
+
return out
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
// apply rewrites all module specifiers in file that match a tsconfig paths pattern.
|
|
73
84
|
func (r *rewriter) apply(file *shimast.SourceFile) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if r == nil || file == nil || len(r.patterns) == 0 {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
|
|
89
|
+
if lit == nil || lit.Kind != shimast.KindStringLiteral {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
spec := lit.Text()
|
|
93
|
+
rewritten, ok := r.rewrite(file.FileName(), spec)
|
|
94
|
+
if ok && rewritten != spec {
|
|
95
|
+
lit.AsStringLiteral().Text = rewritten
|
|
96
|
+
lit.Flags |= shimast.NodeFlagsSynthesized
|
|
97
|
+
lit.Loc = shimcore.UndefinedTextRange()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
// visitModuleSpecifiers recursively walks the AST rooted at node, calling
|
|
103
|
+
// visit for every string-literal module specifier it finds. Covered nodes
|
|
104
|
+
// include import/export declarations, require() calls, dynamic import()
|
|
105
|
+
// expressions, import-equals declarations, and import-type nodes.
|
|
91
106
|
func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
if node == nil {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
switch node.Kind {
|
|
111
|
+
case shimast.KindImportDeclaration:
|
|
112
|
+
visit(node.AsImportDeclaration().ModuleSpecifier)
|
|
113
|
+
case shimast.KindExportDeclaration:
|
|
114
|
+
visit(node.AsExportDeclaration().ModuleSpecifier)
|
|
115
|
+
case shimast.KindImportEqualsDeclaration:
|
|
116
|
+
ref := node.AsImportEqualsDeclaration().ModuleReference
|
|
117
|
+
if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
|
|
118
|
+
visit(ref.AsExternalModuleReference().Expression)
|
|
119
|
+
}
|
|
120
|
+
case shimast.KindImportType:
|
|
121
|
+
arg := node.AsImportTypeNode().Argument
|
|
122
|
+
if arg != nil && arg.Kind == shimast.KindLiteralType {
|
|
123
|
+
visit(arg.AsLiteralTypeNode().Literal)
|
|
124
|
+
}
|
|
125
|
+
case shimast.KindModuleDeclaration:
|
|
126
|
+
decl := node.AsModuleDeclaration()
|
|
127
|
+
if decl != nil {
|
|
128
|
+
visit(decl.Name())
|
|
129
|
+
}
|
|
130
|
+
case shimast.KindCallExpression:
|
|
131
|
+
call := node.AsCallExpression()
|
|
132
|
+
if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
|
|
133
|
+
visit(call.Arguments.Nodes[0])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
137
|
+
visitModuleSpecifiers(child, visit)
|
|
138
|
+
return false
|
|
139
|
+
})
|
|
125
140
|
}
|
|
126
141
|
|
|
142
|
+
// isModuleSpecifierCall reports whether call is a dynamic import() or a
|
|
143
|
+
// CommonJS require() expression.
|
|
127
144
|
func isModuleSpecifierCall(call *shimast.CallExpression) bool {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
if call == nil || call.Expression == nil {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
switch call.Expression.Kind {
|
|
149
|
+
case shimast.KindImportKeyword:
|
|
150
|
+
return true
|
|
151
|
+
case shimast.KindIdentifier:
|
|
152
|
+
return call.Expression.Text() == "require"
|
|
153
|
+
default:
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
139
156
|
}
|
|
140
157
|
|
|
158
|
+
// rewrite resolves specifier from fromSource using the tsconfig paths table and
|
|
159
|
+
// returns the relative output path. Returns (specifier, false) when the specifier
|
|
160
|
+
// is already relative, absolute, or does not match any paths pattern.
|
|
141
161
|
func (r *rewriter) rewrite(fromSource string, specifier string) (string, bool) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
|
|
163
|
+
return specifier, false
|
|
164
|
+
}
|
|
165
|
+
targetSource, ok := r.resolveSource(specifier)
|
|
166
|
+
if !ok {
|
|
167
|
+
return specifier, false
|
|
168
|
+
}
|
|
169
|
+
fromOut := r.outputPathForSource(fromSource)
|
|
170
|
+
targetOut := r.outputPathForSource(targetSource)
|
|
171
|
+
if fromOut == "" || targetOut == "" {
|
|
172
|
+
return specifier, false
|
|
173
|
+
}
|
|
174
|
+
rel, _ := filepath.Rel(filepath.Dir(fromOut), targetOut)
|
|
175
|
+
rel = filepath.ToSlash(rel)
|
|
176
|
+
if !strings.HasPrefix(rel, ".") {
|
|
177
|
+
rel = "./" + rel
|
|
178
|
+
}
|
|
179
|
+
return rel, true
|
|
160
180
|
}
|
|
161
181
|
|
|
182
|
+
// resolveSource finds the source file that a tsconfig paths specifier resolves to.
|
|
183
|
+
// It iterates over sorted patterns and, for each match, tries all substitution
|
|
184
|
+
// targets (with and without known extensions) including index files.
|
|
162
185
|
func (r *rewriter) resolveSource(specifier string) (string, bool) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
for _, pattern := range r.patterns {
|
|
187
|
+
star, ok := matchPattern(pattern.pattern, specifier)
|
|
188
|
+
if !ok {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
for _, target := range pattern.targets {
|
|
192
|
+
candidate := strings.Replace(target, "*", star, 1)
|
|
193
|
+
resolved := normalizePath(filepath.Join(r.basePath, candidate))
|
|
194
|
+
if source, ok := r.lookupSource(resolved); ok {
|
|
195
|
+
return source, true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return "", false
|
|
177
200
|
}
|
|
178
201
|
|
|
202
|
+
// lookupSource checks whether candidate (a normalized path, possibly without
|
|
203
|
+
// extension) corresponds to a known source file. It tries the exact path, the
|
|
204
|
+
// extension-stripped stem, stem with each TS extension, and index files.
|
|
179
205
|
func (r *rewriter) lookupSource(candidate string) (string, bool) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
|
|
207
|
+
return source, true
|
|
208
|
+
}
|
|
209
|
+
stem := stripKnownSourceExtension(normalizePath(candidate))
|
|
210
|
+
if source, ok := r.sourceFiles[stem]; ok {
|
|
211
|
+
return source, true
|
|
212
|
+
}
|
|
213
|
+
for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
|
|
214
|
+
if source, ok := r.sourceFiles[stem+ext]; ok {
|
|
215
|
+
return source, true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
|
|
219
|
+
if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
|
|
220
|
+
return source, true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return "", false
|
|
198
224
|
}
|
|
199
225
|
|
|
226
|
+
// outputPathForSource maps a source file path to its emitted output path under
|
|
227
|
+
// outDir, swapping the source extension for the appropriate JS extension. Returns
|
|
228
|
+
// "" when outDir or rootDir is unset, or when source is outside rootDir.
|
|
200
229
|
func (r *rewriter) outputPathForSource(source string) string {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
if r.outDir == "" || r.rootDir == "" {
|
|
231
|
+
return ""
|
|
232
|
+
}
|
|
233
|
+
rel, err := filepath.Rel(r.rootDir, source)
|
|
234
|
+
if err != nil || isOutsideRelativePath(rel) {
|
|
235
|
+
return ""
|
|
236
|
+
}
|
|
237
|
+
return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
|
|
209
238
|
}
|
|
210
239
|
|
|
240
|
+
// emittedJavaScriptExtension returns the JavaScript file extension that TypeScript
|
|
241
|
+
// emits for a given source path: ".mjs" for ".mts", ".cjs" for ".cts", ".js"
|
|
242
|
+
// for all other extensions.
|
|
211
243
|
func emittedJavaScriptExtension(source string) string {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
switch strings.ToLower(filepath.Ext(source)) {
|
|
245
|
+
case ".mts":
|
|
246
|
+
return ".mjs"
|
|
247
|
+
case ".cts":
|
|
248
|
+
return ".cjs"
|
|
249
|
+
default:
|
|
250
|
+
return ".js"
|
|
251
|
+
}
|
|
220
252
|
}
|
|
221
253
|
|
|
254
|
+
// matchPattern matches specifier against a tsconfig paths pattern (which may
|
|
255
|
+
// contain at most one "*" wildcard). Returns the captured wildcard segment and
|
|
256
|
+
// true on a match, or ("", false) otherwise. Exact patterns are matched with
|
|
257
|
+
// simple equality.
|
|
222
258
|
func matchPattern(pattern string, specifier string) (string, bool) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
259
|
+
if !strings.Contains(pattern, "*") {
|
|
260
|
+
return "", pattern == specifier
|
|
261
|
+
}
|
|
262
|
+
parts := strings.SplitN(pattern, "*", 2)
|
|
263
|
+
if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
|
|
264
|
+
return "", false
|
|
265
|
+
}
|
|
266
|
+
return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
|
|
231
267
|
}
|
|
232
268
|
|
|
269
|
+
// patternRank returns the length of a tsconfig paths pattern after removing its
|
|
270
|
+
// wildcard character. Patterns with a higher rank (longer literal content) are
|
|
271
|
+
// preferred when multiple patterns match the same specifier.
|
|
233
272
|
func patternRank(pattern string) int {
|
|
234
|
-
|
|
273
|
+
return len(strings.ReplaceAll(pattern, "*", ""))
|
|
235
274
|
}
|
|
236
275
|
|
|
276
|
+
// optionalPath resolves value as a path relative to cwd when it is non-empty
|
|
277
|
+
// and not already absolute. Returns "" when value is empty.
|
|
237
278
|
func optionalPath(value string, cwd string) string {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
279
|
+
if value == "" {
|
|
280
|
+
return ""
|
|
281
|
+
}
|
|
282
|
+
if filepath.IsAbs(value) {
|
|
283
|
+
return normalizePath(value)
|
|
284
|
+
}
|
|
285
|
+
return normalizePath(filepath.Join(cwd, value))
|
|
245
286
|
}
|
|
246
287
|
|
|
288
|
+
// commonSourceDir returns the longest common directory prefix of all file paths
|
|
289
|
+
// in files. It is used as rootDir when the tsconfig does not specify one.
|
|
290
|
+
// Returns "" when files is empty.
|
|
247
291
|
func commonSourceDir(files []*shimast.SourceFile) string {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
292
|
+
if len(files) == 0 {
|
|
293
|
+
return ""
|
|
294
|
+
}
|
|
295
|
+
common := normalizePath(filepath.Dir(files[0].FileName()))
|
|
296
|
+
for _, file := range files[1:] {
|
|
297
|
+
dir := normalizePath(filepath.Dir(file.FileName()))
|
|
298
|
+
for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
|
|
299
|
+
next := filepath.Dir(common)
|
|
300
|
+
if next == common {
|
|
301
|
+
return common
|
|
302
|
+
}
|
|
303
|
+
common = normalizePath(next)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return common
|
|
263
307
|
}
|
|
264
308
|
|
|
309
|
+
// normalizePath cleans and converts a file path to forward-slash form.
|
|
265
310
|
func normalizePath(value string) string {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
if value == "" {
|
|
312
|
+
return ""
|
|
313
|
+
}
|
|
314
|
+
return filepath.ToSlash(filepath.Clean(value))
|
|
270
315
|
}
|
|
271
316
|
|
|
317
|
+
// stripKnownSourceExtension removes a recognized TypeScript or JavaScript file
|
|
318
|
+
// extension from value. Declaration extensions (.d.ts, .d.mts, .d.cts) are
|
|
319
|
+
// tried first. Falls back to stripping any extension via filepath.Ext.
|
|
272
320
|
func stripKnownSourceExtension(value string) string {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
321
|
+
lower := strings.ToLower(value)
|
|
322
|
+
for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
|
|
323
|
+
if strings.HasSuffix(lower, ext) {
|
|
324
|
+
return value[:len(value)-len(ext)]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return strings.TrimSuffix(value, filepath.Ext(value))
|
|
280
328
|
}
|
|
281
329
|
|
|
330
|
+
// replaceSourceExtension strips the known source extension from value and
|
|
331
|
+
// appends ext, producing the output file name.
|
|
282
332
|
func replaceSourceExtension(value string, ext string) string {
|
|
283
|
-
|
|
333
|
+
return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
|
|
284
334
|
}
|
|
285
335
|
|
|
336
|
+
// isOutsideRelativePath reports whether a relative path escapes its base
|
|
337
|
+
// directory (i.e. starts with "..").
|
|
286
338
|
func isOutsideRelativePath(rel string) bool {
|
|
287
|
-
|
|
339
|
+
return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
288
340
|
}
|