UNPKG

@wttp/site

Version:

Web3 Transfer Protocol (WTTP) - Site Contracts and deployment tools

351 lines (342 loc) • 15.3 kB
import { task } from "hardhat/config"; import * as fs from "fs"; import * as path from "path"; class WordPressRouteProcessor { constructor(sitePath, configPath, dryRun = false, backup = false) { this.sitePath = sitePath; this.dryRun = dryRun; this.backup = backup; this.stats = { filesProcessed: 0, linksReplaced: 0, redirectsCreated: 0, backupsCreated: 0, errors: [] }; // Load configuration const routesConfigPath = configPath || path.join(sitePath, 'routes.json'); if (!fs.existsSync(routesConfigPath)) { throw new Error(`Routes configuration not found: ${routesConfigPath}`); } try { const configContent = fs.readFileSync(routesConfigPath, 'utf-8'); const loadedConfig = JSON.parse(configContent); // Ensure all required properties exist with defaults this.config = { routes: loadedConfig.routes || {}, settings: { backupOriginals: false, updateCanonicalUrls: true, updateOembedUrls: true, clientSideRedirects: true, preserveQueryParams: true, preserveHashFragments: true, ...loadedConfig.settings }, patterns: { linkAttributes: ['href', 'src', 'action'], urlPatterns: [ 'href=["\']([^"\']*{from}[^"\']*)["\']', 'src=["\']([^"\']*{from}[^"\']*)["\']', 'action=["\']([^"\']*{from}[^"\']*)["\']' ], excludePatterns: ['mailto:', 'tel:', 'javascript:', '#'], ...loadedConfig.patterns } }; } catch (error) { throw new Error(`Failed to parse routes configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async processRoutes() { console.log(`šŸ”„ Processing routes for ${this.sitePath}`); console.log(`šŸ“‹ Found ${Object.keys(this.config.routes).length} route(s) to process`); if (this.dryRun) { console.log('šŸ” DRY RUN MODE - No changes will be made'); } // Process HTML files await this.processHtmlFiles(); // Create client-side redirect script if enabled if (this.config.settings.clientSideRedirects) { await this.createClientRedirectScript(); } this.printStats(); return this.stats; } async processHtmlFiles() { const htmlFiles = this.findHtmlFiles(this.sitePath); console.log(`šŸ“„ Found ${htmlFiles.length} HTML files to process`); for (const filePath of htmlFiles) { await this.processHtmlFile(filePath); } } findHtmlFiles(dirPath) { const htmlFiles = []; const scanDirectory = (dir) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Skip certain directories if (!this.shouldSkipDirectory(entry.name)) { scanDirectory(fullPath); } } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html') && !entry.name.includes('backup') && !entry.name.includes('ninja') && !entry.name.includes('routes')) { // Exclude backup files from processing (*.backup.html, *.ninja.html, *.routes.html) htmlFiles.push(fullPath); } } }; scanDirectory(dirPath); return htmlFiles; } shouldSkipDirectory(dirName) { const skipDirs = ['node_modules', '.git', '.backup', 'wp-admin']; return skipDirs.includes(dirName) || dirName.startsWith('.'); } async processHtmlFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); let modifiedContent = content; let fileChanged = false; // Process each route for (const [fromRoute, routeConfig] of Object.entries(this.config.routes)) { if (routeConfig.method === "redirect") { // Skip link replacement for redirect-only routes continue; } const replacements = this.findAndReplaceLinks(modifiedContent, fromRoute, routeConfig.redirect); modifiedContent = replacements.content; if (replacements.replacementCount > 0) { fileChanged = true; this.stats.linksReplaced += replacements.replacementCount; console.log(` ✨ ${path.relative(this.sitePath, filePath)}: ${replacements.replacementCount} link(s) updated`); } } if (fileChanged) { this.stats.filesProcessed++; if (!this.dryRun) { // Create backup if enabled if (this.backup && this.config.settings.backupOriginals) { await this.createBackup(filePath, content); } // Write modified content fs.writeFileSync(filePath, modifiedContent, 'utf-8'); } } } catch (error) { const errorMsg = `Error processing ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`; this.stats.errors.push(errorMsg); console.error(`āŒ ${errorMsg}`); } } findAndReplaceLinks(content, fromRoute, toRoute) { let modifiedContent = content; let replacementCount = 0; // Generate replacement patterns based on configuration const patterns = this.config.patterns.urlPatterns.map(pattern => pattern.replace('{from}', this.escapeRegex(fromRoute))); for (const pattern of patterns) { const regex = new RegExp(pattern, 'gi'); const matches = modifiedContent.match(regex); if (matches) { modifiedContent = modifiedContent.replace(regex, (match) => { // Check if this match should be excluded if (this.shouldExcludeMatch(match)) { return match; } replacementCount++; return match.replace(fromRoute, toRoute); }); } } return { content: modifiedContent, replacementCount }; } shouldExcludeMatch(match) { return this.config.patterns.excludePatterns.some(pattern => match.toLowerCase().includes(pattern.toLowerCase())); } escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async createBackup(filePath, content) { try { // Generate proper backup filename: index.html -> index.routes.html const dir = path.dirname(filePath); const filename = path.basename(filePath, '.html'); const backupPath = path.join(dir, `${filename}.routes.html`); fs.writeFileSync(backupPath, content, 'utf-8'); this.stats.backupsCreated++; // Add to .wttpignore if it exists await this.addToWttpIgnore(backupPath); } catch (error) { const errorMsg = `Failed to create backup for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`; this.stats.errors.push(errorMsg); } } async addToWttpIgnore(backupPath) { try { const wttpIgnorePath = path.join(this.sitePath, '.wttpignore'); const relativePath = path.relative(this.sitePath, backupPath); if (fs.existsSync(wttpIgnorePath)) { const ignoreContent = fs.readFileSync(wttpIgnorePath, 'utf-8'); if (!ignoreContent.includes(relativePath)) { fs.appendFileSync(wttpIgnorePath, `\n# Route processing backup\n${relativePath}\n`); } } } catch (error) { console.warn(`āš ļø Warning: Could not update .wttpignore: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createClientRedirectScript() { const scriptContent = this.generateRedirectScript(); const scriptPath = path.join(this.sitePath, 'wp-content', 'themes', 'route-redirects.js'); if (!this.dryRun) { // Ensure directory exists const scriptDir = path.dirname(scriptPath); if (!fs.existsSync(scriptDir)) { fs.mkdirSync(scriptDir, { recursive: true }); } fs.writeFileSync(scriptPath, scriptContent, 'utf-8'); this.stats.redirectsCreated = Object.keys(this.config.routes).length; console.log(`šŸ“œ Created client-side redirect script: ${path.relative(this.sitePath, scriptPath)}`); } else { console.log(`šŸ“œ Would create client-side redirect script: ${path.relative(this.sitePath, scriptPath)}`); } } generateRedirectScript() { const routes = Object.entries(this.config.routes) .filter(([_, config]) => config.method !== "replace") .map(([from, config]) => ` '${from}': '${config.redirect}'`); return `/** * WordPress Route Redirects * Auto-generated by wp-routes task */ (function() { 'use strict'; const routes = { ${routes.join(',\n')} }; const settings = ${JSON.stringify(this.config.settings, null, 4)}; function handleRedirect() { const currentPath = window.location.pathname; const targetRoute = routes[currentPath]; if (targetRoute) { let redirectUrl = targetRoute; // Preserve query parameters if enabled if (settings.preserveQueryParams && window.location.search) { redirectUrl += window.location.search; } // Preserve hash fragments if enabled if (settings.preserveHashFragments && window.location.hash) { redirectUrl += window.location.hash; } // Perform redirect if (redirectUrl !== window.location.pathname + window.location.search + window.location.hash) { window.location.replace(redirectUrl); } } } // Run redirect check immediately handleRedirect(); // Also run on DOM content loaded for safety if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', handleRedirect); } })(); `; } printStats() { console.log('\nšŸ“Š Processing Statistics:'); console.log(` šŸ“„ Files processed: ${this.stats.filesProcessed}`); console.log(` šŸ”— Links replaced: ${this.stats.linksReplaced}`); console.log(` šŸ“œ Redirects created: ${this.stats.redirectsCreated}`); console.log(` šŸ’¾ Backups created: ${this.stats.backupsCreated}`); if (this.stats.errors.length > 0) { console.log(`\nāŒ Errors encountered:`); this.stats.errors.forEach(error => console.log(` ${error}`)); } } async dryRunAnalysis() { console.log('\nšŸ” DRY RUN ANALYSIS'); console.log('=================='); const htmlFiles = this.findHtmlFiles(this.sitePath); console.log(`\nšŸ“„ Would process ${htmlFiles.length} HTML files:`); let totalLinks = 0; for (const filePath of htmlFiles) { try { const content = fs.readFileSync(filePath, 'utf-8'); let fileLinks = 0; for (const [fromRoute, routeConfig] of Object.entries(this.config.routes)) { if (routeConfig.method === "redirect") continue; const replacements = this.findAndReplaceLinks(content, fromRoute, routeConfig.redirect); fileLinks += replacements.replacementCount; } if (fileLinks > 0) { console.log(` ✨ ${path.relative(this.sitePath, filePath)}: ${fileLinks} link(s) would be updated`); totalLinks += fileLinks; } } catch (error) { console.log(` āŒ ${path.relative(this.sitePath, filePath)}: Error reading file`); } } console.log(`\nšŸ“‹ Summary:`); console.log(` šŸ”— Total links that would be replaced: ${totalLinks}`); console.log(` šŸ“œ Client-side redirects that would be created: ${Object.keys(this.config.routes).length}`); if (this.config.settings.backupOriginals) { console.log(` šŸ’¾ Backup files that would be created (.routes.html): ${totalLinks > 0 ? htmlFiles.filter(f => { const content = fs.readFileSync(f, 'utf-8'); return Object.entries(this.config.routes).some(([from, config]) => { if (config.method === "redirect") return false; return this.findAndReplaceLinks(content, from, config.redirect).replacementCount > 0; }); }).length : 0}`); } } } task("wp-routes", "Process WordPress site routes using routes.json configuration") .addParam("path", "Path to WordPress site directory") .addOptionalParam("configFile", "Path to routes.json configuration file (defaults to {path}/routes.json)") .addFlag("dryRun", "Show what would be done without making changes") .addFlag("backup", "Create backup files") .addFlag("noRedirect", "Skip creating client-side redirect script") .setAction(async (taskArgs, hre) => { const { path: sitePath, configFile, dryRun, backup, noRedirect } = taskArgs; if (!fs.existsSync(sitePath)) { throw new Error(`Site path does not exist: ${sitePath}`); } try { const processor = new WordPressRouteProcessor(sitePath, configFile, dryRun, backup); // Temporarily disable client redirects if requested if (noRedirect) { const routesConfigPath = configFile || path.join(sitePath, 'routes.json'); const configContent = JSON.parse(fs.readFileSync(routesConfigPath, 'utf-8')); configContent.settings.clientSideRedirects = false; processor['config'] = configContent; } if (dryRun) { await processor.dryRunAnalysis(); return; } const stats = await processor.processRoutes(); if (stats.filesProcessed === 0 && stats.redirectsCreated === 0) { console.log('\nāœ… No changes needed - all routes are already correct!'); } else { console.log('\nāœ… Route processing completed successfully!'); } } catch (error) { console.error('āŒ Error:', error); throw error; } }); //# sourceMappingURL=wp-routes.js.map