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
Current section
Files
lib/phoenix/react_server/runtime/deno.ex
defmodule Phoenix.ReactServer.Runtime.Deno do
@moduledoc """
Deno runtime for Phoenix.ReactServer server.
This runtime uses Deno as the JavaScript runtime for rendering React components.
Deno provides secure defaults, TypeScript support, and excellent performance.
## Configuration
Configure in `runtime.exs`:
```elixir
import Config
config :phoenix_react_ng, Phoenix.ReactServer.Runtime.Deno,
cd: File.cwd!(),
cmd: System.find_executable("deno"),
# 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.deno.bundle`
server_js: Path.expand("deno/server.js", :code.priv_dir(:phoenix_react_ng)),
port: 5226,
env: :dev,
# Security: restrict write access to specific directories
write_dirs: ["/tmp", "/var/tmp"]
```
## Configuration Options
- `:cd` - Working directory for the Deno process (default: current directory)
- `:cmd` - Path to Deno executable (default: system `deno` command)
- `:server_js` - Path to the bundled server JavaScript file
- `:port` - Port for the Deno HTTP server (default: 5226)
- `:env` - Environment mode (`:dev` or `:prod`, default: `:dev`)
- `:write_dirs` - List of directories Deno can write to (security feature)
- `:parent_check_interval` - Interval to check parent process health (default: 5000ms)
## Security Features
Deno runtime provides enhanced security:
- Restricted file system access via `write_dirs`
- Automatic parent process monitoring
- Sandboxed execution environment
## Component Requirements
Components for Deno runtime must:
- Use `.jsx` file extension for proper JSX parsing
- Export a `Component` function
- Be compatible with Deno's module system
## 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.deno.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.Deno.Bundle, as: DenoBundle
alias Phoenix.ReactServer.Config
alias Phoenix.ReactServer.Runtime
alias Phoenix.ReactServer.Runtime.FileWatcher
alias Phoenix.ReactServer.Server
alias Phoenix.ReactServer.Telemetry
@doc """
Starts the Deno 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(
"Deno.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}"
)
Telemetry.record_runtime_startup("Deno", config()[:port])
Server.set_runtime_process(self())
{:noreply, %Runtime{state | runtime_port: port}}
end
@impl true
def config do
user_config = Application.get_env(:phoenix_react_ng, Phoenix.ReactServer.Runtime.Deno, [])
# 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("deno")))
|> Map.put(
:server_js,
Keyword.get(
user_config,
:server_js,
Path.expand("deno/server.js", :code.priv_dir(:phoenix_react_ng))
)
)
case Config.runtime_config(:deno, 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]
server_js = config[:server_js]
deno_port = Integer.to_string(config[:port])
is_dev = config[:env] == :dev
deno_env = if(is_dev, do: "development", else: "production")
# In development, use the source file with deno run
# In production, use the compiled binary directly
{exec_cmd, args} =
if is_dev do
# Use source file in development
source_js = Path.join(Path.dirname(server_js), "server_source.js")
# Security: restrict write access to specific directories
write_dirs = config[:write_dirs] || ["/tmp", "/var/tmp"]
write_args = Enum.flat_map(write_dirs, &["--allow-write=#{&1}"])
args =
[
"run",
"--allow-net",
"--allow-read",
"--allow-env"
] ++
write_args ++
[
"--watch",
"--node-modules-dir"
]
{cmd, args ++ [source_js]}
else
# In production, server_js is the compiled binary
{server_js, []}
end
env =
runtime_env("DENO_PORT", deno_port, "DENO_ENV", deno_env) ++
[{~c"PARENT_CHECK_INTERVAL", ~c"#{config[:parent_check_interval] || 5000}"}]
Port.open(
{:spawn_executable, exec_cmd},
port_options(exec_cmd, args, cd: config[:cd], env: env)
)
end
@impl true
def start_file_watcher(component_base) do
Logger.debug("Building server.js bundle")
config = config()
source_js = Path.join(Path.dirname(config[:server_js]), "server_source.js")
bundle_args = [
"--component-base",
component_base,
"--output",
source_js,
"--cd",
config[:cd]
]
DenoBundle.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
source_js = Path.join(Path.dirname(state.server_js), "server_source.js")
bundle_args = [
"--component-base",
state.component_base,
"--output",
source_js,
"--cd",
state.cd
]
handle_file_change(path, DenoBundle, 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("Deno#{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("Deno.Server exiting")
cleanup_runtime_process(state.runtime_port, reason)
{:stop, reason, state}
end
@impl true
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
Telemetry.measure(
"render_#{method}_#{component}",
[:phoenix, :react, :render],
fn ->
result = make_http_request(server_port, Atom.to_string(method), component, props, timeout)
# Record the result for telemetry
case result do
{:ok, _} -> Telemetry.record_render(component, method, 0, :ok)
{:error, _} -> Telemetry.record_render(component, method, 0, :error)
end
result
end
)
end
@impl true
@spec terminate(term(), Phoenix.ReactServer.Runtime.t()) ::
:normal | :shutdown | {:shutdown, term()}
def terminate(reason, state) do
Logger.debug("Deno.Server terminating")
Telemetry.record_runtime_shutdown("Deno", reason)
cleanup_runtime_process(state.runtime_port, reason)
end
end