UNPKG

@pega/dx-component-builder-sdk

Version:

Utility for building custom UI components

852 lines (684 loc) 26.9 kB
import fs from 'fs'; import path from 'path'; import { join } from 'path'; import { promisify } from 'util'; import https from 'https'; import inquirer from 'inquirer'; import crypto from 'node:crypto'; import { createRequire } from "module"; import chalk from 'chalk'; import mustache from 'mustache'; import pascalCase from 'pascalcase'; import fetch from 'node-fetch'; import kebabcase from 'kebab-case'; import { REACT_COMPONENTS_DIRECTORY_PATH, ANGULAR_COMPONENTS_DIRECTORY_PATH, SDK_CONFIG_JSON_FILENAME, DXCB_CONFIG, DXCB_PUBLISH_ACCESS, TOKEN_PATH, OOTB_COMPONENT_SERVICE_REST_ENDPOINT, DELETE_COMPONENT_SERVICE_REST_ENDPOINT, OOTB_COMPONENTS, FIELD_PATH, WIDGET_PATH, TEMPLATE_PATH, DSE_PATH, INFRA_PATH, COMPONENT_PREFIX_FIELD_PATH, COMPONENT_PREFIX_TEMPLATE_PATH, COMPONENT_PREFIX_WIDGET_PATH, COMPONENT_PREFIX_DSE_PATH, COMPONENT_PREFIX_INFRA_PATH, CATEGORY_CUSTOM, CATEGORY_OVERRIDE, CATEGORY_CONSTELLATION, COMPONENT_DIRECTORY_PREFIX_CUSTOM, COMPONENT_DIRECTORY_SUFFIX_SDK, DXCB_COMPONENT_DEFAULTS, SERVER_CONFIG, SERVER_REST_URL, JEST_TESTS_FUNCTIONAL_PATH } from './constants.js'; export const DXCB_CONFIG_INTERNAL_JSON_FILENAME = 'src/dxcb.config.json'; let componentsDirectoryPath = ""; let framework = ""; const access = promisify(fs.access); export const checkPathAccess = async (path, options = {}) => { const { errorMessage } = options; await updateComponentsDirectoryPath(); try { await access(path, fs.constants.R_OK); } catch (err) { const message = errorMessage || `${chalk.red.bold('ERROR')} Could not able to access path - ${path}`; console.error(message); process.exit(1); } }; export const updateComponentsDirectoryPath = async () => { if (framework === "") { framework = await getFramework(); switch (framework) { case "SDK-Angular": componentsDirectoryPath = ANGULAR_COMPONENTS_DIRECTORY_PATH; break; case "SDK-React": componentsDirectoryPath = REACT_COMPONENTS_DIRECTORY_PATH; break; case "SDK-WebComponents": break; } } } export const visualLCCategory = (category) => { switch (category) { case CATEGORY_CONSTELLATION : return "custom-".concat(category.toLowerCase()); case CATEGORY_CUSTOM : case CATEGORY_OVERRIDE : return category.toLowerCase().concat("-sdk"); } return category.toLowerCase(); } let componentFramework = ""; export const getFramework = async () => { if ( componentFramework === "") { const pegaConfigInternalJsonPath = path.join(path.resolve(), DXCB_CONFIG_INTERNAL_JSON_FILENAME); let internalData = fs.readFileSync(pegaConfigInternalJsonPath, { encoding: 'utf8' }); internalData = internalData && JSON.parse(internalData); componentFramework = internalData.sdkType; } return componentFramework } export const getLocalComponentConfig = async (componentKey, category = CATEGORY_CUSTOM) => { if (category != "custom") { return ""; } let configJSON = null; const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); const directory = join(currentDirectory, componentsDirectoryPath, categoryDirectory, componentKey); // test for the directory if (fs.existsSync(directory)) { // get config.json if exists const configJsonPath = join(directory, "config.json"); if (fs.existsSync(configJsonPath)) { let data = fs.readFileSync(configJsonPath, { encoding: 'utf8'}); configJSON = JSON.parse(data); } } return configJSON; } export const getLocalComponentKeyFromConfig = async (folderName, category) => { const configJSON = await getLocalComponentConfig(folderName, category); if (typeof(configJSON) == 'object') { return configJSON.componentKey; } else { return ""; } } export const deleteLocalComponent = async (componentKey, category = CATEGORY_CUSTOM) => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); const directory = join(currentDirectory, componentsDirectoryPath, categoryDirectory, componentKey); // test for the directory if (fs.existsSync(directory)) { //fs.rmdir(directory, { recursive: true }, err => { fs.rm(directory, { recursive: true }, err => { if (err) { throw err; } const sCat = visualLCCategory(category); console.log(`${chalk.red.bold(componentKey)} is deleted from Local ${sCat} `); }); } else { const categoryLC = visualLCCategory(category); console.log(`${chalk.red.bold(componentKey)} is NOT FOUND from Local ${categoryLC} `); } }; export const getHttpsAgent = (serverConfig) => { const agentOptions = { rejectUnauthorized: false }; if (serverConfig.legacyTLS) { agentOptions.secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT; } return new https.Agent(agentOptions); }; export const deleteServerComponent = async componentKey => { const serverConfig = await getPegaServerConfig(); const { server, user, password } = serverConfig; const url = constructCompleteUrl(server, DELETE_COMPONENT_SERVICE_REST_ENDPOINT); const [componentName, rulesetName, rulesetVersion] = componentKey.split('~|~'); const deleteUrl = `${url}/${componentName}/rulesetname/${rulesetName}/rulesetversion/${rulesetVersion}`; try { const OauthData = fs.readFileSync(TOKEN_PATH, 'utf8'); let status = 500; if (OauthData) { const { access_token: accessToken, token_type: tokenType, refresh_token: refreshToken } = JSON.parse(OauthData); fetch(deleteUrl, { method: 'DELETE', agent: getHttpsAgent(serverConfig), headers: { Authorization: `${tokenType} ${accessToken}` } }) .then(response => { status = response.status; if (status === 401) { throw new Error( 'Error occurred in authentication. Please regenerate using authenticate' ); // console.log(accessTokenUri, refreshToken); /* TODO - Handle refresh_token */ } else if (status === 404){ throw new Error('404: Server resource not found'); } else if (status === 405){ throw new Error('405: Server method not allowed'); } else if (status === 408){ throw new Error('408: Server timed out'); } return response.text(); }) .then(resp => { const respData = JSON.parse(resp); if (respData.status == 200) { console.log(chalk.bold.green(`Success : ${respData.message}`)); } else { throw new Error(`${respData.message}`); } }) .catch(e => { if (status === 403) { // eslint-disable-next-line prefer-promise-reject-errors Promise.reject( `${chalk.bold.red( 'Error forbidden: User does not have privileges to Delete.' )}` ); } else { // eslint-disable-next-line prefer-promise-reject-errors Promise.reject(`${chalk.bold.red(e)}`) } }); } } catch (error) { console.log(`\n${chalk.bold.red(error)}`); } }; export const getOptionsCategory = (options) => { let categoryType = null; if (options.params[3]) { let catType = options.params[3]; switch (catType.toLowerCase()) { case "override" : case "o" : categoryType = "override"; break; case "custom" : case "c" : categoryType = "custom"; break; } } return categoryType; } export const getOptionsSource = (options) => { let sourceType = null; if (options.params[3]) { let srcType = options.params[3]; switch (srcType.toLowerCase()) { case "server" : case "s" : sourceType = "Server"; break; case "local" : case "l" : sourceType = "Local"; break; } } return sourceType; } export const getComponents = async (showSeparator = false, category = CATEGORY_CUSTOM, isName = true) => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); const directory = join(currentDirectory, componentsDirectoryPath, categoryDirectory); let compList = new Array(); switch (category) { case CATEGORY_CUSTOM: case CATEGORY_CONSTELLATION: let fieldComps; let templateComps; let widgetComps if (isName) { // this is new (better) way, where we ALWAYS have the Pascal case, even when the file is in another case, like Kabab // keeping all in Pascal case makes display same for different SDKs and is the case that the customer typed in fieldComps = await getSubComponentsName(directory, FIELD_PATH); templateComps = await getSubComponentsName(directory, TEMPLATE_PATH); widgetComps = await getSubComponentsName(directory, WIDGET_PATH); } else { // this is the old way, were we get the actual name from the file (for Angular this is Kabab case) fieldComps = await getSubComponents(directory, FIELD_PATH); templateComps = await getSubComponents(directory, TEMPLATE_PATH); widgetComps = await getSubComponents(directory, WIDGET_PATH); } if (fieldComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- fields ----")); compList.push(...fieldComps); } if (templateComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- templates ----")); compList.push(...templateComps); } if (widgetComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- widgets ----")); compList.push(...widgetComps); } break; case CATEGORY_OVERRIDE: const formsComps = await getSubComponents(directory, FIELD_PATH); const templatesComps = await getSubComponents(directory, TEMPLATE_PATH); const widgetsComps = await getSubComponents(directory, WIDGET_PATH); const dseComps = await getSubComponents(directory, DSE_PATH); const infraComps = await getSubComponents(directory, INFRA_PATH); if (formsComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- fields ----")); compList.push(...formsComps); } if (templatesComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- templates ----")); compList.push(...templatesComps); } if (widgetsComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- widgets ----")); compList.push(...widgetsComps); } if (dseComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- design system extensions ----")); compList.push(...dseComps); } if (infraComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- infrastructure ----")); compList.push(...infraComps); } break; } return compList; }; export const getSubComponents = async (directory, type) => { const subDirectory = join(directory, type); return fs .readdirSync(subDirectory, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); } export const getSubComponentsName = async (directory, type) => { const subDirectory = join(directory, type); const directoryList = fs.readdirSync(subDirectory, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()); let componentList = new Array(); for ( let i =0; i < directoryList.length; i++) { const directoryObj = directoryList[i]; const configJSON = await getLocalComponentConfig(join(type, directoryObj.name)); componentList.push(configJSON['componentKey']); } return componentList; } export const getDirectoryFiles = async (directory) => { return fs .readdirSync(directory, { withFileTypes: true }) .filter(dirent => !dirent.isDirectory()) .map(dirent => dirent.name); } export const getListOfDirectories = async (directory) => { return fs .readdirSync(directory, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); } export const getComponentsObj = async (showSeparator = false, category = CATEGORY_CUSTOM) => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); const directory = join(currentDirectory, componentsDirectoryPath, categoryDirectory); let compList = new Array(); switch (category) { case CATEGORY_CUSTOM: const fieldComps = await getSubComponentsName(directory, FIELD_PATH); const templateComps = await getSubComponentsName(directory, TEMPLATE_PATH); const widgetComps = await getSubComponentsName(directory, WIDGET_PATH); if (fieldComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- fields ----")); compList.push(...fieldComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_FIELD_PATH + name; return container; } )); } if (templateComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- templates ----")); compList.push(...templateComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_TEMPLATE_PATH + name; return container; } )); } if (widgetComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- widgets ----")); compList.push(...widgetComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_WIDGET_PATH + name; return container; } )); } break; case CATEGORY_OVERRIDE: const formsComps = await getSubComponents(directory, FIELD_PATH); const templatesComps = await getSubComponents(directory, TEMPLATE_PATH); const widgetsComps = await getSubComponents(directory, WIDGET_PATH); const dseComps = await getSubComponents(directory, DSE_PATH); const infraComps = await getSubComponents(directory, INFRA_PATH); if (formsComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- fields ----")); compList.push(...formsComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_FIELD_PATH + name; return container; } )); } if (templatesComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- templates ----")); compList.push(...templatesComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_TEMPLATE_PATH + name; return container; } )); } if (widgetsComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- widgets ----")); compList.push(...widgetsComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_WIDGET_PATH + name; return container; } )); } if (dseComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- designSystemExtensions ----")); compList.push(...dseComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_DSE_PATH + name; return container; } )); } if (infraComps.length > 0) { if (showSeparator) compList.push(new inquirer.Separator("---- infrastructure ----")); compList.push(...infraComps.map( name => { const container = {}; container.name = name; container.value = COMPONENT_PREFIX_INFRA_PATH + name; return container; } )); } break; } return compList; } export const getSubComponentsAndDir = async (directory, type) => { const subDirectory = join(directory, type); const prefix = type.replace("/", "") + "/"; return fs .readdirSync(subDirectory, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => prefix + dirent.name); } export const getComponentsAndSubDirectory = async (category = CATEGORY_CUSTOM) => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); const directory = join(currentDirectory, componentsDirectoryPath, categoryDirectory); const fieldComps = await getSubComponentsAndDir(directory, FIELD_PATH); const templateComps = await getSubComponentsAndDir(directory, TEMPLATE_PATH); const widgetComps = await getSubComponentsAndDir(directory, WIDGET_PATH); let dirAndComp = new Array(); dirAndComp.push(...fieldComps); dirAndComp.push(...templateComps); dirAndComp.push(...widgetComps); return dirAndComp; } export const removeComponentPrefix = (componentName) => { if (componentName.indexOf(COMPONENT_PREFIX_FIELD_PATH) == 0) { componentName = componentName.substring(COMPONENT_PREFIX_FIELD_PATH.length); } else if (componentName.indexOf(COMPONENT_PREFIX_TEMPLATE_PATH) == 0) { componentName = componentName.substring(COMPONENT_PREFIX_TEMPLATE_PATH.length); } else if (componentName.indexOf(COMPONENT_PREFIX_WIDGET_PATH) == 0) { componentName = componentName.substring(COMPONENT_PREFIX_WIDGET_PATH.length); } else if (componentName.indexOf(COMPONENT_PREFIX_DSE_PATH) == 0) { componentName = componentName.substring(COMPONENT_PREFIX_DSE_PATH.length); } else if (componentName.indexOf(COMPONENT_PREFIX_INFRA_PATH) == 0) { componentName = componentName.substring(COMPONENT_PREFIX_INFRA_PATH.length); } return componentName; } export const getPegaConfig = async () => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); return data; }; export const getPegaServerConfig = async () => { const config = await getPegaConfig(); const server = config[SERVER_CONFIG][SERVER_REST_URL]; return { ...config[DXCB_CONFIG][DXCB_PUBLISH_ACCESS], server }; }; export const getComponentDefaults = async () => { const config = await getPegaConfig(); return config[DXCB_CONFIG][DXCB_COMPONENT_DEFAULTS]; }; export const getComponentDirectoryPath = async (componentKey, category = CATEGORY_CUSTOM) => { const currentDirectory = process.cwd(); const pegaConfigJsonPath = join(currentDirectory, SDK_CONFIG_JSON_FILENAME); const categoryDirectory = (category === CATEGORY_CONSTELLATION) ? join("/", COMPONENT_DIRECTORY_PREFIX_CUSTOM + category.toLowerCase()) : join("/", category.toLowerCase() + COMPONENT_DIRECTORY_SUFFIX_SDK); await checkPathAccess(pegaConfigJsonPath); let data = fs.readFileSync(pegaConfigJsonPath, { encoding: 'utf8' }); data = JSON.parse(data); return join(currentDirectory, componentsDirectoryPath, categoryDirectory, componentKey); }; export const compileMustacheTemplate = (file, data) => { const content = fs.readFileSync(file, 'utf8'); return mustache.render(content, data); }; export const isPascalCase = string => { return string === pascalCase(string); }; export const convertIntoPascalCase = string => { return pascalCase(string); }; export const convertIntoKebabCase = string => { let kCase = kebabcase(string); if (kCase.length > 0 && string.length > 0) { if (kCase[0] == '-' && string[0] != '-') { kCase = kCase.substring(1); } } return kCase; } export const reverseKebabCase = string => { if (string[0] != '-') { string = "-".concat(string); } return kebabcase.reverse(string); } export const constructCompleteUrl = (baseServer, endPoint) => { return baseServer.endsWith('/') ? `${baseServer}${endPoint}` : `${baseServer}/${endPoint}`; }; export const sanitize = str => { /* Allow only numbers and case insensitive alphabets */ str = str.replace(/[^a-zA-Z0-9 ]/g, ''); /* spaces will be replaced by - */ str = str.replace(/\s+/g, '-'); return str; }; export const validateSemver = str => { /* basic semver version validation - 0.0.1-dev */ const regex = /^[0-9]\d*\.\d+\.\d+(?:-[a-zA-Z0-9]+)?$/g; return regex.test(str); }; export const validateRulesetVersion = str => { /* Ruleset version range - 01-99 */ if (str.indexOf('00') !== -1) { return false; } /* basic ruleset version validation - 01-01-01 */ const regex = /^\d\d-\d\d-\d\d$/g; return regex.test(str); }; export const showVersion = () => { // get package version, so can display at start const require = createRequire(import.meta.url); const pData = require("@pega/dx-component-builder-sdk/package.json"); console.log(chalk.green("DX Component Builder - SDK v" + pData.version)); // do a check of node version, it should be 18 or greater const arNodeVersion = process.versions.node.split("."); const nodeMajorVersion = parseInt(arNodeVersion[0]); const nodeMinorVersion = parseInt(arNodeVersion[1]); if (nodeMajorVersion < 18) { console.log(chalk.redBright("DX Component Builder - SDK requires node v18 or greater, current version: " + process.version)); process.exit(1); } }; export const getJestFunctionalTestList = async(whichTests) => { const frameWork = await getFramework(); const SDK_PATH = frameWork === "SDK-Angular" ? "sdk-a" : "sdk-r"; const functionalDir = join(path.resolve(), JEST_TESTS_FUNCTIONAL_PATH, SDK_PATH); const libFilter = "localLib"; switch (whichTests) { case "ALL" : return fs .readdirSync(functionalDir, { withFileTypes: true }) .filter(dirent => !dirent.isDirectory() ) .map(dirent => dirent.name); case "Original": return fs .readdirSync(functionalDir, { withFileTypes: true }) .filter(dirent => !dirent.isDirectory() && !dirent.name.startsWith(libFilter)) .map(dirent => dirent.name); case "Library" : return fs .readdirSync(functionalDir, { withFileTypes: true }) .filter(dirent => !dirent.isDirectory() && dirent.name.startsWith(libFilter)) .map(dirent => dirent.name); } } export const getOOTBComponents = async () => { return new Promise((resolve, reject) => { fs.readFile(OOTB_COMPONENTS, 'utf-8', async (err, data) => { if (err) { try { const serverConfig = await getPegaServerConfig(); const { server } = serverConfig; const url = constructCompleteUrl(server, OOTB_COMPONENT_SERVICE_REST_ENDPOINT); const OauthData = fs.readFileSync(TOKEN_PATH, 'utf8'); if (OauthData) { const { access_token: accessToken, token_type: tokenType, refresh_token: refreshToken } = JSON.parse(OauthData); fetch(url, { method: 'GET', agent: getHttpsAgent(serverConfig), headers: { Authorization: `${tokenType} ${accessToken}` } }) .then(response => response.text()) .then(data => { if (data.charAt() === '[') { data = data.slice(1, -1); data = data.replace(/\s/g, ''); fs.writeFile(OOTB_COMPONENTS, data, err => { if (err) { console.error(err); } resolve(data.split(',')); }); } else { data = JSON.parse(data); if (data && data.errors) { const errMessages = data.errors; errMessages.forEach(msgArr => { throw new Error(`Failed with error - ${JSON.stringify(msgArr.message)}`); }); } } }) .catch(e => Promise.reject(`${chalk.bold.red(e)}`)); } } catch (error) { throw new Error('Error occurred in validation', error); } } else { resolve(data.split(',')); } }); }); };