Skip to content
This repository was archived by the owner on Sep 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This project is a lightweight front-end viewer for webpack output generated with
- Module dependency graph and chunk relationships
- Asset and bundle size breakdowns
- Filter the module list and graph by name or regexp, or hide `node_modules`
- Sort any table of modules, chunks or assets by size, name or id
- Warning and error inspection
- Hints for common optimization issues, including circular dependencies
- Upload a generated stats file directly in the app
Expand Down
8 changes: 4 additions & 4 deletions app/pages/assets/assets.pug
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
table.table.table-condensed
thead
tr
th assets
th size
th.sortable-th assets
th.sortable-th(data-sort-first="desc") size
th chunks
th names
th flags
tbody
each asset in stats.assets
tr
td: pre: code= asset.name
td= require("../../formatSize")(asset.size)
td(data-sort=asset.name): pre: code= asset.name
td(data-sort=asset.size)= require("../../formatSize")(asset.size)
td
each chunk in asset.chunks
a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk)}`)= chunk
Expand Down
2 changes: 2 additions & 0 deletions app/pages/assets/page.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
var app = require("../../app");
var sortableTable = require("../../sortableTable");

module.exports = function() {
document.title = "assets";
sortableTable.enable();
$(".page").html(
require("./assets.pug")({
stats: app.stats
Expand Down
14 changes: 7 additions & 7 deletions app/pages/chunk/chunk.pug
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,24 @@
if chunk.modules && chunk.modules.length > 0
.row: .col-md-12: .well
h4 modules
table.table.table-condensed
table.table.table-condensed(data-sort-key="chunk-modules")
thead
tr
th id
th name
th size
th.sortable-th id
th.sortable-th name
th.sortable-th(data-sort-first="desc") size
th chunks
th flags
tbody
each module in chunk.modules
tr
td
td(data-sort=module.id)
if typeof module.uid === "number"
a.btn.btn-success(href=`#module/${module.uid}`)= module.id
else
span.btn.btn-success= module.id
td: pre: code= module.name.split("!").join("\n")
td= require("../../formatSize")(module.size)
td(data-sort=module.name): pre: code= module.name.split("!").join("\n")
td(data-sort=module.size)= require("../../formatSize")(module.size)
td
each chunk in module.chunks
a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk)}`)= chunk
Expand Down
2 changes: 2 additions & 0 deletions app/pages/chunk/page.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
var app = require("../../app");
var modulesGraph = require("../../graphs/modules");
var sortableTable = require("../../sortableTable");

module.exports = function (id) {
id = isNaN(parseInt(id, 10)) ? decodeURIComponent(id) : parseInt(id, 10);
document.title = "chunk " + id;
sortableTable.enable();
$(".page").html(
require("./chunk.pug")({
stats: app.stats,
Expand Down
12 changes: 6 additions & 6 deletions app/pages/chunks/chunks.pug
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
table.table.table-condensed
thead
tr
th id
th.sortable-th id
th names
th modules
th size
th.sortable-th(data-sort-first="desc") modules
th.sortable-th(data-sort-first="desc") size
th parents
th flags
tbody
each chunk in stats.chunks
tr
td: a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk.id)}`)= chunk.id
td(data-sort=chunk.id): a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk.id)}`)= chunk.id
td
each name in chunk.names
code= name
= " "
td= chunk.modules ? chunk.modules.length : "N/A"
td= require("../../formatSize")(chunk.size)
td(data-sort=chunk.modules ? chunk.modules.length : "")= chunk.modules ? chunk.modules.length : "N/A"
td(data-sort=chunk.size)= require("../../formatSize")(chunk.size)
td
each parent in chunk.parents
a.btn.btn-info(href=`#chunk/${encodeURIComponent(parent)}`)= parent
Expand Down
2 changes: 2 additions & 0 deletions app/pages/chunks/page.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
var app = require("../../app");
var chunksGraph = require("../../graphs/chunks");
var sortableTable = require("../../sortableTable");

