feat(ivy): first steps towards ngtsc mode (#23455)

This commit adds a new compiler pipeline that isn't dependent on global
analysis, referred to as 'ngtsc'. This new compiler is accessed by
running ngc with "enableIvy" set to "ngtsc". It reuses the same initialization
logic but creates a new implementation of Program which does not perform the
global-level analysis that AngularCompilerProgram does. It will be the
foundation for the production Ivy compiler.

PR Close #23455
This commit is contained in:
Alex Rickabaugh 2018-04-06 09:53:10 -07:00 committed by Igor Minar
parent f567e1898f
commit ab5bc42da0
44 changed files with 2827 additions and 10 deletions

View File

@ -65,6 +65,8 @@ module.exports = function(config) {
'dist/all/@angular/**/*node_only_spec.js',
'dist/all/@angular/benchpress/**',
'dist/all/@angular/compiler-cli/**',
'dist/all/@angular/compiler-cli/src/ngtsc/**',
'dist/all/@angular/compiler-cli/test/ngtsc/**',
'dist/all/@angular/compiler/test/aot/**',
'dist/all/@angular/compiler/test/render3/**',
'dist/all/@angular/core/test/bundling/**',

View File

@ -25,6 +25,7 @@ ts_library(
tsconfig = ":tsconfig",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/transform",
],
)

View File

@ -40,7 +40,8 @@ export function main(
function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined {
const transformDecorators = options.annotationsAs !== 'decorators';
const transformDecorators =
options.enableIvy !== 'ngtsc' && options.annotationsAs !== 'decorators';
const transformTypesToClosure = options.annotateForClosureCompiler;
if (!transformDecorators && !transformTypesToClosure) {
return undefined;

View File

@ -0,0 +1,77 @@
/**
* @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
*/
import * as ts from 'typescript';
/**
* The TypeScript compiler host used by `ngtsc`.
*
* It's mostly identical to the native `CompilerHost`, but also includes the ability to
* asynchronously resolve resources.
*/
export interface CompilerHost extends ts.CompilerHost {
/**
* Begin processing a resource file.
*
* When the returned Promise resolves, `loadResource` should be able to synchronously produce a
* `string` for the given file.
*/
preloadResource(file: string): Promise<void>;
/**
* Like `readFile`, but reads the contents of a resource file which may have been pre-processed
* by `preloadResource`.
*/
loadResource(file: string): string|undefined;
}
/**
* Implementation of `CompilerHost` which delegates to a native TypeScript host in most cases.
*/
export class NgtscCompilerHost implements CompilerHost {
constructor(private delegate: ts.CompilerHost) {}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
return this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.delegate.getDefaultLibFileName(options);
}
writeFile(
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>): void {
return this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
}
getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }
getDirectories(path: string): string[] { return this.delegate.getDirectories(path); }
getCanonicalFileName(fileName: string): string {
return this.delegate.getCanonicalFileName(fileName);
}
useCaseSensitiveFileNames(): boolean { return this.delegate.useCaseSensitiveFileNames(); }
getNewLine(): string { return this.delegate.getNewLine(); }
fileExists(fileName: string): boolean { return this.delegate.fileExists(fileName); }
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
loadResource(file: string): string|undefined { throw new Error('Method not implemented.'); }
preloadResource(file: string): Promise<void> { throw new Error('Method not implemented.'); }
}

View File

@ -0,0 +1,15 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "metadata",
srcs = glob([
"index.ts",
"src/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/metadata",
deps = [
"//packages/compiler",
],
)

View File

@ -0,0 +1,10 @@
/**
* @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
*/
export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator} from './src/reflector';
export {Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver';

View File

@ -0,0 +1,238 @@
/**
* @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
*/
import * as ts from 'typescript';
/**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
*/
/**
* A reflected parameter of a function, method, or constructor, indicating the name, any
* decorators, and an expression representing a reference to the value side of the parameter's
* declared type, if applicable.
*/
export interface Parameter {
/**
* Name of the parameter as a `ts.BindingName`, which allows the parameter name to be identified
* via sourcemaps.
*/
name: ts.BindingName;
/**
* A `ts.Expression` which represents a reference to the value side of the parameter's type.
*/
typeValueExpr: ts.Expression|null;
/**
* Array of decorators present on the parameter.
*/
decorators: Decorator[];
}
/**
* A reflected decorator, indicating the name, where it was imported from, and any arguments if the
* decorator is a call expression.
*/
export interface Decorator {
/**
* Name of the decorator, extracted from the decoration expression.
*/
name: string;
/**
* Import path (relative to the decorator's file) of the decorator itself.
*/
from: string;
/**
* The decorator node itself (useful for printing sourcemap based references to the decorator).
*/
node: ts.Decorator;
/**
* Any arguments of a call expression, if one is present. If the decorator was not a call
* expression, then this will be an empty array.
*/
args: ts.Expression[];
}
/**
* Reflect a `ts.ClassDeclaration` and determine the list of parameters.
*
* Note that this only reflects the referenced class and not any potential parent class - that must
* be handled by the caller.
*
* @param node the `ts.ClassDeclaration` to reflect
* @param checker a `ts.TypeChecker` used for reflection
* @returns a `Parameter` instance for each argument of the constructor, or `null` if no constructor
*/
export function reflectConstructorParameters(
node: ts.ClassDeclaration, checker: ts.TypeChecker): Parameter[]|null {
// Firstly, look for a constructor.
// clang-format off
const maybeCtor: ts.ConstructorDeclaration[] = node
.members
.filter(element => ts.isConstructorDeclaration(element)) as ts.ConstructorDeclaration[];
// clang-format on
if (maybeCtor.length !== 1) {
// No constructor.
return null;
}
// Reflect each parameter.
return maybeCtor[0].parameters.map(param => reflectParameter(param, checker));
}
/**
* Reflect a `ts.ParameterDeclaration` and determine its name, a token which refers to the value
* declaration of its type (if possible to statically determine), and its decorators, if any.
*/
function reflectParameter(node: ts.ParameterDeclaration, checker: ts.TypeChecker): Parameter {
// The name of the parameter is easy.
const name = node.name;
const decorators = node.decorators &&
node.decorators.map(decorator => reflectDecorator(decorator, checker))
.filter(decorator => decorator !== null) as Decorator[] ||
[];
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: ts.Expression|null = null;
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (node.type !== undefined) {
// It's only valid to convert a type reference to a value reference if the type actually has a
// value declaration associated with it.
const type = checker.getTypeFromTypeNode(node.type);
if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an Expression
// which references the value pointed to by the TypeReference, if possible.
typeValueExpr = typeNodeToValueExpr(node.type);
}
}
return {
name, typeValueExpr, decorators,
};
}
/**
* Reflect a decorator and return a structure describing where it comes from and any arguments.
*
* Only imported decorators are considered, not locally defined decorators.
*/
export function reflectDecorator(decorator: ts.Decorator, checker: ts.TypeChecker): Decorator|null {
// Attempt to resolve the decorator expression into a reference to a concrete Identifier. The
// expression may contain a call to a function which returns the decorator function, in which
// case we want to return the arguments.
let decoratorOfInterest: ts.Expression = decorator.expression;
let args: ts.Expression[] = [];
// Check for call expressions.
if (ts.isCallExpression(decoratorOfInterest)) {
args = Array.from(decoratorOfInterest.arguments);
decoratorOfInterest = decoratorOfInterest.expression;
}
// The final resolved decorator should be a `ts.Identifier` - if it's not, then something is
// wrong and the decorator can't be resolved statically.
if (!ts.isIdentifier(decoratorOfInterest)) {
return null;
}
const importDecl = reflectImportedIdentifier(decoratorOfInterest, checker);
if (importDecl === null) {
return null;
}
return {
...importDecl,
node: decorator, args,
};
}
function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.updateIdentifier(node);
} else {
return null;
}
}
function propertyNameToValue(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else {
return null;
}
}
export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
const map = new Map<string, ts.Expression>();
node.properties.forEach(prop => {
if (ts.isPropertyAssignment(prop)) {
const name = propertyNameToValue(prop.name);
if (name === null) {
return;
}
map.set(name, prop.initializer);
} else if (ts.isShorthandPropertyAssignment(prop)) {
map.set(prop.name.text, prop.name);
} else {
return;
}
});
return map;
}
export function reflectImportedIdentifier(
id: ts.Identifier, checker: ts.TypeChecker): {name: string, from: string}|null {
const symbol = checker.getSymbolAtLocation(id);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
return null;
}
// Ignore decorators that are defined locally (not imported).
const decl: ts.Declaration = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
// Walk back from the specifier to find the declaration, which carries the module specifier.
const importDecl = decl.parent !.parent !.parent !;
// The module specifier is guaranteed to be a string literal, so this should always pass.
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
// Not allowed to happen in TypeScript ASTs.
return null;
}
// Read the module specifier.
const from = importDecl.moduleSpecifier.text;
// Compute the name by which the decorator was exported, not imported.
const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text;
return {from, name};
}

View File

