Packages

Phoenix.ReactServer is use for renders React component as Phoenix Component in heex template. Support render_to_string and render_to_static_markup and cache render result. Only render to string support hyrate react component with phx-hook.

Current section

Files

Jump to
phoenix_react_ng lib phoenix react_server runtime bun.ex
Raw

lib/phoenix/react_server/runtime/bun.ex

defmodule Phoenix.ReactServer.Runtime.Bun do
@moduledoc """
Bun runtime for Phoenix.ReactServer server.
This runtime uses Bun as the JavaScript runtime for rendering React components.
Bun provides fast startup times and excellent performance for server-side rendering.
## Configuration
Configure in `runtime.exs`:
```elixir
import Config
config :phoenix_react_ng, Phoenix.ReactServer.Runtime.Bun,
cd: File.cwd!(),
cmd: System.find_executable("bun"),
# In dev mode, the server_js will be watched and recompiled when changed
# In prod mode, this needs to be precompiled with `mix phx.react.bun.bundle`
server_js: Path.expand("bun/server.js", :code.priv_dir(:phoenix_react_ng)),
port: 5225,
env: :dev
```
## Configuration Options
- `:cd` - Working directory for the Bun process (default: current directory)
- `:cmd` - Path to Bun executable (default: system `bun` command)
- `:server_js` - Path to the bundled server JavaScript file
- `:port` - Port for the Bun HTTP server (default: 5225)
- `:env` - Environment mode (`:dev` or `:prod`, default: `:dev`)
## Development Mode
In development mode (`env: :dev`), the runtime will:
- Start a file watcher for component changes
- Automatically rebuild the server bundle when components change
- Enable hot reloading for React components
## Production Mode
In production mode (`env: :prod`), you must pre-bundle the components:
```bash
mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js
```
"""
require Logger
use Phoenix.ReactServer.Runtime
import Phoenix.ReactServer.Runtime.Common
alias Mix.Tasks.Phx.React.Bun.Bundle, as: BunBundle
alias Phoenix.ReactServer.Config
alias Phoenix.ReactServer.Runtime
alias Phoenix.ReactServer.Runtime.FileWatcher
alias Phoenix.ReactServer.Server
@doc """
Starts the Bun runtime server.
## Parameters
- `init_arg` - Initialization arguments (typically `[]`)
## Returns
- `{:ok, pid}` - Runtime started successfully
- `{:error, reason}` - Failed to start runtime
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(component_base: component_base, render_timeout: render_timeout) do
{:ok,
%Phoenix.ReactServer.Runtime{
component_base: component_base,
render_timeout: render_timeout,
server_js: config()[:server_js],
cd: config()[:cd]
}, {:continue, :start_port}}
end
@impl true
@spec handle_continue(:start_port, Phoenix.ReactServer.Runtime.t()) ::
{:noreply, Phoenix.ReactServer.Runtime.t()}
def handle_continue(
:start_port,
%Phoenix.ReactServer.Runtime{component_base: component_base} = state
) do
if config()[:env] == :dev do
start_file_watcher(component_base)
FileWatcher.set_ref(self())
end
port = start(component_base: component_base)
Logger.debug(
"Bun.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}"
)
Server.set_runtime_process(self())
{:noreply, %Phoenix.ReactServer.Runtime{state | runtime_port: port}}
end
@impl true
@spec config() :: keyword()
def config do
user_config = Application.get_env(:phoenix_react_ng, Phoenix.ReactServer.Runtime.Bun, [])
# Convert user config to map for new config system
user_config_map =
user_config
|> Enum.into(%{})
|> Map.put(:cd, Keyword.get(user_config, :cd, File.cwd!()))
|> Map.put(:cmd, Keyword.get(user_config, :cmd, System.find_executable("bun")))
|> Map.put(
:server_js,
Keyword.get(
user_config,
:server_js,
Path.expand("bun/server.js", :code.priv_dir(:phoenix_react_ng))
)
)
case Config.runtime_config(:bun, user_config_map) do
{:ok, config} -> Config.to_keyword_list(config)
{:error, reason} -> raise ArgumentError, reason
end
end
@impl true
@spec start(Phoenix.ReactServer.Runtime.start_args()) :: port()
def start(component_base: _component_base) do
config = config()
cmd = config[:cmd]
bun_port = Integer.to_string(config[:port])
server_js = config[:server_js]
args = ["--port", bun_port, server_js]
args =
if config[:env] == :dev do
["--watch" | args]
else
args
end
bun_env = if(config[:env] == :dev, do: "development", else: "production")
env = runtime_env("BUN_PORT", bun_port, "BUN_ENV", bun_env)
Port.open({:spawn_executable, cmd}, port_options(cmd, args, cd: config[:cd], env: env))
end
@impl true
@spec start_file_watcher(Path.t()) :: {:ok, pid()} | {:error, term()}
def start_file_watcher(component_base) do
Logger.debug("Building server.js bundle")
config = config()
# Use development mode for development builds, production for production builds
dev_flag = if config[:env] == :dev, do: "--development", else: ""
bundle_args = [
"--component-base",
component_base,
"--output",
config[:server_js],
"--cd",
config[:cd]
]
# Add development flag if in development mode
bundle_args =
if dev_flag != "" do
bundle_args ++ [dev_flag]
else
bundle_args
end
BunBundle.run(bundle_args)
Logger.debug("Starting file watcher")
Runtime.start_file_watcher(ref: self(), path: component_base)
end
@impl true
def handle_info({:component_base_changed, path}, state) do
bundle_args = [
"--component-base",
state.component_base,
"--output",
state.server_js,
"--cd",
state.cd
]
handle_file_change(path, Mix.Tasks.Phx.React.Bun.Bundle, bundle_args)
{:noreply, state}
end
@impl true
def handle_info({_ref, :ok}, state) do
{:noreply, state}
end
@impl true
def handle_info({_port, {:data, msg}}, state) do
Logger.debug(msg)
{:noreply, state}
end
@impl true
def handle_info({port, {:exit_status, exit_status}}, state) do
Logger.warning("Bun#{inspect(port)}: exit_status: #{exit_status}")
Process.exit(self(), :normal)
{:noreply, state}
end
@impl true
def handle_info({:EXIT, _from, reason}, state) do
Logger.debug("Bun.Server exiting")
cleanup_runtime_process(state.runtime_port, reason)
{:stop, reason, state}
end
@impl true
def handle_cast(:shutdown, state) do
{:stop, :normal, state}
end
@impl true
@spec get_rendered_component(
Phoenix.ReactServer.Runtime.method(),
String.t(),
map(),
Phoenix.ReactServer.Runtime.t()
) :: Phoenix.ReactServer.Runtime.render_response()
def get_rendered_component(method, component, props, state)
when method in [:render_to_readable_stream, :render_to_string, :render_to_static_markup] do
server_port = config()[:port]
timeout = state.render_timeout
make_http_request(server_port, Atom.to_string(method), component, props, timeout)
end
@impl true
@spec terminate(term(), Phoenix.ReactServer.Runtime.t()) ::
:normal | :shutdown | {:shutdown, term()}
def terminate(reason, state) do
Logger.debug("Bun.Server terminating")
cleanup_runtime_process(state.runtime_port, reason)
end
end