@department-of-veterans-affairs/generator-vets-website
Version:
Generate a React app for vets-website
394 lines (339 loc) • 12.6 kB
JavaScript
import chalk from 'chalk';
import { store } from './store.js';
import { getTrackedFiles } from './fs-tracker.js';
import { validateAllCliArguments } from './cli-validation.js';
import { getNonInteractiveRequiredFields } from './prompts.js';
/**
* Check if the generator is running in any dry-run mode
* @param {object} options - Generator options
* @returns {boolean}
*/
export function isDryRunMode(options) {
return Boolean(options.dryRunInteractive || options.dryRunNonInteractive);
}
/**
* Initialize dry-run mode in the store
* @param {object} options - Generator options
*/
export function initializeDryRunMode(options) {
const dryRunState = isDryRunMode(options);
store.setDryRun(dryRunState);
}
/**
* Convert generic --dry-run flag to specific dry-run mode
* @param {object} options - Generator options (will be modified)
*/
export function normalizeDryRunOptions(options) {
if (options.dryRun && !options.dryRunInteractive && !options.dryRunNonInteractive) {
options.dryRunNonInteractive = true;
options.dryRun = false; // Clear the generic flag
}
}
/**
* Check if validation should be skipped in dry-run mode
* @param {object} options - Generator options
* @returns {boolean}
*/
export function shouldSkipValidation(options) {
return isDryRunMode(options);
}
/**
* Compute props for dry-run mode
* @param {object} generator - The generator instance
* @param {object} allFields - Field definitions
*/
export function computePropsForDryRun(generator, allFields) {
const workingProps = { ...store.getAllProps() };
Object.keys(allFields).forEach((fieldName) => {
if (generator.options[fieldName] !== undefined) {
workingProps[fieldName] = generator.options[fieldName];
}
});
// Compute missing required fields using their default functions
Object.keys(allFields).forEach((fieldName) => {
const field = allFields[fieldName];
// Skip if we already have a value
if (workingProps[fieldName] !== undefined) {
return;
}
// Try to compute using default function
if (typeof field.default === 'function') {
try {
const computedValue = field.default(workingProps, generator);
workingProps[fieldName] = computedValue;
// Set it in the store so file operations can access it
store.setProp(fieldName, computedValue);
} catch {
// If computation fails, leave it undefined
}
} else if (field.default !== undefined) {
workingProps[fieldName] = field.default;
store.setProp(fieldName, field.default);
}
});
}
/**
* Handle dry-run prompting validation and early exit
* @param {object} generator - The generator instance
* @param {object} allFields - Field definitions
* @returns {Promise<void>} - Resolves if dry-run should continue, exits process if validation fails
*/
export function handleDryRunPrompting(generator, allFields) {
if (!isDryRunMode(generator.options)) {
return null; // Not in dry-run mode, continue with normal prompting
}
console.log(chalk.cyan('\nDRY RUN - Configuration'));
// For interactive dry-run, validate that required fields are provided as CLI args
if (generator.options.dryRunInteractive) {
// AppName is always required
const requiredFields = ['appName'];
const missingFields = [];
// Check if appName is provided
requiredFields.forEach((field) => {
const kebabField = field.replace(/([A-Z])/g, '-$1').toLowerCase();
if (!generator.options[field] && !store.getValue(field)) {
missingFields.push(
`--${kebabField}: Required (other fields can be computed from this)`,
);
}
});
// Determine if this is a form (defaults to true unless explicitly set to false)
const isForm =
generator.options.isForm === undefined
? true // Default to true
: generator.options.isForm === true ||
generator.options.isForm === 'true' ||
generator.options.isForm === 'y';
// If it's a form, formNumber is required
if (isForm && !generator.options.formNumber && !store.getValue('formNumber')) {
missingFields.push('--form-number: Required when creating a form application');
}
// Also validate format/values of any provided CLI arguments
const validationErrors = validateAllCliArguments(generator.options);
const allErrors = [...missingFields, ...validationErrors];
if (allErrors.length > 0) {
console.log(chalk.red('\n❌ Validation errors:'));
allErrors.forEach((error) => {
console.log(chalk.red(` • ${error}`));
});
console.log(chalk.red('\nDRY RUN SUMMARY:'));
console.log(chalk.red('❌ Generator would fail due to validation errors'));
process.exit(1);
}
}
// For non-interactive dry-run, use existing validation with all relevant fields
if (generator.options.dryRunNonInteractive) {
// Use the centralized CLI validation which already handles required fields
const validationErrors = validateAllCliArguments(generator.options);
if (validationErrors.length > 0) {
console.log(chalk.red('\n❌ Validation errors:'));
validationErrors.forEach((error) => {
console.log(chalk.red(` • ${error}`));
});
console.log(chalk.red('\nDRY RUN SUMMARY:'));
console.log(chalk.red('❌ Generator would fail due to validation errors'));
process.exit(1);
}
}
// Early computation of props for dry-run modes so file operations have correct values
computePropsForDryRun(generator, allFields);
// Create strategy based on computed isForm value for dry-run mode
if (!generator.strategy) {
const isFormValue = store.getValue('isForm');
if (isFormValue !== null && isFormValue !== undefined) {
generator.strategy = generator._createStrategy(isFormValue);
}
}
return Promise.resolve();
}
/**
* Check if a field is provided via CLI options
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @returns {boolean}
*/
function isProvidedViaOptions(optionName, options) {
return options[optionName] !== undefined;
}
/**
* Analyze a store value to determine its source
* @param {string} optionName - Name of the option
* @param {object} allFields - Field definitions
* @returns {string} - Source description
*/
function analyzeStoreValue(optionName, allFields) {
const field = allFields[optionName];
if (field && typeof field.default === 'function') {
return 'computed default';
}
if (field && field.default !== undefined) {
return 'default';
}
// If no default in field definition, it might be computed elsewhere
return 'computed';
}
/**
* Get source for dry-run modes (both interactive and non-interactive)
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @param {object} allFields - Field definitions
* @param {boolean} isInteractive - Whether this is interactive mode
* @returns {string} - Source description
*/
function getDryRunSource(optionName, options, allFields, isInteractive) {
if (isProvidedViaOptions(optionName, options)) {
return isInteractive ? 'prompt answer' : 'CLI arg';
}
if (store.getValue(optionName) !== undefined) {
return analyzeStoreValue(optionName, allFields);
}
return 'missing';
}
/**
* Get source for interactive dry-run mode
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @param {object} allFields - Field definitions
* @returns {string} - Source description
*/
function getInteractiveSource(optionName, options, allFields) {
return getDryRunSource(optionName, options, allFields, true);
}
/**
* Get source for non-interactive dry-run mode
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @param {object} allFields - Field definitions
* @returns {string} - Source description
*/
function getNonInteractiveSource(optionName, options, allFields) {
return getDryRunSource(optionName, options, allFields, false);
}
/**
* Get source for default mode (non-dry-run)
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @returns {string} - Source description
*/
function getDefaultSource(optionName, options) {
if (isProvidedViaOptions(optionName, options)) {
return 'CLI arg';
}
if (store.getValue(optionName) !== undefined) {
return 'prompt';
}
return 'missing';
}
/**
* Get the source of an option value (CLI arg, prompt, computed, etc.)
* @param {string} optionName - Name of the option
* @param {object} options - Generator options
* @param {object} allFields - Field definitions
* @returns {string} - Source description
*/
export function getOptionSource(optionName, options, allFields) {
// Special handling for auto-generated/computed fields
const computedFields = ['productId', 'subFolder', 'contentRepoLocation'];
if (computedFields.includes(optionName) && store.getValue(optionName) !== undefined) {
return 'computed';
}
// Route to appropriate handler based on mode
if (options.dryRunInteractive) {
return getInteractiveSource(optionName, options, allFields);
}
if (options.dryRunNonInteractive) {
return getNonInteractiveSource(optionName, options, allFields);
}
return getDefaultSource(optionName, options);
}
/**
* Get the color for displaying a source type
* @param {string} source - Source type
* @returns {Function} - Chalk color function
*/
export function getSourceColor(source) {
switch (source) {
case 'CLI arg':
return chalk.blue;
case 'prompt answer':
return chalk.bold.green;
case 'prompt':
return chalk.gray;
case 'default':
return chalk.gray;
case 'computed':
return chalk.gray;
case 'computed default':
return chalk.gray;
case 'missing':
return chalk.red;
default:
return chalk.gray;
}
}
/**
* Show tracked files and configuration for dry-run mode
* @param {object} generator - The generator instance
* @param {object} allFields - Field definitions
*/
export function showTrackedFiles(generator, allFields) {
console.log(chalk.cyan('\nDRY RUN - Configuration'));
console.log(chalk.yellow('Final configuration that would be used:'));
const metaFields = ['productId', 'dryRunInteractive', 'dryRunNonInteractive'];
const userVisibleFields = Object.keys(allFields).filter(
(fieldName) => !metaFields.includes(fieldName),
);
const missingRequiredFields = [];
const actualRequiredFields = getNonInteractiveRequiredFields(generator.options);
userVisibleFields.forEach((fieldName) => {
const value = store.getValue(fieldName);
const source = getOptionSource(fieldName, generator.options, allFields);
const isActuallyRequired = actualRequiredFields.includes(fieldName);
if (source === 'missing' && isActuallyRequired) {
console.log(
` ${chalk.red('❌')} ${chalk.cyan(fieldName)}: ${chalk.red('missing')}`,
);
missingRequiredFields.push(fieldName);
} else if (value !== undefined) {
const sourceColor = getSourceColor(source);
console.log(
` ${chalk.green('✓')} ${chalk.cyan(fieldName)}: ${chalk.white(
value,
)} ${sourceColor('(' + source + ')')}`,
);
} else if (isActuallyRequired) {
console.log(
` ${chalk.yellow('❓')} ${chalk.cyan(fieldName)}: ${chalk.yellow(
'<would prompt>',
)}`,
);
}
});
if (missingRequiredFields.length > 0) {
console.log(chalk.red('\nDRY RUN SUMMARY:'));
console.log(chalk.red('❌ Generator would fail due to missing required fields:'));
missingRequiredFields.forEach((field) => {
console.log(chalk.red(` • ${field}: Required field is missing`));
});
process.exit(1);
}
console.log(chalk.cyan('\nDRY RUN - File analysis'));
const trackedFiles = getTrackedFiles(generator);
console.log(chalk.yellow('Files to be created/modified:'));
if (trackedFiles.length === 0) {
console.log(chalk.gray(' No files tracked yet'));
} else {
// Sort for consistent output
trackedFiles.sort().forEach((file) => {
console.log(chalk.green(` ${file}`));
});
}
// Final summary
console.log(chalk.cyan('\nDRY RUN SUMMARY:'));
if (trackedFiles.length > 0) {
console.log(
chalk.blue(`Files that would be created/modified: ${trackedFiles.length}`),
);
}
console.log(chalk.green('✅ Generator would complete successfully'));
}