451 lines
15 KiB
Text
451 lines
15 KiB
Text
|
|
#!/usr/bin/env nu
|
|||
|
|
# Production workflow script
|
|||
|
|
|
|||
|
|
use build.nu build_page
|
|||
|
|
use utils.nu *
|
|||
|
|
use optimize-html.nu *
|
|||
|
|
use optimize-css.nu *
|
|||
|
|
use optimize-svg.nu *
|
|||
|
|
|
|||
|
|
# Production build with full optimization
|
|||
|
|
def main [
|
|||
|
|
--analyze(-a) # Show detailed analysis
|
|||
|
|
--compress(-c) # Generate compressed versions
|
|||
|
|
--verbose(-v) # Verbose output
|
|||
|
|
--output(-o): string # Custom output directory
|
|||
|
|
] {
|
|||
|
|
let config = if ($output | is-not-empty) {
|
|||
|
|
(load_config | update dist_dir $output)
|
|||
|
|
} else {
|
|||
|
|
(load_config)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print "🚀 Starting production build..."
|
|||
|
|
print $"📁 Output directory: ($config.dist_dir)"
|
|||
|
|
|
|||
|
|
# Validate configuration
|
|||
|
|
if not (validate_config $config) {
|
|||
|
|
print "❌ Configuration validation failed"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Full production build
|
|||
|
|
let build_result = (measure_time "Production build" {
|
|||
|
|
build_page --clean --verbose --analyze
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Generate compressed versions if requested
|
|||
|
|
if $compress {
|
|||
|
|
generate_compressed_versions $config $verbose
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Performance audit
|
|||
|
|
if $analyze {
|
|||
|
|
performance_audit $config $verbose
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate deployment files
|
|||
|
|
generate_deployment_files $config $verbose
|
|||
|
|
|
|||
|
|
print "✅ Production build completed successfully!"
|
|||
|
|
|
|||
|
|
# Show final summary
|
|||
|
|
show_build_summary $config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate compressed versions (gzip, brotli)
|
|||
|
|
def generate_compressed_versions [config: record, verbose: bool] {
|
|||
|
|
print "📦 Generating compressed versions..."
|
|||
|
|
|
|||
|
|
let files_to_compress = [
|
|||
|
|
$"($config.dist_dir)/($config.html_output)",
|
|||
|
|
$"($config.css_dist)/($config.css_output)",
|
|||
|
|
$"($config.svg_dist)/sprites.svg"
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
let compressed_files = (compress_files $files_to_compress)
|
|||
|
|
|
|||
|
|
if $verbose {
|
|||
|
|
print $"📦 Generated ($compressed_files | length) compressed files"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Comprehensive performance audit
|
|||
|
|
def performance_audit [config: record, verbose: bool] {
|
|||
|
|
print "\n🔍 Performance Audit"
|
|||
|
|
print "===================="
|
|||
|
|
|
|||
|
|
# HTML analysis
|
|||
|
|
let html_file = $"($config.dist_dir)/($config.html_output)"
|
|||
|
|
if ($html_file | path exists) {
|
|||
|
|
let html_content = (open $html_file)
|
|||
|
|
let html_analysis = (analyze_html $html_content)
|
|||
|
|
let performance_hints = (generate_performance_hints $html_content)
|
|||
|
|
|
|||
|
|
print $"📄 HTML Analysis:"
|
|||
|
|
print $" Size: (format_file_size $html_analysis.size_bytes)"
|
|||
|
|
print $" Lines: ($html_analysis.lines)"
|
|||
|
|
print $" External resources: CSS($html_analysis.external_css), JS($html_analysis.external_js), Images($html_analysis.images)"
|
|||
|
|
|
|||
|
|
if ($performance_hints | length) > 0 {
|
|||
|
|
print $" 💡 Performance hints:"
|
|||
|
|
$performance_hints | each { |hint| print $" - ($hint)" }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# CSS analysis
|
|||
|
|
let css_file = $"($config.css_dist)/($config.css_output)"
|
|||
|
|
if ($css_file | path exists) {
|
|||
|
|
let css_content = (open $css_file)
|
|||
|
|
let css_analysis = (analyze_css $css_content)
|
|||
|
|
let css_metrics = (css_performance_metrics $css_content)
|
|||
|
|
|
|||
|
|
print $"\n🎨 CSS Analysis:"
|
|||
|
|
print $" Size: (format_file_size $css_analysis.size_bytes)"
|
|||
|
|
print $" Rules: ($css_analysis.rule_lines)"
|
|||
|
|
print $" Selectors: ($css_metrics.total_selectors)"
|
|||
|
|
print $" Properties: ($css_metrics.total_properties)"
|
|||
|
|
print $" Avg properties per rule: ($css_metrics.avg_properties_per_rule)"
|
|||
|
|
print $" Complex selectors: ($css_metrics.complex_selectors)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# SVG analysis
|
|||
|
|
let svg_files = (glob $"($config.svg_dist)/*.svg")
|
|||
|
|
if ($svg_files | length) > 0 {
|
|||
|
|
print $"\n🖼️ SVG Analysis:"
|
|||
|
|
let svg_report = (svg_performance_report $svg_files)
|
|||
|
|
|
|||
|
|
$svg_report | each { |svg|
|
|||
|
|
print $" ($svg.file | path basename): (format_file_size $svg.size)"
|
|||
|
|
print $" Elements: ~($svg.elements), Paths: ~($svg.paths)"
|
|||
|
|
print $" Animations: ($svg.animations), Valid: ($svg.valid)"
|
|||
|
|
print $" Optimization potential: (format_file_size $svg.optimization_potential)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Overall performance score
|
|||
|
|
calculate_performance_score $config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Calculate overall performance score
|
|||
|
|
def calculate_performance_score [config: record] {
|
|||
|
|
let html_file = $"($config.dist_dir)/($config.html_output)"
|
|||
|
|
let css_file = $"($config.css_dist)/($config.css_output)"
|
|||
|
|
|
|||
|
|
let total_size = ([$html_file, $css_file]
|
|||
|
|
| where ($it | path exists)
|
|||
|
|
| each { |file|
|
|||
|
|
let size_raw = (ls $file | get size | first)
|
|||
|
|
($size_raw | into int)
|
|||
|
|
}
|
|||
|
|
| math sum
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
let score = if $total_size < 50000 {
|
|||
|
|
"A+ (Excellent)"
|
|||
|
|
} else if $total_size < 100000 {
|
|||
|
|
"A (Good)"
|
|||
|
|
} else if $total_size < 200000 {
|
|||
|
|
"B (Average)"
|
|||
|
|
} else {
|
|||
|
|
"C (Needs optimization)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"\n⭐ Performance Score: ($score)"
|
|||
|
|
print $" Total size: (format_file_size $total_size)"
|
|||
|
|
|
|||
|
|
# Recommendations based on size
|
|||
|
|
if $total_size > 100000 {
|
|||
|
|
print "💡 Recommendations:"
|
|||
|
|
print " - Consider splitting CSS into critical/non-critical"
|
|||
|
|
print " - Optimize SVG files further"
|
|||
|
|
print " - Enable server compression (gzip/brotli)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate deployment files
|
|||
|
|
def generate_deployment_files [config: record, verbose: bool] {
|
|||
|
|
print "📋 Generating deployment files..."
|
|||
|
|
|
|||
|
|
# Ensure the output directory exists
|
|||
|
|
if not ($config.dist_dir | path exists) {
|
|||
|
|
mkdir $config.dist_dir
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate .htaccess for Apache
|
|||
|
|
generate_htaccess $config
|
|||
|
|
|
|||
|
|
# Generate nginx.conf snippet
|
|||
|
|
generate_nginx_config $config
|
|||
|
|
|
|||
|
|
# Generate deploy script
|
|||
|
|
generate_deploy_script $config
|
|||
|
|
|
|||
|
|
# Generate robots.txt
|
|||
|
|
generate_robots_txt $config
|
|||
|
|
|
|||
|
|
if $verbose {
|
|||
|
|
print "📋 Generated deployment configuration files"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate Apache .htaccess
|
|||
|
|
def generate_htaccess [config: record] {
|
|||
|
|
let htaccess_content = '# Page Build System - Apache Configuration
|
|||
|
|
|
|||
|
|
# Enable compression
|
|||
|
|
<IfModule mod_deflate.c>
|
|||
|
|
AddOutputFilterByType DEFLATE text/plain
|
|||
|
|
AddOutputFilterByType DEFLATE text/html
|
|||
|
|
AddOutputFilterByType DEFLATE text/xml
|
|||
|
|
AddOutputFilterByType DEFLATE text/css
|
|||
|
|
AddOutputFilterByType DEFLATE application/xml
|
|||
|
|
AddOutputFilterByType DEFLATE application/xhtml+xml
|
|||
|
|
AddOutputFilterByType DEFLATE application/rss+xml
|
|||
|
|
AddOutputFilterByType DEFLATE application/javascript
|
|||
|
|
AddOutputFilterByType DEFLATE application/x-javascript
|
|||
|
|
AddOutputFilterByType DEFLATE image/svg+xml
|
|||
|
|
</IfModule>
|
|||
|
|
|
|||
|
|
# Cache static assets
|
|||
|
|
<IfModule mod_expires.c>
|
|||
|
|
ExpiresActive on
|
|||
|
|
ExpiresByType text/css "access plus 1 year"
|
|||
|
|
ExpiresByType application/javascript "access plus 1 year"
|
|||
|
|
ExpiresByType image/svg+xml "access plus 1 year"
|
|||
|
|
ExpiresByType text/html "access plus 1 hour"
|
|||
|
|
</IfModule>
|
|||
|
|
|
|||
|
|
# Security headers
|
|||
|
|
<IfModule mod_headers.c>
|
|||
|
|
Header always set X-Content-Type-Options nosniff
|
|||
|
|
Header always set X-Frame-Options DENY
|
|||
|
|
Header always set X-XSS-Protection "1; mode=block"
|
|||
|
|
Header always set Content-Security-Policy "default-src '\''self'\''; style-src '\''self'\'' '\''unsafe-inline'\'' fonts.googleapis.com; font-src fonts.gstatic.com; img-src '\''self'\'' data:; script-src '\''none'\'';"
|
|||
|
|
</IfModule>
|
|||
|
|
|
|||
|
|
# MIME types
|
|||
|
|
AddType image/svg+xml .svg
|
|||
|
|
AddType text/css .css'
|
|||
|
|
|
|||
|
|
$htaccess_content | save --force $"($config.dist_dir)/.htaccess"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate Nginx configuration snippet
|
|||
|
|
def generate_nginx_config [config: record] {
|
|||
|
|
let nginx_content = '# Page Build System - Nginx Configuration
|
|||
|
|
# Add this to your nginx server block
|
|||
|
|
|
|||
|
|
location / {
|
|||
|
|
try_files $uri $uri/ =404;
|
|||
|
|
|
|||
|
|
# Cache static assets
|
|||
|
|
location ~* \.(css|js|svg)$ {
|
|||
|
|
expires 1y;
|
|||
|
|
add_header Cache-Control "public, immutable";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# HTML files - shorter cache
|
|||
|
|
location ~* \.html$ {
|
|||
|
|
expires 1h;
|
|||
|
|
add_header Cache-Control "public";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Compression
|
|||
|
|
gzip on;
|
|||
|
|
gzip_vary on;
|
|||
|
|
gzip_min_length 1024;
|
|||
|
|
gzip_types
|
|||
|
|
text/plain
|
|||
|
|
text/css
|
|||
|
|
text/xml
|
|||
|
|
text/javascript
|
|||
|
|
application/javascript
|
|||
|
|
application/xml+rss
|
|||
|
|
application/json
|
|||
|
|
image/svg+xml;
|
|||
|
|
|
|||
|
|
# Security headers
|
|||
|
|
add_header X-Content-Type-Options nosniff;
|
|||
|
|
add_header X-Frame-Options DENY;
|
|||
|
|
add_header X-XSS-Protection "1; mode=block";
|
|||
|
|
add_header Content-Security-Policy "default-src '\''self'\''; style-src '\''self'\'' '\''unsafe-inline'\'' fonts.googleapis.com; font-src fonts.gstatic.com; img-src '\''self'\'' data:; script-src '\''none'\'';";'
|
|||
|
|
|
|||
|
|
$nginx_content | save --force $"($config.dist_dir)/nginx.conf"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate deployment script
|
|||
|
|
def generate_deploy_script [config: record] {
|
|||
|
|
let deploy_script = '#!/bin/bash
|
|||
|
|
# Deploy script for optimized web page
|
|||
|
|
|
|||
|
|
set -e
|
|||
|
|
|
|||
|
|
echo "🚀 Deploying optimized web page..."
|
|||
|
|
|
|||
|
|
# Configuration
|
|||
|
|
DIST_DIR="dist"
|
|||
|
|
REMOTE_HOST="${DEPLOY_HOST:-your-server.com}"
|
|||
|
|
REMOTE_PATH="${DEPLOY_PATH:-/var/www/optimized-page}"
|
|||
|
|
REMOTE_USER="${DEPLOY_USER:-deploy}"
|
|||
|
|
|
|||
|
|
# Check if dist directory exists
|
|||
|
|
if [ ! -d "$DIST_DIR" ]; then
|
|||
|
|
echo "❌ Distribution directory not found. Run build first."
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Deploy via rsync
|
|||
|
|
echo "📦 Uploading files..."
|
|||
|
|
rsync -avz --delete \
|
|||
|
|
--exclude="*.map" \
|
|||
|
|
--exclude="build-manifest.json" \
|
|||
|
|
"$DIST_DIR/" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
|
|||
|
|
|
|||
|
|
echo "✅ Deployment completed!"
|
|||
|
|
echo "🌐 Site should be available at: https://$REMOTE_HOST"'
|
|||
|
|
|
|||
|
|
$deploy_script | save --force $"($config.dist_dir)/deploy.sh"
|
|||
|
|
|
|||
|
|
# Make executable
|
|||
|
|
chmod +x $"($config.dist_dir)/deploy.sh"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate robots.txt
|
|||
|
|
def generate_robots_txt [config: record] {
|
|||
|
|
let robots_content = 'User-agent: *
|
|||
|
|
Allow: /
|
|||
|
|
|
|||
|
|
# Sitemap
|
|||
|
|
Sitemap: https://your-domain.com/sitemap.xml'
|
|||
|
|
|
|||
|
|
$robots_content | save --force $"($config.dist_dir)/robots.txt"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Show build summary
|
|||
|
|
def show_build_summary [config: record] {
|
|||
|
|
print "\n📊 Build Summary"
|
|||
|
|
print "================"
|
|||
|
|
|
|||
|
|
let files = [
|
|||
|
|
{ name: "index.html", path: $"($config.dist_dir)/($config.html_output)" },
|
|||
|
|
{ name: "main.min.css", path: $"($config.css_dist)/($config.css_output)" },
|
|||
|
|
{ name: "sprites.svg", path: $"($config.svg_dist)/sprites.svg" },
|
|||
|
|
{ name: "symbols.svg", path: $"($config.svg_dist)/symbols.svg" }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
$files | each { |file|
|
|||
|
|
if ($file.path | path exists) {
|
|||
|
|
let size_raw = (ls $file.path | get size | first)
|
|||
|
|
let size = ($size_raw | into int)
|
|||
|
|
let size_formatted = (format_file_size $size)
|
|||
|
|
|
|||
|
|
# Check for compressed version
|
|||
|
|
let compressed_path = $"($file.path).gz"
|
|||
|
|
let compressed_info = if ($compressed_path | path exists) {
|
|||
|
|
let compressed_size_raw = (ls $compressed_path | get size | first)
|
|||
|
|
let compressed_size = ($compressed_size_raw | into int)
|
|||
|
|
let savings = (($size - $compressed_size) / $size * 100 | math round -p 1)
|
|||
|
|
$" \(gzipped: (format_file_size $compressed_size), -($savings)%\)"
|
|||
|
|
} else {
|
|||
|
|
""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $" 📄 ($file.name): ($size_formatted)($compressed_info)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Total uncompressed size
|
|||
|
|
let total_size = ($files
|
|||
|
|
| each { |file|
|
|||
|
|
if ($file.path | path exists) {
|
|||
|
|
let size_raw = (ls $file.path | get size | first)
|
|||
|
|
($size_raw | into int)
|
|||
|
|
} else {
|
|||
|
|
0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
| math sum
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
print $"\n📦 Total size: (format_file_size $total_size)"
|
|||
|
|
|
|||
|
|
# Build info
|
|||
|
|
print $"\n📋 Build completed at: (date now | format date '%Y-%m-%d %H:%M:%S')"
|
|||
|
|
print $"📁 Output directory: ($config.dist_dir)"
|
|||
|
|
print $"🌐 Ready for deployment!"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Quick production build
|
|||
|
|
def "main quick" [
|
|||
|
|
--analyze(-a) # Show detailed analysis
|
|||
|
|
--compress(-c) # Generate compressed versions
|
|||
|
|
--verbose(-v) # Verbose output
|
|||
|
|
--output(-o): string # Custom output directory
|
|||
|
|
] {
|
|||
|
|
# Quick build defaults to verbose, user can override other flags
|
|||
|
|
main --analyze=$analyze --compress=$compress --verbose=true --output=$output
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Production build with full analysis
|
|||
|
|
def "main full" [
|
|||
|
|
--analyze(-a) # Show detailed analysis
|
|||
|
|
--compress(-c) # Generate compressed versions
|
|||
|
|
--verbose(-v) # Verbose output
|
|||
|
|
--output(-o): string # Custom output directory
|
|||
|
|
] {
|
|||
|
|
# Full build defaults to analyze+compress+verbose, user can override
|
|||
|
|
let final_analyze = if $analyze != null { $analyze } else { true }
|
|||
|
|
let final_compress = if $compress != null { $compress } else { true }
|
|||
|
|
let final_verbose = if $verbose != null { $verbose } else { true }
|
|||
|
|
|
|||
|
|
main --analyze=$final_analyze --compress=$final_compress --verbose=$final_verbose --output=$output
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Deploy to staging
|
|||
|
|
def "main staging" [
|
|||
|
|
--analyze(-a) # Show detailed analysis
|
|||
|
|
--compress(-c) # Generate compressed versions
|
|||
|
|
--verbose(-v) # Verbose output
|
|||
|
|
--output(-o): string # Custom output directory
|
|||
|
|
] {
|
|||
|
|
print "🚀 Building for staging..."
|
|||
|
|
|
|||
|
|
# Staging defaults to output="staging" and verbose=true, user can override
|
|||
|
|
let final_output = if ($output | is-not-empty) { $output } else { "staging" }
|
|||
|
|
let final_verbose = if $verbose != null { $verbose } else { true }
|
|||
|
|
|
|||
|
|
main --analyze=$analyze --compress=$compress --verbose=$final_verbose --output=$final_output
|
|||
|
|
|
|||
|
|
print $"📦 Staging build ready in ./($final_output)/"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Production help
|
|||
|
|
def "main help" [] {
|
|||
|
|
print "🏭 Production Commands:"
|
|||
|
|
print "======================="
|
|||
|
|
print " nu prod.nu - Full production build (all flags)"
|
|||
|
|
print " nu prod.nu quick - Quick production build"
|
|||
|
|
print " nu prod.nu full - Full build with analysis and compression"
|
|||
|
|
print " nu prod.nu staging - Build for staging environment"
|
|||
|
|
print " nu prod.nu help - Show this help"
|
|||
|
|
print ""
|
|||
|
|
print "Main Command Flags:"
|
|||
|
|
print " --analyze, -a - Show detailed performance analysis"
|
|||
|
|
print " --compress, -c - Generate compressed versions (gzip)"
|
|||
|
|
print " --verbose, -v - Verbose output"
|
|||
|
|
print " --output, -o <DIR> - Custom output directory"
|
|||
|
|
print ""
|
|||
|
|
print "All subcommands support all flags:"
|
|||
|
|
print " quick: defaults to --verbose"
|
|||
|
|
print " full: defaults to --analyze --compress --verbose"
|
|||
|
|
print " staging: defaults to --verbose --output staging"
|
|||
|
|
print ""
|
|||
|
|
print "Examples:"
|
|||
|
|
print " nu prod.nu --analyze --compress # Main with all flags"
|
|||
|
|
print " nu prod.nu --output staging # Main with custom output"
|
|||
|
|
print " nu prod.nu quick -v -o test # Quick build to test dir"
|
|||
|
|
print " nu prod.nu staging -a # Staging with analysis"
|
|||
|
|
print " nu prod.nu full -o production # Full build to production dir"
|
|||
|
|
}
|