Assist_Design/apps/portal/scripts/bundle-monitor.mjs

314 lines
8.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/* eslint-env node */
/* global console, process */
/**
* Bundle size monitoring script
* Analyzes bundle size and reports on performance metrics
*/
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const BUNDLE_SIZE_LIMIT = {
// Size limits in KB
total: 1000, // 1MB total
individual: 250, // 250KB per chunk
vendor: 500, // 500KB for vendor chunks
};
// Note: Performance budgets can be added here when integrated
class BundleMonitor {
constructor() {
this.projectRoot = join(__dirname, "..");
this.buildDir = join(this.projectRoot, ".next");
this.reportFile = join(this.projectRoot, "bundle-report.json");
}
/**
* Analyze bundle size from Next.js build output
*/
analyzeBundleSize() {
const buildManifest = join(this.buildDir, "build-manifest.json");
if (!existsSync(buildManifest)) {
console.error('Build manifest not found. Run "npm run build" first.');
process.exit(1);
}
try {
const manifest = JSON.parse(readFileSync(buildManifest, "utf8"));
const chunks = [];
let totalSize = 0;
// Analyze JavaScript chunks
Object.entries(manifest.pages || {}).forEach(([page, files]) => {
files.forEach(file => {
if (file.endsWith(".js")) {
const filePath = join(this.buildDir, "static", file);
if (existsSync(filePath)) {
const stats = this.getFileStats(filePath);
chunks.push({
page,
file,
size: stats.size,
gzippedSize: stats.gzippedSize,
});
totalSize += stats.size;
}
}
});
});
return {
totalSize,
chunks,
timestamp: Date.now(),
};
} catch (error) {
console.error("Error analyzing bundle:", error.message);
process.exit(1);
}
}
/**
* Get file statistics including gzipped size
*/
getFileStats(filePath) {
try {
const content = readFileSync(filePath);
const size = content.length;
// Estimate gzipped size (rough approximation)
const gzippedSize = Math.round(size * 0.3); // Typical compression ratio
return { size, gzippedSize };
} catch {
return { size: 0, gzippedSize: 0 };
}
}
/**
* Check if bundle sizes are within limits
*/
checkBundleLimits(analysis) {
const issues = [];
// Check total size
const totalSizeKB = analysis.totalSize / 1024;
if (totalSizeKB > BUNDLE_SIZE_LIMIT.total) {
issues.push({
type: "total_size",
message: `Total bundle size (${totalSizeKB.toFixed(1)}KB) exceeds limit (${BUNDLE_SIZE_LIMIT.total}KB)`,
severity: "error",
});
}
// Check individual chunks
analysis.chunks.forEach(chunk => {
const sizeKB = chunk.size / 1024;
if (sizeKB > BUNDLE_SIZE_LIMIT.individual) {
const isVendor = chunk.file.includes("vendor") || chunk.file.includes("node_modules");
const limit = isVendor ? BUNDLE_SIZE_LIMIT.vendor : BUNDLE_SIZE_LIMIT.individual;
if (sizeKB > limit) {
issues.push({
type: "chunk_size",
message: `Chunk ${chunk.file} (${sizeKB.toFixed(1)}KB) exceeds limit (${limit}KB)`,
severity: "warning",
chunk: chunk.file,
});
}
}
});
return issues;
}
/**
* Generate recommendations for bundle optimization
*/
generateRecommendations(analysis) {
const recommendations = [];
// Large chunks recommendations
const largeChunks = analysis.chunks
.filter(chunk => chunk.size / 1024 > 100)
.sort((a, b) => b.size - a.size);
if (largeChunks.length > 0) {
recommendations.push({
type: "code_splitting",
message: "Consider implementing code splitting for large chunks",
chunks: largeChunks.slice(0, 5).map(c => c.file),
});
}
// Vendor chunk recommendations
const vendorChunks = analysis.chunks.filter(
chunk => chunk.file.includes("vendor") || chunk.file.includes("framework")
);
if (vendorChunks.some(chunk => chunk.size / 1024 > 300)) {
recommendations.push({
type: "vendor_optimization",
message: "Consider optimizing vendor chunks or using dynamic imports",
});
}
// Duplicate code detection (simplified)
const pageChunks = analysis.chunks.filter(chunk => chunk.page !== "_app");
if (pageChunks.length > 10) {
recommendations.push({
type: "common_chunks",
message: "Consider extracting common code into shared chunks",
});
}
return recommendations;
}
/**
* Load previous report for comparison
*/
loadPreviousReport() {
if (existsSync(this.reportFile)) {
try {
return JSON.parse(readFileSync(this.reportFile, "utf8"));
} catch (error) {
console.warn("Could not load previous report:", error.message);
}
}
return null;
}
/**
* Save current report
*/
saveReport(report) {
try {
writeFileSync(this.reportFile, JSON.stringify(report, null, 2));
console.log(`Report saved to ${this.reportFile}`);
} catch (error) {
console.error("Could not save report:", error.message);
}
}
/**
* Compare with previous report
*/
compareWithPrevious(current, previous) {
if (!previous) return null;
const currentTotal = current.analysis.totalSize;
const previousTotal = previous.analysis.totalSize;
const sizeDiff = currentTotal - previousTotal;
const percentChange = (sizeDiff / previousTotal) * 100;
return {
sizeDiff,
percentChange,
isRegression: sizeDiff > 10240, // 10KB threshold
};
}
/**
* Generate and display report
*/
run() {
console.log("🔍 Analyzing bundle size...\n");
const analysis = this.analyzeBundleSize();
const issues = this.checkBundleLimits(analysis);
const recommendations = this.generateRecommendations(analysis);
const previous = this.loadPreviousReport();
const comparison = this.compareWithPrevious({ analysis }, previous);
const report = {
timestamp: Date.now(),
analysis,
issues,
recommendations,
comparison,
};
// Display results
this.displayReport(report);
// Save report
this.saveReport(report);
// Exit with error code if there are critical issues
const hasErrors = issues.some(issue => issue.severity === "error");
if (hasErrors) {
console.log("\n❌ Bundle analysis failed due to critical issues.");
process.exit(1);
} else {
console.log("\n✅ Bundle analysis completed successfully.");
}
}
/**
* Display formatted report
*/
displayReport(report) {
const { analysis, issues, recommendations, comparison } = report;
// Bundle size summary
console.log("📊 Bundle Size Summary");
console.log("─".repeat(50));
console.log(`Total Size: ${(analysis.totalSize / 1024).toFixed(1)}KB`);
console.log(`Chunks: ${analysis.chunks.length}`);
if (comparison) {
const sign = comparison.sizeDiff > 0 ? "+" : "";
const color = comparison.isRegression ? "\x1b[31m" : "\x1b[32m";
console.log(
`Change: ${color}${sign}${(comparison.sizeDiff / 1024).toFixed(1)}KB (${comparison.percentChange.toFixed(1)}%)\x1b[0m`
);
}
// Top chunks
console.log("\n📦 Largest Chunks");
console.log("─".repeat(50));
analysis.chunks
.sort((a, b) => b.size - a.size)
.slice(0, 10)
.forEach(chunk => {
console.log(`${(chunk.size / 1024).toFixed(1)}KB - ${chunk.file}`);
});
// Issues
if (issues.length > 0) {
console.log("\n⚠ Issues Found");
console.log("─".repeat(50));
issues.forEach(issue => {
const icon = issue.severity === "error" ? "❌" : "⚠️ ";
console.log(`${icon} ${issue.message}`);
});
}
// Recommendations
if (recommendations.length > 0) {
console.log("\n💡 Recommendations");
console.log("─".repeat(50));
recommendations.forEach(rec => {
console.log(`${rec.message}`);
if (rec.chunks) {
rec.chunks.forEach(chunk => console.log(` - ${chunk}`));
}
});
}
}
}
// Run the monitor
const monitor = new BundleMonitor();
monitor.run();