UNPKG

create-roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

260 lines (225 loc) 8.21 kB
/** * YAML parsing utilities for roadmap content * Handles loading, parsing, and validating roadmap YAML files */ import { RoadmapData, RoadmapItem, RoadmapConfig, RoadmapStatus } from '../types/roadmap'; /** * Parse YAML content into RoadmapData structure * @param yamlContent Raw YAML content as string * @returns Parsed and validated roadmap data */ export function parseRoadmapYaml(yamlContent: string): RoadmapData { try { // Use the yaml library for proper parsing const YAML = require('yaml'); const data = YAML.parse(yamlContent); // Validate and transform the parsed data const validatedData = validateRoadmapData(data); return validatedData; } catch (error) { throw new Error(`Failed to parse roadmap YAML: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Simple YAML parser for basic roadmap structures * Note: In production, use a proper YAML library like 'yaml' or 'js-yaml' */ function parseSimpleYaml(content: string): any { const lines = content.split('\n'); const result: any = {}; let currentSection = ''; let currentItem: any = null; let itemIndex = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Handle top-level sections if (line.endsWith(':') && !line.startsWith(' ') && !line.startsWith('-')) { currentSection = line.slice(0, -1); if (currentSection === 'items') { result[currentSection] = []; } else if (currentSection === 'config') { result[currentSection] = {}; } continue; } // Handle array items (items starting with -) if (line.startsWith('-') && currentSection === 'items') { if (currentItem) { result.items.push(currentItem); } currentItem = { id: `item-${itemIndex++}`, tags: [], links: [] }; // Parse the first property after the dash const firstProp = line.slice(1).trim(); if (firstProp.includes(':')) { const [key, value] = firstProp.split(':', 2); currentItem[key.trim()] = parseValue(value.trim()); } continue; } // Handle properties within items or config if (line.includes(':')) { const [key, value] = line.split(':', 2); const cleanKey = key.trim().replace(/^- /, ''); const cleanValue = parseValue(value.trim()); if (currentSection === 'items' && currentItem) { // Handle special array properties if (cleanKey === 'tags' || cleanKey === 'dependencies') { if (typeof cleanValue === 'string') { currentItem[cleanKey] = cleanValue.split(',').map(s => s.trim()).filter(Boolean); } else { currentItem[cleanKey] = []; } } else { currentItem[cleanKey] = cleanValue; } } else if (currentSection === 'config') { result.config[cleanKey] = cleanValue; } } } // Don't forget the last item if (currentItem && currentSection === 'items') { result.items.push(currentItem); } return result; } /** * Parse individual YAML values with type inference */ function parseValue(value: string): any { if (!value) return ''; // Remove quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { return value.slice(1, -1); } // Parse numbers if (/^\d+$/.test(value)) { return parseInt(value, 10); } if (/^\d+\.\d+$/.test(value)) { return parseFloat(value); } // Parse booleans if (value === 'true') return true; if (value === 'false') return false; return value; } /** * Validate and normalize roadmap data structure */ function validateRoadmapData(data: any): RoadmapData { // Provide default config if missing const defaultConfig: RoadmapConfig = { title: data.config?.title || 'Roadmap', description: data.config?.description || '', columns: [ { id: 'backlog', title: 'Backlog', order: 0, color: '#64748b' }, { id: 'todo', title: 'To Do', order: 1, color: '#3b82f6' }, { id: 'in-progress', title: 'In Progress', order: 2, color: '#f59e0b' }, { id: 'review', title: 'Review', order: 3, color: '#8b5cf6' }, { id: 'done', title: 'Done', order: 4, color: '#10b981' }, ], settings: { showPriority: data.config?.showPriority ?? true, showEffort: data.config?.showEffort ?? true, showDueDates: data.config?.showDueDates ?? true, showAssignees: data.config?.showAssignees ?? true, theme: data.config?.theme || 'auto', enableDragDrop: data.config?.enableDragDrop ?? true, } }; // Validate and normalize items const items: RoadmapItem[] = (data.items || []).map((item: any, index: number) => { return { id: item.id || `item-${index}`, title: item.title || `Untitled Item ${index + 1}`, description: item.description || '', status: isValidStatus(item.status) ? item.status : 'backlog', priority: ['low', 'medium', 'high', 'critical'].includes(item.priority) ? item.priority : 'medium', tags: Array.isArray(item.tags) ? item.tags : [], effort: typeof item.effort === 'number' ? item.effort : undefined, dueDate: item.dueDate || undefined, assignee: item.assignee || undefined, links: Array.isArray(item.links) ? item.links : [], dependencies: Array.isArray(item.dependencies) ? item.dependencies : [], metadata: item.metadata || {} }; }); return { config: { ...defaultConfig, ...data.config }, items }; } /** * Check if a status value is valid */ function isValidStatus(status: any): status is RoadmapStatus { return ['backlog', 'todo', 'in-progress', 'review', 'done', 'blocked'].includes(status); } /** * Convert RoadmapData back to YAML format * @param data Roadmap data to serialize * @returns YAML string representation */ export function serializeRoadmapToYaml(data: RoadmapData): string { const lines: string[] = []; // Add config section lines.push('config:'); lines.push(` title: "${data.config.title}"`); lines.push(` description: "${data.config.description}"`); lines.push(` showPriority: ${data.config.settings.showPriority}`); lines.push(` showEffort: ${data.config.settings.showEffort}`); lines.push(` showDueDates: ${data.config.settings.showDueDates}`); lines.push(` showAssignees: ${data.config.settings.showAssignees}`); lines.push(` theme: ${data.config.settings.theme}`); lines.push(` enableDragDrop: ${data.config.settings.enableDragDrop}`); lines.push(''); // Add items section lines.push('items:'); data.items.forEach(item => { lines.push(`- id: ${item.id}`); lines.push(` title: "${item.title}"`); lines.push(` description: "${item.description}"`); lines.push(` status: ${item.status}`); lines.push(` priority: ${item.priority}`); if (item.tags.length > 0) { lines.push(` tags: ${item.tags.join(', ')}`); } if (item.effort !== undefined) { lines.push(` effort: ${item.effort}`); } if (item.dueDate) { lines.push(` dueDate: ${item.dueDate}`); } if (item.assignee) { lines.push(` assignee: "${item.assignee}"`); } if (item.dependencies && item.dependencies.length > 0) { lines.push(` dependencies: ${item.dependencies.join(', ')}`); } lines.push(''); }); return lines.join('\n'); } /** * Load roadmap data from a file path * @param filePath Path to the YAML file * @returns Promise resolving to parsed roadmap data */ export async function loadRoadmapFromFile(filePath: string): Promise<RoadmapData> { try { // Use Node.js file system for compatibility with Next.js build const fs = await import('fs/promises'); const content = await fs.readFile(filePath, 'utf-8'); return parseRoadmapYaml(content); } catch (error) { throw new Error(`Failed to load roadmap from ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } }