import setWith from 'lodash-es/setWith.js' import * as React from 'react' import { Box, Text, useTheme } from '../ink.js' import type { ValidationError } from '../utils/settings/validation.js' import { type TreeNode, treeify } from '../utils/treeify.js' /** * Builds a nested tree structure from dot-notation paths * Uses lodash setWith to avoid automatic array creation */ function buildNestedTree(errors: ValidationError[]): TreeNode { const tree: TreeNode = {} errors.forEach(error => { if (!error.path) { // Root level error - use empty string as key tree[''] = error.message return } // Try to enhance the path with meaningful values const pathParts = error.path.split('.') let modifiedPath = error.path // If we have an invalid value, try to make the path more readable if ( error.invalidValue !== null && error.invalidValue !== undefined && pathParts.length > 0 ) { const newPathParts: string[] = [] for (let i = 0; i < pathParts.length; i++) { const part = pathParts[i] if (!part) continue const numericPart = parseInt(part, 10) // If this is a numeric index and it's the last part where we have the invalid value if (!isNaN(numericPart) && i === pathParts.length - 1) { // Format the value for display let displayValue: string if (typeof error.invalidValue === 'string') { displayValue = `"${error.invalidValue}"` } else if (error.invalidValue === null) { displayValue = 'null' } else if (error.invalidValue === undefined) { displayValue = 'undefined' } else { displayValue = String(error.invalidValue) } newPathParts.push(displayValue) } else { // Keep other parts as-is newPathParts.push(part) } } modifiedPath = newPathParts.join('.') } setWith(tree, modifiedPath, error.message, Object) }) return tree } /** * Groups and displays validation errors using treeify with deduplication */ export function ValidationErrorsList({ errors, }: { errors: ValidationError[] }): React.ReactNode { const [themeName] = useTheme() if (errors.length === 0) { return null } // Group errors by file const errorsByFile = errors.reduce>( (acc, error) => { const file = error.file || '(file not specified)' if (!acc[file]) { acc[file] = [] } acc[file]!.push(error) return acc }, {}, ) // Sort files alphabetically const sortedFiles = Object.keys(errorsByFile).sort() return ( {sortedFiles.map(file => { const fileErrors = errorsByFile[file] || [] // Sort errors by path fileErrors.sort((a, b) => { if (!a.path && b.path) return -1 if (a.path && !b.path) return 1 return (a.path || '').localeCompare(b.path || '') }) // Build nested tree structure from error paths const errorTree = buildNestedTree(fileErrors) // Collect unique suggestion+docLink pairs const suggestionPairs = new Map< string, { suggestion?: string; docLink?: string } >() fileErrors.forEach(error => { if (error.suggestion || error.docLink) { // Create a key from suggestion+docLink combination const key = `${error.suggestion || ''}|${error.docLink || ''}` if (!suggestionPairs.has(key)) { suggestionPairs.set(key, { suggestion: error.suggestion, docLink: error.docLink, }) } } }) // Render the tree const treeOutput = treeify(errorTree, { showValues: true, themeName, treeCharColors: { treeChar: 'inactive', key: 'text', value: 'inactive', }, }) return ( {file} {treeOutput} {/* Display unique suggestion+docLink pairs */} {suggestionPairs.size > 0 && ( {Array.from(suggestionPairs.values()).map((pair, index) => ( {pair.suggestion && ( {pair.suggestion} )} {pair.docLink && ( Learn more: {pair.docLink} )} ))} )} ) })} ) }