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 {