defmodule OXC.Format do @moduledoc """ Format JavaScript/TypeScript source code with oxfmt. Prettier-compatible formatter built on OXC, ~30× faster than Prettier. Supports JS, JSX, TS, and TSX. ## Examples {:ok, formatted} = OXC.Format.run("const x=1;let y = 2;", "test.js") # "const x = 1;\nlet y = 2;\n" {:ok, formatted} = OXC.Format.run("const x=1", "test.js", semi: false) # "const x = 1\n" """ @type sort_imports_opts :: %{ optional(:ignore_case) => boolean(), optional(:sort_side_effects) => boolean(), optional(:order) => :asc | :desc, optional(:newlines_between) => boolean(), optional(:partition_by_newline) => boolean(), optional(:partition_by_comment) => boolean(), optional(:internal_pattern) => [String.t()] } @type sort_tailwindcss_opts :: %{ optional(:config) => String.t(), optional(:stylesheet) => String.t(), optional(:functions) => [String.t()], optional(:attributes) => [String.t()], optional(:preserve_whitespace) => boolean(), optional(:preserve_duplicates) => boolean() } @type option :: {:print_width, pos_integer()} | {:tab_width, pos_integer()} | {:use_tabs, boolean()} | {:semi, boolean()} | {:single_quote, boolean()} | {:jsx_single_quote, boolean()} | {:trailing_comma, :all | :none} | {:bracket_spacing, boolean()} | {:bracket_same_line, boolean()} | {:arrow_parens, :always | :avoid} | {:end_of_line, :lf | :crlf | :cr} | {:quote_props, :as_needed | :consistent | :preserve} | {:single_attribute_per_line, boolean()} | {:object_wrap, :preserve | :collapse} | {:experimental_operator_position, :start | :end} | {:experimental_ternaries, boolean()} | {:embedded_language_formatting, :auto | :off} | {:sort_imports, boolean() | sort_imports_opts()} | {:sort_tailwindcss, boolean() | sort_tailwindcss_opts()} @doc """ Format source code. ## Options * `:print_width` — line width (default: 80) * `:tab_width` — spaces per indentation level (default: 2) * `:use_tabs` — indent with tabs instead of spaces (default: false) * `:semi` — print semicolons (default: true) * `:single_quote` — use single quotes (default: false) * `:jsx_single_quote` — use single quotes in JSX (default: false) * `:trailing_comma` — `:all` or `:none` (default: `:all`) * `:bracket_spacing` — spaces inside object braces (default: true) * `:bracket_same_line` — put `>` on the same line (default: false) * `:arrow_parens` — `:always` or `:avoid` (default: `:always`) * `:end_of_line` — `:lf`, `:crlf`, or `:cr` (default: `:lf`) * `:quote_props` — `:as_needed`, `:consistent`, or `:preserve` (default: `:as_needed`) * `:single_attribute_per_line` — force one attribute per line in JSX (default: false) * `:object_wrap` — `:preserve` or `:collapse` (default: `:preserve`) * `:experimental_operator_position` — `:start` or `:end` (default: `:end`) * `:experimental_ternaries` — use curious ternaries (default: false) * `:embedded_language_formatting` — `:auto` or `:off` (default: `:auto`) * `:sort_imports` — `true` for defaults, or a map with sub-options: * `:ignore_case` — case-insensitive sorting (default: true) * `:sort_side_effects` — sort side-effect imports (default: false) * `:order` — `:asc` or `:desc` (default: `:asc`) * `:newlines_between` — blank lines between groups (default: true) * `:partition_by_newline` — partition by existing newlines (default: false) * `:partition_by_comment` — partition by comments (default: false) * `:internal_pattern` — prefixes for internal imports (default: `["~/", "@/"]`) * `:sort_tailwindcss` — `true` for defaults, or a map with sub-options: * `:config` — path to Tailwind v3 config * `:stylesheet` — path to Tailwind v4 stylesheet * `:functions` — custom function names containing classes * `:attributes` — additional attributes to sort * `:preserve_whitespace` — preserve whitespace around classes (default: false) * `:preserve_duplicates` — preserve duplicate classes (default: false) ## Examples iex> {:ok, code} = OXC.Format.run("const x=1", "test.js") iex> code "const x = 1;\\n" iex> {:ok, code} = OXC.Format.run("const x=1", "test.js", semi: false) iex> code "const x = 1\\n" iex> {:ok, code} = OXC.Format.run("const x = {a: 1, b: 2}", "test.js", print_width: 20) iex> String.contains?(code, "\\n") true """ @spec run(iodata(), String.t(), [option()]) :: {:ok, String.t()} | {:error, [String.t()]} def run(source, filename, opts \\ []) do opts_map = opts |> Map.new() |> Map.new(fn {k, v} when is_atom(v) and k in [ :trailing_comma, :arrow_parens, :end_of_line, :object_wrap, :experimental_operator_position, :embedded_language_formatting ] -> {k, to_string(v)} {:quote_props, v} -> {:quote_props, String.replace(to_string(v), "_", "-")} {:sort_imports, %{} = m} -> {:sort_imports, Map.new(m, fn {:order, v} -> {:order, to_string(v)} pair -> pair end)} {:sort_tailwindcss, %{} = m} -> {:sort_tailwindcss, m} {k, v} -> {k, v} end) OXC.Format.Native.format(source, filename, opts_map) end @doc """ Like `run/3` but raises on errors. ## Examples iex> OXC.Format.run!("const x=1", "test.js") "const x = 1;\\n" """ @spec run!(iodata(), String.t(), [option()]) :: String.t() def run!(source, filename, opts \\ []) do case run(source, filename, opts) do {:ok, code} -> code {:error, errors} -> raise OXC.Error, message: "OXC format error: #{inspect(errors)}", errors: errors end end end