grunt-webfonts
Version:
SVG to webfont converter for Grunt
903 lines (773 loc) • 28.6 kB
JavaScript
/**
* SVG to webfont converter for Grunt
*
* @requires ttfautohint
* @license
* Copyright Andrey Chalkin <L2jLiga> and Artem Sapegin (http://sapegin.me). All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/L2jLiga/grunt-webfonts/LICENSE
*/
;
const fs = require('node:fs');
const path = require('node:path');
const async = require('async');
const glob = require('glob');
const chalk = require('chalk');
const crypto = require('node:crypto');
const ttf2woff2 = require('ttf2woff2');
const buildTemplateFn = require('lodash/template');
/**
* @param {IGrunt} grunt
*/
module.exports = function webFonts(grunt) {
const wf = require('./util/util');
grunt.registerMultiTask('webfont', 'Compile separate SVG files to webfont', function WebFont() {
/**
* Winston to Grunt logger adapter.
*/
const format = Symbol('format');
const logger = {
warn(...args) {
const msg = this[format](...args);
if (arguments.length > 0) {
grunt.log.writeln('>> '.red + msg.trim().replace(/\n/g, '\n>> '.red));
} else {
grunt.log.writeln('ERROR'.red);
}
return this;
},
error(...args) {
grunt.log.error(...args);
},
log(...args) {
grunt.log.writeln(...args);
},
verbose(...args) {
grunt.verbose.writeln(...args);
},
[format](sep, pathObject) {
const dir = pathObject.dir || pathObject.root;
const base = pathObject.base ||
((pathObject.name || '') + (pathObject.ext || ''));
if (!dir) {
return base;
}
if (dir === pathObject.root) {
return dir + base;
}
return dir + sep + base;
},
};
const allDone = this.async();
const params = this.data;
const options = this.options();
const md5 = crypto.createHash('md5');
/*
* Check for `src` param on target config
*/
this.requiresConfig([this.name, this.target, 'src'].join('.'));
/*
* Check for `dest` param on either target config or global options object
*/
if (params.dest == null && options.dest == null) {
logger.warn('Required property ' + [this.name, this.target, 'dest'].join('.') +
' or ' + [this.name, this.target, 'options.dest'].join('.') + ' missing.');
}
if (options.skip) {
completeTask();
return;
}
// Source files
let files = this.filesSrc.filter(isSvgFile);
if (options.skipLinks === true) {
files = files.filter((filepath) => !fs.lstatSync(filepath).isSymbolicLink());
}
if (!files.length) {
logger.warn('Specified empty list of source SVG files.');
completeTask();
return;
}
// path must be a string, see https://nodejs.org/api/path.html#path_path_extname_path
if (typeof options.template !== 'string') {
options.template = '';
}
// Options
let defaultOptions = {
logger,
fontBaseName: options.font || 'icons',
destCss: options.destCss || params.destCss || params.dest,
destScss: options.destScss || params.destScss || params.destCss || params.dest,
destSass: options.destSass || params.destSass || params.destCss || params.dest,
destLess: options.destLess || params.destLess || params.destCss || params.dest,
destStyl: options.destStyl || params.destStyl || params.destCss || params.dest,
dest: options.dest || params.dest,
relativeFontPath: options.relativeFontPath,
fontPathVariables: options.fontPathVariables || false,
addHashes: options.hashes !== false,
addLigatures: options.ligatures === true,
ligaturesOnly: options.ligaturesOnly === true,
template: options.template,
syntax: options.syntax || 'bem',
templateOptions: options.templateOptions || {},
stylesheets: options.stylesheets || [options.stylesheet || path.extname(options.template).replace(/^\./, '') || 'css'],
htmlDemo: options.htmlDemo !== false,
htmlDemoTemplate: options.htmlDemoTemplate,
htmlDemoFilename: options.htmlDemoFilename,
styles: optionToArray(options.styles, 'font,icon'),
types: optionToArray(options.types, 'eot,woff,ttf'),
order: optionToArray(options.order, wf.fontFormats),
embed: options.embed === true ? ['woff'] : optionToArray(options.embed, false),
rename: options.rename || path.basename,
engine: options.engine || 'node',
autoHint: options.autoHint !== false,
codepoints: options.codepoints,
codepointsFile: options.codepointsFile,
startCodepoint: options.startCodepoint || wf.UNICODE_PUA_START,
ie7: options.ie7 === true,
centerHorizontally: options.centerHorizontally === true,
normalize: options.normalize === true,
zerowidth: options.zerowidth || [],
round: options.round !== undefined ? options.round : 10e12,
fontHeight: options.fontHeight !== undefined ? options.fontHeight : 512,
descent: options.descent !== undefined ? options.descent : 64,
version: options.version !== undefined ? options.version : false,
cache: options.cache || path.join(__dirname, '..', '.cache'),
callback: options.callback,
customOutputs: options.customOutputs || [],
execMaxBuffer: options.execMaxBuffer || 1024 * 200,
};
Object.assign(defaultOptions, {
fontName: defaultOptions.fontBaseName,
destCssPaths: {
css: defaultOptions.destCss,
scss: defaultOptions.destScss,
sass: defaultOptions.destSass,
less: defaultOptions.destLess,
styl: defaultOptions.destStyl,
},
relativeFontPath: defaultOptions.relativeFontPath || path.relative(defaultOptions.destCss, defaultOptions.dest),
destHtml: options.destHtml || defaultOptions.destCss,
fontfaceStyles: defaultOptions.styles.includes('font'),
baseStyles: defaultOptions.styles.includes('icon'),
extraStyles: defaultOptions.styles.includes('extra'),
files,
glyphs: [],
});
defaultOptions.hash = getHash();
defaultOptions.fontFilename = template(options.fontFilename || defaultOptions.fontBaseName, defaultOptions);
defaultOptions.fontFamilyName = template(options.fontFamilyName || defaultOptions.fontBaseName, defaultOptions);
// “Rename” files
defaultOptions.glyphs = defaultOptions.files.map((file) => defaultOptions.rename(file).replace(path.extname(file), ''));
// Check or generate codepoints
// @todo Codepoint can be a Unicode code or character.
let currentCodepoint = defaultOptions.startCodepoint;
if (!defaultOptions.codepoints) defaultOptions.codepoints = {};
if (defaultOptions.codepointsFile) defaultOptions.codepoints = readCodepointsFromFile();
defaultOptions.glyphs.forEach((name) => {
if (!defaultOptions.codepoints[name]) {
defaultOptions.codepoints[name] = getNextCodepoint();
}
});
if (defaultOptions.codepointsFile) saveCodepointsToFile();
// Check if we need to generate font
const previousHash = readHash(this.name, this.target);
logger.verbose('New hash:', defaultOptions.hash, '- previous hash:', previousHash);
if (defaultOptions.hash === previousHash) {
logger.verbose('Config and source files weren’t changed since last run, checking resulting files...');
let regenerationNeeded;
const generatedFiles = wf.generatedFontFiles(defaultOptions);
if (!generatedFiles.length) {
regenerationNeeded = true;
} else {
generatedFiles.push(getDemoFilePath());
defaultOptions.stylesheets.forEach((stylesheet) => {
generatedFiles.push(getCssFilePath(stylesheet));
});
regenerationNeeded = generatedFiles.some((filename) => {
if (!filename) return false;
if (!fs.existsSync(filename)) {
logger.verbose('File', filename, ' is missed.');
return true;
}
return false;
});
}
if (!regenerationNeeded) {
logger.log('Font ' + chalk.cyan(defaultOptions.fontName) + ' wasn’t changed since last run.');
completeTask();
return;
}
}
// Save new hash and run
saveHash(this.name, this.target, defaultOptions.hash);
async.waterfall([
createOutputDirs,
cleanOutputDir,
generateFont,
generateWoff2Font,
generateStylesheets,
generateDemoHtml,
generateCustomOutputs,
printDone,
], completeTask);
/**
* Call callback function if it was specified in the options.
*/
function completeTask() {
if (defaultOptions && defaultOptions.callback && 'call' in defaultOptions.callback) {
defaultOptions.callback(defaultOptions.fontName, defaultOptions.types, defaultOptions.glyphs, defaultOptions.hash);
}
allDone();
}
/**
* Calculate hash to flush browser cache.
* Hash is based on source SVG files contents, task options and grunt-webfont version.
*
* @return {String}
*/
function getHash() {
// Source SVG files contents
defaultOptions.files.forEach((file) => {
md5.update(fs.readFileSync(file, 'utf8'));
});
// Options
md5.update(JSON.stringify(defaultOptions));
// grunt-webfont version
const packageJson = require('../package.json');
md5.update(packageJson.version);
// Templates
if (defaultOptions.template) {
md5.update(fs.readFileSync(defaultOptions.template, 'utf8'));
}
if (defaultOptions.htmlDemoTemplate) {
md5.update(fs.readFileSync(defaultOptions.htmlDemoTemplate, 'utf8'));
}
return md5.digest('hex');
}
/**
* Create output directory
*
* @param {Function} done
*/
function createOutputDirs(done) {
defaultOptions.stylesheets.forEach((stylesheet) => {
fs.mkdirSync(option(defaultOptions.destCssPaths, stylesheet), {recursive: true});
});
fs.mkdirSync(defaultOptions.dest, {recursive: true});
done();
}
/**
* Clean output directory
*
* @param {Function} done
*/
function cleanOutputDir(done) {
const htmlDemoFileMask = path.join(defaultOptions.destCss, defaultOptions.fontBaseName + '*.{css,html}');
const files = glob.sync(htmlDemoFileMask).concat(wf.generatedFontFiles(defaultOptions));
async.forEach(files, (file, next) => {
fs.unlink(file, next);
}, done);
}
/**
* Generate font using selected engine
*
* @param {Function} done
*/
function generateFont(done) {
const engine = require('./engines/' + defaultOptions.engine);
engine(defaultOptions, (result) => {
if (result === false) {
// Font was not created, exit
completeTask();
return;
}
if (result) {
defaultOptions = Object.assign(defaultOptions, result);
}
done();
});
}
/**
* Converts TTF font to WOFF2.
*
* @param {Function} done
*/
function generateWoff2Font(done) {
if (
!defaultOptions.types.includes('woff2') ||
fs.existsSync(wf.getFontPath(defaultOptions, 'woff2'))
) {
done();
return;
}
// Read TTF font
const ttfFontPath = wf.getFontPath(defaultOptions, 'ttf');
const ttfFont = fs.readFileSync(ttfFontPath);
// Remove TTF font if not needed
if (!defaultOptions.types.includes('ttf')) {
fs.unlinkSync(ttfFontPath);
}
// Convert to WOFF2
const woffFont = ttf2woff2(ttfFont);
// Save
const woff2FontPath = wf.getFontPath(defaultOptions, 'woff2');
fs.writeFile(woff2FontPath, woffFont, () => done());
}
/**
* Generate CSS
*
* @param {Function} done
*/
function generateStylesheets(done) {
// Convert codepoints to array of strings
const codepoints = [];
defaultOptions.glyphs.forEach((name) => {
codepoints.push(defaultOptions.codepoints[name].toString(16));
});
defaultOptions.codepoints = codepoints;
// Prepage glyph names to use as CSS classes
defaultOptions.glyphs = defaultOptions.glyphs.map(classNameize);
defaultOptions.stylesheets.sort((keyName) => keyName.indexOf('css')).forEach(generateStylesheet);
done();
}
/**
* Generate CSS
*
* @param {String} stylesheet type: css, scss, ...
*/
function generateStylesheet(stylesheet) {
defaultOptions.relativeFontPath = normalizePath(defaultOptions.relativeFontPath);
// Generate font URLs to use in @font-face
const fontSrcs = [[], []];
defaultOptions.order.forEach((type) => {
if (!defaultOptions.types.includes(type)) return;
wf.fontsSrcsMap[type].forEach((font, idx) => {
if (font) {
fontSrcs[idx].push(generateFontSrc(type, font, stylesheet));
}
});
});
// Convert urls to strings that could be used in CSS
const fontSrcSeparator = option(wf.fontSrcSeparators, stylesheet);
fontSrcs.forEach((font, idx) => {
// defaultOptions.fontSrc1, defaultOptions.fontSrc2
defaultOptions['fontSrc' + (idx + 1)] = font.join(fontSrcSeparator);
});
defaultOptions.fontRawSrcs = fontSrcs;
// Read JSON file corresponding to CSS template
const templateJson = readTemplate(defaultOptions.template, defaultOptions.syntax, '.json', true);
if (templateJson) defaultOptions = Object.assign(defaultOptions, JSON.parse(templateJson.template));
// Now override values with templateOptions
if (defaultOptions.templateOptions) defaultOptions = Object.assign(defaultOptions, defaultOptions.templateOptions);
// Generate CSS
// Use extension of defaultOptions.template file if given, or default to .css
const ext = path.extname(defaultOptions.template) || '.css';
defaultOptions.cssTemplate = readTemplate(defaultOptions.template, defaultOptions.syntax, ext);
const cssContext = Object.assign(defaultOptions, {
iconsStyles: true,
stylesheet,
});
let css = renderTemplate(defaultOptions.cssTemplate, cssContext);
// Fix CSS preprocessors comments: single line comments will be removed after compilation
if (['sass', 'scss', 'less', 'styl'].includes(stylesheet)) {
css = css.replace(/\/\* *(.*?) *\*\//g, '// $1');
}
// Save file
fs.writeFileSync(getCssFilePath(stylesheet), css);
}
/**
* Gets the codepoints from the set filepath in defaultOptions.codepointsFile
* @return {Object} Codepoints
*/
function readCodepointsFromFile() {
if (!defaultOptions.codepointsFile) return {};
if (!fs.existsSync(defaultOptions.codepointsFile)) {
logger.verbose('Codepoints file not found');
return {};
}
const buffer = fs.readFileSync(defaultOptions.codepointsFile);
return JSON.parse(buffer.toString());
}
/**
* Saves the codespoints to the set file
*/
function saveCodepointsToFile() {
if (!defaultOptions.codepointsFile) return;
const codepointsToString = JSON.stringify(defaultOptions.codepoints, null, 4);
try {
fs.writeFileSync(defaultOptions.codepointsFile, codepointsToString);
logger.verbose('Codepoints saved to file "' + defaultOptions.codepointsFile + '".');
} catch (err) {
logger.error(err.message);
}
}
/**
* Prepares base context for templates
* @return {Object} Base template context
*/
function prepareBaseTemplateContext() {
return Object.assign({}, defaultOptions);
}
/**
* Makes custom extends necessary for use with preparing the template context
* object for the HTML demo.
* @return {Object} HTML template context
*/
function prepareHtmlTemplateContext() {
const context = Object.assign({}, defaultOptions);
// Prepare relative font paths for injection into @font-face refs in HTML
const relativeRe = new RegExp(defaultOptions.relativeFontPath, 'g');
const htmlRelativeFontPath = normalizePath(path.relative(defaultOptions.destHtml, defaultOptions.dest));
let fontSrc1 = defaultOptions.fontSrc1.replace(relativeRe, htmlRelativeFontPath);
let fontSrc2 = defaultOptions.fontSrc2.replace(relativeRe, htmlRelativeFontPath);
if (context.fontPathVariables) {
const fontPathVariableName = context.fontFamilyName + '-font-path';
const lessReplacer = new RegExp(`@{${fontPathVariableName}}`, 'g');
const scssReplacer = new RegExp(`$${fontPathVariableName} + `, 'g');
fontSrc1 = fontSrc1.replace(lessReplacer, '').replace(scssReplacer, '');
fontSrc2 = fontSrc2.replace(lessReplacer, '').replace(scssReplacer, '');
}
Object.assign(context, {
fontSrc1,
fontSrc2,
fontfaceStyles: true,
baseStyles: true,
extraStyles: false,
iconsStyles: true,
stylesheet: 'css',
});
return Object.assign(context, {
styles: renderTemplate(defaultOptions.cssTemplate, context),
});
}
/**
* Iterator function used as callback by looping construct below to
* render "custom output" via mini configuration objects specified in
* the array `options.customOutputs`.
*
* @param {Object} outputConfig
*/
function generateCustomOutput(outputConfig) {
// Accesses context
const context = prepareBaseTemplateContext();
Object.assign(context, outputConfig.context);
// Prepares config attributes related to template filepath
const templatePath = outputConfig.template;
const extension = path.extname(templatePath);
const syntax = outputConfig.syntax || '';
// Renders template with given context
const template = readTemplate(templatePath, syntax, extension);
const output = renderTemplate(template, context);
// Prepares config attributes related to destination filepath
const dest = outputConfig.dest || defaultOptions.dest;
let filepath;
let destParent;
let destName;
if (path.extname(dest) === '') {
// If user specifies a directory, filename should be same as template
destParent = dest;
destName = path.basename(outputConfig.template);
filepath = path.join(dest, destName);
} else {
// If user specifies a file, that is our filepath
destParent = path.dirname(dest);
filepath = dest;
}
// Ensure existence of parent directory and output to file as desired
fs.mkdirSync(destParent, {recursive: true});
fs.writeFileSync(filepath, output);
}
/**
* Iterates over entries in the `options.customOutputs` object and,
* on a config-by-config basis, generates the desired results.
* @param {Function} done
*/
function generateCustomOutputs(done) {
if (!defaultOptions.customOutputs || defaultOptions.customOutputs.length < 1) {
done();
return;
}
defaultOptions.customOutputs.forEach(generateCustomOutput);
done();
}
/**
* Generate HTML demo page
*
* @param {Function} done
*/
function generateDemoHtml(done) {
if (!defaultOptions.htmlDemo) {
done();
return;
}
const context = prepareHtmlTemplateContext();
// Generate HTML
const demoTemplate = readTemplate(defaultOptions.htmlDemoTemplate, 'demo', '.html');
const demo = renderTemplate(demoTemplate, context);
try {
fs.mkdirSync(getDemoPath(), {recursive: true});
fs.writeFileSync(getDemoFilePath(), demo);
} catch (err) {
logger.error(err);
}
done();
}
/**
* Print log
*
* @param {Function} done
*/
function printDone(done) {
logger.log('Font ' + chalk.cyan(defaultOptions.fontName) + ' with ' + defaultOptions.glyphs.length + ' glyphs created.');
done();
}
/**
* Helpers
*/
/**
* Convert a string of comma separated words into an array
*
* @param {String} val Input string
* @param {String} defVal Default value
* @return {Array}
*/
function optionToArray(val, defVal) {
if (val === undefined) {
val = defVal;
}
if (!val) {
return [];
}
if (typeof val !== 'string') {
return val;
}
return val.split(',').map((str) => str.trim());
}
/**
* Return a specified option if it exists in an object or `_default` otherwise
*
* @param {Object} map Options object
* @param {String} key Option to find in the object
* @return {*}
*/
function option(map, key) {
return map[key in map ? key : '_default'];
}
/**
* Find next unused codepoint.
*
* @return {Number}
*/
function getNextCodepoint() {
while (Object.values(defaultOptions.codepoints).includes(currentCodepoint)) {
currentCodepoint++;
}
return currentCodepoint;
}
/**
* Check whether file is SVG or not
*
* @param {String} filepath File path
* @return {Boolean}
*/
function isSvgFile(filepath) {
return path.extname(filepath).toLowerCase() === '.svg';
}
/**
* Convert font file to data:uri and remove source file
*
* @param {String} fontFile Font file path
* @return {String} Base64 encoded string
*/
function embedFont(fontFile) {
// Convert to data:uri
const dataUri = fs.readFileSync(fontFile, 'base64');
const type = path.extname(fontFile).substring(1);
const fontUrl = 'data:application/x-font-' + type + ';charset=utf-8;base64,' + dataUri;
// Remove font file
fs.unlinkSync(fontFile);
return fontUrl;
}
/**
* Append a slash to end of a filepath if it not exists and make all slashes forward
*
* @param {String} filepath File path
* @return {String}
*/
function normalizePath(filepath) {
if (!filepath.length) return filepath;
// Make all slashes forward
filepath = filepath.replace(/\\/g, '/');
// Make sure path ends with a slash
if (!filepath.endsWith('/')) {
filepath += '/';
}
return filepath;
}
/**
* Generate URL for @font-face
*
* @param {String} type Type of font
* @param {Object} font URL or Base64 string
* @param {String} stylesheet type: css, scss, ...
* @return {String}
*/
function generateFontSrc(type, font, stylesheet) {
const filename = template(defaultOptions.fontFilename + font.ext, defaultOptions);
let fontPathVariableName = defaultOptions.fontFamilyName + '-font-path';
let url;
if (font.embeddable && defaultOptions.embed.includes(type)) {
url = embedFont(path.join(defaultOptions.dest, filename));
} else {
if (defaultOptions.fontPathVariables && stylesheet !== 'css') {
if (stylesheet === 'less') {
fontPathVariableName = '@' + fontPathVariableName;
defaultOptions.fontPathVariable = fontPathVariableName + ' : "' + defaultOptions.relativeFontPath + '";';
} else {
fontPathVariableName = '$' + fontPathVariableName;
defaultOptions.fontPathVariable = fontPathVariableName + ' : "' + defaultOptions.relativeFontPath + '" !default;';
}
url = filename;
} else {
url = defaultOptions.relativeFontPath + filename;
}
if (defaultOptions.addHashes) {
// Do not add hashes for OldIE
if (url.indexOf('#iefix') === -1) {
// Put hash at the end of an URL or before #hash
url = url.replace(/(#|$)/, '?' + defaultOptions.hash + '$1');
} else {
url = url.replace(/(#|$)/, defaultOptions.hash + '$1');
}
}
}
let src = 'url("' + url + '")';
if (defaultOptions.fontPathVariables && stylesheet !== 'css') {
if (stylesheet === 'less') {
src = 'url("@{' + fontPathVariableName.replace('@', '') + '}' + url + '")';
} else {
src = 'url(' + fontPathVariableName + ' + "' + url + '")';
}
}
if (font.format) src += ' format("' + font.format + '")';
return src;
}
/**
* Reat the template file
*
* @param {String} template Template file path
* @param {String} syntax Syntax (bem, bootstrap, etc.)
* @param {String} ext Extention of the template
* @param {*} optional
* @return {Object} {filename: 'Template filename', template: 'Template code'}
*/
function readTemplate(template, syntax, ext, optional) {
const filename = template ? path.resolve(template.replace(path.extname(template), ext)) : path.join(__dirname, 'templates/' + syntax + ext);
if (fs.existsSync(filename)) {
return {
filename,
template: fs.readFileSync(filename, 'utf8'),
};
} else if (!optional) {
return grunt.fail.fatal('Cannot find template at path: ' + filename);
}
}
/**
* Render template with error reporting
*
* @param {Object} template {filename: 'Template filename', template: 'Template code'}
* @param {Object} context Template context
* @return {String}
*/
function renderTemplate(template, context) {
try {
return buildTemplateFn(template.template)(context);
} catch (error) {
grunt.fail.fatal('Error while rendering template ' + template.filename + ': ' + error.message);
}
}
/**
* Basic template function: replaces {variables}
*
* @param {Template} tmpl Template code
* @param {Object} context Values object
* @return {String}
*/
function template(tmpl, context) {
return tmpl.replace(/{([^}]+)}/g, (value, key) => context[key]);
}
/**
* Prepare string to use as CSS class name
*
* @param {String} str
* @return {String}
*/
function classNameize(str) {
return str.trim().replace(/\s+/g, '-');
}
/**
* Return path of CSS file.
*
* @param {String} stylesheet (css, scss, ...)
* @return {String}
*/
function getCssFilePath(stylesheet) {
const cssFilePrefix = option(wf.cssFilePrefixes, stylesheet);
grunt.log.error(cssFilePrefix);
return path.join(option(defaultOptions.destCssPaths, stylesheet), cssFilePrefix + defaultOptions.fontBaseName + '.' + stylesheet);
}
/**
* Return path of HTML demo file or `null` if its generation was disabled.
*
* @return {String}
*/
function getDemoFilePath() {
if (!defaultOptions.htmlDemo) return null;
const name = defaultOptions.htmlDemoFilename || defaultOptions.fontBaseName;
return path.join(defaultOptions.destHtml, name + '.html');
}
/**
* Return path of HTML demo file or `null` if feature was disabled
*
* @return {String}
*/
function getDemoPath() {
if (!defaultOptions.htmlDemo) return null;
return defaultOptions.destHtml;
}
/**
* Save hash to cache file.
*
* @param {String} name Task name (webfont).
* @param {String} target Task target name.
* @param {String} hash Hash.
*/
function saveHash(name, target, hash) {
const filepath = getHashPath(name, target);
fs.mkdirSync(path.dirname(filepath), {recursive: true});
fs.writeFileSync(filepath, hash);
}
/**
* Read hash from cache file or `null` if file don’t exist.
*
* @param {String} name Task name (webfont).
* @param {String} target Task target name.
* @return {String|null}
*/
function readHash(name, target) {
const filepath = getHashPath(name, target);
return fs.existsSync(filepath) ? fs.readFileSync(filepath, 'utf8') : null;
}
/**
* Return path to cache file.
*
* @param {String} name Task name (webfont).
* @param {String} target Task target name.
* @return {String}
*/
function getHashPath(name, target) {
return path.join(defaultOptions.cache, name, target, 'hash');
}
});
};