Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/phoenix/react.ex
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ defmodule Phoenix.React do
def init(_init_arg) do
children = [
{Phoenix.React.Cache, []},
{Phoenix.React.Runtime, []},
{Phoenix.React.Server, []}
]

Expand Down
25 changes: 24 additions & 1 deletion lib/phoenix/react/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,32 @@ defmodule Phoenix.React.Runtime do

"""

defstruct [:component_base, :port, render_timeout: 300_000]
use DynamicSupervisor

def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end

def init(_init_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end

def start_runtime(runtime, args) do
spec = {runtime, args}
DynamicSupervisor.start_child(__MODULE__, spec)
end

def start_file_watcher(args) do
spec = {Phoenix.React.Runtime.FileWatcher, args}
DynamicSupervisor.start_child(__MODULE__, spec)
end

defstruct [:component_base, :port, :server_js, render_timeout: 300_000]

@type t :: %__MODULE__{
render_timeout: integer(),
component_base: path(),
server_js: path(),
port: port()
}

Expand All @@ -20,6 +41,8 @@ defmodule Phoenix.React.Runtime do

@callback start([{:component_base, path()}, {:render_timeout, integer()}]) :: port()

@callback start_file_watcher(path()) :: :ok

@callback config() :: list()

@callback render_to_string(component(), map(), GenServer.from(), t()) ::
Expand Down
56 changes: 52 additions & 4 deletions lib/phoenix/react/runtime/bun.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,36 @@ defmodule Phoenix.React.Runtime.Bun do
use Phoenix.React.Runtime

def start_link(init_arg) do
IO.inspect(init_arg)
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
end

@impl true
def init(component_base: component_base, render_timeout: render_timeout) do
{:ok, %Runtime{component_base: component_base, render_timeout: render_timeout},
{:continue, :start_port}}
{:ok,
%Runtime{
component_base: component_base,
render_timeout: render_timeout,
server_js: config()[:server_js]
}, {:continue, :start_port}}
end

@impl true
@spec handle_continue(:start_port, Phoenix.React.Runtime.t()) ::
{:noreply, Phoenix.React.Runtime.t()}
def handle_continue(:start_port, %Runtime{component_base: component_base} = state) do
if config()[:env] == :dev do
start_file_watcher(component_base)
end

port = start(component_base: component_base)

Logger.debug(
"Bun.Server started on port: #{inspect(port)} and OS pid: #{get_port_os_pid(port)}"
)

Phoenix.React.Server.set_runtime_process(self())

{:noreply, %Runtime{state | port: port}}
end

Expand All @@ -64,8 +75,12 @@ defmodule Phoenix.React.Runtime.Bun do
def start(component_base: component_base) do
cd = config()[:cd]
cmd = config()[:cmd]
args = ["--port", Integer.to_string(config()[:port]), config()[:server_js]]
bun_env = if(config()[:env] == :dev, do: "development", else: "production")
bun_port = Integer.to_string(config()[:port])
args = ["--port", bun_port, config()[:server_js]]

is_dev = config()[:env] == :dev

bun_env = if(is_dev, do: "development", else: "production")

args =
if config()[:env] == :dev do
Expand All @@ -75,6 +90,9 @@ defmodule Phoenix.React.Runtime.Bun do
end

env = [
{~c"no_proxy", ~c"10.*,127.*,192.168.*,172.16.0.0/12,localhost,127.0.0.1,::1"},
{~c"PORT", ~c"#{bun_port}"},
{~c"BUN_PORT", ~c"#{bun_port}"},
{~c"BUN_ENV", ~c"#{bun_env}"},
{~c"COMPONENT_BASE", ~c"#{component_base}"}
]
Expand All @@ -96,6 +114,36 @@ defmodule Phoenix.React.Runtime.Bun do
end

@impl true
def start_file_watcher(component_base) do
Logger.debug("Building server.js bundle")

Mix.Task.run("phx.react.bun.bundle", [
"--component-base",
component_base,
"--output",
config()[:server_js]
])

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
Logger.debug("component_base changed: #{path}")

Task.async(fn ->
Mix.Task.run("phx.react.bun.bundle", [
"--component-base",
state.component_base,
"--output",
config()[:server_js]
])
end)

{:noreply, state}
end

def handle_info({_port, {:data, msg}}, state) do
Logger.debug(msg)
{:noreply, state}
Expand Down
44 changes: 44 additions & 0 deletions lib/phoenix/react/runtime/file_watcher.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule Phoenix.React.Runtime.FileWatcher do
use GenServer

def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end

def init(args) do
path = Keyword.fetch!(args, :path)
{:ok, watcher_pid} = FileSystem.start_link(dirs: [path])
FileSystem.subscribe(watcher_pid)
IO.puts("Watching #{path} for changes...")

{:ok,
args
|> Keyword.put(:watcher_pid, watcher_pid)
|> Keyword.put(:update_time, System.os_time(:second))}
end

def handle_info({:file_event, _watcher_pid, {path, [:modified, :closed]}}, state) do
IO.puts("File changed: #{path} - Events: [:modified, :closed]")
send(self(), {:throttle_update, path})
{:noreply, state}
end

def handle_info({:file_event, _watcher_pid, {_path, _events}}, state) do
# ref = Keyword.fetch!(state, :ref)
# IO.puts("File changed: #{path} - Events: #{inspect(events)}")
# send(self(), {:throttle_update, path, events})
{:noreply, state}
end

def handle_info({:throttle_update, path}, state) do
update_time = Keyword.fetch!(state, :update_time)
now = System.os_time(:second)

if now > update_time do
Process.send_after(state[:ref], {:component_base_changed, path}, 3_000)
{:noreply, state |> Keyword.put(:update_time, now + 3)}
else
{:noreply, state}
end
end
end
24 changes: 18 additions & 6 deletions lib/phoenix/react/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ defmodule Phoenix.React.Server do
require Logger

alias Phoenix.React.Cache
alias Phoenix.React.Runtime

use GenServer

@type second() :: integer()
@type millisecond() :: integer()

def set_runtime_process(pid) do
GenServer.cast(__MODULE__, {:set_runtime_process, pid})
end

@doc """
Return the configuration of the React Render Server from `Application.get_env(:phoenix_react_server, Phoenix.React)`
"""
Expand Down Expand Up @@ -46,14 +51,21 @@ defmodule Phoenix.React.Server do
runtime = cfg[:runtime]
component_base = cfg[:component_base]
render_timeout = cfg[:render_timeout]
args = [component_base: component_base, render_timeout: render_timeout]

{:ok, runtiem_process} =
GenServer.start_link(runtime,
component_base: component_base,
render_timeout: render_timeout
)
Runtime.start_runtime(runtime, args)

{:ok, %{runtiem_process: runtiem_process}}
{:ok,
%{
runtime: runtime,
component_base: component_base,
render_timeout: render_timeout
}}
end

@impl true
def handle_cast({:set_runtime_process, pid}, state) do
{:noreply, Map.put(state, :runtiem_process, pid)}
end

@impl true
Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Phoenix.React.Mixfile do
use Mix.Project

@source_url "https://github.com/gsmlg-dev/phoenix-react.git"
@version "0.3.0"
@version "0.4.0"

def project do
[
Expand Down Expand Up @@ -47,7 +47,8 @@ defmodule Phoenix.React.Mixfile do
{:httpoison, "~> 2.0"},
{:phoenix_html, "~> 4.1"},
{:phoenix_live_view, "~> 1.0"},
{:ex_doc, ">= 0.0.0", only: :prod, runtime: false}
{:file_system, "~> 1.0"},
{:ex_doc, ">= 0.0.0", only: :doc, runtime: false}
]
end

Expand Down
29 changes: 29 additions & 0 deletions react_demo/assets/component/live_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import MDEditor from '@uiw/react-md-editor';
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

export function Component({ data = "", setData }) {
const { content } = data;

return (
<div className="container">
<h1>Live Form</h1>
<form>
<MDEditor
value={content}
onChange={(value) => setData({ content: value })}
/>
<br />
<br />
<MDEditor.Markdown
source={content}
style={{
whiteSpace: 'pre-wrap',
padding: '1rem',
}}
/>
</form>
</div>
);
}
3 changes: 3 additions & 0 deletions react_demo/assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

@import "./github-markdown.css";

@import "@uiw/react-md-editor/markdown-editor.css";
@import "@uiw/react-markdown-preview/markdown.css";

@layer components {
.table-primary-border {
th, td {
Expand Down
5 changes: 4 additions & 1 deletion react_demo/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"

import { hooks } from './hooks';

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
params: {_csrf_token: csrfToken},
hooks,
})

// connect if there are any LiveViews on the page
Expand Down
Loading