esm-imports-analyzer 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/README.md +113 -0
- package/dist/analysis/cycle-detector.d.ts +3 -0
- package/dist/analysis/cycle-detector.d.ts.map +1 -0
- package/dist/analysis/cycle-detector.js +111 -0
- package/dist/analysis/cycle-detector.js.map +1 -0
- package/dist/analysis/folder-tree.d.ts +3 -0
- package/dist/analysis/folder-tree.d.ts.map +1 -0
- package/dist/analysis/folder-tree.js +141 -0
- package/dist/analysis/folder-tree.js.map +1 -0
- package/dist/analysis/grouper.d.ts +4 -0
- package/dist/analysis/grouper.d.ts.map +1 -0
- package/dist/analysis/grouper.js +156 -0
- package/dist/analysis/grouper.js.map +1 -0
- package/dist/analysis/timing.d.ts +9 -0
- package/dist/analysis/timing.d.ts.map +1 -0
- package/dist/analysis/timing.js +37 -0
- package/dist/analysis/timing.js.map +1 -0
- package/dist/analysis/tree-builder.d.ts +3 -0
- package/dist/analysis/tree-builder.d.ts.map +1 -0
- package/dist/analysis/tree-builder.js +47 -0
- package/dist/analysis/tree-builder.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/cli.js.map +1 -0
- package/dist/loader/hooks.d.ts +4 -0
- package/dist/loader/hooks.d.ts.map +1 -0
- package/dist/loader/hooks.js +79 -0
- package/dist/loader/hooks.js.map +1 -0
- package/dist/loader/register.d.ts +2 -0
- package/dist/loader/register.d.ts.map +1 -0
- package/dist/loader/register.js +35 -0
- package/dist/loader/register.js.map +1 -0
- package/dist/report/generator.d.ts +3 -0
- package/dist/report/generator.d.ts.map +1 -0
- package/dist/report/generator.js +50 -0
- package/dist/report/generator.js.map +1 -0
- package/dist/report/template.html +146 -0
- package/dist/report/ui/cycles-panel.js +80 -0
- package/dist/report/ui/filters.js +13 -0
- package/dist/report/ui/graph.js +1310 -0
- package/dist/report/ui/styles.css +531 -0
- package/dist/report/ui/table.js +209 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
/* global cytoscape, document */
|
|
2
|
+
|
|
3
|
+
// Web Worker source that runs dagre layout off the main thread.
|
|
4
|
+
var DAGRE_WORKER_SRC = [
|
|
5
|
+
'importScripts("https://unpkg.com/dagre@0.7.4/dist/dagre.js");',
|
|
6
|
+
'',
|
|
7
|
+
'self.onmessage = function (e) {',
|
|
8
|
+
' var msg = e.data;',
|
|
9
|
+
' var g = new dagre.graphlib.Graph({ compound: true, multigraph: true });',
|
|
10
|
+
' g.setGraph({',
|
|
11
|
+
' rankdir: msg.rankDir || "TB",',
|
|
12
|
+
' nodesep: msg.nodeSep || 60,',
|
|
13
|
+
' edgesep: msg.edgeSep || 20,',
|
|
14
|
+
' ranksep: msg.rankSep || 80,',
|
|
15
|
+
' });',
|
|
16
|
+
' g.setDefaultEdgeLabel(function () { return {}; });',
|
|
17
|
+
'',
|
|
18
|
+
' for (var i = 0; i < msg.nodes.length; i++) {',
|
|
19
|
+
' var n = msg.nodes[i];',
|
|
20
|
+
' g.setNode(n.id, { width: n.width, height: n.height, label: n.id });',
|
|
21
|
+
' if (n.parent) g.setParent(n.id, n.parent);',
|
|
22
|
+
' }',
|
|
23
|
+
' for (var j = 0; j < msg.edges.length; j++) {',
|
|
24
|
+
' var ed = msg.edges[j];',
|
|
25
|
+
' g.setEdge(ed.source, ed.target, {}, ed.id);',
|
|
26
|
+
' }',
|
|
27
|
+
'',
|
|
28
|
+
' dagre.layout(g);',
|
|
29
|
+
'',
|
|
30
|
+
' var positions = {};',
|
|
31
|
+
' g.nodes().forEach(function (id) {',
|
|
32
|
+
' var nd = g.node(id);',
|
|
33
|
+
' if (nd) positions[id] = { x: nd.x, y: nd.y };',
|
|
34
|
+
' });',
|
|
35
|
+
' self.postMessage(positions);',
|
|
36
|
+
'};',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
var layoutOverlay = null;
|
|
40
|
+
var collapsedGroups = new Set();
|
|
41
|
+
var autoRelayout = true;
|
|
42
|
+
|
|
43
|
+
// Folder tree state
|
|
44
|
+
var groupFolderTrees = {}; // groupId -> FolderTreeNode[] (top-level children)
|
|
45
|
+
var folderState = {}; // folderNodeId -> { children: FolderTreeNode[], groupId: string }
|
|
46
|
+
var parentFolderOf = {}; // nodeId (file or folder) -> parent folderNodeId | null
|
|
47
|
+
var expandedFolders = new Set();
|
|
48
|
+
var suppressAutoSelect = false;
|
|
49
|
+
var cycleEdgeSet = new Set(); // Set of "source->target" edge IDs that are part of cycles
|
|
50
|
+
// Set of group IDs that have folder trees
|
|
51
|
+
var groupsWithFolderTree = new Set();
|
|
52
|
+
|
|
53
|
+
function clearSearch() {
|
|
54
|
+
var input = document.getElementById('search-input');
|
|
55
|
+
if (input && input.value) {
|
|
56
|
+
input.value = '';
|
|
57
|
+
input.dispatchEvent(new Event('input'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function showOverlay() {
|
|
62
|
+
if (!layoutOverlay) layoutOverlay = document.getElementById('layout-overlay');
|
|
63
|
+
if (layoutOverlay) layoutOverlay.classList.remove('hidden');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hideOverlay() {
|
|
67
|
+
if (!layoutOverlay) layoutOverlay = document.getElementById('layout-overlay');
|
|
68
|
+
if (layoutOverlay) layoutOverlay.classList.add('hidden');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runLayout(cy, callback) {
|
|
72
|
+
showOverlay();
|
|
73
|
+
|
|
74
|
+
var nodes = [];
|
|
75
|
+
var edges = [];
|
|
76
|
+
var visibleNodeIds = new Set();
|
|
77
|
+
var expandedGroupIds = new Set();
|
|
78
|
+
|
|
79
|
+
cy.nodes(':visible').forEach(function (node) {
|
|
80
|
+
var isGroup = node.hasClass('group');
|
|
81
|
+
if (isGroup && !collapsedGroups.has(node.id())) {
|
|
82
|
+
expandedGroupIds.add(node.id());
|
|
83
|
+
visibleNodeIds.add(node.id());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
visibleNodeIds.add(node.id());
|
|
87
|
+
var parentId = null;
|
|
88
|
+
if (node.parent().length > 0 && expandedGroupIds.has(node.parent().id())) {
|
|
89
|
+
parentId = node.parent().id();
|
|
90
|
+
}
|
|
91
|
+
nodes.push({
|
|
92
|
+
id: node.id(),
|
|
93
|
+
width: node.outerWidth() || 60,
|
|
94
|
+
height: node.outerHeight() || 40,
|
|
95
|
+
parent: parentId,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expandedGroupIds.forEach(function (gid) {
|
|
100
|
+
nodes.push({ id: gid, width: 0, height: 0, parent: null });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
cy.edges(':visible').forEach(function (edge) {
|
|
104
|
+
if (visibleNodeIds.has(edge.source().id()) && visibleNodeIds.has(edge.target().id())) {
|
|
105
|
+
edges.push({ id: edge.id(), source: edge.source().id(), target: edge.target().id() });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
var blob = new Blob([DAGRE_WORKER_SRC], { type: 'application/javascript' });
|
|
110
|
+
var url = URL.createObjectURL(blob);
|
|
111
|
+
var worker = new Worker(url);
|
|
112
|
+
|
|
113
|
+
worker.onmessage = function (e) {
|
|
114
|
+
var positions = e.data;
|
|
115
|
+
cy.batch(function () {
|
|
116
|
+
cy.nodes().forEach(function (node) {
|
|
117
|
+
if (expandedGroupIds.has(node.id())) return;
|
|
118
|
+
var pos = positions[node.id()];
|
|
119
|
+
if (pos) node.position(pos);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
cy.fit(undefined, 30);
|
|
123
|
+
hideOverlay();
|
|
124
|
+
worker.terminate();
|
|
125
|
+
URL.revokeObjectURL(url);
|
|
126
|
+
if (callback) callback();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
worker.onerror = function () {
|
|
130
|
+
hideOverlay();
|
|
131
|
+
worker.terminate();
|
|
132
|
+
URL.revokeObjectURL(url);
|
|
133
|
+
if (callback) callback();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
worker.postMessage({ nodes: nodes, edges: edges, rankDir: 'TB', nodeSep: 60, edgeSep: 20, rankSep: 80 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function maybeRelayout(cy) {
|
|
140
|
+
if (autoRelayout && !suppressAutoSelect) runLayout(cy);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function maybeRelayoutWithCallback(cy, callback) {
|
|
144
|
+
if (autoRelayout) {
|
|
145
|
+
runLayout(cy, callback);
|
|
146
|
+
} else if (callback) {
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Resolve a module URL to its nearest visible ancestor node ID.
|
|
152
|
+
// Walks: module -> parent folder -> ... -> parent group
|
|
153
|
+
function resolveVisibleNode(cy, moduleURL) {
|
|
154
|
+
// Check if the module node itself is visible
|
|
155
|
+
var node = cy.getElementById(moduleURL);
|
|
156
|
+
if (node.length > 0 && node.visible()) return moduleURL;
|
|
157
|
+
|
|
158
|
+
// Walk up through parent folders
|
|
159
|
+
var folderId = parentFolderOf[moduleURL];
|
|
160
|
+
while (folderId) {
|
|
161
|
+
var folderNode = cy.getElementById(folderId);
|
|
162
|
+
if (folderNode.length > 0 && folderNode.visible()) return folderId;
|
|
163
|
+
folderId = parentFolderOf[folderId];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fall back to parent group
|
|
167
|
+
if (node.length > 0 && node.parent().length > 0) return node.parent().id();
|
|
168
|
+
return moduleURL;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function refreshEdgeVisibility(cy) {
|
|
172
|
+
cy.batch(function () {
|
|
173
|
+
cy.edges('.meta-edge').remove();
|
|
174
|
+
|
|
175
|
+
var metaEdges = {};
|
|
176
|
+
|
|
177
|
+
cy.edges().forEach(function (edge) {
|
|
178
|
+
var srcId = edge.source().id();
|
|
179
|
+
var tgtId = edge.target().id();
|
|
180
|
+
|
|
181
|
+
var effectiveSrc = resolveVisibleNode(cy, srcId);
|
|
182
|
+
var effectiveTgt = resolveVisibleNode(cy, tgtId);
|
|
183
|
+
|
|
184
|
+
// Both resolved to themselves = both visible, show the real edge
|
|
185
|
+
if (effectiveSrc === srcId && effectiveTgt === tgtId) {
|
|
186
|
+
edge.show();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// At least one resolved to a different node — hide real edge, collect meta-edge
|
|
191
|
+
edge.hide();
|
|
192
|
+
|
|
193
|
+
if (effectiveSrc === effectiveTgt) return;
|
|
194
|
+
|
|
195
|
+
var key = effectiveSrc + '||' + effectiveTgt;
|
|
196
|
+
if (!metaEdges[key]) {
|
|
197
|
+
metaEdges[key] = { source: effectiveSrc, target: effectiveTgt, count: 0 };
|
|
198
|
+
}
|
|
199
|
+
metaEdges[key].count++;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
var toAdd = [];
|
|
203
|
+
var keys = Object.keys(metaEdges);
|
|
204
|
+
for (var i = 0; i < keys.length; i++) {
|
|
205
|
+
var me = metaEdges[keys[i]];
|
|
206
|
+
toAdd.push({
|
|
207
|
+
group: 'edges',
|
|
208
|
+
data: {
|
|
209
|
+
id: 'meta-' + keys[i],
|
|
210
|
+
source: me.source,
|
|
211
|
+
target: me.target,
|
|
212
|
+
specifier: me.count + (me.count === 1 ? ' import' : ' imports'),
|
|
213
|
+
},
|
|
214
|
+
classes: 'meta-edge',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (toAdd.length > 0) cy.add(toAdd);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function selectAndHighlight(cy, node) {
|
|
222
|
+
cy.nodes().unselect();
|
|
223
|
+
node.select();
|
|
224
|
+
applySelectionHighlight(cy);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Expand a group: show top-level folder tree children (or all children if no tree)
|
|
228
|
+
function expandGroup(cy, groupNode) {
|
|
229
|
+
collapsedGroups.delete(groupNode.id());
|
|
230
|
+
groupNode.removeClass('collapsed');
|
|
231
|
+
|
|
232
|
+
var gid = groupNode.data('groupId');
|
|
233
|
+
var tree = groupFolderTrees[gid];
|
|
234
|
+
|
|
235
|
+
cy.batch(function () {
|
|
236
|
+
if (tree && tree.length > 0) {
|
|
237
|
+
// Show only top-level folder tree children
|
|
238
|
+
for (var i = 0; i < tree.length; i++) {
|
|
239
|
+
cy.getElementById(tree[i].id).show();
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// No folder tree — show all children flat
|
|
243
|
+
groupNode.children().show();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
refreshEdgeVisibility(cy);
|
|
248
|
+
maybeRelayout(cy);
|
|
249
|
+
if (!suppressAutoSelect) selectAndHighlight(cy, groupNode);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Collapse a group: hide ALL children, reset folder expansion state
|
|
253
|
+
function collapseGroup(cy, groupNode) {
|
|
254
|
+
collapsedGroups.add(groupNode.id());
|
|
255
|
+
groupNode.addClass('collapsed');
|
|
256
|
+
|
|
257
|
+
var gid = groupNode.data('groupId');
|
|
258
|
+
|
|
259
|
+
cy.batch(function () {
|
|
260
|
+
groupNode.children().hide();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Reset folder expansion state for this group
|
|
264
|
+
var toDelete = [];
|
|
265
|
+
expandedFolders.forEach(function (fid) {
|
|
266
|
+
var fs = folderState[fid];
|
|
267
|
+
if (fs && fs.groupId === gid) toDelete.push(fid);
|
|
268
|
+
});
|
|
269
|
+
for (var i = 0; i < toDelete.length; i++) {
|
|
270
|
+
expandedFolders.delete(toDelete[i]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
refreshEdgeVisibility(cy);
|
|
274
|
+
maybeRelayout(cy);
|
|
275
|
+
if (!suppressAutoSelect) selectAndHighlight(cy, groupNode);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Expand a folder: hide the folder node, show its children
|
|
279
|
+
function expandFolder(cy, folderNodeId) {
|
|
280
|
+
var state = folderState[folderNodeId];
|
|
281
|
+
if (!state) return;
|
|
282
|
+
|
|
283
|
+
expandedFolders.add(folderNodeId);
|
|
284
|
+
|
|
285
|
+
var childIds = [];
|
|
286
|
+
cy.batch(function () {
|
|
287
|
+
cy.getElementById(folderNodeId).hide();
|
|
288
|
+
for (var i = 0; i < state.children.length; i++) {
|
|
289
|
+
var childId = state.children[i].id;
|
|
290
|
+
cy.getElementById(childId).show();
|
|
291
|
+
childIds.push(childId);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
refreshEdgeVisibility(cy);
|
|
296
|
+
maybeRelayout(cy);
|
|
297
|
+
|
|
298
|
+
if (!suppressAutoSelect) {
|
|
299
|
+
// Select the newly revealed children
|
|
300
|
+
cy.nodes().unselect();
|
|
301
|
+
for (var j = 0; j < childIds.length; j++) {
|
|
302
|
+
cy.getElementById(childIds[j]).select();
|
|
303
|
+
}
|
|
304
|
+
applySelectionHighlight(cy);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Ensure a module node is visible by expanding its group and ancestor folders
|
|
309
|
+
function revealModule(cy, moduleURL) {
|
|
310
|
+
var node = cy.getElementById(moduleURL);
|
|
311
|
+
if (node.length === 0) return;
|
|
312
|
+
|
|
313
|
+
// Expand parent group if collapsed
|
|
314
|
+
if (node.parent().length > 0 && collapsedGroups.has(node.parent().id())) {
|
|
315
|
+
expandGroup(cy, node.parent());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Collect ancestor folders that need expanding (outermost first)
|
|
319
|
+
var chain = [];
|
|
320
|
+
var fid = parentFolderOf[moduleURL];
|
|
321
|
+
while (fid) {
|
|
322
|
+
if (!expandedFolders.has(fid)) chain.push(fid);
|
|
323
|
+
fid = parentFolderOf[fid];
|
|
324
|
+
}
|
|
325
|
+
chain.reverse();
|
|
326
|
+
for (var i = 0; i < chain.length; i++) {
|
|
327
|
+
expandFolder(cy, chain[i]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function collapseAll(cy) {
|
|
332
|
+
cy.nodes('.group').forEach(function (groupNode) {
|
|
333
|
+
if (groupNode.data('moduleCount') <= 1) return;
|
|
334
|
+
collapsedGroups.add(groupNode.id());
|
|
335
|
+
groupNode.addClass('collapsed');
|
|
336
|
+
});
|
|
337
|
+
expandedFolders.clear();
|
|
338
|
+
cy.batch(function () {
|
|
339
|
+
cy.nodes('.group').forEach(function (groupNode) {
|
|
340
|
+
if (groupNode.data('moduleCount') <= 1) return;
|
|
341
|
+
groupNode.children().hide();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
refreshEdgeVisibility(cy);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function expandAll(cy) {
|
|
348
|
+
cy.batch(function () {
|
|
349
|
+
// Expand all groups
|
|
350
|
+
cy.nodes('.group').forEach(function (groupNode) {
|
|
351
|
+
collapsedGroups.delete(groupNode.id());
|
|
352
|
+
groupNode.removeClass('collapsed');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Expand all folders: hide folder nodes, show their children
|
|
356
|
+
var changed = true;
|
|
357
|
+
while (changed) {
|
|
358
|
+
changed = false;
|
|
359
|
+
cy.nodes('.folder:visible').forEach(function (fNode) {
|
|
360
|
+
var state = folderState[fNode.id()];
|
|
361
|
+
if (state && !expandedFolders.has(fNode.id())) {
|
|
362
|
+
expandedFolders.add(fNode.id());
|
|
363
|
+
fNode.hide();
|
|
364
|
+
for (var i = 0; i < state.children.length; i++) {
|
|
365
|
+
cy.getElementById(state.children[i].id).show();
|
|
366
|
+
}
|
|
367
|
+
changed = true;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Show any remaining module nodes that might still be hidden
|
|
373
|
+
cy.nodes('.module').forEach(function (node) {
|
|
374
|
+
node.show();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
refreshEdgeVisibility(cy);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// --- Directional selection highlighting ---
|
|
382
|
+
var HL_CLASSES = ['hl-selected', 'hl-outgoing', 'hl-incoming', 'hl-cycle', 'dimmed'];
|
|
383
|
+
|
|
384
|
+
function clearSelectionHighlight(cy) {
|
|
385
|
+
for (var i = 0; i < HL_CLASSES.length; i++) {
|
|
386
|
+
cy.elements().removeClass(HL_CLASSES[i]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function applySelectionHighlight(cy) {
|
|
391
|
+
var selected = cy.nodes(':selected');
|
|
392
|
+
if (selected.length === 0) {
|
|
393
|
+
clearSelectionHighlight(cy);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (var i = 0; i < HL_CLASSES.length; i++) {
|
|
398
|
+
cy.elements().removeClass(HL_CLASSES[i]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
var highlighted = cy.collection();
|
|
402
|
+
|
|
403
|
+
function isSelectedOrParent(n) {
|
|
404
|
+
return n.hasClass('hl-selected');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function traceEdges(node) {
|
|
408
|
+
// Outgoing edges from this node
|
|
409
|
+
var outEdges = node.outgoers('edge:visible');
|
|
410
|
+
outEdges.addClass('hl-outgoing');
|
|
411
|
+
highlighted = highlighted.union(outEdges);
|
|
412
|
+
outEdges.targets().forEach(function (t) {
|
|
413
|
+
if (!isSelectedOrParent(t)) t.addClass('hl-outgoing');
|
|
414
|
+
highlighted = highlighted.union(t);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Incoming edges to this node
|
|
418
|
+
var inEdges = node.incomers('edge:visible');
|
|
419
|
+
inEdges.addClass('hl-incoming');
|
|
420
|
+
highlighted = highlighted.union(inEdges);
|
|
421
|
+
inEdges.sources().forEach(function (s) {
|
|
422
|
+
if (!isSelectedOrParent(s) && !s.hasClass('hl-outgoing')) s.addClass('hl-incoming');
|
|
423
|
+
highlighted = highlighted.union(s);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Follow meta-edges for group and folder nodes
|
|
427
|
+
if (node.hasClass('group') || node.hasClass('folder')) {
|
|
428
|
+
node.connectedEdges('.meta-edge:visible').forEach(function (me) {
|
|
429
|
+
var isOut = me.source().id() === node.id();
|
|
430
|
+
if (isOut) {
|
|
431
|
+
me.addClass('hl-outgoing');
|
|
432
|
+
if (!isSelectedOrParent(me.target())) me.target().addClass('hl-outgoing');
|
|
433
|
+
} else {
|
|
434
|
+
me.addClass('hl-incoming');
|
|
435
|
+
if (!isSelectedOrParent(me.source())) me.source().addClass('hl-incoming');
|
|
436
|
+
}
|
|
437
|
+
highlighted = highlighted.union(me).union(me.source()).union(me.target());
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
selected.forEach(function (node) {
|
|
443
|
+
var isExpandedGroup = node.hasClass('group') && !collapsedGroups.has(node.id());
|
|
444
|
+
|
|
445
|
+
node.addClass('hl-selected');
|
|
446
|
+
highlighted = highlighted.union(node);
|
|
447
|
+
|
|
448
|
+
// Trace direct edges from this node
|
|
449
|
+
traceEdges(node);
|
|
450
|
+
|
|
451
|
+
if (isExpandedGroup) {
|
|
452
|
+
// Include all visible children and their internal edges
|
|
453
|
+
var children = node.children(':visible');
|
|
454
|
+
children.forEach(function (child) {
|
|
455
|
+
highlighted = highlighted.union(child);
|
|
456
|
+
});
|
|
457
|
+
children.connectedEdges(':visible').forEach(function (e) {
|
|
458
|
+
highlighted = highlighted.union(e);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Also trace outgoing/incoming from each visible child to external nodes
|
|
462
|
+
children.forEach(function (child) {
|
|
463
|
+
traceEdges(child);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Mark cycle edges among the highlighted edges
|
|
469
|
+
highlighted.edges().forEach(function (edge) {
|
|
470
|
+
if (cycleEdgeSet.has(edge.id())) {
|
|
471
|
+
edge.addClass('hl-cycle');
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
cy.elements().not(highlighted).addClass('dimmed');
|
|
476
|
+
highlighted.forEach(function (ele) {
|
|
477
|
+
if (ele.isNode() && ele.parent().length > 0) {
|
|
478
|
+
ele.parent().removeClass('dimmed');
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// --- Build folder tree elements ---
|
|
484
|
+
// Walk a FolderTreeNode[] recursively and create cytoscape elements for all
|
|
485
|
+
// folders and files. All start hidden; expand logic controls visibility.
|
|
486
|
+
function buildFolderElements(elements, treeNodes, groupNodeId, groupId, parentFolderId) {
|
|
487
|
+
for (var i = 0; i < treeNodes.length; i++) {
|
|
488
|
+
var tn = treeNodes[i];
|
|
489
|
+
if (tn.type === 'folder') {
|
|
490
|
+
elements.push({
|
|
491
|
+
group: 'nodes',
|
|
492
|
+
data: {
|
|
493
|
+
id: tn.id,
|
|
494
|
+
label: tn.label,
|
|
495
|
+
parent: groupNodeId,
|
|
496
|
+
isFolder: true,
|
|
497
|
+
groupId: groupId,
|
|
498
|
+
},
|
|
499
|
+
classes: 'folder',
|
|
500
|
+
});
|
|
501
|
+
folderState[tn.id] = { children: tn.children, groupId: groupId };
|
|
502
|
+
parentFolderOf[tn.id] = parentFolderId;
|
|
503
|
+
buildFolderElements(elements, tn.children, groupNodeId, groupId, tn.id);
|
|
504
|
+
} else {
|
|
505
|
+
// File node — it may already exist from the module creation loop.
|
|
506
|
+
// We record its parentFolder mapping regardless.
|
|
507
|
+
parentFolderOf[tn.moduleURL || tn.id] = parentFolderId;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function initGraph(data) {
|
|
513
|
+
var container = document.getElementById('cy');
|
|
514
|
+
var tooltip = document.getElementById('graph-tooltip');
|
|
515
|
+
|
|
516
|
+
var elements = [];
|
|
517
|
+
var moduleSet = new Set();
|
|
518
|
+
var groupMap = new Map();
|
|
519
|
+
|
|
520
|
+
for (var gi = 0; gi < data.groups.length; gi++) {
|
|
521
|
+
var group = data.groups[gi];
|
|
522
|
+
for (var gmi = 0; gmi < group.modules.length; gmi++) {
|
|
523
|
+
groupMap.set(group.modules[gmi], group);
|
|
524
|
+
}
|
|
525
|
+
if (group.folderTree && group.folderTree.length > 0) {
|
|
526
|
+
groupFolderTrees[group.id] = group.folderTree;
|
|
527
|
+
groupsWithFolderTree.add(group.id);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
// Add group (compound) nodes
|
|
533
|
+
for (var gni = 0; gni < data.groups.length; gni++) {
|
|
534
|
+
var grp = data.groups[gni];
|
|
535
|
+
var groupNodeId = 'group-' + grp.id;
|
|
536
|
+
elements.push({
|
|
537
|
+
group: 'nodes',
|
|
538
|
+
data: {
|
|
539
|
+
id: groupNodeId,
|
|
540
|
+
label: grp.label + ' (' + grp.modules.length + ' modules)',
|
|
541
|
+
isGroup: true,
|
|
542
|
+
groupId: grp.id,
|
|
543
|
+
moduleCount: grp.modules.length,
|
|
544
|
+
},
|
|
545
|
+
classes: grp.isNodeModules ? 'group node-modules' : 'group',
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Build folder tree elements for this group
|
|
549
|
+
if (grp.folderTree && grp.folderTree.length > 0) {
|
|
550
|
+
buildFolderElements(elements, grp.folderTree, groupNodeId, grp.id, null);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Build a set of module URLs that appear in folder trees as files
|
|
555
|
+
// (to set their label from the tree instead of the specifier)
|
|
556
|
+
var folderTreeFileLabels = {};
|
|
557
|
+
function collectFileLabels(treeNodes) {
|
|
558
|
+
for (var i = 0; i < treeNodes.length; i++) {
|
|
559
|
+
var tn = treeNodes[i];
|
|
560
|
+
if (tn.type === 'file' && tn.moduleURL) {
|
|
561
|
+
folderTreeFileLabels[tn.moduleURL] = tn.label;
|
|
562
|
+
} else if (tn.children) {
|
|
563
|
+
collectFileLabels(tn.children);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (var gli = 0; gli < data.groups.length; gli++) {
|
|
568
|
+
if (data.groups[gli].folderTree) collectFileLabels(data.groups[gli].folderTree);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Build parent lookup from recorded data (first occurrence per URL)
|
|
572
|
+
var parentByURL = {};
|
|
573
|
+
for (var pbi = 0; pbi < data.modules.length; pbi++) {
|
|
574
|
+
var pm = data.modules[pbi];
|
|
575
|
+
if (!(pm.resolvedURL in parentByURL)) {
|
|
576
|
+
parentByURL[pm.resolvedURL] = pm.parentURL || null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getImportPath(moduleURL) {
|
|
581
|
+
var chain = [moduleURL];
|
|
582
|
+
var current = moduleURL;
|
|
583
|
+
var visited = {};
|
|
584
|
+
visited[current] = true;
|
|
585
|
+
while (parentByURL[current]) {
|
|
586
|
+
current = parentByURL[current];
|
|
587
|
+
if (visited[current]) break; // avoid infinite loop on cycles
|
|
588
|
+
visited[current] = true;
|
|
589
|
+
chain.push(current);
|
|
590
|
+
}
|
|
591
|
+
chain.reverse();
|
|
592
|
+
return chain.map(function (u) {
|
|
593
|
+
return u.startsWith('file://') ? u.slice(7) : u;
|
|
594
|
+
}).join(' -> ');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getModuleURLsForNode(node) {
|
|
598
|
+
var urls = [];
|
|
599
|
+
if (node.hasClass('module')) {
|
|
600
|
+
urls.push(node.id());
|
|
601
|
+
} else if (node.hasClass('group')) {
|
|
602
|
+
node.descendants('.module').forEach(function (m) { urls.push(m.id()); });
|
|
603
|
+
// Also include hidden modules from collapsed groups
|
|
604
|
+
var gid = node.data('groupId');
|
|
605
|
+
var grp = null;
|
|
606
|
+
for (var i = 0; i < data.groups.length; i++) {
|
|
607
|
+
if (data.groups[i].id === gid) { grp = data.groups[i]; break; }
|
|
608
|
+
}
|
|
609
|
+
if (grp) {
|
|
610
|
+
for (var j = 0; j < grp.modules.length; j++) {
|
|
611
|
+
if (urls.indexOf(grp.modules[j]) === -1) urls.push(grp.modules[j]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} else if (node.hasClass('folder')) {
|
|
615
|
+
(function collectFolderModules(fid) {
|
|
616
|
+
var state = folderState[fid];
|
|
617
|
+
if (!state) return;
|
|
618
|
+
for (var i = 0; i < state.children.length; i++) {
|
|
619
|
+
var child = state.children[i];
|
|
620
|
+
if (child.type === 'file' && child.moduleURL) {
|
|
621
|
+
urls.push(child.moduleURL);
|
|
622
|
+
} else if (child.type === 'folder') {
|
|
623
|
+
collectFolderModules(child.id);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
})(node.id());
|
|
627
|
+
}
|
|
628
|
+
return urls;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Track unique modules (first occurrence)
|
|
632
|
+
var seenModules = new Set();
|
|
633
|
+
for (var mni = 0; mni < data.modules.length; mni++) {
|
|
634
|
+
var mod = data.modules[mni];
|
|
635
|
+
if (seenModules.has(mod.resolvedURL)) continue;
|
|
636
|
+
seenModules.add(mod.resolvedURL);
|
|
637
|
+
|
|
638
|
+
var modGroup = groupMap.get(mod.resolvedURL);
|
|
639
|
+
var totalTime = mod.totalImportTime || 0;
|
|
640
|
+
var isBuiltin = mod.resolvedURL.startsWith('node:');
|
|
641
|
+
|
|
642
|
+
// Use folder tree label if available
|
|
643
|
+
var label = folderTreeFileLabels[mod.resolvedURL] || mod.specifier;
|
|
644
|
+
|
|
645
|
+
elements.push({
|
|
646
|
+
group: 'nodes',
|
|
647
|
+
data: {
|
|
648
|
+
id: mod.resolvedURL,
|
|
649
|
+
label: label,
|
|
650
|
+
parent: modGroup ? 'group-' + modGroup.id : undefined,
|
|
651
|
+
totalTime: totalTime,
|
|
652
|
+
fullPath: mod.resolvedURL,
|
|
653
|
+
isBuiltin: isBuiltin,
|
|
654
|
+
},
|
|
655
|
+
classes: isBuiltin ? 'module builtin' : 'module',
|
|
656
|
+
});
|
|
657
|
+
moduleSet.add(mod.resolvedURL);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Add edges (deduplicate)
|
|
661
|
+
var edgeSet = new Set();
|
|
662
|
+
for (var eni = 0; eni < data.modules.length; eni++) {
|
|
663
|
+
var eMod = data.modules[eni];
|
|
664
|
+
if (eMod.parentURL && moduleSet.has(eMod.parentURL) && moduleSet.has(eMod.resolvedURL)) {
|
|
665
|
+
var edgeId = eMod.parentURL + '->' + eMod.resolvedURL;
|
|
666
|
+
if (edgeSet.has(edgeId)) continue;
|
|
667
|
+
edgeSet.add(edgeId);
|
|
668
|
+
|
|
669
|
+
elements.push({
|
|
670
|
+
group: 'edges',
|
|
671
|
+
data: {
|
|
672
|
+
id: edgeId,
|
|
673
|
+
source: eMod.parentURL,
|
|
674
|
+
target: eMod.resolvedURL,
|
|
675
|
+
specifier: eMod.specifier,
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Build cycle edge lookup
|
|
682
|
+
cycleEdgeSet.clear();
|
|
683
|
+
if (data.cycles) {
|
|
684
|
+
for (var ci = 0; ci < data.cycles.length; ci++) {
|
|
685
|
+
var cycle = data.cycles[ci];
|
|
686
|
+
for (var cj = 0; cj < cycle.modules.length; cj++) {
|
|
687
|
+
var nextIdx = (cj + 1) % cycle.modules.length;
|
|
688
|
+
cycleEdgeSet.add(cycle.modules[cj] + '->' + cycle.modules[nextIdx]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
var cy = cytoscape({
|
|
694
|
+
container: container,
|
|
695
|
+
elements: elements,
|
|
696
|
+
style: [
|
|
697
|
+
{
|
|
698
|
+
selector: 'node.group',
|
|
699
|
+
style: {
|
|
700
|
+
'background-opacity': 0.75,
|
|
701
|
+
'background-color': '#313147',
|
|
702
|
+
'border-color': '#45475a',
|
|
703
|
+
'border-width': 1,
|
|
704
|
+
'label': 'data(label)',
|
|
705
|
+
'text-valign': 'center',
|
|
706
|
+
'text-halign': 'center',
|
|
707
|
+
'font-size': '11px',
|
|
708
|
+
'color': '#a6adc8',
|
|
709
|
+
'padding': '24px',
|
|
710
|
+
'shape': 'round-rectangle',
|
|
711
|
+
'min-width': '80px',
|
|
712
|
+
'min-height': '30px',
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
selector: 'node.group:parent',
|
|
717
|
+
style: {
|
|
718
|
+
'text-valign': 'top',
|
|
719
|
+
'text-margin-y': '-4px',
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
selector: 'node.group.collapsed',
|
|
724
|
+
style: {
|
|
725
|
+
'text-valign': 'center',
|
|
726
|
+
'text-halign': 'center',
|
|
727
|
+
'border-width': 2,
|
|
728
|
+
'border-color': '#585b70',
|
|
729
|
+
'width': function (ele) {
|
|
730
|
+
var label = ele.data('label') || '';
|
|
731
|
+
return Math.max(80, label.length * 6.5 + 24);
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
selector: 'node.group.node-modules',
|
|
737
|
+
style: {
|
|
738
|
+
'background-color': '#282839',
|
|
739
|
+
'border-style': 'dashed',
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
selector: 'node.folder',
|
|
744
|
+
style: {
|
|
745
|
+
'shape': 'round-rectangle',
|
|
746
|
+
'background-color': '#3b3b55',
|
|
747
|
+
'border-color': '#585b70',
|
|
748
|
+
'border-width': 1,
|
|
749
|
+
'label': 'data(label)',
|
|
750
|
+
'font-size': '10px',
|
|
751
|
+
'color': '#a6adc8',
|
|
752
|
+
'text-valign': 'center',
|
|
753
|
+
'text-halign': 'center',
|
|
754
|
+
'width': '80px',
|
|
755
|
+
'height': '30px',
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
selector: 'node.module',
|
|
760
|
+
style: {
|
|
761
|
+
'label': 'data(label)',
|
|
762
|
+
'font-size': '10px',
|
|
763
|
+
'color': '#cdd6f4',
|
|
764
|
+
'text-valign': 'bottom',
|
|
765
|
+
'text-halign': 'center',
|
|
766
|
+
'text-margin-y': '4px',
|
|
767
|
+
'width': 24,
|
|
768
|
+
'height': 24,
|
|
769
|
+
'background-color': '#89b4fa',
|
|
770
|
+
'border-width': 1,
|
|
771
|
+
'border-color': '#45475a',
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
selector: 'node.builtin',
|
|
776
|
+
style: {
|
|
777
|
+
'background-color': '#45475a',
|
|
778
|
+
'color': '#6c7086',
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
selector: 'edge',
|
|
783
|
+
style: {
|
|
784
|
+
'width': 1,
|
|
785
|
+
'line-color': '#585b70',
|
|
786
|
+
'target-arrow-color': '#585b70',
|
|
787
|
+
'target-arrow-shape': 'triangle',
|
|
788
|
+
'curve-style': 'taxi',
|
|
789
|
+
'taxi-direction': 'downward',
|
|
790
|
+
'taxi-turn': '20px',
|
|
791
|
+
'arrow-scale': 0.7,
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
selector: 'edge.meta-edge',
|
|
796
|
+
style: {
|
|
797
|
+
'width': 2,
|
|
798
|
+
'line-color': '#7f849c',
|
|
799
|
+
'target-arrow-color': '#7f849c',
|
|
800
|
+
'target-arrow-shape': 'triangle',
|
|
801
|
+
'curve-style': 'bezier',
|
|
802
|
+
'arrow-scale': 0.8,
|
|
803
|
+
'label': 'data(specifier)',
|
|
804
|
+
'font-size': '9px',
|
|
805
|
+
'color': '#6c7086',
|
|
806
|
+
'text-rotation': 'autorotate',
|
|
807
|
+
'text-margin-y': '-8px',
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
selector: 'edge.cycle-highlight',
|
|
812
|
+
style: {
|
|
813
|
+
'line-color': '#fab387',
|
|
814
|
+
'target-arrow-color': '#fab387',
|
|
815
|
+
'width': 3,
|
|
816
|
+
'z-index': 10,
|
|
817
|
+
'curve-style': 'bezier',
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
selector: 'node.cycle-highlight',
|
|
822
|
+
style: { 'border-color': '#fab387', 'border-width': 3 },
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
selector: 'node.hl-selected',
|
|
826
|
+
style: { 'border-color': '#cba6f7', 'border-width': 3 },
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
selector: 'node.hl-outgoing',
|
|
830
|
+
style: { 'border-color': '#89b4fa', 'border-width': 3 },
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
selector: 'edge.hl-outgoing',
|
|
834
|
+
style: { 'line-color': '#89b4fa', 'target-arrow-color': '#89b4fa', 'width': 2, 'z-index': 5 },
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
selector: 'node.hl-incoming',
|
|
838
|
+
style: { 'border-color': '#a6e3a1', 'border-width': 3 },
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
selector: 'edge.hl-incoming',
|
|
842
|
+
style: { 'line-color': '#a6e3a1', 'target-arrow-color': '#a6e3a1', 'width': 2, 'z-index': 5 },
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
selector: 'edge.hl-cycle',
|
|
846
|
+
style: { 'line-color': '#f9e2af', 'target-arrow-color': '#f9e2af', 'width': 2, 'z-index': 6 },
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
selector: 'node.dimmed',
|
|
850
|
+
style: { 'opacity': 0.2 },
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
selector: 'edge.dimmed',
|
|
854
|
+
style: { 'opacity': 0.08 },
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
selector: ':selected',
|
|
858
|
+
style: { 'border-color': '#cba6f7', 'border-width': 3 },
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
layout: { name: 'preset' },
|
|
862
|
+
minZoom: 0.05,
|
|
863
|
+
maxZoom: 5,
|
|
864
|
+
textureOnViewport: false,
|
|
865
|
+
pixelRatio: 'auto',
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// Collapse all groups then run initial layout
|
|
869
|
+
collapseAll(cy);
|
|
870
|
+
runLayout(cy);
|
|
871
|
+
|
|
872
|
+
// Double-click: expand/collapse groups, expand folders, zoom into modules
|
|
873
|
+
cy.on('dbltap', 'node', function (e) {
|
|
874
|
+
var node = e.target;
|
|
875
|
+
if (node.hasClass('group')) {
|
|
876
|
+
if (node.data('moduleCount') <= 1) return;
|
|
877
|
+
if (collapsedGroups.has(node.id())) {
|
|
878
|
+
expandGroup(cy, node);
|
|
879
|
+
} else {
|
|
880
|
+
collapseGroup(cy, node);
|
|
881
|
+
}
|
|
882
|
+
} else if (node.hasClass('folder')) {
|
|
883
|
+
expandFolder(cy, node.id());
|
|
884
|
+
} else if (node.hasClass('module')) {
|
|
885
|
+
cy.animate({ center: { eles: node }, zoom: 1.2, duration: 300 });
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// Tooltip handling
|
|
890
|
+
cy.on('mouseover', 'node.module', function (e) {
|
|
891
|
+
var d = e.target.data();
|
|
892
|
+
var timeStr = d.totalTime ? d.totalTime.toFixed(2) + ' ms' : '\u2014';
|
|
893
|
+
tooltip.innerHTML =
|
|
894
|
+
'<div class="tooltip-path">' + escapeHtml(d.fullPath) + '</div>' +
|
|
895
|
+
'<div class="tooltip-time">' + timeStr + '</div>';
|
|
896
|
+
tooltip.style.display = 'block';
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
cy.on('mouseover', 'node.folder', function (e) {
|
|
900
|
+
tooltip.innerHTML = '<div class="tooltip-path">' + escapeHtml(e.target.data('label')) + '</div>';
|
|
901
|
+
tooltip.style.display = 'block';
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
cy.on('mousemove', 'node.module, node.folder', function (e) {
|
|
905
|
+
var pos = e.renderedPosition || e.position;
|
|
906
|
+
tooltip.style.left = (pos.x + 16) + 'px';
|
|
907
|
+
tooltip.style.top = (pos.y + 16) + 'px';
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
cy.on('mouseout', 'node.module, node.folder', function () {
|
|
911
|
+
tooltip.style.display = 'none';
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// Save selection on mousedown (before Cytoscape auto-selects)
|
|
915
|
+
var preTapSelectedIds = {};
|
|
916
|
+
cy.on('tapstart', 'node', function () {
|
|
917
|
+
preTapSelectedIds = {};
|
|
918
|
+
cy.nodes(':selected').forEach(function (n) { preTapSelectedIds[n.id()] = true; });
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Single click selects. Shift/Ctrl/Cmd-click toggles.
|
|
922
|
+
// Deferred via setTimeout to run AFTER Cytoscape's own post-tap selection processing.
|
|
923
|
+
cy.on('tap', 'node', function (e) {
|
|
924
|
+
clearSearch();
|
|
925
|
+
var node = e.target;
|
|
926
|
+
var originalEvent = e.originalEvent;
|
|
927
|
+
var additive = originalEvent && (originalEvent.shiftKey || originalEvent.metaKey || originalEvent.ctrlKey);
|
|
928
|
+
var wasSelected = preTapSelectedIds[node.id()] === true;
|
|
929
|
+
var savedIds = preTapSelectedIds;
|
|
930
|
+
|
|
931
|
+
setTimeout(function () {
|
|
932
|
+
if (additive) {
|
|
933
|
+
// Restore pre-tap state, then toggle the clicked node
|
|
934
|
+
cy.nodes().unselect();
|
|
935
|
+
for (var id in savedIds) {
|
|
936
|
+
cy.getElementById(id).select();
|
|
937
|
+
}
|
|
938
|
+
if (wasSelected) { node.unselect(); } else { node.select(); }
|
|
939
|
+
} else {
|
|
940
|
+
cy.nodes().unselect();
|
|
941
|
+
node.select();
|
|
942
|
+
}
|
|
943
|
+
applySelectionHighlight(cy);
|
|
944
|
+
}, 0);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
cy.on('tap', function (e) {
|
|
948
|
+
if (e.target === cy) {
|
|
949
|
+
clearSearch();
|
|
950
|
+
cy.nodes().unselect();
|
|
951
|
+
clearSelectionHighlight(cy);
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
cy.on('dbltap', function (e) {
|
|
956
|
+
if (e.target === cy) {
|
|
957
|
+
clearSearch();
|
|
958
|
+
cy.nodes().unselect();
|
|
959
|
+
clearSelectionHighlight(cy);
|
|
960
|
+
cy.animate({ fit: { eles: cy.elements(), padding: 30 }, duration: 300 });
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Context menu (right-click)
|
|
965
|
+
var ctxMenu = document.createElement('div');
|
|
966
|
+
ctxMenu.className = 'graph-context-menu';
|
|
967
|
+
ctxMenu.style.display = 'none';
|
|
968
|
+
document.querySelector('.graph-container').appendChild(ctxMenu);
|
|
969
|
+
|
|
970
|
+
function hideContextMenu() {
|
|
971
|
+
ctxMenu.style.display = 'none';
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function getNodePath(node) {
|
|
975
|
+
var d = node.data();
|
|
976
|
+
if (d.fullPath) {
|
|
977
|
+
// Module node — strip file:// prefix
|
|
978
|
+
if (d.fullPath.startsWith('file://')) return d.fullPath.slice(7);
|
|
979
|
+
return d.fullPath;
|
|
980
|
+
}
|
|
981
|
+
if (d.groupId && d.isGroup) return d.groupId;
|
|
982
|
+
if (d.isFolder) {
|
|
983
|
+
// Folder ID is ftree::<groupId>::<relativePath> — return groupId + / + relative
|
|
984
|
+
var parts = (d.id || '').split('::');
|
|
985
|
+
if (parts.length >= 3) return parts[1] + '/' + parts.slice(2).join('::');
|
|
986
|
+
return d.id;
|
|
987
|
+
}
|
|
988
|
+
return d.id || '';
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
cy.on('cxttap', 'node', function (e) {
|
|
992
|
+
e.originalEvent.preventDefault();
|
|
993
|
+
var node = e.target;
|
|
994
|
+
var path = getNodePath(node);
|
|
995
|
+
var pos = e.renderedPosition || e.position;
|
|
996
|
+
|
|
997
|
+
ctxMenu.innerHTML = '';
|
|
998
|
+
|
|
999
|
+
function menuItem(label, tooltip, onclick) {
|
|
1000
|
+
var el = document.createElement('div');
|
|
1001
|
+
el.className = 'graph-context-menu-item';
|
|
1002
|
+
el.innerHTML = escapeHtml(label) + ' <span class="menu-info-icon" title="' + escapeAttr(tooltip) + '">\u24D8</span>';
|
|
1003
|
+
el.addEventListener('click', onclick);
|
|
1004
|
+
ctxMenu.appendChild(el);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function copyToClipboard(text) {
|
|
1008
|
+
var ta = document.createElement('textarea');
|
|
1009
|
+
ta.value = text;
|
|
1010
|
+
ta.style.position = 'fixed';
|
|
1011
|
+
ta.style.opacity = '0';
|
|
1012
|
+
document.body.appendChild(ta);
|
|
1013
|
+
ta.select();
|
|
1014
|
+
document.execCommand('copy');
|
|
1015
|
+
document.body.removeChild(ta);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
menuItem('Expand importer files', 'Reveal all modules that import this node', function () {
|
|
1019
|
+
hideContextMenu();
|
|
1020
|
+
expandImporters(cy, node);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
menuItem('Expand imported files', 'Reveal all modules imported by this node', function () {
|
|
1024
|
+
hideContextMenu();
|
|
1025
|
+
expandImported(cy, node);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
menuItem('Copy absolute path', 'Copy the filesystem path to clipboard', function () {
|
|
1029
|
+
copyToClipboard(path);
|
|
1030
|
+
hideContextMenu();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
menuItem('Copy import paths', 'Copy the full import chain from root to each file in this node', function () {
|
|
1034
|
+
var urls = getModuleURLsForNode(node);
|
|
1035
|
+
var paths = [];
|
|
1036
|
+
for (var i = 0; i < urls.length; i++) {
|
|
1037
|
+
paths.push(getImportPath(urls[i]));
|
|
1038
|
+
}
|
|
1039
|
+
copyToClipboard(paths.join('\n\n'));
|
|
1040
|
+
hideContextMenu();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
ctxMenu.style.left = pos.x + 'px';
|
|
1044
|
+
ctxMenu.style.top = pos.y + 'px';
|
|
1045
|
+
ctxMenu.style.display = 'block';
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
cy.on('tap', function () { hideContextMenu(); });
|
|
1049
|
+
document.addEventListener('click', function () { hideContextMenu(); });
|
|
1050
|
+
|
|
1051
|
+
function escapeHtml(str) {
|
|
1052
|
+
var div = document.createElement('div');
|
|
1053
|
+
div.textContent = str;
|
|
1054
|
+
return div.innerHTML;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function escapeAttr(str) {
|
|
1058
|
+
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return cy;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function expandImporters(cy, node) {
|
|
1065
|
+
// Collect all module URLs belonging to this node
|
|
1066
|
+
var targetURLs = new Set();
|
|
1067
|
+
if (node.hasClass('module')) {
|
|
1068
|
+
targetURLs.add(node.id());
|
|
1069
|
+
} else if (node.hasClass('group')) {
|
|
1070
|
+
// All modules in this group (including hidden ones)
|
|
1071
|
+
node.descendants('.module').forEach(function (m) { targetURLs.add(m.id()); });
|
|
1072
|
+
} else if (node.hasClass('folder')) {
|
|
1073
|
+
// Modules that are children of this folder (recursively via folder state)
|
|
1074
|
+
function collectFolderModules(fid) {
|
|
1075
|
+
var state = folderState[fid];
|
|
1076
|
+
if (!state) return;
|
|
1077
|
+
for (var i = 0; i < state.children.length; i++) {
|
|
1078
|
+
var child = state.children[i];
|
|
1079
|
+
if (child.type === 'file' && child.moduleURL) {
|
|
1080
|
+
targetURLs.add(child.moduleURL);
|
|
1081
|
+
} else if (child.type === 'folder') {
|
|
1082
|
+
collectFolderModules(child.id);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
collectFolderModules(node.id());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Find all importer module URLs (sources of edges targeting our modules)
|
|
1090
|
+
var importerURLs = new Set();
|
|
1091
|
+
cy.edges().forEach(function (edge) {
|
|
1092
|
+
if (targetURLs.has(edge.data('target'))) {
|
|
1093
|
+
var src = edge.data('source');
|
|
1094
|
+
if (!targetURLs.has(src)) {
|
|
1095
|
+
importerURLs.add(src);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
if (importerURLs.size === 0) return;
|
|
1101
|
+
|
|
1102
|
+
// Reveal each importer (minimal expansion)
|
|
1103
|
+
suppressAutoSelect = true;
|
|
1104
|
+
importerURLs.forEach(function (url) {
|
|
1105
|
+
revealModule(cy, url);
|
|
1106
|
+
});
|
|
1107
|
+
refreshEdgeVisibility(cy);
|
|
1108
|
+
suppressAutoSelect = false;
|
|
1109
|
+
|
|
1110
|
+
var nodeId = node.id();
|
|
1111
|
+
maybeRelayoutWithCallback(cy, function () {
|
|
1112
|
+
var n = cy.getElementById(nodeId);
|
|
1113
|
+
cy.nodes().unselect();
|
|
1114
|
+
n.select();
|
|
1115
|
+
applySelectionHighlight(cy);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function expandImported(cy, node) {
|
|
1120
|
+
// Collect all module URLs belonging to this node
|
|
1121
|
+
var sourceURLs = new Set();
|
|
1122
|
+
if (node.hasClass('module')) {
|
|
1123
|
+
sourceURLs.add(node.id());
|
|
1124
|
+
} else if (node.hasClass('group')) {
|
|
1125
|
+
node.descendants('.module').forEach(function (m) { sourceURLs.add(m.id()); });
|
|
1126
|
+
} else if (node.hasClass('folder')) {
|
|
1127
|
+
function collectFolderModules(fid) {
|
|
1128
|
+
var state = folderState[fid];
|
|
1129
|
+
if (!state) return;
|
|
1130
|
+
for (var i = 0; i < state.children.length; i++) {
|
|
1131
|
+
var child = state.children[i];
|
|
1132
|
+
if (child.type === 'file' && child.moduleURL) {
|
|
1133
|
+
sourceURLs.add(child.moduleURL);
|
|
1134
|
+
} else if (child.type === 'folder') {
|
|
1135
|
+
collectFolderModules(child.id);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
collectFolderModules(node.id());
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Find all imported module URLs (targets of edges from our modules)
|
|
1143
|
+
var importedURLs = new Set();
|
|
1144
|
+
cy.edges().forEach(function (edge) {
|
|
1145
|
+
if (sourceURLs.has(edge.data('source'))) {
|
|
1146
|
+
var tgt = edge.data('target');
|
|
1147
|
+
if (!sourceURLs.has(tgt)) {
|
|
1148
|
+
importedURLs.add(tgt);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
if (importedURLs.size === 0) return;
|
|
1154
|
+
|
|
1155
|
+
// Reveal each imported module (minimal expansion)
|
|
1156
|
+
suppressAutoSelect = true;
|
|
1157
|
+
importedURLs.forEach(function (url) {
|
|
1158
|
+
revealModule(cy, url);
|
|
1159
|
+
});
|
|
1160
|
+
refreshEdgeVisibility(cy);
|
|
1161
|
+
suppressAutoSelect = false;
|
|
1162
|
+
|
|
1163
|
+
var nodeId = node.id();
|
|
1164
|
+
maybeRelayoutWithCallback(cy, function () {
|
|
1165
|
+
var n = cy.getElementById(nodeId);
|
|
1166
|
+
cy.nodes().unselect();
|
|
1167
|
+
n.select();
|
|
1168
|
+
applySelectionHighlight(cy);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function highlightCycle(cy, cycle) {
|
|
1173
|
+
clearSearch();
|
|
1174
|
+
clearSelectionHighlight(cy);
|
|
1175
|
+
cy.elements().removeClass('cycle-highlight');
|
|
1176
|
+
cy.nodes().unselect();
|
|
1177
|
+
|
|
1178
|
+
suppressAutoSelect = true;
|
|
1179
|
+
// Collapse all, then reveal just enough for cycle members
|
|
1180
|
+
collapseAll(cy);
|
|
1181
|
+
for (var k = 0; k < cycle.modules.length; k++) {
|
|
1182
|
+
revealModule(cy, cycle.modules[k]);
|
|
1183
|
+
}
|
|
1184
|
+
refreshEdgeVisibility(cy);
|
|
1185
|
+
suppressAutoSelect = false;
|
|
1186
|
+
|
|
1187
|
+
maybeRelayoutWithCallback(cy, function () {
|
|
1188
|
+
// Apply cycle highlight after layout completes
|
|
1189
|
+
var nodes = [];
|
|
1190
|
+
for (var i = 0; i < cycle.modules.length; i++) {
|
|
1191
|
+
var node = cy.getElementById(cycle.modules[i]);
|
|
1192
|
+
if (node.length > 0) {
|
|
1193
|
+
node.addClass('cycle-highlight');
|
|
1194
|
+
nodes.push(node);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
var nextIdx = (i + 1) % cycle.modules.length;
|
|
1198
|
+
var edgeId = cycle.modules[i] + '->' + cycle.modules[nextIdx];
|
|
1199
|
+
var edge = cy.getElementById(edgeId);
|
|
1200
|
+
if (edge.length > 0) {
|
|
1201
|
+
edge.show();
|
|
1202
|
+
edge.addClass('cycle-highlight');
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (nodes.length > 0) {
|
|
1207
|
+
var collection = cy.collection();
|
|
1208
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1209
|
+
collection = collection.union(nodes[j]);
|
|
1210
|
+
}
|
|
1211
|
+
// Also include the parent groups so the zoom shows context
|
|
1212
|
+
var withParents = collection;
|
|
1213
|
+
collection.forEach(function (n) {
|
|
1214
|
+
if (n.parent().length > 0) {
|
|
1215
|
+
withParents = withParents.union(n.parent());
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
cy.animate({ fit: { eles: withParents, padding: 40 }, duration: 300 });
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function clearHighlights(cy) {
|
|
1224
|
+
cy.elements().removeClass('cycle-highlight');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function zoomToNode(cy, resolvedURL) {
|
|
1228
|
+
var node = cy.getElementById(resolvedURL);
|
|
1229
|
+
if (node.length > 0) {
|
|
1230
|
+
revealModule(cy, resolvedURL);
|
|
1231
|
+
cy.nodes().unselect();
|
|
1232
|
+
node.select();
|
|
1233
|
+
applySelectionHighlight(cy);
|
|
1234
|
+
cy.animate({ center: { eles: node }, zoom: 2, duration: 300 });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function focusOnNode(cy, resolvedURL) {
|
|
1239
|
+
clearSearch();
|
|
1240
|
+
var node = cy.getElementById(resolvedURL);
|
|
1241
|
+
if (node.length === 0) return;
|
|
1242
|
+
|
|
1243
|
+
// If the module node is already visible, just select + zoom
|
|
1244
|
+
if (node.visible()) {
|
|
1245
|
+
cy.nodes().unselect();
|
|
1246
|
+
node.select();
|
|
1247
|
+
applySelectionHighlight(cy);
|
|
1248
|
+
cy.animate({ center: { eles: node }, zoom: 2, duration: 300 });
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Otherwise: collapse all, reveal just enough, relayout, then select + zoom
|
|
1253
|
+
suppressAutoSelect = true;
|
|
1254
|
+
collapseAll(cy);
|
|
1255
|
+
revealModule(cy, resolvedURL);
|
|
1256
|
+
refreshEdgeVisibility(cy);
|
|
1257
|
+
suppressAutoSelect = false;
|
|
1258
|
+
maybeRelayoutWithCallback(cy, function () {
|
|
1259
|
+
var n = cy.getElementById(resolvedURL);
|
|
1260
|
+
cy.nodes().unselect();
|
|
1261
|
+
n.select();
|
|
1262
|
+
applySelectionHighlight(cy);
|
|
1263
|
+
cy.animate({ center: { eles: n }, zoom: 2, duration: 300 });
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Search: highlight matching nodes in the graph. If a match is inside a
|
|
1268
|
+
// collapsed group or folder, highlight that collapsed ancestor instead.
|
|
1269
|
+
function filterBySearch(cy, query) {
|
|
1270
|
+
if (!query) {
|
|
1271
|
+
cy.nodes().unselect();
|
|
1272
|
+
clearSelectionHighlight(cy);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
var lowerQuery = query.toLowerCase();
|
|
1277
|
+
var toSelect = new Set();
|
|
1278
|
+
|
|
1279
|
+
// Check every module node (visible or not)
|
|
1280
|
+
cy.nodes('.module').forEach(function (node) {
|
|
1281
|
+
var label = (node.data('label') || '').toLowerCase();
|
|
1282
|
+
var path = (node.data('fullPath') || '').toLowerCase();
|
|
1283
|
+
if (label.indexOf(lowerQuery) !== -1 || path.indexOf(lowerQuery) !== -1) {
|
|
1284
|
+
// Resolve to the nearest visible ancestor
|
|
1285
|
+
toSelect.add(resolveVisibleNode(cy, node.id()));
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// Also check folder nodes by label
|
|
1290
|
+
cy.nodes('.folder').forEach(function (node) {
|
|
1291
|
+
var label = (node.data('label') || '').toLowerCase();
|
|
1292
|
+
if (label.indexOf(lowerQuery) !== -1) {
|
|
1293
|
+
if (node.visible()) {
|
|
1294
|
+
toSelect.add(node.id());
|
|
1295
|
+
} else {
|
|
1296
|
+
toSelect.add(resolveVisibleNode(cy, node.id()));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Select all matching visible nodes
|
|
1302
|
+
cy.nodes().unselect();
|
|
1303
|
+
toSelect.forEach(function (nodeId) {
|
|
1304
|
+
var node = cy.getElementById(nodeId);
|
|
1305
|
+
if (node.length > 0 && node.visible()) node.select();
|
|
1306
|
+
});
|
|
1307
|
+
applySelectionHighlight(cy);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
|