fix(core): update unknown tag error for jit standalone components (#45920)

update the error message presented during jit compilation when an unrecognized
tag/element is found in a standalone component so that it does not mention
the ngModule anymore

Note: the aot variant is present in PR #45919

resolves #45818

PR Close #45920
This commit is contained in:
dario-piotrowicz
2022-05-14 11:34:05 +01:00
committed by Alex Rickabaugh
parent dfba192706
commit b1a4b305db
3 changed files with 112 additions and 10 deletions
@@ -24,7 +24,7 @@ import {setUpAttributes} from '../util/attrs_utils';
import {getConstant} from '../util/view_utils';
import {setDirectiveInputsWhichShadowsStyling} from './property';
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, isHostComponentStandalone, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
let shouldThrowErrorOnUnknownElement = false;
@@ -56,7 +56,10 @@ function elementStartFirstCreatePass(
const hasDirectives =
resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex));
ngDevMode && validateElementIsKnown(native, tNode.value, tView.schemas, hasDirectives);
if (ngDevMode) {
const hostIsStandalone = isHostComponentStandalone(lView);
validateElementIsKnown(native, tNode.value, tView.schemas, hasDirectives, hostIsStandalone);
}
if (tNode.attrs !== null) {
computeStaticStyling(tNode, tNode.attrs, false);
@@ -223,10 +226,11 @@ export function ɵɵelement(
* @param tagName Name of the tag to check
* @param schemas Array of schemas
* @param hasDirectives Boolean indicating that the element matches any directive
* @param hostIsStandalone Boolean indicating whether the host is a standalone component
*/
function validateElementIsKnown(
element: RElement, tagName: string|null, schemas: SchemaMetadata[]|null,
hasDirectives: boolean): void {
element: RElement, tagName: string|null, schemas: SchemaMetadata[]|null, hasDirectives: boolean,
hostIsStandalone: boolean): void {
// 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
@@ -247,15 +251,18 @@ function validateElementIsKnown(
!customElements.get(tagName));
if (isUnknown && !matchingSchemas(schemas, tagName)) {
const schemas = `'${hostIsStandalone ? '@Component' : '@NgModule'}.schemas'`;
let message = `'${tagName}' is not a known element:\n`;
message += `1. If '${
tagName}' is an Angular component, then verify that it is part of this module.\n`;
message += `1. If '${tagName}' is an Angular component, then verify that it is ${
hostIsStandalone ? 'included in the \'@Component.imports\' of this component' :
'a part of this module'}.\n`;
if (tagName && tagName.indexOf('-') > -1) {
message += `2. If '${
tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`;
message +=
`2. If '${tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${
schemas} of this component to suppress this message.`;
} else {
message +=
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`;
}
if (shouldThrowErrorOnUnknownElement) {
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message);
@@ -9,16 +9,18 @@ import {Injector} from '../../di';
import {ErrorHandler} from '../../error_handler';
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../../errors';
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
import {Type} from '../../interface/type';
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
import {ViewEncapsulation} from '../../metadata/view';
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization';
import {Sanitizer} from '../../sanitization/sanitizer';
import {assertDefined, assertDomNode, assertEqual, assertGreaterThanOrEqual, assertIndexInRange, assertNotEqual, assertNotSame, assertSame, assertString} from '../../util/assert';
import {assertDefined, assertDomNode, assertEqual, assertGreaterThanOrEqual, assertIndexInRange, assertNotEqual, assertNotSame, assertSame, assertString, throwError} from '../../util/assert';
import {escapeCommentText} from '../../util/dom';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
import {stringify} from '../../util/stringify';
import {assertFirstCreatePass, assertFirstUpdatePass, assertLContainer, assertLView, assertTNodeForLView, assertTNodeForTView} from '../assert';
import {attachPatchData, readPatchedLView} from '../context_discovery';
import {getComponentDef} from '../definition';
import {getFactoryDef} from '../definition_factory';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {throwMultipleComponentError} from '../errors';
@@ -266,6 +268,23 @@ export function createTNodeAtIndex(
return tNode;
}
/**
* Checks if the current component is declared inside of a standalone component template.
*
* @param lView An `LView` that represents a current component that is being rendered.
*/
export function isHostComponentStandalone(lView: LView): boolean {
!ngDevMode && throwError('Must never be called in production mode');
const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView<Type<unknown>>;
const context = declarationLView[CONTEXT];
// Unable to obtain a context, fall back to the non-standalone scenario.
if (!context) return false;
const componentDef = getComponentDef(context.constructor);
return !!(componentDef?.standalone);
}
/**
* When elements are created dynamically after a view blueprint is created (e.g. through
@@ -630,6 +630,82 @@ describe('standalone components, directives and pipes', () => {
});
});
describe('unknown template elements', () => {
const unknownElErrorRegex = (tag: string) => {
const prefix = `'${tag}' is not a known element:`;
const message1 = `1. If '${
tag}' is an Angular component, then verify that it is included in the '@Component.imports' of this component.`;
const message2 = `2. If '${
tag}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.`;
return new RegExp(`${prefix}\s*\n\s*${message1}\s*\n\s*${message2}`);
};
it('should warn the user when an unknown element is present', () => {
const spy = spyOn(console, 'error');
@Component({
standalone: true,
template: '<unknown-tag></unknown-tag>',
})
class AppCmp {
}
TestBed.createComponent(AppCmp);
const errorRegex = unknownElErrorRegex('unknown-tag');
expect(spy).toHaveBeenCalledOnceWith(jasmine.stringMatching(errorRegex));
});
it('should warn the user when multiple unknown elements are present', () => {
const spy = spyOn(console, 'error');
@Component({
standalone: true,
template: '<unknown-tag-A></unknown-tag-A><unknown-tag-B></unknown-tag-B>',
})
class AppCmp {
}
TestBed.createComponent(AppCmp);
const errorRegexA = unknownElErrorRegex('unknown-tag-A');
const errorRegexB = unknownElErrorRegex('unknown-tag-B');
expect(spy).toHaveBeenCalledWith(jasmine.stringMatching(errorRegexA));
expect(spy).toHaveBeenCalledWith(jasmine.stringMatching(errorRegexB));
});
it('should not warn the user when an unknown element is present inside an ng-template', () => {
const spy = spyOn(console, 'error');
@Component({
standalone: true,
template: '<ng-template><unknown-tag></unknown-tag><ng-template>',
})
class AppCmp {
}
TestBed.createComponent(AppCmp);
expect(spy).not.toHaveBeenCalled();
});
it('should warn the user when an unknown element is present in an instantiated embedded view',
() => {
const spy = spyOn(console, 'error');
@Component({
standalone: true,
template: '<ng-template [ngIf]="true"><unknown-tag></unknown-tag><ng-template>',
imports: [CommonModule],
})
class AppCmp {
}
const fixture = TestBed.createComponent(AppCmp);
fixture.detectChanges();
const errorRegex = unknownElErrorRegex('unknown-tag');
expect(spy).toHaveBeenCalledOnceWith(jasmine.stringMatching(errorRegex));
});
});
/*
The following test verify that we don't impose limits when it comes to extending components of
various type (standalone vs. non-standalone).