Skip to content

Commit cd1af12

Browse files
committed
Merge pull request #8486 from Microsoft/symlinked-modules
use CompilerHost.realpath to resolve actual location for symlinks
2 parents bbbb56b + 0a93768 commit cd1af12

12 files changed

+230
-28
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,10 @@
27482748
"category": "Error",
27492749
"code": 6129
27502750
},
2751+
"Resolving real path for '{0}', result '{1}'": {
2752+
"category": "Message",
2753+
"code": 6130
2754+
},
27512755
"Variable '{0}' implicitly has an '{1}' type.": {
27522756
"category": "Error",
27532757
"code": 7005

src/compiler/program.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -570,22 +570,29 @@ namespace ts {
570570
let resolvedFileName = tryLoadModuleUsingOptionalResolutionSettings(moduleName, containingDirectory, nodeLoadModuleByRelativeName,
571571
failedLookupLocations, supportedExtensions, state);
572572

573-
if (resolvedFileName) {
574-
return createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/false, failedLookupLocations);
573+
let isExternalLibraryImport = false;
574+
if (!resolvedFileName) {
575+
if (moduleHasNonRelativeName(moduleName)) {
576+
if (traceEnabled) {
577+
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName);
578+
}
579+
resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state);
580+
isExternalLibraryImport = resolvedFileName !== undefined;
581+
}
582+
else {
583+
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
584+
resolvedFileName = nodeLoadModuleByRelativeName(candidate, supportedExtensions, failedLookupLocations, /*onlyRecordFailures*/ false, state);
585+
}
575586
}
576587

577-
let isExternalLibraryImport = false;
578-
if (moduleHasNonRelativeName(moduleName)) {
588+
if (resolvedFileName && host.realpath) {
589+
const originalFileName = resolvedFileName;
590+
resolvedFileName = normalizePath(host.realpath(resolvedFileName));
579591
if (traceEnabled) {
580-
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName);
592+
trace(host, Diagnostics.Resolving_real_path_for_0_result_1, originalFileName, resolvedFileName);
581593
}
582-
resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state);
583-
isExternalLibraryImport = resolvedFileName !== undefined;
584-
}
585-
else {
586-
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
587-
resolvedFileName = nodeLoadModuleByRelativeName(candidate, supportedExtensions, failedLookupLocations, /*onlyRecordFailures*/ false, state);
588594
}
595+
589596
return createResolvedModule(resolvedFileName, isExternalLibraryImport, failedLookupLocations);
590597
}
591598

@@ -873,6 +880,7 @@ namespace ts {
873880
}
874881

875882
const newLine = getNewLineCharacter(options);
883+
const realpath = sys.realpath && ((path: string) => sys.realpath(path));
876884

877885
return {
878886
getSourceFile,
@@ -886,7 +894,8 @@ namespace ts {
886894
fileExists: fileName => sys.fileExists(fileName),
887895
readFile: fileName => sys.readFile(fileName),
888896
trace: (s: string) => sys.write(s + newLine),
889-
directoryExists: directoryName => sys.directoryExists(directoryName)
897+
directoryExists: directoryName => sys.directoryExists(directoryName),
898+
realpath
890899
};
891900
}
892901

src/compiler/sys.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace ts {
2929
createHash?(data: string): string;
3030
getMemoryUsage?(): number;
3131
exit(exitCode?: number): void;
32+
realpath?(path: string): string;
3233
}
3334

