Skip to content

Instantly share code, notes, and snippets.

@egze
Created June 1, 2021 05:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save egze/eff41ae3592532fe6002b0a5aae3d085 to your computer and use it in GitHub Desktop.
Save egze/eff41ae3592532fe6002b0a5aae3d085 to your computer and use it in GitHub Desktop.
Ensure Hex ans Npm have the same version
defmodule MyApp.YarnChecker do
use Credo.Check,
run_on_all: true,
category: :consistency,
base_priority: :high,
param_defaults: [
package_json: "assets/package.json",
yarn_lock: "assets/yarn.lock",
packages: ["phoenix", "phoenix_html", "phoenix_live_view"]
],
explanations: [
check: """
This check ensures that npm packages are the same verion like their hex.pm counterparts.
For example phoenix, phoenix_live_view, phoenix_html can be installed from npm, instead of hex, but
it's then up to the developer to make sure that their versions match and this something easy to miss.
"""
]
def run_on_all_source_files(exec, _source_files, params) do
packages = Keyword.get(params, :packages)
yarn_lock_path = Keyword.get(params, :yarn_lock)
package_json_path = Keyword.get(params, :package_json)
package_json_file = %SourceFile{filename: package_json_path}
issues =
yarn_lock_path
|> parse_yarn_lock()
|> get_npm_packages(packages)
|> ensure_npm_matches_hex(packages, package_json_file, params)
Credo.Execution.ExecutionIssues.append(exec, issues)
end
defp parse_yarn_lock(path) do
{:ok, parsed_yarn_lock} =
path
|> File.read!()
|> YarnParser.decode()
parsed_yarn_lock
end
defp get_npm_packages(parsed_yarn_lock, packages_to_find) do
packages = packages_to_find |> Enum.map(&(&1 <> "@"))
parsed_yarn_lock.dependencies
|> Enum.reduce(%{}, fn {name_with_version, %{"version" => version}}, acc ->
cond do
String.starts_with?(name_with_version, packages) ->
[name | _] = String.split(name_with_version, "@")
Map.put(acc, name, version)
true ->
acc
end
end)
end
defp ensure_npm_matches_hex(npm_packages, packages_to_compare, package_json_file, params) do
parsed_mix_lock = parse_mix_lock()
issue_meta = IssueMeta.for(package_json_file, params)
packages_map =
packages_to_compare
|> Enum.reduce(%{}, fn package, acc ->
hex_version = Map.get(parsed_mix_lock, package)
npm_version = Map.get(npm_packages, package)
Map.put(acc, package, %{hex: hex_version, npm: npm_version})
end)
packages_map
|> Enum.reduce([], fn {package, versions}, acc ->
if versions.hex != versions.npm do
[issue_for(issue_meta, package, versions) | acc]
else
acc
end
end)
end
defp issue_for(issue_meta, package, versions) do
format_issue(
issue_meta,
message: "#{package} versions don't match. hex: #{versions.hex}, npm: #{versions.npm}"
)
end
defp parse_mix_lock do
"./mix.lock"
|> File.read!()
|> Code.string_to_quoted(warn_on_unnecessary_quotes: false)
|> extract_deps_with_versions()
end
defp extract_deps_with_versions(mix_lock_ast) do
{:ok, {_, _, deps_ast}} = mix_lock_ast
deps_ast
|> Enum.reduce(%{}, fn {name_atom, dep_tuple}, acc ->
{_, _, [_, _, version | _]} = dep_tuple
name_str = to_string(name_atom)
Map.put(acc, name_str, version)
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment