Files
meteor/packages/non-core/bundle-visualizer/server.js

162 lines
3.9 KiB
JavaScript

import assert from "assert";
import { readFileSync as fsReadFileSync } from "fs";
import { Meteor } from "meteor/meteor";
import { WebAppInternals } from "meteor/webapp";
import {
methodNameStats,
packageName,
typeBundle,
typeNodeModules,
typePackage,
} from "./common.js";
if (Meteor.isProduction) {
console.warn([
`=> The "${packageName}" package is currently enabled. Visit your`,
"application in a web browser to view the client bundle analysis and",
"'meteor remove' the package before building/deploying the final bundle.",
].join(" "));
} else {
console.warn([
"=> In order to provide accurate measurements using minified bundles,",
`the "${packageName}" package requires running 'meteor --production'`,
"to simulate production bundling."
].join(" "));
}
function getStatBundles() {
const statFileFilter = f =>
f.type === "json" &&
f.absolutePath &&
f.absolutePath.endsWith(".stats.json");
// Read the stat file, but if it's in any way unusable just return null.
function readOrNull(file) {
try {
return JSON.parse(fsReadFileSync(file, "utf8"));
} catch (err) {
return null;
}
}
const {
staticFiles,
staticFilesByArch,
} = WebAppInternals;
const files = [];
if (staticFilesByArch) {
Object.keys(staticFilesByArch).forEach(arch => {
const staticFiles = staticFilesByArch[arch];
Object.keys(staticFiles).forEach(path => {
files.push({ ...staticFiles[path], arch });
});
});
} else if (staticFiles) {
Object.keys(staticFiles).forEach(path => {
files.push({ ...staticFiles[path], arch: 'bundle' });
});
}
return files.filter(statFileFilter).map(file => ({
name: file.hash,
arch: file.arch,
stats: readOrNull(file.absolutePath),
}));
}
function _childModules(node) {
return Object.keys(node)
.map(module => {
const result = {
name: module,
type: typeNodeModules,
};
if (typeof node[module] === "object") {
result.children = _childModules(node[module]);
} else {
result.size = node[module];
}
return result;
});
}
function d3TreeFromStats(stats) {
assert.strictEqual(typeof stats, "object",
"Must pass a stats object");
assert.strictEqual(typeof stats.minifiedBytesByPackage, "object",
"Stats object must contain a `minifiedBytesByPackage` object");
const sizeOrDetail = (name, node) => {
const result = {
name,
type: typePackage,
};
// A non-leaf is: [size (Number), limb (Object)]
// A leaf is size (Number)
if (Array.isArray(node)) {
const [, detail] = node;
result.children = _childModules(detail);
} else {
result.size = node;
}
return result;
};
// Main entry into the stats is the `minifiedBytesByPackage` attribute.
return Object.keys(stats.minifiedBytesByPackage)
.map(name =>
sizeOrDetail(name
// Change the "packages/bundle.js" name to "(bundle)"
.replace(/^[^\/]+\/(.*)\.js$/, "($1)"),
stats.minifiedBytesByPackage[name]));
}
Meteor.startup(() => {
if (! Package.webapp) {
return;
}
Package.webapp.WebAppInternals.meteorInternalHandlers.use(
methodNameStats,
statsMiddleware
);
});
function statsMiddleware(request, response) {
const statBundles = getStatBundles();
function sendJSON(data) {
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(data, null, 2));
}
// Silently return no data if not simulating production.
if (! Meteor.isProduction) {
return sendJSON(null);
}
if (! (statBundles && statBundles.length)) {
throw new Meteor.Error(
"no-stats-bundles",
"Unable to retrieve stats"
);
}
sendJSON({
name: "main",
children: statBundles.map((statBundle, index, array) => ({
name: statBundle.arch,
type: typeBundle,
children: d3TreeFromStats(statBundle.stats),
}))
});
}