LocalHexdocs
is a Mix task for easily saving and updating Hexdocs files locally on your machine so you always have the most recent documentation for any Elixir, Erlang, or Gleam packages you might want to view.
Clone this repository, create a /packages
subdirectory, then list your desired Hexdocs packages (one per line) in one or more files (named anything you wish) inside this /packages
subdirectory, then fetch the lastest Hexdocs for all packages to your local machine with:
mix local_docs get
(Running this without /packages
files will use the provided default_packages.txt
file. You could also copy to /packages
popular_packages.txt, containing the 1,110 most popular packages on hex.pm.)
You can manually delete older docs versions with the clean
option (see below) OR do a get
AND a clean
with a single command:
mix local_docs get_then_clean
Once downloaded, you can:
- View your documents using the Caddy file server or something similar
- Search your documents using
grep
or something similar - List your downloaded packages
You can also:
- List all packages with local Hexdocs:
mix local_docs list
- List all versions of all packages with local Hexdocs:
mix local_docs versions
- List all packages with multiple local Hexdocs versions:
mix local_docs multiple_versions
- See which package version directories
clean
would delete:
mix local_docs to_clean
- Delete older Hexdocs versions with:
mix local_docs clean
I have tested LocalHexdocs
on my own Linux and Mac machines but own no Windows machines and am unable to test on Windows.
During development and testing on 2025-03-16, performance was great until I hit the following:
"Failed to retrieve package information\nAPI rate limit exceeded for IP [IP address]\n** (MatchError) no match of right hand side value: nil\n
I decided to remove parallelization -- by setting @max_concurrency
to just 1 -- to avoid overloading hexdocs.pm
and to reduce the risk of getting rate-limited.
You can obviously change this module attribute to parallelize processing, but I'm not providing a configuration option for this to avoid pounding the hexdocs.pm
server. Downloading documentation isn't something you need to happen instantaneously. Let's please be appreciative of and nice to our beloved hexdocs.pm
server!
Before you can run LocalHexdocs
, you must run git clone
to download this repository. (Alternatively, you could manually copy the files from https://github.com/JamesLavin/local_hexdocs without downloading the repository, but pulling any future changes would be harder.)
git clone [email protected]:JamesLavin/local_hexdocs.git
OR
git clone https://github.com/JamesLavin/local_hexdocs.git
To pull future LocalHexdocs
updates, cd
into this repository and run:
git pull origin main
After cloning this repository, cd
into it and specify which Elixir packages you wish to pull documentation for.
If you do nothing, running mix local_docs get
in this directory will download all Hexdoc files for packages listed in the provided default_packages.txt
. (There's nothing magical about the packages listed in default_packages.txt
, they were merely my personal list of desired packages when I started this project.)
You can modify default_packages.txt
to add, remove, or comment out (with a leading "#") package names, but this may make it hard to update this project in the future, so I recommend that you:
-
Create a
/packages
subdirectory by runningmkdir packages
-
Create at least one file containing at least one package name -- one line per package name -- within
/packages
You can create a new file (with any name you desire) inside /packages
and/or copy popular_packages.txt
and/or default_packages.txt
to the /packages
subdirectory. For example:
cp popular_packages.txt packages/popular_packages.txt
or
cp default_packages.txt packages/packages.txt
It doesn't matter whether you create your own file(s) or copy files from elsewhere.
You can name these files anything you want. All files in the /packages
directory will be combined (and the concatenated list of package names de-duped). So you could include, for example, a list of packages you personally use, a list of packages your company uses, and a copy of popular_packages.txt
.
You can modify the files' content by adding/removing package names (one package per line) and/or adding comments (starting with #"). For example:
my_packages.txt
:
# This is my list of packages
elixir
gleam
phoenix
phoenix_live_view
ecto
ex_aws_s3
All package names (one per line) in all files in the /packages
directory will be merged into a list of packages to pull documentation for. You can create multiple files, and it's fine if the same package appears more than once, as we de-duplicate before pulling. Again, each package name should be listed on its own line.
We recommend creating a /packages
subdirectory, but you can instead create a packages.txt
file in the top-level directory or rely on default_packages.txt
.
If you don't create a /packages
directory, only default_packages.txt
or packages.txt
will be used from the top-level directory. All other files will be ignored as a source of package names.
packages/*
and packages.txt
are listed in .gitignore
, so updating this repository will not modify/delete your ./packages.txt
file or any files you save in packages/
. You can safely customize them and make them your own.
Once you're happy with your package configuration (whether you choose to create a packages.txt
file or files in /packages/
or use the default_packages.txt
file), you can pull down the latest documentation for each package with this command, executed from the main directory:
mix local_docs get
(I didn't bother creating an executable binary since anyone wanting to download Hexdocs probably already has Elixir installed.)
You will see a stream of output, with one line per package. Each should say one of the following:
- "Docs fetched: /home/user_name/.hex/docs/hexpm/scholar/0.4.0"
- "No package with name made_up_library"
- "Docs already fetched: /home/user_name/.hex/docs/hexpm/sobelow/0.13.0"
After all packages have processed, you should see an Elixir map summarizing what happened.
It contains four keys, each with a list of package names, like:
%{
"Couldn't find docs" => ["coveralls", "cowboy", "iconv", "luerl", "stun"],
"Docs already fetched" => ["amqp/4.0.0", "amqp_client/4.0.3", "axon/0.7.0",
"bamboo/2.4.0", "bamboo_phoenix/1.0.0", "bamboo_smtp/4.2.2", "bandit/1.6.8",
"bcrypt_elixir/3.2.1", "benchee/1.3.1", "bodyguard/2.4.3", "broadway/1.2.1",
...
"vega_lite/0.1.11", "vix/0.33.0", "wallaby/0.30.10", "websock/0.5.3",
"websock_adapter/0.5.8", "xla/0.8.0", "yamerl/0.10.0", "yaml_elixir/2.11.0",
"zigler/0.13.3"],
"Docs fetched" => ["live_svelte/0.15.0", "req/0.5.8", "sobelow/0.13.0"],
"No package with name" => ["made_up_library", "not_real"]
}
After downloading your Hexdoc files, you'll want to view them.
Hex.docs makes it easy to view -- in your default browser -- Hexdocs for a particular package with:
mix hex.docs offline PACKAGE_NAME
One way to browse and view Hexdocs for all your packages is with caddy file-server
(see: CaddyServer.com and the Caddy Github repo). The following will serve your Hexdocs on port 8888 (assuming your hexpm
directory is at ~/.hex/docs/hexpm
):
cd ~/.hex/docs/hexpm
caddy file-server --browse --listen :8888
Downloading documentation makes it easily searchable, so if you download the most popular Elixir/Erlang/Gleam packages' documentation, you can search through them all to discover potentially useful packages.
For example, if you're curious which Elixir packages use JOSE, you can run this query:
$ grep -rl "JOSE" ~/.hex/docs/hexpm
Which should list all files containing "JOSE":
...
./joken/2.6.2/Joken.Signer.html
./joken/2.6.2/Joken.html
./joken/2.6.2/api-reference.html
...
./jose/1.11.10/jose_jwk_oct.html
./jose/1.11.10/jose_jwk_openssh_key.html
./jose/1.11.10/jose_jwk_pem.html
./jose/1.11.10/jose_jwk_set.html
./jose/1.11.10/jose_jwk_use_enc.html
...
./livebook/0.15.4/custom_auth.html
./livebook/0.15.4/dist/search_data-CE645AD5.js
./oidcc/3.3.0/Oidcc.ClientContext.html
./oidcc/3.3.0/Oidcc.ProviderConfiguration.Worker.html
./oidcc/3.3.0/Oidcc.ProviderConfiguration.html
...
To view a list of all Hex packages with downloaded Hexdocs, run:
mix local_docs list
Returns something like:
Packages downloaded in /home/user_name/.hex/docs/hexpm: ["abacus", "absinthe", "absinthe_constraints", "absinthe_error_payload",
"absinthe_federation", "absinthe_graphql_ws", "absinthe_phoenix",
"absinthe_plug", "absinthe_relay", "absinthe_relay_keyset_connection",
"accept", "accessible", "airports", "alembix", "amqp", "amqp_client",
...
"zigler", "zipper", "zstream", "zxcvbn"]
]
mix local_docs versions
Returns something like:
Packages downloaded in /home/user_name/.hex/docs/hexpm: [
...
wallaby: ["0.30.10"],
web_driver_client: ["0.2.0"],
websock: ["0.5.3"],
websock_adapter: ["0.5.8"],
websockex: ["0.4.3"],
wisp: ["1.5.3"],
word_smith: ["0.2.0"],
worker_pool: ["6.4.0"],
workos: ["1.1.0"],
...
]
mix local_docs multiple_versions
Returns something like:
Packages downloaded in /home/user_name/.hex/docs/hexpm with multiple versions: [absinthe: ["1.7.8", "1.7.9"], appsignal: ["2.15.1", "2.15.2"]]
mix local_docs to_clean
Returns something like:
Packages with multiple Hexdocs versions in /home/user_name/.hex/docs/hexpm: [
%{
delete: ["1.7.8"],
keep: "1.7.9",
package: "absinthe",
delete_dirs: ["/home/user_name/.hex/docs/hexpm/absinthe/1.7.8"]
},
%{
delete: ["2.15.1"],
keep: "2.15.2",
package: "appsignal",
delete_dirs: ["/home/user_name/.hex/docs/hexpm/appsignal/2.15.1"]
},
%{
delete: ["3.4.68"],
keep: "3.4.69",
package: "ash",
delete_dirs: ["/home/user_name/.hex/docs/hexpm/ash/3.4.68"]
},
...
%{
delete: ["4.12.0", "4.13.0"],
keep: "5.0.0",
package: "prometheus",
delete_dirs: ["/home/user_name/.hex/docs/hexpm/prometheus/4.12.0",
"/home/user_name/.hex/docs/hexpm/prometheus/4.13.0"]
},
%{
delete: ["0.5.8"],
keep: "0.5.9",
package: "req",
delete_dirs: ["/home/user_name/.hex/docs/hexpm/req/0.5.8"]
},
...
]
To delete the directories listed in delete_dirs
, run:
mix local_docs clean
This should return:
"Deleting /home/user_name/.hex/docs/hexpm/absinthe/1.7.8"
"Deleting /home/user_name/.hex/docs/hexpm/appsignal/2.15.1"
"Deleting /home/user_name/.hex/docs/hexpm/ash/3.4.68"
...
"Deleting /home/user_name/.hex/docs/hexpm/prometheus/4.12.0"
"Deleting /home/user_name/.hex/docs/hexpm/prometheus/4.13.0"
"Deleting /home/user_name/.hex/docs/hexpm/req/0.5.8"
...
Updating your local Hexdocs documentation is as simple as re-running:
mix local_docs get
OR
mix local_docs get_then_clean # to fetch the lastest documentation, then delete all outdated documentation
Each time you run this, LocalHexdocs
will pull the latest version of documentation for each specified package AND any downloaded Hexdocs not at its most recent version.
Fetching the newest package version of any already downloaded Hexdocs -- regardless of whether the package is listed in a LocalHexdocs
package list -- ensures that all your downloaded Hexdocs remain up to date.
To avoid filling your disk with outdated documentation, I recommend running either:
mix local_docs get_then_clean
to automatically delete all outdated documentation after fetching the latest... OR...
mix local_docs to_clean # to see which packages' documentation is outdated
# THEN...
mix local_docs clean # to delete all stale documentation
To ensure you always have the latest documentation, you can create a cron job to periodically run mix local_docs get
(...but please don't run it too frequently or you'll needlessly hammer the Hexdocs.pm server).
Before running a mix task via cron as a user, you may need to cd
into your local_hexdocs
directory and specify your $PATH
. I use .asdf
, and the following works for me on Fedora (which includes user cron jobs inside /etc/crontab
but requires you to specify the user name... In Debian/Ubuntu, I instead used a user-specific cron file -- via crontab -e
-- and did NOT specify the user_name in my cron file). The 5 below means 5 a.m. and the 0 specifies Sunday, so I run my job once per week:
* 5 * * 0 user_name cd /home/user_name/Git/local_hexdocs; PATH=[YOUR_$PATH] /home/user_name/.asdf/shims/mix local_docs get_then_clean >> /home/user_name/local_hexdocs.log 2>&1
The command above directs all output to a log file, so if anything goes wrong, you can debug your problem. E.g.,
$ cat ~/local_hexdocs.log
~/.asdf/installs/elixir/1.17.3-otp-27/bin/elixir: line 248: exec: erl: not found
And, if things go well, you can see exactly what your script did:
...
"Docs already fetched: /home/user_name/.hex/docs/hexpm/mix_audit/2.1.4"
"Docs fetched: /home/user_name/.hex/docs/hexpm/mix_test_interactive/4.3.0"
"Docs already fetched: /home/user_name/.hex/docs/hexpm/mix_test_watch/1.2.0"
"Docs already fetched: /home/user_name/.hex/docs/hexpm/mix_unused/0.4.1"
"Docs fetched: /home/user_name/.hex/docs/hexpm/mjml/5.0.0"
"Docs already fetched: /home/user_name/.hex/docs/hexpm/mjml_eex/0.12.0"
...
- Better handle versions with non-numeric version numbers (violating semantic versioning).
- For now, I'm ignoring them when deciding which versions are redundant.
- I need to add a test.
- I should add an option to display all non-numeric package versions.
- Is it possible to never download such versions?
- After fetching the latest files, automatically look for packages with multiple versions and, if present:
- Provide instructions for removing the older versions
- Offer to immediately remove the older versions
- (If user config/options has opted in) Automatically remove documentation of outdated package versions, with option to keep older versions
- I hit this just once. If it recurs, figure out how to handle it: "** (MatchError) no match of right hand side value: {:error, :eacces}\n (hex 2.0.6) lib/mix/tasks/hex.docs.ex:377: Mix.Tasks.Hex.Docs.extract_docs/2\n (mix 1.16.1) lib/mix/task.ex:478: anonymous fn/3 in Mix.Task.run_task/5\n (mix 1.16.1) lib/mix/cli.ex:96: Mix.CLI.run_task/2\n /Users/user_name/.asdf/installs/elixir/1.16.1-otp-26/bin/mix:2: (file)"
- Recommend grep command that doesn't generate so many "grep: /home/.../.hex/docs/hexpm/mist/4.0.7/fonts/ubuntu-mono-v15-regular-latin.woff2: Permission denied"
- Improve the new unified command-line API
mix local_docs list
could display (perhaps optionally) the individual and total size of downloaded documentation- Function for listing all desired packages without downloaded documentation
- Option to load only the top-N popular packages from
popular_packages.txt
- Display "amqp_client/4.0.3" as "amqp_client (4.0.3)"?
- Regularly update
popular_packages.txt
from current https://hex.pm/packages - Create
awesome_packages.txt
package list containing all packages in https://github.com/h4cc/awesome-elixir? - Should more be done to further avoid pounding
hexdocs.pm
and avoid triggering rate limiting (e.g., pausing between pulls) or is it okay now that I've suppressed parallelization by default?
To run tests, cd
into the project's root directory, then run:
mix test
LocalHexdocs
was created by James Lavin, an Elixir programmer who also loves R (statistics), Linux, PostgreSQL, and running his Kubernetes+Cilium+Helm+Podman+CloudNativePG+Cert-manager cluster.