defmodule Mix.Tasks.Npm.Run do
alias NPM.Package.JSON
@shortdoc "Run an npm script"
@moduledoc """
Run a script defined in `package.json`.
mix npm.run build
mix npm.run test
mix npm.run # List available scripts
Scripts are executed with `node_modules/.bin` prepended to `PATH`.
"""
use Mix.Task
@impl true
def run([]) do
Application.ensure_all_started(:req)
list_scripts()
end
def run([script_name | extra_args]) do
Application.ensure_all_started(:req)
run_script(script_name, extra_args)
end
defp list_scripts do
case JSON.read_scripts() do
{:ok, scripts} when scripts == %{} ->
Mix.shell().info("No scripts found in package.json.")
{:ok, scripts} ->
Mix.shell().info("Available scripts:")
Enum.each(Enum.sort(scripts), fn {name, command} ->
Mix.shell().info(" #{name}: #{command}")
end)
{:error, reason} ->
Mix.shell().error("Failed to read package.json: #{inspect(reason)}")
end
end
defp run_script(name, extra_args) do
case JSON.read_scripts() do
{:ok, scripts} ->
case Map.fetch(scripts, name) do
{:ok, command} ->
execute(command, extra_args)
:error ->
Mix.shell().error("Script \"#{name}\" not found in package.json.")
Mix.shell().info("Available: #{scripts |> Map.keys() |> Enum.join(", ")}")
end
{:error, reason} ->
Mix.shell().error("Failed to read package.json: #{inspect(reason)}")
end
end
defp execute(command, extra_args) do
full_command =
case extra_args do
[] -> command
args -> command <> " " <> Enum.join(args, " ")
end
bin_path = Path.join(File.cwd!(), "node_modules/.bin")
current_path = System.get_env("PATH", "")
env = [{"PATH", "#{bin_path}:#{current_path}"}]
port =
Port.open({:spawn, full_command}, [
:binary,
:exit_status,
:stderr_to_stdout,
env: Enum.map(env, fn {k, v} -> {~c"#{k}", ~c"#{v}"} end)
])
stream_port(port)
end
defp stream_port(port) do
receive do
{^port, {:data, data}} ->
IO.write(data)
stream_port(port)
{^port, {:exit_status, 0}} ->
:ok
{^port, {:exit_status, code}} ->
Mix.shell().error("Script exited with code #{code}")
exit({:shutdown, code})
end
end
end