UNPKG

@pega/custom-dx-components

Version:

Utility for building custom UI components

513 lines (437 loc) 15.9 kB
import path from 'path'; import { URL, fileURLToPath } from 'url'; import fs from 'fs'; import { promisify } from 'util'; import inquirer from 'inquirer'; import ncp from 'ncp'; import chalk from 'chalk'; // eslint-disable-next-line import/order import { checkPathAccess, showVersion , convertIntoPascalCase, compileMustacheTemplate, getComponentDirectoryPath, getPegaServerConfig, getServerType, getComponents, sanitize, validateSemver, updateComponentDefaultLibrary, updateComponentDefaultVersion, copyFileToShared, getLibraryBased, addDebugLog, checkLibraryAndArchives, getConfigDefaults, getSubTypeLabel, getMaxPrefixCount, getStandaloneMaxPrefixCount, getMaxComponentLibraryCount, getStandaloneMaxLibraryFromPrefixCount } from '../../util.js'; import { COMPONENT_SCHEMA, COMPONENTS_DIRECTORY_PATH, TASKS_CONFIG_JSON_FILENAME, COMPONENTS_PATH, CATEGORY_CONSTELLATION, SHARED_FILE_NAMES } from '../../constants.js'; import lib from 'babel-loader'; export const DXCB_CONFIG_INTERNAL_JSON_FILENAME = 'src/dxcb.config.json'; const pegaConfigJsonPath = path.join(path.resolve(), TASKS_CONFIG_JSON_FILENAME); const copy = promisify(ncp); const copyComponentTemplate = async (options) => { if (options.targetDirectory) { return copy(options.templateDirectory, options.targetDirectory, { clobber: false }); } else { return copy(options.templateDirectoryConstellation, options.targetDirectoryConstellation, { clobber: false, filter: (fileName) => { // filter out the file config.json or config.json.mustache if (fileName.indexOf('config.json') >= 0) return false; else return true; } }); } }; export const compileMustacheTemplates = async ( componentDirectory, { componentKey, componentName, componentLabel, library, version, type, subtype, description, organization } ) => { const files = fs.readdirSync(componentDirectory); const componentClassName = convertIntoPascalCase(componentKey); const isLibraryBased = getLibraryBased(); const isTemplate = type === 'Template'; const isBool = subtype === 'Boolean'; const isPhone = subtype === 'Phone'; if (Array.isArray(subtype) && subtype.length === 1) { subtype = subtype.toString(); } let mustacheSubType = subtype; // overrides if (typeof mustacheSubType === 'string') { // eslint-disable-next-line default-case switch (mustacheSubType) { case 'FORM-region': mustacheSubType = 'FORM'; break; case 'DETAILS-region': mustacheSubType = 'DETAILS'; break; case 'Text-Input': mustacheSubType = 'Text'; break; case 'Picklist-Autocomplete': mustacheSubType = 'Picklist'; break; case 'Picklist-RadioButtons': mustacheSubType = 'Picklist'; break; case 'Icon-Button-URL': mustacheSubType = 'Text-URL'; break; } } const isPicklist = subtype === 'Picklist'; // if (allowedApplications.trim() === '') { // allowedApplications = []; // } else { // if (allowedApplications.indexOf(',') !== -1) { // allowedApplications = allowedApplications.split(','); // } // allowedApplications = [].concat(allowedApplications); // } // allowedApplications = allowedApplications.filter((el) => !!el.trim()).map((el) => el.trim()); files.forEach((file) => { const filePath = path.join(componentDirectory, file); const subTypeLabel = getSubTypeLabel(type, subtype).toString(); const output = compileMustacheTemplate(filePath, { COMPONENT_KEY: componentKey, COMPONENT_NAME: componentName, COMPONENT_LABEL: componentLabel, COMPONENT_CLASS_NAME: componentClassName, ORGANIZATION: organization, VERSION: version, LIBRARY: library, TYPE: type, SUB_TYPE: JSON.stringify(mustacheSubType), SUB_TYPE_LABEL: subTypeLabel, DESCRIPTION: description, IS_TEMPLATE: isTemplate, IS_PICKLIST: isPicklist, IS_BOOLEAN: isBool, IS_PHONE: isPhone }); const realFileName = file.replace('.mustache', ''); if ( isLibraryBased) { if (SHARED_FILE_NAMES.includes(realFileName)) { // write to shared copyFileToShared(realFileName, output); // delete file from main components fs.rmSync(filePath); } else if (realFileName.includes(".tsx") || realFileName.includes(".ts") ) { // going to search for shared file names an change the import path let newOutput = output; for (const index in SHARED_FILE_NAMES) { const sFileName = SHARED_FILE_NAMES[index].split(".")[0]; const orgPath = "./".concat(sFileName); const newPath = "../shared/".concat(sFileName); newOutput = newOutput.replaceAll(orgPath, newPath); } fs.writeFileSync(filePath, newOutput); fs.renameSync(filePath, filePath.replace('.mustache', '')); } else { fs.writeFileSync(filePath, output); fs.renameSync(filePath, filePath.replace('.mustache', '')); } } else { fs.writeFileSync(filePath, output); fs.renameSync(filePath, filePath.replace('.mustache', '')); } }); }; export const validateCompile = async ( { library, organization, componentName, componentLabel, version, framework, type, subtype, description }, options ) => { let sSubType = subtype; if (Array.isArray(subtype)) { sSubType = subtype.join('-'); } const isLibraryBased = getLibraryBased(); framework = convertIntoPascalCase(CATEGORY_CONSTELLATION); const templateParentDir = `./templates/${framework}`; let templateDir = `${templateParentDir}/${type}/${sSubType}`; let templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); templateDirectory = templateDirectory.replace(`${path.sep }create-all`, `${path.sep }create`); const defaultPegaServerConfig = await getPegaServerConfig(); const isLaunchpad = defaultPegaServerConfig.serverType === 'launchpad'; const originalFramework = framework; // check of library or version has changed from default, if so, update componentDefaults const componentDefaults = getConfigDefaults(); if (componentDefaults.library == null || componentDefaults.library !== library) { await updateComponentDefaultLibrary(library); } // library based, get version from defaults if (isLibraryBased) { version = componentDefaults.version; if (defaultPegaServerConfig.devBuild) { version = version.concat("-dev"); } } else { if (componentDefaults.version == null || componentDefaults.version !== version) { await updateComponentDefaultVersion(version); } } if (isLaunchpad) { framework = "Launchpad"; templateDir = `./templates/${framework}/${type}/${sSubType}`; templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); templateDirectory = templateDirectory.replace(`${path.sep }create-all`, `${path.sep }create`); // if doesn't exist, revert if (!fs.existsSync(templateDirectory)) { framework = originalFramework; templateDir = `./templates/${framework}/${type}/${sSubType}`; templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); templateDirectory = templateDirectory.replace(`${path.sep }create-all`, `${path.sep }create`); } } const componentKey = `${organization}_${library}_${componentName}`; const targetDirectory = await getComponentDirectoryPath(componentKey); const components = await getComponents(false); if (components.includes(componentKey)) { console.log(chalk.red(`${componentKey} component already exists in ${targetDirectory}`)); return; } // component await copyComponentTemplate({ ...options, targetDirectory, templateDirectory }); await compileMustacheTemplates(targetDirectory, { componentKey, componentName, componentLabel, library, version, type, subtype, description, organization }); console.log(chalk.green(`created ${componentKey} component in ${targetDirectory}`)); }; export const validateCompileAll = async ({ prefix, library, version, organization }, options) => { const defaultPegaServerConfig = await getPegaServerConfig(); // eslint-disable-next-line no-restricted-syntax, guard-for-in for (const fieldIdx in COMPONENT_SCHEMA.subtype.field) { const type = 'Field'; const subtype = COMPONENT_SCHEMA.subtype.field[fieldIdx].value; const prefixNoSpace = prefix.replace(/\s+/g, ''); const componentNameNoSpace = COMPONENT_SCHEMA.subtype.field[fieldIdx].name.replace(/\s+/g, ''); const componentName = ''.concat(prefixNoSpace).concat(componentNameNoSpace); const componentLabel = ''.concat(prefix).concat(' ').concat(COMPONENT_SCHEMA.subtype.field[fieldIdx].name); const description = componentLabel; // eslint-disable-next-line no-await-in-loop await validateCompile( { library, organization, componentName, componentLabel, version, type, subtype, description }, options ); } // eslint-disable-next-line no-restricted-syntax, guard-for-in for (const templateIdx in COMPONENT_SCHEMA.subtype.template) { const type = 'Template'; const subtype = COMPONENT_SCHEMA.subtype.template[templateIdx].value; const prefixNoSpace = prefix.replace(/\s+/g, ''); const templateNameOriginal = COMPONENT_SCHEMA.subtype.template[templateIdx].name; let templateName = convertIntoPascalCase(COMPONENT_SCHEMA.subtype.template[templateIdx].name); let templateLabel = convertIntoPascalCase(COMPONENT_SCHEMA.subtype.template[templateIdx].name); if (templateNameOriginal.indexOf('region') >= 0) { const templateNameBase = convertIntoPascalCase(templateNameOriginal.replace(' region', '')); templateName = 'TwoColumn'.concat(templateNameBase); templateLabel = 'Two Column '.concat(templateNameBase); } const componentName = ''.concat(prefixNoSpace).concat(templateName); const componentLabel = ''.concat(prefix).concat(' ').concat(templateLabel); const description = componentLabel; // eslint-disable-next-line no-await-in-loop await validateCompile( { library, organization, componentName, componentLabel, version, type, subtype, description }, options ); } // eslint-disable-next-line no-restricted-syntax, guard-for-in for (const widgetIdx in COMPONENT_SCHEMA.subtype.widget) { const type = 'Widget'; const subtype = COMPONENT_SCHEMA.subtype.widget[widgetIdx].value; const prefixNoSpace = prefix.replace(/\s+/g, ''); const widgetNameOriginal = COMPONENT_SCHEMA.subtype.widget[widgetIdx].name; let widgetName = convertIntoPascalCase(COMPONENT_SCHEMA.subtype.widget[widgetIdx].name).concat('Widget'); const widgetLabel = convertIntoPascalCase(COMPONENT_SCHEMA.subtype.widget[widgetIdx].name).concat(' Widget'); if (widgetName.indexOf('&') >= 0) { widgetName = convertIntoPascalCase(widgetNameOriginal.replace(' & ', '')); } const componentName = ''.concat(prefixNoSpace).concat(widgetName); const componentLabel = ''.concat(prefix).concat(' ').concat(widgetLabel); const description = componentLabel; // eslint-disable-next-line no-await-in-loop await validateCompile( { library, organization, componentName, componentLabel, version, type, subtype, description }, options ); } }; export default async (options) => { await showVersion(); await checkLibraryAndArchives(); const isLibraryBased = getLibraryBased(); addDebugLog("createAll", "", "+"); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = data && JSON.parse(data); if (!data[COMPONENTS_DIRECTORY_PATH]) { console.error(`${chalk.red.bold('ERROR')} Unable to find components directory path in config.json`); process.exit(1); } const componentData = data[COMPONENTS_PATH]; let library; ({ library } = componentData); let { organization } = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')); const componentDefaults = getConfigDefaults(); componentDefaults.library = library; if (options.params.length >= 7) { const prefix = options.params[3]; const version = options.params[4]; library = options.params[5]; // overwrite organization = options.params[6]; await validateCompileAll( { prefix, library, version, organization }, options ); } else { const maxPrefixCount = isLibraryBased ? getMaxPrefixCount() : getStandaloneMaxPrefixCount(); const serverType = getServerType(); const questions = [ { name: 'prefix', message: value => { if (serverType === "launchpad") { return `Enter prefix label`; } else { return `Enter prefix label (${maxPrefixCount} max chars)`; } }, validate: value => { /* value should not be empty It should not start with a number Only case-insensitive alphanumeric values are allowed */ if (value && !/^\d/.test(value) ) { if (value.length > maxPrefixCount && serverType != "launchpad") { return `${value.length - maxPrefixCount} too many characters.` } return true; } return 'Only alphanumeric values are allowed, starting with alphabets'; } }, { name: 'version', message: 'Enter component version', default: componentDefaults.version, validate: (value) => { if (validateSemver(value)) { return true; } return 'Please provide semver compatible version e.g 0.0.1'; }, when: () => !isLibraryBased } ]; const answers = await inquirer.prompt(questions); const maxLibChars = await getStandaloneMaxLibraryFromPrefixCount(answers.prefix); const libQuestions = [ { name: 'library', message: value => { if (serverType === "launchpad") { return `Enter library name (required)`; } else { return `Enter library name (required) (${maxLibChars} max chars)`; } }, default: componentDefaults.library, validate: (value) => { /* value should not be empty It should not have spaces It should not start with a number Only case-insensitive alphanumeric values are allowed */ if (value && !/^\d/.test(value) && value === sanitize(value)) { if (value.length > maxLibChars && serverType != "launchpad") { return `${value.length - maxLibChars} too many characters.` } return true; } return 'Only alphanumeric values are allowed, starting with alphabets'; }, when: () => !isLibraryBased } ]; const libAnswers = await inquirer.prompt(libQuestions); library = libAnswers.library; if (isLibraryBased) { // library comes from config const orgLib = getConfigDefaults(); library = orgLib.library; } const { version } = answers; let { prefix } = answers; prefix = prefix.replace(/[^a-zA-Z0-9 ]/g, ''); await validateCompileAll( { prefix, library, version, organization }, options ); } addDebugLog("createAll", "END", "-"); };