@pega/custom-dx-components
Version:
Utility for building custom UI components
469 lines (435 loc) • 16.9 kB
JavaScript
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)}`);
}
};