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