mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Alex Rickabaugh
parent
dfba192706
commit
b1a4b305db
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user