webpack-concat-plugin
Version:
Webpack file concatenation.
420 lines (357 loc) • 17.7 kB
JavaScript
;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* @file webpack-concat-plugin
* @author huangxueliang
*/
var fs = require('fs');
var Concat = require('concat-with-sourcemaps');
var UglifyJS = require('uglify-es');
var createHash = require('crypto').createHash;
var path = require('path');
var upath = require('upath');
var globby = require('globby');
var validateOptions = require('schema-utils');
var schema = require('./schema.json');
var ConcatPlugin = function () {
function ConcatPlugin(options) {
_classCallCheck(this, ConcatPlugin);
options = Object.assign({
uglify: false,
sourceMap: false,
fileName: '[name].js',
name: 'result',
injectType: 'prepend',
outputPath: ''
}, options);
if (!options.filesToConcat || !options.filesToConcat.length) {
throw new Error('webpackConcatPlugin: option filesToConcat is required and should not be empty');
}
validateOptions(schema, options, 'webpackConcatPlugin');
options.outputPath = options.outputPath && this.ensureTrailingSlash(options.outputPath);
this.settings = options;
// used to determine if we should emit files during compiler emit event
this.startTime = Date.now();
this.prevTimestamps = {};
this.needCreateNewFile = true;
}
_createClass(ConcatPlugin, [{
key: 'ensureTrailingSlash',
value: function ensureTrailingSlash(string) {
if (string.length && string.substr(-1, 1) !== '/') {
return `${string}/`;
}
return string;
}
}, {
key: 'getFileName',
value: function getFileName(files) {
var filePath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.settings.fileName;
if (!this.needCreateNewFile) {
return this.finalFileName;
}
var fileRegExp = /\[name\]/;
var hashRegExp = /\[hash(?:(?::)([\d]+))?\]/;
if (this.settings.useHash || hashRegExp.test(filePath)) {
var fileHash = this.hashFile(files);
if (!hashRegExp.test(filePath)) {
filePath = filePath.replace(/\.js$/, '.[hash].js');
}
var regResult = hashRegExp.exec(filePath);
var hashLength = regResult[1] ? Number(regResult[1]) : fileHash.length;
filePath = filePath.replace(hashRegExp, fileHash.slice(0, hashLength));
}
return filePath.replace(fileRegExp, this.settings.name);
}
}, {
key: 'hashFile',
value: function hashFile(files) {
if (this.fileHash && !this.needCreateNewFile) {
return this.fileHash;
}
var content = Object.keys(files).reduce(function (fileContent, fileName) {
return fileContent + files[fileName];
}, '');
var _settings = this.settings,
_settings$hashFunctio = _settings.hashFunction,
hashFunction = _settings$hashFunctio === undefined ? 'md5' : _settings$hashFunctio,
_settings$hashDigest = _settings.hashDigest,
hashDigest = _settings$hashDigest === undefined ? 'hex' : _settings$hashDigest;
this.fileHash = createHash(hashFunction).update(content).digest(hashDigest);
if (hashDigest === 'base64') {
// these are not safe url characters.
this.fileHash = this.fileHash.replace(/[/+=]/g, function (c) {
switch (c) {
case '/':
return '_';
case '+':
return '-';
case '=':
return '';
default:
return c;
}
});
}
return this.fileHash;
}
}, {
key: 'getRelativePathAsync',
value: function getRelativePathAsync(context) {
return Promise.all(this.settings.filesToConcat.map(function (f) {
if (globby.hasMagic(f)) {
return globby(f, {
cwd: context,
nodir: true
});
}
return f;
})).then(function (rawResult) {
return rawResult.reduce(function (target, resource) {
return target.concat(resource);
}, []);
}).catch(function (e) {
console.error(e);
});
}
}, {
key: 'resolveReadFiles',
value: function resolveReadFiles(compiler) {
var _this = this;
var self = this;
var readFilePromise = void 0;
var relativePathArrayPromise = this.getRelativePathAsync(compiler.options.context);
this.filesToConcatAbsolutePromise = new Promise(function (resolve, reject) {
compiler.resolverFactory.plugin('resolver normal', function (resolver) {
resolve(relativePathArrayPromise.then(function (relativeFilePathArray) {
return Promise.all(relativeFilePathArray.map(function (relativeFilePath) {
return new Promise(function (resolve, reject) {
return resolver.resolve({}, compiler.options.context, relativeFilePath, {}, function (err, filePath) {
if (err) {
reject(err);
} else {
resolve(filePath);
}
});
});
}));
}).catch(function (e) {
console.error(e);
}));
});
});
var createNewPromise = function createNewPromise() {
self.needCreateNewFile = true;
return _this.filesToConcatAbsolutePromise.then(function (filePathArray) {
return Promise.all(filePathArray.map(function (filePath) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, function (err, data) {
if (err) {
reject(err);
} else {
resolve({
['webpack:///' + upath.relative(compiler.options.context, filePath)]: data.toString()
});
}
});
});
}));
}).catch(function (e) {
console.error(e);
});
};
this.getReadFilePromise = function (createNew) {
if (!readFilePromise || createNew) {
readFilePromise = createNewPromise();
}
return readFilePromise;
};
}
}, {
key: 'resolveConcatAndUglify',
value: function resolveConcatAndUglify(compilation, files) {
var allFiles = files.reduce(function (file1, file2) {
return Object.assign(file1, file2);
}, {});
var content = '';
var mapContent = '';
this.finalFileName = this.getFileName(allFiles);
var fileBaseName = path.basename(this.finalFileName);
if (this.settings.uglify) {
var options = {};
if (typeof this.settings.uglify === 'object') {
options = Object.assign({}, this.settings.uglify, options);
}
if (this.settings.sourceMap) {
options.sourceMap = {
url: `${fileBaseName}.map`,
includeSources: true
};
}
var result = UglifyJS.minify(allFiles, options);
if (result.error) {
throw result.error;
}
content = result.code;
if (this.settings.sourceMap) {
mapContent = result.map.toString();
}
} else {
var concat = new Concat(!!this.settings.sourceMap, this.finalFileName, '\n');
Object.keys(allFiles).forEach(function (fileName) {
concat.add(fileName, allFiles[fileName]);
});
content = concat.content.toString();
if (this.settings.sourceMap) {
content += `//# sourceMappingURL=${fileBaseName}.map`;
mapContent = concat.sourceMap;
}
}
compilation.assets[`${this.settings.outputPath}${this.finalFileName}`] = {
source() {
return content;
},
size() {
return content.length;
}
};
compilation.hooks.moduleAsset.call({
userRequest: `${this.settings.outputPath}${this.settings.name}.js`
}, `${this.settings.outputPath}${this.finalFileName}`);
if (this.settings.sourceMap) {
compilation.assets[`${this.settings.outputPath}${this.finalFileName}.map`] = {
source() {
return mapContent;
},
size() {
return mapContent.length;
}
};
compilation.hooks.moduleAsset.call({
userRequest: `${this.settings.outputPath}${this.settings.name}.js.map`
}, `${this.settings.outputPath}${this.finalFileName}.map`);
}
this.needCreateNewFile = false;
}
}, {
key: 'apply',
value: function apply(compiler) {
var _this2 = this;
// ensure only compile one time per emit
var compileLoopStarted = false;
this.resolveReadFiles(compiler);
var self = this;
var dependenciesChanged = function dependenciesChanged(compilation, filesToConcatAbsolute) {
var fileTimestampsKeys = Object.keys(compilation.fileTimestamps);
// Since there are no time stamps, assume this is the first run and emit files
if (!fileTimestampsKeys.length) {
return true;
}
var changed = fileTimestampsKeys.filter(function (watchfile) {
return (self.prevTimestamps[watchfile] || self.startTime) < (compilation.fileTimestamps[watchfile] || Infinity);
}).some(function (f) {
return filesToConcatAbsolute.includes(f);
});
_this2.prevTimestamps = compilation.fileTimestamps;
return changed;
};
var processCompiling = function processCompiling(compilation, callback) {
self.filesToConcatAbsolutePromise.then(function (filesToConcatAbsolute) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = filesToConcatAbsolute[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var f = _step.value;
compilation.fileDependencies.add(path.relative(compiler.options.context, f));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
if (!dependenciesChanged(compilation, filesToConcatAbsolute)) {
return callback();
}
return self.getReadFilePromise(true).then(function (files) {
self.resolveConcatAndUglify(compilation, files);
callback();
});
}).catch(function (e) {
console.error(e);
});
};
compiler.hooks.compilation.tap('webpackConcatPlugin', function (compilation) {
var assetPath = void 0;
compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration && compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync('webpackConcatPlugin', function (htmlPluginData, callback) {
var getAssetPath = function getAssetPath() {
if (typeof self.settings.publicPath === 'undefined') {
if (typeof compilation.options.output.publicPath === 'undefined') {
return path.relative(path.dirname(htmlPluginData.outputName), `${self.settings.outputPath}${self.finalFileName}`);
}
return `${self.ensureTrailingSlash(compilation.options.output.publicPath)}${self.settings.outputPath}${self.finalFileName}`;
}
if (self.settings.publicPath === false) {
return path.relative(path.dirname(htmlPluginData.outputName), `${self.settings.outputPath}${self.finalFileName}`);
}
return `${self.ensureTrailingSlash(self.settings.publicPath)}${self.settings.outputPath}${self.finalFileName}`;
};
var injectToHtml = function injectToHtml() {
htmlPluginData.assets.webpackConcat = htmlPluginData.assets.webpackConcat || {};
assetPath = getAssetPath();
htmlPluginData.assets.webpackConcat[self.settings.name] = assetPath;
if (self.settings.injectType === 'prepend') {
htmlPluginData.assets.js.unshift(assetPath);
} else if (self.settings.injectType === 'append') {
htmlPluginData.assets.js.push(assetPath);
}
};
if (!self.finalFileName || !compileLoopStarted) {
compileLoopStarted = true;
processCompiling(compilation, function () {
injectToHtml();
callback(null, htmlPluginData);
});
} else {
injectToHtml();
callback(null, htmlPluginData);
}
});
compilation.hooks.htmlWebpackPluginAlterAssetTags && compilation.hooks.htmlWebpackPluginAlterAssetTags.tap('webpackConcatPlugin', function (htmlPluginData) {
if (self.settings.injectType !== 'none') {
var tags = htmlPluginData.head.concat(htmlPluginData.body);
var resultTag = tags.filter(function (tag) {
return tag.attributes.src === assetPath;
});
if (resultTag && resultTag.length && self.settings.attributes) {
Object.assign(resultTag[0].attributes, self.settings.attributes);
}
}
});
});
compiler.hooks.emit.tapAsync('webpackConcatPlugin', function (compilation, callback) {
if (!compileLoopStarted) {
compileLoopStarted = true;
processCompiling(compilation, callback);
} else {
callback();
}
});
compiler.hooks.afterEmit.tap('webpackConcatPlugin', function (compilation) {
compileLoopStarted = false;
});
}
}]);
return ConcatPlugin;
}();
module.exports = ConcatPlugin;