diff --git a/.gitattributes b/.gitattributes index 74f5f4a640948..811a89b549379 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -*.js linguist-language=TypeScript \ No newline at end of file +*.js linguist-language=TypeScript +* -text diff --git a/package.json b/package.json index d6a8f538b78c6..1643ea12ad359 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "istanbul": "latest" }, "scripts": { - "test": "jake runtests" + "test": "jake runtests", + "clean": "jake clean", + "build": "jake local", + "build-tests": "jake tests" } } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 759b235ad0a9b..11177ee5fdc87 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -862,7 +862,9 @@ module ts { // Escape the name in the "require(...)" clause to ensure we find the right symbol. let moduleName = escapeIdentifier(moduleReferenceLiteral.text); - if (!moduleName) return; + if (!moduleName) { + return; + } let isRelative = isExternalModuleNameRelative(moduleName); if (!isRelative) { let symbol = getSymbol(globals, '"' + moduleName + '"', SymbolFlags.ValueModule); @@ -870,20 +872,9 @@ module ts { return symbol; } } - 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 fileName = getResolvedModuleFileName(getSourceFile(location), moduleReferenceLiteral); + let sourceFile = fileName && host.getSourceFile(fileName); if (sourceFile) { if (sourceFile.symbol) { return sourceFile.symbol; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 4a46e375f87f3..a0975553859d1 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5678,7 +5678,8 @@ module ts { // will immediately bail out of walking any subtrees when we can see that their parents // are already correct. let result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /* setParentNode */ true) - + // pass set of modules that were resolved before so 'createProgram' can reuse previous resolution results + result.resolvedModules = sourceFile.resolvedModules; return result; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 38634f1dea6a2..172c2328e19ef 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -422,54 +422,160 @@ module ts { } function processImportedModules(file: SourceFile, basePath: string) { - forEach(file.statements, node => { - if (node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration || node.kind === SyntaxKind.ExportDeclaration) { - let moduleNameExpr = getExternalModuleName(node); - if (moduleNameExpr && moduleNameExpr.kind === SyntaxKind.StringLiteral) { + let imports: LiteralExpression[]; + forEach(file.statements, collectImports); + if (imports) { + ensureResolvedModuleNamesAreUptoDate(file, imports); + for (let importNode of imports) { + resolveModule(importNode); + } + } + else { + file.resolvedModules = undefined; + } + return; + + function findModuleSourceFile(fileName: string, nameLiteral: Expression) { + return findSourceFile(fileName, /* isDefaultLib */ false, file, nameLiteral.pos, nameLiteral.end - nameLiteral.pos); + } + + function collectImports(node: Node): void { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportDeclaration: + let moduleNameExpr = getExternalModuleName(node); + if (!moduleNameExpr || moduleNameExpr.kind !== SyntaxKind.StringLiteral) { + return; + } 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; + if (!moduleNameText) { + return; + } + + (imports || (imports = [])).push(moduleNameExpr); + break; + case SyntaxKind.ModuleDeclaration: + if ((node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An AmbientExternalModuleDeclaration declares an external module. + // This type of declaration is permitted only in the global module. + // The StringLiteral must specify a top - level external module name. + // Relative external module names are not permitted + forEachChild((node).body, node => { + if (isExternalModuleImportEqualsDeclaration(node) && + getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) { + let moduleName = getExternalModuleImportEqualsDeclarationExpression(node); + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules + // only through top - level external module names. Relative external module names are not permitted. + if (moduleName) { + (imports || (imports = [])).push(moduleName); + } } - searchPath = parentPath; - } + }); } + break; + } + } + + function generateImportMap(relativeStartDirectory: string): Map { + // find all of the map files between startDirectory and the project root directory + let foundMaps: Map[] = []; + let currentDirectory = relativeStartDirectory; + while (true) { + let map = tryGetImportMap(currentDirectory); + if (map) + foundMaps.push(map); + // end of the line + if (!currentDirectory) + break; + currentDirectory = getDirectoryPath(currentDirectory); + } + + // merge all of the found maps, in reverse order, into the final map + let finalMap: Map = {}; + forEach(foundMaps.reverse(), foundMap => { + for (let key in foundMap) + finalMap[key] = foundMap[key]; + }); + + return finalMap; + } + + function tryGetImportMap(directory: string): Map { + directory = normalizePath(directory); + let mapFilePath = normalizePath(combinePaths(directory, 'typescript-definition-map.json')); + let absoluteMapFilePath = combinePaths(host.getCurrentDirectory(), mapFilePath); + if (!sys.fileExists(absoluteMapFilePath)) + return null; + + let map: Map = JSON.parse(sys.readFile(absoluteMapFilePath)); + let rootRelativeRoot: Map = {}; + for (let key in map) { + rootRelativeRoot[key] = combinePaths(directory, map[key]); + } + + return rootRelativeRoot; + } + + function tryResolveImportFromMap(importMap: Map, moduleNameExpr: LiteralExpression): string { + let seenKeys: Map = {}; + let currentName = moduleNameExpr.text; + while (true) { + // cycle detection + if (seenKeys[currentName]) + return null; + seenKeys[currentName] = true; + + // follow the key in the map to an alias, file or nothing + currentName = importMap[currentName]; + if (!currentName) + // TODO: add file globbing support as fallback when exact match isn't found + return null; + + // if we find a file then we are done + //if (sys.fileExists(combinePaths(host.getCurrentDirectory(), currentName))) + if (findModuleSourceFile(currentName, moduleNameExpr)) + return currentName; + } + } + + function resolveModule(moduleNameExpr: LiteralExpression): void { + let searchPath = basePath; + let searchName: string; + + if (hasResolvedModuleName(file, moduleNameExpr)) { + let fileName = getResolvedModuleFileName(file, moduleNameExpr); + if (fileName) { + findModuleSourceFile(fileName, moduleNameExpr); } + return; } - else if (node.kind === SyntaxKind.ModuleDeclaration && (node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An AmbientExternalModuleDeclaration declares an external module. - // This type of declaration is permitted only in the global module. - // The StringLiteral must specify a top - level external module name. - // Relative external module names are not permitted - forEachChild((node).body, node => { - if (isExternalModuleImportEqualsDeclaration(node) && - getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) { - - let nameLiteral = getExternalModuleImportEqualsDeclarationExpression(node); - let moduleName = nameLiteral.text; - if (moduleName) { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules - // only through top - level external module names. Relative external module names are not permitted. - let searchName = normalizePath(combinePaths(basePath, moduleName)); - forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, nameLiteral)); - } - } - }); + + let importMap = generateImportMap(searchPath); + let importFromMap = tryResolveImportFromMap(importMap, moduleNameExpr); + if (importFromMap) { + setResolvedModuleName(file, moduleNameExpr, importFromMap); + return; } - }); - function findModuleSourceFile(fileName: string, nameLiteral: Expression) { - return findSourceFile(fileName, /* isDefaultLib */ false, file, nameLiteral.pos, nameLiteral.end - nameLiteral.pos); + while (true) { + searchName = normalizePath(combinePaths(searchPath, moduleNameExpr.text)); + let referencedSourceFile = forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr)); + if (referencedSourceFile) { + setResolvedModuleName(file, moduleNameExpr, referencedSourceFile.fileName); + return; + } + + let parentPath = getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + // mark reference as non-resolved + setResolvedModuleName(file, moduleNameExpr, undefined); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 14e53ee8f728e..d15f6de1ab318 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1159,6 +1159,10 @@ module ts { // Stores a line map for the file. // This field should never be used directly to obtain line map, use getLineMap function instead. /* @internal */ lineMap: number[]; + + // Stores a mapping 'external module reference text' -> 'resolved file name' | undefined + // Content of this fiels should never be used directly - use getResolvedModuleFileName/setResolvedModuleFileName functions instead + /* @internal */ resolvedModules: Map; } export interface ScriptReferenceHost { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5198d8a228811..34394a2e9a95b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -78,6 +78,30 @@ module ts { return node.end - node.pos; } + export function ensureResolvedModuleNamesAreUptoDate(sourceFile: SourceFile, imports: LiteralExpression[]): void { + if (!sourceFile.resolvedModules) { + return; + } + // TOOD: check that imports are consistent + } + + export function hasResolvedModuleName(sourceFile: SourceFile, moduleReferenceLiteral: LiteralExpression): boolean { + return sourceFile.resolvedModules && hasProperty(sourceFile.resolvedModules, moduleReferenceLiteral.text); + } + + export function getResolvedModuleFileName(sourceFile: SourceFile, moduleReferenceLiteral: LiteralExpression): string { + return sourceFile.resolvedModules && sourceFile.resolvedModules[moduleReferenceLiteral.text]; + } + + export function setResolvedModuleName(sourceFile: SourceFile, moduleReferenceLiteral: LiteralExpression, resolvedFileName: string): void { + if (!sourceFile.resolvedModules) { + sourceFile.resolvedModules = {}; + } + + // TODO: check if value for the given key already exists and if yes - that it is the same as new value + sourceFile.resolvedModules[moduleReferenceLiteral.text] = resolvedFileName; + } + // Returns true if this node contains a parse error anywhere underneath it. export function containsParseError(node: Node): boolean { aggregateChildData(node); diff --git a/src/services/services.ts b/src/services/services.ts index 1019c5321929c..d04156838726a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -752,6 +752,7 @@ module ts { public languageVersion: ScriptTarget; public identifiers: Map; public nameTable: Map; + public resolvedModules: Map; private namedDeclarations: Map; diff --git a/tests/baselines/reference/project/import_via_mapping_file/amd/app/main.js b/tests/baselines/reference/project/import_via_mapping_file/amd/app/main.js new file mode 100644 index 0000000000000..595635d148239 --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/amd/app/main.js @@ -0,0 +1,3 @@ +define(["require", "exports", 'foo'], function (require, exports, foo_1) { + var foo = new foo_1.Foo(); +}); diff --git a/tests/baselines/reference/project/import_via_mapping_file/amd/import_via_mapping_file.json b/tests/baselines/reference/project/import_via_mapping_file/amd/import_via_mapping_file.json new file mode 100644 index 0000000000000..8a7e35cdc4566 --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/amd/import_via_mapping_file.json @@ -0,0 +1,18 @@ +{ + "scenario": "Import a module found in a mapping file", + "projectRoot": "tests/cases/projects/import_via_mapping_file", + "inputFiles": [ + "app/main.ts" + ], + "resolvedInputFiles": [ + "lib.d.ts", + "libs/library.ts", + "libs/library2.ts", + "app/main.ts" + ], + "emittedFiles": [ + "libs/library.js", + "libs/library2.js", + "app/main.js" + ] +} \ No newline at end of file diff --git a/tests/baselines/reference/project/import_via_mapping_file/amd/libs/library.js b/tests/baselines/reference/project/import_via_mapping_file/amd/libs/library.js new file mode 100644 index 0000000000000..0448c690ce281 --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/amd/libs/library.js @@ -0,0 +1,8 @@ +define(["require", "exports"], function (require, exports) { + var Foo = (function () { + function Foo() { + } + return Foo; + })(); + exports.Foo = Foo; +}); diff --git a/tests/baselines/reference/project/import_via_mapping_file/node/app/main.js b/tests/baselines/reference/project/import_via_mapping_file/node/app/main.js new file mode 100644 index 0000000000000..14d2ead7d00fe --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/node/app/main.js @@ -0,0 +1,2 @@ +var foo_1 = require('foo'); +var foo = new foo_1.Foo(); diff --git a/tests/baselines/reference/project/import_via_mapping_file/node/import_via_mapping_file.json b/tests/baselines/reference/project/import_via_mapping_file/node/import_via_mapping_file.json new file mode 100644 index 0000000000000..8a7e35cdc4566 --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/node/import_via_mapping_file.json @@ -0,0 +1,18 @@ +{ + "scenario": "Import a module found in a mapping file", + "projectRoot": "tests/cases/projects/import_via_mapping_file", + "inputFiles": [ + "app/main.ts" + ], + "resolvedInputFiles": [ + "lib.d.ts", + "libs/library.ts", + "libs/library2.ts", + "app/main.ts" + ], + "emittedFiles": [ + "libs/library.js", + "libs/library2.js", + "app/main.js" + ] +} \ No newline at end of file diff --git a/tests/baselines/reference/project/import_via_mapping_file/node/libs/library.js b/tests/baselines/reference/project/import_via_mapping_file/node/libs/library.js new file mode 100644 index 0000000000000..75275574b3c12 --- /dev/null +++ b/tests/baselines/reference/project/import_via_mapping_file/node/libs/library.js @@ -0,0 +1,6 @@ +var Foo = (function () { + function Foo() { + } + return Foo; +})(); +exports.Foo = Foo; diff --git a/tests/cases/project/import_via_mapping_file.json b/tests/cases/project/import_via_mapping_file.json new file mode 100644 index 0000000000000..38292a7c956a2 --- /dev/null +++ b/tests/cases/project/import_via_mapping_file.json @@ -0,0 +1,7 @@ +{ + "scenario": "Import a module found in a mapping file", + "projectRoot": "tests/cases/projects/import_via_mapping_file", + "inputFiles": [ + "app/main.ts" + ] +} diff --git a/tests/cases/projects/import_via_mapping_file/app/main.ts b/tests/cases/projects/import_via_mapping_file/app/main.ts new file mode 100644 index 0000000000000..1e97c1500cf35 --- /dev/null +++ b/tests/cases/projects/import_via_mapping_file/app/main.ts @@ -0,0 +1,5 @@ +import { Foo } from 'foo'; +import { Bar } from 'bar'; + +let foo = new Foo(); +let bar = new Bar(); diff --git a/tests/cases/projects/import_via_mapping_file/app/typescript-definition-map.json b/tests/cases/projects/import_via_mapping_file/app/typescript-definition-map.json new file mode 100644 index 0000000000000..e3e1b6d81bc99 --- /dev/null +++ b/tests/cases/projects/import_via_mapping_file/app/typescript-definition-map.json @@ -0,0 +1,3 @@ +{ + "bar": "../libs/library2.ts" +} diff --git a/tests/cases/projects/import_via_mapping_file/libs/library.ts b/tests/cases/projects/import_via_mapping_file/libs/library.ts new file mode 100644 index 0000000000000..08c51dd90dbed --- /dev/null +++ b/tests/cases/projects/import_via_mapping_file/libs/library.ts @@ -0,0 +1,2 @@ +export class Foo { +} diff --git a/tests/cases/projects/import_via_mapping_file/libs/library2.ts b/tests/cases/projects/import_via_mapping_file/libs/library2.ts new file mode 100644 index 0000000000000..057188e14a198 --- /dev/null +++ b/tests/cases/projects/import_via_mapping_file/libs/library2.ts @@ -0,0 +1,2 @@ +export class Bar { +} diff --git a/tests/cases/projects/import_via_mapping_file/typescript-definition-map.json b/tests/cases/projects/import_via_mapping_file/typescript-definition-map.json new file mode 100644 index 0000000000000..39b38de6d83ed --- /dev/null +++ b/tests/cases/projects/import_via_mapping_file/typescript-definition-map.json @@ -0,0 +1,3 @@ +{ + "foo": "libs/library.ts" +}