368 lines
No EOL
12 KiB
Text
368 lines
No EOL
12 KiB
Text
#!/usr/bin/env nu
|
|
# Utility functions for build system
|
|
|
|
# Load configuration from TOML/JSON/Nushell file
|
|
export def load_config [] {
|
|
let config_paths = [
|
|
"config/build.config.toml", # Local TOML (preferred)
|
|
"config/build.config.json", # Local JSON
|
|
"config/build.config.nu", # Local Nushell
|
|
"../config/build.config.toml", # Parent TOML
|
|
"../config/build.config.json", # Parent JSON
|
|
"../config/build.config.nu", # Parent Nushell
|
|
"~/.config/build.config.toml", # Home TOML
|
|
"~/.config/build.config.json" # Home JSON
|
|
]
|
|
|
|
# Find first existing path
|
|
let existing_path = ($config_paths | where ($it | path exists) | first)
|
|
|
|
if ($existing_path | is-empty) {
|
|
error make {
|
|
msg: "Configuration file not found",
|
|
help: "Please create config/build.config.toml (recommended), .json, or .nu"
|
|
}
|
|
}
|
|
|
|
# Load config based on file type
|
|
if ($existing_path | str ends-with ".toml") {
|
|
# Flatten TOML structure for easier access
|
|
let toml_config = (open $existing_path)
|
|
{
|
|
# Directories
|
|
src_dir: $toml_config.directories.src_dir,
|
|
dist_dir: $toml_config.directories.dist_dir,
|
|
css_src: $toml_config.directories.css_src,
|
|
css_dist: $toml_config.directories.css_dist,
|
|
svg_src: $toml_config.directories.svg_src,
|
|
svg_dist: $toml_config.directories.svg_dist,
|
|
|
|
# Output files
|
|
css_output: $toml_config.output.css_output,
|
|
html_output: $toml_config.output.html_output,
|
|
svg_sprites: $toml_config.output.svg_sprites,
|
|
|
|
# CSS settings
|
|
css_modules: $toml_config.css.modules,
|
|
critical_css_modules: $toml_config.css.critical_modules,
|
|
css_minify: $toml_config.css.minify,
|
|
remove_css_comments: $toml_config.css.remove_comments,
|
|
compress_selectors: $toml_config.css.compress_selectors,
|
|
|
|
# SVG settings
|
|
svg_optimize: $toml_config.svg.optimize,
|
|
svg_precision: $toml_config.svg.precision,
|
|
|
|
# HTML settings
|
|
html_minify: $toml_config.html.minify,
|
|
remove_comments: $toml_config.html.remove_comments,
|
|
remove_whitespace: $toml_config.html.remove_whitespace,
|
|
|
|
# Development
|
|
dev_port: $toml_config.development.port,
|
|
dev_host: $toml_config.development.host,
|
|
|
|
# Thresholds
|
|
inline_css_max_size: $toml_config.thresholds.inline_css_max_size,
|
|
inline_svg_max_size: $toml_config.thresholds.inline_svg_max_size,
|
|
max_total_size: $toml_config.thresholds.max_total_size,
|
|
|
|
# Compression
|
|
enable_gzip: $toml_config.compression.enable_gzip,
|
|
enable_brotli: $toml_config.compression.enable_brotli,
|
|
gzip_ratio: $toml_config.compression.gzip_ratio,
|
|
|
|
# Cache
|
|
cache_bust: $toml_config.cache.cache_bust,
|
|
cache_bust_length: $toml_config.cache.cache_bust_length,
|
|
|
|
# Output control
|
|
verbose: $toml_config.output_control.verbose,
|
|
show_file_sizes: $toml_config.output_control.show_file_sizes,
|
|
parallel_builds: $toml_config.output_control.parallel_builds
|
|
}
|
|
} else if ($existing_path | str ends-with ".json") {
|
|
open $existing_path
|
|
} else {
|
|
# For .nu files, source them and get the config
|
|
let temp_file = $"/tmp/temp_config_(random chars -l 8).nu"
|
|
$"source ($existing_path)\n$config" | save $temp_file
|
|
let result = (nu $temp_file)
|
|
rm $temp_file
|
|
$result
|
|
}
|
|
}
|
|
|
|
# Calculate file sizes and format them
|
|
export def format_file_size [size: int] {
|
|
if $size < 1024 {
|
|
$"($size) B"
|
|
} else if $size < (1024 * 1024) {
|
|
let kb = ($size / 1024 | math round -p 1)
|
|
$"($kb) KB"
|
|
} else {
|
|
let mb = ($size / (1024 * 1024) | math round -p 2)
|
|
$"($mb) MB"
|
|
}
|
|
}
|
|
|
|
# Generate cache-busting hash
|
|
export def generate_cache_hash [content: string, length: int = 8] {
|
|
# Simple hash based on content length and first few characters
|
|
# In a real implementation, you'd use a proper hash function
|
|
let hash_base = ($content | str length | into string) + ($content | str substring 0..10)
|
|
let hash = ($hash_base | str replace -ar '[^a-zA-Z0-9]' '' | str substring 0..$length)
|
|
$hash
|
|
}
|
|
|
|
# Create directory if it doesn't exist
|
|
export def ensure_dir [path: string] {
|
|
if not ($path | path exists) {
|
|
mkdir $path
|
|
}
|
|
}
|
|
|
|
# Copy file with error handling
|
|
export def safe_copy [source: string, dest: string] {
|
|
if ($source | path exists) {
|
|
cp $source $dest
|
|
return true
|
|
} else {
|
|
print $"Warning: Source file not found: ($source)"
|
|
return false
|
|
}
|
|
}
|
|
|
|
# Get file modification time
|
|
export def get_file_mtime [file_path: string] {
|
|
if ($file_path | path exists) {
|
|
ls $file_path | get modified | first
|
|
} else {
|
|
"1970-01-01T00:00:00Z" | into datetime
|
|
}
|
|
}
|
|
|
|
# Check if file needs rebuilding
|
|
export def needs_rebuild [target: string, sources: list<string>] {
|
|
if not ($target | path exists) {
|
|
return true
|
|
}
|
|
|
|
let target_time = (get_file_mtime $target)
|
|
let newest_source = ($sources
|
|
| where ($it | path exists)
|
|
| each { |src| get_file_mtime $src }
|
|
| sort -r
|
|
| first
|
|
)
|
|
|
|
$newest_source > $target_time
|
|
}
|
|
|
|
# Watch files for changes - simplified approach
|
|
export def watch_files [files: list<string>, callback: closure] {
|
|
mut initial_times = ($files | each { |file|
|
|
{
|
|
file: $file,
|
|
mtime: (get_file_mtime $file)
|
|
}
|
|
})
|
|
|
|
print $"👁️ Watching ($files | length) files for changes..."
|
|
print "📝 Press Ctrl+C to stop..."
|
|
|
|
# Simple loop - let Ctrl+C interrupt naturally
|
|
# The main script will handle the cleanup message
|
|
loop {
|
|
sleep 1sec
|
|
|
|
let current_times = ($files | each { |file|
|
|
{
|
|
file: $file,
|
|
mtime: (get_file_mtime $file)
|
|
}
|
|
})
|
|
|
|
let changed_pairs = ($current_times | zip $initial_times | where { |pair|
|
|
$pair.0.mtime != $pair.1.mtime
|
|
})
|
|
|
|
let changed_files = if ($changed_pairs | length) > 0 {
|
|
$changed_pairs | each { |pair| $pair.0.file }
|
|
} else {
|
|
[]
|
|
}
|
|
|
|
if ($changed_files | length) > 0 {
|
|
print $"📝 Changed files: ($changed_files | str join ', ')"
|
|
do $callback $changed_files
|
|
|
|
# Update times
|
|
$initial_times = $current_times
|
|
}
|
|
}
|
|
}
|
|
|
|
# Measure execution time
|
|
export def measure_time [description: string, code: closure] {
|
|
let start_time = (date now)
|
|
print $"⏱️ ($description)..."
|
|
|
|
let result = (do $code)
|
|
|
|
let end_time = (date now)
|
|
let duration = ($end_time - $start_time)
|
|
print $"✅ ($description) completed in ($duration)"
|
|
|
|
$result
|
|
}
|
|
|
|
# Progress bar for long operations
|
|
export def progress_bar [current: int, total: int, width: int = 50] {
|
|
let percent = ($current / $total * 100 | math round)
|
|
let filled = ($current / $total * $width | math round)
|
|
let bar_filled = (0..$filled | each { "█" } | str join "")
|
|
let bar_empty = (0..($width - $filled) | each { "░" } | str join "")
|
|
|
|
print -n $"\r[($bar_filled)($bar_empty)] ($percent)% ($current)/($total)"
|
|
|
|
if $current == $total {
|
|
print ""
|
|
}
|
|
}
|
|
|
|
# Validate configuration
|
|
export def validate_config [config: record] {
|
|
let required_fields = [
|
|
"src_dir", "dist_dir", "css_src", "css_dist",
|
|
"svg_src", "svg_dist", "css_output", "html_output"
|
|
]
|
|
|
|
let missing_fields = ($required_fields | where { |field|
|
|
not ($config | columns | any { |col| $col == $field })
|
|
})
|
|
|
|
if ($missing_fields | length) > 0 {
|
|
print $"❌ Missing required config fields: ($missing_fields | str join ', ')"
|
|
return false
|
|
}
|
|
|
|
# Check if source directories exist
|
|
if not ($config.src_dir | path exists) {
|
|
print $"❌ Source directory not found: ($config.src_dir)"
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
# Clean temporary files
|
|
export def cleanup_temp_files [temp_dir: string] {
|
|
if ($temp_dir | path exists) {
|
|
print $"🧹 Cleaning temporary files in ($temp_dir)..."
|
|
ls $temp_dir | where name =~ '\.tmp$' | each { |file|
|
|
rm $file.name
|
|
}
|
|
}
|
|
}
|
|
|
|
# Generate build manifest
|
|
export def generate_build_manifest [config: record, build_info: record] {
|
|
let manifest = {
|
|
build_time: (date now | format date "%Y-%m-%d %H:%M:%S"),
|
|
version: "1.0.0",
|
|
config: $config,
|
|
build_info: $build_info,
|
|
files: {
|
|
html: $"($config.dist_dir)/($config.html_output)",
|
|
css: $"($config.css_dist)/($config.css_output)",
|
|
svg: $"($config.svg_dist)/sprites.svg"
|
|
}
|
|
}
|
|
|
|
$manifest | to json | save $"($config.dist_dir)/build-manifest.json"
|
|
$manifest
|
|
}
|
|
|
|
# Simple HTTP server for development
|
|
export def serve_dev [port: int = 8080, directory: string = "dist"] {
|
|
print $"🌐 Starting development server on http://localhost:($port)"
|
|
print $"📁 Serving directory: ($directory)"
|
|
print "Press Ctrl+C to stop"
|
|
|
|
# Try simple-http-server first (faster and more reliable)
|
|
if (which simple-http-server | length) > 0 {
|
|
print "📡 Using simple-http-server..."
|
|
cd $directory
|
|
simple-http-server --port $port --index
|
|
} else if (which python3 | length) > 0 {
|
|
print "📡 Using Python3 HTTP server..."
|
|
cd $directory
|
|
python3 -m http.server $port
|
|
} else if (which python | length) > 0 {
|
|
print "📡 Using Python2 HTTP server..."
|
|
cd $directory
|
|
python -m SimpleHTTPServer $port
|
|
} else {
|
|
print "❌ No HTTP server found (simple-http-server, python3, or python)."
|
|
print " Install one of the following:"
|
|
print " - simple-http-server: cargo install basic-http-server"
|
|
print " - Python3: brew install python (macOS) or system package manager"
|
|
print $" You can manually serve the ($directory) directory with any static file server."
|
|
}
|
|
}
|
|
|
|
# Compress files with gzip (if available)
|
|
export def compress_files [files: list<string>] {
|
|
if (which gzip | length) > 0 {
|
|
$files | each { |file|
|
|
if ($file | path exists) {
|
|
print $"📦 Compressing ($file)..."
|
|
gzip -k $file # Keep original
|
|
$"($file).gz"
|
|
}
|
|
}
|
|
} else {
|
|
print "⚠️ gzip not available, skipping compression"
|
|
[]
|
|
}
|
|
}
|
|
|
|
# Compare file sizes before/after optimization
|
|
export def compare_sizes [original_files: list<string>, optimized_files: list<string>] {
|
|
print "\n📊 Optimization Results:"
|
|
print "========================"
|
|
|
|
# Collect size data first
|
|
let size_data = ($original_files | zip $optimized_files | each { |pair|
|
|
let original = $pair.0
|
|
let optimized = $pair.1
|
|
|
|
if ($original | path exists) and ($optimized | path exists) {
|
|
let orig_size = (ls $original | get size | first)
|
|
let opt_size = (ls $optimized | get size | first)
|
|
let savings = ($orig_size - $opt_size)
|
|
let savings_percent = ($savings / $orig_size * 100 | math round -p 1)
|
|
|
|
print $" ($original | path basename): (format_file_size $orig_size) → (format_file_size $opt_size) (-($savings_percent)%)"
|
|
|
|
{
|
|
original_size: $orig_size,
|
|
optimized_size: $opt_size
|
|
}
|
|
} else {
|
|
{
|
|
original_size: 0,
|
|
optimized_size: 0
|
|
}
|
|
}
|
|
})
|
|
|
|
# Calculate totals
|
|
let total_original = ($size_data | get original_size | math sum)
|
|
let total_optimized = ($size_data | get optimized_size | math sum)
|
|
let total_savings = ($total_original - $total_optimized)
|
|
let total_savings_percent = (if $total_original > 0 { ($total_savings / $total_original * 100 | math round -p 1) } else { 0 })
|
|
|
|
print $"\nTotal: (format_file_size $total_original) → (format_file_size $total_optimized) (-($total_savings_percent)%)"
|
|
} |