@department-of-veterans-affairs/generator-vets-website
Version:
Generate a React app for vets-website
290 lines (244 loc) • 9.34 kB
JavaScript
/**
* CLI argument validation utilities
* Provides validation functions for command-line arguments to ensure proper format
* and catch errors early in the generator process.
*/
import { getNonInteractiveRequiredFields } from './prompts.js';
/**
* 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)
const invalidChars = /[<>:"\\|?*\s]/;
if (invalidChars.test(rootUrl)) {
return 'Root URL contains invalid characters. Use only letters, numbers, hyphens, and forward slashes. Example: "/burial-allowance" not "/burial allowance"';
}
return true;
}
/**
* Validates that an app name follows naming conventions
* - Can contain spaces (used for display purposes like page titles)
* - Should contain valid characters for display text
* @param {string} appName - The application name to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateAppName(appName) {
if (!appName) return true; // Optional field
// App names can contain spaces since they're used as page titles
// Just check for obviously invalid characters
const invalidChars = /[<>:"\\|?*]/;
if (invalidChars.test(appName)) {
return 'App name contains invalid characters. Avoid: < > : " \\ | ? *. Example: "Burial Allowance Application"';
}
return true;
}
/**
* Validates that an entry name follows naming conventions
* - No spaces allowed
* - Should be kebab-case
* @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; // Optional field
if (entryName.includes(' ')) {
return 'Entry name should not contain spaces. Use kebab-case. Example: "burial-allowance" not "burial allowance"';
}
// Check for invalid characters (more restrictive for entry names)
const invalidChars = /[^a-zA-Z0-9-]/;
if (invalidChars.test(entryName)) {
return 'Entry name should only contain letters, numbers, and hyphens. Example: "burial-allowance"';
}
return true;
}
/**
* Validates form number format
* - Should follow VA form number patterns (e.g., "22-0993", "21P-530")
* @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; // Optional field
// VA form numbers follow patterns like:
// - 21-526EZ (1-2 digits, optional letter, dash, any combination of digits and letters)
// - 22-1990 (2 digits, dash, 4 digits)
// - 10-10EZ (2 digits, dash, 2 digits + 2 letters)
// - 1-2345A (1 digit, dash, 4 digits + 1 letter)
const formPattern = /^\d{1,2}[A-Z]?-[\dA-Z]+$/i;
if (!formPattern.test(formNumber)) {
return 'Form number should follow VA format (e.g., "22-0993", "21-526EZ", "10-10EZ")';
}
return true;
}
/**
* Validates OMB number format
* - Should follow OMB control number format (XXXX-XXXX)
* @param {string} ombNumber - The OMB number to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateOmbNumber(ombNumber) {
if (!ombNumber) return true; // Optional field
const ombPattern = /^\d{4}-\d{4}$/;
if (!ombPattern.test(ombNumber)) {
return 'OMB number should follow format XXXX-XXXX (e.g., "2900-0001")';
}
return true;
}
/**
* Validates expiration date format
* - Should follow M/D/YYYY or MM/DD/YYYY format
* @param {string} expirationDate - The expiration date to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateExpirationDate(expirationDate) {
if (!expirationDate) return true; // Optional field
const datePattern = /^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/\d{4}$/;
if (!datePattern.test(expirationDate)) {
return 'Expiration date should follow M/D/YYYY format. Example: "12/31/2030"';
}
// Validate that it's a future date
const inputDate = new Date(expirationDate);
const today = new Date();
today.setHours(0, 0, 0, 0); // Reset time for date comparison
if (inputDate <= today) {
const nextYear = today.getFullYear() + 1;
return `Expiration date should be in the future. Example: "12/31/${nextYear}"`;
}
return true;
}
/**
* Validates respondent burden
* - Should be a positive number
* @param {string} respondentBurden - The respondent burden in minutes
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateRespondentBurden(respondentBurden) {
if (!respondentBurden) return true; // Optional field
const burden = parseInt(respondentBurden, 10);
if (isNaN(burden) || burden <= 0) {
return 'Respondent burden should be a positive number (minutes). Example: "15" or "30"';
}
return true;
}
/**
* Validates template type
* - Should be one of the allowed values
* @param {string} templateType - The template type to validate
* @returns {boolean|string} - true if valid, error message if invalid
*/
export function validateTemplateType(templateType) {
if (!templateType) return true; // Optional field
const allowedTypes = ['WITH_1_PAGE', 'WITH_4_PAGES', 'FORM_ENGINE'];
if (!allowedTypes.includes(templateType)) {
return `Template type should be one of: ${allowedTypes.join(
', ',
)}. Example: "WITH_1_PAGE"`;
}
return true;
}
/**
* Validates all CLI arguments at once
* @param {Object} options - The CLI options object
* @returns {Array} - Array of error messages (empty if all valid)
*/
export function validateAllCliArguments(options) {
const errors = [];
// First, check for required CLI arguments in non-interactive mode
const requiredArgErrors = validateRequiredCliArguments(options);
errors.push(...requiredArgErrors);
// Then validate format/values of provided arguments
const validators = [
{ field: 'rootUrl', validator: validateRootUrl },
{ field: 'appName', validator: validateAppName },
{ field: 'entryName', validator: validateEntryName },
{ field: 'formNumber', validator: validateFormNumber },
{ field: 'ombNumber', validator: validateOmbNumber },
{ field: 'expirationDate', validator: validateExpirationDate },
{ field: 'respondentBurden', validator: validateRespondentBurden },
{ field: 'templateType', validator: validateTemplateType },
];
validators.forEach(({ field, validator }) => {
const result = validator(options[field]);
if (result !== true) {
errors.push(`${field}: ${result}`);
}
});
return errors;
}
/**
* Detects if non-interactive mode is being used (any CLI arguments provided)
* @param {Object} options - The CLI options object
* @returns {boolean} - true if any CLI arguments are provided
*/
export function isNonInteractiveMode(options) {
if (!options || typeof options !== 'object') {
return false;
}
// If dry-run-interactive is specified, force interactive mode
if (options.dryRunInteractive) {
return false;
}
// If dry-run-non-interactive is specified, force non-interactive mode
if (options.dryRunNonInteractive) {
return true;
}
const cliFields = [
'appName',
'folderName',
'entryName',
'rootUrl',
'isForm',
'contentLoc', // Legacy backward compatibility
'contentRepoLocation', // Primary field name
'slackGroup',
'formNumber',
'trackingPrefix',
'respondentBurden',
'ombNumber',
'expirationDate',
'benefitDescription',
'usesVetsJsonSchema',
'usesMinimalHeader',
'templateType',
];
return cliFields.some((field) => options[field] !== undefined);
}
/**
* Validates that all required CLI arguments are provided in non-interactive mode
* @param {Object} options - The CLI options object
* @returns {Array} - Array of error messages for missing required fields
*/
export function validateRequiredCliArguments(options) {
if (!isNonInteractiveMode(options)) {
return []; // Interactive mode - no validation needed
}
const errors = [];
const requiredFields = getNonInteractiveRequiredFields(options);
requiredFields.forEach((field) => {
if (
options[field] === undefined ||
options[field] === null ||
options[field] === ''
) {
// Show camelCase field names for CLI arguments (preferred format)
errors.push(`--${field}: Required when using non-interactive mode`);
}
});
return errors;
}
// Re-export from prompts for compatibility
export { getNonInteractiveRequiredFields };