From 3b95ea460ea4a4f69e2cdf0871939462aac0632f Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 17 Aug 2015 18:31:12 -0700 Subject: [PATCH 1/6] initial implementation of module resolution for node/requirejs --- Jakefile.js | 3 +- src/compiler/program.ts | 160 +++++++++++++- src/compiler/types.ts | 10 +- tests/cases/unittests/moduleResolution.ts | 254 ++++++++++++++++++++++ 4 files changed, 422 insertions(+), 5 deletions(-) create mode 100644 tests/cases/unittests/moduleResolution.ts diff --git a/Jakefile.js b/Jakefile.js index 396f15e2730d2..1e989bc5a652a 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 8ceccc0e880dd..57df3decf5528 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 326cd2bacba6f..853a83aab3146 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 0000000000000..7ae2cbe6ec568 --- /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 From 049a5fba0711feace47ec024fd6f6923369013d3 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 18 Aug 2015 13:36:08 -0700 Subject: [PATCH 2/6] added tests --- src/compiler/program.ts | 42 ++++---- tests/cases/unittests/moduleResolution.ts | 117 ++++++++++++++-------- 2 files changed, 98 insertions(+), 61 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 30e1c315eb13f..62038aa3c55b1 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -37,13 +37,13 @@ namespace ts { export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { switch(compilerOptions.moduleResolution) { - case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host); - case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions, host); + case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host); + case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions.baseUrl, host); default: return legacyNameResolver(moduleName, containingFile, compilerOptions, host); } } - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { + export function nodeModuleNameResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModule { let containingDirectory = getDirectoryPath(containingFile); if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) { @@ -132,30 +132,34 @@ namespace ts { 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"); + export function baseUrlModuleNameResolver(moduleName: string, containingFile: string, baseUrl: string, host: ModuleResolutionHost): ResolvedModule { + Debug.assert(baseUrl !== undefined); - 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 normalizedModuleName = normalizeSlashes(moduleName); + let basePart = useBaseUrl(moduleName) ? 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); + + let hasSupportedExtension = forEach(supportedExtensions, ext => fileExtensionIs(candidate, ext)); + + if (hasSupportedExtension) { + // module name already has extension - use it as is + let result = tryLoadFile(candidate); if (result) { return result; + } + } + else { + // module name does not have extension - try every 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 { diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index 7ae2cbe6ec568..d0c3c4fdc7da2 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -44,42 +44,38 @@ module ts { 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 isDirectory(fse: Directory | File): boolean { + return (fse).children !== undefined; } - function addFile(path: string, content: string, parent: Directory) { - Debug.assert(parent !== undefined); + function makeFS(files: File[]): Directory { + // create root + let {dir} = splitPath(files[0].name); + let root: Directory = { name: dir, children: {} }; - 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); + for(let f of files) { + addFile(f.name, f.content, root); } - else { - parent.children[dir] = { name: dir, content }; + + 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] = { name: dir, children: {} }); + Debug.assert(isDirectory(d)) + addFile(rel, content, d); + } + else { + parent.children[dir] = { name: dir, content }; + } } + + return root; } - - return root; } - + function splitPath(path: string): { dir: string; rel: string } { let index = path.indexOf(directorySeparator); return index === -1 @@ -87,8 +83,6 @@ module ts { : { 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 { @@ -97,7 +91,7 @@ module ts { let containingFile = { name: containingFileName, content: ""} let moduleFile = { name: moduleFileNameNoExt + ".d.ts", content: "var x;"} - let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile)); + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, moduleFile)); assert.equal(resolution.resolvedFileName, moduleFile.name); assert.isTrue(resolution.failedLookupLocations.length === 0); @@ -107,7 +101,7 @@ module ts { let containingFile = { name: containingFileName, content: ""} let moduleFile = { name: moduleFileNameNoExt + ".ts", content: "var x;"} - let resolution = nodeModuleNameResolver(moduleName, containingFile.name, opts, createModuleResolutionHost(containingFile, moduleFile)); + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, moduleFile)); assert.equal(resolution.resolvedFileName, undefined); assert.equal(resolution.failedLookupLocations.length, 3); @@ -130,12 +124,16 @@ module ts { 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, 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)); + let resolution = nodeModuleNameResolver(moduleName, containingFile.name, 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); @@ -145,19 +143,21 @@ module ts { 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"); + testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "typings", "e.d.ts", "c:/bar/e.d.ts", "c:/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"); + testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "main", "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, opts, createModuleResolutionHost(containingFile, packageJson, indexFile)); + let resolution = nodeModuleNameResolver("./foo", containingFile.name, createModuleResolutionHost(containingFile, packageJson, indexFile)); assert.equal(resolution.resolvedFileName, indexFile.name); // expect 2 failed lookup locations: assert.deepEqual(resolution.failedLookupLocations, [ @@ -171,7 +171,7 @@ module ts { 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)); + 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", @@ -195,14 +195,14 @@ module 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)); + 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, opts, createModuleResolutionHost(containingFile, moduleFile)); + 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", @@ -221,16 +221,36 @@ module ts { }); 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)); + 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.ts"); @@ -250,5 +270,18 @@ module 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"); }); + + 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 From f415097d0d11be69bd563b7cdf53ba0f46ffd726 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 18 Aug 2015 14:52:21 -0700 Subject: [PATCH 3/6] addressed PR feedback --- src/compiler/program.ts | 43 +++-------------------- tests/cases/unittests/moduleResolution.ts | 5 --- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 62038aa3c55b1..dd0cd8ce68e1c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -36,7 +36,7 @@ namespace ts { } export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { - switch(compilerOptions.moduleResolution) { + switch (compilerOptions.moduleResolution) { case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, host); case ModuleResolutionKind.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions.baseUrl, host); default: return legacyNameResolver(moduleName, containingFile, compilerOptions, host); @@ -141,26 +141,7 @@ namespace ts { let failedLookupLocations: string[] = []; - let hasSupportedExtension = forEach(supportedExtensions, ext => fileExtensionIs(candidate, ext)); - - if (hasSupportedExtension) { - // module name already has extension - use it as is - let result = tryLoadFile(candidate); - if (result) { - return result; - } - } - else { - // module name does not have extension - try every supported extension - for(let ext of supportedExtensions) { - let result = tryLoadFile(candidate + ext); - if (result) { - return result; - } - } - } - - return { resolvedFileName: undefined, failedLookupLocations }; + return forEach(supportedExtensions, ext => tryLoadFile(candidate + ext)) || { resolvedFileName: undefined, failedLookupLocations }; function tryLoadFile(location: string): ResolvedModule { if (host.fileExists(location)) { @@ -179,23 +160,9 @@ namespace ts { } 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; + // path is not rooted + // module name does not start with './' or '../' + return getRootLength(moduleName) === 0 && !nameStartsWithDotSlashOrDotDotSlash(moduleName); } export function legacyNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index d0c3c4fdc7da2..265fa7fa2f5de 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -253,7 +253,6 @@ module ts { assert.deepEqual(resolution.failedLookupLocations, expectedFailedLookupLocations) } - 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"); @@ -265,10 +264,6 @@ module ts { 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"); }); it ("load module using base url", () => { From dde7545d3455bf14676c88c9fe3e8e9903d09a20 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 20 Aug 2015 16:13:49 -0700 Subject: [PATCH 4/6] address PR feedback --- src/compiler/commandLineParser.ts | 11 ++- .../diagnosticInformationMap.generated.ts | 1 + src/compiler/diagnosticMessages.json | 5 +- src/compiler/program.ts | 78 +++++++++++-------- src/compiler/types.ts | 6 +- src/harness/harness.ts | 14 +++- src/harness/projectsRunner.ts | 1 + tests/baselines/reference/nodeResolution1.js | 12 +++ .../reference/nodeResolution1.symbols | 9 +++ .../baselines/reference/nodeResolution1.types | 10 +++ tests/baselines/reference/nodeResolution2.js | 10 +++ .../reference/nodeResolution2.symbols | 9 +++ .../baselines/reference/nodeResolution2.types | 9 +++ tests/baselines/reference/nodeResolution3.js | 10 +++ .../reference/nodeResolution3.symbols | 9 +++ .../baselines/reference/nodeResolution3.types | 9 +++ tests/cases/compiler/nodeResolution1.ts | 8 ++ tests/cases/compiler/nodeResolution2.ts | 8 ++ tests/cases/compiler/nodeResolution3.ts | 8 ++ tests/cases/unittests/moduleResolution.ts | 67 +++++++--------- 20 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 tests/baselines/reference/nodeResolution1.js create mode 100644 tests/baselines/reference/nodeResolution1.symbols create mode 100644 tests/baselines/reference/nodeResolution1.types create mode 100644 tests/baselines/reference/nodeResolution2.js create mode 100644 tests/baselines/reference/nodeResolution2.symbols create mode 100644 tests/baselines/reference/nodeResolution2.types create mode 100644 tests/baselines/reference/nodeResolution3.js create mode 100644 tests/baselines/reference/nodeResolution3.symbols create mode 100644 tests/baselines/reference/nodeResolution3.types create mode 100644 tests/cases/compiler/nodeResolution1.ts create mode 100644 tests/cases/compiler/nodeResolution2.ts create mode 100644 tests/cases/compiler/nodeResolution3.ts diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9a984b9a2f12a..c8160276f5c11 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -218,7 +218,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 5ccc0a0db9b40..1f87540949481 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -574,6 +574,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 e138e2ddac489..41bf52e90de5e 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2286,7 +2286,10 @@ "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 dd0cd8ce68e1c..2fc08c2370f89 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -36,10 +36,13 @@ namespace ts { } export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { - switch (compilerOptions.moduleResolution) { + 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.BaseUrl: return baseUrlModuleNameResolver(moduleName, containingFile, compilerOptions.baseUrl, host); - default: return legacyNameResolver(moduleName, containingFile, compilerOptions, host); + case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host); } } @@ -49,46 +52,57 @@ namespace ts { if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) { let failedLookupLocations: string[] = []; let candidate = normalizePath(combinePaths(containingDirectory, moduleName)); - let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host); + let resolvedFileName = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ false, failedLookupLocations, host); - if (result) { - return { resolvedFileName: result, failedLookupLocations }; + if (resolvedFileName) { + return { resolvedFileName, failedLookupLocations }; } - result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host); - return { resolvedFileName: result, failedLookupLocations }; + resolvedFileName = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ false, failedLookupLocations, host); + return { resolvedFileName, failedLookupLocations }; } else { - return loadModuleModuleFromNodeModules(moduleName, containingDirectory, host); + return loadModuleFromNodeModules(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; + function loadNodeModuleFromFile(candidate: string, loadOnlyDts: boolean, failedLookupLocation: string[], host: ModuleResolutionHost): string { + if (loadOnlyDts) { + return tryLoad(".d.ts"); + } + else { + return forEach(supportedExtensions, tryLoad); } - return fileName; + 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, failedLookupLocation: string[], host: ModuleResolutionHost): string { + function loadNodeModuleFromDirectory(candidate: string, loadOnlyDts: boolean, 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; - } + + 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.main) { - let mainFile = removeFileExtension(jsonContent.main); - let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, mainFile)), failedLookupLocation, host); + if (jsonContent.typings) { + let result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), loadOnlyDts, failedLookupLocation, host); if (result) { return result; } @@ -99,10 +113,10 @@ namespace ts { failedLookupLocation.push(packageJsonPath); } - return loadNodeModuleFromFile(combinePaths(candidate, "index"), failedLookupLocation, host); + return loadNodeModuleFromFile(combinePaths(candidate, "index"), loadOnlyDts, failedLookupLocation, host); } - function loadModuleModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModule { + function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModule { let failedLookupLocations: string[] = []; directory = normalizeSlashes(directory); while (true) { @@ -110,12 +124,12 @@ namespace ts { if (baseName !== "node_modules") { let nodeModulesFolder = combinePaths(directory, "node_modules"); let candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); - let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host); + let result = loadNodeModuleFromFile(candidate, /* loadOnlyDts */ true, failedLookupLocations, host); if (result) { return { resolvedFileName: result, failedLookupLocations }; } - result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host); + result = loadNodeModuleFromDirectory(candidate, /* loadOnlyDts */ true, failedLookupLocations, host); if (result) { return { resolvedFileName: result, failedLookupLocations }; } @@ -165,7 +179,7 @@ namespace ts { return getRootLength(moduleName) === 0 && !nameStartsWithDotSlashOrDotDotSlash(moduleName); } - export 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 a963463e33c2f..14157596fb617 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2010,9 +2010,8 @@ namespace ts { } export const enum ModuleResolutionKind { - Legacy = 1, - NodeJs = 2, - BaseUrl = 3 + Classic = 1, + NodeJs = 2 } export interface CompilerOptions { @@ -2054,7 +2053,6 @@ namespace ts { experimentalAsyncFunctions?: boolean; emitDecoratorMetadata?: boolean; moduleResolution?: ModuleResolutionKind - baseUrl?: string; /* @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 a0366c0c40be9..fc2042c827274 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1025,6 +1025,9 @@ module Harness { options.module = ts.ModuleKind.UMD; } else if (setting.value.toLowerCase() === "commonjs") { options.module = ts.ModuleKind.CommonJS; + if (options.moduleResolution === undefined) { + options.moduleResolution = ts.ModuleResolutionKind.Classic; + } } else if (setting.value.toLowerCase() === "system") { options.module = ts.ModuleKind.System; } else if (setting.value.toLowerCase() === "unspecified") { @@ -1036,7 +1039,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 1790956755aee..12320eed0c2d3 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -163,6 +163,7 @@ class ProjectRunner extends RunnerBase { mapRoot: testCase.resolveMapRoot && testCase.mapRoot ? ts.sys.resolvePath(testCase.mapRoot) : testCase.mapRoot, sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot ? ts.sys.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 index 265fa7fa2f5de..42b9e01b55043 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -86,30 +86,24 @@ module ts { 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, createModuleResolutionHost(containingFile, moduleFile)); - + 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); - 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, createModuleResolutionHost(containingFile, moduleFile)); + 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.equal(resolution.resolvedFileName, undefined); - assert.equal(resolution.failedLookupLocations.length, 3); - assert.deepEqual(resolution.failedLookupLocations, [ - moduleFileNameNoExt + ".d.ts", - moduleFileNameNoExt + "/package.json", - moduleFileNameNoExt + "/index.d.ts" - ]) + assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); } } @@ -129,28 +123,21 @@ module ts { testLoadAsFile("c:/foo/bar/baz.ts", "c:/foo", "c:/foo"); }); - function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { + function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { let containingFile = { name: containingFileName }; - let packageJson = { name: packageJsonFileName, content: JSON.stringify({ [fieldName]: fieldRef }) }; + 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 one failed lookup location - attempt to load module as file - assert.equal(resolution.failedLookupLocations.length, 1); + // 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", "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"); - testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "typings", "e.d.ts", "c:/bar/e.d.ts", "c:/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"); - testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "main", "e.d.ts", "c:/bar/e.d.ts", "c:/bar"); + 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", () => { @@ -159,10 +146,12 @@ module ts { 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); - // expect 2 failed lookup locations: assert.deepEqual(resolution.failedLookupLocations, [ - "/a/b/foo.d.ts", - "/c/d.d.ts" + "/a/b/foo.ts", + "/a/b/foo.tsx", + "/a/b/foo.d.ts", + "/a/b/foo/index.ts", + "/a/b/foo/index.tsx", ]); }); }); From dc2c968b089bec6ca62a684f892672ef3b94b81d Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 21 Aug 2015 10:37:13 -0700 Subject: [PATCH 5/6] addressed PR feedback: added comment --- src/harness/harness.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/harness/harness.ts b/src/harness/harness.ts index fc2042c827274..49be582606eec 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1026,6 +1026,10 @@ module Harness { } 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") { From 4f25efbd7938f238e0772d103089aa1878bd1cca Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 21 Aug 2015 10:57:36 -0700 Subject: [PATCH 6/6] simplify module resolution tests --- tests/cases/unittests/moduleResolution.ts | 58 ++--------------------- 1 file changed, 4 insertions(+), 54 deletions(-) diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index 42b9e01b55043..0917e72f1ce4d 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -6,73 +6,23 @@ declare namespace chai.assert { } module ts { - - interface Directory { - name: string; - children: Map; - } - + interface File { name: string content?: string } function createModuleResolutionHost(...files: File[]): ModuleResolutionHost { - let root = makeFS(files); + let map = arrayToMap(files, f => f.name); return { fileExists, readFile }; function fileExists(path: string): boolean { - return findFile(path, root) !== undefined; + return hasProperty(map, path); } 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 makeFS(files: File[]): Directory { - // create root - let {dir} = splitPath(files[0].name); - let root: Directory = { name: dir, children: {} }; - - 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] = { name: dir, children: {} }); - Debug.assert(isDirectory(d)) - addFile(rel, content, d); - } - else { - parent.children[dir] = { name: dir, content }; - } - } - - return root; + return hasProperty(map, path) ? map[path].content : undefined; } }