module.exports = function() {
document.title = "chunks";
sortableTable.enable();
$(".page").html(
require("./chunks.pug")({
stats: app.stats
Expand Down
13 changes: 4 additions & 9 deletions app/pages/modules/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ var app = require("../../app");
var modulesGraph = require("../../graphs/modules");
var moduleFilter = require("../../moduleFilter");
var formatSize = require("../../formatSize");
var sortableTable = require("../../sortableTable");

function renderTable() {
$(".modules-table").html(
require("./table.pug")({
modules: app.stats.modules.filter(moduleFilter.isVisible)
})
);
// Filtering redraws the rows, which would otherwise drop the sort.
sortableTable.restore($(".modules-table table")[0]);
}

function renderSummary() {
Expand All @@ -34,6 +37,7 @@ function renderSummary() {

module.exports = function() {
document.title = "modules";
sortableTable.enable();
$(".page").html(
require("./modules.pug")({
query: moduleFilter.query,
Expand All @@ -43,14 +47,6 @@ module.exports = function() {
renderTable();
renderSummary();

var sortDir;
$(document).on("click", ".size-th", function() {
sortDir = sortDir === "desc" ? "asc" : "desc";
app.stats.modules.sort(function(a, b) {
return sortDir === "asc" ? b.size - a.size : a.size - b.size;
});
renderTable();
});
// The graph follows the same filter through moduleFilter, so only the table
// and the summary are redrawn here.
$(document).on("input", ".module-filter-query", function() {
Expand All @@ -67,7 +63,6 @@ module.exports = function() {
modulesGraph.show();
modulesGraph.setNormal();
return function() {
$(document).off("click", ".size-th");
$(document).off("input", ".module-filter-query");
$(document).off("change", ".module-filter-third-party");
modulesGraph.hide();
Expand Down
14 changes: 7 additions & 7 deletions app/pages/modules/table.pug
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
table.table.table-condensed
table.table.table-condensed(data-sort-key="modules")
thead
tr
th id
th name
th.sortable-th.size-th size
th.sortable-th id
th.sortable-th name
th.sortable-th(data-sort-first="desc") size
th chunks
th flags
tbody
each module in modules
tr
td
td(data-sort=module.id)
if typeof module.uid === "number"
a.btn.btn-success(href=`#module/${module.uid}`)= module.id
else
span.btn.btn-success= module.id
td: pre: code= module.name.split("!").join("\n")
td= require("../../formatSize")(module.size)
td(data-sort=module.name): pre: code= module.name.split("!").join("\n")
td(data-sort=module.size)= require("../../formatSize")(module.size)
td
each chunk in module.chunks
a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk)}`)= chunk
Expand Down
96 changes: 96 additions & 0 deletions app/sortableTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Click-to-sort for the tables of any page (webpack/analyse#28).
//
// Sorting used to live in the modules page and was written against
// app.stats.modules, which is why that was the only table that could sort, and
// only by size. This sorts the rendered rows instead, so a page only has to
// mark a column with .sortable-th, whatever shape the data behind it has.
// Cells that do not sort the way they read - a size printed as "4 KiB", an id
// that is a number inside a link - carry what to sort on in data-sort.

// Remembered per table for the pages that redraw their rows, keyed by the
// data-sort-key of the table.
var lastSort = {};

// Called by every page that has a sortable table. The handler is delegated
// from the document and installed once, so it survives the pages redrawing
// their tables and there is nothing to tear down.
var enabled = false;

exports.enable = function enable() {
if (enabled) return;
enabled = true;
$(document).on("click", "th.sortable-th", function() {
var column = $(this).index();
var table = $(this).closest("table")[0];
if (!table) return;
sort(table, column, nextDirection(this));
});
};

// A column is wanted a particular way round the first time it is clicked:
// sizes and counts biggest first, ids and names from the start. The column
// says which through data-sort-first, since guessing it from the values would
// turn an id into a countdown.
function nextDirection(th) {
if ($(th).hasClass("sorted-desc")) return "asc";
if ($(th).hasClass("sorted-asc")) return "desc";
return th.getAttribute("data-sort-first") === "desc" ? "desc" : "asc";
}

function sort(table, column, direction) {
var $table = $(table);
var body = $table.find("tbody")[0] || table;
var factor = direction === "asc" ? 1 : -1;
// Array.prototype.sort is stable, so rows that compare equal stay in the
// order the page rendered them in.
var rows = $table.find("tbody > tr").get();
rows.sort(function(a, b) {
return factor * compare(valueOf(a, column), valueOf(b, column));
});
rows.forEach(function(row) {
body.appendChild(row);
});
$table.find("th").removeClass("sorted-asc sorted-desc");
$table
.find("th")
.eq(column)
.addClass("sorted-" + direction);
var key = table.getAttribute("data-sort-key");
if (key) lastSort[key] = { column: column, direction: direction };
}

// Puts a redrawn table back in the order it was sorted in. Without it, typing
// in the module filter would silently undo the sort.
exports.restore = function restore(table) {
if (!table) return;
var key = table.getAttribute("data-sort-key");
var last = key && lastSort[key];
if (last) sort(table, last.column, last.direction);
};

function valueOf(row, column) {
var cell = row.cells[column];
if (!cell) return "";
var raw = cell.getAttribute("data-sort");
return exports.valueFrom(raw === null ? cell.textContent : raw);
}

// What a cell sorts as: a number when it reads as one, its text otherwise.
exports.valueFrom = function valueFrom(text) {
text = (text || "").trim();
if (text === "") return "";
var number = Number(text);
return isNaN(number) ? text.toLowerCase() : number;
};

exports.compare = compare;

function compare(a, b) {
var aIsNumber = typeof a === "number";
var bIsNumber = typeof b === "number";
if (aIsNumber && bIsNumber) return a - b;
// Numbers before text, so a cell that holds neither does not land in the
// middle of a column of numbers.
if (aIsNumber !== bIsNumber) return aIsNumber ? -1 : 1;
return a < b ? -1 : a > b ? 1 : 0;
}
15 changes: 15 additions & 0 deletions app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,23 @@
table pre {
margin-bottom: 0px;
}
/* A sortable column says so: a faint pair of arrows until it is sorted on,
then the direction it is sorted in. */
.sortable-th {
cursor: pointer;
white-space: nowrap;
}
.sortable-th:after {
content: " \2195";
opacity: 0.3;
}
.sortable-th.sorted-asc:after {
content: " \2191";
opacity: 1;
}
.sortable-th.sorted-desc:after {
content: " \2193";
opacity: 1;
}

/* Legend under the module and chunk graphs (webpack/analyse#29). Sits between
Expand Down
46 changes: 46 additions & 0 deletions test/sortableTable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Checks the ordering rules behind the sortable table columns. The clicking
// and the row shuffling need a browser; what a cell is worth and how two of
// them compare does not, and that is where the surprises live.
var test = require("node:test");
var assert = require("node:assert");

var sortableTable = require("../app/sortableTable");
var valueFrom = sortableTable.valueFrom;
var compare = sortableTable.compare;

test("reads a cell as a number when it is one", function() {
assert.strictEqual(valueFrom("1846"), 1846);
assert.strictEqual(valueFrom(" 12 "), 12);
assert.strictEqual(valueFrom("0"), 0);
});

test("reads anything else as text, case aside", function() {
assert.strictEqual(valueFrom("./SRC/App.js"), "./src/app.js");
// The size column is printed, not measured, here: it sorts on the
// data-sort number the template writes, never on "4 KiB".
assert.strictEqual(valueFrom("4 KiB"), "4 kib");
assert.strictEqual(valueFrom(""), "");
assert.strictEqual(valueFrom(null), "");
});

test("orders numbers by size, not by their digits", function() {
var sizes = [41, 1846, 205, 28672].sort(compare);
assert.deepStrictEqual(sizes, [41, 205, 1846, 28672]);
});

test("orders text alphabetically", function() {
var names = ["./b.js", "./a.js", "./c.js"].sort(compare);
assert.deepStrictEqual(names, ["./a.js", "./b.js", "./c.js"]);
});

test("keeps cells that hold neither out of the numbers", function() {
// A chunk with no module list prints "N/A" and sorts with an empty value;
// it belongs at one end of the column rather than in the middle of it.
var mixed = [12, "", 3, "n/a"].sort(compare);
assert.deepStrictEqual(mixed, [3, 12, "", "n/a"]);
});

test("compares equal values as equal, so a sort stays stable", function() {
assert.strictEqual(compare(7, 7), 0);
assert.strictEqual(compare("a", "a"), 0);
});