@department-of-veterans-affairs/generator-vets-website
Version:
Generate a React app for vets-website
210 lines (174 loc) • 6.79 kB
JavaScript
/**
* Validation utilities for generator inputs
* Consolidates all validation logic from fields.js and cli-validation.js
*/
import fs from 'node:fs';
import { fieldNameToCliArg } from './filters.js';
/**
* Check if a location is accessible for writing
* @param {string} location - The file/directory path to check
* @returns {boolean} True if accessible, false otherwise
*/
export function hasAccessTo(location) {
try {
fs.accessSync(location, fs.constants.F_OK | fs.constants.W_OK);
return true;
} catch {
return false;
}
}
/**
* Validate folder name format
* @param {string} folder - The folder name to validate
* @returns {boolean|string} true if valid, error message if invalid
*/
export function isInvalidFolderName(folder) {
return !folder.includes(' ') || 'Folder names should not include spaces';
}
/**
* Validate entry name format
* @param {string} entryName - The entry name to validate
* @returns {boolean|string} true if valid, error message if invalid
*/
export function isInvalidEntryName(entryName) {
return !entryName.includes(' ') || 'Bundle names should not include spaces';
}
/**
* Validate Slack group format
* @param {string} slackGroup - The Slack group to validate
* @returns {boolean|string} true if valid, error message if invalid
*/
export function isInvalidSlackGroup(slackGroup) {
if (slackGroup === 'none') return true;
return (
/^@[a-z-]+$/.test(slackGroup) ||
'Slack group must start with @ and contain only lowercase letters and hyphens'
);
}
/**
* Validates that a root URL follows the correct format
* - Must start with '/'
* - Must not end with '/' (will be handled by filter)
* - Should contain valid URL characters
* @param {string} rootUrl - The root URL to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateRootUrl(rootUrl) {
if (!rootUrl) return true; // Allow empty for format validation - required check handled separately
// Must start with '/'
if (!rootUrl.startsWith('/')) {
return 'Root URL must start with a forward slash (/). Example: "/burial-allowance"';
}
// Check for trailing slash
if (rootUrl.endsWith('/')) {
return 'Root URL should not end with a slash (/). Example: "/burial-allowance" not "/burial-allowance/"';
}
// Check for invalid characters (basic validation)
if (!/^\/[a-z0-9\-/_]*$/.test(rootUrl)) {
return 'Root URL should only contain lowercase letters, numbers, hyphens, underscores, and forward slashes. Example: "/burial-allowance"';
}
return true;
}
/**
* Validates form number format
* @param {string} formNumber - The form number to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateFormNumber(formNumber) {
if (!formNumber) return true; // Allow empty for format validation - required check handled separately
// Basic format check: should contain letters, numbers, and hyphens
if (!/^[A-Z0-9-]+$/i.test(formNumber)) {
return 'Form number should only contain letters, numbers, and hyphens. Example: "21-526EZ"';
}
return true;
}
/**
* Validates app name format
* @param {string} appName - The app name to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateAppName(appName) {
if (!appName) return 'Application name is required';
// Check length
if (appName.length < 3) {
return 'Application name must be at least 3 characters long';
}
if (appName.length > 100) {
return 'Application name must be less than 100 characters long';
}
return true;
}
/**
* Validates entry name format
* @param {string} entryName - The entry name to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateEntryName(entryName) {
if (!entryName) return true; // Allow empty for format validation - required check handled separately
// Must be valid JavaScript identifier (roughly)
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(entryName)) {
return 'Entry name must start with a letter and contain only letters and numbers. Example: "burialAllowance"';
}
return true;
}
/**
* Validates folder name format (more comprehensive than isInvalidFolderName)
* @param {string} folderName - The folder name to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateFolderName(folderName) {
if (!folderName) return true; // Allow empty for format validation - required check handled separately
// Check for spaces
if (folderName.includes(' ')) {
return 'Folder names should not include spaces';
}
// Check for invalid characters
if (!/^[a-z0-9\-/_]+$/.test(folderName)) {
return 'Folder name should only contain lowercase letters, numbers, hyphens, underscores, and forward slashes';
}
return true;
}
/**
* Validate that all required fields have values in non-interactive mode
* @param {Object} generator - The Yeoman generator instance
* @param {Object} fieldDefinitions - Field definitions object
* @returns {Array} Array of error messages
*/
export function validateRequiredFields(generator, fieldDefinitions) {
const errors = [];
Object.keys(fieldDefinitions).forEach((fieldName) => {
const field = fieldDefinitions[fieldName];
if (field.required) {
// Ensure generator.props exists before accessing it
const generatorProps = generator.props || {};
const value = generatorProps[fieldName] || generator.options[fieldName];
if (value === undefined || value === null || value === '') {
const cliArgName = fieldNameToCliArg(fieldName);
errors.push(`--${cliArgName}: Required when using non-interactive mode`);
}
}
});
return errors;
}
/**
* Check for duplicate entries in the content-build registry
* @param {Array} registry - The existing registry entries
* @param {Object} newApp - The new app being created
* @returns {Array} Array of duplicate error messages
*/
export function checkForDuplicates(registry, newApp) {
const duplicates = [];
if (registry.find((entry) => entry.appName === newApp.appName)) {
duplicates.push(`App Name: "${newApp.appName}" already exists in registry`);
}
if (registry.find((entry) => entry.entryName === newApp.entryName)) {
duplicates.push(`Entry Name: "${newApp.entryName}" already exists in registry`);
}
if (registry.find((entry) => entry.rootUrl === newApp.rootUrl)) {
duplicates.push(`Root URL: "${newApp.rootUrl}" already exists in registry`);
}
if (registry.find((entry) => entry.productId === newApp.productId)) {
duplicates.push(`Product ID: "${newApp.productId}" already exists in registry`);
}
return duplicates;
}