diff --git a/packages/common/src/directives/ng_switch.ts b/packages/common/src/directives/ng_switch.ts index cb67f7583ef..f4c18eec6c5 100644 --- a/packages/common/src/directives/ng_switch.ts +++ b/packages/common/src/directives/ng_switch.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, DoCheck, Host, Input, Optional, TemplateRef, ViewContainerRef, ɵRuntimeError as RuntimeError, ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/core'; +import {Directive, DoCheck, Host, Input, Optional, TemplateRef, ViewContainerRef, ɵRuntimeError as RuntimeError} from '@angular/core'; + +import {RuntimeErrorCode} from '../errors'; export class SwitchView { private _created = false; @@ -243,7 +245,7 @@ export class NgSwitchDefault { function throwNgSwitchProviderNotFoundError(attrName: string, directiveName: string): never { throw new RuntimeError( - RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR, + RuntimeErrorCode.PARENT_NG_SWITCH_NOT_FOUND, `An element with the "${attrName}" attribute ` + `(matching the "${ directiveName}" directive) must be located inside an element with the "ngSwitch" attribute ` + diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts new file mode 100644 index 00000000000..ac763e1cb25 --- /dev/null +++ b/packages/common/src/errors.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * The list of error codes used in runtime code of the `common` package. + * Reserved error code range: 2000-2999. + */ +export const enum RuntimeErrorCode { + // NgSwitch errors + PARENT_NG_SWITCH_NOT_FOUND = 2000, +} diff --git a/packages/common/test/directives/ng_switch_spec.ts b/packages/common/test/directives/ng_switch_spec.ts index e7c8bd5983e..a0498d227ec 100644 --- a/packages/common/test/directives/ng_switch_spec.ts +++ b/packages/common/test/directives/ng_switch_spec.ts @@ -182,7 +182,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; expect(() => createTestComponent(template)) .toThrowError( - 'NG0305: An element with the "ngSwitchCase" attribute (matching the "NgSwitchCase" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)'); + 'NG02000: An element with the "ngSwitchCase" attribute (matching the "NgSwitchCase" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)'); })); it('should throw error when ngSwitchDefault is used outside of ngSwitch', waitForAsync(() => { @@ -191,7 +191,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; expect(() => createTestComponent(template)) .toThrowError( - 'NG0305: An element with the "ngSwitchDefault" attribute (matching the "NgSwitchDefault" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)'); + 'NG02000: An element with the "ngSwitchDefault" attribute (matching the "NgSwitchDefault" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)'); })); it('should support nested NgSwitch on ng-container with ngTemplateOutlet', () => { diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 2a95bf7286a..1bdc76cf2cb 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -22,7 +22,7 @@ export {ComponentFactory as ɵComponentFactory} from './linker/component_factory export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types'; -export {RuntimeError as ɵRuntimeError, RuntimeErrorCode as ɵRuntimeErrorCode} from './render3/error_code'; +export {RuntimeError as ɵRuntimeError} from './render3/error_code'; export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass'; export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer'; export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer'; diff --git a/packages/core/src/render3/error_code.ts b/packages/core/src/render3/error_code.ts index 1e34bdb268c..946ebb6b378 100644 --- a/packages/core/src/render3/error_code.ts +++ b/packages/core/src/render3/error_code.ts @@ -8,32 +8,40 @@ import {ERROR_DETAILS_PAGE_BASE_URL} from './error_details_base_url'; +/** + * The list of error codes used in runtime code of the `core` package. + * Reserved error code range: 100-999. + * + * Note: the minus sign denotes the fact that a particular code has a detailed guide on + * angular.io. This extra annotation is needed to avoid introducing a separate set to store + * error codes which have guides, which might leak into runtime code. + * + * Full list of available error guides can be found at https://angular.io/errors. + */ export const enum RuntimeErrorCode { - // Internal Errors - // Change Detection Errors - EXPRESSION_CHANGED_AFTER_CHECKED = '100', - RECURSIVE_APPLICATION_REF_TICK = '101', + EXPRESSION_CHANGED_AFTER_CHECKED = -100, + RECURSIVE_APPLICATION_REF_TICK = 101, // Dependency Injection Errors - CYCLIC_DI_DEPENDENCY = '200', - PROVIDER_NOT_FOUND = '201', + CYCLIC_DI_DEPENDENCY = -200, + PROVIDER_NOT_FOUND = -201, // Template Errors - MULTIPLE_COMPONENTS_MATCH = '300', - EXPORT_NOT_FOUND = '301', - PIPE_NOT_FOUND = '302', - UNKNOWN_BINDING = '303', - UNKNOWN_ELEMENT = '304', - TEMPLATE_STRUCTURE_ERROR = '305', + MULTIPLE_COMPONENTS_MATCH = -300, + EXPORT_NOT_FOUND = -301, + PIPE_NOT_FOUND = -302, + UNKNOWN_BINDING = 303, + UNKNOWN_ELEMENT = 304, + TEMPLATE_STRUCTURE_ERROR = 305, // Bootstrap Errors - MULTIPLE_PLATFORMS = '400', - PLATFORM_NOT_FOUND = '401', - ERROR_HANDLER_NOT_FOUND = '402', - BOOTSTRAP_COMPONENTS_NOT_FOUND = '403', - ALREADY_DESTROYED_PLATFORM = '404', - ASYNC_INITIALIZERS_STILL_RUNNING = '405', + MULTIPLE_PLATFORMS = 400, + PLATFORM_NOT_FOUND = 401, + ERROR_HANDLER_NOT_FOUND = 402, + BOOTSTRAP_COMPONENTS_NOT_FOUND = 403, + ALREADY_DESTROYED_PLATFORM = 404, + ASYNC_INITIALIZERS_STILL_RUNNING = 405, // Styling Errors @@ -41,42 +49,26 @@ export const enum RuntimeErrorCode { // i18n Errors - // Compilation Errors + // JIT Compilation Errors } -export class RuntimeError extends Error { - constructor(public code: RuntimeErrorCode, message: string) { - super(formatRuntimeError(code, message)); +export class RuntimeError extends Error { + constructor(public code: T, message: string) { + super(formatRuntimeError(code, message)); } } -// Contains a set of error messages that have details guides at angular.io. -// Full list of available error guides can be found at https://angular.io/errors -/* tslint:disable:no-toplevel-property-access */ -export const RUNTIME_ERRORS_WITH_GUIDES = new Set([ - RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED, - RuntimeErrorCode.CYCLIC_DI_DEPENDENCY, - RuntimeErrorCode.PROVIDER_NOT_FOUND, - RuntimeErrorCode.MULTIPLE_COMPONENTS_MATCH, - RuntimeErrorCode.EXPORT_NOT_FOUND, - RuntimeErrorCode.PIPE_NOT_FOUND, -]); -/* tslint:enable:no-toplevel-property-access */ - /** Called to format a runtime error */ -export function formatRuntimeError(code: RuntimeErrorCode, message: string): string { - const fullCode = code ? `NG0${code}: ` : ''; +export function formatRuntimeError(code: T, message: string): string { + const codeAsNumber = code as unknown as number; + // Error code might be a negative number, which is a special marker that instructs the logic to + // generate a link to the error details page on angular.io. + const fullCode = `NG0${Math.abs(codeAsNumber)}`; - let errorMessage = `${fullCode}${message}`; + let errorMessage = `${fullCode}${message ? ': ' + message : ''}`; - // Some runtime errors are still thrown without `ngDevMode` (for example - // `throwProviderNotFoundError`), so we add `ngDevMode` check here to avoid pulling - // `RUNTIME_ERRORS_WITH_GUIDES` symbol into prod bundles. - // TODO: revisit all instances where `RuntimeError` is thrown and see if `ngDevMode` can be added - // there instead to tree-shake more devmode-only code (and eventually remove `ngDevMode` check - // from this code). - if (ngDevMode && RUNTIME_ERRORS_WITH_GUIDES.has(code)) { - errorMessage = `${errorMessage}. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/NG0${code}`; + if (ngDevMode && codeAsNumber < 0) { + errorMessage = `${errorMessage}. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/${fullCode}`; } return errorMessage; } diff --git a/packages/core/src/render3/errors.ts b/packages/core/src/render3/errors.ts index a8e53004927..1a804960778 100644 --- a/packages/core/src/render3/errors.ts +++ b/packages/core/src/render3/errors.ts @@ -32,8 +32,6 @@ export function throwErrorIfNoChangesMode( ` It seems like the view has been created after its parent and its children have been dirty checked.` + ` Has it been created in a change detection hook?`; } - // TODO: include debug context, see `viewDebugError` function in - // `packages/core/src/view/errors.ts` for reference. throw new RuntimeError(RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED, msg); } diff --git a/packages/core/src/render3/errors_di.ts b/packages/core/src/render3/errors_di.ts index 7177d965adf..0a4ab175524 100644 --- a/packages/core/src/render3/errors_di.ts +++ b/packages/core/src/render3/errors_di.ts @@ -7,6 +7,7 @@ */ import {InjectorType} from '../di/interface/defs'; import {stringify} from '../util/stringify'; + import {RuntimeError, RuntimeErrorCode} from './error_code'; import {stringifyForError} from './util/stringify_utils'; diff --git a/packages/core/test/runtime_error_spec.ts b/packages/core/test/runtime_error_spec.ts new file mode 100644 index 00000000000..540751413c7 --- /dev/null +++ b/packages/core/test/runtime_error_spec.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RuntimeError, RuntimeErrorCode} from '../src/render3/error_code'; + +describe('RuntimeError utils', () => { + it('should format the error message correctly', () => { + // Error with a guide, but without an error message. + let errorInstance = new RuntimeError(RuntimeErrorCode.EXPORT_NOT_FOUND, ''); + expect(errorInstance.toString()) + .toBe('Error: NG0301. Find more at https://angular.io/errors/NG0301'); + + // Error without a guide and an error message. + errorInstance = new RuntimeError(RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR, ''); + expect(errorInstance.toString()).toBe('Error: NG0305'); + + // Error without a guide, but with an error message. + errorInstance = + new RuntimeError(RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR, 'Some error message'); + expect(errorInstance.toString()).toBe('Error: NG0305: Some error message'); + + // Error with both a guide and an error message. + errorInstance = new RuntimeError(RuntimeErrorCode.EXPORT_NOT_FOUND, 'Some error message'); + expect(errorInstance.toString()) + .toBe('Error: NG0301: Some error message. Find more at https://angular.io/errors/NG0301'); + }); +});