feat(ivy): Add AOT handling for bare classes with Input and Output decorators (#25367)

PR Close #25367
This commit is contained in:
Ben Lesh 2018-08-07 12:04:39 -07:00
parent 26066f282e
commit a0a29fdd27
22 changed files with 483 additions and 60 deletions

View File

@ -9,7 +9,7 @@ import {ConstantPool} from '@angular/compiler';
import * as fs from 'fs'; import * as fs from 'fs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations';
import {Decorator} from '../../ngtsc/host'; import {Decorator} from '../../ngtsc/host';
import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform';
@ -18,8 +18,8 @@ import {ParsedClass} from './parsing/parsed_class';
import {ParsedFile} from './parsing/parsed_file'; import {ParsedFile} from './parsing/parsed_file';
import {isDefined} from './utils'; import {isDefined} from './utils';
export interface AnalyzedClass<T = any> extends ParsedClass { export interface AnalyzedClass<A = any, M = any> extends ParsedClass {
handler: DecoratorHandler<T>; handler: DecoratorHandler<A, M>;
analysis: any; analysis: any;
diagnostics?: ts.Diagnostic[]; diagnostics?: ts.Diagnostic[];
compilation: CompileResult[]; compilation: CompileResult[];
@ -31,9 +31,9 @@ export interface AnalyzedFile {
constantPool: ConstantPool; constantPool: ConstantPool;
} }
export interface MatchingHandler<T> { export interface MatchingHandler<A, M> {
handler: DecoratorHandler<T>; handler: DecoratorHandler<A, M>;
decorator: Decorator; match: M;
} }
/** /**
@ -46,7 +46,8 @@ export class FileResourceLoader implements ResourceLoader {
export class Analyzer { export class Analyzer {
resourceLoader = new FileResourceLoader(); resourceLoader = new FileResourceLoader();
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host); scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host);
handlers: DecoratorHandler<any>[] = [ handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.typeChecker, this.host),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader), this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader),
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
@ -76,20 +77,23 @@ export class Analyzer {
protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass
|undefined { |undefined {
const matchingHandlers = const matchingHandlers = this.handlers
this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)})) .map(handler => ({
.filter(isMatchingHandler); handler,
match: handler.detect(clazz.declaration, clazz.decorators),
}))
.filter(isMatchingHandler);
if (matchingHandlers.length > 1) { if (matchingHandlers.length > 1) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
} }
if (matchingHandlers.length == 0) { if (matchingHandlers.length === 0) {
return undefined; return undefined;
} }
const {handler, decorator} = matchingHandlers[0]; const {handler, match} = matchingHandlers[0];
const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator); const {analysis, diagnostics} = handler.analyze(clazz.declaration, match);
let compilation = handler.compile(clazz.declaration, analysis, pool); let compilation = handler.compile(clazz.declaration, analysis, pool);
if (!Array.isArray(compilation)) { if (!Array.isArray(compilation)) {
compilation = [compilation]; compilation = [compilation];
@ -98,6 +102,7 @@ export class Analyzer {
} }
} }
function isMatchingHandler<T>(handler: Partial<MatchingHandler<T>>): handler is MatchingHandler<T> { function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
return !!handler.decorator; handler is MatchingHandler<A, M> {
} return !!handler.match;
}

View File

@ -28,14 +28,18 @@ const TEST_PROGRAM = {
}; };
function createTestHandler() { function createTestHandler() {
const handler = jasmine.createSpyObj<DecoratorHandler<any>>('TestDecoratorHandler', [ const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
'detect', 'detect',
'analyze', 'analyze',
'compile', 'compile',
]); ]);
// Only detect the Component decorator // Only detect the Component decorator
handler.detect.and.callFake( handler.detect.and.callFake((node: ts.Declaration, decorators: Decorator[]) => {
(decorators: Decorator[]) => decorators.find(d => d.name === 'Component')); if (!decorators) {
return undefined;
}
return decorators.find(d => d.name === 'Component');
});
// The "test" analysis is just the name of the decorator being analyzed // The "test" analysis is just the name of the decorator being analyzed
handler.analyze.and.callFake( handler.analyze.and.callFake(
((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null}))); ((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null})));
@ -69,7 +73,7 @@ function createParsedFile(program: ts.Program) {
describe('Analyzer', () => { describe('Analyzer', () => {
describe('analyzeFile()', () => { describe('analyzeFile()', () => {
let program: ts.Program; let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandler<any>>; let testHandler: jasmine.SpyObj<DecoratorHandler<any, any>>;
let result: AnalyzedFile; let result: AnalyzedFile;
beforeEach(() => { beforeEach(() => {
@ -87,9 +91,9 @@ describe('Analyzer', () => {
it('should call detect on the decorator handlers with each class from the parsed file', () => { it('should call detect on the decorator handlers with each class from the parsed file', () => {
expect(testHandler.detect).toHaveBeenCalledTimes(2); expect(testHandler.detect).toHaveBeenCalledTimes(2);
expect(testHandler.detect.calls.allArgs()[0][0]).toEqual([jasmine.objectContaining( expect(testHandler.detect.calls.allArgs()[0][1]).toEqual([jasmine.objectContaining(
{name: 'Component'})]); {name: 'Component'})]);
expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining( expect(testHandler.detect.calls.allArgs()[1][1]).toEqual([jasmine.objectContaining(
{name: 'Injectable'})]); {name: 'Injectable'})]);
}); });

View File

@ -7,6 +7,7 @@
*/ */
export {ResourceLoader} from './src/api'; export {ResourceLoader} from './src/api';
export {BaseDefDecoratorHandler} from './src/base_def';
export {ComponentDecoratorHandler} from './src/component'; export {ComponentDecoratorHandler} from './src/component';
export {DirectiveDecoratorHandler} from './src/directive'; export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable'; export {InjectableDecoratorHandler} from './src/injectable';

View File

@ -0,0 +1,120 @@
/**
* @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 {R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ClassMember, Decorator, ReflectionHost} from '../../host';
import {staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {isAngularCore} from './util';
function containsNgTopLevelDecorator(decorators: Decorator[] | null): boolean {
if (!decorators) {
return false;
}
return decorators.find(
decorator => (decorator.name === 'Component' || decorator.name === 'Directive' ||
decorator.name === 'NgModule') &&
isAngularCore(decorator)) !== undefined;
}
export class BaseDefDecoratorHandler implements
DecoratorHandler<R3BaseRefMetaData, R3BaseRefDecoratorDetection> {
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost, ) {}
detect(node: ts.ClassDeclaration, decorators: Decorator[]|null): R3BaseRefDecoratorDetection
|undefined {
if (containsNgTopLevelDecorator(decorators)) {
// If the class is already decorated by @Component or @Directive let that
// DecoratorHandler handle this. BaseDef is unnecessary.
return undefined;
}
let result: R3BaseRefDecoratorDetection|undefined = undefined;
this.reflector.getMembersOfClass(node).forEach(property => {
const {decorators} = property;
if (decorators) {
for (const decorator of decorators) {
const decoratorName = decorator.name;
if (decoratorName === 'Input' && isAngularCore(decorator)) {
result = result || {};
const inputs = result.inputs = result.inputs || [];
inputs.push({decorator, property});
} else if (decoratorName === 'Output' && isAngularCore(decorator)) {
result = result || {};
const outputs = result.outputs = result.outputs || [];
outputs.push({decorator, property});
}
}
}
});
return result;
}
analyze(node: ts.ClassDeclaration, metadata: R3BaseRefDecoratorDetection):
AnalysisOutput<R3BaseRefMetaData> {
const analysis: R3BaseRefMetaData = {};
if (metadata.inputs) {
const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]};
metadata.inputs.forEach(({decorator, property}) => {
const propName = property.name;
const args = decorator.args;
let value: string|[string, string];
if (args && args.length > 0) {
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
if (typeof resolvedValue !== 'string') {
throw new TypeError('Input alias does not resolve to a string value');
}
value = [resolvedValue, propName];
} else {
value = propName;
}
inputs[propName] = value;
});
}
if (metadata.outputs) {
const outputs = analysis.outputs = {} as{[key: string]: string};
metadata.outputs.forEach(({decorator, property}) => {
const propName = property.name;
const args = decorator.args;
let value: string;
if (args && args.length > 0) {
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
if (typeof resolvedValue !== 'string') {
throw new TypeError('Output alias does not resolve to a string value');
}
value = resolvedValue;
} else {
value = propName;
}
outputs[propName] = value;
});
}
return {analysis};
}
compile(node: ts.Declaration, analysis: R3BaseRefMetaData): CompileResult[]|CompileResult {
const {expression, type} = compileBaseDefFromMetadata(analysis);
return {
name: 'ngBaseDef',
initializer: expression, type,
statements: [],
};
}
}
export interface R3BaseRefDecoratorDetection {
inputs?: Array<{property: ClassMember, decorator: Decorator}>;
outputs?: Array<{property: ClassMember, decorator: Decorator}>;
}

View File

@ -24,7 +24,7 @@ const EMPTY_MAP = new Map<string, Expression>();
/** /**
* `DecoratorHandler` which handles the `@Component` annotation. * `DecoratorHandler` which handles the `@Component` annotation.
*/ */
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> { export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata, Decorator> {
constructor( constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost, private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean, private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
@ -33,7 +33,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>(); private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
detect(decorators: Decorator[]): Decorator|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find( return decorators.find(
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator))); decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
} }

View File

@ -18,12 +18,15 @@ import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwa
const EMPTY_OBJECT: {[key: string]: string} = {}; const EMPTY_OBJECT: {[key: string]: string} = {};
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> { export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata, Decorator> {
constructor( constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost, private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorators: Decorator[]): Decorator|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find( return decorators.find(
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator))); decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
} }

View File

@ -19,11 +19,15 @@ import {getConstructorDependencies, isAngularCore} from './util';
/** /**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/ */
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> { export class InjectableDecoratorHandler implements
DecoratorHandler<R3InjectableMetadata, Decorator> {
constructor(private reflector: ReflectionHost, private isCore: boolean) {} constructor(private reflector: ReflectionHost, private isCore: boolean) {}
detect(decorator: Decorator[]): Decorator|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
return decorator.find( if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator))); decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
} }

