Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/render3/instructions/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
AndrewKushnir marked this conversation as resolved.
Resolved

const tagName = tNode.tagName;

// If the element matches any directive, it's considered as valid.
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/render3/jit/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Comment thread
AndrewKushnir marked this conversation as resolved.
Resolved
}
}
return ngModuleDef;
}
Expand Down
68 changes: 67 additions & 1 deletion packages/core/test/acceptance/ng_module_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down