Current section
Files
Jump to
Current section
Files
lib/hologram/compiler.ex
defmodule Hologram.Compiler do
@moduledoc false
alias Hologram.Commons.CryptographicUtils
alias Hologram.Commons.MapUtils
alias Hologram.Commons.PathUtils
alias Hologram.Commons.PLT
alias Hologram.Commons.SystemUtils
alias Hologram.Commons.TaskUtils
alias Hologram.Commons.Types, as: T
alias Hologram.Compiler.CallGraph
alias Hologram.Compiler.Context
alias Hologram.Compiler.Encoder
alias Hologram.Compiler.IR
alias Hologram.Reflection
@doc """
Aggregates JS imports from all Elixir modules referenced by the given MFAs.
Returns a map with:
- `:imports` — unique imports with generated `$1`, `$2`, ... aliases for JS import statements
- `:bindings` — per-module map of user alias to generated alias for `__bindings__` on module proxies
"""
@spec aggregate_js_imports(list(mfa)) :: %{
imports: list(%{from: String.t(), export: String.t(), alias: String.t()}),
bindings: %{module => %{String.t() => String.t()}}
}
def aggregate_js_imports(mfas) do
modules_with_imports =
mfas
|> filter_elixir_mfas()
|> Enum.map(fn {module, _function, _arity} -> module end)
|> Enum.uniq()
|> Enum.filter(
&(Reflection.has_function?(&1, :__js_imports__, 0) and &1.__js_imports__() != [])
)
unique_imports =
modules_with_imports
|> Enum.flat_map(fn module ->
Enum.map(module.__js_imports__(), fn %{from: from, export: export} ->
{from, export}
end)
end)
|> Enum.uniq()
|> Enum.sort()
alias_map =
unique_imports
|> Enum.with_index(1)
|> Map.new(fn {{from, export}, index} -> {{from, export}, "$#{index}"} end)
imports =
Enum.map(unique_imports, fn {from, export} ->
%{from: from, export: export, alias: alias_map[{from, export}]}
end)
bindings =
Map.new(modules_with_imports, fn module ->
module_bindings =
Map.new(module.__js_imports__(), fn %{as: as, from: from, export: export} ->
{as, alias_map[{from, export}]}
end)
{module, module_bindings}
end)
%{imports: imports, bindings: bindings}
end
@doc """
Builds the call graph of all modules in the project.
"""
@spec build_call_graph :: CallGraph.t()
def build_call_graph do
build_call_graph(build_ir_plt())
end
@doc """
Builds the call graph of all modules in the given IR PLT.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/build_call_graph_1/README.md
"""
@spec build_call_graph(PLT.t()) :: CallGraph.t()
def build_call_graph(ir_plt) do
call_graph = CallGraph.start()
ir_plt
|> PLT.get_all()
|> TaskUtils.async_many(fn {_module, ir} -> CallGraph.build(call_graph, ir) end)
|> Task.await_many(:infinity)
CallGraph.add_non_discoverable_edges(call_graph)
end
@doc """
Builds IR persistent lookup table (PLT) of all modules in the project.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/build_ir_plt_1/README.md
"""
@spec build_ir_plt(T.opts()) :: PLT.t()
# credo:disable-for-lines:26 Credo.Check.Refactor.Nesting
# The above Credo check is disabled because the function is optimised this way
def build_ir_plt(opts \\ []) do
ir_plt = PLT.start(opts)
modules = Reflection.list_elixir_modules()
# Processing modules in chunks of 2 improves performance by ~7%
# (determined experimentally)
chunk_size = 2
modules
|> Enum.chunk_every(chunk_size)
|> TaskUtils.async_many(fn module_chunk ->
Enum.each(module_chunk, fn module ->
beam_path = :code.which(module)
if beam_path != :non_existing do
ir = IR.for_module(module, beam_path)
PLT.put(ir_plt, module, ir)
end
end)
end)
|> Task.await_many(:infinity)
ir_plt
end
@doc """
Builds a persistent lookup table (PLT) containing the BEAM defs digests for all the modules in the project.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/build_module_digest_plt!_1/README.md
"""
@spec build_module_digest_plt!(T.opts()) :: PLT.t()
def build_module_digest_plt!(opts \\ []) do
module_digest_plt = PLT.start(opts)
Reflection.list_elixir_modules()
|> TaskUtils.async_many(&rebuild_module_digest_plt_entry!(&1, module_digest_plt))
|> Task.await_many(:infinity)
module_digest_plt
end
@doc """
Builds page digest PLT, where the keys represent page modules,
and the values are hex digests of their corresponding JavaScript bundles.
"""
@spec build_page_digest_plt(list(map), T.opts()) :: {PLT.t(), T.file_path()}
def build_page_digest_plt(bundle_info, opts) do
page_digest_plt_items =
bundle_info
|> Enum.reject(fn %{entry_name: entry_name} -> entry_name == "runtime" end)
|> Enum.reduce([], fn %{entry_name: page_module, digest: digest}, acc ->
[{page_module, digest} | acc]
end)
page_digest_plt = PLT.start(items: page_digest_plt_items, supervisor: opts[:supervisor])
page_digest_plt_dump_path =
Path.join([opts[:build_dir], Reflection.page_digest_plt_dump_file_name()])
{page_digest_plt, page_digest_plt_dump_path}
end
@doc """
Builds JavaScript code for the given Hologram page.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/build_page_js_4/README.md
"""
@spec build_page_js(module, CallGraph.t(), PLT.t(), MapSet.t(mfa), T.file_path()) :: String.t()
def build_page_js(page_module, call_graph, ir_plt, async_mfas, js_dir) do
mfas = CallGraph.list_page_mfas(call_graph, page_module)
%{imports: imports, bindings: bindings} = aggregate_js_imports(mfas)
import_statements =
imports
|> Enum.map_join("\n", fn %{from: from, export: export, alias: alias} ->
~s'import { #{export} as #{alias} } from "#{from}";'
end)
|> render_block()
js_bindings_registration_call =
bindings
|> render_js_bindings_registration_call()
|> render_block()
erlang_js_dir = Path.join(js_dir, "erlang")
erlang_function_defs =
mfas
|> render_erlang_function_defs(erlang_js_dir)
|> render_block()
elixir_function_defs =
mfas
|> render_elixir_function_defs(ir_plt, async_mfas)
|> render_block()
"""
"use strict";
import PerformanceTimer from "#{js_dir}/performance_timer.mjs";#{import_statements}
const startTime = performance.now();
globalThis.Hologram.pageReachableFunctionDefs = (deps) => {
const {
Bitstring,
ERTS,
HologramBoxedError,
HologramInterpreterError,
Interpreter,
MemoryStorage,
Type,
Utils,
} = deps;#{js_bindings_registration_call}#{erlang_function_defs}#{elixir_function_defs}
}
globalThis.Hologram.pageScriptLoaded = true;
document.dispatchEvent(new CustomEvent("hologram:pageScriptLoaded"));
console.debug("Hologram: page script executed in", PerformanceTimer.diff(startTime));\
"""
end
@doc """
Builds Hologram runtime JavaScript source code.
"""
@spec build_runtime_js(list(mfa), PLT.t(), MapSet.t(mfa), T.file_path()) :: String.t()
def build_runtime_js(runtime_mfas, ir_plt, async_mfas, js_dir) do
erlang_function_defs =
runtime_mfas
|> render_erlang_function_defs(Path.join(js_dir, "erlang"))
|> render_block()
elixir_function_defs =
runtime_mfas
|> render_elixir_function_defs(ir_plt, async_mfas)
|> render_block()
"""
"use strict";
import Bitstring from "#{js_dir}/bitstring.mjs";
import ERTS from "#{js_dir}/erts.mjs";
import Hologram from "#{js_dir}/hologram.mjs";
import HologramBoxedError from "#{js_dir}/errors/boxed_error.mjs";
import HologramInterpreterError from "#{js_dir}/errors/interpreter_error.mjs";
import Interpreter from "#{js_dir}/interpreter.mjs";
import MemoryStorage from "#{js_dir}/memory_storage.mjs";
import PerformanceTimer from "#{js_dir}/performance_timer.mjs";
import Type from "#{js_dir}/type.mjs";
import Utils from "#{js_dir}/utils.mjs";
const startTime = PerformanceTimer.start();#{erlang_function_defs}#{elixir_function_defs}
document.addEventListener("hologram:pageScriptLoaded", () => Hologram.run());
if (globalThis.Hologram.pageScriptLoaded) {
document.dispatchEvent(new CustomEvent("hologram:pageScriptLoaded"));
}
console.debug("Hologram: runtime script executed in", PerformanceTimer.diff(startTime));\
"""
end
@doc """
Bundles multiple entry files.
Includes the source maps of the output files.
The output files' and source maps' file names contain hex digest.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/bundle_2/README.md
"""
@spec bundle(list({term, T.file_path(), String.t()}), T.opts()) :: list(map)
def bundle(entry_files_info, opts) do
entry_files_info
|> TaskUtils.async_many(fn {entry_name, entry_file_path, bundle_name} ->
bundle(entry_name, entry_file_path, bundle_name, opts)
end)
|> Task.await_many(:infinity)
end
@doc """
Bundles the given entry file.
Includes the source map of the output file.
The output file and source map file names contain hex digest.
"""
@spec bundle(term, T.file_path(), String.t(), T.opts()) :: map
# sobelow_skip ["CI.System"]
def bundle(entry_name, entry_file_path, bundle_name, opts) do
output_bundle_path = Path.join(opts[:tmp_dir], "#{entry_name}.output.js")
esbuild_cmd = [
entry_file_path,
"--bundle",
"--log-level=warning",
"--minify",
"--outfile=#{output_bundle_path}",
"--sourcemap",
"--sources-content=true",
"--target=es2021"
]
project_node_modules_path = Path.join([Reflection.root_dir(), "assets", "node_modules"])
node_path =
Enum.join(
[opts[:node_modules_path], project_node_modules_path],
PathUtils.env_path_separator()
)
esbuild_opts = [
env: [{"NODE_PATH", node_path}],
parallelism: true
]
{_exit_msg, exit_status} =
SystemUtils.cmd_cross_platform(opts[:esbuild_bin_path], esbuild_cmd, esbuild_opts)
if exit_status != 0 do
raise RuntimeError,
message:
"esbuild bundler failed for entry file: #{entry_file_path} (probably there were JavaScript syntax errors)"
end
maybe_ensure_bundle_within_size_limit!(entry_name, output_bundle_path)
digest =
output_bundle_path
|> File.read!()
|> CryptographicUtils.digest(:md5, :hex)
static_bundle_path_with_digest = Path.join(opts[:static_dir], "#{bundle_name}-#{digest}.js")
output_source_map_path = output_bundle_path <> ".map"
static_source_map_path_with_digest = static_bundle_path_with_digest <> ".map"
File.rename!(output_bundle_path, static_bundle_path_with_digest)
File.rename!(output_source_map_path, static_source_map_path_with_digest)
js_with_replaced_source_map_url =
static_bundle_path_with_digest
|> File.read!()
|> String.replace(
"//# sourceMappingURL=#{entry_name}.output.js.map",
"//# sourceMappingURL=#{bundle_name}-#{digest}.js.map"
)
File.write!(static_bundle_path_with_digest, js_with_replaced_source_map_url)
%{
bundle_name: bundle_name,
digest: digest,
entry_name: entry_name,
static_bundle_path: static_bundle_path_with_digest,
static_source_map_path: static_source_map_path_with_digest
}
end
@doc """
Creates page bundle entry file.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/create_page_entry_files_4/README.md
"""
@spec create_page_entry_files(list(module), CallGraph.t(), PLT.t(), MapSet.t(mfa), T.opts()) ::
list({module, T.file_path()})
def create_page_entry_files(page_modules, call_graph, ir_plt, async_mfas, opts) do
page_modules
|> TaskUtils.async_many(fn page_module ->
entry_name = Reflection.module_name(page_module)
entry_file_path =
page_module
|> build_page_js(call_graph, ir_plt, async_mfas, opts[:js_dir])
|> create_entry_file(entry_name, opts[:tmp_dir])
{page_module, entry_file_path}
end)
|> Task.await_many(:infinity)
end
@doc """
Creates runtime bundle entry file.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/create_runtime_entry_file_3/README.md
"""
@spec create_runtime_entry_file(list(mfa), PLT.t(), MapSet.t(mfa), T.opts()) :: T.file_path()
def create_runtime_entry_file(runtime_mfas, ir_plt, async_mfas, opts) do
runtime_mfas
|> build_runtime_js(ir_plt, async_mfas, opts[:js_dir])
|> create_entry_file("runtime", opts[:tmp_dir])
end
@doc """
Compares two module digest PLTs and returns the added, removed, and edited modules lists.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/diff_module_digest_plts_2/README.md
"""
@spec diff_module_digest_plts(PLT.t(), PLT.t()) :: %{
added_modules: list(module),
removed_modules: list(module),
edited_modules: list(module)
}
def diff_module_digest_plts(old_plt, new_plt) do
old_digests = PLT.get_all(old_plt)
new_digests = PLT.get_all(new_plt)
diff = MapUtils.diff(old_digests, new_digests)
%{
added_modules: Enum.map(diff.added, fn {module, _digest} -> module end),
removed_modules: diff.removed,
edited_modules: Enum.map(diff.edited, fn {module, _digest} -> module end)
}
end
@doc """
Extracts JavaScript source code for the given ported Erlang function.
Returns the JavaScript function code if it exists in the corresponding .mjs file,
or nil if the file or function doesn't exist.
## Examples
iex> get_erlang_function_js(:erlang, :+, 2, "/path/to/erlang")
"(left, right) => { ... }"
iex> get_erlang_function_js(:maps, :get, 2, "/path/to/erlang")
"(key, map) => { ... }"
iex> get_erlang_function_js(:erlang, :not_implemented, 2, "/path/to/erlang")
nil
"""
@spec get_erlang_function_js(module, atom, non_neg_integer, T.file_path()) :: String.t() | nil
def get_erlang_function_js(module, function, arity, erlang_js_dir) do
file_path =
if module == :erlang do
"#{erlang_js_dir}/erlang.mjs"
else
"#{erlang_js_dir}/#{module}.mjs"
end
if File.exists?(file_path) do
extract_erlang_function_js(file_path, function, arity)
else
nil
end
end
@doc """
Groups the given MFAs by module.
"""
@spec group_mfas_by_module(list(mfa)) :: %{module => mfa}
def group_mfas_by_module(mfas) do
Enum.group_by(mfas, fn {module, _function, _arity} -> module end)
end
@doc """
Installs JavaScript deps which are specified in package.json located in assets_dir.
Saves the package.json digest to package_json_digest.bin file in build_dir.
"""
@spec install_js_deps(T.file_path(), T.file_path()) :: :ok
# sobelow_skip ["CI.System"]
def install_js_deps(assets_dir, build_dir) do
# Run from the project root, not from inside assets_dir, so version managers like
# asdf/mise resolve the Node.js version from the consuming project's config rather
# than any .tool-versions inside a git-checked-out dependency. npm still installs
# into assets_dir via --prefix.
opts = [into: IO.stream(:stdio, :line)]
{_result, exit_status} =
SystemUtils.cmd_cross_platform("npm", ["install", "--prefix", assets_dir], opts)
if exit_status != 0 do
raise RuntimeError, message: "npm install command failed"
end
package_json_digest = get_package_json_digest(assets_dir)
package_json_digest_path = Path.join(build_dir, "package_json_digest.bin")
File.write!(package_json_digest_path, package_json_digest)
end
@doc """
Installs JavaScript deps if package.json has changed or if the deps haven't been installed yet.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/maybe_install_js_deps_2/README.md
"""
@spec maybe_install_js_deps(T.file_path(), T.file_path()) :: :ok | nil
def maybe_install_js_deps(assets_dir, build_dir) do
package_json_digest_path = Path.join(build_dir, "package_json_digest.bin")
package_json_lock_path = Path.join(assets_dir, "package-lock.json")
if !File.exists?(package_json_digest_path) or !File.exists?(package_json_lock_path) do
install_js_deps(assets_dir, build_dir)
else
old_package_json_digest = File.read!(package_json_digest_path)
new_package_json_digest = get_package_json_digest(assets_dir)
if new_package_json_digest != old_package_json_digest do
install_js_deps(assets_dir, build_dir)
end
end
end
@doc """
Loads call graph from a dump file if the file exists or creates an empty call graph.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/maybe_load_call_graph_1/README.md
"""
@spec maybe_load_call_graph(T.file_path(), T.opts()) :: {CallGraph.t(), String.t()}
def maybe_load_call_graph(build_dir, opts \\ []) do
call_graph = CallGraph.start(opts)
call_graph_dump_path = Path.join(build_dir, Reflection.call_graph_dump_file_name())
CallGraph.maybe_load(call_graph, call_graph_dump_path)
{call_graph, call_graph_dump_path}
end
@doc """
Loads IR PLT from a dump file if the file exists or creates an empty PLT.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/maybe_load_ir_plt_1/README.md
"""
@spec maybe_load_ir_plt(T.file_path()) :: {PLT.t(), String.t()}
def maybe_load_ir_plt(build_dir) do
ir_plt = PLT.start()
ir_plt_dump_path = Path.join(build_dir, Reflection.ir_plt_dump_file_name())
PLT.maybe_load(ir_plt, ir_plt_dump_path)
{ir_plt, ir_plt_dump_path}
end
@doc """
Loads module digest PLT from a dump file if the file exists or creates an empty PLT.
Benchmarks: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/maybe_load_module_digest_plt_1/README.md
"""
@spec maybe_load_module_digest_plt(T.file_path(), T.opts()) :: {PLT.t(), String.t()}
def maybe_load_module_digest_plt(build_dir, opts \\ []) do
module_digest_plt = PLT.start(opts)
module_digest_plt_dump_path =
Path.join(build_dir, Reflection.module_digest_plt_dump_file_name())
PLT.maybe_load(module_digest_plt, module_digest_plt_dump_path)
{module_digest_plt, module_digest_plt_dump_path}
end
@doc """
Given a module digests diff, updates the IR persistent lookup table (PLT)
by deleting entries for modules that have been removed,
rebuilding the IR of modules that have been edited,
and adding the IR of new modules.
"""
@spec patch_ir_plt!(PLT.t(), map) :: PLT.t()
def patch_ir_plt!(ir_plt, module_digests_diff) do
delete_tasks =
TaskUtils.async_many(module_digests_diff.removed_modules, &PLT.delete(ir_plt, &1))
rebuild_tasks =
TaskUtils.async_many(
module_digests_diff.edited_modules ++ module_digests_diff.added_modules,
&rebuild_ir_plt_entry!(ir_plt, &1)
)
Task.await_many(delete_tasks, :infinity)
Task.await_many(rebuild_tasks, :infinity)
ir_plt
end
@doc """
Keeps only those IR expressions that are function definitions of the given reachable MFAs.
"""
@spec prune_module_def(IR.ModuleDefinition.t(), list(mfa)) :: IR.ModuleDefinition.t()
def prune_module_def(module_def_ir, reachable_mfas) do
module = module_def_ir.module.value
module_reachable_mfas =
reachable_mfas
|> Enum.filter(fn {reachable_module, _function, _arity} -> reachable_module == module end)
|> MapSet.new()
function_defs =
Enum.filter(module_def_ir.body.expressions, fn
%IR.FunctionDefinition{name: function, arity: arity} ->
MapSet.member?(module_reachable_mfas, {module, function, arity})
_fallback ->
false
end)
%IR.ModuleDefinition{
module: module_def_ir.module,
body: %IR.Block{expressions: function_defs}
}
end
@doc """
Raises a compilation error if any page module lacks a specified route or layout.
Benchmark: https://github.com/bartblast/hologram/blob/master/benchmarks/compiler/validate_page_modules_1/README.md
"""
@spec validate_page_modules(list(module)) :: :ok
def validate_page_modules(page_modules) do
Enum.each(page_modules, fn page_module ->
if !Reflection.has_function?(page_module, :__route__, 0) do
module_name = Reflection.module_name(page_module)
raise Hologram.CompileError,
message:
"page '#{module_name}' doesn't have a route specified (use the route/1 macro to fix it)"
end
if !Reflection.has_function?(page_module, :__layout_module__, 0) do
module_name = Reflection.module_name(page_module)
raise Hologram.CompileError,
message:
"page '#{module_name}' doesn't have a layout module specified (use the layout/1 macro to fix it)"
end
end)
end
defp create_entry_file(js, entry_name, tmp_dir) do
entry_file_path = Path.join(tmp_dir, "#{entry_name}.entry.js")
File.write!(entry_file_path, js)
entry_file_path
end
defp extract_erlang_function_js(file_path, function, arity) do
key = "#{function}/#{arity}"
start_marker = "// Start #{key}"
end_marker = "// End #{key}"
# Matches: start_marker, optional // comment lines, "key": <captured body>, end_marker
regex =
~r/#{Regex.escape(start_marker)}\s+(?:\/\/[^\n]*\s+)*"#{Regex.escape(key)}":\s+(.+),\s+#{Regex.escape(end_marker)}/s
file_contents = File.read!(file_path)
case Regex.run(regex, file_contents) do
[_full_capture, js] -> js
nil -> nil
end
end
defp filter_elixir_mfas(mfas) do
Enum.filter(mfas, fn {module, _function, _arity} -> Reflection.elixir_module?(module) end)
end
defp filter_erlang_mfas(mfas) do
Enum.filter(mfas, fn {module, _function, _arity} -> Reflection.erlang_module?(module) end)
end
defp get_package_json_digest(assets_dir) do
assets_dir
|> Path.join("package.json")
|> File.read!()
|> CryptographicUtils.digest(:sha256, :binary)
end
defp maybe_ensure_bundle_within_size_limit!(entry_name, bundle_path) do
max_bundle_size = Application.get_env(:hologram, :max_bundle_size)
if max_bundle_size do
bundle_size = File.stat!(bundle_path).size
if bundle_size > max_bundle_size do
raise RuntimeError,
message: """
Generated JavaScript bundle '#{entry_name}' is #{bundle_size} bytes, which exceeds the configured maximum of #{max_bundle_size} bytes.
This limit acts as an early warning system to surface abnormally large bundles before they reach your app (e.g., accidentally pulling in too many modules or dependencies).
You can change this limit by setting the [:hologram, :max_bundle_size] config value (in bytes). For example:
config :hologram, max_bundle_size: 2 * 1024 * 1024\
"""
end
end
end
defp rebuild_ir_plt_entry!(ir_plt, module) do
beam_path = :code.which(module)
PLT.put(ir_plt, module, IR.for_module(module, beam_path))
end
defp rebuild_module_digest_plt_entry!(module, module_digest_plt) do
beam_path = :code.which(module)
if beam_path != :non_existing do
digest =
beam_path
|> Reflection.beam_defs()
# Fast and deterministic for change detection
|> :erlang.phash2()
PLT.put(module_digest_plt, module, digest)
end
end
defp render_block(str) do
str = String.trim(str)
if str != "" do
"\n\n" <> str
else
""
end
end
defp render_elixir_function_defs(mfas, ir_plt, async_mfas) do
mfas
|> filter_elixir_mfas()
|> group_mfas_by_module()
|> Enum.sort()
|> TaskUtils.async_many(fn {module, module_mfas} ->
ir_plt
|> PLT.get!(module)
|> prune_module_def(module_mfas)
|> Encoder.encode_ir(%Context{module: module, async_mfas: async_mfas})
end)
|> Task.await_many(:infinity)
|> Enum.join("\n\n")
end
defp render_erlang_function_defs(mfas, erlang_js_dir) do
mfas
|> filter_erlang_mfas()
|> TaskUtils.async_many(fn {module, function, arity} ->
Encoder.encode_erlang_function(module, function, arity, erlang_js_dir)
end)
|> Task.await_many(:infinity)
|> Enum.join("\n\n")
end
defp render_js_bindings_registration_call(bindings) when bindings == %{}, do: ""
defp render_js_bindings_registration_call(bindings) do
modules_arg =
bindings
|> Enum.sort()
|> Enum.map_join(", ", fn {module, module_bindings} ->
module_name = Reflection.module_name(module)
entries =
module_bindings
|> Enum.sort()
|> Enum.map_join(", ", fn {as, alias} -> ~s'"#{as}": #{alias}' end)
~s'"#{module_name}": {#{entries}}'
end)
~s'Interpreter.registerJsBindings({#{modules_arg}});'
end
end