From 9df766095b002ae5be7966762859f312dbb7b804 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 7 Aug 2018 12:04:39 -0700 Subject: [PATCH 1/3] feat(ivy): Add AOT handling for bare classes with Input and Output decorators --- .../compiler-cli/src/ngcc/src/analyzer.ts | 24 +- .../src/ngcc/test/analyzer_spec.ts | 16 +- .../src/ngtsc/annotations/src/base_def.ts | 107 +++++++++ .../src/ngtsc/annotations/src/component.ts | 7 +- .../src/ngtsc/annotations/src/directive.ts | 7 +- .../src/ngtsc/annotations/src/injectable.ts | 10 +- .../src/ngtsc/annotations/src/ng_module.ts | 7 +- .../src/ngtsc/annotations/src/pipe.ts | 9 +- packages/compiler-cli/src/ngtsc/program.ts | 2 + .../src/ngtsc/transform/src/api.ts | 8 +- .../src/ngtsc/transform/src/compilation.ts | 30 +-- .../compiler-cli/src/transformers/program.ts | 2 +- .../compliance/r3_compiler_compliance_spec.ts | 223 ++++++++++++++++++ packages/compiler/src/compiler.ts | 4 +- .../compiler/src/render3/r3_identifiers.ts | 7 + .../compiler/src/render3/view/compiler.ts | 36 +++ .../core/src/core_render3_private_export.ts | 2 + packages/core/src/render3/index.ts | 3 +- packages/core/src/render3/jit/environment.ts | 1 + .../core/test/render3/jit_environment_spec.ts | 1 + 20 files changed, 456 insertions(+), 50 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts index ed7b678efba0..30423bda7c34 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -10,6 +10,7 @@ import * as fs from 'fs'; import * as ts from 'typescript'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; +import {BaseDefDecoratorHandler} from '../../ngtsc/annotations/src/base_def'; import {Decorator} from '../../ngtsc/host'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; @@ -18,8 +19,8 @@ import {ParsedClass} from './parsing/parsed_class'; import {ParsedFile} from './parsing/parsed_file'; import {isDefined} from './utils'; -export interface AnalyzedClass extends ParsedClass { - handler: DecoratorHandler; +export interface AnalyzedClass extends ParsedClass { + handler: DecoratorHandler; analysis: any; diagnostics?: ts.Diagnostic[]; compilation: CompileResult[]; @@ -31,8 +32,8 @@ export interface AnalyzedFile { constantPool: ConstantPool; } -export interface MatchingHandler { - handler: DecoratorHandler; +export interface MatchingHandler { + handler: DecoratorHandler; decorator: Decorator; } @@ -46,7 +47,8 @@ export class FileResourceLoader implements ResourceLoader { export class Analyzer { resourceLoader = new FileResourceLoader(); scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host); - handlers: DecoratorHandler[] = [ + handlers: DecoratorHandler[] = [ + new BaseDefDecoratorHandler(this.typeChecker, this.host), new ComponentDecoratorHandler( this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader), new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), @@ -77,14 +79,17 @@ export class Analyzer { protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass |undefined { const matchingHandlers = - this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)})) + this.handlers + .map( + handler => + ({handler, decorator: handler.detect(clazz.declaration, clazz.decorators)})) .filter(isMatchingHandler); if (matchingHandlers.length > 1) { throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); } - if (matchingHandlers.length == 0) { + if (matchingHandlers.length === 0) { return undefined; } @@ -98,6 +103,7 @@ export class Analyzer { } } -function isMatchingHandler(handler: Partial>): handler is MatchingHandler { +function isMatchingHandler(handler: Partial>): + handler is MatchingHandler { return !!handler.decorator; -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts index 45bb22a44d90..96fc978071b4 100644 --- a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts @@ -28,14 +28,18 @@ const TEST_PROGRAM = { }; function createTestHandler() { - const handler = jasmine.createSpyObj>('TestDecoratorHandler', [ + const handler = jasmine.createSpyObj>('TestDecoratorHandler', [ 'detect', 'analyze', 'compile', ]); // Only detect the Component decorator - handler.detect.and.callFake( - (decorators: Decorator[]) => decorators.find(d => d.name === 'Component')); + handler.detect.and.callFake((node: ts.Declaration, decorators: Decorator[]) => { + if (!decorators) { + return undefined; + } + return decorators.find(d => d.name === 'Component'); + }); // The "test" analysis is just the name of the decorator being analyzed handler.analyze.and.callFake( ((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null}))); @@ -69,7 +73,7 @@ function createParsedFile(program: ts.Program) { describe('Analyzer', () => { describe('analyzeFile()', () => { let program: ts.Program; - let testHandler: jasmine.SpyObj>; + let testHandler: jasmine.SpyObj>; let result: AnalyzedFile; beforeEach(() => { @@ -87,9 +91,9 @@ describe('Analyzer', () => { it('should call detect on the decorator handlers with each class from the parsed file', () => { 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'})]); - expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining( + expect(testHandler.detect.calls.allArgs()[1][1]).toEqual([jasmine.objectContaining( {name: 'Injectable'})]); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts new file mode 100644 index 000000000000..a2a8cf535459 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts @@ -0,0 +1,107 @@ +/** + * @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 { + 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 + // decorator 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 { + 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; + const value: string|[string, string] = args && args.length >= 1 ? + [staticallyResolve(args[0], this.reflector, this.checker) as string, propName] : + 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; + const value = args && args.length >= 1 ? + staticallyResolve(args[0], this.reflector, this.checker) as string : + 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}>; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 15b196759565..17c88dd1ae57 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -24,7 +24,7 @@ const EMPTY_MAP = new Map(); /** * `DecoratorHandler` which handles the `@Component` annotation. */ -export class ComponentDecoratorHandler implements DecoratorHandler { +export class ComponentDecoratorHandler implements DecoratorHandler { constructor( private checker: ts.TypeChecker, private reflector: ReflectionHost, private scopeRegistry: SelectorScopeRegistry, private isCore: boolean, @@ -33,7 +33,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler(); - detect(decorators: Decorator[]): Decorator|undefined { + detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined { + if (!decorators) { + return undefined; + } return decorators.find( decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator))); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 2aebbf94a684..ce32ab2feb30 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -18,12 +18,15 @@ import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwa const EMPTY_OBJECT: {[key: string]: string} = {}; -export class DirectiveDecoratorHandler implements DecoratorHandler { +export class DirectiveDecoratorHandler implements DecoratorHandler { constructor( private checker: ts.TypeChecker, private reflector: ReflectionHost, 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( decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator))); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index fceb6ecfa06f..65711ed3f3c1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -19,11 +19,15 @@ import {getConstructorDependencies, isAngularCore} from './util'; /** * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ -export class InjectableDecoratorHandler implements DecoratorHandler { +export class InjectableDecoratorHandler implements + DecoratorHandler { constructor(private reflector: ReflectionHost, private isCore: boolean) {} - detect(decorator: Decorator[]): Decorator|undefined { - return decorator.find( + detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined { + if (!decorators) { + return undefined; + } + return decorators.find( decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator))); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 0edd97f81755..de80ac54b42d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -26,12 +26,15 @@ export interface NgModuleAnalysis { * * TODO(alxhub): handle injector side of things as well. */ -export class NgModuleDecoratorHandler implements DecoratorHandler { +export class NgModuleDecoratorHandler implements DecoratorHandler { constructor( private checker: ts.TypeChecker, private reflector: ReflectionHost, 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( decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator))); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index eabff8452d57..201d7688a5f0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -16,13 +16,16 @@ import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {SelectorScopeRegistry} from './selector_scope'; import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util'; -export class PipeDecoratorHandler implements DecoratorHandler { +export class PipeDecoratorHandler implements DecoratorHandler { constructor( private checker: ts.TypeChecker, private reflector: ReflectionHost, private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} - detect(decorator: Decorator[]): Decorator|undefined { - return decorator.find( + detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined { + if (!decorators) { + return undefined; + } + return decorators.find( decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator))); } diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 401291e6377c..79901cf2be30 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -13,6 +13,7 @@ import * as ts from 'typescript'; import * as api from '../transformers/api'; 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 {TypeScriptReflectionHost} from './metadata'; 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. const handlers = [ + new BaseDefDecoratorHandler(checker, this.reflector), new ComponentDecoratorHandler( checker, this.reflector, scopeRegistry, this.isCore, this.resourceLoader), new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index b2f188294fe7..e4d995583c9e 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -20,12 +20,12 @@ import {Decorator} from '../../host'; * 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 DecoratorHandler { +export interface DecoratorHandler { /** * Scan a set of reflected decorators and determine if this handler is responsible for compilation * 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 { * `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. */ - preanalyze?(node: ts.Declaration, decorator: Decorator): Promise|undefined; + preanalyze?(node: ts.Declaration, metadata: M): Promise|undefined; /** * Perform analysis on the decorator/class combination, producing instructions for compilation * if successful, or an array of diagnostic messages if the analysis fails or the decorator * isn't valid. */ - analyze(node: ts.Declaration, decorator: Decorator): AnalysisOutput; + analyze(node: ts.Declaration, metadata: M): AnalysisOutput; /** * Generate a description of the field which should be added to the class, including any diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 0dc0d4484304..4d9486a3f8f3 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -19,9 +19,9 @@ import {DtsFileTransformer} from './declaration'; * Record of an adapter which decided to emit a static field, and the analysis it performed to * prepare for that operation. */ -interface EmitFieldOperation { - adapter: DecoratorHandler; - analysis: AnalysisOutput; +interface EmitFieldOperation { + adapter: DecoratorHandler; + analysis: AnalysisOutput; decorator: Decorator; } @@ -36,7 +36,7 @@ export class IvyCompilation { * Tracks classes which have been analyzed and found to have an Ivy decorator, and the * information recorded about them for later compilation. */ - private analysis = new Map>(); + private analysis = new Map>(); /** * Tracks factory information which needs to be generated. @@ -59,7 +59,7 @@ export class IvyCompilation { * `null` in most cases. */ constructor( - private handlers: DecoratorHandler[], private checker: ts.TypeChecker, + private handlers: DecoratorHandler[], private checker: ts.TypeChecker, private reflector: ReflectionHost, private coreImportsFrom: ts.SourceFile|null, private sourceToFactorySymbols: Map>|null) {} @@ -78,15 +78,14 @@ export class IvyCompilation { const analyzeClass = (node: ts.Declaration): void => { // The first step is to reflect the decorators. - const decorators = this.reflector.getDecoratorsOfDeclaration(node); - if (decorators === null) { - return; - } + const classDecorators = this.reflector.getDecoratorsOfDeclaration(node); + // 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) { + const metadata = adapter.detect(node, classDecorators); + if (metadata === undefined) { return; } @@ -97,14 +96,15 @@ export class IvyCompilation { 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. - const analysis = adapter.analyze(node, decorator); + const analysis = adapter.analyze(node, metadata); if (analysis.analysis !== undefined) { this.analysis.set(node, { adapter, - analysis: analysis.analysis, decorator, + analysis: analysis.analysis, + decorator: metadata, }); } @@ -119,7 +119,7 @@ export class IvyCompilation { }; if (preanalyze && adapter.preanalyze !== undefined) { - const preanalysis = adapter.preanalyze(node, decorator); + const preanalysis = adapter.preanalyze(node, metadata); if (preanalysis !== undefined) { promises.push(preanalysis.then(() => completeAnalysis())); } else { diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index d5de23392413..1df3d1b6dcd3 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -14,7 +14,7 @@ import * as ts from 'typescript'; import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics'; import {compareVersions} from '../diagnostics/typescript_version'; -import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index'; +import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata'; import {NgtscProgram} from '../ngtsc/program'; import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api'; diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 6d7fedc66c90..36ef90abb309 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1837,4 +1837,227 @@ 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: \`
{{input1}} {{input2}}
\` + }) + 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(); + + @Output() + output2 = new EventEmitter(); + + clicked() { + this.output1.emit('test'); + this.output2.emit('test'); + } + } + + @Component({ + selector: 'my-component', + template: \`\` + }) + 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(); + + @Output() + output2 = new EventEmitter(); + + @Input() + input1 = 'test'; + + @Input('whatever') + input2 = 'blah'; + + clicked() { + this.output1.emit('test'); + this.output2.emit('test'); + } + } + + @Component({ + selector: 'my-component', + template: \`\` + }) + 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); + debugger; + 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: '' + }) + export class BaseClass { + @Output() + output1 = new EventEmitter(); + + @Input() + input1 = 'whatever'; + + clicked() { + this.output1.emit('test'); + } + } + + @Component({ + selector: 'my-component', + template: \`
What is this developer doing?
\` + }) + export class MyComponent extends BaseClass { + } + + @NgModule({ + declarations: [MyComponent] + }) + export class MyModule {} + ` + } + }; + const result = compile(files, angularFiles); + expect(result.source.indexOf('ngBaseDef')).toBe(-1); + }); + + 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(); + + @Input() + input1 = 'whatever'; + + clicked() { + this.output1.emit('test'); + } + } + + @Component({ + selector: 'my-component', + template: '' + }) + export class MyComponent extends BaseClass { + } + + @NgModule({ + declarations: [MyComponent] + }) + export class MyModule {} + ` + } + }; + const result = compile(files, angularFiles); + expect(result.source.indexOf('ngBaseDef')).toBe(-1); + }); + }); }); diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index ca762310afa7..75d0e561f7f6 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -87,5 +87,5 @@ export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler'; export {makeBindingParser, parseTemplate} from './render3/view/template'; export {R3Reference} from './render3/util'; -export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler'; -// This file only reexports content of the `src` folder. Keep it that way. \ No newline at end of file +export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler'; +// This file only reexports content of the `src` folder. Keep it that way. diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 7a96109e62e7..b18f49bd990e 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -115,6 +115,13 @@ export class Identifiers { 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 ComponentDef: o.ExternalReference = { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index b8a1c6f9e1a5..950b3669642d 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -106,6 +106,42 @@ export function compileDirectiveFromMetadata( 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`. */ diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 53a1cf3c5e92..72c7085e4998 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -8,6 +8,7 @@ // clang-format off export { + defineBase as ɵdefineBase, defineComponent as ɵdefineComponent, defineDirective as ɵdefineDirective, definePipe as ɵdefinePipe, @@ -97,6 +98,7 @@ export { st as ɵst, ld as ɵld, Pp as ɵPp, + BaseDef as ɵBaseDef, ComponentDef as ɵComponentDef, ComponentDefInternal as ɵComponentDefInternal, DirectiveDef as ɵDirectiveDef, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 59ac8becd1d2..114bf8224cb4 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -11,7 +11,7 @@ import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {NgOnChangesFeature} from './features/ng_onchanges_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 {Render3DebugRendererFactory2} from './debug'; @@ -152,6 +152,7 @@ export { // clang-format on export { + BaseDef, ComponentDef, ComponentDefInternal, ComponentTemplate, diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index f35b926effaf..00dbe0d34875 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -18,6 +18,7 @@ import * as sanitization from '../../sanitization/sanitization'; * This should be kept up to date with the public exports of @angular/core. */ export const angularCoreEnv: {[name: string]: Function} = { + 'ɵdefineBase': r3.defineBase, 'ɵdefineComponent': r3.defineComponent, 'ɵdefineDirective': r3.defineDirective, 'defineInjectable': defineInjectable, diff --git a/packages/core/test/render3/jit_environment_spec.ts b/packages/core/test/render3/jit_environment_spec.ts index 134970bdd6a6..04ff0d11e9a6 100644 --- a/packages/core/test/render3/jit_environment_spec.ts +++ b/packages/core/test/render3/jit_environment_spec.ts @@ -12,6 +12,7 @@ import {Identifiers} from '@angular/compiler/src/render3/r3_identifiers'; import {angularCoreEnv} from '../../src/render3/jit/environment'; const INTERFACE_EXCEPTIONS = new Set([ + 'ɵBaseDef', 'ɵComponentDef', 'ɵDirectiveDef', 'ɵInjectorDef', From 0abdcbdd1d77f52065a1b5fe6c6f70af31dc83b7 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 9 Aug 2018 09:16:58 -0700 Subject: [PATCH 2/3] fixup! feat(ivy): Add AOT handling for bare classes with Input and Output decorators --- .../compiler-cli/src/ngcc/src/analyzer.ts | 14 ++++----- .../src/ngtsc/annotations/index.ts | 1 + .../src/ngtsc/annotations/src/base_def.ts | 29 ++++++++++++++----- .../src/ngtsc/transform/src/compilation.ts | 6 ++-- .../compliance/r3_compiler_compliance_spec.ts | 5 ++-- packages/core/src/metadata/directives.ts | 7 ++++- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts index 30423bda7c34..dfc72254b506 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -9,8 +9,7 @@ import {ConstantPool} from '@angular/compiler'; import * as fs from 'fs'; import * as ts from 'typescript'; -import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; -import {BaseDefDecoratorHandler} from '../../ngtsc/annotations/src/base_def'; +import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; import {Decorator} from '../../ngtsc/host'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; @@ -34,7 +33,7 @@ export interface AnalyzedFile { export interface MatchingHandler { handler: DecoratorHandler; - decorator: Decorator; + match: M; } /** @@ -80,9 +79,10 @@ export class Analyzer { |undefined { const matchingHandlers = this.handlers - .map( - handler => - ({handler, decorator: handler.detect(clazz.declaration, clazz.decorators)})) + .map(handler => ({ + handler, + decorator: handler.detect(clazz.declaration, clazz.decorators), + })) .filter(isMatchingHandler); if (matchingHandlers.length > 1) { @@ -105,5 +105,5 @@ export class Analyzer { function isMatchingHandler(handler: Partial>): handler is MatchingHandler { - return !!handler.decorator; + return !!handler.match; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/index.ts b/packages/compiler-cli/src/ngtsc/annotations/index.ts index 3beccc7aa8a6..77a4860842a1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/index.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/index.ts @@ -7,6 +7,7 @@ */ export {ResourceLoader} from './src/api'; +export {BaseDefDecoratorHandler} from './src/base_def'; export {ComponentDecoratorHandler} from './src/component'; export {DirectiveDecoratorHandler} from './src/directive'; export {InjectableDecoratorHandler} from './src/injectable'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts index a2a8cf535459..5924bde1e23a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts @@ -32,7 +32,7 @@ export class BaseDefDecoratorHandler implements |undefined { if (containsNgTopLevelDecorator(decorators)) { // If the class is already decorated by @Component or @Directive let that - // decorator handle this. BaseDef is unnecessary. + // DecoratorHandler handle this. BaseDef is unnecessary. return undefined; } @@ -67,10 +67,16 @@ export class BaseDefDecoratorHandler implements metadata.inputs.forEach(({decorator, property}) => { const propName = property.name; const args = decorator.args; - const value: string|[string, string] = args && args.length >= 1 ? - [staticallyResolve(args[0], this.reflector, this.checker) as string, propName] : - propName; - + 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; }); } @@ -80,9 +86,16 @@ export class BaseDefDecoratorHandler implements metadata.outputs.forEach(({decorator, property}) => { const propName = property.name; const args = decorator.args; - const value = args && args.length >= 1 ? - staticallyResolve(args[0], this.reflector, this.checker) as string : - propName; + 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; }); } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 4d9486a3f8f3..6789d0e807a9 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -22,7 +22,7 @@ import {DtsFileTransformer} from './declaration'; interface EmitFieldOperation { adapter: DecoratorHandler; analysis: AnalysisOutput
; - decorator: Decorator; + metadata: M; } /** @@ -104,7 +104,7 @@ export class IvyCompilation { this.analysis.set(node, { adapter, analysis: analysis.analysis, - decorator: metadata, + metadata: metadata, }); } @@ -185,7 +185,7 @@ export class IvyCompilation { return undefined; } - return this.analysis.get(original) !.decorator; + return this.analysis.get(original) !.metadata; } /** diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 36ef90abb309..4d87cbe64988 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1979,7 +1979,6 @@ describe('compiler compliance', () => { // ... `; const result = compile(files, angularFiles); - debugger; expectEmit(result.source, expectedOutput, 'Invalid base definition'); }); @@ -2019,7 +2018,7 @@ describe('compiler compliance', () => { } }; const result = compile(files, angularFiles); - expect(result.source.indexOf('ngBaseDef')).toBe(-1); + expect(result.source).not.toContain('ngBaseDef'); }); it('should NOT add ngBaseDef if @Directive is present', () => { @@ -2057,7 +2056,7 @@ describe('compiler compliance', () => { } }; const result = compile(files, angularFiles); - expect(result.source.indexOf('ngBaseDef')).toBe(-1); + expect(result.source).not.toContain('ngBaseDef'); }); }); }); diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index a8701e2ed495..8dabc2074684 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -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. * @param key "inputs" or "outputs" @@ -787,7 +792,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any (target: any, name: string, ...args: any[]) => { const constructor = target.constructor; - if (!constructor.hasOwnProperty('ngBaseDef')) { + if (!constructor.hasOwnProperty(NG_BASE_DEF)) { initializeBaseDef(target); } From f1cbae09cca70786bfb6fb7de870d079a9aa8215 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 14 Aug 2018 14:21:48 -0700 Subject: [PATCH 3/3] fixup! feat(ivy): Add AOT handling for bare classes with Input and Output decorators --- packages/compiler-cli/src/ngcc/src/analyzer.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts index dfc72254b506..f7be4aa09246 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -77,13 +77,12 @@ export class Analyzer { protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass |undefined { - const matchingHandlers = - this.handlers - .map(handler => ({ - handler, - decorator: handler.detect(clazz.declaration, clazz.decorators), - })) - .filter(isMatchingHandler); + const matchingHandlers = this.handlers + .map(handler => ({ + handler, + match: handler.detect(clazz.declaration, clazz.decorators), + })) + .filter(isMatchingHandler); if (matchingHandlers.length > 1) { throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); @@ -93,8 +92,8 @@ export class Analyzer { return undefined; } - const {handler, decorator} = matchingHandlers[0]; - const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator); + const {handler, match} = matchingHandlers[0]; + const {analysis, diagnostics} = handler.analyze(clazz.declaration, match); let compilation = handler.compile(clazz.declaration, analysis, pool); if (!Array.isArray(compilation)) { compilation = [compilation];