replace-in-file
Version:
A simple utility to quickly replace text in one or more files.
183 lines (182 loc) • 6.18 kB
JavaScript
import path from 'node:path';
import fs from 'node:fs/promises';
import fsSync from 'node:fs';
/**
* Helper to load options from a config file
*/
export async function loadConfig(file) {
//No config file provided?
if (!file) {
throw new Error(`No config file provided`);
}
//Read file
const json = await fs.readFile(path.resolve(file), 'utf8');
return JSON.parse(json);
}
/**
* Helper to convert a string to Regex if it matches the pattern
*/
export function stringToRegex(str) {
//Array given
if (Array.isArray(str)) {
return str.map(toRegex);
}
//Nothing given
if (typeof str === 'undefined') {
return str;
}
//Convert single value
return toRegex(str);
}
/**
* Convert a single value to a regex if it is wrapped in slashes
*/
function toRegex(str) {
//Not a string, or not wrapped in slashes like /pattern/flags
const regexMatch = /^\/(.*)\/([gimyus]*)$/s;
const match = (typeof str === 'string') ? str.match(regexMatch) : null;
if (!match) {
return str;
}
//Extract pattern and flags and return regex
const [, pattern, flags] = match;
return new RegExp(pattern, flags);
}
/**
* Parse config
*/
export function parseConfig(config) {
//Validate config
if (typeof config !== 'object' || config === null) {
throw new Error('Must specify configuration object');
}
//Fix glob
config.glob = config.glob || {};
//Extract data
const { files, getTargetFile, from, to, processor, processorAsync, ignore, encoding, streaming, maxMatchLength, } = config;
if (typeof processor !== 'undefined') {
if (typeof processor !== 'function' && !Array.isArray(processor)) {
throw new Error(`Processor should be either a function or an array of functions`);
}
}
else if (typeof processorAsync !== 'undefined') {
if (typeof processorAsync !== 'function' && !Array.isArray(processorAsync)) {
throw new Error(`ProcessorAsync should be either a function or an array of functions`);
}
}
else {
if (typeof files === 'undefined') {
throw new Error('Must specify file or files');
}
if (typeof from === 'undefined') {
throw new Error('Must specify string or regex to replace');
}
if (typeof to === 'undefined') {
throw new Error('Must specify a replacement (can be blank string)');
}
if (typeof getTargetFile !== 'undefined' && typeof getTargetFile !== 'function') {
throw new Error(`Target file transformation parameter should be a function that takes the source file path as argument and returns the target file path`);
}
}
//Validate streaming constraints
if (streaming) {
if (typeof processor !== 'undefined' || typeof processorAsync !== 'undefined') {
throw new Error('Streaming is not supported with custom processors');
}
//Compare against the default file systems, as an already parsed config
//will have those merged in (e.g. when the CLI re-parses its options)
const hasCustomFs = (typeof config.fs !== 'undefined' && config.fs !== fs);
const hasCustomFsSync = (typeof config.fsSync !== 'undefined' && config.fsSync !== fsSync);
if (hasCustomFs || hasCustomFsSync) {
throw new Error('Streaming is not supported with a custom file system');
}
}
if (typeof maxMatchLength !== 'undefined') {
if (!Number.isInteger(maxMatchLength) || maxMatchLength < 1) {
throw new Error('Max match length must be a positive integer');
}
}
//Ensure arrays
if (!Array.isArray(files)) {
config.files = [files];
}
if (!Array.isArray(ignore)) {
if (typeof ignore === 'undefined') {
config.ignore = [];
}
else {
config.ignore = [ignore];
}
}
//Since we can't store Regexp in JSON, convert from string if needed
config.from = stringToRegex(from);
//Use default encoding if invalid
if (typeof encoding !== 'string' || encoding === '') {
config.encoding = 'utf-8';
}
//Merge config with defaults
return Object.assign({}, {
ignore: [],
encoding: 'utf-8',
disableGlobs: false,
allowEmptyPaths: false,
countMatches: false,
verbose: false,
quiet: false,
dry: false,
streaming: false,
maxMatchLength: 1024,
glob: {},
cwd: null,
getTargetFile: (source) => source,
fs,
fsSync,
}, config);
}
/**
* Combine CLI script arguments with config options
*/
export function combineConfig(config, argv) {
//Extract options from config
const { allowEmptyPaths, maxMatchLength } = config;
let { from, to, files, ignore, encoding, verbose, disableGlobs, dry, quiet, streaming, } = config;
//Get from/to parameters from CLI args if not defined in options
if (typeof from === 'undefined') {
from = argv._.shift();
}
if (typeof to === 'undefined') {
to = argv._.shift();
}
//Get files and ignored files
if (typeof files === 'undefined') {
files = argv._;
}
if (typeof ignore === 'undefined' && typeof argv.ignore !== 'undefined') {
ignore = argv.ignore;
}
//Other parameters
if (typeof encoding === 'undefined') {
encoding = argv.encoding;
}
if (typeof disableGlobs === 'undefined') {
disableGlobs = !!argv.disableGlobs;
}
if (typeof verbose === 'undefined') {
verbose = !!argv.verbose;
}
if (typeof dry === 'undefined') {
dry = !!argv.dry;
}
if (typeof quiet === 'undefined') {
quiet = !!argv.quiet;
}
if (typeof streaming === 'undefined') {
streaming = !!argv.streaming;
}
//Return through parser to validate
return parseConfig({
from, to, files, ignore, encoding, verbose,
allowEmptyPaths, disableGlobs, dry, quiet, streaming,
maxMatchLength: maxMatchLength ?? argv.maxMatchLength,
});
}