@ -0,0 +1,514 @@
/**
* @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
*/
/**
* resolver.ts implements partial computation of expressions, resolving expressions to static
* values where possible and returning a `DynamicValue` signal when not.
*/
import * as ts from 'typescript';
/**
* Represents a value which cannot be determined statically.
*
* Use `isDynamicValue` to determine whether a `ResolvedValue` is a `DynamicValue`.
*/
export class DynamicValue {
/**
* This is needed so the "is DynamicValue" assertion of `isDynamicValue` actually has meaning.
*
* Otherwise, "is DynamicValue" is akin to "is {}" which doesn't trigger narrowing.
*/
private _isDynamic = true;
}
/**
* An internal flyweight for `DynamicValue`. Eventually the dynamic value will carry information
* on the location of the node that could not be statically computed.
*/
const DYNAMIC_VALUE: DynamicValue = new DynamicValue();
/**
* Used to test whether a `ResolvedValue` is a `DynamicValue`.
*/
export function isDynamicValue(value: any): value is DynamicValue {
return value === DYNAMIC_VALUE;
}
/**
* A value resulting from static resolution.
*
* This could be a primitive, collection type, reference to a `ts.Node` that declares a
* non-primitive value, or a special `DynamicValue` type which indicates the value was not
* available statically.
*/
export type ResolvedValue = number | boolean | string | null | undefined | Reference |
ResolvedValueArray | ResolvedValueMap | DynamicValue;
/**
* An array of `ResolvedValue`s.
*
* This is a reified type to allow the circular reference of `ResolvedValue` -> `ResolvedValueArray`
* ->
* `ResolvedValue`.
*/
export interface ResolvedValueArray extends Array<ResolvedValue> {}
/**
* A map of strings to `ResolvedValue`s.
*
* This is a reified type to allow the circular reference of `ResolvedValue` -> `ResolvedValueMap` ->
* `ResolvedValue`.
*/ export interface ResolvedValueMap extends Map<string, ResolvedValue> {}
/**
* Tracks the scope of a function body, which includes `ResolvedValue`s for the parameters of that
* body.
*/
type Scope = Map<ts.ParameterDeclaration, ResolvedValue>;
/**
* Whether or not to allow references during resolution.
*
* See `StaticInterpreter` for details.
*/
const enum AllowReferences {
No = 0,
Yes = 1,
}
/**
* A reference to a `ts.Node`.
*
* For example, if an expression evaluates to a function or class definition, it will be returned
* as a `Reference` (assuming references are allowed in evaluation).
*/
export class Reference {
constructor(readonly node: ts.Node) {}
}
/**
* Statically resolve the given `ts.Expression` into a `ResolvedValue`.
*
* @param node the expression to statically resolve if possible
* @param checker a `ts.TypeChecker` used to understand the expression
* @returns a `ResolvedValue` representing the resolved value
*/
export function staticallyResolve(node: ts.Expression, checker: ts.TypeChecker): ResolvedValue {
return new StaticInterpreter(
checker, new Map<ts.ParameterDeclaration, ResolvedValue>(), AllowReferences.No)
.visit(node);
}
interface BinaryOperatorDef {
literal: boolean;
op: (a: any, b: any) => ResolvedValue;
}
function literalBinaryOp(op: (a: any, b: any) => any): BinaryOperatorDef {
return {op, literal: true};
}
function referenceBinaryOp(op: (a: any, b: any) => any): BinaryOperatorDef {
return {op, literal: false};
}
const BINARY_OPERATORS = new Map<ts.SyntaxKind, BinaryOperatorDef>([
[ts.SyntaxKind.PlusToken, literalBinaryOp((a, b) => a + b)],
[ts.SyntaxKind.MinusToken, literalBinaryOp((a, b) => a - b)],
[ts.SyntaxKind.AsteriskToken, literalBinaryOp((a, b) => a * b)],
[ts.SyntaxKind.SlashToken, literalBinaryOp((a, b) => a / b)],
[ts.SyntaxKind.PercentToken, literalBinaryOp((a, b) => a % b)],
[ts.SyntaxKind.AmpersandToken, literalBinaryOp((a, b) => a & b)],
[ts.SyntaxKind.BarToken, literalBinaryOp((a, b) => a | b)],
[ts.SyntaxKind.CaretToken, literalBinaryOp((a, b) => a ^ b)],
[ts.SyntaxKind.LessThanToken, literalBinaryOp((a, b) => a < b)],
[ts.SyntaxKind.LessThanEqualsToken, literalBinaryOp((a, b) => a <= b)],
[ts.SyntaxKind.GreaterThanToken, literalBinaryOp((a, b) => a > b)],
[ts.SyntaxKind.GreaterThanEqualsToken, literalBinaryOp((a, b) => a >= b)],
[ts.SyntaxKind.LessThanLessThanToken, literalBinaryOp((a, b) => a << b)],
[ts.SyntaxKind.GreaterThanGreaterThanToken, literalBinaryOp((a, b) => a >> b)],
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, literalBinaryOp((a, b) => a >>> b)],
[ts.SyntaxKind.AsteriskAsteriskToken, literalBinaryOp((a, b) => Math.pow(a, b))],
[ts.SyntaxKind.AmpersandAmpersandToken, referenceBinaryOp((a, b) => a && b)],
[ts.SyntaxKind.BarBarToken, referenceBinaryOp((a, b) => a || b)]
]);
const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
[ts.SyntaxKind.TildeToken, a => ~a], [ts.SyntaxKind.MinusToken, a => -a],
[ts.SyntaxKind.PlusToken, a => +a], [ts.SyntaxKind.ExclamationToken, a => !a]
]);
class StaticInterpreter {
constructor(
private checker: ts.TypeChecker, private scope: Scope,
private allowReferences: AllowReferences) {}
visit(node: ts.Expression): ResolvedValue { return this.visitExpression(node); }
private visitExpression(node: ts.Expression): ResolvedValue {
if (node.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
return false;
} else if (ts.isStringLiteral(node)) {
return node.text;
} else if (ts.isNumericLiteral(node)) {
return parseFloat(node.text);
} else if (ts.isObjectLiteralExpression(node)) {
return this.visitObjectLiteralExpression(node);
} else if (ts.isIdentifier(node)) {
return this.visitIdentifier(node);
} else if (ts.isPropertyAccessExpression(node)) {
return this.visitPropertyAccessExpression(node);
} else if (ts.isCallExpression(node)) {
return this.visitCallExpression(node);
} else if (ts.isConditionalExpression(node)) {
return this.visitConditionalExpression(node);
} else if (ts.isPrefixUnaryExpression(node)) {
return this.visitPrefixUnaryExpression(node);
} else if (ts.isBinaryExpression(node)) {
return this.visitBinaryExpression(node);
} else if (ts.isArrayLiteralExpression(node)) {
return this.visitArrayLiteralExpression(node);
} else if (ts.isParenthesizedExpression(node)) {
return this.visitParenthesizedExpression(node);
} else if (ts.isElementAccessExpression(node)) {
return this.visitElementAccessExpression(node);
} else if (ts.isAsExpression(node)) {
return this.visitExpression(node.expression);
} else if (ts.isNonNullExpression(node)) {
return this.visitExpression(node.expression);
} else if (ts.isClassDeclaration(node)) {
return this.visitDeclaration(node);
} else {
return DYNAMIC_VALUE;
}
}
private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression): ResolvedValue {
const array: ResolvedValueArray = [];
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (ts.isSpreadElement(element)) {
const spread = this.visitExpression(element.expression);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
if (!Array.isArray(spread)) {
throw new Error(`Unexpected value in spread expression: ${spread}`);
}
array.push(...spread);
} else {
const result = this.visitExpression(element);
if (isDynamicValue(result)) {
return DYNAMIC_VALUE;
}
array.push(result);
}
}
return array;
}
private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression): ResolvedValue {
const map: ResolvedValueMap = new Map<string, ResolvedValue>();
for (let i = 0; i < node.properties.length; i++) {
const property = node.properties[i];
if (ts.isPropertyAssignment(property)) {
const name = this.stringNameFromPropertyName(property.name);
// Check whether the name can be determined statically.
if (name === undefined) {
return DYNAMIC_VALUE;
}
map.set(name, this.visitExpression(property.initializer));
} else if (ts.isShorthandPropertyAssignment(property)) {
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
if (symbol === undefined || symbol.valueDeclaration === undefined) {
return DYNAMIC_VALUE;
}
map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration));
} else if (ts.isSpreadAssignment(property)) {
const spread = this.visitExpression(property.expression);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
if (!(spread instanceof Map)) {
throw new Error(`Unexpected value in spread assignment: ${spread}`);
}
spread.forEach((value, key) => map.set(key, value));
} else {
return DYNAMIC_VALUE;
}
}
return map;
}
private visitIdentifier(node: ts.Identifier): ResolvedValue {
let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(node);
if (symbol === undefined) {
return DYNAMIC_VALUE;
}
const result = this.visitSymbol(symbol);
if (this.allowReferences === AllowReferences.Yes && isDynamicValue(result)) {
return new Reference(node);
}
return result;
}
private visitSymbol(symbol: ts.Symbol): ResolvedValue {
while (symbol.flags & ts.SymbolFlags.Alias) {
symbol = this.checker.getAliasedSymbol(symbol);
}
if (symbol.declarations === undefined) {
return DYNAMIC_VALUE;
}
if (symbol.valueDeclaration !== undefined) {
return this.visitDeclaration(symbol.valueDeclaration);
}
return symbol.declarations.reduce<ResolvedValue>((prev, decl) => {
if (!(isDynamicValue(prev) || prev instanceof Reference)) {
return prev;
}
return this.visitDeclaration(decl);
}, DYNAMIC_VALUE);
}
private visitDeclaration(node: ts.Declaration): ResolvedValue {
if (ts.isVariableDeclaration(node)) {
if (!node.initializer) {
return undefined;
}
return this.visitExpression(node.initializer);
} else if (ts.isParameter(node) && this.scope.has(node)) {
return this.scope.get(node) !;
} else if (ts.isExportAssignment(node)) {
return this.visitExpression(node.expression);
} else if (ts.isSourceFile(node)) {
return this.visitSourceFile(node);
}
return this.allowReferences === AllowReferences.Yes ? new Reference(node) : DYNAMIC_VALUE;
}
private visitElementAccessExpression(node: ts.ElementAccessExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
if (node.argumentExpression === undefined) {
throw new Error(`Expected argument in ElementAccessExpression`);
}
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
const rhs = this.withNoReferences.visitExpression(node.argumentExpression);
if (isDynamicValue(rhs)) {
return DYNAMIC_VALUE;
}
if (typeof rhs !== 'string' && typeof rhs !== 'number') {
throw new Error(
`ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`);
}
return this.accessHelper(lhs, rhs);
}
private visitPropertyAccessExpression(node: ts.PropertyAccessExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
const rhs = node.name.text;
// TODO: handle reference to class declaration.
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
return this.accessHelper(lhs, rhs);
}
private visitSourceFile(node: ts.SourceFile): ResolvedValue {
const map = new Map<string, ResolvedValue>();
const symbol = this.checker.getSymbolAtLocation(node);
if (symbol === undefined) {
return DYNAMIC_VALUE;
}
const exports = this.checker.getExportsOfModule(symbol);
exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol)));
return map;
}
private accessHelper(lhs: ResolvedValue, rhs: string|number): ResolvedValue {
const strIndex = `${rhs}`;
if (lhs instanceof Map) {
if (lhs.has(strIndex)) {
return lhs.get(strIndex) !;
} else {
throw new Error(`Invalid map access: [${Array.from(lhs.keys())}] dot ${rhs}`);
}
} else if (Array.isArray(lhs)) {
if (rhs === 'length') {
return rhs.length;
}
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
return DYNAMIC_VALUE;
}
if (rhs < 0 || rhs >= lhs.length) {
throw new Error(`Index out of bounds: ${rhs} vs ${lhs.length}`);
}
return lhs[rhs];
} else if (lhs instanceof Reference) {
const ref = lhs.node;
if (ts.isClassDeclaration(ref)) {
let value: ResolvedValue = undefined;
const member = ref.members.filter(member => isStatic(member))
.find(
member => member.name !== undefined &&
this.stringNameFromPropertyName(member.name) === strIndex);
if (member !== undefined) {
if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) {
value = this.visitExpression(member.initializer);
} else if (ts.isMethodDeclaration(member)) {
value = this.allowReferences === AllowReferences.Yes ? new Reference(member) :
DYNAMIC_VALUE;
}
}
return value;
}
}
throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`);
}
private visitCallExpression(node: ts.CallExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
if (!(lhs instanceof Reference)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`);
} else if (!isFunctionOrMethodDeclaration(lhs.node) || !lhs.node.body) {
throw new Error(
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]}`);
}
const fn = lhs.node;
const body = fn.body as ts.Block;
if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) {
throw new Error('Function body must have a single return statement only.');
}
const ret = body.statements[0] as ts.ReturnStatement;
const newScope: Scope = new Map<ts.ParameterDeclaration, ResolvedValue>();
fn.parameters.forEach((param, index) => {
let value: ResolvedValue = undefined;
if (index < node.arguments.length) {
const arg = node.arguments[index];
value = this.visitExpression(arg);
}
if (value === undefined && param.initializer !== undefined) {
value = this.visitExpression(param.initializer);
}
newScope.set(param, value);
});
return ret.expression !== undefined ? this.withScope(newScope).visitExpression(ret.expression) :
undefined;
}
private visitConditionalExpression(node: ts.ConditionalExpression): ResolvedValue {
const condition = this.withNoReferences.visitExpression(node.condition);
if (isDynamicValue(condition)) {
return condition;
}
if (condition) {
return this.visitExpression(node.whenTrue);
} else {
return this.visitExpression(node.whenFalse);
}
}
private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ResolvedValue {
const operatorKind = node.operator;
if (!UNARY_OPERATORS.has(operatorKind)) {
throw new Error(`Unsupported prefix unary operator: ${ts.SyntaxKind[operatorKind]}`);
}
const op = UNARY_OPERATORS.get(operatorKind) !;
const value = this.visitExpression(node.operand);
return isDynamicValue(value) ? DYNAMIC_VALUE : op(value);
}
private visitBinaryExpression(node: ts.BinaryExpression): ResolvedValue {
const tokenKind = node.operatorToken.kind;
if (!BINARY_OPERATORS.has(tokenKind)) {
throw new Error(`Unsupported binary operator: ${ts.SyntaxKind[tokenKind]}`);
}
const opRecord = BINARY_OPERATORS.get(tokenKind) !;
let lhs: ResolvedValue, rhs: ResolvedValue;
if (opRecord.literal) {
const withNoReferences = this.withNoReferences;
lhs = literal(withNoReferences.visitExpression(node.left));
rhs = literal(withNoReferences.visitExpression(node.right));
} else {
lhs = this.visitExpression(node.left);
rhs = this.visitExpression(node.right);
}
return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs);
}
private visitParenthesizedExpression(node: ts.ParenthesizedExpression): ResolvedValue {
return this.visitExpression(node.expression);
}
private stringNameFromPropertyName(node: ts.PropertyName): string|undefined {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else { // ts.ComputedPropertyName
const literal = this.withNoReferences.visitExpression(node.expression);
return typeof literal === 'string' ? literal : undefined;
}
}
private get withReferences(): StaticInterpreter {
return this.allowReferences === AllowReferences.Yes ?
this :
new StaticInterpreter(this.checker, this.scope, AllowReferences.Yes);
}
private get withNoReferences(): StaticInterpreter {
return this.allowReferences === AllowReferences.No ?
this :
new StaticInterpreter(this.checker, this.scope, AllowReferences.No);
}
private withScope(scope: Scope): StaticInterpreter {
return new StaticInterpreter(this.checker, scope, this.allowReferences);
}
}
function isStatic(element: ts.ClassElement): boolean {
return element.modifiers !== undefined &&
element.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
}
function isFunctionOrMethodDeclaration(node: ts.Node): node is ts.FunctionDeclaration|
ts.MethodDeclaration {
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node);
}
function literal(value: ResolvedValue): any {
if (value === null || value === undefined || typeof value === 'string' ||
typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (isDynamicValue(value)) {
return DYNAMIC_VALUE;
}
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
}

View File

@ -0,0 +1,25 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "test_lib",
testonly = 1,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/metadata",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
],
)

View File

@ -0,0 +1,129 @@
/**
* @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
*/
import * as path from 'path';
import * as ts from 'typescript';
export function makeProgram(files: {name: string, contents: string}[]): ts.Program {
const host = new InMemoryHost();
files.forEach(file => host.writeFile(file.name, file.contents));
const rootNames = files.map(file => host.getCanonicalFileName(file.name));
const program = ts.createProgram(rootNames, {noLib: true, experimentalDecorators: true}, host);
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
fail(diags.map(diag => diag.messageText).join(', '));
throw new Error(`Typescript diagnostics failed!`);
}
return program;
}
export class InMemoryHost implements ts.CompilerHost {
private fileSystem = new Map<string, string>();
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
const contents = this.fileSystem.get(this.getCanonicalFileName(fileName));
if (contents === undefined) {
onError && onError(`File does not exist: ${this.getCanonicalFileName(fileName)})`);
return undefined;
}
return ts.createSourceFile(fileName, contents, languageVersion, undefined, ts.ScriptKind.TS);
}
getDefaultLibFileName(options: ts.CompilerOptions): string { return '/lib.d.ts'; }
writeFile(
fileName: string, data: string, writeByteOrderMark?: boolean,
onError?: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
this.fileSystem.set(this.getCanonicalFileName(fileName), data);
}
getCurrentDirectory(): string { return '/'; }
getDirectories(dir: string): string[] {
const fullDir = this.getCanonicalFileName(dir) + '/';
const dirSet = new Set(Array
// Look at all paths known to the host.
.from(this.fileSystem.keys())
// Filter out those that aren't under the requested directory.
.filter(candidate => candidate.startsWith(fullDir))
// Relativize the rest by the requested directory.
.map(candidate => candidate.substr(fullDir.length))
// What's left are dir/.../file.txt entries, and file.txt entries.
// Get the dirname, which
// yields '.' for the latter and dir/... for the former.
.map(candidate => path.dirname(candidate))
// Filter out the '.' entries, which were files.
.filter(candidate => candidate !== '.')
// Finally, split on / and grab the first entry.
.map(candidate => candidate.split('/', 1)[0]));
// Get the resulting values out of the Set.
return Array.from(dirSet);
}
getCanonicalFileName(fileName: string): string {
return path.posix.normalize(`${this.getCurrentDirectory()}/${fileName}`);
}
useCaseSensitiveFileNames(): boolean { return true; }
getNewLine(): string { return '\n'; }
fileExists(fileName: string): boolean { return this.fileSystem.has(fileName); }
readFile(fileName: string): string|undefined { return this.fileSystem.get(fileName); }
}
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
if (ts.isIdentifier(node)) {
return node.text === name;
}
return false;
}
export function getDeclaration<T extends ts.Declaration>(
program: ts.Program, fileName: string, name: string, assert: (value: any) => value is T): T {
const sf = program.getSourceFile(fileName);
if (!sf) {
throw new Error(`No such file: ${fileName}`);
}
let chosenDecl: ts.Declaration|null = null;
sf.statements.forEach(stmt => {
if (chosenDecl !== null) {
return;
} else if (ts.isVariableStatement(stmt)) {
stmt.declarationList.declarations.forEach(decl => {
if (bindingNameEquals(decl.name, name)) {
chosenDecl = decl;
}
});
} else if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) {
if (stmt.name !== undefined && stmt.name.text === name) {
chosenDecl = stmt;
}
}
});
chosenDecl = chosenDecl as ts.Declaration | null;
if (chosenDecl === null) {
throw new Error(`No such symbol: ${name} in ${fileName}`);
}
if (!assert(chosenDecl)) {
throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`);
}
return chosenDecl;
}

