Skip to content

Commit 21d28f3

Browse files
committed
Release 0.4.5
1 parent 572fdd7 commit 21d28f3

5 files changed

Lines changed: 107 additions & 23 deletions

File tree

CHANGELOG.md

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

3+
## 0.4.5
4+
5+
- Switch default linker strategy from symlink to copy, fixing ESM module resolution for cached packages
6+
- Fix `NodeRunner` entrypoint resolution to follow bin symlinks correctly
7+
- Cache platform binding selection results, reducing resolve time from ~35s to ~1.5s for packages like `oxfmt`
8+
- Generalize platform binding family detection for both old-style (`@oxfmt/darwin-arm64`) and new-style (`@oxfmt/binding-darwin-arm64`) naming
9+
- Avoid grouping non-platform optional dependencies (e.g. `@babel/core`) as platform variants
10+
311
## 0.4.4
412

513
- Fix npm registry packument decoding for optional platform dependency inspection

lib/npm/linker.ex

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,6 @@ defmodule NPM.Linker do
301301
end
302302

303303
defp default_strategy do
304-
case :os.type() do
305-
{:unix, _} -> :symlink
306-
_ -> :copy
307-
end
304+
:copy
308305
end
309306
end

lib/npm/node_runner.ex

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ defmodule NPM.NodeRunner do
33

44
@spec run(String.t(), [String.t()], keyword()) :: {String.t(), non_neg_integer()}
55
def run(entrypoint, args, opts \\ []) do
6-
node_modules_dir = Keyword.get(opts, :node_modules_dir, "node_modules")
6+
node_modules_dir = Path.expand(Keyword.get(opts, :node_modules_dir, "node_modules"))
77
env = Keyword.get(opts, :env, []) ++ NPM.Exec.env(node_modules_dir)
8+
entrypoint = resolve_entrypoint(Path.expand(entrypoint), node_modules_dir)
89

9-
loader_path = write_loader(Path.expand(node_modules_dir), Path.expand(entrypoint))
10+
loader_path = write_loader(node_modules_dir, entrypoint)
1011

