1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 11:43:51 +00:00
Files
browser/scripts/migration/i18n/templates/cli.ts
2025-07-28 15:57:28 +02:00

451 lines
15 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import * as fs from "fs";
import * as path from "path";
import * as chalk from "chalk";
import { Command } from "commander";
import { MigrationConfig } from "../shared/types";
import { TemplateMigrator } from "./template-migrator";
/**
* Find template files matching a pattern
*/
function findTemplateFiles(pattern: string, rootDir: string = process.cwd()): string[] {
const files: string[] = [];
// Handle specific directory patterns like "templates/sample-templates/*.html"
if (pattern.includes("/") && pattern.includes("*")) {
const parts = pattern.split("/");
const dirParts = parts.slice(0, -1);
const filePart = parts[parts.length - 1];
const targetDir = path.join(rootDir, ...dirParts);
if (fs.existsSync(targetDir)) {
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
if (filePart === "*.html" && entry.name.endsWith(".html")) {
files.push(path.join(targetDir, entry.name));
} else if (filePart.includes("*")) {
const regex = new RegExp(filePart.replace(/\*/g, ".*"));
if (regex.test(entry.name)) {
files.push(path.join(targetDir, entry.name));
}
}
}
}
}
return files;
}
// Default recursive search
function walkDir(dir: string) {
if (!fs.existsSync(dir)) {
return;
}
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip common directories that shouldn't contain templates
if (!["node_modules", "dist", "coverage", ".git", ".angular"].includes(entry.name)) {
walkDir(fullPath);
}
} else if (entry.isFile()) {
// Simple pattern matching - for now just check if it ends with .html
if (pattern === "**/*.html" && entry.name.endsWith(".html")) {
files.push(fullPath);
} else if (pattern.includes("*")) {
const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
if (regex.test(fullPath)) {
files.push(fullPath);
}
}
}
}
}
walkDir(rootDir);
return files;
}
const program = new Command();
program
.name("i18n-template-migrator")
.description("CLI tool for migrating Angular templates from i18n pipes to i18n attributes")
.version("1.0.0");
program
.command("analyze")
.description("Analyze current i18n pipe usage in templates")
.option("-p, --pattern <pattern>", "Glob pattern for template files", "**/*.html")
.option("-o, --output <path>", "Output file for analysis report")
.option("-v, --verbose", "Enable verbose logging")
.action(async (options) => {
try {
const config: MigrationConfig = {
sourceRoot: process.cwd(),
tsConfigPath: "./tsconfig.json",
dryRun: true,
verbose: options.verbose || false,
};
console.log(chalk.blue("🔍 Analyzing i18n pipe usage in templates..."));
const migrator = new TemplateMigrator(config);
const templateFiles = findTemplateFiles(options.pattern);
if (templateFiles.length === 0) {
console.log(chalk.yellow("⚠️ No template files found matching pattern"));
return;
}
console.log(chalk.gray(`Found ${templateFiles.length} template files`));
const report = migrator.generateTemplateAnalysisReport(templateFiles);
if (options.output) {
fs.writeFileSync(options.output, report);
console.log(chalk.green(`✅ Analysis report saved to: ${options.output}`));
} else {
console.log(report);
}
} catch (error) {
console.error(chalk.red("❌ Analysis failed:"), error);
process.exit(1);
}
});
program
.command("migrate")
.description("Migrate template files from i18n pipes to i18n attributes")
.option("-p, --pattern <pattern>", "Glob pattern for template files", "**/*.html")
.option("-f, --file <path>", "Migrate specific file only")
.option("-d, --dry-run", "Preview changes without applying them")
.option("-o, --output <path>", "Output directory for migration reports")
.option("-v, --verbose", "Enable verbose logging")
.option("--backup", "Create backup files before migration")
.action(async (options) => {
try {
const config: MigrationConfig = {
sourceRoot: process.cwd(),
tsConfigPath: "./tsconfig.json",
dryRun: options.dryRun || false,
verbose: options.verbose || false,
};
const migrator = new TemplateMigrator(config);
let templateFiles: string[];
if (options.file) {
templateFiles = [path.resolve(options.file)];
console.log(chalk.blue(`📄 Migrating file: ${options.file}`));
} else {
templateFiles = findTemplateFiles(options.pattern);
console.log(
chalk.blue(`🚀 Starting template migration for ${templateFiles.length} files...`),
);
}
if (templateFiles.length === 0) {
console.log(chalk.yellow("⚠️ No template files found matching pattern"));
return;
}
if (options.backup && !options.dryRun) {
console.log(chalk.yellow("📦 Creating backups..."));
await createBackups(templateFiles, options.output || "./migration-reports");
}
const results = await migrator.migrateTemplates(templateFiles);
const stats = migrator.generateMigrationStats(results);
console.log(stats);
// Save detailed report
if (options.output) {
const reportDir = options.output;
if (!fs.existsSync(reportDir)) {
fs.mkdirSync(reportDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const reportPath = path.join(reportDir, `template-migration-report-${timestamp}.md`);
let detailedReport = stats + "\n\n## Detailed Changes\n\n";
results.forEach((result) => {
detailedReport += `### ${result.filePath}\n`;
if (result.success) {
if (result.changes.length > 0) {
result.changes.forEach((change) => {
detailedReport += `- ${change.description}\n`;
if (change.original) {
detailedReport += ` - **Before:** \`${change.original}\`\n`;
}
if (change.replacement) {
detailedReport += ` - **After:** \`${change.replacement}\`\n`;
}
});
} else {
detailedReport += "No changes needed\n";
}
} else {
detailedReport += "**Errors:**\n";
result.errors.forEach((error) => {
detailedReport += `- ${error}\n`;
});
}
detailedReport += "\n";
});
fs.writeFileSync(reportPath, detailedReport);
console.log(chalk.green(`📊 Detailed report saved to: ${reportPath}`));
}
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
const withChanges = results.filter((r) => r.success && r.changes.length > 0).length;
if (failed === 0) {
console.log(
chalk.green(
`✅ Migration completed successfully! ${successful} files processed, ${withChanges} files modified.`,
),
);
} else {
console.log(
chalk.yellow(
`⚠️ Migration completed with warnings. ${successful} successful, ${failed} failed.`,
),
);
process.exit(1);
}
} catch (error) {
console.error(chalk.red("❌ Migration failed:"), error);
process.exit(1);
}
});
program
.command("validate")
.description("Validate migration results and check for remaining i18n pipes")
.option("-p, --pattern <pattern>", "Glob pattern for template files", "**/*.html")
.option("-v, --verbose", "Enable verbose logging")
.action(async (options) => {
try {
const config: MigrationConfig = {
sourceRoot: process.cwd(),
tsConfigPath: "./tsconfig.json",
dryRun: true,
verbose: options.verbose || false,
};
console.log(chalk.blue("🔍 Validating migration results..."));
const migrator = new TemplateMigrator(config);
const templateFiles = findTemplateFiles(options.pattern);
if (templateFiles.length === 0) {
console.log(chalk.yellow("⚠️ No template files found matching pattern"));
return;
}
let totalUsages = 0;
const filesWithUsages: string[] = [];
for (const filePath of templateFiles) {
const usages = migrator.analyzeTemplate(filePath);
if (usages.length > 0) {
totalUsages += usages.length;
filesWithUsages.push(filePath);
if (options.verbose) {
console.log(chalk.yellow(` ${filePath}: ${usages.length} remaining usages`));
usages.forEach((usage) => {
console.log(chalk.gray(` Line ${usage.line}: ${usage.key}`));
});
}
}
}
if (totalUsages === 0) {
console.log(chalk.green("✅ No remaining i18n pipe usage found!"));
} else {
console.log(
chalk.yellow(
`⚠️ Found ${totalUsages} remaining i18n pipe usages in ${filesWithUsages.length} files`,
),
);
if (!options.verbose) {
console.log(chalk.gray("Use --verbose to see detailed usage information"));
}
process.exit(1);
}
} catch (error) {
console.error(chalk.red("❌ Validation failed:"), error);
process.exit(1);
}
});
program
.command("rollback")
.description("Rollback migration using backup files")
.option("-b, --backup-dir <path>", "Path to backup directory", "./migration-reports/backups")
.option("-v, --verbose", "Enable verbose logging")
.action(async (options) => {
try {
console.log(chalk.blue("🔄 Rolling back template migration..."));
const backupDir = options.backup_dir;
if (!fs.existsSync(backupDir)) {
console.error(chalk.red(`❌ Backup directory not found: ${backupDir}`));
process.exit(1);
}
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
if (backupFiles.length === 0) {
console.error(chalk.red("❌ No backup files found"));
process.exit(1);
}
let restoredCount = 0;
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
const originalPath = backupFile.replace(".backup", "");
if (fs.existsSync(originalPath)) {
fs.copyFileSync(backupPath, originalPath);
restoredCount++;
if (options.verbose) {
console.log(chalk.gray(`Restored: ${originalPath}`));
}
}
}
console.log(chalk.green(`✅ Rollback completed! ${restoredCount} files restored.`));
} catch (error) {
console.error(chalk.red("❌ Rollback failed:"), error);
process.exit(1);
}
});
program
.command("compare")
.description("Generate before/after comparison reports")
.option("-f, --file <path>", "Template file to compare")
.option("-o, --output <path>", "Output file for comparison report")
.option("-v, --verbose", "Enable verbose logging")
.action(async (options) => {
try {
if (!options.file) {
console.error(chalk.red("❌ File path is required for comparison"));
process.exit(1);
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(filePath)) {
console.error(chalk.red(`❌ File not found: ${filePath}`));
process.exit(1);
}
const config: MigrationConfig = {
sourceRoot: process.cwd(),
tsConfigPath: "./tsconfig.json",
dryRun: true,
verbose: options.verbose || false,
};
console.log(chalk.blue(`🔍 Generating comparison for: ${options.file}`));
const migrator = new TemplateMigrator(config);
const originalContent = fs.readFileSync(filePath, "utf-8");
const result = await migrator.migrateTemplate(filePath);
if (!result.success) {
console.error(chalk.red("❌ Migration failed:"), result.errors);
process.exit(1);
}
// Apply changes to get transformed content
let transformedContent = originalContent;
for (const change of result.changes.reverse()) {
if (change.original && change.replacement) {
transformedContent = transformedContent.replace(change.original, change.replacement);
}
}
let report = `# Template Migration Comparison\n\n`;
report += `**File:** ${filePath}\n`;
report += `**Changes:** ${result.changes.length}\n\n`;
report += `## Before\n\`\`\`html\n${originalContent}\n\`\`\`\n\n`;
report += `## After\n\`\`\`html\n${transformedContent}\n\`\`\`\n\n`;
if (result.changes.length > 0) {
report += `## Changes\n`;
result.changes.forEach((change, index) => {
report += `### Change ${index + 1}\n`;
report += `**Description:** ${change.description}\n`;
if (change.original) {
report += `**Before:** \`${change.original}\`\n`;
}
if (change.replacement) {
report += `**After:** \`${change.replacement}\`\n`;
}
report += `\n`;
});
}
if (options.output) {
fs.writeFileSync(options.output, report);
console.log(chalk.green(`✅ Comparison report saved to: ${options.output}`));
} else {
console.log(report);
}
} catch (error) {
console.error(chalk.red("❌ Comparison failed:"), error);
process.exit(1);
}
});
async function createBackups(templateFiles: string[], outputDir: string): Promise<void> {
const backupDir = path.join(outputDir, "backups");
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
for (const filePath of templateFiles) {
if (fs.existsSync(filePath)) {
const backupPath = path.join(backupDir, path.basename(filePath) + ".backup");
fs.copyFileSync(filePath, backupPath);
}
}
console.log(chalk.green(`📦 Created backups for ${templateFiles.length} files`));
}
// Handle uncaught errors
process.on("uncaughtException", (error) => {
console.error(chalk.red("❌ Uncaught Exception:"), error);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error(chalk.red("❌ Unhandled Rejection at:"), promise, "reason:", reason);
process.exit(1);
});
program.parse();