angular-scan 0.1.2 → 0.2.0
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 +366 -489
- package/fesm2022/angular-scan.mjs.map +1 -1
- package/package.json +1 -1
- package/types/angular-scan.d.ts +13 -11
|
@@ -1,219 +1,205 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, InjectionToken, inject, DOCUMENT
|
|
3
|
-
import {
|
|
2
|
+
import { signal, Injectable, InjectionToken, inject, DOCUMENT as DOCUMENT$1, isDevMode, Injector, runInInjectionContext, afterEveryRender, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideEnvironmentInitializer, EnvironmentInjector, ApplicationRef, createComponent } from '@angular/core';
|
|
3
|
+
import { DOCUMENT } from '@angular/common';
|
|
4
4
|
|
|
5
5
|
class ComponentTracker {
|
|
6
|
-
// WeakMap: component instance → stats. GC-safe: entries are freed when
|
|
7
|
-
// the component instance is garbage collected after destruction.
|
|
8
6
|
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
7
|
byElement = new Map();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
*/
|
|
8
|
+
totalRenders = signal(0, ...(ngDevMode ? [{ debugName: "totalRenders" }] : /* istanbul ignore next */ []));
|
|
9
|
+
totalUnnecessary = signal(0, ...(ngDevMode ? [{ debugName: "totalUnnecessary" }] : /* istanbul ignore next */ []));
|
|
10
|
+
trackedComponents = signal([], ...(ngDevMode ? [{ debugName: "trackedComponents" }] : /* istanbul ignore next */ []));
|
|
23
11
|
recordRender(instance, hostElement, kind) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
12
|
+
const DEFAULT = {
|
|
13
|
+
componentName: instance.constructor.name,
|
|
14
|
+
hostElement,
|
|
15
|
+
totalRenders: 0,
|
|
16
|
+
unnecessaryRenders: 0,
|
|
17
|
+
lastRenderKind: kind,
|
|
18
|
+
lastRenderTimestamp: 0,
|
|
19
|
+
};
|
|
20
|
+
const prev = this.byInstance.get(instance) ?? DEFAULT;
|
|
21
|
+
const isUnnecessary = kind === 'unnecessary';
|
|
22
|
+
const next = {
|
|
23
|
+
...prev,
|
|
24
|
+
totalRenders: prev.totalRenders + 1,
|
|
25
|
+
unnecessaryRenders: prev.unnecessaryRenders + (isUnnecessary ? 1 : 0),
|
|
26
|
+
lastRenderKind: kind,
|
|
27
|
+
lastRenderTimestamp: Date.now(),
|
|
28
|
+
};
|
|
29
|
+
this.byInstance.set(instance, next);
|
|
30
|
+
this.byElement.set(hostElement, instance);
|
|
31
|
+
this.totalRenders.update((n) => n + 1);
|
|
32
|
+
if (isUnnecessary) {
|
|
33
|
+
this.totalUnnecessary.update((n) => n + 1);
|
|
36
34
|
}
|
|
37
|
-
|
|
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;
|
|
35
|
+
return next;
|
|
46
36
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Rebuild the trackedComponents signal from the current element map.
|
|
49
|
-
* Call once per tick after all recordRender() calls.
|
|
50
|
-
*/
|
|
51
37
|
snapshotTrackedComponents() {
|
|
52
38
|
const list = [];
|
|
53
39
|
for (const [, instance] of this.byElement) {
|
|
54
40
|
const stats = this.byInstance.get(instance);
|
|
55
|
-
if (stats)
|
|
41
|
+
if (stats) {
|
|
56
42
|
list.push(stats);
|
|
43
|
+
}
|
|
57
44
|
}
|
|
58
|
-
this.
|
|
59
|
-
}
|
|
60
|
-
/** Remove a component from tracking (call when its host element is removed). */
|
|
61
|
-
unregister(el) {
|
|
62
|
-
this.byElement.delete(el);
|
|
45
|
+
this.trackedComponents.set(list);
|
|
63
46
|
}
|
|
64
47
|
reset() {
|
|
65
48
|
this.byElement.clear();
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
49
|
+
this.totalRenders.set(0);
|
|
50
|
+
this.totalUnnecessary.set(0);
|
|
51
|
+
this.trackedComponents.set([]);
|
|
69
52
|
}
|
|
70
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
71
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
53
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ComponentTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
54
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ComponentTracker, providedIn: 'root' });
|
|
72
55
|
}
|
|
73
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
56
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ComponentTracker, decorators: [{
|
|
74
57
|
type: Injectable,
|
|
75
58
|
args: [{ providedIn: 'root' }]
|
|
76
59
|
}] });
|
|
77
60
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
};
|
|
61
|
+
/**
|
|
62
|
+
* Returns the Angular debug API (window.ng) if available.
|
|
63
|
+
* Returns null in SSR, production builds, or when Angular DevTools APIs are absent.
|
|
64
|
+
*
|
|
65
|
+
* Safe to call before Angular is bootstrapped — the API is attached to window
|
|
66
|
+
* by Angular's dev mode runtime.
|
|
67
|
+
*/
|
|
68
|
+
function getNgDebugApi() {
|
|
69
|
+
if (typeof window === 'undefined')
|
|
70
|
+
return null;
|
|
71
|
+
const ng = window['ng'];
|
|
72
|
+
if (!ng || typeof ng !== 'object')
|
|
73
|
+
return null;
|
|
74
|
+
const api = ng;
|
|
75
|
+
// ɵsetProfiler is the sentinel for dev mode — absent in production builds
|
|
76
|
+
if (typeof api.ɵsetProfiler !== 'function')
|
|
77
|
+
return null;
|
|
78
|
+
if (typeof api.getHostElement !== 'function')
|
|
79
|
+
return null;
|
|
80
|
+
return api;
|
|
173
81
|
}
|
|
174
82
|
|
|
175
83
|
const ANGULAR_SCAN_OPTIONS = new InjectionToken('ANGULAR_SCAN_OPTIONS', { providedIn: 'root', factory: () => ({}) });
|
|
84
|
+
/** The browser Window object. Resolves to null in SSR. */
|
|
85
|
+
const WINDOW = new InjectionToken('WINDOW', { providedIn: 'root', factory: () => inject(DOCUMENT).defaultView });
|
|
176
86
|
|
|
177
87
|
class ScanConfigService {
|
|
178
88
|
options = inject(ANGULAR_SCAN_OPTIONS);
|
|
179
|
-
enabled = signal(this.options.enabled !== false, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
|
|
180
|
-
showOverlay = signal(true, ...(ngDevMode ? [{ debugName: "showOverlay" }] : []));
|
|
181
|
-
showBadges = signal(this.options.showBadges !== false, ...(ngDevMode ? [{ debugName: "showBadges" }] : []));
|
|
182
|
-
flashDurationMs = signal(this.options.flashDurationMs ?? 500, ...(ngDevMode ? [{ debugName: "flashDurationMs" }] : []));
|
|
183
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
184
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
89
|
+
enabled = signal(this.options.enabled !== false, ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
90
|
+
showOverlay = signal(true, ...(ngDevMode ? [{ debugName: "showOverlay" }] : /* istanbul ignore next */ []));
|
|
91
|
+
showBadges = signal(this.options.showBadges !== false, ...(ngDevMode ? [{ debugName: "showBadges" }] : /* istanbul ignore next */ []));
|
|
92
|
+
flashDurationMs = signal(this.options.flashDurationMs ?? 500, ...(ngDevMode ? [{ debugName: "flashDurationMs" }] : /* istanbul ignore next */ []));
|
|
93
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScanConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
94
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScanConfigService, providedIn: 'root' });
|
|
185
95
|
}
|
|
186
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
96
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScanConfigService, decorators: [{
|
|
187
97
|
type: Injectable,
|
|
188
98
|
args: [{ providedIn: 'root' }]
|
|
189
99
|
}] });
|
|
190
100
|
|
|
101
|
+
const RENDER_COLOR = '255, 200, 0'; // yellow — normal re-render
|
|
102
|
+
const UNNECESSARY_COLOR = '255, 60, 60'; // red — unnecessary render
|
|
103
|
+
function createCanvas(doc) {
|
|
104
|
+
const canvas = doc.createElement('canvas');
|
|
105
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
106
|
+
canvas.setAttribute('role', 'presentation');
|
|
107
|
+
canvas.style.cssText = [
|
|
108
|
+
'position:fixed',
|
|
109
|
+
'top:0',
|
|
110
|
+
'left:0',
|
|
111
|
+
'width:100%',
|
|
112
|
+
'height:100%',
|
|
113
|
+
'pointer-events:none',
|
|
114
|
+
'z-index:2147483646',
|
|
115
|
+
].join(';');
|
|
116
|
+
return canvas;
|
|
117
|
+
}
|
|
118
|
+
function resizeCanvas(canvas, win) {
|
|
119
|
+
canvas.width = win.innerWidth;
|
|
120
|
+
canvas.height = win.innerHeight;
|
|
121
|
+
}
|
|
122
|
+
function toFlashRect(element, kind, durationMs, win) {
|
|
123
|
+
const { left, top, width, height } = element.getBoundingClientRect();
|
|
124
|
+
if (width === 0 || height === 0)
|
|
125
|
+
return null;
|
|
126
|
+
return { x: left, y: top, width, height, kind, startTime: win.performance.now(), durationMs };
|
|
127
|
+
}
|
|
128
|
+
function drawRect(ctx, r, now) {
|
|
129
|
+
const elapsed = now - r.startTime;
|
|
130
|
+
const alpha = Math.max(0, 1 - elapsed / r.durationMs);
|
|
131
|
+
const color = r.kind === 'render' ? RENDER_COLOR : UNNECESSARY_COLOR;
|
|
132
|
+
ctx.fillStyle = `rgba(${color}, ${alpha * 0.1})`;
|
|
133
|
+
ctx.fillRect(r.x, r.y, r.width, r.height);
|
|
134
|
+
ctx.strokeStyle = `rgba(${color}, ${alpha * 0.9})`;
|
|
135
|
+
ctx.lineWidth = 2;
|
|
136
|
+
ctx.strokeRect(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
|
|
137
|
+
}
|
|
138
|
+
function renderFrame(ctx, canvas, rects, now) {
|
|
139
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
140
|
+
const active = rects.filter((r) => now - r.startTime < r.durationMs);
|
|
141
|
+
for (const r of active)
|
|
142
|
+
drawRect(ctx, r, now);
|
|
143
|
+
return active;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates a canvas overlay, attaches it to the document, and starts the render loop.
|
|
147
|
+
* Returns a minimal interface: flash to queue an animation, detach to clean up.
|
|
148
|
+
*/
|
|
149
|
+
function createCanvasOverlay(doc, win) {
|
|
150
|
+
let state = { rects: [], rafId: 0 };
|
|
151
|
+
const canvas = createCanvas(doc);
|
|
152
|
+
doc.body.appendChild(canvas);
|
|
153
|
+
const ctx = canvas.getContext('2d');
|
|
154
|
+
const onResize = () => resizeCanvas(canvas, win);
|
|
155
|
+
const loop = (now) => {
|
|
156
|
+
state = {
|
|
157
|
+
rects: renderFrame(ctx, canvas, state.rects, now),
|
|
158
|
+
rafId: win.requestAnimationFrame(loop),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
resizeCanvas(canvas, win);
|
|
162
|
+
win.addEventListener('resize', onResize);
|
|
163
|
+
state = { ...state, rafId: win.requestAnimationFrame(loop) };
|
|
164
|
+
return {
|
|
165
|
+
flash(element, kind, durationMs) {
|
|
166
|
+
const rect = toFlashRect(element, kind, durationMs, win);
|
|
167
|
+
if (rect)
|
|
168
|
+
state = { ...state, rects: [...state.rects, rect] };
|
|
169
|
+
},
|
|
170
|
+
detach() {
|
|
171
|
+
win.cancelAnimationFrame(state.rafId);
|
|
172
|
+
win.removeEventListener('resize', onResize);
|
|
173
|
+
canvas.remove();
|
|
174
|
+
state = { rects: [], rafId: 0 };
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
191
179
|
class OverlayService {
|
|
192
|
-
document = inject(DOCUMENT);
|
|
193
|
-
|
|
180
|
+
document = inject(DOCUMENT$1);
|
|
181
|
+
win = inject(WINDOW);
|
|
194
182
|
options = inject(ANGULAR_SCAN_OPTIONS);
|
|
195
183
|
config = inject(ScanConfigService);
|
|
196
|
-
canvas =
|
|
197
|
-
// Badge elements keyed by host element
|
|
184
|
+
canvas = null;
|
|
198
185
|
badges = new Map();
|
|
199
186
|
initialize() {
|
|
200
|
-
if (!isDevMode())
|
|
187
|
+
if (!isDevMode() || !this.win || this.options.enabled === false) {
|
|
201
188
|
return;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (this.options.enabled === false)
|
|
205
|
-
return;
|
|
206
|
-
this.canvas.attach(this.document);
|
|
189
|
+
}
|
|
190
|
+
this.canvas = createCanvasOverlay(this.document, this.win);
|
|
207
191
|
}
|
|
208
192
|
destroy() {
|
|
209
|
-
this.canvas
|
|
210
|
-
|
|
193
|
+
this.canvas?.detach();
|
|
194
|
+
this.canvas = null;
|
|
195
|
+
for (const badge of this.badges.values()) {
|
|
211
196
|
badge.remove();
|
|
197
|
+
}
|
|
212
198
|
this.badges.clear();
|
|
213
199
|
}
|
|
214
200
|
onComponentChecked(stats) {
|
|
215
201
|
if (this.config.showOverlay()) {
|
|
216
|
-
this.canvas
|
|
202
|
+
this.canvas?.flash(stats.hostElement, stats.lastRenderKind, this.config.flashDurationMs());
|
|
217
203
|
}
|
|
218
204
|
if (this.config.showBadges()) {
|
|
219
205
|
this.updateBadge(stats);
|
|
@@ -246,7 +232,6 @@ class OverlayService {
|
|
|
246
232
|
'color:#fff',
|
|
247
233
|
'white-space:nowrap',
|
|
248
234
|
].join(';');
|
|
249
|
-
// Host element must be non-static to contain an absolutely-positioned badge
|
|
250
235
|
this.ensurePositioned(stats.hostElement);
|
|
251
236
|
stats.hostElement.appendChild(badge);
|
|
252
237
|
this.badges.set(stats.hostElement, badge);
|
|
@@ -258,73 +243,148 @@ class OverlayService {
|
|
|
258
243
|
}
|
|
259
244
|
ensurePositioned(el) {
|
|
260
245
|
const htmlEl = el;
|
|
261
|
-
if (getComputedStyle(htmlEl).position === 'static') {
|
|
246
|
+
if (this.win?.getComputedStyle(htmlEl).position === 'static') {
|
|
262
247
|
htmlEl.style.position = 'relative';
|
|
263
248
|
}
|
|
264
249
|
}
|
|
265
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
266
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
250
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
251
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: OverlayService, providedIn: 'root' });
|
|
267
252
|
}
|
|
268
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
253
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: OverlayService, decorators: [{
|
|
269
254
|
type: Injectable,
|
|
270
|
-
args: [{
|
|
255
|
+
args: [{
|
|
256
|
+
providedIn: 'root',
|
|
257
|
+
}]
|
|
271
258
|
}] });
|
|
272
259
|
|
|
273
260
|
/**
|
|
274
|
-
*
|
|
275
|
-
* Returns null in SSR, production builds, or when Angular DevTools APIs are absent.
|
|
261
|
+
* Angular profiler event IDs — stable dev-mode contract, same as Angular DevTools.
|
|
276
262
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
263
|
+
* @see https://github.com/angular/angular/blob/main/packages/core/primitives/devtools/src/profiler_types.ts
|
|
264
|
+
* @see https://github.com/angular/angular/blob/main/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts
|
|
279
265
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
266
|
+
const PROFILER_EVENTS = {
|
|
267
|
+
TemplateUpdateStart: 2,
|
|
268
|
+
ChangeDetectionStart: 12,
|
|
269
|
+
ChangeDetectionEnd: 13,
|
|
270
|
+
ChangeDetectionSyncStart: 14,
|
|
271
|
+
ChangeDetectionSyncEnd: 15,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/** Walk mutated nodes UP the DOM to find their owning Angular component host. */
|
|
275
|
+
function buildMutatedHosts(nodes, body, ng) {
|
|
276
|
+
const hosts = new Set();
|
|
277
|
+
for (const node of nodes) {
|
|
278
|
+
let current = node;
|
|
279
|
+
while (current && current !== body) {
|
|
280
|
+
if (current instanceof Element) {
|
|
281
|
+
try {
|
|
282
|
+
if (ng.getComponent(current)) {
|
|
283
|
+
hosts.add(current);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { /* ignore destroyed nodes */ }
|
|
288
|
+
}
|
|
289
|
+
current = current.parentNode;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return hosts;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Map checked instances to PendingUpdate records — pure, no side effects. */
|
|
296
|
+
function collectTickUpdates(instances, mutatedHosts, ng) {
|
|
297
|
+
const updates = [];
|
|
298
|
+
for (const instance of instances) {
|
|
299
|
+
try {
|
|
300
|
+
const hostElement = ng.getHostElement(instance);
|
|
301
|
+
if (!hostElement)
|
|
302
|
+
continue;
|
|
303
|
+
const kind = mutatedHosts.has(hostElement) ? 'render' : 'unnecessary';
|
|
304
|
+
updates.push({ instance, hostElement, kind });
|
|
305
|
+
}
|
|
306
|
+
catch { /* component destroyed */ }
|
|
307
|
+
}
|
|
308
|
+
return updates;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Wires up the Angular profiler + MutationObserver to track per-tick render updates.
|
|
313
|
+
* Returns a teardown function that removes the profiler and disconnects the observer.
|
|
314
|
+
*/
|
|
315
|
+
function createTickProfiler({ ng, body, shouldTrack = () => true, onUpdates, }) {
|
|
316
|
+
let inTick = false;
|
|
317
|
+
let inSyncPhase = false;
|
|
318
|
+
const mutatedNodes = new Set();
|
|
319
|
+
const tickInstances = new Set();
|
|
320
|
+
const observer = new MutationObserver((records) => {
|
|
321
|
+
for (const r of records) {
|
|
322
|
+
mutatedNodes.add(r.target);
|
|
323
|
+
r.addedNodes.forEach((n) => mutatedNodes.add(n));
|
|
324
|
+
r.removedNodes.forEach((n) => mutatedNodes.add(n));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const removeProfiler = ng.ɵsetProfiler((event, instance) => {
|
|
328
|
+
switch (event) {
|
|
329
|
+
case PROFILER_EVENTS.ChangeDetectionStart:
|
|
330
|
+
if (inTick)
|
|
331
|
+
break;
|
|
332
|
+
inTick = true;
|
|
333
|
+
mutatedNodes.clear();
|
|
334
|
+
tickInstances.clear();
|
|
335
|
+
observer.observe(body, {
|
|
336
|
+
subtree: true, childList: true, attributes: true, characterData: true,
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
case PROFILER_EVENTS.ChangeDetectionSyncStart:
|
|
340
|
+
inSyncPhase = true;
|
|
341
|
+
break;
|
|
342
|
+
case PROFILER_EVENTS.ChangeDetectionSyncEnd:
|
|
343
|
+
inSyncPhase = false;
|
|
344
|
+
break;
|
|
345
|
+
case PROFILER_EVENTS.TemplateUpdateStart:
|
|
346
|
+
if (inSyncPhase && instance && shouldTrack(instance)) {
|
|
347
|
+
tickInstances.add(instance);
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
case PROFILER_EVENTS.ChangeDetectionEnd: {
|
|
351
|
+
if (!inTick)
|
|
352
|
+
break;
|
|
353
|
+
inTick = false;
|
|
354
|
+
inSyncPhase = false;
|
|
355
|
+
const pending = observer.takeRecords();
|
|
356
|
+
for (const r of pending) {
|
|
357
|
+
mutatedNodes.add(r.target);
|
|
358
|
+
r.addedNodes.forEach((n) => mutatedNodes.add(n));
|
|
359
|
+
r.removedNodes.forEach((n) => mutatedNodes.add(n));
|
|
360
|
+
}
|
|
361
|
+
observer.disconnect();
|
|
362
|
+
const mutatedHosts = buildMutatedHosts(mutatedNodes, body, ng);
|
|
363
|
+
onUpdates(collectTickUpdates(tickInstances, mutatedHosts, ng));
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return () => {
|
|
369
|
+
removeProfiler();
|
|
370
|
+
observer.disconnect();
|
|
371
|
+
};
|
|
293
372
|
}
|
|
294
373
|
|
|
295
|
-
// Angular profiler event IDs (stable contract in dev mode, used by Angular DevTools)
|
|
296
|
-
const TemplateUpdateStart$1 = 2;
|
|
297
|
-
const ChangeDetectionStart$1 = 12;
|
|
298
|
-
const ChangeDetectionEnd$1 = 13;
|
|
299
|
-
const ChangeDetectionSyncStart$1 = 14;
|
|
300
|
-
const ChangeDetectionSyncEnd$1 = 15;
|
|
301
374
|
class ScannerService {
|
|
302
375
|
tracker = inject(ComponentTracker);
|
|
303
376
|
overlay = inject(OverlayService);
|
|
304
377
|
options = inject(ANGULAR_SCAN_OPTIONS);
|
|
305
|
-
|
|
378
|
+
win = inject(WINDOW);
|
|
379
|
+
document = inject(DOCUMENT$1);
|
|
306
380
|
injector = inject(Injector);
|
|
307
381
|
config = inject(ScanConfigService);
|
|
308
|
-
|
|
382
|
+
teardownProfiler = null;
|
|
309
383
|
afterRenderRef = null;
|
|
310
|
-
mutationObserver = null;
|
|
311
|
-
// Accumulated during a single CD tick (raw DOM nodes, no signal writes)
|
|
312
|
-
mutatedNodes = new Set();
|
|
313
|
-
// Component instances visited during the sync (template update) phase
|
|
314
|
-
tickInstances = new Set();
|
|
315
|
-
// Updates waiting to be flushed in afterEveryRender
|
|
316
384
|
pendingFlush = [];
|
|
317
|
-
// Tracks which phase of CD we're in
|
|
318
|
-
inSyncPhase = false;
|
|
319
|
-
inTick = false;
|
|
320
|
-
// The toolbar component instance — excluded from tracking to avoid self-loops
|
|
321
385
|
toolbarInstance = null;
|
|
322
386
|
initialize() {
|
|
323
|
-
if (!isDevMode())
|
|
324
|
-
return;
|
|
325
|
-
if (!isPlatformBrowser(this.platformId))
|
|
326
|
-
return;
|
|
327
|
-
if (this.options.enabled === false)
|
|
387
|
+
if (!isDevMode() || !this.win || this.options.enabled === false)
|
|
328
388
|
return;
|
|
329
389
|
const ng = getNgDebugApi();
|
|
330
390
|
if (!ng) {
|
|
@@ -332,147 +392,39 @@ class ScannerService {
|
|
|
332
392
|
'Ensure you are running in development mode.');
|
|
333
393
|
return;
|
|
334
394
|
}
|
|
335
|
-
// afterEveryRender is Angular's safe hook for post-render writes.
|
|
336
|
-
// Signal writes here cause exactly one extra toolbar render pass then stabilize,
|
|
337
|
-
// avoiding the infinite loop that queueMicrotask causes in zone.js + signals hybrid apps.
|
|
338
395
|
this.afterRenderRef = runInInjectionContext(this.injector, () => afterEveryRender({ write: () => this.flushPendingUpdates() }));
|
|
339
|
-
this.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
this.removeProfiler = ng.ɵsetProfiler((event, instance) => {
|
|
347
|
-
switch (event) {
|
|
348
|
-
case ChangeDetectionStart$1:
|
|
349
|
-
this.onTickStart();
|
|
350
|
-
break;
|
|
351
|
-
case ChangeDetectionSyncStart$1:
|
|
352
|
-
this.inSyncPhase = true;
|
|
353
|
-
break;
|
|
354
|
-
case ChangeDetectionSyncEnd$1:
|
|
355
|
-
this.inSyncPhase = false;
|
|
356
|
-
break;
|
|
357
|
-
case TemplateUpdateStart$1:
|
|
358
|
-
// Only record during the real update phase, not the dev-mode checkNoChanges pass
|
|
359
|
-
if (this.config.enabled() && this.inSyncPhase && instance && instance !== this.toolbarInstance) {
|
|
360
|
-
this.tickInstances.add(instance);
|
|
361
|
-
}
|
|
362
|
-
break;
|
|
363
|
-
case ChangeDetectionEnd$1:
|
|
364
|
-
this.onTickEnd(ng);
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
396
|
+
this.teardownProfiler = createTickProfiler({
|
|
397
|
+
ng,
|
|
398
|
+
body: this.document.body,
|
|
399
|
+
shouldTrack: (instance) => this.config.enabled() && instance !== this.toolbarInstance,
|
|
400
|
+
onUpdates: (updates) => this.pendingFlush.push(...updates),
|
|
367
401
|
});
|
|
368
402
|
}
|
|
369
|
-
/** Exclude a component instance from render tracking (used for the toolbar). */
|
|
370
403
|
setToolbarInstance(instance) {
|
|
371
404
|
this.toolbarInstance = instance;
|
|
372
405
|
}
|
|
373
|
-
onTickStart() {
|
|
374
|
-
if (this.inTick)
|
|
375
|
-
return;
|
|
376
|
-
this.inTick = true;
|
|
377
|
-
this.mutatedNodes.clear();
|
|
378
|
-
this.tickInstances.clear();
|
|
379
|
-
this.mutationObserver?.observe(document.body, {
|
|
380
|
-
subtree: true,
|
|
381
|
-
childList: true,
|
|
382
|
-
attributes: true,
|
|
383
|
-
characterData: true,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
onTickEnd(ng) {
|
|
387
|
-
if (!this.inTick)
|
|
388
|
-
return;
|
|
389
|
-
this.inTick = false;
|
|
390
|
-
this.inSyncPhase = false;
|
|
391
|
-
// Synchronously flush pending mutation records before disconnecting
|
|
392
|
-
const pending = this.mutationObserver?.takeRecords() ?? [];
|
|
393
|
-
for (const r of pending) {
|
|
394
|
-
this.mutatedNodes.add(r.target);
|
|
395
|
-
r.addedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
396
|
-
r.removedNodes.forEach(n => this.mutatedNodes.add(n));
|
|
397
|
-
}
|
|
398
|
-
this.mutationObserver?.disconnect();
|
|
399
|
-
// Build a set of component host elements that had DOM mutations
|
|
400
|
-
// Walk mutated nodes UP to find their owning component host element
|
|
401
|
-
const mutatedHosts = this.buildMutatedHosts(ng);
|
|
402
|
-
// Collect updates as pure data — NO signal writes here (would trigger CD)
|
|
403
|
-
const pendingUpdates = [];
|
|
404
|
-
for (const instance of this.tickInstances) {
|
|
405
|
-
try {
|
|
406
|
-
const hostElement = ng.getHostElement(instance);
|
|
407
|
-
if (!hostElement)
|
|
408
|
-
continue;
|
|
409
|
-
const hadMutation = mutatedHosts.has(hostElement);
|
|
410
|
-
const kind = hadMutation ? 'render' : 'unnecessary';
|
|
411
|
-
pendingUpdates.push({ instance, hostElement, kind });
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
// Component may have been destroyed during the tick
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
// Queue for afterEveryRender to flush safely — no direct signal writes here.
|
|
418
|
-
this.pendingFlush.push(...pendingUpdates);
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Called by afterEveryRender. Drains pendingFlush into signals safely.
|
|
422
|
-
* If there are no pending updates, returns immediately so Angular stabilizes.
|
|
423
|
-
* If there are updates, signals are written → toolbar re-renders (one extra pass) → stabilizes.
|
|
424
|
-
*/
|
|
425
406
|
flushPendingUpdates() {
|
|
426
407
|
const updates = this.pendingFlush.splice(0);
|
|
427
408
|
if (updates.length === 0)
|
|
428
409
|
return;
|
|
429
|
-
for (const
|
|
430
|
-
const stats = this.tracker.recordRender(
|
|
410
|
+
for (const { instance, hostElement, kind } of updates) {
|
|
411
|
+
const stats = this.tracker.recordRender(instance, hostElement, kind);
|
|
431
412
|
this.overlay.onComponentChecked(stats);
|
|
432
413
|
}
|
|
433
414
|
this.tracker.snapshotTrackedComponents();
|
|
434
415
|
}
|
|
435
|
-
/**
|
|
436
|
-
* Build a Set of host Elements by walking each mutated node up the DOM tree
|
|
437
|
-
* until an Angular component boundary is found.
|
|
438
|
-
* This is O(mutatedNodes × depth) but depth is typically < 20.
|
|
439
|
-
*/
|
|
440
|
-
buildMutatedHosts(ng) {
|
|
441
|
-
const hosts = new Set();
|
|
442
|
-
for (const node of this.mutatedNodes) {
|
|
443
|
-
let current = node;
|
|
444
|
-
while (current && current !== document.body) {
|
|
445
|
-
if (current instanceof Element) {
|
|
446
|
-
try {
|
|
447
|
-
const component = ng.getComponent(current);
|
|
448
|
-
if (component) {
|
|
449
|
-
hosts.add(current);
|
|
450
|
-
break;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
// ignore
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
current = current.parentNode;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return hosts;
|
|
461
|
-
}
|
|
462
416
|
ngOnDestroy() {
|
|
463
|
-
this.
|
|
464
|
-
this.
|
|
417
|
+
this.teardownProfiler?.();
|
|
418
|
+
this.teardownProfiler = null;
|
|
465
419
|
this.afterRenderRef?.destroy();
|
|
466
420
|
this.afterRenderRef = null;
|
|
467
|
-
this.mutationObserver?.disconnect();
|
|
468
|
-
this.mutationObserver = null;
|
|
469
421
|
this.pendingFlush = [];
|
|
470
422
|
this.overlay.destroy();
|
|
471
423
|
}
|
|
472
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
473
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
424
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScannerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
425
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScannerService, providedIn: 'root' });
|
|
474
426
|
}
|
|
475
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
427
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ScannerService, decorators: [{
|
|
476
428
|
type: Injectable,
|
|
477
429
|
args: [{ providedIn: 'root' }]
|
|
478
430
|
}] });
|
|
@@ -480,26 +432,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImpor
|
|
|
480
432
|
class ToolbarComponent {
|
|
481
433
|
tracker = inject(ComponentTracker);
|
|
482
434
|
config = inject(ScanConfigService);
|
|
483
|
-
expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
484
|
-
settingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "settingsOpen" }] : []));
|
|
435
|
+
expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
436
|
+
settingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "settingsOpen" }] : /* istanbul ignore next */ []));
|
|
485
437
|
toggleExpanded() {
|
|
486
|
-
this.expanded.
|
|
487
|
-
if (this.expanded())
|
|
438
|
+
this.expanded.update((v) => !v);
|
|
439
|
+
if (this.expanded()) {
|
|
488
440
|
this.settingsOpen.set(false);
|
|
441
|
+
}
|
|
489
442
|
}
|
|
490
443
|
toggleSettings() {
|
|
491
|
-
this.settingsOpen.
|
|
492
|
-
if (this.settingsOpen())
|
|
444
|
+
this.settingsOpen.update((v) => !v);
|
|
445
|
+
if (this.settingsOpen()) {
|
|
493
446
|
this.expanded.set(false);
|
|
447
|
+
}
|
|
494
448
|
}
|
|
495
449
|
toggleEnabled() {
|
|
496
|
-
this.config.enabled.
|
|
450
|
+
this.config.enabled.update((v) => !v);
|
|
497
451
|
}
|
|
498
452
|
toggleOverlay() {
|
|
499
|
-
this.config.showOverlay.
|
|
453
|
+
this.config.showOverlay.update((v) => !v);
|
|
500
454
|
}
|
|
501
455
|
toggleBadges() {
|
|
502
|
-
this.config.showBadges.
|
|
456
|
+
this.config.showBadges.update((v) => !v);
|
|
503
457
|
}
|
|
504
458
|
resetStats() {
|
|
505
459
|
this.tracker.reset();
|
|
@@ -508,10 +462,10 @@ class ToolbarComponent {
|
|
|
508
462
|
const value = Number(event.target.value);
|
|
509
463
|
this.config.flashDurationMs.set(value);
|
|
510
464
|
}
|
|
511
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
512
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.2", type: ToolbarComponent, isStandalone: true, selector: "angular-scan-toolbar", host: { properties: { "style.display": "\"block\"" } }, ngImport: i0, template: "<div\n class=\"toolbar\"\n role=\"complementary\"\n aria-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]=\"!config.enabled()\"\n [title]=\"config.enabled() ? 'Pause scanning' : 'Resume scanning'\"\n (click)=\"toggleEnabled()\"\n >{{ config.enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"settingsOpen()\"\n title=\"Settings\"\n (click)=\"toggleSettings()\"\n >\u2699</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 (settingsOpen()) {\n <div class=\"toolbar__settings\" role=\"group\" aria-label=\"Scan settings\">\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Enable scanning</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.enabled()\"\n (click)=\"toggleEnabled()\"\n [class.toolbar__toggle--on]=\"config.enabled()\"\n >{{ config.enabled() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Flash overlay</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.showOverlay()\"\n (click)=\"toggleOverlay()\"\n [class.toolbar__toggle--on]=\"config.showOverlay()\"\n >{{ config.showOverlay() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Render badges</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.showBadges()\"\n (click)=\"toggleBadges()\"\n [class.toolbar__toggle--on]=\"config.showBadges()\"\n >{{ config.showBadges() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting toolbar__setting--column\">\n <span class=\"toolbar__setting-label\">\n Flash duration\n <span class=\"toolbar__setting-value\">{{ config.flashDurationMs() }}ms</span>\n </span>\n <input\n class=\"toolbar__slider\"\n type=\"range\"\n min=\"100\"\n max=\"2000\"\n step=\"100\"\n [value]=\"config.flashDurationMs()\"\n (input)=\"onFlashDurationChange($event)\"\n aria-label=\"Flash duration in milliseconds\"\n />\n </label>\n\n <button\n class=\"toolbar__reset\"\n type=\"button\"\n title=\"Clear all render stats\"\n (click)=\"resetStats()\"\n >\u21BA Reset stats</button>\n\n </div>\n }\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>\n", 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}.toolbar__settings{display:flex;flex-direction:column;gap:6px;padding:8px;border-top:1px solid #21262d}.toolbar__setting{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:default}.toolbar__setting--column{flex-direction:column;align-items:stretch;gap:4px}.toolbar__setting-label{color:#8b949e;font-size:10px;display:flex;align-items:center;justify-content:space-between;gap:4px}.toolbar__setting-value{color:#58a6ff;font-weight:600}.toolbar__toggle{background:#21262d;border:1px solid #30363d;color:#8b949e;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:9px;font-weight:600;font-family:inherit;letter-spacing:.05em;min-width:32px;transition:background .15s,color .15s,border-color .15s}.toolbar__toggle--on{background:#58a6ff26;border-color:#58a6ff;color:#58a6ff}.toolbar__toggle:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__slider{width:100%;accent-color:#58a6ff;cursor:pointer;height:4px}.toolbar__reset{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:3px 8px;font-size:10px;font-family:inherit;margin-top:2px;transition:color .15s,border-color .15s;align-self:flex-start}.toolbar__reset:hover{color:#f85149;border-color:#f85149}.toolbar__reset:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
513
467
|
}
|
|
514
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
468
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
515
469
|
type: Component,
|
|
516
470
|
args: [{ selector: 'angular-scan-toolbar', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[style.display]': '"block"' }, template: "<div\n class=\"toolbar\"\n role=\"complementary\"\n aria-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]=\"!config.enabled()\"\n [title]=\"config.enabled() ? 'Pause scanning' : 'Resume scanning'\"\n (click)=\"toggleEnabled()\"\n >{{ config.enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n <button\n class=\"toolbar__btn\"\n type=\"button\"\n [attr.aria-pressed]=\"settingsOpen()\"\n title=\"Settings\"\n (click)=\"toggleSettings()\"\n >\u2699</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 (settingsOpen()) {\n <div class=\"toolbar__settings\" role=\"group\" aria-label=\"Scan settings\">\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Enable scanning</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.enabled()\"\n (click)=\"toggleEnabled()\"\n [class.toolbar__toggle--on]=\"config.enabled()\"\n >{{ config.enabled() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Flash overlay</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.showOverlay()\"\n (click)=\"toggleOverlay()\"\n [class.toolbar__toggle--on]=\"config.showOverlay()\"\n >{{ config.showOverlay() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting\">\n <span class=\"toolbar__setting-label\">Render badges</span>\n <button\n class=\"toolbar__toggle\"\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"config.showBadges()\"\n (click)=\"toggleBadges()\"\n [class.toolbar__toggle--on]=\"config.showBadges()\"\n >{{ config.showBadges() ? 'ON' : 'OFF' }}</button>\n </label>\n\n <label class=\"toolbar__setting toolbar__setting--column\">\n <span class=\"toolbar__setting-label\">\n Flash duration\n <span class=\"toolbar__setting-value\">{{ config.flashDurationMs() }}ms</span>\n </span>\n <input\n class=\"toolbar__slider\"\n type=\"range\"\n min=\"100\"\n max=\"2000\"\n step=\"100\"\n [value]=\"config.flashDurationMs()\"\n (input)=\"onFlashDurationChange($event)\"\n aria-label=\"Flash duration in milliseconds\"\n />\n </label>\n\n <button\n class=\"toolbar__reset\"\n type=\"button\"\n title=\"Clear all render stats\"\n (click)=\"resetStats()\"\n >\u21BA Reset stats</button>\n\n </div>\n }\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>\n", 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}.toolbar__settings{display:flex;flex-direction:column;gap:6px;padding:8px;border-top:1px solid #21262d}.toolbar__setting{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:default}.toolbar__setting--column{flex-direction:column;align-items:stretch;gap:4px}.toolbar__setting-label{color:#8b949e;font-size:10px;display:flex;align-items:center;justify-content:space-between;gap:4px}.toolbar__setting-value{color:#58a6ff;font-weight:600}.toolbar__toggle{background:#21262d;border:1px solid #30363d;color:#8b949e;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:9px;font-weight:600;font-family:inherit;letter-spacing:.05em;min-width:32px;transition:background .15s,color .15s,border-color .15s}.toolbar__toggle--on{background:#58a6ff26;border-color:#58a6ff;color:#58a6ff}.toolbar__toggle:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__slider{width:100%;accent-color:#58a6ff;cursor:pointer;height:4px}.toolbar__reset{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:3px 8px;font-size:10px;font-family:inherit;margin-top:2px;transition:color .15s,border-color .15s;align-self:flex-start}.toolbar__reset:hover{color:#f85149;border-color:#f85149}.toolbar__reset:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}\n"] }]
|
|
517
471
|
}] });
|
|
@@ -542,10 +496,7 @@ function provideAngularScan(options = {}) {
|
|
|
542
496
|
...options,
|
|
543
497
|
};
|
|
544
498
|
return makeEnvironmentProviders([
|
|
545
|
-
{
|
|
546
|
-
provide: ANGULAR_SCAN_OPTIONS,
|
|
547
|
-
useValue: resolvedOptions,
|
|
548
|
-
},
|
|
499
|
+
{ provide: ANGULAR_SCAN_OPTIONS, useValue: resolvedOptions },
|
|
549
500
|
provideEnvironmentInitializer(() => {
|
|
550
501
|
const overlay = inject(OverlayService);
|
|
551
502
|
const scanner = inject(ScannerService);
|
|
@@ -553,29 +504,25 @@ function provideAngularScan(options = {}) {
|
|
|
553
504
|
overlay.initialize();
|
|
554
505
|
scanner.initialize();
|
|
555
506
|
if (opts.showToolbar !== false) {
|
|
556
|
-
|
|
557
|
-
const doc = inject(DOCUMENT$1);
|
|
558
|
-
const appRef = inject(ApplicationRef);
|
|
559
|
-
// Create the toolbar outside Angular's component tree so it doesn't
|
|
560
|
-
// appear in CD tracking. Attach it to ApplicationRef so signals work.
|
|
561
|
-
const toolbarRef = createComponent(ToolbarComponent, {
|
|
562
|
-
environmentInjector: injector,
|
|
563
|
-
});
|
|
564
|
-
doc.body.appendChild(toolbarRef.location.nativeElement);
|
|
565
|
-
appRef.attachView(toolbarRef.hostView);
|
|
566
|
-
// Tell the scanner to ignore the toolbar's own renders
|
|
567
|
-
scanner.setToolbarInstance(toolbarRef.instance);
|
|
507
|
+
mountToolbar(scanner);
|
|
568
508
|
}
|
|
569
509
|
}),
|
|
570
510
|
]);
|
|
571
511
|
}
|
|
512
|
+
/** Create the toolbar outside Angular's component tree and attach it to the DOM. */
|
|
513
|
+
function mountToolbar(scanner) {
|
|
514
|
+
const injector = inject(EnvironmentInjector);
|
|
515
|
+
const doc = inject(DOCUMENT);
|
|
516
|
+
const appRef = inject(ApplicationRef);
|
|
517
|
+
// Creating outside the component tree means it won't appear in CD tracking.
|
|
518
|
+
// Attaching to ApplicationRef ensures signals and change detection work.
|
|
519
|
+
const toolbarRef = createComponent(ToolbarComponent, { environmentInjector: injector });
|
|
520
|
+
doc.body.appendChild(toolbarRef.location.nativeElement);
|
|
521
|
+
appRef.attachView(toolbarRef.hostView);
|
|
522
|
+
// Tell the scanner to exclude the toolbar's own renders
|
|
523
|
+
scanner.setToolbarInstance(toolbarRef.instance);
|
|
524
|
+
}
|
|
572
525
|
|
|
573
|
-
// Profiler event constants
|
|
574
|
-
const TemplateUpdateStart = 2;
|
|
575
|
-
const ChangeDetectionStart = 12;
|
|
576
|
-
const ChangeDetectionEnd = 13;
|
|
577
|
-
const ChangeDetectionSyncStart = 14;
|
|
578
|
-
const ChangeDetectionSyncEnd = 15;
|
|
579
526
|
/**
|
|
580
527
|
* Imperative API for angular-scan.
|
|
581
528
|
*
|
|
@@ -593,146 +540,76 @@ const ChangeDetectionSyncEnd = 15;
|
|
|
593
540
|
*/
|
|
594
541
|
function scan(options = {}) {
|
|
595
542
|
if (!isDevMode())
|
|
596
|
-
return
|
|
543
|
+
return () => { };
|
|
597
544
|
if (typeof window === 'undefined')
|
|
598
|
-
return
|
|
545
|
+
return () => { };
|
|
599
546
|
if (options.enabled === false)
|
|
600
|
-
return
|
|
547
|
+
return () => { };
|
|
601
548
|
const ng = getNgDebugApi();
|
|
602
549
|
if (!ng) {
|
|
603
550
|
console.warn('[angular-scan] Angular debug APIs (window.ng) not available. ' +
|
|
604
551
|
'Ensure you are running in development mode.');
|
|
605
|
-
return
|
|
552
|
+
return () => { };
|
|
606
553
|
}
|
|
607
554
|
const durationMs = options.flashDurationMs ?? 500;
|
|
608
555
|
const showBadges = options.showBadges !== false;
|
|
609
|
-
const overlay =
|
|
610
|
-
overlay.attach(document);
|
|
611
|
-
// State for the current tick
|
|
612
|
-
let inTick = false;
|
|
613
|
-
let inSyncPhase = false;
|
|
614
|
-
const mutatedNodes = new Set();
|
|
615
|
-
const tickInstances = new Set();
|
|
556
|
+
const overlay = createCanvasOverlay(document, window);
|
|
616
557
|
const renderCounts = new WeakMap();
|
|
617
|
-
const unnecessaryCounts = new WeakMap();
|
|
618
558
|
const badges = new Map();
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
break;
|
|
631
|
-
inTick = true;
|
|
632
|
-
mutatedNodes.clear();
|
|
633
|
-
tickInstances.clear();
|
|
634
|
-
observer.observe(document.body, {
|
|
635
|
-
subtree: true, childList: true, attributes: true, characterData: true,
|
|
636
|
-
});
|
|
637
|
-
break;
|
|
638
|
-
case ChangeDetectionSyncStart:
|
|
639
|
-
inSyncPhase = true;
|
|
640
|
-
break;
|
|
641
|
-
case ChangeDetectionSyncEnd:
|
|
642
|
-
inSyncPhase = false;
|
|
643
|
-
break;
|
|
644
|
-
case TemplateUpdateStart:
|
|
645
|
-
if (inSyncPhase && instance)
|
|
646
|
-
tickInstances.add(instance);
|
|
647
|
-
break;
|
|
648
|
-
case ChangeDetectionEnd: {
|
|
649
|
-
if (!inTick)
|
|
650
|
-
break;
|
|
651
|
-
inTick = false;
|
|
652
|
-
inSyncPhase = false;
|
|
653
|
-
const pending = observer.takeRecords();
|
|
654
|
-
for (const r of pending) {
|
|
655
|
-
mutatedNodes.add(r.target);
|
|
656
|
-
r.addedNodes.forEach(n => mutatedNodes.add(n));
|
|
657
|
-
r.removedNodes.forEach(n => mutatedNodes.add(n));
|
|
658
|
-
}
|
|
659
|
-
observer.disconnect();
|
|
660
|
-
// Build mutated host set
|
|
661
|
-
const mutatedHosts = new Set();
|
|
662
|
-
for (const node of mutatedNodes) {
|
|
663
|
-
let current = node;
|
|
664
|
-
while (current && current !== document.body) {
|
|
665
|
-
if (current instanceof Element) {
|
|
666
|
-
try {
|
|
667
|
-
if (ng.getComponent(current)) {
|
|
668
|
-
mutatedHosts.add(current);
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
catch { /* ignore */ }
|
|
673
|
-
}
|
|
674
|
-
current = current.parentNode;
|
|
559
|
+
const teardown = createTickProfiler({
|
|
560
|
+
ng,
|
|
561
|
+
body: document.body,
|
|
562
|
+
onUpdates: (updates) => {
|
|
563
|
+
queueMicrotask(() => {
|
|
564
|
+
for (const { instance, hostElement, kind } of updates) {
|
|
565
|
+
const count = (renderCounts.get(instance) ?? 0) + 1;
|
|
566
|
+
renderCounts.set(instance, count);
|
|
567
|
+
overlay.flash(hostElement, kind, durationMs);
|
|
568
|
+
if (showBadges) {
|
|
569
|
+
updateBadge(badges, hostElement, count, kind);
|
|
675
570
|
}
|
|
676
571
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
for (const inst of tickInstances) {
|
|
680
|
-
try {
|
|
681
|
-
const hostEl = ng.getHostElement(inst);
|
|
682
|
-
if (!hostEl)
|
|
683
|
-
continue;
|
|
684
|
-
const kind = mutatedHosts.has(hostEl) ? 'render' : 'unnecessary';
|
|
685
|
-
updates.push({ instance: inst, hostEl, kind });
|
|
686
|
-
}
|
|
687
|
-
catch { /* component destroyed */ }
|
|
688
|
-
}
|
|
689
|
-
queueMicrotask(() => {
|
|
690
|
-
for (const { instance, hostEl, kind } of updates) {
|
|
691
|
-
const count = (renderCounts.get(instance) ?? 0) + 1;
|
|
692
|
-
renderCounts.set(instance, count);
|
|
693
|
-
if (kind === 'unnecessary') {
|
|
694
|
-
unnecessaryCounts.set(instance, (unnecessaryCounts.get(instance) ?? 0) + 1);
|
|
695
|
-
}
|
|
696
|
-
overlay.flash(hostEl, kind, durationMs);
|
|
697
|
-
if (showBadges)
|
|
698
|
-
updateBadge(hostEl, count, kind);
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
break;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
572
|
+
});
|
|
573
|
+
},
|
|
704
574
|
});
|
|
705
|
-
function updateBadge(hostEl, count, kind) {
|
|
706
|
-
let badge = badges.get(hostEl);
|
|
707
|
-
if (!badge) {
|
|
708
|
-
badge = document.createElement('div');
|
|
709
|
-
badge.setAttribute('aria-hidden', 'true');
|
|
710
|
-
badge.setAttribute('role', 'presentation');
|
|
711
|
-
badge.style.cssText = [
|
|
712
|
-
'position:absolute', 'top:2px', 'right:2px',
|
|
713
|
-
'z-index:2147483645', 'pointer-events:none',
|
|
714
|
-
'font:bold 9px/13px monospace', 'padding:1px 4px',
|
|
715
|
-
'border-radius:3px', 'min-width:16px', 'text-align:center', 'color:#fff',
|
|
716
|
-
].join(';');
|
|
717
|
-
const htmlEl = hostEl;
|
|
718
|
-
if (getComputedStyle(htmlEl).position === 'static')
|
|
719
|
-
htmlEl.style.position = 'relative';
|
|
720
|
-
hostEl.appendChild(badge);
|
|
721
|
-
badges.set(hostEl, badge);
|
|
722
|
-
}
|
|
723
|
-
badge.style.background = kind === 'unnecessary' ? '#f44336' : '#ff9800';
|
|
724
|
-
badge.textContent = String(count);
|
|
725
|
-
}
|
|
726
575
|
return () => {
|
|
727
|
-
|
|
728
|
-
observer.disconnect();
|
|
576
|
+
teardown();
|
|
729
577
|
overlay.detach();
|
|
730
|
-
for (const b of badges.values())
|
|
578
|
+
for (const b of badges.values()) {
|
|
731
579
|
b.remove();
|
|
580
|
+
}
|
|
732
581
|
badges.clear();
|
|
733
582
|
};
|
|
734
583
|
}
|
|
735
|
-
function
|
|
584
|
+
function updateBadge(badges, hostEl, count, kind) {
|
|
585
|
+
let badge = badges.get(hostEl);
|
|
586
|
+
if (!badge) {
|
|
587
|
+
badge = document.createElement('div');
|
|
588
|
+
badge.setAttribute('aria-hidden', 'true');
|
|
589
|
+
badge.setAttribute('role', 'presentation');
|
|
590
|
+
badge.style.cssText = [
|
|
591
|
+
'position:absolute',
|
|
592
|
+
'top:2px',
|
|
593
|
+
'right:2px',
|
|
594
|
+
'z-index:2147483645',
|
|
595
|
+
'pointer-events:none',
|
|
596
|
+
'font:bold 9px/13px monospace',
|
|
597
|
+
'padding:1px 4px',
|
|
598
|
+
'border-radius:3px',
|
|
599
|
+
'min-width:16px',
|
|
600
|
+
'text-align:center',
|
|
601
|
+
'color:#fff',
|
|
602
|
+
].join(';');
|
|
603
|
+
const htmlEl = hostEl;
|
|
604
|
+
if (getComputedStyle(htmlEl).position === 'static') {
|
|
605
|
+
htmlEl.style.position = 'relative';
|
|
606
|
+
}
|
|
607
|
+
hostEl.appendChild(badge);
|
|
608
|
+
badges.set(hostEl, badge);
|
|
609
|
+
}
|
|
610
|
+
badge.style.background = kind === 'unnecessary' ? '#f44336' : '#ff9800';
|
|
611
|
+
badge.textContent = String(count);
|
|
612
|
+
}
|
|
736
613
|
|
|
737
614
|
/*
|
|
738
615
|
* Public API Surface of angular-scan
|