View File

@ -0,0 +1,145 @@
/**
* @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
*/
import * as ts from 'typescript';
import {Parameter, reflectConstructorParameters} from '../src/reflector';
import {getDeclaration, makeProgram} from './in_memory_typescript';
describe('reflector', () => {
describe('ctor params', () => {
it('should reflect a single argument', () => {
const program = makeProgram([{
name: 'entry.ts',
contents: `
class Bar {}
class Foo {
constructor(bar: Bar) {}
}
`
}]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar');
});
it('should reflect a decorated argument', () => {
const program = makeProgram([
{
name: 'dec.ts',
contents: `
export function dec(target: any, key: string, index: number) {
}
`
},
{
name: 'entry.ts',
contents: `
import {dec} from './dec';
class Bar {}
class Foo {
constructor(@dec bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with a call', () => {
const program = makeProgram([
{
name: 'dec.ts',
contents: `
export function dec(target: any, key: string, index: number) {
}
`
},
{
name: 'entry.ts',
contents: `
import {dec} from './dec';
class Bar {}
class Foo {
constructor(@dec bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with an indirection', () => {
const program = makeProgram([
{
name: 'bar.ts',
contents: `
export class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import {Bar} from './bar';
import * as star from './bar';
class Foo {
constructor(bar: Bar, otherBar: star.Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
expect(args.length).toBe(2);
expectArgument(args[0], 'bar', 'Bar');
expectArgument(args[1], 'otherBar', 'star.Bar');
});
});
});
function expectArgument(
arg: Parameter, name: string, type?: string, decorator?: string, decoratorFrom?: string): void {
expect(argExpressionToString(arg.name)).toEqual(name);
if (type === undefined) {
expect(arg.typeValueExpr).toBeNull();
} else {
expect(arg.typeValueExpr).not.toBeNull();
expect(argExpressionToString(arg.typeValueExpr !)).toEqual(type);
}
if (decorator !== undefined) {
expect(arg.decorators.length).toBeGreaterThan(0);
expect(arg.decorators.some(dec => dec.name === decorator && dec.from === decoratorFrom))
.toBe(true);
}
}
function argExpressionToString(name: ts.Node): string {
if (ts.isIdentifier(name)) {
return name.text;
} else if (ts.isPropertyAccessExpression(name)) {
return `${argExpressionToString(name.expression)}.${name.name.text}`;
} else {
throw new Error(`Unexpected node in arg expression: ${ts.SyntaxKind[name.kind]}.`);
}
}

View File

@ -0,0 +1,193 @@
/**
* @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
*/
import * as ts from 'typescript';
import {ResolvedValue, staticallyResolve} from '../src/resolver';
import {getDeclaration, makeProgram} from './in_memory_typescript';
function makeSimpleProgram(contents: string): ts.Program {
return makeProgram([{name: 'entry.ts', contents}]);
}
function makeExpression(
code: string, expr: string): {expression: ts.Expression, checker: ts.TypeChecker} {
const program = makeProgram([{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}]);
const checker = program.getTypeChecker();
const decl = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
return {
expression: decl.initializer !,
checker,
};
}
function evaluate<T extends ResolvedValue>(code: string, expr: string): T {
const {expression, checker} = makeExpression(code, expr);
return staticallyResolve(expression, checker) as T;
}
describe('ngtsc metadata', () => {
it('reads a file correctly', () => {
const program = makeProgram([
{
name: 'entry.ts',
contents: `
import {Y} from './other';
const A = Y;
export const X = A;
`
},
{
name: 'other.ts',
contents: `
export const Y = 'test';
`
}
]);
const decl = getDeclaration(program, 'entry.ts', 'X', ts.isVariableDeclaration);
const value = staticallyResolve(decl.initializer !, program.getTypeChecker());
expect(value).toEqual('test');
});
it('map access works',
() => { expect(evaluate('const obj = {a: "test"};', 'obj.a')).toEqual('test'); });
it('function calls work', () => {
expect(evaluate(`function foo(bar) { return bar; }`, 'foo("test")')).toEqual('test');
});
it('conditionals work', () => {
expect(evaluate(`const x = false; const y = x ? 'true' : 'false';`, 'y')).toEqual('false');
});
it('addition works', () => { expect(evaluate(`const x = 1 + 2;`, 'x')).toEqual(3); });
it('static property on class works',
() => { expect(evaluate(`class Foo { static bar = 'test'; }`, 'Foo.bar')).toEqual('test'); });
it('static property call works', () => {
expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")'))
.toEqual('test');
});
it('indirected static property call works', () => {
expect(
evaluate(
`class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")'))
.toEqual('test');
});
it('array works', () => {
expect(evaluate(`const x = 'test'; const y = [1, x, 2];`, 'y')).toEqual([1, 'test', 2]);
});
it('array spread works', () => {
expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c'))
.toEqual([1, 2, 3, 4, 5]);
});
it('&& operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a && b')).toEqual('world');
expect(evaluate(`const a = false, b = 'world';`, 'a && b')).toEqual(false);
expect(evaluate(`const a = 'hello', b = 0;`, 'a && b')).toEqual(0);
});
it('|| operations work', () => {
expect(evaluate(`const a = 'hello', b = 'world';`, 'a || b')).toEqual('hello');
expect(evaluate(`const a = false, b = 'world';`, 'a || b')).toEqual('world');
expect(evaluate(`const a = 'hello', b = 0;`, 'a || b')).toEqual('hello');
});
it('parentheticals work',
() => { expect(evaluate(`const a = 3, b = 4;`, 'a * (a + b)')).toEqual(21); });
it('array access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[1] + a[0]')).toEqual(3); });
it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('reads values from default exports', () => {
const program = makeProgram([
{name: 'second.ts', contents: 'export default {property: "test"}'},
{
name: 'entry.ts',
contents: `
import mod from './second';
const target$ = mod.property;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
debugger;
expect(staticallyResolve(expr, checker)).toEqual('test');
});
it('reads values from named exports', () => {
const program = makeProgram([
{name: 'second.ts', contents: 'export const a = {property: "test"};'},
{
name: 'entry.ts',
contents: `
import * as mod from './second';
const target$ = mod.a.property;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
expect(staticallyResolve(expr, checker)).toEqual('test');
});
it('chain of re-exports works', () => {
const program = makeProgram([
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
{name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`},
{
name: 'entry.ts',
contents: `import * as mod from './direct-reexport'; const target$ = mod.value.property;`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
expect(staticallyResolve(expr, checker)).toEqual('test');
});
it('map spread works', () => {
const map: Map<string, number> = evaluate<Map<string, number>>(
`const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c');
const obj: {[key: string]: number} = {};
map.forEach((value, key) => obj[key] = value);
expect(obj).toEqual({
a: 1,
b: 2,
c: 3,
});
});
it('indirected-via-object function call works', () => {
expect(evaluate(
`
function fn(res) { return res; }
const obj = {fn};
`,
'obj.fn("test")'))
.toEqual('test');
});
});

View File

@ -0,0 +1,145 @@
/**
* @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
*/
import {GeneratedFile} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import * as api from '../transformers/api';
import {CompilerHost} from './compiler_host';
import {InjectableCompilerAdapter, IvyCompilation, ivyTransformFactory} from './transform';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
private host: api.CompilerHost, oldProgram?: api.Program) {
this.tsProgram =
ts.createProgram(rootNames, options, host, oldProgram && oldProgram.getTsProgram());
}
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic> {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<api.Diagnostic> {
return [];
}
getTsSyntacticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<api.Diagnostic> {
return [];
}
getTsSemanticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken);
}
getNgSemanticDiagnostics(
fileName?: string|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<api.Diagnostic> {
return [];
}
loadNgStructureAsync(): Promise<void> { return Promise.resolve(); }
listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
throw new Error('Method not implemented.');
}
getLibrarySummaries(): Map<string, api.LibrarySummary> {
throw new Error('Method not implemented.');
}
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
throw new Error('Method not implemented.');
}
getEmittedSourceFiles(): Map<string, ts.SourceFile> {
throw new Error('Method not implemented.');
}
emit(opts?: {
emitFlags?: api.EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: api.CustomTransformers,
emitCallback?: api.TsEmitCallback,
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback
}): ts.EmitResult {
const emitCallback = opts && opts.emitCallback || defaultEmitCallback;
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
const checker = this.tsProgram.getTypeChecker();
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const adapters = [new InjectableCompilerAdapter(checker)];
const compilation = new IvyCompilation(adapters, checker);
// Analyze every source file in the program.
this.tsProgram.getSourceFiles()
.filter(file => !file.fileName.endsWith('.d.ts'))
.forEach(file => compilation.analyze(file));
// Since there is no .d.ts transformation API, .d.ts files are transformed during write.
const writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void) | undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>) => {
if (fileName.endsWith('.d.ts')) {
data = sourceFiles.reduce(
(data, sf) => compilation.transformedDtsFor(sf.fileName, data), data);
}
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
};
// Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
emitOnlyDtsFiles: false, writeFile,
customTransformers: {
before: [ivyTransformFactory(compilation)],
},
});
return emitResult;
}
}
const defaultEmitCallback: api.TsEmitCallback =
({program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles,
customTransformers}) =>
program.emit(
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
const diagnostics: ts.Diagnostic[] = [];
let emitSkipped = false;
const emittedFiles: string[] = [];
for (const er of emitResults) {
diagnostics.push(...er.diagnostics);
emitSkipped = emitSkipped || er.emitSkipped;
emittedFiles.push(...(er.emittedFiles || []));
}
return {diagnostics, emitSkipped, emittedFiles};
}

View File

@ -0,0 +1,16 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "transform",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/transform",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
],
)

View File

@ -0,0 +1,11 @@
/**
* @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
*/
export {IvyCompilation} from './src/compilation';
export {InjectableCompilerAdapter} from './src/injectable';
export {ivyTransformFactory} from './src/transform';

View File

@ -0,0 +1,61 @@
/**
* @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
*/
import {Expression, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
/**
* Provides the interface between a decorator compiler from @angular/compiler and the Typescript
* compiler/transform.
*
* The decorator compilers in @angular/compiler do not depend on Typescript. The adapter is
* responsible for extracting the information required to perform compilation from the decorators
* and Typescript source, invoking the decorator compiler, and returning the result.
*/
export interface CompilerAdapter<A> {
/**
* Scan a set of reflected decorators and determine if this adapter is responsible for compilation
* of one of them.
*/
detect(decorator: Decorator[]): Decorator|undefined;
/**
* Perform analysis on the decorator/class combination, producing instructions for compilation
* if successful, or an array of diagnostic messages if the analysis fails or the decorator
* isn't valid.
*/
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<A>;
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
*/
compile(node: ts.ClassDeclaration, analysis: A): AddStaticFieldInstruction;
}
/**
* The output of an analysis operation, consisting of possibly an arbitrary analysis object (used as
* the input to code generation) and potentially diagnostics if there were errors uncovered during
* analysis.
*/
export interface AnalysisOutput<A> {
analysis?: A;
diagnostics?: ts.Diagnostic[];
}
/**
* A description of the static field to add to a class, including an initialization expression
* and a type for the .d.ts file.
*/
export interface AddStaticFieldInstruction {
field: string;
initializer: Expression;
type: Type;
}

View File

@ -0,0 +1,157 @@
/**
* @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
*/
import {Expression, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, reflectDecorator} from '../../metadata';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
import {DtsFileTransformer} from './declaration';
import {ImportManager, translateType} from './translator';
/**
* Record of an adapter which decided to emit a static field, and the analysis it performed to
* prepare for that operation.
*/
interface EmitFieldOperation<T> {
adapter: CompilerAdapter<T>;
analysis: AnalysisOutput<T>;
decorator: ts.Decorator;
}
/**
* Manages a compilation of Ivy decorators into static fields across an entire ts.Program.
*
* The compilation is stateful - source files are analyzed and records of the operations that need
* to be performed during the transform/emit process are maintained internally.
*/
export class IvyCompilation {
/**
* Tracks classes which have been analyzed and found to have an Ivy decorator, and the
* information recorded about them for later compilation.
*/
private analysis = new Map<ts.ClassDeclaration, EmitFieldOperation<any>>();
/**
* Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations.
*/
private dtsMap = new Map<string, DtsFileTransformer>();
constructor(private adapters: CompilerAdapter<any>[], private checker: ts.TypeChecker) {}
/**
* Analyze a source file and produce diagnostics for it (if any).
*/
analyze(sf: ts.SourceFile): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = [];
const visit = (node: ts.Node) => {
// Process nodes recursively, and look for class declarations with decorators.
if (ts.isClassDeclaration(node) && node.decorators !== undefined) {
// The first step is to reflect the decorators, which will identify decorators
// that are imported from another module.
const decorators =
node.decorators.map(decorator => reflectDecorator(decorator, this.checker))
.filter(decorator => decorator !== null) as Decorator[];
// Look through the CompilerAdapters to see if any are relevant.
this.adapters.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators);
if (decorator === undefined) {
return;
}
// Check for multiple decorators on the same node. Technically speaking this
// could be supported, but right now it's an error.
if (this.analysis.has(node)) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}
// Run analysis on the decorator. This will produce either diagnostics, an
// analysis result, or both.
const analysis = adapter.analyze(node, decorator);
if (analysis.diagnostics !== undefined) {
diagnostics.push(...analysis.diagnostics);
}
if (analysis.analysis !== undefined) {
this.analysis.set(node, {
adapter,
analysis: analysis.analysis,
decorator: decorator.node,
});
}
});
}
ts.forEachChild(node, visit);
};
visit(sf);
return diagnostics;
}
/**
* Perform a compilation operation on the given class declaration and return instructions to an
* AST transformer if any are available.
*/
compileIvyFieldFor(node: ts.ClassDeclaration): AddStaticFieldInstruction|undefined {
// Look to see whether the original node was analyzed. If not, there's nothing to do.
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
if (!this.analysis.has(original)) {
return undefined;
}
const op = this.analysis.get(original) !;
// Run the actual compilation, which generates an Expression for the Ivy field.
const res = op.adapter.compile(node, op.analysis);
// Look up the .d.ts transformer for the input file and record that a field was generated,
// which will allow the .d.ts to be transformed later.
const fileName = node.getSourceFile().fileName;
const dtsTransformer = this.getDtsTransformer(fileName);
dtsTransformer.recordStaticField(node.name !.text, res);
// Return the instruction to the transformer so the field will be added.
return res;
}
/**
* Lookup the `ts.Decorator` which triggered transformation of a particular class declaration.
*/
ivyDecoratorFor(node: ts.ClassDeclaration): ts.Decorator|undefined {
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
if (!this.analysis.has(original)) {
return undefined;
}
return this.analysis.get(original) !.decorator;
}
/**
* Process a .d.ts source string and return a transformed version that incorporates the changes
* made to the source file.
*/
transformedDtsFor(tsFileName: string, dtsOriginalSource: string): string {
// No need to transform if no changes have been requested to the input file.
if (!this.dtsMap.has(tsFileName)) {
return dtsOriginalSource;
}
// Return the transformed .d.ts source.
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource);
}
private getDtsTransformer(tsFileName: string): DtsFileTransformer {
if (!this.dtsMap.has(tsFileName)) {
this.dtsMap.set(tsFileName, new DtsFileTransformer());
}
return this.dtsMap.get(tsFileName) !;
}
}

