@wttp/site
Version:
Web3 Transfer Protocol (WTTP) - Site Contracts and deployment tools
408 lines ⢠17.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("hardhat/config");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class WordPressMinimizer {
constructor(sitePath) {
this.fileUsage = new Map();
this.htmlFiles = [];
this.cssFiles = [];
this.indexFileMap = new Map(); // Maps directory paths to index.html files
this.sitePath = path.resolve(sitePath);
}
async analyze() {
console.log(`š Analyzing WordPress site at: ${this.sitePath}`);
// Find all files in the site
await this.discoverFiles();
// Crawl HTML files for references
await this.crawlHtmlFiles();
// Crawl CSS files for references
await this.crawlCssFiles();
// Calculate statistics
return this.calculateStats();
}
async discoverFiles() {
const walkDir = (dir) => {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
const relativePath = path.relative(this.sitePath, fullPath);
if (file.isDirectory()) {
walkDir(fullPath);
}
else {
// Skip special files that should never be considered unused
if (file.name === '.wttpignore' || file.name === 'routes.json') {
continue;
}
const stats = fs.statSync(fullPath);
this.fileUsage.set(relativePath, {
path: relativePath,
usageCount: 0,
size: stats.size
});
// Track HTML and CSS files for crawling
if (file.name.endsWith('.html')) {
this.htmlFiles.push(fullPath);
// Map index.html files to their directory paths for reference matching
if (file.name === 'index.html') {
const dirPath = path.dirname(relativePath);
// Map both the directory and directory with trailing slash
if (dirPath === '.') {
// Root index.html maps to empty path and root path
this.indexFileMap.set('', relativePath);
this.indexFileMap.set('.', relativePath);
}
else {
this.indexFileMap.set(dirPath, relativePath);
this.indexFileMap.set(dirPath + path.sep, relativePath);
}
}
}
else if (file.name.endsWith('.css')) {
this.cssFiles.push(fullPath);
}
}
}
};
walkDir(this.sitePath);
console.log(`š Found ${this.fileUsage.size} files to analyze`);
}
async crawlHtmlFiles() {
console.log(`š Crawling ${this.htmlFiles.length} HTML files...`);
for (const htmlFile of this.htmlFiles) {
const content = fs.readFileSync(htmlFile, 'utf-8');
this.findReferencesInHtml(content);
}
}
findReferencesInHtml(content) {
// Patterns to match various types of file references
const patterns = [
// src attributes
/src=["']([^"']+)["']/gi,
// href attributes
/href=["']([^"']+)["']/gi,
// CSS background-image
/background-image:\s*url\(["']?([^"')]+)["']?\)/gi,
// CSS url() references
/url\(["']?([^"')]+)["']?\)/gi,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
const url = match[1];
this.processReference(url);
}
}
// Handle srcset which can have multiple URLs
const srcsetPattern = /srcset=["']([^"']+)["']/gi;
let srcsetMatch;
while ((srcsetMatch = srcsetPattern.exec(content)) !== null) {
const srcsetValue = srcsetMatch[1];
// Parse srcset format: "url1 size1, url2 size2, ..."
const srcsetUrls = srcsetValue.split(',').map(item => {
// Each item is "url width" or "url density"
const parts = item.trim().split(/\s+/);
return parts[0]; // Return just the URL part
}).filter(url => url && url.length > 0);
srcsetUrls.forEach(url => this.processReference(url));
}
}
async crawlCssFiles() {
console.log(`šØ Crawling ${this.cssFiles.length} CSS files...`);
for (const cssFile of this.cssFiles) {
const content = fs.readFileSync(cssFile, 'utf-8');
this.findReferencesInCss(content);
}
}
findReferencesInCss(content) {
// CSS url() references
const urlPattern = /url\(["']?([^"')]+)["']?\)/gi;
let match;
while ((match = urlPattern.exec(content)) !== null) {
const url = match[1];
this.processReference(url);
}
}
processReference(url) {
// Skip external URLs, data URLs, and anchors
if (url.startsWith('http') || url.startsWith('//') || url.startsWith('data:') || url.startsWith('#') || url.startsWith('mailto:')) {
return;
}
// Remove query parameters and fragments
const cleanUrl = url.split('?')[0].split('#')[0];
// Convert to relative path from site root
let relativePath = cleanUrl;
if (relativePath.startsWith('/')) {
relativePath = relativePath.substring(1);
}
// Normalize path separators for Windows
relativePath = relativePath.replace(/\//g, path.sep);
// First try direct file match
let usage = this.fileUsage.get(relativePath);
if (usage) {
usage.usageCount++;
return;
}
// If no direct match, check if this might be a directory reference to an index.html
// Remove trailing path separator if present
const normalizedPath = relativePath.endsWith(path.sep) ? relativePath.slice(0, -1) : relativePath;
// Check if this directory path maps to an index.html file
const indexFile = this.indexFileMap.get(normalizedPath) || this.indexFileMap.get(normalizedPath + path.sep);
if (indexFile) {
const indexUsage = this.fileUsage.get(indexFile);
if (indexUsage) {
indexUsage.usageCount++;
if (process.env.DEBUG_WP_MINIMIZE) {
console.log(`š Directory reference "${url}" mapped to index file: ${indexFile}`);
}
return;
}
}
// Debug: log missing references to understand the issue
if (process.env.DEBUG_WP_MINIMIZE) {
console.log(`š Reference not found: "${relativePath}" (original: "${url}")`);
}
}
calculateStats() {
let totalFiles = 0;
let usedFiles = 0;
let unusedFiles = 0;
let totalSize = 0;
let usedSize = 0;
let unusedSize = 0;
for (const usage of this.fileUsage.values()) {
totalFiles++;
totalSize += usage.size;
if (usage.usageCount > 0) {
usedFiles++;
usedSize += usage.size;
}
else {
unusedFiles++;
unusedSize += usage.size;
}
}
const savings = (unusedSize / totalSize) * 100;
return {
totalFiles,
usedFiles,
unusedFiles,
totalSize,
usedSize,
unusedSize,
savings
};
}
getUnusedFiles() {
return Array.from(this.fileUsage.values()).filter(usage => usage.usageCount === 0);
}
getUsedFiles() {
return Array.from(this.fileUsage.values()).filter(usage => usage.usageCount > 0);
}
async generateWttpIgnore() {
const unusedFiles = this.getUnusedFiles();
const header = [
"# WordPress Minimizer - Generated .wttpignore",
`# Generated on: ${new Date().toISOString()}`,
`# Unused files: ${unusedFiles.length}`,
`# Space savings: ${this.formatBytes(unusedFiles.reduce((sum, f) => sum + f.size, 0))}`,
"",
"# Special files (always ignored):",
"routes.json",
"",
"# Unused files (safe to ignore):"
];
const ignorePatterns = unusedFiles.map(file => file.path);
return [...header, ...ignorePatterns].join('\n');
}
async deleteUnusedFiles() {
const unusedFiles = this.getUnusedFiles();
let deletedCount = 0;
let deletedSize = 0;
const affectedDirectories = new Set();
console.log(`šļø Deleting ${unusedFiles.length} unused files...`);
for (const file of unusedFiles) {
const fullPath = path.join(this.sitePath, file.path);
try {
if (fs.existsSync(fullPath)) {
fs.unlinkSync(fullPath);
deletedCount++;
deletedSize += file.size;
// Track the directory that contained this file
const dir = path.dirname(file.path);
if (dir !== '.' && dir !== '') {
affectedDirectories.add(dir);
}
}
}
catch (error) {
console.warn(`ā ļø Could not delete ${file.path}: ${error}`);
}
}
// Clean up empty directories
const deletedDirectories = await this.deleteEmptyDirectories(affectedDirectories);
console.log(`ā
Deleted ${deletedCount} files and ${deletedDirectories} empty folders, saved ${this.formatBytes(deletedSize)}`);
}
async deleteEmptyDirectories(affectedDirectories) {
let deletedDirectoryCount = 0;
// Sort directories by depth (deepest first) to ensure we delete child directories before parents
const sortedDirectories = Array.from(affectedDirectories).sort((a, b) => {
const depthA = a.split(path.sep).length;
const depthB = b.split(path.sep).length;
return depthB - depthA; // Deepest first
});
for (const dir of sortedDirectories) {
const fullDirPath = path.join(this.sitePath, dir);
try {
if (fs.existsSync(fullDirPath)) {
const items = fs.readdirSync(fullDirPath);
// If directory is empty, delete it
if (items.length === 0) {
fs.rmdirSync(fullDirPath);
deletedDirectoryCount++;
if (process.env.DEBUG_WP_MINIMIZE) {
console.log(`š Deleted empty directory: ${dir}`);
}
// Check if parent directory is now empty too
const parentDir = path.dirname(dir);
if (parentDir !== '.' && parentDir !== '' && parentDir !== dir) {
affectedDirectories.add(parentDir);
}
}
}
}
catch (error) {
if (process.env.DEBUG_WP_MINIMIZE) {
console.warn(`ā ļø Could not delete directory ${dir}: ${error}`);
}
}
}
// If we deleted directories, recursively check for more empty parent directories
if (deletedDirectoryCount > 0) {
const remainingDirectories = new Set();
for (const dir of affectedDirectories) {
const parentDir = path.dirname(dir);
if (parentDir !== '.' && parentDir !== '' && parentDir !== dir) {
remainingDirectories.add(parentDir);
}
}
if (remainingDirectories.size > 0) {
const additionalDeleted = await this.deleteEmptyDirectories(remainingDirectories);
deletedDirectoryCount += additionalDeleted;
}
}
return deletedDirectoryCount;
}
formatBytes(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
printReport(stats) {
console.log('\nš WordPress Site Analysis Report');
console.log('ā'.repeat(50));
console.log(`š Total files: ${stats.totalFiles}`);
console.log(`ā
Used files: ${stats.usedFiles}`);
console.log(`ā Unused files: ${stats.unusedFiles}`);
console.log(`š¦ Total size: ${this.formatBytes(stats.totalSize)}`);
console.log(`š¾ Used size: ${this.formatBytes(stats.usedSize)}`);
console.log(`šļø Unused size: ${this.formatBytes(stats.unusedSize)}`);
console.log(`š° Potential savings: ${stats.savings.toFixed(1)}%`);
if (stats.unusedFiles > 0) {
console.log('\nš Top unused files by size:');
const unusedFiles = this.getUnusedFiles()
.sort((a, b) => b.size - a.size)
.slice(0, 10);
for (const file of unusedFiles) {
console.log(` ${this.formatBytes(file.size).padStart(8)} - ${file.path}`);
}
}
}
}
(0, config_1.task)("wp-minimize", "Minimize WordPress site by identifying unused files")
.addParam("path", "Path to WordPress site directory")
.addFlag("deleteFiles", "Delete unused files instead of creating .wttpignore")
.addFlag("dryRun", "Show what would be done without making changes")
.addFlag("debug", "Enable debug output to see reference processing")
.setAction(async (taskArgs, hre) => {
const { path: sitePath, deleteFiles, dryRun, debug } = taskArgs;
if (debug) {
process.env.DEBUG_WP_MINIMIZE = "true";
}
if (!fs.existsSync(sitePath)) {
throw new Error(`Site path does not exist: ${sitePath}`);
}
const minimizer = new WordPressMinimizer(sitePath);
try {
// Analyze the site
const stats = await minimizer.analyze();
// Print report
minimizer.printReport(stats);
if (stats.unusedFiles === 0) {
console.log('\nš Site is already optimized! No unused files found.');
return;
}
if (dryRun) {
console.log('\nš Dry run mode - no changes will be made');
return;
}
if (deleteFiles) {
// Delete unused files
console.log('\nā ļø WARNING: This will permanently delete unused files!');
console.log('Press Ctrl+C to cancel or wait 5 seconds to continue...');
await new Promise(resolve => setTimeout(resolve, 5000));
await minimizer.deleteUnusedFiles();
}
else {
// Generate .wttpignore file
const ignoreContent = await minimizer.generateWttpIgnore();
const ignorePath = path.join(sitePath, '.wttpignore');
fs.writeFileSync(ignorePath, ignoreContent);
console.log(`\nš Generated .wttpignore file: ${ignorePath}`);
console.log('š” Tip: Use --delete-files flag to permanently remove unused files');
}
}
catch (error) {
console.error('ā Error:', error);
throw error;
}
});
//# sourceMappingURL=wp-minimize.js.map