refactor(compiler_cli): move it into modules/@angular and integrate properly into the build

This also does no more depend on a version
on npm for the compiler_cli.

Also runs the tests for tools/metadata
This commit is contained in:
Tobias Bosch
2016-05-03 09:24:09 -07:00
parent 3cfe281790
commit eba6e7946d
41 changed files with 410 additions and 252 deletions
@@ -0,0 +1,176 @@
/**
* Transform template html and css into executable code.
* Intended to be used in a build step.
*/
import * as ts from 'typescript';
import * as path from 'path';
import * as compiler from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import {StaticReflector} from './static_reflector';
import {CompileMetadataResolver, HtmlParser, DirectiveNormalizer, Lexer, Parser,
TemplateParser, DomElementSchemaRegistry, StyleCompiler,
ViewCompiler, TypeScriptEmitter
} from './compiler_private';
import {Parse5DomAdapter} from '@angular/platform-server';
import {MetadataCollector} from 'ts-metadata-collector';
import {NodeReflectorHost} from './reflector_host';
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const PREAMBLE = `/**
* This file is generated by the Angular 2 template compiler.
* Do not edit.
*/
`;
// TODO(alexeagle): we end up passing options and ngOptions everywhere.
// Maybe this should extend ts.CompilerOptions so we only need this one.
export interface AngularCompilerOptions {
// Absolute path to a directory where generated file structure is written
genDir: string;
// Path to the directory containing the tsconfig.json file.
basePath: string;
// Don't do the template code generation
skipTemplateCodegen: boolean;
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
skipMetadataEmit: boolean;
// Lookup angular's symbols using the old angular2/... npm namespace.
legacyPackageLayout: boolean;
// Print extra information while running the compiler
trace: boolean;
}
export class CodeGenerator {
constructor(private options: ts.CompilerOptions, private ngOptions: AngularCompilerOptions,
private program: ts.Program, public host: ts.CompilerHost,
private staticReflector: StaticReflector, private resolver: CompileMetadataResolver,
private compiler: compiler.OfflineCompiler,
private reflectorHost: NodeReflectorHost) {}
private generateSource(metadatas: compiler.CompileDirectiveMetadata[]) {
const normalize = (metadata: compiler.CompileDirectiveMetadata) => {
const directiveType = metadata.type.runtime;
const directives = this.resolver.getViewDirectivesMetadata(directiveType);
const pipes = this.resolver.getViewPipesMetadata(directiveType);
return new compiler.NormalizedComponentWithViewDirectives(metadata, directives, pipes);
};
return this.compiler.compileTemplates(metadatas.map(normalize));
}
private readComponents(absSourcePath: string) {
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
const metadata = this.staticReflector.getModuleMetadata(absSourcePath);
if (!metadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`);
return result;
}
const symbols = Object.keys(metadata['metadata']);
if (!symbols || !symbols.length) {
return result;
}
for (const symbol of symbols) {
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
let directive: compiler.CompileDirectiveMetadata;
directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType);
if (!directive || !directive.isComponent) {
continue;
}
result.push(this.compiler.normalizeDirectiveMetadata(directive));
}
return result;
}
// Write codegen in a directory structure matching the sources.
private calculateEmitPath(filePath: string) {
let root = this.ngOptions.basePath;
for (let eachRootDir of this.options.rootDirs || []) {
if (this.ngOptions.trace) {
console.log(
`Check if ${filePath} is under rootDirs element ${eachRootDir}`);
}
if (path.relative(eachRootDir, filePath).indexOf('.') !== 0) {
root = eachRootDir;
}
}
return path.join(this.ngOptions.genDir, path.relative(root, filePath));
}
// TODO(tbosch): add a cache for shared css files
// TODO(tbosch): detect cycles!
private generateStylesheet(filepath: string, shim: boolean): Promise<any> {
return this.compiler.loadAndCompileStylesheet(filepath, shim, '.ts')
.then((sourceWithImports) => {
const emitPath = this.calculateEmitPath(sourceWithImports.source.moduleUrl);
// TODO(alexeagle): should include the sourceFile to the WriteFileCallback
this.host.writeFile(emitPath, PREAMBLE + sourceWithImports.source.source, false);
return Promise.all(
sourceWithImports.importedUrls.map(url => this.generateStylesheet(url, shim)));
});
}
codegen(): Promise<any> {
Parse5DomAdapter.makeCurrent();
const generateOneFile = (absSourcePath: string) =>
Promise.all(this.readComponents(absSourcePath))
.then((metadatas: compiler.CompileDirectiveMetadata[]) => {
if (!metadatas || !metadatas.length) {
return;
}
let stylesheetPromises: Promise<any>[] = [];
metadatas.forEach((metadata) => {
let stylesheetPaths = metadata && metadata.template && metadata.template.styleUrls;
if (stylesheetPaths) {
stylesheetPaths.forEach((path) => {
stylesheetPromises.push(this.generateStylesheet(
path, metadata.template.encapsulation === ViewEncapsulation.Emulated));
});
}
});
const generated = this.generateSource(metadatas);
const sourceFile = this.program.getSourceFile(absSourcePath);
const emitPath = this.calculateEmitPath(generated.moduleUrl);
this.host.writeFile(emitPath, PREAMBLE + generated.source, false, () => {},
[sourceFile]);
return Promise.all(stylesheetPromises);
})
.catch((e) => { console.error(e.stack); });
return Promise.all(
this.program.getRootFileNames().filter(f => !GENERATED_FILES.test(f)).map(generateOneFile));
}
static create(ngOptions: AngularCompilerOptions, program: ts.Program, options: ts.CompilerOptions,
compilerHost: ts.CompilerHost): CodeGenerator {
const xhr: compiler.XHR = {get: (s: string) => Promise.resolve(compilerHost.readFile(s))};
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
const reflectorHost = new NodeReflectorHost(program, compilerHost, options, ngOptions);
const staticReflector = new StaticReflector(reflectorHost);
const htmlParser = new HtmlParser();
const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser);
const parser = new Parser(new Lexer());
const tmplParser = new TemplateParser(parser, new DomElementSchemaRegistry(), htmlParser,
/*console*/ null, []);
const offlineCompiler = new compiler.OfflineCompiler(
normalizer, tmplParser, new StyleCompiler(urlResolver),
new ViewCompiler(new compiler.CompilerConfig(true, true, true)),
new TypeScriptEmitter(reflectorHost), xhr);
const resolver = new CompileMetadataResolver(
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
new compiler.ViewResolver(staticReflector), null, null, staticReflector);
return new CodeGenerator(options, ngOptions, program, compilerHost,
staticReflector, resolver, offlineCompiler, reflectorHost);
}
}
@@ -0,0 +1,93 @@
import * as ts from 'typescript';
import * as path from 'path';
import {convertDecorators} from 'tsickle';
import {NodeReflectorHost} from './reflector_host';
import {AngularCompilerOptions} from './codegen';
/**
* Implementation of CompilerHost that forwards all methods to another instance.
* Useful for partial implementations to override only methods they care about.
*/
export abstract class DelegatingHost implements ts.CompilerHost {
constructor(protected delegate: ts.CompilerHost) {}
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) =>
this.delegate.getSourceFile(fileName, languageVersion, onError);
getCancellationToken = () => this.delegate.getCancellationToken();
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.delegate.getDefaultLibFileName(options);
getDefaultLibLocation = () => this.delegate.getDefaultLibLocation();
writeFile: ts.WriteFileCallback = this.delegate.writeFile;
getCurrentDirectory = () => this.delegate.getCurrentDirectory();
getCanonicalFileName = (fileName: string) => this.delegate.getCanonicalFileName(fileName);
useCaseSensitiveFileNames = () => this.delegate.useCaseSensitiveFileNames();
getNewLine = () => this.delegate.getNewLine();
fileExists = (fileName: string) => this.delegate.fileExists(fileName);
readFile = (fileName: string) => this.delegate.readFile(fileName);
trace = (s: string) => this.delegate.trace(s);
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
}
export class TsickleHost extends DelegatingHost {
// Additional diagnostics gathered by pre- and post-emit transformations.
public diagnostics: ts.Diagnostic[] = [];
private TSICKLE_SUPPORT = `
interface DecoratorInvocation {
type: Function;
args?: any[];
}
`;
constructor(delegate: ts.CompilerHost, private options: ts.CompilerOptions) { super(delegate); }
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
const originalContent = this.delegate.readFile(fileName);
let newContent = originalContent;
if (!/\.d\.ts$/.test(fileName)) {
const converted = convertDecorators(fileName, originalContent);
if (converted.diagnostics) {
this.diagnostics.push(...converted.diagnostics);
}
newContent = converted.output + this.TSICKLE_SUPPORT;
}
return ts.createSourceFile(fileName, newContent, languageVersion, true);
}
}
const IGNORED_FILES = /\.ngfactory\.js$|\.css\.js$|\.css\.shim\.js$/;
export class MetadataWriterHost extends DelegatingHost {
private reflectorHost: NodeReflectorHost;
constructor(delegate: ts.CompilerHost, program: ts.Program, options: ts.CompilerOptions,
ngOptions: AngularCompilerOptions) {
super(delegate);
this.reflectorHost = new NodeReflectorHost(program, this, options, ngOptions);
}
writeFile: ts.WriteFileCallback = (fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void,
sourceFiles?: ts.SourceFile[]) => {
if (/\.d\.ts$/.test(fileName)) {
// Let the original file be written first; this takes care of creating parent directories
this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
// TODO: remove this early return after https://github.com/Microsoft/TypeScript/pull/8412 is
// released
return;
}
if (IGNORED_FILES.test(fileName)) {
return;
}
if (!sourceFiles) {
throw new Error('Metadata emit requires the sourceFiles are passed to WriteFileCallback. ' +
'Update to TypeScript ^1.9.0-dev');
}
if (sourceFiles.length > 1) {
throw new Error('Bundled emit with --out is not supported');
}
this.reflectorHost.writeMetadata(fileName, sourceFiles[0]);
}
}
@@ -0,0 +1,37 @@
import {__compiler_private__ as _c} from '@angular/compiler';
export var AssetUrl: typeof _c.AssetUrl = _c.AssetUrl;
export type AssetUrl = _c.AssetUrl;
export var ImportGenerator: typeof _c.ImportGenerator = _c.ImportGenerator;
export type ImportGenerator = _c.ImportGenerator;
export type CompileMetadataResolver = _c.CompileMetadataResolver;
export var CompileMetadataResolver: typeof _c.CompileMetadataResolver = _c.CompileMetadataResolver;
export type HtmlParser = _c.HtmlParser;
export var HtmlParser: typeof _c.HtmlParser = _c.HtmlParser;
export type DirectiveNormalizer = _c.DirectiveNormalizer;
export var DirectiveNormalizer: typeof _c.DirectiveNormalizer = _c.DirectiveNormalizer;
export type Lexer = _c.Lexer;
export var Lexer: typeof _c.Lexer = _c.Lexer;
export type Parser = _c.Parser;
export var Parser: typeof _c.Parser = _c.Parser;
export type TemplateParser = _c.TemplateParser;
export var TemplateParser: typeof _c.TemplateParser = _c.TemplateParser;
export type DomElementSchemaRegistry = _c.DomElementSchemaRegistry;
export var DomElementSchemaRegistry: typeof _c.DomElementSchemaRegistry = _c.DomElementSchemaRegistry;
export type StyleCompiler = _c.StyleCompiler;
export var StyleCompiler: typeof _c.StyleCompiler = _c.StyleCompiler;
export type ViewCompiler = _c.ViewCompiler;
export var ViewCompiler: typeof _c.ViewCompiler = _c.ViewCompiler;
export type TypeScriptEmitter = _c.TypeScriptEmitter;
export var TypeScriptEmitter: typeof _c.TypeScriptEmitter = _c.TypeScriptEmitter;
@@ -0,0 +1,4 @@
import {__core_private__ as r, __core_private_types__ as t} from '@angular/core';
export type ReflectorReader = t.ReflectorReader;
export var ReflectorReader: typeof t.ReflectorReader = r.ReflectorReader;
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {tsc, check} from './tsc';
import {MetadataWriterHost, TsickleHost} from './compiler_host';
import {NodeReflectorHost} from './reflector_host';
import {CodeGenerator} from './codegen';
import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector';
const DEBUG = false;
function debug(msg: string, ...o: any[]) {
if (DEBUG) console.log(msg, ...o);
}
export function main(project: string, basePath?: string): Promise<any> {
try {
let projectDir = project;
if (fs.lstatSync(project).isFile()) {
projectDir = path.dirname(project);
}
// file names in tsconfig are resolved relative to this absolute path
basePath = path.join(process.cwd(), basePath || projectDir);
// read the configuration options from wherever you store them
const {parsed, ngOptions} = tsc.readConfiguration(project, basePath);
ngOptions.basePath = basePath;
const host = ts.createCompilerHost(parsed.options, true);
let codegenStep: Promise<any>;
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const errors = program.getOptionsDiagnostics();
check(errors);
const doCodegen = ngOptions.skipTemplateCodegen ?
Promise.resolve(null) :
CodeGenerator.create(ngOptions, program, parsed.options, host).codegen();
return doCodegen.then(() => {
tsc.typeCheck(host, program);
// Emit *.js with Decorators lowered to Annotations, and also *.js.map
const tsicklePreProcessor = new TsickleHost(host, parsed.options);
tsc.emit(tsicklePreProcessor, program);
if (!ngOptions.skipMetadataEmit) {
// Emit *.metadata.json and *.d.ts
// Not in the same emit pass with above, because tsickle erases
// decorators which we want to read or document.
// Do this emit second since TypeScript will create missing directories for us
// in the standard emit.
const metadataWriter = new MetadataWriterHost(host, program, parsed.options, ngOptions);
tsc.emit(metadataWriter, program);
}
});
} catch (e) {
return Promise.reject(e);
}
}
// CLI entry point
if (require.main === module) {
const args = require('minimist')(process.argv.slice(2));
main(args.p || args.project || '.', args.basePath)
.then(exitCode => process.exit(exitCode))
.catch(e => {
console.error(e.stack);
console.error("Compilation failed");
process.exit(1);
});
}
@@ -0,0 +1,184 @@
import {StaticReflectorHost, StaticSymbol} from './static_reflector';
import * as ts from 'typescript';
import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector';
import * as fs from 'fs';
import * as path from 'path';
import {AngularCompilerOptions} from './codegen';
import {ImportGenerator, AssetUrl} from './compiler_private';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator {
private metadataCollector = new MetadataCollector();
constructor(private program: ts.Program, private compilerHost: ts.CompilerHost,
private options: ts.CompilerOptions, private ngOptions: AngularCompilerOptions) {}
angularImportLocations() {
if (this.ngOptions.legacyPackageLayout) {
return {
coreDecorators: 'angular2/src/core/metadata',
diDecorators: 'angular2/src/core/di/decorators',
diMetadata: 'angular2/src/core/di/metadata',
provider: 'angular2/src/core/di/provider'
};
} else {
return {
coreDecorators: '@angular/core/src/metadata',
diDecorators: '@angular/core/src/di/decorators',
diMetadata: '@angular/core/src/di/metadata',
provider: '@angular/core/src/di/provider'
};
}
}
private resolve(m: string, containingFile: string) {
const resolved =
ts.resolveModuleName(m, containingFile, this.options, this.compilerHost).resolvedModule;
return resolved ? resolved.resolvedFileName : null;
};
private resolveAssetUrl(url: string, containingFile: string): string {
let assetUrl = AssetUrl.parse(url);
if (assetUrl) {
return this.resolve(`${assetUrl.packageName}/${assetUrl.modulePath}`, containingFile);
}
return url;
}
/**
* 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.
*/
getImportPath(containingFile: string, importedFile: string) {
importedFile = this.resolveAssetUrl(importedFile, containingFile);
containingFile = this.resolveAssetUrl(containingFile, '');
// TODO(tbosch): if a file does not yet exist (because we compile it later),
// we still need to create it so that the `resolve` method works!
if (!this.compilerHost.fileExists(importedFile)) {
if (this.ngOptions.trace) {
console.log(`Generating empty file ${importedFile} to allow resolution of import`);
}
this.compilerHost.writeFile(importedFile, '', false);
fs.writeFileSync(importedFile, '');
}
const parts = importedFile.replace(EXT, '').split(path.sep).filter(p => !!p);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.resolve('.' + path.sep + candidate, containingFile) === importedFile) {
return `./${candidate}`;
}
if (this.resolve(candidate, containingFile) === importedFile) {
return candidate;
}
}
throw new Error(
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
findDeclaration(module: string, symbolName: string, containingFile: string,
containingModule?: string): StaticSymbol {
if (!containingFile || !containingFile.length) {
if (module.indexOf(".") === 0) {
throw new Error("Resolution of relative paths requires a containing file.");
}
// Any containing file gives the same result for absolute imports
containingFile = path.join(this.ngOptions.basePath, 'index.ts');
}
try {
const filePath = this.resolve(module, containingFile);
if (!filePath) {
throw new Error(`Could not resolve module ${module} relative to ${containingFile}`);
}
const tc = this.program.getTypeChecker();
const sf = this.program.getSourceFile(filePath);
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}`);
}
if (symbol &&
symbol.flags & ts.SymbolFlags.Alias) { // This is an alias, follow what it aliases
symbol = tc.getAliasedSymbol(symbol);
}
const declaration = symbol.getDeclarations()[0];
const declarationFile = declaration.getSourceFile().fileName;
return this.getStaticSymbol(declarationFile, symbol.getName());
} catch (e) {
console.error(`can't resolve module ${module} from ${containingFile}`);
throw e;
}
}
private typeCache = new Map<string, StaticSymbol>();
/**
* 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.
*/
getStaticSymbol(declarationFile: string, name: string): StaticSymbol {
let key = `"${declarationFile}".${name}`;
let result = this.typeCache.get(key);
if (!result) {
result = new StaticSymbol(declarationFile, name);
this.typeCache.set(key, result);
}
return result;
}
// TODO(alexeagle): take a statictype
getMetadataFor(filePath: string): ModuleMetadata {
if (!fs.existsSync(filePath)) {
throw new Error(`No such file '${filePath}'`);
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (fs.existsSync(metadataPath)) {
return this.readMetadata(metadataPath);
}
}
let sf = this.program.getSourceFile(filePath);
if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`);
}
const metadata = this.metadataCollector.getMetadata(sf, this.program.getTypeChecker());
return metadata;
}
readMetadata(filePath: string) {
try {
const result = JSON.parse(fs.readFileSync(filePath, {encoding: 'utf-8'}));
return result;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
}
}
writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) {
// TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is
// released
if (/*DTS*/ /\.js$/.test(emitFilePath)) {
const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json');
const metadata =
this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker());
if (metadata && metadata.metadata) {
const metadataText = JSON.stringify(metadata);
fs.writeFileSync(path, metadataText, {encoding: 'utf-8'});
}
}
}
}
@@ -0,0 +1,392 @@
import {
AttributeMetadata,
DirectiveMetadata,
ComponentMetadata,
ContentChildrenMetadata,
ContentChildMetadata,
InputMetadata,
HostBindingMetadata,
HostListenerMetadata,
OutputMetadata,
PipeMetadata,
ViewChildMetadata,
ViewChildrenMetadata,
ViewQueryMetadata,
QueryMetadata,
} from '@angular/core';
import {ReflectorReader} from './core_private';
import {Provider} from '@angular/core';
import {
HostMetadata,
OptionalMetadata,
InjectableMetadata,
SelfMetadata,
SkipSelfMetadata,
InjectMetadata,
} from "@angular/core";
/**
* The host of the static resolver is expected to be able to provide module metadata in the form of
* ModuleMetadata. Angular 2 CLI will produce this metadata for a module whenever a .d.ts files is
* produced and the module has exported variables or classes with decorators. Module metadata can
* also be produced directly from TypeScript sources by using MetadataCollector in tools/metadata.
*/
export interface StaticReflectorHost {
/**
* Return a ModuleMetadata for the given module.
*
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any};
/**
* Resolve a symbol from an import statement form, to the file where it is declared.
* @param module the location imported from
* @param containingFile for relative imports, the path of the file containing the import
*/
findDeclaration(modulePath: string, symbolName: string, containingFile?: string): StaticSymbol;
getStaticSymbol(declarationFile: string, name: string): StaticSymbol;
angularImportLocations():
{coreDecorators: string, diDecorators: string, diMetadata: string, provider: string};
}
/**
* A token representing the a reference to a static type.
*
* This token is unique for a filePath and name and can be used as a hash table key.
*/
export class StaticSymbol {
constructor(public filePath: string, public name: string) {}
}
/**
* A static reflector implements enough of the Reflector API that is necessary to compile
* templates statically.
*/
export class StaticReflector implements ReflectorReader {
private annotationCache = new Map<StaticSymbol, any[]>();
private propertyCache = new Map<StaticSymbol, {[key: string]: any}>();
private parameterCache = new Map<StaticSymbol, any[]>();
private metadataCache = new Map<string, {[key: string]: any}>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
constructor(private host: StaticReflectorHost) { this.initializeConversionMap(); }
importUri(typeOrFunc: any): string { return (<StaticSymbol>typeOrFunc).filePath; }
public annotations(type: StaticSymbol): any[] {
let annotations = this.annotationCache.get(type);
if (!annotations) {
let classMetadata = this.getTypeMetadata(type);
if (classMetadata['decorators']) {
annotations = this.simplify(type, classMetadata['decorators']);
} else {
annotations = [];
}
this.annotationCache.set(type, annotations.filter(ann => !!ann));
}
return annotations;
}
public propMetadata(type: StaticSymbol): {[key: string]: any} {
let propMetadata = this.propertyCache.get(type);
if (!propMetadata) {
let classMetadata = this.getTypeMetadata(type);
let members = classMetadata ? classMetadata['members'] : {};
propMetadata = mapStringMap(members, (propData, propName) => {
let prop = (<any[]>propData).find(a => a['__symbolic'] == 'property');
if (prop && prop['decorators']) {
return this.simplify(type, prop['decorators']);
} else {
return [];
}
});
this.propertyCache.set(type, propMetadata);
}
return propMetadata;
}
public parameters(type: StaticSymbol): any[] {
if (!(type instanceof StaticSymbol)) {
throw new Error(`parameters recieved ${JSON.stringify(type)} which is not a StaticSymbol`);
}
try {
let parameters = this.parameterCache.get(type);
if (!parameters) {
let classMetadata = this.getTypeMetadata(type);
let members = classMetadata ? classMetadata['members'] : null;
let ctorData = members ? members['__ctor__'] : null;
if (ctorData) {
let ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor');
let parameterTypes = <any[]>this.simplify(type, ctor['parameters']);
let parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators']);
parameters = [];
parameterTypes.forEach( (paramType, index) => {
let nestedResult: any[] = [];
if (paramType) {
nestedResult.push(paramType);
}
let decorators = parameterDecorators ? parameterDecorators[index] : null;
if (decorators) {
nestedResult.push(...decorators);
}
parameters.push(nestedResult);
});
}
if (!parameters) {
parameters = [];
}
this.parameterCache.set(type, parameters);
}
return parameters;
} catch (e) {
console.log(`Failed on type ${JSON.stringify(type)} with error ${e}`);
throw e;
}
}
private registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => {
let argValues: any[] = [];
args.forEach( (arg, index) => {
let argValue: any;
if (typeof arg === 'object' && !arg['__symbolic']) {
argValue = mapStringMap(arg, (value, key) => this.simplify(context, value));
} else {
argValue = this.simplify(context, arg);
}
argValues.push(argValue);
});
var metadata = Object.create(ctor.prototype);
ctor.apply(metadata, argValues);
return metadata;
});
}
private initializeConversionMap(): void {
const {coreDecorators, diDecorators, diMetadata, provider} = this.host.angularImportLocations();
this.registerDecoratorOrConstructor(this.host.findDeclaration(provider, 'Provider'), Provider);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'Host'),
HostMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'Injectable'),
InjectableMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'Self'),
SelfMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'SkipSelf'),
SkipSelfMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'Inject'),
InjectMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diDecorators, 'Optional'),
OptionalMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Attribute'),
AttributeMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Query'),
QueryMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'ViewQuery'),
ViewQueryMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'ContentChild'),
ContentChildMetadata);
this.registerDecoratorOrConstructor(
this.host.findDeclaration(coreDecorators, 'ContentChildren'), ContentChildrenMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'ViewChild'),
ViewChildMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'ViewChildren'),
ViewChildrenMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Input'),
InputMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Output'),
OutputMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Pipe'),
PipeMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'HostBinding'),
HostBindingMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'HostListener'),
HostListenerMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Directive'),
DirectiveMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(coreDecorators, 'Component'),
ComponentMetadata);
// Note: Some metadata classes can be used directly with Provider.deps.
this.registerDecoratorOrConstructor(this.host.findDeclaration(diMetadata, 'HostMetadata'),
HostMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diMetadata, 'SelfMetadata'),
SelfMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diMetadata, 'SkipSelfMetadata'),
SkipSelfMetadata);
this.registerDecoratorOrConstructor(this.host.findDeclaration(diMetadata, 'OptionalMetadata'),
OptionalMetadata);
}
/** @internal */
public simplify(context: StaticSymbol, value: any): any {
let _this = this;
function simplify(expression: any): any {
if (isPrimitive(expression)) {
return expression;
}
if (expression instanceof Array) {
let result: any[] = [];
for (let item of(<any>expression)) {
result.push(simplify(item));
}
return result;
}
if (expression) {
if (expression['__symbolic']) {
let staticSymbol: StaticSymbol;
switch (expression['__symbolic']) {
case "binop":
let left = simplify(expression['left']);
let right = simplify(expression['right']);
switch (expression['operator']) {
case '&&':
return left && right;
case '||':
return left || right;
case '|':
return left | right;
case '^':
return left ^ right;
case '&':
return left & right;
case '==':
return left == right;
case '!=':
return left != right;
case '===':
return left === right;
case '!==':
return left !== right;
case '<':
return left < right;
case '>':
return left > right;
case '<=':
return left <= right;
case '>=':
return left >= right;
case '<<':
return left << right;
case '>>':
return left >> right;
case '+':
return left + right;
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
}
return null;
case "pre":
let operand = simplify(expression['operand']);
switch (expression['operator']) {
case '+':
return operand;
case '-':
return -operand;
case '!':
return !operand;
case '~':
return ~operand;
}
return null;
case "index":
let indexTarget = simplify(expression['expression']);
let index = simplify(expression['index']);
if (indexTarget && isPrimitive(index)) return indexTarget[index];
return null;
case "select":
let selectTarget = simplify(expression['expression']);
let member = simplify(expression['member']);
if (selectTarget && isPrimitive(member)) return selectTarget[member];
return null;
case "reference":
if (expression['module']) {
staticSymbol = _this.host.findDeclaration(expression['module'], expression['name'],
context.filePath);
} else {
staticSymbol = _this.host.getStaticSymbol(context.filePath, expression['name']);
}
let result = staticSymbol;
let moduleMetadata = _this.getModuleMetadata(staticSymbol.filePath);
let declarationValue =
moduleMetadata ? moduleMetadata['metadata'][staticSymbol.name] : null;
if (declarationValue) {
result = _this.simplify(staticSymbol, declarationValue);
}
return result;
case "class":
return context;
case "new":
case "call":
let target = expression['expression'];
staticSymbol =
_this.host.findDeclaration(target['module'], target['name'], context.filePath);
let converter = _this.conversionMap.get(staticSymbol);
if (converter) {
let args = expression['arguments'];
if (!args) {
args = [];
}
return converter(context, args);
} else {
return context;
}
}
return null;
}
return mapStringMap(expression, (value, name) => simplify(value));
}
return null;
}
return simplify(value);
}
/**
* @param module an absolute path to a module file.
*/
public getModuleMetadata(module: string): {[key: string]: any} {
let moduleMetadata = this.metadataCache.get(module);
if (!moduleMetadata) {
moduleMetadata = this.host.getMetadataFor(module);
if (!moduleMetadata) {
moduleMetadata = {__symbolic: "module", module: module, metadata: {}};
}
this.metadataCache.set(module, moduleMetadata);
}
return moduleMetadata;
}
private getTypeMetadata(type: StaticSymbol): {[key: string]: any} {
let moduleMetadata = this.getModuleMetadata(type.filePath);
let result = moduleMetadata['metadata'][type.name];
if (!result) {
result = {__symbolic: "class"};
}
return result;
}
}
function mapStringMap(input: {[key: string]: any},
transform: (value: any, key: string) => any): {[key: string]: any} {
if (!input) return {};
var result: {[key: string]: any} = {};
Object.keys(input).forEach((key) => { result[key] = transform(input[key], key); });
return result;
}
function isPrimitive(o: any):boolean {
return o === null || (typeof o !== "function" && typeof o !== "object");
}
+104
View File
@@ -0,0 +1,104 @@
import * as ts from 'typescript';
// Don't import from fs in general, that's the CompilerHost's job
import {lstatSync} from 'fs';
import * as path from 'path';
import {AngularCompilerOptions} from './codegen';
import {TsickleHost} from './compiler_host';
/**
* Our interface to the TypeScript standard compiler.
* If you write an Angular compiler plugin for another build tool,
* you should implement a similar interface.
*/
export interface CompilerInterface {
readConfiguration(
project: string,
basePath: string): {parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions};
typeCheck(compilerHost: ts.CompilerHost, program: ts.Program): void;
emit(compilerHost: ts.CompilerHost, program: ts.Program): number;
}
const DEBUG = false;
const SOURCE_EXTENSION = /\.[jt]s$/;
function debug(msg: string, ...o: any[]) {
if (DEBUG) console.log(msg, ...o);
}
export function formatDiagnostics(diags: ts.Diagnostic[]): string {
return diags.map((d) => {
let res = ts.DiagnosticCategory[d.category];
if (d.file) {
res += ' at ' + d.file.fileName + ':';
const {line, character} = d.file.getLineAndCharacterOfPosition(d.start);
res += (line + 1) + ':' + (character + 1) + ':';
}
res += ' ' + ts.flattenDiagnosticMessageText(d.messageText, '\n');
return res;
})
.join('\n');
}
export function check(diags: ts.Diagnostic[]) {
if (diags && diags.length && diags[0]) {
throw new Error(formatDiagnostics(diags));
}
}
export class Tsc implements CompilerInterface {
public ngOptions: AngularCompilerOptions;
public parsed: ts.ParsedCommandLine;
private basePath: string;
readConfiguration(project: string, basePath: string) {
this.basePath = basePath;
// Allow a directory containing tsconfig.json as the project value
if (lstatSync(project).isDirectory()) {
project = path.join(project, "tsconfig.json");
}
const {config, error} = ts.readConfigFile(project, ts.sys.readFile);
check([error]);
this.parsed =
ts.parseJsonConfigFileContent(config, {readDirectory: ts.sys.readDirectory}, basePath);
check(this.parsed.errors);
// Default codegen goes to the current directory
// Parsed options are already converted to absolute paths
this.ngOptions = config.angularCompilerOptions || {};
this.ngOptions.genDir = path.join(basePath, this.ngOptions.genDir || '.');
return {parsed: this.parsed, ngOptions: this.ngOptions};
}
typeCheck(compilerHost: ts.CompilerHost, oldProgram: ts.Program): void {
// Create a new program since codegen files were created after making the old program
const program =
ts.createProgram(this.parsed.fileNames, this.parsed.options, compilerHost, oldProgram);
debug("Checking global diagnostics...");
check(program.getGlobalDiagnostics());
let diagnostics: ts.Diagnostic[] = [];
debug("Type checking...");
for (let sf of program.getSourceFiles()) {
diagnostics.push(...ts.getPreEmitDiagnostics(program, sf));
}
check(diagnostics);
}
emit(compilerHost: TsickleHost, oldProgram: ts.Program): number {
// Create a new program since the host may be different from the old program.
const program = ts.createProgram(this.parsed.fileNames, this.parsed.options, compilerHost);
debug("Emitting outputs...");
const emitResult = program.emit();
let diagnostics: ts.Diagnostic[] = [];
diagnostics.push(...emitResult.diagnostics);
check(compilerHost.diagnostics);
return emitResult.emitSkipped ? 1 : 0;
}
}
export var tsc: CompilerInterface = new Tsc();