refactor(devtools): use a custom logger and error

Use a custom logger that prefixes the messages with `[Angular DevTools]`;
Add ability for dev-only log messages;
Add Angular-DevTools-specific `Error`;

(cherry picked from commit 1371c946aa)
This commit is contained in:
Georgi Serev
2026-08-25 19:55:04 +03:00
committed by leonsenft
parent 55eeb46418
commit ea76f72428
29 changed files with 364 additions and 56 deletions
@@ -10,5 +10,6 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib:client_event_subscribers",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree",
"//devtools/projects/ng-devtools-backend/src/lib/router-tree",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
],
)
@@ -7,11 +7,7 @@ ts_project(
srcs = ["index.ts"],
deps = [
":client_event_subscribers",
"//devtools/projects/ng-devtools-backend/src/lib/component-inspector",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest",
"//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler",
"//devtools/projects/ng-devtools-backend/src/lib/shared/ng-debug-api",
"//devtools/projects/ng-devtools-backend/src/lib/shared/state-serializer",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/protocol",
],
)
@@ -61,6 +57,7 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/shared/ng-debug-api",
"//devtools/projects/ng-devtools-backend/src/lib/shared/state-serializer",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:serialization",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:signal-graph-ref",
"//devtools/projects/protocol",
@@ -43,7 +43,10 @@ describe('ClientEventSubscriber', () => {
});
it('should setup inspector', () => {
subscribeToClientEvents(messageBusMock, {profiler: MockProfiler});
subscribeToClientEvents(messageBusMock, {
devtoolsDevMode: true,
depsForTestOnly: {profiler: MockProfiler},
});
expect(messageBusMock.on).toHaveBeenCalledWith('inspectorStart', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('inspectorEnd', jasmine.any(Function));
@@ -53,7 +53,7 @@ import {
import {start as startProfiling, stop as stopProfiling} from './profiling/capture';
import {disableTimingAPI, enableTimingAPI} from './profiling/timing-api';
import {getProfiler, Profiler} from './profiling/profiler';
import {ComponentTreeNode} from './shared/interfaces';
import {ComponentTreeNode, DevtoolsBackendConfig} from './shared/interfaces';
import {
ngDebugClient,
ngDebugDependencyInjectionApiIsSupported,
@@ -75,16 +75,20 @@ import {
removeHydrationHighlights,
} from './hydration/hydration-highlighting';
import {removeAllHighlights} from './shared/highlighter';
import {debugLog, log, setupLogging} from './shared/utils/log';
type InspectorRef = {ref: ComponentInspector | null};
export const subscribeToClientEvents = (
messageBus: MessageBus<Events>,
depsForTestOnly?: {
profiler?: new (...args: any[]) => Profiler;
config?: DevtoolsBackendConfig & {
depsForTestOnly?: {
profiler?: new (...args: any[]) => Profiler;
};
},
): void => {
const inspector: InspectorRef = {ref: null};
setupLogging(config?.devtoolsDevMode ?? false);
messageBus.on('shutdown', shutdownCallback(messageBus));
@@ -123,9 +127,9 @@ export const subscribeToClientEvents = (
const SAFE_LOG_LEVELS = new Set(['log', 'info', 'warn', 'debug', 'error']);
messageBus.on('log', ({message, level}) => {
if (SAFE_LOG_LEVELS.has(level)) {
console[level](`[Angular DevTools]: ${message}`);
log[level](message);
} else {
console.warn(`[Angular DevTools]: Invalid log level attempted: ${level}`);
debugLog.warn(`Invalid log level attempted: ${level}`);
}
});
@@ -139,7 +143,7 @@ export const subscribeToClientEvents = (
// update requests, instead we want to request an update at most
// once every 250ms
runOutsideAngular(() => {
getProfiler(depsForTestOnly)
getProfiler(config?.depsForTestOnly)
.changeDetection$.pipe(debounceTime(250))
.subscribe(() => messageBus.emit('componentTreeDirty'));
});
@@ -198,7 +202,7 @@ const navigateRouteCallback = (messageBus: MessageBus<Events>) => (path: string)
if (router) {
ngDebugClient().ɵnavigateByUrl?.(router, path);
} else {
console.warn('Router not found or navigateByUrl method not available');
log.warn('Router not found or navigateByUrl method not available');
}
};
@@ -255,7 +259,7 @@ const getNestedPropertiesCallback =
for (const prop of propPath) {
data = unwrapSignal(data[prop]);
if (!data) {
console.error('Cannot access the properties', propPath, 'of', node);
log.error('Cannot access the properties', propPath, 'of', node);
}
}
messageBus.emit('nestedProperties', [
@@ -313,7 +317,7 @@ const getSignalNestedPropertiesCallback =
for (const prop of propPath) {
data = (data as Record<string, object>)[prop];
if (!data) {
console.error('Cannot access the properties', propPath, 'of', node);
log.error('Cannot access the properties', propPath, 'of', node);
}
}
messageBus.emit('signalNestedProperties', [
@@ -26,6 +26,7 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker",
"//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
"//devtools/projects/protocol",
],
)
@@ -20,7 +20,9 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/ng-debug-api",
"//devtools/projects/ng-devtools-backend/src/lib/shared/state-serializer",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
"//devtools/projects/protocol",
],
)
@@ -52,6 +52,8 @@ import {AcxChangeDetectionStrategy, ChangeDetectionStrategy, Framework} from '..
import {mutateNestedProp} from '../property-mutation/property-mutation';
import {getLViewFromDirectiveOrElementInstance} from '../tree-strategies/ltree';
import {getAppRoots} from './get-roots';
import {log} from '../../shared/utils/log';
import {AngularDevtoolsError} from '../../shared/utils/error';
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
@@ -263,7 +265,7 @@ const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
};
}
default: {
throw new Error(`Unknown framework: "${framework}".`);
throw new AngularDevtoolsError(`Unknown framework: "${framework}".`);
}
}
}
@@ -273,7 +275,7 @@ const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
try {
return dir.constructor.ɵcmp ? dir.constructor.ɵcmp[key] : dir.constructor.ɵdir[key];
} catch {
console.warn(`Could not find metadata for key: ${key} in directive:`, dir);
log.warn(`Could not find metadata for key: ${key} in directive:`, dir);
return undefined;
}
};
@@ -311,7 +313,9 @@ export function getDirectiveCdStrategy(dir: any): ChangeDetection | undefined {
return undefined;
default:
throw new Error(`Unknown framework: "${(metadata as {framework: string}).framework}".`);
throw new AngularDevtoolsError(
`Unknown framework: "${(metadata as {framework: string}).framework}".`,
);
}
}
@@ -449,7 +453,7 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
const metadata = getInjectorMetadata(injector);
if (metadata === null) {
console.error('Angular DevTools: Could not serialize injector.', injector);
log.error('Could not serialize injector.', injector);
return null;
}
@@ -480,7 +484,7 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
return {type: 'environment', name: stripUnderscore(metadata.source ?? ''), providers};
}
console.error('Angular DevTools: Could not serialize injector.', injector);
log.error('Could not serialize injector.', injector);
return null;
}
@@ -545,7 +549,7 @@ function elementToDirectiveNames(element: HTMLElement): string[] {
export function getElementInjectorElement(elementInjector: Injector): HTMLElement {
if (!isElementInjector(elementInjector)) {
throw new Error('Injector is not an element injector');
throw new AngularDevtoolsError('Injector is not an element injector');
}
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
@@ -742,7 +746,7 @@ export const updateState = (updatedStateData: UpdatedStateData): void => {
const ng = ngDebugClient();
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest());
if (!node) {
console.warn(
log.warn(
'Could not update the state of component',
updatedStateData,
'because the component was not found',
@@ -771,7 +775,7 @@ export function logValue(valueInfo: {
}): void {
const node = queryDirectiveForest(valueInfo.directiveId.element, buildDirectiveForest());
if (!node) {
console.warn(
log.warn(
'Could not log the value of component',
valueInfo,
'because the directive was not found',
@@ -782,27 +786,27 @@ export function logValue(valueInfo: {
if (node.directives && valueInfo.directiveId.directive !== undefined) {
const directiveInstance = node.directives[valueInfo.directiveId.directive].instance;
if (valueInfo.keyPath === null) {
logToConsole(directiveInstance);
logSignalToConsole(directiveInstance);
return;
}
const value = valueInfo.keyPath.reduce((obj, key) => obj && obj[key], directiveInstance);
logToConsole(value);
logSignalToConsole(value);
return;
}
if (node.component) {
const compInstance = node.component.instance;
if (valueInfo.keyPath === null) {
logToConsole(compInstance);
logSignalToConsole(compInstance);
return;
}
const value = valueInfo.keyPath.reduce((obj, key) => obj && obj[key], compInstance);
logToConsole(value);
logSignalToConsole(value);
return;
}
}
function logToConsole(value: unknown) {
function logSignalToConsole(value: unknown) {
// tslint:disable-next-line:no-console
console.log(unwrapSignal(value));
}
@@ -10,6 +10,7 @@ import {ElementPosition} from '../../../../protocol';
import {ComponentTreeNode, DirectiveInstance} from '../shared/interfaces';
import {getProfiler} from '../profiling/profiler';
import {IdentityTracker, IndexedNode, IndexingOutput} from './identity-tracker/identity-tracker';
import {debugLog} from '../shared/utils/log';
// Global reference.
let directiveForestManager: DirectiveForestManager;
@@ -27,7 +28,7 @@ export class DirectiveForestManager {
getDirectivePosition(dir: DirectiveInstance): ElementPosition | undefined {
const result = this._tracker.getDirectivePosition(dir);
if (result === undefined) {
console.warn('Unable to find position of', dir);
debugLog.warn('Unable to find position of', dir);
}
return result;
}
@@ -35,7 +36,7 @@ export class DirectiveForestManager {
getDirectiveId(dir: DirectiveInstance): number | undefined {
const result = this._tracker.getDirectiveId(dir);
if (result === undefined) {
console.warn('Unable to find ID of', result);
debugLog.warn('Unable to find ID of', result);
}
return result;
}
@@ -7,6 +7,7 @@ ts_project(
srcs = ["property-mutation.ts"],
deps = [
"//:node_modules/@angular/core",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
],
)
@@ -8,6 +8,7 @@
import type {Signal, WritableSignal} from '@angular/core';
import {isSignal} from '../../shared/utils/general';
import {AngularDevtoolsError} from '../../shared/utils/error';
/** Represents a property access operation. */
interface PropertyAccess {
@@ -76,13 +77,13 @@ interface PropertyAccess {
* (`receiver[prop] = newValue;`).
*/
export function mutateNestedProp(obj: any, keyPath: string[], newValue: unknown): void {
if (keyPath.length === 0) throw new Error('At least one key is required.');
if (keyPath.length === 0) throw new AngularDevtoolsError('At least one key is required.');
const nestedProps = Array.from(getNestedProps(obj, keyPath));
// Check for nested signals.
const signalCount = nestedProps.filter((prop) => isSignal(prop.value)).length;
if (signalCount > 1) throw new Error('Cannot mutate nested signals.');
if (signalCount > 1) throw new AngularDevtoolsError('Cannot mutate nested signals.');
// Check for a single signal.
const signalIndex = nestedProps.findIndex((prop) => isSignal(prop.value));
@@ -95,7 +96,7 @@ export function mutateNestedProp(obj: any, keyPath: string[], newValue: unknown)
.slice(0, signalIndex + 1)
.map((prop) => prop.key)
.join('.');
throw new Error(`Cannot mutate a readonly signal at \`${propPath}\`.`);
throw new AngularDevtoolsError(`Cannot mutate a readonly signal at \`${propPath}\`.`);
}
sig.value.set(immutableUpdate(props, newValue));
@@ -107,7 +108,7 @@ export function mutateNestedProp(obj: any, keyPath: string[], newValue: unknown)
const finalProp = nestedProps[nestedProps.length - 1];
const descriptor = getInheritedPropertyDescriptor(finalProp.receiver, finalProp.key);
if (descriptor && descriptor.get && !descriptor.set) {
throw new Error(`Cannot mutate getter property: ${finalProp.key}`);
throw new AngularDevtoolsError(`Cannot mutate getter property: ${finalProp.key}`);
}
finalProp.receiver[finalProp.key] = newValue;
@@ -124,15 +125,19 @@ function* getNestedProps(
// Prevent Prototype Pollution
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
throw new Error(`Access to property \`${key}\` is blocked for security reasons.`);
throw new AngularDevtoolsError(
`Access to property \`${key}\` is blocked for security reasons.`,
);
}
if (Array.isArray(receiver) && parseInt(key) >= receiver.length) {
throw new Error(`Cannot access index ${key} for array of length ${receiver.length}.`);
throw new AngularDevtoolsError(
`Cannot access index ${key} for array of length ${receiver.length}.`,
);
}
if (!(key in receiver)) {
throw new Error(`Property \`${key}\` is not defined on the object.`);
throw new AngularDevtoolsError(`Property \`${key}\` is not defined on the object.`);
}
const value = receiver[key];
@@ -170,7 +175,9 @@ function immutableUpdate(props: PropertyAccess[], newValue: unknown): any {
[prop.key]: immutableUpdate(remainingProps, newValue),
};
} else {
throw new Error(`Cannot immutably update type: ${prop.receiver.constructor.name}`);
throw new AngularDevtoolsError(
`Cannot immutably update type: ${prop.receiver.constructor.name}`,
);
}
}
@@ -210,7 +217,7 @@ function isWritableSignal<Value>(sig: Signal<Value>): sig is WritableSignal<Valu
function assertSafeToImmutablyUpdate(obj: {}): void {
// `new MyClass()` case.
if (obj.constructor !== Object) {
throw new Error(`Cannot immutably update type: ${obj.constructor.name}`);
throw new AngularDevtoolsError(`Cannot immutably update type: ${obj.constructor.name}`);
}
// `{ get foo() { return 'foo'; } }` case.
@@ -220,7 +227,7 @@ function assertSafeToImmutablyUpdate(obj: {}): void {
if (!descriptor) continue;
if (descriptor.get || descriptor.set) {
throw new Error('Cannot immutably update object with getters or setters.');
throw new AngularDevtoolsError('Cannot immutably update object with getters or setters.');
}
}
}
@@ -18,6 +18,7 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/utils:control-flow",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/ng-debug-api",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
"//devtools/projects/protocol",
],
@@ -20,6 +20,7 @@ import {
createControlFlowTreeNode,
isControlFlowBlock,
} from '../utils/control-flow';
import {AngularDevtoolsError} from '../../shared/utils/error';
interface TreeExtractionContext {
blocksIterator: ControlFlowBlocksIterator;
@@ -107,7 +108,7 @@ function groupControlFlowBlocksChildren(
) {
const currentBlock = ctx.blocksIterator.currentBlock;
if (!currentBlock) {
throw new Error('There is no current block in the control flow block iterator.');
throw new AngularDevtoolsError('There is no current block in the control flow block iterator.');
}
ctx.blocksIterator.advance();
@@ -9,6 +9,7 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/highlighter",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/protocol",
],
)
@@ -17,6 +17,7 @@ import {
HighlightType,
} from '../shared/highlighter/highlights';
import {ComponentTreeNode} from '../shared/interfaces';
import {AngularDevtoolsError} from '../shared/utils/error';
export function highlightHydrationNodes(): void {
const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest();
@@ -58,7 +59,7 @@ function highlightHydrationElement(node: Element, {status}: HydrationStatus) {
template = hydrationSkippedHighlightTemplate;
break;
default:
throw new Error(`Unsupported hydration status highlighting: ${status}`);
throw new AngularDevtoolsError(`Unsupported hydration status highlighting: ${status}`);
}
highlightElement(node, template, {'icon': [status]});
@@ -7,9 +7,13 @@
*/
import {Events, MessageBus} from '../../../protocol';
import {DevtoolsBackendConfig} from './shared/interfaces';
import {subscribeToClientEvents} from './client-event-subscribers';
export const initializeMessageBus = (messageBus: MessageBus<Events>) => {
subscribeToClientEvents(messageBus);
export const initializeMessageBus = (
messageBus: MessageBus<Events>,
config?: DevtoolsBackendConfig,
) => {
subscribeToClientEvents(messageBus, config);
};
@@ -17,7 +17,9 @@ ts_project(
"//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler",
"//devtools/projects/ng-devtools-backend/src/lib/shared:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/shared/highlighter",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:general",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
"//devtools/projects/protocol",
],
)
@@ -22,6 +22,8 @@ import {DirectiveForestManager, getDirectiveForestManager} from '../directive-fo
import {IdentityTracker} from '../directive-forest/identity-tracker/identity-tracker';
import {getProfiler, Hooks} from './profiler';
import {getDirectiveName} from '../directive-forest/component-tree/component-tree';
import {debugLog} from '../shared/utils/log';
import {AngularDevtoolsError} from '../shared/utils/error';
let inProgress = false;
let inChangeDetection = false;
@@ -36,7 +38,7 @@ const DIRECTIVE_CONTROL_FLOW: {[key in ControlFlowBlockType]: ElementProfile['ty
export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
if (inProgress) {
throw new Error('Recording already in progress');
throw new AngularDevtoolsError('Recording already in progress');
}
eventMap = new Map<any, DirectiveProfile>();
inProgress = true;
@@ -131,7 +133,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
profile.changeDetection = current + duration;
frameDuration += duration;
} else {
console.warn('Could not find profile for', component);
debugLog.warn('Could not find profile for', component);
}
},
onDestroy(
@@ -183,7 +185,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
return;
}
if (!dir) {
console.warn('Could not find directive in onLifecycleHook callback', directive, hookName);
debugLog.warn('Could not find directive in onLifecycleHook callback', directive, hookName);
return;
}
const duration = performance.now() - startTimestamp;
@@ -216,7 +218,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
return;
}
if (!entry) {
console.warn(
debugLog.warn(
'Could not find directive or component in onOutputEnd callback',
componentOrDirective,
outputName,
@@ -272,7 +274,7 @@ const insertElementProfile = (
const pos = position[i];
if (!frames[pos]) {
// TODO(mgechev): consider how to ensure we don't hit this case
console.warn('Unable to find parent node for', profile, original);
debugLog.warn('Unable to find parent node for', profile, original);
return;
}
frames = frames[pos].children;
@@ -12,6 +12,8 @@ ts_project(
deps = [
"//:node_modules/@angular/core",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
"//devtools/projects/protocol",
],
)
@@ -25,6 +27,7 @@ ts_test_library(
deps = [
":highlighter",
"//:node_modules/@angular/core",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:log",
],
)
@@ -9,6 +9,7 @@
import {EventEmitter} from '@angular/core';
import {Highlight, HighlightLabelDefinition, HighlightTemplate, HighlightType} from './highlights';
import {OVERLAY_CLASS} from './dom';
import {debugLog} from '../utils/log';
function createTemplate(overrides?: Partial<HighlightTemplate<any>>): HighlightTemplate<any> {
return {
@@ -222,12 +223,12 @@ describe('Highlight', () => {
const highlight = createHighlight(createTemplate(), {
title: document.createElement('div'),
});
spyOn(console, 'warn');
spyOn(debugLog, 'warn');
highlight.destroy();
highlight.destroy();
expect(console.warn).toHaveBeenCalledOnceWith(
expect(debugLog.warn).toHaveBeenCalledOnceWith(
'The highlight has already been destroyed. Check references storing.',
);
});
@@ -9,6 +9,8 @@
import {EventEmitter} from '@angular/core';
import {HydrationStatus} from '../../../../../protocol';
import {positionOverlayElement, setLabelElementVisibility} from './dom';
import {debugLog} from '../utils/log';
import {AngularDevtoolsError} from '../utils/error';
//
// Types & classes
@@ -112,7 +114,7 @@ export class Highlight<T extends HighlightLabelDefinition = HighlightLabelDefini
// Getting the warning, means that there might be a problem
// with the code (i.e. there is chance for a memory leak).
if (this.destroyed) {
console.warn('The highlight has already been destroyed. Check references storing.');
debugLog.warn('The highlight has already been destroyed. Check references storing.');
return;
}
this.destroyEvents.emit([this]);
@@ -152,7 +154,7 @@ function validateTemplateLabels(template: HighlightTemplate) {
for (const {x} of Object.values(template.labels)) {
if (usedXPos.has(x)) {
throw new Error(
throw new AngularDevtoolsError(
`The template (type: ${template.type}) has multiple labels with '${x}' X position.`,
);
}
@@ -219,7 +221,7 @@ function createHydrationHighlightTemplate(
} else if (type === 'skipped') {
icon = HYDRATION_SKIPPED_SVG;
} else {
throw new Error(`No icon specified for type ${type}`);
throw new AngularDevtoolsError(`No icon specified for type ${type}`);
}
const svg = new DOMParser().parseFromString(icon, 'image/svg+xml')
@@ -8,6 +8,10 @@
import {DevToolsNode} from '../../../../protocol';
export interface DevtoolsBackendConfig {
devtoolsDevMode?: boolean;
}
export interface DebuggingAPI {
getComponent(node: Node): ComponentInstance;
getDirectives(node: Node): DirectiveInstance[];
@@ -12,6 +12,7 @@ ng_project(
"//:node_modules/@angular/core",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest:core-enums",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree:get-roots",
"//devtools/projects/ng-devtools-backend/src/lib/shared/utils:error",
"//devtools/projects/protocol",
],
)
@@ -9,12 +9,13 @@
import type {ɵFrameworkAgnosticGlobalUtils as GlobalUtils} from '@angular/core';
import {getAppRoots} from '../../directive-forest/component-tree/get-roots';
import {Framework} from '../../directive-forest/core-enums';
import {AngularDevtoolsError} from '../utils/error';
/** Returns a handle to window.ng APIs (global angular debugging). */
export const ngDebugClient = () => {
if (typeof (window as any).ng === 'undefined') {
throw new Error(
'Angular DevTools: Angular debugging APIs are not available. Ensure that your Angular app is in development mode and does not invoke `enableProdMode()`.',
throw new AngularDevtoolsError(
'Angular debugging APIs are not available. Ensure that your Angular app is in development mode and does not invoke `enableProdMode()`.',
);
}
return (window as any).ng as Partial<GlobalUtils>;
@@ -23,13 +23,25 @@ ts_project(
],
)
ts_project(
name = "log",
srcs = ["log.ts"],
)
ts_project(
name = "error",
srcs = ["error.ts"],
)
ts_test_library(
name = "test_lib",
srcs = [
"log.spec.ts",
"serialization.spec.ts",
"signal-graph-ref.spec.ts",
],
deps = [
":log",
":serialization",
":signal-graph-ref",
"//:node_modules/@angular/core",
@@ -0,0 +1,15 @@
/**
* @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.dev/license
*/
/** Angular DevTools error. Use instead of `Error`. */
export class AngularDevtoolsError extends Error {
constructor(message: string) {
super(message);
this.name = 'AngularDevtoolsError';
}
}
@@ -0,0 +1,170 @@
/**
* @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.dev/license
*/
// tslint:disable:no-console
import {DEBUG_LOG_MSG_PREFIX, LOG_MSG_PREFIX, debugLog, log, setupLogging} from './log';
describe('log utils', () => {
afterEach(() => setupLogging(false));
describe('log', () => {
it('should log with the user-facing prefix', () => {
spyOn(console, 'log');
log('hello', 42);
expect(console.log).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'hello', 42);
});
it('should log warnings with the user-facing prefix', () => {
spyOn(console, 'warn');
log.warn('careful');
expect(console.warn).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'careful');
});
it('should log debug messages with the user-facing prefix', () => {
spyOn(console, 'debug');
log.debug('debugging');
expect(console.debug).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'debugging');
});
it('should log info messages with the user-facing prefix', () => {
spyOn(console, 'info');
log.info('info');
expect(console.info).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'info');
});
it('should log errors with the user-facing prefix', () => {
spyOn(console, 'error');
log.error('oops');
expect(console.error).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'oops');
});
it('should support log.log for compatibility', () => {
spyOn(console, 'log');
log.log('compat');
expect(console.log).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'compat');
});
it('should log regardless of dev mode', () => {
spyOn(console, 'log');
setupLogging(true);
log('dev');
expect(console.log).toHaveBeenCalledWith(LOG_MSG_PREFIX, 'dev');
});
});
describe('debugLog', () => {
it('should not log when dev mode is disabled', () => {
spyOn(console, 'log');
debugLog('hidden');
expect(console.log).not.toHaveBeenCalled();
});
it('should not log warnings when dev mode is disabled', () => {
spyOn(console, 'warn');
debugLog.warn('hidden');
expect(console.warn).not.toHaveBeenCalled();
});
it('should not log debug messages when dev mode is disabled', () => {
spyOn(console, 'debug');
debugLog.debug('hidden');
expect(console.debug).not.toHaveBeenCalled();
});
it('should not log info messages when dev mode is disabled', () => {
spyOn(console, 'info');
debugLog.info('hidden');
expect(console.info).not.toHaveBeenCalled();
});
it('should not log errors when dev mode is disabled', () => {
spyOn(console, 'error');
debugLog.error('hidden');
expect(console.error).not.toHaveBeenCalled();
});
it('should log with the dev-only prefix when dev mode is enabled', () => {
spyOn(console, 'log');
setupLogging(true);
debugLog('shown', {a: 1});
expect(console.log).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'shown', {a: 1});
});
it('should log warnings with the dev-only prefix when dev mode is enabled', () => {
spyOn(console, 'warn');
setupLogging(true);
debugLog.warn('shown');
expect(console.warn).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'shown');
});
it('should log debug messages with the dev-only prefix when dev mode is enabled', () => {
spyOn(console, 'debug');
setupLogging(true);
debugLog.debug('shown');
expect(console.debug).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'shown');
});
it('should log info messages with the dev-only prefix when dev mode is enabled', () => {
spyOn(console, 'info');
setupLogging(true);
debugLog.info('shown');
expect(console.info).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'shown');
});
it('should log errors with the dev-only prefix when dev mode is enabled', () => {
spyOn(console, 'error');
setupLogging(true);
debugLog.error('shown');
expect(console.error).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'shown');
});
it('should support debugLog.log for compatibility', () => {
spyOn(console, 'log');
setupLogging(true);
debugLog.log('compat');
expect(console.log).toHaveBeenCalledWith(DEBUG_LOG_MSG_PREFIX, 'compat');
});
});
});
@@ -0,0 +1,62 @@
/**
* @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.dev/license
*/
let isNgDevtoolsDevMode: boolean = false;
export const LOG_MSG_PREFIX = '[Angular DevTools]';
export const DEBUG_LOG_MSG_PREFIX = '[Angular DevTools DEV]';
type LogType = 'log' | 'warn' | 'debug' | 'info' | 'error';
export function setupLogging(devtoolsDevMode: boolean) {
isNgDevtoolsDevMode = devtoolsDevMode;
}
function logInternal(logType: LogType, prefix: string, logMsg: boolean, ...data: any[]) {
if (logMsg) {
console[logType](prefix, ...data);
}
}
const debugLogInternal = (logType: LogType, ...data: any[]) =>
logInternal(logType, DEBUG_LOG_MSG_PREFIX, isNgDevtoolsDevMode, ...data);
const userLogInternal = (logType: LogType, ...data: any[]) =>
logInternal(logType, LOG_MSG_PREFIX, true, ...data);
/**
* Log Angular DevTools dev-only messages.
* Use `log` for user-facing messages.
*/
export function debugLog(...data: any[]) {
debugLogInternal('log', ...data);
}
debugLog.warn = (...data: any[]) => debugLogInternal('warn', ...data);
debugLog.debug = (...data: any[]) => debugLogInternal('debug', ...data);
debugLog.info = (...data: any[]) => debugLogInternal('info', ...data);
debugLog.error = (...data: any[]) => debugLogInternal('error', ...data);
// For compatibility purposes
debugLog.log = (...data: any[]) => debugLogInternal('log', ...data);
/**
* Log Angular DevTools user-facing messages.
* Use `debugLog` for dev-only messages.
*/
export function log(...data: any[]) {
userLogInternal('log', ...data);
}
log.warn = (...data: any[]) => userLogInternal('warn', ...data);
log.debug = (...data: any[]) => userLogInternal('debug', ...data);
log.info = (...data: any[]) => userLogInternal('info', ...data);
log.error = (...data: any[]) => userLogInternal('error', ...data);
// For compatibility purposes
log.log = (...data: any[]) => userLogInternal('log', ...data);
@@ -14,3 +14,4 @@ export * from './lib';
export {findNodeFromSerializedPosition} from './lib/directive-forest/component-tree/component-tree';
export {viewSourceFromRouter} from './lib/client-event-subscribers';
export {type RoutePropertyType} from './lib/router-tree/router-tree';
export type {DevtoolsBackendConfig} from './lib/shared/interfaces';
@@ -44,4 +44,7 @@ export const DEMO_ROUTES: Routes = [
initializeMessageBus(
new ZoneUnawareIFrameMessageBus(DEVTOOLS_BACKEND_URI, DEVTOOLS_FRONTEND_URI, () => window.parent),
{
devtoolsDevMode: true,
},
);