Skip to content

Commit f49da2a

Browse files
committed
test(lint): cover file URL config imports
1 parent 7973dfd commit f49da2a

2 files changed

Lines changed: 94 additions & 17 deletions

File tree

packages/lint/linthost/config.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,30 +1242,48 @@ func isConfigObject(value any) bool {
12421242
return ok
12431243
}
12441244

1245-
// relativeImportSpecifier computes the ESM import specifier for `location`
1246-
// relative to `fromDir`. The result always starts with "./" or "../" so it is
1247-
// treated as a relative path by the ESM loader rather than as a bare package
1248-
// name.
1249-
func relativeImportSpecifier(fromDir, location string) (string, error) {
1250-
relative, err := filepath.Rel(fromDir, location)
1251-
if err != nil {
1252-
return "", fmt.Errorf("@ttsc/lint: resolve relative config import %s: %w", location, err)
1253-
}
1254-
relative = filepath.ToSlash(relative)
1255-
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
1256-
return relative, nil
1257-
}
1258-
return "./" + relative, nil
1259-
}
1260-
12611245
func fileURL(location string) string {
1246+
volume := filepath.VolumeName(location)
1247+
if volume != "" {
1248+
volumePath := filepath.ToSlash(volume)
1249+
rest := strings.TrimPrefix(filepath.ToSlash(location[len(volume):]), "/")
1250+
if strings.HasPrefix(volumePath, "//?/UNC") {
1251+
return uncFileURL(rest)
1252+
}
1253+
if strings.HasPrefix(volumePath, "//?/") {
1254+
pathname := "/" + strings.TrimPrefix(volumePath, "//?/")
1255+
if rest != "" {
1256+
pathname += "/" + rest
1257+
}
1258+
return (&url.URL{Scheme: "file", Path: pathname}).String()
1259+
}
1260+
if strings.HasPrefix(volumePath, "//") {
1261+
return uncFileURL(strings.TrimPrefix(volumePath, "//") + "/" + rest)
1262+
}
1263+
}
12621264
pathname := filepath.ToSlash(location)
1263-
if filepath.VolumeName(location) != "" && !strings.HasPrefix(pathname, "/") {
1265+
if volume != "" && !strings.HasPrefix(pathname, "/") {
12641266
pathname = "/" + pathname
12651267
}
12661268
return (&url.URL{Scheme: "file", Path: pathname}).String()
12671269
}
12681270

1271+
func uncFileURL(pathname string) string {
1272+
server, remainder, ok := strings.Cut(pathname, "/")
1273+
if !ok || server == "" {
1274+
return (&url.URL{Scheme: "file", Path: "/" + strings.TrimPrefix(pathname, "/")}).String()
1275+
}
1276+
share, tail, _ := strings.Cut(remainder, "/")
1277+
if share == "" {
1278+
return (&url.URL{Scheme: "file", Path: "/" + strings.TrimPrefix(pathname, "/")}).String()
1279+
}
1280+
urlPath := "/" + share
1281+
if tail != "" {
1282+
urlPath += "/" + tail
1283+
}
1284+
return (&url.URL{Scheme: "file", Host: server, Path: urlPath}).String()
1285+
}
1286+
12691287
// typeScriptConfigLoaderSource returns the TypeScript source of the ephemeral
12701288
// loader script that ttsx executes to evaluate a TypeScript lint config file.
12711289
// `importLiteral` is a JSON-encoded file URL (produced by json.Marshal). It is
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package linthost
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
)
7+
8+
// TestFileURLMatchesNodeShape pins the file URL strings generated for the
9+
// ephemeral TypeScript config loader. Node consumes these with dynamic import,
10+
// so the Go side must match Node's pathToFileURL shape for Windows drive and
11+
// UNC paths, including characters that require URL escaping.
12+
func TestFileURLMatchesNodeShape(t *testing.T) {
13+
type testCase struct {
14+
name string
15+
location string
16+
want string
17+
}
18+
19+
cases := []testCase{
20+
{
21+
name: "posix escapes path characters",
22+
location: "/tmp/a b/#lint%.config.ts",
23+
want: "file:///tmp/a%20b/%23lint%25.config.ts",
24+
},
25+
}
26+
27+
if runtime.GOOS == "windows" {
28+
cases = append(cases,
29+
testCase{
30+
name: "windows drive path",
31+
location: `C:\a b\#lint%.config.ts`,
32+
want: "file:///C:/a%20b/%23lint%25.config.ts",
33+
},
34+
testCase{
35+
name: "windows unc path",
36+
location: `\\server\share\a b\lint.config.ts`,
37+
want: "file://server/share/a%20b/lint.config.ts",
38+
},
39+
testCase{
40+
name: "windows extended drive path",
41+
location: `\\?\C:\a b\lint.config.ts`,
42+
want: "file:///C:/a%20b/lint.config.ts",
43+
},
44+
testCase{
45+
name: "windows extended unc path",
46+
location: `\\?\UNC\server\share\a b\lint.config.ts`,
47+
want: "file://server/share/a%20b/lint.config.ts",
48+
},
49+
)
50+
}
51+
52+
for _, c := range cases {
53+
t.Run(c.name, func(t *testing.T) {
54+
if got := fileURL(c.location); got != c.want {
55+
t.Fatalf("fileURL(%q) = %q, want %q", c.location, got, c.want)
56+
}
57+
})
58+
}
59+
}

0 commit comments

Comments
 (0)