Skip to content

Commit 413af1e

Browse files
committed
Release 0.4.6
1 parent 21d28f3 commit 413af1e

5 files changed

Lines changed: 89 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.4.6
4+
5+
- Add packument disk cache (`~/.npm_ex/packuments/`) with 1h TTL — avoids refetching registry metadata on repeat installs
6+
- Skip resolution entirely when lockfile matches `package.json` and `node_modules` is intact
7+
38
## 0.4.5
49

510
- Switch default linker strategy from symlink to copy, fixing ESM module resolution for cached packages

lib/npm.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,22 @@ defmodule NPM do
208208

209209
defp full_install(deps) do
210210
{:ok, old_lockfile} = NPM.Lockfile.read()
211+
212+
if old_lockfile != %{} and lockfile_matches?(old_lockfile, deps) and node_modules_intact?(old_lockfile) do
213+
Mix.shell().info("Already up to date.")
214+
:ok
215+
else
216+
resolve_and_install(deps, old_lockfile)
217+
end
218+
end
219+
220+
defp node_modules_intact?(lockfile) do
221+
Enum.all?(lockfile, fn {name, _entry} ->
222+
Path.join([@node_modules, name, "package.json"]) |> File.exists?()
223+
end)
224+
end
225+
226+
defp resolve_and_install(deps, old_lockfile) do
211227
{:ok, overrides} = NPM.PackageJSON.read_overrides()
212228

213229
{resolve_us, result} =

lib/npm/packument_cache.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule NPM.PackumentCache do
2+
@moduledoc false
3+
4+
@default_ttl_seconds 3600
5+
6+
def dir do
7+
Path.join(NPM.Cache.dir(), "packuments")
8+
end
9+
10+
@spec get(String.t()) :: {:ok, term()} | :miss
11+
def get(package) do
12+
path = path_for(package)
13+
14+
with {:ok, %{mtime: mtime}} <- File.stat(path, time: :posix),
15+
true <- System.os_time(:second) - mtime < ttl(),
16+
{:ok, data} <- File.read(path) do
17+
{:ok, :erlang.binary_to_term(data)}
18+
else
19+
_ -> :miss
20+
end
21+
rescue
22+
_ -> :miss
23+
end
24+
25+
@spec put(String.t(), term()) :: :ok
26+
def put(package, packument) do
27+
path = path_for(package)
28+
File.mkdir_p!(Path.dirname(path))
29+
File.write!(path, :erlang.term_to_binary(packument))
30+
:ok
31+
end
32+
33+
defp path_for(package) do
34+
encoded = String.replace(package, "/", "__")
35+
Path.join(dir(), "#{encoded}.etf")
36+
end
37+
38+
defp ttl do
39+
Application.get_env(:npm, :packument_cache_ttl, @default_ttl_seconds)
40+
end
41+
end

lib/npm/registry.ex

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,40 @@ defmodule NPM.Registry do
4242
@doc "Fetch the abbreviated packument for a package."
4343
@spec get_packument(String.t()) :: {:ok, packument()} | {:error, term()}
4444
def get_packument(package) do
45+
case NPM.PackumentCache.get(package) do
46+
{:ok, packument} ->
47+
{:ok, packument}
48+
49+
:miss ->
50+
fetch_packument(package)
51+
end
52+
end
53+
54+
defp fetch_packument(package) do
4555
url = "#{registry_url()}/#{encode_package(package)}"
4656
headers = auth_headers() ++ [accept: "application/vnd.npm.install-v1+json"]
47-
48-
fetch_with_retry(url, headers, @max_retries)
57+
fetch_with_retry(package, url, headers, @max_retries)
4958
end
5059

51-
defp fetch_with_retry(url, headers, retries_left) do
60+
defp fetch_with_retry(package, url, headers, retries_left) do
5261
result = Req.get(url, headers: headers, decode_body: false)
5362

5463
case classify_result(result) do
55-
{:ok, body} -> {:ok, body |> decode_body() |> parse_packument()}
56-
{:retry, _} when retries_left > 0 -> retry(url, headers, retries_left)
57-
{_, error} -> error
64+
{:ok, body} ->
65+
packument = body |> decode_body() |> parse_packument()
66+
NPM.PackumentCache.put(package, packument)
67+
{:ok, packument}
68+
69+
{:retry, _} when retries_left > 0 ->
70+
retry(package, url, headers, retries_left)
71+
72+
{_, error} ->
73+
error
5874
end
5975
end
6076

77+
78+
6179
defp classify_result({:ok, %{status: 200, body: body}}), do: {:ok, body}
6280
defp classify_result({:ok, %{status: 404}}), do: {:error, {:error, :not_found}}
6381
defp classify_result({:ok, %{status: 401}}), do: {:error, {:error, :unauthorized}}
@@ -66,9 +84,9 @@ defmodule NPM.Registry do
6684
defp classify_result({:ok, %{status: s}}), do: {:error, {:error, {:http, s}}}
6785
defp classify_result({:error, reason}), do: {:retry, {:error, reason}}
6886

69-
defp retry(url, headers, retries_left) do
87+
defp retry(package, url, headers, retries_left) do
7088
Process.sleep(1000 * (@max_retries - retries_left + 1))
71-
fetch_with_retry(url, headers, retries_left - 1)
89+
fetch_with_retry(package, url, headers, retries_left - 1)
7290
end
7391

7492
defp auth_headers do

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule NPM.MixProject do
22
use Mix.Project
33

4-
@version "0.4.5"
4+
@version "0.4.6"
55
@source_url "https://github.com/elixir-volt/npm_ex"
66

77
def project do

0 commit comments

Comments
 (0)