lib/deno_ex.ex

defmodule DenoEx do
  @default_executable_path :deno_ex |> :code.priv_dir() |> Path.join("bin")
  @env_location_variable "DENO_LOCATION"

  alias DenoEx.Pipe

  @moduledoc """
  DenoEx is used to run javascript and typescript files in a safe environment by utilizing
  [Deno](https://deno.com/runtime).

  ## Basics

  ## Configuration

  Configuration of the deno installation directory can be set in a few ways. We can use an
  environment variable, application config, or pass it directly to the run command. The
  different configurations are there to facilitate different working situations. The
  priorities are `function options` > `application configuration` > `environment`.

  ### Function Option

       iex> DenoEx.run({:file, Path.join(~w[test support hello.ts])}, [], [deno_location: "#{@default_executable_path}"])
       {:ok, "Hello, world.#{"\\n"}"}

  ### Application Configuration

       import Config

       config :deno_ex,
         exectutable_location: Path.join(~w[path containing deno])

  ### ENV Variable

    `#{@env_location_variable}=path`
  """

  @executable_path Application.compile_env(
                     :deno_ex,
                     :exectutable_location,
                     @default_executable_path
                   )

  @typedoc """
  The path to the script that should be executed, or a tuple denoting
  what should be passed to the Deno executable over STDIN.
  """
  @type script() :: {:file, Path.t()} | {:stdin, IO.chardata()}

  @typedoc "The list of arguements to be passed to the script"
  @type script_arguments() :: [String.t()]

  @typedoc "The arguments for deno"
  @type options() :: Pipe.options()

  @doc """
  Uses `deno run` to run a Deno script.

  ## Options

    #{NimbleOptions.docs(Pipe.run_options_schema())}

    Please refer to [Deno Permissions](https://deno.com/manual@v1.33.1/basics/permissions) for more details.

  ## Examples

       iex> DenoEx.run({:file, Path.join(~w[test support hello.ts])})
       {:ok, "Hello, world.#{"\\n"}"}

       iex> DenoEx.run({:file, Path.join(~w[test support args_echo.ts])}, ~w[foo bar])
       {:ok, "foo bar#{"\\n"}"}

       iex> DenoEx.run({:stdin, "console.log(\\"Hello, world.\\")"})
       {:ok, "Hello, world.#{"\\n"}"}
  """
  @spec run(script(), script_arguments(), options(), timeout()) :: {:ok | :error, String.t()}
  def run(script, script_arguments \\ [], options \\ [], timeout \\ :timer.seconds(5)) do
    script
    |> Pipe.new(script_arguments, options)
    |> Pipe.run()
    |> Pipe.yield(timeout)
    |> then(fn
      {:ok, pipe} ->
        {:ok, pipe |> Pipe.output() |> Enum.join("")}

      {:error, pipe} ->
        {:error, pipe |> Pipe.output() |> Enum.join("")}

      {:timeout, pipe} ->
        {:timeout, pipe |> Pipe.output() |> Enum.join("")}
    end)
  end

  @doc """
  Vendors Deno dependencies into the given location
  """
  def vendor_dependencies(script_paths, vendor_location, lock_file_location, args) do
    deno_path = Path.join(DenoEx.executable_path(), "deno")

    System.cmd(
      deno_path,
      ~w[vendor #{Enum.join(script_paths, " ")} --output #{vendor_location} --lock=#{lock_file_location}] ++ args
    )
  end

  @doc """
  Locks the Deno dependencies
  """
  def lock_dependencies(script_paths, lock_file_location, _args) do
    deno_path = Path.join(DenoEx.executable_path(), "deno")

    Enum.each(script_paths, fn script_path ->
      System.cmd(deno_path, ~w[cache --lock=#{lock_file_location} #{script_path}])
    end)
  end

  @doc """
  Returns the location where the deno script is expected to be located.
  """
  @spec executable_path() :: String.t()
  def executable_path do
    System.get_env(@env_location_variable, @executable_path)
  end

  @doc """
  Returns the vendor location where deno script dependencies will be stored
  """
  @spec vendor_dir(atom()) :: String.t()
  def vendor_dir(app) do
    Path.join([:code.priv_dir(app), "deno"])
  end

  @doc """
  Returns the default import map path
  """
  @spec import_map_path(atom()) :: String.t()
  def import_map_path(app) do
    Path.join(vendor_dir(app), "import_map.json")
  end

  @doc """
  Returns the default lock file path
  """
  @spec lock_file_path(atom()) :: String.t()
  def lock_file_path(app) do
    Path.join([vendor_dir(app), "deno.lock"])
  end
end