@validkeys/ollypop-ts
Version:
Automatic TypeScript barrel file generator CLI.
196 lines • 7.92 kB
JavaScript
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