Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jerkovicl/910500e0ffc3f21bdf923f01d8190707 to your computer and use it in GitHub Desktop.
Save jerkovicl/910500e0ffc3f21bdf923f01d8190707 to your computer and use it in GitHub Desktop.
A migration to migrate all material buttons to use the input for the appearance instead of the selector
const fs = require('fs').promises;
const path = require('path');
const { EOL } = require('os'); // End Of Line character
// --- Configuration ---
const PROJECT_ROOT = process.argv[2] || '.'; // Get project root from arg or use current dir
const EXCLUDE_DIRS = ['node_modules', '.angular', 'dist', 'e2e', 'coverage']; // Directories to skip
const HTML_SUFFIX = '.html';
const APPLY_CHANGES = process.argv.includes('--apply'); // Check for --apply flag
// --- End Configuration ---
console.log(`--- Angular Material Button Migration Script ---`);
if (APPLY_CHANGES) {
console.log("--- Mode: APPLY CHANGES ---");
} else {
console.log("--- Mode: DRY RUN (use --apply to modify files) ---");
}
console.log(`Project Root: ${path.resolve(PROJECT_ROOT)}`);
console.log(`Excluding: ${EXCLUDE_DIRS.join(', ')}`);
console.log("-------------------------------------------------");
let totalFilesProcessed = 0;
let totalFilesChanged = 0;
/**
* Recursively finds files with a specific suffix in a directory, excluding specified paths.
*/
async function findFiles(dir, suffix, exclude) {
// (Function unchanged)
let results = [];
try {
const list = await fs.readdir(dir, { withFileTypes: true });
for (const dirent of list) {
const fullPath = path.resolve(dir, dirent.name);
const relativePath = path.relative(PROJECT_ROOT, fullPath);
if (exclude.some(ex => relativePath === ex || relativePath.startsWith(ex + path.sep))) {
continue;
}
if (dirent.isDirectory()) {
results = results.concat(await findFiles(fullPath, suffix, exclude));
} else if (dirent.isFile() && dirent.name.endsWith(suffix)) {
results.push(fullPath);
}
}
} catch (err) {
if (err.code !== 'EACCES' && err.code !== 'EPERM') {
console.warn(`Warning: Could not read directory ${dir}: ${err.message}`);
}
}
return results;
}
/**
* MODIFIED: Replaces a legacy Material button attribute with the new matButton variant syntax.
* Removes the legacy attribute AND DOES NOT ensure 'mat-button' attribute is present.
* *** WARNING: This modification is likely incorrect for standard usage. ***
*/
function replaceLegacyButton(content, legacyAttr, newVariant, filePath) {
const regex = new RegExp(`(<([a-zA-Z0-9\\-_]+)[^>]*?)(\\s+${legacyAttr})(\\s*[^>]*?>)`, 'g');
let changed = false;
const lines = content.split(EOL);
const newLines = lines.map((line, index) => {
return line.replace(regex, (match, tagOpen, tagName, legacyAttrFull, tagEnd) => {
changed = true;
// --- MODIFICATION ---
// Intentionally do NOT add 'mat-button'. The baseButtonAttr is always empty.
const baseButtonAttr = '';
// --- END MODIFICATION ---
// Construct the NEW string, omitting legacyAttrFull and baseButtonAttr
const replacement = `${tagOpen}${baseButtonAttr} matButton="${newVariant}"${tagEnd}`;
console.log(` [${path.basename(filePath)} L:${index + 1}] Removing '${legacyAttrFull.trim()}', Adding 'matButton="${newVariant}"' on <${tagName}> (WARN: Not ensuring mat-button)`);
return replacement;
});
});
return { newContent: newLines.join(EOL), changed };
}
/**
* MODIFIED: Changes elements that have ONLY 'mat-button' (no matButton variant)
* to have ONLY 'matButton="text"', REMOVING the original 'mat-button'.
* *** WARNING: This modification is likely incorrect for standard usage. ***
*/
function addTextVariant(content, filePath) {
// Find tags with 'mat-button' but NOT 'matButton='
const regex = new RegExp(`(<([a-zA-Z0-9\\-_]+)[^>]*?)(\\smat-button\\b)(?![^>]*\\smatButton=)([^>]*?>)`, 'g');
let changed = false;
const lines = content.split(EOL);
const newLines = lines.map((line, index) => {
return line.replace(regex, (match, tagOpen, tagName, matButtonAttr, tagEnd) => {
// matButtonAttr here contains the matched ' mat-button'
changed = true;
// --- MODIFICATION ---
// Construct replacement using ONLY the new variant, REMOVING the original mat-button
const replacement = `${tagOpen} matButton="text"${tagEnd}`;
// --- END MODIFICATION ---
console.log(` [${path.basename(filePath)} L:${index + 1}] Replacing '${matButtonAttr.trim()}' with 'matButton="text"' on <${tagName}> (WARN: Removed mat-button)`);
return replacement;
});
});
return { newContent: newLines.join(EOL), changed };
}
/**
* Processes a single HTML file for button migrations.
*/
async function processHtmlFile(filePath) {
// (Function unchanged logic, but uses modified replace/add functions)
const relativePath = path.relative(PROJECT_ROOT, filePath);
console.log(`Processing: ${relativePath}`);
totalFilesProcessed++;
try {
const originalContent = await fs.readFile(filePath, 'utf-8');
let currentContent = originalContent;
let fileChanged = false;
let changes = []; // Store change descriptions
// Apply replacements using the MODIFIED functions
let resultStroked = replaceLegacyButton(currentContent, 'mat-stroked-button', 'outlined', filePath);
currentContent = resultStroked.newContent;
if (resultStroked.changed) changes.push('mat-stroked-button');
let resultFlat = replaceLegacyButton(currentContent, 'mat-flat-button', 'filled', filePath);
currentContent = resultFlat.newContent;
if (resultFlat.changed) changes.push('mat-flat-button');
let resultRaised = replaceLegacyButton(currentContent, 'mat-raised-button', 'elevated', filePath);
currentContent = resultRaised.newContent;
if (resultRaised.changed) changes.push('mat-raised-button');
// Apply text variant addition using the MODIFIED function
let resultText = addTextVariant(currentContent, filePath);
currentContent = resultText.newContent;
if (resultText.changed) changes.push('mat-button -> matButton="text"');
fileChanged = resultStroked.changed || resultFlat.changed || resultRaised.changed || resultText.changed;
if (fileChanged) {
totalFilesChanged++;
console.log(` -> Changes detected (${changes.join(', ')}) in ${relativePath}`);
if (APPLY_CHANGES) {
try {
await fs.writeFile(filePath, currentContent, 'utf-8');
console.log(` -> SUCCESS: Updated ${relativePath}`);
} catch (writeErr) {
console.error(` -> ERROR: Failed to write changes to ${relativePath}: ${writeErr.message}`);
}
} else {
console.log(` -> DRY RUN: Would update ${relativePath}`);
}
}
} catch (error) {
console.error(`Error processing file ${relativePath}: ${error.message}`);
}
}
/**
* Main execution function.
*/
async function main() {
// (Function unchanged)
const resolvedProjectRoot = path.resolve(PROJECT_ROOT);
try {
const htmlFiles = await findFiles(resolvedProjectRoot, HTML_SUFFIX, EXCLUDE_DIRS);
console.log(`Found ${htmlFiles.length} HTML files to analyze.`);
if (htmlFiles.length === 0) {
console.log("No HTML files found. Exiting.");
return;
}
// Process files sequentially
for (const file of htmlFiles) {
await processHtmlFile(file);
}
console.log("-------------------------------------------------");
console.log("Migration Summary:");
console.log(` Total HTML files scanned: ${totalFilesProcessed}`);
console.log(` Total HTML files ${APPLY_CHANGES ? 'modified' : 'needing changes'}: ${totalFilesChanged}`);
if (!APPLY_CHANGES && totalFilesChanged > 0) {
console.log("\nRun with the --apply flag to actually modify the files (Review WARNINGS first!).");
} else if (APPLY_CHANGES && totalFilesChanged > 0) {
console.log("\nReview changes and test your application thoroughly for broken button styles!");
}
console.log("-------------------------------------------------");
} catch (error) {
console.error('\n--- An error occurred during the migration process ---');
console.error(error);
process.exitCode = 1; // Indicate failure
}
}
// --- Run the script ---
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment