@wttp/site
Version:
Web3 Transfer Protocol (WTTP) - Site Contracts and deployment tools
351 lines (342 loc) ⢠15.3 kB
JavaScript
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