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,209 @@
|
|
|
1
|
+
/* global document */
|
|
2
|
+
|
|
3
|
+
function initTable(data, cy) {
|
|
4
|
+
var tableBody = document.getElementById('table-body');
|
|
5
|
+
var countInput = document.getElementById('slowest-count');
|
|
6
|
+
|
|
7
|
+
// Build URL -> ModuleNode index from the full tree
|
|
8
|
+
var nodeByURL = {};
|
|
9
|
+
function indexTree(nodes) {
|
|
10
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
11
|
+
nodeByURL[nodes[i].resolvedURL] = nodes[i];
|
|
12
|
+
indexTree(nodes[i].children);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
indexTree(data.tree);
|
|
16
|
+
|
|
17
|
+
// Build timing lookup (first occurrence per URL)
|
|
18
|
+
var timingByURL = {};
|
|
19
|
+
for (var i = 0; i < data.modules.length; i++) {
|
|
20
|
+
var m = data.modules[i];
|
|
21
|
+
if (!timingByURL[m.resolvedURL]) {
|
|
22
|
+
timingByURL[m.resolvedURL] = {
|
|
23
|
+
totalImportTime: m.totalImportTime || 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Collect all unique modules that have tree nodes
|
|
29
|
+
var allModules = [];
|
|
30
|
+
var seen = {};
|
|
31
|
+
for (var i = 0; i < data.modules.length; i++) {
|
|
32
|
+
var url = data.modules[i].resolvedURL;
|
|
33
|
+
if (!seen[url] && nodeByURL[url]) {
|
|
34
|
+
seen[url] = true;
|
|
35
|
+
allModules.push(nodeByURL[url]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getMetric(node, column) {
|
|
40
|
+
var t = timingByURL[node.resolvedURL];
|
|
41
|
+
if (column === 'time') return t ? t.totalImportTime : node.totalTime;
|
|
42
|
+
if (column === 'imports') return countAllChildren(node);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildRoots(count, sortColumn) {
|
|
47
|
+
var sorted = allModules.slice().sort(function (a, b) {
|
|
48
|
+
return getMetric(b, sortColumn) - getMetric(a, sortColumn);
|
|
49
|
+
});
|
|
50
|
+
return sorted.slice(0, count);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var currentSort = { column: 'time', direction: 'desc' };
|
|
54
|
+
|
|
55
|
+
function getDisplayPath(url) {
|
|
56
|
+
if (url.startsWith('file://')) return url.slice(7);
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function countAllChildren(node) {
|
|
61
|
+
var count = node.children.length;
|
|
62
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
63
|
+
count += countAllChildren(node.children[i]);
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderRow(node, depth, parentElement) {
|
|
69
|
+
var row = document.createElement('div');
|
|
70
|
+
row.className = 'table-row';
|
|
71
|
+
row.dataset.url = node.resolvedURL;
|
|
72
|
+
row.dataset.depth = depth;
|
|
73
|
+
|
|
74
|
+
var hasChildren = node.children.length > 0;
|
|
75
|
+
var indent = depth * 20;
|
|
76
|
+
var childCount = countAllChildren(node);
|
|
77
|
+
var t = timingByURL[node.resolvedURL] || { totalImportTime: node.totalTime };
|
|
78
|
+
var timeDisplay = t.totalImportTime ? t.totalImportTime.toFixed(2) + ' ms' : '\u2014';
|
|
79
|
+
|
|
80
|
+
row.innerHTML =
|
|
81
|
+
'<div class="module-name" style="padding-left: ' + indent + 'px">' +
|
|
82
|
+
'<span class="chevron' + (hasChildren ? '' : ' empty') + '">' + (hasChildren ? '\u25B6' : '') + '</span>' +
|
|
83
|
+
'<span class="module-label" title="' + escapeAttr(node.resolvedURL) + '">' + escapeHtml(depth === 0 ? getDisplayPath(node.resolvedURL) : node.specifier) + '</span>' +
|
|
84
|
+
'</div>' +
|
|
85
|
+
'<div class="time-value">' + timeDisplay + '</div>' +
|
|
86
|
+
'<div class="imports-count">' + childCount + '</div>';
|
|
87
|
+
|
|
88
|
+
parentElement.appendChild(row);
|
|
89
|
+
|
|
90
|
+
// Click to zoom in graph
|
|
91
|
+
row.addEventListener('click', function (e) {
|
|
92
|
+
if (e.target.classList.contains('chevron')) return;
|
|
93
|
+
if (cy) {
|
|
94
|
+
focusOnNode(cy, node.resolvedURL);
|
|
95
|
+
}
|
|
96
|
+
// Highlight this row
|
|
97
|
+
var rows = document.querySelectorAll('.table-row');
|
|
98
|
+
for (var j = 0; j < rows.length; j++) {
|
|
99
|
+
rows[j].classList.remove('highlighted');
|
|
100
|
+
}
|
|
101
|
+
row.classList.add('highlighted');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Expand/collapse children
|
|
105
|
+
if (hasChildren) {
|
|
106
|
+
var expanded = false;
|
|
107
|
+
var childContainer = document.createElement('div');
|
|
108
|
+
childContainer.className = 'child-container';
|
|
109
|
+
childContainer.style.display = 'none';
|
|
110
|
+
parentElement.appendChild(childContainer);
|
|
111
|
+
|
|
112
|
+
var chevron = row.querySelector('.chevron');
|
|
113
|
+
chevron.addEventListener('click', function () {
|
|
114
|
+
expanded = !expanded;
|
|
115
|
+
chevron.classList.toggle('expanded', expanded);
|
|
116
|
+
childContainer.style.display = expanded ? 'block' : 'none';
|
|
117
|
+
|
|
118
|
+
if (expanded && childContainer.children.length === 0) {
|
|
119
|
+
var sortedChildren = sortNodes(node.children, currentSort);
|
|
120
|
+
for (var k = 0; k < sortedChildren.length; k++) {
|
|
121
|
+
renderRow(sortedChildren[k], depth + 1, childContainer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sortNodes(nodes, sort) {
|
|
129
|
+
var sorted = nodes.slice();
|
|
130
|
+
sorted.sort(function (a, b) {
|
|
131
|
+
var cmp = 0;
|
|
132
|
+
if (sort.column === 'name') {
|
|
133
|
+
cmp = a.specifier.localeCompare(b.specifier);
|
|
134
|
+
} else {
|
|
135
|
+
cmp = getMetric(a, sort.column) - getMetric(b, sort.column);
|
|
136
|
+
}
|
|
137
|
+
return sort.direction === 'desc' ? -cmp : cmp;
|
|
138
|
+
});
|
|
139
|
+
return sorted;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderTable() {
|
|
143
|
+
tableBody.innerHTML = '';
|
|
144
|
+
var count = parseInt(countInput.value, 10) || 20;
|
|
145
|
+
var roots = sortNodes(buildRoots(count, currentSort.column), currentSort);
|
|
146
|
+
for (var i = 0; i < roots.length; i++) {
|
|
147
|
+
renderRow(roots[i], 0, tableBody);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Count input change
|
|
152
|
+
countInput.addEventListener('input', function () {
|
|
153
|
+
renderTable();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Column sorting
|
|
157
|
+
document.querySelectorAll('.table-header-row span[data-sort]').forEach(function (header) {
|
|
158
|
+
header.addEventListener('click', function () {
|
|
159
|
+
var col = header.dataset.sort;
|
|
160
|
+
if (currentSort.column === col) {
|
|
161
|
+
currentSort.direction = currentSort.direction === 'desc' ? 'asc' : 'desc';
|
|
162
|
+
} else {
|
|
163
|
+
currentSort.column = col;
|
|
164
|
+
currentSort.direction = 'desc';
|
|
165
|
+
}
|
|
166
|
+
// Update sort arrows
|
|
167
|
+
document.querySelectorAll('.sort-arrow').forEach(function (arrow) {
|
|
168
|
+
arrow.textContent = '';
|
|
169
|
+
});
|
|
170
|
+
var arrow = header.querySelector('.sort-arrow');
|
|
171
|
+
if (arrow) {
|
|
172
|
+
arrow.textContent = currentSort.direction === 'desc' ? ' \u25BC' : ' \u25B2';
|
|
173
|
+
}
|
|
174
|
+
renderTable();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
renderTable();
|
|
179
|
+
|
|
180
|
+
function escapeHtml(str) {
|
|
181
|
+
var div = document.createElement('div');
|
|
182
|
+
div.textContent = str;
|
|
183
|
+
return div.innerHTML;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function escapeAttr(str) {
|
|
187
|
+
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
filter: function (query) {
|
|
192
|
+
// Only filter top-level rows (depth 0), not expanded children
|
|
193
|
+
var rows = tableBody.querySelectorAll('.table-row[data-depth="0"]');
|
|
194
|
+
var lowerQuery = query.toLowerCase();
|
|
195
|
+
for (var i = 0; i < rows.length; i++) {
|
|
196
|
+
var url = (rows[i].dataset.url || '').toLowerCase();
|
|
197
|
+
var text = rows[i].textContent.toLowerCase();
|
|
198
|
+
var match = !query || url.indexOf(lowerQuery) !== -1 || text.indexOf(lowerQuery) !== -1;
|
|
199
|
+
// Hide/show the row and its sibling child-container
|
|
200
|
+
rows[i].style.display = match ? '' : 'none';
|
|
201
|
+
var next = rows[i].nextElementSibling;
|
|
202
|
+
if (next && next.classList.contains('child-container')) {
|
|
203
|
+
next.style.display = match ? next.style.display : 'none';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
rerender: renderTable,
|
|
208
|
+
};
|
|
209
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface ImportRecord {
|
|
2
|
+
specifier: string;
|
|
3
|
+
resolvedURL: string;
|
|
4
|
+
parentURL: string | null;
|
|
5
|
+
importStartTime: number;
|
|
6
|
+
totalImportTime?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ModuleNode {
|
|
9
|
+
resolvedURL: string;
|
|
10
|
+
specifier: string;
|
|
11
|
+
totalTime: number;
|
|
12
|
+
children: ModuleNode[];
|
|
13
|
+
parentURL: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface Cycle {
|
|
16
|
+
modules: string[];
|
|
17
|
+
length: number;
|
|
18
|
+
}
|
|
19
|
+
export interface FolderTreeNode {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
type: 'folder' | 'file';
|
|
23
|
+
moduleURL?: string;
|
|
24
|
+
children: FolderTreeNode[];
|
|
25
|
+
}
|
|
26
|
+
export interface Group {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
packageJsonPath: string;
|
|
30
|
+
modules: string[];
|
|
31
|
+
isNodeModules: boolean;
|
|
32
|
+
folderTree?: FolderTreeNode[];
|
|
33
|
+
}
|
|
34
|
+
export interface ReportData {
|
|
35
|
+
metadata: {
|
|
36
|
+
command: string;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
nodeVersion: string;
|
|
39
|
+
totalModules: number;
|
|
40
|
+
totalTime: number;
|
|
41
|
+
};
|
|
42
|
+
modules: ImportRecord[];
|
|
43
|
+
tree: ModuleNode[];
|
|
44
|
+
groups: Group[];
|
|
45
|
+
cycles: Cycle[];
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "esm-imports-analyzer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tool to generate an HTML visualization of the imports of an ESM Node.js application. It executes the application, tracks the imports during runtime, and creates the visualization after the execution is complete.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node",
|
|
7
|
+
"esm",
|
|
8
|
+
"imports",
|
|
9
|
+
"analyzer",
|
|
10
|
+
"optimization"
|
|
11
|
+
],
|
|
12
|
+
"author": "Patricio Palladino <email@patriciopalladino.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"esm-imports-analyzer": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"types": "dist/cli.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.5.0",
|
|
24
|
+
"typescript": "^6.0.2"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"build": "rm -rf dist && tsc && cp -r src/report/ui dist/report/ui && cp src/report/template.html dist/report/template.html",
|
|
29
|
+
"test": "node --test test/**/*.test.ts",
|
|
30
|
+
"test:unit": "node --test test/unit/**/*.test.ts",
|
|
31
|
+
"test:integration": "node --test test/integration/**/*.test.ts"
|
|
32
|
+
}
|
|
33
|
+
}
|