build(ngc): run integration test hermetically

This ensures we run in a clean directory, using our real distribution. It finds bugs like @internal
APIs needed to type-check in the offline compiler, as well as problems in package.json.

Also move tsc-wrapped under tools/@angular
This commit is contained in:
Alex Eagle
2016-05-27 16:22:16 -07:00
parent c6064a30a1
commit 3331321f64
35 changed files with 79 additions and 64 deletions

View File

@ -0,0 +1,191 @@
import * as ts from 'typescript';
import {Evaluator, ImportMetadata, ImportSpecifierMetadata} from './evaluator';
import {ClassMetadata, ConstructorMetadata, ModuleMetadata, MemberMetadata, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata} from './schema';
import {Symbols} from './symbols';
/**
* Collect decorator metadata from a TypeScript module.
*/
export class MetadataCollector {
constructor() {}
collectImports(sourceFile: ts.SourceFile) {
let imports: ImportMetadata[] = [];
const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, '');
function visit(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
const importDecl = <ts.ImportDeclaration>node;
const from = stripQuotes(importDecl.moduleSpecifier.getText());
const newImport = {from};
if (!importDecl.importClause) {
// Bare imports do not bring symbols into scope, so we don't need to record them
break;
}
if (importDecl.importClause.name) {
(<any>newImport)['defaultName'] = importDecl.importClause.name.text;
}
const bindings = importDecl.importClause.namedBindings;
if (bindings) {
switch (bindings.kind) {
case ts.SyntaxKind.NamedImports:
const namedImports: ImportSpecifierMetadata[] = [];
(<ts.NamedImports>bindings).elements.forEach(i => {
const namedImport = {name: i.name.text};
if (i.propertyName) {
(<any>namedImport)['propertyName'] = i.propertyName.text;
}
namedImports.push(namedImport);
});
(<any>newImport)['namedImports'] = namedImports;
break;
case ts.SyntaxKind.NamespaceImport:
(<any>newImport)['namespace'] = (<ts.NamespaceImport>bindings).name.text;
break;
}
}
imports.push(newImport);
break;
}
ts.forEachChild(node, visit);
}
ts.forEachChild(sourceFile, visit);
return imports;
}
/**
* Returns a JSON.stringify friendly form describing the decorators of the exported classes from
* the source file that is expected to correspond to a module.
*/
public getMetadata(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): ModuleMetadata {
const locals = new Symbols();
const evaluator = new Evaluator(typeChecker, locals, this.collectImports(sourceFile));
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
}
function referenceFromType(type: ts.Type): MetadataSymbolicReferenceExpression {
if (type) {
let symbol = type.getSymbol();
if (symbol) {
return evaluator.symbolReference(symbol);
}
}
}
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
let result: ClassMetadata = {__symbolic: 'class'};
function getDecorators(decorators: ts.Decorator[]): MetadataSymbolicExpression[] {
if (decorators && decorators.length)
return decorators.map(decorator => objFromDecorator(decorator));
return undefined;
}
// Add class decorators
if (classDeclaration.decorators) {
result.decorators = getDecorators(classDeclaration.decorators);
}
// member decorators
let members: MetadataMap = null;
function recordMember(name: string, metadata: MemberMetadata) {
if (!members) members = {};
let data = members.hasOwnProperty(name) ? members[name] : [];
data.push(metadata);
members[name] = data;
}
for (const member of classDeclaration.members) {
let isConstructor = false;
switch (member.kind) {
case ts.SyntaxKind.Constructor:
isConstructor = true;
// fallthrough
case ts.SyntaxKind.MethodDeclaration:
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
const methodDecorators = getDecorators(method.decorators);
const parameters = method.parameters;
const parameterDecoratorData: MetadataSymbolicExpression[][] = [];
const parametersData: MetadataSymbolicReferenceExpression[] = [];
let hasDecoratorData: boolean = false;
let hasParameterData: boolean = false;
for (const parameter of parameters) {
const parameterData = getDecorators(parameter.decorators);
parameterDecoratorData.push(parameterData);
hasDecoratorData = hasDecoratorData || !!parameterData;
if (isConstructor) {
const parameterType = typeChecker.getTypeAtLocation(parameter);
parametersData.push(referenceFromType(parameterType) || null);
hasParameterData = true;
}
}
const data: MethodMetadata = {__symbolic: isConstructor ? 'constructor' : 'method'};
const name = isConstructor ? '__ctor__' : evaluator.nameOf(member.name);
if (methodDecorators) {
data.decorators = methodDecorators;
}
if (hasDecoratorData) {
data.parameterDecorators = parameterDecoratorData;
}
if (hasParameterData) {
(<ConstructorMetadata>data).parameters = parametersData;
}
recordMember(name, data);
break;
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
const property = <ts.PropertyDeclaration>member;
const propertyDecorators = getDecorators(property.decorators);
if (propertyDecorators) {
recordMember(
evaluator.nameOf(property.name),
{__symbolic: 'property', decorators: propertyDecorators});
}
break;
}
}
if (members) {
result.members = members;
}
return result.decorators || members ? result : undefined;
}
let metadata: {[name: string]: (ClassMetadata | MetadataValue)};
const symbols = typeChecker.getSymbolsInScope(sourceFile, ts.SymbolFlags.ExportValue);
for (var symbol of symbols) {
for (var declaration of symbol.getDeclarations()) {
switch (declaration.kind) {
case ts.SyntaxKind.ClassDeclaration:
const classDeclaration = <ts.ClassDeclaration>declaration;
if (classDeclaration.decorators) {
if (!metadata) metadata = {};
metadata[classDeclaration.name.text] = classMetadataOf(classDeclaration);
}
break;
case ts.SyntaxKind.VariableDeclaration:
const variableDeclaration = <ts.VariableDeclaration>declaration;
if (variableDeclaration.initializer) {
const value = evaluator.evaluateNode(variableDeclaration.initializer);
if (value !== undefined) {
if (evaluator.isFoldable(variableDeclaration.initializer)) {
// Record the value for use in other initializers
locals.set(symbol, value);
}
if (!metadata) metadata = {};
metadata[evaluator.nameOf(variableDeclaration.name)] =
evaluator.evaluateNode(variableDeclaration.initializer);
}
}
break;
}
}
}
return metadata && {__symbolic: 'module', metadata};
}
}

