From a0a29fdd2710096a4d9c8fd4e176eafe4774265d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 7 Aug 2018 12:04:39 -0700 Subject: [PATCH] feat(ivy): Add AOT handling for bare classes with Input and Output decorators (#25367) PR Close #25367 --- .../compiler-cli/src/ngcc/src/analyzer.ts | 37 +-- .../src/ngcc/test/analyzer_spec.ts | 16 +- .../src/ngtsc/annotations/index.ts | 1 + .../src/ngtsc/annotations/src/base_def.ts | 120 ++++++++++ .../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 | 34 +-- .../compiler-cli/src/transformers/program.ts | 2 +- .../compliance/r3_compiler_compliance_spec.ts | 222 ++++++++++++++++++ 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/metadata/directives.ts | 7 +- packages/core/src/render3/index.ts | 3 +- packages/core/src/render3/jit/environment.ts | 1 + .../core/test/render3/jit_environment_spec.ts | 1 + 22 files changed, 483 insertions(+), 60 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 ed7b678efb..f7be4aa092 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -9,7 +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, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; import {Decorator} from '../../ngtsc/host'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; @@ -18,8 +18,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,9 +31,9 @@ export interface AnalyzedFile { constantPool: ConstantPool; } -export interface MatchingHandler { - handler: DecoratorHandler; - decorator: Decorator; +export interface MatchingHandler { + handler: DecoratorHandler; + match: M; } /** @@ -46,7 +46,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), @@ -76,20 +77,23 @@ 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)})) - .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.'); } - if (matchingHandlers.length == 0) { + if (matchingHandlers.length === 0) { 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]; @@ -98,6 +102,7 @@ export class Analyzer { } } -function isMatchingHandler(handler: Partial>): handler is MatchingHandler { - return !!handler.decorator; -} \ No newline at end of file +function isMatchingHandler(handler: Partial>): + handler is MatchingHandler { + return !!handler.match; +} diff --git a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts index 45bb22a44d..96fc978071 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/index.ts b/packages/compiler-cli/src/ngtsc/annotations/index.ts index 3beccc7aa8..77a4860842 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 new file mode 100644 index 0000000000..5924bde1e2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts @@ -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 { + 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 { + 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}>; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 78230c98bb..7f8805d46f 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 2aebbf94a6..ce32ab2feb 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 fceb6ecfa0..65711ed3f3 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 0edd97f817..de80ac54b4 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 eabff8452d..201d7688a5 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 401291e637..79901cf2be 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 b2f188294f..e4d995583c 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 0dc0d44843..6789d0e807 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -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 * prepare for that operation. */ -interface EmitFieldOperation { - adapter: DecoratorHandler; - analysis: AnalysisOutput; - decorator: Decorator; +interface EmitFieldOperation { + adapter: DecoratorHandler; + analysis: AnalysisOutput; + metadata: M; } /** @@ -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, + metadata: 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 { @@ -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/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index d5de233924..1df3d1b6dc 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 6d7fedc66c..4d87cbe649 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,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: \`
{{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); + 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).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(); + + @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).not.toContain('ngBaseDef'); + }); + }); }); diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index ca762310af..75d0e561f7 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 7a96109e62..b18f49bd99 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 b8a1c6f9e1..950b366964 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 53a1cf3c5e..72c7085e49 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/metadata/directives.ts b/packages/core/src/metadata/directives.ts index a8701e2ed4..8dabc20746 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); } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 59ac8becd1..114bf8224c 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 f35b926eff..00dbe0d348 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 134970bdd6..04ff0d11e9 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',