|
10 | 10 | # and automatically edit the PR description with this. |
11 | 11 | # |
12 | 12 |
|
13 | | -Mix.install([:jason, :req]) |
| 13 | +Mix.install([:jason]) |
14 | 14 |
|
15 | 15 | defmodule Scanner do |
16 | 16 | def scan(mix_lock_content) do |
|
71 | 71 | defmodule YarnScanner do |
72 | 72 | @path "apps/transport/client" |
73 | 73 |
|
74 | | - def at(:local), |
75 | | - do: parse(File.read!("#{@path}/yarn.lock"), File.read!("#{@path}/package.json")) |
| 74 | + # name => sorted unique list of resolved versions (a name may appear at |
| 75 | + # several versions, since we bundle multiple versions at times). |
| 76 | + def versions(:local), do: parse(File.read!("#{@path}/yarn.lock")) |
76 | 77 |
|
77 | | - def at(ref) do |
78 | | - {pj, 0} = System.cmd("git", ["show", "#{ref}:#{@path}/package.json"]) |
| 78 | + def versions(ref) do |
79 | 79 | {yl, 0} = System.cmd("git", ["show", "#{ref}:#{@path}/yarn.lock"]) |
80 | | - parse(yl, pj) |
| 80 | + parse(yl) |
81 | 81 | end |
82 | 82 |
|
83 | | - def diff_url(name, v1, v2) do |
84 | | - with {:ok, %{status: 200, body: body}} <- Req.get("https://registry.npmjs.org/#{name}"), |
85 | | - url <- get_in(body, ["repository", "url"]) || "", |
86 | | - [_, owner, repo] <- Regex.run(~r{github\.com[/:]([^/]+)/([^/.]+)}, url) do |
87 | | - "https://github.com/#{owner}/#{repo}/compare/v#{v1}...v#{v2}" |
88 | | - else |
89 | | - _ -> "https://www.npmjs.com/package/#{name}?activeTab=versions" |
90 | | - end |
| 83 | + # Names declared top-level in a ref's package.json (deps / devDeps / |
| 84 | + # resolutions). Used only to classify a dep into the top-level vs the |
| 85 | + # transitive section — not to filter anything out. |
| 86 | + def direct(:local), do: parse_direct(File.read!("#{@path}/package.json")) |
| 87 | + |
| 88 | + def direct(ref) do |
| 89 | + {pj, 0} = System.cmd("git", ["show", "#{ref}:#{@path}/package.json"]) |
| 90 | + parse_direct(pj) |
91 | 91 | end |
92 | 92 |
|
93 | | - defp parse(lock, json) do |
94 | | - direct = |
95 | | - json |
96 | | - |> Jason.decode!() |
97 | | - |> Map.take(~w(dependencies devDependencies resolutions)) |
98 | | - |> Map.values() |
99 | | - |> Enum.flat_map(&Map.keys/1) |
100 | | - |> MapSet.new() |
| 93 | + defp parse_direct(json) do |
| 94 | + json |
| 95 | + |> Jason.decode!() |
| 96 | + |> Map.take(~w(dependencies devDependencies resolutions)) |
| 97 | + |> Map.values() |
| 98 | + |> Enum.flat_map(&Map.keys/1) |
| 99 | + |> MapSet.new() |
| 100 | + end |
101 | 101 |
|
| 102 | + # npmdiff.dev diffs the published tarballs (the npm equivalent of |
| 103 | + # diff.hex.pm). Scoped names need the scope slash percent-encoded. |
| 104 | + def diff_url(name, v1, v2) do |
| 105 | + "https://npmdiff.dev/#{String.replace(name, "/", "%2F")}/#{v1}/#{v2}/" |
| 106 | + end |
| 107 | + |
| 108 | + defp parse(lock) do |
102 | 109 | lock |
103 | 110 | |> String.split("\n\n") |
104 | 111 | |> Enum.reduce(%{}, fn block, acc -> |
105 | 112 | with [_, name] <- Regex.run(~r/^"?((?:@[^\/"]+\/)?[^@"\s]+)@/m, block), |
106 | | - true <- MapSet.member?(direct, name), |
107 | 113 | [_, ver] <- Regex.run(~r/version "([^"]+)"/, block) do |
108 | | - Map.put(acc, name, ver) |
| 114 | + Map.update(acc, name, [ver], &Enum.uniq([ver | &1])) |
109 | 115 | else |
110 | 116 | _ -> acc |
111 | 117 | end |
112 | 118 | end) |
| 119 | + |> Map.new(fn {name, vers} -> {name, Enum.sort(vers)} end) |
113 | 120 | end |
114 | 121 | end |
115 | 122 |
|
116 | | -IO.puts "\n### JS dependencies (top-level only)" |
117 | | -IO.puts "(may include duplicates, since we bundle multiple versions at times)\n" |
118 | | -yarn_master = YarnScanner.at("master") |
119 | | -yarn_current = YarnScanner.at(:local) |
120 | | - |
121 | | -(Map.keys(yarn_master) ++ Map.keys(yarn_current)) |
122 | | -|> Enum.uniq() |
123 | | -|> Enum.sort() |
124 | | -|> Enum.each(fn name -> |
125 | | - case {yarn_master[name], yarn_current[name]} do |
126 | | - {v1, v2} when not is_nil(v1) and not is_nil(v2) and v1 != v2 -> |
127 | | - IO.puts "* #{YarnScanner.diff_url(name, v1, v2)}" |
128 | | - {nil, v2} when not is_nil(v2) -> IO.puts "* ADDED: #{name}@#{v2}" |
129 | | - {v1, nil} when not is_nil(v1) -> IO.puts "* REMOVED: #{name}@#{v1}" |
130 | | - _ -> :ok |
| 123 | +yarn_master = YarnScanner.versions("master") |
| 124 | +yarn_current = YarnScanner.versions(:local) |
| 125 | +# Union of both refs so a name dropped from resolutions on one side is still |
| 126 | +# classified the same way. |
| 127 | +direct = MapSet.union(YarnScanner.direct("master"), YarnScanner.direct(:local)) |
| 128 | + |
| 129 | +# Same treatment for everyone; only the section a line lands in differs. |
| 130 | +render = fn name -> |
| 131 | + case {Map.get(yarn_master, name), Map.get(yarn_current, name)} do |
| 132 | + {same, same} -> |
| 133 | + nil |
| 134 | + |
| 135 | + {nil, news} -> |
| 136 | + "* ADDED: #{name}@#{Enum.join(news, ", ")}" |
| 137 | + |
| 138 | + {olds, nil} -> |
| 139 | + "* REMOVED: #{name}@#{Enum.join(olds, ", ")}" |
| 140 | + |
| 141 | + {[old], [new]} -> |
| 142 | + "* #{YarnScanner.diff_url(name, old, new)}" |
| 143 | + |
| 144 | + {olds, news} -> |
| 145 | + # Multi-version (bundled duplicates): no single tarball diff applies. |
| 146 | + "* #{name}: #{Enum.join(olds, ", ")} → #{Enum.join(news, ", ")}" |
131 | 147 | end |
132 | | -end) |
| 148 | +end |
| 149 | + |
| 150 | +{top_level, lock_only} = |
| 151 | + (Map.keys(yarn_master) ++ Map.keys(yarn_current)) |
| 152 | + |> Enum.uniq() |
| 153 | + |> Enum.sort() |
| 154 | + |> Enum.map(&{&1, render.(&1)}) |
| 155 | + |> Enum.reject(fn {_name, line} -> is_nil(line) end) |
| 156 | + |> Enum.split_with(fn {name, _line} -> MapSet.member?(direct, name) end) |
| 157 | + |
| 158 | +IO.puts "\n### JS dependencies — top-level (package.json: dependencies / devDependencies / resolutions)\n" |
| 159 | +Enum.each(top_level, fn {_name, line} -> IO.puts(line) end) |
| 160 | + |
| 161 | +IO.puts "\n### JS dependencies — transitive (yarn.lock only)" |
| 162 | +IO.puts "(may include duplicates, since we bundle multiple versions at times)\n" |
| 163 | +Enum.each(lock_only, fn {_name, line} -> IO.puts(line) end) |
0 commit comments