@@ -338,9 +338,10 @@ func computeModuleSpecifiers(
338
338
339
339
for _ ,modulePath := range modulePaths {
340
340
var specifier string
341
- if modulePath .IsInNodeModules {
342
- specifier = tryGetModuleNameAsNodeModule (modulePath ,info ,importingSourceFile ,host ,compilerOptions ,userPreferences /*packageNameOnly*/ ,false ,options .OverrideImportMode )
343
- }
341
+ // Try to generate a node module specifier for all paths, not just those marked IsInNodeModules
342
+ // This handles symlinked packages where the real path doesn't contain node_modules
343
+ // but a package name can still be determined
344
+ specifier = tryGetModuleNameAsNodeModule (modulePath ,info ,importingSourceFile ,host ,compilerOptions ,userPreferences /*packageNameOnly*/ ,false ,options .OverrideImportMode )
344
345
if len (specifier )> 0 && ! (forAutoImport && isExcludedByRegex (specifier ,preferences .excludeRegexes )) {
345
346
nodeModulesSpecifiers = append (nodeModulesSpecifiers ,specifier )
346
347
if modulePath .IsRedirect {
@@ -378,9 +379,16 @@ func computeModuleSpecifiers(
378
379
}else {
379
380
pathsSpecifiers = append (pathsSpecifiers ,local )
380
381
}
381
- }else if forAutoImport || (! importedFileIsInNodeModules || ! modulePath .IsInNodeModules ) {
382
- // Only add to relative specifiers if this path is NOT in node_modules.
383
- // For symlinked packages, we want node_modules paths to be prioritized over real paths.
382
+ }else if forAutoImport || ! importedFileIsInNodeModules || modulePath .IsInNodeModules {
383
+ // Why this extra conditional, not just an `else`? If some path to the file contained
384
+ // 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"),
385
+ // that means we had to go through a *sibling's* node_modules, not one we can access directly.
386
+ // If some path to the file was in node_modules but another was not, this likely indicates that
387
+ // we have a monorepo structure with symlinks. In this case, the non-nodeModules path is
388
+ // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package
389
+ // in a monorepo is probably not portable. So, the module specifier we actually go with will be
390
+ // the relative path through node_modules, so that the declaration emitter can produce a
391
+ // portability error. (See declarationEmitReexportedSymlinkReference3)
384
392
relativeSpecifiers = append (relativeSpecifiers ,local )
385
393
}
386
394
}
@@ -638,6 +646,50 @@ func tryGetModuleNameFromRootDirs(
638
646
return processEnding (shortest ,allowedEndings ,compilerOptions ,host )
639
647
}
640
648
649
+ func tryGetPackageNameForSymlinkedFile (fileName string ,info Info ,host ModuleSpecifierGenerationHost )string {
650
+ // Look for package.json in the file's directory or parent directories
651
+ currentDir := tspath .GetDirectoryPath (fileName )
652
+ for len (currentDir )> 0 {
653
+ packageJsonPath := tspath .CombinePaths (currentDir ,"package.json" )
654
+ if host .FileExists (packageJsonPath ) {
655
+ packageInfo := host .GetPackageJsonInfo (packageJsonPath )
656
+ if packageInfo != nil && packageInfo .GetContents ()!= nil {
657
+ packageName ,ok := packageInfo .GetContents ().Name .GetValue ()
658
+ if ok && len (packageName )> 0 {
659
+ // Check if this package can be resolved from the importing source directory
660
+ // by looking for a symlink in node_modules
661
+ if canResolveAsPackage (packageName ,info .SourceDirectory ,host ) {
662
+ return packageName
663
+ }
664
+ }
665
+ }
666
+ }
667
+ parentDir := tspath .GetDirectoryPath (currentDir )
668
+ if parentDir == currentDir {
669
+ break
670
+ }
671
+ currentDir = parentDir
672
+ }
673
+ return ""
674
+ }
675
+
676
+ func canResolveAsPackage (packageName string ,importingDir string ,host ModuleSpecifierGenerationHost )bool {
677
+ // Walk up from importing directory looking for node_modules that contains this package
678
+ currentDir := importingDir
679
+ for len (currentDir )> 0 {
680
+ nodeModulesPath := tspath .CombinePaths (currentDir ,"node_modules" ,packageName )
681
+ if host .FileExists (tspath .CombinePaths (nodeModulesPath ,"package.json" )) {
682
+ return true
683
+ }
684
+ parentDir := tspath .GetDirectoryPath (currentDir )
685
+ if parentDir == currentDir {
686
+ break
687
+ }
688
+ currentDir = parentDir
689
+ }
690
+ return false
691
+ }
692
+
641
693
func tryGetModuleNameAsNodeModule (
642
694
pathObj ModulePath ,
643
695
info Info ,
@@ -650,7 +702,9 @@ func tryGetModuleNameAsNodeModule(
650
702
)string {
651
703
parts := getNodeModulePathParts (pathObj .FileName )
652
704
if parts == nil {
653
- return ""
705
+ // For symlinked packages, the real path may not contain node_modules
706
+ // Try to infer package name by looking for package.json
707
+ return tryGetPackageNameForSymlinkedFile (pathObj .FileName ,info ,host )
654
708
}
655
709
656
710
// Simplify the full file path to something that can be resolved by Node.