defmodule Mix.Tasks.Firebird.Target.Watch do @moduledoc """ Watch for file changes and automatically recompile to WASM. Monitors source directories for changes and triggers incremental WASM compilation when files are modified. Uses content hashing to avoid unnecessary recompilation. ## Usage # Watch with default settings mix firebird.target.watch # Watch specific directories mix firebird.target.watch --dirs lib/wasm_modules # Watch with optimizations enabled mix firebird.target.watch --optimize --tco # Custom output directory mix firebird.target.watch --output priv/wasm # Custom poll interval (milliseconds) mix firebird.target.watch --interval 500 ## How It Works 1. Performs an initial compilation of all stale sources 2. Polls source directories for file changes every second (configurable) 3. Uses content hashing to detect actual changes (not just mtime) 4. Only recompiles files that actually changed 5. Reports compilation results in real-time Press Ctrl+C to stop watching. """ use Mix.Task @shortdoc "Watch for changes and recompile to WASM" @switches [ output: :string, dirs: :string, files: :string, optimize: :boolean, tco: :boolean, inline: :boolean, wat_only: :boolean, verbose: :boolean, verify: :boolean, interval: :integer ] @aliases [ o: :output, d: :dirs, v: :verbose, i: :interval ] @impl Mix.Task def run(args) do {opts, _rest, _invalid} = OptionParser.parse(args, switches: @switches, aliases: @aliases) interval = Keyword.get(opts, :interval, 1000) Mix.shell().info("🔥 Firebird WASM Target — Watch Mode") Mix.shell().info(" Poll interval: #{interval}ms") Mix.shell().info(" Press Ctrl+C to stop") Mix.shell().info("") # Initial compilation Mix.shell().info("📦 Initial compilation...") compile_opts = Keyword.delete(opts, :interval) Mix.Tasks.Firebird.Target.Compile.run(build_args(compile_opts)) Mix.shell().info("") Mix.shell().info("👀 Watching for changes...") Mix.shell().info("") # Start watch loop watch_loop(compile_opts, snapshot(compile_opts), interval) end defp watch_loop(opts, prev_snapshot, interval) do Process.sleep(interval) current = snapshot(opts) changed = detect_changes(prev_snapshot, current) if Enum.any?(changed) do Mix.shell().info("") timestamp = Calendar.strftime(DateTime.utc_now(), "%H:%M:%S") Mix.shell().info("🔄 [#{timestamp}] Changes detected in #{length(changed)} file(s):") Enum.each(changed, fn path -> Mix.shell().info(" #{Path.relative_to_cwd(path)}") end) Mix.shell().info("") # Recompile only changed files file_args = ["--files", Enum.join(changed, ",")] extra_args = build_args(Keyword.drop(opts, [:files, :dirs])) Mix.Tasks.Firebird.Target.Compile.run(file_args ++ extra_args) Mix.shell().info("") Mix.shell().info("👀 Watching for changes...") watch_loop(opts, current, interval) else watch_loop(opts, prev_snapshot, interval) end end defp snapshot(opts) do sources = discover_sources(opts) Map.new(sources, fn path -> hash = case File.read(path) do {:ok, content} -> :crypto.hash(:sha256, content) _ -> nil end {path, hash} end) end defp detect_changes(prev, current) do # Files that are new or have different content hashes Enum.flat_map(current, fn {path, hash} -> case Map.get(prev, path) do ^hash -> [] _ -> [path] end end) end defp discover_sources(opts) do dirs = case Keyword.get(opts, :dirs) do nil -> ["lib"] dirs_str -> String.split(dirs_str, ",", trim: true) end files = case Keyword.get(opts, :files) do nil -> [] files_str -> String.split(files_str, ",", trim: true) end explicit = Enum.filter(files, &File.regular?/1) discovered = dirs |> Enum.flat_map(fn dir -> if File.dir?(dir) do Path.wildcard(Path.join(dir, "**/*.{ex,exs}")) else [] end end) |> Enum.filter(fn path -> case File.read(path) do {:ok, content} -> String.contains?(content, "@wasm true") or String.contains?(path, "wasm_modules") _ -> false end end) (explicit ++ discovered) |> Enum.sort() |> Enum.uniq() end defp build_args(opts) do Enum.flat_map(opts, fn {:output, val} -> ["--output", val] {:dirs, val} -> ["--dirs", val] {:files, val} -> ["--files", val] {:optimize, true} -> ["--optimize"] {:tco, true} -> ["--tco"] {:inline, true} -> ["--inline"] {:wat_only, true} -> ["--wat-only"] {:verbose, true} -> ["--verbose"] {:verify, true} -> ["--verify"] _ -> [] end) end end