View File

@ -0,0 +1,106 @@
import {writeFileSync} from 'fs';
import {convertDecorators} from 'tsickle';
import * as ts from 'typescript';
import {MetadataCollector} from './collector';
/**
* 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) { 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 metadataCollector = new MetadataCollector();
constructor(delegate: ts.CompilerHost, private program: ts.Program) { super(delegate); }
private 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);
writeFileSync(path, metadataText, {encoding: 'utf-8'});
}
}
}
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.writeMetadata(fileName, sourceFiles[0]);
};
}

View File

@ -0,0 +1,467 @@
import * as ts from 'typescript';
import {MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression} from './schema';
import {Symbols} from './symbols';
// TOOD: Remove when tools directory is upgraded to support es6 target
interface Map<K, V> {
has(k: K): boolean;
set(k: K, v: V): void;
get(k: K): V;
delete (k: K): void;
}
interface MapConstructor {
new<K, V>(): Map<K, V>;
}
declare var Map: MapConstructor;
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
const expression = callExpression.expression;
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
const propertyAccessExpression = <ts.PropertyAccessExpression>expression;
const name = propertyAccessExpression.name;
if (name.kind == ts.SyntaxKind.Identifier) {
return name.text === memberName;
}
}
return false;
}
function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
const expression = callExpression.expression;
if (expression.kind === ts.SyntaxKind.Identifier) {
const identifier = <ts.Identifier>expression;
return identifier.text === ident;
}
return false;
}
/**
* ts.forEachChild stops iterating children when the callback return a truthy value.
* This method inverts this to implement an `every` style iterator. It will return
* true if every call to `cb` returns `true`.
*/
function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
return !ts.forEachChild(node, node => !cb(node));
}
function isPrimitive(value: any): boolean {
return Object(value) !== value;
}
function isDefined(obj: any): boolean {
return obj !== undefined;
}
// import {propertyName as name} from 'place'
// import {name} from 'place'
export interface ImportSpecifierMetadata {
name: string;
propertyName?: string;
}
export interface ImportMetadata {
defaultName?: string; // import d from 'place'
namespace?: string; // import * as d from 'place'
namedImports?: ImportSpecifierMetadata[]; // import {a} from 'place'
from: string; // from 'place'
}
/**
* Produce a symbolic representation of an expression folding values into their final value when
* possible.
*/
export class Evaluator {
constructor(
private typeChecker: ts.TypeChecker, private symbols: Symbols,
private imports: ImportMetadata[]) {}
symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression {
if (symbol) {
let module: string;
let name = symbol.name;
for (const eachImport of this.imports) {
if (symbol.name === eachImport.defaultName) {
module = eachImport.from;
name = undefined;
}
if (eachImport.namedImports) {
for (const named of eachImport.namedImports) {
if (symbol.name === named.name) {
name = named.propertyName ? named.propertyName : named.name;
module = eachImport.from;
break;
}
}
}
}
return {__symbolic: 'reference', name, module};
}
}
private findImportNamespace(node: ts.Node) {
if (node.kind === ts.SyntaxKind.PropertyAccessExpression) {
const lhs = (<ts.PropertyAccessExpression>node).expression;
if (lhs.kind === ts.SyntaxKind.Identifier) {
// TOOD: Use Array.find when tools directory is upgraded to support es6 target
for (const eachImport of this.imports) {
if (eachImport.namespace === (<ts.Identifier>lhs).text) {
return eachImport;
}
}
}
}
}
private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression {
const importNamespace = this.findImportNamespace(node);
if (importNamespace) {
const result = this.symbolReference(
this.typeChecker.getSymbolAtLocation((<ts.PropertyAccessExpression>node).name));
result.module = importNamespace.from;
return result;
}
return this.symbolReference(this.typeChecker.getSymbolAtLocation(node));
}
nameOf(node: ts.Node): string {
if (node.kind == ts.SyntaxKind.Identifier) {
return (<ts.Identifier>node).text;
}
return <string>this.evaluateNode(node);
}
/**
* Returns true if the expression represented by `node` can be folded into a literal expression.
*
* For example, a literal is always foldable. This means that literal expressions such as `1.2`
* `"Some value"` `true` `false` are foldable.
*
* - An object literal is foldable if all the properties in the literal are foldable.
* - An array literal is foldable if all the elements are foldable.
* - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR.
* - A property access is foldable if the object is foldable.
* - A array index is foldable if index expression is foldable and the array is foldable.
* - Binary operator expressions are foldable if the left and right expressions are foldable and
* it is one of '+', '-', '*', '/', '%', '||', and '&&'.
* - An identifier is foldable if a value can be found for its symbol in the evaluator symbol
* table.
*/
public isFoldable(node: ts.Node): boolean {
return this.isFoldableWorker(node, new Map<ts.Node, boolean>());
}
private isFoldableWorker(node: ts.Node, folding: Map<ts.Node, boolean>): boolean {
if (node) {
switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression:
return everyNodeChild(node, child => {
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
const propertyAssignment = <ts.PropertyAssignment>child;
return this.isFoldableWorker(propertyAssignment.initializer, folding);
}
return false;
});
case ts.SyntaxKind.ArrayLiteralExpression:
return everyNodeChild(node, child => this.isFoldableWorker(child, folding));
case ts.SyntaxKind.CallExpression:
const callExpression = <ts.CallExpression>node;
// We can fold a <array>.concat(<v>).
if (isMethodCallOf(callExpression, 'concat') && callExpression.arguments.length === 1) {
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
if (this.isFoldableWorker(arrayNode, folding) &&
this.isFoldableWorker(callExpression.arguments[0], folding)) {
// It needs to be an array.
const arrayValue = this.evaluateNode(arrayNode);
if (arrayValue && Array.isArray(arrayValue)) {
return true;
}
}
}
// We can fold a call to CONST_EXPR
if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1)
return this.isFoldableWorker(callExpression.arguments[0], folding);
return false;
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
return true;
case ts.SyntaxKind.ParenthesizedExpression:
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
return this.isFoldableWorker(parenthesizedExpression.expression, folding);
case ts.SyntaxKind.BinaryExpression:
const binaryExpression = <ts.BinaryExpression>node;
switch (binaryExpression.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
case ts.SyntaxKind.MinusToken:
case ts.SyntaxKind.AsteriskToken:
case ts.SyntaxKind.SlashToken:
case ts.SyntaxKind.PercentToken:
case ts.SyntaxKind.AmpersandAmpersandToken:
case ts.SyntaxKind.BarBarToken:
return this.isFoldableWorker(binaryExpression.left, folding) &&
this.isFoldableWorker(binaryExpression.right, folding);
}
case ts.SyntaxKind.PropertyAccessExpression:
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
return this.isFoldableWorker(propertyAccessExpression.expression, folding);
case ts.SyntaxKind.ElementAccessExpression:
const elementAccessExpression = <ts.ElementAccessExpression>node;
return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
case ts.SyntaxKind.Identifier:
let symbol = this.typeChecker.getSymbolAtLocation(node);
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = this.typeChecker.getAliasedSymbol(symbol);
}
if (this.symbols.has(symbol)) return true;
// If this is a reference to a foldable variable then it is foldable too.
const variableDeclaration = <ts.VariableDeclaration>(
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) {
const initializer = variableDeclaration.initializer;
if (folding.has(initializer)) {
// A recursive reference is not foldable.
return false;
}
folding.set(initializer, true);
const result = this.isFoldableWorker(initializer, folding);
folding.delete(initializer);
return result;
}
break;
}
}
return false;
}
/**
* Produce a JSON serialiable object representing `node`. The foldable values in the expression
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
*/
public evaluateNode(node: ts.Node): MetadataValue {
switch (node.kind) {
case ts.SyntaxKind.ObjectLiteralExpression:
let obj: MetadataValue = {};
let allPropertiesDefined = true;
ts.forEachChild(node, child => {
switch (child.kind) {
case ts.SyntaxKind.PropertyAssignment:
const assignment = <ts.PropertyAssignment>child;
const propertyName = this.nameOf(assignment.name);
const propertyValue = this.evaluateNode(assignment.initializer);
(<any>obj)[propertyName] = propertyValue;
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined;
}
});
if (allPropertiesDefined) return obj;
break;
case ts.SyntaxKind.ArrayLiteralExpression:
let arr: MetadataValue[] = [];
let allElementsDefined = true;
ts.forEachChild(node, child => {
const value = this.evaluateNode(child);
arr.push(value);
allElementsDefined = isDefined(value) && allElementsDefined;
});
if (allElementsDefined) return arr;
break;
case ts.SyntaxKind.CallExpression:
const callExpression = <ts.CallExpression>node;
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
if (this.isFoldable(callExpression)) {
if (isMethodCallOf(callExpression, 'concat')) {
const arrayValue = <MetadataValue[]>this.evaluateNode(
(<ts.PropertyAccessExpression>callExpression.expression).expression);
return arrayValue.concat(args[0]);
}
}
// Always fold a CONST_EXPR even if the argument is not foldable.
if (isCallOf(callExpression, 'CONST_EXPR') && callExpression.arguments.length === 1) {
return args[0];
}
if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) {
const firstArgument = callExpression.arguments[0];
if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) {
const arrowFunction = <ts.ArrowFunction>firstArgument;
return this.evaluateNode(arrowFunction.body);
}
}
const expression = this.evaluateNode(callExpression.expression);
if (isDefined(expression) && args.every(isDefined)) {
const result:
MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
if (args && args.length) {
result.arguments = args;
}
return result;
}
break;
case ts.SyntaxKind.NewExpression:
const newExpression = <ts.NewExpression>node;
const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg));
const newTarget = this.evaluateNode(newExpression.expression);
if (isDefined(newTarget) && newArgs.every(isDefined)) {
const result: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget};
if (newArgs.length) {
result.arguments = newArgs;
}
return result;
}
break;
case ts.SyntaxKind.PropertyAccessExpression: {
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
const expression = this.evaluateNode(propertyAccessExpression.expression);
const member = this.nameOf(propertyAccessExpression.name);
if (this.isFoldable(propertyAccessExpression.expression)) return (<any>expression)[member];
if (this.findImportNamespace(propertyAccessExpression)) {
return this.nodeSymbolReference(propertyAccessExpression);
}
if (isDefined(expression)) {
return {__symbolic: 'select', expression, member};
}
break;
}
case ts.SyntaxKind.ElementAccessExpression: {
const elementAccessExpression = <ts.ElementAccessExpression>node;
const expression = this.evaluateNode(elementAccessExpression.expression);
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
if (this.isFoldable(elementAccessExpression.expression) &&
this.isFoldable(elementAccessExpression.argumentExpression))
return (<any>expression)[<string|number>index];
if (isDefined(expression) && isDefined(index)) {
return {__symbolic: 'index', expression, index};
}
break;
}
case ts.SyntaxKind.Identifier:
let symbol = this.typeChecker.getSymbolAtLocation(node);
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = this.typeChecker.getAliasedSymbol(symbol);
}
if (this.symbols.has(symbol)) return this.symbols.get(symbol);
if (this.isFoldable(node)) {
// isFoldable implies, in this context, symbol declaration is a VariableDeclaration
const variableDeclaration = <ts.VariableDeclaration>(
symbol.declarations && symbol.declarations.length && symbol.declarations[0]);
return this.evaluateNode(variableDeclaration.initializer);
}
return this.nodeSymbolReference(node);
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
return (<ts.LiteralExpression>node).text;
case ts.SyntaxKind.StringLiteral:
return (<ts.StringLiteral>node).text;
case ts.SyntaxKind.NumericLiteral:
return parseFloat((<ts.LiteralExpression>node).text);
case ts.SyntaxKind.NullKeyword:
return null;
case ts.SyntaxKind.TrueKeyword:
return true;
case ts.SyntaxKind.FalseKeyword:
return false;
case ts.SyntaxKind.ParenthesizedExpression:
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
return this.evaluateNode(parenthesizedExpression.expression);
case ts.SyntaxKind.TypeAssertionExpression:
const typeAssertion = <ts.TypeAssertion>node;
return this.evaluateNode(typeAssertion.expression);
case ts.SyntaxKind.PrefixUnaryExpression:
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
const operand = this.evaluateNode(prefixUnaryExpression.operand);
if (isDefined(operand) && isPrimitive(operand)) {
switch (prefixUnaryExpression.operator) {
case ts.SyntaxKind.PlusToken:
return +operand;
case ts.SyntaxKind.MinusToken:
return -operand;
case ts.SyntaxKind.TildeToken:
return ~operand;
case ts.SyntaxKind.ExclamationToken:
return !operand;
}
}
let operatorText: string;
switch (prefixUnaryExpression.operator) {
case ts.SyntaxKind.PlusToken:
operatorText = '+';
break;
case ts.SyntaxKind.MinusToken:
operatorText = '-';
break;
case ts.SyntaxKind.TildeToken:
operatorText = '~';
break;
case ts.SyntaxKind.ExclamationToken:
operatorText = '!';
break;
default:
return undefined;
}
return {__symbolic: 'pre', operator: operatorText, operand: operand};
case ts.SyntaxKind.BinaryExpression:
const binaryExpression = <ts.BinaryExpression>node;
const left = this.evaluateNode(binaryExpression.left);
const right = this.evaluateNode(binaryExpression.right);
if (isDefined(left) && isDefined(right)) {
if (isPrimitive(left) && isPrimitive(right))
switch (binaryExpression.operatorToken.kind) {
case ts.SyntaxKind.BarBarToken:
return <any>left || <any>right;
case ts.SyntaxKind.AmpersandAmpersandToken:
return <any>left && <any>right;
case ts.SyntaxKind.AmpersandToken:
return <any>left & <any>right;
case ts.SyntaxKind.BarToken:
return <any>left | <any>right;
case ts.SyntaxKind.CaretToken:
return <any>left ^ <any>right;
case ts.SyntaxKind.EqualsEqualsToken:
return <any>left == <any>right;
case ts.SyntaxKind.ExclamationEqualsToken:
return <any>left != <any>right;
case ts.SyntaxKind.EqualsEqualsEqualsToken:
return <any>left === <any>right;
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
return <any>left !== <any>right;
case ts.SyntaxKind.LessThanToken:
return <any>left < <any>right;
case ts.SyntaxKind.GreaterThanToken:
return <any>left > <any>right;
case ts.SyntaxKind.LessThanEqualsToken:
return <any>left <= <any>right;
case ts.SyntaxKind.GreaterThanEqualsToken:
return <any>left >= <any>right;
case ts.SyntaxKind.LessThanLessThanToken:
return (<any>left) << (<any>right);
case ts.SyntaxKind.GreaterThanGreaterThanToken:
return <any>left >> <any>right;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return <any>left >>> <any>right;
case ts.SyntaxKind.PlusToken:
return <any>left + <any>right;
case ts.SyntaxKind.MinusToken:
return <any>left - <any>right;
case ts.SyntaxKind.AsteriskToken:
return <any>left * <any>right;
case ts.SyntaxKind.SlashToken:
return <any>left / <any>right;
case ts.SyntaxKind.PercentToken:
return <any>left % <any>right;
}
return {
__symbolic: 'binop',
operator: binaryExpression.operatorToken.getText(),
left: left,
right: right
};
}
break;
}
return undefined;
}
}

