Skip to content

Commit

Permalink
feat: Support build in roduction mode. (#5)
Browse files Browse the repository at this point in the history
## Run in release mode

Bundle components with server.js to one file.

```shell
mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js 
```

Config `runtime` to `Phoenix.React.Runtime.Bun` in `runtime.exs`

```elixir

config :phoenix_react_server, Phoenix.React.Runtime.Bun,
  cmd: System.find_executable("bun"),
  server_js: Path.expand("../priv/react/server.js", __DIR__),
  env: :prod
```

---------

Co-authored-by: Jonathan Gao <[email protected]>
  • Loading branch information
gsmlg and GSMLG-BOT authored Feb 11, 2025
1 parent 510273a commit 2115e64
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 427 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ See the [docs](https://hexdocs.pm/phoenix_react_server/) for more information.
Add deps in `mix.exs`

```elixir
{:phoenix_react_server, "~> 0.2"},
{:phoenix_react_server, "~> 0.3"},
```

## Configuration
Expand All @@ -40,8 +40,6 @@ Supported `runtime`

- [x] `Phoenix.React.Runtime.Bun`
- [ ] `Phoenix.React.Runtime.Deno`
- [ ] `Phoenix.React.Runtime.Node`
- [ ] `Phoenix.React.Runtime.Lambda`

Add Render Server in your application Supervisor tree.

Expand Down Expand Up @@ -100,25 +98,22 @@ Import in html helpers in `react_demo_web.ex`
end
```

## Use in otp release
## Run in release mode

Transcompile react component in release.
Bundle components with server.js to one file.

```shell
bun build --outdir=priv/react/component ./assets/component/**
mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js
```

Config `runtime` to `Phoenix.React.Runtime.Bun` in `runtime.exs`

```elixir
config :phoenix_react_server, Phoenix.React,
# Change `component_base` to `priv/react/component`
component_base: :code.priv(:react_demo, "react/component")

config :phoenix_react_server, Phoenix.React.Runtime.Bun,
# include `react-dom/server` and `jsx-runtime`
cd: "/path/to/dir/include/node_modules/and/bunfig.toml",
cmd: "/path/to/bun",
cmd: System.find_executable("bun"),
server_js: Path.expand("../priv/react/server.js", __DIR__),
port: 12666,
env: :prod
```

Expand All @@ -145,3 +140,8 @@ hydrateRoot(
);
</script>
```

# DEMO

Path `./react_demo`

89 changes: 89 additions & 0 deletions lib/phoenix/mix/build/bun.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
defmodule Mix.Tasks.Phx.React.Bun.Bundle do
@moduledoc """
Create server.js bundle for `bun` runtime,
bundle all components and render server in one file for otp release.
## Usage
```shell
mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js
```
"""
use Mix.Task

@shortdoc "Bundle components into server.js"
def run(args) do
{opts, _argv} =
OptionParser.parse!(args, strict: [component_base: :string, output: :string])

component_base = Keyword.get(opts, :component_base)
base_dir = Path.absname(component_base, File.cwd!()) |> Path.expand()

components =
if File.dir?(base_dir) do
find_files(base_dir)
else
throw("component_base dir is not exists: #{base_dir}")
end

output = Keyword.get(opts, :output)
IO.puts("Bundle component in directory [#{component_base}] into #{output}")

files =
components
|> Enum.map(fn abs_path ->
filename = Path.relative_to(abs_path, base_dir)
ext = Path.extname(filename)
basename = Path.basename(filename, ext)
{basename, abs_path}
end)

quoted = EEx.compile_file("#{__DIR__}/server.js.eex")
{result, _bindings} = Code.eval_quoted(quoted, files: files, base_dir: base_dir)
tmp_file = "#{File.cwd!()}/server.js"
File.write!(tmp_file, result)

{out, code} =
System.cmd("bun", ["build", "--target=bun", "--outdir=#{Path.dirname(output)}", tmp_file])

if code != 0 do
IO.puts(out)
throw("bun build failed(#{code})")
end

File.rm!(tmp_file)
rescue
error ->
IO.inspect(error)
catch
error ->
IO.inspect(error)
end

def find_files(dir) do
find_files(dir, [])
end

defp find_files(dir, acc) do
case File.ls(dir) do
{:ok, entries} ->
entries
|> Enum.reduce(acc, fn entry, acc ->
path = Path.join(dir, entry)

cond do
# Recurse into subdirectories
File.dir?(path) -> find_files(path, acc)
# Collect files
File.regular?(path) -> [path | acc]
true -> acc
end
end)

# Ignore errors (e.g., permission issues)
{:error, _} ->
acc
end
end
end
148 changes: 148 additions & 0 deletions lib/phoenix/mix/build/server.js.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { serve, readableStreamToJSON, readableStreamToText } from 'bun';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';

const __comMap = {};
<%= for {{name, file}, idx} <- Enum.with_index(files) do %>
import { Component as __component_<%= idx %> } from "<%= file %>";
__comMap["<%= name %>"] = __component_<%= idx %>;
<% end %>

const { COMPONENT_BASE, BUN_ENV } = process.env;

const isDev = BUN_ENV === 'development';

const server = serve({
development: isDev,
async fetch(req) {
try {
let bodyStream = req.body;
if (isDev) {
const [t1, t2] = bodyStream.tee();
const bodyText = await readableStreamToText(t2);
console.log('Request: ', req.method, req.url, bodyText);
bodyStream = t1;
}
const { url } = req;
const uri = new URL(url);
const { pathname } = uri;

if (pathname.startsWith('/stop')) {
setImmediate(() => server.stop());
return new Response('{"message":"ok"}', {
headers: {
"Content-Type": "application/json",
},
});
}

if (pathname.startsWith('/static_markup/')) {
const props = await readableStreamToJSON(bodyStream);
const fileName = pathname.replace(/^\/static_markup\//, '');
const Component = __comMap[fileName];
if (!Component) {
return new Response(`Not Found, component not found.`, {
status: 404,
headers: {
"Content-Type": "text/html",
},
});
}
const jsxNode = <Component {...props} />;
const html = renderToStaticMarkup(jsxNode);
return new Response(html, {
headers: {
"Content-Type": "text/html",
},
});
}

if (pathname.startsWith('/component/')) {
const props = await readableStreamToJSON(bodyStream);
const fileName = pathname.replace(/^\/component\//, '');
const Component = __comMap[fileName];
const jsxNode = <Component {...props} />;
const html = renderToString(jsxNode);
return new Response(html, {
headers: {
"Content-Type": "text/html",
},
});
}

return new Response(`Not Found, not matched request.`, {
status: 404,
headers: {
"Content-Type": "text/html",
},
});
} catch(error) {
throw error;
}
},
error(error) {
const html = `
<div role="alert" class="alert alert-error">
<div>
<div class="font-bold">${error}</div>
<pre style="white-space: pre-wrap;">${error.stack}</pre>
</div>
</div>
`;
return new Response(html, {
status: 500,
headers: {
"Content-Type": "text/html",
},
});
},
});

console.log(`Server started at http://localhost:${server.port}`);
console.log(`COMPONENT_BASE`, COMPONENT_BASE);
console.log(`BUN_ENV`, BUN_ENV);

const ppid = process.ppid;
setInterval(() => {
if (process.ppid !== ppid) {
console.log("Parent process exited. Shutting down server...");
server.stop();
process.exit(0);
}
}, 1000);

(async () => {
for await (const line of console) {
console.log(`stdin > ${line}`);
}
console.log('stdin closed');
await server.stop();
console.log("Cleanup done. Exiting.");
process.exit(0);
})();

const shutdown = async (signal) => {
console.log(`\nReceived ${signal}. Cleaning up...`);

await server.stop();

console.log("Cleanup done. Exiting.");
process.exit(0);
};

process.on('SIGINT', () => {
shutdown("SIGINT");
});

process.on('SIGTERM', () => {
shutdown("SIGTERM");
});

process.on("exit", () => {
console.log("Parent process exited. Shutting down server...");
shutdown("exit");
});

process.stdin.on("close", () => {
console.log("Parent process closed stdin. Exiting...");
shutdown("close");
});
18 changes: 7 additions & 11 deletions lib/phoenix/react.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ defmodule Phoenix.React do
- [x] `Phoenix.React.Runtime.Bun`
- [ ] `Phoenix.React.Runtime.Deno`
- [ ] `Phoenix.React.Runtime.Node`
- [ ] `Phoenix.React.Runtime.Lambda`
Add Render Server in your application Supervisor tree.
Expand Down Expand Up @@ -99,28 +97,26 @@ defmodule Phoenix.React do
end
```
## Use in otp release
## Run in release mode
Transcompile react component in release.
Bundle components with server.js to one file.
```shell
bun build --outdir=priv/react/component ./assets/component/**
mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js
```
Config `runtime` to `Phoenix.React.Runtime.Bun` in `runtime.exs`
```elixir
config :phoenix_react_server, Phoenix.React,
# Change `component_base` to `priv/react/component`
component_base: :code.priv(:react_demo, "react/component")
config :phoenix_react_server, Phoenix.React.Runtime.Bun,
# include `react-dom/server` and `jsx-runtime`
cd: "/path/to/dir/include/node_modules/and/bunfig.toml",
cmd: "/path/to/bun",
cmd: System.find_executable("bun"),
server_js: Path.expand("../priv/react/server.js", __DIR__),
port: 12666,
env: :prod
```
## Hydrate at client side
Hydrate react component at client side.
Expand Down
2 changes: 1 addition & 1 deletion 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.2.1"
@version "0.3.0"

def project do
[
Expand Down
17 changes: 16 additions & 1 deletion react_demo/config/prod.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Config

config :react_demo, ReactDemoWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: 4666],
debug_errors: true,
secret_key_base: "kmpnmk2HbLCXfguCqhwY4cT0ed5d7cBirQWJXq/MX8/68Y0VakTLJKjNH4dM2IEU",
check_origin: false

# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix assets.deploy` task,
Expand All @@ -8,5 +14,14 @@ import Config
config :react_demo, ReactDemoWeb.Endpoint,
cache_static_manifest: "priv/static/cache_manifest.json"


config :phoenix_react_server, Phoenix.React.Runtime.Bun,
cd: Path.expand("..", __DIR__),
cmd: System.find_executable("bun"),
server_js: Path.expand("../priv/react/server.js", __DIR__),
port: 5124,
env: :dev


# Do not print debug messages in production
config :logger, level: :info
config :logger, :console, format: "[$level] $message\n"
Loading

0 comments on commit 2115e64

Please sign in to comment.