generated from gsmlg/elixir-package-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support build in roduction mode. (#5)
## 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
Showing
14 changed files
with
384 additions
and
427 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.