Skip to content

Commit 71933e9

Browse files
committedFeb 10, 2017
UglifyJsPlugin: extract comments to separate file
License comments use up a lot of space, especially when using many small libraries with large license blocks. With this addition, you can extract all license comments to a separate file and remove them from the bundle files. A small banner points to the file containing all license information such that the user can find it if needed. We add a new option extractComments to the UglifyJsPlugin. It can be omitted, then the behavior does not change, or it can be: - true: All comments that normally would be preserved by the comments option will be moved to a separate file. If the original file is named foo.js, then the comments will be stored to foo.js.LICENSE - regular expression (given as RegExp or string) or a function (astNode, comment) -> boolean: All comments that match the given expression (resp. are evaluated to true by the function) will be extracted to the separate file. The comments option specifies whether the comment will be preserved, i.e. it is possible to preserve some comments (e.g. annotations) while extracting others or even preserving comments that have been extracted. - an object consisting of the following keys, all optional: - condition: regular expression or function (see previous point) - file: The file where the extracted comments will be stored. Can be either a string (filename) or function (string) -> string which will be given the original filename. Default is to append the suffix .LICENSE to the original filename. - banner: The banner text that points to the extracted file and will be added on top of the original file. will be added to the original file. Can be false (no banner), a string, or a function (string) -> string that will be called with the filename where extracted comments have been stored. Will be wrapped into comment. Default: /*! For license information please see foo.js.LICENSE */
·
v5.99.9v2.3.0
1 parent 1cc7272 commit 71933e9

File tree

5 files changed

+267
-4
lines changed

5 files changed

+267
-4
lines changed
 