View File

@ -0,0 +1,64 @@
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {tsc, check} from './tsc';
import NgOptions from './options';
import {MetadataWriterHost, TsickleHost} from './compiler_host';
export type CodegenExtension = (ngOptions: NgOptions, program: ts.Program, host: ts.CompilerHost) =>
Promise<any>;
export function main(project: string, basePath?: string, codegen?: CodegenExtension): 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);
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const errors = program.getOptionsDiagnostics();
check(errors);
if (ngOptions.skipTemplateCodegen || !codegen) {
codegen = () => Promise.resolve(null);
}
return codegen(ngOptions, program, host).then(() => {
tsc.typeCheck(host, program);
// Emit *.js with Decorators lowered to Annotations, and also *.js.map
const tsicklePreProcessor = new TsickleHost(host);
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);
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);
});
}

View File

@ -0,0 +1,20 @@
import * as ts from 'typescript';
interface Options extends ts.CompilerOptions {
// 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 produce .metadata.json files (they don't work for bundled emit with --out)
skipMetadataEmit: boolean;
// Don't produce .ngfactory.ts or .css.shim.ts files
skipTemplateCodegen: boolean;
// Print extra information while running the compiler
trace: boolean;
}
export default Options;

View File

@ -0,0 +1,138 @@
export interface ModuleMetadata {
__symbolic: 'module';
metadata: {[name: string]: (ClassMetadata | MetadataValue)};
}
export function isModuleMetadata(value: any): value is ModuleMetadata {
return value && value.__symbolic === 'module';
}
export interface ClassMetadata {
__symbolic: 'class';
decorators?: MetadataSymbolicExpression[];
members?: MetadataMap;
}
export function isClassMetadata(value: any): value is ClassMetadata {
return value && value.__symbolic === 'class';
}
export interface MetadataMap { [name: string]: MemberMetadata[]; }
export interface MemberMetadata {
__symbolic: 'constructor'|'method'|'property';
decorators?: MetadataSymbolicExpression[];
}
export function isMemberMetadata(value: any): value is MemberMetadata {
if (value) {
switch (value.__symbolic) {
case 'constructor':
case 'method':
case 'property':
return true;
}
}
return false;
}
export interface MethodMetadata extends MemberMetadata {
__symbolic: 'constructor'|'method';
parameterDecorators?: MetadataSymbolicExpression[][];
}
export function isMethodMetadata(value: any): value is MemberMetadata {
return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method');
}
export interface ConstructorMetadata extends MethodMetadata {
__symbolic: 'constructor';
parameters?: MetadataSymbolicExpression[];
}
export function isConstructorMetadata(value: any): value is ConstructorMetadata {
return value && value.__symbolic === 'constructor';
}
export type MetadataValue =
string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression;
export interface MetadataObject { [name: string]: MetadataValue; }
export interface MetadataArray { [name: number]: MetadataValue; }
export interface MetadataSymbolicExpression {
__symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'
}
export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
if (value) {
switch (value.__symbolic) {
case 'binary':
case 'call':
case 'index':
case 'new':
case 'pre':
case 'reference':
case 'select':
return true;
}
}
return false;
}
export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression {
__symbolic: 'binary';
operator: '&&'|'||'|'|'|'^'|'&'|'=='|'!='|'==='|'!=='|'<'|'>'|'<='|'>='|'instanceof'|'in'|'as'|
'<<'|'>>'|'>>>'|'+'|'-'|'*'|'/'|'%'|'**';
left: MetadataValue;
right: MetadataValue;
}
export function isMetadataSymbolicBinaryExpression(value: any):
value is MetadataSymbolicBinaryExpression {
return value && value.__symbolic === 'binary';
}
export interface MetadataSymbolicIndexExpression extends MetadataSymbolicExpression {
__symbolic: 'index';
expression: MetadataValue;
index: MetadataValue;
}
export function isMetadataSymbolicIndexExpression(value: any):
value is MetadataSymbolicIndexExpression {
return value && value.__symbolic === 'index';
}
export interface MetadataSymbolicCallExpression extends MetadataSymbolicExpression {
__symbolic: 'call'|'new';
expression: MetadataValue;
arguments?: MetadataValue[];
}
export function isMetadataSymbolicCallExpression(value: any):
value is MetadataSymbolicCallExpression {
return value && (value.__symbolic === 'call' || value.__symbolic === 'new');
}
export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression {
__symbolic: 'pre';
operator: '+'|'-'|'~'|'!';
operand: MetadataValue;
}
export function isMetadataSymbolicPrefixExpression(value: any):
value is MetadataSymbolicPrefixExpression {
return value && value.__symbolic === 'pre';
}
export interface MetadataSymbolicReferenceExpression extends MetadataSymbolicExpression {
__symbolic: 'reference';
name: string;
module: string;
}
export function isMetadataSymbolicReferenceExpression(value: any):
value is MetadataSymbolicReferenceExpression {
return value && value.__symbolic === 'reference';
}
export interface MetadataSymbolicSelectExpression extends MetadataSymbolicExpression {
__symbolic: 'select';
expression: MetadataValue;
name: string;
}
export function isMetadataSymbolicSelectExpression(value: any):
value is MetadataSymbolicSelectExpression {
return value && value.__symbolic === 'select';
}

