2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2016-05-27 19:22:16 -04:00
|
|
|
import {AngularCompilerOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
|
2016-04-29 00:57:16 -04:00
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as path from 'path';
|
2016-06-08 19:38:52 -04:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
|
|
|
import {AssetUrl, ImportGenerator} from './compiler_private';
|
|
|
|
import {StaticReflectorHost, StaticSymbol} from './static_reflector';
|
2016-05-24 13:53:48 -04:00
|
|
|
|
2016-04-29 00:57:16 -04:00
|
|
|
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
|
|
|
const DTS = /\.d\.ts$/;
|
|
|
|
|
2016-06-09 17:51:53 -04:00
|
|
|
export interface ReflectorHostContext {
|
2016-07-13 14:15:23 -04:00
|
|
|
fileExists(fileName: string): boolean;
|
|
|
|
directoryExists(directoryName: string): boolean;
|
|
|
|
readFile(fileName: string): string;
|
|
|
|
assumeFileExists(fileName: string): void;
|
2016-06-09 17:51:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
2016-05-01 14:22:39 -04:00
|
|
|
private metadataCollector = new MetadataCollector();
|
2016-06-09 17:51:53 -04:00
|
|
|
private context: ReflectorHostContext;
|
2016-06-08 19:38:52 -04:00
|
|
|
constructor(
|
|
|
|
private program: ts.Program, private compilerHost: ts.CompilerHost,
|
|
|
|
private options: AngularCompilerOptions, context?: ReflectorHostContext) {
|
2016-06-09 17:51:53 -04:00
|
|
|
this.context = context || new NodeReflectorHostContext();
|
|
|
|
}
|
2016-05-01 14:22:39 -04:00
|
|
|
|
|
|
|
angularImportLocations() {
|
2016-05-24 13:53:48 -04:00
|
|
|
return {
|
|
|
|
coreDecorators: '@angular/core/src/metadata',
|
|
|
|
diDecorators: '@angular/core/src/di/decorators',
|
|
|
|
diMetadata: '@angular/core/src/di/metadata',
|
2016-06-13 18:56:51 -04:00
|
|
|
diOpaqueToken: '@angular/core/src/di/opaque_token',
|
2016-05-31 12:15:17 -04:00
|
|
|
animationMetadata: '@angular/core/src/animation/metadata',
|
2016-05-24 13:53:48 -04:00
|
|
|
provider: '@angular/core/src/di/provider'
|
|
|
|
};
|
2016-05-01 14:22:39 -04:00
|
|
|
}
|
2016-08-02 14:45:14 -04:00
|
|
|
|
2016-04-29 00:57:16 -04:00
|
|
|
private resolve(m: string, containingFile: string) {
|
|
|
|
const resolved =
|
2016-07-13 14:15:23 -04:00
|
|
|
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
|
2016-05-02 12:38:46 -04:00
|
|
|
return resolved ? resolved.resolvedFileName : null;
|
2016-04-29 00:57:16 -04:00
|
|
|
};
|
|
|
|
|
2016-05-03 20:31:40 -04:00
|
|
|
private normalizeAssetUrl(url: string): string {
|
|
|
|
let assetUrl = AssetUrl.parse(url);
|
|
|
|
return assetUrl ? `${assetUrl.packageName}/${assetUrl.modulePath}` : null;
|
|
|
|
}
|
2016-05-02 12:38:46 -04:00
|
|
|
|
|
|
|
private resolveAssetUrl(url: string, containingFile: string): string {
|
2016-05-03 20:31:40 -04:00
|
|
|
let assetUrl = this.normalizeAssetUrl(url);
|
2016-05-02 12:38:46 -04:00
|
|
|
if (assetUrl) {
|
2016-05-03 20:31:40 -04:00
|
|
|
return this.resolve(assetUrl, containingFile);
|
2016-05-02 12:38:46 -04:00
|
|
|
}
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2016-04-29 00:57:16 -04:00
|
|
|
/**
|
|
|
|
* We want a moduleId that will appear in import statements in the generated code.
|
|
|
|
* These need to be in a form that system.js can load, so absolute file paths don't work.
|
|
|
|
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
|
|
|
|
* they are resolvable by the moduleResolution strategy from the CompilerHost.
|
|
|
|
*/
|
2016-05-02 12:38:46 -04:00
|
|
|
getImportPath(containingFile: string, importedFile: string) {
|
2016-05-03 00:21:10 -04:00
|
|
|
importedFile = this.resolveAssetUrl(importedFile, containingFile);
|
2016-05-02 12:38:46 -04:00
|
|
|
containingFile = this.resolveAssetUrl(containingFile, '');
|
2016-04-29 00:57:16 -04:00
|
|
|
|
2016-08-02 14:45:14 -04:00
|
|
|
// If a file does not yet exist (because we compile it later), we still need to
|
|
|
|
// assume it exists it so that the `resolve` method works!
|
2016-05-02 12:38:46 -04:00
|
|
|
if (!this.compilerHost.fileExists(importedFile)) {
|
2016-07-13 14:15:23 -04:00
|
|
|
this.context.assumeFileExists(importedFile);
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
2016-05-02 12:38:46 -04:00
|
|
|
|
2016-06-03 15:23:35 -04:00
|
|
|
const importModuleName = importedFile.replace(EXT, '');
|
|
|
|
const parts = importModuleName.split(path.sep).filter(p => !!p);
|
2016-05-02 12:38:46 -04:00
|
|
|
|
2016-04-29 00:57:16 -04:00
|
|
|
for (let index = parts.length - 1; index >= 0; index--) {
|
|
|
|
let candidate = parts.slice(index, parts.length).join(path.sep);
|
2016-05-02 12:38:46 -04:00
|
|
|
if (this.resolve('.' + path.sep + candidate, containingFile) === importedFile) {
|
|
|
|
return `./${candidate}`;
|
|
|
|
}
|
|
|
|
if (this.resolve(candidate, containingFile) === importedFile) {
|
|
|
|
return candidate;
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
}
|
2016-06-03 15:23:35 -04:00
|
|
|
|
|
|
|
// Try a relative import
|
|
|
|
let candidate = path.relative(path.dirname(containingFile), importModuleName);
|
|
|
|
if (this.resolve(candidate, containingFile) === importedFile) {
|
|
|
|
return candidate;
|
|
|
|
}
|
|
|
|
|
2016-04-29 00:57:16 -04:00
|
|
|
throw new Error(
|
2016-05-02 12:38:46 -04:00
|
|
|
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
findDeclaration(
|
|
|
|
module: string, symbolName: string, containingFile: string,
|
|
|
|
containingModule?: string): StaticSymbol {
|
2016-04-29 00:57:16 -04:00
|
|
|
if (!containingFile || !containingFile.length) {
|
2016-06-08 19:38:52 -04:00
|
|
|
if (module.indexOf('.') === 0) {
|
|
|
|
throw new Error('Resolution of relative paths requires a containing file.');
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
// Any containing file gives the same result for absolute imports
|
2016-05-24 13:53:48 -04:00
|
|
|
containingFile = path.join(this.options.basePath, 'index.ts');
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2016-05-03 20:31:40 -04:00
|
|
|
let assetUrl = this.normalizeAssetUrl(module);
|
|
|
|
if (assetUrl) {
|
|
|
|
module = assetUrl;
|
|
|
|
}
|
2016-04-29 00:57:16 -04:00
|
|
|
const filePath = this.resolve(module, containingFile);
|
|
|
|
|
|
|
|
if (!filePath) {
|
2016-07-11 20:26:35 -04:00
|
|
|
// If the file cannot be found the module is probably referencing a declared module
|
|
|
|
// for which there is no disambiguating file and we also don't need to track
|
|
|
|
// re-exports. Just use the module name.
|
|
|
|
return this.getStaticSymbol(module, symbolName);
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const tc = this.program.getTypeChecker();
|
|
|
|
const sf = this.program.getSourceFile(filePath);
|
2016-06-09 17:51:53 -04:00
|
|
|
if (!sf || !(<any>sf).symbol) {
|
|
|
|
// The source file was not needed in the compile but we do need the values from
|
2016-08-02 14:45:14 -04:00
|
|
|
// the corresponding .ts files stored in the .metadata.json file. Check the file
|
|
|
|
// for exports to see if the file is exported.
|
|
|
|
return this.resolveExportedSymbol(filePath, symbolName) ||
|
|
|
|
this.getStaticSymbol(filePath, symbolName);
|
2016-06-09 17:51:53 -04:00
|
|
|
}
|
2016-04-29 00:57:16 -04:00
|
|
|
|
|
|
|
let symbol = tc.getExportsOfModule((<any>sf).symbol).find(m => m.name === symbolName);
|
|
|
|
if (!symbol) {
|
|
|
|
throw new Error(`can't find symbol ${symbolName} exported from module ${filePath}`);
|
|
|
|
}
|
2016-04-29 00:58:51 -04:00
|
|
|
if (symbol &&
|
|
|
|
symbol.flags & ts.SymbolFlags.Alias) { // This is an alias, follow what it aliases
|
2016-04-29 00:57:16 -04:00
|
|
|
symbol = tc.getAliasedSymbol(symbol);
|
|
|
|
}
|
|
|
|
const declaration = symbol.getDeclarations()[0];
|
|
|
|
const declarationFile = declaration.getSourceFile().fileName;
|
|
|
|
|
2016-05-02 12:38:46 -04:00
|
|
|
return this.getStaticSymbol(declarationFile, symbol.getName());
|
2016-04-29 00:57:16 -04:00
|
|
|
} catch (e) {
|
|
|
|
console.error(`can't resolve module ${module} from ${containingFile}`);
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private typeCache = new Map<string, StaticSymbol>();
|
2016-08-02 14:45:14 -04:00
|
|
|
private resolverCache = new Map<string, ModuleMetadata>();
|
2016-04-29 00:57:16 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
|
|
|
|
* All types passed to the StaticResolver should be pseudo-types returned by this method.
|
|
|
|
*
|
|
|
|
* @param declarationFile the absolute path of the file where the symbol is declared
|
|
|
|
* @param name the name of the type.
|
|
|
|
*/
|
2016-05-02 12:38:46 -04:00
|
|
|
getStaticSymbol(declarationFile: string, name: string): StaticSymbol {
|
2016-04-29 00:57:16 -04:00
|
|
|
let key = `"${declarationFile}".${name}`;
|
|
|
|
let result = this.typeCache.get(key);
|
|
|
|
if (!result) {
|
2016-05-02 12:38:46 -04:00
|
|
|
result = new StaticSymbol(declarationFile, name);
|
2016-04-29 00:57:16 -04:00
|
|
|
this.typeCache.set(key, result);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
getMetadataFor(filePath: string): ModuleMetadata {
|
2016-07-13 14:15:23 -04:00
|
|
|
if (!this.context.fileExists(filePath)) {
|
2016-07-11 20:26:35 -04:00
|
|
|
// If the file doesn't exists then we cannot return metadata for the file.
|
|
|
|
// This will occur if the user refernced a declared module for which no file
|
|
|
|
// exists for the module (i.e. jQuery or angularjs).
|
|
|
|
return;
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
if (DTS.test(filePath)) {
|
|
|
|
const metadataPath = filePath.replace(DTS, '.metadata.json');
|
2016-07-13 14:15:23 -04:00
|
|
|
if (this.context.fileExists(metadataPath)) {
|
2016-04-29 00:57:16 -04:00
|
|
|
return this.readMetadata(metadataPath);
|
|
|
|
}
|
2016-07-06 17:26:31 -04:00
|
|
|
} else {
|
|
|
|
const sf = this.program.getSourceFile(filePath);
|
|
|
|
if (!sf) {
|
|
|
|
throw new Error(`Source file ${filePath} not present in program.`);
|
|
|
|
}
|
|
|
|
return this.metadataCollector.getMetadata(sf);
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
readMetadata(filePath: string) {
|
|
|
|
try {
|
2016-08-02 14:45:14 -04:00
|
|
|
return this.resolverCache.get(filePath) || JSON.parse(this.context.readFile(filePath));
|
2016-04-29 00:57:16 -04:00
|
|
|
} catch (e) {
|
|
|
|
console.error(`Failed to read JSON file ${filePath}`);
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2016-08-02 14:45:14 -04:00
|
|
|
|
|
|
|
private getResolverMetadata(filePath: string): ModuleMetadata {
|
|
|
|
let metadata = this.resolverCache.get(filePath);
|
|
|
|
if (!metadata) {
|
|
|
|
metadata = this.getMetadataFor(filePath);
|
|
|
|
this.resolverCache.set(filePath, metadata);
|
|
|
|
}
|
|
|
|
return metadata;
|
|
|
|
}
|
|
|
|
|
|
|
|
private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol {
|
|
|
|
const resolveModule = (moduleName: string): string => {
|
|
|
|
const resolvedModulePath = this.resolve(moduleName, filePath);
|
|
|
|
if (!resolvedModulePath) {
|
|
|
|
throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`);
|
|
|
|
}
|
|
|
|
return resolvedModulePath;
|
|
|
|
};
|
|
|
|
let metadata = this.getResolverMetadata(filePath);
|
|
|
|
if (metadata) {
|
|
|
|
// If we have metadata for the symbol, this is the original exporting location.
|
|
|
|
if (metadata.metadata[symbolName]) {
|
|
|
|
return this.getStaticSymbol(filePath, symbolName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no, try to find the symbol in one of the re-export location
|
|
|
|
if (metadata.exports) {
|
|
|
|
// Try and find the symbol in the list of explicitly re-exported symbols.
|
|
|
|
for (const moduleExport of metadata.exports) {
|
|
|
|
if (moduleExport.export) {
|
|
|
|
const exportSymbol = moduleExport.export.find(symbol => {
|
|
|
|
if (typeof symbol === 'string') {
|
|
|
|
return symbol == symbolName;
|
|
|
|
} else {
|
|
|
|
return symbol.as == symbolName;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (exportSymbol) {
|
|
|
|
let symName = symbolName;
|
|
|
|
if (typeof exportSymbol !== 'string') {
|
|
|
|
symName = exportSymbol.name;
|
|
|
|
}
|
|
|
|
return this.resolveExportedSymbol(resolveModule(moduleExport.from), symName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to find the symbol via export * directives.
|
|
|
|
for (const moduleExport of metadata.exports) {
|
|
|
|
if (!moduleExport.export) {
|
|
|
|
const resolvedModule = resolveModule(moduleExport.from);
|
|
|
|
const candidateSymbol = this.resolveExportedSymbol(resolvedModule, symbolName);
|
|
|
|
if (candidateSymbol) return candidateSymbol;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2016-04-29 00:57:16 -04:00
|
|
|
}
|
2016-06-09 17:51:53 -04:00
|
|
|
|
|
|
|
export class NodeReflectorHostContext implements ReflectorHostContext {
|
2016-07-13 14:15:23 -04:00
|
|
|
private assumedExists: {[fileName: string]: boolean} = {};
|
2016-06-09 17:51:53 -04:00
|
|
|
|
2016-07-13 14:15:23 -04:00
|
|
|
fileExists(fileName: string): boolean {
|
|
|
|
return this.assumedExists[fileName] || fs.existsSync(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
directoryExists(directoryName: string): boolean {
|
|
|
|
try {
|
|
|
|
return fs.statSync(directoryName).isDirectory();
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
readFile(fileName: string): string { return fs.readFileSync(fileName, 'utf8'); }
|
2016-06-09 17:51:53 -04:00
|
|
|
|
2016-07-13 14:15:23 -04:00
|
|
|
assumeFileExists(fileName: string): void { this.assumedExists[fileName] = true; }
|
2016-06-09 17:51:53 -04:00
|
|
|
}
|