@@ -258,7 +258,8 @@ namespace ts.server {
258258 private compilerOptions : CompilerOptions ,
259259 public compileOnSaveEnabled : boolean ,
260260 directoryStructureHost : DirectoryStructureHost ,
261- currentDirectory : string | undefined ) {
261+ currentDirectory : string | undefined ,
262+ customRealpath ?: ( s : string ) => string ) {
262263 this . directoryStructureHost = directoryStructureHost ;
263264 this . currentDirectory = this . projectService . getNormalizedAbsolutePath ( currentDirectory || "" ) ;
264265 this . getCanonicalFileName = this . projectService . toCanonicalFileName ;
@@ -286,7 +287,7 @@ namespace ts.server {
286287 }
287288
288289 if ( host . realpath ) {
289- this . realpath = path => host . realpath ! ( path ) ;
290+ this . realpath = customRealpath || ( path => host . realpath ! ( path ) ) ;
290291 }
291292
292293 // Use the current directory as resolution root only if the project created using current directory string
@@ -1660,6 +1661,12 @@ namespace ts.server {
16601661 }
16611662 }
16621663
1664+ /*@internal */
1665+ interface SymlinkedDirectory {
1666+ real : string ;
1667+ realPath : Path ;
1668+ }
1669+
16631670 /**
16641671 * If a file is opened, the server will look for a tsconfig (or jsconfig)
16651672 * and if successfull create a ConfiguredProject for it.
@@ -1673,6 +1680,8 @@ namespace ts.server {
16731680 readonly canonicalConfigFilePath : NormalizedPath ;
16741681 private projectReferenceCallbacks : ResolvedProjectReferenceCallbacks | undefined ;
16751682 private mapOfDeclarationDirectories : Map < true > | undefined ;
1683+ private symlinkedDirectories : Map < SymlinkedDirectory | false > | undefined ;
1684+ private symlinkedFiles : Map < string > | undefined ;
16761685
16771686 /* @internal */
16781687 pendingReload : ConfigFileProgramReloadLevel | undefined ;
@@ -1714,7 +1723,9 @@ namespace ts.server {
17141723 /*compilerOptions*/ { } ,
17151724 /*compileOnSaveEnabled*/ false ,
17161725 cachedDirectoryStructureHost ,
1717- getDirectoryPath ( configFileName ) ) ;
1726+ getDirectoryPath ( configFileName ) ,
1727+ projectService . host . realpath && ( s => this . getRealpath ( s ) )
1728+ ) ;
17181729 this . canonicalConfigFilePath = asNormalizedPath ( projectService . toCanonicalFileName ( configFileName ) ) ;
17191730 }
17201731
@@ -1727,18 +1738,34 @@ namespace ts.server {
17271738 useSourceOfProjectReferenceRedirect = ( ) => ! ! this . languageServiceEnabled &&
17281739 ! this . getCompilerOptions ( ) . disableSourceOfProjectReferenceRedirect ;
17291740
1741+ private fileExistsIfProjectReferenceDts ( file : string ) {
1742+ const source = this . projectReferenceCallbacks ! . getSourceOfProjectReferenceRedirect ( file ) ;
1743+ return source !== undefined ?
1744+ isString ( source ) ? super . fileExists ( source ) : true :
1745+ undefined ;
1746+ }
1747+
17301748 /**
17311749 * This implementation of fileExists checks if the file being requested is
17321750 * .d.ts file for the referenced Project.
17331751 * If it is it returns true irrespective of whether that file exists on host
17341752 */
17351753 fileExists ( file : string ) : boolean {
1754+ if ( super . fileExists ( file ) ) return true ;
1755+ if ( ! this . useSourceOfProjectReferenceRedirect ( ) || ! this . projectReferenceCallbacks ) return false ;
1756+ if ( ! isDeclarationFileName ( file ) ) return false ;
1757+
17361758 // Project references go to source file instead of .d.ts file
1737- if ( this . useSourceOfProjectReferenceRedirect ( ) && this . projectReferenceCallbacks ) {
1738- const source = this . projectReferenceCallbacks . getSourceOfProjectReferenceRedirect ( file ) ;
1739- if ( source ) return isString ( source ) ? super . fileExists ( source ) : true ;
1740- }
1741- return super . fileExists ( file ) ;
1759+ return this . fileOrDirectoryExistsUsingSource ( file , /*isFile*/ true ) ;
1760+ }
1761+
1762+ private directoryExistsIfProjectReferenceDeclDir ( dir : string ) {
1763+ const dirPath = this . toPath ( dir ) ;
1764+ const dirPathWithTrailingDirectorySeparator = `${ dirPath } ${ directorySeparator } ` ;
1765+ return forEachKey (
1766+ this . mapOfDeclarationDirectories ! ,
1767+ declDirPath => dirPath === declDirPath || startsWith ( declDirPath , dirPathWithTrailingDirectorySeparator )
1768+ ) ;
17421769 }
17431770
17441771 /**
@@ -1747,14 +1774,17 @@ namespace ts.server {
17471774 * If it is it returns true irrespective of whether that directory exists on host
17481775 */
17491776 directoryExists ( path : string ) : boolean {
1750- if ( super . directoryExists ( path ) ) return true ;
1777+ if ( super . directoryExists ( path ) ) {
1778+ this . handleDirectoryCouldBeSymlink ( path ) ;
1779+ return true ;
1780+ }
17511781 if ( ! this . useSourceOfProjectReferenceRedirect ( ) || ! this . projectReferenceCallbacks ) return false ;
17521782
17531783 if ( ! this . mapOfDeclarationDirectories ) {
17541784 this . mapOfDeclarationDirectories = createMap ( ) ;
17551785 this . projectReferenceCallbacks . forEachResolvedProjectReference ( ref => {
17561786 if ( ! ref ) return ;
1757- const out = ref . commandLine . options . outFile || ref . commandLine . options . outDir ;
1787+ const out = ref . commandLine . options . outFile || ref . commandLine . options . out ;
17581788 if ( out ) {
17591789 this . mapOfDeclarationDirectories ! . set ( getDirectoryPath ( this . toPath ( out ) ) , true ) ;
17601790 }
@@ -1767,12 +1797,74 @@ namespace ts.server {
17671797 }
17681798 } ) ;
17691799 }
1770- const dirPath = this . toPath ( path ) ;
1771- const dirPathWithTrailingDirectorySeparator = `${ dirPath } ${ directorySeparator } ` ;
1772- return ! ! forEachKey (
1773- this . mapOfDeclarationDirectories ,
1774- declDirPath => dirPath === declDirPath || startsWith ( declDirPath , dirPathWithTrailingDirectorySeparator )
1775- ) ;
1800+
1801+ return this . fileOrDirectoryExistsUsingSource ( path , /*isFile*/ false ) ;
1802+ }
1803+
1804+ private realpathIfSymlinkedProjectReferenceDts ( s : string ) : string | undefined {
1805+ return this . symlinkedFiles && this . symlinkedFiles . get ( this . toPath ( s ) ) ;
1806+ }
1807+
1808+ private getRealpath ( s : string ) : string {
1809+ return this . realpathIfSymlinkedProjectReferenceDts ( s ) ||
1810+ this . projectService . host . realpath ! ( s ) ;
1811+ }
1812+
1813+ private handleDirectoryCouldBeSymlink ( directory : string ) {
1814+ if ( ! this . useSourceOfProjectReferenceRedirect ( ) || ! this . projectReferenceCallbacks ) return ;
1815+
1816+ // Because we already watch node_modules, handle symlinks in there
1817+ if ( ! this . realpath || ! stringContains ( directory , nodeModulesPathPart ) ) return ;
1818+ if ( ! this . symlinkedDirectories ) this . symlinkedDirectories = createMap ( ) ;
1819+ const directoryPath = ensureTrailingDirectorySeparator ( this . toPath ( directory ) ) ;
1820+ if ( this . symlinkedDirectories . has ( directoryPath ) ) return ;
1821+
1822+ const real = this . projectService . host . realpath ! ( directory ) ;
1823+ let realPath : Path ;
1824+ if ( real === directory ||
1825+ ( realPath = ensureTrailingDirectorySeparator ( this . toPath ( real ) ) ) === directoryPath ) {
1826+ // not symlinked
1827+ this . symlinkedDirectories . set ( directoryPath , false ) ;
1828+ return ;
1829+ }
1830+
1831+ this . symlinkedDirectories . set ( directoryPath , {
1832+ real : ensureTrailingDirectorySeparator ( real ) ,
1833+ realPath
1834+ } ) ;
1835+ }
1836+
1837+ private fileOrDirectoryExistsUsingSource ( fileOrDirectory : string , isFile : boolean ) : boolean {
1838+ const fileOrDirectoryExistsUsingSource = isFile ?
1839+ ( file : string ) => this . fileExistsIfProjectReferenceDts ( file ) :
1840+ ( dir : string ) => this . directoryExistsIfProjectReferenceDeclDir ( dir ) ;
1841+ // Check current directory or file
1842+ const result = fileOrDirectoryExistsUsingSource ( fileOrDirectory ) ;
1843+ if ( result !== undefined ) return result ;
1844+
1845+ if ( ! this . symlinkedDirectories ) return false ;
1846+ const fileOrDirectoryPath = this . toPath ( fileOrDirectory ) ;
1847+ if ( ! stringContains ( fileOrDirectoryPath , nodeModulesPathPart ) ) return false ;
1848+ if ( isFile && this . symlinkedFiles && this . symlinkedFiles . has ( fileOrDirectoryPath ) ) return true ;
1849+
1850+ // If it contains node_modules check if its one of the symlinked path we know of
1851+ return firstDefinedIterator (
1852+ this . symlinkedDirectories . entries ( ) ,
1853+ ( [ directoryPath , symlinkedDirectory ] ) => {
1854+ if ( ! symlinkedDirectory || ! startsWith ( fileOrDirectoryPath , directoryPath ) ) return undefined ;
1855+ const result = fileOrDirectoryExistsUsingSource ( fileOrDirectoryPath . replace ( directoryPath , symlinkedDirectory . realPath ) ) ;
1856+ if ( isFile && result ) {
1857+ if ( ! this . symlinkedFiles ) this . symlinkedFiles = createMap ( ) ;
1858+ // Store the real path for the file'
1859+ const absolutePath = getNormalizedAbsolutePath ( fileOrDirectory , this . currentDirectory ) ;
1860+ this . symlinkedFiles . set (
1861+ fileOrDirectoryPath ,
1862+ `${ symlinkedDirectory . real } ${ absolutePath . replace ( new RegExp ( directoryPath , "i" ) , "" ) } `
1863+ ) ;
1864+ }
1865+ return result ;
1866+ }
1867+ ) || false ;
17761868 }
17771869
17781870 /**
@@ -1785,6 +1877,8 @@ namespace ts.server {
17851877 this . pendingReload = ConfigFileProgramReloadLevel . None ;
17861878 this . projectReferenceCallbacks = undefined ;
17871879 this . mapOfDeclarationDirectories = undefined ;
1880+ this . symlinkedDirectories = undefined ;
1881+ this . symlinkedFiles = undefined ;
17881882 let result : boolean ;
17891883 switch ( reloadLevel ) {
17901884 case ConfigFileProgramReloadLevel . Partial :
@@ -1917,6 +2011,8 @@ namespace ts.server {
19172011 this . configFileSpecs = undefined ;
19182012 this . projectReferenceCallbacks = undefined ;
19192013 this . mapOfDeclarationDirectories = undefined ;
2014+ this . symlinkedDirectories = undefined ;
2015+ this . symlinkedFiles = undefined ;
19202016 super . close ( ) ;
19212017 }
19222018
0 commit comments