Skip to content

Commit d0f1f6e

Browse files
authored
Merge pull request webpack#4134 from SebastianS90/uglifyjs-extract-comments
UglifyJsPlugin: extract comments to separate file
2 parents f91cb92 + d2461da commit d0f1f6e

5 files changed

Lines changed: 272 additions & 5 deletions

File tree

lib/optimize/UglifyJsPlugin.js

Lines changed: 89 additions & 1 deletion
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,57 @@ 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+
break;
130+
case "function":
131+
break;
132+
case "string":
133+
if(condition[key] === "all") {
134+
condition[key] = () => true;
135+
break;
136+
}
137+
var regex = new RegExp(condition[key]);
138+
condition[key] = (astNode, comment) => regex.test(comment.value);
139+
break;
140+
default:
141+
regex = condition[key];
142+
condition[key] = (astNode, comment) => regex.test(comment.value);
143+
}
144+
});
145+
146+
// Redefine the comments function to extract and preserve
147+
// comments according to the two conditions
148+
output.comments = (astNode, comment) => {
149+
if(condition.extract(astNode, comment)) {
150+
extractedComments.push(
151+
comment.type === "comment2" ? "/*" + comment.value + "*/" : "//" + comment.value
152+
);
153+
}
154+
return condition.preserve(astNode, comment);
155+
};
156+
}
105157
let map;
106158
if(options.sourceMap) {
107159
map = uglify.SourceMap({ // eslint-disable-line new-cap
@@ -114,9 +166,45 @@ class UglifyJsPlugin {
114166
ast.print(stream);
115167
if(map) map = map + "";
116168
const stringifiedStream = stream + "";
117-
asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
169+
let outputSource = (map ?
118170
new SourceMapSource(stringifiedStream, file, JSON.parse(map), input, inputSourceMap) :
119171
new RawSource(stringifiedStream));
172+
if(extractedComments.length > 0) {
173+
let commentsFile = options.extractComments.filename || file + ".LICENSE";
174+
if(typeof commentsFile === "function") {
175+
commentsFile = commentsFile(file);
176+
}
177+
178+
// Write extracted comments to commentsFile
179+
const commentsSource = new RawSource(extractedComments.join("\n\n") + "\n");
180+
if(commentsFile in compilation.assets) {
181+
// commentsFile already exists, append new comments...
182+
if(compilation.assets[commentsFile] instanceof ConcatSource) {
183+
compilation.assets[commentsFile].add("\n");
184+
compilation.assets[commentsFile].add(commentsSource);
185+
} else {
186+
compilation.assets[commentsFile] = new ConcatSource(
187+
compilation.assets[commentsFile], "\n", commentsSource
188+
);
189+
}
190+
} else {
191+
compilation.assets[commentsFile] = commentsSource;
192+
}
193+
194+
// Add a banner to the original file
195+
if(options.extractComments.banner !== false) {
196+
let banner = options.extractComments.banner || "For license information please see " + commentsFile;
197+
if(typeof banner === "function") {
198+
banner = banner(commentsFile);
199+
}
200+
if(banner) {
201+
outputSource = new ConcatSource(
202+
"/*! " + banner + " */\n", outputSource
203+
);
204+
}
205+
}
206+
}
207+
asset.__UglifyJsPlugin = compilation.assets[file] = outputSource;
120208
if(warnings.length > 0) {
121209
compilation.warnings.push(new Error(file + " from UglifyJs\n" + warnings.join("\n")));
122210
}

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+
filename: 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+
filename: "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)