diff --git a/scripts/migration/i18n/BACKUP-SYSTEM.md b/scripts/migration/i18n/BACKUP-SYSTEM.md
new file mode 100644
index 00000000000..669b6b4c0f4
--- /dev/null
+++ b/scripts/migration/i18n/BACKUP-SYSTEM.md
@@ -0,0 +1,230 @@
+# Improved Backup System
+
+## Overview
+
+The migration tools now include an improved backup system that preserves full file paths, enabling safe rollback operations even for files in nested directory structures.
+
+## Problem Solved
+
+**Previous Issue**: The original backup system only stored filenames without paths:
+
+```
+backups/
+├── component.html.backup # Lost path info!
+├── template.html.backup # Could be from anywhere!
+└── form.html.backup # No way to restore correctly!
+```
+
+**New Solution**: Path-preserving backup system:
+
+```
+backups/
+├── path-mapping.json # Maps backup files to original paths
+├── src_app_components_login.html.backup # Unique filename with path info
+├── src_shared_templates_form.html.backup # No naming conflicts
+└── libs_ui_components_button.html.backup # Safe restoration
+```
+
+## How It Works
+
+### 1. Backup Creation
+
+When creating backups, the system:
+
+1. **Generates unique backup filenames** by replacing path separators with underscores:
+
+ ```typescript
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+ ```
+
+2. **Creates a path mapping file** that tracks original locations:
+
+ ```json
+ {
+ "src_app_login.html.backup": "/full/path/to/src/app/login.html",
+ "libs_ui_button.html.backup": "/full/path/to/libs/ui/button.html"
+ }
+ ```
+
+3. **Copies files to backup directory** with the unique names.
+
+### 2. Backup Restoration
+
+When restoring backups, the system:
+
+1. **Reads the path mapping file** to get original locations
+2. **Creates missing directories** if they don't exist
+3. **Restores files to their exact original paths**
+4. **Validates each restoration** before proceeding
+
+## Usage Examples
+
+### TypeScript Migration with Backup
+
+```bash
+# Create backups and migrate
+npm run migrate -- --backup --output ./migration-reports
+
+# If something goes wrong, rollback
+npm run cli -- rollback --backup-dir ./migration-reports/backups
+```
+
+### Template Migration with Backup
+
+```bash
+# Create backups and migrate templates
+npm run template-migrate -- --pattern "src/**/*.html" --backup --output ./reports
+
+# Rollback if needed
+npm run template-cli -- rollback --backup-dir ./reports/backups
+```
+
+## File Structure
+
+### Backup Directory Structure
+
+```
+migration-reports/
+└── backups/
+ ├── path-mapping.json # Critical: Maps backup files to originals
+ ├── src_app_login_login.component.html.backup
+ ├── src_shared_ui_button.component.html.backup
+ ├── libs_forms_input.component.html.backup
+ └── apps_web_dashboard_main.component.html.backup
+```
+
+### Path Mapping Format
+
+```json
+{
+ "src_app_login_login.component.html.backup": "/project/src/app/login/login.component.html",
+ "src_shared_ui_button.component.html.backup": "/project/src/shared/ui/button.component.html",
+ "libs_forms_input.component.html.backup": "/project/libs/forms/input.component.html"
+}
+```
+
+## Safety Features
+
+### 1. Path Validation
+
+- Verifies path mapping file exists before restoration
+- Warns about orphaned backup files without mappings
+- Creates missing directories during restoration
+
+### 2. Error Handling
+
+- Graceful handling of missing mapping files
+- Clear error messages for corrupted backups
+- Verbose logging for troubleshooting
+
+### 3. Backward Compatibility Detection
+
+```bash
+❌ Path mapping file not found. Cannot restore files safely.
+This backup was created with an older version that doesn't preserve paths.
+```
+
+## Migration from Old Backup System
+
+If you have backups created with the old system (without path mapping):
+
+1. **Manual Restoration Required**: The old backups cannot be automatically restored
+2. **Identify Original Locations**: You'll need to manually determine where files belong
+3. **Create New Backups**: Re-run migrations with `--backup` to create proper backups
+
+## Testing
+
+The backup system includes comprehensive tests covering:
+
+- Path preservation across nested directories
+- Restoration accuracy
+- Missing directory creation
+- Error handling scenarios
+- Orphaned file detection
+
+Run tests:
+
+```bash
+npm test -- templates/backup-system.spec.ts
+```
+
+## Best Practices
+
+### 1. Always Use Backups for Production
+
+```bash
+# Good: Creates backups before migration
+npm run template-migrate -- --pattern "src/**/*.html" --backup
+
+# Risky: No backup created
+npm run template-migrate -- --pattern "src/**/*.html"
+```
+
+### 2. Verify Backup Creation
+
+```bash
+# Check that path-mapping.json exists
+ls -la migration-reports/backups/path-mapping.json
+
+# Verify backup count matches expected files
+cat migration-reports/backups/path-mapping.json | jq 'keys | length'
+```
+
+### 3. Test Rollback on Small Set First
+
+```bash
+# Test rollback on a few files first
+npm run template-migrate -- --file "src/app/test.html" --backup
+npm run template-cli -- rollback --backup-dir ./migration-reports/backups
+```
+
+## Troubleshooting
+
+### Issue: "Path mapping file not found"
+
+**Cause**: Backup was created with old version or mapping file was deleted
+**Solution**: Cannot auto-restore; manual restoration required
+
+### Issue: "No mapping found for backup file"
+
+**Cause**: Backup file exists but not in mapping (corrupted backup)
+**Solution**: Check backup integrity; may need to recreate
+
+### Issue: Restoration fails with permission errors
+
+**Cause**: Insufficient permissions to create directories or write files
+**Solution**: Check file permissions and disk space
+
+## Implementation Details
+
+### Filename Sanitization
+
+```typescript
+// Convert paths to safe filenames
+const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+
+// Examples:
+// "src/app/login.html" → "src_app_login.html.backup"
+// "libs\\ui\\button.html" → "libs_ui_button.html.backup"
+```
+
+### Directory Creation
+
+```typescript
+// Ensure target directory exists during restoration
+const originalDir = path.dirname(originalPath);
+if (!fs.existsSync(originalDir)) {
+ fs.mkdirSync(originalDir, { recursive: true });
+}
+```
+
+### Path Mapping Storage
+
+```typescript
+// Save mapping as JSON for easy parsing
+const mappingPath = path.join(backupDir, "path-mapping.json");
+fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+```
+
+This improved backup system ensures that migration operations can be safely reversed, even in complex project structures with deeply nested files.
diff --git a/scripts/migration/i18n/templates/backup-system.spec.ts b/scripts/migration/i18n/templates/backup-system.spec.ts
new file mode 100644
index 00000000000..9c5ff863be5
--- /dev/null
+++ b/scripts/migration/i18n/templates/backup-system.spec.ts
@@ -0,0 +1,222 @@
+import * as fs from "fs";
+import * as path from "path";
+
+describe("Backup System", () => {
+ const testDir = path.join(__dirname, "test-backup-system");
+ const backupDir = path.join(testDir, "backups");
+
+ const originalTemplate = `
+
{{ 'title' | i18n }}
+
{{ 'description' | i18n }}
+
`;
+
+ const modifiedTemplate = ``;
+
+ beforeEach(() => {
+ // Create test directory structure
+ if (fs.existsSync(testDir)) {
+ fs.rmSync(testDir, { recursive: true });
+ }
+ fs.mkdirSync(testDir, { recursive: true });
+
+ // Create nested directory structure to test path preservation
+ const nestedDir = path.join(testDir, "nested", "deep");
+ fs.mkdirSync(nestedDir, { recursive: true });
+
+ // Create test files
+ fs.writeFileSync(path.join(testDir, "test1.html"), originalTemplate);
+ fs.writeFileSync(path.join(nestedDir, "test2.html"), originalTemplate);
+ });
+
+ afterEach(() => {
+ if (fs.existsSync(testDir)) {
+ fs.rmSync(testDir, { recursive: true });
+ }
+ });
+
+ describe("backup creation", () => {
+ it("should create backups with path mapping", () => {
+ const templateFiles = [
+ path.join(testDir, "test1.html"),
+ path.join(testDir, "nested", "deep", "test2.html"),
+ ];
+
+ // Simulate the backup creation logic
+ const pathMapping: Record = {};
+
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir, { recursive: true });
+ }
+
+ for (const filePath of templateFiles) {
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+ const backupPath = path.join(backupDir, backupFileName);
+
+ fs.copyFileSync(filePath, backupPath);
+ pathMapping[backupFileName] = filePath;
+ }
+
+ // Save path mapping
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
+ // Verify backup files exist
+ expect(fs.existsSync(mappingPath)).toBe(true);
+ expect(Object.keys(pathMapping)).toHaveLength(2);
+
+ // Verify backup files contain original content
+ for (const [backupFileName, originalPath] of Object.entries(pathMapping)) {
+ const backupPath = path.join(backupDir, backupFileName);
+ expect(fs.existsSync(backupPath)).toBe(true);
+
+ const backupContent = fs.readFileSync(backupPath, "utf-8");
+ const originalContent = fs.readFileSync(originalPath, "utf-8");
+ expect(backupContent).toBe(originalContent);
+ }
+ });
+
+ it("should handle nested directory paths correctly", () => {
+ const filePath = path.join(testDir, "nested", "deep", "test2.html");
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+
+ // Should convert slashes/backslashes to underscores
+ expect(backupFileName).toContain("_");
+ expect(backupFileName).not.toContain("/");
+ expect(backupFileName).not.toContain("\\");
+ expect(backupFileName.endsWith(".backup")).toBe(true);
+ });
+ });
+
+ describe("backup restoration", () => {
+ it("should restore files to original locations", () => {
+ const templateFiles = [
+ path.join(testDir, "test1.html"),
+ path.join(testDir, "nested", "deep", "test2.html"),
+ ];
+
+ // Create backups
+ const pathMapping: Record = {};
+
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir, { recursive: true });
+ }
+
+ for (const filePath of templateFiles) {
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+ const backupPath = path.join(backupDir, backupFileName);
+
+ fs.copyFileSync(filePath, backupPath);
+ pathMapping[backupFileName] = filePath;
+ }
+
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
+ // Modify original files
+ for (const filePath of templateFiles) {
+ fs.writeFileSync(filePath, modifiedTemplate);
+ }
+
+ // Verify files are modified
+ for (const filePath of templateFiles) {
+ const content = fs.readFileSync(filePath, "utf-8");
+ expect(content).toBe(modifiedTemplate);
+ }
+
+ // Restore from backups
+ const loadedMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
+ const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
+
+ for (const backupFile of backupFiles) {
+ const backupPath = path.join(backupDir, backupFile);
+ const originalPath = loadedMapping[backupFile];
+
+ if (originalPath) {
+ // Ensure directory exists
+ const originalDir = path.dirname(originalPath);
+ if (!fs.existsSync(originalDir)) {
+ fs.mkdirSync(originalDir, { recursive: true });
+ }
+
+ fs.copyFileSync(backupPath, originalPath);
+ }
+ }
+
+ // Verify files are restored
+ for (const filePath of templateFiles) {
+ const content = fs.readFileSync(filePath, "utf-8");
+ expect(content).toBe(originalTemplate);
+ }
+ });
+
+ it("should handle missing directories during restoration", () => {
+ const filePath = path.join(testDir, "new", "nested", "path", "test.html");
+ const backupFileName = "test_new_nested_path_test.html.backup";
+ const pathMapping = { [backupFileName]: filePath };
+
+ // Create backup directory and mapping
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir, { recursive: true });
+ }
+
+ const backupPath = path.join(backupDir, backupFileName);
+ fs.writeFileSync(backupPath, originalTemplate);
+
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
+ // Restore (should create missing directories)
+ const originalDir = path.dirname(filePath);
+ if (!fs.existsSync(originalDir)) {
+ fs.mkdirSync(originalDir, { recursive: true });
+ }
+ fs.copyFileSync(backupPath, filePath);
+
+ // Verify file was restored and directories were created
+ expect(fs.existsSync(filePath)).toBe(true);
+ expect(fs.readFileSync(filePath, "utf-8")).toBe(originalTemplate);
+ });
+ });
+
+ describe("error handling", () => {
+ it("should handle missing path mapping file", () => {
+ // Create backup files without mapping
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir, { recursive: true });
+ }
+
+ fs.writeFileSync(path.join(backupDir, "test.html.backup"), originalTemplate);
+
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+
+ // Should detect missing mapping file
+ expect(fs.existsSync(mappingPath)).toBe(false);
+ });
+
+ it("should handle backup files without mapping entries", () => {
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir, { recursive: true });
+ }
+
+ // Create backup file
+ const backupFileName = "orphaned.html.backup";
+ fs.writeFileSync(path.join(backupDir, backupFileName), originalTemplate);
+
+ // Create mapping without this file
+ const pathMapping = { "other.html.backup": "/some/other/path.html" };
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
+ const loadedMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
+
+ // Should not find mapping for orphaned file
+ expect(loadedMapping[backupFileName]).toBeUndefined();
+ });
+ });
+});
diff --git a/scripts/migration/i18n/templates/cli.ts b/scripts/migration/i18n/templates/cli.ts
index 1cc9d2054f7..de2b6e788a4 100644
--- a/scripts/migration/i18n/templates/cli.ts
+++ b/scripts/migration/i18n/templates/cli.ts
@@ -306,12 +306,23 @@ program
try {
console.log(chalk.blue("🔄 Rolling back template migration..."));
- const backupDir = options.backup_dir;
+ const backupDir = options.backupDir;
if (!fs.existsSync(backupDir)) {
console.error(chalk.red(`❌ Backup directory not found: ${backupDir}`));
process.exit(1);
}
+ // Check for path mapping file
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ if (!fs.existsSync(mappingPath)) {
+ console.error(chalk.red("❌ Path mapping file not found. Cannot restore files safely."));
+ console.log(
+ chalk.gray("This backup was created with an older version that doesn't preserve paths."),
+ );
+ process.exit(1);
+ }
+
+ const pathMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
if (backupFiles.length === 0) {
@@ -322,15 +333,24 @@ program
let restoredCount = 0;
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
- const originalPath = backupFile.replace(".backup", "");
+ const originalPath = pathMapping[backupFile];
- if (fs.existsSync(originalPath)) {
- fs.copyFileSync(backupPath, originalPath);
- restoredCount++;
+ if (!originalPath) {
+ console.warn(chalk.yellow(`⚠️ No mapping found for backup file: ${backupFile}`));
+ continue;
+ }
- if (options.verbose) {
- console.log(chalk.gray(`Restored: ${originalPath}`));
- }
+ // Ensure the directory exists
+ const originalDir = path.dirname(originalPath);
+ if (!fs.existsSync(originalDir)) {
+ fs.mkdirSync(originalDir, { recursive: true });
+ }
+
+ fs.copyFileSync(backupPath, originalPath);
+ restoredCount++;
+
+ if (options.verbose) {
+ console.log(chalk.gray(`Restored: ${originalPath}`));
}
}
@@ -426,13 +446,25 @@ async function createBackups(templateFiles: string[], outputDir: string): Promis
fs.mkdirSync(backupDir, { recursive: true });
}
+ // Create a mapping file to track original paths
+ const pathMapping: Record = {};
+
for (const filePath of templateFiles) {
if (fs.existsSync(filePath)) {
- const backupPath = path.join(backupDir, path.basename(filePath) + ".backup");
+ // Create a unique backup filename that preserves path info
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+ const backupPath = path.join(backupDir, backupFileName);
+
fs.copyFileSync(filePath, backupPath);
+ pathMapping[backupFileName] = filePath;
}
}
+ // Save the path mapping for restoration
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
console.log(chalk.green(`📦 Created backups for ${templateFiles.length} files`));
}
diff --git a/scripts/migration/i18n/typescript/cli.ts b/scripts/migration/i18n/typescript/cli.ts
index 1b8b375211e..5d333b4d2c6 100644
--- a/scripts/migration/i18n/typescript/cli.ts
+++ b/scripts/migration/i18n/typescript/cli.ts
@@ -189,12 +189,23 @@ program
try {
console.log(chalk.blue("🔄 Rolling back migration..."));
- const backupDir = options.backup_dir;
+ const backupDir = options.backupDir;
if (!fs.existsSync(backupDir)) {
console.error(chalk.red(`❌ Backup directory not found: ${backupDir}`));
process.exit(1);
}
+ // Check for path mapping file
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ if (!fs.existsSync(mappingPath)) {
+ console.error(chalk.red("❌ Path mapping file not found. Cannot restore files safely."));
+ console.log(
+ chalk.gray("This backup was created with an older version that doesn't preserve paths."),
+ );
+ process.exit(1);
+ }
+
+ const pathMapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
const backupFiles = fs.readdirSync(backupDir).filter((f) => f.endsWith(".backup"));
if (backupFiles.length === 0) {
@@ -205,15 +216,24 @@ program
let restoredCount = 0;
for (const backupFile of backupFiles) {
const backupPath = path.join(backupDir, backupFile);
- const originalPath = backupFile.replace(".backup", "");
+ const originalPath = pathMapping[backupFile];
- if (fs.existsSync(originalPath)) {
- fs.copyFileSync(backupPath, originalPath);
- restoredCount++;
+ if (!originalPath) {
+ console.warn(chalk.yellow(`⚠️ No mapping found for backup file: ${backupFile}`));
+ continue;
+ }
- if (options.verbose) {
- console.log(chalk.gray(`Restored: ${originalPath}`));
- }
+ // Ensure the directory exists
+ const originalDir = path.dirname(originalPath);
+ if (!fs.existsSync(originalDir)) {
+ fs.mkdirSync(originalDir, { recursive: true });
+ }
+
+ fs.copyFileSync(backupPath, originalPath);
+ restoredCount++;
+
+ if (options.verbose) {
+ console.log(chalk.gray(`Restored: ${originalPath}`));
}
}
@@ -234,13 +254,25 @@ async function createBackups(migrator: TypeScriptMigrator, outputDir: string): P
const usages = migrator.analyzeUsage();
const filesToBackup = new Set(usages.map((u) => u.filePath));
+ // Create a mapping file to track original paths
+ const pathMapping: Record = {};
+
for (const filePath of filesToBackup) {
if (fs.existsSync(filePath)) {
- const backupPath = path.join(backupDir, path.basename(filePath) + ".backup");
+ // Create a unique backup filename that preserves path info
+ const relativePath = path.relative(process.cwd(), filePath);
+ const backupFileName = relativePath.replace(/[/\\]/g, "_") + ".backup";
+ const backupPath = path.join(backupDir, backupFileName);
+
fs.copyFileSync(filePath, backupPath);
+ pathMapping[backupFileName] = filePath;
}
}
+ // Save the path mapping for restoration
+ const mappingPath = path.join(backupDir, "path-mapping.json");
+ fs.writeFileSync(mappingPath, JSON.stringify(pathMapping, null, 2));
+
console.log(chalk.green(`📦 Created backups for ${filesToBackup.size} files`));
}