Skip to content
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 @@ -9,6 +9,7 @@ This project is a lightweight front-end viewer for webpack output generated with
- Overview of build timing, module counts, chunk counts, and assets
- 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`
- Warning and error inspection
- Hints for common optimization issues, including circular dependencies
- Upload a generated stats file directly in the app
Expand Down
56 changes: 52 additions & 4 deletions app/graphs/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var forceAtlas2 = require("graphology-layout-forceatlas2");
var rescale = require("./rescale");
var theme = require("./theme");
var legend = require("./legend");
var moduleFilter = require("../moduleFilter");
var formatSize = require("../formatSize");
var percentageToColor = require("../percentageToColor").greenRed;
var percentageToColor2 = require("../percentageToColor").blue;
Expand Down Expand Up @@ -224,11 +225,41 @@ var CHUNK_SELECTION_LEGEND = {
]
};

// The filter is set on the modules page but stays on while browsing single
// modules and chunks, where there is no filter box in sight. Saying so in the
// legend is what keeps a half-empty graph from looking like a bug.
function filterGroup() {
if (!moduleFilter.isActive()) return null;
var summary = moduleFilter.summary(app.stats.modules);
return {
title: "filter",
items: [
{
text:
"showing " +
summary.visible +
" of " +
summary.total +
" modules, set on the modules page"
}
]
};
}

// The module whose page is open is drawn whether or not it passes the filter:
// it can be reached from a link on another module or from a chunk, and hiding
// the very thing the page is about would only look broken.
function isDrawn(module) {
if (!module) return false;
return moduleFilter.isVisible(module) || module.uid === activeModuleUid;
}

var currentSelectionGroup = null;

function showLegend(selectionGroup) {
legend.render(
legendElement,
selectionGroup ? GRAPH_LEGEND.concat(selectionGroup) : GRAPH_LEGEND
);
currentSelectionGroup = selectionGroup;
var groups = GRAPH_LEGEND.concat(selectionGroup || [], filterGroup() || []);
legend.render(legendElement, groups);
}

showLegend(null);
Expand Down Expand Up @@ -270,6 +301,10 @@ var s = new Sigma(graph, element, {
labelRenderedSizeThreshold: 6,
nodeReducer: function(node, data) {
var display = Object.assign({}, data);
if (!isDrawn(data.module)) {
display.hidden = true;
return display;
}
if (data.highlighted) display.label = data.fullLabel;
if (!selection) return display;

Expand Down Expand Up @@ -309,6 +344,14 @@ var s = new Sigma(graph, element, {
},
edgeReducer: function(edge, data) {
var display = Object.assign({}, data);
// An edge is only meaningful while both of its modules are on screen.
if (
!isDrawn(app.mapModulesUid[data.sourceModuleUid]) ||
!isDrawn(app.mapModulesUid[data.targetModuleUid])
) {
display.hidden = true;
return display;
}
// sigma 1's `edgeColor: "target"` setting: an explicit edge colour wins,
// otherwise the edge takes the colour of its target node.
var color =
Expand Down Expand Up @@ -341,6 +384,11 @@ var s = new Sigma(graph, element, {
}
});

moduleFilter.onChange(function() {
showLegend(currentSelectionGroup);
s.refresh();
});

var layout = new FA2Layout(graph, {
settings: Object.assign(forceAtlas2.inferSettings(graph), {
// Carried over from the old web_modules/sigma.js shim and show() below.
Expand Down
106 changes: 106 additions & 0 deletions app/moduleFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Shared filter for the module list and the module graph (webpack/analyse#11).
//
// A build of any size arrives with far more modules than the ones being looked
// for, and until now the only way to see just your own was to edit the stats
// file by hand. The filter state lives here rather than in the page, so the
// table and the graph always agree on what is showing, and the graph keeps the
// filter while navigating between modules and chunks.

var listeners = [];

exports.query = "";
exports.hideThirdParty = false;
// Set when the query looks like a regular expression but does not compile. The
// filter falls back to showing everything, so a half-typed pattern never
// blanks the page.
exports.error = null;

var match = null;

exports.set = function set(changes) {
if ("query" in changes) exports.query = changes.query;
if ("hideThirdParty" in changes)
exports.hideThirdParty = changes.hideThirdParty;
match = compile(exports.query);
listeners.forEach(function(listener) {
listener();
});
};

exports.onChange = function onChange(listener) {
listeners.push(listener);
};

exports.isActive = function isActive() {
return exports.hideThirdParty || (!!match && !exports.error);
};

exports.isVisible = function isVisible(module) {
if (exports.hideThirdParty && isThirdParty(module)) return false;
if (!match || exports.error) return true;
return match(module);
};

exports.summary = function summary(modules) {
var result = { visible: 0, total: 0, visibleSize: 0, totalSize: 0 };
modules.forEach(function(module) {
var size = module.size || 0;
result.total++;
result.totalSize += size;
if (!exports.isVisible(module)) return;
result.visible++;
result.visibleSize += size;
});
return result;
};

// "/^\.\/src/i" is read as a regular expression, anything else as a
// case-insensitive substring, which is what a filter box is expected to do.
function compile(query) {
exports.error = null;
query = (query || "").trim();
if (!query) return null;
var re = query.match(/^\/(.*)\/([a-z]*)$/);
if (re) {
try {
var expression = new RegExp(re[1], re[2]);
return function(module) {
return expression.test(module.name || module.identifier || "");
};
} catch (err) {
exports.error = err.message;
return null;
}
}
var needle = query.toLowerCase();
return function(module) {
return (
(module.name || "").toLowerCase().indexOf(needle) >= 0 ||
(module.identifier || "").toLowerCase().indexOf(needle) >= 0
);
};
}

// A module name puts its loaders in front of the resource
// ("css-loader!./a.css") and a context module puts its mode and matcher behind
// the directory ("./dir sync ^\.\/.*$"), so what has to be tested is neither
// the whole name nor simply its last piece. Getting this wrong would hide a
// source file only because a third-party loader built it.
function resourceOf(module) {
var name = module.name || module.identifier || "";
var parts = name.split("!");
var resource = parts[parts.length - 1];
var context = resource.match(
/^(\S+) (?:sync|eager|weak|lazy|lazy-once|optional)\b/
);
return context ? context[1] : resource;
}

// `~` is how webpack 1 wrote node_modules in the names it reported.
var THIRD_PARTY = /(^|[\\/])(node_modules|~)[\\/]/;

function isThirdParty(module) {
return THIRD_PARTY.test(resourceOf(module));
}

exports.isThirdParty = isThirdParty;
54 changes: 14 additions & 40 deletions app/pages/modules/modules.pug
Original file line number Diff line number Diff line change
@@ -1,40 +1,14 @@
table.table.table-condensed
thead
tr
th id
th name
th.sortable-th.size-th size
th chunks
th flags
tbody
each module in stats.modules
tr
td
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
each chunk in module.chunks
a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk)}`)= chunk
= " "
td
if module.built
span.label.label-success built
= " "
if !module.cacheable
span.label.label-warning not cacheable
= " "
if module.prefetched
span.label.label-success prefetched
= " "
if module.failed
span.label.label-danger failed
= " "
if module.warnings
span.label.label-warning= module.warnings + " warnings"
= " "
if module.errors
span.label.label-danger= module.errors + " errors"
.module-filter
label.module-filter-label(for="module-filter-query") filter
input#module-filter-query.form-control.input-sm.module-filter-query(
type="text",
placeholder="part of a name, or /regexp/",
value=query,
autocomplete="off"
)
label.module-filter-option(title="Modules whose file lives in node_modules, written ~ in webpack 1 stats")
input.module-filter-third-party(type="checkbox", checked=hideThirdParty)
| hide node_modules
span.module-filter-summary
span.module-filter-error.text-danger
.modules-table
50 changes: 47 additions & 3 deletions app/pages/modules/page.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
var app = require("../../app");
var modulesGraph = require("../../graphs/modules");
var moduleFilter = require("../../moduleFilter");
var formatSize = require("../../formatSize");

