UNPKG

@pega/custom-dx-components

Version:

Utility for building custom UI components

452 lines (401 loc) 12.2 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'; import { convertIntoPascalCase, compileMustacheTemplate, getComponentDirectoryPath, getComponents, getPegaServerConfig, sanitize, validateSemver, showVersion, updateComponentDefaultLibrary, updateComponentDefaultVersion, copyFileToShared, getLibraryBased, addDebugLog, checkLibraryAndArchives, getConfigDefaults } from '../../util.js'; import { COMPONENT_SCHEMA, SHARED_FILE_NAMES } from '../../constants.js'; const copy = promisify(ncp); const copyComponentTemplate = async options => { return copy(options.templateDirectory, options.targetDirectory, { clobber: false }); }; const compileMustacheTemplates = async ( componentDirectory, { componentKey, componentName, componentLabel, library, version, type, subtype, description, organization, allowedApplications = '' } ) => { 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; 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 output = compileMustacheTemplate(filePath, { COMPONENT_KEY: componentKey, COMPONENT_NAME: componentName, COMPONENT_LABEL: componentLabel, COMPONENT_CLASS_NAME: componentClassName, ORGANIZATION: organization, VERSION: version, LIBRARY: library, ALLOWEDAPPS: JSON.stringify(allowedApplications), TYPE: type, SUB_TYPE: JSON.stringify(mustacheSubType), 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', '')); } }); }; const validateCompile = async ( { componentName, componentLabel, library, version, framework, type, subtype, description, allowedApplications, organization }, options ) => { const isLibraryBased = getLibraryBased(); let sSubType = subtype; if (Array.isArray(subtype)) { sSubType = subtype.join("-"); } // for now, until add question back in if (framework == undefined || framework == "") { framework = "Constellation"; } const orgLib = getConfigDefaults(); if (isLibraryBased) { // library comes from config library = orgLib.library; } // check of library or version has changed from default, if so, update componentDefaults const defaultPegaServerConfig = await getPegaServerConfig(); // update defaults when changes if (orgLib.library == null || orgLib.library !== library) { await updateComponentDefaultLibrary(library); } // library based, get version from defaults if (isLibraryBased) { version = orgLib.buildVersion; } else { if (orgLib.version == null || orgLib.version !== version) { await updateComponentDefaultVersion(version); } } const isLaunchpad = defaultPegaServerConfig.serverType === 'launchpad'; let originalFramework = framework; let templateDir = `./templates/${framework}/${type}/${sSubType}`; let templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); if (isLaunchpad) { framework = "Launchpad"; templateDir = `./templates/${framework}/${type}/${sSubType}`; templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); // if doesn't exist, revert if (!fs.existsSync(templateDirectory)) { framework = originalFramework; templateDir = `./templates/${framework}/${type}/${sSubType}`; templateDirectory = fileURLToPath(new URL(templateDir, import.meta.url)); } } if (organization == null) { ({ organization } = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8'))); } const componentKey = `${organization}_${library}_${componentName}`; const targetDirectory = await getComponentDirectoryPath(componentKey); const components = await getComponents(); if (components.includes(componentKey)) { console.log(chalk.red(`${componentKey} component already exists in ${targetDirectory}`)); return; } await copyComponentTemplate({ ...options, targetDirectory, templateDirectory }); await compileMustacheTemplates(targetDirectory, { componentKey, componentName, componentLabel, library, version, type, subtype, description, organization, allowedApplications }); console.log(chalk.green(`${componentKey} component is created in ${targetDirectory}`)); }; export default async options => { await showVersion(); await checkLibraryAndArchives(); const isLibraryBased = getLibraryBased(); addDebugLog("create", "", "+"); if (options.params.length >= 12) { const type = options.params[3]; const subtype = options.params[4]; const componentName = options.params[5]; const componentLabel = options.params[6]; const version = options.params[7]; const library = options.params[8]; const allowedApplications = options.params[9]; const description = options.params[10]; const organization = options.params[11]; const framework = "Constellation"; await validateCompile( { componentName, componentLabel, library, version, framework, type, subtype, description, allowedApplications, organization }, options ); } else { const componentDefaults = getConfigDefaults(); const questions = [ { name: 'type', type: 'rawlist', message: 'Enter type of component', default: componentDefaults.type, choices: COMPONENT_SCHEMA.type }, { name: 'subtype', type: 'rawlist', message: 'Enter subtype of component', default: componentDefaults.subtype, choices: COMPONENT_SCHEMA.subtype.field, when(answers) { return answers.type === 'Field'; } }, { name: 'subtype', type: 'rawlist', message: 'Enter subtype of component', default: componentDefaults.subtype, choices: COMPONENT_SCHEMA.subtype.template, when(answers) { return answers.type === 'Template'; } }, { name: 'subtype', type: 'rawlist', message: 'Enter subtype of component', default: [componentDefaults.subtype || COMPONENT_SCHEMA.subtype.widget[0]], choices: COMPONENT_SCHEMA.subtype.widget, when(answers) { return answers.type === 'Widget'; }, validate: value => { /* value should not be empty */ if (Array.isArray(value) && value.length) { return true; } return 'Please select subtype'; } }, { name: 'componentName', message: 'Enter component name (required)', 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)) { return true; } return 'Only alphanumeric values are allowed, starting with alphabets, no spaces.'; } }, { name: 'componentLabel', message: 'Enter component label for display (required)', validate: value => { if (value) { return true; } return 'Please provide value for label'; } }, { 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 }, { name: 'library', message: 'Enter library name (required)', 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)) { return true; } return 'Only alphanumeric values are allowed, starting with alphabets'; }, when: () => !isLibraryBased }, { name: 'allowedApplications', message: 'Please enter the application names to be supported (comma-separated). ', suffix: 'Keep empty for all applications', when: () => !isLibraryBased }, { name: 'description', message: 'Enter description for the component (default is label)', default: componentDefaults.description } ]; await inquirer.prompt(questions).then(async answers => { const { componentName, componentLabel, library, version, framework, type, subtype, description, allowedApplications } = answers; await validateCompile( { componentName, componentLabel, library, version, framework, type, subtype, description, allowedApplications }, options ); }); } addDebugLog("create", "END", "-"); };