import { FileCode01Icon, LayoutAlt01Icon, FolderIcon } from '@expo/styleguide-icons'; import { HTMLAttributes, ReactNode } from 'react'; type FileTreeProps = HTMLAttributes & { files?: (string | [string, string])[]; }; type FileObject = { name: string; note?: string; files: FileObject[]; }; export function FileTree({ files = [], ...rest }: FileTreeProps) { return (
{renderStructure(generateStructure(files))}
); } /** * Given an array of file paths, generate a tree structure. * @param files * @returns */ function generateStructure(files: (string | [string, string])[]): FileObject[] { const structure: FileObject[] = []; function modifyPath(path: string, note?: string) { const parts = path.split('/'); let currentLevel = structure; parts.forEach((part, index) => { const existingPath = currentLevel.find(item => item.name === part); if (existingPath) { currentLevel = existingPath.files; } else { const newPart: FileObject = { name: part, files: [], }; if (note && index === parts.length - 1) { newPart.note = note; } currentLevel.push(newPart); currentLevel = newPart.files; } }); } files.forEach(path => { if (Array.isArray(path)) { return modifyPath(path[0], path[1]); } else { return modifyPath(path); } }); return structure; } function renderStructure(structure: FileObject[], level = 0): ReactNode { return structure.map(({ name, note, files }, index) => { const FileIcon = getIconForFile(name); return files.length ? (
{' '.repeat(level)}
{renderStructure(files, level + 1)}
) : (
{' '.repeat(Math.max(level, 0))}
); }); } function TextWithNote({ name, note, className, }: { name: string; note?: string; className: string; }) { return ( {/* File/folder name */} {name} {note && ( <> {/* divider pushing */} {/* Optional note */} {note} )} ); } function getIconForFile(filename: string) { if (/_layout\.[jt]sx?/.test(filename)) { return LayoutAlt01Icon; } return FileCode01Icon; }