3435
export interface FileWatcher {
@@ -73,6 +74,7 @@ namespace ts {
7374
readDirectory(path: string, extension?: string, exclude?: string[]): string[];
7475
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
7576
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
77+
realpath(path: string): string;
7678
};
7779

7880
export var sys: System = (function () {
@@ -527,12 +529,15 @@ namespace ts {
527529
},
528530
exit(exitCode?: number): void {
529531
process.exit(exitCode);
532+
},
533+
realpath(path: string): string {
534+
return _fs.realpathSync(path);
530535
}
531536
};
532537
}
533538

534539
function getChakraSystem(): System {
535-
540+
const realpath = ChakraHost.realpath && ((path: string) => ChakraHost.realpath(path));
536541
return {
537542
newLine: ChakraHost.newLine || "\r\n",
538543
args: ChakraHost.args,
@@ -558,6 +563,7 @@ namespace ts {
558563
getCurrentDirectory: () => ChakraHost.currentDirectory,
559564
readDirectory: ChakraHost.readDirectory,
560565
exit: ChakraHost.quit,
566+
realpath
561567
};
562568
}
563569

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,6 +2779,7 @@ namespace ts {
27792779
readFile(fileName: string): string;
27802780
trace?(s: string): void;
27812781
directoryExists?(directoryName: string): boolean;
2782+
realpath?(path: string): string;
27822783
}
27832784

27842785
export interface ResolvedModule {

src/harness/compilerRunner.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,16 @@ class CompilerBaselineRunner extends RunnerBase {
8989
otherFiles = [];
9090

9191
if (testCaseContent.settings["noImplicitReferences"] || /require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) {
92-
toBeCompiled.push({ unitName: this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content });
92+
toBeCompiled.push({ unitName: this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions });
9393
units.forEach(unit => {
9494
if (unit.name !== lastUnit.name) {
95-
otherFiles.push({ unitName: this.makeUnitName(unit.name, rootDir), content: unit.content });
95+
otherFiles.push({ unitName: this.makeUnitName(unit.name, rootDir), content: unit.content, fileOptions: unit.fileOptions });
9696
}
9797
});
9898
}
9999
else {
100100
toBeCompiled = units.map(unit => {
101-
return { unitName: this.makeUnitName(unit.name, rootDir), content: unit.content };
101+
return { unitName: this.makeUnitName(unit.name, rootDir), content: unit.content, fileOptions: unit.fileOptions };
102102
});
103103
}
104104

src/harness/harness.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -855,21 +855,31 @@ namespace Harness {
855855
// Local get canonical file name function, that depends on passed in parameter for useCaseSensitiveFileNames
856856
const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames);
857857

858-
const fileMap: ts.FileMap<ts.SourceFile> = ts.createFileMap<ts.SourceFile>();
858+
let realPathMap: ts.FileMap<string>;
859+
const fileMap: ts.FileMap<() => ts.SourceFile> = ts.createFileMap<() => ts.SourceFile>();
859860
for (const file of inputFiles) {
860861
if (file.content !== undefined) {
861862
const fileName = ts.normalizePath(file.unitName);
862-
const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget);
863863
const path = ts.toPath(file.unitName, currentDirectory, getCanonicalFileName);
864-
fileMap.set(path, sourceFile);
864+
if (file.fileOptions && file.fileOptions["symlink"]) {
865+
const link = file.fileOptions["symlink"];
866+
const linkPath = ts.toPath(link, currentDirectory, getCanonicalFileName);
867+
if (!realPathMap) {
868+
realPathMap = ts.createFileMap<string>();
869+
}
870+
realPathMap.set(linkPath, fileName);
871+
fileMap.set(path, (): ts.SourceFile => { throw new Error("Symlinks should always be resolved to a realpath first"); });
872+
}
873+
const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget);
874+
fileMap.set(path, () => sourceFile);
865875
}
866876
}
867877

868878
function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget) {
869879
fileName = ts.normalizePath(fileName);
870880
const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName);
871881
if (fileMap.contains(path)) {
872-
return fileMap.get(path);
882+
return fileMap.get(path)();
873883
}
874884
else if (fileName === fourslashFileName) {
875885
const tsFn = "tests/cases/fourslash/" + fourslashFileName;
@@ -898,11 +908,16 @@ namespace Harness {
898908
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
899909
getNewLine: () => newLine,
900910
fileExists: fileName => {
901-
return fileMap.contains(ts.toPath(fileName, currentDirectory, getCanonicalFileName));
911+
const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName);
912+
return fileMap.contains(path) || (realPathMap && realPathMap.contains(path));
902913
},
903914
readFile: (fileName: string): string => {
904-
return fileMap.get(ts.toPath(fileName, currentDirectory, getCanonicalFileName)).getText();
905-
}
915+
return fileMap.get(ts.toPath(fileName, currentDirectory, getCanonicalFileName))().getText();
916+
},
917+
realpath: realPathMap && ((f: string) => {
918+
const path = ts.toPath(f, currentDirectory, getCanonicalFileName);
919+
return realPathMap.contains(path) ? realPathMap.get(path) : path;
920+
})
906921
};
907922
}
908923

