diff --git a/.gitignore b/.gitignore index f487ea6dd29ba..47c305c279378 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +!tests/**/node_modules/ built/* tests/cases/*.js tests/cases/*/*.js diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8536f24a7e2c5..7be66af3e9bb3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -859,17 +859,9 @@ module ts { } let fileName: string; let sourceFile: SourceFile; - while (true) { - fileName = normalizePath(combinePaths(searchPath, moduleName)); - sourceFile = forEach(supportedExtensions, extension => host.getSourceFile(fileName + extension)); - if (sourceFile || isRelative) { - break; - } - let parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; + let resolvedName = host.resolveExternalModule(moduleName, searchPath); + if (resolvedName) { + sourceFile = host.getSourceFile(resolvedName); } if (sourceFile) { if (sourceFile.symbol) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index f5bf44ec809b1..7cf4292e9e861 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -106,7 +106,9 @@ module ts { getCurrentDirectory: () => currentDirectory || (currentDirectory = sys.getCurrentDirectory()), useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, getCanonicalFileName, - getNewLine: () => newLine + getNewLine: () => newLine, + readFile: sys.readFile, + fileExists: sys.fileExists }; } @@ -155,6 +157,7 @@ module ts { let commonSourceDirectory: string; let diagnosticsProducingTypeChecker: TypeChecker; let noDiagnosticsTypeChecker: TypeChecker; + let resolvedExternalModuleCache: Map = {}; let start = new Date().getTime(); @@ -184,6 +187,7 @@ module ts { getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(), getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), + resolveExternalModule }; return program; @@ -236,6 +240,60 @@ module ts { emitTime += new Date().getTime() - start; return emitResult; } + + function resolveExternalModule(moduleName: string, containingFile: string): string { + let cacheLookupName = moduleName + containingFile; + if (resolvedExternalModuleCache[cacheLookupName]) { + return resolvedExternalModuleCache[cacheLookupName]; + } + if (resolvedExternalModuleCache[cacheLookupName] === '') { + return undefined; + } + function getNameIfExists(fileName: string): string { + if (host.fileExists(fileName)) { + return fileName; + } + } + while (true) { + // Look at files by all extensions + let found = forEach(supportedExtensions, + extension => getNameIfExists(normalizePath(combinePaths(containingFile, moduleName)) + extension)); + // Also look at all files by node_modules + if (!found) { + found = forEach(supportedExtensions, + extension => getNameIfExists(normalizePath(combinePaths(combinePaths(containingFile, "node_modules"), moduleName)) + extension)); + } + // Also look at package.json's main in node_modules + if (!found) { + // If we found a package.json then look at its main field + let pkgJson = getNameIfExists(normalizePath(combinePaths(combinePaths(combinePaths(containingFile, "node_modules"), moduleName), "package.json"))); + if (pkgJson) { + let pkgFile = JSON.parse(host.readFile(pkgJson)); + if (pkgFile.main) { + var indexFileName = removeFileExtension(combinePaths(getDirectoryPath(pkgJson), pkgFile.main)); + found = forEach(supportedExtensions, + extension => getNameIfExists(indexFileName + extension)) + } + } + } + // look at node_modules index + if (!found) { + found = forEach(supportedExtensions, + extension => getNameIfExists(normalizePath(combinePaths(combinePaths(combinePaths(containingFile, "node_modules"), moduleName), "index")) + extension)); + } + + // Finally cache and return or continue up the directory tree + if (found) { + return resolvedExternalModuleCache[cacheLookupName] = found; + } + let parentPath = getDirectoryPath(containingFile); + if (parentPath === containingFile) { + resolvedExternalModuleCache[cacheLookupName] = ''; + return undefined; + } + containingFile = parentPath; + } + } function getSourceFile(fileName: string) { fileName = host.getCanonicalFileName(normalizeSlashes(fileName)); @@ -428,18 +486,9 @@ module ts { if (moduleNameExpr && moduleNameExpr.kind === SyntaxKind.StringLiteral) { let moduleNameText = (moduleNameExpr).text; if (moduleNameText) { - let searchPath = basePath; - let searchName: string; - while (true) { - searchName = normalizePath(combinePaths(searchPath, moduleNameText)); - if (forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr))) { - break; - } - let parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; + let resolvedName = resolveExternalModule(moduleNameText, basePath); + if (resolvedName) { + findModuleSourceFile(resolvedName, moduleNameExpr); } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 86e680ca8b047..99e12ee0b4407 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1069,6 +1069,8 @@ module ts { * Gets a type checker that can be used to semantically analyze source fils in the program. */ getTypeChecker(): TypeChecker; + + resolveExternalModule(moduleName: string, basePath: string): string; /* @internal */ getCommonSourceDirectory(): string; @@ -1133,6 +1135,7 @@ module ts { export interface TypeCheckerHost { getCompilerOptions(): CompilerOptions; + resolveExternalModule(moduleName: string, basePath: string): string; getSourceFiles(): SourceFile[]; getSourceFile(fileName: string): SourceFile; } @@ -1878,6 +1881,8 @@ module ts { getCanonicalFileName(fileName: string): string; useCaseSensitiveFileNames(): boolean; getNewLine(): string; + readFile(path: string): string; + fileExists(path: string): boolean; } export interface TextSpan { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index ef4410b3e3367..a0674afb5536d 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2230,6 +2230,8 @@ module FourSlash { { let host = Harness.Compiler.createCompilerHost([{ unitName: Harness.Compiler.fourslashFileName, content: undefined }], (fn, contents) => fourslashJsOutput = contents, + (fn) => undefined, + (fn) => true, ts.ScriptTarget.Latest, ts.sys.useCaseSensitiveFileNames); @@ -2252,6 +2254,8 @@ module FourSlash { { unitName: fileName, content: content } ], (fn, contents) => result = contents, + (fn) => result, + (fn) => true, ts.ScriptTarget.Latest, ts.sys.useCaseSensitiveFileNames); diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 9ed515eb5b60b..9d4d09f5b5503 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -822,6 +822,8 @@ module Harness { export function createCompilerHost(inputFiles: { unitName: string; content: string; }[], writeFile: (fn: string, contents: string, writeByteOrderMark: boolean) => void, + readFile: (fn: string) => string, + fileExists: (fn: string) => boolean, scriptTarget: ts.ScriptTarget, useCaseSensitiveFileNames: boolean, // the currentDirectory is needed for rwcRunner to passed in specified current directory to compiler host @@ -876,6 +878,8 @@ module Harness { }, getDefaultLibFileName: options => defaultLibFileName, writeFile, + readFile, + fileExists, getCanonicalFileName, useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getNewLine: () => newLine @@ -1121,8 +1125,16 @@ module Harness { var fileOutputs: GeneratedFile[] = []; var programFiles = inputFiles.concat(includeBuiltFiles).map(file => file.unitName); - var program = ts.createProgram(programFiles, options, createCompilerHost(inputFiles.concat(includeBuiltFiles).concat(otherFiles), + var compilerHostInputFiles = inputFiles.concat(includeBuiltFiles).concat(otherFiles); + var program = ts.createProgram(programFiles, options, createCompilerHost(compilerHostInputFiles, (fn, contents, writeByteOrderMark) => fileOutputs.push({ fileName: fn, code: contents, writeByteOrderMark: writeByteOrderMark }), + (fn) => { + var found = compilerHostInputFiles.filter(f=>f.unitName === fn)[0]; + return found ? found.content : undefined; + }, + (fn) => { + return !!compilerHostInputFiles.some(f=> f.unitName === fn) + }, options.target, useCaseSensitiveFileNames, currentDirectory, options.newLine)); var emitResult = program.emit(); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 0e0b8d829183d..caac75187a680 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -190,6 +190,7 @@ module Harness.LanguageService { log(s: string): void { } trace(s: string): void { } error(s: string): void { } + fileExists = (fn: string) => this.getFilenames().some(serviceAdaptorFileName=> serviceAdaptorFileName === fn); } export class NativeLanugageServiceAdapter implements LanguageServiceAdapter { @@ -236,6 +237,7 @@ module Harness.LanguageService { log(s: string): void { this.nativeHost.log(s); } trace(s: string): void { this.nativeHost.trace(s); } error(s: string): void { this.nativeHost.error(s); } + fileExists(fn: string) { return this.nativeHost.fileExists(fn); } } class ClassifierShimProxy implements ts.Classifier { diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 39221d55b2945..f571c59a2f442 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -125,7 +125,10 @@ class ProjectRunner extends RunnerBase { function compileProjectFiles(moduleKind: ts.ModuleKind, getInputFiles: ()=> string[], getSourceFileText: (fileName: string) => string, - writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void): CompileProjectFilesResult { + writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void, + readFile: (fn: string) => string, + fileExists: (fn: string) => boolean + ): CompileProjectFilesResult { var program = ts.createProgram(getInputFiles(), createCompilerOptions(), createCompilerHost()); var errors = ts.getPreEmitDiagnostics(program); @@ -186,6 +189,8 @@ class ProjectRunner extends RunnerBase { getSourceFile, getDefaultLibFileName: options => Harness.Compiler.defaultLibFileName, writeFile, + readFile, + fileExists, getCurrentDirectory, getCanonicalFileName: Harness.Compiler.getCanonicalFileName, useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, @@ -199,7 +204,7 @@ class ProjectRunner extends RunnerBase { var outputFiles: BatchCompileProjectTestCaseEmittedFile[] = []; - var projectCompilerResult = compileProjectFiles(moduleKind, () => testCase.inputFiles, getSourceFileText, writeFile); + var projectCompilerResult = compileProjectFiles(moduleKind, () => testCase.inputFiles, getSourceFileText, writeFile, readFile, fileExists); return { moduleKind, program: projectCompilerResult.program, @@ -269,6 +274,22 @@ class ProjectRunner extends RunnerBase { outputFiles.push({ emittedFileName: fileName, code: data, fileName: diskRelativeName, writeByteOrderMark: writeByteOrderMark }); } + + function getFullDiskFileName(fileName: string): string { + return ts.isRootedDiskPath(fileName) + ? fileName + : ts.normalizeSlashes(testCase.projectRoot) + "/" + ts.normalizeSlashes(fileName); + } + + function readFile(fileName: string) { + fileName = getFullDiskFileName(fileName); + return ts.sys.readFile(fileName); + } + + function fileExists(fileName: string) { + fileName = getFullDiskFileName(fileName); + return ts.sys.fileExists(fileName); + } } function compileCompileDTsFiles(compilerResult: BatchCompileProjectTestCaseResult) { @@ -301,7 +322,7 @@ class ProjectRunner extends RunnerBase { } }); - return compileProjectFiles(compilerResult.moduleKind,getInputFiles, getSourceFileText, writeFile); + return compileProjectFiles(compilerResult.moduleKind,getInputFiles, getSourceFileText, writeFile, readFile, fileExists); function findOutpuDtsFile(fileName: string) { return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.emittedFileName === fileName ? outputFile : undefined); @@ -315,6 +336,20 @@ class ProjectRunner extends RunnerBase { function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { } + + function getFullDiskFileName(fileName: string): string { + return ts.isRootedDiskPath(fileName) + ? fileName + : ts.normalizeSlashes(getCurrentDirectory()) + "/" + ts.normalizeSlashes(fileName); + } + + function readFile(fileName: string) { + return ts.sys.readFile(getFullDiskFileName(fileName)); + } + + function fileExists(fileName: string) { + return ts.forEach(allInputFiles, inputFile => inputFile.emittedFileName === fileName ? true : false); + } } function getErrorsBaseline(compilerResult: CompileProjectFilesResult) { diff --git a/src/services/services.ts b/src/services/services.ts index 4374f3f3ea893..24684b6db8d02 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -960,6 +960,9 @@ module ts { log? (s: string): void; trace? (s: string): void; error? (s: string): void; + + // Needed for mocking support + fileExists? (s: string): boolean; } // @@ -1795,7 +1798,9 @@ module ts { useCaseSensitiveFileNames: () => false, getCanonicalFileName: fileName => fileName, getCurrentDirectory: () => "", - getNewLine: () => (sys && sys.newLine) || "\r\n" + getNewLine: () => (sys && sys.newLine) || "\r\n", + readFile: sys.readFile, + fileExists: sys.fileExists }; var program = createProgram([inputFileName], options, compilerHost); @@ -2433,7 +2438,9 @@ module ts { getNewLine: () => host.getNewLine ? host.getNewLine() : "\r\n", getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, - getCurrentDirectory: () => host.getCurrentDirectory() + readFile: (fileName) => sys.readFile(fileName), + fileExists: host.fileExists ? (fn) => host.fileExists(fn) : sys.fileExists, + getCurrentDirectory: () => host.getCurrentDirectory(), }); // Release any files we have acquired in the old program but are diff --git a/src/services/shims.ts b/src/services/shims.ts index 27c70ddc878a4..633e4871c0267 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -56,6 +56,7 @@ module ts { getDefaultLibFileName(options: string): string; getNewLine?(): string; getProjectVersion?(): string; + fileExists(fileName: string): boolean; } /** Public interface of the the of a config service shim instance.*/ @@ -260,6 +261,10 @@ module ts { public error(s: string): void { this.shimHost.error(s); } + + public fileExists(fn: string): boolean { + return this.shimHost.fileExists(fn); + } public getProjectVersion(): string { if (!this.shimHost.getProjectVersion) { diff --git a/tests/baselines/reference/project/nodeModules/amd/consumeNodeModules.js b/tests/baselines/reference/project/nodeModules/amd/consumeNodeModules.js new file mode 100644 index 0000000000000..b63e477150137 --- /dev/null +++ b/tests/baselines/reference/project/nodeModules/amd/consumeNodeModules.js @@ -0,0 +1,10 @@ +define(["require", "exports", "file-at-root", "somemodule/nested-file", "find-by-index", "find-by-package-main"], function (require, exports, file_at_root_1, nested_file_1, find_by_index_1, find_by_package_main_1) { + var fileAtRoot = new file_at_root_1.FileAtRoot(); + fileAtRoot.fileAtRoot = '123'; + var nestedFile = new nested_file_1.NestedFile(); + nestedFile.nestedFile = '123'; + var findByIndex = new find_by_index_1.FindByIndex(); + findByIndex.findByIndex = '123'; + var findByPackageMain = new find_by_package_main_1.FindByPackageMain(); + findByPackageMain.findByPackageMain = '123'; +}); diff --git a/tests/baselines/reference/project/nodeModules/amd/nodeModules.json b/tests/baselines/reference/project/nodeModules/amd/nodeModules.json new file mode 100644 index 0000000000000..b253a1a1a22f6 --- /dev/null +++ b/tests/baselines/reference/project/nodeModules/amd/nodeModules.json @@ -0,0 +1,20 @@ +{ + "scenario": "nodeModules", + "projectRoot": "tests/cases/projects/nodeModules", + "inputFiles": [ + "consumeNodeModules.ts" + ], + "baselineCheck": true, + "runTest": true, + "resolvedInputFiles": [ + "lib.d.ts", + "node_modules/file-at-root.d.ts", + "node_modules/somemodule/nested-file.d.ts", + "node_modules/find-by-index/index.d.ts", + "node_modules/find-by-package-main/dist/findByPackageMain.d.ts", + "consumeNodeModules.ts" + ], + "emittedFiles": [ + "consumeNodeModules.js" + ] +} \ No newline at end of file diff --git a/tests/baselines/reference/project/nodeModules/node/consumeNodeModules.js b/tests/baselines/reference/project/nodeModules/node/consumeNodeModules.js new file mode 100644 index 0000000000000..c0c519d23c192 --- /dev/null +++ b/tests/baselines/reference/project/nodeModules/node/consumeNodeModules.js @@ -0,0 +1,12 @@ +var file_at_root_1 = require("file-at-root"); +var fileAtRoot = new file_at_root_1.FileAtRoot(); +fileAtRoot.fileAtRoot = '123'; +var nested_file_1 = require("somemodule/nested-file"); +var nestedFile = new nested_file_1.NestedFile(); +nestedFile.nestedFile = '123'; +var find_by_index_1 = require("find-by-index"); +var findByIndex = new find_by_index_1.FindByIndex(); +findByIndex.findByIndex = '123'; +var find_by_package_main_1 = require("find-by-package-main"); +var findByPackageMain = new find_by_package_main_1.FindByPackageMain(); +findByPackageMain.findByPackageMain = '123'; diff --git a/tests/baselines/reference/project/nodeModules/node/nodeModules.json b/tests/baselines/reference/project/nodeModules/node/nodeModules.json new file mode 100644 index 0000000000000..b253a1a1a22f6 --- /dev/null +++ b/tests/baselines/reference/project/nodeModules/node/nodeModules.json @@ -0,0 +1,20 @@ +{ + "scenario": "nodeModules", + "projectRoot": "tests/cases/projects/nodeModules", + "inputFiles": [ + "consumeNodeModules.ts" + ], + "baselineCheck": true, + "runTest": true, + "resolvedInputFiles": [ + "lib.d.ts", + "node_modules/file-at-root.d.ts", + "node_modules/somemodule/nested-file.d.ts", + "node_modules/find-by-index/index.d.ts", + "node_modules/find-by-package-main/dist/findByPackageMain.d.ts", + "consumeNodeModules.ts" + ], + "emittedFiles": [ + "consumeNodeModules.js" + ] +} \ No newline at end of file diff --git a/tests/cases/project/nodeModules.json b/tests/cases/project/nodeModules.json new file mode 100644 index 0000000000000..c1c6047a4fada --- /dev/null +++ b/tests/cases/project/nodeModules.json @@ -0,0 +1,9 @@ +{ + "scenario": "nodeModules", + "projectRoot": "tests/cases/projects/nodeModules", + "inputFiles": [ + "consumeNodeModules.ts" + ], + "baselineCheck": true, + "runTest": true +} \ No newline at end of file diff --git a/tests/cases/projects/nodeModules/consumeNodeModules.ts b/tests/cases/projects/nodeModules/consumeNodeModules.ts new file mode 100644 index 0000000000000..8f9c87ff60f3a --- /dev/null +++ b/tests/cases/projects/nodeModules/consumeNodeModules.ts @@ -0,0 +1,15 @@ +import {FileAtRoot} from "file-at-root"; +var fileAtRoot = new FileAtRoot(); +fileAtRoot.fileAtRoot = '123'; + +import {NestedFile} from "somemodule/nested-file"; +var nestedFile = new NestedFile(); +nestedFile.nestedFile = '123'; + +import {FindByIndex} from "find-by-index"; +var findByIndex = new FindByIndex(); +findByIndex.findByIndex = '123'; + +import {FindByPackageMain} from "find-by-package-main"; +var findByPackageMain = new FindByPackageMain(); +findByPackageMain.findByPackageMain = '123'; diff --git a/tests/cases/projects/nodeModules/node_modules/file-at-root.d.ts b/tests/cases/projects/nodeModules/node_modules/file-at-root.d.ts new file mode 100644 index 0000000000000..3693660feea3b --- /dev/null +++ b/tests/cases/projects/nodeModules/node_modules/file-at-root.d.ts @@ -0,0 +1,3 @@ +export declare class FileAtRoot{ + fileAtRoot: string; +} diff --git a/tests/cases/projects/nodeModules/node_modules/find-by-index/index.d.ts b/tests/cases/projects/nodeModules/node_modules/find-by-index/index.d.ts new file mode 100644 index 0000000000000..ed9c7b4fc8784 --- /dev/null +++ b/tests/cases/projects/nodeModules/node_modules/find-by-index/index.d.ts @@ -0,0 +1,3 @@ +export declare class FindByIndex { + findByIndex: string; +} diff --git a/tests/cases/projects/nodeModules/node_modules/find-by-package-main/dist/findByPackageMain.d.ts b/tests/cases/projects/nodeModules/node_modules/find-by-package-main/dist/findByPackageMain.d.ts new file mode 100644 index 0000000000000..3b8ec6b06ab46 --- /dev/null +++ b/tests/cases/projects/nodeModules/node_modules/find-by-package-main/dist/findByPackageMain.d.ts @@ -0,0 +1,3 @@ +export declare class FindByPackageMain { + findByPackageMain: string; +} diff --git a/tests/cases/projects/nodeModules/node_modules/find-by-package-main/package.json b/tests/cases/projects/nodeModules/node_modules/find-by-package-main/package.json new file mode 100644 index 0000000000000..a12ddb2a0b56d --- /dev/null +++ b/tests/cases/projects/nodeModules/node_modules/find-by-package-main/package.json @@ -0,0 +1,3 @@ +{ + "main": "dist/findByPackageMain" +} \ No newline at end of file diff --git a/tests/cases/projects/nodeModules/node_modules/somemodule/nested-file.d.ts b/tests/cases/projects/nodeModules/node_modules/somemodule/nested-file.d.ts new file mode 100644 index 0000000000000..b5277bda44732 --- /dev/null +++ b/tests/cases/projects/nodeModules/node_modules/somemodule/nested-file.d.ts @@ -0,0 +1,3 @@ +export declare class NestedFile { + nestedFile: string; +}