From 386eeee8561ad11cf4900615f3cc91627b5a17bb Mon Sep 17 00:00:00 2001 From: Micah Zoltu Date: Sun, 31 May 2015 15:45:41 -0700 Subject: [PATCH 1/4] Never normalize end-of-lines on clone/commit. --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From f0801f2dd32aca906a253028ac245516c78a4cd9 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 26 May 2015 12:13:33 -0700 Subject: [PATCH 2/4] remove module resolution from the checker in favor of resolvedModules map stored in SourceFile --- src/compiler/checker.ts | 21 ++----- src/compiler/parser.ts | 3 +- src/compiler/program.ts | 121 +++++++++++++++++++++++++------------- src/compiler/types.ts | 4 ++ src/compiler/utilities.ts | 24 ++++++++ src/services/services.ts | 1 + 6 files changed, 116 insertions(+), 58 deletions(-) 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..c91d9712ef77f 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -422,54 +422,91 @@ 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; } - 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)); - } - } - }); + } + + 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; } - }); - 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; From 335106fd636116bbd5cd4fdbfc9be18125c70b04 Mon Sep 17 00:00:00 2001 From: Micah Zoltu Date: Sun, 31 May 2015 14:55:37 -0700 Subject: [PATCH 3/4] Adds more jake commands. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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" } } From 9dae5d808f29204820f2b6517c4edc0c4b75bef1 Mon Sep 17 00:00:00 2001 From: Micah Zoltu Date: Sun, 31 May 2015 21:27:45 -0700 Subject: [PATCH 4/4] Adds support for module resolution via a mapping file. Proof of concept for module resolution via mapping file(s). Includes limited test coverage, no caching, and generally pretty crappy code. If the spec for this feature lands on something near this PR I don't mind fixing it up, though I don't know how to write idiomatic TSC code so the PR will still likely need some maintainer love. NOTE: Currently only supports references within the project root directory. Since dependencies managed by a package manager are likely to sit *outside* of the project root directory this solution falls apart a little bit. Unfortunately, I am not sure how to find a mapping file that is shipped with third party modules. This is particularly important for dependencies that have dependencies. --- src/compiler/program.ts | 69 +++++++++++++++++++ .../import_via_mapping_file/amd/app/main.js | 3 + .../amd/import_via_mapping_file.json | 18 +++++ .../amd/libs/library.js | 8 +++ .../import_via_mapping_file/node/app/main.js | 2 + .../node/import_via_mapping_file.json | 18 +++++ .../node/libs/library.js | 6 ++ .../project/import_via_mapping_file.json | 7 ++ .../import_via_mapping_file/app/main.ts | 5 ++ .../app/typescript-definition-map.json | 3 + .../import_via_mapping_file/libs/library.ts | 2 + .../import_via_mapping_file/libs/library2.ts | 2 + .../typescript-definition-map.json | 3 + 13 files changed, 146 insertions(+) create mode 100644 tests/baselines/reference/project/import_via_mapping_file/amd/app/main.js create mode 100644 tests/baselines/reference/project/import_via_mapping_file/amd/import_via_mapping_file.json create mode 100644 tests/baselines/reference/project/import_via_mapping_file/amd/libs/library.js create mode 100644 tests/baselines/reference/project/import_via_mapping_file/node/app/main.js create mode 100644 tests/baselines/reference/project/import_via_mapping_file/node/import_via_mapping_file.json create mode 100644 tests/baselines/reference/project/import_via_mapping_file/node/libs/library.js create mode 100644 tests/cases/project/import_via_mapping_file.json create mode 100644 tests/cases/projects/import_via_mapping_file/app/main.ts create mode 100644 tests/cases/projects/import_via_mapping_file/app/typescript-definition-map.json create mode 100644 tests/cases/projects/import_via_mapping_file/libs/library.ts create mode 100644 tests/cases/projects/import_via_mapping_file/libs/library2.ts create mode 100644 tests/cases/projects/import_via_mapping_file/typescript-definition-map.json diff --git a/src/compiler/program.ts b/src/compiler/program.ts index c91d9712ef77f..172c2328e19ef 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -479,6 +479,68 @@ module ts { } } + 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; @@ -491,6 +553,13 @@ module ts { return; } + let importMap = generateImportMap(searchPath); + let importFromMap = tryResolveImportFromMap(importMap, moduleNameExpr); + if (importFromMap) { + setResolvedModuleName(file, moduleNameExpr, importFromMap); + return; + } + while (true) { searchName = normalizePath(combinePaths(searchPath, moduleNameExpr.text)); let referencedSourceFile = forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr)); 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" +}