@ng-annotate/angular 0.1.1
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/dist/fesm2022/ng-annotate-angular.mjs +702 -0
- package/dist/fesm2022/ng-annotate-angular.mjs.map +1 -0
- package/dist/types/ng-annotate-angular.d.ts +135 -0
- package/package.json +40 -0
- package/schematics/collection.json +9 -0
- package/schematics/ng-add/index.js +139 -0
- package/schematics/ng-add/index.ts +181 -0
- package/schematics/tsconfig.json +16 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, NgZone, ChangeDetectorRef, HostListener, ViewChild, ChangeDetectionStrategy, Component, isDevMode, provideAppInitializer, ApplicationRef, EnvironmentInjector, createComponent, NgModule, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import { JsonPipe } from '@angular/common';
|
|
5
|
+
import * as i1 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
|
|
8
|
+
const DOM_SNAPSHOT_MAX = 5000;
|
|
9
|
+
class InspectorService {
|
|
10
|
+
getComponentContext(element) {
|
|
11
|
+
const component = this.findNearestComponent(element);
|
|
12
|
+
if (!component)
|
|
13
|
+
return null;
|
|
14
|
+
const componentName = component.constructor.name;
|
|
15
|
+
const { component: componentFilePath, template: templateFilePath } = this.resolveFilePaths(componentName);
|
|
16
|
+
const selector = this.getSelector(component);
|
|
17
|
+
const inputs = this.getInputs(component);
|
|
18
|
+
const domSnapshot = this.snapshot(element);
|
|
19
|
+
const componentTreePath = this.buildTreePath(element);
|
|
20
|
+
return {
|
|
21
|
+
componentName,
|
|
22
|
+
componentFilePath,
|
|
23
|
+
...(templateFilePath ? { templateFilePath } : {}),
|
|
24
|
+
selector,
|
|
25
|
+
inputs,
|
|
26
|
+
domSnapshot,
|
|
27
|
+
componentTreePath,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
findNearestComponent(element) {
|
|
31
|
+
let current = element;
|
|
32
|
+
while (current) {
|
|
33
|
+
try {
|
|
34
|
+
const comp = ng.getComponent(current);
|
|
35
|
+
if (comp)
|
|
36
|
+
return comp;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
current = current.parentElement;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
getSelector(component) {
|
|
46
|
+
try {
|
|
47
|
+
const cmp = component.constructor.ɵcmp;
|
|
48
|
+
if (!cmp?.selectors?.length)
|
|
49
|
+
return 'unknown-selector';
|
|
50
|
+
const first = cmp.selectors[0];
|
|
51
|
+
if (!first.length)
|
|
52
|
+
return 'unknown-selector';
|
|
53
|
+
// Element selector: [['app-foo']]
|
|
54
|
+
// Attribute selector: [['', 'appFoo', '']]
|
|
55
|
+
if (first[0] === '') {
|
|
56
|
+
// Attribute selector — find non-empty entries
|
|
57
|
+
const attrParts = [];
|
|
58
|
+
for (let i = 1; i < first.length; i += 2) {
|
|
59
|
+
if (typeof first[i] === 'string' && first[i]) {
|
|
60
|
+
attrParts.push(`[${String(first[i])}]`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return attrParts.join('') || 'unknown-selector';
|
|
64
|
+
}
|
|
65
|
+
return String(first[0]);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return 'unknown-selector';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getInputs(component) {
|
|
72
|
+
try {
|
|
73
|
+
const cmp = component.constructor.ɵcmp;
|
|
74
|
+
if (!cmp?.inputs)
|
|
75
|
+
return {};
|
|
76
|
+
const result = {};
|
|
77
|
+
for (const [propName] of Object.entries(cmp.inputs)) {
|
|
78
|
+
if (typeof propName === 'symbol')
|
|
79
|
+
continue;
|
|
80
|
+
if (propName.startsWith('ɵ'))
|
|
81
|
+
continue;
|
|
82
|
+
result[propName] = component[propName];
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
buildTreePath(element) {
|
|
91
|
+
const path = [];
|
|
92
|
+
let current = element.parentElement;
|
|
93
|
+
while (current) {
|
|
94
|
+
try {
|
|
95
|
+
const comp = ng.getComponent(current);
|
|
96
|
+
if (comp) {
|
|
97
|
+
path.unshift(comp.constructor.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
current = current.parentElement;
|
|
104
|
+
}
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
snapshot(element) {
|
|
108
|
+
const html = element.outerHTML;
|
|
109
|
+
if (html.length <= DOM_SNAPSHOT_MAX)
|
|
110
|
+
return html;
|
|
111
|
+
return html.slice(0, DOM_SNAPSHOT_MAX) + '<!-- truncated -->';
|
|
112
|
+
}
|
|
113
|
+
resolveFilePaths(componentName) {
|
|
114
|
+
try {
|
|
115
|
+
const manifest = window
|
|
116
|
+
.__NG_ANNOTATE_MANIFEST__;
|
|
117
|
+
const entry = manifest?.[componentName];
|
|
118
|
+
if (entry)
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
124
|
+
return { component: `(unresolved: ${componentName})` };
|
|
125
|
+
}
|
|
126
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InspectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
127
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InspectorService });
|
|
128
|
+
}
|
|
129
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InspectorService, decorators: [{
|
|
130
|
+
type: Injectable
|
|
131
|
+
}] });
|
|
132
|
+
|
|
133
|
+
class BridgeService {
|
|
134
|
+
session$ = new BehaviorSubject(null);
|
|
135
|
+
annotations$ = new BehaviorSubject([]);
|
|
136
|
+
connected$ = new BehaviorSubject(false);
|
|
137
|
+
zone = inject(NgZone);
|
|
138
|
+
ws = null;
|
|
139
|
+
reconnectTimer = null;
|
|
140
|
+
init() {
|
|
141
|
+
this.connect();
|
|
142
|
+
}
|
|
143
|
+
connect() {
|
|
144
|
+
const wsUrl = `ws://${location.host}/__annotate`;
|
|
145
|
+
this.ws = new WebSocket(wsUrl);
|
|
146
|
+
this.ws.onopen = () => {
|
|
147
|
+
this.zone.run(() => {
|
|
148
|
+
this.connected$.next(true);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
this.ws.onmessage = (event) => {
|
|
152
|
+
this.zone.run(() => {
|
|
153
|
+
try {
|
|
154
|
+
const data = JSON.parse(event.data);
|
|
155
|
+
if (data.type === 'session:created') {
|
|
156
|
+
this.session$.next(data.session);
|
|
157
|
+
}
|
|
158
|
+
else if (data.type === 'annotations:sync') {
|
|
159
|
+
this.annotations$.next(data.annotations);
|
|
160
|
+
}
|
|
161
|
+
else if (data.type === 'annotation:created') {
|
|
162
|
+
const annotation = data.annotation;
|
|
163
|
+
const current = this.annotations$.getValue();
|
|
164
|
+
this.annotations$.next([...current, annotation]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// ignore malformed messages
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
this.ws.onclose = () => {
|
|
173
|
+
this.zone.run(() => {
|
|
174
|
+
this.connected$.next(false);
|
|
175
|
+
this.reconnectTimer = setTimeout(() => { this.connect(); }, 3000);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
this.ws.onerror = (event) => {
|
|
179
|
+
console.warn('[ng-annotate] WebSocket error', event);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
createAnnotation(payload) {
|
|
183
|
+
this.send({ type: 'annotation:create', payload });
|
|
184
|
+
}
|
|
185
|
+
replyToAnnotation(id, message) {
|
|
186
|
+
this.send({ type: 'annotation:reply', id, message });
|
|
187
|
+
}
|
|
188
|
+
deleteAnnotation(id) {
|
|
189
|
+
this.send({ type: 'annotation:delete', id });
|
|
190
|
+
}
|
|
191
|
+
send(msg) {
|
|
192
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
193
|
+
this.ws.send(JSON.stringify(msg));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
ngOnDestroy() {
|
|
197
|
+
if (this.reconnectTimer !== null) {
|
|
198
|
+
clearTimeout(this.reconnectTimer);
|
|
199
|
+
this.reconnectTimer = null;
|
|
200
|
+
}
|
|
201
|
+
this.ws?.close();
|
|
202
|
+
}
|
|
203
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BridgeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
204
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BridgeService });
|
|
205
|
+
}
|
|
206
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BridgeService, decorators: [{
|
|
207
|
+
type: Injectable
|
|
208
|
+
}] });
|
|
209
|
+
|
|
210
|
+
class OverlayComponent {
|
|
211
|
+
textArea;
|
|
212
|
+
mode = 'hidden';
|
|
213
|
+
hoveredContext = null;
|
|
214
|
+
highlightRect = null;
|
|
215
|
+
selectedContext = null;
|
|
216
|
+
annotationText = '';
|
|
217
|
+
selectionText = '';
|
|
218
|
+
threadAnnotation = null;
|
|
219
|
+
replyText = '';
|
|
220
|
+
badges = [];
|
|
221
|
+
inspector = inject(InspectorService);
|
|
222
|
+
bridge = inject(BridgeService);
|
|
223
|
+
cdr = inject(ChangeDetectorRef);
|
|
224
|
+
ngOnInit() {
|
|
225
|
+
this.bridge.annotations$.subscribe((annotations) => {
|
|
226
|
+
this.updateBadges(annotations);
|
|
227
|
+
this.cdr.markForCheck();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
toggleInspect(event) {
|
|
231
|
+
event?.preventDefault();
|
|
232
|
+
if (this.mode === 'hidden')
|
|
233
|
+
this.mode = 'inspect';
|
|
234
|
+
else if (this.mode === 'inspect')
|
|
235
|
+
this.mode = 'hidden';
|
|
236
|
+
else if (this.mode === 'annotate')
|
|
237
|
+
this.mode = 'inspect';
|
|
238
|
+
this.cdr.markForCheck();
|
|
239
|
+
}
|
|
240
|
+
onEscape() {
|
|
241
|
+
if (this.mode === 'annotate')
|
|
242
|
+
this.mode = 'inspect';
|
|
243
|
+
else if (this.mode === 'inspect')
|
|
244
|
+
this.mode = 'hidden';
|
|
245
|
+
else if (this.mode === 'thread')
|
|
246
|
+
this.mode = 'hidden';
|
|
247
|
+
this.cdr.markForCheck();
|
|
248
|
+
}
|
|
249
|
+
onScrollOrResize() {
|
|
250
|
+
if (this.badges.length > 0) {
|
|
251
|
+
this.refreshBadgePositions();
|
|
252
|
+
this.cdr.markForCheck();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
onMouseMove(event) {
|
|
256
|
+
if (this.mode !== 'inspect')
|
|
257
|
+
return;
|
|
258
|
+
const target = event.target;
|
|
259
|
+
if (target.closest('nga-overlay'))
|
|
260
|
+
return;
|
|
261
|
+
const context = this.inspector.getComponentContext(target);
|
|
262
|
+
this.hoveredContext = context;
|
|
263
|
+
if (context) {
|
|
264
|
+
const rect = target.getBoundingClientRect();
|
|
265
|
+
this.highlightRect = {
|
|
266
|
+
top: `${rect.top.toString()}px`,
|
|
267
|
+
left: `${rect.left.toString()}px`,
|
|
268
|
+
width: `${rect.width.toString()}px`,
|
|
269
|
+
height: `${rect.height.toString()}px`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.highlightRect = null;
|
|
274
|
+
}
|
|
275
|
+
this.cdr.markForCheck();
|
|
276
|
+
}
|
|
277
|
+
onClick(event) {
|
|
278
|
+
if (this.mode !== 'inspect')
|
|
279
|
+
return;
|
|
280
|
+
const target = event.target;
|
|
281
|
+
if (target.closest('nga-overlay'))
|
|
282
|
+
return;
|
|
283
|
+
const context = this.inspector.getComponentContext(target);
|
|
284
|
+
if (!context)
|
|
285
|
+
return;
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
event.stopPropagation();
|
|
288
|
+
this.selectedContext = context;
|
|
289
|
+
this.annotationText = '';
|
|
290
|
+
this.selectionText = window.getSelection()?.toString() ?? '';
|
|
291
|
+
this.mode = 'annotate';
|
|
292
|
+
this.cdr.markForCheck();
|
|
293
|
+
setTimeout(() => { this.textArea?.nativeElement.focus(); }, 0);
|
|
294
|
+
}
|
|
295
|
+
submit() {
|
|
296
|
+
if (!this.selectedContext || !this.annotationText.trim())
|
|
297
|
+
return;
|
|
298
|
+
this.bridge.createAnnotation({
|
|
299
|
+
...this.selectedContext,
|
|
300
|
+
annotationText: this.annotationText.trim(),
|
|
301
|
+
selectionText: this.selectionText || undefined,
|
|
302
|
+
});
|
|
303
|
+
this.selectedContext = null;
|
|
304
|
+
this.annotationText = '';
|
|
305
|
+
this.mode = 'inspect';
|
|
306
|
+
this.cdr.markForCheck();
|
|
307
|
+
}
|
|
308
|
+
cancel() {
|
|
309
|
+
this.mode = 'inspect';
|
|
310
|
+
this.cdr.markForCheck();
|
|
311
|
+
}
|
|
312
|
+
openThread(annotation) {
|
|
313
|
+
this.threadAnnotation = annotation;
|
|
314
|
+
this.mode = 'thread';
|
|
315
|
+
this.cdr.markForCheck();
|
|
316
|
+
}
|
|
317
|
+
closeThread() {
|
|
318
|
+
this.threadAnnotation = null;
|
|
319
|
+
this.mode = 'hidden';
|
|
320
|
+
this.cdr.markForCheck();
|
|
321
|
+
}
|
|
322
|
+
sendReply() {
|
|
323
|
+
if (!this.threadAnnotation || !this.replyText.trim())
|
|
324
|
+
return;
|
|
325
|
+
this.bridge.replyToAnnotation(this.threadAnnotation.id, this.replyText.trim());
|
|
326
|
+
this.replyText = '';
|
|
327
|
+
this.cdr.markForCheck();
|
|
328
|
+
}
|
|
329
|
+
inputEntries() {
|
|
330
|
+
if (!this.selectedContext)
|
|
331
|
+
return [];
|
|
332
|
+
return Object.entries(this.selectedContext.inputs)
|
|
333
|
+
.slice(0, 5)
|
|
334
|
+
.map(([key, value]) => ({ key, value }));
|
|
335
|
+
}
|
|
336
|
+
updateBadges(annotations) {
|
|
337
|
+
this.badges = annotations
|
|
338
|
+
.map((annotation) => {
|
|
339
|
+
const el = this.findComponentElement(annotation.componentName, annotation.selector);
|
|
340
|
+
if (!el)
|
|
341
|
+
return null;
|
|
342
|
+
const rect = el.getBoundingClientRect();
|
|
343
|
+
return {
|
|
344
|
+
annotation,
|
|
345
|
+
top: `${rect.top.toString()}px`,
|
|
346
|
+
left: `${(rect.left + rect.width - 12).toString()}px`,
|
|
347
|
+
icon: this.badgeIcon(annotation.status),
|
|
348
|
+
label: `${annotation.componentName}: ${annotation.annotationText.slice(0, 40)}`,
|
|
349
|
+
};
|
|
350
|
+
})
|
|
351
|
+
.filter((b) => b !== null);
|
|
352
|
+
}
|
|
353
|
+
refreshBadgePositions() {
|
|
354
|
+
this.badges = this.badges.map((badge) => {
|
|
355
|
+
const el = this.findComponentElement(badge.annotation.componentName, badge.annotation.selector);
|
|
356
|
+
if (!el)
|
|
357
|
+
return badge;
|
|
358
|
+
const rect = el.getBoundingClientRect();
|
|
359
|
+
return {
|
|
360
|
+
...badge,
|
|
361
|
+
top: `${rect.top.toString()}px`,
|
|
362
|
+
left: `${(rect.left + rect.width - 12).toString()}px`,
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
findComponentElement(componentName, selector) {
|
|
367
|
+
const bySelector = document.querySelector(selector);
|
|
368
|
+
if (bySelector)
|
|
369
|
+
return bySelector;
|
|
370
|
+
const all = document.querySelectorAll('*');
|
|
371
|
+
for (const el of Array.from(all)) {
|
|
372
|
+
try {
|
|
373
|
+
const comp = window
|
|
374
|
+
.ng;
|
|
375
|
+
if (comp?.getComponent(el)?.constructor.name === componentName)
|
|
376
|
+
return el;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// ignore
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
badgeIcon(status) {
|
|
385
|
+
const icons = {
|
|
386
|
+
pending: '●',
|
|
387
|
+
acknowledged: '◐',
|
|
388
|
+
resolved: '✓',
|
|
389
|
+
dismissed: '✕',
|
|
390
|
+
};
|
|
391
|
+
return icons[status];
|
|
392
|
+
}
|
|
393
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: OverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
394
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: OverlayComponent, isStandalone: true, selector: "nga-overlay", host: { listeners: { "document:keydown.alt.shift.a": "toggleInspect($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onScrollOrResize()", "window:resize": "onScrollOrResize()", "document:mousemove": "onMouseMove($event)", "document:click": "onClick($event)" } }, viewQueries: [{ propertyName: "textArea", first: true, predicate: ["textArea"], descendants: true }], ngImport: i0, template: `
|
|
395
|
+
<!-- Keyboard hint -->
|
|
396
|
+
@if (mode === 'hidden') {
|
|
397
|
+
<div class="nga-keyboard-hint">
|
|
398
|
+
<kbd>Alt+Shift+A</kbd> to annotate
|
|
399
|
+
</div>
|
|
400
|
+
}
|
|
401
|
+
@if (mode === 'inspect') {
|
|
402
|
+
<div class="nga-keyboard-hint">
|
|
403
|
+
Click a component <kbd>Esc</kbd> to cancel
|
|
404
|
+
</div>
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
<!-- Inspect highlight rect -->
|
|
408
|
+
@if (mode === 'inspect' && hoveredContext !== null && highlightRect !== null) {
|
|
409
|
+
<div
|
|
410
|
+
class="nga-highlight-rect"
|
|
411
|
+
[style.top]="highlightRect.top"
|
|
412
|
+
[style.left]="highlightRect.left"
|
|
413
|
+
[style.width]="highlightRect.width"
|
|
414
|
+
[style.height]="highlightRect.height"
|
|
415
|
+
>
|
|
416
|
+
<span class="nga-component-label">{{ hoveredContext.componentName }}</span>
|
|
417
|
+
</div>
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
<!-- Annotate panel -->
|
|
421
|
+
@if (mode === 'annotate' && selectedContext !== null) {
|
|
422
|
+
<div class="nga-annotate-panel">
|
|
423
|
+
<h3 class="nga-panel-title">{{ selectedContext.componentName }}</h3>
|
|
424
|
+
|
|
425
|
+
@if (inputEntries().length > 0) {
|
|
426
|
+
<div class="nga-inputs">
|
|
427
|
+
@for (entry of inputEntries(); track entry.key) {
|
|
428
|
+
<div class="nga-input-row">
|
|
429
|
+
<span class="nga-input-key">{{ entry.key }}:</span>
|
|
430
|
+
<span class="nga-input-val">{{ entry.value | json }}</span>
|
|
431
|
+
</div>
|
|
432
|
+
}
|
|
433
|
+
</div>
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@if (selectionText) {
|
|
437
|
+
<div class="nga-selection">
|
|
438
|
+
<em>"{{ selectionText }}"</em>
|
|
439
|
+
</div>
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
<textarea
|
|
443
|
+
#textArea
|
|
444
|
+
class="nga-textarea"
|
|
445
|
+
[(ngModel)]="annotationText"
|
|
446
|
+
placeholder="Describe the change..."
|
|
447
|
+
rows="4"
|
|
448
|
+
></textarea>
|
|
449
|
+
|
|
450
|
+
<div class="nga-actions">
|
|
451
|
+
<button class="nga-btn nga-btn-submit" (click)="submit()" [disabled]="annotationText.trim() === ''">
|
|
452
|
+
Submit
|
|
453
|
+
</button>
|
|
454
|
+
<button class="nga-btn nga-btn-cancel" (click)="cancel()">Cancel</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
<!-- Thread panel -->
|
|
460
|
+
@if (mode === 'thread' && threadAnnotation !== null) {
|
|
461
|
+
<div class="nga-thread-panel">
|
|
462
|
+
<h3 class="nga-panel-title">{{ threadAnnotation.componentName }}</h3>
|
|
463
|
+
|
|
464
|
+
<div class="nga-replies">
|
|
465
|
+
@for (reply of threadAnnotation.replies; track reply.message) {
|
|
466
|
+
<div class="nga-reply">
|
|
467
|
+
<span class="nga-reply-author nga-reply-author--{{ reply.author }}">{{ reply.author }}</span>
|
|
468
|
+
<span class="nga-reply-text">{{ reply.message }}</span>
|
|
469
|
+
</div>
|
|
470
|
+
}
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<input
|
|
474
|
+
class="nga-reply-input"
|
|
475
|
+
type="text"
|
|
476
|
+
[(ngModel)]="replyText"
|
|
477
|
+
placeholder="Reply..."
|
|
478
|
+
(keydown.enter)="sendReply()"
|
|
479
|
+
/>
|
|
480
|
+
|
|
481
|
+
<div class="nga-actions">
|
|
482
|
+
<button class="nga-btn nga-btn-submit" (click)="sendReply()" [disabled]="replyText.trim() === ''">
|
|
483
|
+
Send
|
|
484
|
+
</button>
|
|
485
|
+
<button class="nga-btn nga-btn-cancel" (click)="closeThread()">Close</button>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
<!-- Annotation badges -->
|
|
491
|
+
@for (badge of badges; track badge.annotation.id) {
|
|
492
|
+
<div
|
|
493
|
+
class="nga-badge nga-badge--{{ badge.annotation.status }}"
|
|
494
|
+
[style.top]="badge.top"
|
|
495
|
+
[style.left]="badge.left"
|
|
496
|
+
(click)="openThread(badge.annotation)"
|
|
497
|
+
[title]="badge.label"
|
|
498
|
+
>
|
|
499
|
+
{{ badge.icon }}
|
|
500
|
+
</div>
|
|
501
|
+
}
|
|
502
|
+
`, isInline: true, styles: [":host{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999}.nga-highlight-rect{position:fixed;border:2px solid #3b82f6;background:#3b82f61a;pointer-events:none;transition:top .05s,left .05s,width .05s,height .05s}.nga-component-label{position:absolute;top:-22px;left:0;background:#1e293b;color:#f8fafc;font-family:monospace;font-size:11px;padding:2px 6px;border-radius:3px;white-space:nowrap}.nga-annotate-panel,.nga-thread-panel{pointer-events:all;position:fixed;right:16px;top:50%;transform:translateY(-50%);background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 24px #00000026;padding:16px;min-width:320px;max-width:400px}.nga-panel-title{margin:0 0 12px;font-size:14px;font-weight:600;color:#1e293b;font-family:monospace}.nga-inputs{margin-bottom:10px}.nga-input-row{display:flex;gap:8px;font-size:12px;font-family:monospace;margin-bottom:4px}.nga-input-key{color:#64748b;min-width:80px}.nga-input-val{color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nga-selection{font-size:12px;color:#475569;margin-bottom:8px;font-style:italic}.nga-textarea,.nga-reply-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:4px;padding:8px;font-size:13px;font-family:inherit;resize:vertical;margin-bottom:10px}.nga-textarea:focus,.nga-reply-input:focus{outline:none;border-color:#3b82f6}.nga-actions{display:flex;gap:8px}.nga-btn{padding:6px 14px;border-radius:4px;font-size:13px;cursor:pointer;border:none;transition:opacity .15s}.nga-btn:disabled{opacity:.4;cursor:not-allowed}.nga-btn-submit{background:#3b82f6;color:#fff}.nga-btn-cancel{background:#f1f5f9;color:#475569}.nga-replies{max-height:200px;overflow-y:auto;margin-bottom:10px}.nga-reply{display:flex;gap:8px;margin-bottom:8px;font-size:13px}.nga-reply-author{font-weight:600;min-width:48px}.nga-reply-author--agent{color:#7c3aed}.nga-reply-author--user{color:#2563eb}.nga-badge{pointer-events:all;position:fixed;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;cursor:pointer;transform:translate(-50%,-50%)}.nga-badge--pending{background:#3b82f6;color:#fff}.nga-badge--acknowledged{background:#f59e0b;color:#fff}.nga-badge--resolved{background:#22c55e;color:#fff}.nga-badge--dismissed{background:#94a3b8;color:#fff}.nga-keyboard-hint{pointer-events:none;position:fixed;bottom:16px;right:16px;background:#0f172ab3;color:#f8fafc;font-size:12px;padding:6px 10px;border-radius:6px}.nga-keyboard-hint kbd{background:#ffffff26;border-radius:3px;padding:1px 5px;font-family:monospace}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
503
|
+
}
|
|
504
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: OverlayComponent, decorators: [{
|
|
505
|
+
type: Component,
|
|
506
|
+
args: [{ selector: 'nga-overlay', changeDetection: ChangeDetectionStrategy.OnPush, imports: [JsonPipe, FormsModule], template: `
|
|
507
|
+
<!-- Keyboard hint -->
|
|
508
|
+
@if (mode === 'hidden') {
|
|
509
|
+
<div class="nga-keyboard-hint">
|
|
510
|
+
<kbd>Alt+Shift+A</kbd> to annotate
|
|
511
|
+
</div>
|
|
512
|
+
}
|
|
513
|
+
@if (mode === 'inspect') {
|
|
514
|
+
<div class="nga-keyboard-hint">
|
|
515
|
+
Click a component <kbd>Esc</kbd> to cancel
|
|
516
|
+
</div>
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
<!-- Inspect highlight rect -->
|
|
520
|
+
@if (mode === 'inspect' && hoveredContext !== null && highlightRect !== null) {
|
|
521
|
+
<div
|
|
522
|
+
class="nga-highlight-rect"
|
|
523
|
+
[style.top]="highlightRect.top"
|
|
524
|
+
[style.left]="highlightRect.left"
|
|
525
|
+
[style.width]="highlightRect.width"
|
|
526
|
+
[style.height]="highlightRect.height"
|
|
527
|
+
>
|
|
528
|
+
<span class="nga-component-label">{{ hoveredContext.componentName }}</span>
|
|
529
|
+
</div>
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
<!-- Annotate panel -->
|
|
533
|
+
@if (mode === 'annotate' && selectedContext !== null) {
|
|
534
|
+
<div class="nga-annotate-panel">
|
|
535
|
+
<h3 class="nga-panel-title">{{ selectedContext.componentName }}</h3>
|
|
536
|
+
|
|
537
|
+
@if (inputEntries().length > 0) {
|
|
538
|
+
<div class="nga-inputs">
|
|
539
|
+
@for (entry of inputEntries(); track entry.key) {
|
|
540
|
+
<div class="nga-input-row">
|
|
541
|
+
<span class="nga-input-key">{{ entry.key }}:</span>
|
|
542
|
+
<span class="nga-input-val">{{ entry.value | json }}</span>
|
|
543
|
+
</div>
|
|
544
|
+
}
|
|
545
|
+
</div>
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@if (selectionText) {
|
|
549
|
+
<div class="nga-selection">
|
|
550
|
+
<em>"{{ selectionText }}"</em>
|
|
551
|
+
</div>
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
<textarea
|
|
555
|
+
#textArea
|
|
556
|
+
class="nga-textarea"
|
|
557
|
+
[(ngModel)]="annotationText"
|
|
558
|
+
placeholder="Describe the change..."
|
|
559
|
+
rows="4"
|
|
560
|
+
></textarea>
|
|
561
|
+
|
|
562
|
+
<div class="nga-actions">
|
|
563
|
+
<button class="nga-btn nga-btn-submit" (click)="submit()" [disabled]="annotationText.trim() === ''">
|
|
564
|
+
Submit
|
|
565
|
+
</button>
|
|
566
|
+
<button class="nga-btn nga-btn-cancel" (click)="cancel()">Cancel</button>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
<!-- Thread panel -->
|
|
572
|
+
@if (mode === 'thread' && threadAnnotation !== null) {
|
|
573
|
+
<div class="nga-thread-panel">
|
|
574
|
+
<h3 class="nga-panel-title">{{ threadAnnotation.componentName }}</h3>
|
|
575
|
+
|
|
576
|
+
<div class="nga-replies">
|
|
577
|
+
@for (reply of threadAnnotation.replies; track reply.message) {
|
|
578
|
+
<div class="nga-reply">
|
|
579
|
+
<span class="nga-reply-author nga-reply-author--{{ reply.author }}">{{ reply.author }}</span>
|
|
580
|
+
<span class="nga-reply-text">{{ reply.message }}</span>
|
|
581
|
+
</div>
|
|
582
|
+
}
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<input
|
|
586
|
+
class="nga-reply-input"
|
|
587
|
+
type="text"
|
|
588
|
+
[(ngModel)]="replyText"
|
|
589
|
+
placeholder="Reply..."
|
|
590
|
+
(keydown.enter)="sendReply()"
|
|
591
|
+
/>
|
|
592
|
+
|
|
593
|
+
<div class="nga-actions">
|
|
594
|
+
<button class="nga-btn nga-btn-submit" (click)="sendReply()" [disabled]="replyText.trim() === ''">
|
|
595
|
+
Send
|
|
596
|
+
</button>
|
|
597
|
+
<button class="nga-btn nga-btn-cancel" (click)="closeThread()">Close</button>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
<!-- Annotation badges -->
|
|
603
|
+
@for (badge of badges; track badge.annotation.id) {
|
|
604
|
+
<div
|
|
605
|
+
class="nga-badge nga-badge--{{ badge.annotation.status }}"
|
|
606
|
+
[style.top]="badge.top"
|
|
607
|
+
[style.left]="badge.left"
|
|
608
|
+
(click)="openThread(badge.annotation)"
|
|
609
|
+
[title]="badge.label"
|
|
610
|
+
>
|
|
611
|
+
{{ badge.icon }}
|
|
612
|
+
</div>
|
|
613
|
+
}
|
|
614
|
+
`, styles: [":host{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999}.nga-highlight-rect{position:fixed;border:2px solid #3b82f6;background:#3b82f61a;pointer-events:none;transition:top .05s,left .05s,width .05s,height .05s}.nga-component-label{position:absolute;top:-22px;left:0;background:#1e293b;color:#f8fafc;font-family:monospace;font-size:11px;padding:2px 6px;border-radius:3px;white-space:nowrap}.nga-annotate-panel,.nga-thread-panel{pointer-events:all;position:fixed;right:16px;top:50%;transform:translateY(-50%);background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 24px #00000026;padding:16px;min-width:320px;max-width:400px}.nga-panel-title{margin:0 0 12px;font-size:14px;font-weight:600;color:#1e293b;font-family:monospace}.nga-inputs{margin-bottom:10px}.nga-input-row{display:flex;gap:8px;font-size:12px;font-family:monospace;margin-bottom:4px}.nga-input-key{color:#64748b;min-width:80px}.nga-input-val{color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nga-selection{font-size:12px;color:#475569;margin-bottom:8px;font-style:italic}.nga-textarea,.nga-reply-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:4px;padding:8px;font-size:13px;font-family:inherit;resize:vertical;margin-bottom:10px}.nga-textarea:focus,.nga-reply-input:focus{outline:none;border-color:#3b82f6}.nga-actions{display:flex;gap:8px}.nga-btn{padding:6px 14px;border-radius:4px;font-size:13px;cursor:pointer;border:none;transition:opacity .15s}.nga-btn:disabled{opacity:.4;cursor:not-allowed}.nga-btn-submit{background:#3b82f6;color:#fff}.nga-btn-cancel{background:#f1f5f9;color:#475569}.nga-replies{max-height:200px;overflow-y:auto;margin-bottom:10px}.nga-reply{display:flex;gap:8px;margin-bottom:8px;font-size:13px}.nga-reply-author{font-weight:600;min-width:48px}.nga-reply-author--agent{color:#7c3aed}.nga-reply-author--user{color:#2563eb}.nga-badge{pointer-events:all;position:fixed;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;cursor:pointer;transform:translate(-50%,-50%)}.nga-badge--pending{background:#3b82f6;color:#fff}.nga-badge--acknowledged{background:#f59e0b;color:#fff}.nga-badge--resolved{background:#22c55e;color:#fff}.nga-badge--dismissed{background:#94a3b8;color:#fff}.nga-keyboard-hint{pointer-events:none;position:fixed;bottom:16px;right:16px;background:#0f172ab3;color:#f8fafc;font-size:12px;padding:6px 10px;border-radius:6px}.nga-keyboard-hint kbd{background:#ffffff26;border-radius:3px;padding:1px 5px;font-family:monospace}\n"] }]
|
|
615
|
+
}], propDecorators: { textArea: [{
|
|
616
|
+
type: ViewChild,
|
|
617
|
+
args: ['textArea']
|
|
618
|
+
}], toggleInspect: [{
|
|
619
|
+
type: HostListener,
|
|
620
|
+
args: ['document:keydown.alt.shift.a', ['$event']]
|
|
621
|
+
}], onEscape: [{
|
|
622
|
+
type: HostListener,
|
|
623
|
+
args: ['document:keydown.escape']
|
|
624
|
+
}], onScrollOrResize: [{
|
|
625
|
+
type: HostListener,
|
|
626
|
+
args: ['window:scroll']
|
|
627
|
+
}, {
|
|
628
|
+
type: HostListener,
|
|
629
|
+
args: ['window:resize']
|
|
630
|
+
}], onMouseMove: [{
|
|
631
|
+
type: HostListener,
|
|
632
|
+
args: ['document:mousemove', ['$event']]
|
|
633
|
+
}], onClick: [{
|
|
634
|
+
type: HostListener,
|
|
635
|
+
args: ['document:click', ['$event']]
|
|
636
|
+
}] } });
|
|
637
|
+
|
|
638
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class -- required by NgModule pattern
|
|
639
|
+
class NgAnnotateModule {
|
|
640
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgAnnotateModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
641
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: NgAnnotateModule });
|
|
642
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgAnnotateModule, providers: isDevMode()
|
|
643
|
+
? [
|
|
644
|
+
InspectorService,
|
|
645
|
+
BridgeService,
|
|
646
|
+
provideAppInitializer(() => {
|
|
647
|
+
const bridge = inject(BridgeService);
|
|
648
|
+
const appRef = inject(ApplicationRef);
|
|
649
|
+
const envInjector = inject(EnvironmentInjector);
|
|
650
|
+
bridge.init();
|
|
651
|
+
const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });
|
|
652
|
+
appRef.attachView(overlayRef.hostView);
|
|
653
|
+
document.body.appendChild(overlayRef.location.nativeElement);
|
|
654
|
+
}),
|
|
655
|
+
]
|
|
656
|
+
: [] });
|
|
657
|
+
}
|
|
658
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NgAnnotateModule, decorators: [{
|
|
659
|
+
type: NgModule,
|
|
660
|
+
args: [{
|
|
661
|
+
providers: isDevMode()
|
|
662
|
+
? [
|
|
663
|
+
InspectorService,
|
|
664
|
+
BridgeService,
|
|
665
|
+
provideAppInitializer(() => {
|
|
666
|
+
const bridge = inject(BridgeService);
|
|
667
|
+
const appRef = inject(ApplicationRef);
|
|
668
|
+
const envInjector = inject(EnvironmentInjector);
|
|
669
|
+
bridge.init();
|
|
670
|
+
const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });
|
|
671
|
+
appRef.attachView(overlayRef.hostView);
|
|
672
|
+
document.body.appendChild(overlayRef.location.nativeElement);
|
|
673
|
+
}),
|
|
674
|
+
]
|
|
675
|
+
: [],
|
|
676
|
+
}]
|
|
677
|
+
}] });
|
|
678
|
+
|
|
679
|
+
function provideNgAnnotate() {
|
|
680
|
+
return makeEnvironmentProviders([
|
|
681
|
+
InspectorService,
|
|
682
|
+
BridgeService,
|
|
683
|
+
provideAppInitializer(() => {
|
|
684
|
+
if (!isDevMode())
|
|
685
|
+
return;
|
|
686
|
+
const bridge = inject(BridgeService);
|
|
687
|
+
const appRef = inject(ApplicationRef);
|
|
688
|
+
const envInjector = inject(EnvironmentInjector);
|
|
689
|
+
bridge.init();
|
|
690
|
+
const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });
|
|
691
|
+
appRef.attachView(overlayRef.hostView);
|
|
692
|
+
document.body.appendChild(overlayRef.location.nativeElement);
|
|
693
|
+
}),
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Generated bundle index. Do not edit.
|
|
699
|
+
*/
|
|
700
|
+
|
|
701
|
+
export { BridgeService, InspectorService, NgAnnotateModule, OverlayComponent, provideNgAnnotate };
|
|
702
|
+
//# sourceMappingURL=ng-annotate-angular.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ng-annotate-angular.mjs","sources":["../../src/inspector.service.ts","../../src/bridge.service.ts","../../src/overlay/overlay.component.ts","../../src/ng-annotate.module.ts","../../src/provide-ng-annotate.ts","../../src/ng-annotate-angular.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport type { ComponentContext } from './types';\n\ninterface NgDevMode {\n getComponent: (element: Element) => unknown;\n}\n\ndeclare const ng: NgDevMode;\n\ninterface NgCmp {\n selectors?: unknown[][];\n inputs?: Record<string, string>;\n}\n\ninterface ComponentInstance {\n constructor: { name: string; ɵcmp?: NgCmp } & (new (...args: unknown[]) => unknown);\n [key: string]: unknown;\n}\n\ninterface ManifestEntry {\n component: string;\n template?: string;\n}\n\nconst DOM_SNAPSHOT_MAX = 5000;\n\n@Injectable()\nexport class InspectorService {\n getComponentContext(element: Element): ComponentContext | null {\n const component = this.findNearestComponent(element);\n if (!component) return null;\n\n const componentName = (component as ComponentInstance).constructor.name;\n const { component: componentFilePath, template: templateFilePath } =\n this.resolveFilePaths(componentName);\n const selector = this.getSelector(component as ComponentInstance);\n const inputs = this.getInputs(component as ComponentInstance);\n const domSnapshot = this.snapshot(element);\n const componentTreePath = this.buildTreePath(element);\n\n return {\n componentName,\n componentFilePath,\n ...(templateFilePath ? { templateFilePath } : {}),\n selector,\n inputs,\n domSnapshot,\n componentTreePath,\n };\n }\n\n private findNearestComponent(element: Element): unknown {\n let current: Element | null = element;\n while (current) {\n try {\n const comp = ng.getComponent(current);\n if (comp) return comp;\n } catch {\n // ignore\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private getSelector(component: ComponentInstance): string {\n try {\n const cmp = component.constructor.ɵcmp;\n if (!cmp?.selectors?.length) return 'unknown-selector';\n const first = cmp.selectors[0];\n if (!first.length) return 'unknown-selector';\n\n // Element selector: [['app-foo']]\n // Attribute selector: [['', 'appFoo', '']]\n if (first[0] === '') {\n // Attribute selector — find non-empty entries\n const attrParts: string[] = [];\n for (let i = 1; i < first.length; i += 2) {\n if (typeof first[i] === 'string' && first[i]) {\n attrParts.push(`[${String(first[i])}]`);\n }\n }\n return attrParts.join('') || 'unknown-selector';\n }\n return String(first[0]);\n } catch {\n return 'unknown-selector';\n }\n }\n\n private getInputs(component: ComponentInstance): Record<string, unknown> {\n try {\n const cmp = component.constructor.ɵcmp;\n if (!cmp?.inputs) return {};\n const result: Record<string, unknown> = {};\n for (const [propName] of Object.entries(cmp.inputs)) {\n if (typeof propName === 'symbol') continue;\n if (propName.startsWith('ɵ')) continue;\n result[propName] = (component as Record<string, unknown>)[propName];\n }\n return result;\n } catch {\n return {};\n }\n }\n\n private buildTreePath(element: Element): string[] {\n const path: string[] = [];\n let current: Element | null = element.parentElement;\n while (current) {\n try {\n const comp = ng.getComponent(current);\n if (comp) {\n path.unshift((comp as ComponentInstance).constructor.name);\n }\n } catch {\n // ignore\n }\n current = current.parentElement;\n }\n return path;\n }\n\n private snapshot(element: Element): string {\n const html = element.outerHTML;\n if (html.length <= DOM_SNAPSHOT_MAX) return html;\n return html.slice(0, DOM_SNAPSHOT_MAX) + '<!-- truncated -->';\n }\n\n private resolveFilePaths(componentName: string): { component: string; template?: string } {\n try {\n const manifest = (window as unknown as { __NG_ANNOTATE_MANIFEST__?: Record<string, ManifestEntry> })\n .__NG_ANNOTATE_MANIFEST__;\n const entry = manifest?.[componentName];\n if (entry) return entry;\n } catch {\n // ignore\n }\n return { component: `(unresolved: ${componentName})` };\n }\n}\n","import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';\nimport { BehaviorSubject } from 'rxjs';\nimport type { Session, Annotation } from './types';\n\ntype BridgeMessage =\n | { type: 'session:created'; session: Session }\n | { type: 'annotations:sync'; annotations: Annotation[] }\n | { type: 'annotation:created'; annotation: Annotation }\n | { type: string };\n\n@Injectable()\nexport class BridgeService implements OnDestroy {\n readonly session$ = new BehaviorSubject<Session | null>(null);\n readonly annotations$ = new BehaviorSubject<Annotation[]>([]);\n readonly connected$ = new BehaviorSubject<boolean>(false);\n\n private readonly zone = inject(NgZone);\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n init(): void {\n this.connect();\n }\n\n private connect(): void {\n const wsUrl = `ws://${location.host}/__annotate`;\n this.ws = new WebSocket(wsUrl);\n\n this.ws.onopen = () => {\n this.zone.run(() => {\n this.connected$.next(true);\n });\n };\n\n this.ws.onmessage = (event: MessageEvent<string>) => {\n this.zone.run(() => {\n try {\n const data = JSON.parse(event.data) as BridgeMessage;\n if (data.type === 'session:created') {\n this.session$.next((data as Extract<BridgeMessage, { type: 'session:created' }>).session);\n } else if (data.type === 'annotations:sync') {\n this.annotations$.next(\n (data as Extract<BridgeMessage, { type: 'annotations:sync' }>).annotations,\n );\n } else if (data.type === 'annotation:created') {\n const annotation = (\n data as Extract<BridgeMessage, { type: 'annotation:created' }>\n ).annotation;\n const current = this.annotations$.getValue();\n this.annotations$.next([...current, annotation]);\n }\n } catch {\n // ignore malformed messages\n }\n });\n };\n\n this.ws.onclose = () => {\n this.zone.run(() => {\n this.connected$.next(false);\n this.reconnectTimer = setTimeout(() => { this.connect(); }, 3000);\n });\n };\n\n this.ws.onerror = (event) => {\n console.warn('[ng-annotate] WebSocket error', event);\n };\n }\n\n createAnnotation(payload: Record<string, unknown>): void {\n this.send({ type: 'annotation:create', payload });\n }\n\n replyToAnnotation(id: string, message: string): void {\n this.send({ type: 'annotation:reply', id, message });\n }\n\n deleteAnnotation(id: string): void {\n this.send({ type: 'annotation:delete', id });\n }\n\n private send(msg: Record<string, unknown>): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n ngOnDestroy(): void {\n if (this.reconnectTimer !== null) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n this.ws?.close();\n }\n}\n","import {\n Component,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n HostListener,\n OnInit,\n ViewChild,\n ElementRef,\n inject,\n} from '@angular/core';\nimport { JsonPipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { InspectorService } from '../inspector.service';\nimport { BridgeService } from '../bridge.service';\nimport type { Annotation, ComponentContext, AnnotationStatus } from '../types';\n\ntype OverlayMode = 'hidden' | 'inspect' | 'annotate' | 'thread';\n\ninterface HighlightRect {\n top: string;\n left: string;\n width: string;\n height: string;\n}\n\ninterface AnnotationBadge {\n annotation: Annotation;\n top: string;\n left: string;\n icon: string;\n label: string;\n}\n\n@Component({\n selector: 'nga-overlay',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [JsonPipe, FormsModule],\n styles: [`\n :host {\n position: fixed; top: 0; left: 0; width: 100%; height: 100%;\n pointer-events: none; z-index: 9999;\n }\n .nga-highlight-rect {\n position: fixed; border: 2px solid #3b82f6;\n background: rgba(59,130,246,0.1); pointer-events: none;\n transition: top 0.05s, left 0.05s, width 0.05s, height 0.05s;\n }\n .nga-component-label {\n position: absolute; top: -22px; left: 0; background: #1e293b; color: #f8fafc;\n font-family: monospace; font-size: 11px; padding: 2px 6px;\n border-radius: 3px; white-space: nowrap;\n }\n .nga-annotate-panel, .nga-thread-panel {\n pointer-events: all; position: fixed; right: 16px; top: 50%;\n transform: translateY(-50%); background: #ffffff; border: 1px solid #e2e8f0;\n border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.15);\n padding: 16px; min-width: 320px; max-width: 400px;\n }\n .nga-panel-title {\n margin: 0 0 12px; font-size: 14px; font-weight: 600;\n color: #1e293b; font-family: monospace;\n }\n .nga-inputs { margin-bottom: 10px; }\n .nga-input-row {\n display: flex; gap: 8px; font-size: 12px;\n font-family: monospace; margin-bottom: 4px;\n }\n .nga-input-key { color: #64748b; min-width: 80px; }\n .nga-input-val {\n color: #1e293b; overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap;\n }\n .nga-selection {\n font-size: 12px; color: #475569; margin-bottom: 8px; font-style: italic;\n }\n .nga-textarea, .nga-reply-input {\n width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1;\n border-radius: 4px; padding: 8px; font-size: 13px;\n font-family: inherit; resize: vertical; margin-bottom: 10px;\n }\n .nga-textarea:focus, .nga-reply-input:focus {\n outline: none; border-color: #3b82f6;\n }\n .nga-actions { display: flex; gap: 8px; }\n .nga-btn {\n padding: 6px 14px; border-radius: 4px; font-size: 13px;\n cursor: pointer; border: none; transition: opacity 0.15s;\n }\n .nga-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n .nga-btn-submit { background: #3b82f6; color: #ffffff; }\n .nga-btn-cancel { background: #f1f5f9; color: #475569; }\n .nga-replies { max-height: 200px; overflow-y: auto; margin-bottom: 10px; }\n .nga-reply { display: flex; gap: 8px; margin-bottom: 8px; font-size: 13px; }\n .nga-reply-author { font-weight: 600; min-width: 48px; }\n .nga-reply-author--agent { color: #7c3aed; }\n .nga-reply-author--user { color: #2563eb; }\n .nga-badge {\n pointer-events: all; position: fixed; width: 18px; height: 18px;\n border-radius: 50%; display: flex; align-items: center;\n justify-content: center; font-size: 10px; cursor: pointer;\n transform: translate(-50%, -50%);\n }\n .nga-badge--pending { background: #3b82f6; color: #ffffff; }\n .nga-badge--acknowledged { background: #f59e0b; color: #ffffff; }\n .nga-badge--resolved { background: #22c55e; color: #ffffff; }\n .nga-badge--dismissed { background: #94a3b8; color: #ffffff; }\n .nga-keyboard-hint {\n pointer-events: none; position: fixed; bottom: 16px; right: 16px;\n background: rgba(15,23,42,0.7); color: #f8fafc; font-size: 12px;\n padding: 6px 10px; border-radius: 6px;\n }\n .nga-keyboard-hint kbd {\n background: rgba(255,255,255,0.15); border-radius: 3px;\n padding: 1px 5px; font-family: monospace;\n }\n `],\n template: `\n <!-- Keyboard hint -->\n @if (mode === 'hidden') {\n <div class=\"nga-keyboard-hint\">\n <kbd>Alt+Shift+A</kbd> to annotate\n </div>\n }\n @if (mode === 'inspect') {\n <div class=\"nga-keyboard-hint\">\n Click a component <kbd>Esc</kbd> to cancel\n </div>\n }\n\n <!-- Inspect highlight rect -->\n @if (mode === 'inspect' && hoveredContext !== null && highlightRect !== null) {\n <div\n class=\"nga-highlight-rect\"\n [style.top]=\"highlightRect.top\"\n [style.left]=\"highlightRect.left\"\n [style.width]=\"highlightRect.width\"\n [style.height]=\"highlightRect.height\"\n >\n <span class=\"nga-component-label\">{{ hoveredContext.componentName }}</span>\n </div>\n }\n\n <!-- Annotate panel -->\n @if (mode === 'annotate' && selectedContext !== null) {\n <div class=\"nga-annotate-panel\">\n <h3 class=\"nga-panel-title\">{{ selectedContext.componentName }}</h3>\n\n @if (inputEntries().length > 0) {\n <div class=\"nga-inputs\">\n @for (entry of inputEntries(); track entry.key) {\n <div class=\"nga-input-row\">\n <span class=\"nga-input-key\">{{ entry.key }}:</span>\n <span class=\"nga-input-val\">{{ entry.value | json }}</span>\n </div>\n }\n </div>\n }\n\n @if (selectionText) {\n <div class=\"nga-selection\">\n <em>\"{{ selectionText }}\"</em>\n </div>\n }\n\n <textarea\n #textArea\n class=\"nga-textarea\"\n [(ngModel)]=\"annotationText\"\n placeholder=\"Describe the change...\"\n rows=\"4\"\n ></textarea>\n\n <div class=\"nga-actions\">\n <button class=\"nga-btn nga-btn-submit\" (click)=\"submit()\" [disabled]=\"annotationText.trim() === ''\">\n Submit\n </button>\n <button class=\"nga-btn nga-btn-cancel\" (click)=\"cancel()\">Cancel</button>\n </div>\n </div>\n }\n\n <!-- Thread panel -->\n @if (mode === 'thread' && threadAnnotation !== null) {\n <div class=\"nga-thread-panel\">\n <h3 class=\"nga-panel-title\">{{ threadAnnotation.componentName }}</h3>\n\n <div class=\"nga-replies\">\n @for (reply of threadAnnotation.replies; track reply.message) {\n <div class=\"nga-reply\">\n <span class=\"nga-reply-author nga-reply-author--{{ reply.author }}\">{{ reply.author }}</span>\n <span class=\"nga-reply-text\">{{ reply.message }}</span>\n </div>\n }\n </div>\n\n <input\n class=\"nga-reply-input\"\n type=\"text\"\n [(ngModel)]=\"replyText\"\n placeholder=\"Reply...\"\n (keydown.enter)=\"sendReply()\"\n />\n\n <div class=\"nga-actions\">\n <button class=\"nga-btn nga-btn-submit\" (click)=\"sendReply()\" [disabled]=\"replyText.trim() === ''\">\n Send\n </button>\n <button class=\"nga-btn nga-btn-cancel\" (click)=\"closeThread()\">Close</button>\n </div>\n </div>\n }\n\n <!-- Annotation badges -->\n @for (badge of badges; track badge.annotation.id) {\n <div\n class=\"nga-badge nga-badge--{{ badge.annotation.status }}\"\n [style.top]=\"badge.top\"\n [style.left]=\"badge.left\"\n (click)=\"openThread(badge.annotation)\"\n [title]=\"badge.label\"\n >\n {{ badge.icon }}\n </div>\n }\n `,\n})\nexport class OverlayComponent implements OnInit {\n @ViewChild('textArea') textArea?: ElementRef<HTMLTextAreaElement>;\n\n mode: OverlayMode = 'hidden';\n hoveredContext: ComponentContext | null = null;\n highlightRect: HighlightRect | null = null;\n selectedContext: ComponentContext | null = null;\n annotationText = '';\n selectionText = '';\n threadAnnotation: Annotation | null = null;\n replyText = '';\n badges: AnnotationBadge[] = [];\n\n private readonly inspector = inject(InspectorService);\n private readonly bridge = inject(BridgeService);\n private readonly cdr = inject(ChangeDetectorRef);\n\n ngOnInit(): void {\n this.bridge.annotations$.subscribe((annotations) => {\n this.updateBadges(annotations);\n this.cdr.markForCheck();\n });\n }\n\n @HostListener('document:keydown.alt.shift.a', ['$event'])\n toggleInspect(event?: Event): void {\n event?.preventDefault();\n if (this.mode === 'hidden') this.mode = 'inspect';\n else if (this.mode === 'inspect') this.mode = 'hidden';\n else if (this.mode === 'annotate') this.mode = 'inspect';\n this.cdr.markForCheck();\n }\n\n @HostListener('document:keydown.escape')\n onEscape(): void {\n if (this.mode === 'annotate') this.mode = 'inspect';\n else if (this.mode === 'inspect') this.mode = 'hidden';\n else if (this.mode === 'thread') this.mode = 'hidden';\n this.cdr.markForCheck();\n }\n\n @HostListener('window:scroll')\n @HostListener('window:resize')\n onScrollOrResize(): void {\n if (this.badges.length > 0) {\n this.refreshBadgePositions();\n this.cdr.markForCheck();\n }\n }\n\n @HostListener('document:mousemove', ['$event'])\n onMouseMove(event: MouseEvent): void {\n if (this.mode !== 'inspect') return;\n const target = event.target as Element;\n if (target.closest('nga-overlay')) return;\n const context = this.inspector.getComponentContext(target);\n this.hoveredContext = context;\n if (context) {\n const rect = target.getBoundingClientRect();\n this.highlightRect = {\n top: `${rect.top.toString()}px`,\n left: `${rect.left.toString()}px`,\n width: `${rect.width.toString()}px`,\n height: `${rect.height.toString()}px`,\n };\n } else {\n this.highlightRect = null;\n }\n this.cdr.markForCheck();\n }\n\n @HostListener('document:click', ['$event'])\n onClick(event: MouseEvent): void {\n if (this.mode !== 'inspect') return;\n const target = event.target as Element;\n if (target.closest('nga-overlay')) return;\n const context = this.inspector.getComponentContext(target);\n if (!context) return;\n\n event.preventDefault();\n event.stopPropagation();\n\n this.selectedContext = context;\n this.annotationText = '';\n this.selectionText = window.getSelection()?.toString() ?? '';\n this.mode = 'annotate';\n this.cdr.markForCheck();\n\n setTimeout(() => { this.textArea?.nativeElement.focus(); }, 0);\n }\n\n submit(): void {\n if (!this.selectedContext || !this.annotationText.trim()) return;\n this.bridge.createAnnotation({\n ...this.selectedContext,\n annotationText: this.annotationText.trim(),\n selectionText: this.selectionText || undefined,\n });\n this.selectedContext = null;\n this.annotationText = '';\n this.mode = 'inspect';\n this.cdr.markForCheck();\n }\n\n cancel(): void {\n this.mode = 'inspect';\n this.cdr.markForCheck();\n }\n\n openThread(annotation: Annotation): void {\n this.threadAnnotation = annotation;\n this.mode = 'thread';\n this.cdr.markForCheck();\n }\n\n closeThread(): void {\n this.threadAnnotation = null;\n this.mode = 'hidden';\n this.cdr.markForCheck();\n }\n\n sendReply(): void {\n if (!this.threadAnnotation || !this.replyText.trim()) return;\n this.bridge.replyToAnnotation(this.threadAnnotation.id, this.replyText.trim());\n this.replyText = '';\n this.cdr.markForCheck();\n }\n\n inputEntries(): { key: string; value: unknown }[] {\n if (!this.selectedContext) return [];\n return Object.entries(this.selectedContext.inputs)\n .slice(0, 5)\n .map(([key, value]) => ({ key, value }));\n }\n\n private updateBadges(annotations: Annotation[]): void {\n this.badges = annotations\n .map((annotation) => {\n const el = this.findComponentElement(annotation.componentName, annotation.selector);\n if (!el) return null;\n const rect = el.getBoundingClientRect();\n return {\n annotation,\n top: `${rect.top.toString()}px`,\n left: `${(rect.left + rect.width - 12).toString()}px`,\n icon: this.badgeIcon(annotation.status),\n label: `${annotation.componentName}: ${annotation.annotationText.slice(0, 40)}`,\n };\n })\n .filter((b): b is AnnotationBadge => b !== null);\n }\n\n private refreshBadgePositions(): void {\n this.badges = this.badges.map((badge) => {\n const el = this.findComponentElement(badge.annotation.componentName, badge.annotation.selector);\n if (!el) return badge;\n const rect = el.getBoundingClientRect();\n return {\n ...badge,\n top: `${rect.top.toString()}px`,\n left: `${(rect.left + rect.width - 12).toString()}px`,\n };\n });\n }\n\n private findComponentElement(componentName: string, selector: string): Element | null {\n const bySelector = document.querySelector(selector);\n if (bySelector) return bySelector;\n\n const all = document.querySelectorAll('*');\n for (const el of Array.from(all)) {\n try {\n const comp = (window as unknown as { ng?: { getComponent: (el: Element) => { constructor: { name: string } } | null } })\n .ng;\n if (comp?.getComponent(el)?.constructor.name === componentName) return el;\n } catch {\n // ignore\n }\n }\n return null;\n }\n\n private badgeIcon(status: AnnotationStatus): string {\n const icons: Record<AnnotationStatus, string> = {\n pending: '●',\n acknowledged: '◐',\n resolved: '✓',\n dismissed: '✕',\n };\n return icons[status];\n }\n}\n","import {\n NgModule,\n isDevMode,\n provideAppInitializer,\n inject,\n ApplicationRef,\n EnvironmentInjector,\n createComponent,\n} from '@angular/core';\nimport { InspectorService } from './inspector.service';\nimport { BridgeService } from './bridge.service';\nimport { OverlayComponent } from './overlay/overlay.component';\n\n@NgModule({\n providers: isDevMode()\n ? [\n InspectorService,\n BridgeService,\n provideAppInitializer(() => {\n const bridge = inject(BridgeService);\n const appRef = inject(ApplicationRef);\n const envInjector = inject(EnvironmentInjector);\n bridge.init();\n const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });\n appRef.attachView(overlayRef.hostView);\n document.body.appendChild(overlayRef.location.nativeElement);\n }),\n ]\n : [],\n})\n// eslint-disable-next-line @typescript-eslint/no-extraneous-class -- required by NgModule pattern\nexport class NgAnnotateModule {}\n","import {\n ApplicationRef,\n EnvironmentInjector,\n createComponent,\n inject,\n isDevMode,\n makeEnvironmentProviders,\n provideAppInitializer,\n} from '@angular/core';\nimport { InspectorService } from './inspector.service';\nimport { BridgeService } from './bridge.service';\nimport { OverlayComponent } from './overlay/overlay.component';\n\nexport function provideNgAnnotate() {\n return makeEnvironmentProviders([\n InspectorService,\n BridgeService,\n provideAppInitializer(() => {\n if (!isDevMode()) return;\n const bridge = inject(BridgeService);\n const appRef = inject(ApplicationRef);\n const envInjector = inject(EnvironmentInjector);\n bridge.init();\n const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });\n appRef.attachView(overlayRef.hostView);\n document.body.appendChild(overlayRef.location.nativeElement);\n }),\n ]);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { OnDestroy, OnInit, ElementRef } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
declare class NgAnnotateModule {
|
|
6
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgAnnotateModule, never>;
|
|
7
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<NgAnnotateModule, never, never, never>;
|
|
8
|
+
static ɵinj: i0.ɵɵInjectorDeclaration<NgAnnotateModule>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare function provideNgAnnotate(): i0.EnvironmentProviders;
|
|
12
|
+
|
|
13
|
+
type AnnotationStatus = 'pending' | 'acknowledged' | 'resolved' | 'dismissed';
|
|
14
|
+
interface AnnotationReply {
|
|
15
|
+
id: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
author: 'agent' | 'user';
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
interface Annotation {
|
|
21
|
+
id: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
status: AnnotationStatus;
|
|
25
|
+
replies: AnnotationReply[];
|
|
26
|
+
componentName: string;
|
|
27
|
+
componentFilePath: string;
|
|
28
|
+
templateFilePath?: string;
|
|
29
|
+
selector: string;
|
|
30
|
+
inputs: Record<string, unknown>;
|
|
31
|
+
domSnapshot: string;
|
|
32
|
+
componentTreePath: string[];
|
|
33
|
+
annotationText: string;
|
|
34
|
+
selectionText?: string;
|
|
35
|
+
}
|
|
36
|
+
interface Session {
|
|
37
|
+
id: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
lastSeenAt: string;
|
|
40
|
+
active: boolean;
|
|
41
|
+
url: string;
|
|
42
|
+
}
|
|
43
|
+
interface ComponentContext {
|
|
44
|
+
componentName: string;
|
|
45
|
+
componentFilePath: string;
|
|
46
|
+
templateFilePath?: string;
|
|
47
|
+
selector: string;
|
|
48
|
+
inputs: Record<string, unknown>;
|
|
49
|
+
domSnapshot: string;
|
|
50
|
+
componentTreePath: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare class InspectorService {
|
|
54
|
+
getComponentContext(element: Element): ComponentContext | null;
|
|
55
|
+
private findNearestComponent;
|
|
56
|
+
private getSelector;
|
|
57
|
+
private getInputs;
|
|
58
|
+
private buildTreePath;
|
|
59
|
+
private snapshot;
|
|
60
|
+
private resolveFilePaths;
|
|
61
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<InspectorService, never>;
|
|
62
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<InspectorService>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare class BridgeService implements OnDestroy {
|
|
66
|
+
readonly session$: BehaviorSubject<Session>;
|
|
67
|
+
readonly annotations$: BehaviorSubject<Annotation[]>;
|
|
68
|
+
readonly connected$: BehaviorSubject<boolean>;
|
|
69
|
+
private readonly zone;
|
|
70
|
+
private ws;
|
|
71
|
+
private reconnectTimer;
|
|
72
|
+
init(): void;
|
|
73
|
+
private connect;
|
|
74
|
+
createAnnotation(payload: Record<string, unknown>): void;
|
|
75
|
+
replyToAnnotation(id: string, message: string): void;
|
|
76
|
+
deleteAnnotation(id: string): void;
|
|
77
|
+
private send;
|
|
78
|
+
ngOnDestroy(): void;
|
|
79
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<BridgeService, never>;
|
|
80
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<BridgeService>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type OverlayMode = 'hidden' | 'inspect' | 'annotate' | 'thread';
|
|
84
|
+
interface HighlightRect {
|
|
85
|
+
top: string;
|
|
86
|
+
left: string;
|
|
87
|
+
width: string;
|
|
88
|
+
height: string;
|
|
89
|
+
}
|
|
90
|
+
interface AnnotationBadge {
|
|
91
|
+
annotation: Annotation;
|
|
92
|
+
top: string;
|
|
93
|
+
left: string;
|
|
94
|
+
icon: string;
|
|
95
|
+
label: string;
|
|
96
|
+
}
|
|
97
|
+
declare class OverlayComponent implements OnInit {
|
|
98
|
+
textArea?: ElementRef<HTMLTextAreaElement>;
|
|
99
|
+
mode: OverlayMode;
|
|
100
|
+
hoveredContext: ComponentContext | null;
|
|
101
|
+
highlightRect: HighlightRect | null;
|
|
102
|
+
selectedContext: ComponentContext | null;
|
|
103
|
+
annotationText: string;
|
|
104
|
+
selectionText: string;
|
|
105
|
+
threadAnnotation: Annotation | null;
|
|
106
|
+
replyText: string;
|
|
107
|
+
badges: AnnotationBadge[];
|
|
108
|
+
private readonly inspector;
|
|
109
|
+
private readonly bridge;
|
|
110
|
+
private readonly cdr;
|
|
111
|
+
ngOnInit(): void;
|
|
112
|
+
toggleInspect(event?: Event): void;
|
|
113
|
+
onEscape(): void;
|
|
114
|
+
onScrollOrResize(): void;
|
|
115
|
+
onMouseMove(event: MouseEvent): void;
|
|
116
|
+
onClick(event: MouseEvent): void;
|
|
117
|
+
submit(): void;
|
|
118
|
+
cancel(): void;
|
|
119
|
+
openThread(annotation: Annotation): void;
|
|
120
|
+
closeThread(): void;
|
|
121
|
+
sendReply(): void;
|
|
122
|
+
inputEntries(): {
|
|
123
|
+
key: string;
|
|
124
|
+
value: unknown;
|
|
125
|
+
}[];
|
|
126
|
+
private updateBadges;
|
|
127
|
+
private refreshBadgePositions;
|
|
128
|
+
private findComponentElement;
|
|
129
|
+
private badgeIcon;
|
|
130
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayComponent, never>;
|
|
131
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<OverlayComponent, "nga-overlay", never, {}, {}, never, never, true, never>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { BridgeService, InspectorService, NgAnnotateModule, OverlayComponent, provideNgAnnotate };
|
|
135
|
+
export type { Annotation, AnnotationReply, AnnotationStatus, ComponentContext, Session };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ng-annotate/angular",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"schematics": "./schematics/collection.json",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/yngvebn/ngagentify"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"schematics"
|
|
12
|
+
],
|
|
13
|
+
"main": "dist/fesm2022/ng-annotate-angular.mjs",
|
|
14
|
+
"module": "dist/fesm2022/ng-annotate-angular.mjs",
|
|
15
|
+
"types": "dist/types/ng-annotate-angular.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/types/ng-annotate-angular.d.ts",
|
|
19
|
+
"default": "./dist/fesm2022/ng-annotate-angular.mjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "ng-packagr -p ng-package.json && npm run build:schematics",
|
|
24
|
+
"build:watch": "ng-packagr -p ng-package.json --watch",
|
|
25
|
+
"build:schematics": "tsc -p schematics/tsconfig.json",
|
|
26
|
+
"lint": "eslint src/",
|
|
27
|
+
"lint:fix": "eslint src/ --fix"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@angular/core": ">=21.0.0",
|
|
31
|
+
"rxjs": ">=7.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@angular-devkit/schematics": ">=17.0.0",
|
|
35
|
+
"@angular/core": "^21.0.0",
|
|
36
|
+
"ng-packagr": "^21.2.0",
|
|
37
|
+
"rxjs": "^7.0.0",
|
|
38
|
+
"typescript": "~5.9.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = default_1;
|
|
4
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
5
|
+
const tasks_1 = require("@angular-devkit/schematics/tasks");
|
|
6
|
+
const MIN_ANGULAR_MAJOR = 21;
|
|
7
|
+
function checkAngularVersion() {
|
|
8
|
+
return (tree) => {
|
|
9
|
+
const pkgPath = 'package.json';
|
|
10
|
+
if (!tree.exists(pkgPath))
|
|
11
|
+
return;
|
|
12
|
+
const pkg = JSON.parse(tree.read(pkgPath).toString('utf-8'));
|
|
13
|
+
const version = (pkg['dependencies']?.['@angular/core'] ?? pkg['devDependencies']?.['@angular/core']) || '';
|
|
14
|
+
const match = version.match(/(\d+)/);
|
|
15
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
16
|
+
if (major > 0 && major < MIN_ANGULAR_MAJOR) {
|
|
17
|
+
throw new schematics_1.SchematicsException(`@ng-annotate/angular requires Angular ${MIN_ANGULAR_MAJOR} or higher. ` +
|
|
18
|
+
`Found: ${version}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function addVitePlugin() {
|
|
23
|
+
return (tree, context) => {
|
|
24
|
+
const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
|
|
25
|
+
const viteConfigPath = candidates.find((p) => tree.exists(p));
|
|
26
|
+
if (!viteConfigPath) {
|
|
27
|
+
context.logger.warn('⚠️ Could not find vite.config.ts — add the plugin manually:\n' +
|
|
28
|
+
" import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n" +
|
|
29
|
+
' plugins: [...ngAnnotateMcp()]');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let content = tree.read(viteConfigPath).toString('utf-8');
|
|
33
|
+
if (content.includes('@ng-annotate/vite-plugin')) {
|
|
34
|
+
context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Insert import after the last existing import line
|
|
38
|
+
content = content.replace(/(^import .+$(\r?\n)?)+/m, (match) => match + "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n");
|
|
39
|
+
// Insert spread into plugins array (handles `plugins: [` or `plugins:[`)
|
|
40
|
+
content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
|
|
41
|
+
tree.overwrite(viteConfigPath, content);
|
|
42
|
+
context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function addProviders() {
|
|
46
|
+
return (tree, context) => {
|
|
47
|
+
const candidates = [
|
|
48
|
+
'src/app/app.config.ts',
|
|
49
|
+
'src/app.config.ts',
|
|
50
|
+
'app/app.config.ts',
|
|
51
|
+
];
|
|
52
|
+
const appConfigPath = candidates.find((p) => tree.exists(p));
|
|
53
|
+
if (!appConfigPath) {
|
|
54
|
+
context.logger.warn('⚠️ Could not find app.config.ts — add provideNgAnnotate() manually:\n' +
|
|
55
|
+
" import { provideNgAnnotate } from '@ng-annotate/angular';\n" +
|
|
56
|
+
' providers: [provideNgAnnotate()]');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let content = tree.read(appConfigPath).toString('utf-8');
|
|
60
|
+
if (content.includes('provideNgAnnotate')) {
|
|
61
|
+
context.logger.info('provideNgAnnotate already present, skipping.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Insert import after the last existing import line
|
|
65
|
+
content = content.replace(/(^import .+$(\r?\n)?)+/m, (match) => match + "import { provideNgAnnotate } from '@ng-annotate/angular';\n");
|
|
66
|
+
// Insert into providers array
|
|
67
|
+
content = content.replace(/providers\s*:\s*\[/, 'providers: [\n provideNgAnnotate(),');
|
|
68
|
+
tree.overwrite(appConfigPath, content);
|
|
69
|
+
context.logger.info(`✅ Added provideNgAnnotate() to ${appConfigPath}`);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function addMcpConfig() {
|
|
73
|
+
return (tree, context) => {
|
|
74
|
+
// .mcp.json — Claude Code (needs cmd /c on Windows to spawn npx.cmd)
|
|
75
|
+
if (!tree.exists('.mcp.json')) {
|
|
76
|
+
const isWindows = process.platform === 'win32';
|
|
77
|
+
const mcpConfig = {
|
|
78
|
+
mcpServers: {
|
|
79
|
+
'ng-annotate': isWindows
|
|
80
|
+
? { command: 'cmd', args: ['/c', 'npx', '@ng-annotate/mcp-server'] }
|
|
81
|
+
: { command: 'npx', args: ['@ng-annotate/mcp-server'] },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
tree.create('.mcp.json', JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
85
|
+
context.logger.info('✅ Created .mcp.json');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
context.logger.info('.mcp.json already exists, skipping.');
|
|
89
|
+
}
|
|
90
|
+
// .vscode/mcp.json — VS Code Copilot (spawns npx.cmd natively on Windows)
|
|
91
|
+
const vscodeMcpPath = '.vscode/mcp.json';
|
|
92
|
+
if (!tree.exists(vscodeMcpPath)) {
|
|
93
|
+
const vscodeMcpConfig = {
|
|
94
|
+
servers: {
|
|
95
|
+
'ng-annotate': {
|
|
96
|
+
type: 'stdio',
|
|
97
|
+
command: 'npx',
|
|
98
|
+
args: ['@ng-annotate/mcp-server'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
tree.create(vscodeMcpPath, JSON.stringify(vscodeMcpConfig, null, 2) + '\n');
|
|
103
|
+
context.logger.info('✅ Created .vscode/mcp.json');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
context.logger.info('.vscode/mcp.json already exists, skipping.');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function addDevDependency() {
|
|
111
|
+
return (tree, context) => {
|
|
112
|
+
const pkgPath = 'package.json';
|
|
113
|
+
if (!tree.exists(pkgPath))
|
|
114
|
+
return;
|
|
115
|
+
const pkg = JSON.parse(tree.read(pkgPath).toString('utf-8'));
|
|
116
|
+
pkg['devDependencies'] ?? (pkg['devDependencies'] = {});
|
|
117
|
+
let changed = false;
|
|
118
|
+
if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
|
|
119
|
+
pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
|
|
120
|
+
changed = true;
|
|
121
|
+
context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
|
|
122
|
+
}
|
|
123
|
+
if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
|
|
124
|
+
pkg['devDependencies']['@ng-annotate/mcp-server'] = 'latest';
|
|
125
|
+
changed = true;
|
|
126
|
+
context.logger.info('✅ Added @ng-annotate/mcp-server to devDependencies');
|
|
127
|
+
}
|
|
128
|
+
if (changed) {
|
|
129
|
+
tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
130
|
+
context.addTask(new tasks_1.NodePackageInstallTask());
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function default_1() {
|
|
135
|
+
return (tree, context) => {
|
|
136
|
+
context.logger.info('Setting up @ng-annotate/vite-plugin...');
|
|
137
|
+
return (0, schematics_1.chain)([checkAngularVersion(), addDevDependency(), addVitePlugin(), addProviders(), addMcpConfig()])(tree, context);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Rule, SchematicContext, Tree, chain, SchematicsException } from '@angular-devkit/schematics';
|
|
2
|
+
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
|
|
3
|
+
|
|
4
|
+
const MIN_ANGULAR_MAJOR = 21;
|
|
5
|
+
|
|
6
|
+
function checkAngularVersion(): Rule {
|
|
7
|
+
return (tree: Tree) => {
|
|
8
|
+
const pkgPath = 'package.json';
|
|
9
|
+
if (!tree.exists(pkgPath)) return;
|
|
10
|
+
|
|
11
|
+
const pkg = JSON.parse(tree.read(pkgPath)!.toString('utf-8')) as Record<
|
|
12
|
+
string,
|
|
13
|
+
Record<string, string>
|
|
14
|
+
>;
|
|
15
|
+
const version: string =
|
|
16
|
+
(pkg['dependencies']?.['@angular/core'] ?? pkg['devDependencies']?.['@angular/core']) || '';
|
|
17
|
+
const match = version.match(/(\d+)/);
|
|
18
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
19
|
+
|
|
20
|
+
if (major > 0 && major < MIN_ANGULAR_MAJOR) {
|
|
21
|
+
throw new SchematicsException(
|
|
22
|
+
`@ng-annotate/angular requires Angular ${MIN_ANGULAR_MAJOR} or higher. ` +
|
|
23
|
+
`Found: ${version}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addVitePlugin(): Rule {
|
|
30
|
+
return (tree: Tree, context: SchematicContext) => {
|
|
31
|
+
const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
|
|
32
|
+
const viteConfigPath = candidates.find((p) => tree.exists(p));
|
|
33
|
+
|
|
34
|
+
if (!viteConfigPath) {
|
|
35
|
+
context.logger.warn(
|
|
36
|
+
'⚠️ Could not find vite.config.ts — add the plugin manually:\n' +
|
|
37
|
+
" import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n" +
|
|
38
|
+
' plugins: [...ngAnnotateMcp()]',
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let content = tree.read(viteConfigPath)!.toString('utf-8');
|
|
44
|
+
|
|
45
|
+
if (content.includes('@ng-annotate/vite-plugin')) {
|
|
46
|
+
context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Insert import after the last existing import line
|
|
51
|
+
content = content.replace(
|
|
52
|
+
/(^import .+$(\r?\n)?)+/m,
|
|
53
|
+
(match) => match + "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Insert spread into plugins array (handles `plugins: [` or `plugins:[`)
|
|
57
|
+
content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
|
|
58
|
+
|
|
59
|
+
tree.overwrite(viteConfigPath, content);
|
|
60
|
+
context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function addProviders(): Rule {
|
|
65
|
+
return (tree: Tree, context: SchematicContext) => {
|
|
66
|
+
const candidates = [
|
|
67
|
+
'src/app/app.config.ts',
|
|
68
|
+
'src/app.config.ts',
|
|
69
|
+
'app/app.config.ts',
|
|
70
|
+
];
|
|
71
|
+
const appConfigPath = candidates.find((p) => tree.exists(p));
|
|
72
|
+
|
|
73
|
+
if (!appConfigPath) {
|
|
74
|
+
context.logger.warn(
|
|
75
|
+
'⚠️ Could not find app.config.ts — add provideNgAnnotate() manually:\n' +
|
|
76
|
+
" import { provideNgAnnotate } from '@ng-annotate/angular';\n" +
|
|
77
|
+
' providers: [provideNgAnnotate()]',
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let content = tree.read(appConfigPath)!.toString('utf-8');
|
|
83
|
+
|
|
84
|
+
if (content.includes('provideNgAnnotate')) {
|
|
85
|
+
context.logger.info('provideNgAnnotate already present, skipping.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Insert import after the last existing import line
|
|
90
|
+
content = content.replace(
|
|
91
|
+
/(^import .+$(\r?\n)?)+/m,
|
|
92
|
+
(match) => match + "import { provideNgAnnotate } from '@ng-annotate/angular';\n",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Insert into providers array
|
|
96
|
+
content = content.replace(/providers\s*:\s*\[/, 'providers: [\n provideNgAnnotate(),');
|
|
97
|
+
|
|
98
|
+
tree.overwrite(appConfigPath, content);
|
|
99
|
+
context.logger.info(`✅ Added provideNgAnnotate() to ${appConfigPath}`);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addMcpConfig(): Rule {
|
|
104
|
+
return (tree: Tree, context: SchematicContext) => {
|
|
105
|
+
// .mcp.json — Claude Code (needs cmd /c on Windows to spawn npx.cmd)
|
|
106
|
+
if (!tree.exists('.mcp.json')) {
|
|
107
|
+
const isWindows = process.platform === 'win32';
|
|
108
|
+
const mcpConfig = {
|
|
109
|
+
mcpServers: {
|
|
110
|
+
'ng-annotate': isWindows
|
|
111
|
+
? { command: 'cmd', args: ['/c', 'npx', '@ng-annotate/mcp-server'] }
|
|
112
|
+
: { command: 'npx', args: ['@ng-annotate/mcp-server'] },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
tree.create('.mcp.json', JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
116
|
+
context.logger.info('✅ Created .mcp.json');
|
|
117
|
+
} else {
|
|
118
|
+
context.logger.info('.mcp.json already exists, skipping.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// .vscode/mcp.json — VS Code Copilot (spawns npx.cmd natively on Windows)
|
|
122
|
+
const vscodeMcpPath = '.vscode/mcp.json';
|
|
123
|
+
if (!tree.exists(vscodeMcpPath)) {
|
|
124
|
+
const vscodeMcpConfig = {
|
|
125
|
+
servers: {
|
|
126
|
+
'ng-annotate': {
|
|
127
|
+
type: 'stdio',
|
|
128
|
+
command: 'npx',
|
|
129
|
+
args: ['@ng-annotate/mcp-server'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
tree.create(vscodeMcpPath, JSON.stringify(vscodeMcpConfig, null, 2) + '\n');
|
|
134
|
+
context.logger.info('✅ Created .vscode/mcp.json');
|
|
135
|
+
} else {
|
|
136
|
+
context.logger.info('.vscode/mcp.json already exists, skipping.');
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function addDevDependency(): Rule {
|
|
142
|
+
return (tree: Tree, context: SchematicContext) => {
|
|
143
|
+
const pkgPath = 'package.json';
|
|
144
|
+
if (!tree.exists(pkgPath)) return;
|
|
145
|
+
|
|
146
|
+
const pkg = JSON.parse(tree.read(pkgPath)!.toString('utf-8')) as Record<
|
|
147
|
+
string,
|
|
148
|
+
Record<string, string>
|
|
149
|
+
>;
|
|
150
|
+
pkg['devDependencies'] ??= {};
|
|
151
|
+
|
|
152
|
+
let changed = false;
|
|
153
|
+
|
|
154
|
+
if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
|
|
155
|
+
pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
|
|
156
|
+
changed = true;
|
|
157
|
+
context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
|
|
161
|
+
pkg['devDependencies']['@ng-annotate/mcp-server'] = 'latest';
|
|
162
|
+
changed = true;
|
|
163
|
+
context.logger.info('✅ Added @ng-annotate/mcp-server to devDependencies');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (changed) {
|
|
167
|
+
tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
168
|
+
context.addTask(new NodePackageInstallTask());
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default function (): Rule {
|
|
174
|
+
return (tree: Tree, context: SchematicContext) => {
|
|
175
|
+
context.logger.info('Setting up @ng-annotate/vite-plugin...');
|
|
176
|
+
return chain([checkAngularVersion(), addDevDependency(), addVitePlugin(), addProviders(), addMcpConfig()])(
|
|
177
|
+
tree,
|
|
178
|
+
context,
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "CommonJS",
|
|
4
|
+
"moduleResolution": "node",
|
|
5
|
+
"target": "ES2020",
|
|
6
|
+
"lib": ["ES2020"],
|
|
7
|
+
"declaration": false,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"rootDir": ".",
|
|
12
|
+
"outDir": "."
|
|
13
|
+
},
|
|
14
|
+
"include": ["./**/*.ts"],
|
|
15
|
+
"exclude": ["**/*.spec.ts", "node_modules"]
|
|
16
|
+
}
|