1112
try do
12-
System.cmd("node", [loader_path | args],
13+
node_args = ["--preserve-symlinks", "--preserve-symlinks-main", loader_path | args]
14+
15+
System.cmd("node", node_args,
1316
env: env,
1417
stderr_to_stdout: true,
1518
cd: Keyword.get(opts, :cd, File.cwd!())
@@ -19,16 +22,63 @@ defmodule NPM.NodeRunner do
1922
end
2023
end
2124

25+
defp resolve_entrypoint(entrypoint, node_modules_dir) do
26+
bin_dir = Path.join(node_modules_dir, ".bin")
27+
28+
if String.starts_with?(entrypoint, bin_dir) do
29+
command = Path.basename(entrypoint)
30+
31+
case find_package_entrypoint(command, node_modules_dir) do
32+
{:ok, resolved} -> resolved
33+
:error -> entrypoint
34+
end
35+
else
36+
entrypoint
37+
end
38+
end
39+
40+
defp find_package_entrypoint(command, node_modules_dir) do
41+
bin_path = Path.join([node_modules_dir, ".bin", command])
42+
real_bin_path = resolve_symlink(bin_path)
43+
44+
with {:ok, content} <- File.read(real_bin_path),
45+
[_, rel_path] <- Regex.run(~r{import\s+["'](\.\./[^"']+)["']}, content) do
46+
{:ok, Path.expand(rel_path, Path.dirname(real_bin_path))}
47+
else
48+
_ ->
49+
case NPM.Exec.which(command, node_modules_dir) do
50+
{:ok, pkg_path} -> {:ok, Path.expand(pkg_path)}
51+
_ -> :error
52+
end
53+
end
54+
end
55+
56+
defp resolve_symlink(path, depth \\ 10)
57+
defp resolve_symlink(path, 0), do: path
58+
59+
defp resolve_symlink(path, depth) do
60+
case File.read_link(path) do
61+
{:ok, target} ->
62+
resolve_symlink(Path.expand(target, Path.dirname(path)), depth - 1)
63+
64+
{:error, _} ->
65+
path
66+
end
67+
end
68+
2269
defp write_loader(node_modules_dir, entrypoint) do
23-
path = Path.join(System.tmp_dir!(), "npm-node-runner-#{System.unique_integer([:positive])}.mjs")
70+
path = Path.join(Path.dirname(node_modules_dir), ".npm-node-runner-#{System.unique_integer([:positive])}.mjs")
2471

2572
File.write!(path, """
2673
import { createRequire } from 'node:module'
27-
const require = createRequire(import.meta.url)
28-
globalThis.require = require
29-
process.env.NODE_PATH = #{inspect(node_modules_dir)}
74+
import { pathToFileURL } from 'node:url'
75+
76+
const nmDir = #{inspect(node_modules_dir)}
77+
globalThis.require = createRequire(pathToFileURL(nmDir + '/').href)
78+
process.env.NODE_PATH = nmDir
3079
require('node:module').Module._initPaths()
31-
await import(#{inspect(entrypoint)})
80+
81+
await import(pathToFileURL(#{inspect(entrypoint)}).href)
3282
""")
3383

3484
path

lib/npm/platform_optional.ex

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@ defmodule NPM.PlatformOptional do
22
@moduledoc false
33

44
@spec select(map()) :: map()
5+
def select(optional_dependencies) when map_size(optional_dependencies) == 0, do: %{}
6+
57
def select(optional_dependencies) when is_map(optional_dependencies) do
6-
grouped = Enum.group_by(optional_dependencies, fn {name, _range} -> package_family(name) end)
8+
cache_key = :erlang.phash2(optional_dependencies)
9+
10+
case Process.get({__MODULE__, cache_key}) do
11+
nil ->
12+
result = do_select(optional_dependencies)
13+
Process.put({__MODULE__, cache_key}, result)
14+
result
15+
16+
cached ->
17+
cached
18+
end
19+
end
720

8-
grouped
21+
defp do_select(optional_dependencies) do
22+
optional_dependencies
23+
|> Enum.group_by(fn {name, _range} -> package_family(name) end)
924
|> Enum.flat_map(fn {_family, deps} -> select_group(deps) end)
1025
|> Map.new()
1126
end
@@ -48,19 +63,33 @@ defmodule NPM.PlatformOptional do
4863
end
4964
end
5065

66+
@platform_tokens ~w(darwin linux win32 freebsd android openharmony arm64 x64 ia32 arm x86 ppc64 s390x riscv64 musl gnu msvc gnueabihf musleabihf)
67+
5168
defp package_family(name) do
52-
cond do
53-
String.starts_with?(name, "@oxfmt/binding-") -> "@oxfmt/binding"
54-
String.starts_with?(name, "@oxlint/binding-") -> "@oxlint/binding"
55-
true -> name
69+
if platform_binding?(name) do
70+
case Regex.run(~r/^(@[^\/]+\/)/, name) do
71+
[scope | _] -> scope
72+
nil ->
73+
case String.split(name, "-", parts: 2) do
74+
[prefix, _] -> prefix
75+
_ -> name
76+
end
77+
end
78+
else
79+
name
5680
end
5781
end
5882

83+
defp platform_binding?(name) do
84+
lower = String.downcase(name)
85+
Enum.count(@platform_tokens, &String.contains?(lower, &1)) >= 2
86+
end
87+
5988
@spec current_match(String.t()) :: boolean()
6089
def current_match(name) do
61-
current_os = NPM.Platform.current_os()
62-
current_cpu = NPM.Platform.current_cpu()
63-
String.contains?(name, "-#{current_os}-") and String.ends_with?(name, "-#{current_cpu}") or
64-
String.ends_with?(name, "-#{current_os}-#{current_cpu}")
90+
os = NPM.Platform.current_os()
91+
cpu = NPM.Platform.current_cpu()
92+
lower = String.downcase(name)
93+
String.contains?(lower, os) and String.contains?(lower, cpu)
6594
end
6695
end

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.4"
4+
@version "0.4.5"
55
@source_url "https://github.com/elixir-volt/npm_ex"
66

77
def project do

0 commit comments

Comments
 (0)