gulp-pipemin
Version:
Streamlined resource transformations configured in html.
414 lines (357 loc) • 12.1 kB
JavaScript
var path = require('path');
var fs = require('fs');
var EOL = require('os').EOL;
var through = require('through2');
var gutil = require('gulp-util');
var glob = require('glob');
var minimatch = require('minimatch');
var when = require('when');
var es = require('event-stream');
var Readable = require('stream').Readable;
var gulpConcat = require('gulp-concat');
var _ = require('lodash');
module.exports = function (options) {
options = options || {};
var startReg = /<!--\s*build:(\w+)(?:\(([^\)]+?)\))?\s+(\/?([^\s]+?))?\s*-->/gim;
var endReg = /<!--\s*endbuild\s*-->/gim;
var jsReg = /<\s*script\s+.*?src\s*=\s*("|')((?:[^\1]|\\\1)+?)\1.*?><\s*\/\s*script\s*>/gi;
var cssReg = /<\s*link\s+.*?href\s*=\s*("|')((?:[^\1]|\\\1)+?)\1.*?>/gi;
var startCondReg = /<!--\[[^\]]+\]>/gim;
var endCondReg = /<!\[endif\]-->/gim;
var basePath, mainPath, mainName, alternatePath;
function getPath (name) {
var filePath = path.join(path.relative(basePath, mainPath), name);
var isStatic = name.split('.').pop() === 'js' || name.split('.').pop() === 'css';
if (options.outputRelativePath && isStatic) {
filePath = options.outputRelativePath + name;
}
return filePath;
}
function createFile (name, content) {
return new gutil.File({
path: getPath(name),
contents: new Buffer(content)
})
}
function getBlockType (content) {
return !cssReg.test(content) ? 'js' : 'css';
}
function readStream (streamCtor, callback) {
var files = [];
var deferred = when.defer();
var stream = streamCtor();
stream.on('data', function (file) {
if (file.isStream()) {
this.emit('error', gutil.PluginError('gulp-usemin', 'Streams in assets are not supported!'));
}
file.base = path.resolve(file.base);
file.path = path.resolve(file.path);
files.push(file);
});
stream.on('end', function () {
if (options.debugStreamFiles) {
console.log('asssets:\n', files.map(function (f) {
return f.base + ' :: ' + f.path;
}).join('\n '));
}
deferred.resolve(files);
});
return deferred.promise;
}
function produceMatcher (fileArray) {
var allFiles = fileArray.map(function (file) {
return file.path;
});
var filesByPath = fileArray.reduce(function (obj, file) {
obj[file.path] = file;
return obj;
}, {});
var notMatched = allFiles.slice(); // clone
return {
matching: function (patterns) {
function doMatch (pattern) {
return minimatch.match(allFiles, pattern);
}
var incs = patterns.inc.map(doMatch);
var excs = patterns.exc.map(doMatch);
var matched = _.difference(_.union.apply(null, incs), _.union.apply(null, excs));
// filter array
notMatched = notMatched.filter(function (i) {
return matched.indexOf(i) < 0;
});
return matched.map(function (path) {
return filesByPath[path];
});
},
notMatched: function () {
return notMatched.map(function (path) {
return filesByPath[path];
});
}
}
}
function getFiles (content, reg, alternatePath, matcherPromise) {
var paths = [];
var promises = [];
var files = [];
var i, l;
var arrayDetect = /^\[.*\]$/;
var arrayParse = /'((?:[^']|\\')*[^'\\])'(?:\s*,\s*)?/g;
content
.replace(startCondReg, '')
.replace(endCondReg, '')
.replace(/<!--(?:(?:.|\r|\n)*?)-->/gim, '')
.replace(reg, function (a, quote, pathPattern) {
var patterns;
var arrayDetected = pathPattern.match(arrayDetect);
if (arrayDetected) {
patterns = [];
pathPattern.replace(arrayParse, function (a, pattern) {
patterns.push(pattern);
});
}
else {
patterns = [pathPattern];
}
var inc = [],
exc = [];
_.each(patterns, function (pattern) {
var isExc = false;
if (pattern[0] === '!') {
isExc = true;
pattern = pattern.slice(1);
}
var filePath = path.resolve(path.join(alternatePath || options.path || mainPath, pattern));
if (options.assetsDir) {
filePath = path.resolve(path.join(options.assetsDir, path.relative(basePath, filePath)));
}
(isExc ? exc : inc).push(filePath);
});
paths.push({inc: inc, exc: exc, src: pathPattern});
});
if (!matcherPromise) {
function globSync (pattern) {
return glob.sync(pattern);
}
// read files from filesystem
for (i = 0, l = paths.length; i < l; ++i) {
var incs = paths[i].inc.map(globSync);
var excs = paths[i].exc.map(globSync);
var filepaths = _.difference(_.union.apply(null, incs), _.union.apply(null, excs));
if (filepaths[0] === undefined) {
throw new gutil.PluginError('gulp-usemin', 'Path ' + paths[i] + ' not found!');
}
promises.push.apply(promises, filepaths.map(function (filepath) {
var fileDeferred = when.defer();
fs.readFile(filepath, function (err, data) {
if (err) {
throw err;
}
fileDeferred.resolve(new gutil.File({
path: filepath,
contents: data
}));
});
return fileDeferred.promise;
}));
}
return when.all(promises);
}
else {
// read files from stream
for (i = 0, l = paths.length; i < l; ++i) {
var matching = matcherPromise.matching(paths[i]);
if (matching[0] === undefined) {
throw new gutil.PluginError('gulp-usemin', 'Pattern ' + paths[i].src + ' not in stream!');
}
files.push.apply(files, matching);
}
return when.resolve(files);
}
}
function concatThrough (name) {
return gulpConcat(name);
}
function wrapLazypipe (lazypipe) {
return function (stream, concat) {
if (!_.isUndefined(concat)) {
return stream
.pipe(concat)
.pipe(lazypipe());
}
else {
return stream
.pipe(lazypipe());
}
};
}
function processTask (pipeline, name, files) {
if (pipeline === null) {
return null;
}
var tip = new Readable({objectMode: true});
tip._read = function () {
if (files.length > 0) {
var file = files.shift();
this.push(file);
}
else {
this.push(null);
}
};
var concatTask = name ? concatThrough(name) : undefined;
if (typeof pipeline === 'function') {
// lazypipe support
if (typeof pipeline.pipe === 'function') {
return wrapLazypipe(pipeline)(tip, concatTask);
}
return pipeline(tip, concatTask);
}
else if (name) {
return tip.pipe(concatTask);
}
else {
return tip;
}
}
function process (name, files, pipelineId) {
var pipeline = options[pipelineId];
if (typeof pipeline === 'undefined') {
pipeline = [];
}
return processTask(pipeline, name, files);
}
function processHtml (content, matcherProducer) {
var html = [];
var sections = content.split(endReg);
var promise = when.resolve();
var streams = [];
for (var i = 0, l = sections.length; i < l; ++i) {
if (sections[i].match(startReg)) {
var section = sections[i].split(startReg);
alternatePath = section[2];
(function (section) {
promise = promise
.then(function () {
html.push(section[0]);
});
}(section));
var startCondLine = section[5].match(startCondReg);
var endCondLine = section[5].match(endCondReg);
if (startCondLine && endCondLine) {
(function (startCondLine) {
promise = promise.then(function () {
html.push(startCondLine[0]);
});
}(startCondLine))
}
if (section[1] !== 'remove') {
(function (section, alternatePath) {
var type = getBlockType(section[5]);
promise = promise
.then(matcherProducer)
.then(function (matcher) {
return getFiles(section[5], type === 'js' ? jsReg : cssReg, alternatePath, matcher)
})
.then(function (files) {
var name = section[4];
streams.push(process(name, files, section[1]));
var filePaths = name ? [section[3]] : files.map(function (f) {
return '/' + path.relative(f.base, f.path).split(path.sep).join('/');
});
filePaths
.map(function (path) {
return [path, getPath(path)]
})
.forEach(function (filePath) {
var relPath = filePath[0].replace(path.basename(filePath[0]), path.basename(filePath[1]));
if (type === 'js') {
html.push('<script src="' + relPath + '"></script>');
} else {
html.push('<link rel="stylesheet" href="' + relPath + '"/>');
}
});
});
}(section, alternatePath));
}
if (startCondLine && endCondLine) {
(function (endCondLine) {
promise = promise.then(function () {
html.push(endCondLine[0]);
});
}(endCondLine));
}
}
else {
(function (section) {
promise = promise.then(function () {
html.push(section);
});
}(sections[i]))
}
}
return promise.then(function () {
streams.push(process(mainName, [createFile(mainName, html.join(''))], 'html'));
return es.merge.apply(es, streams.filter(function (stream) {
return stream !== null;
}));
});
}
var matcherPromise, matcherProducer;
if (options.assetsStream) {
matcherPromise = readStream(options.assetsStream)
.then(function (filesList) {
return produceMatcher(filesList);
});
matcherProducer = function () {
return matcherPromise;
}
}
return through.obj(function (file, enc, callback) {
if (file.isNull()) {
this.push(file); // Do nothing if no contents
callback();
}
else if (file.isStream()) {
this.emit('error', new gutil.PluginError('gulp-usemin', 'Streams are not supported!'));
callback();
}
else {
basePath = file.base;
mainPath = path.dirname(file.path);
mainName = path.basename(file.path);
var push = this.push.bind(this);
processHtml(String(file.contents), matcherProducer)
.then(function (stream) {
stream.on('data', function (file) {
push(file);
});
stream.on('end', function () {
callback();
})
});
}
}, function (callback) {
// push not processed files down the stream
if (options.other && matcherPromise) {
var push = this.push.bind(this);
matcherPromise.then(function (filesMatcher) {
var rest = filesMatcher.notMatched();
var stream = processTask(options.other, options.othersName, rest);
if (stream === null) {
callback();
return;
}
stream.on('data', function (file) {
push(file);
});
stream.on('end', function () {
callback();
})
});
}
else {
callback();
}
});
};