Skip to main content

lib/vize.ex

defmodule Vize do
  @moduledoc """
  Elixir bindings for the [Vize](https://vizejs.dev) Vue.js toolchain.

  Compile, lint, and analyze Vue Single File Components at native speed
  via Rust NIFs. Includes Vapor mode IR for BEAM-native SSR.

      iex> {:ok, result} = Vize.compile_sfc(\"""
      ...> <template><div>{{ msg }}</div></template>
      ...> <script setup>
      ...> const msg = 'hello'
      ...> </script>
      ...> \""")
      iex> result.code =~ "msg"
      true

  ## Vapor IR

  The `vapor_ir/1` function exposes Vue's Vapor mode intermediate
  representation as Elixir maps — enabling BEAM-native SSR without
  executing JavaScript:

      iex> {:ok, ir} = Vize.vapor_ir("<div>{{ msg }}</div>")
      iex> [template] = ir.templates
      iex> template =~ "<div>"
      true
  """

  @type macro_artifact :: %{
          required(:kind) => String.t(),
          required(:name) => String.t(),
          required(:source) => String.t(),
          required(:content) => String.t(),
          required(:start) => non_neg_integer(),
          required(:end) => non_neg_integer(),
          optional(:code) => String.t()
        }

  @type sfc_result :: %{
          code: String.t(),
          css: String.t() | nil,
          errors: [map()],
          warnings: [map()],
          template_hash: String.t() | nil,
          style_hash: String.t() | nil,
          script_hash: String.t() | nil,
          macro_artifacts: [macro_artifact()]
        }

  @type template_result :: %{
          code: String.t(),
          preamble: String.t(),
          helpers: [String.t()]
        }

  @type vapor_result :: Vize.Vapor.Result.t()

  @type ssr_result :: %{
          code: String.t(),
          preamble: String.t()
        }

  @type ir_result :: %{
          templates: [String.t()],
          components: [String.t()],
          directives: [String.t()],
          block: map(),
          element_template_map: [{non_neg_integer(), non_neg_integer()}]
        }

  @type diagnostic :: %{message: String.t(), name: String.t() | nil}

  # ── SFC Parsing ──

  @doc """
  Parse a Vue Single File Component into its constituent blocks.

  Returns the SFC descriptor with template, script, script_setup, styles,
  and custom_blocks — without compiling.

  ## Examples

      iex> {:ok, descriptor} = Vize.parse_sfc(\"""
      ...> <template><div>hello</div></template>
      ...> <script setup>const x = 1</script>
      ...> <style scoped>.red { color: red }</style>
      ...> \""")
      iex> descriptor.template.content =~ "hello"
      true
      iex> descriptor.script_setup.setup
      true
      iex> hd(descriptor.styles).scoped
      true
  """
  @spec parse_sfc(String.t()) :: {:ok, map()} | {:error, String.t()}
  def parse_sfc(source) do
    Vize.Native.parse_sfc_nif(source)
  end

  @doc """
  Like `parse_sfc/1` but raises on errors.
  """
  @spec parse_sfc!(String.t()) :: map()
  def parse_sfc!(source) do
    case parse_sfc(source) do
      {:ok, descriptor} -> descriptor
      {:error, reason} -> raise "Vize parse error: #{reason}"
    end
  end

  # ── SFC Analysis ──

  @doc """
  Analyze a Vue Single File Component into a semantic Croquis summary.

  ## Options

    * `:mode` — analysis mode: `:full`, `:lint`, `:compile`, or `:declaration`
  """
  @spec analyze_sfc(String.t(), keyword()) :: {:ok, Vize.Croquis.t()} | {:error, Vize.Error.t()}
  def analyze_sfc(source, opts \\ []) do
    mode = opts |> Keyword.get(:mode, :full) |> to_string()

    case Vize.Native.analyze_sfc_nif(source, mode) do
      {:ok, result} -> {:ok, Vize.Croquis.new(result)}
      {:error, reason} -> {:error, error("Vize SFC analysis error", [reason])}
    end
  end

  @doc "Like `analyze_sfc/2` but raises `Vize.Error` on errors."
  @spec analyze_sfc!(String.t(), keyword()) :: Vize.Croquis.t()
  def analyze_sfc!(source, opts \\ []) do
    case analyze_sfc(source, opts) do
      {:ok, croquis} -> croquis
      {:error, error} -> raise error
    end
  end

  # ── SFC Compilation ──

  @doc """
  Compile a Vue Single File Component to JavaScript + CSS.

  Handles `<template>`, `<script>`, `<script setup>`, and `<style>` blocks.

  ## Options

    * `:vapor` — compile in Vapor mode (default: `false`)
    * `:ssr` — compile for server-side rendering (default: `false`)
    * `:filename` — SFC filename for scope ID generation and source maps (e.g. `"App.vue"`)
    * `:scope_id` — explicit scope ID for scoped CSS (default: auto-generated from filename)
    * `:custom_renderer` — treat lowercase non-HTML tags as renderer-native
      elements instead of Vue components (default: `false`)
    * `:strip_types` — strip TypeScript type annotations from the output
      using OXC, returning plain JavaScript (default: `false`)

  ## Examples

      iex> {:ok, result} = Vize.compile_sfc(\"""
      ...> <template><button @click="count++">{{ count }}</button></template>
      ...> <script setup>
      ...> import { ref } from 'vue'
      ...> const count = ref(0)
      ...> </script>
      ...> \""")
      iex> result.code =~ "count"
      true
      iex> result.errors
      []
  """
  @spec compile_sfc(String.t(), keyword()) :: {:ok, sfc_result()} | {:error, String.t()}
  def compile_sfc(source, opts \\ []) do
    vapor = Keyword.get(opts, :vapor, false)
    ssr = Keyword.get(opts, :ssr, false)
    filename = Keyword.get(opts, :filename, "")
    scope_id = Keyword.get(opts, :scope_id, "")
    custom_renderer = Keyword.get(opts, :custom_renderer, false)
    strip_types = Keyword.get(opts, :strip_types, false)

    Vize.Native.compile_sfc_nif(
      source,
      filename,
      scope_id,
      vapor,
      ssr,
      custom_renderer,
      strip_types
    )
  end

  @doc """
  Like `compile_sfc/2` but raises on errors.
  """
  @spec compile_sfc!(String.t(), keyword()) :: sfc_result()
  def compile_sfc!(source, opts \\ []) do
    case compile_sfc(source, opts) do
      {:ok, result} -> result
      {:error, reason} -> raise "Vize compile error: #{reason}"
    end
  end

  # ── Template Compilation ──

  @doc """
  Compile a Vue template string to a render function.

  This compiles just the template (not a full SFC). Useful for
  on-the-fly template compilation.

  ## Options

    * `:mode` — output mode, `"function"` (default) or `"module"`
    * `:ssr` — compile for SSR (default: `false`)

  ## Examples

      iex> {:ok, result} = Vize.compile_template("<div>{{ msg }}</div>")
      iex> result.code =~ "msg"
      true
  """
  @spec compile_template(String.t(), keyword()) ::
          {:ok, template_result()} | {:error, [String.t()]}
  def compile_template(source, opts \\ []) do
    mode = opts |> Keyword.get(:mode, "function") |> to_string()
    ssr = Keyword.get(opts, :ssr, false)
    Vize.Native.compile_template_nif(source, mode, ssr)
  end

  @doc """
  Like `compile_template/2` but raises on errors.
  """
  @spec compile_template!(String.t(), keyword()) :: template_result()
  def compile_template!(source, opts \\ []) do
    case compile_template(source, opts) do
      {:ok, result} -> result
      {:error, errors} -> raise "Vize template compile error: #{inspect(errors)}"
    end
  end

  # ── SSR Compilation ──

  @doc """
  Compile a Vue template for server-side rendering.

  Generates JavaScript with `_push()` calls that produce HTML strings.
  The output is meant to be executed in a JS runtime (e.g. QuickBEAM).

  ## Examples

      iex> {:ok, result} = Vize.compile_ssr("<div>{{ msg }}</div>")
      iex> result.code =~ "_push"
      true
  """
  @spec compile_ssr(String.t()) :: {:ok, ssr_result()} | {:error, [String.t()]}
  def compile_ssr(source) do
    Vize.Native.compile_ssr_nif(source)
  end

  @doc """
  Like `compile_ssr/1` but raises on errors.
  """
  @spec compile_ssr!(String.t()) :: ssr_result()
  def compile_ssr!(source) do
    case compile_ssr(source) do
      {:ok, result} -> result
      {:error, errors} -> raise "Vize SSR compile error: #{inspect(errors)}"
    end
  end

  # ── Vapor Mode ──

  @doc """
  Compile a Vue template to Vapor mode JavaScript.

  Vapor mode generates fine-grained reactive code that manipulates
  the DOM directly, without a virtual DOM.

  ## Options

    * `:ssr` — compile for SSR (default: `false`)

  ## Examples

      iex> {:ok, result} = Vize.compile_vapor("<div>{{ msg }}</div>")
      iex> result.code =~ "template"
      true
      iex> length(result.templates) > 0
      true
  """
  @spec compile_vapor(String.t(), keyword()) :: {:ok, vapor_result()} | {:error, Vize.Error.t()}
  def compile_vapor(source, opts \\ []) do
    ssr = Keyword.get(opts, :ssr, false)
    diagnostics = Keyword.get(opts, :diagnostics, false)

    template_syntax =
      opts |> Keyword.get(:template_syntax, :standard) |> normalize_template_syntax()

    case Vize.Native.compile_vapor_nif(source, ssr, diagnostics, template_syntax) do
      {:ok, result} -> {:ok, Vize.Vapor.Result.new(result)}
      {:error, errors} -> {:error, error("Vize vapor compile error", errors)}
    end
  end

  @doc """
  Like `compile_vapor/2` but raises on errors.
  """
  @spec compile_vapor!(String.t(), keyword()) :: vapor_result()
  def compile_vapor!(source, opts \\ []) do
    case compile_vapor(source, opts) do
      {:ok, result} -> result
      {:error, error} -> raise error
    end
  end

  # ── Vapor IR ──

  @doc """
  Get the Vapor mode intermediate representation as Elixir maps.

  This is the key function for BEAM-native SSR. Instead of generating
  JavaScript, it returns the structured IR that describes how to render
  the template — enabling a pure Elixir renderer.

  The IR contains:

    * `:templates` — static HTML template strings
    * `:components` — component names used in the template
    * `:directives` — directive names used in the template
    * `:block` — the root block with operations, effects, and returns
    * `:element_template_map` — list of `{element_id, template_index}` tuples

  Expressions are either plain strings (dynamic) or `{:static, value}`
  tuples (compile-time constants).

  Each operation has a `:kind` field indicating its type:

    * `:set_prop` — set an attribute/property on an element
    * `:set_text` — set text content (interpolation)
    * `:set_event` — bind an event handler
    * `:set_html` — set innerHTML (v-html)
    * `:if_node` — v-if/v-else-if/v-else chain
    * `:for_node` — v-for loop
    * `:create_component` — child component
    * `:child_ref` / `:next_ref` — DOM traversal helpers

  ## Examples

      iex> {:ok, ir} = Vize.vapor_ir("<div :class=\\"cls\\">{{ msg }}</div>")
      iex> [_template] = ir.templates
      iex> ir.block.effects |> List.flatten() |> Enum.any?(&match?(%{kind: :set_text}, &1))
      true
  """
  @spec vapor_ir(String.t()) :: {:ok, ir_result()} | {:error, [String.t()]}
  def vapor_ir(source) do
    Vize.Native.vapor_ir_nif(source)
  end

  @doc """
  Like `vapor_ir/1` but raises on errors.
  """
  @spec vapor_ir!(String.t()) :: ir_result()
  def vapor_ir!(source) do
    case vapor_ir(source) do
      {:ok, ir} -> ir
      {:error, errors} -> raise "Vize vapor IR error: #{inspect(errors)}"
    end
  end

  @doc """
  Compile a Vue template into a statics/slots split ready for LiveView `%Rendered{}`.

  Returns `{:ok, split}` where `split` has:
  - `"statics"` — list of static HTML strings (interleaved between dynamic slots)
  - `"slots"` — ordered list of slot descriptors, each with `:kind` and values/sub-IR
  - `:templates` — raw template strings (for sub-block rendering)
  - `:element_template_map` — element ID → template index mapping

  The statics + slots can be directly assembled into a `%Phoenix.LiveView.Rendered{}`
  struct by evaluating each slot against assigns.
  """
  @spec vapor_split(String.t()) :: {:ok, map()} | {:error, [String.t()]}
  def vapor_split(source) do
    Vize.Native.vapor_split_nif(source)
  end

  @spec vapor_split!(String.t()) :: map()
  def vapor_split!(source) do
    case vapor_split(source) do
      {:ok, split} -> split
      {:error, errors} -> raise "Vize vapor split error: #{inspect(errors)}"
    end
  end

  # ── Linting ──

  @doc """
  Lint a Vue SFC source string.

  Returns a list of diagnostics with `:message` and optionally `:name`
  (the rule name).

  ## Examples

      iex> {:ok, diagnostics} = Vize.lint("<template><img></template>", "test.vue")
      iex> is_list(diagnostics)
      true
  """
  @spec lint(String.t(), String.t()) :: {:ok, [diagnostic()]}
  def lint(source, filename \\ "component.vue") do
    Vize.Native.lint_nif(source, filename)
  end

  # ── CSS Compilation ──

  @type css_result :: Vize.CSS.css_result()

  @deprecated "Use Vize.CSS.compile/2 instead"
  defdelegate compile_css(source, opts \\ []), to: Vize.CSS, as: :compile

  @deprecated "Use Vize.CSS.compile!/2 instead"
  defdelegate compile_css!(source, opts \\ []), to: Vize.CSS, as: :compile!

  @deprecated "Use Vize.CSS.bundle/2 instead"
  defdelegate bundle_css(entry_path, opts \\ []), to: Vize.CSS, as: :bundle

  @deprecated "Use Vize.CSS.bundle!/2 instead"
  defdelegate bundle_css!(entry_path, opts \\ []), to: Vize.CSS, as: :bundle!

  @deprecated "Use Vize.CSS.parse_ast/2 instead"
  defdelegate parse_css_ast(source, opts \\ []), to: Vize.CSS, as: :parse_ast

  @deprecated "Use Vize.CSS.parse_ast!/2 instead"
  defdelegate parse_css_ast!(source, opts \\ []), to: Vize.CSS, as: :parse_ast!

  @deprecated "Use Vize.CSS.print_ast/2 instead"
  defdelegate print_css_ast(ast, opts \\ []), to: Vize.CSS, as: :print_ast

  @deprecated "Use Vize.CSS.print_ast!/2 instead"
  defdelegate print_css_ast!(ast, opts \\ []), to: Vize.CSS, as: :print_ast!

  # ── Declaration .d.ts Generation ──

  @type dts_result :: %{dts: String.t()}

  @doc """
  Generate a TypeScript declaration file (`.d.ts`) from a Vue SFC.

  Analyzes the SFC's script blocks and produces a lightweight type
  surface for component consumers — including prop types, emit signatures,
  exposed bindings, and slot definitions.

  ## Options

    * `:filename` — SFC filename for diagnostics (default: `"component.vue"`)

  ## Examples

      iex> {:ok, result} = Vize.generate_dts("<script setup>const msg = 1</script>")
      iex> is_binary(result.dts)
      true
  """
  @spec generate_dts(String.t(), keyword()) :: {:ok, dts_result()} | {:error, String.t()}
  def generate_dts(source, opts \\ []) do
    filename = Keyword.get(opts, :filename, "")
    Vize.Native.generate_dts_nif(source, filename)
  end

  @doc "Like `generate_dts/2` but raises on errors."
  @spec generate_dts!(String.t(), keyword()) :: dts_result()
  def generate_dts!(source, opts \\ []) do
    case generate_dts(source, opts) do
      {:ok, result} -> result
      {:error, reason} -> raise "Vize declaration generation error: #{reason}"
    end
  end

  defp normalize_template_syntax(:standard), do: "standard"
  defp normalize_template_syntax(:quirks), do: "quirks"
  defp normalize_template_syntax("standard"), do: "standard"
  defp normalize_template_syntax("quirks"), do: "quirks"

  defp error(message, errors), do: Vize.Error.new(message, errors)
end