UNPKG

replacer-util

Version:

Find and replace strings or template outputs in text files (CLI tool designed for use in npm package.json scripts)

170 lines (168 loc) 8.46 kB
//! replacer-util v1.3.2 ~~ https://github.com/center-key/replacer-util ~~ MIT License import { globSync } from 'glob'; import { isBinary } from 'istextorbinary'; import { Liquid } from 'liquidjs'; import chalk from 'chalk'; import fs from 'fs'; import log from 'fancy-log'; import path from 'path'; import slash from 'slash'; const task = { normalizeFolder(folderPath) { return !folderPath ? '' : slash(path.normalize(folderPath)).replace(/\/$/, ''); }, isTextFile(filename) { return fs.statSync(filename).isFile() && !isBinary(filename); }, readPackageJson() { const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const fixHiddenKeys = (pkgObj) => { const unhide = (key) => { const newKey = key.replace(/[@./]/g, '-'); if (!pkgObj[newKey]) pkgObj[newKey] = pkgObj[key]; }; Object.keys(pkgObj).forEach(unhide); }; if (pkg.dependencies) fixHiddenKeys(pkg.dependencies); if (pkg.devDependencies) fixHiddenKeys(pkg.devDependencies); return pkg; }, }; const replacer = { transform(sourceFolder, targetFolder, options) { const defaults = { cd: null, concat: null, exclude: null, extensions: [], find: null, noSourceMap: false, regex: null, replacement: null, templatingOn: true, }; const settings = { ...defaults, ...options }; const startTime = Date.now(); const startFolder = settings.cd ? task.normalizeFolder(settings.cd) + '/' : ''; const source = task.normalizeFolder(startFolder + sourceFolder); const target = task.normalizeFolder(startFolder + targetFolder); const concatFile = settings.concat ? path.join(target, settings.concat) : null; const missingFind = !settings.find && !settings.regex && !!settings.replacement; if (targetFolder) fs.mkdirSync(target, { recursive: true }); const errorMessage = !sourceFolder ? 'Must specify the source folder path.' : !targetFolder ? 'Must specify the target folder path.' : !fs.existsSync(source) ? 'Source folder does not exist: ' + source : !fs.existsSync(target) ? 'Target folder cannot be created: ' + target : !fs.statSync(source).isDirectory() ? 'Source is not a folder: ' + source : !fs.statSync(target).isDirectory() ? 'Target is not a folder: ' + target : missingFind ? 'Must specify search text with --find or --regex' : null; if (errorMessage) throw new Error('[replacer-util] ' + errorMessage); const globFiles = () => exts.map(ext => globSync(source + '/**/*' + ext)).flat().sort(); const keep = (file) => !settings.exclude || !file.includes(settings.exclude); const relativeFolders = (file) => file.substring(source.length, file.length - path.basename(file).length); const renameFile = (file) => settings.rename ? target + relativeFolders(file) + settings.rename : null; const getFileRoute = (file) => ({ origin: file, dest: concatFile ?? renameFile(file) ?? target + '/' + file.substring(source.length + 1), }); const exts = settings.extensions.length ? settings.extensions : ['']; const filesRaw = settings.filename ? [source + '/' + settings.filename] : globFiles(); const filtered = filesRaw.filter(task.isTextFile).filter(keep); const fileRoutes = filtered.map(file => slash(file)).map(getFileRoute); const pkg = task.readPackageJson(); const normalizeEol = /\r/g; const normalizeEof = /\s*$(?!\n)/; const sourceMapLine = /^\/.#\ssourceMappingURL=.*\n/gm; const header = settings.header ? settings.header + '\n' : ''; const rep = settings.replacement ?? ''; const getFileInfo = (origin) => { const parsedPath = path.parse(origin); const dir = slash(parsedPath.dir); const filePath = dir + '/' + slash(parsedPath.base); const folder = path.basename(dir); const date = fs.statSync(origin).mtime; const dateFormat = { day: 'numeric', month: 'long', year: 'numeric' }; const modified = date.toLocaleString([], dateFormat); const timestamp = date.toISOString(); return { ...parsedPath, dir, folder, path: filePath, date, modified, timestamp }; }; const getWebRoot = (origin) => { const depth = origin.substring(source.length).split('/').length - 2; return depth === 0 ? '.' : '..' + '/..'.repeat(depth - 1); }; const createEngine = (file) => { const globals = { package: pkg, file: getFileInfo(file.origin), webRoot: getWebRoot(file.origin), }; const engine = new Liquid({ globals }); const versionFormatter = (numIds) => (str) => str.replace(/[^0-9]*/, '').split('.').slice(0, numIds).join('.'); engine.registerFilter('version', versionFormatter(3)); engine.registerFilter('minor-version', versionFormatter(2)); engine.registerFilter('major-version', versionFormatter(1)); return engine; }; const extractPageVars = (engine, file) => { const tags = engine.parseFileSync(file); const toPair = (tag) => [tag.key, tag.value.initial.postfix[0]?.content]; const tagPairs = tags.filter(tag => tag.name === 'assign').map(toPair); return Object.fromEntries(tagPairs); }; const processFile = (file, index) => { const engine = createEngine(file); const pageVars = settings.content ? extractPageVars(engine, file.origin) : {}; const render = (text) => engine.parseAndRenderSync(text, pageVars); const append = settings.concat && index > 0; const altText = settings.content ? render(settings.content) : null; const text = altText ?? fs.readFileSync(file.origin, 'utf-8'); const content = render(header) + text; const newStr = render(rep); const out1 = settings.templatingOn ? render(content) : content; const out2 = out1.replace(normalizeEol, '').replace(normalizeEof, '\n'); const out3 = settings.find ? out2.replaceAll(settings.find, newStr) : out2; const out4 = settings.regex ? out3.replace(settings.regex, newStr) : out3; const out5 = settings.noSourceMap ? out4.replace(sourceMapLine, '') : out4; const out6 = out5.trimStart(); const final = append && settings.header ? '\n' + out6 : out6; fs.mkdirSync(path.dirname(file.dest), { recursive: true }); return append ? fs.appendFileSync(file.dest, final) : fs.writeFileSync(file.dest, final); }; fileRoutes.map(processFile); const relativePaths = (file) => ({ origin: file.origin.substring(source.length + 1), dest: file.dest.substring(target.length + 1), }); return { source: source, target: target, count: fileRoutes.length, duration: Date.now() - startTime, files: fileRoutes.map(relativePaths), }; }, reporter(results, options) { const defaults = { summaryOnly: false, }; const settings = { ...defaults, ...options }; const name = chalk.gray('replacer'); const source = chalk.blue.bold(results.source); const target = chalk.magenta(results.target); const arrow = { big: chalk.gray.bold(' ⟹ '), little: chalk.gray.bold('→') }; const infoColor = results.count ? chalk.white : chalk.red.bold; const info = infoColor(`(files: ${results.count}, ${results.duration}ms)`); log(name, source, arrow.big, target, info); const logFile = (file) => log(name, chalk.white(file.origin), arrow.little, chalk.green(file.dest)); if (!settings.summaryOnly) results.files.forEach(logFile); return results; }, }; export { replacer };