feat: introduce source maps for templates (#15011)

The main use case for the generated source maps is to give
errors a meaningful context in terms of the original source
that the user wrote.

Related changes that are included in this commit:

* renamed virtual folders used for jit:
  * ng://<module type>/module.ngfactory.js
  * ng://<module type>/<comp type>.ngfactory.js
  * ng://<module type>/<comp type>.html (for inline templates)
* error logging:
  * all errors that happen in templates are logged
    from the place of the nearest element.
  * instead of logging error messages and stacks separately,
    we log the actual error. This is needed so that browsers apply
    source maps to the stack correctly.
  * error type and error is logged as one log entry.

Note that long-stack-trace zone has a bug that 
disables source maps for stack traces,
see https://github.com/angular/zone.js/issues/661.

BREAKING CHANGE:

- DebugNode.source no more returns the source location of a node.  

Closes 14013
This commit is contained in:
Tobias Bosch
2017-03-14 09:16:15 -07:00
committed by Chuck Jazdzewski
parent 1c1085b140
commit cdc882bd36
48 changed files with 1196 additions and 515 deletions

View File

@ -7,7 +7,7 @@
*/
import {Injector} from '../di';
import {RenderDebugInfo} from '../render/api';
import {DebugContext} from '../view/index';
export class EventListener { constructor(public name: string, public callback: Function){}; }
@ -19,7 +19,7 @@ export class DebugNode {
listeners: EventListener[];
parent: DebugElement;
constructor(nativeNode: any, parent: DebugNode, private _debugInfo: RenderDebugInfo) {
constructor(nativeNode: any, parent: DebugNode, private _debugContext: DebugContext) {
this.nativeNode = nativeNode;
if (parent && parent instanceof DebugElement) {
parent.addChild(this);
@ -29,19 +29,24 @@ export class DebugNode {
this.listeners = [];
}
get injector(): Injector { return this._debugInfo ? this._debugInfo.injector : null; }
get injector(): Injector { return this._debugContext ? this._debugContext.injector : null; }
get componentInstance(): any { return this._debugInfo ? this._debugInfo.component : null; }
get componentInstance(): any { return this._debugContext ? this._debugContext.component : null; }
get context(): any { return this._debugInfo ? this._debugInfo.context : null; }
get context(): any { return this._debugContext ? this._debugContext.context : null; }
get references(): {[key: string]: any} {
return this._debugInfo ? this._debugInfo.references : null;
return this._debugContext ? this._debugContext.references : null;
}
get providerTokens(): any[] { return this._debugInfo ? this._debugInfo.providerTokens : null; }
get providerTokens(): any[] {
return this._debugContext ? this._debugContext.providerTokens : null;
}
get source(): string { return this._debugInfo ? this._debugInfo.source : null; }
/**
* @deprecated since v4
*/
get source(): string { return 'Deprecated since v4'; }
}
/**
@ -56,8 +61,8 @@ export class DebugElement extends DebugNode {
childNodes: DebugNode[];
nativeElement: any;
constructor(nativeNode: any, parent: any, _debugInfo: RenderDebugInfo) {
super(nativeNode, parent, _debugInfo);
constructor(nativeNode: any, parent: any, _debugContext: DebugContext) {
super(nativeNode, parent, _debugContext);
this.properties = {};
this.attributes = {};
this.classes = {};

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ERROR_ORIGINAL_ERROR, getDebugContext, getOriginalError} from './errors';
import {ERROR_ORIGINAL_ERROR, getDebugContext, getErrorLogger, getOriginalError} from './errors';
/**
@ -49,36 +50,23 @@ export class ErrorHandler {
constructor(rethrowError: boolean = false) { this.rethrowError = rethrowError; }
handleError(error: any): void {
this._console.error(`EXCEPTION: ${this._extractMessage(error)}`);
const originalError = this._findOriginalError(error);
const context = this._findContext(error);
// Note: Browser consoles show the place from where console.error was called.
// We can use this to give users additional information about the error.
const errorLogger = getErrorLogger(error);
if (error instanceof Error) {
const originalError = this._findOriginalError(error);
const originalStack = this._findOriginalStack(error);
const context = this._findContext(error);
if (originalError) {
this._console.error(`ORIGINAL EXCEPTION: ${this._extractMessage(originalError)}`);
}
if (originalStack) {
this._console.error('ORIGINAL STACKTRACE:');
this._console.error(originalStack);
}
if (context) {
this._console.error('ERROR CONTEXT:');
this._console.error(context);
}
errorLogger(this._console, `ERROR`, error);
if (originalError) {
errorLogger(this._console, `ORIGINAL ERROR`, originalError);
}
if (context) {
errorLogger(this._console, 'ERROR CONTEXT', context);
}
if (this.rethrowError) throw error;
}
/** @internal */
_extractMessage(error: any): string {
return error instanceof Error ? error.message : error.toString();
}
/** @internal */
_findContext(error: any): any {
if (error) {
@ -98,20 +86,6 @@ export class ErrorHandler {
return e;
}
/** @internal */
_findOriginalStack(error: Error): string {
let e: any = error;
let stack: string = e.stack;
while (e instanceof Error && getOriginalError(e)) {
e = getOriginalError(e);
if (e instanceof Error && e.stack) {
stack = e.stack;
}
}
return stack;
}
}
export function wrappedError(message: string, originalError: any): Error {

View File

@ -12,6 +12,7 @@ export const ERROR_TYPE = 'ngType';
export const ERROR_COMPONENT_TYPE = 'ngComponentType';
export const ERROR_DEBUG_CONTEXT = 'ngDebugContext';
export const ERROR_ORIGINAL_ERROR = 'ngOriginalError';
export const ERROR_LOGGER = 'ngErrorLogger';
export function getType(error: Error): Function {
@ -25,3 +26,12 @@ export function getDebugContext(error: Error): DebugContext {
export function getOriginalError(error: Error): Error {
return (error as any)[ERROR_ORIGINAL_ERROR];
}
export function getErrorLogger(error: Error): (console: Console, ...values: any[]) => void {
return (error as any)[ERROR_LOGGER] || defaultErrorLogger;
}
function defaultErrorLogger(console: Console, ...values: any[]) {
(<any>console.error)(...values);
}

View File

@ -6,14 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '../application_ref';
import {Renderer2, RendererType2} from '../render/api';
import {SecurityContext} from '../security';
import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementHandleEventFn, NodeData, NodeDef, NodeFlags, OutputDef, OutputType, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, asElementData, asProviderData} from './types';
import {checkAndUpdateBinding, dispatchEvent, elementEventFullName, filterQueryId, getParentRenderElement, resolveViewDefinition, sliceErrorStack, splitMatchedQueriesDsl, splitNamespace} from './util';
const NOOP: any = () => {};
import {NOOP, checkAndUpdateBinding, dispatchEvent, elementEventFullName, filterQueryId, getParentRenderElement, resolveViewDefinition, splitMatchedQueriesDsl, splitNamespace} from './util';
export function anchorDef(
flags: NodeFlags, matchedQueriesDsl: [string | number, QueryValueType][],
@ -24,8 +21,6 @@ export function anchorDef(
}
flags |= NodeFlags.TypeElement;
const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl);
// skip the call to sliceErrorStack itself + the call to this function.
const source = isDevMode() ? sliceErrorStack(2, 3) : '';
const template = templateFactory ? resolveViewDefinition(templateFactory) : null;
return {
@ -45,7 +40,7 @@ export function anchorDef(
element: {
ns: undefined,
name: undefined,
attrs: undefined, template, source,
attrs: undefined, template,
componentProvider: undefined,
componentView: undefined,
componentRendererType: undefined,
@ -71,12 +66,10 @@ export function elementDef(
string, SecurityContext
])[],
outputs?: ([string, string])[], handleEvent?: ElementHandleEventFn,
componentView?: () => ViewDefinition, componentRendererType?: RendererType2): NodeDef {
componentView?: ViewDefinitionFactory, componentRendererType?: RendererType2): NodeDef {
if (!handleEvent) {
handleEvent = NOOP;
}
// skip the call to sliceErrorStack itself + the call to this function.
const source = isDevMode() ? sliceErrorStack(2, 3) : '';
const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl);
let ns: string;
let name: string;
@ -146,7 +139,6 @@ export function elementDef(
ns,
name,
attrs,
source,
template: undefined,
// will bet set by the view definition
componentProvider: undefined, componentView, componentRendererType,

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ERROR_DEBUG_CONTEXT, ERROR_ORIGINAL_ERROR, getDebugContext} from '../errors';
import {ERROR_DEBUG_CONTEXT, ERROR_LOGGER, getDebugContext} from '../errors';
import {DebugContext, ViewState} from './types';
export function expressionChangedAfterItHasBeenCheckedError(
@ -21,19 +21,27 @@ export function expressionChangedAfterItHasBeenCheckedError(
return viewDebugError(msg, context);
}
export function viewWrappedDebugError(originalError: any, context: DebugContext): Error {
const err = viewDebugError(originalError.message, context);
(err as any)[ERROR_ORIGINAL_ERROR] = originalError;
export function viewWrappedDebugError(err: any, context: DebugContext): Error {
if (!(err instanceof Error)) {
// errors that are not Error instances don't have a stack,
// so it is ok to wrap them into a new Error object...
err = new Error(err.toString());
}
_addDebugContext(err, context);
return err;
}
export function viewDebugError(msg: string, context: DebugContext): Error {
const err = new Error(msg);
(err as any)[ERROR_DEBUG_CONTEXT] = context;
err.stack = context.source;
_addDebugContext(err, context);
return err;
}
function _addDebugContext(err: Error, context: DebugContext) {
(err as any)[ERROR_DEBUG_CONTEXT] = context;
(err as any)[ERROR_LOGGER] = context.logError.bind(context);
}
export function isViewDebugError(err: Error): boolean {
return !!getDebugContext(err);
}

View File

@ -16,8 +16,8 @@ import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './err
import {resolveDep} from './provider';
import {dirtyParentQueries, getQueryValue} from './query';
import {createInjector} from './refs';
import {ArgumentType, BindingType, CheckType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asPureExpressionData} from './types';
import {checkBinding, isComponentView, renderNode, viewParentEl} from './util';
import {ArgumentType, BindingType, CheckType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, NodeLogger, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asPureExpressionData} from './types';
import {NOOP, checkBinding, isComponentView, renderNode, viewParentEl} from './util';
import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createEmbeddedView, createRootView, destroyView} from './view';
let initialized = false;
@ -357,13 +357,6 @@ class DebugContext_ implements DebugContext {
}
return references;
}
get source(): string {
if (this.nodeDef.flags & NodeFlags.TypeText) {
return this.nodeDef.text.source;
} else {
return this.elDef.element.source;
}
}
get componentRenderElement() {
const elData = findHostElement(this.elOrCompView);
return elData ? elData.renderElement : undefined;
@ -372,6 +365,31 @@ class DebugContext_ implements DebugContext {
return this.nodeDef.flags & NodeFlags.TypeText ? renderNode(this.view, this.nodeDef) :
renderNode(this.elView, this.elDef);
}
logError(console: Console, ...values: any[]) {
let logViewFactory: ViewDefinitionFactory;
let logNodeIndex: number;
if (this.nodeDef.flags & NodeFlags.TypeText) {
logViewFactory = this.view.def.factory;
logNodeIndex = this.nodeDef.index;
} else {
logViewFactory = this.elView.def.factory;
logNodeIndex = this.elDef.index;
}
let currNodeIndex = -1;
let nodeLogger: NodeLogger = () => {
currNodeIndex++;
if (currNodeIndex === logNodeIndex) {
return console.error.bind(console, ...values);
} else {
return NOOP;
}
};
logViewFactory(nodeLogger);
if (currNodeIndex < logNodeIndex) {
console.error('Illegal state: the ViewDefinitionFactory did not call the logger!');
(<any>console.error)(...values);
}
}
}
function findHostElement(view: ViewData): ElementData {

View File

@ -6,15 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '../application_ref';
import {looseIdentical} from '../util';
import {BindingDef, BindingType, DebugContext, NodeData, NodeDef, NodeFlags, RootData, Services, TextData, ViewData, ViewFlags, asElementData, asTextData} from './types';
import {checkAndUpdateBinding, getParentRenderElement, sliceErrorStack} from './util';
import {checkAndUpdateBinding, getParentRenderElement} from './util';
export function textDef(ngContentIndex: number, constants: string[]): NodeDef {
// skip the call to sliceErrorStack itself + the call to this function.
const source = isDevMode() ? sliceErrorStack(2, 3) : '';
const bindings: BindingDef[] = new Array(constants.length - 1);
for (let i = 1; i < constants.length; i++) {
bindings[i - 1] = {
@ -46,7 +43,7 @@ export function textDef(ngContentIndex: number, constants: string[]): NodeDef {
outputs: [],
element: undefined,
provider: undefined,
text: {prefix: constants[0], source},
text: {prefix: constants[0]},
query: undefined,
ngContent: undefined
};

View File

@ -22,6 +22,7 @@ import {Sanitizer, SecurityContext} from '../security';
// -------------------------------------
export interface ViewDefinition {
factory: ViewDefinitionFactory;
flags: ViewFlags;
updateDirectives: ViewUpdateFn;
updateRenderer: ViewUpdateFn;
@ -45,9 +46,22 @@ export interface ViewDefinition {
nodeMatchedQueries: number;
}
export type ViewDefinitionFactory = () => ViewDefinition;
/**
* Factory for ViewDefinitions.
* We use a function so we can reexeute it in case an error happens and use the given logger
* function to log the error from the definition of the node, which is shown in all browser
* logs.
*/
export interface ViewDefinitionFactory { (logger: NodeLogger): ViewDefinition; }
export type ViewUpdateFn = (check: NodeCheckFn, view: ViewData) => void;
/**
* Function to call console.error at the right source location. This is an indirection
* via another function as browser will log the location that actually called
* `console.error`.
*/
export interface NodeLogger { (): () => void; }
export interface ViewUpdateFn { (check: NodeCheckFn, view: ViewData): void; }
// helper functions to create an overloaded function type.
export interface NodeCheckFn {
@ -57,11 +71,12 @@ export interface NodeCheckFn {
v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, v9?: any): any;
}
export type ViewHandleEventFn =
(view: ViewData, nodeIndex: number, eventName: string, event: any) => boolean;
export const enum ArgumentType {Inline, Dynamic}
export interface ViewHandleEventFn {
(view: ViewData, nodeIndex: number, eventName: string, event: any): boolean;
}
/**
* Bitmask for ViewDefintion.flags.
*/
@ -221,11 +236,10 @@ export interface ElementDef {
* that are located on this element.
*/
allProviders: {[tokenKey: string]: NodeDef};
source: string;
handleEvent: ElementHandleEventFn;
}
export type ElementHandleEventFn = (view: ViewData, eventName: string, event: any) => boolean;
export interface ElementHandleEventFn { (view: ViewData, eventName: string, event: any): boolean; }
export interface ProviderDef {
token: any;
@ -250,10 +264,7 @@ export const enum DepFlags {
Value = 2 << 2,
}
export interface TextDef {
prefix: string;
source: string;
}
export interface TextDef { prefix: string; }
export interface QueryDef {
id: number;
@ -318,7 +329,7 @@ export const enum ViewState {
Destroyed = 1 << 3
}
export type DisposableFn = () => void;
export interface DisposableFn { (): void; }
/**
* Node instance data.
@ -428,9 +439,9 @@ export abstract class DebugContext {
abstract get providerTokens(): any[];
abstract get references(): {[key: string]: any};
abstract get context(): any;
abstract get source(): string;
abstract get componentRenderElement(): any;
abstract get renderNode(): any;
abstract logError(console: Console, ...values: any[]): void;
}
// -------------------------------------

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '../application_ref';
import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
import {SimpleChange} from '../change_detection/change_detection_util';
import {Injector} from '../di';
@ -18,7 +17,9 @@ import {Renderer, RendererType2} from '../render/api';
import {looseIdentical, stringify} from '../util';
import {expressionChangedAfterItHasBeenCheckedError, isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors';
import {DebugContext, ElementData, NodeData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types';
import {DebugContext, ElementData, NodeData, NodeDef, NodeFlags, NodeLogger, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types';
export const NOOP: any = () => {};
const _tokenKeyCache = new Map<any, string>();
@ -194,29 +195,13 @@ const VIEW_DEFINITION_CACHE = new WeakMap<any, ViewDefinition>();
export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition {
let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory);
if (!value) {
value = factory();
value = factory(() => NOOP);
value.factory = factory;
VIEW_DEFINITION_CACHE.set(factory, value);
}
return value;
}
export function sliceErrorStack(start: number, end: number): string {
let err: any;
try {
throw new Error();
} catch (e) {
err = e;
}
const stack = err.stack || '';
const lines = stack.split('\n');
if (lines[0].startsWith('Error')) {
// Chrome always adds the message to the stack as well...
start++;
end++;
}
return lines.slice(start, end).join('\n');
}
export function rootRenderNodes(view: ViewData): any[] {
const renderNodes: any[] = [];
visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);

View File

@ -18,9 +18,7 @@ import {checkAndUpdateQuery, createQuery, queryDef} from './query';
import {createTemplateData, createViewContainerData} from './refs';
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
import {ArgumentType, CheckType, ElementData, ElementDef, NodeData, NodeDef, NodeFlags, ProviderData, ProviderDef, RootData, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList, asTextData} from './types';
import {checkBindingNoChanges, isComponentView, resolveViewDefinition, viewParentEl} from './util';
const NOOP = (): any => undefined;
import {NOOP, checkBindingNoChanges, isComponentView, resolveViewDefinition, viewParentEl} from './util';
export function viewDef(
flags: ViewFlags, nodes: NodeDef[], updateDirectives?: ViewUpdateFn,
@ -137,6 +135,8 @@ export function viewDef(
const handleEvent: ViewHandleEventFn = (view, nodeIndex, eventName, event) =>
nodes[nodeIndex].element.handleEvent(view, eventName, event);
return {
// Will be filled later...
factory: undefined,
nodeFlags: viewNodeFlags,
rootNodeFlags: viewRootNodeFlags,
nodeMatchedQueries: viewMatchedQueries, flags,