Files
replace-in-file/lib/replace-in-file.js
2017-02-15 07:27:43 +13:00

275 lines
5.9 KiB
JavaScript

'use strict';
/**
* Module dependencies
*/
const fs = require('fs');
const glob = require('glob');
const chalk = require('chalk');
/**
* Defaults
*/
const defaults = {
allowEmptyPaths: false,
encoding: 'utf-8',
};
/**
* Parse config
*/
function parseConfig(config) {
//Validate config
if (typeof config !== 'object' || config === null) {
throw new Error('Must specify configuration object');
}
//Backwards compatibilility
if (typeof config.replace !== 'undefined' &&
typeof config.from === 'undefined') {
console.log(
chalk.yellow('Option `replace` is deprecated. Use `from` instead.')
);
config.from = config.replace;
}
if (typeof config.with !== 'undefined' &&
typeof config.to === 'undefined') {
console.log(
chalk.yellow('Option `with` is deprecated. Use `to` instead.')
);
config.to = config.with;
}
//Validate values
if (typeof config.files === 'undefined') {
throw new Error('Must specify file or files');
}
if (typeof config.from === 'undefined') {
throw new Error('Must specify string or regex to replace');
}
if (typeof config.to === 'undefined') {
throw new Error('Must specify a replacement (can be blank string)');
}
//Use default encoding if invalid
if (typeof config.encoding !== 'string' || config.encoding === '') {
config.encoding = 'utf-8';
}
//Merge config with defaults
return Object.assign({}, defaults, config);
}
/**
* Get replacement helper
*/
function getReplacement(replace, isArray, i) {
if (isArray && typeof replace[i] === 'undefined') {
return null;
}
if (isArray) {
return replace[i];
}
return replace;
}
/**
* Helper to make replacements
*/
function makeReplacements(contents, from, to) {
//Turn into array
if (!Array.isArray(from)) {
from = [from];
}
//Check if replace value is an array
const isArray = Array.isArray(to);
//Make replacements
from.forEach((item, i) => {
//Get replacement value
const replacement = getReplacement(to, isArray, i);
if (replacement === null) {
return;
}
//Make replacement
contents = contents.replace(item, replacement);
});
//Return modified contents
return contents;
}
/**
* Helper to replace in a single file (sync)
*/
function replaceSync(file, from, to, enc) {
//Read contents
const contents = fs.readFileSync(file, enc);
//Replace contents and check if anything changed
const newContents = makeReplacements(contents, from, to);
if (newContents === contents) {
return false;
}
//Write to file
fs.writeFileSync(file, newContents, enc);
return true;
}
/**
* Helper to replace in a single file (async)
*/
function replaceAsync(file, from, to, enc) {
return new Promise((resolve, reject) => {
fs.readFile(file, enc, (error, contents) => {
//istanbul ignore if
if (error) {
return reject(error);
}
//Replace contents and check if anything changed
let newContents = makeReplacements(contents, from, to);
if (newContents === contents) {
return resolve({file, hasChanged: false});
}
//Write to file
fs.writeFile(file, newContents, enc, error => {
//istanbul ignore if
if (error) {
return reject(error);
}
resolve({file, hasChanged: true});
});
});
});
}
/**
* Promise wrapper for glob
*/
function globPromise(pattern, allowEmptyPaths) {
return new Promise((resolve, reject) => {
glob(pattern, {nodir: true}, (error, files) => {
//istanbul ignore if: hard to make glob error
if (error) {
return reject(error);
}
//Error if no files match, unless allowEmptyPaths is true
if (files.length === 0 && !allowEmptyPaths) {
return reject(new Error('No files match the pattern: ' + pattern));
}
//Resolve
resolve(files);
});
});
}
/**
* Replace in file helper
*/
function replaceInFile(config, cb) {
//Parse config
try {
config = parseConfig(config);
}
catch (error) {
if (cb) {
return cb(error, null);
}
return Promise.reject(error);
}
//Get config and globs
// const {files, from, to, allowEmptyPaths, encoding} = config;
const files = config.files;
const from = config.from;
const to = config.to;
const encoding = config.encoding;
const allowEmptyPaths = config.allowEmptyPaths;
const globs = Array.isArray(files) ? files : [files];
//Find files
return Promise
.all(globs.map(pattern => globPromise(pattern, allowEmptyPaths)))
//Flatten array
.then(files => [].concat.apply([], files))
//Make replacements
.then(files => Promise.all(files.map(file => {
return replaceAsync(file, from, to, encoding);
})))
//Convert results to array of changed files
.then(results => {
return results
.filter(result => result.hasChanged)
.map(result => result.file);
})
//Handle via callback or return
.then(changedFiles => {
if (cb) {
cb(null, changedFiles);
}
return changedFiles;
})
//Handle error via callback, or rethrow
.catch(error => {
if (cb) {
cb(error);
}
else {
throw error;
}
});
}
/**
* Sync API
*/
replaceInFile.sync = function(config) {
//Parse config
config = parseConfig(config);
//Get config and globs
// const {files, from, to, encoding} = config;
const files = config.files;
const from = config.from;
const to = config.to;
const encoding = config.encoding;
const globs = Array.isArray(files) ? files : [files];
const changedFiles = [];
//Process synchronously
globs.forEach(pattern => {
glob
.sync(pattern, {nodir: true})
.forEach(file => {
if (replaceSync(file, from, to, encoding)) {
changedFiles.push(file);
}
});
});
//Return changed files
return changedFiles;
};
//Export
module.exports = replaceInFile;