From ea76f7242898235bc8ac7aa132f048c57dfeecef Mon Sep 17 00:00:00 2001 From: Georgi Serev Date: Tue, 25 Aug 2026 19:55:04 +0300 Subject: [PATCH] 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 1371c946aa9ac5e857059cfb6394d5c2a0d83e1a) --- .../ng-devtools-backend/src/BUILD.bazel | 1 + .../ng-devtools-backend/src/lib/BUILD.bazel | 7 +- .../src/lib/client-event-subscribers.spec.ts | 5 +- .../src/lib/client-event-subscribers.ts | 22 ++- .../src/lib/directive-forest/BUILD.bazel | 1 + .../component-tree/BUILD.bazel | 2 + .../component-tree/component-tree.ts | 30 ++-- .../src/lib/directive-forest/manager.ts | 5 +- .../property-mutation/BUILD.bazel | 1 + .../property-mutation/property-mutation.ts | 27 +-- .../tree-strategies/BUILD.bazel | 1 + .../tree-strategies/render-tree.ts | 3 +- .../src/lib/hydration/BUILD.bazel | 1 + .../lib/hydration/hydration-highlighting.ts | 3 +- .../ng-devtools-backend/src/lib/index.ts | 8 +- .../src/lib/profiling/BUILD.bazel | 2 + .../src/lib/profiling/capture.ts | 12 +- .../src/lib/shared/highlighter/BUILD.bazel | 3 + .../lib/shared/highlighter/highlights.spec.ts | 5 +- .../src/lib/shared/highlighter/highlights.ts | 8 +- .../src/lib/shared/interfaces.ts | 4 + .../src/lib/shared/ng-debug-api/BUILD.bazel | 1 + .../lib/shared/ng-debug-api/ng-debug-api.ts | 5 +- .../src/lib/shared/utils/BUILD.bazel | 12 ++ .../src/lib/shared/utils/error.ts | 15 ++ .../src/lib/shared/utils/log.spec.ts | 170 ++++++++++++++++++ .../src/lib/shared/utils/log.ts | 62 +++++++ .../ng-devtools-backend/src/public-api.ts | 1 + devtools/src/app/demo-app/demo-app.routes.ts | 3 + 29 files changed, 364 insertions(+), 56 deletions(-) create mode 100644 devtools/projects/ng-devtools-backend/src/lib/shared/utils/error.ts create mode 100644 devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.spec.ts create mode 100644 devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.ts diff --git a/devtools/projects/ng-devtools-backend/src/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/BUILD.bazel index 307b1f206bd..bae52411c3d 100644 --- a/devtools/projects/ng-devtools-backend/src/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel index 2bf2d764a51..07204dc482d 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel @@ -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", diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts index a814e0890c7..63122a21e54 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts @@ -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)); diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index 2b25a74ee9b..d12d93594a1 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -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, - 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) => (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)[prop]; if (!data) { - console.error('Cannot access the properties', propPath, 'of', node); + log.error('Cannot access the properties', propPath, 'of', node); } } messageBus.emit('signalNestedProperties', [ diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel index 1850fce51ed..fcd51565529 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/BUILD.bazel index a226f8df8b6..f55d384bce9 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/component-tree.ts index 0135176e5ca..689ce5b5b55 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/component-tree/component-tree.ts @@ -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(); export const nodeInjectorToResolutionPath = new WeakMap(); @@ -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 { 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)); } diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts index cfef9ab363d..06f0f36df02 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts @@ -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; } diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/BUILD.bazel index 0c6c7ca63be..a4897693bbd 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/property-mutation.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/property-mutation.ts index df51d8a5ebf..6b6e54336c5 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/property-mutation.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/property-mutation/property-mutation.ts @@ -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(sig: Signal): sig is WritableSignal) => { - subscribeToClientEvents(messageBus); +export const initializeMessageBus = ( + messageBus: MessageBus, + config?: DevtoolsBackendConfig, +) => { + subscribeToClientEvents(messageBus, config); }; diff --git a/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel index fb96f4c77f1..ac12c6f5f9a 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts index 5077e4d5526..32638dc6de9 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts @@ -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(); inProgress = true; @@ -131,7 +133,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { 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 => { 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 => { 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; diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/BUILD.bazel index c57a4809c0a..fe1d8fdab8f 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/BUILD.bazel @@ -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", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.spec.ts index dd9b46d0a09..812d652061e 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.spec.ts @@ -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 { 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.', ); }); diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.ts b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.ts index 686544d5c65..dd64bc1e8ea 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/highlighter/highlights.ts @@ -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 { 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; diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/utils/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/BUILD.bazel index caf24724815..edc4e5267f8 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/shared/utils/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/BUILD.bazel @@ -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", diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/utils/error.ts b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/error.ts new file mode 100644 index 00000000000..362c3b5e664 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/error.ts @@ -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'; + } +} diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.spec.ts new file mode 100644 index 00000000000..f59f09b80fe --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.spec.ts @@ -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'); + }); + }); +}); diff --git a/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.ts b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.ts new file mode 100644 index 00000000000..62fb88de82e --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/shared/utils/log.ts @@ -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); diff --git a/devtools/projects/ng-devtools-backend/src/public-api.ts b/devtools/projects/ng-devtools-backend/src/public-api.ts index 7e88227c763..1432ea7e2e2 100644 --- a/devtools/projects/ng-devtools-backend/src/public-api.ts +++ b/devtools/projects/ng-devtools-backend/src/public-api.ts @@ -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'; diff --git a/devtools/src/app/demo-app/demo-app.routes.ts b/devtools/src/app/demo-app/demo-app.routes.ts index 7862d9cbf7a..37a3157c376 100644 --- a/devtools/src/app/demo-app/demo-app.routes.ts +++ b/devtools/src/app/demo-app/demo-app.routes.ts @@ -44,4 +44,7 @@ export const DEMO_ROUTES: Routes = [ initializeMessageBus( new ZoneUnawareIFrameMessageBus(DEVTOOLS_BACKEND_URI, DEVTOOLS_FRONTEND_URI, () => window.parent), + { + devtoolsDevMode: true, + }, );