@pega/custom-dx-components
Version:
Utility for building custom UI components
513 lines (437 loc) • 15.9 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';
// 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", "-");
};