|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
* Bootstrap 4 to Bootstrap 5 Migration Helper
|
|
|
* This script helps convert Bootstrap 4 HTML to Bootstrap 5
|
|
|
*/
|
|
|
|
|
|
import fs from 'fs';
|
|
|
import path from 'path';
|
|
|
|
|
|
const MIGRATIONS = {
|
|
|
// Data attributes
|
|
|
attributes: [
|
|
|
{ from: /data-toggle=/g, to: 'data-bs-toggle=' },
|
|
|
{ from: /data-target=/g, to: 'data-bs-target=' },
|
|
|
{ from: /data-dismiss=/g, to: 'data-bs-dismiss=' },
|
|
|
{ from: /data-placement=/g, to: 'data-bs-placement=' },
|
|
|
{ from: /data-content=/g, to: 'data-bs-content=' },
|
|
|
{ from: /data-trigger=/g, to: 'data-bs-trigger=' },
|
|
|
{ from: /data-offset=/g, to: 'data-bs-offset=' },
|
|
|
{ from: /data-spy=/g, to: 'data-bs-spy=' },
|
|
|
{ from: /data-ride=/g, to: 'data-bs-ride=' },
|
|
|
{ from: /data-slide=/g, to: 'data-bs-slide=' },
|
|
|
{ from: /data-slide-to=/g, to: 'data-bs-slide-to=' },
|
|
|
{ from: /data-parent=/g, to: 'data-bs-parent=' },
|
|
|
],
|
|
|
|
|
|
// Classes
|
|
|
classes: [
|
|
|
// Grid
|
|
|
{ from: /\bno-gutters\b/g, to: 'g-0' },
|
|
|
|
|
|
// Utilities
|
|
|
{ from: /\btext-left\b/g, to: 'text-start' },
|
|
|
{ from: /\btext-right\b/g, to: 'text-end' },
|
|
|
{ from: /\bfloat-left\b/g, to: 'float-start' },
|
|
|
{ from: /\bfloat-right\b/g, to: 'float-end' },
|
|
|
{ from: /\bborder-left\b/g, to: 'border-start' },
|
|
|
{ from: /\bborder-right\b/g, to: 'border-end' },
|
|
|
{ from: /\brounded-left\b/g, to: 'rounded-start' },
|
|
|
{ from: /\brounded-right\b/g, to: 'rounded-end' },
|
|
|
{ from: /\bml-(\d+)\b/g, to: 'ms-$1' },
|
|
|
{ from: /\bmr-(\d+)\b/g, to: 'me-$1' },
|
|
|
{ from: /\bpl-(\d+)\b/g, to: 'ps-$1' },
|
|
|
{ from: /\bpr-(\d+)\b/g, to: 'pe-$1' },
|
|
|
|
|
|
// Forms
|
|
|
{ from: /\bform-group\b/g, to: 'mb-3' },
|
|
|
{ from: /\bform-row\b/g, to: 'row' },
|
|
|
{ from: /\bform-inline\b/g, to: 'd-flex align-items-center' },
|
|
|
{ from: /\bcustom-control\b/g, to: 'form-check' },
|
|
|
{ from: /\bcustom-control-input\b/g, to: 'form-check-input' },
|
|
|
{ from: /\bcustom-control-label\b/g, to: 'form-check-label' },
|
|
|
{ from: /\bcustom-select\b/g, to: 'form-select' },
|
|
|
{ from: /\bcustom-file\b/g, to: 'form-control' },
|
|
|
{ from: /\bcustom-range\b/g, to: 'form-range' },
|
|
|
{ from: /\bform-control-file\b/g, to: 'form-control' },
|
|
|
|
|
|
// Input groups
|
|
|
{ from: /\binput-group-append\b/g, to: 'input-group-text' },
|
|
|
{ from: /\binput-group-prepend\b/g, to: 'input-group-text' },
|
|
|
|
|
|
// Dropdowns
|
|
|
{ from: /\bdropdown-menu-right\b/g, to: 'dropdown-menu-end' },
|
|
|
{ from: /\bdropdown-menu-left\b/g, to: 'dropdown-menu-start' },
|
|
|
{ from: /\bdropleft\b/g, to: 'dropstart' },
|
|
|
{ from: /\bdropright\b/g, to: 'dropend' },
|
|
|
|
|
|
// Badges
|
|
|
{ from: /\bbadge-pill\b/g, to: 'rounded-pill' },
|
|
|
{ from: /\bbadge-(\w+)\b/g, to: 'bg-$1' },
|
|
|
|
|
|
// Close button
|
|
|
{ from: /\bclose\b/g, to: 'btn-close' },
|
|
|
|
|
|
// Utilities
|
|
|
{ from: /\bfont-weight-bold\b/g, to: 'fw-bold' },
|
|
|
{ from: /\bfont-weight-normal\b/g, to: 'fw-normal' },
|
|
|
{ from: /\bfont-weight-light\b/g, to: 'fw-light' },
|
|
|
{ from: /\bfont-italic\b/g, to: 'fst-italic' },
|
|
|
|
|
|
// Screen readers
|
|
|
{ from: /\bsr-only\b/g, to: 'visually-hidden' },
|
|
|
{ from: /\bsr-only-focusable\b/g, to: 'visually-hidden-focusable' },
|
|
|
|
|
|
// Embed
|
|
|
{ from: /\bembed-responsive\b/g, to: 'ratio' },
|
|
|
{ from: /\bembed-responsive-16by9\b/g, to: 'ratio-16x9' },
|
|
|
{ from: /\bembed-responsive-4by3\b/g, to: 'ratio-4x3' },
|
|
|
{ from: /\bembed-responsive-1by1\b/g, to: 'ratio-1x1' },
|
|
|
|
|
|
// Jumbotron (removed in BS5)
|
|
|
{ from: /\bjumbotron\b/g, to: 'bg-light p-5 rounded-3' },
|
|
|
|
|
|
// Media object (removed in BS5)
|
|
|
{ from: /\bmedia\b/g, to: 'd-flex' },
|
|
|
{ from: /\bmedia-body\b/g, to: 'flex-grow-1 ms-3' },
|
|
|
],
|
|
|
|
|
|
// JavaScript changes
|
|
|
javascript: [
|
|
|
// jQuery to vanilla JS
|
|
|
{ from: /\$\(document\)\.ready\(function\(\)/g, to: 'document.addEventListener(\'DOMContentLoaded\', function()' },
|
|
|
{ from: /\$\('([^']+)'\)\.tooltip\(\)/g, to: 'new bootstrap.Tooltip(document.querySelector(\'$1\'))' },
|
|
|
{ from: /\$\('([^']+)'\)\.popover\(\)/g, to: 'new bootstrap.Popover(document.querySelector(\'$1\'))' },
|
|
|
{ from: /\$\('([^']+)'\)\.modal\('show'\)/g, to: 'new bootstrap.Modal(document.querySelector(\'$1\')).show()' },
|
|
|
{ from: /\$\('([^']+)'\)\.modal\('hide'\)/g, to: 'bootstrap.Modal.getInstance(document.querySelector(\'$1\')).hide()' },
|
|
|
{ from: /\$\('([^']+)'\)\.collapse\('toggle'\)/g, to: 'new bootstrap.Collapse(document.querySelector(\'$1\'), {toggle: true})' },
|
|
|
]
|
|
|
};
|
|
|
|
|
|
function migrateFile(filePath) {
|
|
|
let content = fs.readFileSync(filePath, 'utf8');
|
|
|
const originalContent = content;
|
|
|
let changesMade = [];
|
|
|
|
|
|
// Apply attribute migrations
|
|
|
MIGRATIONS.attributes.forEach(migration => {
|
|
|
const matches = content.match(migration.from);
|
|
|
if (matches) {
|
|
|
content = content.replace(migration.from, migration.to);
|
|
|
changesMade.push(`✓ Replaced ${matches.length} instances of ${migration.from.source}`);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Apply class migrations
|
|
|
MIGRATIONS.classes.forEach(migration => {
|
|
|
const matches = content.match(migration.from);
|
|
|
if (matches) {
|
|
|
content = content.replace(migration.from, migration.to);
|
|
|
changesMade.push(`✓ Replaced ${matches.length} instances of ${migration.from.source}`);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Apply JavaScript migrations if it's a JS file
|
|
|
if (filePath.endsWith('.js') || filePath.endsWith('.html')) {
|
|
|
MIGRATIONS.javascript.forEach(migration => {
|
|
|
const matches = content.match(migration.from);
|
|
|
if (matches) {
|
|
|
content = content.replace(migration.from, migration.to);
|
|
|
changesMade.push(`✓ Replaced ${matches.length} instances of ${migration.from.source}`);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Additional manual checks
|
|
|
const warnings = [];
|
|
|
|
|
|
if (content.includes('jquery')) {
|
|
|
warnings.push('⚠️ File still contains jQuery references - manual migration needed');
|
|
|
}
|
|
|
|
|
|
if (content.includes('.card-deck')) {
|
|
|
warnings.push('⚠️ Card decks removed in BS5 - use grid system instead');
|
|
|
}
|
|
|
|
|
|
if (content.includes('.form-group')) {
|
|
|
warnings.push('⚠️ Some form-group classes may need manual adjustment');
|
|
|
}
|
|
|
|
|
|
if (content.includes('data-toggle') && !content.includes('data-bs-toggle')) {
|
|
|
warnings.push('⚠️ Some data-toggle attributes may have been missed');
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
filePath,
|
|
|
modified: content !== originalContent,
|
|
|
content,
|
|
|
changesMade,
|
|
|
warnings
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function migrateDirectory(dirPath, options = {}) {
|
|
|
const results = [];
|
|
|
const files = fs.readdirSync(dirPath);
|
|
|
|
|
|
files.forEach(file => {
|
|
|
const fullPath = path.join(dirPath, file);
|
|
|
const stat = fs.statSync(fullPath);
|
|
|
|
|
|
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
|
|
|
results.push(...migrateDirectory(fullPath, options));
|
|
|
} else if (stat.isFile() && (file.endsWith('.html') || file.endsWith('.js'))) {
|
|
|
const result = migrateFile(fullPath);
|
|
|
|
|
|
if (result.modified && !options.dryRun) {
|
|
|
fs.writeFileSync(fullPath, result.content);
|
|
|
}
|
|
|
|
|
|
results.push(result);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
// CLI usage
|
|
|
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
|
const args = process.argv.slice(2);
|
|
|
const targetPath = args[0] || '.';
|
|
|
const dryRun = args.includes('--dry-run');
|
|
|
|
|
|
console.log('🚀 Bootstrap 4 to 5 Migration Tool');
|
|
|
console.log('==================================');
|
|
|
console.log(`Target: ${path.resolve(targetPath)}`);
|
|
|
console.log(`Mode: ${dryRun ? 'Dry run' : 'Live migration'}`);
|
|
|
console.log('');
|
|
|
|
|
|
const results = fs.statSync(targetPath).isDirectory()
|
|
|
? migrateDirectory(targetPath, { dryRun })
|
|
|
: [migrateFile(targetPath)];
|
|
|
|
|
|
results.forEach(result => {
|
|
|
if (result.modified || result.warnings.length > 0) {
|
|
|
console.log(`\n📄 ${result.filePath}`);
|
|
|
|
|
|
if (result.changesMade.length > 0) {
|
|
|
console.log('\nChanges made:');
|
|
|
result.changesMade.forEach(change => console.log(` ${change}`));
|
|
|
}
|
|
|
|
|
|
if (result.warnings.length > 0) {
|
|
|
console.log('\nWarnings:');
|
|
|
result.warnings.forEach(warning => console.log(` ${warning}`));
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const modifiedCount = results.filter(r => r.modified).length;
|
|
|
const warningCount = results.filter(r => r.warnings.length > 0).length;
|
|
|
|
|
|
console.log('\n==================================');
|
|
|
console.log(`✅ Processed ${results.length} files`);
|
|
|
console.log(`📝 Modified ${modifiedCount} files`);
|
|
|
console.log(`⚠️ ${warningCount} files need manual review`);
|
|
|
|
|
|
if (dryRun) {
|
|
|
console.log('\n💡 This was a dry run. Use without --dry-run to apply changes.');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export { migrateFile, migrateDirectory, MIGRATIONS };
|