/**
* @module flitter-flap/FlapHelper
*/
const ncp = require('ncp')
const path = require('path')
const fs = require('fs')
const touch = require('touch')
const del = require('del')
const format = require('js-beautify').js
/**
* Get an object containing some helper functions to be used by migrations.
* These functions will run in non-modification mode if dry_run is true.
*
* @param {boolean} [dry_run = true] - If true, the helper function that are returned will print their changes to the console rather than actually modifying files.
*/
module.exports = exports = function(dry_run = true){
return {
/**
* If true, the user has requested that migrations print their changes to the console, rather than modifying files.
* This is set so that any custom migrations that don't use the provided helper functions can honor dry mode.
* @constant
* @type {boolean}
*/
dry_run,
/**
* Check if a file or directory exists. If it does, resolve true. If not, resolve false.
* @param {string} dir - path to check existence of
* @returns {Promise<boolean>} - resolves true or false if the resource exists or not respectively
*/
exists(dir){
return new Promise(
(resolve, reject) => {
fs.stat(dir, (err) => {
if ( !err ){
resolve(true)
}
else if ( err.code === 'ENOENT' ){
resolve(false)
}
else {
reject(err)
}
})
}
)
},
/**
* Copy a file or folder from one place to another. If dry mode, just print the fully-qualified paths of the source and destination.
* @param {string} from - path to the file/directory to be copied
* @param {string} to - path to the destination
* @returns {Promise<void>}
*/
async copy(from, to){
from = path.resolve(from)
to = path.resolve(to)
if ( dry_run ){
console.log("Will copy file/directory: "+from)
console.log("to: "+to)
}
else {
await new Promise(
(resolve, reject) => {
ncp(from, to, (error) => {
if ( error ) reject(error)
resolve()
})
}
)
}
},
/**
* Delete the file/directory at the specified path. If dry mode, print the path to be deleted.
* If dry mode, print the fully-qualified path of the resource to be deleted.
* @param {string} file - path to the file/directory to be deleted. accepts globs.
* @returns {Promise<void>}
*/
async delete(glob){
if ( dry_run ){
console.log("Will delete the path matching glob: "+glob)
}
else {
await del([glob])
}
},
/**
* Create the provided directory recursively. Any super-directories needed will be created as well.
* If dry mode, print the fully-qualified path of the directory to be created.
* @param {string} dir - path of the directory to be created
* @returns {Promise<void>}
*/
async mkdir(dir){
dir = path.resolve(dir)
if ( dry_run ){
console.log("Will create directory: "+dir)
}
else {
await fs.promises.mkdir(dir, {recursive:true})
}
},
/**
* Create the specified file if it doesn't exist. Otherwise, update its timestamp.
* The file's directory must exist for this to work. If dry mode, print the fully-qualified path of the file
* to be created.
* @param {string} file - path of the file to be created.
* @returns {Promise<void>}
*/
async touch(file){
file = path.resolve(file)
if ( dry_run ){
console.log("Will create the file (unless it exists): "+file)
}
else {
await touch(file)
}
},
/**
* Write the contents to the file at the specified path. If it doesn't exist, the file will be created.
* The directory of the file must exist for this to work. If dry mode, print the fully-qualified path of the
* file to be written, as well as the contents.
* @param {string} file - path of the file to be written
* @param {string} contents - contents to write to the file
* @returns {Promise<void>}
*/
async write_file(file, contents){
file = path.resolve(file)
await this.touch(file)
if ( dry_run ){
console.log("Adding contents to file: "+file)
console.log("\n"+contents+"\n")
}
else {
await fs.promises.writeFile(file, contents)
}
},
/**
* Delete a line that is matched by the search string from the provided file. This will delete ONLY the first
* line it matches. The line must contain the search query, which can be either a string or regex value.
* If dry mode, print the fully-qualified path of the file, as well as the contents and number of the line
* to be deleted.
* @param {string} file - path of the file from which the line should be deleted
* @param {string|RegExp} to_find_line - search query used to match the line
* @returns {Promise<void>}
*/
async delete_line(file, to_find_line){
file = path.resolve(file)
let contents = await fs.promises.readFile(file)
let lines = contents.toString().split("\n")
for ( let i in lines ){
i = parseInt(i)
const line = lines[i]
if ( line.search(to_find_line) >= 0 ){
lines.splice(i, 1)
if ( dry_run ){
console.log("")
console.log("Will remove line from file: "+file)
console.log("(line "+i+"): "+line)
}
break
}
}
if ( !dry_run ){
let contents = ""
for ( let i in lines ){
contents = contents + lines[i]
if ( i < (lines.length-1) ) {
contents = contents + "\n"
}
}
await fs.promises.unlink(file, contents)
await fs.promises.writeFile(file, contents)
}
},
/**
* Insert a line into the specified file after the line matched by the search query. This function will insert
* the specified text after the first instance of a line that contains the search query, whether it be a string
* or regex value. If dry mode, print the fully-qualified path of the file to be modified, as well as the
* content and numbers of the existing and new lines.
* @param {string} file - path of the file in which the line should be inserted
* @param {string|RegExp} to_find_line_before - search query used to find the line under which the new line should be inserted
* @param {string} to_insert - line to be inserted in the file
* @param {boolean} [search_insert_before = false] - if true, will insert the string BEFORE the found line
* @returns {Promise<void>}
*/
async insert_line(file, to_find_line_before, to_insert, search_insert_before = false){
file = path.resolve(file)
let contents = await fs.promises.readFile(file)
let lines = contents.toString().split("\n")
for ( let i in lines ){
i = parseInt(i)
const line = lines[i]
if ( line.search(to_find_line_before) >= 0 ){
const splicer = search_insert_before ? i : i+1
lines.splice(splicer, 0, to_insert)
if ( dry_run ){
console.log("")
console.log("Will insert line in file: "+file)
if ( !search_insert_before ) console.log("After (line "+i+"): "+lines[i])
console.log(" + "+lines[splicer])
if ( search_insert_before ) console.log("Before (line "+(i+1)+"): "+lines[i+1])
}
break
}
}
if ( !dry_run ){
let contents = ""
for ( let i in lines ){
contents = contents + lines[i]
if ( i < (lines.length-1) ) {
contents = contents + "\n"
}
}
await fs.promises.unlink(file, contents)
await fs.promises.writeFile(file, contents)
}
},
/**
* Find and replace instances of the search query in the specified file. The specified query is applied once
* per line. This means that if the search query is a string, it will only replace the first instance of
* the string on each line. Regex is allowed to compensate for this. If the 'all' toggle is false, the search
* will stop after the first line matching the search string is found. If dry run, print the fully-qualified
* path of the file to be modified, as well as the numbers, before, and after contents of each line to be
* modified.
* @param {string} file - path of the file to be modified
* @param {string|RegExp} to_find - search query of text to be replaced
* @param {string} replace_with - text to be inserted
* @param {boolean} [all = true] - if true, then the search will be performed on each line of the file. Otherwise, stop after the first match.
* @returns {Promise<void>}
*/
async find_replace(file, to_find, replace_with, all=true){
file = path.resolve(file)
let contents = await fs.promises.readFile(file)
let lines = contents.toString().split("\n")
let modify_lines = []
for ( let i in lines ){
const line = lines[i]
if ( line.search(to_find) >= 0 ){
modify_lines.push(i)
if ( !all ){
break
}
}
}
if ( dry_run ){
console.log("Will Modify Lines in: "+file)
for ( let i in modify_lines ){
const ind = modify_lines[i]
console.log("old (line "+ind+"): "+lines[ind])
console.log("new (line "+ind+"): "+lines[ind].replace(to_find, replace_with))
console.log("")
}
}
else {
for ( let i in modify_lines ){
const ind = modify_lines[i]
lines[ind] = lines[ind].replace(to_find, replace_with)
}
let contents = ""
for ( let i in lines ){
contents = contents + lines[i]
if ( i < (lines.length-1) ) {
contents = contents + "\n"
}
}
await fs.promises.unlink(file, contents)
await fs.promises.writeFile(file, contents)
}
},
/**
* Count the number of times the provided regex is matched in a string.
* @param {string} string - string to be searched
* @param {RegExp} regex - Regular expression to match against
* @returns {number} - the number of times the string matches the regex
*/
count(string, regex){
return ((string || '').match(regex) || []).length
},
/**
* Find and replace instances of the search query in the specified file. The specified query is applied once
* to the entire file. Regex or a string are allowed as search criteria. Optionally, a replace_handler function
* can be provided to alter the replacements before they are inserted.
* @param {string} file - path of the file to be modified
* @param {string|RegExp} to_find - search query of text to be replaced
* @param {string} replace_with - text to be inserted
* @param {function} [replace_handler = (m) => m] - handler called on each match to alter before insert. must return a string.
* @param {boolean} [reformat = true] - if true, the file will be reformatted before write
* @returns {Promise<void>}
*/
async find_replace_file(file, to_find, replace_with, replace_handler = (m) => m, reformat = true){
file = path.resolve(file)
let contents = await (await fs.promises.readFile(file)).toString()
if ( dry_run ){
const re_to_find = new RegExp(to_find)
let match;
let i = 0; const max = this.count(contents, to_find);
while ( match = re_to_find.exec(contents) ){
if ( !i < max ) break;
const str = contents.substring(0, match.index)
const lines = str.split(/\r\n|\r|\n/).length
console.log(`old (line ${lines}): `, format(match[0]))
console.log(`new (line ${lines}): `, format(replace_handler(replace_with.replace(/%orig/g, match[0]))))
console.log("")
i++;
}
}
else {
let replace_contents = contents.replace(to_find, (match) => {
return replace_handler(replace_with.replace(/%orig/g, match))
})
if ( reformat ) replace_contents = format(replace_contents)
await fs.promises.unlink(file)
await fs.promises.writeFile(file, replace_contents)
}
}
}
}