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 {