From 658087be7e3bcae0685fcced15afad2a22a4b1e6 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Sun, 24 Nov 2019 13:44:24 -0800 Subject: [PATCH] fix(ivy): prevent unknown element check for AOT-compiled components (#34024) Prior to this commit, the unknown element can happen twice for AOT-compiled components: once during compilation and once again at runtime. Due to the fact that `schemas` information is not present on Component and NgModule defs after AOT compilation, the second check (at runtime) may fail, even though the same check was successful at compile time. This commit updates the code to avoid the second check for AOT-compiled components by checking whether `schemas` information is present in a logic that executes the unknown element check. PR Close #34024 --- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 38 +++++++++++ .../core/src/render3/instructions/element.ts | 8 +++ packages/core/src/render3/jit/module.ts | 7 ++ .../core/test/acceptance/ng_module_spec.ts | 68 ++++++++++++++++++- 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 824ab3d8df..88cdaaad29 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -3009,6 +3009,44 @@ runInEachFileSystem(os => { `/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(Service, [{ type: Injectable, args: [{ providedIn: 'root' }] }], null, null); })();`); }); + it('should not include `schemas` in component and module defs', () => { + env.write('test.ts', ` + import {Component, NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; + + @Component({ + selector: 'comp', + template: '', + schemas: [NO_ERRORS_SCHEMA], + }) + class MyComp {} + + @NgModule({ + declarations: [MyComp], + schemas: [NO_ERRORS_SCHEMA], + }) + class MyModule {} + `); + + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(trim(` + MyComp.ɵcmp = i0.ɵɵdefineComponent({ + type: MyComp, + selectors: [["comp"]], + decls: 1, + vars: 0, + template: function MyComp_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵelement(0, "custom-el"); + } + }, + encapsulation: 2 + }); + `)); + expect(jsContents) + .toContain(trim('MyModule.ɵmod = i0.ɵɵdefineNgModule({ type: MyModule });')); + }); + it('should emit setClassMetadata calls for all types', () => { env.write('test.ts', ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 9d23c50b78..c40219ce66 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -255,6 +255,14 @@ function setDirectiveStylingInput( function validateElement( hostView: LView, element: RElement, tNode: TNode, hasDirectives: boolean): void { + const schemas = hostView[TVIEW].schemas; + + // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT + // mode where this check happens at compile time. In JIT mode, `schemas` is always present and + // defined as an array (as an empty array in case `schemas` field is not defined) and we should + // execute the check below. + if (schemas === null) return; + const tagName = tNode.tagName; // If the element matches any directive, it's considered as valid. diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index e24b51f80c..a5cac93fca 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -128,6 +128,13 @@ export function compileNgModuleDefs( schemas: ngModule.schemas ? flatten(ngModule.schemas) : null, id: ngModule.id || null, }); + // Set `schemas` on ngModuleDef to an empty array in JIT mode to indicate that runtime + // should verify that there are no unknown elements in a template. In AOT mode, that check + // happens at compile time and `schemas` information is not present on Component and Module + // defs after compilation (so the check doesn't happen the second time at runtime). + if (!ngModuleDef.schemas) { + ngModuleDef.schemas = []; + } } return ngModuleDef; } diff --git a/packages/core/test/acceptance/ng_module_spec.ts b/packages/core/test/acceptance/ng_module_spec.ts index 09c4c96cff..78f1457ea5 100644 --- a/packages/core/test/acceptance/ng_module_spec.ts +++ b/packages/core/test/acceptance/ng_module_spec.ts @@ -7,7 +7,8 @@ */ import {CommonModule} from '@angular/common'; -import {CUSTOM_ELEMENTS_SCHEMA, Component, NO_ERRORS_SCHEMA, NgModule} from '@angular/core'; +import {CUSTOM_ELEMENTS_SCHEMA, Component, NO_ERRORS_SCHEMA, NgModule, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element} from '@angular/core'; + import {TestBed} from '@angular/core/testing'; import {modifiedInIvy, onlyInIvy} from '@angular/private/testing'; @@ -193,6 +194,71 @@ describe('NgModule', () => { }).toThrowError(/'custom' is not a known element/); }); + onlyInIvy('test relies on Ivy-specific AOT format') + .it('should not throw unknown element error for AOT-compiled components', () => { + /* + * @Component({ + * selector: 'comp', + * template: '', + * }) + * class MyComp {} + */ + class MyComp { + static ɵfac = () => new MyComp(); + static ɵcmp = defineComponent({ + type: MyComp, + selectors: [['comp']], + decls: 1, + vars: 0, + template: function MyComp_Template(rf, ctx) { + if (rf & 1) { + element(0, 'custom-el'); + } + }, + encapsulation: 2 + }); + } + setClassMetadata( + MyComp, [{ + type: Component, + args: [{ + selector: 'comp', + template: '', + }] + }], + null, null); + + /* + * @NgModule({ + * declarations: [MyComp], + * schemas: [NO_ERRORS_SCHEMA], + * }) + * class MyModule {} + */ + class MyModule { + static ɵmod = defineNgModule({type: MyModule}); + static ɵinj = defineInjector({factory: () => new MyModule()}); + } + setClassMetadata( + MyModule, [{ + type: NgModule, + args: [{ + declarations: [MyComp], + schemas: [NO_ERRORS_SCHEMA], + }] + }], + null, null); + + TestBed.configureTestingModule({ + imports: [MyModule], + }); + + expect(() => { + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + }).not.toThrow(); + }); + it('should not throw unknown element error with CUSTOM_ELEMENTS_SCHEMA', () => { @Component({template: ``}) class MyComp {