9cc916e8bc
Add optional chaining and null checks in build scripts to handle potentially undefined array elements. Add style prop to Card, Badge, and Placeholder components, and onClick prop to Text component to support inline styling in screen components. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
import { Glob } from 'bun';
|
|
import type { ParsedFeature, ParsedScenario, ParsedStep } from '../src/types/gherkin';
|
|
|
|
async function parseFeatures(): Promise<ParsedFeature[]> {
|
|
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é que ', "Étant donné qu'", 'Étant donné', 'Etant donné que ', "Etant donné qu'", '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<string, number>);
|
|
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<string, number>);
|
|
console.log(byPriority);
|
|
}).catch(console.error);
|