View File

@ -0,0 +1,56 @@
/**
* @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
*/
import * as ts from 'typescript';
import {AddStaticFieldInstruction} from './api';
import {ImportManager, translateType} from './translator';
/**
* Processes .d.ts file text and adds static field declarations, with types.
*/
export class DtsFileTransformer {
private ivyFields = new Map<string, AddStaticFieldInstruction>();
private imports = new ImportManager();
/**
* Track that a static field was added to the code for a class.
*/
recordStaticField(name: string, decl: AddStaticFieldInstruction): void {
this.ivyFields.set(name, decl);
}
/**
* Process the .d.ts text for a file and add any declarations which were recorded.
*/
transform(dts: string): string {
const dtsFile =
ts.createSourceFile('out.d.ts', dts, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
for (let i = dtsFile.statements.length - 1; i >= 0; i--) {
const stmt = dtsFile.statements[i];
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined &&
this.ivyFields.has(stmt.name.text)) {
const desc = this.ivyFields.get(stmt.name.text) !;
const before = dts.substring(0, stmt.end - 1);
const after = dts.substring(stmt.end - 1);
const type = translateType(desc.type, this.imports);
dts = before + ` static ${desc.field}: ${type};\n` + after;
}
}
const imports = this.imports.getAllImports();
if (imports.length !== 0) {
dts = imports.map(i => `import * as ${i.as} from '${i.name}';\n`).join() + dts;
}
return dts;
}
}

