/**
* @class Mitosis
* @author Ricupero Marco
*/
class Mitosis {
/**
* @method fetch
* @author Ricupero Marco
* @description Fetch a path for its content and return a Promise which will success with a full list of files
* @param {string} srcPath target path
* @param {Object} settings fetch settings
* @param {boolean} [settings.excludeHidden=true] Determinates if hidden files should be excluded or included in the process
* @param {boolean} [settings.excludeSymLinks=true] Determinates if symbolic links should be excluded or included in the process
* @returns {Promise<{files: Array<string>, directories: Array<string>}>} All found files and directories in the given srcPath
* @async
* @requires fs
* @requires path
* @requires lodash
* @memberof Mitosis
*/
static async fetch(srcPath, {
excludeHidden = true,
excludeSymLinks = true
} = {
excludeHidden: true,
excludeSymLinks: true
}) {
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const promiser = syncFunction => {
return async (...params) => syncFunction(...params);
};
fs.existsSync(srcPath);
fs.accessSync(srcPath, fs.constants.R_OK);
return promiser(fs.readdirSync)(srcPath)
.then(dirContent => {
const targetContent = excludeHidden ? dirContent.filter(entry => !(/^\./).test(entry)) : dirContent;
return Promise.all(targetContent.map(entry => {
const entryPath = path.join(srcPath, entry);
return promiser(fs.statSync)(entryPath)
.then(stat => (!excludeSymLinks || !stat.isSymbolicLink()) && stat)
.then(stat => {
if (!stat) {
return null;
}
if(stat.isDirectory()) {
return Mitosis.fetch(entryPath, {
excludeHidden,
excludeSymLinks
}).then(({ directories = [], files = [] }) => ({
directories: [entryPath].concat(directories),
files
}));
}
return {files: [entryPath], directories: [path.normalize(srcPath)]};
});
}).concat(targetContent.length ? [] : {directories: [path.normalize(srcPath)]}))
.then((fetchList = []) => {
const out = {directories: [], files: []};
fetchList.forEach(({directories = [], files = []}) => {
out.files = out.files.concat(files);
out.directories = out.directories.concat(directories);
});
out.directories = _.sortedUniq(out.directories.map(path.normalize));
return out;
});
});
}
/**
* @method multiCaseReplacer
* @author Ricupero Marco
* @description Generate a custom function wich replace text with the given parameters
* @param {string} targetString target string to replace with replacingString
* @param {string} replacingString string to replace with
* @returns {Function} Replacing function
* @requires lodash
* @memberof Mitosis
*/
static multiCaseReplacer(targetString, replacingString) {
const _ = require('lodash');
const replacers = [
_.camelCase,
str => _.upperFirst(_.camelCase(str)),
_.kebabCase,
str => _.kebabCase(str).toUpperCase(),
_.snakeCase,
str => _.snakeCase(str).toUpperCase(),
_.startCase,
str => _.startCase(str).toLowerCase(),
str => _.upperFirst(_.startCase(str).toLowerCase()),
str => _.startCase(str).toUpperCase()
];
return str => ([str].concat(replacers)).reduce((text, replacer) => {
return text.replace(new RegExp(replacer(targetString), 'g'), replacer(replacingString));
});
}
/**
* @method fetch
* @author Ricupero Marco
* @description Path ending part
* @static
* @param {string} targetPath target path
* @returns {string} Returns only last directory/resource of the given path, excluding additional parameters
* @requires path
* @memberof Mitosis
*/
static pathFinalDir(targetPath = "") {
const path = require('path');
const normalizedPath = path.normalize(targetPath);
if(path.sep !== '\\' && normalizedPath.includes('\\')) {
return path.win32.basename(normalizedPath);
}
return path.basename(normalizedPath);
}
/**
* @method mkDirRecursive
* @author Ricupero Marco
* @description Recursive mkDir creating each dir of the provided path
* @static
* @param {string} [cwd=process.cwd()] current working dir
* @param {string} relativePath target relative path
* @returns {Promise<void>} Recursive mkDir Promise
* @async
* @requires fs
* @requires path
* @memberof Mitosis
*/
static mkDirRecursive({cwd = process.cwd(), relativePath = ""}) {
return new Promise((resolve, reject) => {
const fs = require('fs');
const path = require('path');
const splittedPath = ['.'].concat(relativePath.split(/\\|\//));
const mkDirList = splittedPath.reduce((prev, cur) => {
const prevList = [].concat(prev);
return prevList.concat(path.join(prevList.slice(-1)[0], cur));
}).filter(dir => !(/^(?:(?:\.{1,2}(?:[\\/]\.{1,2})*)|\w:)$/).test(dir));
try {
mkDirList.forEach(dir => {
const destDirFullPath = path.join(cwd, dir);
if (!fs.existsSync(destDirFullPath)) {
fs.mkdirSync(destDirFullPath);
}
});
resolve();
} catch(err) {
reject(err);
}
});
}
/**
* @method copy
* @author Ricupero Marco
* @description Perform a copy of the source path over the destination path replacing the gin string with the replacing string over file names and contents
* @param {string} srcPath Source path to copy from
* @param {string} destPath Destination path to copy to
* @param {Object} [settings = {targetString, replacingString, cwd}] Additional copy settings
* @param {string} [settings.targetString = pathFinalDir(srcPath)] Target/Source string to replace with replacingString on the copy
* @param {string} [settings.replacingString = pathFinalDir(destPath)] Replacing/Destination string
* @param {string} [settings.encoding = 'utf8'] Encoding to use for read/write
* @param {string} [settings.cwd = process.cwd()] Current working directory
* @returns {Promise<{files: Array<string>, directories: Array<string>}>} All copied files and directories in the given destPath
* @async
* @requires fs
* @requires path
* @requires @marketto/js-logger
* @memberof Mitosis
*/
static copy(srcPath, destPath, {
targetString = null,
replacingString = this.pathFinalDir(destPath),
encoding = 'utf8',
cwd = process.cwd()
} = {
targetString: null,
replacingString: this.pathFinalDir(destPath),
encoding: 'utf8',
cwd: process.cwd()
}) {
const fs = require('fs');
const path = require('path');
const logger = require("@marketto/js-logger").global();
const srcFullPath = path.isAbsolute(srcPath) ? srcPath : path.join(cwd, srcPath);
const destFullPath = path.isAbsolute(destPath) ? destPath : path.join(cwd, destPath);
targetString = targetString || this.pathFinalDir(srcFullPath);
logger.info(`Replacing ${targetString} => ${replacingString} in paths and contents...`);
const replacer = this.multiCaseReplacer(targetString, replacingString);
const promiser = syncFunction => {
return async (...params) => syncFunction(...params);
};
const copiedResources = {
directories: [],
files: []
};
return this.fetch(srcFullPath)
.then(fetchedData => {
logger.info('Source path fetched');
logger.info('Making directories...');
return this.mkDirRecursive({
cwd,
relativePath: path.relative(cwd, destFullPath)
}).then(() => fetchedData);
})
.then(({files, directories}) => {
const baseSrcPath = path.join(srcFullPath, '..');
const baseDestPath = path.join(destFullPath, '..');
directories
.sort()
.forEach(srcDirFullPath => {
const destDir = replacer(path.relative(baseSrcPath, srcDirFullPath));
const destDirFullPath = path.join(baseDestPath, destDir);
if (!fs.existsSync(destDirFullPath)) {
fs.mkdirSync(destDirFullPath);
}
copiedResources.directories.push(path.relative(cwd, destDirFullPath));
});
logger.info('Copying files...');
return Promise.all(files.map(srcFile => {
const destFile = replacer(path.relative(srcFullPath, srcFile));
const destFileFullPath = path.join(destFullPath, destFile);
logger.info(`Reading ${srcFile}`);
return promiser(fs.readFileSync)(srcFile, {encoding})
.then((data = '') => {
logger.info(`Replacing ${path.basename(destFileFullPath)} content...`);
return replacer(data);
})
.then(destinationData => {
logger.info(`Writing ${destFileFullPath} ...`);
return promiser(fs.writeFileSync)(destFileFullPath, destinationData)
.then(() => logger.info(`Successfully created ${destFileFullPath}`));
})
.then(() => copiedResources.files.push(path.join(destPath, destFile)));
}));
})
.then(() => logger.info('Copy Complete'))
.then(() => copiedResources);
}
}
module.exports = Mitosis;