UNPKG

replace-in-file

Version:

A simple utility to quickly replace text in one or more files.

183 lines (182 loc) • 6.18 kB
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, }); }