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
This commit is contained in:
Andrew Kushnir 2019-11-24 13:44:24 -08:00 committed by Miško Hevery
parent 8c33f91529
commit 658087be7e
4 changed files with 120 additions and 1 deletions

View File

@ -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: '<custom-el></custom-el>',
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';

View File

@ -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.

View File

@ -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;
}

View File

@ -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: '<custom-el></custom-el>',
* })
* 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: '<custom-el></custom-el>',
}]
}],
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: `<custom-el></custom-el>`})
class MyComp {