View File

@ -26,12 +26,15 @@ export interface NgModuleAnalysis {
* *
* TODO(alxhub): handle injector side of things as well. * TODO(alxhub): handle injector side of things as well.
*/ */
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> { export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> {
constructor( constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost, private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorators: Decorator[]): Decorator|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find( return decorators.find(
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator))); decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
} }

View File

@ -16,13 +16,16 @@ import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope'; import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util'; import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> { export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata, Decorator> {
constructor( constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost, private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorator: Decorator[]): Decorator|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
return decorator.find( if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator))); decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator)));
} }

View File

@ -13,6 +13,7 @@ import * as ts from 'typescript';
import * as api from '../transformers/api'; import * as api from '../transformers/api';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFactoryTransform} from './factories'; import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFactoryTransform} from './factories';
import {TypeScriptReflectionHost} from './metadata'; import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader'; import {FileResourceLoader, HostResourceLoader} from './resource_loader';
@ -169,6 +170,7 @@ export class NgtscProgram implements api.Program {
// Set up the IvyCompilation, which manages state for the Ivy transformer. // Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [ const handlers = [
new BaseDefDecoratorHandler(checker, this.reflector),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
checker, this.reflector, scopeRegistry, this.isCore, this.resourceLoader), checker, this.reflector, scopeRegistry, this.isCore, this.resourceLoader),
new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),

