From f91f222b55f249089d267c72a9c0ab5b09d7c932 Mon Sep 17 00:00:00 2001 From: Payam Valadkhan Date: Wed, 20 Sep 2023 19:07:38 -0400 Subject: [PATCH] fix(compiler-cli): resolve component encapsulation enum in local compilation mode (#51848) Currently the field encapsulation undergoes some static analysis to check if it is `ViewEncapsulation` enum. Such static check fails in local compilation mode in g3 as the symbol cannot be resolved. On the other hand this field has to be resolved statically as its value determined the generated code. So in local compilation mode we add a lighter resolving logic which relies only on local information. PR Close #51848 --- .../annotations/common/src/evaluation.ts | 33 ++++++ .../annotations/component/src/handler.ts | 6 +- .../test/ngtsc/local_compilation_spec.ts | 100 ++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts index 8d63aa03207..81978b08a84 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ViewEncapsulation} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; @@ -34,6 +35,38 @@ export function resolveEnumValue( return resolved; } +/** + * Resolves a EncapsulationEnum expression locally on best effort without having to calculate the + * reference. This suites local compilation mode where each file is compiled individually. + * + * The static analysis is still needed in local compilation mode since the value of this enum will + * be used later to decide the generated code for styles. + */ +export function resolveEncapsulationEnumValueLocally(expr?: ts.Expression): number|null { + if (!expr) { + return null; + } + + const exprText = expr.getText().trim(); + + for (const key in ViewEncapsulation) { + if (!Number.isNaN(Number(key))) { + continue; + } + + const suffix = `ViewEncapsulation.${key}`; + + // Check whether the enum is imported by name or used by import namespace (e.g., + // core.ViewEncapsulation.None) + if (exprText === suffix || exprText.endsWith(`.${suffix}`)) { + const ans = Number(ViewEncapsulation[key]); + return ans; + } + } + + return null; +} + /** Determines if the result of an evaluation is a string array. */ export function isStringArray(resolvedValue: ResolvedValue): resolvedValue is string[] { return Array.isArray(resolvedValue) && resolvedValue.every(elem => typeof elem === 'string'); diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index c875cfa7dca..ac0d5a977b6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -26,7 +26,7 @@ import {TypeCheckableDirectiveMeta, TypeCheckContext} from '../../../typecheck/a import {ExtendedTemplateChecker} from '../../../typecheck/extended/api'; import {getSourceFile} from '../../../util/src/typescript'; import {Xi18nContext} from '../../../xi18n'; -import {combineResolvers, compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, ReferencesRegistry, removeIdentifierReferences, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, validateHostDirectives, wrapFunctionExpressionsInParens,} from '../../common'; +import {combineResolvers, compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, ReferencesRegistry, removeIdentifierReferences, resolveEncapsulationEnumValueLocally, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, validateHostDirectives, wrapFunctionExpressionsInParens,} from '../../common'; import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive'; import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module'; @@ -233,7 +233,9 @@ export class ComponentDecoratorHandler implements const {decorator: component, metadata, inputs, outputs, hostDirectives, rawHostDirectives} = directiveResult; const encapsulation: number = - resolveEnumValue(this.evaluator, component, 'encapsulation', 'ViewEncapsulation') ?? + (this.compilationMode !== CompilationMode.LOCAL ? + resolveEnumValue(this.evaluator, component, 'encapsulation', 'ViewEncapsulation') : + resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ?? ViewEncapsulation.Emulated; let changeDetection: number|Expression|null = null; diff --git a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts index 7e234ec7ad0..cd40769f1a9 100644 --- a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts +++ b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts @@ -364,6 +364,106 @@ runInEachFileSystem(() => { expect(jsContents).toContain('changeDetection: SomeWeirdThing'); }); + + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with no styles', + () => { + env.write('test.ts', ` + import {Component, ViewEncapsulation} from '@angular/core'; + + @Component({ + encapsulation: ViewEncapsulation.Emulated, + template: 'Hello world!', + }) + export class MainComponent { + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // If there is no style, don't generate css selectors on elements by setting + // encapsulation to none (=2) + expect(jsContents).toContain('encapsulation: 2'); + }); + + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with styles', + () => { + env.write('test.ts', ` + import {Component, ViewEncapsulation} from '@angular/core'; + + @Component({ + encapsulation: ViewEncapsulation.Emulated, + styles: ['color: blue'], + template: 'Hello world!', + }) + export class MainComponent { + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // encapsulation is set only for non-default value + expect(jsContents).not.toContain('encapsulation: 0'); + expect(jsContents).toContain('styles: ["color: blue"]'); + }); + + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.ShadowDom', + () => { + env.write('test.ts', ` + import {Component, ViewEncapsulation} from '@angular/core'; + + @Component({ + encapsulation: ViewEncapsulation.ShadowDom, + template: 'Hello world!', + }) + export class MainComponent { + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('encapsulation: 3'); + }); + + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.None', + () => { + env.write('test.ts', ` + import {Component, ViewEncapsulation} from '@angular/core'; + + @Component({ + encapsulation: ViewEncapsulation.None, + template: 'Hello world!', + }) + export class MainComponent { + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('encapsulation: 2'); + }); + + it('should default encapsulation to Emulated', () => { + env.write('test.ts', ` + import {Component, ViewEncapsulation} from '@angular/core'; + + @Component({ + template: 'Hello world!', + }) + export class MainComponent { + } + `); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // If there is no style, don't generate css selectors on elements by setting encapsulation + // to none (=2) + expect(jsContents).toContain('encapsulation: 2'); + }); }); describe('constructor injection', () => {