Packages
A Mix task that watches source files, rebuilds docs on changes, and serves them with live reload.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/docs.watch.ex
defmodule Mix.Tasks.Docs.Watch do
@shortdoc "Builds docs, serves them locally, and rebuilds on file changes"
@moduledoc """
Watches source files and rebuilds documentation on changes,
serving the result with live reload.
$ mix docs.watch
## Options
* `--port` / `-p` - port to serve on (default: 4001)
* `--no-open` - don't open the browser automatically
"""
use Mix.Task
@default_port 4001
@debounce_ms 500
@impl true
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
strict: [port: :integer, no_open: :boolean],
aliases: [p: :port]
)
port = opts[:port] || @default_port
Application.ensure_all_started(:file_system)
docs_config =
try do
resolve_docs_config()
catch
_, _ -> []
end
doc_dir = docs_config[:output] || "doc"
dirs = watched_dirs(docs_config)
info("Building docs...")
case rebuild_docs() do
:ok ->
:ok
{:error, output} ->
Mix.shell().error(output)
info("Will retry on file changes")
end
{:ok, _} = MixWatchDocs.Server.start_link(port: port, doc_dir: doc_dir)
{:ok, _} = MixWatchDocs.Watcher.start_link(dirs: dirs, debounce_ms: @debounce_ms)
url = "http://localhost:#{port}"
info("Serving docs at #{url}")
info("Watching #{Enum.map_join(dirs, ", ", &Path.relative_to_cwd/1)} for changes")
unless opts[:no_open], do: open_browser(url)
Process.sleep(:infinity)
end
@doc false
def rebuild do
info("Rebuilding...")
case rebuild_docs() do
:ok ->
MixWatchDocs.Server.reload()
info("Done.")
{:error, output} ->
Mix.shell().error(output)
end
end
defp rebuild_docs do
case System.cmd("mix", ["docs"], stderr_to_stdout: true) do
{_, 0} -> :ok
{output, _} -> {:error, String.trim(output)}
end
end
defp resolve_docs_config do
case Mix.Project.config()[:docs] do
fun when is_function(fun, 0) -> fun.()
config when is_list(config) -> config
_ -> []
end
end
defp watched_dirs(docs_config) do
source_dirs = Mix.Project.config()[:elixirc_paths] || ["lib"]
extra_dirs =
(docs_config[:extras] || [])
|> Enum.flat_map(fn
{path, _} when is_binary(path) -> [Path.dirname(path)]
path when is_binary(path) -> [Path.dirname(path)]
_ -> []
end)
|> Enum.reject(&(&1 == "."))
(source_dirs ++ extra_dirs)
|> Enum.map(&Path.expand/1)
|> Enum.uniq()
|> Enum.filter(&File.dir?/1)
|> remove_subdirs()
end
defp remove_subdirs(dirs) do
Enum.reject(dirs, fn dir ->
Enum.any?(dirs, fn other ->
other != dir and String.starts_with?(dir, other <> "/")
end)
end)
end
defp info(msg), do: Mix.shell().info("[docs.watch] #{msg}")
defp open_browser(url) do
case :os.type() do
{:unix, :darwin} -> System.cmd("open", [url])
{:unix, _} -> System.cmd("xdg-open", [url])
{:win32, _} -> System.cmd("cmd", ["/c", "start", url])
end
end
end