feat(ivy): emit metadata along with all Angular types (#26860)

This commit causes a call to setClassMetadata() to be emitted for every
type being compiled by ngtsc (every Angular type). With this metadata,
the TestBed should be able to recompile these classes when overriding
decorator information.

Testing strategy: Tests in the previous commit for
generateSetClassMetadataCall() verify that the metadata as generated is
correct. This commit enables the generation for each DecoratorHandler,
and a test is added to ngtsc_spec to verify all decorated types have
metadata generated for them.

PR Close #26860
This commit is contained in:
Alex Rickabaugh 2018-10-30 11:19:10 -07:00 committed by Matias Niemelä
parent 492576114d
commit 8634d0bcd8
8 changed files with 85 additions and 21 deletions

View File

@ -127,8 +127,11 @@ describe('Renderer', () => {
decorators: [jasmine.objectContaining({name: 'Directive'})], decorators: [jasmine.objectContaining({name: 'Directive'})],
})); }));
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toEqual( .toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
`A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });`); type: Directive,
args: [{ selector: '[a]' }]
}], null, { foo: [] });
A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });`);
}); });
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool, CssSelector, Expression, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; import {ConstantPool, CssSelector, Expression, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -18,6 +18,7 @@ import {TypeCheckContext, TypeCheckableDirectiveMeta} from '../../typecheck';
import {ResourceLoader} from './api'; import {ResourceLoader} from './api';
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive'; import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
import {generateSetClassMetadataCall} from './metadata';
import {ScopeDirective, SelectorScopeRegistry} from './selector_scope'; import {ScopeDirective, SelectorScopeRegistry} from './selector_scope';
import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util'; import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util';
@ -26,6 +27,7 @@ const EMPTY_MAP = new Map<string, Expression>();
export interface ComponentHandlerData { export interface ComponentHandlerData {
meta: R3ComponentMetadata; meta: R3ComponentMetadata;
parsedTemplate: TmplAstNode[]; parsedTemplate: TmplAstNode[];
metadataStmt: Statement|null;
} }
/** /**
@ -208,6 +210,7 @@ export class ComponentDecoratorHandler implements
animations, animations,
viewProviders viewProviders
}, },
metadataStmt: generateSetClassMetadataCall(node, this.reflector, this.isCore),
parsedTemplate: template.nodes, parsedTemplate: template.nodes,
}, },
typeCheck: true, typeCheck: true,
@ -242,10 +245,14 @@ export class ComponentDecoratorHandler implements
} }
const res = compileComponentFromMetadata(metadata, pool, makeBindingParser()); const res = compileComponentFromMetadata(metadata, pool, makeBindingParser());
const statements = res.statements;
if (analysis.metadataStmt !== null) {
statements.push(analysis.metadataStmt);
}
return { return {
name: 'ngComponentDef', name: 'ngComponentDef',
initializer: res.expression, initializer: res.expression, statements,
statements: res.statements,
type: res.type, type: res.type,
}; };
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings} from '@angular/compiler'; import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, Statement, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -14,12 +14,16 @@ import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '.
import {Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {SelectorScopeRegistry} from './selector_scope'; import {SelectorScopeRegistry} from './selector_scope';
import {extractDirectiveGuards, getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util'; import {extractDirectiveGuards, getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {}; const EMPTY_OBJECT: {[key: string]: string} = {};
export interface DirectiveHandlerData { meta: R3DirectiveMetadata; } export interface DirectiveHandlerData {
meta: R3DirectiveMetadata;
metadataStmt: Statement|null;
}
export class DirectiveDecoratorHandler implements export class DirectiveDecoratorHandler implements
DecoratorHandler<DirectiveHandlerData, Decorator> { DecoratorHandler<DirectiveHandlerData, Decorator> {
constructor( constructor(
@ -63,6 +67,7 @@ export class DirectiveDecoratorHandler implements
return { return {
analysis: { analysis: {
meta: analysis, meta: analysis,
metadataStmt: generateSetClassMetadataCall(node, this.reflector, this.isCore),
} }
}; };
} }
@ -70,10 +75,14 @@ export class DirectiveDecoratorHandler implements
compile(node: ts.ClassDeclaration, analysis: DirectiveHandlerData, pool: ConstantPool): compile(node: ts.ClassDeclaration, analysis: DirectiveHandlerData, pool: ConstantPool):
CompileResult { CompileResult {
const res = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser()); const res = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser());
const statements = res.statements;
if (analysis.metadataStmt !== null) {
statements.push(analysis.metadataStmt);
}
return { return {
name: 'ngDirectiveDef', name: 'ngDirectiveDef',
initializer: res.expression, initializer: res.expression,
statements: res.statements, statements: statements,
type: res.type, type: res.type,
}; };
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, Statement, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -14,9 +14,13 @@ import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral} from '../../metadata'; import {reflectObjectLiteral} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {getConstructorDependencies, isAngularCore} from './util'; import {getConstructorDependencies, isAngularCore} from './util';
export interface InjectableHandlerData { meta: R3InjectableMetadata; } export interface InjectableHandlerData {
meta: R3InjectableMetadata;
metadataStmt: Statement|null;
}
/** /**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
@ -37,16 +41,20 @@ export class InjectableDecoratorHandler implements
return { return {
analysis: { analysis: {
meta: extractInjectableMetadata(node, decorator, this.reflector, this.isCore), meta: extractInjectableMetadata(node, decorator, this.reflector, this.isCore),
metadataStmt: generateSetClassMetadataCall(node, this.reflector, this.isCore),
}, },
}; };
} }
compile(node: ts.ClassDeclaration, analysis: InjectableHandlerData): CompileResult { compile(node: ts.ClassDeclaration, analysis: InjectableHandlerData): CompileResult {
const res = compileIvyInjectable(analysis.meta); const res = compileIvyInjectable(analysis.meta);
const statements = res.statements;
if (analysis.metadataStmt !== null) {
statements.push(analysis.metadataStmt);
}
return { return {
name: 'ngInjectableDef', name: 'ngInjectableDef',
initializer: res.expression, initializer: res.expression, statements,
statements: res.statements,
type: res.type, type: res.type,
}; };
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool, Expression, LiteralArrayExpr, R3DirectiveMetadata, R3InjectorMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileInjector, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; import {ConstantPool, Expression, LiteralArrayExpr, R3DirectiveMetadata, R3InjectorMetadata, R3NgModuleMetadata, Statement, WrappedNodeExpr, compileInjector, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -14,12 +14,14 @@ import {Decorator, ReflectionHost} from '../../host';
import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {SelectorScopeRegistry} from './selector_scope'; import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util'; import {getConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util';
export interface NgModuleAnalysis { export interface NgModuleAnalysis {
ngModuleDef: R3NgModuleMetadata; ngModuleDef: R3NgModuleMetadata;
ngInjectorDef: R3InjectorMetadata; ngInjectorDef: R3InjectorMetadata;
metadataStmt: Statement|null;
} }
/** /**
@ -130,7 +132,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
return { return {
analysis: { analysis: {
ngModuleDef, ngInjectorDef, ngModuleDef,
ngInjectorDef,
metadataStmt: generateSetClassMetadataCall(node, this.reflector, this.isCore),
}, },
factorySymbolName: node.name !== undefined ? node.name.text : undefined, factorySymbolName: node.name !== undefined ? node.name.text : undefined,
}; };
@ -139,17 +143,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
compile(node: ts.ClassDeclaration, analysis: NgModuleAnalysis): CompileResult[] { compile(node: ts.ClassDeclaration, analysis: NgModuleAnalysis): CompileResult[] {
const ngInjectorDef = compileInjector(analysis.ngInjectorDef); const ngInjectorDef = compileInjector(analysis.ngInjectorDef);
const ngModuleDef = compileNgModule(analysis.ngModuleDef); const ngModuleDef = compileNgModule(analysis.ngModuleDef);
const ngModuleStatements = ngModuleDef.additionalStatements;
if (analysis.metadataStmt !== null) {
ngModuleStatements.push(analysis.metadataStmt);
}
return [ return [
{ {
name: 'ngModuleDef', name: 'ngModuleDef',
initializer: ngModuleDef.expression, initializer: ngModuleDef.expression,
statements: [], statements: ngModuleStatements,
type: ngModuleDef.type, type: ngModuleDef.type,
}, },
{ {
name: 'ngInjectorDef', name: 'ngInjectorDef',
initializer: ngInjectorDef.expression, initializer: ngInjectorDef.expression,
statements: [], statements: ngInjectorDef.statements,
type: ngInjectorDef.type, type: ngInjectorDef.type,
}, },
]; ];

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {LiteralExpr, R3PipeMetadata, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler'; import {LiteralExpr, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -14,10 +14,14 @@ import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {SelectorScopeRegistry} from './selector_scope'; import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util'; import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
export interface PipeHandlerData { meta: R3PipeMetadata; } export interface PipeHandlerData {
meta: R3PipeMetadata;
metadataStmt: Statement|null;
}
export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, Decorator> { export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, Decorator> {
constructor( constructor(
@ -85,16 +89,20 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
pipeName, pipeName,
deps: getConstructorDependencies(clazz, this.reflector, this.isCore), pure, deps: getConstructorDependencies(clazz, this.reflector, this.isCore), pure,
}, },
metadataStmt: generateSetClassMetadataCall(clazz, this.reflector, this.isCore),
}, },
}; };
} }
compile(node: ts.ClassDeclaration, analysis: PipeHandlerData): CompileResult { compile(node: ts.ClassDeclaration, analysis: PipeHandlerData): CompileResult {
const res = compilePipeFromMetadata(analysis.meta); const res = compilePipeFromMetadata(analysis.meta);
const statements = res.statements;
if (analysis.metadataStmt !== null) {
statements.push(analysis.metadataStmt);
}
return { return {
name: 'ngPipeDef', name: 'ngPipeDef',
initializer: res.expression, initializer: res.expression, statements,
statements: [],
type: res.type, type: res.type,
}; };
} }

View File

@ -748,4 +748,25 @@ describe('ngtsc behavioral tests', () => {
const jsContents = env.getContents('test.js'); const jsContents = env.getContents('test.js');
expect(jsContents).toContain('directives: function () { return [CmpB]; }'); expect(jsContents).toContain('directives: function () { return [CmpB]; }');
}); });
it('should emit setClassMetadata calls for all types', () => {
env.tsconfig();
env.write('test.ts', `
import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core';
@Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {}
@Directive({selector: 'dir'}) class TestDirective {}
@Injectable() class TestInjectable {}
@NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {}
@Pipe({name: 'pipe'}) class TestPipe {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('ɵsetClassMetadata(TestComponent, ');
expect(jsContents).toContain('ɵsetClassMetadata(TestDirective, ');
expect(jsContents).toContain('ɵsetClassMetadata(TestInjectable, ');
expect(jsContents).toContain('ɵsetClassMetadata(TestNgModule, ');
expect(jsContents).toContain('ɵsetClassMetadata(TestPipe, ');
});
}); });

View File

@ -231,7 +231,7 @@
"name": "AttrAst" "name": "AttrAst"
}, },
{ {
"name": "Attribute" "name": "Attribute$1"
}, },
{ {
"name": "B64_DIGITS" "name": "B64_DIGITS"