Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External module resolution #3325

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.js linguist-language=TypeScript
*.js linguist-language=TypeScript
* -text
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"istanbul": "latest"
},
"scripts": {
"test": "jake runtests"
"test": "jake runtests",
"clean": "jake clean",
"build": "jake local",
"build-tests": "jake tests"
}
}
21 changes: 6 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,28 +862,19 @@ 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);
if (symbol) {
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;
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
188 changes: 147 additions & 41 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (<LiteralExpression>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(<LiteralExpression>moduleNameExpr);
break;
case SyntaxKind.ModuleDeclaration:
if ((<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((<ModuleDeclaration>node).body, node => {
if (isExternalModuleImportEqualsDeclaration(node) &&
getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) {
let moduleName = <LiteralExpression>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<string> {
// find all of the map files between startDirectory and the project root directory
let foundMaps: Map<string>[] = [];
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<string> = {};
forEach(foundMaps.reverse(), foundMap => {
for (let key in foundMap)
finalMap[key] = foundMap[key];
});

return finalMap;
}

function tryGetImportMap(directory: string): Map<string> {
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<string> = JSON.parse(sys.readFile(absoluteMapFilePath));
let rootRelativeRoot: Map<string> = {};
for (let key in map) {
rootRelativeRoot[key] = combinePaths(directory, map[key]);
}

return rootRelativeRoot;
}

function tryResolveImportFromMap(importMap: Map<string>, moduleNameExpr: LiteralExpression): string {
let seenKeys: Map<boolean> = {};
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 && (<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((<ModuleDeclaration>node).body, node => {
if (isExternalModuleImportEqualsDeclaration(node) &&
getExternalModuleImportEqualsDeclarationExpression(node).kind === SyntaxKind.StringLiteral) {

let nameLiteral = <LiteralExpression>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, <LiteralExpression>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, <LiteralExpression>moduleNameExpr, referencedSourceFile.fileName);
return;
}

let parentPath = getDirectoryPath(searchPath);
if (parentPath === searchPath) {
break;
}
searchPath = parentPath;
}
// mark reference as non-resolved
setResolvedModuleName(file, <LiteralExpression>moduleNameExpr, undefined);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
}

export interface ScriptReferenceHost {
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ module ts {
public languageVersion: ScriptTarget;
public identifiers: Map<string>;
public nameTable: Map<string>;
public resolvedModules: Map<string>;

private namedDeclarations: Map<Declaration[]>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
define(["require", "exports", 'foo'], function (require, exports, foo_1) {
var foo = new foo_1.Foo();
});
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
define(["require", "exports"], function (require, exports) {
var Foo = (function () {
function Foo() {
}
return Foo;
})();
exports.Foo = Foo;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
var foo_1 = require('foo');
var foo = new foo_1.Foo();
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var Foo = (function () {
function Foo() {
}
return Foo;
})();
exports.Foo = Foo;
7 changes: 7 additions & 0 deletions tests/cases/project/import_via_mapping_file.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
5 changes: 5 additions & 0 deletions tests/cases/projects/import_via_mapping_file/app/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Foo } from 'foo';
import { Bar } from 'bar';

let foo = new Foo();
let bar = new Bar();
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bar": "../libs/library2.ts"
}
2 changes: 2 additions & 0 deletions tests/cases/projects/import_via_mapping_file/libs/library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export class Foo {
}
2 changes: 2 additions & 0 deletions tests/cases/projects/import_via_mapping_file/libs/library2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export class Bar {
}
Loading