- Refactored the SignupWorkflowService to throw a DomainHttpException for legacy account conflicts, improving error handling. - Updated the SignupForm component to include initialEmail and showFooterLinks props, enhancing user experience during account creation. - Improved the AccountStep in the SignupForm to allow users to add optional details, such as date of birth and gender, for a more personalized signup process. - Enhanced the PasswordStep to include terms acceptance and marketing consent options, ensuring compliance and user engagement. - Updated various catalog views to improve layout and user guidance, streamlining the onboarding process for new users.
311 lines
8.4 KiB
JavaScript
311 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 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 "pnpm 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();
|