react-native-bundle-discovery-ui 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +12 -255
- package/assets/global.css +1606 -105
- package/dist/bin.js +63 -20
- package/dist/client/fonts.js +45 -0
- package/dist/client/issues.js +5 -0
- package/dist/client/pages/_common.js +279 -98
- package/dist/client/pages/_module.js +347 -0
- package/dist/client/pages/default.js +144 -63
- package/dist/client/pages/module.js +2 -309
- package/dist/client/pages/moduleDiff.js +76 -0
- package/dist/client/pages/package.js +172 -154
- package/dist/client/prepare.js +15 -0
- package/dist/client/queryHelpers.js +487 -91
- package/dist/client/sizeHeat.js +7 -0
- package/dist/client/views/{_theme.js → _colorScheme.js} +2 -9
- package/dist/client/views/_html.js +52 -0
- package/dist/client/views/codeDiff.js +66 -0
- package/dist/client/views/compare.js +237 -0
- package/dist/client/views/importGraph.js +255 -0
- package/dist/client/views/insights.js +81 -0
- package/dist/client/views/moduleOverview.js +122 -0
- package/dist/client/views/packageOverview.js +116 -0
- package/dist/client/views/persistedFilterInput.js +27 -0
- package/dist/client/views/reportBar.js +89 -0
- package/dist/client/views/treemap.js +598 -0
- package/dist/discoveryrc.js +27 -7
- package/dist/rsdoctor.js +19 -99
- package/dist/src-DV3VzcQY.js +1548 -0
- package/package.json +9 -5
- package/assets/highcharts.css +0 -1
- package/dist/client/_virtual/_rolldown/runtime.js +0 -37
- package/dist/client/views/foamtree.js +0 -103
- package/dist/client/views/highcharts.js +0 -33
- package/dist/src-DPAqywOJ.js +0 -201
- package/vendors/foamtree.js +0 -9008
- package/vendors/high-contrast-dark.js +0 -1
- package/vendors/highcharts-exporting.js +0 -13
- package/vendors/highcharts-networkgraph.js +0 -1
- package/vendors/highcharts.js +0 -10
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/client/views/_html.ts
|
|
3
|
+
function escapeHTML(str) {
|
|
4
|
+
return String(str ?? "").replace(
|
|
5
|
+
/[&<>"']/g,
|
|
6
|
+
(c) =>
|
|
7
|
+
({
|
|
8
|
+
"&": "&",
|
|
9
|
+
"<": "<",
|
|
10
|
+
">": ">",
|
|
11
|
+
'"': """,
|
|
12
|
+
"'": "'",
|
|
13
|
+
})[c],
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
function formatBytes(bytes) {
|
|
17
|
+
if (!bytes) return "0 B";
|
|
18
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
19
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), 3);
|
|
20
|
+
return `${Number.parseFloat((bytes / 1024 ** i).toFixed(i ? 1 : 0))} ${units[i]}`;
|
|
21
|
+
}
|
|
22
|
+
function formatPercent(value) {
|
|
23
|
+
const pct = value * 100;
|
|
24
|
+
if (pct === 0) return "0%";
|
|
25
|
+
if (pct < 0.01) return "<0.01%";
|
|
26
|
+
return `${pct < 10 ? pct.toFixed(2) : pct.toFixed(1)}%`;
|
|
27
|
+
}
|
|
28
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
29
|
+
return `${count.toLocaleString()} ${count === 1 ? singular : pluralForm}`;
|
|
30
|
+
}
|
|
31
|
+
function card({ label, value, sub, kind = "", bar }) {
|
|
32
|
+
return `
|
|
33
|
+
<div class="mo-card${kind ? ` mo-card-${kind}` : ""}">
|
|
34
|
+
<div class="mo-card-label">${label}</div>
|
|
35
|
+
<div class="mo-card-value">${value}</div>
|
|
36
|
+
${sub ? `<div class="mo-card-sub">${sub}</div>` : ""}
|
|
37
|
+
${bar === void 0 ? "" : `<div class="mo-bar"><div style="width:${Math.max(bar * 100, 1).toFixed(2)}%"></div></div>`}
|
|
38
|
+
</div>`;
|
|
39
|
+
}
|
|
40
|
+
function chip(html, kind = "", href) {
|
|
41
|
+
const cls = `mo-chip${kind ? ` mo-chip-${kind}` : ""}`;
|
|
42
|
+
return href
|
|
43
|
+
? `<a class="${cls}" href="${escapeHTML(href)}">${html}</a>`
|
|
44
|
+
: `<span class="${cls}">${html}</span>`;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
exports.card = card;
|
|
48
|
+
exports.chip = chip;
|
|
49
|
+
exports.escapeHTML = escapeHTML;
|
|
50
|
+
exports.formatBytes = formatBytes;
|
|
51
|
+
exports.formatPercent = formatPercent;
|
|
52
|
+
exports.plural = plural;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const require_views__html = require("./_html.js");
|
|
2
|
+
let diff = require("diff");
|
|
3
|
+
//#region src/client/views/codeDiff.ts
|
|
4
|
+
const CONTEXT = 3;
|
|
5
|
+
function toLines(before, after) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
let oldNo = 1;
|
|
8
|
+
let newNo = 1;
|
|
9
|
+
(0, diff.diffLines)(before, after).forEach((part) => {
|
|
10
|
+
const kind = part.added ? "add" : part.removed ? "del" : "same";
|
|
11
|
+
part.value
|
|
12
|
+
.replace(/\n$/, "")
|
|
13
|
+
.split("\n")
|
|
14
|
+
.forEach((text) => {
|
|
15
|
+
lines.push({
|
|
16
|
+
kind,
|
|
17
|
+
text,
|
|
18
|
+
oldNo: kind === "add" ? null : oldNo++,
|
|
19
|
+
newNo: kind === "del" ? null : newNo++,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
function lineHTML({ kind, text, oldNo, newNo }) {
|
|
26
|
+
return `<div class="cd-line cd-${kind}"><span class="cd-no">${oldNo ?? ""}</span><span class="cd-no">${newNo ?? ""}</span><span class="cd-sign">${kind === "add" ? "+" : kind === "del" ? "−" : " "}</span><span class="cd-text">${require_views__html.escapeHTML(text)}</span></div>`;
|
|
27
|
+
}
|
|
28
|
+
function render(lines) {
|
|
29
|
+
const added = lines.filter((line) => line.kind === "add").length;
|
|
30
|
+
const removed = lines.filter((line) => line.kind === "del").length;
|
|
31
|
+
if (added + removed === 0)
|
|
32
|
+
return `<div class="cd-empty">No differences</div>`;
|
|
33
|
+
const visible = lines.map((_, i) =>
|
|
34
|
+
lines
|
|
35
|
+
.slice(Math.max(0, i - CONTEXT), i + CONTEXT + 1)
|
|
36
|
+
.some((line) => line.kind !== "same"),
|
|
37
|
+
);
|
|
38
|
+
let html = "";
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < lines.length) {
|
|
41
|
+
if (visible[i]) {
|
|
42
|
+
html += lineHTML(lines[i]);
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const start = i;
|
|
47
|
+
while (i < lines.length && !visible[i]) i++;
|
|
48
|
+
const folded = lines.slice(start, i);
|
|
49
|
+
html += `<button type="button" class="cd-fold">⋯ ${folded.length} unchanged ${folded.length === 1 ? "line" : "lines"}</button><div class="cd-folded" hidden>${folded.map(lineHTML).join("")}</div>`;
|
|
50
|
+
}
|
|
51
|
+
return `
|
|
52
|
+
<div class="cd-stats"><span class="cd-stat-add">+${added}</span> <span class="cd-stat-del">−${removed}</span> lines</div>
|
|
53
|
+
<div class="cd-code">${html}</div>`;
|
|
54
|
+
}
|
|
55
|
+
discovery.view.define("code-diff", async (el, _config, data) => {
|
|
56
|
+
const { before, after } = data;
|
|
57
|
+
const root = el;
|
|
58
|
+
root.innerHTML = render(toLines(await before, await after));
|
|
59
|
+
root.addEventListener("click", (event) => {
|
|
60
|
+
const button = event.target.closest(".cd-fold");
|
|
61
|
+
if (!button) return;
|
|
62
|
+
button.nextElementSibling?.removeAttribute("hidden");
|
|
63
|
+
button.remove();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//#endregion
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const require_views__html = require("./_html.js");
|
|
2
|
+
//#region src/client/views/compare.ts
|
|
3
|
+
const ROWS_LIMIT = 10;
|
|
4
|
+
function formatDelta(bytes) {
|
|
5
|
+
if (bytes === 0) return "0 B";
|
|
6
|
+
return `${bytes > 0 ? "+" : "−"}${require_views__html.formatBytes(Math.abs(bytes))}`;
|
|
7
|
+
}
|
|
8
|
+
function deltaHTML(bytes) {
|
|
9
|
+
return `<span class="${bytes > 0 ? "mo-bad" : bytes < 0 ? "mo-good" : ""}">${formatDelta(bytes)}</span>`;
|
|
10
|
+
}
|
|
11
|
+
function change(delta, amount, before, from) {
|
|
12
|
+
const percent = before
|
|
13
|
+
? ` · ${((Math.abs(delta) / before) * 100).toFixed(2)}%`
|
|
14
|
+
: "";
|
|
15
|
+
return `<span class="cmp-change">${delta === 0 ? `<span class="cmp-pill">= no change</span>` : `<span class="cmp-pill cmp-pill-${delta > 0 ? "up" : "down"}">${delta > 0 ? "▲" : "▼"} ${amount}${percent}</span>`}<span class="cmp-from">from ${from}</span></span>`;
|
|
16
|
+
}
|
|
17
|
+
function countChange(before, after) {
|
|
18
|
+
const delta = after - before;
|
|
19
|
+
return change(
|
|
20
|
+
delta,
|
|
21
|
+
Math.abs(delta).toLocaleString(),
|
|
22
|
+
before,
|
|
23
|
+
before.toLocaleString(),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function row({ name, href, badge = "", detail = "", delta }) {
|
|
27
|
+
const tag = href ? "a" : "div";
|
|
28
|
+
return `
|
|
29
|
+
<${tag} class="cmp-row"${href ? ` href="${require_views__html.escapeHTML(href)}"` : ""}>
|
|
30
|
+
<span class="cmp-row-name" title="${require_views__html.escapeHTML(name)}">${require_views__html.escapeHTML(name)}${badge}</span>
|
|
31
|
+
<span class="cmp-row-detail">${detail}</span>
|
|
32
|
+
<span class="cmp-row-delta">${deltaHTML(delta)}</span>
|
|
33
|
+
</${tag}>`;
|
|
34
|
+
}
|
|
35
|
+
function section(title, total, rows) {
|
|
36
|
+
if (rows.length === 0) return "";
|
|
37
|
+
const visible = rows.slice(0, ROWS_LIMIT).join("");
|
|
38
|
+
const hidden = rows.slice(ROWS_LIMIT).join("");
|
|
39
|
+
return `
|
|
40
|
+
<div class="in-top cmp-section">
|
|
41
|
+
<div class="mo-card-label">${title} · ${total}</div>
|
|
42
|
+
${visible}
|
|
43
|
+
${
|
|
44
|
+
hidden
|
|
45
|
+
? `<div class="cmp-more" hidden>${hidden}</div>
|
|
46
|
+
<button type="button" class="cmp-more-btn">Show ${rows.length - ROWS_LIMIT} more</button>`
|
|
47
|
+
: ""
|
|
48
|
+
}
|
|
49
|
+
</div>`;
|
|
50
|
+
}
|
|
51
|
+
function sumDelta(items) {
|
|
52
|
+
return items.reduce((sum, item) => sum + item.deltaInBytes, 0);
|
|
53
|
+
}
|
|
54
|
+
function renderCards({ summary, packages }) {
|
|
55
|
+
const { before, after, deltaInBytes } = summary;
|
|
56
|
+
return `<div class="mo-cards">${[
|
|
57
|
+
require_views__html.card({
|
|
58
|
+
label: "Bundle size",
|
|
59
|
+
value: require_views__html.formatBytes(after.sizeInBytes),
|
|
60
|
+
sub: change(
|
|
61
|
+
deltaInBytes,
|
|
62
|
+
require_views__html.formatBytes(Math.abs(deltaInBytes)),
|
|
63
|
+
before.sizeInBytes,
|
|
64
|
+
require_views__html.formatBytes(before.sizeInBytes),
|
|
65
|
+
),
|
|
66
|
+
kind: deltaInBytes > 0 ? "danger" : deltaInBytes < 0 ? "good" : "accent",
|
|
67
|
+
}),
|
|
68
|
+
require_views__html.card({
|
|
69
|
+
label: "Modules",
|
|
70
|
+
value: after.modules.toLocaleString(),
|
|
71
|
+
sub: countChange(before.modules, after.modules),
|
|
72
|
+
}),
|
|
73
|
+
require_views__html.card({
|
|
74
|
+
label: "Packages",
|
|
75
|
+
value: after.packages.toLocaleString(),
|
|
76
|
+
sub: countChange(before.packages, after.packages),
|
|
77
|
+
}),
|
|
78
|
+
require_views__html.card({
|
|
79
|
+
label: "New duplicates",
|
|
80
|
+
value: require_views__html.plural(
|
|
81
|
+
packages.newDuplicates.length,
|
|
82
|
+
"package",
|
|
83
|
+
),
|
|
84
|
+
sub: "packages bundled more times than before",
|
|
85
|
+
kind: packages.newDuplicates.length ? "danger" : "",
|
|
86
|
+
}),
|
|
87
|
+
require_views__html.card({
|
|
88
|
+
label: "New deprecated",
|
|
89
|
+
value: require_views__html.plural(
|
|
90
|
+
packages.deprecated.added.length,
|
|
91
|
+
"package",
|
|
92
|
+
),
|
|
93
|
+
sub: countChange(
|
|
94
|
+
packages.deprecated.beforeCount,
|
|
95
|
+
packages.deprecated.afterCount,
|
|
96
|
+
),
|
|
97
|
+
kind: packages.deprecated.added.length ? "danger" : "",
|
|
98
|
+
}),
|
|
99
|
+
].join("")}</div>`;
|
|
100
|
+
}
|
|
101
|
+
function renderWarnings({ transformOptionsDiff, summary }) {
|
|
102
|
+
const items = transformOptionsDiff.map(
|
|
103
|
+
({ option, before, after }) =>
|
|
104
|
+
`<b>${require_views__html.escapeHTML(option)}</b>: ${require_views__html.escapeHTML(before)} → ${require_views__html.escapeHTML(after)}`,
|
|
105
|
+
);
|
|
106
|
+
if (summary.before.platform !== summary.after.platform)
|
|
107
|
+
items.unshift(
|
|
108
|
+
`<b>platform</b>: ${require_views__html.escapeHTML(summary.before.platform)} → ${require_views__html.escapeHTML(summary.after.platform)}`,
|
|
109
|
+
);
|
|
110
|
+
if (items.length === 0) return "";
|
|
111
|
+
return `
|
|
112
|
+
<div class="cmp-warning">
|
|
113
|
+
The reports were built with different options, so sizes are not directly comparable: ${items.join(", ")}
|
|
114
|
+
</div>`;
|
|
115
|
+
}
|
|
116
|
+
function render({ comparison: c, modulePaths }) {
|
|
117
|
+
const paths = new Set(modulePaths);
|
|
118
|
+
const moduleHref = (path) =>
|
|
119
|
+
paths.has(path) ? discovery.encodePageHash("module", path) : null;
|
|
120
|
+
const packageHref = (name) => discovery.encodePageHash("package", name);
|
|
121
|
+
const versions = (list) => require_views__html.escapeHTML(list.join(", "));
|
|
122
|
+
const contributors = c.biggestContributors.map((item) =>
|
|
123
|
+
row({
|
|
124
|
+
name: item.name,
|
|
125
|
+
detail: item.note ? require_views__html.escapeHTML(item.note) : "",
|
|
126
|
+
delta: item.deltaInBytes,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
const versionChanges = c.packages.versionChanges.map((pkg) =>
|
|
130
|
+
row({
|
|
131
|
+
name: pkg.name,
|
|
132
|
+
href: packageHref(pkg.name),
|
|
133
|
+
detail: `${versions(pkg.beforeVersions)} → ${versions(pkg.afterVersions)}`,
|
|
134
|
+
delta: pkg.deltaInBytes,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
const addedPackages = c.packages.added.map((pkg) =>
|
|
138
|
+
row({
|
|
139
|
+
name: pkg.name,
|
|
140
|
+
href: packageHref(pkg.name),
|
|
141
|
+
detail: versions(pkg.versions),
|
|
142
|
+
delta: pkg.sizeInBytes,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
const removedPackages = c.packages.removed.map((pkg) =>
|
|
146
|
+
row({
|
|
147
|
+
name: pkg.name,
|
|
148
|
+
detail: versions(pkg.versions),
|
|
149
|
+
delta: -pkg.sizeInBytes,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
const newDuplicates = c.packages.newDuplicates.map((dup) =>
|
|
153
|
+
row({
|
|
154
|
+
name: dup.name,
|
|
155
|
+
href: packageHref(dup.name),
|
|
156
|
+
badge: ` ${require_views__html.chip(`×${dup.count}`, "danger")}`,
|
|
157
|
+
detail: require_views__html.escapeHTML(
|
|
158
|
+
dup.entries.map((entry) => entry.version).join(", "),
|
|
159
|
+
),
|
|
160
|
+
delta: dup.sizeInBytes,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
const newDeprecated = c.packages.deprecated.added.map((pkg) => {
|
|
164
|
+
const name = pkg.id.slice(0, pkg.id.lastIndexOf("@"));
|
|
165
|
+
return row({
|
|
166
|
+
name: pkg.id,
|
|
167
|
+
href: packageHref(name),
|
|
168
|
+
badge: ` ${require_views__html.chip("deprecated", "deprecated")}`,
|
|
169
|
+
detail: require_views__html.escapeHTML(pkg.reason ?? ""),
|
|
170
|
+
delta: 0,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
const addedModules = c.modules.added.map((m) =>
|
|
174
|
+
row({
|
|
175
|
+
name: m.path,
|
|
176
|
+
href: moduleHref(m.path),
|
|
177
|
+
delta: m.deltaInBytes,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
const removedModules = c.modules.removed.map((m) =>
|
|
181
|
+
row({
|
|
182
|
+
name: m.path,
|
|
183
|
+
delta: m.deltaInBytes,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
const changedModules = c.modules.changed.map((m) =>
|
|
187
|
+
row({
|
|
188
|
+
name: m.path,
|
|
189
|
+
href: paths.has(m.path)
|
|
190
|
+
? discovery.encodePageHash("module-diff", m.path)
|
|
191
|
+
: null,
|
|
192
|
+
detail: `${require_views__html.formatBytes(m.beforeSizeInBytes)} → ${require_views__html.formatBytes(m.afterSizeInBytes)}`,
|
|
193
|
+
delta: m.deltaInBytes,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
const hasChanges =
|
|
197
|
+
c.summary.deltaInBytes !== 0 ||
|
|
198
|
+
c.modules.added.length +
|
|
199
|
+
c.modules.removed.length +
|
|
200
|
+
c.modules.changed.length >
|
|
201
|
+
0 ||
|
|
202
|
+
c.packages.versionChanges.length +
|
|
203
|
+
c.packages.added.length +
|
|
204
|
+
c.packages.removed.length >
|
|
205
|
+
0;
|
|
206
|
+
return `
|
|
207
|
+
<div class="cmp-title">
|
|
208
|
+
Compared with ${require_views__html.chip(require_views__html.escapeHTML(c.beforeFile))}
|
|
209
|
+
</div>
|
|
210
|
+
${renderWarnings(c)}
|
|
211
|
+
${renderCards(c)}
|
|
212
|
+
${hasChanges ? "" : `<div class="dup-empty cmp-empty">No differences between the reports.</div>`}
|
|
213
|
+
<div class="cmp-sections">
|
|
214
|
+
${section("Biggest growth", "by package / folder", contributors)}
|
|
215
|
+
${section("Package version changes", require_views__html.plural(versionChanges.length, "package"), versionChanges)}
|
|
216
|
+
${section("New duplicates", require_views__html.plural(newDuplicates.length, "package"), newDuplicates)}
|
|
217
|
+
${section("New deprecated packages", require_views__html.plural(newDeprecated.length, "package"), newDeprecated)}
|
|
218
|
+
${section("Added packages", `${require_views__html.plural(addedPackages.length, "package")} · ${formatDelta(c.packages.added.reduce((sum, pkg) => sum + pkg.sizeInBytes, 0))}`, addedPackages)}
|
|
219
|
+
${section("Removed packages", `${require_views__html.plural(removedPackages.length, "package")} · ${formatDelta(-c.packages.removed.reduce((sum, pkg) => sum + pkg.sizeInBytes, 0))}`, removedPackages)}
|
|
220
|
+
${section("Added modules", `${require_views__html.plural(addedModules.length, "module")} · ${formatDelta(sumDelta(c.modules.added))}`, addedModules)}
|
|
221
|
+
${section("Removed modules", `${require_views__html.plural(removedModules.length, "module")} · ${formatDelta(sumDelta(c.modules.removed))}`, removedModules)}
|
|
222
|
+
${section("Changed modules", `${require_views__html.plural(changedModules.length, "module")} · net ${formatDelta(sumDelta(c.modules.changed))}`, changedModules)}
|
|
223
|
+
</div>`;
|
|
224
|
+
}
|
|
225
|
+
discovery.view.define("bundle-compare", (el, _config, data) => {
|
|
226
|
+
const viewData = data;
|
|
227
|
+
if (!viewData?.comparison) return;
|
|
228
|
+
const root = el;
|
|
229
|
+
root.innerHTML = render(viewData);
|
|
230
|
+
root.addEventListener("click", (event) => {
|
|
231
|
+
const button = event.target.closest(".cmp-more-btn");
|
|
232
|
+
if (!button) return;
|
|
233
|
+
button.previousElementSibling?.removeAttribute("hidden");
|
|
234
|
+
button.remove();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
//#endregion
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const require_views__colorScheme = require("./_colorScheme.js");
|
|
2
|
+
const require_views__html = require("./_html.js");
|
|
3
|
+
let d3_hierarchy = require("d3-hierarchy");
|
|
4
|
+
//#region src/client/views/importGraph.ts
|
|
5
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
6
|
+
const ROW_HEIGHT = 24;
|
|
7
|
+
const LEVEL_GAP = 220;
|
|
8
|
+
const MIN_HEIGHT = 240;
|
|
9
|
+
const MAX_HEIGHT = 640;
|
|
10
|
+
const MIN_RADIUS = 4;
|
|
11
|
+
const LABEL_WIDTH = 180;
|
|
12
|
+
const MIN_SCALE = 0.2;
|
|
13
|
+
const MAX_SCALE = 6;
|
|
14
|
+
const COLORS = {
|
|
15
|
+
current: "#e5484d",
|
|
16
|
+
root: "#f5b700",
|
|
17
|
+
own: "#8e6fe0",
|
|
18
|
+
nodeModule: "#2b9fd8",
|
|
19
|
+
};
|
|
20
|
+
function nodeColor(node) {
|
|
21
|
+
if (node.isCurrent) return COLORS.current;
|
|
22
|
+
if (node.isRoot) return COLORS.root;
|
|
23
|
+
return node.isNodeModule ? COLORS.nodeModule : COLORS.own;
|
|
24
|
+
}
|
|
25
|
+
function basename(path) {
|
|
26
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
27
|
+
}
|
|
28
|
+
function svg(tag, attrs = {}) {
|
|
29
|
+
const el = document.createElementNS(SVG_NS, tag);
|
|
30
|
+
for (const [key, value] of Object.entries(attrs))
|
|
31
|
+
el.setAttribute(key, String(value));
|
|
32
|
+
return el;
|
|
33
|
+
}
|
|
34
|
+
function legend() {
|
|
35
|
+
return `${[
|
|
36
|
+
[COLORS.current, "Current module"],
|
|
37
|
+
[COLORS.root, "Entry point"],
|
|
38
|
+
[COLORS.own, "Your code"],
|
|
39
|
+
[COLORS.nodeModule, "node_modules"],
|
|
40
|
+
]
|
|
41
|
+
.map(
|
|
42
|
+
([color, text]) =>
|
|
43
|
+
`<span><span class="ig-dot" style="background:${color}"></span>${text}</span>`,
|
|
44
|
+
)
|
|
45
|
+
.join(
|
|
46
|
+
"",
|
|
47
|
+
)}<span class="ig-hint">Arrows point to the imported module · scroll to zoom · drag to pan · click to open</span>`;
|
|
48
|
+
}
|
|
49
|
+
function layout(data) {
|
|
50
|
+
const parentOf = new Map(data.links.map((l) => [l.source, l.target]));
|
|
51
|
+
const root = (0, d3_hierarchy.stratify)()
|
|
52
|
+
.id((d) => d.id)
|
|
53
|
+
.parentId((d) => parentOf.get(d.id))(data.nodes)
|
|
54
|
+
.sort((a, b) => b.data.size - a.data.size);
|
|
55
|
+
return (0, d3_hierarchy.tree)().nodeSize([ROW_HEIGHT, LEVEL_GAP])(root);
|
|
56
|
+
}
|
|
57
|
+
function createGraph(host, data) {
|
|
58
|
+
const dark = require_views__colorScheme.isDark();
|
|
59
|
+
const points = layout(data).descendants();
|
|
60
|
+
const maxSize = Math.max(1, ...points.map((p) => p.data.size));
|
|
61
|
+
const radius = (p) => MIN_RADIUS + Math.sqrt(p.data.size / maxSize) * 7;
|
|
62
|
+
const minX = Math.min(...points.map((p) => p.x)) - ROW_HEIGHT;
|
|
63
|
+
const maxX = Math.max(...points.map((p) => p.x)) + ROW_HEIGHT;
|
|
64
|
+
const maxY = Math.max(...points.map((p) => p.y)) + LABEL_WIDTH;
|
|
65
|
+
const height = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, maxX - minX));
|
|
66
|
+
host.innerHTML = `<div class="ig-legend">${legend()}</div>`;
|
|
67
|
+
const rootEl = svg("svg", {
|
|
68
|
+
class: "ig-svg",
|
|
69
|
+
height,
|
|
70
|
+
});
|
|
71
|
+
const defs = svg("defs");
|
|
72
|
+
const marker = svg("marker", {
|
|
73
|
+
id: "ig-arrow",
|
|
74
|
+
viewBox: "0 -4 8 8",
|
|
75
|
+
refX: 8,
|
|
76
|
+
refY: 0,
|
|
77
|
+
markerWidth: 7,
|
|
78
|
+
markerHeight: 7,
|
|
79
|
+
orient: "auto",
|
|
80
|
+
});
|
|
81
|
+
marker.append(
|
|
82
|
+
svg("path", {
|
|
83
|
+
d: "M0,-4L8,0L0,4",
|
|
84
|
+
class: "ig-arrow",
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
defs.append(marker);
|
|
88
|
+
const viewport = svg("g");
|
|
89
|
+
const linksLayer = svg("g", { class: "ig-links" });
|
|
90
|
+
const nodesLayer = svg("g", { class: "ig-nodes" });
|
|
91
|
+
viewport.append(linksLayer, nodesLayer);
|
|
92
|
+
rootEl.append(defs, viewport);
|
|
93
|
+
const tooltip = document.createElement("div");
|
|
94
|
+
tooltip.className = "treemap-tooltip";
|
|
95
|
+
host.append(rootEl, tooltip);
|
|
96
|
+
const linkEls = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const point of points) {
|
|
98
|
+
const parent = point.parent;
|
|
99
|
+
if (!parent) continue;
|
|
100
|
+
const startY = point.y - radius(point);
|
|
101
|
+
const endY = parent.y + radius(parent) + 2;
|
|
102
|
+
const midY = (startY + endY) / 2;
|
|
103
|
+
const path = svg("path", {
|
|
104
|
+
d: `M${startY},${point.x}C${midY},${point.x} ${midY},${parent.x} ${endY},${parent.x}`,
|
|
105
|
+
"marker-end": "url(#ig-arrow)",
|
|
106
|
+
});
|
|
107
|
+
linksLayer.append(path);
|
|
108
|
+
linkEls.set(point, path);
|
|
109
|
+
}
|
|
110
|
+
const nodeEls = /* @__PURE__ */ new Map();
|
|
111
|
+
const pointByEl = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const point of points) {
|
|
113
|
+
const group = svg("g", {
|
|
114
|
+
class: "ig-node",
|
|
115
|
+
transform: `translate(${point.y},${point.x})`,
|
|
116
|
+
});
|
|
117
|
+
group.append(
|
|
118
|
+
svg("circle", {
|
|
119
|
+
r: radius(point),
|
|
120
|
+
fill: nodeColor(point.data),
|
|
121
|
+
stroke: dark ? "#161618" : "#f1f1ef",
|
|
122
|
+
"stroke-width": 1.5,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
const label = svg("text", {
|
|
126
|
+
x: radius(point) + 4,
|
|
127
|
+
y: 4,
|
|
128
|
+
});
|
|
129
|
+
label.textContent = basename(point.data.id);
|
|
130
|
+
group.append(label);
|
|
131
|
+
nodesLayer.append(group);
|
|
132
|
+
nodeEls.set(point, group);
|
|
133
|
+
pointByEl.set(group, point);
|
|
134
|
+
}
|
|
135
|
+
const width = host.clientWidth;
|
|
136
|
+
let scale = Math.min(
|
|
137
|
+
1,
|
|
138
|
+
Math.max(MIN_SCALE, Math.min(width / maxY, height / (maxX - minX))),
|
|
139
|
+
);
|
|
140
|
+
let tx = 22 * scale;
|
|
141
|
+
let ty = (height - (maxX - minX) * scale) / 2 - minX * scale;
|
|
142
|
+
function applyTransform() {
|
|
143
|
+
viewport.setAttribute(
|
|
144
|
+
"transform",
|
|
145
|
+
`translate(${tx},${ty}) scale(${scale})`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
applyTransform();
|
|
149
|
+
function highlight(point) {
|
|
150
|
+
rootEl.classList.toggle("ig-has-hover", !!point);
|
|
151
|
+
const active = new Set(
|
|
152
|
+
point ? [...point.ancestors(), ...(point.children ?? [])] : [],
|
|
153
|
+
);
|
|
154
|
+
for (const [p, el] of nodeEls)
|
|
155
|
+
el.classList.toggle("ig-active", active.has(p));
|
|
156
|
+
for (const [p, el] of linkEls)
|
|
157
|
+
el.classList.toggle(
|
|
158
|
+
"ig-active",
|
|
159
|
+
active.has(p) && !!p.parent && active.has(p.parent),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function moveTooltip(event) {
|
|
163
|
+
const box = host.getBoundingClientRect();
|
|
164
|
+
const x = event.clientX - box.left;
|
|
165
|
+
const y = event.clientY - box.top;
|
|
166
|
+
tooltip.style.left = `${Math.max(0, Math.min(x + 12, box.width - tooltip.offsetWidth - 4))}px`;
|
|
167
|
+
tooltip.style.top = `${y + 16}px`;
|
|
168
|
+
}
|
|
169
|
+
let drag = null;
|
|
170
|
+
for (const [point, el] of nodeEls) {
|
|
171
|
+
el.addEventListener("pointerenter", (event) => {
|
|
172
|
+
if (drag) return;
|
|
173
|
+
highlight(point);
|
|
174
|
+
const via = point.depth - 1;
|
|
175
|
+
tooltip.innerHTML = [
|
|
176
|
+
`<b>${require_views__html.escapeHTML(point.data.id)}</b>`,
|
|
177
|
+
`<b>Size</b>: ${require_views__html.formatBytes(point.data.size)}`,
|
|
178
|
+
point.data.isCurrent
|
|
179
|
+
? "Current module"
|
|
180
|
+
: via === 0
|
|
181
|
+
? "Imports it directly"
|
|
182
|
+
: `Imports it via ${via} ${via === 1 ? "module" : "modules"}`,
|
|
183
|
+
].join("<br/>");
|
|
184
|
+
tooltip.style.display = "block";
|
|
185
|
+
moveTooltip(event);
|
|
186
|
+
});
|
|
187
|
+
el.addEventListener("pointermove", moveTooltip);
|
|
188
|
+
el.addEventListener("pointerleave", () => {
|
|
189
|
+
highlight(null);
|
|
190
|
+
tooltip.style.display = "none";
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
rootEl.addEventListener("pointerdown", (event) => {
|
|
194
|
+
const nodeEl = event.target.closest(".ig-node");
|
|
195
|
+
drag = {
|
|
196
|
+
x: event.clientX,
|
|
197
|
+
y: event.clientY,
|
|
198
|
+
moved: false,
|
|
199
|
+
point: nodeEl ? pointByEl.get(nodeEl) : void 0,
|
|
200
|
+
};
|
|
201
|
+
rootEl.setPointerCapture(event.pointerId);
|
|
202
|
+
});
|
|
203
|
+
rootEl.addEventListener("pointermove", (event) => {
|
|
204
|
+
if (!drag) return;
|
|
205
|
+
const dx = event.clientX - drag.x;
|
|
206
|
+
const dy = event.clientY - drag.y;
|
|
207
|
+
if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 3) return;
|
|
208
|
+
drag.moved = true;
|
|
209
|
+
drag.x = event.clientX;
|
|
210
|
+
drag.y = event.clientY;
|
|
211
|
+
tx += dx;
|
|
212
|
+
ty += dy;
|
|
213
|
+
tooltip.style.display = "none";
|
|
214
|
+
applyTransform();
|
|
215
|
+
});
|
|
216
|
+
rootEl.addEventListener("pointerup", () => {
|
|
217
|
+
const current = drag;
|
|
218
|
+
drag = null;
|
|
219
|
+
if (current?.point && !current.moved && !current.point.data.isCurrent)
|
|
220
|
+
window.location.hash = discovery.encodePageHash(
|
|
221
|
+
"module",
|
|
222
|
+
current.point.data.id,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
rootEl.addEventListener(
|
|
226
|
+
"wheel",
|
|
227
|
+
(event) => {
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
const next = Math.min(
|
|
230
|
+
MAX_SCALE,
|
|
231
|
+
Math.max(MIN_SCALE, scale * Math.exp(-event.deltaY * 0.002)),
|
|
232
|
+
);
|
|
233
|
+
const box = rootEl.getBoundingClientRect();
|
|
234
|
+
const x = event.clientX - box.left;
|
|
235
|
+
const y = event.clientY - box.top;
|
|
236
|
+
tx = x - ((x - tx) * next) / scale;
|
|
237
|
+
ty = y - ((y - ty) * next) / scale;
|
|
238
|
+
scale = next;
|
|
239
|
+
applyTransform();
|
|
240
|
+
},
|
|
241
|
+
{ passive: false },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
discovery.view.define("import-graph", (el, _config, data) => {
|
|
245
|
+
const host = el;
|
|
246
|
+
const graph = data;
|
|
247
|
+
if (!graph?.nodes?.length) return;
|
|
248
|
+
const observer = new ResizeObserver(() => {
|
|
249
|
+
if (!host.isConnected || host.clientWidth === 0) return;
|
|
250
|
+
observer.disconnect();
|
|
251
|
+
createGraph(host, graph);
|
|
252
|
+
});
|
|
253
|
+
observer.observe(host);
|
|
254
|
+
});
|
|
255
|
+
//#endregion
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require("../sizeHeat.js");
|
|
2
|
+
const require_views__html = require("./_html.js");
|
|
3
|
+
//#region src/client/views/insights.ts
|
|
4
|
+
function topList(title, items, page, badge) {
|
|
5
|
+
if (items.length === 0) return "";
|
|
6
|
+
return `
|
|
7
|
+
<div class="in-top">
|
|
8
|
+
<div class="mo-card-label">${title}</div>
|
|
9
|
+
${items
|
|
10
|
+
.map(
|
|
11
|
+
(item, index) => `
|
|
12
|
+
<a class="in-row" href="${require_views__html.escapeHTML(discovery.encodePageHash(page, item.name))}">
|
|
13
|
+
<span class="in-row-name" title="${require_views__html.escapeHTML(item.name)}">${require_views__html.escapeHTML(item.name)}${badge ? badge(item) : ""}</span>
|
|
14
|
+
<span class="in-row-size${index < 3 && item.size > 716800 ? ` size-heat size-heat-${3 - index}` : ""}">${require_views__html.formatBytes(item.size)}</span>
|
|
15
|
+
<span class="in-row-share">${require_views__html.formatPercent(item.share)}</span>
|
|
16
|
+
<span class="in-row-bar"><span style="width:${Math.max(item.bar * 100, 1).toFixed(2)}%"></span></span>
|
|
17
|
+
</a>`,
|
|
18
|
+
)
|
|
19
|
+
.join("")}
|
|
20
|
+
</div>`;
|
|
21
|
+
}
|
|
22
|
+
function renderCards(o) {
|
|
23
|
+
const savingsSub = !o.hasRecommendations
|
|
24
|
+
? "production report needed"
|
|
25
|
+
: o.potentialSavings
|
|
26
|
+
? `${require_views__html.formatPercent(o.potentialSavingsShare)} of bundle · ${require_views__html.plural(o.findingsWithSavings, "fix", "fixes")}`
|
|
27
|
+
: "nothing to remove";
|
|
28
|
+
return `<div class="mo-cards">${[
|
|
29
|
+
require_views__html.card({
|
|
30
|
+
label: "Bundle size",
|
|
31
|
+
value: require_views__html.formatBytes(o.totalSize),
|
|
32
|
+
sub: [o.platform, require_views__html.plural(o.modulesCount, "module")]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(" · "),
|
|
35
|
+
kind: "accent",
|
|
36
|
+
}),
|
|
37
|
+
require_views__html.card({
|
|
38
|
+
label: "Your code",
|
|
39
|
+
value: require_views__html.formatBytes(o.sourceSize),
|
|
40
|
+
sub: `${require_views__html.formatPercent(o.sourceShare)} of bundle · ${require_views__html.plural(o.ownModulesCount, "module")}`,
|
|
41
|
+
bar: o.sourceShare,
|
|
42
|
+
}),
|
|
43
|
+
require_views__html.card({
|
|
44
|
+
label: "node_modules",
|
|
45
|
+
value: require_views__html.formatBytes(o.nodeModulesSize),
|
|
46
|
+
sub: `${require_views__html.formatPercent(o.nodeModulesShare)} of bundle · ${require_views__html.plural(o.packagesCount, "package")}`,
|
|
47
|
+
bar: o.nodeModulesShare,
|
|
48
|
+
}),
|
|
49
|
+
require_views__html.card({
|
|
50
|
+
label: "Duplicates",
|
|
51
|
+
value: require_views__html.plural(o.duplicatePackages, "package"),
|
|
52
|
+
sub: `${require_views__html.plural(o.duplicateModules, "module")} with copies`,
|
|
53
|
+
kind: o.duplicatePackages ? "danger" : "",
|
|
54
|
+
}),
|
|
55
|
+
require_views__html.card({
|
|
56
|
+
label: "Potential savings",
|
|
57
|
+
value: o.hasRecommendations
|
|
58
|
+
? `~${require_views__html.formatBytes(o.potentialSavings)}`
|
|
59
|
+
: "–",
|
|
60
|
+
sub: savingsSub,
|
|
61
|
+
kind: o.potentialSavings ? "good" : "",
|
|
62
|
+
}),
|
|
63
|
+
].join("")}</div>`;
|
|
64
|
+
}
|
|
65
|
+
function renderTopLists(o) {
|
|
66
|
+
const copiesBadge = (pkg) =>
|
|
67
|
+
pkg.copies > 1
|
|
68
|
+
? ` <span class="mo-chip mo-chip-danger">×${pkg.copies}</span>`
|
|
69
|
+
: "";
|
|
70
|
+
return `
|
|
71
|
+
<div class="in-tops">
|
|
72
|
+
${topList(`Heaviest packages · top 5 = ${require_views__html.formatPercent(o.topPackagesShare)} of bundle`, o.topPackages, "package", copiesBadge)}
|
|
73
|
+
${topList("Heaviest modules in your code", o.topOwnModules, "module")}
|
|
74
|
+
</div>`;
|
|
75
|
+
}
|
|
76
|
+
discovery.view.define("bundle-insights", (el, _config, data) => {
|
|
77
|
+
const insights = data;
|
|
78
|
+
if (!insights) return;
|
|
79
|
+
el.innerHTML = renderCards(insights) + renderTopLists(insights);
|
|
80
|
+
});
|
|
81
|
+
//#endregion
|