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', () => {