diff --git a/Jakefile.js b/Jakefile.js
index 396f15e2730..1e989bc5a65 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -143,7 +143,8 @@ var harnessSources = harnessCoreSources.concat([
"convertToBase64.ts",
"transpile.ts",
"reuseProgramStructure.ts",
- "cachingInServerLSHost.ts"
+ "cachingInServerLSHost.ts",
+ "moduleResolution.ts"
].map(function (f) {
return path.join(unittestsDirectory, f);
})).concat([
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index 8ceccc0e880..57df3decf55 100644
--- a/src/compiler/program.ts
+++ b/src/compiler/program.ts
@@ -36,11 +36,165 @@ namespace ts {
}
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
- // TODO: use different resolution strategy based on compiler options
- return legacyNameResolver(moduleName, containingFile, compilerOptions, host);
+ switch(compilerOptions.moduleResolution) {
+ case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host);
+ case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions, host);
+ default: return legacyNameResolver(moduleName, containingFile, compilerOptions, host);
+ }
+ }
+
+ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
+ let containingDirectory = getDirectoryPath(containingFile);
+
+ if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
+ let failedLookupLocations: string[] = [];
+ let candidate = normalizePath(combinePaths(containingDirectory, moduleName));
+ let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host);
+
+ if (result) {
+ return { resolvedFileName: result, failedLookupLocations };
+ }
+
+ result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host);
+ return { resolvedFileName: result, failedLookupLocations };
+ }
+ else {
+ return loadModuleModuleFromNodeModules(moduleName, containingDirectory, host);
+ }
+ }
+
+ function loadNodeModuleFromFile(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
+ // load only .d.ts files
+ let fileName = fileExtensionIs(candidate, ".d.ts") ? candidate : candidate + ".d.ts";
+ if (!host.fileExists(fileName)) {
+ failedLookupLocation.push(fileName);
+ return undefined;
+ }
+
+ return fileName;
+ }
+
+ function loadNodeModuleFromDirectory(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string {
+ let packageJsonPath = combinePaths(candidate, "package.json");
+ if (host.fileExists(packageJsonPath)) {
+ let jsonText = host.readFile(packageJsonPath);
+ let jsonContent = jsonText ? <{ typings?: string, main?: string }>JSON.parse(jsonText) : { typings: undefined, main:undefined };
+ if (jsonContent.typings) {
+ let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host);
+ if (result) {
+ return result;
+ }
+ }
+
+ if (jsonContent.main) {
+ let mainFile = removeFileExtension(jsonContent.main);
+ let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, mainFile)), failedLookupLocation, host);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ else {
+ // record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
+ failedLookupLocation.push(packageJsonPath);
+ }
+
+ return loadNodeModuleFromFile(combinePaths(candidate, "index"), failedLookupLocation, host);
+ }
+
+ function loadModuleModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModule {
+ let failedLookupLocations: string[] = [];
+ directory = normalizeSlashes(directory);
+ while (true) {
+ let baseName = getBaseFileName(directory);
+ if (baseName !== "node_modules") {
+ let nodeModulesFolder = combinePaths(directory, "node_modules");
+ let candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
+ let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host);
+ if (result) {
+ return { resolvedFileName: result, failedLookupLocations };
+ }
+
+ result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host);
+ if (result) {
+ return { resolvedFileName: result, failedLookupLocations };
+ }
+ }
+
+ let parentPath = getDirectoryPath(directory);
+ if (parentPath === directory) {
+ break;
+ }
+
+ directory = parentPath;
+ }
+
+ return { resolvedFileName: undefined, failedLookupLocations };
+ }
+
+ export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
+ Debug.assert(compilerOptions.baseUrl !== undefined, "baseUrl is mandatory when using this module resolution strategy");
+
+ let normalizedModuleName = normalizeSlashes(moduleName);
+
+ // treat module name as url that is relative to containing file if
+ let basePart = useBaseUrl(moduleName) ? compilerOptions.baseUrl : getDirectoryPath(containingFile);
+ let candidate = normalizePath(combinePaths(basePart, moduleName));
+
+
+ let failedLookupLocations: string[] = [];
+ // first - try to load file as is
+ let result = tryLoadFile(candidate);
+ if (result) {
+ return result;
+ }
+ // then try all supported extension
+ for(let ext of supportedExtensions) {
+ let result = tryLoadFile(candidate + ext);
+ if (result) {
+ return result;
+ }
+ }
+
+ return { resolvedFileName: undefined, failedLookupLocations };
+
+ function tryLoadFile(location: string): ResolvedModule {
+ if (host.fileExists(location)) {
+ return { resolvedFileName: location, failedLookupLocations };
+ }
+ else {
+ failedLookupLocations.push(location);
+ return undefined;
+ }
+ }
+ }
+
+ function nameStartsWithDotSlashOrDotDotSlash(name: string) {
+ let i = name.lastIndexOf("./", 1);
+ return i === 0 || (i === 1 && name.charCodeAt(0) === CharacterCodes.dot);
+ }
+
+ function useBaseUrl(moduleName: string): boolean {
+ // path is rooted
+ if (getRootLength(moduleName) !== 0) {
+ return false;
+ }
+
+ // module name starts with './' or '../'
+ if (nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
+ return false;
+ }
+
+ // module name has one of supported extesions
+ for(let ext of supportedExtensions ) {
+ if (fileExtensionIs(moduleName, ext)) {
+ return false;
+ }
+ }
+ return true;
}
- function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
+ export function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule {
// module names that contain '!' are used to reference resources and are not resolved to actual files on disk
if (moduleName.indexOf('!') != -1) {
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index 326cd2bacba..853a83aab31 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -2004,7 +2004,13 @@ namespace ts {
Error,
Message,
}
-
+
+ export const enum ModuleResolutionKind {
+ Legacy = 1,
+ NodeJs = 2,
+ BaseUrl = 3
+ }
+
export interface CompilerOptions {
allowNonTsExtensions?: boolean;
charset?: string;
@@ -2043,6 +2049,8 @@ namespace ts {
experimentalDecorators?: boolean;
experimentalAsyncFunctions?: boolean;
emitDecoratorMetadata?: boolean;
+ moduleResolution?: ModuleResolutionKind
+ baseUrl?: string;
/* @internal */ stripInternal?: boolean;
// Skip checking lib.d.ts to help speed up tests.
diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts
new file mode 100644
index 00000000000..7ae2cbe6ec5
--- /dev/null
+++ b/tests/cases/unittests/moduleResolution.ts
@@ -0,0 +1,254 @@
+///
+///
+
+declare namespace chai.assert {
+ function deepEqual(actual: any, expected: any): void;
+}
+
+module ts {
+
+ interface Directory {
+ name: string;
+ children: Map;
+ }
+
+ interface File {
+ name: string
+ content?: string
+ }
+
+ function createModuleResolutionHost(...files: File[]): ModuleResolutionHost {
+ let root = makeFS(files);
+
+ return { fileExists, readFile };
+
+ function fileExists(path: string): boolean {
+ return findFile(path, root) !== undefined;
+ }
+
+ function readFile(path: string): string {
+ let f = findFile(path, root);
+ return f && f.content;
+ }
+
+ function findFile(path: string, fse: File | Directory): File {
+ if (!fse) {
+ return undefined;
+ }
+
+ if (isDirectory(fse)) {
+ let {dir, rel} = splitPath(path);
+ return findFile(rel, (fse).children[dir]);
+ }
+ else {
+ return !path && fse;
+ }
+ }
+ }
+
+ function isDirectory(fse: Directory | File): boolean {
+ return (fse).children !== undefined;
+ }
+
+ function createDirectory(name: string): Directory {
+ return { name, children: {} }
+ }
+
+ function makeFS(files: File[]): Directory {
+ // create root
+ let {dir} = splitPath(files[0].name);
+ let root: Directory = createDirectory(dir);
+
+ for(let f of files) {
+ addFile(f.name, f.content, root);
+ }
+
+ function addFile(path: string, content: string, parent: Directory) {
+ Debug.assert(parent !== undefined);
+
+ let {dir, rel} = splitPath(path);
+ if (rel) {
+ let d = parent.children[dir] || (parent.children[dir] = createDirectory(dir));
+ Debug.assert(isDirectory(d))
+ addFile(rel, content, d);
+ }
+ else {
+ parent.children[dir] = { name: dir, content };
+ }
+ }
+
+ return root;
+ }
+
+ function splitPath(path: string): { dir: string; rel: string } {
+ let index = path.indexOf(directorySeparator);
+ return index === -1
+ ? { dir: path, rel: undefined }
+ : { dir: path.substr(0, index), rel: path.substr(index + 1) };
+ }
+
+ let opts: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs };
+
+ describe("Node module resolution - relative paths", () => {
+
+ function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void {
+ {
+ // loading only .d.ts files
+
+ let containingFile = { name: containingFileName, content: ""}
+ let moduleFile = { name: moduleFileNameNoExt + ".d.ts", content: "var x;"}
+ let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
+
+ assert.equal(resolution.resolvedFileName, moduleFile.name);
+ assert.isTrue(resolution.failedLookupLocations.length === 0);
+ }
+ {
+ // does not try to load .ts files
+
+ let containingFile = { name: containingFileName, content: ""}
+ let moduleFile = { name: moduleFileNameNoExt + ".ts", content: "var x;"}
+ let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
+
+ assert.equal(resolution.resolvedFileName, undefined);
+ assert.equal(resolution.failedLookupLocations.length, 3);
+ assert.deepEqual(resolution.failedLookupLocations, [
+ moduleFileNameNoExt + ".d.ts",
+ moduleFileNameNoExt + "/package.json",
+ moduleFileNameNoExt + "/index.d.ts"
+ ])
+ }
+ }
+
+ it("module name that starts with './' resolved as relative file name", () => {
+ testLoadAsFile("/foo/bar/baz.ts", "/foo/bar/foo", "./foo");
+ });
+
+ it("module name that starts with '../' resolved as relative file name", () => {
+ testLoadAsFile("/foo/bar/baz.ts", "/foo/foo", "../foo");
+ });
+
+ it("module name that starts with '/' script extension resolved as relative file name", () => {
+ testLoadAsFile("/foo/bar/baz.ts", "/foo", "/foo");
+ });
+
+ function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldName: string, fieldRef: string, moduleFileName: string, moduleName: string): void {
+ let containingFile = { name: containingFileName };
+ let packageJson = { name: packageJsonFileName, content: JSON.stringify({ [fieldName]: fieldRef }) };
+ let moduleFile = { name: moduleFileName };
+ let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, packageJson, moduleFile));
+ assert.equal(resolution.resolvedFileName, moduleFile.name);
+ // expect one failed lookup location - attempt to load module as file
+ assert.equal(resolution.failedLookupLocations.length, 1);
+ }
+
+ it("module name as directory - load from typings", () => {
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "typings", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar");
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "typings", "e.d.ts", "/a/bar/e.d.ts", "../../bar");
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "typings", "e.d.ts", "/bar/e.d.ts", "/bar");
+ });
+
+ it("module name as directory - load from main", () => {
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "main", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar");
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "main", "e.d.ts", "/a/bar/e.d.ts", "../../bar");
+ testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "main", "e.d.ts", "/bar/e.d.ts", "/bar");
+ });
+
+ it ("module name as directory - load index.d.ts", () => {
+ let containingFile = {name: "/a/b/c.ts"};
+ let packageJson = {name: "/a/b/foo/package.json", content: JSON.stringify({main: "/c/d"})};
+ let indexFile = { name: "/a/b/foo/index.d.ts" };
+ let resolution = nodeModuleNameResolver("./foo", containingFile.name, opts, createModuleResolutionHost(containingFile, packageJson, indexFile));
+ assert.equal(resolution.resolvedFileName, indexFile.name);
+ // expect 2 failed lookup locations:
+ assert.deepEqual(resolution.failedLookupLocations, [
+ "/a/b/foo.d.ts",
+ "/c/d.d.ts"
+ ]);
+ });
+ });
+
+ describe("Node module resolution - non-relative paths", () => {
+ it("load module as file - ts files not loaded", () => {
+ let containingFile = { name: "/a/b/c/d/e.ts" };
+ let moduleFile = { name: "/a/b/node_modules/foo.ts" };
+ let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
+ assert.equal(resolution.resolvedFileName, undefined);
+ assert.deepEqual(resolution.failedLookupLocations, [
+ "/a/b/c/d/node_modules/foo.d.ts",
+ "/a/b/c/d/node_modules/foo/package.json",
+ "/a/b/c/d/node_modules/foo/index.d.ts",
+ "/a/b/c/node_modules/foo.d.ts",
+ "/a/b/c/node_modules/foo/package.json",
+ "/a/b/c/node_modules/foo/index.d.ts",
+ "/a/b/node_modules/foo.d.ts",
+ "/a/b/node_modules/foo/package.json",
+ "/a/b/node_modules/foo/index.d.ts",
+ "/a/node_modules/foo.d.ts",
+ "/a/node_modules/foo/package.json",
+ "/a/node_modules/foo/index.d.ts",
+ "/node_modules/foo.d.ts",
+ "/node_modules/foo/package.json",
+ "/node_modules/foo/index.d.ts"
+ ])
+ });
+
+ it("load module as file", () => {
+ let containingFile = { name: "/a/b/c/d/e.ts" };
+ let moduleFile = { name: "/a/b/node_modules/foo.d.ts" };
+ let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
+ assert.equal(resolution.resolvedFileName, moduleFile.name);
+ });
+
+ it("load module as directory", () => {
+ let containingFile = { name: "/a/node_modules/b/c/node_modules/d/e.ts" };
+ let moduleFile = { name: "/a/node_modules/foo/index.d.ts" };
+ let resolution = nodeModuleNameResolver("foo", containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile));
+ assert.equal(resolution.resolvedFileName, moduleFile.name);
+ assert.deepEqual(resolution.failedLookupLocations, [
+ "/a/node_modules/b/c/node_modules/d/node_modules/foo.d.ts",
+ "/a/node_modules/b/c/node_modules/d/node_modules/foo/package.json",
+ "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.d.ts",
+ "/a/node_modules/b/c/node_modules/foo.d.ts",
+ "/a/node_modules/b/c/node_modules/foo/package.json",
+ "/a/node_modules/b/c/node_modules/foo/index.d.ts",
+ "/a/node_modules/b/node_modules/foo.d.ts",
+ "/a/node_modules/b/node_modules/foo/package.json",
+ "/a/node_modules/b/node_modules/foo/index.d.ts",
+ "/a/node_modules/foo.d.ts",
+ "/a/node_modules/foo/package.json"
+ ]);
+ });
+ });
+
+ describe("BaseUrl mode", () => {
+ function getCompilerOptions(baseUrl: string): CompilerOptions {
+ return { baseUrl, moduleResolution: ModuleResolutionKind.BaseUrl };
+ }
+
+ it ("load module as relative url", () => {
+ function test(containingFileName: string, moduleFileName: string, moduleName: string): void {
+ let containingFile = {name: containingFileName };
+ let moduleFile = { name: moduleFileName };
+ let resolution = baseUrlModuleNameResolver(moduleName, containingFile.name, getCompilerOptions(""), createModuleResolutionHost(containingFile, moduleFile));
+ assert.equal(resolution.resolvedFileName, moduleFile.name);
+ }
+
+ test("/a/b/c/d.ts", "/foo.ts", "/foo.ts");
+ test("/a/b/c/d.ts", "/foo.ts", "/foo");
+ test("/a/b/c/d.ts", "/foo.d.ts", "/foo");
+ test("/a/b/c/d.ts", "/foo.tsx", "/foo");
+
+ test("/a/b/c/d.ts", "/a/b/c/foo.ts", "./foo");
+ test("/a/b/c/d.ts", "/a/b/c/foo.d.ts", "./foo");
+ test("/a/b/c/d.ts", "/a/b/c/foo.tsx", "./foo");
+
+ test("/a/b/c/d.ts", "/a/b/foo.ts", "../foo");
+ test("/a/b/c/d.ts", "/a/b/foo.d.ts", "../foo");
+ test("/a/b/c/d.ts", "/a/b/foo.tsx", "../foo");
+
+ test("/a/b/c/d.ts", "/a/b/c/foo.ts", "foo.ts");
+ test("/a/b/c/d.ts", "/a/b/c/foo.tsx", "foo.tsx");
+ test("/a/b/c/d.ts", "/a/b/c/foo.d.ts", "foo.d.ts");
+ });
+ });
+}
\ No newline at end of file