Skip to content
This repository has been archived by the owner on Dec 5, 2019. It is now read-only.

feat: add support for parallelization && caching (options.parallel) #77

Merged
merged 35 commits into from
Jul 20, 2017
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b9ff34b
refactor: minify()
aui Jul 8, 2017
43208ec
add: Use multi-process and cache to speed up the build
aui Jul 8, 2017
754037c
add: maxWorkers、cache、cacheDirectory
aui Jul 8, 2017
f3f0656
test: Disable caching
aui Jul 8, 2017
04ea42b
Use multi-process and cache to speed up the build
aui Jul 8, 2017
7e9c080
fix: cache bug
aui Jul 8, 2017
acd748b
test: Disable caching
aui Jul 8, 2017
aca16f4
test: uglify
aui Jul 9, 2017
67ae3fb
merge: upstream/master
aui Jul 10, 2017
8e50528
Keep the "WebPack-default" consistent
aui Jul 10, 2017
744a435
refactor: options.parallel
aui Jul 10, 2017
90ef4d4
refactor
aui Jul 10, 2017
289f5d0
refactor: compute-cluster -> worker-farm
aui Jul 10, 2017
2f63c88
fix: options.parallel
aui Jul 10, 2017
bd20b62
test: Disable caching
aui Jul 10, 2017
b2537a3
test: worker.js
aui Jul 10, 2017
36f455e
test: coverage
aui Jul 10, 2017
1a51733
test: fix
aui Jul 11, 2017
24dcfb4
refactor
aui Jul 11, 2017
44aff1c
refactor: options.parallel.workers
aui Jul 11, 2017
b944ffa
refactor: serialization
aui Jul 11, 2017
b7fe125
refactor: serialization
aui Jul 12, 2017
48fee75
refactor: rename the internal variable
aui Jul 12, 2017
5d1d3ec
test: options.parallel
aui Jul 12, 2017
16a270e
refactor: remove `Date`
aui Jul 12, 2017
68d6b58
refactor: serialization
aui Jul 13, 2017
fdea537
refactor: use eslint for serialization checks
aui Jul 17, 2017
9ecb608
refactor: remove eslint
aui Jul 18, 2017
50035c5
refactor: the default configuration does not use "parallel"
aui Jul 18, 2017
29ec4b1
refactor: rename variable
aui Jul 19, 2017
7051b8e
test: remove private API
aui Jul 19, 2017
cef8262
refactor: safeguard to not spawn more workers
aui Jul 19, 2017
3a68612
add: options.parallel
aui Jul 19, 2017
3aebef6
test: clean up
aui Jul 20, 2017
e53ae99
refactor: buildError
aui Jul 20, 2017
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
917 changes: 467 additions & 450 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
"webpack-defaults": "webpack-defaults"
},
"dependencies": {
"cacache": "^9.2.9",
"eslint": "^4.0.0",
"espree": "^3.4.3",
"find-cache-dir": "^1.0.0",
"source-map": "^0.5.6",
"uglify-es": "^3.0.24",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already included in #74 I will merge that PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#74 merged, this should also update some of the snapshots you updated aswell :)

"webpack-sources": "^1.0.1"
"webpack-sources": "^1.0.1",
"worker-farm": "^1.4.1"
},
"devDependencies": {
"babel-cli": "^6.24.1",
Expand Down
232 changes: 81 additions & 151 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@ import { SourceMapConsumer } from 'source-map';
import { SourceMapSource, RawSource, ConcatSource } from 'webpack-sources';
import RequestShortener from 'webpack/lib/RequestShortener';
import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers';
import uglify from 'uglify-es';
import Uglify from './uglify';

/* eslint-disable
no-param-reassign
*/

const warningRegex = /\[.+:([0-9]+),([0-9]+)\]/;

const defaultUglifyOptions = {
output: {
comments: /^\**!|@preserve|@license|@cc_on/,
beautify: false,
semicolons: true,
shebang: true,
},
};

class UglifyJsPlugin {
constructor(options) {
if (typeof options !== 'object' || Array.isArray(options)) {
Expand All @@ -34,23 +25,8 @@ class UglifyJsPlugin {

this.options.test = this.options.test || /\.js($|\?)/i;
this.options.warningsFilter = this.options.warningsFilter || (() => true);

this.uglifyOptions = this.options.uglifyOptions || {};
}

static buildDefaultUglifyOptions({ ecma, warnings, parse = {}, compress = {}, mangle, output, toplevel, ie8 }) {
return {
ecma,
warnings,
parse,
compress,
mangle: mangle == null ? true : mangle,
// Ignoring sourcemap from options
sourceMap: null,
output: { ...defaultUglifyOptions.output, ...output },
toplevel,
ie8,
};
this.options.uglifyOptions = this.options.uglifyOptions || {};
this.options.parallel = this.options.parallel || {};
}

static buildError(err, file, sourceMap, requestShortener) {
Expand All @@ -64,8 +40,8 @@ class UglifyJsPlugin {
return new Error(`${file} from UglifyJs\n${err.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${err.line},${err.col}]`);
}
return new Error(`${file} from UglifyJs\n${err.message} [${file}:${err.line},${err.col}]`);
} else if (err.msg) {
return new Error(`${file} from UglifyJs\n${err.msg}`);
} else if (err.message) {
return new Error(`${file} from UglifyJs\n${err.message}`);
}
return new Error(`${file} from UglifyJs\n${err.stack}`);
}
Expand All @@ -91,71 +67,9 @@ class UglifyJsPlugin {
}, []);
}

static buildCommentsFunction(options, uglifyOptions, extractedComments) {
const condition = {};
const commentsOpts = uglifyOptions.output.comments;
if (typeof options.extractComments === 'string' || options.extractComments instanceof RegExp) {
// extractComments specifies the extract condition and commentsOpts specifies the preserve condition
condition.preserve = commentsOpts;
condition.extract = options.extractComments;
} else if (Object.prototype.hasOwnProperty.call(options.extractComments, 'condition')) {
// Extract condition is given in extractComments.condition
condition.preserve = commentsOpts;
condition.extract = options.extractComments.condition;
} else {
// No extract condition is given. Extract comments that match commentsOpts instead of preserving them
condition.preserve = false;
condition.extract = commentsOpts;
}

// Ensure that both conditions are functions
['preserve', 'extract'].forEach((key) => {
let regexStr;
let regex;
switch (typeof (condition[key])) {
case 'boolean':
condition[key] = condition[key] ? () => true : () => false;
break;
case 'function':
break;
case 'string':
if (condition[key] === 'all') {
condition[key] = () => true;
break;
}
if (condition[key] === 'some') {
condition[key] = (astNode, comment) => comment.type === 'comment2' && /@preserve|@license|@cc_on/i.test(comment.value);
break;
}
regexStr = condition[key];
condition[key] = (astNode, comment) => new RegExp(regexStr).test(comment.value);
break;
default:
regex = condition[key];
condition[key] = (astNode, comment) => (regex.test(comment.value));
}
});

// Redefine the comments function to extract and preserve
// comments according to the two conditions
return (astNode, comment) => {
if (condition.extract(astNode, comment)) {
extractedComments.push(
comment.type === 'comment2' ? `/*${comment.value}*/` : `//${comment.value}`,
);
}
return condition.preserve(astNode, comment);
};
}

apply(compiler) {
const requestShortener = new RequestShortener(compiler.context);
// Copy uglify options
const uglifyOptions = UglifyJsPlugin.buildDefaultUglifyOptions(this.uglifyOptions);
// Making sure output options exists if there is an extractComments options
if (this.options.extractComments) {
uglifyOptions.output = uglifyOptions.output || {};
}


compiler.plugin('compilation', (compilation) => {
if (this.options.sourceMap) {
Expand All @@ -166,13 +80,13 @@ class UglifyJsPlugin {
}

compilation.plugin('optimize-chunk-assets', (chunks, callback) => {
const uglify = new Uglify(this.options.parallel);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!this.options.parallel || (os.cpus().length - 1) === 0) we need to use uglify.minify() as we do atm, without any worker related logic attached please :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest commit: uglify/index.js handles this logic, it keeps the plugin's original workflow :)

const uglifiedAssets = new WeakSet();
const tasks = [];
chunks.reduce((acc, chunk) => acc.concat(chunk.files || []), [])
.concat(compilation.additionalChunkAssets || [])
.filter(ModuleFilenameHelpers.matchObject.bind(null, this.options))
.forEach((file) => {
// Reseting sourcemap to null
uglifyOptions.sourceMap = null;
let sourceMap;
const asset = compilation.assets[file];
if (uglifiedAssets.has(asset)) {
Expand All @@ -182,6 +96,7 @@ class UglifyJsPlugin {
try {
let input;
let inputSourceMap;
const cacheKey = `${compiler.outputPath}/${file}`;
if (this.options.sourceMap) {
if (asset.sourceAndMap) {
const sourceAndMap = asset.sourceAndMap();
Expand All @@ -192,89 +107,104 @@ class UglifyJsPlugin {
input = asset.source();
}
sourceMap = new SourceMapConsumer(inputSourceMap);
// Add source map data
uglifyOptions.sourceMap = {
content: inputSourceMap,
};
} else {
input = asset.source();
}

// Handling comment extraction
const extractedComments = [];
let commentsFile = false;
if (this.options.extractComments) {
uglifyOptions.output.comments = UglifyJsPlugin.buildCommentsFunction(this.options, uglifyOptions, extractedComments);

commentsFile = this.options.extractComments.filename || `${file}.LICENSE`;
if (typeof commentsFile === 'function') {
commentsFile = commentsFile(file);
}
}

// Calling uglify
const { error, map, code, warnings } = uglify.minify({ [file]: input }, uglifyOptions);
tasks.push({
cacheKey,
file,
input,
sourceMap,
inputSourceMap,
commentsFile,
extractComments: this.options.extractComments,
uglifyOptions: this.options.uglifyOptions,
});
} catch (error) {
compilation.errors.push(UglifyJsPlugin.buildError(error, file, sourceMap, compilation, requestShortener));
}
});

// Handling results
// Error case: add errors, and go to next file
if (error) {
compilation.errors.push(UglifyJsPlugin.buildError(error, file, sourceMap, compilation, requestShortener));
return;
}
uglify.runTasks(tasks, (tasksError, results) => {
if (tasksError) {
compilation.errors.push(tasksError);
return;
}

let outputSource;
if (map) {
outputSource = new SourceMapSource(code, file, JSON.parse(map), input, inputSourceMap);
} else {
outputSource = new RawSource(code);
}
results.forEach((data, index) => {
const { file, input, sourceMap, inputSourceMap, commentsFile } = tasks[index];
const { error, map, code, warnings, extractedComments } = data;

// Handling results
// Error case: add errors, and go to next file
if (error) {
compilation.errors.push(UglifyJsPlugin.buildError(error, file, sourceMap, compilation, requestShortener));
return;
}

let outputSource;
if (map) {
outputSource = new SourceMapSource(code, file, JSON.parse(map), input, inputSourceMap);
} else {
outputSource = new RawSource(code);
}

// Write extracted comments to commentsFile
if (commentsFile && extractedComments.length > 0) {
// Add a banner to the original file
if (this.options.extractComments.banner !== false) {
let banner = this.options.extractComments.banner || `For license information please see ${commentsFile}`;
if (typeof banner === 'function') {
banner = banner(commentsFile);
}
if (banner) {
outputSource = new ConcatSource(
`/*! ${banner} */\n`, outputSource,
);
}
// Write extracted comments to commentsFile
if (commentsFile && extractedComments.length > 0) {
// Add a banner to the original file
if (this.options.extractComments.banner !== false) {
let banner = this.options.extractComments.banner || `For license information please see ${commentsFile}`;
if (typeof banner === 'function') {
banner = banner(commentsFile);
}
if (banner) {
outputSource = new ConcatSource(
`/*! ${banner} */\n`, outputSource,
);
}
}

const commentsSource = new RawSource(`${extractedComments.join('\n\n')}\n`);
if (commentsFile in compilation.assets) {
// commentsFile already exists, append new comments...
if (compilation.assets[commentsFile] instanceof ConcatSource) {
compilation.assets[commentsFile].add('\n');
compilation.assets[commentsFile].add(commentsSource);
} else {
compilation.assets[commentsFile] = new ConcatSource(
compilation.assets[commentsFile], '\n', commentsSource,
);
}
const commentsSource = new RawSource(`${extractedComments.join('\n\n')}\n`);
if (commentsFile in compilation.assets) {
// commentsFile already exists, append new comments...
if (compilation.assets[commentsFile] instanceof ConcatSource) {
compilation.assets[commentsFile].add('\n');
compilation.assets[commentsFile].add(commentsSource);
} else {
compilation.assets[commentsFile] = commentsSource;
compilation.assets[commentsFile] = new ConcatSource(
compilation.assets[commentsFile], '\n', commentsSource,
);
}
} else {
compilation.assets[commentsFile] = commentsSource;
}
}

// Updating assets
uglifiedAssets.add(compilation.assets[file] = outputSource);
// Updating assets
uglifiedAssets.add(compilation.assets[file] = outputSource);

// Handling warnings
if (warnings) {
const warnArr = UglifyJsPlugin.buildWarnings(warnings, file, sourceMap, this.options.warningsFilter, requestShortener);
if (warnArr.length > 0) {
compilation.warnings.push(new Error(`${file} from UglifyJs\n${warnArr.join('\n')}`));
}
// Handling warnings
if (warnings) {
const warnArr = UglifyJsPlugin.buildWarnings(warnings, file, sourceMap, this.options.warningsFilter, requestShortener);
if (warnArr.length > 0) {
compilation.warnings.push(new Error(`${file} from UglifyJs\n${warnArr.join('\n')}`));
}
} catch (error) {
compilation.errors.push(UglifyJsPlugin.buildError(error, file, sourceMap, compilation, requestShortener));
}
});
callback();

uglify.exit();
callback();
});
});
});
}
Expand Down
20 changes: 20 additions & 0 deletions src/uglify/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import crypto from 'crypto';
import cacache from 'cacache';

const sha512 = 'sha512';
const getHash = data => `${sha512}-${
crypto.createHash(sha512).update(data).digest('base64')
}`;

export const get = (cacheDirectory, key, identifier) => cacache.get(cacheDirectory, key).then(({ data, metadata }) => {
const hash = getHash(identifier);
if (metadata.hash !== hash) {
return Promise.reject(new Error('The cache has expired'));
}
return JSON.parse(data);
});

export const put = (cacheDirectory, key, data, identifier) => {
const hash = getHash(identifier);
return cacache.put(cacheDirectory, key, JSON.stringify(data), { metadata: { hash } });
};
Loading