grunt-cache-killer
Version:
Kill your asset cache file problems by updating their filenames and any references to them.
386 lines (312 loc) • 14.3 kB
JavaScript
/*
* grunt-cache-killer
* https://github.com/custom-dev-tools/grunt-cache-killer
*
* Copyright (c) 2022 Matthew Rath
* Licensed under the MIT license.
*/
'use strict';
// Load the required Nodejs libraries.
var crypto = require('crypto');
var fileSystem = require('fs');
var util = require('util');
// Grunt task module.
module.exports = function (grunt) {
grunt.registerMultiTask('cacheKiller', 'Kill your asset cache file problems by updating their filenames and any references to them.', function () {
/**
* Get filename from path.
*
* @param $path {string}
* @returns {string}
*/
function getFile($path) {
return $path.split('\\').pop().split('/').pop();
}
/**
* Get base from path.
*
* @param $path {string}
* @returns {string}
*/
function getBase($path) {
return $path.replace(getFile($path), '');
}
/**
* Get timestamp.
*
* @returns {number}
*/
function getTimeStamp() {
return +new Date();
}
/**
* Get datetime stamp.
*
* @returns {string}
*/
function getDateTimeStamp() {
var date = new Date();
return date.getFullYear().toString().padStart(2, '0') +
(date.getMonth() + 1).toString().padStart(2, '0') +
date.getDate().toString().padStart(2, '0') +
date.getHours().toString().padStart(2, '0') +
date.getMinutes().toString().padStart(2, '0') +
date.getSeconds().toString().padStart(2, '0');
}
/**
* Get hash of file.
*
* @param $file {string}
* @param $algorithm {string}
* @returns {string}
*/
function getHash($file, $algorithm) {
var $length = 8192;
var $fileDescriptor = fileSystem.openSync($file, 'r');
var $hash = crypto.createHash($algorithm);
var $buffer = Buffer.alloc($length);
try {
var $bytesRead;
do {
$bytesRead = fileSystem.readSync($fileDescriptor, $buffer, 0, $length, null);
$hash.update($buffer.slice(0, $bytesRead));
} while ($bytesRead === $length);
} finally {
fileSystem.closeSync($fileDescriptor);
}
return $hash.digest('hex');
}
/**
* Check if the mask is a function.
*
* @param $mask (string)
* @returns {boolean}
*/
function isMaskFunction($mask) {
return ($mask.substr(0, 1) === '{' && $mask.substr(-1) === '}');
}
/**
* Trim the curly braces from the mask function.
*
* @param $mask (string}
* @returns {string}
*/
function trimMaskFunction($mask) {
return $mask.slice(1, -1);
}
/**
* Check for a valid mask function.
*
* @param $maskFunction {string}
* @param $maskFunctions {array}
* @returns {boolean}
*/
function isValidMaskFunction($maskFunction, $maskFunctions) {
return ($maskFunctions.indexOf($maskFunction)) > -1;
}
/**
* Get mask value.
*
* @param $mask {string}
* @param $file {string}
* @returns {string}
*/
function getMaskValue($mask, $file) {
// Check if a mask function is being used.
if (isMaskFunction($mask)) {
// Trim the curly braces from the mask function.
$mask = trimMaskFunction($mask);
// If using a timestamp.
if ($mask === 'timestamp') {
return getTimeStamp().toString();
}
// If using a datetime stamp.
if ($mask === 'datetimestamp') {
return getDateTimeStamp();
}
// Using an OpenSSL digest algorithm.
return getHash($file, $mask);
}
// Just a string.
return $mask;
}
/**
* Strip characters from beginning of string.
*
* @param $string {string}
* @param $length {number}
* @returns {string}
*/
function trimByLength($string, $length) {
return ($length !== -1) ? $string.substring($string.length - $length) : $string;
}
/**
* Find matching asset using asset mask file.
*
* @param $path {string}
* @param $pre {string}
* @param $post {string}
* @returns {string|object}
*/
function findMatchingAsset($path, $pre, $post) {
// Synchronously read the asset filename(s) from the path.
var $files = fileSystem.readdirSync($path);
// Iterate over the asset filename(s).
for (var i = 0; i < $files.length; i++) {
// Check if 'pre' and 'post' strings match the start and end parts of the asset filename respectively.
if ($pre === $files[i].substr(0, $pre.length) && $post === $files[i].substr(-$post.length)) {
// A matching asset was found.
return $files[i];
}
}
// A matching asset was not found.
return null;
}
// --------------------------------------------------------------------------------
// Debugging.
grunt.log.debug('');
grunt.log.debug('Debugging');
grunt.log.debug('---------');
grunt.log.debug('To assist in the debugging process, please copy the below and paste into your bug report. Thank you.\n');
// Set the default mask placeholder.
var $maskPlaceholder = '[mask]';
// Set the valid option types.
var $validOptionsTypes = {
'prepend': 'string',
'append': 'string',
'mask': 'string',
'length': 'number'
};
// Set the default options. Any option(s) passed in will override the default option(s).
var $options = this.options({
prepend: '',
append: '',
mask: '{md5}',
length: -1
});
// Debugging.
grunt.log.debug('Options object: ' + util.inspect($options, false, null, true) + '\n');
// Set the valid mask functions types.
var $validMaskFunctionTypes = ['timestamp', 'datetimestamp'].concat(crypto.getHashes()).sort(function (a, b) {
return a.localeCompare(b, undefined, {sensitivity: 'base'});
});
// Debugging.
grunt.log.debug('Validating option types.');
// Validate the options types.
for (var $key in $validOptionsTypes) {
// Check if the option type is incorrect.
if (typeof ($options[$key]) !== $validOptionsTypes[$key]) {
// Capitalise the first letter.
var $type = typeof ($options[$key]);
$type = $type.charAt(0).toUpperCase() + $type.slice(1);
grunt.fail.warn(this.target + ' : The option \'' + $key + '\' must be a ' + $validOptionsTypes[$key] + '. ' + $type + ' given.');
}
}
// Debugging.
grunt.log.debug('Success..!\n');
grunt.log.debug('Validating mask function type.');
// Validate the mask function type.
if (isMaskFunction($options.mask) && !isValidMaskFunction(trimMaskFunction($options.mask), $validMaskFunctionTypes)) {
grunt.fail.warn(this.target + ' : The options mask \'' + $options.mask + '\' is not a valid mask. Valid masks include ' + $validMaskFunctionTypes.join(', ') + '.');
}
// Debugging.
grunt.log.debug('Success..!\n');
grunt.log.debug('Building tasks list.');
grunt.log.debug('Working..!.\n');
// Build the tasks list.
var $tasks = {};
for (var i = 0; i < this.files.length; i++) {
$tasks[i] = {};
$tasks[i].asset = {};
$tasks[i].asset.name = {};
$tasks[i].asset.mask = {};
$tasks[i].asset.rename = {};
$tasks[i].asset.rename.from = {};
$tasks[i].asset.rename.to = {};
$tasks[i].templates = this.files[i].orig.src;
// Debugging.
grunt.log.debug('Validating template filenames.');
// Check if the template filename(s) exist.
for (var j = 0; j < $tasks[i].templates.length; j++) {
if (!fileSystem.existsSync($tasks[i].templates[j])) {
grunt.fail.warn(this.target + ' : The template file \'' + $tasks[i].templates[j] + '\' does not exist.');
}
}
// Debugging.
grunt.log.debug('Success..!\n');
$tasks[i].asset.name.full = this.files[i].orig.dest;
$tasks[i].asset.name.base = getBase($tasks[i].asset.name.full);
$tasks[i].asset.name.file = getFile($tasks[i].asset.name.full);
$tasks[i].asset.name.pre = $tasks[i].asset.name.file.split($maskPlaceholder)[0];
$tasks[i].asset.name.post = $tasks[i].asset.name.file.split($maskPlaceholder)[1];
// Debugging.
grunt.log.debug('Validating position of mask placeholder.');
// Check if the position of the mask placeholder within the asset filename is valid.
if ($tasks[i].asset.name.pre === '' || $tasks[i].asset.name.post === '') {
grunt.fail.warn(this.target + ' : The position of the [mask] placeholder cannot be at the very beginning or very end of the asset filename. \'' + $tasks[i].asset.name.full + '\' given.');
}
// Debugging.
grunt.log.debug('Success..!\n');
$tasks[i].asset.rename.from.file = findMatchingAsset($tasks[i].asset.name.base, $tasks[i].asset.name.pre, $tasks[i].asset.name.post);
$tasks[i].asset.rename.from.full = $tasks[i].asset.name.base + $tasks[i].asset.rename.from.file;
// Debugging.
grunt.log.debug('Validating asset filename.');
// Check if the asset filename exists.
if ($tasks[i].asset.rename.from.file === null) {
grunt.fail.warn(this.target + ' : The masked asset file \'' + $tasks[i].asset.name.full + '\' does not exist.');
}
// Debugging.
grunt.log.debug('Success..!\n');
$tasks[i].asset.mask.prepend = $options.prepend;
$tasks[i].asset.mask.value = {};
$tasks[i].asset.mask.value.raw = getMaskValue($options.mask, $tasks[i].asset.rename.from.full);
$tasks[i].asset.mask.value.computed = trimByLength($tasks[i].asset.mask.value.raw, $options.length);
$tasks[i].asset.mask.append = $options.append;
$tasks[i].asset.rename.to.file = $tasks[i].asset.name.pre + $tasks[i].asset.mask.prepend + $tasks[i].asset.mask.value.computed + $tasks[i].asset.mask.append + $tasks[i].asset.name.post;
$tasks[i].asset.rename.to.full = $tasks[i].asset.name.base + $tasks[i].asset.rename.to.file;
}
// Debugging.
grunt.log.debug('Build tasks list.');
grunt.log.debug('Success..!\n');
grunt.log.debug('Tasks list object:\n' + util.inspect($tasks, false, null, true) + '\n');
// Let's begin.
grunt.log.writeln('');
// Iterate over the tasks list.
for (var k = 0; k < Object.keys($tasks).length; k++) {
// Debugging.
grunt.log.debug('Synchronously renaming asset file.');
// Synchronously rename the asset file.
fileSystem.renameSync($tasks[k].asset.rename.from.full, $tasks[k].asset.rename.to.full);
// Debugging.
grunt.log.debug('Success..!\n');
// Show the successful asset renaming message.
grunt.log.ok(this.target + ' : Asset file \'' + $tasks[k].asset.rename.from.file + '\' renamed to \'' + $tasks[k].asset.rename.to.file + '\'.');
// Update any reference to the asset file in the template file(s).
for (var l = 0; l < $tasks[k].templates.length; l++) {
// Debugging.
grunt.log.debug('');
grunt.log.debug('Synchronously read contents of template.');
// Synchronously read the contents of the template.
var content = fileSystem.readFileSync($tasks[k].templates[l], 'utf8');
// Debugging.
grunt.log.debug('Success..!\n');
grunt.log.debug('Replace template\'s matching content.');
// Replace the template's matching content.
var result = content.replaceAll($tasks[k].asset.rename.from.file, $tasks[k].asset.rename.to.file);
// Debugging.
grunt.log.debug('Success..!\n');
grunt.log.debug('Synchronously write contents to template.');
// Synchronously write the contents to the template.
fileSystem.writeFileSync($tasks[k].templates[l], result, 'utf8');
// Debugging.
grunt.log.debug('Success..!\n');
// Show the successful template update message.
grunt.log.ok(this.target + ' : Reference(s) updated in template file \'' + $tasks[k].templates[l] + '\'.');
}
// Add line between target(s) and goodbye.
grunt.log.writeln('');
}
// Goodbye.
grunt.log.writeln('Success..!');
});
};