‎lib/optimize/UglifyJsPlugin.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const SourceMapConsumer = require("source-map").SourceMapConsumer;
88
const SourceMapSource = require("webpack-sources").SourceMapSource;
99
const RawSource = require("webpack-sources").RawSource;
10+
const ConcatSource = require("webpack-sources").ConcatSource;
1011
const RequestShortener = require("../RequestShortener");
1112
const ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
1213
const uglify = require("uglify-js");
@@ -102,6 +103,54 @@ class UglifyJsPlugin {
102103
for(let k in options.output) {
103104
output[k] = options.output[k];
104105
}
106+
const extractedComments = [];
107+
if(options.extractComments) {
108+
const condition = {};
109+
if(typeof options.extractComments === "string" || options.extractComments instanceof RegExp) {
110+
// extractComments specifies the extract condition and output.comments specifies the preserve condition
111+
condition.preserve = output.comments;
112+
condition.extract = options.extractComments;
113+
} else if(Object.prototype.hasOwnProperty.call(options.extractComments, "condition")) {
114+
// Extract condition is given in extractComments.condition
115+
condition.preserve = output.comments;
116+
condition.extract = options.extractComments.condition;
117+
} else {
118+
// No extract condition is given. Extract comments that match output.comments instead of preserving them
119+
condition.preserve = false;
120+
condition.extract = output.comments;
121+
}
122+
123+
// Ensure that both conditions are functions
124+
["preserve", "extract"].forEach(key => {
125+
switch(typeof condition[key]) {
126+
case "boolean":
127+
var b = condition[key];
128+
condition[key] = () => b;
129+
case "function": // eslint-disable-line no-fallthrough
130+
break;
131+
case "string":
132+
if(condition[key] === "all") {
133+
condition[key] = () => true;
134+
break;
135+
}
136+
condition[key] = new RegExp(condition[key]);
137+
default: // eslint-disable-line no-fallthrough
138+
var regex = condition[key];
139+
condition[key] = (astNode, comment) => regex.test(comment.value);
140+
}
141+
});
142+
143+
// Redefine the comments function to extract and preserve
144+
// comments according to the two conditions
145+
output.comments = (astNode, comment) => {
146+
if(condition.extract(astNode, comment)) {
147+
extractedComments.push(
148+
comment.type === "comment2" ? "/*" + comment.value + "*/" : "//" + comment.value
149+
);
150+
}
151+
return condition.preserve(astNode, comment);
152+
};
153+
}
105154
let map;
106155
if(options.sourceMap) {
107156
map = uglify.SourceMap({ // eslint-disable-line new-cap
@@ -117,6 +166,41 @@ class UglifyJsPlugin {
117166
asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
118167
new SourceMapSource(stringifiedStream, file, JSON.parse(map), input, inputSourceMap) :
119168
new RawSource(stringifiedStream));
169+
if(extractedComments.length > 0) {
170+
let commentsFile = options.extractComments.file || file + ".LICENSE";
171+
if(typeof commentsFile === "function") {
172+
commentsFile = commentsFile(file);
173+
}
174+
175+
// Write extracted comments to commentsFile
176+
const commentsSource = new RawSource(extractedComments.join("\n\n") + "\n");
177+
if(commentsFile in compilation.assets) {
178+
// commentsFile already exists, append new comments...
179+
if(compilation.assets[commentsFile] instanceof ConcatSource) {
180+
compilation.assets[commentsFile].add("\n");
181+
compilation.assets[commentsFile].add(commentsSource);
182+
} else {
183+
compilation.assets[commentsFile] = new ConcatSource(
184+
compilation.assets[commentsFile], "\n", commentsSource
185+
);
186+
}
187+
} else {
188+
compilation.assets[commentsFile] = commentsSource;
189+
}
190+
191+
// Add a banner to the original file
192+
if(options.extractComments.banner !== false) {
193+
let banner = options.extractComments.banner || "For license information please see " + commentsFile;
194+
if(typeof banner === "function") {
195+
banner = banner(commentsFile);
196+
}
197+
if(banner) {
198+
asset.__UglifyJsPlugin = compilation.assets[file] = new ConcatSource(
199+
"/*! " + banner + " */\n", compilation.assets[file]
200+
);
201+
}
202+
}
203+
}
120204
if(warnings.length > 0) {
121205
compilation.warnings.push(new Error(file + " from UglifyJs\n" + warnings.join("\n")));
122206
}

‎test/UglifyJsPlugin.test.js

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,16 @@ describe("UglifyJsPlugin", function() {
224224
},
225225
mangle: false,
226226
beautify: true,
227-
comments: false
227+
comments: false,
228+
extractComments: {
229+
condition: 'should be extracted',
230+
file: function(file) {
231+
return file.replace(/(\.\w+)$/, '.license$1');
232+
},
233+
banner: function(licenseFile) {
234+
return 'License information can be found in ' + licenseFile;
235+
}
236+
}
228237
});
229238
plugin.apply(compilerEnv);
230239
eventBindings = pluginEnvironment.getEventBindings();
@@ -305,6 +314,19 @@ describe("UglifyJsPlugin", function() {
305314
};
306315
},
307316
},
317+
"test4.js": {
318+
source: function() {
319+
return "/*! this comment should be extracted */ function foo(longVariableName) { /* this will not be extracted */ longVariableName = 1; } // another comment that should be extracted to a separate file\n function foo2(bar) { return bar; }";
320+
},
321+
map: function() {
322+
return {
323+
version: 3,
324+
sources: ["test.js"],
325+
names: ["foo", "longVariableName"],
326+
mappings: "AAAA,QAASA,KAAIC,kBACTA,iBAAmB"
327+
};
328+
}
329+
},
308330
};
309331
compilation.errors = [];
310332
compilation.warnings = [];
@@ -524,6 +546,120 @@ describe("UglifyJsPlugin", function() {
524546
});
525547
});
526548
});
549+
550+
it("extracts license information to separate file", function() {
551+
compilationEventBinding.handler([{
552+
files: ["test4.js"]
553+
}], function() {
554+
compilation.errors.length.should.be.exactly(0);
555+
compilation.assets["test4.license.js"]._value.should.containEql("/*! this comment should be extracted */");
556+
compilation.assets["test4.license.js"]._value.should.containEql("// another comment that should be extracted to a separate file");
557+
compilation.assets["test4.license.js"]._value.should.not.containEql("/* this will not be extracted */");
558+
});
559+
});
560+
});
561+
});
562+
});
563+
});
564+
565+
describe("when applied with extract option set to a single file", function() {
566+
let eventBindings;
567+
let eventBinding;
568+
569+
beforeEach(function() {
570+
const pluginEnvironment = new PluginEnvironment();
571+
const compilerEnv = pluginEnvironment.getEnvironmentStub();
572+
compilerEnv.context = "";
573+
574+
const plugin = new UglifyJsPlugin({
575+
comments: "all",
576+
extractComments: {
577+
condition: /.*/,
578+
file: "extracted-comments.js"
579+
}
580+
});
581+
plugin.apply(compilerEnv);
582+
eventBindings = pluginEnvironment.getEventBindings();
583+
});
584+
585+
it("binds one event handler", function() {
586+
eventBindings.length.should.be.exactly(1);
587+
});
588+
589+
describe("compilation handler", function() {
590+
beforeEach(function() {
591+
eventBinding = eventBindings[0];
592+
});
593+
594+
it("binds to compilation event", function() {
595+
eventBinding.name.should.be.exactly("compilation");
596+
});
597+
598+
describe("when called", function() {
599+
let chunkPluginEnvironment;
600+
let compilationEventBindings;
601+
let compilationEventBinding;
602+
let compilation;
603+
604+
beforeEach(function() {
605+
chunkPluginEnvironment = new PluginEnvironment();
606+
compilation = chunkPluginEnvironment.getEnvironmentStub();
607+
compilation.assets = {
608+
"test.js": {
609+
source: function() {
610+
return "/* This is a comment from test.js */ function foo(bar) { return bar; }";
611+
}
612+
},
613+
"test2.js": {
614+
source: function() {
615+
return "// This is a comment from test2.js\nfunction foo2(bar) { return bar; }";
616+
}
617+
},
618+
"test3.js": {
619+
source: function() {
620+
return "/* This is a comment from test3.js */ function foo3(bar) { return bar; }\n// This is another comment from test3.js\nfunction foobar3(baz) { return baz; }";
621+
}
622+
},
623+
};
624+
compilation.errors = [];
625+
compilation.warnings = [];
626+
627+
eventBinding.handler(compilation);
628+
compilationEventBindings = chunkPluginEnvironment.getEventBindings();
629+
});
630+
631+
it("binds one event handler", function() {
632+
compilationEventBindings.length.should.be.exactly(1);
633+
});
634+
635+
describe("optimize-chunk-assets handler", function() {
636+
beforeEach(function() {
637+
compilationEventBinding = compilationEventBindings[0];
638+
});
639+
640+
it("preserves comments", function() {
641+
compilationEventBinding.handler([{
642+
files: ["test.js", "test2.js", "test3.js"]
643+
}], function() {
644+
compilation.assets["test.js"].source().should.containEql("/*");
645+
compilation.assets["test2.js"].source().should.containEql("//");
646+
compilation.assets["test3.js"].source().should.containEql("/*");
647+
compilation.assets["test3.js"].source().should.containEql("//");
648+
});
649+
});
650+
651+
it("extracts comments to specified file", function() {
652+
compilationEventBinding.handler([{
653+
files: ["test.js", "test2.js", "test3.js"]
654+
}], function() {
655+
compilation.errors.length.should.be.exactly(0);
656+
compilation.assets["extracted-comments.js"].source().should.containEql("/* This is a comment from test.js */");
657+
compilation.assets["extracted-comments.js"].source().should.containEql("// This is a comment from test2.js");
658+
compilation.assets["extracted-comments.js"].source().should.containEql("/* This is a comment from test3.js */");
659+
compilation.assets["extracted-comments.js"].source().should.containEql("// This is another comment from test3.js");
660+
compilation.assets["extracted-comments.js"].source().should.not.containEql("function");
661+
});
662+
});
527663
});
528664
});
529665
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** @preserve comment should be extracted extract-test.1 */
2+
3+
var foo = {};
4+
5+
// comment should be stripped extract-test.2
6+
7+
/*!
8+
* comment should be extracted extract-test.3
9+
*/
10+
11+
/**
12+
* comment should be stripped extract-test.4
13+
*/
14+
15+
module.exports = foo;

‎test/configCases/plugins/uglifyjs-plugin/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,25 @@ it("should pass mangle options", function() {
2323
source.should.containEql("function r(n){return function(n){try{t()}catch(t){n(t)}}}");
2424
});
2525

26+
it("should extract comments to separate file", function() {
27+
var fs = require("fs"),
28+
path = require("path");
29+
var source = fs.readFileSync(path.join(__dirname, "extract.js.LICENSE"), "utf-8");
30+
source.should.containEql("comment should be extracted extract-test.1");
31+
source.should.not.containEql("comment should be stripped extract-test.2");
32+
source.should.containEql("comment should be extracted extract-test.3");
33+
source.should.not.containEql("comment should be stripped extract-test.4");
34+
});
35+
36+
it("should remove extracted comments and insert a banner", function() {
37+
var fs = require("fs"),
38+
path = require("path");
39+
var source = fs.readFileSync(path.join(__dirname, "extract.js"), "utf-8");
40+
source.should.not.containEql("comment should be extracted extract-test.1");
41+
source.should.not.containEql("comment should be stripped extract-test.2");
42+
source.should.not.containEql("comment should be extracted extract-test.3");
43+
source.should.not.containEql("comment should be stripped extract-test.4");
44+
source.should.containEql("/*! For license information please see extract.js.LICENSE */");
45+
});
2646

2747
require.include("./test.js");

‎test/configCases/plugins/uglifyjs-plugin/webpack.config.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@ module.exports = {
77
entry: {
88
bundle0: ["./index.js"],
99
vendors: ["./vendors.js"],
10-
ie8: ["./ie8.js"]
10+
ie8: ["./ie8.js"],
11+
extract: ["./extract.js"]
1112
},
1213
output: {
1314
filename: "[name].js"
1415
},
1516
plugins: [
1617
new webpack.optimize.UglifyJsPlugin({
1718
comments: false,
18-
exclude: ["vendors.js"],
19+
exclude: ["vendors.js", "extract.js"],
1920
mangle: {
2021
screw_ie8: false
2122
}
22-
})
23+
}),
24+
new webpack.optimize.UglifyJsPlugin({
25+
extractComments: true,
26+
include: ["extract.js"],
27+
mangle: {
28+
screw_ie8: false
29+
}
30+
}),
2331
]
2432
};

0 commit comments

Comments
 (0)
Please sign in to comment.