boil-cli-tool
Version:
CLI tool - boilerplate template manager and generator
225 lines (224 loc) • 9.97 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFunctionValues = exports.extractFunctionInputArgs = exports.undefinedFunctions = exports.splitArgs = exports.generateBoilerplate = exports.dirExists = exports.validateArgs = exports.compareUserRequiredArgs = exports.userProvidedArgs = exports.localAndGlobalArgs = exports.getTemplateArgs = void 0;
const tslib_1 = require("tslib");
// packages
const fs_1 = require("fs");
const path_1 = tslib_1.__importDefault(require("path"));
const read_data_1 = tslib_1.__importDefault(require("read-data"));
const lodash_1 = require("lodash");
const pipe_1 = tslib_1.__importDefault(require("lodash/fp/pipe"));
// utils
const utils_1 = require("../utils");
const extractArg = (arg) => {
const placeholderRegex = /___([^_]+?)___/g;
// trim to ensure whitespaces around the arg are removed
return [...arg.matchAll(placeholderRegex)].map((match) => match[1]?.trim());
};
const containsBrackets = (arg) => arg.match(/\(.*?\)/);
const replaceArgs = (content, argPlaceholderValues // e.g. {name: 'App', filetype: 'js'}
) => {
// remove whitespaces only between '___' pairs, e.g. "___ WORD ___" => "___WORD___"
const contentWithTrimmedPlaceholders = content.replace(/___\s*(.*?)\s*___/g, (_, group) => {
return `___${group.replace(/\s+/g, "")}___`;
});
// replace arg placeholders with values
const newContent = Object.keys(argPlaceholderValues).reduce((output, arg) => {
return output.replace(`___${arg}___`, argPlaceholderValues[arg]);
}, contentWithTrimmedPlaceholders);
// 'replace' only finds the first match ('replaceAll' not yet supported)
// so, keep running this function recursively until no template args remain
const extractedArgs = lodash_1.uniq(extractArg(newContent));
if (extractedArgs.length > 0) {
return replaceArgs(newContent, argPlaceholderValues);
}
return newContent;
};
// regex looks for anything between double underscores (___*___)
const extractArgsArray = (arg) => {
const templateArg = extractArg(arg);
// trim whitespaces
if (templateArg) {
return templateArg.map((arg) => arg.trim());
}
return [];
};
exports.getTemplateArgs = (template) => {
const rootPath = `./.boilerplate/${template}`;
const args = [];
// recursively look for template args (___*___) in directory names, file names and within files
const argsFromDirectoriesFilenamesFileContent = (path) => {
const directoriesAndFiles = fs_1.readdirSync(path);
directoriesAndFiles.forEach((dirOrFile) => {
const nestedFile = `${path}/${dirOrFile}`;
// if a file then extract template args from its contents
if (fs_1.lstatSync(nestedFile).isFile()) {
const content = fs_1.readFileSync(nestedFile, "utf8");
extractArgsArray(content).forEach((arg) => args.push(arg));
}
// if a directory then extract template args from its name
extractArgsArray(dirOrFile).forEach((arg) => {
args.push(arg);
});
// if nested directories exist then recursively look for template args at that path
const nestedPath = `${path}/${dirOrFile}`;
if (fs_1.lstatSync(nestedPath).isDirectory()) {
const nestedDirectories = fs_1.readdirSync(nestedPath);
if (nestedDirectories) {
argsFromDirectoriesFilenamesFileContent(nestedPath);
}
}
});
};
// start looking for args in the template root path
argsFromDirectoriesFilenamesFileContent(rootPath);
// return array of unique arg names
return lodash_1.uniq(args);
};
exports.localAndGlobalArgs = (template) => {
const rootPath = `./.boilerplate`;
let args = {};
const getArgs = (path) => {
if (fs_1.existsSync(path)) {
const argsObject = read_data_1.default.sync(path) || {};
args = { ...args, ...argsObject };
}
};
const globalPath = `${rootPath}/global.args.yml`;
const localPath = `${rootPath}/${template}/local.args.yml`;
getArgs(globalPath);
getArgs(localPath);
return args;
};
exports.userProvidedArgs = (template) => {
const inputs = process.argv;
const templateIndex = inputs.indexOf(template);
const inputsAfterTemplate = inputs.slice(templateIndex + 1);
return pipe_1.default(lodash_1.chunk, lodash_1.fromPairs)(inputsAfterTemplate, 2);
};
exports.compareUserRequiredArgs = (requiredArgs, userArgs) => Object.values(requiredArgs).map((requiredArg) => Object.keys(userArgs).reduce((output, userArg) => {
const value = userArgs[userArg];
const [nameRegex, shorthandRegex] = [
userArg.match(/(?<=--).*/g),
userArg.match(/(?<=-).*/g),
];
const userObj = {
name: nameRegex && nameRegex[0],
shorthand: !nameRegex && shorthandRegex[0],
};
const nameMatch = requiredArg.name === userObj.name;
const shorthandMatch = requiredArg.shorthand === userObj.shorthand;
if (nameMatch || shorthandMatch)
return { ...requiredArg, value };
return output;
}, {}));
exports.validateArgs = (comparedArgs, requiredArgs) => {
return comparedArgs.map((args, idx) => {
let output = args;
const hasArgs = Object.keys(output).length > 0;
if (!hasArgs) {
const requiredArg = Object.values(requiredArgs)[idx];
output = { ...requiredArg, value: requiredArg.default };
}
const validInputAgainstOptions = output.options
? !!output.options.find((option) => option === output.value)
: !!output.value; // if the value is undefined then this arg will be false
return { ...output, valid: validInputAgainstOptions };
});
};
exports.dirExists = (path) => fs_1.existsSync(path);
exports.generateBoilerplate = (template, source, args) => {
const rootPath = `./.boilerplate/${template}`;
const withValues = (str) => replaceArgs(str, args);
const makeFilesFolders = (path) => {
const directoriesAndFiles = fs_1.readdirSync(path);
directoriesAndFiles.forEach((dirOrFile) => {
if (dirOrFile !== "local.args.yml") {
const nestedPath = `${path}/${dirOrFile}`;
const stats = fs_1.lstatSync(nestedPath);
const [isFile, isDirectory] = [stats.isFile(), stats.isDirectory()];
const writePath = withValues(nestedPath.replace(rootPath, source));
const formattedPath = writePath.replace("//", "/");
const successMsg = () => {
return console.log(`${utils_1.emoji(":white_check_mark:")} writing: ${formattedPath}`);
};
const failMsg = () => {
return console.log(`${utils_1.emoji(":no_entry:")} '${formattedPath}' already exists`);
};
// if directory then replace any args in folder name with value
// also, recursively callback 'makeFilesFolders' to look for any nested files/folders
if (isDirectory) {
if (fs_1.existsSync(writePath)) {
failMsg();
}
else {
fs_1.mkdirSync(writePath);
makeFilesFolders(nestedPath);
successMsg();
}
}
// if file then replace any args in file name with value
// also, write the file contents (also replacing any args with values)
if (isFile) {
if (fs_1.existsSync(writePath)) {
failMsg();
}
else {
const data = withValues(fs_1.readFileSync(nestedPath, "utf8"));
fs_1.writeFileSync(writePath, data);
successMsg();
}
}
}
});
};
makeFilesFolders(rootPath);
};
exports.splitArgs = (args) => {
return args.reduce((argsObject, arg) => {
const output = lodash_1.cloneDeep(argsObject);
if (containsBrackets(arg)) {
output.functionalArgs.push(arg);
}
else {
output.templateArgs.push(arg);
}
return output;
}, { templateArgs: [], functionalArgs: [] });
};
const extractFunctionName = (fn) => {
const bracket = containsBrackets(fn);
const bracketIndex = bracket.index;
return fn.slice(0, bracketIndex);
};
exports.undefinedFunctions = (args) => {
const root = fs_1.readdirSync("./.boilerplate");
const missingFunctions = args.filter((arg) => {
return !root.find((file) => file === `${extractFunctionName(arg)}.js`);
});
return missingFunctions.map((fn) => `${extractFunctionName(fn)}.js`);
};
const extractInputArgs = (fn) => {
return fn
.replace(extractFunctionName(fn), "") // remove function name
.slice(1, -1) // remove enclosing () brackets
.split(",")
.map((fn) => fn.trim());
};
exports.extractFunctionInputArgs = (functions) => {
const inputArgs = functions
.map((fn) => extractInputArgs(fn))
.flat()
.filter((arg) => arg.length > 0); // exclude empty strings (functions with no args)
return lodash_1.uniq(inputArgs);
};
exports.getFunctionValues = (functions, args) => {
return functions.reduce((output, fn) => {
const functionName = extractFunctionName(fn);
const inputArgs = extractInputArgs(fn).map((val) => args[val]);
const functionPath = path_1.default.relative(__dirname, `.boilerplate/${functionName}.js`);
const templateFunction = require(functionPath);
const result = templateFunction(...inputArgs);
return { ...output, [fn]: result };
}, {});
};