From b603e6518072dff6bc793a7c83d0afe7810cf2b3 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 8 Aug 2025 17:01:58 +0200 Subject: [PATCH 01/48] Implement yarn pnp api --- internal/pnp/manifestparser.go | 235 ++++++++++++++++++++++++++++++++ internal/pnp/pnp.go | 49 +++++++ internal/pnp/pnpapi.go | 238 +++++++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 internal/pnp/manifestparser.go create mode 100644 internal/pnp/pnp.go create mode 100644 internal/pnp/pnpapi.go diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go new file mode 100644 index 0000000000..65178ca4be --- /dev/null +++ b/internal/pnp/manifestparser.go @@ -0,0 +1,235 @@ +package pnp + +import ( + "encoding/json" + "fmt" + "os" + "path" + + "github.com/dlclark/regexp2" +) + +type LinkType string + +const ( + LinkTypeSoft LinkType = "SOFT" + LinkTypeHard LinkType = "HARD" +) + +type PackageDependency struct { + Ident string + Reference string // Either the direct reference or alias reference + AliasName string // Empty if not an alias +} + +func (pd PackageDependency) IsAlias() bool { + return pd.AliasName != "" +} + +type PackageInfo struct { + PackageLocation string `json:"packageLocation"` + PackageDependencies []PackageDependency `json:"packageDependencies,omitempty"` + LinkType LinkType `json:"linkType,omitempty"` + DiscardFromLookup bool `json:"discardFromLookup,omitempty"` + PackagePeers []string `json:"packagePeers,omitempty"` +} + +type Locator struct { + Name string `json:"name"` + Reference string `json:"reference"` +} + +type FallbackExclusion struct { + Name string `json:"name"` + Entries []string `json:"entries"` +} + +type PnpManifestData struct { + dirPath string + + ignorePatternData *regexp2.Regexp + enableTopLevelFallback bool + + fallbackPool [][2]string + fallbackExclusionMap map[string]*FallbackExclusion + + dependencyTreeRoots []Locator + + // Nested maps for package registry (ident -> reference -> PackageInfo) + packageRegistryMap map[string]map[string]*PackageInfo +} + +func parseManifestFromPath(manifestPath string) (*PnpManifestData, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("failed to read .pnp.data.json file: %w", err) + } + + var rawData map[string]interface{} + if err := json.Unmarshal(data, &rawData); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + pnpData, err := parsePnpManifest(rawData, manifestPath) + if err != nil { + return nil, fmt.Errorf("failed to parse PnP data: %w", err) + } + + return pnpData, nil +} + +// TODO add error handling for corrupted data +func parsePnpManifest(rawData map[string]interface{}, manifestPath string) (*PnpManifestData, error) { + data := &PnpManifestData{dirPath: path.Dir(manifestPath)} + + if roots, ok := rawData["dependencyTreeRoots"].([]interface{}); ok { + for _, root := range roots { + if rootMap, ok := root.(map[string]interface{}); ok { + data.dependencyTreeRoots = append(data.dependencyTreeRoots, Locator{ + Name: getField(rootMap, "name", parseString), + Reference: getField(rootMap, "reference", parseString), + }) + } + } + } + + ignorePatternData := getField(rawData, "ignorePatternData", parseString) + if ignorePatternData != "" { + data.ignorePatternData = regexp2.MustCompile(ignorePatternData, regexp2.None) + } + + data.enableTopLevelFallback = getField(rawData, "enableTopLevelFallback", parseBool) + + data.fallbackPool = getField(rawData, "fallbackPool", parseStringPairs) + + data.fallbackExclusionMap = make(map[string]*FallbackExclusion) + + if exclusions, ok := rawData["fallbackExclusionList"].([]interface{}); ok { + for _, exclusion := range exclusions { + if exclusionMap, ok := exclusion.(map[string]interface{}); ok { + exclusionEntry := &FallbackExclusion{ + Name: getField(exclusionMap, "name", parseString), + Entries: getField(exclusionMap, "entries", parseStringArray), + } + data.fallbackExclusionMap[exclusionEntry.Name] = exclusionEntry + } + } + } + + data.packageRegistryMap = make(map[string]map[string]*PackageInfo) + + if registryData, ok := rawData["packageRegistryData"].([]interface{}); ok { + for _, entry := range registryData { + if entryArr, ok := entry.([]interface{}); ok && len(entryArr) == 2 { + packageName := parseString(entryArr[0]) + + if data.packageRegistryMap[packageName] == nil { + data.packageRegistryMap[packageName] = make(map[string]*PackageInfo) + } + + if versions, ok := entryArr[1].([]interface{}); ok { + for _, version := range versions { + if versionArr, ok := version.([]interface{}); ok && len(versionArr) == 2 { + versionStr := parseString(versionArr[0]) + + if infoMap, ok := versionArr[1].(map[string]interface{}); ok { + packageInfo := &PackageInfo{ + PackageLocation: getField(infoMap, "packageLocation", parseString), + PackageDependencies: getField(infoMap, "packageDependencies", parsePackageDependencies), + LinkType: LinkType(getField(infoMap, "linkType", parseString)), + DiscardFromLookup: getField(infoMap, "discardFromLookup", parseBool), + PackagePeers: getField(infoMap, "packagePeers", parseStringArray), + } + + data.packageRegistryMap[packageName][versionStr] = packageInfo + } + } + } + } + } + } + } + + return data, nil +} + +// Helper functions for parsing JSON values - following patterns from tsoptions.parseString, etc. +func parseString(value interface{}) string { + if str, ok := value.(string); ok { + return str + } + return "" +} + +func parseBool(value interface{}) bool { + if val, ok := value.(bool); ok { + return val + } + return false +} + +func parseStringArray(value interface{}) []string { + if arr, ok := value.([]interface{}); ok { + if arr == nil { + return nil + } + result := make([]string, 0, len(arr)) + for _, v := range arr { + if str, ok := v.(string); ok { + result = append(result, str) + } + } + return result + } + return nil +} + +func parseStringPairs(value interface{}) [][2]string { + var result [][2]string + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + result = append(result, [2]string{ + parseString(pair[0]), + parseString(pair[1]), + }) + } + } + } + return result +} + +func parsePackageDependencies(value interface{}) []PackageDependency { + var result []PackageDependency + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + ident := parseString(pair[0]) + + // Check if second element is string (simple reference) or array (alias) + if str, ok := pair[1].(string); ok { + result = append(result, PackageDependency{ + Ident: ident, + Reference: str, + AliasName: "", + }) + } else if aliasPair, ok := pair[1].([]interface{}); ok && len(aliasPair) == 2 { + result = append(result, PackageDependency{ + Ident: ident, + Reference: parseString(aliasPair[1]), + AliasName: parseString(aliasPair[0]), + }) + } + } + } + } + return result +} + +func getField[T any](m map[string]interface{}, key string, parser func(interface{}) T) T { + if val, exists := m[key]; exists { + return parser(val) + } + var zero T + return zero +} diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go new file mode 100644 index 0000000000..f0b3841bde --- /dev/null +++ b/internal/pnp/pnp.go @@ -0,0 +1,49 @@ +package pnp + +import ( + "sync" + "sync/atomic" +) + +var ( + isPnpApiInitialized atomic.Uint32 + cachedPnpApi *PnpApi + pnpMu sync.Mutex +) + +// Clears the singleton PnP API cache +func ClearPnpCache() { + pnpMu.Lock() + defer pnpMu.Unlock() + cachedPnpApi = nil + isPnpApiInitialized.Store(0) +} + +// GetPnpApi returns the PnP API for the given file path. Will return nil if the PnP API is not available. +func GetPnpApi(filePath string) *PnpApi { + // Check if PnP API is already initialized using atomic read (no lock needed) + if isPnpApiInitialized.Load() == 1 { + return cachedPnpApi + } + + pnpMu.Lock() + defer pnpMu.Unlock() + // Double-check after acquiring lock + if isPnpApiInitialized.Load() == 1 { + return cachedPnpApi + } + + pnpApi := &PnpApi{url: filePath} + + manifestData, err := pnpApi.findClosestPnpManifest() + if err == nil { + pnpApi.manifest = manifestData + cachedPnpApi = pnpApi + } else { + // Couldn't load PnP API + cachedPnpApi = nil + } + + isPnpApiInitialized.Store(1) + return cachedPnpApi +} diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go new file mode 100644 index 0000000000..f1e94b358d --- /dev/null +++ b/internal/pnp/pnpapi.go @@ -0,0 +1,238 @@ +package pnp + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" +) + +type PnpApi struct { + url string + manifest *PnpManifestData +} + +func (p *PnpApi) RefreshManifest() error { + var newData *PnpManifestData + var err error + + if p.manifest == nil { + newData, err = p.findClosestPnpManifest() + } else { + newData, err = parseManifestFromPath(p.manifest.dirPath) + } + + if err != nil { + return err + } + + p.manifest = newData + return nil +} + +func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (string, error) { + if p.manifest == nil { + panic(fmt.Errorf("ResolveToUnqualified called with no PnP manifest available")) + } + + ident, modulePath, err := p.ParseBareIdentifier(specifier) + if err != nil { + // Skipping resolution + return "", nil + } + + parentLocator, err := p.FindLocator(parentPath) + if err != nil || parentLocator == nil { + // Skipping resolution + return "", nil + } + + parentPkg := p.GetPackage(*parentLocator) + + var referenceOrAlias *PackageDependency + for _, dep := range parentPkg.PackageDependencies { + if dep.Ident == ident { + referenceOrAlias = &dep + break + } + } + + // If not found, try fallback if enabled + if referenceOrAlias == nil { + if p.manifest.enableTopLevelFallback { + excluded := false + if exclusion, ok := p.manifest.fallbackExclusionMap[parentLocator.Name]; ok { + for _, entry := range exclusion.Entries { + if entry == parentLocator.Reference { + excluded = true + break + } + } + } + if !excluded { + fallback := p.ResolveViaFallback(ident) + if fallback != nil { + referenceOrAlias = fallback + } + } + } + } + + // undeclared dependency + if referenceOrAlias == nil { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + // unfulfilled peer dependency + if !referenceOrAlias.IsAlias() && referenceOrAlias.Reference == "" { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s (a peer dependency) but it isn't provided by its ancestors/your application; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + var dependencyPkg *PackageInfo + if referenceOrAlias.IsAlias() { + dependencyPkg = p.GetPackage(Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference}) + } else { + dependencyPkg = p.GetPackage(Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) + } + + return filepath.Join(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil +} + +func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { + directoryPath := p.url + + for { + pnpPath := path.Join(directoryPath, ".pnp.cjs") + if _, err := os.Stat(pnpPath); err == nil { + manifestPath := path.Join(directoryPath, ".pnp.data.json") + return parseManifestFromPath(manifestPath) + } + + directoryPath = path.Dir(directoryPath) + if directoryPath == "/" { + return nil, fmt.Errorf("no PnP manifest found") + } + } +} + +func (p *PnpApi) GetPackage(locator Locator) *PackageInfo { + packageRegistryMap := p.manifest.packageRegistryMap + packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference] + if !ok { + panic(fmt.Sprintf("%s should have an entry in the package registry", locator.Name)) + } + + return packageInfo +} + +func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { + var bestLength int + var bestLocator *Locator + + relativePath, err := filepath.Rel(p.manifest.dirPath, parentPath) + if err != nil { + return nil, err + } + + if p.manifest.ignorePatternData != nil { + match, err := p.manifest.ignorePatternData.MatchString(relativePath) + if err != nil { + return nil, err + } + + if match { + return nil, nil + } + } + + var relativePathWithDot string + if strings.HasPrefix(relativePath, "../") { + relativePathWithDot = relativePath + } else { + relativePathWithDot = "./" + relativePath + } + + for name, referenceMap := range p.manifest.packageRegistryMap { + for reference, packageInfo := range referenceMap { + if packageInfo.DiscardFromLookup { + continue + } + + if len(packageInfo.PackageLocation) <= bestLength { + continue + } + + if strings.HasPrefix(relativePathWithDot, packageInfo.PackageLocation) { + bestLength = len(packageInfo.PackageLocation) + bestLocator = &Locator{Name: name, Reference: reference} + } + } + } + + return bestLocator, nil +} + +func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { + topLevelPkg := p.GetPackage(Locator{Name: "", Reference: ""}) + + if topLevelPkg != nil { + for _, dep := range topLevelPkg.PackageDependencies { + if dep.Ident == name { + return &dep + } + } + } + + for _, dep := range p.manifest.fallbackPool { + if dep[0] == name { + return &PackageDependency{ + Ident: dep[0], + Reference: dep[1], + AliasName: "", + } + } + } + + return nil +} + +func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath string, err error) { + if len(specifier) == 0 { + return "", "", fmt.Errorf("Empty specifier: %s", specifier) + } + + firstSlash := strings.Index(specifier, "/") + + if specifier[0] == '@' { + if firstSlash == -1 { + return "", "", fmt.Errorf("Invalid specifier: %s", specifier) + } + + secondSlash := strings.Index(specifier[firstSlash+1:], "/") + + if secondSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash+1+secondSlash] + } + } else { + firstSlash := strings.Index(specifier, "/") + + if firstSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash] + } + } + + modulePath = specifier[len(ident):] + + return ident, modulePath, nil +} From a68a9773f8ce4d865015fdf1d1f9b494681464f5 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 8 Aug 2025 17:02:18 +0200 Subject: [PATCH 02/48] Create zipvfs + tests --- internal/vfs/zipvfs/zipvfs.go | 112 +++++++++++++++++ internal/vfs/zipvfs/zipvfs_test.go | 189 +++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 internal/vfs/zipvfs/zipvfs.go create mode 100644 internal/vfs/zipvfs/zipvfs_test.go diff --git a/internal/vfs/zipvfs/zipvfs.go b/internal/vfs/zipvfs/zipvfs.go new file mode 100644 index 0000000000..d49bdb3e23 --- /dev/null +++ b/internal/vfs/zipvfs/zipvfs.go @@ -0,0 +1,112 @@ +package zipvfs + +import ( + "archive/zip" + "path/filepath" + "strings" + + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/iovfs" +) + +type FS struct { + fs vfs.FS +} + +var _ vfs.FS = (*FS)(nil) + +func From(fs vfs.FS) *FS { + fsys := &FS{fs: fs} + return fsys +} + +func (fsys *FS) DirectoryExists(path string) bool { + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.DirectoryExists(formattedPath) +} + +func (fsys *FS) FileExists(path string) bool { + if strings.HasSuffix(path, ".zip") { + return fsys.fs.FileExists(path) + } + + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.FileExists(formattedPath) +} + +func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries { + fs, formattedPath, zipPath := getMatchingFS(fsys, path) + entries := fs.GetAccessibleEntries(formattedPath) + + for i, dir := range entries.Directories { + entries.Directories[i] = filepath.Join(zipPath, dir) + } + + for i, file := range entries.Files { + entries.Files[i] = filepath.Join(zipPath, file) + } + + return entries +} + +func (fsys *FS) ReadFile(path string) (contents string, ok bool) { + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.ReadFile(formattedPath) +} + +func (fsys *FS) Realpath(path string) string { + fs, formattedPath, zipPath := getMatchingFS(fsys, path) + return filepath.Join(zipPath, fs.Realpath(formattedPath)) +} + +func (fsys *FS) Remove(path string) error { + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.Remove(formattedPath) +} + +func (fsys *FS) Stat(path string) vfs.FileInfo { + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.Stat(formattedPath) +} + +func (fsys *FS) UseCaseSensitiveFileNames() bool { + return fsys.fs.UseCaseSensitiveFileNames() +} + +func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + fs, formattedPath, zipPath := getMatchingFS(fsys, root) + return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { + return walkFn(filepath.Join(zipPath, path), d, err) + })) +} + +func (fsys *FS) WriteFile(path string, data string, writeByteOrderMark bool) error { + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.WriteFile(formattedPath, data, writeByteOrderMark) +} + +func isZipPath(path string) bool { + return strings.Contains(path, ".zip/") || strings.HasSuffix(path, ".zip") +} + +func splitZipPath(path string) (string, string) { + parts := strings.Split(path, ".zip/") + if len(parts) < 2 { + return path, "/" + } + return parts[0] + ".zip", "/" + parts[1] +} + +func getMatchingFS(fsys *FS, path string) (vfs.FS, string, string) { + if !isZipPath(path) { + return fsys.fs, path, "" + } + + zipPath, internalPath := splitZipPath(path) + zipReader, err := zip.OpenReader(zipPath) + if err != nil { + return fsys.fs, path, "" + } + + return iovfs.From(zipReader, fsys.fs.UseCaseSensitiveFileNames()), internalPath, zipPath +} diff --git a/internal/vfs/zipvfs/zipvfs_test.go b/internal/vfs/zipvfs/zipvfs_test.go new file mode 100644 index 0000000000..82bdffc835 --- /dev/null +++ b/internal/vfs/zipvfs/zipvfs_test.go @@ -0,0 +1,189 @@ +package zipvfs_test + +import ( + "archive/zip" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "github.com/microsoft/typescript-go/internal/vfs/zipvfs" + "gotest.tools/v3/assert" +) + +// TODO: refine generated tests + +func createTestZip(t *testing.T, files map[string]string) string { + t.Helper() + + tmpDir := t.TempDir() + zipPath := filepath.Join(tmpDir, "test.zip") + + file, err := os.Create(zipPath) + assert.NilError(t, err) + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + for name, content := range files { + f, err := w.Create(name) + assert.NilError(t, err) + _, err = f.Write([]byte(content)) + assert.NilError(t, err) + } + + return zipPath +} + +func TestZipVFS_BasicFileOperations(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/src/index.ts": "export const hello = 'world';", + "/project/package.json": `{"name": "test"}`, + }, true) + + fs := zipvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/src/index.ts")) + assert.Assert(t, !fs.FileExists("/project/nonexistent.ts")) + + content, ok := fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world';", content) + + assert.Assert(t, fs.DirectoryExists("/project/src")) + assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) +} + +func TestZipVFS_ZipFileDetection(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "package.json": `{"name": "test-project"}`, + } + + zipPath := createTestZip(t, zipFiles) + + underlyingFS := vfstest.FromMap(map[string]string{ + zipPath: "zip content placeholder", + }, true) + + fs := zipvfs.From(underlyingFS) + + fmt.Println(zipPath) + assert.Assert(t, fs.FileExists(zipPath)) + + zipInternalPath := zipPath + "/src/index.ts" + + _ = fs.FileExists(zipInternalPath) + _, _ = fs.ReadFile(zipInternalPath) +} + +func TestZipVFS_ErrorHandling(t *testing.T) { + t.Parallel() + + fs := zipvfs.From(osvfs.FS()) + + t.Run("NonexistentZipFile", func(t *testing.T) { + result := fs.FileExists("/nonexistent/path/archive.zip/file.txt") + assert.Assert(t, !result) + + _, ok := fs.ReadFile("/nonexistent/archive.zip/file.txt") + assert.Assert(t, !ok) + }) + + t.Run("InvalidZipFile", func(t *testing.T) { + tmpDir := t.TempDir() + fakePath := filepath.Join(tmpDir, "fake.zip") + err := os.WriteFile(fakePath, []byte("not a zip file"), 0o644) + assert.NilError(t, err) + result := fs.FileExists(fakePath + "/file.txt") + assert.Assert(t, !result) + }) +} + +func TestZipVFS_CaseSensitivity(t *testing.T) { + t.Parallel() + + sensitiveFS := zipvfs.From(vfstest.FromMap(map[string]string{}, true)) + assert.Assert(t, sensitiveFS.UseCaseSensitiveFileNames()) + insensitiveFS := zipvfs.From(vfstest.FromMap(map[string]string{}, false)) + assert.Assert(t, !insensitiveFS.UseCaseSensitiveFileNames()) +} + +func TestZipVFS_FallbackToRegularFiles(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + regularFile := filepath.Join(tmpDir, "regular.ts") + err := os.WriteFile(regularFile, []byte("regular content"), 0o644) + assert.NilError(t, err) + + fs := zipvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(regularFile)) + + content, ok := fs.ReadFile(regularFile) + assert.Assert(t, ok) + assert.Equal(t, "regular content", content) + assert.Assert(t, fs.DirectoryExists(tmpDir)) +} + +func TestZipPath_Detection(t *testing.T) { + t.Parallel() + + testCases := []struct { + path string + shouldBeZip bool + }{ + {"/normal/path/file.txt", false}, + {"/path/to/archive.zip", true}, + {"/path/to/archive.zip/internal/file.txt", true}, + {"/path/archive.zip/nested/dir/file.ts", true}, + {"/path/file.zip.txt", false}, + {"/absolute/archive.zip", true}, + {"/absolute/archive.zip/file.txt", true}, + } + + fs := zipvfs.From(vfstest.FromMap(map[string]string{}, true)) + + for _, tc := range testCases { + t.Run(tc.path, func(t *testing.T) { + _ = fs.FileExists(tc.path) + _, _ = fs.ReadFile(tc.path) + }) + } +} + +func TestZipVFS_RealZipIntegration(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "src/utils/helpers.ts": "export function add(a: number, b: number) { return a + b; }", + "package.json": `{"name": "test-project", "version": "1.0.0"}`, + "tsconfig.json": `{"compilerOptions": {"target": "es2020"}}`, + } + + zipPath := createTestZip(t, zipFiles) + fs := zipvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(zipPath)) + + indexPath := zipPath + "/src/index.ts" + packagePath := zipPath + "/package.json" + _ = fs.FileExists(indexPath) + _ = fs.FileExists(packagePath) + _ = fs.DirectoryExists(zipPath + "/src") + + _, _ = fs.ReadFile(indexPath) + _, _ = fs.ReadFile(packagePath) + + _ = fs.GetAccessibleEntries(zipPath) + _ = fs.GetAccessibleEntries(zipPath + "/src") + _ = fs.Realpath(indexPath) +} From 7ab707a059fc0bac8a0812566d61819268f594a3 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 8 Aug 2025 17:15:05 +0200 Subject: [PATCH 03/48] Wrap all vfs to handle zip --- cmd/tsgo/lsp.go | 3 ++- cmd/tsgo/sys.go | 3 ++- internal/api/server.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 35c973a3a0..5b67939797 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/pprof" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/zipvfs" ) func runLSP(args []string) int { @@ -37,7 +38,7 @@ func runLSP(args []string) int { defer profileSession.Stop() } - fs := bundled.WrapFS(osvfs.FS()) + fs := bundled.WrapFS(zipvfs.From(osvfs.FS())) defaultLibraryPath := bundled.LibPath() typingsLocation := getGlobalTypingsCacheLocation() diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index 741a48bac3..0d3bbe29d7 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/zipvfs" "golang.org/x/term" ) @@ -73,7 +74,7 @@ func newSystem() *osSys { return &osSys{ cwd: tspath.NormalizePath(cwd), - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(zipvfs.From(osvfs.FS())), defaultLibraryPath: bundled.LibPath(), writer: os.Stdout, start: time.Now(), diff --git a/internal/api/server.go b/internal/api/server.go index 5f88ad20d3..26b6b21187 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -15,6 +15,7 @@ import ( "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/zipvfs" ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageType -output=stringer_generated.go @@ -97,7 +98,7 @@ func NewServer(options *ServerOptions) *Server { w: bufio.NewWriter(options.Out), stderr: options.Err, cwd: options.Cwd, - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(zipvfs.From(osvfs.FS())), defaultLibraryPath: options.DefaultLibraryPath, } logger := project.NewLogger([]io.Writer{options.Err}, "", project.LogLevelVerbose) From faea6f7a15a6fa1dbd3dd9c384692be2247e462c Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 12 Aug 2025 15:10:17 +0200 Subject: [PATCH 04/48] Temporarly insert virtual path handling in zipvfs --- internal/vfs/zipvfs/zipvfs.go | 95 +++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/internal/vfs/zipvfs/zipvfs.go b/internal/vfs/zipvfs/zipvfs.go index d49bdb3e23..ff18c760f8 100644 --- a/internal/vfs/zipvfs/zipvfs.go +++ b/internal/vfs/zipvfs/zipvfs.go @@ -2,7 +2,9 @@ package zipvfs import ( "archive/zip" + "path" "path/filepath" + "strconv" "strings" "github.com/microsoft/typescript-go/internal/vfs" @@ -21,11 +23,20 @@ func From(fs vfs.FS) *FS { } func (fsys *FS) DirectoryExists(path string) bool { + path, _, _ = resolveVirtual(path) + + if strings.HasSuffix(path, ".zip") { + return fsys.fs.FileExists(path) + } + fs, formattedPath, _ := getMatchingFS(fsys, path) + return fs.DirectoryExists(formattedPath) } func (fsys *FS) FileExists(path string) bool { + path, _, _ = resolveVirtual(path) + if strings.HasSuffix(path, ".zip") { return fsys.fs.FileExists(path) } @@ -35,36 +46,49 @@ func (fsys *FS) FileExists(path string) bool { } func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries { + path, hash, basePath := resolveVirtual(path) + fs, formattedPath, zipPath := getMatchingFS(fsys, path) entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { - entries.Directories[i] = filepath.Join(zipPath, dir) + fullPath := filepath.Join(zipPath, dir) + entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) } for i, file := range entries.Files { - entries.Files[i] = filepath.Join(zipPath, file) + fullPath := filepath.Join(zipPath, file) + entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) } return entries } func (fsys *FS) ReadFile(path string) (contents string, ok bool) { + path, _, _ = resolveVirtual(path) + fs, formattedPath, _ := getMatchingFS(fsys, path) return fs.ReadFile(formattedPath) } func (fsys *FS) Realpath(path string) string { + path, hash, basePath := resolveVirtual(path) + fs, formattedPath, zipPath := getMatchingFS(fsys, path) - return filepath.Join(zipPath, fs.Realpath(formattedPath)) + fullPath := filepath.Join(zipPath, fs.Realpath(formattedPath)) + return makeVirtualPath(basePath, hash, fullPath) } func (fsys *FS) Remove(path string) error { + path, _, _ = resolveVirtual(path) + fs, formattedPath, _ := getMatchingFS(fsys, path) return fs.Remove(formattedPath) } func (fsys *FS) Stat(path string) vfs.FileInfo { + path, _, _ = resolveVirtual(path) + fs, formattedPath, _ := getMatchingFS(fsys, path) return fs.Stat(formattedPath) } @@ -74,13 +98,18 @@ func (fsys *FS) UseCaseSensitiveFileNames() bool { } func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + root, hash, basePath := resolveVirtual(root) + fs, formattedPath, zipPath := getMatchingFS(fsys, root) return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { - return walkFn(filepath.Join(zipPath, path), d, err) + fullPath := filepath.Join(zipPath, path) + return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) })) } func (fsys *FS) WriteFile(path string, data string, writeByteOrderMark bool) error { + path, _, _ = resolveVirtual(path) + fs, formattedPath, _ := getMatchingFS(fsys, path) return fs.WriteFile(formattedPath, data, writeByteOrderMark) } @@ -110,3 +139,61 @@ func getMatchingFS(fsys *FS, path string) (vfs.FS, string, string) { return iovfs.From(zipReader, fsys.fs.UseCaseSensitiveFileNames()), internalPath, zipPath } + +// TODO insert virtual path handling more properly (with a vfs wrapper maybe) +func resolveVirtual(path string) (realPath string, hash string, basePath string) { + idx := strings.Index(path, "/__virtual__/") + if idx == -1 { + return path, "", "" + } + + base := path[:idx] + rest := path[idx+len("/__virtual__/"):] + parts := strings.SplitN(rest, "/", 3) + if len(parts) < 3 { + // Not enough parts to match the pattern, return as is + return path, "", "" + } + hash = parts[0] + subpath := parts[2] + depth, err := strconv.Atoi(parts[1]) + if err != nil || depth < 0 { + // Invalid n, return as is + return path, "", "" + } + + basePath = path[:idx] + "/__virtual__" + + // Apply dirname n times to base + for i := 0; i < depth; i++ { + base = filepath.Dir(base) + } + // Join base and subpath + if base == "/" { + return "/" + subpath, hash, basePath + } + + return filepath.Join(base, subpath), hash, basePath +} + +func makeVirtualPath(basePath string, hash string, targetPath string) string { + if basePath == "" || hash == "" { + return targetPath + } + + relativePath, err := filepath.Rel(path.Dir(basePath), targetPath) + if err != nil { + panic("Could not make virtual path: " + err.Error()) + } + + segments := strings.Split(relativePath, "/") + + depth := 0 + for depth < len(segments) && segments[depth] == ".." { + depth++ + } + + subPath := strings.Join(segments[depth:], "/") + + return path.Join(basePath, hash, strconv.Itoa(depth), subPath) +} From d42bf239e31989fb585db0834e7dec610c1071aa Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 12 Aug 2025 15:12:05 +0200 Subject: [PATCH 05/48] Insert pnp in most important resolvers --- internal/core/compileroptions.go | 21 ++++++ internal/module/resolver.go | 43 ++++++++++++ internal/modulespecifiers/specifiers.go | 92 ++++++++++++++++++++++--- internal/pnp/pnpapi.go | 38 +++++++++- 4 files changed, 184 insertions(+), 10 deletions(-) diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 8ce79b2d02..71737ede7a 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -294,6 +295,26 @@ func (options *CompilerOptions) GetJSXTransformEnabled() bool { } func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) (result []string, fromConfig bool) { + nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(currentDirectory) + + pnpTypes := []string{} + pnpApi := pnp.GetPnpApi(currentDirectory) + if pnpApi != nil { + pnpTypes = pnpApi.GetPnpTypeRoots(currentDirectory) + } + + if len(nmTypes) > 0 { + return append(nmTypes, pnpTypes...), nmFromConfig + } + + if len(pnpTypes) > 0 { + return pnpTypes, false + } + + return nil, false +} + +func (options *CompilerOptions) GetNodeModulesTypeRoots(currentDirectory string) (result []string, fromConfig bool) { if options.TypeRoots != nil { return options.TypeRoots, true } diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 680a44d551..610932143a 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/semver" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -883,6 +884,12 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn } func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { + pnpApi := pnp.GetPnpApi(r.containingDirectory) + if pnpApi != nil { + // TODO add caching here + return r.loadModuleFromImmediateNodeModulesDirectoryPnP(ext, r.containingDirectory, typesScopeOnly) + } + result, _ := tspath.ForEachAncestorDirectory( r.containingDirectory, func(directory string) (result *resolved, stop bool) { @@ -922,11 +929,47 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions return continueSearching() } +func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectoryPnP(extensions extensions, directory string, typesScopeOnly bool) *resolved { + if !typesScopeOnly { + if packageResult := r.loadModuleFromPnpResolution(extensions, r.name, directory); !packageResult.shouldContinueSearching() { + return packageResult + } + } + + if extensions&extensionsDeclaration != 0 { + result := r.loadModuleFromPnpResolution(extensionsDeclaration, "@types/"+r.mangleScopedPackageName(r.name), directory) + + return result + } + + return nil +} + +func (r *resolutionState) loadModuleFromPnpResolution(ext extensions, moduleName string, issuer string) *resolved { + pnpApi := pnp.GetPnpApi(issuer) + + if pnpApi != nil { + packageName, rest := ParsePackageName(moduleName) + // TODO: bubble up yarn resolution errors, instead of _ + packageDirectory, _ := pnpApi.ResolveToUnqualified(packageName, issuer) + if packageDirectory != "" { + candidate := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, rest)) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, true /* nodeModulesDirectoryExists */, candidate, rest, packageDirectory) + } + } + + return nil +} + func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string, nodeModulesDirectoryExists bool) *resolved { candidate := tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName)) packageName, rest := ParsePackageName(moduleName) packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, nodeModulesDirectoryExists, candidate, rest, packageDirectory) +} + +func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectoryImpl(ext extensions, nodeModulesDirectoryExists bool, candidate string, rest string, packageDirectory string) *resolved { var rootPackageInfo *packagejson.InfoCacheEntry // First look for a nested package.json, as in `node_modules/foo/bar/package.json` packageInfo := r.getPackageJsonInfo(candidate, !nodeModulesDirectoryExists) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index cb377869f1..fc2c23ecaf 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -223,9 +224,23 @@ func getEachFileNameOfModule( // so we only need to remove them from the realpath filenames. for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { + isInNodeModules := containsNodeModules(p) + + // TODO: understand what this change actually impacts + if !isInNodeModules { + pnpApi := pnp.GetPnpApi(p) + if pnpApi != nil { + fromLocator, _ := pnpApi.FindLocator(importingFileName) + toLocator, _ := pnpApi.FindLocator(p) + if fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name { + isInNodeModules = true + } + } + } + results = append(results, ModulePath{ FileName: p, - IsInNodeModules: containsNodeModules(p), + IsInNodeModules: isInNodeModules, IsRedirect: referenceRedirect == p, }) } @@ -644,6 +659,54 @@ func tryGetModuleNameAsNodeModule( overrideMode core.ResolutionMode, ) string { parts := getNodeModulePathParts(pathObj.FileName) + + // TODO understand what feature this part impacts + pnpPackageName := "" + + pnpApi := pnp.GetPnpApi(importingSourceFile.FileName()) + if pnpApi != nil { + fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) + toLocator, _ := pnpApi.FindLocator(pathObj.FileName) + + // Don't use the package name when the imported file is inside + // the source directory (prefer a relative path instead) + if fromLocator == toLocator { + return "" + } + + if fromLocator != nil && toLocator != nil { + fromInfo := pnpApi.GetPackage(*fromLocator) + + useToLocator := false + + for i := range fromInfo.PackageDependencies { + isAlias := fromInfo.PackageDependencies[i].IsAlias() + if isAlias && fromInfo.PackageDependencies[i].AliasName == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } else if fromInfo.PackageDependencies[i].Ident == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } + } + + if useToLocator { + pnpPackageName = toLocator.Name + } + } + + if parts != nil { + toInfo := pnpApi.GetPackage(*toLocator) + parts = &NodeModulePathParts{ + TopLevelNodeModulesIndex: -1, + TopLevelPackageNameIndex: -1, + // The last character from packageLocation is the trailing "/", we want to point to it + PackageRootIndex: len(toInfo.PackageLocation) - 1, + FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), + } + } + } + if parts == nil { return "" } @@ -668,6 +731,7 @@ func tryGetModuleNameAsNodeModule( overrideMode, options, allowedEndings, + pnpPackageName, ) moduleFileToTry := pkgJsonResults.moduleFileToTry packageRootPath := pkgJsonResults.packageRootPath @@ -701,17 +765,25 @@ func tryGetModuleNameAsNodeModule( return "" } - globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation() - // Get a path that's relative to node_modules or the importing file's path - // if node_modules folder is in this folder or any of its parent folders, no need to keep it. - pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex] + // If PnP is enabled the node_modules entries we'll get will always be relevant even if they + // are located in a weird path apparently outside of the source directory + if pnpApi == nil { + globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation() + // Get a path that's relative to node_modules or the importing file's path + // if node_modules folder is in this folder or any of its parent folders, no need to keep it. + pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex] - if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) { - return "" + if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) { + return "" + } } // If the module was found in @types, get the actual Node package name nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] + if pnpPackageName != "" { + nodeModulesDirectoryName = pnpPackageName + moduleSpecifier[parts.PackageRootIndex:] + } + return getPackageNameFromTypesPackageName(nodeModulesDirectoryName) } @@ -730,6 +802,7 @@ func tryDirectoryWithPackageJson( overrideMode core.ResolutionMode, options *core.CompilerOptions, allowedEndings []ModuleSpecifierEnding, + pnpPackageName string, ) pkgJsonDirAttemptResult { rootIdx := parts.PackageRootIndex if rootIdx == -1 { @@ -763,7 +836,10 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := pnpPackageName + if packageName == "" { + packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + } conditions := module.GetConditions(options, importMode) var fromExports string diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index f1e94b358d..3e605bc279 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -6,6 +6,8 @@ import ( "path" "path/filepath" "strings" + + "github.com/microsoft/typescript-go/internal/tspath" ) type PnpApi struct { @@ -165,12 +167,15 @@ func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { continue } + // TODO check that we do need this + packageLocation := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation) + if len(packageInfo.PackageLocation) <= bestLength { continue } - if strings.HasPrefix(relativePathWithDot, packageInfo.PackageLocation) { - bestLength = len(packageInfo.PackageLocation) + if strings.HasPrefix(relativePathWithDot, packageLocation) { + bestLength = len(packageLocation) bestLocator = &Locator{Name: name, Reference: reference} } } @@ -236,3 +241,32 @@ func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath return ident, modulePath, nil } + +func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { + if p.manifest == nil { + return []string{} + } + + currentDirectory = tspath.NormalizePath(currentDirectory) + + currentPackage, err := p.FindLocator(currentDirectory) + if err != nil { + return []string{} + } + + if currentPackage == nil { + return []string{} + } + + packageDependencies := p.GetPackage(*currentPackage).PackageDependencies + + typeRoots := []string{} + for _, dep := range packageDependencies { + if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" { + packageInfo := p.GetPackage(Locator{Name: dep.Ident, Reference: dep.Reference}) + typeRoots = append(typeRoots, path.Dir(path.Join(p.manifest.dirPath, packageInfo.PackageLocation))) + } + } + + return typeRoots +} From ccb452464de485429fde3b4aebab467480becc3d Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 12 Aug 2025 15:12:25 +0200 Subject: [PATCH 06/48] Add some todos for potential unimplemented places --- internal/ls/string_completions.go | 1 + internal/project/project.go | 44 +++++++++++++++++++++++++++++++ internal/vfs/osvfs/os.go | 2 ++ 3 files changed, 47 insertions(+) diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 6af4212224..007c64d565 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -518,6 +518,7 @@ func getStringLiteralCompletionsFromModuleNames( preferences *UserPreferences, ) *stringLiteralCompletions { // !!! needs `getModeForUsageLocationWorker` + // TODO investigate if we will need to update this for pnp, once available return nil } diff --git a/internal/project/project.go b/internal/project/project.go index b4a22c3a60..753c0b4f16 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -291,6 +291,50 @@ func (p *Project) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile // GetResolvedProjectReference implements compiler.CompilerHost. func (p *Project) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { + // TODO, we might need to add the code below for pnp (converted to go) + + // With Plug'n'Play, dependencies that list peer dependencies + // are "virtualized": they are resolved to a unique (virtual) + // path that the underlying filesystem layer then resolve back + // to the original location. + // + // When a workspace depends on another workspace with peer + // dependencies, this other workspace will thus be resolved to + // a unique path that won't match what the initial project has + // listed in its `references` field, and TS thus won't leverage + // the reference at all. + // + // To avoid that, we compute here the virtualized paths for the + // user-provided references in our references by directly querying + // the PnP API. This way users don't have to know the virtual paths, + // but we still support them just fine even through references. + + // pnpApi := pnp.GetPnpApi(fileName) + // if pnpApi != nil { + // pnpApi.GetPackage(fileName) + // } + + // const basePath = this.getCurrentDirectory(); + + // const getPnpPath = (path: string) => { + // try { + // const pnpApi = getPnpApi(`${path}/`); + // if (!pnpApi) { + // return path; + // } + // const targetLocator = pnpApi.findPackageLocator(`${path}/`); + // const { packageLocation } = pnpApi.getPackageInformation(targetLocator); + // const request = combinePaths(targetLocator.name, getRelativePathFromDirectory(packageLocation, path, /*ignoreCase*/ false)); + // return pnpApi.resolveToUnqualified(request, `${basePath}/`); + // } + // catch { + // // something went wrong with the resolution, try not to fail + // return path; + // } + // }; + + // Then run getPnpPath on all resolvedReferences from acquireConfig() + return p.host.ConfigFileRegistry().acquireConfig(fileName, path, p, nil) } diff --git a/internal/vfs/osvfs/os.go b/internal/vfs/osvfs/os.go index 47439b59d1..3345b017fc 100644 --- a/internal/vfs/osvfs/os.go +++ b/internal/vfs/osvfs/os.go @@ -31,6 +31,8 @@ type osFS struct { // We do this right at startup to minimize the chance that executable gets moved or deleted. var isFileSystemCaseSensitive = func() bool { + // TODO: return true if pnp api is available + // win32/win64 are case insensitive platforms if runtime.GOOS == "windows" { return false From c505d838d6a1e4999707aef1dedf51043c441bcd Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 13 Aug 2025 15:45:03 +0200 Subject: [PATCH 07/48] Add basic caching for zipvfs --- internal/vfs/zipvfs/zipvfs.go | 127 +++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/internal/vfs/zipvfs/zipvfs.go b/internal/vfs/zipvfs/zipvfs.go index ff18c760f8..374b953278 100644 --- a/internal/vfs/zipvfs/zipvfs.go +++ b/internal/vfs/zipvfs/zipvfs.go @@ -6,49 +6,66 @@ import ( "path/filepath" "strconv" "strings" + "sync" + "time" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/iovfs" ) -type FS struct { - fs vfs.FS +type cachedZipReader struct { + reader *zip.ReadCloser + lastUsed time.Time + zipMTime time.Time } -var _ vfs.FS = (*FS)(nil) +type zipFS struct { + fs vfs.FS + maxOpenReaders int + cachedZipReadersMap map[string]*cachedZipReader + cacheReaderMutex sync.Mutex +} + +var _ vfs.FS = (*zipFS)(nil) -func From(fs vfs.FS) *FS { - fsys := &FS{fs: fs} - return fsys +func From(fs vfs.FS) *zipFS { + zipfs := &zipFS{ + fs: fs, + maxOpenReaders: 80, + cachedZipReadersMap: make(map[string]*cachedZipReader), + cacheReaderMutex: sync.Mutex{}, + } + + return zipfs } -func (fsys *FS) DirectoryExists(path string) bool { +func (zipfs *zipFS) DirectoryExists(path string) bool { path, _, _ = resolveVirtual(path) if strings.HasSuffix(path, ".zip") { - return fsys.fs.FileExists(path) + return zipfs.fs.FileExists(path) } - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.DirectoryExists(formattedPath) } -func (fsys *FS) FileExists(path string) bool { +func (zipfs *zipFS) FileExists(path string) bool { path, _, _ = resolveVirtual(path) if strings.HasSuffix(path, ".zip") { - return fsys.fs.FileExists(path) + return zipfs.fs.FileExists(path) } - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.FileExists(formattedPath) } -func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries { +func (zipfs *zipFS) GetAccessibleEntries(path string) vfs.Entries { path, hash, basePath := resolveVirtual(path) - fs, formattedPath, zipPath := getMatchingFS(fsys, path) + fs, formattedPath, zipPath := getMatchingFS(zipfs, path) entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { @@ -64,53 +81,53 @@ func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries { return entries } -func (fsys *FS) ReadFile(path string) (contents string, ok bool) { +func (zipfs *zipFS) ReadFile(path string) (contents string, ok bool) { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.ReadFile(formattedPath) } -func (fsys *FS) Realpath(path string) string { +func (zipfs *zipFS) Realpath(path string) string { path, hash, basePath := resolveVirtual(path) - fs, formattedPath, zipPath := getMatchingFS(fsys, path) + fs, formattedPath, zipPath := getMatchingFS(zipfs, path) fullPath := filepath.Join(zipPath, fs.Realpath(formattedPath)) return makeVirtualPath(basePath, hash, fullPath) } -func (fsys *FS) Remove(path string) error { +func (zipfs *zipFS) Remove(path string) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.Remove(formattedPath) } -func (fsys *FS) Stat(path string) vfs.FileInfo { +func (zipfs *zipFS) Stat(path string) vfs.FileInfo { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.Stat(formattedPath) } -func (fsys *FS) UseCaseSensitiveFileNames() bool { - return fsys.fs.UseCaseSensitiveFileNames() +func (zipfs *zipFS) UseCaseSensitiveFileNames() bool { + return zipfs.fs.UseCaseSensitiveFileNames() } -func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { +func (zipfs *zipFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { root, hash, basePath := resolveVirtual(root) - fs, formattedPath, zipPath := getMatchingFS(fsys, root) + fs, formattedPath, zipPath := getMatchingFS(zipfs, root) return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { fullPath := filepath.Join(zipPath, path) return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) })) } -func (fsys *FS) WriteFile(path string, data string, writeByteOrderMark bool) error { +func (zipfs *zipFS) WriteFile(path string, data string, writeByteOrderMark bool) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(fsys, path) + fs, formattedPath, _ := getMatchingFS(zipfs, path) return fs.WriteFile(formattedPath, data, writeByteOrderMark) } @@ -126,18 +143,60 @@ func splitZipPath(path string) (string, string) { return parts[0] + ".zip", "/" + parts[1] } -func getMatchingFS(fsys *FS, path string) (vfs.FS, string, string) { +func getMatchingFS(zipfs *zipFS, path string) (vfs.FS, string, string) { if !isZipPath(path) { - return fsys.fs, path, "" + return zipfs.fs, path, "" } zipPath, internalPath := splitZipPath(path) - zipReader, err := zip.OpenReader(zipPath) - if err != nil { - return fsys.fs, path, "" + + zipStat := zipfs.fs.Stat(zipPath) + if zipStat == nil { + return zipfs.fs, path, "" + } + + var usedReader *cachedZipReader + + zipfs.cacheReaderMutex.Lock() + defer zipfs.cacheReaderMutex.Unlock() + + zipMTime := zipStat.ModTime() + + cachedReader, ok := zipfs.cachedZipReadersMap[zipPath] + if ok && cachedReader.zipMTime.Equal(zipMTime) { + cachedReader.lastUsed = time.Now() + usedReader = cachedReader + } else { + zipReader, err := zip.OpenReader(zipPath) + if err != nil { + return zipfs.fs, path, "" + } + + if len(zipfs.cachedZipReadersMap) >= zipfs.maxOpenReaders { + zipfs.deleteOldestReader() + } + + usedReader = &cachedZipReader{reader: zipReader, lastUsed: time.Now(), zipMTime: zipMTime} + zipfs.cachedZipReadersMap[zipPath] = usedReader } - return iovfs.From(zipReader, fsys.fs.UseCaseSensitiveFileNames()), internalPath, zipPath + return iovfs.From(usedReader.reader, zipfs.fs.UseCaseSensitiveFileNames()), internalPath, zipPath +} + +func (zipfs *zipFS) deleteOldestReader() { + var oldestReader *cachedZipReader + var oldestReaderPath string + for path, reader := range zipfs.cachedZipReadersMap { + if oldestReader == nil || reader.lastUsed.Before(oldestReader.lastUsed) { + oldestReader = reader + oldestReaderPath = path + } + } + + if oldestReader != nil { + oldestReader.reader.Close() + delete(zipfs.cachedZipReadersMap, oldestReaderPath) + } } // TODO insert virtual path handling more properly (with a vfs wrapper maybe) From e6f86e86abfb292e889b037cfeaf91aff1042972 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 13 Aug 2025 18:20:14 +0200 Subject: [PATCH 08/48] Improve data structure for FindLocator --- internal/pnp/manifestparser.go | 63 ++++++++++++++++++++++++++++++---- internal/pnp/pnpapi.go | 31 +++++++---------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index 65178ca4be..e14b25f180 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -5,8 +5,10 @@ import ( "fmt" "os" "path" + "strings" "github.com/dlclark/regexp2" + "github.com/microsoft/typescript-go/internal/tspath" ) type LinkType string @@ -44,6 +46,18 @@ type FallbackExclusion struct { Entries []string `json:"entries"` } +type PackageTrieData struct { + ident string + reference string + info *PackageInfo +} + +type PackageRegistryTrie struct { + pathSegment string + childrenPathSegments map[string]*PackageRegistryTrie + packageData *PackageTrieData +} + type PnpManifestData struct { dirPath string @@ -56,7 +70,8 @@ type PnpManifestData struct { dependencyTreeRoots []Locator // Nested maps for package registry (ident -> reference -> PackageInfo) - packageRegistryMap map[string]map[string]*PackageInfo + packageRegistryMap map[string]map[string]*PackageInfo + packageRegistryTrie *PackageRegistryTrie } func parseManifestFromPath(manifestPath string) (*PnpManifestData, error) { @@ -121,16 +136,16 @@ func parsePnpManifest(rawData map[string]interface{}, manifestPath string) (*Pnp if registryData, ok := rawData["packageRegistryData"].([]interface{}); ok { for _, entry := range registryData { if entryArr, ok := entry.([]interface{}); ok && len(entryArr) == 2 { - packageName := parseString(entryArr[0]) + ident := parseString(entryArr[0]) - if data.packageRegistryMap[packageName] == nil { - data.packageRegistryMap[packageName] = make(map[string]*PackageInfo) + if data.packageRegistryMap[ident] == nil { + data.packageRegistryMap[ident] = make(map[string]*PackageInfo) } if versions, ok := entryArr[1].([]interface{}); ok { for _, version := range versions { if versionArr, ok := version.([]interface{}); ok && len(versionArr) == 2 { - versionStr := parseString(versionArr[0]) + reference := parseString(versionArr[0]) if infoMap, ok := versionArr[1].(map[string]interface{}); ok { packageInfo := &PackageInfo{ @@ -141,7 +156,8 @@ func parsePnpManifest(rawData map[string]interface{}, manifestPath string) (*Pnp PackagePeers: getField(infoMap, "packagePeers", parseStringArray), } - data.packageRegistryMap[packageName][versionStr] = packageInfo + data.packageRegistryMap[ident][reference] = packageInfo + data.addPackageToTrie(ident, reference, packageInfo) } } } @@ -153,6 +169,41 @@ func parsePnpManifest(rawData map[string]interface{}, manifestPath string) (*Pnp return data, nil } +func (data *PnpManifestData) addPackageToTrie(ident string, reference string, packageInfo *PackageInfo) { + if data.packageRegistryTrie == nil { + data.packageRegistryTrie = &PackageRegistryTrie{ + pathSegment: "", + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + packageData := &PackageTrieData{ + ident: ident, + reference: reference, + info: packageInfo, + } + + packagePath := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation) + packagePathSegments := strings.Split(packagePath, "/") + + currentTrie := data.packageRegistryTrie + + for _, segment := range packagePathSegments { + if currentTrie.childrenPathSegments[segment] == nil { + currentTrie.childrenPathSegments[segment] = &PackageRegistryTrie{ + pathSegment: segment, + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + currentTrie = currentTrie.childrenPathSegments[segment] + } + + currentTrie.packageData = packageData +} + // Helper functions for parsing JSON values - following patterns from tsoptions.parseString, etc. func parseString(value interface{}) string { if str, ok := value.(string); ok { diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 3e605bc279..d74ae8cb1c 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -135,9 +135,6 @@ func (p *PnpApi) GetPackage(locator Locator) *PackageInfo { } func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { - var bestLength int - var bestLocator *Locator - relativePath, err := filepath.Rel(p.manifest.dirPath, parentPath) if err != nil { return nil, err @@ -161,27 +158,23 @@ func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { relativePathWithDot = "./" + relativePath } - for name, referenceMap := range p.manifest.packageRegistryMap { - for reference, packageInfo := range referenceMap { - if packageInfo.DiscardFromLookup { - continue - } + pathSegments := strings.Split(relativePathWithDot, "/") + currentTrie := p.manifest.packageRegistryTrie - // TODO check that we do need this - packageLocation := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation) + // Go down the trie, looking for the latest defined packageInfo that matches the path + for _, segment := range pathSegments { + if currentTrie.childrenPathSegments[segment] == nil { + break + } - if len(packageInfo.PackageLocation) <= bestLength { - continue - } + currentTrie = currentTrie.childrenPathSegments[segment] + } - if strings.HasPrefix(relativePathWithDot, packageLocation) { - bestLength = len(packageLocation) - bestLocator = &Locator{Name: name, Reference: reference} - } - } + if currentTrie.packageData == nil { + return nil, fmt.Errorf("no package found for path %s", relativePath) } - return bestLocator, nil + return &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference}, nil } func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { From 17d4e68365a44862be6c23fa6882fd39951d49cc Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 14 Aug 2025 13:48:39 +0200 Subject: [PATCH 09/48] Parse pnp data from .pnp.cjs --- internal/pnp/manifestparser.go | 47 +++++++++++++++++++++++++++------- internal/pnp/pnp.go | 2 ++ internal/pnp/pnpapi.go | 3 +-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index e14b25f180..11aad0286e 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -74,18 +74,47 @@ type PnpManifestData struct { packageRegistryTrie *PackageRegistryTrie } -func parseManifestFromPath(manifestPath string) (*PnpManifestData, error) { - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("failed to read .pnp.data.json file: %w", err) +func parseManifestFromPath(manifestDir string) (*PnpManifestData, error) { + pnpDataString := "" + + data, err := os.ReadFile(path.Join(manifestDir, ".pnp.data.json")) + if err == nil { + pnpDataString = string(data) + } else { + data, err := os.ReadFile(path.Join(manifestDir, ".pnp.cjs")) + if err != nil { + return nil, fmt.Errorf("failed to read .pnp.cjs file: %w", err) + } + + pnpString := string(data) + + manifestRegex := regexp2.MustCompile(`(const[ \r\n]+RAW_RUNTIME_STATE[ \r\n]*=[ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`, regexp2.None) + matches, err := manifestRegex.FindStringMatch(pnpString) + if err != nil || matches == nil { + return nil, fmt.Errorf("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?") + } + + start := matches.Index + matches.Length + var b strings.Builder + b.Grow(len(pnpString)) + for i := start; i < len(pnpString); i++ { + if pnpString[i] == '\'' { + break + } + + if pnpString[i] != '\\' { + b.WriteByte(pnpString[i]) + } + } + pnpDataString = b.String() } var rawData map[string]interface{} - if err := json.Unmarshal(data, &rawData); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) + if err := json.Unmarshal([]byte(pnpDataString), &rawData); err != nil { + return nil, fmt.Errorf("failed to parse JSON PnP data: %w", err) } - pnpData, err := parsePnpManifest(rawData, manifestPath) + pnpData, err := parsePnpManifest(rawData, manifestDir) if err != nil { return nil, fmt.Errorf("failed to parse PnP data: %w", err) } @@ -94,8 +123,8 @@ func parseManifestFromPath(manifestPath string) (*PnpManifestData, error) { } // TODO add error handling for corrupted data -func parsePnpManifest(rawData map[string]interface{}, manifestPath string) (*PnpManifestData, error) { - data := &PnpManifestData{dirPath: path.Dir(manifestPath)} +func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpManifestData, error) { + data := &PnpManifestData{dirPath: manifestDir} if roots, ok := rawData["dependencyTreeRoots"].([]interface{}); ok { for _, root := range roots { diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index f0b3841bde..dc2ac97f6e 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -1,6 +1,7 @@ package pnp import ( + "fmt" "sync" "sync/atomic" ) @@ -41,6 +42,7 @@ func GetPnpApi(filePath string) *PnpApi { cachedPnpApi = pnpApi } else { // Couldn't load PnP API + fmt.Println("Error loading PnP API", err) cachedPnpApi = nil } diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index d74ae8cb1c..2d30d25d5f 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -113,8 +113,7 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { for { pnpPath := path.Join(directoryPath, ".pnp.cjs") if _, err := os.Stat(pnpPath); err == nil { - manifestPath := path.Join(directoryPath, ".pnp.data.json") - return parseManifestFromPath(manifestPath) + return parseManifestFromPath(directoryPath) } directoryPath = path.Dir(directoryPath) From aa12649d5c0db1d9278faecd502c978bb36a2b59 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 18 Aug 2025 18:01:55 +0200 Subject: [PATCH 10/48] Use pointers for GetPackage and implement WIP GetResolvedProjectReference --- internal/modulespecifiers/specifiers.go | 4 +- internal/pnp/pnpapi.go | 14 +++--- internal/project/project.go | 63 +++++++++++++++---------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index fc2c23ecaf..e73ec5aa47 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -675,7 +675,7 @@ func tryGetModuleNameAsNodeModule( } if fromLocator != nil && toLocator != nil { - fromInfo := pnpApi.GetPackage(*fromLocator) + fromInfo := pnpApi.GetPackage(fromLocator) useToLocator := false @@ -696,7 +696,7 @@ func tryGetModuleNameAsNodeModule( } if parts != nil { - toInfo := pnpApi.GetPackage(*toLocator) + toInfo := pnpApi.GetPackage(toLocator) parts = &NodeModulePathParts{ TopLevelNodeModulesIndex: -1, TopLevelPackageNameIndex: -1, diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 2d30d25d5f..d37fc5ca82 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -50,7 +50,7 @@ func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (stri return "", nil } - parentPkg := p.GetPackage(*parentLocator) + parentPkg := p.GetPackage(parentLocator) var referenceOrAlias *PackageDependency for _, dep := range parentPkg.PackageDependencies { @@ -99,9 +99,9 @@ func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (stri var dependencyPkg *PackageInfo if referenceOrAlias.IsAlias() { - dependencyPkg = p.GetPackage(Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference}) + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference}) } else { - dependencyPkg = p.GetPackage(Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) } return filepath.Join(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil @@ -123,7 +123,7 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { } } -func (p *PnpApi) GetPackage(locator Locator) *PackageInfo { +func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo { packageRegistryMap := p.manifest.packageRegistryMap packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference] if !ok { @@ -177,7 +177,7 @@ func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { } func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { - topLevelPkg := p.GetPackage(Locator{Name: "", Reference: ""}) + topLevelPkg := p.GetPackage(&Locator{Name: "", Reference: ""}) if topLevelPkg != nil { for _, dep := range topLevelPkg.PackageDependencies { @@ -250,12 +250,12 @@ func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { return []string{} } - packageDependencies := p.GetPackage(*currentPackage).PackageDependencies + packageDependencies := p.GetPackage(currentPackage).PackageDependencies typeRoots := []string{} for _, dep := range packageDependencies { if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" { - packageInfo := p.GetPackage(Locator{Name: dep.Ident, Reference: dep.Reference}) + packageInfo := p.GetPackage(&Locator{Name: dep.Ident, Reference: dep.Reference}) typeRoots = append(typeRoots, path.Dir(path.Join(p.manifest.dirPath, packageInfo.PackageLocation))) } } diff --git a/internal/project/project.go b/internal/project/project.go index 753c0b4f16..8192799ed2 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -291,7 +291,11 @@ func (p *Project) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile // GetResolvedProjectReference implements compiler.CompilerHost. func (p *Project) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - // TODO, we might need to add the code below for pnp (converted to go) + config := p.host.ConfigFileRegistry().acquireConfig(fileName, path, p, nil) + if config == nil { + return nil + } + // TODO fix this and identify cases where we do need this // With Plug'n'Play, dependencies that list peer dependencies // are "virtualized": they are resolved to a unique (virtual) @@ -308,34 +312,43 @@ func (p *Project) GetResolvedProjectReference(fileName string, path tspath.Path) // user-provided references in our references by directly querying // the PnP API. This way users don't have to know the virtual paths, // but we still support them just fine even through references. - // pnpApi := pnp.GetPnpApi(fileName) // if pnpApi != nil { - // pnpApi.GetPackage(fileName) + // basePath := p.GetCurrentDirectory() + + // getPnpPath := func(path string) string { + // targetLocator, err := pnpApi.FindLocator(path + "/") + // if err != nil { + // return path + // } + + // packageLocation := tspath.ResolvePath(basePath, pnpApi.GetPackage(targetLocator).PackageLocation) + + // compareOptions := tspath.ComparePathsOptions{ + // UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(), + // CurrentDirectory: basePath, + // } + + // request := tspath.CombinePaths(targetLocator.Name, tspath.GetRelativePathFromDirectory(packageLocation, path, compareOptions)) + // unqualified, err := pnpApi.ResolveToUnqualified(request, basePath+"/") + // if err != nil { + // return path + // } + + // return unqualified + // } + + // for _, ref := range config.ProjectReferences() { + // ref.Path = getPnpPath(ref.Path) + // } + + // fmt.Println("Project refs after") + // for _, ref := range config.ProjectReferences() { + // fmt.Println("Project ref", ref.Path) + // } // } - // const basePath = this.getCurrentDirectory(); - - // const getPnpPath = (path: string) => { - // try { - // const pnpApi = getPnpApi(`${path}/`); - // if (!pnpApi) { - // return path; - // } - // const targetLocator = pnpApi.findPackageLocator(`${path}/`); - // const { packageLocation } = pnpApi.getPackageInformation(targetLocator); - // const request = combinePaths(targetLocator.name, getRelativePathFromDirectory(packageLocation, path, /*ignoreCase*/ false)); - // return pnpApi.resolveToUnqualified(request, `${basePath}/`); - // } - // catch { - // // something went wrong with the resolution, try not to fail - // return path; - // } - // }; - - // Then run getPnpPath on all resolvedReferences from acquireConfig() - - return p.host.ConfigFileRegistry().acquireConfig(fileName, path, p, nil) + return config } // Updates the program if needed. From 1e204b1d6ff2df8fb6e233e17bc4dd34339f6e6f Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 19 Aug 2025 13:23:29 +0200 Subject: [PATCH 11/48] Generate zip URIs for Go to definition --- internal/ls/converters.go | 9 ++++++++- internal/tspath/path.go | 4 ++++ internal/vfs/zipvfs/zipvfs.go | 7 ++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/ls/converters.go b/internal/ls/converters.go index 18238662f3..3582fa2c60 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -178,7 +178,14 @@ func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part)) } - return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/")) + var prefix string + if tspath.IsZipPath(fileName) { + prefix = "zip:" + } else { + prefix = "file:" + } + + return lsproto.DocumentUri(prefix + "//" + volume + strings.Join(parts, "/")) } func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos { diff --git a/internal/tspath/path.go b/internal/tspath/path.go index a0d722b233..fe525611fb 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -920,3 +920,7 @@ func ForEachAncestorDirectoryPath[T any](directory Path, callback func(directory func HasExtension(fileName string) bool { return strings.Contains(GetBaseFileName(fileName), ".") } + +func IsZipPath(path string) bool { + return strings.Contains(path, ".zip/") || strings.HasSuffix(path, ".zip") +} diff --git a/internal/vfs/zipvfs/zipvfs.go b/internal/vfs/zipvfs/zipvfs.go index 374b953278..35c1c71819 100644 --- a/internal/vfs/zipvfs/zipvfs.go +++ b/internal/vfs/zipvfs/zipvfs.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/iovfs" ) @@ -131,10 +132,6 @@ func (zipfs *zipFS) WriteFile(path string, data string, writeByteOrderMark bool) return fs.WriteFile(formattedPath, data, writeByteOrderMark) } -func isZipPath(path string) bool { - return strings.Contains(path, ".zip/") || strings.HasSuffix(path, ".zip") -} - func splitZipPath(path string) (string, string) { parts := strings.Split(path, ".zip/") if len(parts) < 2 { @@ -144,7 +141,7 @@ func splitZipPath(path string) (string, string) { } func getMatchingFS(zipfs *zipFS, path string) (vfs.FS, string, string) { - if !isZipPath(path) { + if !tspath.IsZipPath(path) { return zipfs.fs, path, "" } From 9eafbd47b1bdb3966911b148c80b476d828c3495 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 19 Aug 2025 14:51:01 +0200 Subject: [PATCH 12/48] Use pnp api in readPackageJsonPeerDependencies --- internal/module/resolver.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 610932143a..8c5ae892ac 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -1741,8 +1741,20 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa packageDirectory := r.realPath(packageJsonInfo.PackageDirectory) nodeModules := packageDirectory[:strings.LastIndex(packageDirectory, "/node_modules")+len("/node_modules")] + "/" builder := strings.Builder{} + // TODO: find an example that needs this change + pnpApi := pnp.GetPnpApi(packageJsonInfo.PackageDirectory) for name := range peerDependencies.Value { - peerPackageJson := r.getPackageJsonInfo(nodeModules+name /*onlyRecordFailures*/, false) + var peerDependencyPath string + + if pnpApi != nil { + peerDependencyPath, _ = pnpApi.ResolveToUnqualified(name, packageDirectory) + } + + if peerDependencyPath == "" { + peerDependencyPath = nodeModules + name + } + + peerPackageJson := r.getPackageJsonInfo(peerDependencyPath, false /*onlyRecordFailures*/) if peerPackageJson != nil { version := peerPackageJson.Contents.Version.Value builder.WriteString("+") From 312940451fc128ae8d75c81582b144aec28b28e1 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 19 Aug 2025 17:20:19 +0200 Subject: [PATCH 13/48] Enable typescript on zip files in IDE --- _extension/src/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/_extension/src/client.ts b/_extension/src/client.ts index 46fd90ebb0..4c4838cbe2 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -28,7 +28,14 @@ export class Client { this.clientOptions = { documentSelector: [ ...jsTsLanguageModes.map(language => ({ scheme: "file", language })), - ...jsTsLanguageModes.map(language => ({ scheme: "untitled", language })), + ...jsTsLanguageModes.map(language => ({ + scheme: "untitled", + language, + })), + ...jsTsLanguageModes.map(language => ({ + scheme: "zip", + language, + })), ], outputChannel: this.outputChannel, traceOutputChannel: this.traceOutputChannel, From 1c44670fd3aa82d939c2afcd2fe69ea9a9e7ea9b Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 19 Aug 2025 17:32:38 +0200 Subject: [PATCH 14/48] Rephrase todo comment on caching --- internal/module/resolver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 8c5ae892ac..a5454e5f94 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -886,7 +886,7 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { pnpApi := pnp.GetPnpApi(r.containingDirectory) if pnpApi != nil { - // TODO add caching here + // TODO: stop at global cache too? return r.loadModuleFromImmediateNodeModulesDirectoryPnP(ext, r.containingDirectory, typesScopeOnly) } From cd3fa197c812cf7f8405a27eb89feb48ff7015cc Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 21 Aug 2025 10:42:01 +0200 Subject: [PATCH 15/48] Fix type roots not working depending on where tsc is run --- internal/module/resolver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index a5454e5f94..a753c0f2f5 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -178,7 +178,7 @@ func (r *Resolver) ResolveTypeReferenceDirective( compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) - typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory()) + typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(containingDirectory) if traceEnabled { r.host.Trace(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2.Format(typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ","))) r.traceResolutionUsingProjectReference(redirectedReference) From 53c7edc9b3b02f72e975dc49632ad25e3c9bdafa Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 21 Aug 2025 14:00:07 +0200 Subject: [PATCH 16/48] Don't log anything when the pnp api is not available --- internal/pnp/pnp.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index dc2ac97f6e..f0b3841bde 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -1,7 +1,6 @@ package pnp import ( - "fmt" "sync" "sync/atomic" ) @@ -42,7 +41,6 @@ func GetPnpApi(filePath string) *PnpApi { cachedPnpApi = pnpApi } else { // Couldn't load PnP API - fmt.Println("Error loading PnP API", err) cachedPnpApi = nil } From 94b4f5d45571db0ba5648b0521347a43b16c444b Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 11 Sep 2025 13:15:03 +0200 Subject: [PATCH 17/48] Add comment on pnp api spec --- internal/pnp/pnpapi.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index d37fc5ca82..66dcac4ea6 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -1,5 +1,14 @@ package pnp +/* + * Yarn Plug'n'Play (generally referred to as Yarn PnP) is the default installation strategy in modern releases of Yarn. + * Yarn PnP generates a single Node.js loader file in place of the typical node_modules folder. + * This loader file, named .pnp.cjs, contains all information about your project's dependency tree, informing your tools as to + * the location of the packages on the disk and letting them know how to resolve require and import calls. + * + * The full specification is available at https://yarnpkg.com/advanced/pnp-spec + */ + import ( "fmt" "os" From 0045eaca14365963c33e4a287b5d3a525b5faa74 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 11 Sep 2025 13:24:55 +0200 Subject: [PATCH 18/48] Use zipfs only when pnp api is available --- cmd/tsgo/lsp.go | 13 +++++++++++-- cmd/tsgo/sys.go | 11 ++++++++++- internal/api/server.go | 11 ++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 5b67939797..3306856a67 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -9,8 +9,10 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/pprof" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/zipvfs" ) @@ -38,7 +40,14 @@ func runLSP(args []string) int { defer profileSession.Stop() } - fs := bundled.WrapFS(zipvfs.From(osvfs.FS())) + pnpApi := pnp.GetPnpApi(core.Must(os.Getwd())) + var fs vfs.FS + if pnpApi != nil { + fs = zipvfs.From(osvfs.FS()) + } else { + fs = osvfs.FS() + } + defaultLibraryPath := bundled.LibPath() typingsLocation := getGlobalTypingsCacheLocation() @@ -47,7 +56,7 @@ func runLSP(args []string) int { Out: lsp.ToWriter(os.Stdout), Err: os.Stderr, Cwd: core.Must(os.Getwd()), - FS: fs, + FS: bundled.WrapFS(fs), DefaultLibraryPath: defaultLibraryPath, TypingsLocation: typingsLocation, }) diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index f9e80f30a6..099d45cc73 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/execute/tsc" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" @@ -67,9 +68,17 @@ func newSystem() *osSys { os.Exit(int(tsc.ExitStatusInvalidProject_OutputsSkipped)) } + pnpApi := pnp.GetPnpApi(tspath.NormalizePath(cwd)) + var fs vfs.FS + if pnpApi != nil { + fs = zipvfs.From(osvfs.FS()) + } else { + fs = osvfs.FS() + } + return &osSys{ cwd: tspath.NormalizePath(cwd), - fs: bundled.WrapFS(zipvfs.From(osvfs.FS())), + fs: bundled.WrapFS(fs), defaultLibraryPath: bundled.LibPath(), writer: os.Stdout, start: time.Now(), diff --git a/internal/api/server.go b/internal/api/server.go index 85ec7bf773..e87a19a42c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -15,6 +15,7 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/vfs" @@ -94,12 +95,20 @@ func NewServer(options *ServerOptions) *Server { panic("Cwd is required") } + pnpApi := pnp.GetPnpApi(options.Cwd) + var fs vfs.FS + if pnpApi != nil { + fs = zipvfs.From(osvfs.FS()) + } else { + fs = osvfs.FS() + } + server := &Server{ r: bufio.NewReader(options.In), w: bufio.NewWriter(options.Out), stderr: options.Err, cwd: options.Cwd, - fs: bundled.WrapFS(zipvfs.From(osvfs.FS())), + fs: bundled.WrapFS(fs), defaultLibraryPath: options.DefaultLibraryPath, } logger := logging.NewLogger(options.Err) From 6cbb866e601aa84c921f9879d2dcf0d53ab11d9f Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 11 Sep 2025 15:13:59 +0200 Subject: [PATCH 19/48] Extract tryGetModuleNameFromPnp --- internal/modulespecifiers/specifiers.go | 180 +++++++++++++++++------- 1 file changed, 129 insertions(+), 51 deletions(-) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 7d255bde59..885cc5d2e3 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -696,7 +696,10 @@ func tryGetModuleNameFromRootDirs( return processEnding(shortest, allowedEndings, compilerOptions, host) } -func tryGetModuleNameAsNodeModule( +// TODO test this feature +// Help to identify the feature to test: when you're adding a new symbol into the file, TS will suggest you to import it from another file. +// To do that it'll map that other file path into an import path, and that's the function responsible for that. +func tryGetModuleNameAsPnpPackage( pathObj ModulePath, info Info, importingSourceFile SourceFileForSpecifierGeneration, @@ -706,52 +709,52 @@ func tryGetModuleNameAsNodeModule( packageNameOnly bool, overrideMode core.ResolutionMode, ) string { + pnpApi := pnp.GetPnpApi(importingSourceFile.FileName()) + if pnpApi == nil { + return "" + } + parts := GetNodeModulePathParts(pathObj.FileName) - // TODO understand what feature this part impacts pnpPackageName := "" + fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) + toLocator, _ := pnpApi.FindLocator(pathObj.FileName) - pnpApi := pnp.GetPnpApi(importingSourceFile.FileName()) - if pnpApi != nil { - fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) - toLocator, _ := pnpApi.FindLocator(pathObj.FileName) - - // Don't use the package name when the imported file is inside - // the source directory (prefer a relative path instead) - if fromLocator == toLocator { - return "" - } + // Don't use the package name when the imported file is inside + // the source directory (prefer a relative path instead) + if fromLocator == toLocator { + return "" + } - if fromLocator != nil && toLocator != nil { - fromInfo := pnpApi.GetPackage(fromLocator) + if fromLocator != nil && toLocator != nil { + fromInfo := pnpApi.GetPackage(fromLocator) - useToLocator := false + useToLocator := false - for i := range fromInfo.PackageDependencies { - isAlias := fromInfo.PackageDependencies[i].IsAlias() - if isAlias && fromInfo.PackageDependencies[i].AliasName == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { - useToLocator = true - break - } else if fromInfo.PackageDependencies[i].Ident == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { - useToLocator = true - break - } + for i := range fromInfo.PackageDependencies { + isAlias := fromInfo.PackageDependencies[i].IsAlias() + if isAlias && fromInfo.PackageDependencies[i].AliasName == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } else if fromInfo.PackageDependencies[i].Ident == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break } + } - if useToLocator { - pnpPackageName = toLocator.Name - } + if useToLocator { + pnpPackageName = toLocator.Name } + } - if parts != nil { - toInfo := pnpApi.GetPackage(toLocator) - parts = &NodeModulePathParts{ - TopLevelNodeModulesIndex: -1, - TopLevelPackageNameIndex: -1, - // The last character from packageLocation is the trailing "/", we want to point to it - PackageRootIndex: len(toInfo.PackageLocation) - 1, - FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), - } + if parts != nil { + toInfo := pnpApi.GetPackage(toLocator) + parts = &NodeModulePathParts{ + TopLevelNodeModulesIndex: -1, + TopLevelPackageNameIndex: -1, + // The last character from packageLocation is the trailing "/", we want to point to it + PackageRootIndex: len(toInfo.PackageLocation) - 1, + FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), } } @@ -763,7 +766,6 @@ func tryGetModuleNameAsNodeModule( preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "") allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone) - caseSensitive := host.UseCaseSensitiveFileNames() moduleSpecifier := pathObj.FileName isPackageRootPath := false if !packageNameOnly { @@ -813,24 +815,100 @@ func tryGetModuleNameAsNodeModule( return "" } - // If PnP is enabled the node_modules entries we'll get will always be relevant even if they - // are located in a weird path apparently outside of the source directory - if pnpApi == nil { - globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation() - // Get a path that's relative to node_modules or the importing file's path - // if node_modules folder is in this folder or any of its parent folders, no need to keep it. - pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex] + // If the module was found in @types, get the actual Node package name + nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] + if pnpPackageName != "" { + nodeModulesDirectoryName = pnpPackageName + moduleSpecifier[parts.PackageRootIndex:] + } - if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) { - return "" + return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) +} + +func tryGetModuleNameAsNodeModule( + pathObj ModulePath, + info Info, + importingSourceFile SourceFileForSpecifierGeneration, + host ModuleSpecifierGenerationHost, + options *core.CompilerOptions, + userPreferences UserPreferences, + packageNameOnly bool, + overrideMode core.ResolutionMode, +) string { + pnpModuleName := tryGetModuleNameAsPnpPackage(pathObj, info, importingSourceFile, host, options, userPreferences, packageNameOnly, overrideMode) + if pnpModuleName != "" { + return pnpModuleName + } + + parts := GetNodeModulePathParts(pathObj.FileName) + if parts == nil { + return "" + } + + // Simplify the full file path to something that can be resolved by Node. + preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "") + allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone) + + caseSensitive := host.UseCaseSensitiveFileNames() + moduleSpecifier := pathObj.FileName + isPackageRootPath := false + if !packageNameOnly { + packageRootIndex := parts.PackageRootIndex + var moduleFileName string + for true { + // If the module could be imported by a directory name, use that directory's name + pkgJsonResults := tryDirectoryWithPackageJson( + *parts, + pathObj, + importingSourceFile, + host, + overrideMode, + options, + allowedEndings, + "", + ) + moduleFileToTry := pkgJsonResults.moduleFileToTry + packageRootPath := pkgJsonResults.packageRootPath + blockedByExports := pkgJsonResults.blockedByExports + verbatimFromExports := pkgJsonResults.verbatimFromExports + if blockedByExports { + return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution + } + if verbatimFromExports { + return moduleFileToTry + } + //} + if len(packageRootPath) > 0 { + moduleSpecifier = packageRootPath + isPackageRootPath = true + break + } + if len(moduleFileName) == 0 { + moduleFileName = moduleFileToTry + } + // try with next level of directory + packageRootIndex = core.IndexAfter(pathObj.FileName, "/", packageRootIndex+1) + if packageRootIndex == -1 { + moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host) + break + } } } + if pathObj.IsRedirect && !isPackageRootPath { + return "" + } + + globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation() + // Get a path that's relative to node_modules or the importing file's path + // if node_modules folder is in this folder or any of its parent folders, no need to keep it. + pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex] + + if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) { + return "" + } + // If the module was found in @types, get the actual Node package name nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] - if pnpPackageName != "" { - nodeModulesDirectoryName = pnpPackageName + moduleSpecifier[parts.PackageRootIndex:] - } return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } @@ -850,7 +928,7 @@ func tryDirectoryWithPackageJson( overrideMode core.ResolutionMode, options *core.CompilerOptions, allowedEndings []ModuleSpecifierEnding, - pnpPackageName string, + packageNameOverride string, ) pkgJsonDirAttemptResult { rootIdx := parts.PackageRootIndex if rootIdx == -1 { @@ -884,7 +962,7 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := pnpPackageName + packageName := packageNameOverride if packageName == "" { packageName = GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } From 7db373fe94107d6d37261a72bb270d611a7c6a60 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 12 Sep 2025 13:31:22 +0200 Subject: [PATCH 20/48] Mark files from __virtual__ folders as ExternalLibraryImport --- internal/module/resolver.go | 6 +++--- internal/pnp/pnp.go | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 4d6eeeae64..acf7ed76ae 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -476,7 +476,7 @@ func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, false, true) return r.createResolvedModule( resolved, - resolved != nil && strings.Contains(resolved.path, "/node_modules/"), + resolved != nil && (strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path)), ) } return r.createResolvedModule(nil, false) @@ -1079,7 +1079,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectoryImpl(ext ext } func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule { - isExternalLibraryImport := resolved != nil && strings.Contains(resolved.path, "/node_modules/") + isExternalLibraryImport := resolved != nil && (strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path)) if r.compilerOptions.PreserveSymlinks != core.TSTrue && isExternalLibraryImport && resolved.originalPath == "" && @@ -1127,7 +1127,7 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.ResolvedFileName = resolved.path resolvedTypeReferenceDirective.Primary = primary resolvedTypeReferenceDirective.PackageId = resolved.packageId - resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") + resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path) if r.compilerOptions.PreserveSymlinks != core.TSTrue { originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path) diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index f0b3841bde..19d6f291b5 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -1,6 +1,7 @@ package pnp import ( + "strings" "sync" "sync/atomic" ) @@ -47,3 +48,10 @@ func GetPnpApi(filePath string) *PnpApi { isPnpApiInitialized.Store(1) return cachedPnpApi } + +// Checks for `IsFromExternalLibrary“ only look at the presence of `/node_modules/` in the path, +// but some virtual pnp packages don't have this folder, while they should still be considered external libraries +// This function is used whenever `IsFromExternalLibrary` is evaluated +func IsPnpVirtualPath(path string) bool { + return strings.Contains(path, "/__virtual__/") +} From 512f118718feaa4f38705c51f9718e3445c5c2aa Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 15 Sep 2025 11:35:40 +0200 Subject: [PATCH 21/48] Make go to definition work in zip files + add test case --- internal/ls/converters_test.go | 6 ++++++ internal/lsp/lsproto/lsp.go | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/ls/converters_test.go b/internal/ls/converters_test.go index 25fc94bce2..2faa0af22e 100644 --- a/internal/ls/converters_test.go +++ b/internal/ls/converters_test.go @@ -31,6 +31,9 @@ func TestDocumentURIToFileName(t *testing.T) { {"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"}, {"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"}, + {"zip:///path/to/archive.zip/file.ts", "/path/to/archive.zip/file.ts"}, + {"zip:///d:/work/tsgo932/lib/archive.zip/utils.ts", "d:/work/tsgo932/lib/archive.zip/utils.ts"}, + {"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"}, {"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"}, {"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"}, @@ -69,6 +72,9 @@ func TestFileNameToDocumentURI(t *testing.T) { {"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"}, {"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"}, + {"/path/to/archive.zip/file.ts", "zip:///path/to/archive.zip/file.ts"}, + {"d:/work/tsgo932/lib/archive.zip/utils.ts", "zip:///d%3A/work/tsgo932/lib/archive.zip/utils.ts"}, + {"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"}, {"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"}, {"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"}, diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index dd172077c2..85727faccd 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -14,7 +14,7 @@ import ( type DocumentUri string // !!! func (uri DocumentUri) FileName() string { - if strings.HasPrefix(string(uri), "file://") { + if strings.HasPrefix(string(uri), "file://") || strings.HasPrefix(string(uri), "zip:") { parsed := core.Must(url.Parse(string(uri))) if parsed.Host != "" { return "//" + parsed.Host + parsed.Path @@ -23,7 +23,6 @@ func (uri DocumentUri) FileName() string { } // Leave all other URIs escaped so we can round-trip them. - scheme, path, ok := strings.Cut(string(uri), ":") if !ok { panic(fmt.Sprintf("invalid URI: %s", uri)) From c7f184672fb94f607cc96e52e544d345ac7463a0 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 15 Sep 2025 14:31:05 +0200 Subject: [PATCH 22/48] Only display importable symbols on completion from pnpapi --- internal/ls/autoimports.go | 6 ++++++ internal/pnp/pnpapi.go | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index 3b38e0d637..d2f39a2c9e 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -17,6 +17,7 @@ import ( "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -441,6 +442,11 @@ func (l *LanguageService) isImportable( // } fromPath := fromFile.FileName() + pnpApi := pnp.GetPnpApi(fromPath) + if pnpApi != nil { + return pnpApi.IsImportable(fromPath, toFile.FileName()) + } + useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() modulePaths := modulespecifiers.GetEachFileNameOfModule( diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 66dcac4ea6..7c2dd5e091 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -271,3 +271,27 @@ func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { return typeRoots } + +func (p *PnpApi) IsImportable(fromFileName string, toFileName string) bool { + fromLocator, errFromLocator := p.FindLocator(fromFileName) + toLocator, errToLocator := p.FindLocator(toFileName) + + if fromLocator == nil || toLocator == nil || errFromLocator != nil || errToLocator != nil { + return false + } + + fromInfo := p.GetPackage(fromLocator) + for _, dep := range fromInfo.PackageDependencies { + if dep.Reference == toLocator.Reference { + if dep.IsAlias() && dep.AliasName == toLocator.Name { + return true + } + + if dep.Ident == toLocator.Name { + return true + } + } + } + + return false +} From a8e777e6a62985cc0d749834e75a7e79415550b5 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 17 Sep 2025 14:45:18 +0200 Subject: [PATCH 23/48] Fix import suggestion and autocomplete with yarn pnp --- internal/modulespecifiers/specifiers.go | 35 ++++++++----------------- internal/pnp/pnp.go | 12 +++++++++ internal/pnp/pnpapi.go | 9 +++++++ 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 885cc5d2e3..4afc609df2 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -272,23 +272,12 @@ func GetEachFileNameOfModule( // so we only need to remove them from the realpath filenames. for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { - isInNodeModules := ContainsNodeModules(p) - - // TODO: understand what this change actually impacts - if !isInNodeModules { - pnpApi := pnp.GetPnpApi(p) - if pnpApi != nil { - fromLocator, _ := pnpApi.FindLocator(importingFileName) - toLocator, _ := pnpApi.FindLocator(p) - if fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name { - isInNodeModules = true - } - } - } - results = append(results, ModulePath{ - FileName: p, - IsInNodeModules: isInNodeModules, + FileName: p, + // TODO: test this + // It impacts tagging external workspace dependencies as module dependencies, to trigger a module name resolver for + // import suggestions + IsInNodeModules: ContainsNodeModules(p) || pnp.IsInPnpModule(importingFileName, p), IsRedirect: referenceRedirect == p, }) } @@ -331,7 +320,7 @@ func GetEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p) || pnp.IsInPnpModule(importingFileName, p), IsRedirect: referenceRedirect == p, }) } @@ -714,8 +703,6 @@ func tryGetModuleNameAsPnpPackage( return "" } - parts := GetNodeModulePathParts(pathObj.FileName) - pnpPackageName := "" fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) toLocator, _ := pnpApi.FindLocator(pathObj.FileName) @@ -747,14 +734,15 @@ func tryGetModuleNameAsPnpPackage( } } - if parts != nil { + var parts *NodeModulePathParts + if toLocator != nil { toInfo := pnpApi.GetPackage(toLocator) + packageRootAbsolutePath := pnpApi.GetPackageLocationAbsolutePath(toInfo) parts = &NodeModulePathParts{ TopLevelNodeModulesIndex: -1, TopLevelPackageNameIndex: -1, - // The last character from packageLocation is the trailing "/", we want to point to it - PackageRootIndex: len(toInfo.PackageLocation) - 1, - FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), + PackageRootIndex: len(packageRootAbsolutePath), + FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), } } @@ -909,7 +897,6 @@ func tryGetModuleNameAsNodeModule( // If the module was found in @types, get the actual Node package name nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] - return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index 19d6f291b5..fb9be4c782 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -55,3 +55,15 @@ func GetPnpApi(filePath string) *PnpApi { func IsPnpVirtualPath(path string) bool { return strings.Contains(path, "/__virtual__/") } + +func IsInPnpModule(fromFileName string, toFileName string) bool { + pnpApi := GetPnpApi(fromFileName) + if pnpApi == nil { + return false + } + + fromLocator, _ := pnpApi.FindLocator(fromFileName) + toLocator, _ := pnpApi.FindLocator(toFileName) + // The targeted filename is in a pnp module different from the requesting filename + return fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name +} diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 7c2dd5e091..8b63eb3cf0 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -295,3 +295,12 @@ func (p *PnpApi) IsImportable(fromFileName string, toFileName string) bool { return false } + +func (p *PnpApi) GetPackageLocationAbsolutePath(packageInfo *PackageInfo) string { + if packageInfo == nil { + return "" + } + + packageLocation := packageInfo.PackageLocation + return filepath.Join(p.manifest.dirPath, packageLocation) +} From cc09cad777410fbb67a4df826b28265345256b89 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 17 Sep 2025 14:49:16 +0200 Subject: [PATCH 24/48] Remove fonction that has been moved after rebase --- internal/project/project.go | 63 ------------------------------------- 1 file changed, 63 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index ef1684d3d0..17689ce507 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -175,69 +175,6 @@ func NewProject( return project } -// GetResolvedProjectReference implements compiler.CompilerHost. -func (p *Project) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - return nil - // config := p.host.configFileRegistry.acquireConfigForProject(fileName, path, p, nil) - // if config == nil { - // return nil - // } - // TODO fix this and identify cases where we do need this - - // With Plug'n'Play, dependencies that list peer dependencies - // are "virtualized": they are resolved to a unique (virtual) - // path that the underlying filesystem layer then resolve back - // to the original location. - // - // When a workspace depends on another workspace with peer - // dependencies, this other workspace will thus be resolved to - // a unique path that won't match what the initial project has - // listed in its `references` field, and TS thus won't leverage - // the reference at all. - // - // To avoid that, we compute here the virtualized paths for the - // user-provided references in our references by directly querying - // the PnP API. This way users don't have to know the virtual paths, - // but we still support them just fine even through references. - // pnpApi := pnp.GetPnpApi(fileName) - // if pnpApi != nil { - // basePath := p.GetCurrentDirectory() - - // getPnpPath := func(path string) string { - // targetLocator, err := pnpApi.FindLocator(path + "/") - // if err != nil { - // return path - // } - - // packageLocation := tspath.ResolvePath(basePath, pnpApi.GetPackage(targetLocator).PackageLocation) - - // compareOptions := tspath.ComparePathsOptions{ - // UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(), - // CurrentDirectory: basePath, - // } - - // request := tspath.CombinePaths(targetLocator.Name, tspath.GetRelativePathFromDirectory(packageLocation, path, compareOptions)) - // unqualified, err := pnpApi.ResolveToUnqualified(request, basePath+"/") - // if err != nil { - // return path - // } - - // return unqualified - // } - - // for _, ref := range config.ProjectReferences() { - // ref.Path = getPnpPath(ref.Path) - // } - - // fmt.Println("Project refs after") - // for _, ref := range config.ProjectReferences() { - // fmt.Println("Project ref", ref.Path) - // } - // } - - // return config -} - func (p *Project) Name() string { return p.configFileName } From 9c27cbe28702541bf9ebd41134760ef93148c4c2 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 22 Sep 2025 15:22:13 +0200 Subject: [PATCH 25/48] Make GetEffectiveTypeRoots use the correct baseDir for pnp resolution --- internal/core/compileroptions.go | 38 ++++++++++++++++---------------- internal/module/resolver.go | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 34acf17088..8fc64b261a 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -294,12 +294,27 @@ func (options *CompilerOptions) GetJSXTransformEnabled() bool { } func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) (result []string, fromConfig bool) { - nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(currentDirectory) + if options.TypeRoots != nil { + return options.TypeRoots, true + } + var baseDir string + if options.ConfigFilePath != "" { + baseDir = tspath.GetDirectoryPath(options.ConfigFilePath) + } else { + baseDir = currentDirectory + if baseDir == "" { + // This was accounted for in the TS codebase, but only for third-party API usage + // where the module resolution host does not provide a getCurrentDirectory(). + panic("cannot get effective type roots without a config file path or current directory") + } + } + + nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(baseDir) pnpTypes := []string{} - pnpApi := pnp.GetPnpApi(currentDirectory) + pnpApi := pnp.GetPnpApi(baseDir) if pnpApi != nil { - pnpTypes = pnpApi.GetPnpTypeRoots(currentDirectory) + pnpTypes = pnpApi.GetPnpTypeRoots(baseDir) } if len(nmTypes) > 0 { @@ -313,22 +328,7 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) ( return nil, false } -func (options *CompilerOptions) GetNodeModulesTypeRoots(currentDirectory string) (result []string, fromConfig bool) { - if options.TypeRoots != nil { - return options.TypeRoots, true - } - var baseDir string - if options.ConfigFilePath != "" { - baseDir = tspath.GetDirectoryPath(options.ConfigFilePath) - } else { - baseDir = currentDirectory - if baseDir == "" { - // This was accounted for in the TS codebase, but only for third-party API usage - // where the module resolution host does not provide a getCurrentDirectory(). - panic("cannot get effective type roots without a config file path or current directory") - } - } - +func (options *CompilerOptions) GetNodeModulesTypeRoots(baseDir string) (result []string, fromConfig bool) { typeRoots := make([]string, 0, strings.Count(baseDir, "/")) tspath.ForEachAncestorDirectory(baseDir, func(dir string) (any, bool) { typeRoots = append(typeRoots, tspath.CombinePaths(dir, "node_modules", "@types")) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index acf7ed76ae..feccf6da35 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -208,7 +208,7 @@ func (r *Resolver) ResolveTypeReferenceDirective( compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) - typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(containingDirectory) + typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory()) if traceBuilder != nil { traceBuilder.write(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2.Format(typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ","))) traceBuilder.traceResolutionUsingProjectReference(redirectedReference) From 4e844d1b2b13596f786d9bd3ff91f004a57b0cb6 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 22 Sep 2025 15:22:36 +0200 Subject: [PATCH 26/48] Fix parsing for pnpExclusionList --- internal/pnp/manifestparser.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index 11aad0286e..b46f8ba14c 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -150,10 +150,12 @@ func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpM if exclusions, ok := rawData["fallbackExclusionList"].([]interface{}); ok { for _, exclusion := range exclusions { - if exclusionMap, ok := exclusion.(map[string]interface{}); ok { + if exclusionArr, ok := exclusion.([]interface{}); ok && len(exclusionArr) == 2 { + name := parseString(exclusionArr[0]) + entries := parseStringArray(exclusionArr[1]) exclusionEntry := &FallbackExclusion{ - Name: getField(exclusionMap, "name", parseString), - Entries: getField(exclusionMap, "entries", parseStringArray), + Name: name, + Entries: entries, } data.fallbackExclusionMap[exclusionEntry.Name] = exclusionEntry } From e2bbb8b82db8cf6ac431a6f08a4009d35935202e Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 22 Sep 2025 17:17:47 +0200 Subject: [PATCH 27/48] Rename zipvfs to pnpvfs --- cmd/tsgo/lsp.go | 4 +- cmd/tsgo/sys.go | 4 +- internal/api/server.go | 4 +- .../{zipvfs/zipvfs.go => pnpvfs/pnpvfs.go} | 91 +++++++++---------- .../zipvfs_test.go => pnpvfs/pnpvfs_test.go} | 34 ++++--- 5 files changed, 67 insertions(+), 70 deletions(-) rename internal/vfs/{zipvfs/zipvfs.go => pnpvfs/pnpvfs.go} (67%) rename internal/vfs/{zipvfs/zipvfs_test.go => pnpvfs/pnpvfs_test.go} (84%) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 3306856a67..85c8fecf36 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -14,7 +14,7 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" - "github.com/microsoft/typescript-go/internal/vfs/zipvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) func runLSP(args []string) int { @@ -43,7 +43,7 @@ func runLSP(args []string) int { pnpApi := pnp.GetPnpApi(core.Must(os.Getwd())) var fs vfs.FS if pnpApi != nil { - fs = zipvfs.From(osvfs.FS()) + fs = pnpvfs.From(osvfs.FS()) } else { fs = osvfs.FS() } diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index 099d45cc73..b0bd2fefc9 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -12,7 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" - "github.com/microsoft/typescript-go/internal/vfs/zipvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/term" ) @@ -71,7 +71,7 @@ func newSystem() *osSys { pnpApi := pnp.GetPnpApi(tspath.NormalizePath(cwd)) var fs vfs.FS if pnpApi != nil { - fs = zipvfs.From(osvfs.FS()) + fs = pnpvfs.From(osvfs.FS()) } else { fs = osvfs.FS() } diff --git a/internal/api/server.go b/internal/api/server.go index e87a19a42c..cd8186bc9d 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -20,7 +20,7 @@ import ( "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" - "github.com/microsoft/typescript-go/internal/vfs/zipvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageType -output=stringer_generated.go @@ -98,7 +98,7 @@ func NewServer(options *ServerOptions) *Server { pnpApi := pnp.GetPnpApi(options.Cwd) var fs vfs.FS if pnpApi != nil { - fs = zipvfs.From(osvfs.FS()) + fs = pnpvfs.From(osvfs.FS()) } else { fs = osvfs.FS() } diff --git a/internal/vfs/zipvfs/zipvfs.go b/internal/vfs/pnpvfs/pnpvfs.go similarity index 67% rename from internal/vfs/zipvfs/zipvfs.go rename to internal/vfs/pnpvfs/pnpvfs.go index 23a0101143..83776487c6 100644 --- a/internal/vfs/zipvfs/zipvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -1,4 +1,4 @@ -package zipvfs +package pnpvfs import ( "archive/zip" @@ -20,53 +20,53 @@ type cachedZipReader struct { zipMTime time.Time } -type zipFS struct { +type pnpFS struct { fs vfs.FS maxOpenReaders int cachedZipReadersMap map[string]*cachedZipReader cacheReaderMutex sync.Mutex } -var _ vfs.FS = (*zipFS)(nil) +var _ vfs.FS = (*pnpFS)(nil) -func From(fs vfs.FS) *zipFS { - zipfs := &zipFS{ +func From(fs vfs.FS) *pnpFS { + pnpFS := &pnpFS{ fs: fs, maxOpenReaders: 80, cachedZipReadersMap: make(map[string]*cachedZipReader), cacheReaderMutex: sync.Mutex{}, } - return zipfs + return pnpFS } -func (zipfs *zipFS) DirectoryExists(path string) bool { +func (pnpFS *pnpFS) DirectoryExists(path string) bool { path, _, _ = resolveVirtual(path) if strings.HasSuffix(path, ".zip") { - return zipfs.fs.FileExists(path) + return pnpFS.fs.FileExists(path) } - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.DirectoryExists(formattedPath) } -func (zipfs *zipFS) FileExists(path string) bool { +func (pnpFS *pnpFS) FileExists(path string) bool { path, _, _ = resolveVirtual(path) if strings.HasSuffix(path, ".zip") { - return zipfs.fs.FileExists(path) + return pnpFS.fs.FileExists(path) } - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.FileExists(formattedPath) } -func (zipfs *zipFS) GetAccessibleEntries(path string) vfs.Entries { +func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries { path, hash, basePath := resolveVirtual(path) - fs, formattedPath, zipPath := getMatchingFS(zipfs, path) + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { @@ -82,60 +82,60 @@ func (zipfs *zipFS) GetAccessibleEntries(path string) vfs.Entries { return entries } -func (zipfs *zipFS) ReadFile(path string) (contents string, ok bool) { +func (pnpFS *pnpFS) ReadFile(path string) (contents string, ok bool) { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.ReadFile(formattedPath) } -func (zipfs *zipFS) Chtimes(path string, mtime time.Time, atime time.Time) error { +func (pnpFS *pnpFS) Chtimes(path string, mtime time.Time, atime time.Time) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.Chtimes(formattedPath, mtime, atime) } -func (zipfs *zipFS) Realpath(path string) string { +func (pnpFS *pnpFS) Realpath(path string) string { path, hash, basePath := resolveVirtual(path) - fs, formattedPath, zipPath := getMatchingFS(zipfs, path) + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) fullPath := filepath.Join(zipPath, fs.Realpath(formattedPath)) return makeVirtualPath(basePath, hash, fullPath) } -func (zipfs *zipFS) Remove(path string) error { +func (pnpFS *pnpFS) Remove(path string) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.Remove(formattedPath) } -func (zipfs *zipFS) Stat(path string) vfs.FileInfo { +func (pnpFS *pnpFS) Stat(path string) vfs.FileInfo { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.Stat(formattedPath) } -func (zipfs *zipFS) UseCaseSensitiveFileNames() bool { - return zipfs.fs.UseCaseSensitiveFileNames() +func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool { + return pnpFS.fs.UseCaseSensitiveFileNames() } -func (zipfs *zipFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { +func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { root, hash, basePath := resolveVirtual(root) - fs, formattedPath, zipPath := getMatchingFS(zipfs, root) + fs, formattedPath, zipPath := getMatchingFS(pnpFS, root) return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { fullPath := filepath.Join(zipPath, path) return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) })) } -func (zipfs *zipFS) WriteFile(path string, data string, writeByteOrderMark bool) error { +func (pnpFS *pnpFS) WriteFile(path string, data string, writeByteOrderMark bool) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(zipfs, path) + fs, formattedPath, _ := getMatchingFS(pnpFS, path) return fs.WriteFile(formattedPath, data, writeByteOrderMark) } @@ -147,50 +147,50 @@ func splitZipPath(path string) (string, string) { return parts[0] + ".zip", "/" + parts[1] } -func getMatchingFS(zipfs *zipFS, path string) (vfs.FS, string, string) { +func getMatchingFS(pnpFS *pnpFS, path string) (vfs.FS, string, string) { if !tspath.IsZipPath(path) { - return zipfs.fs, path, "" + return pnpFS.fs, path, "" } zipPath, internalPath := splitZipPath(path) - zipStat := zipfs.fs.Stat(zipPath) + zipStat := pnpFS.fs.Stat(zipPath) if zipStat == nil { - return zipfs.fs, path, "" + return pnpFS.fs, path, "" } var usedReader *cachedZipReader - zipfs.cacheReaderMutex.Lock() - defer zipfs.cacheReaderMutex.Unlock() + pnpFS.cacheReaderMutex.Lock() + defer pnpFS.cacheReaderMutex.Unlock() zipMTime := zipStat.ModTime() - cachedReader, ok := zipfs.cachedZipReadersMap[zipPath] + cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath] if ok && cachedReader.zipMTime.Equal(zipMTime) { cachedReader.lastUsed = time.Now() usedReader = cachedReader } else { zipReader, err := zip.OpenReader(zipPath) if err != nil { - return zipfs.fs, path, "" + return pnpFS.fs, path, "" } - if len(zipfs.cachedZipReadersMap) >= zipfs.maxOpenReaders { - zipfs.deleteOldestReader() + if len(pnpFS.cachedZipReadersMap) >= pnpFS.maxOpenReaders { + pnpFS.deleteOldestReader() } usedReader = &cachedZipReader{reader: zipReader, lastUsed: time.Now(), zipMTime: zipMTime} - zipfs.cachedZipReadersMap[zipPath] = usedReader + pnpFS.cachedZipReadersMap[zipPath] = usedReader } - return iovfs.From(usedReader.reader, zipfs.fs.UseCaseSensitiveFileNames()), internalPath, zipPath + return iovfs.From(usedReader.reader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath } -func (zipfs *zipFS) deleteOldestReader() { +func (pnpFS *pnpFS) deleteOldestReader() { var oldestReader *cachedZipReader var oldestReaderPath string - for path, reader := range zipfs.cachedZipReadersMap { + for path, reader := range pnpFS.cachedZipReadersMap { if oldestReader == nil || reader.lastUsed.Before(oldestReader.lastUsed) { oldestReader = reader oldestReaderPath = path @@ -199,11 +199,10 @@ func (zipfs *zipFS) deleteOldestReader() { if oldestReader != nil { oldestReader.reader.Close() - delete(zipfs.cachedZipReadersMap, oldestReaderPath) + delete(pnpFS.cachedZipReadersMap, oldestReaderPath) } } -// TODO insert virtual path handling more properly (with a vfs wrapper maybe) func resolveVirtual(path string) (realPath string, hash string, basePath string) { idx := strings.Index(path, "/__virtual__/") if idx == -1 { diff --git a/internal/vfs/zipvfs/zipvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go similarity index 84% rename from internal/vfs/zipvfs/zipvfs_test.go rename to internal/vfs/pnpvfs/pnpvfs_test.go index 82bdffc835..06f839a03d 100644 --- a/internal/vfs/zipvfs/zipvfs_test.go +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -1,4 +1,4 @@ -package zipvfs_test +package pnpvfs_test import ( "archive/zip" @@ -8,13 +8,11 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" - "github.com/microsoft/typescript-go/internal/vfs/zipvfs" "gotest.tools/v3/assert" ) -// TODO: refine generated tests - func createTestZip(t *testing.T, files map[string]string) string { t.Helper() @@ -38,7 +36,7 @@ func createTestZip(t *testing.T, files map[string]string) string { return zipPath } -func TestZipVFS_BasicFileOperations(t *testing.T) { +func TestPnpVfs_BasicFileOperations(t *testing.T) { t.Parallel() underlyingFS := vfstest.FromMap(map[string]string{ @@ -46,7 +44,7 @@ func TestZipVFS_BasicFileOperations(t *testing.T) { "/project/package.json": `{"name": "test"}`, }, true) - fs := zipvfs.From(underlyingFS) + fs := pnpvfs.From(underlyingFS) assert.Assert(t, fs.FileExists("/project/src/index.ts")) assert.Assert(t, !fs.FileExists("/project/nonexistent.ts")) @@ -58,7 +56,7 @@ func TestZipVFS_BasicFileOperations(t *testing.T) { assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) } -func TestZipVFS_ZipFileDetection(t *testing.T) { +func TestPnpVfs_ZipFileDetection(t *testing.T) { t.Parallel() zipFiles := map[string]string{ @@ -72,7 +70,7 @@ func TestZipVFS_ZipFileDetection(t *testing.T) { zipPath: "zip content placeholder", }, true) - fs := zipvfs.From(underlyingFS) + fs := pnpvfs.From(underlyingFS) fmt.Println(zipPath) assert.Assert(t, fs.FileExists(zipPath)) @@ -83,10 +81,10 @@ func TestZipVFS_ZipFileDetection(t *testing.T) { _, _ = fs.ReadFile(zipInternalPath) } -func TestZipVFS_ErrorHandling(t *testing.T) { +func TestPnpVfs_ErrorHandling(t *testing.T) { t.Parallel() - fs := zipvfs.From(osvfs.FS()) + fs := pnpvfs.From(osvfs.FS()) t.Run("NonexistentZipFile", func(t *testing.T) { result := fs.FileExists("/nonexistent/path/archive.zip/file.txt") @@ -106,16 +104,16 @@ func TestZipVFS_ErrorHandling(t *testing.T) { }) } -func TestZipVFS_CaseSensitivity(t *testing.T) { +func TestPnpVfs_CaseSensitivity(t *testing.T) { t.Parallel() - sensitiveFS := zipvfs.From(vfstest.FromMap(map[string]string{}, true)) + sensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) assert.Assert(t, sensitiveFS.UseCaseSensitiveFileNames()) - insensitiveFS := zipvfs.From(vfstest.FromMap(map[string]string{}, false)) + insensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, false)) assert.Assert(t, !insensitiveFS.UseCaseSensitiveFileNames()) } -func TestZipVFS_FallbackToRegularFiles(t *testing.T) { +func TestPnpVfs_FallbackToRegularFiles(t *testing.T) { t.Parallel() tmpDir := t.TempDir() @@ -123,7 +121,7 @@ func TestZipVFS_FallbackToRegularFiles(t *testing.T) { err := os.WriteFile(regularFile, []byte("regular content"), 0o644) assert.NilError(t, err) - fs := zipvfs.From(osvfs.FS()) + fs := pnpvfs.From(osvfs.FS()) assert.Assert(t, fs.FileExists(regularFile)) @@ -149,7 +147,7 @@ func TestZipPath_Detection(t *testing.T) { {"/absolute/archive.zip/file.txt", true}, } - fs := zipvfs.From(vfstest.FromMap(map[string]string{}, true)) + fs := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { @@ -159,7 +157,7 @@ func TestZipPath_Detection(t *testing.T) { } } -func TestZipVFS_RealZipIntegration(t *testing.T) { +func TestPnpVfs_RealZipIntegration(t *testing.T) { t.Parallel() zipFiles := map[string]string{ @@ -170,7 +168,7 @@ func TestZipVFS_RealZipIntegration(t *testing.T) { } zipPath := createTestZip(t, zipFiles) - fs := zipvfs.From(osvfs.FS()) + fs := pnpvfs.From(osvfs.FS()) assert.Assert(t, fs.FileExists(zipPath)) From 2bd397fbf1cc16a6350e37ee398771f36e660b7f Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 22 Sep 2025 17:22:56 +0200 Subject: [PATCH 28/48] Always return true for pnpvfs.UseCaseSensitiveFileNames() --- internal/vfs/osvfs/os.go | 2 -- internal/vfs/pnpvfs/pnpvfs.go | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/vfs/osvfs/os.go b/internal/vfs/osvfs/os.go index 5fc2a964a1..d811d6ff71 100644 --- a/internal/vfs/osvfs/os.go +++ b/internal/vfs/osvfs/os.go @@ -32,8 +32,6 @@ type osFS struct { // We do this right at startup to minimize the chance that executable gets moved or deleted. var isFileSystemCaseSensitive = func() bool { - // TODO: return true if pnp api is available - // win32/win64 are case insensitive platforms if runtime.GOOS == "windows" { return false diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index 83776487c6..cb4d4fece6 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -119,7 +119,8 @@ func (pnpFS *pnpFS) Stat(path string) vfs.FileInfo { } func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool { - return pnpFS.fs.UseCaseSensitiveFileNames() + // pnp fs is always case sensitive + return true } func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { From a96aab53d5e01afc24ca0984621ca43202f63e09 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 8 Oct 2025 09:48:01 +0200 Subject: [PATCH 29/48] Improve pnpvfs init --- cmd/tsgo/lsp.go | 7 +++---- cmd/tsgo/sys.go | 7 +++---- internal/api/server.go | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 85c8fecf36..c079f2255b 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -40,12 +40,11 @@ func runLSP(args []string) int { defer profileSession.Stop() } + var fs vfs.FS = osvfs.FS() + pnpApi := pnp.GetPnpApi(core.Must(os.Getwd())) - var fs vfs.FS if pnpApi != nil { - fs = pnpvfs.From(osvfs.FS()) - } else { - fs = osvfs.FS() + fs = pnpvfs.From(fs) } defaultLibraryPath := bundled.LibPath() diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index b0bd2fefc9..14fd41089c 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -68,12 +68,11 @@ func newSystem() *osSys { os.Exit(int(tsc.ExitStatusInvalidProject_OutputsSkipped)) } + var fs vfs.FS = osvfs.FS() + pnpApi := pnp.GetPnpApi(tspath.NormalizePath(cwd)) - var fs vfs.FS if pnpApi != nil { - fs = pnpvfs.From(osvfs.FS()) - } else { - fs = osvfs.FS() + fs = pnpvfs.From(fs) } return &osSys{ diff --git a/internal/api/server.go b/internal/api/server.go index 69ef07691e..6244227f7f 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -95,12 +95,11 @@ func NewServer(options *ServerOptions) *Server { panic("Cwd is required") } + var fs vfs.FS = osvfs.FS() + pnpApi := pnp.GetPnpApi(options.Cwd) - var fs vfs.FS if pnpApi != nil { - fs = pnpvfs.From(osvfs.FS()) - } else { - fs = osvfs.FS() + fs = pnpvfs.From(fs) } server := &Server{ From 227839e5f646c91f0fcba9f2867ff5a2df878700 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 8 Oct 2025 09:48:45 +0200 Subject: [PATCH 30/48] Extract pnp type roots appending --- internal/core/compileroptions.go | 16 ++-------------- internal/pnp/pnp.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 805bd728a0..8f2de216a1 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -318,21 +318,9 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) ( nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(baseDir) - pnpTypes := []string{} - pnpApi := pnp.GetPnpApi(baseDir) - if pnpApi != nil { - pnpTypes = pnpApi.GetPnpTypeRoots(baseDir) - } - - if len(nmTypes) > 0 { - return append(nmTypes, pnpTypes...), nmFromConfig - } - - if len(pnpTypes) > 0 { - return pnpTypes, false - } + typeRoots, nmFromConfig := pnp.AppendPnpTypeRoots(nmTypes, baseDir, nmFromConfig) - return nil, false + return typeRoots, nmFromConfig } func (options *CompilerOptions) GetNodeModulesTypeRoots(baseDir string) (result []string, fromConfig bool) { diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index fb9be4c782..2508f528bb 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -67,3 +67,21 @@ func IsInPnpModule(fromFileName string, toFileName string) bool { // The targeted filename is in a pnp module different from the requesting filename return fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name } + +func AppendPnpTypeRoots(nmTypes []string, baseDir string, nmFromConfig bool) ([]string, bool) { + pnpTypes := []string{} + pnpApi := GetPnpApi(baseDir) + if pnpApi != nil { + pnpTypes = pnpApi.GetPnpTypeRoots(baseDir) + } + + if len(nmTypes) > 0 { + return append(nmTypes, pnpTypes...), nmFromConfig + } + + if len(pnpTypes) > 0 { + return pnpTypes, false + } + + return nil, false +} From bd256ad6183adb3821270470a62e4f9174aa3667 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Wed, 8 Oct 2025 10:33:40 +0200 Subject: [PATCH 31/48] Add comments on pnp helpers and create a IsExternalLibraryImport helper --- internal/module/resolver.go | 11 ++++++++--- internal/pnp/pnp.go | 8 -------- internal/tspath/path.go | 11 +++++++++++ internal/vfs/pnpvfs/pnpvfs.go | 2 ++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index feccf6da35..6c5a504c88 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -476,7 +476,7 @@ func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, false, true) return r.createResolvedModule( resolved, - resolved != nil && (strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path)), + resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)), ) } return r.createResolvedModule(nil, false) @@ -960,6 +960,11 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions return continueSearching() } +/* +With Plug and Play, we directly resolve the path of the moduleName using the PnP API, instead of searching for it in the node_modules directory + +See github.com/microsoft/typescript-go/internal/pnp package for more details +*/ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectoryPnP(extensions extensions, directory string, typesScopeOnly bool) *resolved { if !typesScopeOnly { if packageResult := r.loadModuleFromPnpResolution(extensions, r.name, directory); !packageResult.shouldContinueSearching() { @@ -1079,7 +1084,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectoryImpl(ext ext } func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule { - isExternalLibraryImport := resolved != nil && (strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path)) + isExternalLibraryImport := resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)) if r.compilerOptions.PreserveSymlinks != core.TSTrue && isExternalLibraryImport && resolved.originalPath == "" && @@ -1127,7 +1132,7 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.ResolvedFileName = resolved.path resolvedTypeReferenceDirective.Primary = primary resolvedTypeReferenceDirective.PackageId = resolved.packageId - resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") || pnp.IsPnpVirtualPath(resolved.path) + resolvedTypeReferenceDirective.IsExternalLibraryImport = tspath.IsExternalLibraryImport(resolved.path) if r.compilerOptions.PreserveSymlinks != core.TSTrue { originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path) diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index 2508f528bb..5fb27440f4 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -1,7 +1,6 @@ package pnp import ( - "strings" "sync" "sync/atomic" ) @@ -49,13 +48,6 @@ func GetPnpApi(filePath string) *PnpApi { return cachedPnpApi } -// Checks for `IsFromExternalLibrary“ only look at the presence of `/node_modules/` in the path, -// but some virtual pnp packages don't have this folder, while they should still be considered external libraries -// This function is used whenever `IsFromExternalLibrary` is evaluated -func IsPnpVirtualPath(path string) bool { - return strings.Contains(path, "/__virtual__/") -} - func IsInPnpModule(fromFileName string, toFileName string) bool { pnpApi := GetPnpApi(fromFileName) if pnpApi == nil { diff --git a/internal/tspath/path.go b/internal/tspath/path.go index 7448ec9d70..fca73540b9 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -868,6 +868,17 @@ func IsExternalModuleNameRelative(moduleName string) bool { return PathIsRelative(moduleName) || IsRootedDiskPath(moduleName) } +func IsExternalLibraryImport(path string) bool { + // When PnP is enabled, some internal libraries can be resolved as virtual packages, which should be treated as external libraries + // Since these virtual pnp packages don't have a `/node_modules/` folder, we need to check for the presence of `/__virtual__/` in the path + // See https://yarnpkg.com/advanced/lexicon#virtual-package for more details + return strings.Contains(path, "/node_modules/") || isPnpVirtualPath(path) +} + +func isPnpVirtualPath(path string) bool { + return strings.Contains(path, "/__virtual__/") +} + type ComparePathsOptions struct { UseCaseSensitiveFileNames bool CurrentDirectory string diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index cb4d4fece6..b6dd6d2d81 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -204,6 +204,8 @@ func (pnpFS *pnpFS) deleteOldestReader() { } } +// 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 +// See https://yarnpkg.com/advanced/lexicon#virtual-package and https://yarnpkg.com/advanced/pnpapi#resolvevirtual for more details func resolveVirtual(path string) (realPath string, hash string, basePath string) { idx := strings.Index(path, "/__virtual__/") if idx == -1 { From df10a92c96e361f952a8aa22edcd7e1e07f44e0e Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 9 Oct 2025 10:20:38 +0200 Subject: [PATCH 32/48] Refine zipvfs tests and fix issue with GetAccessibleEntries --- internal/vfs/pnpvfs/pnpvfs.go | 4 +-- internal/vfs/pnpvfs/pnpvfs_test.go | 42 +++++++++++++++++++----------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index b6dd6d2d81..fd58a72e65 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -70,12 +70,12 @@ func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries { entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { - fullPath := filepath.Join(zipPath, dir) + fullPath := filepath.Join(zipPath, formattedPath, dir) entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) } for i, file := range entries.Files { - fullPath := filepath.Join(zipPath, file) + fullPath := filepath.Join(zipPath, formattedPath, file) entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) } diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go index 06f839a03d..996b9b3869 100644 --- a/internal/vfs/pnpvfs/pnpvfs_test.go +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" @@ -76,9 +77,11 @@ func TestPnpVfs_ZipFileDetection(t *testing.T) { assert.Assert(t, fs.FileExists(zipPath)) zipInternalPath := zipPath + "/src/index.ts" + assert.Assert(t, fs.FileExists(zipInternalPath)) - _ = fs.FileExists(zipInternalPath) - _, _ = fs.ReadFile(zipInternalPath) + content, ok := fs.ReadFile(zipInternalPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) } func TestPnpVfs_ErrorHandling(t *testing.T) { @@ -110,7 +113,8 @@ func TestPnpVfs_CaseSensitivity(t *testing.T) { sensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) assert.Assert(t, sensitiveFS.UseCaseSensitiveFileNames()) insensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, false)) - assert.Assert(t, !insensitiveFS.UseCaseSensitiveFileNames()) + // pnpvfs is always case sensitive + assert.Assert(t, insensitiveFS.UseCaseSensitiveFileNames()) } func TestPnpVfs_FallbackToRegularFiles(t *testing.T) { @@ -147,12 +151,9 @@ func TestZipPath_Detection(t *testing.T) { {"/absolute/archive.zip/file.txt", true}, } - fs := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) - for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { - _ = fs.FileExists(tc.path) - _, _ = fs.ReadFile(tc.path) + assert.Assert(t, tspath.IsZipPath(tc.path) == tc.shouldBeZip) }) } } @@ -174,14 +175,25 @@ func TestPnpVfs_RealZipIntegration(t *testing.T) { indexPath := zipPath + "/src/index.ts" packagePath := zipPath + "/package.json" - _ = fs.FileExists(indexPath) - _ = fs.FileExists(packagePath) - _ = fs.DirectoryExists(zipPath + "/src") + assert.Assert(t, fs.FileExists(indexPath)) + assert.Assert(t, fs.FileExists(packagePath)) + assert.Assert(t, fs.DirectoryExists(zipPath+"/src")) + + content, ok := fs.ReadFile(indexPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) + + content, ok = fs.ReadFile(packagePath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["package.json"]) + + entries := fs.GetAccessibleEntries(zipPath) + assert.DeepEqual(t, entries.Files, []string{zipPath + "/package.json", zipPath + "/tsconfig.json"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src"}) - _, _ = fs.ReadFile(indexPath) - _, _ = fs.ReadFile(packagePath) + entries = fs.GetAccessibleEntries(zipPath + "/src") + assert.DeepEqual(t, entries.Files, []string{zipPath + "/src/index.ts"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src/utils"}) - _ = fs.GetAccessibleEntries(zipPath) - _ = fs.GetAccessibleEntries(zipPath + "/src") - _ = fs.Realpath(indexPath) + assert.Equal(t, fs.Realpath(indexPath), indexPath) } From fe6e4a57ec1387b636bf4b4b78c13838e8bcf472 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 9 Oct 2025 15:12:20 +0200 Subject: [PATCH 33/48] Initialise the FS at the session level for LSP --- cmd/tsgo/lsp.go | 12 ++---------- internal/lsp/server.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index c079f2255b..8a15de6eea 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -9,12 +9,9 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp" - "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/pprof" "github.com/microsoft/typescript-go/internal/tspath" - "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" - "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) func runLSP(args []string) int { @@ -40,12 +37,7 @@ func runLSP(args []string) int { defer profileSession.Stop() } - var fs vfs.FS = osvfs.FS() - - pnpApi := pnp.GetPnpApi(core.Must(os.Getwd())) - if pnpApi != nil { - fs = pnpvfs.From(fs) - } + fs := bundled.WrapFS(osvfs.FS()) defaultLibraryPath := bundled.LibPath() typingsLocation := getGlobalTypingsCacheLocation() @@ -55,7 +47,7 @@ func runLSP(args []string) int { Out: lsp.ToWriter(os.Stdout), Err: os.Stderr, Cwd: core.Must(os.Getwd()), - FS: bundled.WrapFS(fs), + FS: fs, DefaultLibraryPath: defaultLibraryPath, TypingsLocation: typingsLocation, }) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 54f78f2e16..ab2796290e 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -20,11 +20,13 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/sync/errgroup" "golang.org/x/text/language" ) @@ -670,6 +672,13 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali cwd = s.cwd } + pnpApi := pnp.GetPnpApi(cwd) + + fs := s.fs + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + s.session = project.NewSession(&project.SessionInit{ Options: &project.SessionOptions{ CurrentDirectory: cwd, @@ -680,7 +689,7 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali LoggingEnabled: true, DebounceDelay: 500 * time.Millisecond, }, - FS: s.fs, + FS: fs, Logger: s.logger, Client: s, NpmExecutor: s, From 0b8d7da8d31e1594aa81b26d9423d1253e4e1898 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 9 Oct 2025 15:16:53 +0200 Subject: [PATCH 34/48] Add tests for virtual paths and panic on fs.WriteFile --- internal/vfs/pnpvfs/pnpvfs.go | 8 ++- internal/vfs/pnpvfs/pnpvfs_test.go | 86 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index fd58a72e65..c07f855c39 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -32,7 +32,7 @@ var _ vfs.FS = (*pnpFS)(nil) func From(fs vfs.FS) *pnpFS { pnpFS := &pnpFS{ fs: fs, - maxOpenReaders: 80, + maxOpenReaders: 80, // Max number of zip files that can be open at the same time cachedZipReadersMap: make(map[string]*cachedZipReader), cacheReaderMutex: sync.Mutex{}, } @@ -136,7 +136,11 @@ func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { func (pnpFS *pnpFS) WriteFile(path string, data string, writeByteOrderMark bool) error { path, _, _ = resolveVirtual(path) - fs, formattedPath, _ := getMatchingFS(pnpFS, path) + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + if zipPath != "" { + panic("cannot write to zip file") + } + return fs.WriteFile(formattedPath, data, writeByteOrderMark) } diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go index 996b9b3869..4c1a031d25 100644 --- a/internal/vfs/pnpvfs/pnpvfs_test.go +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -5,9 +5,12 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" + "github.com/microsoft/typescript-go/internal/testutil" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" @@ -55,6 +58,22 @@ func TestPnpVfs_BasicFileOperations(t *testing.T) { assert.Assert(t, fs.DirectoryExists("/project/src")) assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) + + var files []string + fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + + assert.DeepEqual(t, files, []string{"/project/package.json", "/project/src/index.ts"}) + + fs.WriteFile("/project/src/index.ts", "export const hello = 'world2';", false) + + content, ok = fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world2';", content) } func TestPnpVfs_ZipFileDetection(t *testing.T) { @@ -105,6 +124,17 @@ func TestPnpVfs_ErrorHandling(t *testing.T) { result := fs.FileExists(fakePath + "/file.txt") assert.Assert(t, !result) }) + + t.Run("WriteToZipFile", func(t *testing.T) { + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + } + zipPath := createTestZip(t, zipFiles) + + testutil.AssertPanics(t, func() { + fs.WriteFile(zipPath+"/src/index.ts", "hello, world", false) + }, "cannot write to zip file") + }) } func TestPnpVfs_CaseSensitivity(t *testing.T) { @@ -158,6 +188,47 @@ func TestZipPath_Detection(t *testing.T) { } } +func TestPnpVfs_VirtualPathHandling(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/packages/packageA/indexA.ts": "export const helloA = 'world';", + "/project/packages/packageA/package.json": `{"name": "packageA"}`, + "/project/packages/packageB/indexB.ts": "export const helloB = 'world';", + "/project/packages/packageB/package.json": `{"name": "packageB"}`, + }, true) + + fs := pnpvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/packages/__virtual__/packageA-virtual-123456/0/packageA/package.json")) + assert.Assert(t, fs.FileExists("/project/packages/subfolder/__virtual__/packageA-virtual-123456/1/packageA/package.json")) + + content, ok := fs.ReadFile("/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json") + assert.Assert(t, ok) + assert.Equal(t, `{"name": "packageB"}`, content) + + assert.Assert(t, fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/packageB")) + assert.Assert(t, !fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/nonexistent")) + + entries := fs.GetAccessibleEntries("/project/packages/__virtual__/packageB-virtual-123456/0/packageB") + assert.DeepEqual(t, entries.Files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) + assert.DeepEqual(t, entries.Directories, []string(nil)) + + files := []string{} + fs.WalkDir("/project/packages/__virtual__/packageB-virtual-123456/0/packageB", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.DeepEqual(t, files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) +} + func TestPnpVfs_RealZipIntegration(t *testing.T) { t.Parallel() @@ -196,4 +267,19 @@ func TestPnpVfs_RealZipIntegration(t *testing.T) { assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src/utils"}) assert.Equal(t, fs.Realpath(indexPath), indexPath) + + files := []string{} + fs.WalkDir(zipPath, func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.DeepEqual(t, files, []string{zipPath + "/package.json", zipPath + "/src/index.ts", zipPath + "/src/utils/helpers.ts", zipPath + "/tsconfig.json"}) + + assert.Assert(t, fs.FileExists(zipPath+"/src/__virtual__/src-virtual-123456/0/index.ts")) + + splitZipPath := strings.Split(zipPath, "/") + beforeZipVirtualPath := strings.Join(splitZipPath[0:len(splitZipPath)-2], "/") + "/__virtual__/zip-virtual-123456/0/" + strings.Join(splitZipPath[len(splitZipPath)-2:], "/") + "/src/index.ts" + assert.Assert(t, fs.FileExists(beforeZipVirtualPath)) } From be2aad8ef399582764ed48818c12f5a14d7ec9da Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 10 Oct 2025 13:57:16 +0200 Subject: [PATCH 35/48] Fix current unit tests --- _submodules/TypeScript | 2 +- internal/pnp/pnpapi.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_submodules/TypeScript b/_submodules/TypeScript index bc7d42611e..1ee9e0d9a2 160000 --- a/_submodules/TypeScript +++ b/_submodules/TypeScript @@ -1 +1 @@ -Subproject commit bc7d42611e35678c7cbddb104aa4b117a95ccdfa +Subproject commit 1ee9e0d9a24b629da3a8cae2748616af1dc8fc0c diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 8b63eb3cf0..7852c593a5 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -126,7 +126,7 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { } directoryPath = path.Dir(directoryPath) - if directoryPath == "/" { + if tspath.IsDiskPathRoot(directoryPath) { return nil, fmt.Errorf("no PnP manifest found") } } From 90bd2e050fa9569a3c5769b118f60d7565c5d8db Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 14 Oct 2025 15:57:45 +0200 Subject: [PATCH 36/48] Make a pnp cache compatible with tests --- internal/pnp/manifestparser.go | 4 ++ internal/pnp/pnp.go | 70 ++++++++++++++++++-- internal/testutil/harnessutil/harnessutil.go | 6 ++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index b46f8ba14c..554735a27a 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -109,6 +109,10 @@ func parseManifestFromPath(manifestDir string) (*PnpManifestData, error) { pnpDataString = b.String() } + return parseManifestFromData(pnpDataString, manifestDir) +} + +func parseManifestFromData(pnpDataString string, manifestDir string) (*PnpManifestData, error) { var rawData map[string]interface{} if err := json.Unmarshal([]byte(pnpDataString), &rawData); err != nil { return nil, fmt.Errorf("failed to parse JSON PnP data: %w", err) diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index 5fb27440f4..4b73a6b2e6 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -1,26 +1,76 @@ package pnp import ( + "runtime" + "strconv" + "strings" "sync" "sync/atomic" + "testing" ) var ( isPnpApiInitialized atomic.Uint32 cachedPnpApi *PnpApi pnpMu sync.Mutex + // testPnpCache stores per-goroutine PnP APIs for test isolation + // Key is goroutine ID (as int) + testPnpCache sync.Map // map[int]*PnpApi ) -// Clears the singleton PnP API cache -func ClearPnpCache() { - pnpMu.Lock() - defer pnpMu.Unlock() - cachedPnpApi = nil - isPnpApiInitialized.Store(0) +// getGoroutineID returns the current goroutine ID +// It is usually not recommended to work with goroutine IDs, but it is the most non-intrusive way to setup a parallel testing environment for PnP API +func getGoroutineID() int { + var buf [64]byte + n := runtime.Stack(buf[:], false) + idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] + id, _ := strconv.Atoi(idField) + return id +} + +// Sets the PnP API for the given manifest data and manifest directory, used for testing +// This creates a goroutine-specific cache entry that won't interfere with other parallel tests. +func OverridePnpApi(manifestDataRaw string) *PnpApi { + if manifestDataRaw == "" { + return nil + } + + var pnpApi *PnpApi + + manifestData, err := parseManifestFromData(manifestDataRaw, "/") + if err != nil { + pnpApi = nil + } else if manifestData != nil { + pnpApi = &PnpApi{ + url: "/", + manifest: manifestData, + } + } + + // Store in goroutine-specific cache using goroutine ID + // This allows parallel tests to have isolated PnP configurations + gid := getGoroutineID() + testPnpCache.Store(gid, pnpApi) + + return pnpApi +} + +// ClearTestPnpCache clears the test-specific PnP API cache for the current goroutine +func ClearTestPnpCache() { + gid := getGoroutineID() + testPnpCache.Delete(gid) } // GetPnpApi returns the PnP API for the given file path. Will return nil if the PnP API is not available. func GetPnpApi(filePath string) *PnpApi { + // If in a test, check for PnP API overrides + if testing.Testing() { + gid := getGoroutineID() + if api, ok := testPnpCache.Load(gid); ok { + return api.(*PnpApi) + } + } + // Check if PnP API is already initialized using atomic read (no lock needed) if isPnpApiInitialized.Load() == 1 { return cachedPnpApi @@ -48,6 +98,14 @@ func GetPnpApi(filePath string) *PnpApi { return cachedPnpApi } +// Clears the singleton PnP API cache +func ClearPnpCache() { + pnpMu.Lock() + defer pnpMu.Unlock() + cachedPnpApi = nil + isPnpApiInitialized.Store(0) +} + func IsInPnpModule(fromFileName string, toFileName string) bool { pnpApi := GetPnpApi(fromFileName) if pnpApi == nil { diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 772eb169c0..5616e4871b 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/testutil" @@ -208,6 +209,11 @@ func CompileFilesEx( fs = bundled.WrapFS(fs) fs = NewOutputRecorderFS(fs) + manifestData, _ := fs.ReadFile("/.pnp.data.json") + // Instantiate a unique PnP API per goroutine, or disable PnP if no manifestData found + pnp.OverridePnpApi(manifestData) + defer pnp.ClearTestPnpCache() + host := createCompilerHost(fs, bundled.LibPath(), currentDirectory) var configFile *tsoptions.TsConfigSourceFile var errors []*ast.Diagnostic From 8cbd23d32496bbd17bb6de5f1b36220a92feba65 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 14 Oct 2025 16:16:43 +0200 Subject: [PATCH 37/48] Add simple pnp test --- .../reference/compiler/simplePnpTest.js | 110 ++++++++++++++++++ .../reference/compiler/simplePnpTest.symbols | 39 +++++++ .../reference/compiler/simplePnpTest.types | 42 +++++++ .../tests/cases/compiler/simplePnpTest.ts | 95 +++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 testdata/baselines/reference/compiler/simplePnpTest.js create mode 100644 testdata/baselines/reference/compiler/simplePnpTest.symbols create mode 100644 testdata/baselines/reference/compiler/simplePnpTest.types create mode 100644 testdata/tests/cases/compiler/simplePnpTest.ts diff --git a/testdata/baselines/reference/compiler/simplePnpTest.js b/testdata/baselines/reference/compiler/simplePnpTest.js new file mode 100644 index 0000000000..dd2c8c8672 --- /dev/null +++ b/testdata/baselines/reference/compiler/simplePnpTest.js @@ -0,0 +1,110 @@ +//// [tests/cases/compiler/simplePnpTest.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +//// [index.d.ts] +export declare function helperA(value: string): string; + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +//// [index.d.ts] +export declare function helperB(value: number): string; + +//// [index.ts] +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processData = processData; +// Workspace package that imports both third-party dependencies +const package_a_1 = require("package-a"); +const package_b_1 = require("package-b"); +function processData(text, num) { + const resultA = (0, package_a_1.helperA)(text); + const resultB = (0, package_b_1.helperB)(num); + return `${resultA} | ${resultB}`; +} diff --git a/testdata/baselines/reference/compiler/simplePnpTest.symbols b/testdata/baselines/reference/compiler/simplePnpTest.symbols new file mode 100644 index 0000000000..4ba200ab83 --- /dev/null +++ b/testdata/baselines/reference/compiler/simplePnpTest.symbols @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/simplePnpTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : Symbol(helperA, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : Symbol(helperB, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) + +import { helperB } from 'package-b'; +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) + +export function processData(text: string, num: number): string { +>processData : Symbol(processData, Decl(index.ts, 2, 36)) +>text : Symbol(text, Decl(index.ts, 4, 28)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + const resultA = helperA(text); +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) +>text : Symbol(text, Decl(index.ts, 4, 28)) + + const resultB = helperB(num); +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + return `${resultA} | ${resultB}`; +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +} diff --git a/testdata/baselines/reference/compiler/simplePnpTest.types b/testdata/baselines/reference/compiler/simplePnpTest.types new file mode 100644 index 0000000000..58a7359370 --- /dev/null +++ b/testdata/baselines/reference/compiler/simplePnpTest.types @@ -0,0 +1,42 @@ +//// [tests/cases/compiler/simplePnpTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : (value: string) => string +>value : string + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : (value: number) => string +>value : number + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : (value: string) => string + +import { helperB } from 'package-b'; +>helperB : (value: number) => string + +export function processData(text: string, num: number): string { +>processData : (text: string, num: number) => string +>text : string +>num : number + + const resultA = helperA(text); +>resultA : string +>helperA(text) : string +>helperA : (value: string) => string +>text : string + + const resultB = helperB(num); +>resultB : string +>helperB(num) : string +>helperB : (value: number) => string +>num : number + + return `${resultA} | ${resultB}`; +>`${resultA} | ${resultB}` : string +>resultA : string +>resultB : string +} diff --git a/testdata/tests/cases/compiler/simplePnpTest.ts b/testdata/tests/cases/compiler/simplePnpTest.ts new file mode 100644 index 0000000000..bca8508ee2 --- /dev/null +++ b/testdata/tests/cases/compiler/simplePnpTest.ts @@ -0,0 +1,95 @@ +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: package.json +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.js +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts +export declare function helperA(value: string): string; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.js +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts +export declare function helperB(value: number): string; + +// @filename: /src/index.ts +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} \ No newline at end of file From 55881575b7c8523ae0743e98105f4c5366567d4b Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 17 Oct 2025 11:05:00 +0200 Subject: [PATCH 38/48] Simplify caching and use permanent cache for pnpvfs --- internal/vfs/pnpvfs/pnpvfs.go | 43 +++++------------------------------ 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index c07f855c39..dd8e5b7be7 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -14,16 +14,9 @@ import ( "github.com/microsoft/typescript-go/internal/vfs/iovfs" ) -type cachedZipReader struct { - reader *zip.ReadCloser - lastUsed time.Time - zipMTime time.Time -} - type pnpFS struct { fs vfs.FS - maxOpenReaders int - cachedZipReadersMap map[string]*cachedZipReader + cachedZipReadersMap map[string]*zip.ReadCloser cacheReaderMutex sync.Mutex } @@ -32,8 +25,7 @@ var _ vfs.FS = (*pnpFS)(nil) func From(fs vfs.FS) *pnpFS { pnpFS := &pnpFS{ fs: fs, - maxOpenReaders: 80, // Max number of zip files that can be open at the same time - cachedZipReadersMap: make(map[string]*cachedZipReader), + cachedZipReadersMap: make(map[string]*zip.ReadCloser), cacheReaderMutex: sync.Mutex{}, } @@ -164,16 +156,13 @@ func getMatchingFS(pnpFS *pnpFS, path string) (vfs.FS, string, string) { return pnpFS.fs, path, "" } - var usedReader *cachedZipReader + var usedReader *zip.ReadCloser pnpFS.cacheReaderMutex.Lock() defer pnpFS.cacheReaderMutex.Unlock() - zipMTime := zipStat.ModTime() - cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath] - if ok && cachedReader.zipMTime.Equal(zipMTime) { - cachedReader.lastUsed = time.Now() + if ok { usedReader = cachedReader } else { zipReader, err := zip.OpenReader(zipPath) @@ -181,31 +170,11 @@ func getMatchingFS(pnpFS *pnpFS, path string) (vfs.FS, string, string) { return pnpFS.fs, path, "" } - if len(pnpFS.cachedZipReadersMap) >= pnpFS.maxOpenReaders { - pnpFS.deleteOldestReader() - } - - usedReader = &cachedZipReader{reader: zipReader, lastUsed: time.Now(), zipMTime: zipMTime} + usedReader = zipReader pnpFS.cachedZipReadersMap[zipPath] = usedReader } - return iovfs.From(usedReader.reader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath -} - -func (pnpFS *pnpFS) deleteOldestReader() { - var oldestReader *cachedZipReader - var oldestReaderPath string - for path, reader := range pnpFS.cachedZipReadersMap { - if oldestReader == nil || reader.lastUsed.Before(oldestReader.lastUsed) { - oldestReader = reader - oldestReaderPath = path - } - } - - if oldestReader != nil { - oldestReader.reader.Close() - delete(pnpFS.cachedZipReadersMap, oldestReaderPath) - } + return iovfs.From(usedReader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath } // 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 From c13f422e3a7d7f9fc58b78523d7553c636ac0d06 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Tue, 21 Oct 2025 17:39:32 +0200 Subject: [PATCH 39/48] Fix FindLocator implementation --- internal/pnp/pnpapi.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 7852c593a5..baaecf43e7 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -166,23 +166,30 @@ func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { relativePathWithDot = "./" + relativePath } + var bestLength int + var bestLocator *Locator pathSegments := strings.Split(relativePathWithDot, "/") currentTrie := p.manifest.packageRegistryTrie // Go down the trie, looking for the latest defined packageInfo that matches the path - for _, segment := range pathSegments { - if currentTrie.childrenPathSegments[segment] == nil { + for index, segment := range pathSegments { + currentTrie = currentTrie.childrenPathSegments[segment] + + if currentTrie == nil || currentTrie.childrenPathSegments == nil { break } - currentTrie = currentTrie.childrenPathSegments[segment] + if currentTrie.packageData != nil && index >= bestLength { + bestLength = index + bestLocator = &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference} + } } - if currentTrie.packageData == nil { + if bestLocator == nil { return nil, fmt.Errorf("no package found for path %s", relativePath) } - return &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference}, nil + return bestLocator, nil } func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { From a6c6db89257b660e59fdd798a8045245387e6054 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 23 Oct 2025 11:40:37 +0200 Subject: [PATCH 40/48] Add more pnp resolver tests --- .../{simplePnpTest.js => pnpSimpleTest.js} | 2 +- ...ePnpTest.symbols => pnpSimpleTest.symbols} | 2 +- ...implePnpTest.types => pnpSimpleTest.types} | 2 +- .../pnpTransitiveDependencies.errors.txt | 121 ++++++++++++++ .../compiler/pnpTransitiveDependencies.js | 153 ++++++++++++++++++ .../pnpTransitiveDependencies.symbols | 95 +++++++++++ .../compiler/pnpTransitiveDependencies.types | 98 +++++++++++ .../compiler/pnpTypeRootsResolution.js | 103 ++++++++++++ .../compiler/pnpTypeRootsResolution.symbols | 60 +++++++ .../compiler/pnpTypeRootsResolution.types | 53 ++++++ .../{simplePnpTest.ts => pnpSimpleTest.ts} | 2 + .../compiler/pnpTransitiveDependencies.ts | 117 ++++++++++++++ .../cases/compiler/pnpTypeRootsResolution.ts | 91 +++++++++++ 13 files changed, 896 insertions(+), 3 deletions(-) rename testdata/baselines/reference/compiler/{simplePnpTest.js => pnpSimpleTest.js} (96%) rename testdata/baselines/reference/compiler/{simplePnpTest.symbols => pnpSimpleTest.symbols} (94%) rename testdata/baselines/reference/compiler/{simplePnpTest.types => pnpSimpleTest.types} (92%) create mode 100644 testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt create mode 100644 testdata/baselines/reference/compiler/pnpTransitiveDependencies.js create mode 100644 testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols create mode 100644 testdata/baselines/reference/compiler/pnpTransitiveDependencies.types create mode 100644 testdata/baselines/reference/compiler/pnpTypeRootsResolution.js create mode 100644 testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols create mode 100644 testdata/baselines/reference/compiler/pnpTypeRootsResolution.types rename testdata/tests/cases/compiler/{simplePnpTest.ts => pnpSimpleTest.ts} (99%) create mode 100644 testdata/tests/cases/compiler/pnpTransitiveDependencies.ts create mode 100644 testdata/tests/cases/compiler/pnpTypeRootsResolution.ts diff --git a/testdata/baselines/reference/compiler/simplePnpTest.js b/testdata/baselines/reference/compiler/pnpSimpleTest.js similarity index 96% rename from testdata/baselines/reference/compiler/simplePnpTest.js rename to testdata/baselines/reference/compiler/pnpSimpleTest.js index dd2c8c8672..4f17149ae5 100644 --- a/testdata/baselines/reference/compiler/simplePnpTest.js +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.js @@ -1,4 +1,4 @@ -//// [tests/cases/compiler/simplePnpTest.ts] //// +//// [tests/cases/compiler/pnpSimpleTest.ts] //// //// [.pnp.cjs] module.exports = {}; diff --git a/testdata/baselines/reference/compiler/simplePnpTest.symbols b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols similarity index 94% rename from testdata/baselines/reference/compiler/simplePnpTest.symbols rename to testdata/baselines/reference/compiler/pnpSimpleTest.symbols index 4ba200ab83..c2e6e68b83 100644 --- a/testdata/baselines/reference/compiler/simplePnpTest.symbols +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols @@ -1,4 +1,4 @@ -//// [tests/cases/compiler/simplePnpTest.ts] //// +//// [tests/cases/compiler/pnpSimpleTest.ts] //// === /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === export declare function helperA(value: string): string; diff --git a/testdata/baselines/reference/compiler/simplePnpTest.types b/testdata/baselines/reference/compiler/pnpSimpleTest.types similarity index 92% rename from testdata/baselines/reference/compiler/simplePnpTest.types rename to testdata/baselines/reference/compiler/pnpSimpleTest.types index 58a7359370..90ce12017b 100644 --- a/testdata/baselines/reference/compiler/simplePnpTest.types +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.types @@ -1,4 +1,4 @@ -//// [tests/cases/compiler/simplePnpTest.ts] //// +//// [tests/cases/compiler/pnpSimpleTest.ts] //// === /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === export declare function helperA(value: string): string; diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt new file mode 100644 index 0000000000..a3063b9048 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt @@ -0,0 +1,121 @@ +/src/index.ts(5,36): error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + +==== /.pnp.cjs (0 errors) ==== + module.exports = {}; + +==== /.pnp.data.json (0 errors) ==== + { + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] + } + +==== /package.json (0 errors) ==== + { + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } + } + +==== /packages/package-a/package.json (0 errors) ==== + { + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } + } + +==== /packages/package-a/index.ts (0 errors) ==== + import type { ConfigOptions } from 'package-b'; + + export interface HelperResult { + message: string; + config: ConfigOptions; + } + + export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; + } + +==== /packages/package-b/package.json (0 errors) ==== + { + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } + } + +==== /packages/package-b/index.ts (0 errors) ==== + export interface ConfigOptions { + enabled: boolean; + timeout: number; + } + + export function helperB(value: number): string { + return "Helper B: " + value; + } + +==== /src/index.ts (1 errors) ==== + // Test that the project can import package-a directly + // package-a's types depend on package-b's types (ConfigOptions) + import { helperA } from 'package-a'; + import type { HelperResult } from 'package-a'; + import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + ~~~~~~~~~~~ +!!! error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); + } + + // Test that the project CANNOT import package-b directly even though package-a uses it + // This should cause an error since package-b is not in project's dependencies + export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js new file mode 100644 index 0000000000..b55c670226 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js @@ -0,0 +1,153 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +//// [index.ts] +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +//// [index.ts] +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +//// [index.ts] +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperB = helperB; +function helperB(value) { + return "Helper B: " + value; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperA = helperA; +function helperA(value, config) { + return { + message: "Helper A: " + value, + config: config + }; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDirectDependency = useDirectDependency; +exports.attemptDirectImport = attemptDirectImport; +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +const package_a_1 = require("package-a"); +function useDirectDependency(text) { + const config = { enabled: true, timeout: 5000 }; + return (0, package_a_1.helperA)(text, config); +} +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +function attemptDirectImport() { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols new file mode 100644 index 0000000000..752518b2eb --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols @@ -0,0 +1,95 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) + +export interface HelperResult { +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + message: string; +>message : Symbol(HelperResult.message, Decl(index.ts, 2, 31)) + + config: ConfigOptions; +>config : Symbol(HelperResult.config, Decl(index.ts, 3, 18)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : Symbol(helperA, Decl(index.ts, 5, 1)) +>value : Symbol(value, Decl(index.ts, 7, 24)) +>config : Symbol(config, Decl(index.ts, 7, 38)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + return { + message: "Helper A: " + value, +>message : Symbol(message, Decl(index.ts, 8, 10)) +>value : Symbol(value, Decl(index.ts, 7, 24)) + + config: config +>config : Symbol(config, Decl(index.ts, 9, 34)) +>config : Symbol(config, Decl(index.ts, 7, 38)) + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 0)) + + enabled: boolean; +>enabled : Symbol(ConfigOptions.enabled, Decl(index.ts, 0, 32)) + + timeout: number; +>timeout : Symbol(ConfigOptions.timeout, Decl(index.ts, 1, 19)) +} + +export function helperB(value: number): string { +>helperB : Symbol(helperB, Decl(index.ts, 3, 1)) +>value : Symbol(value, Decl(index.ts, 5, 24)) + + return "Helper B: " + value; +>value : Symbol(value, Decl(index.ts, 5, 24)) +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) + +import type { HelperResult } from 'package-a'; +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : Symbol(useDirectDependency, Decl(index.ts, 4, 47)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : Symbol(config, Decl(index.ts, 7, 7)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) +>enabled : Symbol(enabled, Decl(index.ts, 7, 33)) +>timeout : Symbol(timeout, Decl(index.ts, 7, 48)) + + return helperA(text, config); +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>config : Symbol(config, Decl(index.ts, 7, 7)) +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : Symbol(attemptDirectImport, Decl(index.ts, 9, 1)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + + return { enabled: false, timeout: 1000 }; +>enabled : Symbol(enabled, Decl(index.ts, 14, 10)) +>timeout : Symbol(timeout, Decl(index.ts, 14, 26)) +} + diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types new file mode 100644 index 0000000000..67a56d6ef3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types @@ -0,0 +1,98 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : ConfigOptions + +export interface HelperResult { + message: string; +>message : string + + config: ConfigOptions; +>config : ConfigOptions +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : (value: string, config: ConfigOptions) => HelperResult +>value : string +>config : ConfigOptions + + return { +>{ message: "Helper A: " + value, config: config } : { message: string; config: ConfigOptions; } + + message: "Helper A: " + value, +>message : string +>"Helper A: " + value : string +>"Helper A: " : "Helper A: " +>value : string + + config: config +>config : ConfigOptions +>config : ConfigOptions + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { + enabled: boolean; +>enabled : boolean + + timeout: number; +>timeout : number +} + +export function helperB(value: number): string { +>helperB : (value: number) => string +>value : number + + return "Helper B: " + value; +>"Helper B: " + value : string +>"Helper B: " : "Helper B: " +>value : number +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult + +import type { HelperResult } from 'package-a'; +>HelperResult : HelperResult + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : any + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : (text: string) => HelperResult +>text : string + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : ConfigOptions +>{ enabled: true, timeout: 5000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>true : true +>timeout : number +>5000 : 5000 + + return helperA(text, config); +>helperA(text, config) : HelperResult +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult +>text : string +>config : ConfigOptions +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : () => ConfigOptions + + return { enabled: false, timeout: 1000 }; +>{ enabled: false, timeout: 1000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>false : false +>timeout : number +>1000 : 1000 +} + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js new file mode 100644 index 0000000000..c1e9014271 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js @@ -0,0 +1,103 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +//// [package.json] +{ + "name": "server-lib", + "version": "2.0.0" +} + +//// [package.json] +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +//// [index.d.ts] +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +//// [index.ts] +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer(); + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.server = void 0; +exports.handleRequest = handleRequest; +const server_lib_1 = require("server-lib"); +function handleRequest(req, res) { + res.json({ data: 'Hello, world!' }); +} +exports.server = (0, server_lib_1.createServer)(); diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols new file mode 100644 index 0000000000..739cb2367e --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols @@ -0,0 +1,60 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { +>Request : Symbol(Request, Decl(index.d.ts, 0, 0)) + + params: Record; +>params : Symbol(Request.params, Decl(index.d.ts, 0, 26)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + query: Record; +>query : Symbol(Request.query, Decl(index.d.ts, 1, 34)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export interface Response { +>Response : Symbol(Response, Decl(index.d.ts, 3, 1)) + + send(body: Record): void; +>send : Symbol(Response.send, Decl(index.d.ts, 5, 27)) +>body : Symbol(body, Decl(index.d.ts, 6, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + json(body: Record): void; +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>body : Symbol(body, Decl(index.d.ts, 7, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export declare function createServer(): Record; +>createServer : Symbol(createServer, Decl(index.d.ts, 8, 1)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + +import { createServer } from 'server-lib'; +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : Symbol(handleRequest, Decl(index.ts, 2, 42)) +>req : Symbol(req, Decl(index.ts, 4, 30)) +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + + res.json({ data: 'Hello, world!' }); +>res.json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>data : Symbol(data, Decl(index.ts, 5, 12)) +} + +export const server = createServer(); +>server : Symbol(server, Decl(index.ts, 8, 12)) +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types new file mode 100644 index 0000000000..963f0b9c68 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types @@ -0,0 +1,53 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { + params: Record; +>params : Record + + query: Record; +>query : Record +} + +export interface Response { + send(body: Record): void; +>send : (body: Record) => void +>body : Record + + json(body: Record): void; +>json : (body: Record) => void +>body : Record +} + +export declare function createServer(): Record; +>createServer : () => Record + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Request +>Response : Response + +import { createServer } from 'server-lib'; +>createServer : () => Record + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : (req: Request, res: Response) => void +>req : Request +>res : Response + + res.json({ data: 'Hello, world!' }); +>res.json({ data: 'Hello, world!' }) : void +>res.json : (body: Record) => void +>res : Response +>json : (body: Record) => void +>{ data: 'Hello, world!' } : { data: string; } +>data : string +>'Hello, world!' : "Hello, world!" +} + +export const server = createServer(); +>server : Record +>createServer() : Record +>createServer : () => Record + diff --git a/testdata/tests/cases/compiler/simplePnpTest.ts b/testdata/tests/cases/compiler/pnpSimpleTest.ts similarity index 99% rename from testdata/tests/cases/compiler/simplePnpTest.ts rename to testdata/tests/cases/compiler/pnpSimpleTest.ts index bca8508ee2..7c4b2603ee 100644 --- a/testdata/tests/cases/compiler/simplePnpTest.ts +++ b/testdata/tests/cases/compiler/pnpSimpleTest.ts @@ -1,3 +1,5 @@ +// @strict: true + // @filename: /.pnp.cjs module.exports = {}; diff --git a/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts new file mode 100644 index 0000000000..41fdc6d77e --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts @@ -0,0 +1,117 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +// @filename: /packages/package-a/index.ts +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +// @filename: /packages/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +// @filename: /packages/package-b/index.ts +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +// @filename: /src/index.ts +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts new file mode 100644 index 0000000000..8515344153 --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts @@ -0,0 +1,91 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +// @filename: /.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/package.json +{ + "name": "server-lib", + "version": "2.0.0" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/package.json +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +// @filename: /src/index.ts +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer(); From cf02630cd8b073b75999aceb6138005dd4052a0a Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 23 Oct 2025 14:00:31 +0200 Subject: [PATCH 41/48] Add declaration emit test for pnp --- .../compiler/pnpDeclarationEmitWorkspace.js | 117 ++++++++++++++++++ .../pnpDeclarationEmitWorkspace.symbols | 78 ++++++++++++ .../pnpDeclarationEmitWorkspace.types | 65 ++++++++++ .../compiler/pnpDeclarationEmitWorkspace.ts | 106 ++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js create mode 100644 testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols create mode 100644 testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types create mode 100644 testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js new file mode 100644 index 0000000000..3c97ccfecf --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js @@ -0,0 +1,117 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +//// [package.json] +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.ts" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +//// [index.d.ts] +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +//// [index.ts] +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.factory = void 0; +exports.initializeService = initializeService; +const other_subpath_1 = require("package-a/other-subpath"); +function initializeService(url) { + return (0, other_subpath_1.createServiceConfig)(url); +} +exports.factory = other_subpath_1.createServiceConfig; + + +//// [index.d.ts] +import type { ServiceConfig } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; +export declare function initializeService(url: string): ServiceConfig; +export declare const factory: typeof createServiceConfig; +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols new file mode 100644 index 0000000000..c99e148d54 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols @@ -0,0 +1,78 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) +>ConfigFactory : Symbol(ConfigFactory, Decl(index.ts, 0, 28)) + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export function initializeService(url: string): ServiceConfig { +>initializeService : Symbol(initializeService, Decl(index.ts, 1, 62)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + return createServiceConfig(url); +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +} + +export const factory = createServiceConfig; +>factory : Symbol(factory, Decl(index.ts, 7, 12)) +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export interface AppConfig { +>AppConfig : Symbol(AppConfig, Decl(index.ts, 7, 43)) + + service: ServiceConfig; +>service : Symbol(AppConfig.service, Decl(index.ts, 9, 28)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + debug: boolean; +>debug : Symbol(AppConfig.debug, Decl(index.ts, 10, 25)) +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + timeout: number; +>timeout : Symbol(BaseConfig.timeout, Decl(index.d.ts, 0, 29)) + + retries: number; +>retries : Symbol(BaseConfig.retries, Decl(index.d.ts, 1, 18)) +} + +export interface DataOptions { +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) + + format: "json" | "xml"; +>format : Symbol(DataOptions.format, Decl(index.d.ts, 5, 30)) + + encoding: string; +>encoding : Symbol(DataOptions.encoding, Decl(index.d.ts, 6, 25)) +} + +export interface ServiceConfig extends BaseConfig { +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + endpoint: string; +>endpoint : Symbol(ServiceConfig.endpoint, Decl(index.d.ts, 10, 51)) + + options: DataOptions; +>options : Symbol(ServiceConfig.options, Decl(index.d.ts, 11, 19)) +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : Symbol(ConfigFactory, Decl(index.d.ts, 13, 1)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 15, 29)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.d.ts, 15, 64)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 17, 44)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types new file mode 100644 index 0000000000..9e51fc8ba3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types @@ -0,0 +1,65 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : ServiceConfig +>ConfigFactory : ConfigFactory + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : (endpoint: string) => ServiceConfig + +export function initializeService(url: string): ServiceConfig { +>initializeService : (url: string) => ServiceConfig +>url : string + + return createServiceConfig(url); +>createServiceConfig(url) : ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig +>url : string +} + +export const factory = createServiceConfig; +>factory : (endpoint: string) => ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig + +export interface AppConfig { + service: ServiceConfig; +>service : ServiceConfig + + debug: boolean; +>debug : boolean +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { + timeout: number; +>timeout : number + + retries: number; +>retries : number +} + +export interface DataOptions { + format: "json" | "xml"; +>format : "json" | "xml" + + encoding: string; +>encoding : string +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; +>endpoint : string + + options: DataOptions; +>options : DataOptions +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : ConfigFactory +>endpoint : string + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : (endpoint: string) => ServiceConfig +>endpoint : string + diff --git a/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts new file mode 100644 index 0000000000..a66ac2db0c --- /dev/null +++ b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts @@ -0,0 +1,106 @@ +// @strict: true +// @declaration: true +// @currentDirectory: / + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +// @filename: /tsconfig.json +{ + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.ts" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +// @filename: /packages/package-a/index.d.ts +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +// @filename: /src/index.ts +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} From 05e27f14fab73708562d7e4070499442fda94d63 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Thu, 23 Oct 2025 17:21:00 +0200 Subject: [PATCH 42/48] Remove usage of filepath --- internal/pnp/pnpapi.go | 11 ++++------- internal/vfs/pnpvfs/pnpvfs.go | 22 ++++++++++------------ internal/vfs/pnpvfs/pnpvfs_test.go | 7 +++---- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index baaecf43e7..8a0a100d66 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -13,7 +13,6 @@ import ( "fmt" "os" "path" - "path/filepath" "strings" "github.com/microsoft/typescript-go/internal/tspath" @@ -113,7 +112,7 @@ func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (stri dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) } - return filepath.Join(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil + return tspath.CombinePaths(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil } func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { @@ -143,10 +142,8 @@ func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo { } func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { - relativePath, err := filepath.Rel(p.manifest.dirPath, parentPath) - if err != nil { - return nil, err - } + relativePath := tspath.GetRelativePathFromDirectory(p.manifest.dirPath, parentPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) if p.manifest.ignorePatternData != nil { match, err := p.manifest.ignorePatternData.MatchString(relativePath) @@ -309,5 +306,5 @@ func (p *PnpApi) GetPackageLocationAbsolutePath(packageInfo *PackageInfo) string } packageLocation := packageInfo.PackageLocation - return filepath.Join(p.manifest.dirPath, packageLocation) + return tspath.CombinePaths(p.manifest.dirPath, packageLocation) } diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index dd8e5b7be7..899b802651 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -3,7 +3,6 @@ package pnpvfs import ( "archive/zip" "path" - "path/filepath" "strconv" "strings" "sync" @@ -26,7 +25,6 @@ func From(fs vfs.FS) *pnpFS { pnpFS := &pnpFS{ fs: fs, cachedZipReadersMap: make(map[string]*zip.ReadCloser), - cacheReaderMutex: sync.Mutex{}, } return pnpFS @@ -62,12 +60,12 @@ func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries { entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { - fullPath := filepath.Join(zipPath, formattedPath, dir) + fullPath := tspath.CombinePaths(zipPath, formattedPath, dir) entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) } for i, file := range entries.Files { - fullPath := filepath.Join(zipPath, formattedPath, file) + fullPath := tspath.CombinePaths(zipPath, formattedPath, file) entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) } @@ -92,7 +90,7 @@ func (pnpFS *pnpFS) Realpath(path string) string { path, hash, basePath := resolveVirtual(path) fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) - fullPath := filepath.Join(zipPath, fs.Realpath(formattedPath)) + fullPath := tspath.CombinePaths(zipPath, fs.Realpath(formattedPath)) return makeVirtualPath(basePath, hash, fullPath) } @@ -120,7 +118,7 @@ func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { fs, formattedPath, zipPath := getMatchingFS(pnpFS, root) return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { - fullPath := filepath.Join(zipPath, path) + fullPath := tspath.CombinePaths(zipPath, path) return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) })) } @@ -204,14 +202,14 @@ func resolveVirtual(path string) (realPath string, hash string, basePath string) // Apply dirname n times to base for i := 0; i < depth; i++ { - base = filepath.Dir(base) + base = tspath.GetDirectoryPath(base) } // Join base and subpath if base == "/" { return "/" + subpath, hash, basePath } - return filepath.Join(base, subpath), hash, basePath + return tspath.CombinePaths(base, subpath), hash, basePath } func makeVirtualPath(basePath string, hash string, targetPath string) string { @@ -219,10 +217,10 @@ func makeVirtualPath(basePath string, hash string, targetPath string) string { return targetPath } - relativePath, err := filepath.Rel(path.Dir(basePath), targetPath) - if err != nil { - panic("Could not make virtual path: " + err.Error()) - } + relativePath := tspath.GetRelativePathFromDirectory( + tspath.GetDirectoryPath(basePath), + targetPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) segments := strings.Split(relativePath, "/") diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go index 4c1a031d25..85a974a36b 100644 --- a/internal/vfs/pnpvfs/pnpvfs_test.go +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -4,7 +4,6 @@ import ( "archive/zip" "fmt" "os" - "path/filepath" "strings" "testing" @@ -21,7 +20,7 @@ func createTestZip(t *testing.T, files map[string]string) string { t.Helper() tmpDir := t.TempDir() - zipPath := filepath.Join(tmpDir, "test.zip") + zipPath := tspath.CombinePaths(tmpDir, "test.zip") file, err := os.Create(zipPath) assert.NilError(t, err) @@ -118,7 +117,7 @@ func TestPnpVfs_ErrorHandling(t *testing.T) { t.Run("InvalidZipFile", func(t *testing.T) { tmpDir := t.TempDir() - fakePath := filepath.Join(tmpDir, "fake.zip") + fakePath := tspath.CombinePaths(tmpDir, "fake.zip") err := os.WriteFile(fakePath, []byte("not a zip file"), 0o644) assert.NilError(t, err) result := fs.FileExists(fakePath + "/file.txt") @@ -151,7 +150,7 @@ func TestPnpVfs_FallbackToRegularFiles(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - regularFile := filepath.Join(tmpDir, "regular.ts") + regularFile := tspath.CombinePaths(tmpDir, "regular.ts") err := os.WriteFile(regularFile, []byte("regular content"), 0o644) assert.NilError(t, err) From 9872c4e167ce0e93fc781a09bcb99b673d8fdfce Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 24 Oct 2025 11:32:45 +0200 Subject: [PATCH 43/48] Error handling for wrong regexes of ignorePatternData in pnp manifest --- internal/pnp/manifestparser.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index 554735a27a..257d843f04 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -3,12 +3,12 @@ package pnp import ( "encoding/json" "fmt" - "os" "path" "strings" "github.com/dlclark/regexp2" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" ) type LinkType string @@ -74,16 +74,16 @@ type PnpManifestData struct { packageRegistryTrie *PackageRegistryTrie } -func parseManifestFromPath(manifestDir string) (*PnpManifestData, error) { +func parseManifestFromPath(fs vfs.FS, manifestDir string) (*PnpManifestData, error) { pnpDataString := "" - data, err := os.ReadFile(path.Join(manifestDir, ".pnp.data.json")) - if err == nil { + data, ok := fs.ReadFile(path.Join(manifestDir, ".pnp.data.json")) + if ok { pnpDataString = string(data) } else { - data, err := os.ReadFile(path.Join(manifestDir, ".pnp.cjs")) - if err != nil { - return nil, fmt.Errorf("failed to read .pnp.cjs file: %w", err) + data, ok := fs.ReadFile(path.Join(manifestDir, ".pnp.cjs")) + if !ok { + return nil, fmt.Errorf("failed to read .pnp.cjs file") } pnpString := string(data) @@ -143,7 +143,12 @@ func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpM ignorePatternData := getField(rawData, "ignorePatternData", parseString) if ignorePatternData != "" { - data.ignorePatternData = regexp2.MustCompile(ignorePatternData, regexp2.None) + ignorePatternDataRegexp, err := regexp2.Compile(ignorePatternData, regexp2.None) + if err != nil { + return nil, fmt.Errorf("failed to compile ignore pattern data: %w", err) + } + + data.ignorePatternData = ignorePatternDataRegexp } data.enableTopLevelFallback = getField(rawData, "enableTopLevelFallback", parseBool) From f8e8d8af2eca995cf436d22770efffbf09f9e2cc Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 24 Oct 2025 13:56:50 +0200 Subject: [PATCH 44/48] Fix pnpVfs with usages of tspath.CombinePaths --- internal/vfs/pnpvfs/pnpvfs.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index 899b802651..4101ef4628 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -60,12 +60,12 @@ func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries { entries := fs.GetAccessibleEntries(formattedPath) for i, dir := range entries.Directories { - fullPath := tspath.CombinePaths(zipPath, formattedPath, dir) + fullPath := tspath.CombinePaths(zipPath+formattedPath, dir) entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) } for i, file := range entries.Files { - fullPath := tspath.CombinePaths(zipPath, formattedPath, file) + fullPath := tspath.CombinePaths(zipPath+formattedPath, file) entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) } @@ -90,7 +90,7 @@ func (pnpFS *pnpFS) Realpath(path string) string { path, hash, basePath := resolveVirtual(path) fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) - fullPath := tspath.CombinePaths(zipPath, fs.Realpath(formattedPath)) + fullPath := zipPath + fs.Realpath(formattedPath) return makeVirtualPath(basePath, hash, fullPath) } @@ -118,7 +118,7 @@ func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { fs, formattedPath, zipPath := getMatchingFS(pnpFS, root) return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { - fullPath := tspath.CombinePaths(zipPath, path) + fullPath := zipPath + path return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) })) } From b98ce24372a8bf05b781b71198952e2a1d67709b Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 24 Oct 2025 13:57:43 +0200 Subject: [PATCH 45/48] Remove usages of os, abstract the provided fs and init PnP API separately --- cmd/tsgo/sys.go | 2 +- internal/api/server.go | 2 +- internal/lsp/server.go | 3 +- internal/pnp/manifestparser.go | 3 +- internal/pnp/pnp.go | 39 +++++++++++--------- internal/pnp/pnpapi.go | 15 ++++++-- internal/testutil/harnessutil/harnessutil.go | 2 +- 7 files changed, 38 insertions(+), 28 deletions(-) diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index 14fd41089c..4593195034 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -70,7 +70,7 @@ func newSystem() *osSys { var fs vfs.FS = osvfs.FS() - pnpApi := pnp.GetPnpApi(tspath.NormalizePath(cwd)) + pnpApi := pnp.InitPnpApi(fs, tspath.NormalizePath(cwd)) if pnpApi != nil { fs = pnpvfs.From(fs) } diff --git a/internal/api/server.go b/internal/api/server.go index 6244227f7f..68e4a9141c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -97,7 +97,7 @@ func NewServer(options *ServerOptions) *Server { var fs vfs.FS = osvfs.FS() - pnpApi := pnp.GetPnpApi(options.Cwd) + pnpApi := pnp.InitPnpApi(fs, options.Cwd) if pnpApi != nil { fs = pnpvfs.From(fs) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f05aac2aab..9b35385ca5 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -669,9 +669,8 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali cwd = s.cwd } - pnpApi := pnp.GetPnpApi(cwd) - fs := s.fs + pnpApi := pnp.InitPnpApi(fs, cwd) if pnpApi != nil { fs = pnpvfs.From(fs) } diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index 257d843f04..f8e74e8f22 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -8,7 +8,6 @@ import ( "github.com/dlclark/regexp2" "github.com/microsoft/typescript-go/internal/tspath" - "github.com/microsoft/typescript-go/internal/vfs" ) type LinkType string @@ -74,7 +73,7 @@ type PnpManifestData struct { packageRegistryTrie *PackageRegistryTrie } -func parseManifestFromPath(fs vfs.FS, manifestDir string) (*PnpManifestData, error) { +func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, error) { pnpDataString := "" data, ok := fs.ReadFile(path.Join(manifestDir, ".pnp.data.json")) diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go index 4b73a6b2e6..2832bb3791 100644 --- a/internal/pnp/pnp.go +++ b/internal/pnp/pnp.go @@ -30,7 +30,7 @@ func getGoroutineID() int { // Sets the PnP API for the given manifest data and manifest directory, used for testing // This creates a goroutine-specific cache entry that won't interfere with other parallel tests. -func OverridePnpApi(manifestDataRaw string) *PnpApi { +func OverridePnpApi(fs PnpApiFS, manifestDataRaw string) *PnpApi { if manifestDataRaw == "" { return nil } @@ -42,6 +42,7 @@ func OverridePnpApi(manifestDataRaw string) *PnpApi { pnpApi = nil } else if manifestData != nil { pnpApi = &PnpApi{ + fs: fs, url: "/", manifest: manifestData, } @@ -61,21 +62,7 @@ func ClearTestPnpCache() { testPnpCache.Delete(gid) } -// GetPnpApi returns the PnP API for the given file path. Will return nil if the PnP API is not available. -func GetPnpApi(filePath string) *PnpApi { - // If in a test, check for PnP API overrides - if testing.Testing() { - gid := getGoroutineID() - if api, ok := testPnpCache.Load(gid); ok { - return api.(*PnpApi) - } - } - - // Check if PnP API is already initialized using atomic read (no lock needed) - if isPnpApiInitialized.Load() == 1 { - return cachedPnpApi - } - +func InitPnpApi(fs PnpApiFS, filePath string) *PnpApi { pnpMu.Lock() defer pnpMu.Unlock() // Double-check after acquiring lock @@ -83,7 +70,7 @@ func GetPnpApi(filePath string) *PnpApi { return cachedPnpApi } - pnpApi := &PnpApi{url: filePath} + pnpApi := &PnpApi{fs: fs, url: filePath} manifestData, err := pnpApi.findClosestPnpManifest() if err == nil { @@ -98,6 +85,24 @@ func GetPnpApi(filePath string) *PnpApi { return cachedPnpApi } +// GetPnpApi returns the PnP API for the given file path. Will return nil if the PnP API is not available or not initialized +func GetPnpApi(filePath string) *PnpApi { + // If in a test, check for PnP API overrides + if testing.Testing() { + gid := getGoroutineID() + if api, ok := testPnpCache.Load(gid); ok { + return api.(*PnpApi) + } + } + + // Check if PnP API is already initialized using atomic read (no lock needed) + if isPnpApiInitialized.Load() == 1 { + return cachedPnpApi + } + + return nil +} + // Clears the singleton PnP API cache func ClearPnpCache() { pnpMu.Lock() diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 8a0a100d66..4ce804e591 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -11,7 +11,6 @@ package pnp import ( "fmt" - "os" "path" "strings" @@ -19,10 +18,18 @@ import ( ) type PnpApi struct { + fs PnpApiFS url string manifest *PnpManifestData } +// FS abstraction used by the PnpApi to access the file system +// We can't use the vfs.FS interface because it creates an import cycle: core -> pnp -> vfs -> core +type PnpApiFS interface { + FileExists(path string) bool + ReadFile(path string) (contents string, ok bool) +} + func (p *PnpApi) RefreshManifest() error { var newData *PnpManifestData var err error @@ -30,7 +37,7 @@ func (p *PnpApi) RefreshManifest() error { if p.manifest == nil { newData, err = p.findClosestPnpManifest() } else { - newData, err = parseManifestFromPath(p.manifest.dirPath) + newData, err = parseManifestFromPath(p.fs, p.manifest.dirPath) } if err != nil { @@ -120,8 +127,8 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { for { pnpPath := path.Join(directoryPath, ".pnp.cjs") - if _, err := os.Stat(pnpPath); err == nil { - return parseManifestFromPath(directoryPath) + if p.fs.FileExists(pnpPath) { + return parseManifestFromPath(p.fs, directoryPath) } directoryPath = path.Dir(directoryPath) diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 5616e4871b..a842ac091b 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -211,7 +211,7 @@ func CompileFilesEx( manifestData, _ := fs.ReadFile("/.pnp.data.json") // Instantiate a unique PnP API per goroutine, or disable PnP if no manifestData found - pnp.OverridePnpApi(manifestData) + pnp.OverridePnpApi(fs, manifestData) defer pnp.ClearTestPnpCache() host := createCompilerHost(fs, bundled.LibPath(), currentDirectory) From ded9d9f4738f04f5c425dffccfbf599ec1b1361e Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 24 Oct 2025 16:40:39 +0200 Subject: [PATCH 46/48] Cleanup code and update comments --- cmd/tsgo/lsp.go | 1 - internal/lsp/lsproto/lsp.go | 1 + internal/module/resolver.go | 2 +- internal/modulespecifiers/specifiers.go | 9 ++------- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index cb146ca6d9..0dd04c57fb 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -42,7 +42,6 @@ func runLSP(args []string) int { } fs := bundled.WrapFS(osvfs.FS()) - defaultLibraryPath := bundled.LibPath() typingsLocation := getGlobalTypingsCacheLocation() diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index 85727faccd..ea59298bcb 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -23,6 +23,7 @@ func (uri DocumentUri) FileName() string { } // Leave all other URIs escaped so we can round-trip them. + scheme, path, ok := strings.Cut(string(uri), ":") if !ok { panic(fmt.Sprintf("invalid URI: %s", uri)) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 67e02d6aa0..20b64f2a55 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -917,7 +917,7 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { pnpApi := pnp.GetPnpApi(r.containingDirectory) if pnpApi != nil { - // TODO: stop at global cache too? + // !!! stop at global cache return r.loadModuleFromImmediateNodeModulesDirectoryPnP(ext, r.containingDirectory, typesScopeOnly) } diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 4afc609df2..d40572697a 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -273,10 +273,7 @@ func GetEachFileNameOfModule( for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ - FileName: p, - // TODO: test this - // It impacts tagging external workspace dependencies as module dependencies, to trigger a module name resolver for - // import suggestions + FileName: p, IsInNodeModules: ContainsNodeModules(p) || pnp.IsInPnpModule(importingFileName, p), IsRedirect: referenceRedirect == p, }) @@ -685,9 +682,7 @@ func tryGetModuleNameFromRootDirs( return processEnding(shortest, allowedEndings, compilerOptions, host) } -// TODO test this feature -// Help to identify the feature to test: when you're adding a new symbol into the file, TS will suggest you to import it from another file. -// To do that it'll map that other file path into an import path, and that's the function responsible for that. +// TODO: This code partially duplicates tryGetModuleNameAsNodeModule, is it better to keep it isolated from the node module version or should we merge them? func tryGetModuleNameAsPnpPackage( pathObj ModulePath, info Info, From 18c94aae87c25d993c7f20e89f03e7388e327dba Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Fri, 24 Oct 2025 17:36:43 +0200 Subject: [PATCH 47/48] Apply lint fixes --- internal/pnp/manifestparser.go | 29 ++++++++++++++--------------- internal/pnp/pnpapi.go | 16 +++++++++------- internal/vfs/pnpvfs/pnpvfs.go | 2 +- internal/vfs/pnpvfs/pnpvfs_test.go | 22 +++++++++++++++++----- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go index f8e74e8f22..8e75f2edc6 100644 --- a/internal/pnp/manifestparser.go +++ b/internal/pnp/manifestparser.go @@ -1,11 +1,12 @@ package pnp import ( - "encoding/json" + "errors" "fmt" - "path" "strings" + "github.com/go-json-experiment/json" + "github.com/dlclark/regexp2" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -76,33 +77,31 @@ type PnpManifestData struct { func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, error) { pnpDataString := "" - data, ok := fs.ReadFile(path.Join(manifestDir, ".pnp.data.json")) + data, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.data.json")) if ok { - pnpDataString = string(data) + pnpDataString = data } else { - data, ok := fs.ReadFile(path.Join(manifestDir, ".pnp.cjs")) + pnpScriptString, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.cjs")) if !ok { - return nil, fmt.Errorf("failed to read .pnp.cjs file") + return nil, errors.New("failed to read .pnp.cjs file") } - pnpString := string(data) - manifestRegex := regexp2.MustCompile(`(const[ \r\n]+RAW_RUNTIME_STATE[ \r\n]*=[ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`, regexp2.None) - matches, err := manifestRegex.FindStringMatch(pnpString) + matches, err := manifestRegex.FindStringMatch(pnpScriptString) if err != nil || matches == nil { - return nil, fmt.Errorf("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?") + return nil, errors.New("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?") } start := matches.Index + matches.Length var b strings.Builder - b.Grow(len(pnpString)) - for i := start; i < len(pnpString); i++ { - if pnpString[i] == '\'' { + b.Grow(len(pnpScriptString)) + for i := start; i < len(pnpScriptString); i++ { + if pnpScriptString[i] == '\'' { break } - if pnpString[i] != '\\' { - b.WriteByte(pnpString[i]) + if pnpScriptString[i] != '\\' { + b.WriteByte(pnpScriptString[i]) } } pnpDataString = b.String() diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go index 4ce804e591..e3e949dcd6 100644 --- a/internal/pnp/pnpapi.go +++ b/internal/pnp/pnpapi.go @@ -10,8 +10,8 @@ package pnp */ import ( + "errors" "fmt" - "path" "strings" "github.com/microsoft/typescript-go/internal/tspath" @@ -50,7 +50,7 @@ func (p *PnpApi) RefreshManifest() error { func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (string, error) { if p.manifest == nil { - panic(fmt.Errorf("ResolveToUnqualified called with no PnP manifest available")) + panic("ResolveToUnqualified called with no PnP manifest available") } ident, modulePath, err := p.ParseBareIdentifier(specifier) @@ -126,14 +126,14 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { directoryPath := p.url for { - pnpPath := path.Join(directoryPath, ".pnp.cjs") + pnpPath := tspath.CombinePaths(directoryPath, ".pnp.cjs") if p.fs.FileExists(pnpPath) { return parseManifestFromPath(p.fs, directoryPath) } - directoryPath = path.Dir(directoryPath) + directoryPath = tspath.GetDirectoryPath(directoryPath) if tspath.IsDiskPathRoot(directoryPath) { - return nil, fmt.Errorf("no PnP manifest found") + return nil, errors.New("no PnP manifest found") } } } @@ -142,7 +142,7 @@ func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo { packageRegistryMap := p.manifest.packageRegistryMap packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference] if !ok { - panic(fmt.Sprintf("%s should have an entry in the package registry", locator.Name)) + panic(locator.Name + " should have an entry in the package registry") } return packageInfo @@ -276,7 +276,9 @@ func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { for _, dep := range packageDependencies { if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" { packageInfo := p.GetPackage(&Locator{Name: dep.Ident, Reference: dep.Reference}) - typeRoots = append(typeRoots, path.Dir(path.Join(p.manifest.dirPath, packageInfo.PackageLocation))) + typeRoots = append(typeRoots, tspath.GetDirectoryPath( + tspath.CombinePaths(p.manifest.dirPath, packageInfo.PackageLocation), + )) } } diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go index 4101ef4628..d88894a21f 100644 --- a/internal/vfs/pnpvfs/pnpvfs.go +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -201,7 +201,7 @@ func resolveVirtual(path string) (realPath string, hash string, basePath string) basePath = path[:idx] + "/__virtual__" // Apply dirname n times to base - for i := 0; i < depth; i++ { + for range depth { base = tspath.GetDirectoryPath(base) } // Join base and subpath diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go index 85a974a36b..f5d12fa775 100644 --- a/internal/vfs/pnpvfs/pnpvfs_test.go +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -59,16 +59,18 @@ func TestPnpVfs_BasicFileOperations(t *testing.T) { assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) var files []string - fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error { + err := fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error { if !d.IsDir() { files = append(files, path) } return nil }) + assert.NilError(t, err) assert.DeepEqual(t, files, []string{"/project/package.json", "/project/src/index.ts"}) - fs.WriteFile("/project/src/index.ts", "export const hello = 'world2';", false) + err = fs.WriteFile("/project/src/index.ts", "export const hello = 'world2';", false) + assert.NilError(t, err) content, ok = fs.ReadFile("/project/src/index.ts") assert.Assert(t, ok) @@ -108,6 +110,8 @@ func TestPnpVfs_ErrorHandling(t *testing.T) { fs := pnpvfs.From(osvfs.FS()) t.Run("NonexistentZipFile", func(t *testing.T) { + t.Parallel() + result := fs.FileExists("/nonexistent/path/archive.zip/file.txt") assert.Assert(t, !result) @@ -116,22 +120,27 @@ func TestPnpVfs_ErrorHandling(t *testing.T) { }) t.Run("InvalidZipFile", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() fakePath := tspath.CombinePaths(tmpDir, "fake.zip") err := os.WriteFile(fakePath, []byte("not a zip file"), 0o644) assert.NilError(t, err) + result := fs.FileExists(fakePath + "/file.txt") assert.Assert(t, !result) }) t.Run("WriteToZipFile", func(t *testing.T) { + t.Parallel() + zipFiles := map[string]string{ "src/index.ts": "export const hello = 'world';", } zipPath := createTestZip(t, zipFiles) testutil.AssertPanics(t, func() { - fs.WriteFile(zipPath+"/src/index.ts", "hello, world", false) + _ = fs.WriteFile(zipPath+"/src/index.ts", "hello, world", false) }, "cannot write to zip file") }) } @@ -182,6 +191,7 @@ func TestZipPath_Detection(t *testing.T) { for _, tc := range testCases { t.Run(tc.path, func(t *testing.T) { + t.Parallel() assert.Assert(t, tspath.IsZipPath(tc.path) == tc.shouldBeZip) }) } @@ -216,12 +226,13 @@ func TestPnpVfs_VirtualPathHandling(t *testing.T) { assert.DeepEqual(t, entries.Directories, []string(nil)) files := []string{} - fs.WalkDir("/project/packages/__virtual__/packageB-virtual-123456/0/packageB", func(path string, d vfs.DirEntry, err error) error { + err := fs.WalkDir("/project/packages/__virtual__/packageB-virtual-123456/0/packageB", func(path string, d vfs.DirEntry, err error) error { if !d.IsDir() { files = append(files, path) } return nil }) + assert.NilError(t, err) assert.DeepEqual(t, files, []string{ "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", @@ -268,12 +279,13 @@ func TestPnpVfs_RealZipIntegration(t *testing.T) { assert.Equal(t, fs.Realpath(indexPath), indexPath) files := []string{} - fs.WalkDir(zipPath, func(path string, d vfs.DirEntry, err error) error { + err := fs.WalkDir(zipPath, func(path string, d vfs.DirEntry, err error) error { if !d.IsDir() { files = append(files, path) } return nil }) + assert.NilError(t, err) assert.DeepEqual(t, files, []string{zipPath + "/package.json", zipPath + "/src/index.ts", zipPath + "/src/utils/helpers.ts", zipPath + "/tsconfig.json"}) assert.Assert(t, fs.FileExists(zipPath+"/src/__virtual__/src-virtual-123456/0/index.ts")) From 4551f2bcf40568c675d91c534bf4a27cb57e07c6 Mon Sep 17 00:00:00 2001 From: Guyllian Gomez Date: Mon, 27 Oct 2025 17:00:15 +0100 Subject: [PATCH 48/48] Remove unnecessary comment --- internal/module/resolver.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 47e6d59a49..2bfc3f1423 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -1788,7 +1788,6 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa } nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/" builder := strings.Builder{} - // TODO: find an example that needs this change pnpApi := pnp.GetPnpApi(packageJsonInfo.PackageDirectory) for name := range peerDependencies.Value { var peerDependencyPath string