ngx-locatorjs 0.1.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/LICENSE +21 -0
- package/README.ko.md +214 -0
- package/README.md +207 -0
- package/dist/browser/auto.d.ts +2 -0
- package/dist/browser/auto.d.ts.map +1 -0
- package/dist/browser/auto.js +2 -0
- package/dist/browser/index.d.ts +33 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +528 -0
- package/dist/node/cmp-scan.js +158 -0
- package/dist/node/config-setup.js +342 -0
- package/dist/node/file-opener.js +214 -0
- package/package.json +70 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
const DEFAULT_ENDPOINTS = {
|
|
2
|
+
openInEditor: '/__open-in-editor',
|
|
3
|
+
openInEditorSearch: '/__open-in-editor-search',
|
|
4
|
+
componentMap: '/__cmp-map',
|
|
5
|
+
};
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
endpoints: DEFAULT_ENDPOINTS,
|
|
8
|
+
prefetchMap: true,
|
|
9
|
+
enableHover: true,
|
|
10
|
+
enableClick: true,
|
|
11
|
+
showTooltip: true,
|
|
12
|
+
showClickFeedback: true,
|
|
13
|
+
debug: false,
|
|
14
|
+
};
|
|
15
|
+
let OPTIONS = DEFAULT_OPTIONS;
|
|
16
|
+
let INSTALLED = false;
|
|
17
|
+
let CMP_MAP = null;
|
|
18
|
+
let mapLoadPromise = null;
|
|
19
|
+
function normalizeMap(map) {
|
|
20
|
+
if (!map.filePathsByClassName || Object.keys(map.filePathsByClassName).length === 0) {
|
|
21
|
+
const rebuilt = {};
|
|
22
|
+
Object.values(map.detailByFilePath).forEach((info) => {
|
|
23
|
+
if (!rebuilt[info.className])
|
|
24
|
+
rebuilt[info.className] = [];
|
|
25
|
+
if (!rebuilt[info.className].includes(info.filePath)) {
|
|
26
|
+
rebuilt[info.className].push(info.filePath);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
map.filePathsByClassName = rebuilt;
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
async function ensureMap(forceRefresh = false) {
|
|
34
|
+
if (CMP_MAP && !forceRefresh)
|
|
35
|
+
return CMP_MAP;
|
|
36
|
+
const timestamp = Date.now();
|
|
37
|
+
const res = await fetch(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`, {
|
|
38
|
+
cache: 'no-store',
|
|
39
|
+
headers: {
|
|
40
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
41
|
+
Pragma: 'no-cache',
|
|
42
|
+
Expires: '0',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
try {
|
|
47
|
+
CMP_MAP = normalizeMap(JSON.parse(text));
|
|
48
|
+
return CMP_MAP;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (OPTIONS.debug) {
|
|
52
|
+
console.error('[angular-locator] JSON parse error:', e);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Failed to parse response as JSON. Got: ${text.substring(0, 100)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function ensureMapIfNeeded() {
|
|
58
|
+
if (CMP_MAP || mapLoadPromise)
|
|
59
|
+
return;
|
|
60
|
+
mapLoadPromise = ensureMap()
|
|
61
|
+
.catch(() => undefined)
|
|
62
|
+
.finally(() => {
|
|
63
|
+
mapLoadPromise = null;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Calculates relevance between the current URL path and a file path.
|
|
68
|
+
* Higher score means stronger relevance.
|
|
69
|
+
*/
|
|
70
|
+
function calculatePathRelevance(filePath, currentUrl) {
|
|
71
|
+
const urlSegments = currentUrl.split('/').filter(Boolean);
|
|
72
|
+
const fileSegments = filePath.split('/').filter(Boolean);
|
|
73
|
+
let score = 0;
|
|
74
|
+
for (const urlSeg of urlSegments) {
|
|
75
|
+
if (fileSegments.includes(urlSeg)) {
|
|
76
|
+
score += 10;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < urlSegments.length - 1; i++) {
|
|
80
|
+
const pattern = `${urlSegments[i]}/${urlSegments[i + 1]}`;
|
|
81
|
+
if (filePath.includes(pattern)) {
|
|
82
|
+
score += 20;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const lastUrlSeg = urlSegments[urlSegments.length - 1];
|
|
86
|
+
if (lastUrlSeg && filePath.toLowerCase().includes(lastUrlSeg.toLowerCase())) {
|
|
87
|
+
score += 30;
|
|
88
|
+
}
|
|
89
|
+
return score;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Picks the best match among multiple files that share the same selector,
|
|
93
|
+
* based on the current URL.
|
|
94
|
+
*/
|
|
95
|
+
function selectBestMatchingFile(candidates) {
|
|
96
|
+
if (candidates.length === 1)
|
|
97
|
+
return candidates[0];
|
|
98
|
+
const currentUrl = window.location.pathname;
|
|
99
|
+
let bestMatch = candidates[0];
|
|
100
|
+
let bestScore = -1;
|
|
101
|
+
for (const filePath of candidates) {
|
|
102
|
+
const score = calculatePathRelevance(filePath, currentUrl);
|
|
103
|
+
if (score > bestScore) {
|
|
104
|
+
bestScore = score;
|
|
105
|
+
bestMatch = filePath;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return bestMatch;
|
|
109
|
+
}
|
|
110
|
+
function getComponentInfoByClassName(className) {
|
|
111
|
+
if (!CMP_MAP)
|
|
112
|
+
return null;
|
|
113
|
+
const classNameCandidates = getClassNameCandidates(className);
|
|
114
|
+
for (const candidate of classNameCandidates) {
|
|
115
|
+
const candidates = CMP_MAP.filePathsByClassName?.[candidate];
|
|
116
|
+
if (!candidates?.length)
|
|
117
|
+
continue;
|
|
118
|
+
const filePath = selectBestMatchingFile(candidates);
|
|
119
|
+
const info = CMP_MAP.detailByFilePath[filePath] ?? null;
|
|
120
|
+
if (info)
|
|
121
|
+
return info;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function getClassNameCandidates(className) {
|
|
126
|
+
const candidates = [];
|
|
127
|
+
const push = (value) => {
|
|
128
|
+
if (!value)
|
|
129
|
+
return;
|
|
130
|
+
if (!candidates.includes(value))
|
|
131
|
+
candidates.push(value);
|
|
132
|
+
};
|
|
133
|
+
push(className);
|
|
134
|
+
const trimmed = className.replace(/^_+/, '');
|
|
135
|
+
push(trimmed);
|
|
136
|
+
return candidates;
|
|
137
|
+
}
|
|
138
|
+
function getAngularRuntimeComponent(el) {
|
|
139
|
+
const ng = window.ng;
|
|
140
|
+
if (!ng)
|
|
141
|
+
return null;
|
|
142
|
+
const getComponentFn = ng.getOwningComponent || ng.getComponent;
|
|
143
|
+
if (typeof getComponentFn !== 'function')
|
|
144
|
+
return null;
|
|
145
|
+
let cur = el;
|
|
146
|
+
while (cur) {
|
|
147
|
+
try {
|
|
148
|
+
const cmp = getComponentFn(cur);
|
|
149
|
+
if (cmp)
|
|
150
|
+
return cmp;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
cur = cur.parentElement;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function getNearestComponent(el) {
|
|
160
|
+
if (!CMP_MAP)
|
|
161
|
+
return null;
|
|
162
|
+
const runtimeComponent = getAngularRuntimeComponent(el);
|
|
163
|
+
const runtimeClassName = runtimeComponent?.constructor?.name;
|
|
164
|
+
if (!runtimeClassName)
|
|
165
|
+
return null;
|
|
166
|
+
const info = getComponentInfoByClassName(runtimeClassName);
|
|
167
|
+
if (!info)
|
|
168
|
+
return null;
|
|
169
|
+
return {
|
|
170
|
+
constructor: { name: info.className },
|
|
171
|
+
__isMockComponent: true,
|
|
172
|
+
__cmpInfo: info,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function openFile(absPath, line = 1, col = 1) {
|
|
176
|
+
const url = `${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`;
|
|
177
|
+
try {
|
|
178
|
+
await fetch(url);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (OPTIONS.debug) {
|
|
182
|
+
console.warn(e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function openFileWithSearch(absPath, searchTerms) {
|
|
187
|
+
const url = `${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`;
|
|
188
|
+
try {
|
|
189
|
+
await fetch(url);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (OPTIONS.debug) {
|
|
193
|
+
console.warn('[angular-locator] Search failed:', e);
|
|
194
|
+
}
|
|
195
|
+
await openFile(absPath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function addStyles() {
|
|
199
|
+
if (document.getElementById('angular-locator-styles'))
|
|
200
|
+
return;
|
|
201
|
+
const style = document.createElement('style');
|
|
202
|
+
style.id = 'angular-locator-styles';
|
|
203
|
+
style.textContent = `
|
|
204
|
+
.dev-highlight-overlay {
|
|
205
|
+
position: fixed;
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
z-index: 99998;
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
border: 1px solid rgba(96, 165, 250, 0.6);
|
|
210
|
+
background: rgba(96, 165, 250, 0.08);
|
|
211
|
+
box-shadow:
|
|
212
|
+
0 0 0 1px rgba(96, 165, 250, 0.05) inset,
|
|
213
|
+
0 4px 12px rgba(15, 23, 42, 0.18);
|
|
214
|
+
transition: all 0.12s ease;
|
|
215
|
+
}
|
|
216
|
+
.dev-tooltip {
|
|
217
|
+
position: absolute;
|
|
218
|
+
background: rgba(15, 23, 42, 0.9);
|
|
219
|
+
color: #e5e7eb;
|
|
220
|
+
padding: 6px 8px;
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
223
|
+
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.28);
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
226
|
+
z-index: 99999;
|
|
227
|
+
pointer-events: none;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
letter-spacing: 0.2px;
|
|
230
|
+
}
|
|
231
|
+
.dev-click-feedback {
|
|
232
|
+
position: fixed;
|
|
233
|
+
background: rgba(15, 23, 42, 0.9);
|
|
234
|
+
color: #e2e8f0;
|
|
235
|
+
padding: 8px 12px;
|
|
236
|
+
border-radius: 10px;
|
|
237
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
238
|
+
box-shadow:
|
|
239
|
+
0 8px 24px rgba(15, 23, 42, 0.4),
|
|
240
|
+
0 0 14px rgba(34, 211, 238, 0.25);
|
|
241
|
+
font-size: 13px;
|
|
242
|
+
font-weight: 500;
|
|
243
|
+
z-index: 100000;
|
|
244
|
+
animation: dev-fade-out 1.6s ease-out forwards;
|
|
245
|
+
}
|
|
246
|
+
@keyframes dev-fade-out {
|
|
247
|
+
0% { opacity: 0; transform: translateY(6px) scale(0.98); }
|
|
248
|
+
15% { opacity: 1; transform: translateY(0) scale(1); }
|
|
249
|
+
100% { opacity: 0; transform: translateY(-8px) scale(0.99); }
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
document.head.appendChild(style);
|
|
253
|
+
}
|
|
254
|
+
function showClickFeedback(x, y, message) {
|
|
255
|
+
if (!OPTIONS.showClickFeedback)
|
|
256
|
+
return;
|
|
257
|
+
const feedback = document.createElement('div');
|
|
258
|
+
feedback.className = 'dev-click-feedback';
|
|
259
|
+
feedback.textContent = message;
|
|
260
|
+
feedback.style.left = x + 'px';
|
|
261
|
+
feedback.style.top = y + 'px';
|
|
262
|
+
document.body.appendChild(feedback);
|
|
263
|
+
setTimeout(() => feedback.remove(), 2000);
|
|
264
|
+
}
|
|
265
|
+
function findTemplatePosition(clickedElement, componentElement) {
|
|
266
|
+
const searchTerms = [];
|
|
267
|
+
if (clickedElement.id) {
|
|
268
|
+
searchTerms.push(`id="${clickedElement.id}"`);
|
|
269
|
+
searchTerms.push(`#${clickedElement.id}`);
|
|
270
|
+
}
|
|
271
|
+
const classes = Array.from(clickedElement.classList).filter((cls) => !cls.startsWith('ng-') && !cls.startsWith('_ng') && cls.length > 2);
|
|
272
|
+
if (classes.length > 0) {
|
|
273
|
+
searchTerms.push(`class="${classes.join(' ')}"`);
|
|
274
|
+
classes.forEach((cls) => searchTerms.push(`${cls}`));
|
|
275
|
+
}
|
|
276
|
+
const tagName = clickedElement.tagName.toLowerCase();
|
|
277
|
+
searchTerms.push(`<${tagName}`);
|
|
278
|
+
Array.from(clickedElement.attributes).forEach((attr) => {
|
|
279
|
+
if (attr.name.startsWith('(') || attr.name.startsWith('[') || attr.name.startsWith('*')) {
|
|
280
|
+
searchTerms.push(attr.name);
|
|
281
|
+
}
|
|
282
|
+
if (attr.name.startsWith('data-') || attr.name.includes('ng-')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (attr.value && attr.value.length > 0 && attr.value.length < 50) {
|
|
286
|
+
searchTerms.push(`${attr.name}="${attr.value}"`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const text = clickedElement.textContent?.trim();
|
|
290
|
+
if (text && text.length > 3 && text.length < 100 && !text.includes('\n')) {
|
|
291
|
+
searchTerms.push(text);
|
|
292
|
+
}
|
|
293
|
+
const parent = clickedElement.parentElement;
|
|
294
|
+
if (parent && parent !== componentElement) {
|
|
295
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
296
|
+
searchTerms.push(`${parentTag} ${tagName}`);
|
|
297
|
+
searchTerms.push(`<${parentTag}.*<${tagName}`);
|
|
298
|
+
}
|
|
299
|
+
return { searchTerms };
|
|
300
|
+
}
|
|
301
|
+
async function handleAltOpen(ev, el) {
|
|
302
|
+
if (!CMP_MAP) {
|
|
303
|
+
try {
|
|
304
|
+
await ensureMap();
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
if (OPTIONS.debug) {
|
|
308
|
+
console.warn('[angular-locator] Failed to load component map on click:', e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const cmp = getNearestComponent(el);
|
|
313
|
+
if (!cmp)
|
|
314
|
+
return;
|
|
315
|
+
const className = cmp.constructor?.name;
|
|
316
|
+
const info = cmp.__cmpInfo;
|
|
317
|
+
if (!info)
|
|
318
|
+
return;
|
|
319
|
+
const targetFile = ev.shiftKey ? 'component' : info.templateUrl ? 'template' : 'component';
|
|
320
|
+
showClickFeedback(ev.clientX, ev.clientY, `Opening ${className} ${targetFile}...`);
|
|
321
|
+
try {
|
|
322
|
+
if (ev.shiftKey) {
|
|
323
|
+
await openFile(info.filePath);
|
|
324
|
+
}
|
|
325
|
+
else if (info.templateUrl) {
|
|
326
|
+
const position = findTemplatePosition(el, el);
|
|
327
|
+
await openFileWithSearch(info.templateUrl, position.searchTerms);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
await openFile(info.filePath);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
if (OPTIONS.debug) {
|
|
335
|
+
console.warn('[angular-locator] File opening failed, refreshing component map:', e);
|
|
336
|
+
}
|
|
337
|
+
showClickFeedback(ev.clientX, ev.clientY, 'File not found, refreshing map...');
|
|
338
|
+
await refreshComponentMap();
|
|
339
|
+
const newCmp = getNearestComponent(el);
|
|
340
|
+
const newInfo = newCmp?.__cmpInfo;
|
|
341
|
+
if (newInfo) {
|
|
342
|
+
showClickFeedback(ev.clientX, ev.clientY, 'Retrying with updated map...');
|
|
343
|
+
if (ev.shiftKey) {
|
|
344
|
+
await openFile(newInfo.filePath);
|
|
345
|
+
}
|
|
346
|
+
else if (newInfo.templateUrl) {
|
|
347
|
+
const position = findTemplatePosition(el, el);
|
|
348
|
+
await openFileWithSearch(newInfo.templateUrl, position.searchTerms);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
await openFile(newInfo.filePath);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
showClickFeedback(ev.clientX, ev.clientY, 'Component not found in updated map');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function handleAltClick(ev) {
|
|
360
|
+
if (!OPTIONS.enableClick)
|
|
361
|
+
return;
|
|
362
|
+
if (!ev.altKey)
|
|
363
|
+
return;
|
|
364
|
+
ev.preventDefault();
|
|
365
|
+
ev.stopPropagation();
|
|
366
|
+
const el = ev.target;
|
|
367
|
+
if (!el)
|
|
368
|
+
return;
|
|
369
|
+
await handleAltOpen(ev, el);
|
|
370
|
+
}
|
|
371
|
+
let isAltPressed = false;
|
|
372
|
+
let currentTooltip = null;
|
|
373
|
+
let currentHighlightOverlay = null;
|
|
374
|
+
let lastHighlightedElement = null;
|
|
375
|
+
function removeHighlights() {
|
|
376
|
+
if (currentHighlightOverlay) {
|
|
377
|
+
currentHighlightOverlay.remove();
|
|
378
|
+
currentHighlightOverlay = null;
|
|
379
|
+
}
|
|
380
|
+
if (currentTooltip) {
|
|
381
|
+
currentTooltip.remove();
|
|
382
|
+
currentTooltip = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function handleMouseMove(ev) {
|
|
386
|
+
if (!OPTIONS.enableHover)
|
|
387
|
+
return;
|
|
388
|
+
if (!isAltPressed || !ev.altKey) {
|
|
389
|
+
if (isAltPressed && !ev.altKey) {
|
|
390
|
+
isAltPressed = false;
|
|
391
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
392
|
+
removeHighlights();
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!CMP_MAP) {
|
|
397
|
+
ensureMapIfNeeded();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const el = ev.target;
|
|
401
|
+
if (lastHighlightedElement === el)
|
|
402
|
+
return;
|
|
403
|
+
lastHighlightedElement = el;
|
|
404
|
+
removeHighlights();
|
|
405
|
+
const cmp = getNearestComponent(el);
|
|
406
|
+
if (!cmp)
|
|
407
|
+
return;
|
|
408
|
+
const className = cmp.constructor?.name;
|
|
409
|
+
const info = cmp.__cmpInfo;
|
|
410
|
+
if (!className || !info)
|
|
411
|
+
return;
|
|
412
|
+
try {
|
|
413
|
+
const rect = el.getBoundingClientRect();
|
|
414
|
+
const overlay = document.createElement('div');
|
|
415
|
+
overlay.className = 'dev-highlight-overlay';
|
|
416
|
+
overlay.style.left = rect.left + 'px';
|
|
417
|
+
overlay.style.top = rect.top + 'px';
|
|
418
|
+
overlay.style.width = rect.width + 'px';
|
|
419
|
+
overlay.style.height = rect.height + 'px';
|
|
420
|
+
document.body.appendChild(overlay);
|
|
421
|
+
currentHighlightOverlay = overlay;
|
|
422
|
+
if (OPTIONS.showTooltip) {
|
|
423
|
+
const tooltip = document.createElement('div');
|
|
424
|
+
tooltip.className = 'dev-tooltip';
|
|
425
|
+
tooltip.textContent = `${className} • Click: template • Shift+Click: .ts`;
|
|
426
|
+
tooltip.style.left = ev.clientX + 10 + 'px';
|
|
427
|
+
tooltip.style.top = ev.clientY - 30 + 'px';
|
|
428
|
+
document.body.appendChild(tooltip);
|
|
429
|
+
currentTooltip = tooltip;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
if (OPTIONS.debug) {
|
|
434
|
+
console.warn('[angular-locator] Failed during hover:', e);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function handleKeyDown(ev) {
|
|
439
|
+
if (!OPTIONS.enableHover)
|
|
440
|
+
return;
|
|
441
|
+
if ((ev.key === 'Alt' || ev.key === 'AltGraph') && !isAltPressed) {
|
|
442
|
+
isAltPressed = true;
|
|
443
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function handleKeyUp(ev) {
|
|
447
|
+
if (!OPTIONS.enableHover)
|
|
448
|
+
return;
|
|
449
|
+
if ((ev.key === 'Alt' || ev.key === 'AltGraph' || !ev.altKey) && isAltPressed) {
|
|
450
|
+
isAltPressed = false;
|
|
451
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
452
|
+
removeHighlights();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function installInternal(options) {
|
|
456
|
+
if (INSTALLED)
|
|
457
|
+
return;
|
|
458
|
+
INSTALLED = true;
|
|
459
|
+
OPTIONS = options;
|
|
460
|
+
addStyles();
|
|
461
|
+
if (OPTIONS.prefetchMap) {
|
|
462
|
+
try {
|
|
463
|
+
await ensureMap();
|
|
464
|
+
if (OPTIONS.debug) {
|
|
465
|
+
console.log('[angular-locator] Component map preloaded successfully');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
if (OPTIONS.debug) {
|
|
470
|
+
console.warn('[angular-locator] Failed to preload component map:', e);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (OPTIONS.enableClick) {
|
|
475
|
+
document.addEventListener('click', handleAltClick, true);
|
|
476
|
+
}
|
|
477
|
+
if (OPTIONS.enableHover) {
|
|
478
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
479
|
+
document.addEventListener('keyup', handleKeyUp);
|
|
480
|
+
}
|
|
481
|
+
window.addEventListener('scroll', () => {
|
|
482
|
+
if (currentHighlightOverlay) {
|
|
483
|
+
removeHighlights();
|
|
484
|
+
}
|
|
485
|
+
}, { passive: true });
|
|
486
|
+
window.addEventListener('resize', () => {
|
|
487
|
+
if (currentHighlightOverlay) {
|
|
488
|
+
removeHighlights();
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
window.addEventListener('blur', () => {
|
|
492
|
+
isAltPressed = false;
|
|
493
|
+
removeHighlights();
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
export async function installAngularLocator(options = {}) {
|
|
497
|
+
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
498
|
+
return;
|
|
499
|
+
const mergedOptions = {
|
|
500
|
+
...DEFAULT_OPTIONS,
|
|
501
|
+
...options,
|
|
502
|
+
endpoints: {
|
|
503
|
+
...DEFAULT_ENDPOINTS,
|
|
504
|
+
...options.endpoints,
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
await installInternal(mergedOptions);
|
|
508
|
+
}
|
|
509
|
+
export async function refreshComponentMap() {
|
|
510
|
+
try {
|
|
511
|
+
CMP_MAP = null;
|
|
512
|
+
await ensureMap(true);
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
if (OPTIONS.debug) {
|
|
516
|
+
console.error('[angular-locator] Failed to refresh component map:', e);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export async function preloadComponentMap() {
|
|
521
|
+
await ensureMap();
|
|
522
|
+
}
|
|
523
|
+
export function isAngularLocatorInstalled() {
|
|
524
|
+
return INSTALLED;
|
|
525
|
+
}
|
|
526
|
+
export const _internal = {
|
|
527
|
+
ensureMap,
|
|
528
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const DEFAULT_INCLUDE_GLOBS = [
|
|
6
|
+
'src/**/*.{ts,tsx}',
|
|
7
|
+
'projects/**/*.{ts,tsx}',
|
|
8
|
+
'apps/**/*.{ts,tsx}',
|
|
9
|
+
'libs/**/*.{ts,tsx}',
|
|
10
|
+
];
|
|
11
|
+
const DEFAULT_EXCLUDE_GLOBS = [
|
|
12
|
+
'**/node_modules/**',
|
|
13
|
+
'**/dist/**',
|
|
14
|
+
'**/.angular/**',
|
|
15
|
+
'**/coverage/**',
|
|
16
|
+
'**/*.spec.ts',
|
|
17
|
+
'**/*.test.ts',
|
|
18
|
+
'**/*.e2e.ts',
|
|
19
|
+
];
|
|
20
|
+
const root = process.cwd();
|
|
21
|
+
const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
|
|
22
|
+
const configPath = path.resolve(root, CONFIG_FILENAME);
|
|
23
|
+
function readConfig() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function toPosix(p) {
|
|
32
|
+
return p.replace(/\\/g, '/');
|
|
33
|
+
}
|
|
34
|
+
function prefixWorkspaceRoot(glob, workspaceRoot) {
|
|
35
|
+
if (!workspaceRoot || workspaceRoot === '.' || workspaceRoot === './')
|
|
36
|
+
return glob;
|
|
37
|
+
if (path.isAbsolute(glob))
|
|
38
|
+
return glob;
|
|
39
|
+
const rootPosix = toPosix(workspaceRoot).replace(/\/+$/, '');
|
|
40
|
+
const globPosix = toPosix(glob).replace(/^\/+/, '');
|
|
41
|
+
if (globPosix.startsWith(rootPosix + '/'))
|
|
42
|
+
return globPosix;
|
|
43
|
+
return `${rootPosix}/${globPosix}`;
|
|
44
|
+
}
|
|
45
|
+
function globToNeedle(glob) {
|
|
46
|
+
return toPosix(glob).replace(/\*\*/g, '').replace(/\*/g, '');
|
|
47
|
+
}
|
|
48
|
+
function isExcluded(filePath, excludeGlobs) {
|
|
49
|
+
const normalized = toPosix(filePath);
|
|
50
|
+
return excludeGlobs.some((pattern) => {
|
|
51
|
+
const needle = globToNeedle(pattern);
|
|
52
|
+
if (!needle)
|
|
53
|
+
return false;
|
|
54
|
+
return normalized.includes(needle);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function main() {
|
|
58
|
+
const cfg = readConfig();
|
|
59
|
+
const workspaceRoot = cfg.workspaceRoot?.trim() || '.';
|
|
60
|
+
const includeGlobs = cfg.scan?.includeGlobs ?? DEFAULT_INCLUDE_GLOBS;
|
|
61
|
+
const excludeGlobs = cfg.scan?.excludeGlobs ?? DEFAULT_EXCLUDE_GLOBS;
|
|
62
|
+
const effectiveIncludeGlobs = includeGlobs.map((g) => prefixWorkspaceRoot(g, workspaceRoot));
|
|
63
|
+
const outDir = path.resolve(root, '.open-in-editor');
|
|
64
|
+
const outFile = path.join(outDir, 'component-map.json');
|
|
65
|
+
const cacheFile = path.join(outDir, 'scan-cache.json');
|
|
66
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
67
|
+
function loadCache() {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function saveCache(cache) {
|
|
76
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
77
|
+
}
|
|
78
|
+
function getFileStats(filePaths) {
|
|
79
|
+
const stats = {};
|
|
80
|
+
for (const filePath of filePaths) {
|
|
81
|
+
try {
|
|
82
|
+
const stat = fs.statSync(filePath);
|
|
83
|
+
stats[filePath] = stat.mtimeMs;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// File may have been deleted.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return stats;
|
|
90
|
+
}
|
|
91
|
+
const project = new Project({
|
|
92
|
+
skipAddingFilesFromTsConfig: true,
|
|
93
|
+
});
|
|
94
|
+
project.addSourceFilesAtPaths(effectiveIncludeGlobs);
|
|
95
|
+
const sourceFiles = project
|
|
96
|
+
.getSourceFiles()
|
|
97
|
+
.filter((sf) => !isExcluded(sf.getFilePath(), excludeGlobs));
|
|
98
|
+
const filePaths = sourceFiles.map((sf) => sf.getFilePath());
|
|
99
|
+
const currentStats = getFileStats(filePaths);
|
|
100
|
+
const previousCache = loadCache();
|
|
101
|
+
const hasChanges = filePaths.some((filePath) => !previousCache[filePath] || previousCache[filePath] !== currentStats[filePath]);
|
|
102
|
+
const cachedPaths = Object.keys(previousCache);
|
|
103
|
+
const hasNewOrDeletedFiles = filePaths.length !== cachedPaths.length ||
|
|
104
|
+
filePaths.some((p) => !previousCache[p]) ||
|
|
105
|
+
cachedPaths.some((p) => !currentStats[p]);
|
|
106
|
+
if (!hasChanges && !hasNewOrDeletedFiles && fs.existsSync(outFile)) {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
const detailByFilePath = {};
|
|
110
|
+
const filePathsByClassName = {};
|
|
111
|
+
for (const sf of sourceFiles) {
|
|
112
|
+
const filePath = sf.getFilePath();
|
|
113
|
+
const classes = sf.getClasses();
|
|
114
|
+
for (const cls of classes) {
|
|
115
|
+
const decorators = cls.getDecorators();
|
|
116
|
+
const comp = decorators.find((d) => d.getName() === 'Component');
|
|
117
|
+
if (!comp)
|
|
118
|
+
continue;
|
|
119
|
+
const arg = comp.getCallExpression()?.getArguments()[0];
|
|
120
|
+
if (!arg || !arg.asKind(SyntaxKind.ObjectLiteralExpression))
|
|
121
|
+
continue;
|
|
122
|
+
const obj = arg.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
123
|
+
const templateUrlProp = obj.getProperty('templateUrl');
|
|
124
|
+
const templateUrl = templateUrlProp
|
|
125
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
126
|
+
?.getInitializer()
|
|
127
|
+
?.getText()
|
|
128
|
+
.replace(/^`|^'|^"|"|'|`$/g, '');
|
|
129
|
+
const className = cls.getName();
|
|
130
|
+
if (!className)
|
|
131
|
+
continue;
|
|
132
|
+
const absTs = path.resolve(root, filePath);
|
|
133
|
+
const absTpl = templateUrl ? path.resolve(path.dirname(absTs), templateUrl) : undefined;
|
|
134
|
+
detailByFilePath[absTs] = {
|
|
135
|
+
className,
|
|
136
|
+
filePath: absTs,
|
|
137
|
+
templateUrl: absTpl,
|
|
138
|
+
};
|
|
139
|
+
if (!filePathsByClassName[className])
|
|
140
|
+
filePathsByClassName[className] = [];
|
|
141
|
+
if (!filePathsByClassName[className].includes(absTs)) {
|
|
142
|
+
filePathsByClassName[className].push(absTs);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const out = {
|
|
147
|
+
generatedAt: new Date().toISOString(),
|
|
148
|
+
detailByFilePath,
|
|
149
|
+
filePathsByClassName,
|
|
150
|
+
};
|
|
151
|
+
fs.writeFileSync(outFile, JSON.stringify(out, null, 2));
|
|
152
|
+
saveCache(currentStats);
|
|
153
|
+
console.log(`[cmp-scan] ✅ Saved ${Object.keys(detailByFilePath).length} components to ${path.relative(root, outFile)}`);
|
|
154
|
+
}
|
|
155
|
+
main().catch((err) => {
|
|
156
|
+
console.error('[cmp-scan] Failed:', err);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|