function renderTable() {
$(".page").html(
require("./modules.pug")({
stats: app.stats
$(".modules-table").html(
require("./table.pug")({
modules: app.stats.modules.filter(moduleFilter.isVisible)
})
);
}

function renderSummary() {
var summary = moduleFilter.summary(app.stats.modules);
$(".module-filter-summary").text(
moduleFilter.isActive()
? "showing " +
summary.visible +
" of " +
summary.total +
" modules, " +
formatSize(summary.visibleSize) +
" of " +
formatSize(summary.totalSize)
: summary.total + " modules, " + formatSize(summary.totalSize)
);
// An unfinished regexp is a normal thing to have in a filter box, so it is
// reported next to the field rather than emptying the table.
$(".module-filter-error").text(
moduleFilter.error ? "not a valid regexp: " + moduleFilter.error : ""
);
}

module.exports = function() {
document.title = "modules";
$(".page").html(
require("./modules.pug")({
query: moduleFilter.query,
hideThirdParty: moduleFilter.hideThirdParty
})
);
renderTable();
renderSummary();

var sortDir;
$(document).on("click", ".size-th", function() {
Expand All @@ -21,11 +51,25 @@ module.exports = function() {
});
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() {
moduleFilter.set({ query: this.value });
renderTable();
renderSummary();
});
$(document).on("change", ".module-filter-third-party", function() {
moduleFilter.set({ hideThirdParty: this.checked });
renderTable();
renderSummary();
});

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();
};
};
42 changes: 42 additions & 0 deletions app/pages/modules/table.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
table.table.table-condensed
thead
tr
th id
th name
th.sortable-th.size-th size
th chunks
th flags
tbody
each module in modules
tr
td
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
each chunk in module.chunks
a.btn.btn-info(href=`#chunk/${encodeURIComponent(chunk)}`)= chunk
= " "
td
if module.built
span.label.label-success built
= " "
if !module.cacheable
span.label.label-warning not cacheable
= " "
if module.prefetched
span.label.label-success prefetched
= " "
if module.failed
span.label.label-danger failed
= " "
if module.warnings
span.label.label-warning= module.warnings + " warnings"
= " "
if module.errors
span.label.label-danger= module.errors + " errors"
if modules.length === 0
p.text-muted No module matches the filter.
4 changes: 2 additions & 2 deletions app/pages/select/application.pug
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ nav.navbar.navbar-default
li: a(href="#warnings") Warnings
li: a(href="#errors") Errors
li: a(href="#hints") Hints
#sigma-modules(style="width: 99%; height: 500px; display:none;")
#sigma-chunks(style="width: 99%; height: 500px; display:none;")
#sigma-modules(style="display:none;")
#sigma-chunks(style="display:none;")
.page

.modal.fade: .modal-dialog: .modal-content
Expand Down
Loading