#!/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] { 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, 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] { 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, optimized_files: list] { 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)%)" }