lilacs
Version:
A web frontend building tool for teamwork, with automaticly compiling, merging, minifying, syncing files to server, supporting distributed servers, ensuring css or html files' inline reference with correct absolute path, and more.
472 lines (350 loc) • 18.1 kB
JavaScript
var Gutil = require('gulp-util');
var Merge = require('merge');
var Path = require('path');
var Tool = require('./tool');
var Revisioner = (function () {
'use strict';
var Revisioner = function(options) {
var defaults = {
'hashLength': 8,
'dontGlobal': [ /^\/favicon.ico$/g ],
'dontRenameFile': [],
'dontUpdateReference': [],
'dontSearchFile': [],
'fileNameVersion': 'rev-version.json',
'fileNameManifest': 'rev-manifest.json',
'prefix': '',
'referenceToRegexs': referenceToRegexs,
'annotator': annotator,
'replacer': replacer,
'debug': false
};
this.options = Merge(defaults, options);
// File pool, any file passed into the Revisioner is stored in this object
this.files = {};
this.filesTemp = [];
// Stores the combined hash of all processed files, used to create the version file
this.hashCombined = '';
// Stores the before : after path of assets, used to create the manifset file
this.manifest = {};
// Enable / Disable logger based on supplied options
this.log = (this.options.debug) ? Gutil.log : function () {};
// Make tools available client side callbacks supplied in options
this.Tool = Tool;
var nonFileNameChar = '[^a-zA-Z0-9\\.\\-\\_\\/]';
var qoutes = '\'|"';
function referenceToRegexs(reference) {
var escapedRefPathBase = Tool.path_without_ext(reference.path).replace(/([^0-9a-z])/ig, '\\$1');
var escapedRefPathExt = Path.extname(reference.path).replace(/([^0-9a-z])/ig, '\\$1');
var regExp, regExps = [];
var isJSReference = reference.path.match(/\.js$/);
// @senntyou code blow will change content of js, move it
// Extensionless javascript file references has to to be qouted
//if (isJSReference) {
// regExp = '('+ qoutes +')(' + escapedRefPathBase + ')()('+ qoutes + '|$)';
// regExps.push(new RegExp(regExp, 'g'));
//}
// Expect left and right sides of the reference to be a non-filename type character, escape special regex chars
regExp = '('+ nonFileNameChar +')(' + escapedRefPathBase + ')(' + escapedRefPathExt + ')('+ nonFileNameChar + '|$)';
regExps.push(new RegExp(regExp, 'g'));
return regExps;
}
function annotator(contents, path) {
return [{'contents': contents}];
}
function replacer(fragment, replaceRegExp, newReference, referencedFile) {
fragment.contents = fragment.contents.replace(replaceRegExp, '$1' + newReference + '$3$4');
}
};
Revisioner.prototype.versionFile = function () {
var out = {
hash: this.hashCombined,
timestamp: new Date()
};
return new Gutil.File({
cwd: this.pathCwd,
base: this.pathBase,
path: Path.join(this.pathBase, this.options.fileNameVersion),
contents: new Buffer(JSON.stringify(out, null, 2))
});
};
Revisioner.prototype.manifestFile = function () {
return new Gutil.File({
cwd: this.pathCwd,
base: this.pathBase,
path: Path.join(this.pathBase, this.options.fileNameManifest),
contents: new Buffer(JSON.stringify(this.manifest, null, 2))
});
};
/**
* Used to feed files into the Revisioner, sets up the original filename and hash.
*/
Revisioner.prototype.processFile = function (file) {
if (!this.pathCwd) {
this.pathCwd = file.cwd;
}
// Chnage relative paths to absolute
if (!file.base.match(/^(\/|[a-z]:)/i)) {
file.base = Tool.join_path(file.cwd, file.base);
}
// Normalize the base common to all the files
if (!this.pathBase) {
this.pathBase = file.base;
} else if (file.base.indexOf(this.pathBase) === -1) {
var levelsBase = this.pathBase.split(/[\/|\\]/);
var levelsFile = file.base.split(/[\/|\\]/);
var common = [];
for (var level = 0, length = levelsFile.length; level < length; level++) {
if (level < levelsBase.length && level < levelsFile.length &&
levelsBase[level] === levelsFile[level]) {
common.push(levelsFile[level]);
continue;
}
}
if (common[common.length - 1] !== '') {
common.push('');
}
this.pathBase = common.join('/');
}
// Set original values before any processing occurs
file.revPathOriginal = file.revOrigPath = file.path;
file.revFilenameExtOriginal = Path.extname(file.path);
file.revFilenameOriginal = Path.basename(file.path, file.revFilenameExtOriginal);
file.revHashOriginal = this.Tool.md5(String(file.contents));
file.revContentsOriginal = file.contents;
this.filesTemp.push(file);
};
/**
* Resolves references, renames files, updates references. To be called after all the files
* have been fed into the Revisioner (ie. At the end of the file stream)
*/
Revisioner.prototype.run = function () {
this.hashCombined = '';
// Go through and correct the base path now that we have proccessed all the files coming in
for (var i = 0, length = this.filesTemp.length; i < length; i++) {
this.filesTemp[i].base = this.pathBase;
var path = this.Tool.get_relative_path(this.pathBase, this.filesTemp[i].path);
this.files[path] = this.filesTemp[i];
}
// Resolve references to other files
for (var path in this.files) {
this.resolveReferences(this.files[path]);
}
// Resolve and set revisioned filename based on hash + reference hashes and ignore rules
for (var path in this.files) {
this.revisionFilename(this.files[path]);
}
// Consolidate the concatinated hash of all the files, into a single hash for the version file
this.hashCombined = this.Tool.md5(this.hashCombined);
// Update references to revisioned filenames
for (var path in this.files) {
this.updateReferences(this.files[path]);
}
};
/**
* Go through each file in the file pool, search for references to any other file in the pool.
*/
Revisioner.prototype.resolveReferences = function (fileResolveReferencesIn) {
var contents = String(fileResolveReferencesIn.revContentsOriginal);
fileResolveReferencesIn.revReferencePaths = {};
fileResolveReferencesIn.revReferenceFiles = {};
// Don't try and resolve references in binary files or files that have been blacklisted
if (this.Tool.is_binary_file(fileResolveReferencesIn) || !this.shouldSearchFile(fileResolveReferencesIn)) {
return;
}
var referenceGroupRelative = [];
var referenceGroupAbsolute = [];
var referenceGroupsContainer = {
'relative': referenceGroupRelative,
'absolute': referenceGroupAbsolute
};
// For the current file (fileResolveReferencesIn), look for references to any other file in the project
for (var path in this.files) {
// Organize them by relative vs absolute reference types
var fileCurrentReference = this.files[path];
var references;
references = this.Tool.get_reference_representations_relative(fileCurrentReference, fileResolveReferencesIn);
for (var i = 0, length = references.length; i < length; i++) {
referenceGroupRelative.push({
'file': this.files[path],
'path': references[i]
});
}
references = this.Tool.get_reference_representations_absolute(fileCurrentReference, fileResolveReferencesIn);
for (var i = 0, length = references.length; i < length; i++) {
referenceGroupAbsolute.push({
'file': this.files[path],
'path': references[i]
});
}
}
// Priority relative references higher than absolute
for (var referenceType in referenceGroupsContainer) {
var referenceGroup = referenceGroupsContainer[referenceType];
for (var referenceIndex = 0, referenceGroupLength = referenceGroup.length; referenceIndex < referenceGroupLength; referenceIndex++) {
var reference = referenceGroup[referenceIndex];
var regExps = this.options.referenceToRegexs(reference);
for (var j = 0; j < regExps.length; j++) {
if (contents.match(regExps[j])) {
// Only register this reference if we don't have one already by the same path
if (!fileResolveReferencesIn.revReferencePaths[reference.path]) {
fileResolveReferencesIn.revReferenceFiles[reference.file.path] = reference.file;
fileResolveReferencesIn.revReferencePaths[reference.path] = {
'regExps': [regExps[j]],
'file': reference.file,
'path': reference.path
};
this.log('gulp-rev-all:', 'Found', referenceType, 'reference [', Gutil.colors.magenta(reference.path), '] -> [', Gutil.colors.green(reference.file.path), '] in [', Gutil.colors.blue(fileResolveReferencesIn.revPathOriginal), ']');
} else if (fileResolveReferencesIn.revReferencePaths[reference.path].file.revPathOriginal === reference.file.revPathOriginal) {
// Append the other regexes to account for inconsitent use
fileResolveReferencesIn.revReferencePaths[reference.path].regExps.push(regExps[j]);
} else {
this.log('gulp-rev-all:', 'Possible ambiguous refrence detected [', Gutil.colors.red(fileResolveReferencesIn.revReferencePaths[reference.path].path), ' (', fileResolveReferencesIn.revReferencePaths[reference.path].file.revPathOriginal, ')] <-> [', Gutil.colors.red(reference.path), '(', Gutil.colors.red(reference.file.revPathOriginal), ')]');
}
}
}
}
}
};
/**
* Calculate hash based contents and references.
* hash = hash(file hash + hash(hash references 1 + hash reference N)..)
*/
Revisioner.prototype.calculateHash = function (file, stack) {
stack = stack || [];
var hash = file.revHashOriginal;
stack.push(file);
// Resolve hash for child references
if (Object.keys(file.revReferenceFiles).length > 0) {
for (var key in file.revReferenceFiles) {
// Prevent infinite loops caused by circular references, don't recurse if we've already encountered this file
if (stack.indexOf(file.revReferenceFiles[key]) === -1) {
hash += this.calculateHash(file.revReferenceFiles[key], stack);
}
}
// Consolidate many hashes into one
hash = this.Tool.md5(hash);
}
return hash;
};
/**
* Revision filename based on internal contents + references.
*/
Revisioner.prototype.revisionFilename = function (file) {
var filename = file.revFilenameOriginal;
var ext = file.revFilenameExtOriginal;
file.revHash = this.calculateHash(file);
// Allow the client to transform the final filename
if (this.options.transformFilename) {
filename = this.options.transformFilename.call(this, file, file.revHash);
} else {
filename = filename + '.' + file.revHash.substr(0, this.options.hashLength) + ext;
}
file.revFilename = filename;
// file.revFilenameNoExt = Tool.path_without_ext(file.revFilename);
if (this.shouldFileBeRenamed(file)) {
file.path = this.Tool.join_path(Path.dirname(file.path), filename);
}
// Maintain the combined hash used in version file
this.hashCombined += file.revHash;
// Maintain the manifset file
var pathOriginal = this.Tool.get_relative_path(this.pathBase, file.revPathOriginal, true);
var pathRevisioned = this.Tool.get_relative_path(file.base, file.path, true);
this.manifest[pathOriginal] = pathRevisioned;
file.revPath = pathRevisioned;
};
/**
* Update the contents of a file with the revisioned filenames of its references.
*/
Revisioner.prototype.updateReferences = function (file) {
// Don't try and update references in binary files or blacklisted files
if (this.Tool.is_binary_file(file) || !this.shouldSearchFile(file)) {
return;
}
var contents = String(file.revContentsOriginal);
var annotatedContent = this.options.annotator(contents, file.revPathOriginal);
for (var pathReference in file.revReferencePaths) {
var reference = file.revReferencePaths[pathReference];
// Replace regular filename with revisioned version
var referencePath = reference.path.substr(0, reference.path.length - (reference.file.revFilenameOriginal.length + reference.file.revFilenameExtOriginal.length));
var pathReferenceReplace = referencePath + reference.file.revFilename;
if (this.options.transformPath) {
// Transform path using client supplied transformPath callback,
pathReferenceReplace = this.options.transformPath.call(this, pathReferenceReplace, reference.path, reference.file, file);
} else if (this.options.prefix && pathReferenceReplace[0] === '/') {
// Append with user supplied prefix
pathReferenceReplace = this.Tool.join_path_url(this.options.prefix, pathReferenceReplace);
}
if (this.shouldUpdateReference(reference.file)) {
// The extention should remain constant so we dont add extentions to references without extentions
var noExtReplace = Tool.path_without_ext(pathReferenceReplace);
for(var i = 0; i < annotatedContent.length; i++){
for(var j = 0; j < reference.regExps.length; j++){
this.options.replacer(annotatedContent[i], reference.regExps[j], noExtReplace, reference.file);
}
}
}
}
contents = annotatedContent.map(function(annotation) { return annotation.contents; }).join('');
file.contents = new Buffer(contents);
};
/**
* Determines if a file should be renamed based on dontRenameFile supplied in options.
*/
Revisioner.prototype.shouldFileBeRenamed = function (file) {
var filename = this.Tool.get_relative_path(file.base, file.revPathOriginal);
for (var i = this.options.dontGlobal.length; i--;) {
var regex = (this.options.dontGlobal[i] instanceof RegExp) ? this.options.dontGlobal[i] : new RegExp(this.options.dontGlobal[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
for (var i = this.options.dontRenameFile.length; i--;) {
var regex = (this.options.dontRenameFile[i] instanceof RegExp) ? this.options.dontRenameFile[i] : new RegExp(this.options.dontRenameFile[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
return true;
};
/**
* Determines if a particular reference should be updated across assets based on dontUpdateReference supplied in options.
*/
Revisioner.prototype.shouldUpdateReference = function (file) {
var filename = this.Tool.get_relative_path(file.base, file.revPathOriginal);
for (var i = this.options.dontGlobal.length; i--;) {
var regex = (this.options.dontGlobal[i] instanceof RegExp) ? this.options.dontGlobal[i] : new RegExp(this.options.dontGlobal[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
for (var i = this.options.dontUpdateReference.length; i--;) {
var regex = (this.options.dontUpdateReference[i] instanceof RegExp) ? this.options.dontUpdateReference[i] : new RegExp(this.options.dontUpdateReference[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
return true;
};
/**
* Determines if a particular reference should be updated across assets based on dontUpdateReference supplied in options.
*/
Revisioner.prototype.shouldSearchFile = function (file) {
var filename = this.Tool.get_relative_path(file.base, file.revPathOriginal);
for (var i = this.options.dontGlobal.length; i--;) {
var regex = (this.options.dontGlobal[i] instanceof RegExp) ? this.options.dontGlobal[i] : new RegExp(this.options.dontGlobal[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
for (var i = this.options.dontSearchFile.length; i--;) {
var regex = (this.options.dontSearchFile[i] instanceof RegExp) ? this.options.dontSearchFile[i] : new RegExp(this.options.dontSearchFile[i] + '$', 'ig');
if (filename.match(regex)) {
return false;
}
}
return true;
};
return Revisioner;
})();
module.exports = Revisioner;