View File

@ -0,0 +1,181 @@
/**
* @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
*/
import {Expression, IvyInjectableDep, IvyInjectableMetadata, LiteralExpr, WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableCompilerAdapter implements CompilerAdapter<IvyInjectableMetadata> {
constructor(private checker: ts.TypeChecker) {}
detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find(dec => dec.name === 'Injectable' && dec.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<IvyInjectableMetadata> {
return {
analysis: extractInjectableMetadata(node, decorator, this.checker),
};
}
compile(node: ts.ClassDeclaration, analysis: IvyInjectableMetadata): AddStaticFieldInstruction {
const res = compileIvyInjectable(analysis);
return {
field: 'ngInjectableDef',
initializer: res.expression,
type: res.type,
};
}
}
/**
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the input
* metadata needed to run `compileIvyInjectable`.
*/
function extractInjectableMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator,
checker: ts.TypeChecker): IvyInjectableMetadata {
if (clazz.name === undefined) {
throw new Error(`@Injectables must have names`);
}
const name = clazz.name.text;
const type = new WrappedNodeExpr(clazz.name);
if (decorator.args.length === 0) {
return {
name,
type,
providedIn: new LiteralExpr(null),
useType: getUseType(clazz, checker),
};
} else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
// Firstly make sure the decorator argument is an inline literal - if not, it's illegal to
// transport references from one location to another. This is the problem that lowering
// used to solve - if this restriction proves too undesirable we can re-implement lowering.
if (!ts.isObjectLiteralExpression(metaNode)) {
throw new Error(`In Ivy, decorator metadata must be inline.`);
}
// Resolve the fields of the literal into a map of field name to expression.
const meta = reflectObjectLiteral(metaNode);
let providedIn: Expression = new LiteralExpr(null);
if (meta.has('providedIn')) {
providedIn = new WrappedNodeExpr(meta.get('providedIn') !);
}
if (meta.has('useValue')) {
return {name, type, providedIn, useValue: new WrappedNodeExpr(meta.get('useValue') !)};
} else if (meta.has('useExisting')) {
return {name, type, providedIn, useExisting: new WrappedNodeExpr(meta.get('useExisting') !)};
} else if (meta.has('useClass')) {
return {name, type, providedIn, useClass: new WrappedNodeExpr(meta.get('useClass') !)};
} else if (meta.has('useFactory')) {
// useFactory is special - the 'deps' property must be analyzed.
const factory = new WrappedNodeExpr(meta.get('useFactory') !);
const deps: IvyInjectableDep[] = [];
if (meta.has('deps')) {
const depsExpr = meta.get('deps') !;
if (!ts.isArrayLiteralExpression(depsExpr)) {
throw new Error(`In Ivy, deps metadata must be inline.`);
}
if (depsExpr.elements.length > 0) {
throw new Error(`deps not yet supported`);
}
deps.push(...depsExpr.elements.map(dep => getDep(dep, checker)));
}
return {name, type, providedIn, useFactory: {factory, deps}};
} else {
const useType = getUseType(clazz, checker);
return {name, type, providedIn, useType};
}
} else {
throw new Error(`Too many arguments to @Injectable`);
}
}
function getUseType(clazz: ts.ClassDeclaration, checker: ts.TypeChecker): IvyInjectableDep[] {
const useType: IvyInjectableDep[] = [];
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
ctorParams.forEach(param => {
let tokenExpr = param.typeValueExpr;
let optional = false, self = false, skipSelf = false;
param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => {
if (dec.name === 'Inject') {
if (dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
} else if (dec.name === 'Optional') {
optional = true;
} else if (dec.name === 'SkipSelf') {
skipSelf = true;
} else if (dec.name === 'Self') {
self = true;
} else {
throw new Error(`Unexpected decorator ${dec.name} on parameter.`);
}
if (tokenExpr === null) {
throw new Error(`No suitable token for parameter!`);
}
});
const token = new WrappedNodeExpr(tokenExpr);
useType.push({token, optional, self, skipSelf});
});
return useType;
}
function getDep(dep: ts.Expression, checker: ts.TypeChecker): IvyInjectableDep {
const depObj = {
token: new WrappedNodeExpr(dep),
optional: false,
self: false,
skipSelf: false,
};
function maybeUpdateDecorator(dec: ts.Identifier, token?: ts.Expression): void {
const source = reflectImportedIdentifier(dec, checker);
if (source === null || source.from !== '@angular/core') {
return;
}
switch (source.name) {
case 'Inject':
if (token !== undefined) {
depObj.token = new WrappedNodeExpr(token);
}
break;
case 'Optional':
depObj.optional = true;
break;
case 'SkipSelf':
depObj.skipSelf = true;
break;
case 'Self':
depObj.self = true;
break;
}
}
if (ts.isArrayLiteralExpression(dep)) {
dep.elements.forEach(el => {
if (ts.isIdentifier(el)) {
maybeUpdateDecorator(el);
} else if (ts.isNewExpression(el) && ts.isIdentifier(el.expression)) {
const token = el.arguments && el.arguments.length > 0 && el.arguments[0] || undefined;
maybeUpdateDecorator(el.expression, token);
}
});
}
return depObj;
}

