Files
old-saburly-blog/node_modules/gulp-rev-all/revisioner.js
2017-10-23 15:54:25 +02:00

487 lines
16 KiB
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,
'includeFilesInManifest': ['.css', '.js']
};
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$/);
// 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()
};
var file = 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: this
});
file.revisioner = this;
return file;
};
Revisioner.prototype.manifestFile = function () {
var file = 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)),
});
file.revisioner = this;
return file;
};
/**
* 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 = {};
var referenceGroupRelative = [];
var referenceGroupAbsolute = [];
fileResolveReferencesIn.referenceGroupsContainer = {
'relative': referenceGroupRelative,
'absolute': referenceGroupAbsolute
};
// 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;
}
// 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 fileResolveReferencesIn.referenceGroupsContainer) {
var referenceGroup = fileResolveReferencesIn.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 reference 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);
}
}
// This file's hash should change if any of its references will be prefixed.
if (this.options.prefix &&
Object.keys(file.referenceGroupsContainer.absolute).length) {
hash += this.options.prefix;
}
// 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);
// Add only specific file types to the manifest file
if (this.options.includeFilesInManifest.indexOf(ext) !== -1) {
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;