provisioning-outreach/presentations/rust-laspalmas-250926/auroraframe/scripts/utils.nu

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)%)"
}