Current section

Files

Jump to
ckeditor5_phoenix lib installer npm_registry.ex
Raw

lib/installer/npm_registry.ex

defmodule CKEditor5.Installer.NPMRegistry do
@moduledoc """
Module responsible for interacting with the NPM registry. It fetches and extracts NPM packages to certain locations.
"""
require Logger
@registry "https://registry.npmjs.org/"
@doc """
Downloads and unpacks an NPM package from the registry.
## Arguments
* `package_name` - The name of the NPM package.
* `version` - The version of the package to install.
* `destination` - The destination directory where the package should be installed (e.g., "deps/my_package").
* `callback` - A function that takes the path to the extracted package (in tmp) and allows modification before moving to destination.
"""
def install_package(package_name, version, destination, callback \\ fn _ -> :ok end) do
ensure_deps_started()
tmp_dir = System.tmp_dir!()
tarball_filename = "#{package_name}-#{version}.tgz"
tarball_path = Path.join(tmp_dir, tarball_filename)
extract_path = Path.join(tmp_dir, "#{package_name}-#{version}-extracted")
try do
# 1. Fetch metadata
metadata_url = "#{@registry}#{package_name}/#{version}"
Logger.info("Fetching metadata from #{metadata_url}")
{:ok, metadata} = fetch_json(metadata_url)
tarball_url = get_in(metadata, ["dist", "tarball"])
if is_nil(tarball_url) do
raise "Could not find tarball URL for #{package_name} v#{version}"
end
# 2. Download tarball
Logger.info("Downloading #{package_name} v#{version}...")
:ok = download_file(tarball_url, tarball_path)
# 3. Extract tarball
File.mkdir_p!(extract_path)
:ok = extract_tarball(tarball_path, extract_path)
# NPM packages usually extract into a "package" folder
package_root = Path.join(extract_path, "package")
working_path =
if File.exists?(package_root) do
package_root
else
extract_path
end
# 4. Run modification callback
callback.(working_path)
# 5. Move to destination
# Ensure parent of destination exists
File.mkdir_p!(Path.dirname(destination))
# Remove destination if it exists to ensure clean install
File.rm_rf!(destination)
File.cp_r!(working_path, destination)
Logger.info("Installed #{package_name} to #{destination}")
after
File.rm(tarball_path)
File.rm_rf(extract_path)
end
end
defp ensure_deps_started do
:inets.start()
:ssl.start()
end
defp fetch_json(url) do
case :httpc.request(:get, {String.to_charlist(url), []}, http_options(), body_format: :binary) do
{:ok, {{_, 200, _}, _, body}} -> Jason.decode(body)
{:ok, {{_, status, _}, _, _}} -> {:error, "HTTP request failed with status #{status}"}
{:error, reason} -> {:error, reason}
end
end
defp download_file(url, path) do
case :httpc.request(:get, {String.to_charlist(url), []}, http_options(),
stream: String.to_charlist(path)
) do
{:ok, :saved_to_file} -> :ok
{:error, reason} -> {:error, reason}
end
end
defp http_options do
[
ssl: [
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 3,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
]
end
defp extract_tarball(tarball_path, dest_dir) do
:erl_tar.extract(String.to_charlist(tarball_path), [
:compressed,
{:cwd, String.to_charlist(dest_dir)}
])
end
end