@@ -923,7 +938,8 @@ namespace Harness {
923938
{ name: "libFiles", type: "string" },
924939
{ name: "noErrorTruncation", type: "boolean" },
925940
{ name: "suppressOutputPathCheck", type: "boolean" },
926-
{ name: "noImplicitReferences", type: "boolean" }
941+
{ name: "noImplicitReferences", type: "boolean" },
942+
{ name: "symlink", type: "string" }
927943
];
928944

929945
let optionsIndex: ts.Map<ts.CommandLineOption>;
@@ -978,6 +994,7 @@ namespace Harness {
978994
export interface TestFile {
979995
unitName: string;
980996
content: string;
997+
fileOptions?: any;
981998
}
982999

9831000
export interface CompilationOutput {
@@ -1415,10 +1432,8 @@ namespace Harness {
14151432
// Comment line, check for global/file @options and record them
14161433
optionRegex.lastIndex = 0;
14171434
const metaDataName = testMetaData[1].toLowerCase();
1418-
if (metaDataName === "filename") {
1419-
currentFileOptions[testMetaData[1]] = testMetaData[2];
1420-
}
1421-
else {
1435+
currentFileOptions[testMetaData[1]] = testMetaData[2];
1436+
if (metaDataName !== "filename") {
14221437
continue;
14231438
}
14241439

src/services/shims.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,15 @@ namespace ts {
431431
export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost {
432432

433433
public directoryExists: (directoryName: string) => boolean;
434+
public realpath: (path: string) => string;
434435

435436
constructor(private shimHost: CoreServicesShimHost) {
436437
if ("directoryExists" in this.shimHost) {
437438
this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName);
438439
}
440+
if ("realpath" in this.shimHost) {
441+
this.realpath = path => this.shimHost.realpath(path);
442+
}
439443
}
440444

441445
public readDirectory(rootDir: string, extension: string, exclude: string[], depth?: number): string[] {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//// [tests/cases/compiler/moduleResolutionWithSymlinks.ts] ////
2+
3+
//// [index.ts]
4+
5+
export class MyClass{}
6+
7+
//// [index.ts]
8+
import {MyClass} from "library-a";
9+
export { MyClass as MyClass2 }
10+
11+
//// [app.ts]
12+
import { MyClass } from "./library-a";
13+
import { MyClass2 } from "./library-b";
14+
15+
let x: MyClass;
16+
let y: MyClass2;
17+
x = y;
18+
y = x;
19+
20+
//// [index.js]
21+
"use strict";
22+
var MyClass = (function () {
23+
function MyClass() {
24+
}
25+
return MyClass;
26+
}());
27+
exports.MyClass = MyClass;
28+
//// [index.js]
29+
"use strict";
30+
var library_a_1 = require("library-a");
31+
exports.MyClass2 = library_a_1.MyClass;
32+
//// [app.js]
33+
"use strict";
34+
var x;
35+
var y;
36+
x = y;
37+
y = x;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
=== /src/app.ts ===
2+
import { MyClass } from "./library-a";
3+
>MyClass : Symbol(MyClass, Decl(app.ts, 0, 8))
4+
5+
import { MyClass2 } from "./library-b";
6+
>MyClass2 : Symbol(MyClass2, Decl(app.ts, 1, 8))
7+
8+
let x: MyClass;
9+
>x : Symbol(x, Decl(app.ts, 3, 3))
10+
>MyClass : Symbol(MyClass, Decl(app.ts, 0, 8))
11+
12+
let y: MyClass2;
13+
>y : Symbol(y, Decl(app.ts, 4, 3))
14+
>MyClass2 : Symbol(MyClass2, Decl(app.ts, 1, 8))
15+
16+
x = y;
17+
>x : Symbol(x, Decl(app.ts, 3, 3))
18+
>y : Symbol(y, Decl(app.ts, 4, 3))
19+
20+
y = x;
21+
>y : Symbol(y, Decl(app.ts, 4, 3))
22+
>x : Symbol(x, Decl(app.ts, 3, 3))
23+
24+
=== /src/library-a/index.ts ===
25+
26+
export class MyClass{}
27+
>MyClass : Symbol(MyClass, Decl(index.ts, 0, 0))
28+
29+
=== /src/library-b/index.ts ===
30+
import {MyClass} from "library-a";
31+
>MyClass : Symbol(MyClass, Decl(index.ts, 0, 8))
32+
33+
export { MyClass as MyClass2 }
34+
>MyClass : Symbol(MyClass2, Decl(index.ts, 1, 8))
35+
>MyClass2 : Symbol(MyClass2, Decl(index.ts, 1, 8))
36+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[
2+
"======== Resolving module './library-a' from '/src/app.ts'. ========",
3+
"Module resolution kind is not specified, using 'NodeJs'.",
4+
"Loading module as file / folder, candidate module location '/src/library-a'.",
5+
"File '/src/library-a.ts' does not exist.",
6+
"File '/src/library-a.tsx' does not exist.",
7+
"File '/src/library-a.d.ts' does not exist.",
8+
"File '/src/library-a/package.json' does not exist.",
9+
"File '/src/library-a/index.ts' exist - use it as a name resolution result.",
10+
"Resolving real path for '/src/library-a/index.ts', result '/src/library-a/index.ts'",
11+
"======== Module name './library-a' was successfully resolved to '/src/library-a/index.ts'. ========",
12+
"======== Resolving module './library-b' from '/src/app.ts'. ========",
13+
"Module resolution kind is not specified, using 'NodeJs'.",
14+
"Loading module as file / folder, candidate module location '/src/library-b'.",
15+
"File '/src/library-b.ts' does not exist.",
16+
"File '/src/library-b.tsx' does not exist.",
17+
"File '/src/library-b.d.ts' does not exist.",
18+
"File '/src/library-b/package.json' does not exist.",
19+
"File '/src/library-b/index.ts' exist - use it as a name resolution result.",
20+
"Resolving real path for '/src/library-b/index.ts', result '/src/library-b/index.ts'",
21+
"======== Module name './library-b' was successfully resolved to '/src/library-b/index.ts'. ========",
22+
"======== Resolving module 'library-a' from '/src/library-b/index.ts'. ========",
23+
"Module resolution kind is not specified, using 'NodeJs'.",
24+
"Loading module 'library-a' from 'node_modules' folder.",
25+
"File '/src/library-b/node_modules/library-a.ts' does not exist.",
26+
"File '/src/library-b/node_modules/library-a.tsx' does not exist.",
27+
"File '/src/library-b/node_modules/library-a.d.ts' does not exist.",
28+
"File '/src/library-b/node_modules/library-a/package.json' does not exist.",
29+
"File '/src/library-b/node_modules/library-a/index.ts' exist - use it as a name resolution result.",
30+
"Resolving real path for '/src/library-b/node_modules/library-a/index.ts', result '/src/library-a/index.ts'",
31+
"======== Module name 'library-a' was successfully resolved to '/src/library-a/index.ts'. ========"
32+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
=== /src/app.ts ===
2+
import { MyClass } from "./library-a";
3+
>MyClass : typeof MyClass
4+
5+
import { MyClass2 } from "./library-b";
6+
>MyClass2 : typeof MyClass
7+
8+
let x: MyClass;
9+
>x : MyClass
10+
>MyClass : MyClass
11+
12+
let y: MyClass2;
13+
>y : MyClass
14+
>MyClass2 : MyClass
15+
16+
x = y;
17+
>x = y : MyClass
18+
>x : MyClass
19+
>y : MyClass
20+
21+
y = x;
22+
>y = x : MyClass
23+
>y : MyClass
24+
>x : MyClass
25+
26+
=== /src/library-a/index.ts ===
27+
28+
export class MyClass{}
29+
>MyClass : MyClass
30+
31+
=== /src/library-b/index.ts ===
32+
import {MyClass} from "library-a";
33+
>MyClass : typeof MyClass
34+
35+
export { MyClass as MyClass2 }
36+
>MyClass : typeof MyClass
37+
>MyClass2 : typeof MyClass
38+

0 commit comments

Comments
 (0)