@pega/custom-dx-components
Version:
Utility for building custom UI components
470 lines (410 loc) • 13 kB
JavaScript
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,
getServerType,
sanitize,
validateSemver,
showVersion,
updateComponentDefaultLibrary,
updateComponentDefaultVersion,
copyFileToShared,
getLibraryBased,
addDebugLog,
checkLibraryAndArchives,
getConfigDefaults,
getSubTypeLabel,
getMaxComponentNameCount,
getStandaloneMaxLibraryCount,
getStandaloneMaxComponentNameCount,
getStandaloneMaxLibraryFromPrefixCount
} 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 }
) => {
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 subTypeLabel = getSubTypeLabel(type, subtype);
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 || componentLabel,
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,
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
});
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 >= 11) {
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 description = options.params[9];
const organization = options.params[10];
const framework = "Constellation";
await validateCompile(
{
componentName,
componentLabel,
library,
version,
framework,
type,
subtype,
description,
organization
},
options
);
} else {
const framework = "Constellation";
const componentDefaults = getConfigDefaults();
const serverType = getServerType();
const maxChars = isLibraryBased ? getMaxComponentNameCount() : getStandaloneMaxComponentNameCount();
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: value => {
if (serverType === "launchpad") {
return `Enter component name (required)`;
}
else {
return `Enter component name (required) (${maxChars} max chars)`;
}
},
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 > maxChars && serverType != "launchpad") {
return `${value.length - maxChars} too many characters.`
}
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
}
];
const answers = await inquirer.prompt(questions);
const maxLibChars = await getStandaloneMaxLibraryCount(answers.componentName);
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
},
{
name: 'description',
message: 'Enter description for the component (default is label)',
default: componentDefaults.description
}
];
const libAnswers = await inquirer.prompt(libQuestions);
await validateCompile(
{
componentName : answers.componentName,
componentLabel : answers.componentLabel,
library : libAnswers.library,
version : answers.version,
framework,
type : answers.type,
subtype : answers.subtype,
description : libAnswers.description
},
options
);
}
addDebugLog("create", "END", "-");
};