Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions internal/compiler/projectreferencedtsfakinghost.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,31 @@ func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory
})
}

// registerNodeModulesSymlinkFromAncestor walks up from a node_modules path to
// the nearest ancestor directory that actually exists on disk and, if that
// directory is a symlink (e.g. a symlinked package root in a workspace install),
// records it in knownSymlinks. This handles the case where the resolver probes a
// path inside an unbuilt package before directory-probing the package root, so
// handleDirectoryCouldBeSymlink has not fired for it yet.
func (fs *projectReferenceDtsFakingVfs) registerNodeModulesSymlinkFromAncestor(fileOrDirectory string) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very special case-y. Surely any dir we resolve into could be under be a symlink? How are we not working with realpaths already?

if !strings.Contains(fileOrDirectory, "/node_modules/") {
return
}
realFS := fs.projectReferenceFileMapper.opts.Host.FS()
dir := tspath.GetDirectoryPath(fileOrDirectory)
for strings.Contains(dir+"/", "/node_modules/") {
if realFS.DirectoryExists(dir) {
fs.handleDirectoryCouldBeSymlink(dir)
return
}
parent := tspath.GetDirectoryPath(dir)
if parent == dir {
return
}
dir = parent
}
}

func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrDirectory string, isFile bool) bool {
fileOrDirectoryExistsUsingSource := core.IfElse(isFile, fs.fileExistsIfProjectReferenceDts, fs.directoryExistsIfProjectReferenceDeclDir)
// Check current directory or file
Expand All @@ -170,6 +195,13 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD
return result == core.TSTrue
}

// The resolver can probe a path inside an unbuilt referenced package (whose
// declaration outputs do not exist on disk) before it ever directory-probes
// that package's symlinked root, so the symlink may not be in knownSymlinks
// yet. Discover and register it on demand from the nearest existing ancestor
// directory, then fall through to the existing symlink-aware lookup.
fs.registerNodeModulesSymlinkFromAncestor(fileOrDirectory)

knownDirectoryLinks := fs.knownSymlinks.Directories()
if knownDirectoryLinks.Size() == 0 {
return false
Expand Down
39 changes: 39 additions & 0 deletions internal/project/projectreferencesprogram_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ func TestProjectReferencesProgram(t *testing.T) {
assert.Assert(t, barFile != nil)
})

t.Run("references through symlink to a directory subpath (index file)", func(t *testing.T) {
t.Parallel()
files, aTest, bIndex := filesForSymlinkReferencesToDirectoryIndexSubpath("")
session, _ := projecttestutil.Setup(files)

uri := lsconv.FileNameToDocumentURI(aTest)
session.DidOpenFile(context.Background(), uri, 1, files[aTest].(string), lsproto.LanguageKindTypeScript)

snapshot := session.Snapshot()
projects := snapshot.ProjectCollection.Projects()
assert.Equal(t, len(projects), 1)
p := projects[0]
assert.Equal(t, p.Kind, project.KindConfigured)

indexFile := p.Program.GetSourceFile(bIndex)
assert.Assert(t, indexFile != nil)
})

t.Run("when new file is added to referenced project", func(t *testing.T) {
t.Parallel()
files := filesForReferencedProjectProgram(false)
Expand Down Expand Up @@ -360,6 +378,27 @@ func filesForSymlinkReferencesInSubfolder(preserveSymlinks bool, scope string) (
return files, aTest, bFoo, bBar
}

// filesForSymlinkReferencesToDirectoryIndexSubpath imports a directory subpath
// (`b/lib/File`, whose source is `B/src/File/index.ts`) of a symlinked,
// unbuilt referenced project. The resolver directory-probes the unbuilt subpath
// before the package root, so the redirect must still map it to source.
func filesForSymlinkReferencesToDirectoryIndexSubpath(scope string) (files map[string]any, aTest string, bIndex string) {
aTest = "/user/username/projects/myproject/packages/A/src/test.ts"
bIndex = "/user/username/projects/myproject/packages/B/src/File/index.ts"
files = map[string]any{
"/user/username/projects/myproject/packages/B/package.json": `{}`,
aTest: fmt.Sprintf(`
import { helper } from '%sb/lib/File';
helper();
`, scope),
bIndex: `export function helper() { }`,
fmt.Sprintf(`/user/username/projects/myproject/node_modules/%sb`, scope): vfstest.Symlink("/user/username/projects/myproject/packages/B"),
}
addConfigForPackage(files, "A", false, []string{"../B"})
addConfigForPackage(files, "B", false, nil)
return files, aTest, bIndex
}

func addConfigForPackage(files map[string]any, packageName string, preserveSymlinks bool, references []string) {
compilerOptions := map[string]any{
"outDir": "lib",
Expand Down