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
This commit is contained in:
Payam Valadkhan
2023-09-20 19:07:38 -04:00
committed by Dylan Hunn
parent 377a7abfda
commit f91f222b55
3 changed files with 137 additions and 2 deletions
@@ -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');
@@ -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;
@@ -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: '<span>Hello world!</span>',
})
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: '<span>Hello world!</span>',
})
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: '<span>Hello world!</span>',
})
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: '<span>Hello world!</span>',
})
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: '<span>Hello world!</span>',
})
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', () => {