diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 8d2f05ddc1..54ad077323 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -12,7 +12,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ResolvedReference} from '../../imports'; -import {PartialEvaluator} from '../../partial_evaluator'; +import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {TypeCheckContext, TypeCheckableDirectiveMeta} from '../../typecheck'; @@ -21,7 +21,7 @@ import {ResourceLoader} from './api'; import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive'; import {generateSetClassMetadataCall} from './metadata'; import {ScopeDirective, SelectorScopeRegistry} from './selector_scope'; -import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util'; +import {extractDirectiveGuards, isAngularCore, isAngularCoreReference, unwrapExpression} from './util'; const EMPTY_MAP = new Map(); const EMPTY_ARRAY: any[] = []; @@ -226,17 +226,18 @@ export class ComponentDecoratorHandler implements styles.push(...styleUrls.map(styleUrl => this.resourceLoader.load(styleUrl, containingFile))); } - let encapsulation: number = 0; - if (component.has('encapsulation')) { - encapsulation = parseInt(this.evaluator.evaluate(component.get('encapsulation') !) as string); - } + const encapsulation: number = + this._resolveEnumValue(component, 'encapsulation', 'ViewEncapsulation') || 0; + + const changeDetection: number|null = + this._resolveEnumValue(component, 'changeDetection', 'ChangeDetectionStrategy'); let animations: Expression|null = null; if (component.has('animations')) { animations = new WrappedNodeExpr(component.get('animations') !); } - return { + const output = { analysis: { meta: { ...metadata, @@ -260,6 +261,10 @@ export class ComponentDecoratorHandler implements }, typeCheck: true, }; + if (changeDetection !== null) { + (output.analysis.meta as R3ComponentMetadata).changeDetection = changeDetection; + } + return output; } typeCheck(ctx: TypeCheckContext, node: ts.Declaration, meta: ComponentHandlerData): void { @@ -327,6 +332,23 @@ export class ComponentDecoratorHandler implements return meta; } + private _resolveEnumValue( + component: Map, field: string, enumSymbolName: string): number|null { + let resolved: number|null = null; + if (component.has(field)) { + const expr = component.get(field) !; + const value = this.evaluator.evaluate(expr) as any; + if (value instanceof EnumValue && isAngularCoreReference(value.enumRef, enumSymbolName)) { + resolved = value.resolved as number; + } else { + throw new FatalDiagnosticError( + ErrorCode.VALUE_HAS_WRONG_TYPE, expr, + `${field} must be a member of ${enumSymbolName} enum from @angular/core`); + } + } + return resolved; + } + private _extractStyleUrls(component: Map): string[]|null { if (!component.has('styleUrls')) { return null; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 96943aec96..5c58709cc6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -85,6 +85,11 @@ export function isAngularCore(decorator: Decorator): boolean { return decorator.import !== null && decorator.import.from === '@angular/core'; } +export function isAngularCoreReference(reference: Reference, symbolName: string) { + return reference instanceof AbsoluteReference && reference.moduleName === '@angular/core' && + reference.symbolName === symbolName; +} + /** * Unwrap a `ts.Expression`, removing outer type-casts or parentheses until the expression is in its * lowest level form. diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index 70769e80fb..d44736bc80 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -257,7 +257,8 @@ export class StaticInterpreter { node.members.forEach(member => { const name = this.stringNameFromPropertyName(member.name, context); if (name !== undefined) { - map.set(name, new EnumValue(enumRef, name)); + const resolved = member.initializer && this.visit(member.initializer, context); + map.set(name, new EnumValue(enumRef, name, resolved)); } }); return map; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts index 219563aefd..a2191e19a7 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts @@ -70,7 +70,9 @@ export interface ResolvedValueArray extends Array {} * Contains a `Reference` to the enumeration itself, and the name of the referenced member. */ export class EnumValue { - constructor(readonly enumRef: Reference, readonly name: string) {} + constructor( + readonly enumRef: Reference, readonly name: string, + readonly resolved: ResolvedValue) {} } /** diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 3df9f80d8e..80168a3511 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -50,11 +50,11 @@ describe('compiler compliance: styling', () => { const files = { app: { 'spec.ts': ` - import {Component, NgModule} from '@angular/core'; + import {Component, NgModule, ViewEncapsulation} from '@angular/core'; @Component({ selector: "my-component", - encapsulation: ${ViewEncapsulation.None}, + encapsulation: ViewEncapsulation.None, styles: ["div.tall { height: 123px; }", ":host.small p { height:5px; }"], template: "..." }) @@ -77,10 +77,10 @@ describe('compiler compliance: styling', () => { const files = { app: { 'spec.ts': ` - import {Component, NgModule} from '@angular/core'; + import {Component, NgModule, ViewEncapsulation} from '@angular/core'; @Component({ - encapsulation: ${ViewEncapsulation.Native}, + encapsulation: ViewEncapsulation.Native, selector: "my-component", styles: ["div.cool { color: blue; }", ":host.nice p { color: gold; }"], template: "..." diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index 831831738d..f7115fe6b8 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -64,3 +64,15 @@ export interface SimpleChanges { [propName: string]: any; } export type ɵNgModuleDefWithMeta = any; export type ɵDirectiveDefWithMeta = any; + +export enum ViewEncapsulation { + Emulated = 0, + Native = 1, + None = 2, + ShadowDom = 3 +} + +export enum ChangeDetectionStrategy { + OnPush = 0, + Default = 1 +} \ No newline at end of file diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 66f2bdd6b4..c2b6272b11 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -820,6 +820,73 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toContain('interpolation1("", ctx.text, "")'); }); + it('should handle `encapsulation` field', () => { + env.tsconfig(); + env.write(`test.ts`, ` + import {Component, ViewEncapsulation} from '@angular/core'; + @Component({ + selector: 'comp-a', + template: '...', + encapsulation: ViewEncapsulation.None + }) + class CompA {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('encapsulation: 2'); + }); + + it('should throw if `encapsulation` contains invalid value', () => { + env.tsconfig(); + env.write('test.ts', ` + import {Component} from '@angular/core'; + @Component({ + selector: 'comp-a', + template: '...', + encapsulation: 'invalid-value' + }) + class CompA {} + `); + const errors = env.driveDiagnostics(); + expect(errors[0].messageText) + .toContain('encapsulation must be a member of ViewEncapsulation enum from @angular/core'); + }); + + it('should handle `changeDetection` field', () => { + env.tsconfig(); + env.write(`test.ts`, ` + import {Component, ChangeDetectionStrategy} from '@angular/core'; + @Component({ + selector: 'comp-a', + template: '...', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class CompA {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('changeDetection: 0'); + }); + + it('should throw if `changeDetection` contains invalid value', () => { + env.tsconfig(); + env.write('test.ts', ` + import {Component} from '@angular/core'; + @Component({ + selector: 'comp-a', + template: '...', + changeDetection: 'invalid-value' + }) + class CompA {} + `); + const errors = env.driveDiagnostics(); + expect(errors[0].messageText) + .toContain( + 'changeDetection must be a member of ChangeDetectionStrategy enum from @angular/core'); + }); + it('should correctly recognize local symbols', () => { env.tsconfig(); env.write('module.ts', `