Skip to content

Commit 0848012

Browse files
author
Ben Newman
committed
Report per-bundle stats from minifier.
1 parent 95c4bcd commit 0848012

File tree

8 files changed

+223
-34
lines changed

8 files changed

+223
-34
lines changed

packages/babel-compiler/babel.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Babel = {
2121
// Deprecated, now a no-op.
2222
validateExtraFeatures: Function.prototype,
2323

24+
parse: function (source) {
25+
return Npm.require('meteor-babel').parse(source);
26+
},
27+
2428
compile: function (source, options) {
2529
var meteorBabel = Npm.require('meteor-babel');
2630
options = options || getDefaultOptions();

packages/standard-minifier-js/package.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ Package.registerBuildPlugin({
99
name: "minifyStdJS",
1010
use: [
1111
'minifier-js',
12+
'babel-compiler',
13+
'ecmascript'
1214
],
1315
sources: [
1416
'plugin/minify-js.js',
17+
'plugin/stats.js',
18+
'plugin/visitor.js',
19+
'plugin/utils.js',
1520
],
1621
});
1722

packages/standard-minifier-js/plugin/minify-js.js

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { extractModuleSizesTree } from "./stats.js";
2+
13
Plugin.registerMinifier({
24
extensions: ['js'],
35
archMatching: 'web'
@@ -108,37 +110,52 @@ MeteorBabelMinifier.prototype.processFilesForBundle = function(files, options) {
108110
}
109111
}
110112

111-
var allJs = '';
112-
files.forEach(function (file) {
113-
// Don't reminify *.min.js.
114-
if (/\.min\.js$/.test(file.getPathInBundle())) {
115-
allJs += file.getContentsAsString();
116-
} else {
117-
var minified;
118-
119-
try {
120-
minified = meteorJsMinify(file.getContentsAsString());
113+
const toBeAdded = {
114+
data: "",
115+
stats: Object.create(null)
116+
};
121117

122-
if (!(minified && typeof minified.code === "string")) {
123-
throw new Error();
124-
}
125-
} catch (err) {
126-
var filePath = file.getPathInBundle();
118+
files.forEach(file => {
119+
// Don't reminify *.min.js.
120+
if (/\.min\.js$/.test(file.getPathInBundle())) {
121+
toBeAdded.data += file.getContentsAsString();
122+
} else {
123+
var minified;
127124

128-
maybeThrowMinifyErrorBySourceFile(err, file);
125+
try {
126+
minified = meteorJsMinify(file.getContentsAsString());
129127

130-
err.message += " while minifying " + filePath;
131-
throw err;
128+
if (!(minified && typeof minified.code === "string")) {
129+
throw new Error();
132130
}
133131

134-
allJs += minified.code;
132+
} catch (err) {
133+
var filePath = file.getPathInBundle();
134+
135+
maybeThrowMinifyErrorBySourceFile(err, file);
136+
137+
err.message += " while minifying " + filePath;
138+
throw err;
135139
}
136-
allJs += '\n\n';
137140

138-
Plugin.nudge();
139-
});
141+
const tree = extractModuleSizesTree(minified.code);
142+
if (tree) {
143+
toBeAdded.stats[file.getPathInBundle()] =
144+
[Buffer.byteLength(minified.code), tree];
145+
} else {
146+
toBeAdded.stats[file.getPathInBundle()] =
147+
Buffer.byteLength(minified.code);
148+
}
149+
150+
toBeAdded.data += minified.code;
151+
}
152+
153+
toBeAdded.data += '\n\n';
154+
155+
Plugin.nudge();
156+
});
140157

141158
if (files.length) {
142-
files[0].addJavaScript({ data: allJs });
159+
files[0].addJavaScript(toBeAdded);
143160
}
144161
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Visitor from "./visitor.js";
2+
3+
// This RegExp will be used to scan the source for calls to meteorInstall,
4+
// taking into consideration that the function name may have been mangled
5+
// to something other than "meteorInstall" by the minifier.
6+
const meteorInstallRegExp = new RegExp([
7+
// If meteorInstall is called by its unminified name, then that's what
8+
// we should be looking for in the AST.
9+
/\b(meteorInstall)\(\{/,
10+
// If the meteorInstall function name has been minified, we can figure
11+
// out its mangled name by examining the import assingment.
12+
/\b(\w+)=Package.modules.meteorInstall\b/,
13+
/\b(\w+)=Package\["modules-runtime"\].meteorInstall\b/,
14+
].map(exp => exp.source).join("|"));
15+
16+
export function extractModuleSizesTree(source) {
17+
const match = meteorInstallRegExp.exec(source);
18+
if (match) {
19+
const ast = Babel.parse(source);
20+
const name = match[1] || match[2] || match[3];
21+
meteorInstallVisitor.visit(ast, name, source);
22+
return meteorInstallVisitor.tree;
23+
}
24+
}
25+
26+
const meteorInstallVisitor = new (class extends Visitor {
27+
reset(root, meteorInstallName, source) {
28+
this.name = meteorInstallName;
29+
this.source = source;
30+
this.tree = null;
31+
}
32+
33+
visitCallExpression(node) {
34+
if (this.tree !== null) {
35+
return;
36+
}
37+
38+
if (isIdWithName(node.callee, this.name)) {
39+
const source = this.source;
40+
41+
function walk(expr) {
42+
if (expr.type !== "ObjectExpression") {
43+
return Buffer.byteLength(source.slice(expr.start, expr.end));
44+
}
45+
46+
const contents = Object.create(null);
47+
48+
expr.properties.forEach(prop => {
49+
const keyName = getKeyName(prop.key);
50+
if (typeof keyName === "string") {
51+
contents[keyName] = walk(prop.value);
52+
}
53+
});
54+
55+
return contents;
56+
}
57+
58+
this.tree = walk(node.arguments[0]);
59+
60+
} else {
61+
this.visitChildren(node);
62+
}
63+
}
64+
});
65+
66+
function isIdWithName(node, name) {
67+
return node &&
68+
node.type === "Identifier" &&
69+
node.name === name;
70+
}
71+
72+
function getKeyName(key) {
73+
if (key.type === "Identifier") {
74+
return key.name;
75+
}
76+
77+
if (key.type === "StringLiteral" ||
78+
key.type === "Literal") {
79+
return key.value;
80+
}
81+
82+
return null;
83+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use strict";
2+
3+
const codeOfA = "A".charCodeAt(0);
4+
const codeOfZ = "Z".charCodeAt(0);
5+
6+
export function isObject(value) {
7+
return typeof value === "object" && value !== null;
8+
}
9+
10+
// Without a complete list of Node .type names, we have to settle for this
11+
// fuzzy matching of object shapes. However, the infeasibility of
12+
// maintaining a complete list of type names is one of the reasons we're
13+
// using the FastPath/Visitor abstraction in the first place.
14+
export function isNodeLike(value) {
15+
return isObject(value) &&
16+
! Array.isArray(value) &&
17+
isCapitalized(value.type);
18+
}
19+
20+
function isCapitalized(string) {
21+
if (typeof string !== "string") {
22+
return false;
23+
}
24+
const code = string.charCodeAt(0);
25+
return code >= codeOfA && code <= codeOfZ;
26+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"use strict";
2+
3+
import {
4+
isObject,
5+
isNodeLike,
6+
} from "./utils.js";
7+
8+
const codeOfUnderscore = "_".charCodeAt(0);
9+
10+
export default class Visitor {
11+
visit(root) {
12+
this.reset.apply(this, arguments);
13+
this.visitWithoutReset(root);
14+
}
15+
16+
visitWithoutReset(node) {
17+
if (Array.isArray(node)) {
18+
node.forEach(this.visitWithoutReset, this);
19+
} else if (isNodeLike(node)) {
20+
const method = this["visit" + node.type];
21+
if (typeof method === "function") {
22+
// The method must call this.visitChildren(node) to continue
23+
// traversing.
24+
method.call(this, node);
25+
} else {
26+
this.visitChildren(node);
27+
}
28+
}
29+
}
30+
31+
visitChildren(node) {
32+
if (! isNodeLike(node)) {
33+
return;
34+
}
35+
36+
const keys = Object.keys(node);
37+
const keyCount = keys.length;
38+
39+
for (let i = 0; i < keyCount; ++i) {
40+
const key = keys[i];
41+
42+
if (key === "loc" || // Ignore .loc.{start,end} objects.
43+
// Ignore "private" properties added by Babel.
44+
key.charCodeAt(0) === codeOfUnderscore) {
45+
continue;
46+
}
47+
48+
const child = node[key];
49+
if (! isObject(child)) {
50+
// Ignore properties whose values aren't objects.
51+
continue;
52+
}
53+
54+
this.visitWithoutReset(child);
55+
}
56+
}
57+
}

tools/isobuild/bundler.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,10 @@ class Target {
12861286
newFile.setUrlToHash('.js', '?meteor_js_resource=true');
12871287
}
12881288

1289+
if (! dynamic) {
1290+
console.log(file.stats);
1291+
}
1292+
12891293
return newFile;
12901294
});
12911295

tools/isobuild/minifier-plugin.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,9 @@ export class JsFile extends InputFile {
6060
// - sourceMap
6161
// - path
6262
// - hash?
63+
// - stats?
6364
addJavaScript(options) {
64-
const self = this;
65-
self._minifiedFiles.push({
66-
data: options.data,
67-
sourceMap: options.sourceMap,
68-
path: options.path
69-
});
65+
this._minifiedFiles.push({ ...options });
7066
}
7167
}
7268

@@ -75,12 +71,9 @@ export class CssFile extends InputFile {
7571
// - sourceMap
7672
// - path
7773
// - hash?
74+
// - stats?
7875
addStylesheet(options) {
79-
this._minifiedFiles.push({
80-
data: options.data,
81-
sourceMap: options.sourceMap,
82-
path: options.path
83-
});
76+
this._minifiedFiles.push({ ...options });
8477
}
8578
}
8679

0 commit comments

Comments
 (0)