angular-scan 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/angular-scan.mjs +685 -0
- package/fesm2022/angular-scan.mjs.map +1 -0
- package/package.json +14 -3
- package/types/angular-scan.d.ts +59 -0
- package/ng-package.json +0 -7
- package/src/lib/angular-scan.spec.ts +0 -22
- package/src/lib/angular-scan.ts +0 -9
- package/src/lib/component-tracker.ts +0 -80
- package/src/lib/ng-debug.ts +0 -23
- package/src/lib/overlay/canvas-overlay.ts +0 -121
- package/src/lib/overlay/overlay.service.ts +0 -85
- package/src/lib/provide-angular-scan.ts +0 -77
- package/src/lib/scan.ts +0 -176
- package/src/lib/scanner.service.ts +0 -197
- package/src/lib/tokens.ts +0 -7
- package/src/lib/toolbar/toolbar.component.html +0 -66
- package/src/lib/toolbar/toolbar.component.scss +0 -147
- package/src/lib/toolbar/toolbar.component.ts +0 -29
- package/src/lib/types.ts +0 -36
- package/src/public-api.ts +0 -7
- package/tsconfig.lib.json +0 -13
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -10
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, Injectable, InjectionToken, inject, DOCUMENT, PLATFORM_ID, isDevMode, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideEnvironmentInitializer, EnvironmentInjector, ApplicationRef, createComponent } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
class ComponentTracker {
|
|
6
|
+
// WeakMap: component instance → stats. GC-safe: entries are freed when
|
|
7
|
+
// the component instance is garbage collected after destruction.
|
|
8
|
+
byInstance = new WeakMap();
|
|
9
|
+
// Strong map: host element → component instance. Used for mutation-to-component
|
|
10
|
+
// lookup. Entries must be manually removed when components are destroyed.
|
|
11
|
+
byElement = new Map();
|
|
12
|
+
_totalRenders = signal(0, ...(ngDevMode ? [{ debugName: "_totalRenders" }] : []));
|
|
13
|
+
_totalUnnecessary = signal(0, ...(ngDevMode ? [{ debugName: "_totalUnnecessary" }] : []));
|
|
14
|
+
_trackedComponents = signal([], ...(ngDevMode ? [{ debugName: "_trackedComponents" }] : []));
|
|
15
|
+
totalRenders = this._totalRenders.asReadonly();
|
|
16
|
+
totalUnnecessary = this._totalUnnecessary.asReadonly();
|
|
17
|
+
trackedComponents = this._trackedComponents.asReadonly();
|
|
18
|
+
/**
|
|
19
|
+
* Record a render event for a component.
|
|
20
|
+
* NOTE: Must be called outside of a profiler callback to avoid triggering CD.
|
|
21
|
+
* Use queueMicrotask() to defer calls from profiler events.
|
|
22
|
+
*/
|
|
23
|
+
recordRender(instance, hostElement, kind) {
|
|
24
|
+
let stats = this.byInstance.get(instance);
|
|
25
|
+
if (!stats) {
|
|
26
|
+
stats = {
|
|
27
|
+
componentName: instance.constructor.name,
|
|
28
|
+
hostElement,
|
|
29
|
+
totalRenders: 0,
|
|
30
|
+
unnecessaryRenders: 0,
|
|
31
|
+
lastRenderKind: kind,
|
|
32
|
+
lastRenderTimestamp: 0,
|
|
33
|
+
};
|
|
34
|
+
this.byInstance.set(instance, stats);
|
|
35
|
+
this.byElement.set(hostElement, instance);
|
|
36
|
+
}
|
|
37
|
+
stats.totalRenders++;
|
|
38
|
+
stats.lastRenderKind = kind;
|
|
39
|
+
stats.lastRenderTimestamp = Date.now();
|
|
40
|
+
if (kind === 'unnecessary') {
|
|
41
|
+
stats.unnecessaryRenders++;
|
|
42
|
+
this._totalUnnecessary.update(n => n + 1);
|
|
43
|
+
}
|
|
44
|
+
this._totalRenders.update(n => n + 1);
|
|
45
|
+
return stats;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Rebuild the trackedComponents signal from the current element map.
|
|
49
|
+
* Call once per tick after all recordRender() calls.
|
|
50
|
+
*/
|
|
51
|
+
snapshotTrackedComponents() {
|
|
52
|
+
const list = [];
|
|
53
|
+
for (const [, instance] of this.byElement) {
|
|
54
|
+
const stats = this.byInstance.get(instance);
|
|
55
|
+
if (stats)
|
|
56
|
+
list.push(stats);
|
|
57
|
+
}
|
|
58
|
+
this._trackedComponents.set(list);
|
|
59
|
+
}
|
|
60
|
+
/** Remove a component from tracking (call when its host element is removed). */
|
|
61
|
+
unregister(el) {
|
|
62
|
+
this.byElement.delete(el);
|
|
63
|
+
}
|
|
64
|
+
reset() {
|
|
65
|
+
this.byElement.clear();
|
|
66
|
+
this._totalRenders.set(0);
|
|
67
|
+
this._totalUnnecessary.set(0);
|
|
68
|
+
this._trackedComponents.set([]);
|
|
69
|
+
}
|
|
70
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
71
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, providedIn: 'root' });
|
|
72
|
+
}
|
|
73
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, decorators: [{
|
|
74
|
+
type: Injectable,
|
|
75
|
+
args: [{ providedIn: 'root' }]
|
|
76
|
+
}] });
|
|
77
|
+
|
|
78
|
+
// rgb(...) strings for fill and stroke
|
|
79
|
+
const RENDER_COLOR = '255, 200, 0'; // yellow — normal re-render
|
|
80
|
+
const UNNECESSARY_COLOR = '255, 60, 60'; // red — unnecessary render
|
|
81
|
+
class CanvasOverlay {
|
|
82
|
+
canvas = null;
|
|
83
|
+
ctx = null;
|
|
84
|
+
rects = [];
|
|
85
|
+
rafId = 0;
|
|
86
|
+
attached = false;
|
|
87
|
+
attach(doc) {
|
|
88
|
+
if (this.attached)
|
|
89
|
+
return;
|
|
90
|
+
const canvas = doc.createElement('canvas');
|
|
91
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
92
|
+
canvas.setAttribute('role', 'presentation');
|
|
93
|
+
canvas.style.cssText = [
|
|
94
|
+
'position:fixed',
|
|
95
|
+
'top:0',
|
|
96
|
+
'left:0',
|
|
97
|
+
'width:100%',
|
|
98
|
+
'height:100%',
|
|
99
|
+
'pointer-events:none',
|
|
100
|
+
`z-index:2147483646`,
|
|
101
|
+
].join(';');
|
|
102
|
+
doc.body.appendChild(canvas);
|
|
103
|
+
this.canvas = canvas;
|
|
104
|
+
this.ctx = canvas.getContext('2d');
|
|
105
|
+
this.attached = true;
|
|
106
|
+
this.resize();
|
|
107
|
+
window.addEventListener('resize', this.onResize);
|
|
108
|
+
this.scheduleFrame();
|
|
109
|
+
}
|
|
110
|
+
detach() {
|
|
111
|
+
if (!this.attached)
|
|
112
|
+
return;
|
|
113
|
+
this.attached = false;
|
|
114
|
+
cancelAnimationFrame(this.rafId);
|
|
115
|
+
window.removeEventListener('resize', this.onResize);
|
|
116
|
+
this.canvas?.remove();
|
|
117
|
+
this.canvas = null;
|
|
118
|
+
this.ctx = null;
|
|
119
|
+
this.rects = [];
|
|
120
|
+
}
|
|
121
|
+
/** Queue a flash animation over an element's bounding rect. */
|
|
122
|
+
flash(element, kind, durationMs) {
|
|
123
|
+
if (!this.attached)
|
|
124
|
+
return;
|
|
125
|
+
const rect = element.getBoundingClientRect();
|
|
126
|
+
if (rect.width === 0 || rect.height === 0)
|
|
127
|
+
return;
|
|
128
|
+
this.rects.push({
|
|
129
|
+
x: rect.left,
|
|
130
|
+
y: rect.top,
|
|
131
|
+
width: rect.width,
|
|
132
|
+
height: rect.height,
|
|
133
|
+
kind,
|
|
134
|
+
startTime: performance.now(),
|
|
135
|
+
durationMs,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
onResize = () => this.resize();
|
|
139
|
+
resize() {
|
|
140
|
+
if (!this.canvas)
|
|
141
|
+
return;
|
|
142
|
+
this.canvas.width = window.innerWidth;
|
|
143
|
+
this.canvas.height = window.innerHeight;
|
|
144
|
+
}
|
|
145
|
+
scheduleFrame() {
|
|
146
|
+
if (!this.attached)
|
|
147
|
+
return;
|
|
148
|
+
this.rafId = requestAnimationFrame(this.drawFrame);
|
|
149
|
+
}
|
|
150
|
+
drawFrame = (now) => {
|
|
151
|
+
if (!this.ctx || !this.canvas || !this.attached)
|
|
152
|
+
return;
|
|
153
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
154
|
+
// Decay alphas and remove expired rects
|
|
155
|
+
this.rects = this.rects.filter(r => {
|
|
156
|
+
const elapsed = now - r.startTime;
|
|
157
|
+
return elapsed < r.durationMs;
|
|
158
|
+
});
|
|
159
|
+
for (const r of this.rects) {
|
|
160
|
+
const elapsed = now - r.startTime;
|
|
161
|
+
const alpha = Math.max(0, 1 - elapsed / r.durationMs);
|
|
162
|
+
const color = r.kind === 'render' ? RENDER_COLOR : UNNECESSARY_COLOR;
|
|
163
|
+
// Semi-transparent fill
|
|
164
|
+
this.ctx.fillStyle = `rgba(${color}, ${alpha * 0.1})`;
|
|
165
|
+
this.ctx.fillRect(r.x, r.y, r.width, r.height);
|
|
166
|
+
// Solid border
|
|
167
|
+
this.ctx.strokeStyle = `rgba(${color}, ${alpha * 0.9})`;
|
|
168
|
+
this.ctx.lineWidth = 2;
|
|
169
|
+
this.ctx.strokeRect(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
|
|
170
|
+
}
|
|
171
|
+
this.scheduleFrame();
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ANGULAR_SCAN_OPTIONS = new InjectionToken('ANGULAR_SCAN_OPTIONS', { providedIn: 'root', factory: () => ({}) });
|
|
176
|
+
|
|
177
|
+
class OverlayService {
|
|
178
|
+
document = inject(DOCUMENT);
|
|
179
|
+
platformId = inject(PLATFORM_ID);
|
|
180
|
+
options = inject(ANGULAR_SCAN_OPTIONS);
|
|
181
|
+
canvas = new CanvasOverlay();
|
|
182
|
+
// Badge elements keyed by host element
|
|
183
|
+
badges = new Map();
|
|
184
|
+
initialize() {
|
|
185
|
+
if (!isDevMode())
|
|
186
|
+
return;
|
|
187
|
+
if (!isPlatformBrowser(this.platformId))
|
|
188
|
+
return;
|
|
189
|
+
if (this.options.enabled === false)
|
|
190
|
+
return;
|
|
191
|
+
this.canvas.attach(this.document);
|
|
192
|
+
}
|
|
193
|
+
destroy() {
|
|
194
|
+
this.canvas.detach();
|
|
195
|
+
for (const badge of this.badges.values())
|
|
196
|
+
badge.remove();
|
|
197
|
+
this.badges.clear();
|
|
198
|
+
}
|
|
199
|
+
onComponentChecked(stats) {
|
|
200
|
+
const durationMs = this.options.flashDurationMs ?? 500;
|
|
201
|
+
this.canvas.flash(stats.hostElement, stats.lastRenderKind, durationMs);
|
|
202
|
+
if (this.options.showBadges !== false) {
|
|
203
|
+
this.updateBadge(stats);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
removeBadge(el) {
|
|
207
|
+
this.badges.get(el)?.remove();
|
|
208
|
+
this.badges.delete(el);
|
|
209
|
+
}
|
|
210
|
+
updateBadge(stats) {
|
|
211
|
+
let badge = this.badges.get(stats.hostElement);
|
|
212
|
+
if (!badge) {
|
|
213
|
+
badge = this.document.createElement('div');
|
|
214
|
+
badge.setAttribute('aria-hidden', 'true');
|
|
215
|
+
badge.setAttribute('role', 'presentation');
|
|
216
|
+
badge.style.cssText = [
|
|
217
|
+
'position:absolute',
|
|
218
|
+
'top:2px',
|
|
219
|
+
'right:2px',
|
|
220
|
+
'z-index:2147483645',
|
|
221
|
+
'pointer-events:none',
|
|
222
|
+
'font:bold 9px/13px monospace',
|
|
223
|
+
'padding:1px 4px',
|
|
224
|
+
'border-radius:3px',
|
|
225
|
+
'min-width:16px',
|
|
226
|
+
'text-align:center',
|
|
227
|
+
'color:#fff',
|
|
228
|
+
'white-space:nowrap',
|
|
229
|
+
].join(';');
|
|
230
|
+
// Host element must be non-static to contain an absolutely-positioned badge
|
|
231
|
+
this.ensurePositioned(stats.hostElement);
|
|
232
|
+
stats.hostElement.appendChild(badge);
|
|
233
|
+
this.badges.set(stats.hostElement, badge);
|
|
234
|
+
}
|
|
235
|
+
const isUnnecessary = stats.lastRenderKind === 'unnecessary';
|
|
236
|
+
badge.style.background = isUnnecessary ? '#f44336' : '#ff9800';
|
|
237
|
+
badge.textContent = String(stats.totalRenders);
|
|
238
|
+
badge.title = `${stats.componentName}: ${stats.totalRenders} renders, ${stats.unnecessaryRenders} unnecessary`;
|
|
239
|
+
}
|
|
240
|
+
ensurePositioned(el) {
|
|
241
|
+
const htmlEl = el;
|
|
242
|
+
if (getComputedStyle(htmlEl).position === 'static') {
|
|
243
|
+
htmlEl.style.position = 'relative';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
247
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, providedIn: 'root' });
|
|
248
|
+
}
|
|
249
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, decorators: [{
|
|
250
|
+
type: Injectable,
|
|
251
|
+
args: [{ providedIn: 'root' }]
|
|
252
|
+
}] });
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Returns the Angular debug API (window.ng) if available.
|
|
256
|
+
* Returns null in SSR, production builds, or when Angular DevTools APIs are absent.
|
|
257
|
+
*
|
|
258
|
+
* Safe to call before Angular is bootstrapped — the API is attached to window
|
|
259
|
+
* by Angular's dev mode runtime.
|
|
260
|
+
*/
|
|
261
|
+
function getNgDebugApi() {
|
|
262
|
+
if (typeof window === 'undefined')
|
|
263
|
+
return null;
|
|
264
|
+
const ng = window['ng'];
|
|
265
|
+
if (!ng || typeof ng !== 'object')
|
|
266
|
+
return null;
|
|
267
|
+
const api = ng;
|
|
268
|
+
// ɵsetProfiler is the sentinel for dev mode — absent in production builds
|
|
269
|
+
if (typeof api.ɵsetProfiler !== 'function')
|
|
270
|
+
return null;
|
|
271
|
+
if (typeof api.getHostElement !== 'function')
|
|
272
|
+
return null;
|
|
273
|
+
return api;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Angular profiler event IDs (stable contract in dev mode, used by Angular DevTools)
|
|
277
|
+
const TemplateUpdateStart$1 = 2;
|
|
278
|
+
const ChangeDetectionStart$1 = 12;
|
|
279
|
+
const ChangeDetectionEnd$1 = 13;
|
|
280
|
+
const ChangeDetectionSyncStart$1 = 14;
|
|
281
|
+
const ChangeDetectionSyncEnd$1 = 15;
|
|
282
|
+
class ScannerService {
|
|
283
|
+
tracker = inject(ComponentTracker);
|
|
284
|
+
overlay = inject(OverlayService);
|
|
285
|
+
options = inject(ANGULAR_SCAN_OPTIONS);
|
|
286
|
+
platformId = inject(PLATFORM_ID);
|
|
287
|
+
removeProfiler = null;
|
|
288
|
+
mutationObserver = null;
|
|
289
|
+
// Accumulated during a single CD tick (raw DOM nodes, no signal writes)
|
|
290
|
+
mutatedNodes = new Set();
|
|
291
|
+
// Component instances visited during the sync (template update) phase
|
|
292
|
+
tickInstances = new Set();
|
|
293
|
+
// Tracks which phase of CD we're in
|
|
294
|
+
inSyncPhase = false;
|
|
295
|
+
inTick = false;
|
|
296
|
+
// The toolbar component instance — excluded from tracking to avoid self-loops
|
|
297
|
+
toolbarInstance = null;
|
|
298
|
+
initialize() {
|
|
299
|
+
if (!isDevMode())
|
|
300
|
+
return;
|
|
301
|
+
if (!isPlatformBrowser(this.platformId))
|
|
302
|
+
return;
|
|
303
|
+
if (this.options.enabled === false)
|
|
304
|
+
return;
|
|
305
|
+
const ng = getNgDebugApi();
|
|
306
|
+
if (!ng) {
|
|
307
|
+
console.warn('[angular-scan] Angular debug APIs (window.ng) not available. ' +
|
|
308
|
+
'Ensure you are running in development mode.');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.mutationObserver = new MutationObserver(records => {
|
|
312
|
+
for (const r of records) {
|
|
313
|
+
this.mutatedNodes.add(r.target);
|
|
314
|
+
r.addedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
315
|
+
r.removedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
this.removeProfiler = ng.ɵsetProfiler((event, instance) => {
|
|
319
|
+
switch (event) {
|
|
320
|
+
case ChangeDetectionStart$1:
|
|
321
|
+
this.onTickStart();
|
|
322
|
+
break;
|
|
323
|
+
case ChangeDetectionSyncStart$1:
|
|
324
|
+
this.inSyncPhase = true;
|
|
325
|
+
break;
|
|
326
|
+
case ChangeDetectionSyncEnd$1:
|
|
327
|
+
this.inSyncPhase = false;
|
|
328
|
+
break;
|
|
329
|
+
case TemplateUpdateStart$1:
|
|
330
|
+
// Only record during the real update phase, not the dev-mode checkNoChanges pass
|
|
331
|
+
if (this.inSyncPhase && instance && instance !== this.toolbarInstance) {
|
|
332
|
+
this.tickInstances.add(instance);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
case ChangeDetectionEnd$1:
|
|
336
|
+
this.onTickEnd(ng);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/** Exclude a component instance from render tracking (used for the toolbar). */
|
|
342
|
+
setToolbarInstance(instance) {
|
|
343
|
+
this.toolbarInstance = instance;
|
|
344
|
+
}
|
|
345
|
+
onTickStart() {
|
|
346
|
+
if (this.inTick)
|
|
347
|
+
return;
|
|
348
|
+
this.inTick = true;
|
|
349
|
+
this.mutatedNodes.clear();
|
|
350
|
+
this.tickInstances.clear();
|
|
351
|
+
this.mutationObserver?.observe(document.body, {
|
|
352
|
+
subtree: true,
|
|
353
|
+
childList: true,
|
|
354
|
+
attributes: true,
|
|
355
|
+
characterData: true,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
onTickEnd(ng) {
|
|
359
|
+
if (!this.inTick)
|
|
360
|
+
return;
|
|
361
|
+
this.inTick = false;
|
|
362
|
+
this.inSyncPhase = false;
|
|
363
|
+
// Synchronously flush pending mutation records before disconnecting
|
|
364
|
+
const pending = this.mutationObserver?.takeRecords() ?? [];
|
|
365
|
+
for (const r of pending) {
|
|
366
|
+
this.mutatedNodes.add(r.target);
|
|
367
|
+
r.addedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
368
|
+
r.removedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
369
|
+
}
|
|
370
|
+
this.mutationObserver?.disconnect();
|
|
371
|
+
// Build a set of component host elements that had DOM mutations
|
|
372
|
+
// Walk mutated nodes UP to find their owning component host element
|
|
373
|
+
const mutatedHosts = this.buildMutatedHosts(ng);
|
|
374
|
+
// Collect updates as pure data — NO signal writes here (would trigger CD)
|
|
375
|
+
const pendingUpdates = [];
|
|
376
|
+
for (const instance of this.tickInstances) {
|
|
377
|
+
try {
|
|
378
|
+
const hostElement = ng.getHostElement(instance);
|
|
379
|
+
if (!hostElement)
|
|
380
|
+
continue;
|
|
381
|
+
const hadMutation = mutatedHosts.has(hostElement);
|
|
382
|
+
const kind = hadMutation ? 'render' : 'unnecessary';
|
|
383
|
+
pendingUpdates.push({ instance, hostElement, kind });
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Component may have been destroyed during the tick
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Defer all signal writes out of the profiler callback to avoid triggering a new CD cycle
|
|
390
|
+
queueMicrotask(() => {
|
|
391
|
+
for (const update of pendingUpdates) {
|
|
392
|
+
const stats = this.tracker.recordRender(update.instance, update.hostElement, update.kind);
|
|
393
|
+
this.overlay.onComponentChecked(stats);
|
|
394
|
+
}
|
|
395
|
+
this.tracker.snapshotTrackedComponents();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Build a Set of host Elements by walking each mutated node up the DOM tree
|
|
400
|
+
* until an Angular component boundary is found.
|
|
401
|
+
* This is O(mutatedNodes × depth) but depth is typically < 20.
|
|
402
|
+
*/
|
|
403
|
+
buildMutatedHosts(ng) {
|
|
404
|
+
const hosts = new Set();
|
|
405
|
+
for (const node of this.mutatedNodes) {
|
|
406
|
+
let current = node;
|
|
407
|
+
while (current && current !== document.body) {
|
|
408
|
+
if (current instanceof Element) {
|
|
409
|
+
try {
|
|
410
|
+
const component = ng.getComponent(current);
|
|
411
|
+
if (component) {
|
|
412
|
+
hosts.add(current);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// ignore
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
current = current.parentNode;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return hosts;
|
|
424
|
+
}
|
|
425
|
+
ngOnDestroy() {
|
|
426
|
+
this.removeProfiler?.();
|
|
427
|
+
this.removeProfiler = null;
|
|
428
|
+
this.mutationObserver?.disconnect();
|
|
429
|
+
this.mutationObserver = null;
|
|
430
|
+
this.overlay.destroy();
|
|
431
|
+
}
|
|
432
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
433
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, providedIn: 'root' });
|
|
434
|
+
}
|
|
435
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, decorators: [{
|
|
436
|
+
type: Injectable,
|
|
437
|
+
args: [{ providedIn: 'root' }]
|
|
438
|
+
}] });
|
|
439
|
+
|
|
440
|
+
class ToolbarComponent {
|
|
441
|
+
tracker = inject(ComponentTracker);
|
|
442
|
+
expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
443
|
+
enabled = signal(true, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
|
|
444
|
+
toggleExpanded() {
|
|
445
|
+
this.expanded.update(v => !v);
|
|
446
|
+
}
|
|
447
|
+
toggleEnabled() {
|
|
448
|
+
this.enabled.update(v => !v);
|
|
449
|
+
}
|
|
450
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
451
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.1", type: ToolbarComponent, isStandalone: true, selector: "angular-scan-toolbar", host: { properties: { "style.display": "\"block\"" } }, ngImport: i0, template: "<div\nclass=\"toolbar\"\nrole=\"complementary\"\naria-label=\"Angular Scan DevTools\"\n>\n<div class=\"toolbar__header\">\n <span class=\"toolbar__logo\" aria-hidden=\"true\">\u25C9</span>\n <span class=\"toolbar__title\">angular-scan</span>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"enabled()\"\n [title]=\"enabled() ? 'Pause scanning' : 'Resume scanning'\"\n (click)=\"toggleEnabled()\"\n >{{ enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"expanded()\"\n [title]=\"expanded() ? 'Collapse inspector' : 'Expand inspector'\"\n (click)=\"toggleExpanded()\"\n >{{ expanded() ? '\u25B2' : '\u25BC' }}</button>\n</div>\n\n<div class=\"toolbar__stats\">\n <span class=\"toolbar__stat\" title=\"Total change detection checks\">\n <span class=\"toolbar__stat-label\">checks</span>\n <strong class=\"toolbar__stat-value\">{{ tracker.totalRenders() }}</strong>\n </span>\n <span\n class=\"toolbar__stat\"\n [class.toolbar__stat--warn]=\"tracker.totalUnnecessary() > 0\"\n title=\"Unnecessary renders: component was checked but DOM did not change\"\n >\n <span class=\"toolbar__stat-label\">wasted</span>\n <strong class=\"toolbar__stat-value\">{{ tracker.totalUnnecessary() }}</strong>\n </span>\n</div>\n\n@if (expanded()) {\n <div\n class=\"toolbar__inspector\"\n role=\"list\"\n aria-label=\"Component render counts\"\n >\n @for (comp of tracker.trackedComponents(); track comp.hostElement) {\n <div\n class=\"toolbar__component\"\n role=\"listitem\"\n [class.toolbar__component--warn]=\"comp.lastRenderKind === 'unnecessary'\"\n [title]=\"comp.componentName + ' \u2014 ' + comp.totalRenders + ' checks, ' + comp.unnecessaryRenders + ' wasted'\"\n >\n <span class=\"toolbar__component-name\">{{ comp.componentName }}</span>\n <span class=\"toolbar__component-count\">{{ comp.totalRenders }}</span>\n @if (comp.unnecessaryRenders > 0) {\n <span class=\"toolbar__component-wasted\" aria-label=\"wasted renders\">\n {{ comp.unnecessaryRenders }}W\n </span>\n }\n </div>\n }\n </div>\n}\n</div>", styles: [":host{position:fixed;bottom:0;right:0;z-index:2147483647;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,monospace;font-size:11px;line-height:1.4}.toolbar{background:#0f1117;color:#c9d1d9;border:1px solid #30363d;border-bottom:none;border-right:none;border-radius:6px 0 0;min-width:200px;max-width:300px;box-shadow:-2px -2px 12px #00000080}.toolbar__header{display:flex;align-items:center;gap:4px;padding:5px 8px;border-bottom:1px solid #21262d}.toolbar__logo{color:#58a6ff;font-size:12px}.toolbar__title{font-weight:600;color:#58a6ff;flex:1;letter-spacing:.02em}.toolbar__btn{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:10px;transition:color .15s,border-color .15s}.toolbar__btn:hover{color:#c9d1d9;border-color:#58a6ff}.toolbar__btn:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__stats{display:flex;gap:12px;padding:5px 8px}.toolbar__stat{display:flex;flex-direction:column;gap:1px}.toolbar__stat-label{color:#8b949e;font-size:9px;text-transform:uppercase;letter-spacing:.05em}.toolbar__stat-value{color:#c9d1d9}.toolbar__stat--warn .toolbar__stat-value{color:#f85149}.toolbar__inspector{max-height:220px;overflow-y:auto;border-top:1px solid #21262d}.toolbar__inspector::-webkit-scrollbar{width:4px}.toolbar__inspector::-webkit-scrollbar-track{background:transparent}.toolbar__inspector::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}.toolbar__component{display:flex;align-items:center;gap:6px;padding:3px 8px;border-bottom:1px solid #161b22;transition:background .1s}.toolbar__component:hover{background:#161b22}.toolbar__component--warn{background:#f851490f}.toolbar__component-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}.toolbar__component-count{font-weight:600;color:#ffa657;min-width:20px;text-align:right}.toolbar__component-wasted{color:#f85149;font-size:9px;font-weight:600}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
452
|
+
}
|
|
453
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
454
|
+
type: Component,
|
|
455
|
+
args: [{ selector: 'angular-scan-toolbar', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[style.display]': '"block"' }, template: "<div\nclass=\"toolbar\"\nrole=\"complementary\"\naria-label=\"Angular Scan DevTools\"\n>\n<div class=\"toolbar__header\">\n <span class=\"toolbar__logo\" aria-hidden=\"true\">\u25C9</span>\n <span class=\"toolbar__title\">angular-scan</span>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"enabled()\"\n [title]=\"enabled() ? 'Pause scanning' : 'Resume scanning'\"\n (click)=\"toggleEnabled()\"\n >{{ enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"expanded()\"\n [title]=\"expanded() ? 'Collapse inspector' : 'Expand inspector'\"\n (click)=\"toggleExpanded()\"\n >{{ expanded() ? '\u25B2' : '\u25BC' }}</button>\n</div>\n\n<div class=\"toolbar__stats\">\n <span class=\"toolbar__stat\" title=\"Total change detection checks\">\n <span class=\"toolbar__stat-label\">checks</span>\n <strong class=\"toolbar__stat-value\">{{ tracker.totalRenders() }}</strong>\n </span>\n <span\n class=\"toolbar__stat\"\n [class.toolbar__stat--warn]=\"tracker.totalUnnecessary() > 0\"\n title=\"Unnecessary renders: component was checked but DOM did not change\"\n >\n <span class=\"toolbar__stat-label\">wasted</span>\n <strong class=\"toolbar__stat-value\">{{ tracker.totalUnnecessary() }}</strong>\n </span>\n</div>\n\n@if (expanded()) {\n <div\n class=\"toolbar__inspector\"\n role=\"list\"\n aria-label=\"Component render counts\"\n >\n @for (comp of tracker.trackedComponents(); track comp.hostElement) {\n <div\n class=\"toolbar__component\"\n role=\"listitem\"\n [class.toolbar__component--warn]=\"comp.lastRenderKind === 'unnecessary'\"\n [title]=\"comp.componentName + ' \u2014 ' + comp.totalRenders + ' checks, ' + comp.unnecessaryRenders + ' wasted'\"\n >\n <span class=\"toolbar__component-name\">{{ comp.componentName }}</span>\n <span class=\"toolbar__component-count\">{{ comp.totalRenders }}</span>\n @if (comp.unnecessaryRenders > 0) {\n <span class=\"toolbar__component-wasted\" aria-label=\"wasted renders\">\n {{ comp.unnecessaryRenders }}W\n </span>\n }\n </div>\n }\n </div>\n}\n</div>", styles: [":host{position:fixed;bottom:0;right:0;z-index:2147483647;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,monospace;font-size:11px;line-height:1.4}.toolbar{background:#0f1117;color:#c9d1d9;border:1px solid #30363d;border-bottom:none;border-right:none;border-radius:6px 0 0;min-width:200px;max-width:300px;box-shadow:-2px -2px 12px #00000080}.toolbar__header{display:flex;align-items:center;gap:4px;padding:5px 8px;border-bottom:1px solid #21262d}.toolbar__logo{color:#58a6ff;font-size:12px}.toolbar__title{font-weight:600;color:#58a6ff;flex:1;letter-spacing:.02em}.toolbar__btn{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:10px;transition:color .15s,border-color .15s}.toolbar__btn:hover{color:#c9d1d9;border-color:#58a6ff}.toolbar__btn:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__stats{display:flex;gap:12px;padding:5px 8px}.toolbar__stat{display:flex;flex-direction:column;gap:1px}.toolbar__stat-label{color:#8b949e;font-size:9px;text-transform:uppercase;letter-spacing:.05em}.toolbar__stat-value{color:#c9d1d9}.toolbar__stat--warn .toolbar__stat-value{color:#f85149}.toolbar__inspector{max-height:220px;overflow-y:auto;border-top:1px solid #21262d}.toolbar__inspector::-webkit-scrollbar{width:4px}.toolbar__inspector::-webkit-scrollbar-track{background:transparent}.toolbar__inspector::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}.toolbar__component{display:flex;align-items:center;gap:6px;padding:3px 8px;border-bottom:1px solid #161b22;transition:background .1s}.toolbar__component:hover{background:#161b22}.toolbar__component--warn{background:#f851490f}.toolbar__component-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}.toolbar__component-count{font-weight:600;color:#ffa657;min-width:20px;text-align:right}.toolbar__component-wasted{color:#f85149;font-size:9px;font-weight:600}\n"] }]
|
|
456
|
+
}] });
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Provides angular-scan for development-time render tracking.
|
|
460
|
+
*
|
|
461
|
+
* Add to your application providers:
|
|
462
|
+
* ```ts
|
|
463
|
+
* bootstrapApplication(AppComponent, {
|
|
464
|
+
* providers: [provideAngularScan()]
|
|
465
|
+
* });
|
|
466
|
+
* ```
|
|
467
|
+
*
|
|
468
|
+
* This is a complete no-op in production builds (`isDevMode() === false`),
|
|
469
|
+
* so it can safely be left in your app config.
|
|
470
|
+
*/
|
|
471
|
+
function provideAngularScan(options = {}) {
|
|
472
|
+
// Return empty providers in production — entire library is tree-shaken
|
|
473
|
+
if (!isDevMode()) {
|
|
474
|
+
return makeEnvironmentProviders([]);
|
|
475
|
+
}
|
|
476
|
+
const resolvedOptions = {
|
|
477
|
+
enabled: true,
|
|
478
|
+
flashDurationMs: 500,
|
|
479
|
+
showBadges: true,
|
|
480
|
+
showToolbar: true,
|
|
481
|
+
...options,
|
|
482
|
+
};
|
|
483
|
+
return makeEnvironmentProviders([
|
|
484
|
+
{
|
|
485
|
+
provide: ANGULAR_SCAN_OPTIONS,
|
|
486
|
+
useValue: resolvedOptions,
|
|
487
|
+
},
|
|
488
|
+
provideEnvironmentInitializer(() => {
|
|
489
|
+
const overlay = inject(OverlayService);
|
|
490
|
+
const scanner = inject(ScannerService);
|
|
491
|
+
const opts = inject(ANGULAR_SCAN_OPTIONS);
|
|
492
|
+
overlay.initialize();
|
|
493
|
+
scanner.initialize();
|
|
494
|
+
if (opts.showToolbar !== false) {
|
|
495
|
+
const injector = inject(EnvironmentInjector);
|
|
496
|
+
const doc = inject(DOCUMENT$1);
|
|
497
|
+
const appRef = inject(ApplicationRef);
|
|
498
|
+
// Create the toolbar outside Angular's component tree so it doesn't
|
|
499
|
+
// appear in CD tracking. Attach it to ApplicationRef so signals work.
|
|
500
|
+
const toolbarRef = createComponent(ToolbarComponent, {
|
|
501
|
+
environmentInjector: injector,
|
|
502
|
+
});
|
|
503
|
+
doc.body.appendChild(toolbarRef.location.nativeElement);
|
|
504
|
+
appRef.attachView(toolbarRef.hostView);
|
|
505
|
+
// Tell the scanner to ignore the toolbar's own renders
|
|
506
|
+
scanner.setToolbarInstance(toolbarRef.instance);
|
|
507
|
+
}
|
|
508
|
+
}),
|
|
509
|
+
]);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Profiler event constants
|
|
513
|
+
const TemplateUpdateStart = 2;
|
|
514
|
+
const ChangeDetectionStart = 12;
|
|
515
|
+
const ChangeDetectionEnd = 13;
|
|
516
|
+
const ChangeDetectionSyncStart = 14;
|
|
517
|
+
const ChangeDetectionSyncEnd = 15;
|
|
518
|
+
/**
|
|
519
|
+
* Imperative API for angular-scan.
|
|
520
|
+
*
|
|
521
|
+
* Use this when you cannot modify application providers (e.g. micro-frontends,
|
|
522
|
+
* third-party apps). Call before `bootstrapApplication()`.
|
|
523
|
+
*
|
|
524
|
+
* ```ts
|
|
525
|
+
* // main.ts
|
|
526
|
+
* import { scan } from 'angular-scan';
|
|
527
|
+
* scan();
|
|
528
|
+
* bootstrapApplication(AppComponent, appConfig);
|
|
529
|
+
* ```
|
|
530
|
+
*
|
|
531
|
+
* @returns A teardown function that stops scanning and removes the overlay.
|
|
532
|
+
*/
|
|
533
|
+
function scan(options = {}) {
|
|
534
|
+
if (!isDevMode())
|
|
535
|
+
return noop;
|
|
536
|
+
if (typeof window === 'undefined')
|
|
537
|
+
return noop;
|
|
538
|
+
if (options.enabled === false)
|
|
539
|
+
return noop;
|
|
540
|
+
const ng = getNgDebugApi();
|
|
541
|
+
if (!ng) {
|
|
542
|
+
console.warn('[angular-scan] Angular debug APIs (window.ng) not available. ' +
|
|
543
|
+
'Ensure you are running in development mode.');
|
|
544
|
+
return noop;
|
|
545
|
+
}
|
|
546
|
+
const durationMs = options.flashDurationMs ?? 500;
|
|
547
|
+
const showBadges = options.showBadges !== false;
|
|
548
|
+
const overlay = new CanvasOverlay();
|
|
549
|
+
overlay.attach(document);
|
|
550
|
+
// State for the current tick
|
|
551
|
+
let inTick = false;
|
|
552
|
+
let inSyncPhase = false;
|
|
553
|
+
const mutatedNodes = new Set();
|
|
554
|
+
const tickInstances = new Set();
|
|
555
|
+
const renderCounts = new WeakMap();
|
|
556
|
+
const unnecessaryCounts = new WeakMap();
|
|
557
|
+
const badges = new Map();
|
|
558
|
+
const observer = new MutationObserver(records => {
|
|
559
|
+
for (const r of records) {
|
|
560
|
+
mutatedNodes.add(r.target);
|
|
561
|
+
r.addedNodes.forEach(n => mutatedNodes.add(n));
|
|
562
|
+
r.removedNodes.forEach(n => mutatedNodes.add(n));
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
const removeProfiler = ng.ɵsetProfiler((event, instance) => {
|
|
566
|
+
switch (event) {
|
|
567
|
+
case ChangeDetectionStart:
|
|
568
|
+
if (inTick)
|
|
569
|
+
break;
|
|
570
|
+
inTick = true;
|
|
571
|
+
mutatedNodes.clear();
|
|
572
|
+
tickInstances.clear();
|
|
573
|
+
observer.observe(document.body, {
|
|
574
|
+
subtree: true, childList: true, attributes: true, characterData: true,
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
case ChangeDetectionSyncStart:
|
|
578
|
+
inSyncPhase = true;
|
|
579
|
+
break;
|
|
580
|
+
case ChangeDetectionSyncEnd:
|
|
581
|
+
inSyncPhase = false;
|
|
582
|
+
break;
|
|
583
|
+
case TemplateUpdateStart:
|
|
584
|
+
if (inSyncPhase && instance)
|
|
585
|
+
tickInstances.add(instance);
|
|
586
|
+
break;
|
|
587
|
+
case ChangeDetectionEnd: {
|
|
588
|
+
if (!inTick)
|
|
589
|
+
break;
|
|
590
|
+
inTick = false;
|
|
591
|
+
inSyncPhase = false;
|
|
592
|
+
const pending = observer.takeRecords();
|
|
593
|
+
for (const r of pending) {
|
|
594
|
+
mutatedNodes.add(r.target);
|
|
595
|
+
r.addedNodes.forEach(n => mutatedNodes.add(n));
|
|
596
|
+
r.removedNodes.forEach(n => mutatedNodes.add(n));
|
|
597
|
+
}
|
|
598
|
+
observer.disconnect();
|
|
599
|
+
// Build mutated host set
|
|
600
|
+
const mutatedHosts = new Set();
|
|
601
|
+
for (const node of mutatedNodes) {
|
|
602
|
+
let current = node;
|
|
603
|
+
while (current && current !== document.body) {
|
|
604
|
+
if (current instanceof Element) {
|
|
605
|
+
try {
|
|
606
|
+
if (ng.getComponent(current)) {
|
|
607
|
+
mutatedHosts.add(current);
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch { /* ignore */ }
|
|
612
|
+
}
|
|
613
|
+
current = current.parentNode;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Collect updates without writing to any signals
|
|
617
|
+
const updates = [];
|
|
618
|
+
for (const inst of tickInstances) {
|
|
619
|
+
try {
|
|
620
|
+
const hostEl = ng.getHostElement(inst);
|
|
621
|
+
if (!hostEl)
|
|
622
|
+
continue;
|
|
623
|
+
const kind = mutatedHosts.has(hostEl) ? 'render' : 'unnecessary';
|
|
624
|
+
updates.push({ instance: inst, hostEl, kind });
|
|
625
|
+
}
|
|
626
|
+
catch { /* component destroyed */ }
|
|
627
|
+
}
|
|
628
|
+
queueMicrotask(() => {
|
|
629
|
+
for (const { instance, hostEl, kind } of updates) {
|
|
630
|
+
const count = (renderCounts.get(instance) ?? 0) + 1;
|
|
631
|
+
renderCounts.set(instance, count);
|
|
632
|
+
if (kind === 'unnecessary') {
|
|
633
|
+
unnecessaryCounts.set(instance, (unnecessaryCounts.get(instance) ?? 0) + 1);
|
|
634
|
+
}
|
|
635
|
+
overlay.flash(hostEl, kind, durationMs);
|
|
636
|
+
if (showBadges)
|
|
637
|
+
updateBadge(hostEl, count, kind);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
function updateBadge(hostEl, count, kind) {
|
|
645
|
+
let badge = badges.get(hostEl);
|
|
646
|
+
if (!badge) {
|
|
647
|
+
badge = document.createElement('div');
|
|
648
|
+
badge.setAttribute('aria-hidden', 'true');
|
|
649
|
+
badge.setAttribute('role', 'presentation');
|
|
650
|
+
badge.style.cssText = [
|
|
651
|
+
'position:absolute', 'top:2px', 'right:2px',
|
|
652
|
+
'z-index:2147483645', 'pointer-events:none',
|
|
653
|
+
'font:bold 9px/13px monospace', 'padding:1px 4px',
|
|
654
|
+
'border-radius:3px', 'min-width:16px', 'text-align:center', 'color:#fff',
|
|
655
|
+
].join(';');
|
|
656
|
+
const htmlEl = hostEl;
|
|
657
|
+
if (getComputedStyle(htmlEl).position === 'static')
|
|
658
|
+
htmlEl.style.position = 'relative';
|
|
659
|
+
hostEl.appendChild(badge);
|
|
660
|
+
badges.set(hostEl, badge);
|
|
661
|
+
}
|
|
662
|
+
badge.style.background = kind === 'unnecessary' ? '#f44336' : '#ff9800';
|
|
663
|
+
badge.textContent = String(count);
|
|
664
|
+
}
|
|
665
|
+
return () => {
|
|
666
|
+
removeProfiler();
|
|
667
|
+
observer.disconnect();
|
|
668
|
+
overlay.detach();
|
|
669
|
+
for (const b of badges.values())
|
|
670
|
+
b.remove();
|
|
671
|
+
badges.clear();
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function noop() { }
|
|
675
|
+
|
|
676
|
+
/*
|
|
677
|
+
* Public API Surface of angular-scan
|
|
678
|
+
*/
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Generated bundle index. Do not edit.
|
|
682
|
+
*/
|
|
683
|
+
|
|
684
|
+
export { provideAngularScan, scan };
|
|
685
|
+
//# sourceMappingURL=angular-scan.mjs.map
|