View File

@ -0,0 +1,96 @@
/**
* @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
*/
import {WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler';
import * as ts from 'typescript';
import {IvyCompilation} from './compilation';
import {ImportManager, translateExpression} from './translator';
export function ivyTransformFactory(compilation: IvyCompilation):
ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (file: ts.SourceFile): ts.SourceFile => {
return transformIvySourceFile(compilation, context, file);
};
};
}
/**
* A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
*/
function transformIvySourceFile(
compilation: IvyCompilation, context: ts.TransformationContext,
file: ts.SourceFile): ts.SourceFile {
const importManager = new ImportManager();
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
const sf = visitNode(file);
// Generate the import statements to prepend.
const imports = importManager.getAllImports().map(
i => ts.createImportDeclaration(
undefined, undefined,
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))),
ts.createLiteral(i.name)));
// Prepend imports if needed.
if (imports.length > 0) {
sf.statements = ts.createNodeArray([...imports, ...sf.statements]);
}
return sf;
// Helper function to process a class declaration.
function visitClassDeclaration(node: ts.ClassDeclaration): ts.ClassDeclaration {
// Determine if this class has an Ivy field that needs to be added, and compile the field
// to an expression if so.
const res = compilation.compileIvyFieldFor(node);
if (res !== undefined) {
// There is a field to add. Translate the initializer for the field into TS nodes.
const exprNode = translateExpression(res.initializer, importManager);
// Create a static property declaration for the new field.
const property = ts.createProperty(
undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined,
exprNode);
// Replace the class declaration with an updated version.
node = ts.updateClassDeclaration(
node,
// Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator(node.decorators, compilation.ivyDecoratorFor(node) !),
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
[...node.members, property]);
}
// Recurse into the class declaration in case there are nested class declarations.
return ts.visitEachChild(node, child => visitNode(child), context);
}
// Helper function that recurses through the nodes and processes each one.
function visitNode<T extends ts.Node>(node: T): T;
function visitNode(node: ts.Node): ts.Node {
if (ts.isClassDeclaration(node)) {
return visitClassDeclaration(node);
} else {
return ts.visitEachChild(node, child => visitNode(child), context);
}
}
}
function maybeFilterDecorator(
decorators: ts.NodeArray<ts.Decorator>| undefined,
toRemove: ts.Decorator): ts.NodeArray<ts.Decorator>|undefined {
if (decorators === undefined) {
return undefined;
}
const filtered = decorators.filter(dec => ts.getOriginalNode(dec) !== toRemove);
if (filtered.length === 0) {
return undefined;
}
return ts.createNodeArray(filtered);
}

View File

