You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
369 lines
15 KiB
369 lines
15 KiB
/**
|
|
* @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)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|