View File

@ -20,12 +20,12 @@ import {Decorator} from '../../host';
* responsible for extracting the information required to perform compilation from the decorators * responsible for extracting the information required to perform compilation from the decorators
* and Typescript source, invoking the decorator compiler, and returning the result. * and Typescript source, invoking the decorator compiler, and returning the result.
*/ */
export interface DecoratorHandler<A> { export interface DecoratorHandler<A, M> {
/** /**
* Scan a set of reflected decorators and determine if this handler is responsible for compilation * Scan a set of reflected decorators and determine if this handler is responsible for compilation
* of one of them. * of one of them.
*/ */
detect(decorator: Decorator[]): Decorator|undefined; detect(node: ts.Declaration, decorators: Decorator[]|null): M|undefined;
/** /**
@ -34,14 +34,14 @@ export interface DecoratorHandler<A> {
* `preAnalyze` is optional and is not guaranteed to be called through all compilation flows. It * `preAnalyze` is optional and is not guaranteed to be called through all compilation flows. It
* will only be called if asynchronicity is supported in the CompilerHost. * will only be called if asynchronicity is supported in the CompilerHost.
*/ */
preanalyze?(node: ts.Declaration, decorator: Decorator): Promise<void>|undefined; preanalyze?(node: ts.Declaration, metadata: M): Promise<void>|undefined;
/** /**
* Perform analysis on the decorator/class combination, producing instructions for compilation * 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 * if successful, or an array of diagnostic messages if the analysis fails or the decorator
* isn't valid. * isn't valid.
*/ */
analyze(node: ts.Declaration, decorator: Decorator): AnalysisOutput<A>; analyze(node: ts.Declaration, metadata: M): AnalysisOutput<A>;
/** /**
* Generate a description of the field which should be added to the class, including any * Generate a description of the field which should be added to the class, including any

View File

@ -19,10 +19,10 @@ import {DtsFileTransformer} from './declaration';
* Record of an adapter which decided to emit a static field, and the analysis it performed to * Record of an adapter which decided to emit a static field, and the analysis it performed to
* prepare for that operation. * prepare for that operation.
*/ */
interface EmitFieldOperation<T> { interface EmitFieldOperation<A, M> {
adapter: DecoratorHandler<T>; adapter: DecoratorHandler<A, M>;
analysis: AnalysisOutput<T>; analysis: AnalysisOutput<A>;
decorator: Decorator; metadata: M;
} }
/** /**
@ -36,7 +36,7 @@ export class IvyCompilation {
* Tracks classes which have been analyzed and found to have an Ivy decorator, and the * Tracks classes which have been analyzed and found to have an Ivy decorator, and the
* information recorded about them for later compilation. * information recorded about them for later compilation.
*/ */
private analysis = new Map<ts.Declaration, EmitFieldOperation<any>>(); private analysis = new Map<ts.Declaration, EmitFieldOperation<any, any>>();
/** /**
* Tracks factory information which needs to be generated. * Tracks factory information which needs to be generated.
@ -59,7 +59,7 @@ export class IvyCompilation {
* `null` in most cases. * `null` in most cases.
*/ */
constructor( constructor(
private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker, private handlers: DecoratorHandler<any, any>[], private checker: ts.TypeChecker,
private reflector: ReflectionHost, private coreImportsFrom: ts.SourceFile|null, private reflector: ReflectionHost, private coreImportsFrom: ts.SourceFile|null,
private sourceToFactorySymbols: Map<string, Set<string>>|null) {} private sourceToFactorySymbols: Map<string, Set<string>>|null) {}
@ -78,15 +78,14 @@ export class IvyCompilation {
const analyzeClass = (node: ts.Declaration): void => { const analyzeClass = (node: ts.Declaration): void => {
// The first step is to reflect the decorators. // The first step is to reflect the decorators.
const decorators = this.reflector.getDecoratorsOfDeclaration(node); const classDecorators = this.reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
// Look through the DecoratorHandlers to see if any are relevant. // Look through the DecoratorHandlers to see if any are relevant.
this.handlers.forEach(adapter => { this.handlers.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class. // An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators); const metadata = adapter.detect(node, classDecorators);
if (decorator === undefined) { if (metadata === undefined) {
return; return;
} }
@ -97,14 +96,15 @@ export class IvyCompilation {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
} }
// Run analysis on the decorator. This will produce either diagnostics, an // Run analysis on the metadata. This will produce either diagnostics, an
// analysis result, or both. // analysis result, or both.
const analysis = adapter.analyze(node, decorator); const analysis = adapter.analyze(node, metadata);
if (analysis.analysis !== undefined) { if (analysis.analysis !== undefined) {
this.analysis.set(node, { this.analysis.set(node, {
adapter, adapter,
analysis: analysis.analysis, decorator, analysis: analysis.analysis,
metadata: metadata,
}); });
} }
@ -119,7 +119,7 @@ export class IvyCompilation {
}; };
if (preanalyze && adapter.preanalyze !== undefined) { if (preanalyze && adapter.preanalyze !== undefined) {
const preanalysis = adapter.preanalyze(node, decorator); const preanalysis = adapter.preanalyze(node, metadata);
if (preanalysis !== undefined) { if (preanalysis !== undefined) {
promises.push(preanalysis.then(() => completeAnalysis())); promises.push(preanalysis.then(() => completeAnalysis()));
} else { } else {
@ -185,7 +185,7 @@ export class IvyCompilation {
return undefined; return undefined;
} }
return this.analysis.get(original) !.decorator; return this.analysis.get(original) !.metadata;
} }
/** /**

View File

@ -14,7 +14,7 @@ import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics'; import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {compareVersions} from '../diagnostics/typescript_version'; import {compareVersions} from '../diagnostics/typescript_version';
import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index'; import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata';
import {NgtscProgram} from '../ngtsc/program'; 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 {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api';

View File

@ -1837,4 +1837,226 @@ describe('compiler compliance', () => {
}); });
}); });
}); });
describe('inherited bare classes', () => {
it('should add ngBaseDef if one or more @Input is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Input} from '@angular/core';
export class BaseClass {
@Input()
input1 = 'test';
@Input('alias2')
input2 = 'whatever';
}
@Component({
selector: 'my-component',
template: \`<div>{{input1}} {{input2}}</div>\`
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = i0.ɵdefineBase({
inputs: {
input1: "input1",
input2: ["alias2", "input2"]
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if one or more @Output is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Output, EventEmitter} from '@angular/core';
export class BaseClass {
@Output()
output1 = new EventEmitter<string>();
@Output()
output2 = new EventEmitter<string>();
clicked() {
this.output1.emit('test');
this.output2.emit('test');
}
}
@Component({
selector: 'my-component',
template: \`<button (click)="clicked()">Click Me</button>\`
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = i0.ɵdefineBase({
outputs: {
output1: "output1",
output2: "output2"
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a mixture of @Input and @Output props are present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Input, Output, EventEmitter} from '@angular/core';
export class BaseClass {
@Output()
output1 = new EventEmitter<string>();
@Output()
output2 = new EventEmitter<string>();
@Input()
input1 = 'test';
@Input('whatever')
input2 = 'blah';
clicked() {
this.output1.emit('test');
this.output2.emit('test');
}
}
@Component({
selector: 'my-component',
template: \`<button (click)="clicked()">Click Me</button>\`
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = i0.ɵdefineBase({
inputs: {
input1: "input1",
input2: ["whatever", "input2"]
},
outputs: {
output1: "output1",
output2: "output2"
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should NOT add ngBaseDef if @Component is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, Output, EventEmitter} from '@angular/core';
@Component({
selector: 'whatever',
template: '<button (click)="clicked()">Click {{input1}}</button>'
})
export class BaseClass {
@Output()
output1 = new EventEmitter<string>();
@Input()
input1 = 'whatever';
clicked() {
this.output1.emit('test');
}
}
@Component({
selector: 'my-component',
template: \`<div>What is this developer doing?</div>\`
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const result = compile(files, angularFiles);
expect(result.source).not.toContain('ngBaseDef');
});
it('should NOT add ngBaseDef if @Directive is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule, Output, EventEmitter} from '@angular/core';
@Directive({
selector: 'whatever',
})
export class BaseClass {
@Output()
output1 = new EventEmitter<string>();
@Input()
input1 = 'whatever';
clicked() {
this.output1.emit('test');
}
}
@Component({
selector: 'my-component',
template: '<button (click)="clicked()">Click {{input1}}</button>'
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const result = compile(files, angularFiles);
expect(result.source).not.toContain('ngBaseDef');
});
});
}); });

View File

@ -87,5 +87,5 @@ export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler'; export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
export {makeBindingParser, parseTemplate} from './render3/view/template'; export {makeBindingParser, parseTemplate} from './render3/view/template';
export {R3Reference} from './render3/util'; export {R3Reference} from './render3/util';
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler'; export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler';
// This file only reexports content of the `src` folder. Keep it that way. // This file only reexports content of the `src` folder. Keep it that way.

View File

@ -115,6 +115,13 @@ export class Identifiers {
static directiveInject: o.ExternalReference = {name: 'ɵdirectiveInject', moduleName: CORE}; static directiveInject: o.ExternalReference = {name: 'ɵdirectiveInject', moduleName: CORE};
static defineBase: o.ExternalReference = {name: 'ɵdefineBase', moduleName: CORE};
static BaseDef: o.ExternalReference = {
name: 'ɵBaseDef',
moduleName: CORE,
};
static defineComponent: o.ExternalReference = {name: 'ɵdefineComponent', moduleName: CORE}; static defineComponent: o.ExternalReference = {name: 'ɵdefineComponent', moduleName: CORE};
static ComponentDef: o.ExternalReference = { static ComponentDef: o.ExternalReference = {

View File

@ -106,6 +106,42 @@ export function compileDirectiveFromMetadata(
return {expression, type, statements}; return {expression, type, statements};
} }
export interface R3BaseRefMetaData {
inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string};
}
/**
* Compile a base definition for the render3 runtime as defined by {@link R3BaseRefMetadata}
* @param meta the metadata used for compilation.
*/
export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData) {
const definitionMap = new DefinitionMap();
if (meta.inputs) {
const inputs = meta.inputs;
const inputsMap = Object.keys(inputs).map(key => {
const v = inputs[key];
const value = Array.isArray(v) ? o.literalArr(v.map(vx => o.literal(vx))) : o.literal(v);
return {key, value, quoted: false};
});
definitionMap.set('inputs', o.literalMap(inputsMap));
}
if (meta.outputs) {
const outputs = meta.outputs;
const outputsMap = Object.keys(outputs).map(key => {
const value = o.literal(outputs[key]);
return {key, value, quoted: false};
});
definitionMap.set('outputs', o.literalMap(outputsMap));
}
const expression = o.importExpr(R3.defineBase).callFn([definitionMap.toLiteralMap()]);
const type = new o.ExpressionType(o.importExpr(R3.BaseDef));
return {expression, type};
}
/** /**
* Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`. * Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`.
*/ */

View File

@ -8,6 +8,7 @@
// clang-format off // clang-format off
export { export {
defineBase as ɵdefineBase,
defineComponent as ɵdefineComponent, defineComponent as ɵdefineComponent,
defineDirective as ɵdefineDirective, defineDirective as ɵdefineDirective,
definePipe as ɵdefinePipe, definePipe as ɵdefinePipe,
@ -97,6 +98,7 @@ export {
st as ɵst, st as ɵst,
ld as ɵld, ld as ɵld,
Pp as ɵPp, Pp as ɵPp,
BaseDef as ɵBaseDef,
ComponentDef as ɵComponentDef, ComponentDef as ɵComponentDef,
ComponentDefInternal as ɵComponentDefInternal, ComponentDefInternal as ɵComponentDefInternal,
DirectiveDef as ɵDirectiveDef, DirectiveDef as ɵDirectiveDef,

View File

@ -779,6 +779,11 @@ const initializeBaseDef = (target: any): void => {
} }
}; };
/**
* Used to get the minified alias of ngBaseDef
*/
const NG_BASE_DEF = Object.keys({ngBaseDef: true})[0];
/** /**
* Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators. * Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators.
* @param key "inputs" or "outputs" * @param key "inputs" or "outputs"
@ -787,7 +792,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any
(target: any, name: string, ...args: any[]) => { (target: any, name: string, ...args: any[]) => {
const constructor = target.constructor; const constructor = target.constructor;
if (!constructor.hasOwnProperty('ngBaseDef')) { if (!constructor.hasOwnProperty(NG_BASE_DEF)) {
initializeBaseDef(target); initializeBaseDef(target);
} }

View File

@ -11,7 +11,7 @@ import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe
import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {PublicFeature} from './features/public_feature'; import {PublicFeature} from './features/public_feature';
import {ComponentDef, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition'; import {BaseDef, ComponentDef, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition';
export {ComponentFactory, ComponentFactoryResolver, ComponentRef, WRAP_RENDERER_FACTORY2} from './component_ref'; export {ComponentFactory, ComponentFactoryResolver, ComponentRef, WRAP_RENDERER_FACTORY2} from './component_ref';
export {Render3DebugRendererFactory2} from './debug'; export {Render3DebugRendererFactory2} from './debug';
@ -152,6 +152,7 @@ export {
// clang-format on // clang-format on
export { export {
BaseDef,
ComponentDef, ComponentDef,
ComponentDefInternal, ComponentDefInternal,
ComponentTemplate, ComponentTemplate,

View File

@ -18,6 +18,7 @@ import * as sanitization from '../../sanitization/sanitization';
* This should be kept up to date with the public exports of @angular/core. * This should be kept up to date with the public exports of @angular/core.
*/ */
export const angularCoreEnv: {[name: string]: Function} = { export const angularCoreEnv: {[name: string]: Function} = {
'ɵdefineBase': r3.defineBase,
'ɵdefineComponent': r3.defineComponent, 'ɵdefineComponent': r3.defineComponent,
'ɵdefineDirective': r3.defineDirective, 'ɵdefineDirective': r3.defineDirective,
'defineInjectable': defineInjectable, 'defineInjectable': defineInjectable,

View File

@ -12,6 +12,7 @@ import {Identifiers} from '@angular/compiler/src/render3/r3_identifiers';
import {angularCoreEnv} from '../../src/render3/jit/environment'; import {angularCoreEnv} from '../../src/render3/jit/environment';
const INTERFACE_EXCEPTIONS = new Set<string>([ const INTERFACE_EXCEPTIONS = new Set<string>([
'ɵBaseDef',
'ɵComponentDef', 'ɵComponentDef',
'ɵDirectiveDef', 'ɵDirectiveDef',
'ɵInjectorDef', 'ɵInjectorDef',