UNPKG

boil-cli-tool

Version:

CLI tool - boilerplate template manager and generator

225 lines (224 loc) 9.97 kB
"use strict"; 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 }; }, {}); };