feat(ivy): ngtsc compiles @Component, @Directive, @NgModule (#24427)

This change supports compilation of components, directives, and modules
within ngtsc. Support is not complete, but is enough to compile and test
//packages/core/test/bundling/todo in full AOT mode. Code size benefits
are not yet achieved as //packages/core itself does not get compiled, and
some decorators (e.g. @Input) are not stripped, leading to unwanted code
being retained by the tree-shaker. This will be improved in future commits.

PR Close #24427
This commit is contained in:
Alex Rickabaugh
2018-06-14 14:36:45 -07:00
committed by Miško Hevery
parent 0f7e4fae20
commit 27bc7dcb43
69 changed files with 1884 additions and 607 deletions
@@ -7,7 +7,7 @@
*/
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core';
import {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition';
import {TableCell, buildTable, emptyTable} from '../util';
@@ -15,7 +15,7 @@ export class LargeTableComponent {
data: TableCell[][] = emptyTable;
/** @nocollapse */
static ngComponentDef: ComponentDef<LargeTableComponent> = defineComponent({
static ngComponentDef: ComponentDefInternal<LargeTableComponent> = defineComponent({
type: LargeTableComponent,
selectors: [['largetable']],
template: function(rf: RenderFlags, ctx: LargeTableComponent) {
+3 -3
View File
@@ -7,7 +7,7 @@
*/
import {ɵC as C, ɵE as E, ɵRenderFlags as RenderFlags, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core';
import {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
import {ComponentDefInternal} from '@angular/core/src/render3/interfaces/definition';
import {TreeNode, buildTree, emptyTree} from '../util';
@@ -35,7 +35,7 @@ export class TreeComponent {
data: TreeNode = emptyTree;
/** @nocollapse */
static ngComponentDef: ComponentDef<TreeComponent> = defineComponent({
static ngComponentDef: ComponentDefInternal<TreeComponent> = defineComponent({
type: TreeComponent,
selectors: [['tree']],
template: function(rf: RenderFlags, ctx: TreeComponent) {
@@ -95,7 +95,7 @@ export class TreeFunction {
data: TreeNode = emptyTree;
/** @nocollapse */
static ngComponentDef: ComponentDef<TreeFunction> = defineComponent({
static ngComponentDef: ComponentDefInternal<TreeFunction> = defineComponent({
type: TreeFunction,
selectors: [['tree']],
template: function(rf: RenderFlags, ctx: TreeFunction) {
+1
View File
@@ -25,6 +25,7 @@ ts_library(
tsconfig = ":tsconfig",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/transform",
],
)
@@ -0,0 +1,17 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "annotations",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/annotations",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform",
],
)
@@ -0,0 +1,13 @@
/**
* @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 {ComponentDecoratorHandler} from './src/component';
export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable';
export {NgModuleDecoratorHandler} from './src/ng_module';
export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope';
@@ -0,0 +1,117 @@
/**
* @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 {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, reflectNonStaticField, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {extractDirectiveMetadata} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
const EMPTY_MAP = new Map<string, Expression>();
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Component' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveMetadata = extractDirectiveMetadata(node, decorator, this.checker);
if (directiveMetadata === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies
// that no analysis was produced.
return {};
}
// Next, read the `@Component`-specific fields.
const component = reflectObjectLiteral(meta);
// Resolve and parse the template.
if (!component.has('template')) {
throw new Error(`For now, components must directly have a template.`);
}
const templateExpr = component.get('template') !;
const templateStr = staticallyResolve(templateExpr, this.checker);
if (typeof templateStr !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
}
let preserveWhitespaces: boolean = false;
if (component.has('preserveWhitespaces')) {
const value = staticallyResolve(component.get('preserveWhitespaces') !, this.checker);
if (typeof value !== 'boolean') {
throw new Error(`preserveWhitespaces must resolve to a boolean if present`);
}
preserveWhitespaces = value;
}
const template = parseTemplate(
templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`,
{preserveWhitespaces});
if (template.errors !== undefined) {
throw new Error(
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
}
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (directiveMetadata.selector !== null) {
this.scopeRegistry.registerSelector(node, directiveMetadata.selector);
}
return {
analysis: {
...directiveMetadata,
template,
viewQueries: [],
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP,
directives: EMPTY_MAP,
}
};
}
compile(node: ts.ClassDeclaration, analysis: R3ComponentMetadata): CompileResult {
const pool = new ConstantPool();
// Check whether this component was registered with an NgModule. If so, it should be compiled
// under that module's compilation scope.
const scope = this.scopeRegistry.lookupCompilationScope(node);
if (scope !== null) {
// Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during compile() the whole compilation unit has been
// fully analyzed.
analysis = {...analysis, ...scope};
}
const res = compileComponentFromMetadata(analysis, pool, makeBindingParser());
return {
field: 'ngComponentDef',
initializer: res.expression,
statements: pool.statements,
type: res.type,
};
}
}
@@ -0,0 +1,212 @@
/**
* @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 {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, staticallyResolve} from '../../metadata';
import {DecoratedNode, getDecoratedClassElements, reflectNonStaticField, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Directive' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
const analysis = extractDirectiveMetadata(node, decorator, this.checker);
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) {
this.scopeRegistry.registerSelector(node, analysis.selector);
}
return {analysis};
}
compile(node: ts.ClassDeclaration, analysis: R3DirectiveMetadata): CompileResult {
const pool = new ConstantPool();
const res = compileDirectiveFromMetadata(analysis, pool, makeBindingParser());
return {
field: 'ngDirectiveDef',
initializer: res.expression,
statements: pool.statements,
type: res.type,
};
}
}
/**
* Helper function to extract metadata from a `Directive` or `Component`.
*/
export function extractDirectiveMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker): R3DirectiveMetadata|
undefined {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
const directive = reflectObjectLiteral(meta);
if (directive.has('jit')) {
// The only allowed value is true, so there's no need to expand further.
return undefined;
}
// Precompute a list of ts.ClassElements that have decorators. This includes things like @Input,
// @Output, @HostBinding, etc.
const decoratedElements = getDecoratedClassElements(clazz, checker);
// Construct the map of inputs both from the @Directive/@Component decorator, and the decorated
// fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker);
const inputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Input'), checker);
// And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
const outputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Output'), checker);
// Parse the selector.
let selector = '';
if (directive.has('selector')) {
const resolved = staticallyResolve(directive.get('selector') !, checker);
if (typeof resolved !== 'string') {
throw new Error(`Selector must be a string`);
}
selector = resolved;
}
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = reflectNonStaticField(clazz, 'ngOnChanges') !== null;
return {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, checker),
host: {
attributes: {},
listeners: {},
properties: {},
},
lifecycle: {
usesOnChanges,
},
inputs: {...inputsFromMeta, ...inputsFromFields},
outputs: {...outputsFromMeta, ...outputsFromFields},
queries: [], selector,
type: new WrappedNodeExpr(clazz.name !),
typeSourceSpan: null !,
};
}
function assertIsStringArray(value: any[]): value is string[] {
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string') {
throw new Error(`Failed to resolve @Directive.inputs[${i}] to a string`);
}
}
return true;
}
type DecoratedProperty = DecoratedNode<ts.PropertyDeclaration|ts.AccessorDeclaration>;
/**
* Find all fields in the array of `DecoratedNode`s that have a decorator of the given type.
*/
function findDecoratedFields(
elements: DecoratedNode<ts.ClassElement>[], decoratorModule: string,
decoratorName: string): DecoratedProperty[] {
return elements
.map(entry => {
const element = entry.element;
// Only consider properties and accessors. Filter out everything else.
if (!ts.isPropertyDeclaration(element) && !ts.isAccessor(element)) {
return null;
}
// Extract the array of matching decorators (there could be more than one).
const decorators = entry.decorators.filter(
decorator => decorator.name === decoratorName && decorator.from === decoratorModule);
if (decorators.length === 0) {
// No matching decorators, don't include this element.
return null;
}
return {element, decorators};
})
// Filter out nulls.
.filter(entry => entry !== null) as DecoratedProperty[];
}
/**
* Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the
* correctly shaped metadata object.
*/
function parseFieldToPropertyMapping(
directive: Map<string, ts.Expression>, field: string,
checker: ts.TypeChecker): {[field: string]: string} {
if (!directive.has(field)) {
return EMPTY_OBJECT;
}
// Resolve the field of interest from the directive metadata to a string[].
const metaValues = staticallyResolve(directive.get(field) !, checker);
if (!Array.isArray(metaValues) || !assertIsStringArray(metaValues)) {
throw new Error(`Failed to resolve @Directive.${field}`);
}
return metaValues.reduce(
(results, value) => {
// Either the value is 'field' or 'field: property'. In the first case, `property` will
// be undefined, in which case the field name should also be used as the property name.
const [field, property] = value.split(':', 2).map(str => str.trim());
results[field] = property || field;
return results;
},
{} as{[field: string]: string});
}
/**
* Parse property decorators (e.g. `Input` or `Output`) and return the correctly shaped metadata
* object.
*/
function parseDecoratedFields(
fields: DecoratedProperty[], checker: ts.TypeChecker): {[field: string]: string} {
return fields.reduce(
(results, field) => {
const fieldName = (field.element.name as ts.Identifier).text;
field.decorators.forEach(decorator => {
// The decorator either doesn't have an argument (@Input()) in which case the property
// name is used, or it has one argument (@Output('named')).
if (decorator.args.length === 0) {
results[fieldName] = fieldName;
} else if (decorator.args.length === 1) {
const property = staticallyResolve(decorator.args[0], checker);
if (typeof property !== 'string') {
throw new Error(`Decorator argument must resolve to a string`);
}
results[fieldName] = property;
} else {
// Too many arguments.
throw new Error(
`Decorator must have 0 or 1 arguments, got ${decorator.args.length} argument(s)`);
}
});
return results;
},
{} as{[field: string]: string});
}
@@ -11,14 +11,15 @@ import * as ts from 'typescript';
import {Decorator} from '../../metadata';
import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform/src/api';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
import {getConstructorDependencies} from './util';
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableCompilerAdapter implements CompilerAdapter<R3InjectableMetadata> {
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
constructor(private checker: ts.TypeChecker) {}
detect(decorator: Decorator[]): Decorator|undefined {
@@ -31,11 +32,12 @@ export class InjectableCompilerAdapter implements CompilerAdapter<R3InjectableMe
};
}
compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): AddStaticFieldInstruction {
compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): CompileResult {
const res = compileIvyInjectable(analysis);
return {
field: 'ngInjectableDef',
initializer: res.expression,
statements: [],
type: res.type,
};
}
@@ -105,38 +107,7 @@ function extractInjectableMetadata(
}
}
function getConstructorDependencies(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
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, host: false, resolved: R3ResolvedDependencyType.Token});
});
return useType;
}
function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata {
const meta: R3DependencyMetadata = {
@@ -0,0 +1,116 @@
/**
* @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 {ConstantPool, Expression, R3DirectiveMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {referenceToExpression} from './util';
/**
* Compiles @NgModule annotations to ngModuleDef fields.
*
* TODO(alxhub): handle injector side of things as well.
*/
export class NgModuleDecoratorHandler implements DecoratorHandler<R3NgModuleMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'NgModule' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3NgModuleMetadata> {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
const ngModule = reflectObjectLiteral(meta);
if (ngModule.has('jit')) {
// The only allowed value is true, so there's no need to expand further.
return {};
}
// Extract the module declarations, imports, and exports.
let declarations: Reference[] = [];
if (ngModule.has('declarations')) {
const declarationMeta = staticallyResolve(ngModule.get('declarations') !, this.checker);
declarations = resolveTypeList(declarationMeta, 'declarations');
}
let imports: Reference[] = [];
if (ngModule.has('imports')) {
const importsMeta = staticallyResolve(ngModule.get('imports') !, this.checker);
imports = resolveTypeList(importsMeta, 'imports');
}
let exports: Reference[] = [];
if (ngModule.has('exports')) {
const exportsMeta = staticallyResolve(ngModule.get('exports') !, this.checker);
exports = resolveTypeList(exportsMeta, 'exports');
}
// Register this module's information with the SelectorScopeRegistry. This ensures that during
// the compile() phase, the module's metadata is available for selector scope computation.
this.scopeRegistry.registerModule(node, {declarations, imports, exports});
const context = node.getSourceFile();
return {
analysis: {
type: new WrappedNodeExpr(node.name !),
bootstrap: [],
declarations: declarations.map(decl => referenceToExpression(decl, context)),
exports: exports.map(exp => referenceToExpression(exp, context)),
imports: imports.map(imp => referenceToExpression(imp, context)),
emitInline: false,
},
};
}
compile(node: ts.ClassDeclaration, analysis: R3NgModuleMetadata): CompileResult {
const res = compileNgModule(analysis);
return {
field: 'ngModuleDef',
initializer: res.expression,
statements: [],
type: res.type,
};
}
}
/**
* Compute a list of `Reference`s from a resolved metadata value.
*/
function resolveTypeList(resolvedList: ResolvedValue, name: string): Reference[] {
const refList: Reference[] = [];
if (!Array.isArray(resolvedList)) {
throw new Error(`Expected array when reading property ${name}`);
}
resolvedList.forEach((entry, idx) => {
if (Array.isArray(entry)) {
// Recurse into nested arrays.
refList.push(...resolveTypeList(entry, name));
} else if (entry instanceof Reference) {
if (!entry.expressable) {
throw new Error(`Value at position ${idx} in ${name} array is not expressable`);
} else if (!ts.isClassDeclaration(entry.node)) {
throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`);
}
refList.push(entry);
} else {
// TODO(alxhub): expand ModuleWithProviders.
throw new Error(`Value at position ${idx} in ${name} array is not a reference`);
}
});
return refList;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
/**
* @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, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference, reflectConstructorParameters} from '../../metadata';
import {reflectImportedIdentifier} from '../../metadata/src/reflector';
export function getConstructorDependencies(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
ctorParams.forEach(param => {
let tokenExpr = param.typeValueExpr;
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
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 if (dec.name === 'Host') {
host = true;
} else if (dec.name === 'Attribute') {
if (dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Attribute().`);
}
tokenExpr = dec.args[0];
resolved = R3ResolvedDependencyType.Attribute;
} else {
throw new Error(`Unexpected decorator ${dec.name} on parameter.`);
}
});
if (tokenExpr === null) {
throw new Error(
`No suitable token for parameter ${(param.name as ts.Identifier).text} of class ${clazz.name!.text} with decorators ${param.decorators.map(dec => dec.from + '#' + dec.name).join(',')}`);
}
if (ts.isIdentifier(tokenExpr)) {
const importedSymbol = reflectImportedIdentifier(tokenExpr, checker);
if (importedSymbol !== null && importedSymbol.from === '@angular/core') {
switch (importedSymbol.name) {
case 'ElementRef':
resolved = R3ResolvedDependencyType.ElementRef;
break;
case 'Injector':
resolved = R3ResolvedDependencyType.Injector;
break;
case 'TemplateRef':
resolved = R3ResolvedDependencyType.TemplateRef;
break;
case 'ViewContainerRef':
resolved = R3ResolvedDependencyType.ViewContainerRef;
break;
default:
// Leave as a Token or Attribute.
}
}
}
const token = new WrappedNodeExpr(tokenExpr);
useType.push({token, optional, self, skipSelf, host, resolved});
});
return useType;
}
export function referenceToExpression(ref: Reference, context: ts.SourceFile): Expression {
const exp = ref.toExpression(context);
if (exp === null) {
throw new Error(`Could not refer to ${ts.SyntaxKind[ref.node.kind]}`);
}
return exp;
}
@@ -0,0 +1,28 @@
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",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
],
)
@@ -0,0 +1,79 @@
/**
* @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 {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {NgModuleDecoratorHandler} from '../src/ng_module';
import {SelectorScopeRegistry} from '../src/selector_scope';
describe('SelectorScopeRegistry', () => {
it('absolute imports work', () => {
const {program} = makeProgram([
{
name: 'node_modules/@angular/core/index.d.ts',
contents: `
export interface NgComponentDef<A, B> {}
export interface NgModuleDef<A, B, C, D> {}
`
},
{
name: 'node_modules/some_library/index.d.ts',
contents: `
import {NgComponentDef, NgModuleDef} from '@angular/core';
import * as i0 from './component';
export declare class SomeModule {
static ngModuleDef: NgModuleDef<SomeModule, [i0.SomeCmp], any, [i0.SomeCmp]>;
}
export declare class SomeCmp {
static ngComponentDef: NgComponentDef<SomeCmp, 'some-cmp'>;
}
`
},
{
name: 'node_modules/some_library/component.d.ts',
contents: `
export declare class SomeCmp {}
`
},
{
name: 'entry.ts',
contents: `
export class ProgramCmp {}
export class ProgramModule {}
`
},
]);
const checker = program.getTypeChecker();
const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
const SomeModule = getDeclaration(
program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration);
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const registry = new SelectorScopeRegistry(checker);
registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
exports: [],
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
});
registry.registerSelector(ProgramCmp, 'program-cmp');
const scope = registry.lookupCompilationScope(ProgramCmp) !;
expect(scope).toBeDefined();
expect(scope.directives).toBeDefined();
expect(scope.directives.size).toBe(1);
});
});
@@ -10,6 +10,7 @@ ts_library(
]),
module_name = "@angular/compiler-cli/src/ngtsc/metadata",
deps = [
"//packages:types",
"//packages/compiler",
],
)
@@ -6,5 +6,6 @@
* 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';
export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator, reflectNonStaticField, reflectObjectLiteral, reflectStaticField, reflectTypeEntityToDeclaration,} from './src/reflector';
export {AbsoluteReference, Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver';
@@ -236,3 +236,103 @@ export function reflectImportedIdentifier(
return {from, name};
}
export interface DecoratedNode<T extends ts.Node> {
element: T;
decorators: Decorator[];
}
export function getDecoratedClassElements(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): DecoratedNode<ts.ClassElement>[] {
const decoratedElements: DecoratedNode<ts.ClassElement>[] = [];
clazz.members.forEach(element => {
if (element.decorators !== undefined) {
const decorators = element.decorators.map(decorator => reflectDecorator(decorator, checker))
.filter(decorator => decorator != null) as Decorator[];
if (decorators.length > 0) {
decoratedElements.push({element, decorators});
}
}
});
return decoratedElements;
}
export function reflectStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers === undefined ||
!member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
}
export function reflectNonStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers !== undefined &&
member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
}
export function reflectTypeEntityToDeclaration(
type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string | null} {
let realSymbol = checker.getSymbolAtLocation(type);
if (realSymbol === undefined) {
throw new Error(`Cannot resolve type entity to symbol`);
}
while (realSymbol.flags & ts.SymbolFlags.Alias) {
realSymbol = checker.getAliasedSymbol(realSymbol);
}
let node: ts.Declaration|null = null;
if (realSymbol.valueDeclaration !== undefined) {
node = realSymbol.valueDeclaration;
} else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) {
node = realSymbol.declarations[0];
} else {
throw new Error(`Cannot resolve type entity symbol to declaration`);
}
if (ts.isQualifiedName(type)) {
if (!ts.isIdentifier(type.left)) {
throw new Error(`Cannot handle qualified name with non-identifier lhs`);
}
const symbol = checker.getSymbolAtLocation(type.left);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
throw new Error(`Cannot resolve qualified type entity lhs to symbol`);
}
const decl = symbol.declarations[0];
if (ts.isNamespaceImport(decl)) {
const clause = decl.parent !;
const importDecl = clause.parent !;
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
throw new Error(`Module specifier is not a string`);
}
return {node, from: importDecl.moduleSpecifier.text};
} else {
throw new Error(`Unknown import type?`);
}
} else {
return {node, from: null};
}
}
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
],
@@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {ResolvedValue, staticallyResolve} from '../src/resolver';
import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver';
function makeSimpleProgram(contents: string): ts.Program {
return makeProgram([{name: 'entry.ts', contents}]).program;
@@ -116,6 +117,62 @@ describe('ngtsc metadata', () => {
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('imports work', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
{
name: 'entry.ts',
contents: `
import {foo} from './second';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof ExternalExpr)) {
return fail('Expected expression reference to be an external (import) expression');
}
expect(reference.value.moduleName).toBe('./second');
expect(reference.value.name).toBe('foo');
});
it('absolute imports work', () => {
const {program} = makeProgram([
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
{
name: 'entry.ts',
contents: `
import {foo} from 'some_library';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof ExternalExpr)) {
return fail('Expected expression reference to be an external (import) expression');
}
expect(reference.value.moduleName).toBe('some_library');
expect(reference.value.name).toBe('foo');
});
it('reads values from default exports', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export default {property: "test"}'},
@@ -130,7 +187,6 @@ describe('ngtsc metadata', () => {
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
debugger;
expect(staticallyResolve(expr, checker)).toEqual('test');
});
+10 -3
View File
@@ -12,8 +12,9 @@ import * as ts from 'typescript';
import * as api from '../transformers/api';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, SelectorScopeRegistry} from './annotations';
import {CompilerHost} from './compiler_host';
import {InjectableCompilerAdapter, IvyCompilation, ivyTransformFactory} from './transform';
import {IvyCompilation, ivyTransformFactory} from './transform';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
@@ -89,10 +90,16 @@ export class NgtscProgram implements api.Program {
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
const checker = this.tsProgram.getTypeChecker();
const scopeRegistry = new SelectorScopeRegistry(checker);
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const adapters = [new InjectableCompilerAdapter(checker)];
const compilation = new IvyCompilation(adapters, checker);
const handlers = [
new ComponentDecoratorHandler(checker, scopeRegistry),
new DirectiveDecoratorHandler(checker, scopeRegistry),
new InjectableDecoratorHandler(checker),
new NgModuleDecoratorHandler(checker, scopeRegistry),
];
const compilation = new IvyCompilation(handlers, checker);
// Analyze every source file in the program.
this.tsProgram.getSourceFiles()
@@ -15,7 +15,10 @@ export function makeProgram(files: {name: string, contents: string}[]):
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 program = ts.createProgram(
rootNames,
{noLib: true, experimentalDecorators: true, moduleResolution: ts.ModuleResolutionKind.NodeJs},
host);
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
throw new Error(
@@ -12,5 +12,6 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/util",
],
)
@@ -6,6 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
export * from './src/api';
export {IvyCompilation} from './src/compilation';
export {InjectableCompilerAdapter} from './src/injectable';
export {ivyTransformFactory} from './src/transform';
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, Type} from '@angular/compiler';
import {Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
@@ -15,13 +15,13 @@ 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
* The decorator compilers in @angular/compiler do not depend on Typescript. The handler 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> {
export interface DecoratorHandler<A> {
/**
* Scan a set of reflected decorators and determine if this adapter is responsible for compilation
* Scan a set of reflected decorators and determine if this handler is responsible for compilation
* of one of them.
*/
detect(decorator: Decorator[]): Decorator|undefined;
@@ -37,7 +37,7 @@ export interface CompilerAdapter<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;
compile(node: ts.ClassDeclaration, analysis: A): CompileResult;
}
/**
@@ -54,8 +54,9 @@ export interface AnalysisOutput<A> {
* 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 {
export interface CompileResult {
field: string;
initializer: Expression;
statements: Statement[];
type: Type;
}
@@ -11,17 +11,18 @@ import * as ts from 'typescript';
import {Decorator, reflectDecorator} from '../../metadata';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
import {AnalysisOutput, CompileResult, DecoratorHandler} 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>;
adapter: DecoratorHandler<T>;
analysis: AnalysisOutput<T>;
decorator: ts.Decorator;
}
@@ -44,7 +45,7 @@ export class IvyCompilation {
*/
private dtsMap = new Map<string, DtsFileTransformer>();
constructor(private adapters: CompilerAdapter<any>[], private checker: ts.TypeChecker) {}
constructor(private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker) {}
/**
* Analyze a source file and produce diagnostics for it (if any).
@@ -60,8 +61,8 @@ export class IvyCompilation {
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 => {
// Look through the DecoratorHandlers to see if any are relevant.
this.handlers.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators);
if (decorator === undefined) {
@@ -101,7 +102,7 @@ export class IvyCompilation {
* 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 {
compileIvyFieldFor(node: ts.ClassDeclaration): CompileResult|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)) {
@@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AddStaticFieldInstruction} from './api';
import {CompileResult} from './api';
import {ImportManager, translateType} from './translator';
@@ -17,15 +17,13 @@ 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 ivyFields = new Map<string, CompileResult>();
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);
}
recordStaticField(name: string, decl: CompileResult): void { this.ivyFields.set(name, decl); }
/**
* Process the .d.ts text for a file and add any declarations which were recorded.
@@ -9,8 +9,10 @@
import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor';
import {IvyCompilation} from './compilation';
import {ImportManager, translateExpression} from './translator';
import {ImportManager, translateExpression, translateStatement} from './translator';
export function ivyTransformFactory(compilation: IvyCompilation):
ts.TransformerFactory<ts.SourceFile> {
@@ -21,6 +23,40 @@ export function ivyTransformFactory(compilation: IvyCompilation):
};
}
class IvyVisitor extends Visitor {
constructor(private compilation: IvyCompilation, private importManager: ImportManager) {
super();
}
visitClassDeclaration(node: ts.ClassDeclaration):
VisitListEntryResult<ts.Statement, 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 = this.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, this.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, this.compilation.ivyDecoratorFor(node) !),
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
[...node.members, property]);
const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager));
return {node, before: statements};
}
return {node};
}
}
/**
* A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
*/
@@ -30,7 +66,7 @@ function transformIvySourceFile(
const importManager = new ImportManager();
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
const sf = visitNode(file);
const sf = visit(file, new IvyVisitor(compilation, importManager), context);
// Generate the import statements to prepend.
const imports = importManager.getAllImports().map(
@@ -44,44 +80,8 @@ function transformIvySourceFile(
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 {
@@ -6,9 +6,28 @@
* 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 {ArrayType, AssertNotNull, BinaryOperator, 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, Statement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
[BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken],
[BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken],
[BinaryOperator.Divide, ts.SyntaxKind.SlashToken],
[BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken],
[BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken],
[BinaryOperator.Lower, ts.SyntaxKind.LessThanToken],
[BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken],
[BinaryOperator.Minus, ts.SyntaxKind.MinusToken],
[BinaryOperator.Modulo, ts.SyntaxKind.PercentToken],
[BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken],
[BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken],
[BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken],
[BinaryOperator.Or, ts.SyntaxKind.BarBarToken],
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
]);
export class ImportManager {
private moduleToIndex = new Map<string, string>();
private nextIndex = 0;
@@ -32,6 +51,10 @@ export function translateExpression(expression: Expression, imports: ImportManag
return expression.visitExpression(new ExpressionTranslatorVisitor(imports), null);
}
export function translateStatement(statement: Statement, imports: ImportManager): ts.Statement {
return statement.visitStatement(new ExpressionTranslatorVisitor(imports), null);
}
export function translateType(type: Type, imports: ImportManager): string {
return type.visitType(new TypeTranslatorVisitor(imports), null);
}
@@ -39,16 +62,23 @@ export function translateType(type: Type, imports: ImportManager): string {
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
constructor(private imports: ImportManager) {}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any) {
throw new Error('Method not implemented.');
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): ts.VariableStatement {
return ts.createVariableStatement(
undefined,
ts.createVariableDeclarationList([ts.createVariableDeclaration(
stmt.name, undefined, stmt.value && stmt.value.visitExpression(this, context))]));
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) {
throw new Error('Method not implemented.');
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): ts.FunctionDeclaration {
return ts.createFunctionDeclaration(
undefined, undefined, undefined, stmt.name, undefined,
stmt.params.map(param => ts.createParameter(undefined, undefined, undefined, param.name)),
undefined,
ts.createBlock(stmt.statements.map(child => child.visitStatement(this, context))));
}
visitExpressionStmt(stmt: ExpressionStatement, context: any) {
throw new Error('Method not implemented.');
visitExpressionStmt(stmt: ExpressionStatement, context: any): ts.ExpressionStatement {
return ts.createStatement(stmt.expr.visitExpression(this, context));
}
visitReturnStmt(stmt: ReturnStatement, context: any): ts.ReturnStatement {
@@ -59,7 +89,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
throw new Error('Method not implemented.');
}
visitIfStmt(stmt: IfStmt, context: any) { throw new Error('Method not implemented.'); }
visitIfStmt(stmt: IfStmt, context: any): ts.IfStatement {
return ts.createIf(
stmt.condition.visitExpression(this, context),
ts.createBlock(stmt.trueCase.map(child => child.visitStatement(this, context))),
stmt.falseCase.length > 0 ?
ts.createBlock(stmt.falseCase.map(child => child.visitStatement(this, context))) :
undefined);
}
visitTryCatchStmt(stmt: TryCatchStmt, context: any) {
throw new Error('Method not implemented.');
@@ -89,12 +126,17 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
throw new Error('Method not implemented.');
}
visitWritePropExpr(expr: WritePropExpr, context: any): never {
throw new Error('Method not implemented.');
visitWritePropExpr(expr: WritePropExpr, context: any): ts.BinaryExpression {
return ts.createBinary(
ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name),
ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context));
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
throw new Error('Method not implemented.');
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): ts.CallExpression {
const target = ast.receiver.visitExpression(this, context);
return ts.createCall(
ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): ts.CallExpression {
@@ -156,20 +198,26 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): never {
throw new Error('Method not implemented.');
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): ts.Expression {
if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
}
const binEx = ts.createBinary(
ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator) !,
ast.rhs.visitExpression(this, context));
return ast.parens ? ts.createParen(binEx) : binEx;
}
visitReadPropExpr(ast: ReadPropExpr, context: any): never {
throw new Error('Method not implemented.');
visitReadPropExpr(ast: ReadPropExpr, context: any): ts.PropertyAccessExpression {
return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name);
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): never {
throw new Error('Method not implemented.');
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): ts.ArrayLiteralExpression {
return ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context)));
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): ts.ObjectLiteralExpression {
@@ -297,8 +345,9 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
visitReadKeyExpr(ast: ReadKeyExpr, context: any) { throw new Error('Method not implemented.'); }
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any) {
throw new Error('Method not implemented.');
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): string {
const values = ast.entries.map(expr => expr.visitExpression(this, context));
return `[${values.join(',')}]`;
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any) {
@@ -33,7 +33,7 @@ export abstract class Visitor {
/**
* Maps statements to an array of statements that should be inserted before them.
*/
private _before = new Map<ts.Statement, ts.Statement[]>();
private _before = new Map<ts.Node, ts.Statement[]>();
/**
* Visit a class declaration, returning at least the transformed declaration and optionally other
@@ -44,16 +44,15 @@ export abstract class Visitor {
return {node};
}
private _visitClassDeclaration(node: ts.ClassDeclaration, context: ts.TransformationContext):
ts.ClassDeclaration {
const result = this.visitClassDeclaration(node);
const visited = ts.visitEachChild(result.node, child => this._visit(child, context), context);
private _visitListEntryNode<T extends ts.Statement>(
node: T, visitor: (node: T) => VisitListEntryResult<ts.Statement, T>): T {
const result = visitor(node);
if (result.before !== undefined) {
// Record that some nodes should be inserted before the given declaration. The declaration's
// parent's _visit call is responsible for performing this insertion.
this._before.set(visited, result.before);
this._before.set(result.node, result.before);
}
return visited;
return result.node;
}
/**
@@ -61,11 +60,6 @@ export abstract class Visitor {
*/
visitOtherNode<T extends ts.Node>(node: T): T { return node; }
private _visitOtherNode<T extends ts.Node>(node: T, context: ts.TransformationContext): T {
return ts.visitEachChild(
this.visitOtherNode(node), child => this._visit(child, context), context);
}
/**
* @internal
*/
@@ -73,10 +67,14 @@ export abstract class Visitor {
// First, visit the node. visitedNode starts off as `null` but should be set after visiting
// is completed.
let visitedNode: T|null = null;
node = ts.visitEachChild(node, child => this._visit(child, context), context) as T;
if (ts.isClassDeclaration(node)) {
visitedNode = this._visitClassDeclaration(node, context) as typeof node;
visitedNode = this._visitListEntryNode(
node, (node: ts.ClassDeclaration) => this.visitClassDeclaration(node)) as typeof node;
} else {
visitedNode = this._visitOtherNode(node, context);
visitedNode = this.visitOtherNode(node);
}
// If the visited node has a `statements` array then process them, maybe replacing the visited
@@ -16,6 +16,8 @@ function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
return null !;
}
export const Component = callableClassDecorator();
export const Directive = callableClassDecorator();
export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator();

Some files were not shown because too many files have changed in this diff Show More