@ -0,0 +1,319 @@
/**
* @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
*/
import {ArrayType, AssertNotNull, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
export class ImportManager {
private moduleToIndex = new Map<string, string>();
private nextIndex = 0;
generateNamedImport(moduleName: string): string {
if (!this.moduleToIndex.has(moduleName)) {
this.moduleToIndex.set(moduleName, `i${this.nextIndex++}`);
}
return this.moduleToIndex.get(moduleName) !;
}
getAllImports(): {name: string, as: string}[] {
return Array.from(this.moduleToIndex.keys()).map(name => {
const as = this.moduleToIndex.get(name) !;
return {name, as};
});
}
}
export function translateExpression(expression: Expression, imports: ImportManager): ts.Expression {
return expression.visitExpression(new ExpressionTranslatorVisitor(imports), null);
}
export function translateType(type: Type, imports: ImportManager): string {
return type.visitType(new TypeTranslatorVisitor(imports), null);
}
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
constructor(private imports: ImportManager) {}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any) {
throw new Error('Method not implemented.');
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) {
throw new Error('Method not implemented.');
}
visitExpressionStmt(stmt: ExpressionStatement, context: any) {
throw new Error('Method not implemented.');
}
visitReturnStmt(stmt: ReturnStatement, context: any): ts.ReturnStatement {
return ts.createReturn(stmt.value.visitExpression(this, context));
}
visitDeclareClassStmt(stmt: ClassStmt, context: any) {
throw new Error('Method not implemented.');
}
visitIfStmt(stmt: IfStmt, context: any) { throw new Error('Method not implemented.'); }
visitTryCatchStmt(stmt: TryCatchStmt, context: any) {
throw new Error('Method not implemented.');
}
visitThrowStmt(stmt: ThrowStmt, context: any) { throw new Error('Method not implemented.'); }
visitCommentStmt(stmt: CommentStmt, context: any): never {
throw new Error('Method not implemented.');
}
visitJSDocCommentStmt(stmt: JSDocCommentStmt, context: any): never {
throw new Error('Method not implemented.');
}
visitReadVarExpr(ast: ReadVarExpr, context: any): ts.Identifier {
return ts.createIdentifier(ast.name !);
}
visitWriteVarExpr(expr: WriteVarExpr, context: any): ts.BinaryExpression {
return ts.createBinary(
ts.createIdentifier(expr.name), ts.SyntaxKind.EqualsToken,
expr.value.visitExpression(this, context));
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitWritePropExpr(expr: WritePropExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): ts.CallExpression {
return ts.createCall(
ast.fn.visitExpression(this, context), undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
}
visitInstantiateExpr(ast: InstantiateExpr, context: any): ts.NewExpression {
return ts.createNew(
ast.classExpr.visitExpression(this, context), undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
}
visitLiteralExpr(ast: LiteralExpr, context: any): ts.Expression {
if (ast.value === undefined) {
return ts.createIdentifier('undefined');
} else if (ast.value === null) {
return ts.createNull();
} else {
return ts.createLiteral(ast.value);
}
}
visitExternalExpr(ast: ExternalExpr, context: any): ts.PropertyAccessExpression {
if (ast.value.moduleName === null || ast.value.name === null) {
throw new Error(`Import unknown module or symbol ${ast.value}`);
}
return ts.createPropertyAccess(
ts.createIdentifier(this.imports.generateNamedImport(ast.value.moduleName)),
ts.createIdentifier(ast.value.name));
}
visitConditionalExpr(ast: ConditionalExpr, context: any): ts.ParenthesizedExpression {
return ts.createParen(ts.createConditional(
ast.condition.visitExpression(this, context), ast.trueCase.visitExpression(this, context),
ast.falseCase !.visitExpression(this, context)));
}
visitNotExpr(ast: NotExpr, context: any): ts.PrefixUnaryExpression {
return ts.createPrefix(
ts.SyntaxKind.ExclamationToken, ast.condition.visitExpression(this, context));
}
visitAssertNotNullExpr(ast: AssertNotNull, context: any): ts.NonNullExpression {
return ts.createNonNullExpression(ast.condition.visitExpression(this, context));
}
visitCastExpr(ast: CastExpr, context: any): ts.Expression {
return ast.value.visitExpression(this, context);
}
visitFunctionExpr(ast: FunctionExpr, context: any): ts.FunctionExpression {
return ts.createFunctionExpression(
undefined, undefined, ast.name || undefined, undefined,
ast.params.map(
param => ts.createParameter(
undefined, undefined, undefined, param.name, undefined, undefined, undefined)),
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitReadPropExpr(ast: ReadPropExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): ts.ObjectLiteralExpression {
const entries = ast.entries.map(
entry => ts.createPropertyAssignment(
entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key),
entry.value.visitExpression(this, context)));
return ts.createObjectLiteral(entries);
}
visitCommaExpr(ast: CommaExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any { return ast.node; }
}
export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
constructor(private imports: ImportManager) {}
visitBuiltinType(type: BuiltinType, context: any): string {
switch (type.name) {
case BuiltinTypeName.Bool:
return 'boolean';
case BuiltinTypeName.Dynamic:
return 'any';
case BuiltinTypeName.Int:
case BuiltinTypeName.Number:
return 'number';
case BuiltinTypeName.String:
return 'string';
default:
throw new Error(`Unsupported builtin type: ${BuiltinTypeName[type.name]}`);
}
}
visitExpressionType(type: ExpressionType, context: any): any {
return type.value.visitExpression(this, context);
}
visitArrayType(type: ArrayType, context: any): string {
return `Array<${type.visitType(this, context)}>`;
}
visitMapType(type: MapType, context: any): string {
if (type.valueType !== null) {
return `{[key: string]: ${type.valueType.visitType(this, context)}}`;
} else {
return '{[key: string]: any}';
}
}
visitReadVarExpr(ast: ReadVarExpr, context: any): string {
if (ast.name === null) {
throw new Error(`ReadVarExpr with no variable name in type`);
}
return ast.name;
}
visitWriteVarExpr(expr: WriteVarExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitWritePropExpr(expr: WritePropExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitInstantiateExpr(ast: InstantiateExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitLiteralExpr(ast: LiteralExpr, context: any): string {
if (typeof ast.value === 'string') {
const escaped = ast.value.replace(/\'/g, '\\\'');
return `'${escaped}'`;
} else {
return `${ast.value}`;
}
}
visitExternalExpr(ast: ExternalExpr, context: any): string {
if (ast.value.moduleName === null || ast.value.name === null) {
throw new Error(`Import unknown module or symbol`);
}
const base = `${this.imports.generateNamedImport(ast.value.moduleName)}.${ast.value.name}`;
if (ast.typeParams !== null) {
const generics = ast.typeParams.map(type => type.visitType(this, context)).join(', ');
return `${base}<${generics}>`;
} else {
return base;
}
}
visitConditionalExpr(ast: ConditionalExpr, context: any) {
throw new Error('Method not implemented.');
}
visitNotExpr(ast: NotExpr, context: any) { throw new Error('Method not implemented.'); }
visitAssertNotNullExpr(ast: AssertNotNull, context: any) {
throw new Error('Method not implemented.');
}
visitCastExpr(ast: CastExpr, context: any) { throw new Error('Method not implemented.'); }
visitFunctionExpr(ast: FunctionExpr, context: any) { throw new Error('Method not implemented.'); }
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any) {
throw new Error('Method not implemented.');
}
visitReadPropExpr(ast: ReadPropExpr, context: any) { throw new Error('Method not implemented.'); }
visitReadKeyExpr(ast: ReadKeyExpr, context: any) { throw new Error('Method not implemented.'); }
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any) {
throw new Error('Method not implemented.');
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any) {
throw new Error('Method not implemented.');
}
visitCommaExpr(ast: CommaExpr, context: any) { throw new Error('Method not implemented.'); }
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any) {
const node: ts.Node = ast.node;
if (ts.isIdentifier(node)) {
return node.text;
} else {
throw new Error(
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
}
}
}

View File

@ -184,7 +184,7 @@ export interface CompilerOptions extends ts.CompilerOptions {
*
* @experimental
*/
enableIvy?: boolean;
enableIvy?: boolean|'ngtsc';
/** @internal */
collectAllErrors?: boolean;

View File

@ -12,6 +12,7 @@ import * as ts from 'typescript';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {METADATA_VERSION, ModuleMetadata} from '../metadata/index';
import {NgtscCompilerHost} from '../ngtsc/compiler_host';
import {CompilerHost, CompilerOptions, LibrarySummary} from './api';
import {MetadataReaderHost, createMetadataReaderCache, readMetadata} from './metadata_reader';
@ -23,6 +24,9 @@ const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export function createCompilerHost(
{options, tsHost = ts.createCompilerHost(options, true)}:
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
if (options.enableIvy) {
return new NgtscCompilerHost(tsHost);
}
return tsHost;
}

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {error} from './util';
export interface Node { sourceSpan: ParseSourceSpan|null; }
@ -20,7 +21,7 @@ const _VALID_IDENTIFIER_RE = /^[$A-Z_][0-9A-Z_$]*$/i;
export class TypeScriptNodeEmitter {
updateSourceFile(sourceFile: ts.SourceFile, stmts: Statement[], preamble?: string):
[ts.SourceFile, Map<ts.Node, Node>] {
const converter = new _NodeEmitterVisitor();
const converter = new NodeEmitterVisitor();
// [].concat flattens the result so that each `visit...` method can also return an array of
// stmts.
const statements: any[] = [].concat(
@ -62,7 +63,7 @@ export class TypeScriptNodeEmitter {
export function updateSourceFile(
sourceFile: ts.SourceFile, module: PartialModule,
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
const converter = new _NodeEmitterVisitor();
const converter = new NodeEmitterVisitor();
converter.loadExportedVariableIdentifiers(sourceFile);
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
@ -148,7 +149,7 @@ function firstAfter<T>(a: T[], predicate: (value: T) => boolean) {
// A recorded node is a subtype of the node that is marked as being recorded. This is used
// to ensure that NodeEmitterVisitor.record has been called on all nodes returned by the
// NodeEmitterVisitor
type RecordedNode<T extends ts.Node = ts.Node> = (T & { __recorded: any; }) | null;
export type RecordedNode<T extends ts.Node = ts.Node> = (T & { __recorded: any;}) | null;
function escapeLiteral(value: string): string {
return value.replace(/(\"|\\)/g, '\\$1').replace(/(\n)|(\r)/g, function(v, n, r) {
@ -183,7 +184,7 @@ function isExportTypeStatement(statement: ts.Statement): boolean {
/**
* Visits an output ast and produces the corresponding TypeScript synthetic nodes.
*/
class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
private _nodeMap = new Map<ts.Node, Node>();
private _importsWithPrefixes = new Map<string, string>();
private _reexports = new Map<string, {name: string, as: string}[]>();
@ -461,6 +462,9 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
return commentStmt;
}
// ExpressionVisitor
visitWrappedNodeExpr(expr: WrappedNodeExpr<any>) { return this.record(expr, expr.node); }
// ExpressionVisitor
visitReadVarExpr(expr: ReadVarExpr) {
switch (expr.builtin) {

View File

@ -15,6 +15,7 @@ import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {compareVersions} from '../diagnostics/typescript_version';
import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {NgtscProgram} from '../ngtsc/program';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api';
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
@ -286,6 +287,9 @@ class AngularCompilerProgram implements Program {
emitCallback?: TsEmitCallback,
mergeEmitResultsCallback?: TsMergeEmitResultsCallback,
} = {}): ts.EmitResult {
if (this.options.enableIvy === 'ngtsc') {
throw new Error('Cannot run legacy compiler in ngtsc mode');
}
return this.options.enableIvy === true ? this._emitRender3(parameters) :
this._emitRender2(parameters);
}
@ -903,6 +907,9 @@ export function createProgram({rootNames, options, host, oldProgram}: {
options: CompilerOptions,
host: CompilerHost, oldProgram?: Program
}): Program {
if (options.enableIvy === 'ngtsc') {
return new NgtscProgram(rootNames, options, host, oldProgram);
}
return new AngularCompilerProgram(rootNames, options, host, oldProgram);
}

View File

@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "ts_library", "ts_web_test")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "ngtsc_lib",
testonly = 1,
srcs = [
"ngtsc_spec.ts",
],
deps = [
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/test:test_utils",
],
)
jasmine_node_test(
name = "ngtsc",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
data = [
"//packages/compiler-cli/test/ngtsc/fake_core:npm_package",
],
deps = [
":ngtsc_lib",
"//tools/testing:node_no_angular",
],
)

View File

@ -0,0 +1,22 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library", "ng_package")
ts_library(
name = "fake_core",
srcs = [
"index.ts",
],
module_name = "@angular/core",
)
ng_package(
name = "npm_package",
srcs = [
"package.json",
],
entry_point = "packages/fake_core/index.js",
deps = [
":fake_core",
],
)

View File

@ -0,0 +1,13 @@
`fake_core` is a library designed to expose some of the same symbols as `@angular/core`, without
requiring compilation of the whole of `@angular/core`. This enables unit tests for the compiler to
be written without incurring long rebuilds for every change.
* `@angular/core` is compiled with `@angular/compiler-cli`, and therefore has an implicit dependency
on it. Therefore core must be rebuilt if the compiler changes.
* Tests for the compiler which intend to build code that depends on `@angular/core` must have
a data dependency on `@angular/core`. Therefore core must be built to run the compiler tests, and
thus rebuilt if the compiler changes.
This rebuild cycle is expensive and slow. `fake_core` avoids this by exposing a subset of the
`@angular/core` API, which enables applications to be built by the ngtsc compiler without
needing a full version of core present at compile time.

View File

@ -0,0 +1,25 @@
/**
* @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
*/
type FnWithArg<T> = (arg?: any) => T;
function callableClassDecorator(): FnWithArg<(clazz: any) => any> {
return null !;
}
function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
return null !;
}
export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator();
export const Inject = callableParamDecorator();
export const Self = callableParamDecorator();
export const SkipSelf = callableParamDecorator();
export const Optional = callableParamDecorator();

View File

@ -0,0 +1,13 @@
{
"name": "@angular/core",
"version": "0.0.0-FAKE-FOR-TESTS",
"description": "Fake version of core for ngtsc tests",
"main": "./bundles/fake_core.umd.js",
"module": "./fesm5/fake_core.js",
"es2015": "./fesm2015/fake_core.js",
"esm5": "./esm5/index.js",
"esm2015": "./esm2015/index.js",
"fesm5": "./fesm5/fake_core.js",
"fesm2015": "./fesm2015/fake_core.js",
"typings": "./index.d.ts"
}

View File

@ -0,0 +1,125 @@
/**
* @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
*/
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {main, readCommandLineAndConfiguration, watchMode} from '../../src/main';
import {TestSupport, isInBazel, makeTempDir, setup} from '../test_support';
function setupFakeCore(support: TestSupport): void {
const fakeCore = path.join(
process.env.TEST_SRCDIR, 'angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package');
const nodeModulesPath = path.join(support.basePath, 'node_modules');
const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core');
fs.symlinkSync(fakeCore, angularCoreDirectory);
}
function getNgRootDir() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
return moduleFilename.substr(0, distIndex);
}
describe('ngtsc behavioral tests', () => {
if (!isInBazel()) {
// These tests should be excluded from the non-Bazel build.
return;
}
let basePath: string;
let outDir: string;
let write: (fileName: string, content: string) => void;
let errorSpy: jasmine.Spy&((s: string) => void);
function shouldExist(fileName: string) {
if (!fs.existsSync(path.resolve(outDir, fileName))) {
throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`);
}
}
function shouldNotExist(fileName: string) {
if (fs.existsSync(path.resolve(outDir, fileName))) {
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`);
}
}
function getContents(fileName: string): string {
shouldExist(fileName);
const modulePath = path.resolve(outDir, fileName);
return fs.readFileSync(modulePath, 'utf8');
}
function writeConfig(
tsconfig: string =
'{"extends": "./tsconfig-base.json", "angularCompilerOptions": {"enableIvy": "ngtsc"}}') {
write('tsconfig.json', tsconfig);
}
beforeEach(() => {
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
const support = setup();
basePath = support.basePath;
outDir = path.join(basePath, 'built');
process.chdir(basePath);
write = (fileName: string, content: string) => { support.write(fileName, content); };
setupFakeCore(support);
write('tsconfig-base.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"skipLibCheck": true,
"noImplicitAny": true,
"types": [],
"outDir": "built",
"rootDir": ".",
"baseUrl": ".",
"declaration": true,
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"lib": ["es6", "dom"],
"typeRoots": ["node_modules/@types"]
},
"angularCompilerOptions": {
"enableIvy": "ngtsc"
}
}`);
});
it('should compile without errors', () => {
writeConfig();
write('test.ts', `
import {Injectable} from '@angular/core';
@Injectable()
export class Dep {}
@Injectable()
export class Service {
constructor(dep: Dep) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents).toContain('Dep.ngInjectableDef =');
expect(jsContents).toContain('Service.ngInjectableDef =');
expect(jsContents).not.toContain('__decorate');
const dtsContents = getContents('test.d.ts');
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Dep>;');
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Service>;');
});
});

View File

@ -18,5 +18,5 @@ export interface AotCompilerOptions {
fullTemplateTypeCheck?: boolean;
allowEmptyCodegenFiles?: boolean;
strictInjectionParameters?: boolean;
enableIvy?: boolean;
enableIvy?: boolean|'ngtsc';
}

View File

@ -67,7 +67,7 @@ export * from './ml_parser/html_tags';
export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {NgModuleCompiler} from './ng_module_compiler';
export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast';
export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export * from './output/ts_emitter';
export * from './parse_util';
@ -78,4 +78,5 @@ export * from './template_parser/template_parser';
export {ViewCompiler} from './view_compiler/view_compiler';
export {getParseErrors, isSyntaxError, syntaxError, Version} from './util';
export {SourceMap} from './output/source_map';
// This file only reexports content of the `src` folder. Keep it that way.
export * from './injectable_compiler_2';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -295,6 +295,7 @@ class KeyVisitor implements o.ExpressionVisitor {
`EX:${ast.value.runtime.name}`;
}
visitWrappedNodeExpr = invalid;
visitReadVarExpr = invalid;
visitWriteVarExpr = invalid;
visitWriteKeyExpr = invalid;

View File

@ -0,0 +1,101 @@
/**
* @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
*/
import {InjectFlags} from './core';
import * as o from './output/output_ast';
import {Identifiers} from './render3/r3_identifiers';
type MapEntry = {
key: string; quoted: boolean; value: o.Expression;
};
function mapToMapExpression(map: {[key: string]: o.Expression}): o.LiteralMapExpr {
const result = Object.keys(map).map(key => ({key, value: map[key], quoted: false}));
return o.literalMap(result);
}
export interface InjectableDef {
expression: o.Expression;
type: o.Type;
}
export interface IvyInjectableDep {
token: o.Expression;
optional: boolean;
self: boolean;
skipSelf: boolean;
}
export interface IvyInjectableMetadata {
name: string;
type: o.Expression;
providedIn: o.Expression;
useType?: IvyInjectableDep[];
useClass?: o.Expression;
useFactory?: {factory: o.Expression; deps: IvyInjectableDep[];};
useExisting?: o.Expression;
useValue?: o.Expression;
}
export function compileIvyInjectable(meta: IvyInjectableMetadata): InjectableDef {
let ret: o.Expression = o.NULL_EXPR;
if (meta.useType !== undefined) {
const args = meta.useType.map(dep => injectDep(dep));
ret = new o.InstantiateExpr(meta.type, args);
} else if (meta.useClass !== undefined) {
const factory =
new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory');
ret = new o.InvokeFunctionExpr(factory, []);
} else if (meta.useValue !== undefined) {
ret = meta.useValue;
} else if (meta.useExisting !== undefined) {
ret = o.importExpr(Identifiers.inject).callFn([meta.useExisting]);
} else if (meta.useFactory !== undefined) {
const args = meta.useFactory.deps.map(dep => injectDep(dep));
ret = new o.InvokeFunctionExpr(meta.useFactory.factory, args);
} else {
throw new Error('No instructions for injectable compiler!');
}
const token = meta.type;
const providedIn = meta.providedIn;
const factory =
o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`);
const expression = o.importExpr({
moduleName: '@angular/core',
name: 'defineInjectable',
}).callFn([mapToMapExpression({token, factory, providedIn})]);
const type = new o.ExpressionType(o.importExpr(
{
moduleName: '@angular/core',
name: 'InjectableDef',
},
[new o.ExpressionType(meta.type)]));
return {
expression, type,
};
}
function injectDep(dep: IvyInjectableDep): o.Expression {
const defaultValue = dep.optional ? o.NULL_EXPR : o.literal(undefined);
const flags = o.literal(
InjectFlags.Default | (dep.self && InjectFlags.Self || 0) |
(dep.skipSelf && InjectFlags.SkipSelf || 0));
if (!dep.optional && !dep.skipSelf && !dep.self) {
return o.importExpr(Identifiers.inject).callFn([dep.token]);
} else {
return o.importExpr(Identifiers.inject).callFn([
dep.token,
defaultValue,
flags,
]);
}
}

View File

@ -312,6 +312,9 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(expr, `)`);
return null;
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): never {
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): any {
let varName = ast.name !;
if (ast.builtin != null) {

View File

@ -70,6 +70,9 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
ctx.println(stmt, `};`);
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): never {
throw new Error('Cannot emit a WrappedNodeExpr in Javascript.');
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): string|null {
if (ast.builtin === o.BuiltinVar.This) {
ctx.print(ast, 'self');

View File

@ -279,6 +279,21 @@ export class ReadVarExpr extends Expression {
}
}
export class WrappedNodeExpr<T> extends Expression {
constructor(public node: T, type?: Type|null, sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof WrappedNodeExpr && this.node === e.node;
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWrappedNodeExpr(this, context);
}
}
export class WriteVarExpr extends Expression {
public value: Expression;
@ -722,6 +737,7 @@ export interface ExpressionVisitor {
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any;
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any;
visitCommaExpr(ast: CommaExpr, context: any): any;
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any;
}
export const THIS_EXPR = new ReadVarExpr(BuiltinVar.This, null, null);
@ -973,6 +989,10 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
visitReadVarExpr(ast: ReadVarExpr, context: any): any { return this.transformExpr(ast, context); }
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any {
return this.transformExpr(ast, context);
}
visitWriteVarExpr(expr: WriteVarExpr, context: any): any {
return this.transformExpr(
new WriteVarExpr(
@ -1199,6 +1219,7 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
}
visitArrayType(type: ArrayType, context: any): any { return this.visitType(type, context); }
visitMapType(type: MapType, context: any): any { return this.visitType(type, context); }
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any { return ast; }
visitReadVarExpr(ast: ReadVarExpr, context: any): any {
return this.visitExpression(ast, context);
}

View File

@ -114,6 +114,9 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
}
throw new Error(`Not declared variable ${expr.name}`);
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: _ExecutionContext): never {
throw new Error('Cannot interpret a WrappedNodeExpr.');
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: _ExecutionContext): any {
let varName = ast.name !;
if (ast.builtin != null) {

View File

@ -169,6 +169,10 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor
return null;
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): never {
throw new Error('Cannot visit a WrappedNodeExpr when outputting Typescript.');
}
visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `(<`);
ast.type !.visitType(this, ctx);

View File

@ -71,6 +71,12 @@ export class Identifiers {
static projection: o.ExternalReference = {name: 'ɵP', moduleName: CORE};
static projectionDef: o.ExternalReference = {name: 'ɵpD', moduleName: CORE};
static refreshComponent: o.ExternalReference = {name: 'ɵr', moduleName: CORE};
static directiveLifeCycle: o.ExternalReference = {name: 'ɵl', moduleName: CORE};
static inject: o.ExternalReference = {name: 'inject', moduleName: CORE};
static injectAttribute: o.ExternalReference = {name: 'ɵinjectAttribute', moduleName: CORE};
static injectElementRef: o.ExternalReference = {name: 'ɵinjectElementRef', moduleName: CORE};

View File

@ -23,3 +23,10 @@ ts_library(
"//packages/platform-server/testing",
],
)
ts_library(
name = "node_no_angular",
testonly = 1,
srcs = ["init_node_no_angular_spec.ts"],
deps = ["//packages:types"],
)

9
tools/testing/README.md Normal file
View File

@ -0,0 +1,9 @@
The spec helper files here set up the global testing environment prior to the execution of specs.
There are 3 options:
* `init_node_spec` - configures a node environment to test Angular applications with
platform-server.
* `init_node_no_angular_spec` - configures a node environment for testing without setting up
Angular's testbed (no dependency on Angular packages is incurred).
* `init_browser_spec` - configures a browser environment to test Angular applications.

View File

@ -0,0 +1,26 @@
/**
* @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
*/
// This hack is needed to get jasmine, node and zone working inside bazel.
// 1) we load `jasmine-core` which contains the ENV: it, describe etc...
const jasmineCore: any = require('jasmine-core');
// 2) We create an instance of `jasmine` ENV.
const patchedJasmine = jasmineCore.boot(jasmineCore);
// 3) Save the `jasmine` into global so that `zone.js/dist/jasmine-patch.js` can get a hold of it to
// patch it.
(global as any)['jasmine'] = patchedJasmine;
// 4) Change the `jasmine-core` to make sure that all subsequent jasmine's have the same ENV,
// otherwise the patch will not work.
// This is needed since Bazel creates a new instance of jasmine and it's ENV and we want to make
// sure it gets the same one.
jasmineCore.boot = function() {
return patchedJasmine;
};
(global as any).isNode = true;
(global as any).isBrowser = false;