UNPKG

@validkeys/ollypop-ts

Version:

Automatic TypeScript barrel file generator CLI.

196 lines 7.92 kB
import { promises as fs } from 'fs'; import path from 'path'; import { FileScanner } from './scanner.js'; import { TemplateEngine } from './templates.js'; export class BarrelGenerator { scanner; templateEngine; verbose = false; constructor() { this.scanner = new FileScanner(); this.templateEngine = new TemplateEngine(); } async generateBarrels(definitions, options) { this.verbose = options?.verbose ?? false; for (const definition of definitions) { await this.generateBarrel(definition); } } async generateBarrel(definition) { if (!definition.name?.trim()) { throw new Error('Barrel definition must have a non-empty name'); } if (this.verbose) { console.log(`🔧 Generating barrel: ${definition.name}`); } if (!definition.output?.trim()) { throw new Error(`Barrel '${definition.name}': output path cannot be empty`); } return this.generateVariableBasedBarrel(definition); } registerTemplate(name, template) { this.templateEngine.registerTemplate(name, template); } async generateVariableBasedBarrel(definition) { const startTime = Date.now(); const mergedOptions = { followSymlinks: false, preserveExtensions: definition.options?.preserveExtensions ?? false, extensions: definition.options?.extensions ?? ['.ts', '.tsx'], validateExports: definition.options?.validateExports ?? false, dryRun: definition.options?.dryRun || false, }; const templateEngine = new TemplateEngine(); const context = { files: [], options: { namedExports: false, preserveExtensions: mergedOptions.preserveExtensions, sortExports: true, addBanner: true, customBanner: undefined, verbose: this.verbose, }, outputPath: definition.output, templateConfig: definition.template, exclude: definition.exclude || [], banner: '', metadata: { generatedAt: new Date().toISOString(), fileCount: 0, }, }; if (this.verbose) { console.log(`[ollypop] Generating barrel file: ${definition.output}`); console.log(` Using template: ${definition.template.name}`); console.log(' Context:'); Object.entries(context).forEach(([key, value]) => { console.log(`----------- ${key}:`, typeof value === 'object' ? JSON.stringify(value) : value); }); } const content = templateEngine.generate(definition.template.name, context); if (!mergedOptions.dryRun) { const mode = definition.template?.mode || 'replace'; if (mode === 'partial-replace') { await this.writeWithMarkers(definition.output, content, { startMarker: '// AUTO-GENERATED EXPORTS - START', endMarker: '// AUTO-GENERATED EXPORTS - END', preserveContent: true, }); } else { const banner = '// This file is auto-generated by @validkeys/ollypop-ts\n// Do not edit this file manually - your changes will be overwritten\n\n'; const fullContent = banner + content; await this.writeBarrelFile(definition.output, fullContent); } const duration = Date.now() - startTime; console.log(` ✅ Generated in ${duration}ms`); } else { console.log(` 🔍 Dry run - content would be:`); console.log(content .split('\n') .map((line) => ` ${line}`) .join('\n')); } } async writeBarrelFile(outputPath, content) { const outputDir = path.dirname(outputPath); const shouldWrite = await this.shouldWriteFile(outputPath, content); if (!shouldWrite) { if (this.verbose) { console.log(` 📋 Skipped (no changes): ${outputPath}`); } return; } await fs.mkdir(outputDir, { recursive: true }); await fs.writeFile(outputPath, content, 'utf8'); if (this.verbose) { console.log(` ✅ Written: ${outputPath}`); } } async shouldWriteFile(filePath, newContent, existingContent) { try { if (existingContent === undefined) { existingContent = await fs.readFile(filePath, 'utf-8'); } const normalizeContent = (content) => { return content .replace(/\r\n/g, '\n') .replace(/\n+$/g, '\n') .trim(); }; const normalizedExisting = normalizeContent(existingContent); const normalizedNew = normalizeContent(newContent); return normalizedExisting !== normalizedNew; } catch (error) { return true; } } async writeWithMarkers(outputPath, content, markers) { try { let existingContent = ''; let fileExists = true; try { existingContent = await fs.readFile(outputPath, 'utf8'); } catch { fileExists = false; } const startMarker = markers.startMarker || '// AUTO-GENERATED EXPORTS - START'; const endMarker = markers.endMarker || '// AUTO-GENERATED EXPORTS - END'; let updatedContent; if (fileExists && existingContent.includes(startMarker)) { const beforeMarker = existingContent.substring(0, existingContent.indexOf(startMarker)); const afterMarkerIndex = existingContent.indexOf(endMarker); const afterMarker = afterMarkerIndex !== -1 ? existingContent.substring(afterMarkerIndex + endMarker.length) : ''; updatedContent = [ beforeMarker.trimEnd(), startMarker, '', content.trim(), '', endMarker, afterMarker.trimStart(), ] .filter((line) => line !== undefined) .join('\n'); } else { const baseContent = fileExists ? existingContent : ''; updatedContent = [ baseContent.trimEnd(), baseContent ? '' : '', startMarker, '', content.trim(), '', endMarker ] .filter((line) => line !== null && line !== undefined) .join('\n'); } const shouldWrite = await this.shouldWriteFile(outputPath, updatedContent, fileExists ? existingContent : undefined); if (!shouldWrite) { if (this.verbose) { console.log(` 📋 Skipped (no changes): ${outputPath}`); } return; } const outputDir = path.dirname(outputPath); await fs.mkdir(outputDir, { recursive: true }); await fs.writeFile(outputPath, updatedContent, 'utf8'); if (this.verbose) { const action = fileExists ? 'Updated' : 'Created'; console.log(` ✅ ${action}: ${outputPath}`); } } catch (error) { throw new Error(`Failed to write with markers: ${error}`); } } } //# sourceMappingURL=generator.js.map