defmodule Mix.Tasks.Npm.Install do
@shortdoc "Install npm packages"
@moduledoc """
Install npm packages.
mix npm.install # Install all deps from package.json/workspaces
mix npm.install lodash # Add latest version
mix npm.install lodash@^4.0 # Add with specific range
mix npm.install @types/node@^20 # Add scoped package
mix npm.install --frozen # Fail if lockfile is stale (CI)
mix npm.install --production # Skip devDependencies
mix npm.install eslint --save-dev # Add to devDependencies
mix npm.install lodash react vue # Add multiple packages
Resolves all root and workspace dependencies using the PubGrub solver,
writes `npm.lock`, and links packages into `node_modules/`.
"""
use Mix.Task
@impl true
def run(args) do
Application.ensure_all_started(:req)
{opts, positional} = parse_args(args)
case positional do
[] -> NPM.install(opts) |> handle_result()
specs -> Enum.each(specs, &(&1 |> install_spec(opts) |> handle_result()))
end
end
defp parse_args(args) do
{parsed, rest, _} =
OptionParser.parse(args,
strict: [
frozen: :boolean,
production: :boolean,
save_dev: :boolean,
save_exact: :boolean,
save_optional: :boolean
]
)
{parsed, rest}
end
defp install_spec(spec, opts) do
{name, range} = parse_package_spec(spec)
add_opts =
Enum.filter(
[dev: opts[:save_dev], optional: opts[:save_optional], exact: opts[:save_exact]],
&elem(&1, 1)
)
NPM.add(name, range, add_opts)
end
defp handle_result(:ok), do: :ok
defp handle_result({:error, reason}), do: Mix.raise("npm.install failed: #{inspect(reason)}")
@doc false
def parse_package_spec(spec) do
case spec do
# @scope/pkg@range
"@" <> rest ->
case String.split(rest, "@", parts: 2) do
[scoped_name, range] -> {"@" <> scoped_name, range}
[scoped_name] -> {"@" <> scoped_name, "latest"}
end
# pkg@range
_ ->
case String.split(spec, "@", parts: 2) do
[name, range] -> {name, range}
[name] -> {name, "latest"}
end
end
end
end