View File

@ -0,0 +1,36 @@
import * as ts from 'typescript';
// TOOD: Remove when tools directory is upgraded to support es6 target
interface Map<K, V> {
has(v: V): boolean;
set(k: K, v: V): void;
get(k: K): V;
}
interface MapConstructor {
new<K, V>(): Map<K, V>;
}
declare var Map: MapConstructor;
var a: Array<number>;
/**
* A symbol table of ts.Symbol to a folded value used during expression folding in Evaluator.
*
* This is a thin wrapper around a Map<> using the first declaration location instead of the symbol
* itself as the key. In the TypeScript binder and type checker, mulitple symbols are sometimes
* created for a symbol depending on what scope it is in (e.g. export vs. local). Using the
* declaration node as the key results in these duplicate symbols being treated as identical.
*/
export class Symbols {
private map = new Map<ts.Node, any>();
public has(symbol: ts.Symbol): boolean { return this.map.has(symbol.getDeclarations()[0]); }
public set(symbol: ts.Symbol, value: any): void {
this.map.set(symbol.getDeclarations()[0], value);
}
public get(symbol: ts.Symbol): any { return this.map.get(symbol.getDeclarations()[0]); }
static empty: Symbols = new Symbols();
}

View File

@ -0,0 +1,110 @@
import * as path from 'path';
import * as ts from 'typescript';
import AngularCompilerOptions from './options';
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;
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;
constructor(private readFile = ts.sys.readFile, private readDirectory = ts.sys.readDirectory) {}
readConfiguration(project: string, basePath: string) {
this.basePath = basePath;
// Allow a directory containing tsconfig.json as the project value
try {
this.readDirectory(project);
project = path.join(project, 'tsconfig.json');
} catch (e) {
// Was not a directory, continue on assuming it's a file
}
const {config, error} = ts.readConfigFile(project, this.readFile);
check([error]);
this.parsed =
ts.parseJsonConfigFileContent(config, {readDirectory: this.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 || '.');
for (const key of Object.keys(this.parsed.options)) {
this.ngOptions[key] = this.parsed.options[key];
}
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();