UNPKG

@pega/custom-dx-components

Version:

Utility for building custom UI components

469 lines (435 loc) 16.9 kB
import path from 'path'; import fs from 'fs'; import chalk from 'chalk'; import inquirer from 'inquirer'; import Ajv from 'ajv'; import { COMPONENT_SCHEMA } from '../../constants.js'; import { getComponentDirectoryPath, getComponents, isComponentValidLength, getConfigDefaults, getLibraryBasedCL } from '../../util.js'; const ajv = new Ajv({ allErrors: true }); const combinationTypes = ['PAGE', 'CASE']; const configDef = getConfigDefaults(); const isLibraryBasedCL = getLibraryBasedCL(); const allowedSubtypes = [ ...new Set([ ...COMPONENT_SCHEMA.subtype.field.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.template.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.widget.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.presentation.map(({ value }) => value) ]) ]; // additional subtypes are manually added below const allowedOverrideSubtypes = [ ...new Set([ ...COMPONENT_SCHEMA.subtype.field.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.template.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.widget.map(({ value }) => value), ...COMPONENT_SCHEMA.subtype.presentation.map(({ value }) => value), 'ACTION', 'DATA_CAPTURE', 'SUMMARY', 'CASEVIEW', 'DATAVIEW', 'LIST' ]) ]; const schema = { type: 'object', properties: { baseComponentName: { type: 'string' }, componentKey: { type: 'string', pattern: '^[A-Za-z0-9]+_[A-Za-z0-9]+_[A-Za-z0-9]+$' }, name: { type: 'string', pattern: '^[A-Za-z0-9]+_[A-Za-z0-9]+_[A-Za-z0-9]+$' }, label: { type: 'string' }, description: { type: 'string' }, organization: { type: 'string' }, version: { type: 'string' }, library: { type: 'string' }, type: { type: 'string', enum: COMPONENT_SCHEMA.type.map(({ value }) => value) }, subtype: { anyOf: [{ type: 'string' }, { type: 'array' }], enum: allowedSubtypes }, icon: { type: 'string' }, properties: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, { type: 'object', properties: { format: { type: 'string' }, source: { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, cascadeElements: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { key: { type: 'string' }, name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['key', 'name', 'label', 'format'] } ] } } }, required: ['format'] }, { type: 'object', properties: { label: { type: 'string' }, format: { type: 'string' }, properties: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, { type: 'object', properties: { variant: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['variant', 'label', 'format'] }, { type: 'object', properties: { format: { type: 'string' }, elements: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { key: { type: 'string' }, name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' }, variant: { type: 'string' } }, required: ['key', 'format', 'label'] } ] } } }, required: ['format', 'elements'] } ] } } }, required: ['label', 'format'] } ] } }, defaultConfig: { type: 'object' } }, required: ['componentKey', 'name', 'description', 'type', 'subtype', 'properties'], additionalProperties: true }; const overrideSchema = { type: 'object', properties: { baseComponentName: { type: 'string' }, componentKey: { type: 'string', pattern: '^[A-Za-z0-9]+$' }, name: { type: 'string', pattern: '^[A-Za-z0-9]+$' }, label: { type: 'string' }, description: { type: 'string' }, organization: { type: 'string' }, version: { type: 'string' }, library: { type: 'string' }, type: { type: 'string', enum: COMPONENT_SCHEMA.type.map(({ value }) => value) }, subtype: { anyOf: [{ type: 'string' }, { type: 'array' }], enum: allowedOverrideSubtypes }, icon: { type: 'string' }, properties: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, { type: 'object', properties: { format: { type: 'string' }, source: { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, cascadeElements: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { key: { type: 'string' }, name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['key', 'name', 'label', 'format'] } ] } } }, required: ['format'] }, { type: 'object', properties: { label: { type: 'string' }, format: { type: 'string' }, properties: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['name', 'label', 'format'] }, { type: 'object', properties: { variant: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' } }, required: ['variant', 'label', 'format'] }, { type: 'object', properties: { format: { type: 'string' }, elements: { type: 'array', uniqueItems: true, items: { anyOf: [ { type: 'object', properties: { key: { type: 'string' }, name: { type: 'string' }, label: { type: 'string' }, format: { type: 'string' }, variant: { type: 'string' } }, required: ['key', 'format', 'label'] } ] } } }, required: ['format', 'elements'] } ] } } }, required: ['label', 'format'] } ] } }, defaultConfig: { type: 'object' } }, required: ['name', 'type', 'subtype', 'properties', 'componentKey'], additionalProperties: true }; const validateSchema = ajv.compile(schema); const validateOverrideSchema = ajv.compile(overrideSchema); /** * Validates that a Presentation component uses only allowed subtypes, and that * presentation-only subtypes are not used on other component types. * Throws an Error if validation fails. */ export function validatePresentationSubtype(data) { const presentationSubtypes = COMPONENT_SCHEMA.subtype.presentation.map(({ value }) => value); if (data.type === 'Presentation' && !presentationSubtypes.includes(data.subtype)) { throw new Error( `Error: Invalid subtype '${data.subtype}' for Presentation component '${data.name}'.\n --> Allowed subtypes for Presentation are: ${presentationSubtypes.join(', ')}.` ); } if (data.type !== 'Presentation' && presentationSubtypes.includes(data.subtype)) { throw new Error( `Error: Subtype '${data.subtype}' is only allowed for Presentation components. Component '${data.name}' has type '${data.type}'.` ); } } export default async option => { // addDebugLog("validate","", ""); let componentToValidate = null; let throwTheError = false; if (option.params && option.params[3]) { componentToValidate = option.params[3]; throwTheError = true; } else if (option && option.task === 'validateSchema') { const localComponents = await getComponents(); const answer = await inquirer.prompt({ name: 'component', type: 'rawlist', message: 'Select component to validate', choices: localComponents }); componentToValidate = answer.component; } else if (option.name) { componentToValidate = option.value; throwTheError = true; } else { componentToValidate = option; throwTheError = true; } if (!isComponentValidLength(componentToValidate)) { throw new Error(`Invalid: componentName is too long.`); } if (isLibraryBasedCL) { if (componentToValidate.indexOf(`${configDef.currentOrgLib}_`) != 0) { const arPrefix = componentToValidate.split('_'); const sPrefix = `${arPrefix[0]}_${arPrefix[1]}`; throw new Error( `Component: ${componentToValidate} prefix invalid.\n --> '${sPrefix}' prefix does not match configuration '${configDef.currentOrgLib}'. \n --> Org_Lib prefix of component must match Organization and Library values \n --> in package.json and tasks.config.json respectively.` ); } } const componentDirectory = await getComponentDirectoryPath(componentToValidate); let configFileName = 'config.json'; const configFile = path.join(componentDirectory, configFileName); let configData; try { configData = fs.readFileSync(path.resolve(configFile), 'utf8'); } catch (err) { // for now, if now file just end console.log(`${chalk.bold.yellow(`Config file not available for: ${componentToValidate}`)}`); return; } // const data = JSON.parse(fs.readFileSync(path.resolve(configFile), 'utf8')); let data; try { data = JSON.parse(configData); } catch (er) { throw new Error(`Invalid schema json in config.json: ${er}`); } const valid = validateSchema(data); if (valid) { if (data.description === '') { throw new Error(`Invalid schema json in config.json: 'description' cannot be blank.`); } validatePresentationSubtype(data); if (componentToValidate && !/^[A-Za-z0-9]+_[A-Za-z0-9]+_[A-Za-z0-9]+$/.test(componentToValidate)) { if (throwTheError) { throw new Error( `${chalk.redBright(`Error: Invalid name of component '${componentToValidate}' .\n Component name (and component folder) MUST be of the form: 'Org_Lib_ComponentName'.`)}` ); } console.log( `${chalk.redBright(`Error: Invalid name of component '${componentToValidate}' .\n Component name (and component folder) MUST be of the form: 'Org_Lib_ComponentName'.`)}` ); process.exit(1); } if (isLibraryBasedCL) { if (data.library !== configDef.library) { throw new Error( `${chalk.redBright(`Error: Invalid library in config.json for component '${data.name}'.\n -->Library: '${data.library}' must match tasks.config library: '${configDef.library}'.`)}` ); } if (data.organization !== configDef.organization) { throw new Error( `${chalk.redBright(`Error: Invalid organization in config.json for component '${data.name}'.\n -->Organization: '${data.organization}' must match package.json organization: '${configDef.organization}'.`)}` ); } if (data.componentKey.indexOf(`${configDef.currentOrgLib}_`) !== 0) { const arPrefix = data.componentKey.split('_'); const sPrefix = `${arPrefix[0]}_${arPrefix[1]}`; throw new Error( `${chalk.redBright(`Error: Invalid componentKey in config.json for component '${data.name}'.\n --> Component prefix Org_Lib: '${sPrefix}' must match '${configDef.currentOrgLib}'. \n --> These represent organization and library values found in package.json and tasks.config.json respectively. `)}` ); } if (data.componentKey !== componentToValidate) { throw new Error( `${chalk.redBright(`Error: Invalid folder name or componentKey in config.json for component '${data.name}'.\n --> Component folder name: '${componentToValidate}' must match component key '${data.componentKey}'.\n --> NOTE: Values are case sensitve.`)}` ); } if (data.name !== componentToValidate) { throw new Error( `${chalk.redBright(`Error: Invalid folder name or name in config.json for component '${data.name}'.\n --> Component folder name: '${componentToValidate}' must match name '${data.name}'.\n --> NOTE: Values are case sensitve.`)}` ); } } else { if (data.componentKey !== componentToValidate) { throw new Error( `${chalk.redBright(`Error: Invalid folder name or componentKey in config.json for component '${data.name}'.\n --> Component folder name: '${componentToValidate}' must match component key '${data.componentKey}'.\n --> NOTE: Values are case sensitve.`)}` ); } if (data.name !== componentToValidate) { throw new Error( `${chalk.redBright(`Error: Invalid folder name or name in config.json for component '${data.name}'.\n --> Component folder name: '${componentToValidate}' must match name '${data.name}'.\n --> NOTE: Values are case sensitve.`)}` ); } } console.log(`${chalk.bold.green(componentToValidate)} schema is valid`); } else { throw new Error(`Invalid: ${ajv.errorsText(validateSchema.errors)}`); } };