diff --git a/Jakefile.js b/Jakefile.js index d11951d9da764..ea13cce6685f8 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/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9fa05a5d43e76..7a7ba34da87eb 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -225,7 +225,16 @@ namespace ts { type: "boolean", experimental: true, description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators - } + }, + { + name: "moduleResolution", + type: { + "node": ModuleResolutionKind.NodeJs, + "classic": ModuleResolutionKind.Classic + }, + experimental: true, + description: Diagnostics.Specifies_module_resolution_strategy_Colon_node_Node_or_classic_TypeScript_pre_1_6 + } ]; export function parseCommandLine(commandLine: string[]): ParsedCommandLine { diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index df3693fd99c22..76c68b4c3b4b0 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -565,6 +565,7 @@ namespace ts { Enables_experimental_support_for_emitting_type_metadata_for_decorators: { code: 6066, category: DiagnosticCategory.Message, key: "Enables experimental support for emitting type metadata for decorators." }, Option_experimentalAsyncFunctions_cannot_be_specified_when_targeting_ES5_or_lower: { code: 6067, category: DiagnosticCategory.Message, key: "Option 'experimentalAsyncFunctions' cannot be specified when targeting ES5 or lower." }, Enables_experimental_support_for_ES7_async_functions: { code: 6068, category: DiagnosticCategory.Message, key: "Enables experimental support for ES7 async functions." }, + Specifies_module_resolution_strategy_Colon_node_Node_or_classic_TypeScript_pre_1_6: { code: 6069, category: DiagnosticCategory.Message, key: "Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) ." }, Variable_0_implicitly_has_an_1_type: { code: 7005, category: DiagnosticCategory.Error, key: "Variable '{0}' implicitly has an '{1}' type." }, Parameter_0_implicitly_has_an_1_type: { code: 7006, category: DiagnosticCategory.Error, key: "Parameter '{0}' implicitly has an '{1}' type." }, Member_0_implicitly_has_an_1_type: { code: 7008, category: DiagnosticCategory.Error, key: "Member '{0}' implicitly has an '{1}' type." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f17ceb9675865..8113722ff3c77 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2250,7 +2250,11 @@ "category": "Message", "code": 6068 }, - + "Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) .": { + "category": "Message", + "code": 6069 + }, + "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 diff --git a/src/compiler/program.ts b/src/compiler/program.ts index a4cf5febd6317..fb35e63149bca 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -36,11 +36,150 @@ 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); + let moduleResolution = compilerOptions.moduleResolution !== undefined + ? compilerOptions.moduleResolution + : compilerOptions.module === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic; + + switch (moduleResolution) { + case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host); + case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host); + } + } + + export function nodeModuleNameResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModule { + let containingDirectory = getDirectoryPath(containingFile); + + if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) { + let failedLookupLocations: string[] = []; + let candidate = normalizePath(combinePaths(containingDirectory, moduleName)); + let resolvedFileName = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ false, failedLookupLocations, host); + + if (resolvedFileName) { + return { resolvedFileName, failedLookupLocations }; + } + + resolvedFileName = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ false, failedLookupLocations, host); + return { resolvedFileName, failedLookupLocations }; + } + else { + return loadModuleFromNodeModules(moduleName, containingDirectory, host); + } + } + + function loadNodeModuleFromFile(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string { + if (loadOnlyDts) { + return tryLoad(".d.ts"); + } + else { + return forEach(supportedExtensions, tryLoad); + } + + function tryLoad(ext: string): string { + let fileName = fileExtensionIs(candidate, ext) ? candidate : candidate + ext; + if (host.fileExists(fileName)) { + return fileName; + } + else { + failedLookupLocation.push(fileName); + return undefined; + } + } + } + + function loadNodeModuleFromDirectory(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string { + let packageJsonPath = combinePaths(candidate, "package.json"); + if (host.fileExists(packageJsonPath)) { + + let jsonContent: { typings?: string }; + + try { + let jsonText = host.readFile(packageJsonPath); + jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined }; + } + catch (e) { + // gracefully handle if readFile fails or returns not JSON + jsonContent = { typings: undefined }; + } + + if (jsonContent.typings) { + let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), loadOnlyDts, 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"), loadOnlyDts, failedLookupLocation, host); + } + + function loadModuleFromNodeModules(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, /* loadOnlyDts */ true, failedLookupLocations, host); + if (result) { + return { resolvedFileName: result, failedLookupLocations }; + } + + result = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ true, 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, baseUrl: string, host: ModuleResolutionHost): ResolvedModule { + Debug.assert(baseUrl !== undefined); + + let normalizedModuleName = normalizeSlashes(moduleName); + let basePart = useBaseUrl(moduleName) ? baseUrl : getDirectoryPath(containingFile); + let candidate = normalizePath(combinePaths(basePart, moduleName)); + + let failedLookupLocations: string[] = []; + + return forEach(supportedExtensions, ext => tryLoadFile(candidate + ext)) || { 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 not rooted + // module name does not start with './' or '../' + return getRootLength(moduleName) === 0 && !nameStartsWithDotSlashOrDotDotSlash(moduleName); } - function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { + export function classicNameResolver(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 91fcbb3478f44..e1204f91aca09 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2009,7 +2009,12 @@ namespace ts { Error, Message, } - + + export const enum ModuleResolutionKind { + Classic = 1, + NodeJs = 2 + } + export interface CompilerOptions { allowNonTsExtensions?: boolean; charset?: string; @@ -2049,6 +2054,7 @@ namespace ts { experimentalDecorators?: boolean; experimentalAsyncFunctions?: boolean; emitDecoratorMetadata?: boolean; + moduleResolution?: ModuleResolutionKind /* @internal */ stripInternal?: boolean; // Skip checking lib.d.ts to help speed up tests. diff --git a/src/harness/harness.ts b/src/harness/harness.ts index db026628e08cf..de67594885179 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1049,6 +1049,13 @@ module Harness { options.module = ts.ModuleKind.UMD; } else if (setting.value.toLowerCase() === "commonjs") { options.module = ts.ModuleKind.CommonJS; + if (options.moduleResolution === undefined) { + // TODO: currently we have relative module names pretty much in all tests that use CommonJS module target. + // Such names could never be resolved in Node however classic resolution strategy still can handle them. + // Changing all module names to relative will be a major overhaul in code (but we'll do this anyway) so as a temporary measure + // we'll use ts.ModuleResolutionKind.Classic for CommonJS modules. + options.moduleResolution = ts.ModuleResolutionKind.Classic; + } } else if (setting.value.toLowerCase() === "system") { options.module = ts.ModuleKind.System; } else if (setting.value.toLowerCase() === "unspecified") { @@ -1060,7 +1067,16 @@ module Harness { options.module = setting.value; } break; - + case "moduleresolution": + switch((setting.value || "").toLowerCase()) { + case "classic": + options.moduleResolution = ts.ModuleResolutionKind.Classic; + break; + case "node": + options.moduleResolution = ts.ModuleResolutionKind.NodeJs; + break; + } + break; case "target": case "codegentarget": if (typeof setting.value === "string") { diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 94d45e8306148..fe4ab8ea8ea4f 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -163,6 +163,7 @@ class ProjectRunner extends RunnerBase { mapRoot: testCase.resolveMapRoot && testCase.mapRoot ? Harness.IO.resolvePath(testCase.mapRoot) : testCase.mapRoot, sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot ? Harness.IO.resolvePath(testCase.sourceRoot) : testCase.sourceRoot, module: moduleKind, + moduleResolution: ts.ModuleResolutionKind.Classic, // currently all tests use classic module resolution kind, this will change in the future noResolve: testCase.noResolve, rootDir: testCase.rootDir }; diff --git a/tests/baselines/reference/nodeResolution1.js b/tests/baselines/reference/nodeResolution1.js new file mode 100644 index 0000000000000..3516342e3b22f --- /dev/null +++ b/tests/baselines/reference/nodeResolution1.js @@ -0,0 +1,12 @@ +//// [tests/cases/compiler/nodeResolution1.ts] //// + +//// [a.ts] + +export var x = 1; + +//// [b.ts] +import y = require("./a"); + +//// [a.js] +exports.x = 1; +//// [b.js] diff --git a/tests/baselines/reference/nodeResolution1.symbols b/tests/baselines/reference/nodeResolution1.symbols new file mode 100644 index 0000000000000..0cc98206e90fb --- /dev/null +++ b/tests/baselines/reference/nodeResolution1.symbols @@ -0,0 +1,9 @@ +=== tests/cases/compiler/b.ts === +import y = require("./a"); +>y : Symbol(y, Decl(b.ts, 0, 0)) + +=== tests/cases/compiler/a.ts === + +export var x = 1; +>x : Symbol(x, Decl(a.ts, 1, 10)) + diff --git a/tests/baselines/reference/nodeResolution1.types b/tests/baselines/reference/nodeResolution1.types new file mode 100644 index 0000000000000..4f29acfcc032d --- /dev/null +++ b/tests/baselines/reference/nodeResolution1.types @@ -0,0 +1,10 @@ +=== tests/cases/compiler/b.ts === +import y = require("./a"); +>y : typeof y + +=== tests/cases/compiler/a.ts === + +export var x = 1; +>x : number +>1 : number + diff --git a/tests/baselines/reference/nodeResolution2.js b/tests/baselines/reference/nodeResolution2.js new file mode 100644 index 0000000000000..a5935461088c9 --- /dev/null +++ b/tests/baselines/reference/nodeResolution2.js @@ -0,0 +1,10 @@ +//// [tests/cases/compiler/nodeResolution2.ts] //// + +//// [a.d.ts] + +export var x: number; + +//// [b.ts] +import y = require("a"); + +//// [b.js] diff --git a/tests/baselines/reference/nodeResolution2.symbols b/tests/baselines/reference/nodeResolution2.symbols new file mode 100644 index 0000000000000..643d285e7b8c6 --- /dev/null +++ b/tests/baselines/reference/nodeResolution2.symbols @@ -0,0 +1,9 @@ +=== tests/cases/compiler/b.ts === +import y = require("a"); +>y : Symbol(y, Decl(b.ts, 0, 0)) + +=== tests/cases/compiler/node_modules/a.d.ts === + +export var x: number; +>x : Symbol(x, Decl(a.d.ts, 1, 10)) + diff --git a/tests/baselines/reference/nodeResolution2.types b/tests/baselines/reference/nodeResolution2.types new file mode 100644 index 0000000000000..896af7f8bdaf6 --- /dev/null +++ b/tests/baselines/reference/nodeResolution2.types @@ -0,0 +1,9 @@ +=== tests/cases/compiler/b.ts === +import y = require("a"); +>y : typeof y + +=== tests/cases/compiler/node_modules/a.d.ts === + +export var x: number; +>x : number + diff --git a/tests/baselines/reference/nodeResolution3.js b/tests/baselines/reference/nodeResolution3.js new file mode 100644 index 0000000000000..6e0ec60832301 --- /dev/null +++ b/tests/baselines/reference/nodeResolution3.js @@ -0,0 +1,10 @@ +//// [tests/cases/compiler/nodeResolution3.ts] //// + +//// [index.d.ts] + +export var x: number; + +//// [a.ts] +import y = require("b"); + +//// [a.js] diff --git a/tests/baselines/reference/nodeResolution3.symbols b/tests/baselines/reference/nodeResolution3.symbols new file mode 100644 index 0000000000000..0fca588676f1c --- /dev/null +++ b/tests/baselines/reference/nodeResolution3.symbols @@ -0,0 +1,9 @@ +=== tests/cases/compiler/a.ts === +import y = require("b"); +>y : Symbol(y, Decl(a.ts, 0, 0)) + +=== tests/cases/compiler/node_modules/b/index.d.ts === + +export var x: number; +>x : Symbol(x, Decl(index.d.ts, 1, 10)) + diff --git a/tests/baselines/reference/nodeResolution3.types b/tests/baselines/reference/nodeResolution3.types new file mode 100644 index 0000000000000..82a1ccb27d34a --- /dev/null +++ b/tests/baselines/reference/nodeResolution3.types @@ -0,0 +1,9 @@ +=== tests/cases/compiler/a.ts === +import y = require("b"); +>y : typeof y + +=== tests/cases/compiler/node_modules/b/index.d.ts === + +export var x: number; +>x : number + diff --git a/tests/cases/compiler/nodeResolution1.ts b/tests/cases/compiler/nodeResolution1.ts new file mode 100644 index 0000000000000..19316ef251b5d --- /dev/null +++ b/tests/cases/compiler/nodeResolution1.ts @@ -0,0 +1,8 @@ +// @module: commonjs +// @moduleResolution: node + +// @filename: a.ts +export var x = 1; + +// @filename: b.ts +import y = require("./a"); \ No newline at end of file diff --git a/tests/cases/compiler/nodeResolution2.ts b/tests/cases/compiler/nodeResolution2.ts new file mode 100644 index 0000000000000..9d1972c72390e --- /dev/null +++ b/tests/cases/compiler/nodeResolution2.ts @@ -0,0 +1,8 @@ +// @module: commonjs +// @moduleResolution: node + +// @filename: node_modules/a.d.ts +export var x: number; + +// @filename: b.ts +import y = require("a"); \ No newline at end of file diff --git a/tests/cases/compiler/nodeResolution3.ts b/tests/cases/compiler/nodeResolution3.ts new file mode 100644 index 0000000000000..ede8f76dbbeef --- /dev/null +++ b/tests/cases/compiler/nodeResolution3.ts @@ -0,0 +1,8 @@ +// @module: commonjs +// @moduleResolution: node + +// @filename: node_modules/b/index.d.ts +export var x: number; + +// @filename: a.ts +import y = require("b"); \ No newline at end of file diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts new file mode 100644 index 0000000000000..0917e72f1ce4d --- /dev/null +++ b/tests/cases/unittests/moduleResolution.ts @@ -0,0 +1,221 @@ +/// +/// + +declare namespace chai.assert { + function deepEqual(actual: any, expected: any): void; +} + +module ts { + + interface File { + name: string + content?: string + } + + function createModuleResolutionHost(...files: File[]): ModuleResolutionHost { + let map = arrayToMap(files, f => f.name); + + return { fileExists, readFile }; + + function fileExists(path: string): boolean { + return hasProperty(map, path); + } + + function readFile(path: string): string { + return hasProperty(map, path) ? map[path].content : undefined; + } + } + + 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) }; + } + + describe("Node module resolution - relative paths", () => { + + function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { + for (let ext of supportedExtensions) { + let containingFile = { name: containingFileName } + let moduleFile = { name: moduleFileNameNoExt + ext } + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, moduleFile)); + assert.equal(resolution.resolvedFileName, moduleFile.name); + + let failedLookupLocations: string[] = []; + let dir = getDirectoryPath(containingFileName); + for (let e of supportedExtensions) { + if (e === ext) { + break; + } + else { + failedLookupLocations.push(normalizePath(getRootLength(moduleName) === 0 ? combinePaths(dir, moduleName) : moduleName) + e); + } + } + + assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); + } + } + + 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"); + }); + + it("module name that starts with 'c:/' script extension resolved as relative file name", () => { + testLoadAsFile("c:/foo/bar/baz.ts", "c:/foo", "c:/foo"); + }); + + function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { + let containingFile = { name: containingFileName }; + let packageJson = { name: packageJsonFileName, content: JSON.stringify({ "typings": fieldRef }) }; + let moduleFile = { name: moduleFileName }; + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, packageJson, moduleFile)); + assert.equal(resolution.resolvedFileName, moduleFile.name); + // expect three failed lookup location - attempt to load module as file with all supported extensions + assert.equal(resolution.failedLookupLocations.length, 3); + } + + it("module name as directory - load from typings", () => { + testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "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", "e.d.ts", "/a/bar/e.d.ts", "../../bar"); + testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "e.d.ts", "/bar/e.d.ts", "/bar"); + testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "e.d.ts", "c:/bar/e.d.ts", "c:/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, createModuleResolutionHost(containingFile, packageJson, indexFile)); + assert.equal(resolution.resolvedFileName, indexFile.name); + assert.deepEqual(resolution.failedLookupLocations, [ + "/a/b/foo.ts", + "/a/b/foo.tsx", + "/a/b/foo.d.ts", + "/a/b/foo/index.ts", + "/a/b/foo/index.tsx", + ]); + }); + }); + + 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, 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, 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, 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", () => { + + 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, "", createModuleResolutionHost(containingFile, moduleFile)); + assert.equal(resolution.resolvedFileName, moduleFile.name); + let expectedFailedLookupLocations: string[] = []; + + let moduleNameHasExt = forEach(supportedExtensions, e => fileExtensionIs(moduleName, e)); + if (!moduleNameHasExt) { + let dir = getDirectoryPath(containingFileName); + + // add candidates with extensions that precede extension of the actual module name file in the list of supportd extensions + for (let ext of supportedExtensions) { + + let hasExtension = ext !== ".ts" + ? fileExtensionIs(moduleFileName, ext) + : fileExtensionIs(moduleFileName, ".ts") && !fileExtensionIs(moduleFileName, ".d.ts"); + + if (hasExtension) { + break; + } + else { + expectedFailedLookupLocations.push(normalizePath(combinePaths(dir, moduleName + ext))); + } + } + } + + assert.deepEqual(resolution.failedLookupLocations, expectedFailedLookupLocations) + } + + 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"); + }); + + it ("load module using base url", () => { + function test(containingFileName: string, moduleFileName: string, moduleName: string, baseUrl: string): void { + let containingFile = { name: containingFileName }; + let moduleFile = { name: moduleFileName }; + let resolution = baseUrlModuleNameResolver(moduleName, containingFileName, baseUrl, createModuleResolutionHost(containingFile, moduleFile)); + assert.equal(resolution.resolvedFileName, moduleFile.name); + } + + test("/a/base/c/d.ts", "/a/base/c/d/e.ts", "c/d/e", "/a/base"); + test("/a/base/c/d.ts", "/a/base/c/d/e.d.ts", "c/d/e", "/a/base"); + test("/a/base/c/d.ts", "/a/base/c/d/e.tsx", "c/d/e", "/a/base"); + }); + }); +} \ No newline at end of file