import { Glob } from 'bun'; import type { ParsedFeature, ParsedScenario, ParsedStep } from '../src/types/gherkin'; async function parseFeatures(): Promise { const glob = new Glob('features/**/*.feature'); const features: ParsedFeature[] = []; for await (const filePath of glob.scan('.')) { const content = await Bun.file(filePath).text(); const parsed = parseGherkinContent(content, filePath); if (parsed) { features.push(parsed); } } features.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.category.localeCompare(b.category); }); const output = `// Auto-generated by scripts/parse-features.ts // Do not edit manually import type { ParsedFeature } from '../types/gherkin'; export const parsedFeatures: ParsedFeature[] = ${JSON.stringify(features, null, 2)}; export function getFeatureById(id: string): ParsedFeature | undefined { return parsedFeatures.find(f => f.id === id); } export function getFeaturesByCategory(category: string): ParsedFeature[] { return parsedFeatures.filter(f => f.category === category); } export function getFeaturesByPriority(priority: number): ParsedFeature[] { return parsedFeatures.filter(f => f.priority === priority); } export function getAllCategories(): string[] { return [...new Set(parsedFeatures.map(f => f.category))]; } export function getAllPriorities(): number[] { return [...new Set(parsedFeatures.map(f => f.priority))].sort((a, b) => a - b); } `; await Bun.write('src/data/features.ts', output); console.log(`Parsed ${features.length} feature files`); return features; } function parseGherkinContent(content: string, filePath: string): ParsedFeature | null { const lines = content.split('\n'); // Extract tags from the beginning const tagLines: string[] = []; let contentStartIndex = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#')) { contentStartIndex = i + 1; continue; } if (line.startsWith('@')) { tagLines.push(line); contentStartIndex = i + 1; } else if (line !== '') { break; } } const tagMatches = tagLines.join(' ').match(/@[\w-]+/g) || []; // Extract category and priority from tags const categoryTag = tagMatches.find(t => ['@WORKSHOP', '@EVENT', '@USER', '@MEETING', '@NOTIF'].includes(t) ); const category = categoryTag ? categoryTag.slice(1) : 'UNKNOWN'; const priorityTag = tagMatches.find(t => t.startsWith('@priority-')); const priority = priorityTag ? parseInt(priorityTag.replace('@priority-', '')) : 3; // Extract feature name const featureMatch = content.match(/Fonctionnalité:\s*(.+?)(?:\n|$)/); const name = featureMatch?.[1]?.trim() || 'Unknown Feature'; // Extract US ID from name const idMatch = name.match(/US-(\d+)/i); const id = idMatch ? `us-${idMatch[1]}` : filePath.replace(/.*\//, '').replace('.feature', ''); // Extract description (lines after Feature until Contexte or Scénario) const descLines: string[] = []; let inDescription = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Fonctionnalité:')) { inDescription = true; continue; } if (inDescription) { if (trimmed.startsWith('Contexte:') || trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) { break; } if (trimmed && !trimmed.startsWith('@') && !trimmed.startsWith('#')) { descLines.push(trimmed); } } } const description = descLines.join(' ').trim(); // Parse scenarios const scenarios: ParsedScenario[] = []; let currentScenario: ParsedScenario | null = null; let inBackground = false; const background: ParsedStep[] = []; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Contexte:')) { inBackground = true; continue; } if (trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) { inBackground = false; if (currentScenario) { scenarios.push(currentScenario); } const scenarioName = trimmed.replace(/^(Scénario:|Plan du Scénario:)\s*/, '').trim(); currentScenario = { name: scenarioName, tags: [], steps: [], }; continue; } // Parse steps const stepKeywords = ['Étant donné', 'Etant donné', 'Quand', 'Lorsque', 'Alors', 'Et', 'Mais']; for (const keyword of stepKeywords) { if (trimmed.startsWith(keyword)) { const step: ParsedStep = { keyword, text: trimmed.slice(keyword.length).trim(), }; if (inBackground) { background.push(step); } else if (currentScenario) { currentScenario.steps.push(step); } break; } } } // Don't forget the last scenario if (currentScenario) { scenarios.push(currentScenario); } return { id, name, description, tags: tagMatches, category, priority, background: background.length > 0 ? background : undefined, scenarios, filePath, rawContent: content, }; } // Run parser parseFeatures().then(features => { console.log('Features by category:'); const byCategory = features.reduce((acc, f) => { acc[f.category] = (acc[f.category] || 0) + 1; return acc; }, {} as Record); console.log(byCategory); console.log('\nFeatures by priority:'); const byPriority = features.reduce((acc, f) => { acc[`P${f.priority}`] = (acc[`P${f.priority}`] || 0) + 1; return acc; }, {} as Record); console.log(byPriority); }).catch(console.error);