Skip to content

Commit 0e4dadc

Browse files
committed
Add pnpVFS + tests
1 parent 2a8a705 commit 0e4dadc

File tree

2 files changed

+531
-0
lines changed

2 files changed

+531
-0
lines changed

internal/vfs/pnpvfs/pnpvfs.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package pnpvfs
2+
3+
import (
4+
"archive/zip"
5+
"path"
6+
"strconv"
7+
"strings"
8+
"sync"
9+
"time"
10+
11+
"github.com/microsoft/typescript-go/internal/tspath"
12+
"github.com/microsoft/typescript-go/internal/vfs"
13+
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
14+
)
15+
16+
type pnpFS struct {
17+
fs vfs.FS
18+
cachedZipReadersMap map[string]*zip.ReadCloser
19+
cacheReaderMutex sync.Mutex
20+
}
21+
22+
var _ vfs.FS = (*pnpFS)(nil)
23+
24+
func From(fs vfs.FS) *pnpFS {
25+
pnpFS := &pnpFS{
26+
fs: fs,
27+
cachedZipReadersMap: make(map[string]*zip.ReadCloser),
28+
}
29+
30+
return pnpFS
31+
}
32+
33+
func (pnpFS *pnpFS) DirectoryExists(path string) bool {
34+
path, _, _ = resolveVirtual(path)
35+
36+
if strings.HasSuffix(path, ".zip") {
37+
return pnpFS.fs.FileExists(path)
38+
}
39+
40+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
41+
42+
return fs.DirectoryExists(formattedPath)
43+
}
44+
45+
func (pnpFS *pnpFS) FileExists(path string) bool {
46+
path, _, _ = resolveVirtual(path)
47+
48+
if strings.HasSuffix(path, ".zip") {
49+
return pnpFS.fs.FileExists(path)
50+
}
51+
52+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
53+
return fs.FileExists(formattedPath)
54+
}
55+
56+
func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries {
57+
path, hash, basePath := resolveVirtual(path)
58+
59+
fs, formattedPath, zipPath := getMatchingFS(pnpFS, path)
60+
entries := fs.GetAccessibleEntries(formattedPath)
61+
62+
for i, dir := range entries.Directories {
63+
fullPath := tspath.CombinePaths(zipPath+formattedPath, dir)
64+
entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath)
65+
}
66+
67+
for i, file := range entries.Files {
68+
fullPath := tspath.CombinePaths(zipPath+formattedPath, file)
69+
entries.Files[i] = makeVirtualPath(basePath, hash, fullPath)
70+
}
71+
72+
return entries
73+
}
74+
75+
func (pnpFS *pnpFS) ReadFile(path string) (contents string, ok bool) {
76+
path, _, _ = resolveVirtual(path)
77+
78+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
79+
return fs.ReadFile(formattedPath)
80+
}
81+
82+
func (pnpFS *pnpFS) Chtimes(path string, mtime time.Time, atime time.Time) error {
83+
path, _, _ = resolveVirtual(path)
84+
85+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
86+
return fs.Chtimes(formattedPath, mtime, atime)
87+
}
88+
89+
func (pnpFS *pnpFS) Realpath(path string) string {
90+
path, hash, basePath := resolveVirtual(path)
91+
92+
fs, formattedPath, zipPath := getMatchingFS(pnpFS, path)
93+
fullPath := zipPath + fs.Realpath(formattedPath)
94+
return makeVirtualPath(basePath, hash, fullPath)
95+
}
96+
97+
func (pnpFS *pnpFS) Remove(path string) error {
98+
path, _, _ = resolveVirtual(path)
99+
100+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
101+
return fs.Remove(formattedPath)
102+
}
103+
104+
func (pnpFS *pnpFS) Stat(path string) vfs.FileInfo {
105+
path, _, _ = resolveVirtual(path)
106+
107+
fs, formattedPath, _ := getMatchingFS(pnpFS, path)
108+
return fs.Stat(formattedPath)
109+
}
110+
111+
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
112+
// pnp fs is always case sensitive
113+
return true
114+
}
115+
116+
func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
117+
root, hash, basePath := resolveVirtual(root)
118+
119+
fs, formattedPath, zipPath := getMatchingFS(pnpFS, root)
120+
return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error {
121+
fullPath := zipPath + path
122+
return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err)
123+
}))
124+
}
125+
126+
func (pnpFS *pnpFS) WriteFile(path string, data string, writeByteOrderMark bool) error {
127+
path, _, _ = resolveVirtual(path)
128+
129+
fs, formattedPath, zipPath := getMatchingFS(pnpFS, path)
130+
if zipPath != "" {
131+
panic("cannot write to zip file")
132+
}
133+
134+
return fs.WriteFile(formattedPath, data, writeByteOrderMark)
135+
}
136+
137+
func splitZipPath(path string) (string, string) {
138+
parts := strings.Split(path, ".zip/")
139+
if len(parts) < 2 {
140+
return path, "/"
141+
}
142+
return parts[0] + ".zip", "/" + parts[1]
143+
}
144+
145+
func getMatchingFS(pnpFS *pnpFS, path string) (vfs.FS, string, string) {
146+
if !tspath.IsZipPath(path) {
147+
return pnpFS.fs, path, ""
148+
}
149+
150+
zipPath, internalPath := splitZipPath(path)
151+
152+
zipStat := pnpFS.fs.Stat(zipPath)
153+
if zipStat == nil {
154+
return pnpFS.fs, path, ""
155+
}
156+
157+
var usedReader *zip.ReadCloser
158+
159+
pnpFS.cacheReaderMutex.Lock()
160+
defer pnpFS.cacheReaderMutex.Unlock()
161+
162+
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
163+
if ok {
164+
usedReader = cachedReader
165+
} else {
166+
zipReader, err := zip.OpenReader(zipPath)
167+
if err != nil {
168+
return pnpFS.fs, path, ""
169+
}
170+
171+
usedReader = zipReader
172+
pnpFS.cachedZipReadersMap[zipPath] = usedReader
173+
}
174+
175+
return iovfs.From(usedReader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath
176+
}
177+
178+
// Virtual paths are used to make different paths resolve to the same real file or folder, which is necessary in some cases when PnP is enabled
179+
// See https://yarnpkg.com/advanced/lexicon#virtual-package and https://yarnpkg.com/advanced/pnpapi#resolvevirtual for more details
180+
func resolveVirtual(path string) (realPath string, hash string, basePath string) {
181+
idx := strings.Index(path, "/__virtual__/")
182+
if idx == -1 {
183+
return path, "", ""
184+
}
185+
186+
base := path[:idx]
187+
rest := path[idx+len("/__virtual__/"):]
188+
parts := strings.SplitN(rest, "/", 3)
189+
if len(parts) < 3 {
190+
// Not enough parts to match the pattern, return as is
191+
return path, "", ""
192+
}
193+
hash = parts[0]
194+
subpath := parts[2]
195+
depth, err := strconv.Atoi(parts[1])
196+
if err != nil || depth < 0 {
197+
// Invalid n, return as is
198+
return path, "", ""
199+
}
200+
201+
basePath = path[:idx] + "/__virtual__"
202+
203+
// Apply dirname n times to base
204+
for range depth {
205+
base = tspath.GetDirectoryPath(base)
206+
}
207+
// Join base and subpath
208+
if base == "/" {
209+
return "/" + subpath, hash, basePath
210+
}
211+
212+
return tspath.CombinePaths(base, subpath), hash, basePath
213+
}
214+
215+
func makeVirtualPath(basePath string, hash string, targetPath string) string {
216+
if basePath == "" || hash == "" {
217+
return targetPath
218+
}
219+
220+
relativePath := tspath.GetRelativePathFromDirectory(
221+
tspath.GetDirectoryPath(basePath),
222+
targetPath,
223+
tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true})
224+
225+
segments := strings.Split(relativePath, "/")
226+
227+
depth := 0
228+
for depth < len(segments) && segments[depth] == ".." {
229+
depth++
230+
}
231+
232+
subPath := strings.Join(segments[depth:], "/")
233+
234+
return path.Join(basePath, hash, strconv.Itoa(depth), subPath)
235+
}

0 commit comments

Comments
 (0)