Skip to main content

lib/oxc/bundle.ex

defmodule OXC.Bundle do
  @moduledoc """
  Composable JavaScript bundling pipeline.

  Build immutable bundle configuration with pipeline-friendly functions, then
  execute the native Rolldown run once with `run/1`.
  """

  alias OXC.Bundle.{Entry, Result}

  defstruct entries: [],
            files: [],
            cwd: "",
            outdir: nil,
            format: :esm,
            exports: :auto,
            minify: false,
            treeshake: false,
            sourcemap: false,
            drop_console: false,
            banner: nil,
            footer: nil,
            preamble: nil,
            define: %{},
            external: [],
            conditions: [],
            main_fields: [],
            modules: [],
            module_types: %{},
            preserve_entry_signatures: nil,
            jsx: :automatic,
            jsx_factory: "",
            jsx_fragment: "",
            import_source: "",
            target: "",
            entry_file_names: nil,
            chunk_file_names: nil,
            asset_file_names: nil

  @type t :: %__MODULE__{}

  @spec new(keyword()) :: t()
  def new(opts \\ []) do
    struct!(__MODULE__)
    |> entries(Keyword.get(opts, :entries, []))
    |> apply_opts(Keyword.delete(opts, :entries))
  end

  @spec entry(
          t(),
          Entry.t()
          | map()
          | {String.t(), String.t()}
          | {String.t(), String.t(), iodata()}
          | String.t()
        ) :: t()
  def entry(%__MODULE__{} = bundle, entry), do: entries(bundle, bundle.entries ++ [entry])

  def file(%__MODULE__{} = bundle, {path, source}),
    do: files(bundle, bundle.files ++ [{path, source}])

  def files(%__MODULE__{} = bundle, files) when is_list(files), do: %{bundle | files: files}

  @spec entries(t(), [
          Entry.t()
          | map()
          | {String.t(), String.t()}
          | {String.t(), String.t(), iodata()}
          | String.t()
        ]) :: t()
  def entries(%__MODULE__{} = bundle, entries) when is_list(entries) do
    %{bundle | entries: Enum.map(entries, &Entry.new/1)}
  end

  def cwd(%__MODULE__{} = bundle, cwd), do: %{bundle | cwd: cwd || ""}
  def outdir(%__MODULE__{} = bundle, outdir), do: %{bundle | outdir: outdir}
  def format(%__MODULE__{} = bundle, format), do: %{bundle | format: format}

  def resolve(%__MODULE__{} = bundle, opts) do
    %{
      bundle
      | external: Keyword.get(opts, :external, bundle.external),
        conditions: Keyword.get(opts, :conditions, bundle.conditions),
        main_fields: Keyword.get(opts, :main_fields, bundle.main_fields),
        modules: Keyword.get(opts, :modules, bundle.modules)
    }
  end

  def transform(%__MODULE__{} = bundle, opts) do
    %{
      bundle
      | jsx: Keyword.get(opts, :jsx, bundle.jsx),
        jsx_factory: Keyword.get(opts, :jsx_factory, bundle.jsx_factory),
        jsx_fragment: Keyword.get(opts, :jsx_fragment, bundle.jsx_fragment),
        import_source: Keyword.get(opts, :import_source, bundle.import_source),
        target: Keyword.get(opts, :target, bundle.target),
        define: Keyword.get(opts, :define, bundle.define),
        module_types: Keyword.get(opts, :module_types, bundle.module_types)
    }
  end

  def output(%__MODULE__{} = bundle, opts) do
    %{
      bundle
      | entry_file_names: Keyword.get(opts, :entry_file_names, bundle.entry_file_names),
        chunk_file_names: Keyword.get(opts, :chunk_file_names, bundle.chunk_file_names),
        asset_file_names: Keyword.get(opts, :asset_file_names, bundle.asset_file_names),
        banner: Keyword.get(opts, :banner, bundle.banner),
        footer: Keyword.get(opts, :footer, bundle.footer),
        preamble: Keyword.get(opts, :preamble, bundle.preamble),
        sourcemap: Keyword.get(opts, :sourcemap, bundle.sourcemap),
        exports: Keyword.get(opts, :exports, bundle.exports),
        preserve_entry_signatures:
          Keyword.get(opts, :preserve_entry_signatures, bundle.preserve_entry_signatures)
    }
  end

  def minify(%__MODULE__{} = bundle, value \\ true), do: %{bundle | minify: value}
  def treeshake(%__MODULE__{} = bundle, value \\ true), do: %{bundle | treeshake: value}

  @spec run(t()) :: {:ok, Result.t()} | {:error, [map()]}
  def run(%__MODULE__{} = bundle) do
    case OXC.Native.bundle_run(to_native(bundle)) do
      {:ok, result} -> {:ok, Result.new(result)}
      {:error, errors} -> {:error, errors}
    end
  end

  defp apply_opts(bundle, opts) do
    Enum.reduce(opts, bundle, fn
      {:cwd, value}, acc -> cwd(acc, value)
      {:outdir, value}, acc -> outdir(acc, value)
      {:format, value}, acc -> format(acc, value)
      {:resolve, value}, acc -> resolve(acc, value)
      {:transform, value}, acc -> transform(acc, value)
      {:output, value}, acc -> output(acc, value)
      {:minify, value}, acc -> minify(acc, value)
      {:treeshake, value}, acc -> treeshake(acc, value)
      {key, value}, acc when is_map_key(acc, key) -> Map.put(acc, key, value)
      _other, acc -> acc
    end)
  end

  defp to_native(%__MODULE__{} = bundle) do
    %{
      entries: Enum.map(bundle.entries, &Entry.to_native/1),
      files: Enum.map(bundle.files, fn {path, source} -> %{path: path, source: source} end),
      cwd: bundle.cwd || "",
      outdir: bundle.outdir,
      format: bundle.format,
      exports: bundle.exports,
      minify: bundle.minify,
      treeshake: bundle.treeshake,
      sourcemap: bundle.sourcemap,
      drop_console: bundle.drop_console,
      banner: bundle.banner,
      footer: bundle.footer,
      preamble: bundle.preamble,
      define: bundle.define,
      external: bundle.external,
      conditions: bundle.conditions,
      main_fields: bundle.main_fields,
      modules: bundle.modules,
      module_types: bundle.module_types,
      preserve_entry_signatures: bundle.preserve_entry_signatures,
      jsx: bundle.jsx,
      jsx_factory: bundle.jsx_factory,
      jsx_fragment: bundle.jsx_fragment,
      import_source: bundle.import_source,
      target: bundle.target,
      entry_file_names: bundle.entry_file_names,
      chunk_file_names: bundle.chunk_file_names,
      asset_file_names: bundle.asset_file_names
    }
  end
end