From 7ce127688f4ac9ee912fed7d0fc56dfed3286516 Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Fri, 20 Sep 2024 12:37:54 +0200 Subject: [PATCH 1/6] First pass of refacto and setting up file hierarchy --- lib/app.ex | 26 ++++++++ lib/esbuild.ex | 18 ++++++ lib/pool.ex | 41 ++++++++++++ lib/reaxt.ex | 110 -------------------------------- lib/reaxt_error.ex | 28 ++++++++ lib/render.ex | 54 ++++++++++++++++ lib/tasks.esbuild.ex | 61 ++++++++++++++++++ lib/tasks.ex | 148 ------------------------------------------- lib/tasks.reaxt.ex | 72 +++++++++++++++++++++ lib/tasks.webpack.ex | 75 ++++++++++++++++++++++ lib/utils.ex | 14 ++++ lib/webpack.ex | 25 ++------ 12 files changed, 396 insertions(+), 276 deletions(-) create mode 100644 lib/app.ex create mode 100644 lib/esbuild.ex create mode 100644 lib/pool.ex delete mode 100644 lib/reaxt.ex create mode 100644 lib/reaxt_error.ex create mode 100644 lib/render.ex create mode 100644 lib/tasks.esbuild.ex delete mode 100644 lib/tasks.ex create mode 100644 lib/tasks.reaxt.ex create mode 100644 lib/tasks.webpack.ex create mode 100644 lib/utils.ex diff --git a/lib/app.ex b/lib/app.ex new file mode 100644 index 0000000..4db50d2 --- /dev/null +++ b/lib/app.ex @@ -0,0 +1,26 @@ +defmodule Reaxt.App do + use Application + + def start(_,_) do + hot_reload_processes = [ + # WebPack.Events, + # WebPack.EventManager, + # WebPack.Compiler, + ] + + base_processes = [ + Reaxt.PoolsSup + ] + + children = if Application.get_env(:reaxt, :hot, false) do + Enum.concat(base_processes, hot_reload_processes) + else + base_processes + end + + result = Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one) + # WebPack.Util.build_stats() + + result + end +end diff --git a/lib/esbuild.ex b/lib/esbuild.ex new file mode 100644 index 0000000..b5ee707 --- /dev/null +++ b/lib/esbuild.ex @@ -0,0 +1,18 @@ +defmodule Reaxt.Esbuild do + + def esbuild_config do + Application.get_env(:reaxt, :webpack_config, "build.js") + end + +end + +defmodule Reaxt.Esbuild.Compiler do + def child_spec(arg) do + %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } + end + def start_link(_) do + cmd = "node ./node_modules/reaxt/webpack_server #{WebPack.Util.webpack_config}" + hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" + Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) + end +end diff --git a/lib/pool.ex b/lib/pool.ex new file mode 100644 index 0000000..1133c17 --- /dev/null +++ b/lib/pool.ex @@ -0,0 +1,41 @@ +defmodule Reaxt.PoolsSup do + alias :poolboy, as: Pool + + use Supervisor + require Logger + + def transaction(pool_name, fct) do + Pool.transaction(pool_name, fct) + end + + def start_link(arg) do + Supervisor.start_link(__MODULE__,arg, name: __MODULE__) + end + + def init(_) do + pool_size = Application.get_env(:reaxt, :pool_size, 1) + pool_overflow = Application.get_env(:reaxt, :pool_max_overflow, 5) + server_dir = "#{Reaxt.Utils.web_priv}/#{Application.get_env(:reaxt, :server_dir)}" + server_files = Path.wildcard("#{server_dir}/*.js") + + if server_files == [] do + Logger.error("#server JS not yet compiled in #{server_dir}, compile it before with `mix webpack.compile`") + throw {:error, :serverjs_not_compiled} + else + children = Enum.map(server_files, fn server -> + parsed_js_name = server |> Path.basename(".js") |> String.replace(~r/[0-9][a-z][A-Z]/,"_") + pool_name = :"react_#{parsed_js_name}_pool" + + args = [ + worker_module: Reaxt.Render, + size: pool_size, + max_overflow: pool_overflow, + name: {:local, pool_name} + ] + Pool.child_spec(pool_name, args, server) + end) + + Supervisor.init(children, strategy: :one_for_one) + end + end +end diff --git a/lib/reaxt.ex b/lib/reaxt.ex deleted file mode 100644 index 02e036a..0000000 --- a/lib/reaxt.ex +++ /dev/null @@ -1,110 +0,0 @@ -defmodule ReaxtError do - defexception [:message,:args,:js_render,:js_stack] - def exception({:handler_error,module,submodule,args,error,stack}) do - params=%{ - module: module, - submodule: submodule, - args: args - } - %ReaxtError{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} - end - def exception({:render_error,params,error,stack,js_render}) do - %ReaxtError{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} - end - defp parse_stack(stack) do - Regex.scan(~r/at (.*) \((.*):([0-9]*):[0-9]*\)/,stack) - |> Enum.map(fn [_,function,url,line]-> - if String.contains?(url,"/priv") and !(function in ["Port.next_term","Socket.read_term"]) do - {line,_} = Integer.parse(line) - [_,after_priv] = String.split(url,"/priv/",parts: 2) - {JS,:"#{function}",0,file: '#{WebPack.Util.web_priv}/#{after_priv}', line: line} - end - end) - |> Enum.filter(&!is_nil(&1)) - end -end -defmodule Reaxt do - alias :poolboy, as: Pool - require Logger - - def render_result(chunk,module,data,timeout) when not is_tuple(module), do: - render_result(chunk,{module,nil},data,timeout) - def render_result(chunk,{module,submodule},data,timeout) do - Pool.transaction(:"react_#{chunk}_pool",fn worker-> - GenServer.call(worker,{:render,module,submodule,data,timeout},timeout+100) - end) - end - - def render!(module,data,timeout \\ 5_000, chunk \\ :server) do - case render_result(chunk,module,data,timeout) do - {:ok,res}->res - {:error,err}-> - try do raise(ReaxtError,err) - rescue ex-> - [_|stack] = __STACKTRACE__ - reraise ex, ((ex.js_stack || []) ++ stack) - end - end - end - - def render(module,data, timeout \\ 5_000) do - try do - render!(module,data,timeout) - rescue - ex-> - case ex do - %{js_render: js_render} when is_binary(js_render)-> - Logger.error(Exception.message(ex)) - %{css: "",html: "", js_render: js_render} - _ -> - reraise ex, __STACKTRACE__ - end - end - end - - def reload do - WebPack.Util.build_stats - Supervisor.terminate_child(Reaxt.App, Reaxt.App.PoolsSup) - Supervisor.restart_child(Reaxt.App, Reaxt.App.PoolsSup) - end - - def start_link(server_path) do - init = Poison.encode!(Application.get_env(:reaxt,:global_config,nil)) - Exos.Proc.start_link("node #{server_path}",init,[cd: '#{WebPack.Util.web_priv}']) - end - - defmodule App do - use Application - def start(_,_) do - result = Supervisor.start_link( - [App.PoolsSup] ++ List.wrap(if Application.get_env(:reaxt,:hot) do [ - WebPack.Events, - WebPack.EventManager, - WebPack.Compiler, - #{WebPack.StartBlocker,:infinity} # choice : wait for build or "mix webpack.compile" before launch - ] end), name: __MODULE__, strategy: :one_for_one) - WebPack.Util.build_stats - result - end - defmodule PoolsSup do - use Supervisor - def start_link(arg) do Supervisor.start_link(__MODULE__,arg, name: __MODULE__) end - def init(_) do - pool_size = Application.get_env(:reaxt,:pool_size) - pool_overflow = Application.get_env(:reaxt,:pool_max_overflow) - server_dir = "#{WebPack.Util.web_priv}/#{Application.get_env(:reaxt,:server_dir)}" - server_files = Path.wildcard("#{server_dir}/*.js") - if server_files == [] do - Logger.error("#server JS not yet compiled in #{server_dir}, compile it before with `mix webpack.compile`") - throw {:error,:serverjs_not_compiled} - else - Supervisor.init( - for server<-server_files do - pool = :"react_#{server |> Path.basename(".js") |> String.replace(~r/[0-9][a-z][A-Z]/,"_")}_pool" - Pool.child_spec(pool,[worker_module: Reaxt,size: pool_size, max_overflow: pool_overflow, name: {:local,pool}], server) - end, strategy: :one_for_one) - end - end - end - end -end diff --git a/lib/reaxt_error.ex b/lib/reaxt_error.ex new file mode 100644 index 0000000..28f9aae --- /dev/null +++ b/lib/reaxt_error.ex @@ -0,0 +1,28 @@ +defmodule Reaxt.Error do + defexception [:message,:args,:js_render,:js_stack] + + def exception({:handler_error,module,submodule,args,error,stack}) do + params = %{ + module: module, + submodule: submodule, + args: args + } + %__MODULE__{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} + end + + def exception({:render_error,params,error,stack,js_render}) do + %__MODULE__{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} + end + + defp parse_stack(stack) do + Regex.scan(~r/at (.*) \((.*):([0-9]*):[0-9]*\)/, stack) + |> Enum.filter(fn [_, function, url, _line] -> + String.contains?(url, "/priv") and not (function in ["Port.next_term","Socket.read_term"]) + end) + |> Enum.map(fn [_, function, url, line] -> + {line, _} = Integer.parse(line) + [_, after_priv] = String.split(url,"/priv/", parts: 2) + {JS, :"#{function}", 0, file: '#{Reaxt.Utils.web_priv}/#{after_priv}', line: line} + end) + end +end diff --git a/lib/render.ex b/lib/render.ex new file mode 100644 index 0000000..d6baf7a --- /dev/null +++ b/lib/render.ex @@ -0,0 +1,54 @@ +defmodule Reaxt.Render do + require Logger + + def render_result(chunk, module, data, timeout) when not is_tuple(module) do + render_result(chunk, {module, nil}, data, timeout) + end + + def render_result(chunk, {module, submodule}, data, timeout) do + Reaxt.PoolsSup.transaction(:"react_#{chunk}_pool", fn worker-> + GenServer.call(worker, {:render, module, submodule, data, timeout}, timeout + 100) + end) + end + + def render!(module, data, timeout \\ 5_000, chunk \\ :server) do + case render_result(chunk, module, data, timeout) do + {:ok, res} -> + res + {:error, err} -> + try do + raise(Reaxt.Error, err) + rescue ex -> + [_ | stack] = __STACKTRACE__ + stack = List.wrap(ex.js_stack) |> Enum.concat(stack) + reraise ex, stack + end + end + end + + def render(module,data, timeout \\ 5_000) do + try do + render!(module,data,timeout) + rescue + ex -> + case ex do + %{js_render: js_render} when is_binary(js_render) -> + Logger.error(Exception.message(ex)) + %{css: "",html: "", js_render: js_render} + _ -> + reraise ex, __STACKTRACE__ + end + end + end + + def reload do + WebPack.Util.build_stats + :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) + Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) + end + + def start_link(server_path) do + init = Poison.encode!(Application.get_env(:reaxt, :global_config, nil)) + Exos.Proc.start_link("node #{server_path}", init, [cd: '#{Reaxt.Utils.web_priv}']) + end +end diff --git a/lib/tasks.esbuild.ex b/lib/tasks.esbuild.ex new file mode 100644 index 0000000..f6c188c --- /dev/null +++ b/lib/tasks.esbuild.ex @@ -0,0 +1,61 @@ +defmodule Mix.Tasks.Esbuild.Compile do + use Mix.Task + + @shortdoc "Compiles Esbuild" + @esbuild "./node_modules/esbuild/bin/esbuild.js" + + def run(_) do + case compile() do + {json, 0} -> + File.write!("priv/webpack.stats.json", json) + {:ok, []} + + {ret, x} when x in [1,2] -> + require Logger + ret + |> Poison.decode!() + |> Map.fetch!("errors") + |> Enum.map(fn + bin when is_binary(bin) -> Logger.error(bin) + %{"message" => bin} when is_binary(bin) -> Logger.error(bin) + end) + {:error,[]} + end + end + + def compile() do + config = "./" <> Reaxt.Esbuild.esbuild_config() + esbuild = @esbuild + System.cmd( + "node", + [esbuild, "--config", config, "--json"], + into: "", + cd: Reaxt.Utils.web_app(), + env: [{"MIX_ENV", "#{Mix.env()}"}] + ) + end +end + +defmodule Mix.Tasks.Compile.ReaxtEsbuild do + use Mix.Task.Compiler + + def run(args) do + IO.puts("[Reaxt] Running Esbuild compiler...") + Mix.Task.run("reaxt.validate", args ++ ["--reaxt-skip-compiler-check"]) + + if !File.exists?(Path.join(Reaxt.Utils.web_app, "node_modules")) do + Mix.Task.run("npm.install", args) + else + installed_version = Poison.decode!(File.read!("#{Reaxt.Utils.web_app}/node_modules/reaxt/package.json"))["version"] + current_version = Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))["version"] + if installed_version !== current_version, do: + Mix.Task.run("npm.install", args) + end + + if !Application.get_env(:reaxt, :hot) do + Mix.Task.run("esbuild.compile", args) + else + {:ok, []} + end + end +end diff --git a/lib/tasks.ex b/lib/tasks.ex deleted file mode 100644 index bcf608a..0000000 --- a/lib/tasks.ex +++ /dev/null @@ -1,148 +0,0 @@ -defmodule Mix.Tasks.Npm.Install do - use Mix.Task - - @shortdoc "`npm install` in web_dir + npm install server side dependencies" - def run(_args) do - System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: WebPack.Util.web_app) - # TOIMPROVE- did not found a better hack to avoid npm install symlink : first make a tar gz package, then npm install it - reaxt_tgz = "#{System.tmp_dir}/reaxt.tgz" - System.cmd("tar", ["zcf",reaxt_tgz,"commonjs_reaxt"],into: IO.stream(:stdio, :line), cd: "#{:code.priv_dir(:reaxt)}") - System.cmd("npm",["install","--no-save",reaxt_tgz], into: IO.stream(:stdio, :line), cd: WebPack.Util.web_app) - end -end - -defmodule Mix.Tasks.Webpack.Analyseapp do - use Mix.Task - - @shortdoc "Generate webpack stats analysing application, resulting priv/static is meant to be versionned" - def run(_args) do - File.rm_rf!("priv/static") - {_,0} = System.cmd("git",["clone","-b","ajax-sse-loading","https://github.com/awetzel/analyse"], into: IO.stream(:stdio, :line)) - {_,0} = System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: "analyse") - {_,0} = System.cmd("grunt",[], into: IO.stream(:stdio, :line), cd: "analyse") - File.cp_r!("analyse/dist", "priv/static") - File.rm_rf!("analyse") - end -end - -defmodule Mix.Tasks.Webpack.Compile do - use Mix.Task - - @shortdoc "Compiles Webpack" - @webpack "./node_modules/webpack/bin/webpack.js" - - def run(_) do - case compile() do - {json, 0} -> - File.write!("priv/webpack.stats.json", json) - {:ok, []} - - {ret, x} when x in [1,2] -> - require Logger - ret - |> Poison.decode!() - |> Map.fetch!("errors") - |> Enum.map(fn - bin when is_binary(bin) -> Logger.error(bin) - %{"message" => bin} when is_binary(bin) -> Logger.error(bin) - end) - {:error,[]} - end - end - - def compile() do - config = "./"<>WebPack.Util.webpack_config - webpack = @webpack - System.cmd( - "node", - [webpack, "--config", config, "--json"], - into: "", - cd: WebPack.Util.web_app(), - env: [{"MIX_ENV", "#{Mix.env()}"}] - ) - end -end - -defmodule Mix.Tasks.Reaxt.Validate do - use Mix.Task - - @shortdoc "Validates that reaxt is setup correct" - def run(args) do - if Enum.all?(args, &(&1 != "--reaxt-skip-validation")) do - validate(args) - end - end - - def validate(args) do - if WebPack.Util.web_priv == :no_app_specified, do: - Mix.raise """ - Reaxt :otp_app is not configured. - Add following to config.exs - - config :reaxt, :otp_app, :your_app - - """ - - packageJsonPath = Path.join(WebPack.Util.web_app, "package.json") - if not File.exists?(packageJsonPath), do: - Mix.raise """ - Reaxt could not find a package.json in #{WebPack.Util.web_app}. - Add package.json to #{WebPack.Util.web_app} or configure a new - web_app directory in config.exs: - - config :reaxt, :web_app, "webapp" - - """ - - packageJson = Poison.decode!(File.read!(packageJsonPath)) - if packageJson["devDependencies"]["webpack"] == nil, do: - Mix.raise """ - Reaxt requires webpack as a devDependency in #{packageJsonPath}. - Add a dependency to 'webpack' like: - - { - devDependencies: { - "webpack": "^1.4.13" - } - } - """ - - if (Enum.all?(args, &(&1 != "--reaxt-skip-compiler-check")) - and Enum.all? (Mix.Project.get!).project[:compilers], &(&1 != :reaxt_webpack)), do: - Mix.raise """ - Reaxt has a built in compiler that compiles the web app. - Remember to add it to the list of compilers in mix.exs: - - def project do - [... - app: :your_app, - compilers: [:reaxt_webpack] ++ Mix.compilers, - ...] - end - """ - end -end - -defmodule Mix.Tasks.Compile.ReaxtWebpack do - use Mix.Task.Compiler - - def run(args) do - IO.puts("[Reaxt] Running compiler...") - Mix.Task.run("reaxt.validate", args ++ ["--reaxt-skip-compiler-check"]) - - if !File.exists?(Path.join(WebPack.Util.web_app, "node_modules")) do - Mix.Task.run("npm.install", args) - else - installed_version = Poison.decode!(File.read!("#{WebPack.Util.web_app}/node_modules/reaxt/package.json"))["version"] - current_version = Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))["version"] - if installed_version !== current_version, do: - Mix.Task.run("npm.install", args) - end - - if !Application.get_env(:reaxt,:hot) do - Mix.Task.run("webpack.compile", args) - else - {:ok, []} - end - end -end diff --git a/lib/tasks.reaxt.ex b/lib/tasks.reaxt.ex new file mode 100644 index 0000000..ca285e0 --- /dev/null +++ b/lib/tasks.reaxt.ex @@ -0,0 +1,72 @@ +defmodule Mix.Tasks.Npm.Install do + use Mix.Task + + @shortdoc "`npm install` in web_dir + npm install server side dependencies" + def run(_args) do + System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: Reaxt.Utils.web_app) + # TOIMPROVE- did not found a better hack to avoid npm install symlink : first make a tar gz package, then npm install it + reaxt_tgz = "#{System.tmp_dir}/reaxt.tgz" + System.cmd("tar", ["zcf", reaxt_tgz, "commonjs_reaxt"],into: IO.stream(:stdio, :line), cd: "#{:code.priv_dir(:reaxt)}") + System.cmd("npm",["install", "--no-save", reaxt_tgz], into: IO.stream(:stdio, :line), cd: Reaxt.Utils.web_app) + end +end + +defmodule Mix.Tasks.Reaxt.Validate do + use Mix.Task + + @shortdoc "Validates that reaxt is setup correct" + def run(args) do + if Enum.all?(args, &(&1 != "--reaxt-skip-validation")) do + validate(args) + end + end + + def validate(args) do + if Reaxt.Utils.web_priv == :no_app_specified, do: + Mix.raise """ + Reaxt :otp_app is not configured. + Add following to config.exs + + config :reaxt, :otp_app, :your_app + + """ + + packageJsonPath = Path.join(Reaxt.Utils.web_app, "package.json") + if not File.exists?(packageJsonPath), do: + Mix.raise """ + Reaxt could not find a package.json in #{Reaxt.Utils.web_app}. + Add package.json to #{Reaxt.Utils.web_app} or configure a new + web_app directory in config.exs: + + config :reaxt, :web_app, "webapp" + + """ + + packageJson = Poison.decode!(File.read!(packageJsonPath)) + if packageJson["devDependencies"]["webpack"] == nil, do: + Mix.raise """ + Reaxt requires webpack as a devDependency in #{packageJsonPath}. + Add a dependency to 'webpack' like: + + { + devDependencies: { + "webpack": "^1.4.13" + } + } + """ + + if (Enum.all?(args, &(&1 != "--reaxt-skip-compiler-check")) + and Enum.all? (Mix.Project.get!).project[:compilers], &(&1 != :reaxt_webpack)), do: + Mix.raise """ + Reaxt has a built in compiler that compiles the web app. + Remember to add it to the list of compilers in mix.exs: + + def project do + [... + app: :your_app, + compilers: [:reaxt_webpack] ++ Mix.compilers, + ...] + end + """ + end +end diff --git a/lib/tasks.webpack.ex b/lib/tasks.webpack.ex new file mode 100644 index 0000000..28df144 --- /dev/null +++ b/lib/tasks.webpack.ex @@ -0,0 +1,75 @@ +defmodule Mix.Tasks.Webpack.Analyseapp do + use Mix.Task + + @shortdoc "Generate webpack stats analysing application, resulting priv/static is meant to be versionned" + def run(_args) do + File.rm_rf!("priv/static") + {_,0} = System.cmd("git",["clone","-b","ajax-sse-loading","https://github.com/awetzel/analyse"], into: IO.stream(:stdio, :line)) + {_,0} = System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: "analyse") + {_,0} = System.cmd("grunt",[], into: IO.stream(:stdio, :line), cd: "analyse") + File.cp_r!("analyse/dist", "priv/static") + File.rm_rf!("analyse") + end +end + +defmodule Mix.Tasks.Webpack.Compile do + use Mix.Task + + @shortdoc "Compiles Webpack" + @webpack "./node_modules/webpack/bin/webpack.js" + + def run(_) do + case compile() do + {json, 0} -> + File.write!("priv/webpack.stats.json", json) + {:ok, []} + + {ret, x} when x in [1,2] -> + require Logger + ret + |> Poison.decode!() + |> Map.fetch!("errors") + |> Enum.map(fn + bin when is_binary(bin) -> Logger.error(bin) + %{"message" => bin} when is_binary(bin) -> Logger.error(bin) + end) + {:error,[]} + end + end + + def compile() do + config = "./"<>WebPack.Util.webpack_config + webpack = @webpack + System.cmd( + "node", + [webpack, "--config", config, "--json"], + into: "", + cd: Reaxt.Utils.web_app(), + env: [{"MIX_ENV", "#{Mix.env()}"}] + ) + end +end + +defmodule Mix.Tasks.Compile.ReaxtWebpack do + use Mix.Task.Compiler + + def run(args) do + IO.puts("[Reaxt] Running Webpack compiler...") + Mix.Task.run("reaxt.validate", args ++ ["--reaxt-skip-compiler-check"]) + + if !File.exists?(Path.join(Reaxt.Utils.web_app, "node_modules")) do + Mix.Task.run("npm.install", args) + else + installed_version = Poison.decode!(File.read!("#{Reaxt.Utils.web_app}/node_modules/reaxt/package.json"))["version"] + current_version = Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))["version"] + if installed_version !== current_version, do: + Mix.Task.run("npm.install", args) + end + + if !Application.get_env(:reaxt,:hot) do + Mix.Task.run("webpack.compile", args) + else + {:ok, []} + end + end +end diff --git a/lib/utils.ex b/lib/utils.ex new file mode 100644 index 0000000..79bf662 --- /dev/null +++ b/lib/utils.ex @@ -0,0 +1,14 @@ +defmodule Reaxt.Utils do + def web_priv do + case Application.get_env :reaxt, :otp_app, :no_app_specified do + :no_app_specified -> + :no_app_specified + web_app -> + :code.priv_dir(web_app) + end + end + + def web_app do + Application.get_env(:reaxt, :web_app, "web") + end +end diff --git a/lib/webpack.ex b/lib/webpack.ex index aec51d3..ccbe0e9 100644 --- a/lib/webpack.ex +++ b/lib/webpack.ex @@ -63,7 +63,7 @@ defmodule WebPack.Plug.Static do get "/webpack/stats.json" do conn |> put_resp_content_type("application/json") - |> send_file(200,"#{WebPack.Util.web_priv}/webpack.stats.json") + |> send_file(200,"#{Reaxt.Utils.web_priv}/webpack.stats.json") |> halt end get "/webpack", do: %{conn|path_info: ["webpack","static","index.html"]} @@ -78,7 +78,7 @@ defmodule WebPack.Plug.Static do get "/webpack/client.js" do conn |> put_resp_content_type("application/javascript") - |> send_file(200,"#{WebPack.Util.web_app}/node_modules/reaxt/webpack_client.js") + |> send_file(200,"#{Reaxt.Utils.web_app}/node_modules/reaxt/webpack_client.js") |> halt end match _, do: conn @@ -120,8 +120,8 @@ defmodule WebPack.EventManager do WebPack.Util.build_stats if(!state.init) do Logger.info("[reaxt-webpack] client done, restart servers") - :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.App.PoolsSup) - {:ok,_} = Supervisor.restart_child(Reaxt.App, Reaxt.App.PoolsSup) + :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) + {:ok,_} = Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) end _ = case ev do %{error: "soft fail", error_details: details} -> @@ -169,7 +169,7 @@ defmodule WebPack.Compiler do def start_link(_) do cmd = "node ./node_modules/reaxt/webpack_server #{WebPack.Util.webpack_config}" hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" - Exos.Proc.start_link(cmd<>hot_arg,[],[cd: WebPack.Util.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) + Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) end end @@ -178,20 +178,9 @@ defmodule WebPack.Util do Application.get_env(:reaxt,:webpack_config,"webpack.config.js") end - def web_priv do - case Application.get_env :reaxt, :otp_app, :no_app_specified do - :no_app_specified -> :no_app_specified - web_app -> :code.priv_dir(web_app) - end - end - - def web_app do - Application.get_env :reaxt, :web_app, "web" - end - def build_stats do - if File.exists?("#{web_priv()}/webpack.stats.json") do - all_stats = Poison.decode!(File.read!("#{web_priv()}/webpack.stats.json")) + if File.exists?("#{Reaxt.Utils.web_priv()}/webpack.stats.json") do + all_stats = Poison.decode!(File.read!("#{Reaxt.Utils.web_priv()}/webpack.stats.json")) stats_array = all_stats["children"] stats = Enum.map(stats_array,fn stats-> %{assetsByChunkName: stats["assetsByChunkName"], From 193a9cebed9e59a0d2822028698746e99af2acbb Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Tue, 24 Sep 2024 21:30:09 +0200 Subject: [PATCH 2/6] Seems like its starting to work ! --- lib/pool.ex | 6 ++--- lib/reaxt_error.ex | 9 +++++--- lib/render.ex | 6 ++--- lib/tasks.esbuild.ex | 36 ++++++++++++++--------------- lib/utils.ex | 20 ++++++++++++++++ mix.exs | 6 +---- priv/commonjs_reaxt/react_server.js | 2 +- 7 files changed, 52 insertions(+), 33 deletions(-) diff --git a/lib/pool.ex b/lib/pool.ex index 1133c17..7bf1f2f 100644 --- a/lib/pool.ex +++ b/lib/pool.ex @@ -13,9 +13,9 @@ defmodule Reaxt.PoolsSup do end def init(_) do - pool_size = Application.get_env(:reaxt, :pool_size, 1) - pool_overflow = Application.get_env(:reaxt, :pool_max_overflow, 5) - server_dir = "#{Reaxt.Utils.web_priv}/#{Application.get_env(:reaxt, :server_dir)}" + pool_size = Reaxt.Utils.pool_size() + pool_overflow = Reaxt.Utils.max_pool_overflow() + server_dir = "#{Reaxt.Utils.web_priv()}/#{Reaxt.Utils.server_dir()}" server_files = Path.wildcard("#{server_dir}/*.js") if server_files == [] do diff --git a/lib/reaxt_error.ex b/lib/reaxt_error.ex index 28f9aae..f0fc460 100644 --- a/lib/reaxt_error.ex +++ b/lib/reaxt_error.ex @@ -1,4 +1,4 @@ -defmodule Reaxt.Error do +defmodule ReaxtError do defexception [:message,:args,:js_render,:js_stack] def exception({:handler_error,module,submodule,args,error,stack}) do @@ -7,11 +7,14 @@ defmodule Reaxt.Error do submodule: submodule, args: args } - %__MODULE__{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} + %ReaxtError{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} end def exception({:render_error,params,error,stack,js_render}) do - %__MODULE__{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} + %ReaxtError{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} + end + def exception(rest) do + %ReaxtError{message: "JS Render Exception : #{inspect(rest)}", args: "", js_render: "", js_stack: ""} end defp parse_stack(stack) do diff --git a/lib/render.ex b/lib/render.ex index d6baf7a..39ddeab 100644 --- a/lib/render.ex +++ b/lib/render.ex @@ -17,10 +17,10 @@ defmodule Reaxt.Render do res {:error, err} -> try do - raise(Reaxt.Error, err) + raise(ReaxtError, err) rescue ex -> [_ | stack] = __STACKTRACE__ - stack = List.wrap(ex.js_stack) |> Enum.concat(stack) + # stack = List.wrap(ex[:js_stack]) |> Enum.concat(stack) reraise ex, stack end end @@ -42,7 +42,7 @@ defmodule Reaxt.Render do end def reload do - WebPack.Util.build_stats + if Reaxt.Utils.is_webpack?(), do: WebPack.Util.build_stats :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) end diff --git a/lib/tasks.esbuild.ex b/lib/tasks.esbuild.ex index f6c188c..5454875 100644 --- a/lib/tasks.esbuild.ex +++ b/lib/tasks.esbuild.ex @@ -5,30 +5,30 @@ defmodule Mix.Tasks.Esbuild.Compile do @esbuild "./node_modules/esbuild/bin/esbuild.js" def run(_) do - case compile() do - {json, 0} -> - File.write!("priv/webpack.stats.json", json) - {:ok, []} - - {ret, x} when x in [1,2] -> - require Logger - ret - |> Poison.decode!() - |> Map.fetch!("errors") - |> Enum.map(fn - bin when is_binary(bin) -> Logger.error(bin) - %{"message" => bin} when is_binary(bin) -> Logger.error(bin) - end) - {:error,[]} - end + compile() + # case compile() do + # {json, 0} -> + # File.write!("priv/webpack.stats.json", json) + # {:ok, []} + + # {ret, x} when x in [1,2] -> + # require Logger + # ret + # |> Poison.decode!() + # |> Map.fetch!("errors") + # |> Enum.map(fn + # bin when is_binary(bin) -> Logger.error(bin) + # %{"message" => bin} when is_binary(bin) -> Logger.error(bin) + # end) + # {:error,[]} + # end end def compile() do config = "./" <> Reaxt.Esbuild.esbuild_config() - esbuild = @esbuild System.cmd( "node", - [esbuild, "--config", config, "--json"], + [config], into: "", cd: Reaxt.Utils.web_app(), env: [{"MIX_ENV", "#{Mix.env()}"}] diff --git a/lib/utils.ex b/lib/utils.ex index 79bf662..7d74698 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -8,6 +8,26 @@ defmodule Reaxt.Utils do end end + def bundler() do + Application.get_env(:reaxt, :bundler, :webpack) + end + + def is_webpack?() do + bundler() == :webpack + end + + def server_dir() do + Application.get_env(:reaxt, :server_dir, "react_servers") + end + + def pool_size() do + Application.get_env(:reaxt, :pool_size, 1) + end + + def max_pool_overflow() do + Application.get_env(:reaxt, :pool_max_overflow, 5) + end + def web_app do Application.get_env(:reaxt, :web_app, "web") end diff --git a/mix.exs b/mix.exs index cb84dd6..638c474 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Reaxt.Mixfile do use Mix.Project - def version, do: "4.1.0" + def version, do: "5.0.0" defp description do """ @@ -28,10 +28,6 @@ defmodule Reaxt.Mixfile do env: [ otp_app: :reaxt, #the OTP application containing compiled JS server hot: false, # false | true | :client hot compilation and loading - pool_size: 1, #pool size of react renderes - webpack_config: "webpack.config.js", - server_dir: "react_servers", - pool_max_overflow: 5 #maximum pool extension when the pool is full ]] end diff --git a/priv/commonjs_reaxt/react_server.js b/priv/commonjs_reaxt/react_server.js index a5ad7dc..8fe71f1 100644 --- a/priv/commonjs_reaxt/react_server.js +++ b/priv/commonjs_reaxt/react_server.js @@ -57,7 +57,7 @@ Server(function(term,from,state,done){ done("reply",Bert.tuple(Bert.atom("error"),Bert.tuple(Bert.atom("handler_error"),module,submodule,args,"timeout",Bert.atom("nil")))) },timeout) - import(`./../../components/${module}`).then((handler)=>{ + import(`./../../components/${module}.js`).then((handler)=>{ handler = handler.default submodule = (submodule == "nil") ? undefined : submodule handler = (!submodule) ? handler : handler[submodule] From c24c53be0bb0dfcfc72571691017126cf3af9d70 Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Thu, 26 Sep 2024 22:54:18 +0200 Subject: [PATCH 3/6] Fix client_entry_addition for esbuild --- priv/commonjs_reaxt/client_entry_addition.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/commonjs_reaxt/client_entry_addition.js b/priv/commonjs_reaxt/client_entry_addition.js index 449bb4c..ebf9649 100644 --- a/priv/commonjs_reaxt/client_entry_addition.js +++ b/priv/commonjs_reaxt/client_entry_addition.js @@ -6,7 +6,7 @@ function default_client_render(props,render,param){ } window.reaxt_render = function(module,submodule,props,param){ - return import(`./../../components/${module}`).then((module)=>{ + return import(`./../../components/${module}.js`).then((module)=>{ module = module.default submodule = (submodule) ? module[submodule] :module submodule.reaxt_client_render = submodule.reaxt_client_render || default_client_render From e6882b7bd7ea9bfe62153a81b0d7a74d94648c65 Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Thu, 26 Sep 2024 23:03:03 +0200 Subject: [PATCH 4/6] Change directory structure Working client js with esbuild --- lib/app.ex | 30 ++++++++++++------- lib/esbuild.ex | 18 ----------- lib/esbuild/esbuild.ex | 7 +++++ lib/{ => esbuild}/tasks.esbuild.ex | 20 ++----------- lib/utils.ex | 4 +++ lib/{ => webpack}/tasks.webpack.ex | 0 lib/{ => webpack}/webpack.ex | 20 ++++++------- ...ebpack_server.js => webpack_hot_server.js} | 9 ------ 8 files changed, 43 insertions(+), 65 deletions(-) delete mode 100644 lib/esbuild.ex create mode 100644 lib/esbuild/esbuild.ex rename lib/{ => esbuild}/tasks.esbuild.ex (68%) rename lib/{ => webpack}/tasks.webpack.ex (100%) rename lib/{ => webpack}/webpack.ex (92%) rename priv/commonjs_reaxt/{webpack_server.js => webpack_hot_server.js} (77%) diff --git a/lib/app.ex b/lib/app.ex index 4db50d2..2af51b4 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -1,26 +1,36 @@ defmodule Reaxt.App do use Application + require Logger def start(_,_) do - hot_reload_processes = [ - # WebPack.Events, - # WebPack.EventManager, - # WebPack.Compiler, - ] + hot_reload_processes = if Reaxt.Utils.is_hot?() do + hot_processes(Reaxt.Utils.bundler) + else + [] + end base_processes = [ Reaxt.PoolsSup ] - children = if Application.get_env(:reaxt, :hot, false) do - Enum.concat(base_processes, hot_reload_processes) - else - base_processes - end + children = Enum.concat(base_processes, hot_reload_processes) result = Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one) # WebPack.Util.build_stats() result end + + def hot_processes(:webpack) do + [ + WebPack.Hot.Events, + WebPack.Hot.EventManager, + WebPack.Hot.Compiler, + ] + end + + def hot_processes(_) do + Logger.warning("[Reaxt] Hot reload is not supported for bundle #{inspect Reaxt.Utils.bundler()}") + [] + end end diff --git a/lib/esbuild.ex b/lib/esbuild.ex deleted file mode 100644 index b5ee707..0000000 --- a/lib/esbuild.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Reaxt.Esbuild do - - def esbuild_config do - Application.get_env(:reaxt, :webpack_config, "build.js") - end - -end - -defmodule Reaxt.Esbuild.Compiler do - def child_spec(arg) do - %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } - end - def start_link(_) do - cmd = "node ./node_modules/reaxt/webpack_server #{WebPack.Util.webpack_config}" - hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" - Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) - end -end diff --git a/lib/esbuild/esbuild.ex b/lib/esbuild/esbuild.ex new file mode 100644 index 0000000..c3c212c --- /dev/null +++ b/lib/esbuild/esbuild.ex @@ -0,0 +1,7 @@ +defmodule Reaxt.Esbuild do + + def esbuild_config do + Application.get_env(:reaxt, :esbuild_config, "build.js") + end + +end diff --git a/lib/tasks.esbuild.ex b/lib/esbuild/tasks.esbuild.ex similarity index 68% rename from lib/tasks.esbuild.ex rename to lib/esbuild/tasks.esbuild.ex index 5454875..af9fc0c 100644 --- a/lib/tasks.esbuild.ex +++ b/lib/esbuild/tasks.esbuild.ex @@ -2,26 +2,10 @@ defmodule Mix.Tasks.Esbuild.Compile do use Mix.Task @shortdoc "Compiles Esbuild" - @esbuild "./node_modules/esbuild/bin/esbuild.js" def run(_) do - compile() - # case compile() do - # {json, 0} -> - # File.write!("priv/webpack.stats.json", json) - # {:ok, []} - - # {ret, x} when x in [1,2] -> - # require Logger - # ret - # |> Poison.decode!() - # |> Map.fetch!("errors") - # |> Enum.map(fn - # bin when is_binary(bin) -> Logger.error(bin) - # %{"message" => bin} when is_binary(bin) -> Logger.error(bin) - # end) - # {:error,[]} - # end + {_logs, 0} = compile() + :ok end def compile() do diff --git a/lib/utils.ex b/lib/utils.ex index 7d74698..977e2a8 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -16,6 +16,10 @@ defmodule Reaxt.Utils do bundler() == :webpack end + def is_hot?() do + Application.get_env(:reaxt, :hot, false) + end + def server_dir() do Application.get_env(:reaxt, :server_dir, "react_servers") end diff --git a/lib/tasks.webpack.ex b/lib/webpack/tasks.webpack.ex similarity index 100% rename from lib/tasks.webpack.ex rename to lib/webpack/tasks.webpack.ex diff --git a/lib/webpack.ex b/lib/webpack/webpack.ex similarity index 92% rename from lib/webpack.ex rename to lib/webpack/webpack.ex index ccbe0e9..469cbdc 100644 --- a/lib/webpack.ex +++ b/lib/webpack/webpack.ex @@ -1,4 +1,4 @@ -defmodule WebPack.Events do +defmodule WebPack.Hot.Events do def child_spec(_) do Registry.child_spec(keys: :duplicate, name: __MODULE__) end @@ -48,7 +48,7 @@ defmodule WebPack.Plug.Static do def wait_compilation(conn,_) do if Application.get_env(:reaxt,:hot) do try do - :ok = GenServer.call(WebPack.EventManager,:wait?,30_000) + :ok = GenServer.call(WebPack.Hot.EventManager,:wait?,30_000) catch :exit,{:timeout,_} -> :ok end @@ -71,9 +71,9 @@ defmodule WebPack.Plug.Static do conn=conn |> put_resp_header("content-type", "text/event-stream") |> send_chunked(200) - hot? = Application.get_env(:reaxt,:hot) + hot? = Reaxt.Utils.is_hot?() if hot? == :client, do: chunk(conn, "event: hot\ndata: nothing\n\n") - if hot? do WebPack.Events.stream_chunks(conn) else conn end + if hot? do WebPack.Hot.Events.stream_chunks(conn) else conn end end get "/webpack/client.js" do conn @@ -100,13 +100,13 @@ defmodule WebPack.StartBlocker do end end -defmodule WebPack.EventManager do +defmodule WebPack.Hot.EventManager do use GenServer require Logger def start_link(_) do GenServer.start_link(__MODULE__,[], name: __MODULE__) end def init([]) do - WebPack.Events.register!() + WebPack.Hot.Events.register!() {:ok,%{init: true,pending: [], compiling: false, compiled: false}} end @@ -157,19 +157,19 @@ defmodule WebPack.EventManager do def done(state) do for from<-state.pending do GenServer.reply(from,:ok) end - WebPack.Events.dispatch(%{event: "done"}) + WebPack.Hot.Events.dispatch(%{event: "done"}) %{state| pending: [], init: false, compiling: false, compiled: true} end end -defmodule WebPack.Compiler do +defmodule WebPack.Hot.Compiler do def child_spec(arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } end def start_link(_) do - cmd = "node ./node_modules/reaxt/webpack_server #{WebPack.Util.webpack_config}" + cmd = "node ./node_modules/reaxt/webpack_hot_server #{WebPack.Util.webpack_config}" hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" - Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) + Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Hot.Events.dispatch/1) end end diff --git a/priv/commonjs_reaxt/webpack_server.js b/priv/commonjs_reaxt/webpack_hot_server.js similarity index 77% rename from priv/commonjs_reaxt/webpack_server.js rename to priv/commonjs_reaxt/webpack_hot_server.js index 3b11749..574c980 100644 --- a/priv/commonjs_reaxt/webpack_server.js +++ b/priv/commonjs_reaxt/webpack_hot_server.js @@ -4,15 +4,6 @@ var webpack = require("webpack"), var multi_config = require(process.cwd()+"/"+process.argv[2]) -//if(process.argv[2] === "hot"){ -// // add hotmodule plugin to client -// client_config.plugins = (client_config.plugins || []).concat([new webpack.HotModuleReplacementPlugin()]) -// // add reloading code to entries -// client_config.add_to_entries(client_config,"webpack/hot/dev-server") -// // remove external which cause conflicts in hot loading -// client_config.externals = {} -//} - var client_stats,client_err function maybe_done() { if(client_err) port.write({event: "client_done", error: JSON.stringify(client_err)}) From b4601d9a2d077b3d0ffd76833c6b59450b041554 Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Tue, 1 Oct 2024 21:37:27 +0200 Subject: [PATCH 5/6] Keep on cleaning the stuff --- CHANGELOG.md | 4 +++ lib/app.ex | 5 ++-- lib/esbuild/tasks.esbuild.ex | 2 +- lib/reaxt_error.ex | 8 +++--- lib/reaxt_index.ex | 47 ++++++++++++++++++++++++++++++ lib/render.ex | 12 ++++++-- lib/tasks.reaxt.ex | 15 ++++++++-- lib/utils.ex | 2 +- lib/webpack/tasks.webpack.ex | 2 +- lib/webpack/webpack.ex | 55 +++++++----------------------------- mix.exs | 13 ++++----- 11 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 lib/reaxt_index.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a8de7..4eb90ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +* Support for esbuild + ### Changed * Updated poolboy, it itself now uses rebar3 to build. diff --git a/lib/app.ex b/lib/app.ex index 2af51b4..e49e913 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -16,7 +16,7 @@ defmodule Reaxt.App do children = Enum.concat(base_processes, hot_reload_processes) result = Supervisor.start_link(children, name: __MODULE__, strategy: :one_for_one) - # WebPack.Util.build_stats() + if Reaxt.Utils.is_webpack?(), do: Reaxt.Index.Generator.build_webpack_stats() result end @@ -30,7 +30,6 @@ defmodule Reaxt.App do end def hot_processes(_) do - Logger.warning("[Reaxt] Hot reload is not supported for bundle #{inspect Reaxt.Utils.bundler()}") - [] + raise "[Reaxt] Hot reload is not supported for bundle #{inspect Reaxt.Utils.bundler()}" end end diff --git a/lib/esbuild/tasks.esbuild.ex b/lib/esbuild/tasks.esbuild.ex index af9fc0c..7e8f937 100644 --- a/lib/esbuild/tasks.esbuild.ex +++ b/lib/esbuild/tasks.esbuild.ex @@ -36,7 +36,7 @@ defmodule Mix.Tasks.Compile.ReaxtEsbuild do Mix.Task.run("npm.install", args) end - if !Application.get_env(:reaxt, :hot) do + if !Reaxt.Utils.is_hot?() do Mix.Task.run("esbuild.compile", args) else {:ok, []} diff --git a/lib/reaxt_error.ex b/lib/reaxt_error.ex index f0fc460..a9a3944 100644 --- a/lib/reaxt_error.ex +++ b/lib/reaxt_error.ex @@ -1,4 +1,4 @@ -defmodule ReaxtError do +defmodule Reaxt.Error do defexception [:message,:args,:js_render,:js_stack] def exception({:handler_error,module,submodule,args,error,stack}) do @@ -7,14 +7,14 @@ defmodule ReaxtError do submodule: submodule, args: args } - %ReaxtError{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} + %Reaxt.Error{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} end def exception({:render_error,params,error,stack,js_render}) do - %ReaxtError{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} + %Reaxt.Error{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} end def exception(rest) do - %ReaxtError{message: "JS Render Exception : #{inspect(rest)}", args: "", js_render: "", js_stack: ""} + %Reaxt.Error{message: "JS Render Exception : #{inspect(rest)}", args: "", js_render: "", js_stack: ""} end defp parse_stack(stack) do diff --git a/lib/reaxt_index.ex b/lib/reaxt_index.ex new file mode 100644 index 0000000..b3967c4 --- /dev/null +++ b/lib/reaxt_index.ex @@ -0,0 +1,47 @@ +defmodule Reaxt.Index.Generator do + def webpack_config do + Application.get_env(:reaxt,:webpack_config,"webpack.config.js") + end + + def build_webpack_stats do + if File.exists?("#{Reaxt.Utils.web_priv()}/webpack.stats.json") do + all_stats = Poison.decode!(File.read!("#{Reaxt.Utils.web_priv()}/webpack.stats.json")) + stats_array = all_stats["children"] + stats = Enum.map(stats_array, fn stats-> + %{assetsByChunkName: stats["assetsByChunkName"], + errors: stats["errors"], + warnings: stats["warnings"]} + end) + + _ = Code.compiler_options(ignore_module_conflict: true) + defmodule Elixir.Reaxt.Index do + @stats stats + def stats, do: @stats + def file_of(name) do + r = Enum.find_value(WebPack.stats, fn %{assetsByChunkName: assets}-> assets["#{name}"] end) + case r do + [f|_]->f + f -> f + end + end + + @header_script if(Reaxt.Utils.is_hot?(), do: ~s()) + @header_global Poison.encode!(Reaxt.Render.get_global_config()) + + def header do + "\n#{@header_script}" + end + end + _ = Code.compiler_options(ignore_module_conflict: false) + end + end +end + +defmodule Elixir.Reaxt.Index do + def stats, do: %{assetsByChunkName: %{}} + def file_of(_), do: nil + def header do + global_config = Poison.encode!(Reaxt.Render.get_global_config()) + "" + end +end diff --git a/lib/render.ex b/lib/render.ex index 39ddeab..c406f40 100644 --- a/lib/render.ex +++ b/lib/render.ex @@ -1,6 +1,14 @@ defmodule Reaxt.Render do require Logger + def get_global_config do + Application.get_env(:reaxt, :global_config, %{}) + end + + def set_global_config(config) do + Application.put_env(:reaxt, :global_config, config) + end + def render_result(chunk, module, data, timeout) when not is_tuple(module) do render_result(chunk, {module, nil}, data, timeout) end @@ -17,7 +25,7 @@ defmodule Reaxt.Render do res {:error, err} -> try do - raise(ReaxtError, err) + raise(Reaxt.Error, err) rescue ex -> [_ | stack] = __STACKTRACE__ # stack = List.wrap(ex[:js_stack]) |> Enum.concat(stack) @@ -42,7 +50,7 @@ defmodule Reaxt.Render do end def reload do - if Reaxt.Utils.is_webpack?(), do: WebPack.Util.build_stats + if Reaxt.Utils.is_webpack?(), do: Reaxt.Index.Generator.build_webpack_stats() :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) end diff --git a/lib/tasks.reaxt.ex b/lib/tasks.reaxt.ex index ca285e0..2034016 100644 --- a/lib/tasks.reaxt.ex +++ b/lib/tasks.reaxt.ex @@ -22,6 +22,16 @@ defmodule Mix.Tasks.Reaxt.Validate do end def validate(args) do + if Reaxt.Utils.bundler() not in [:webpack, :esbuild], do: + Mix.raise """ + Reaxt :bundler is not configured. + Add following to config.exs + + config :reaxt, :bundler, :webpack + OR + config :reaxt, :bundler, :esbuild + """ + if Reaxt.Utils.web_priv == :no_app_specified, do: Mix.raise """ Reaxt :otp_app is not configured. @@ -31,7 +41,7 @@ defmodule Mix.Tasks.Reaxt.Validate do """ - packageJsonPath = Path.join(Reaxt.Utils.web_app, "package.json") + packageJsonPath = Path.join(Reaxt.Utils.web_app(), "package.json") if not File.exists?(packageJsonPath), do: Mix.raise """ Reaxt could not find a package.json in #{Reaxt.Utils.web_app}. @@ -55,8 +65,9 @@ defmodule Mix.Tasks.Reaxt.Validate do } """ + good_compilers = [:reaxt_webpack, :reaxt_esbuild] if (Enum.all?(args, &(&1 != "--reaxt-skip-compiler-check")) - and Enum.all? (Mix.Project.get!).project[:compilers], &(&1 != :reaxt_webpack)), do: + and not Enum.any?((Mix.Project.get!).project[:compilers], &(&1 in good_compilers))), do: Mix.raise """ Reaxt has a built in compiler that compiles the web app. Remember to add it to the list of compilers in mix.exs: diff --git a/lib/utils.ex b/lib/utils.ex index 977e2a8..d03efd0 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -9,7 +9,7 @@ defmodule Reaxt.Utils do end def bundler() do - Application.get_env(:reaxt, :bundler, :webpack) + Application.get_env(:reaxt, :bundler) end def is_webpack?() do diff --git a/lib/webpack/tasks.webpack.ex b/lib/webpack/tasks.webpack.ex index 28df144..3286f67 100644 --- a/lib/webpack/tasks.webpack.ex +++ b/lib/webpack/tasks.webpack.ex @@ -38,7 +38,7 @@ defmodule Mix.Tasks.Webpack.Compile do end def compile() do - config = "./"<>WebPack.Util.webpack_config + config = "./"<>Reaxt.Webpack.webpack_config() webpack = @webpack System.cmd( "node", diff --git a/lib/webpack/webpack.ex b/lib/webpack/webpack.ex index 469cbdc..4f0096f 100644 --- a/lib/webpack/webpack.ex +++ b/lib/webpack/webpack.ex @@ -1,3 +1,9 @@ +defmodule Reaxt.Webpack do + def webpack_config do + Application.get_env(:reaxt,:webpack_config,"webpack.config.js") + end +end + defmodule WebPack.Hot.Events do def child_spec(_) do Registry.child_spec(keys: :duplicate, name: __MODULE__) @@ -117,7 +123,7 @@ defmodule WebPack.Hot.EventManager do def handle_info({:event,%{event: "client_done"}=ev},state) do Logger.info("[reaxt-webpack] client done, build_stats") - WebPack.Util.build_stats + _ = Reaxt.Index.Generator.build_webpack_stats() if(!state.init) do Logger.info("[reaxt-webpack] client done, restart servers") :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) @@ -137,8 +143,8 @@ defmodule WebPack.Hot.EventManager do _ -> :ok end - for {_idx,build}<-WebPack.stats, error<-build.errors, do: Logger.warning(error) - for {_idx,build}<-WebPack.stats, warning<-build.warnings, do: Logger.warning(warning) + for {_idx, build} <- Reaxt.Index.stats, error<-build.errors, do: Logger.warning(error) + for {_idx, build} <- Reaxt.Index.stats, warning<-build.warnings, do: Logger.warning(warning) {:noreply,done(state)} end @@ -167,49 +173,8 @@ defmodule WebPack.Hot.Compiler do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } end def start_link(_) do - cmd = "node ./node_modules/reaxt/webpack_hot_server #{WebPack.Util.webpack_config}" + cmd = "node ./node_modules/reaxt/webpack_hot_server #{Reaxt.Webpack.webpack_config()}" hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Hot.Events.dispatch/1) end end - -defmodule WebPack.Util do - def webpack_config do - Application.get_env(:reaxt,:webpack_config,"webpack.config.js") - end - - def build_stats do - if File.exists?("#{Reaxt.Utils.web_priv()}/webpack.stats.json") do - all_stats = Poison.decode!(File.read!("#{Reaxt.Utils.web_priv()}/webpack.stats.json")) - stats_array = all_stats["children"] - stats = Enum.map(stats_array,fn stats-> - %{assetsByChunkName: stats["assetsByChunkName"], - errors: stats["errors"], - warnings: stats["warnings"]} - end) - _ = Code.compiler_options(ignore_module_conflict: true) - defmodule Elixir.WebPack do - @stats stats - def stats, do: @stats - def file_of(name) do - r = Enum.find_value(WebPack.stats, fn %{assetsByChunkName: assets}-> assets["#{name}"] end) - case r do - [f|_]->f - f -> f - end - end - @header_script if(Application.get_env(:reaxt,:hot), do: ~s()) - @header_global Poison.encode!(Application.get_env(:reaxt,:global_config)) - def header, do: - "\n#{@header_script}" - end - _ = Code.compiler_options(ignore_module_conflict: false) - end - end -end - -defmodule Elixir.WebPack do - def stats, do: %{assetsByChunkName: %{}} - def file_of(_), do: nil - def header, do: "" -end diff --git a/mix.exs b/mix.exs index 638c474..8bfabfa 100644 --- a/mix.exs +++ b/mix.exs @@ -24,19 +24,18 @@ defmodule Reaxt.Mixfile do def application do [applications: [:logger, :poolboy, :exos, :plug, :poison], - mod: {Reaxt.App,[]}, - env: [ - otp_app: :reaxt, #the OTP application containing compiled JS server - hot: false, # false | true | :client hot compilation and loading - ]] + mod: {Reaxt.App, []} + ] end defp deps do - [{:exos, "~> 2.0"}, + [ + {:exos, "~> 2.0"}, {:poolboy, "~> 1.5"}, {:plug, "~> 1.15"}, {:poison,"~> 5.0"}, - {:ex_doc, "~> 0.31", only: :dev, runtime: false}] + {:ex_doc, "~> 0.31", only: :dev, runtime: false} + ] end defp package do From 4427763ef82eaf3627f18bf45526727c89048ecd Mon Sep 17 00:00:00 2001 From: Louis VENTRE Date: Mon, 4 Nov 2024 18:54:56 +0100 Subject: [PATCH 6/6] Add mix format Add some docs --- .formatter.exs | 9 ++ lib/app.ex | 17 ++-- lib/esbuild/esbuild.ex | 4 +- lib/esbuild/tasks.esbuild.ex | 18 +++- lib/pool.ex | 40 +++++--- lib/reaxt_error.ex | 37 ++++++-- lib/reaxt_index.ex | 39 +++++--- lib/render.ex | 21 +++-- lib/tasks.reaxt.ex | 118 +++++++++++++---------- lib/utils.ex | 24 +++-- lib/webpack/tasks.webpack.ex | 39 +++++--- lib/webpack/webpack.ex | 176 +++++++++++++++++++++++------------ mix.exs | 24 +++-- 13 files changed, 369 insertions(+), 197 deletions(-) create mode 100644 .formatter.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..1b61e2d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,9 @@ +locals_without_parens = [ + plug: 1, + plug: 2 +] + +[ + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: locals_without_parens +] diff --git a/lib/app.ex b/lib/app.ex index e49e913..edb1246 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -2,12 +2,13 @@ defmodule Reaxt.App do use Application require Logger - def start(_,_) do - hot_reload_processes = if Reaxt.Utils.is_hot?() do - hot_processes(Reaxt.Utils.bundler) - else - [] - end + def start(_, _) do + hot_reload_processes = + if Reaxt.Utils.is_hot?() do + hot_processes(Reaxt.Utils.bundler()) + else + [] + end base_processes = [ Reaxt.PoolsSup @@ -25,11 +26,11 @@ defmodule Reaxt.App do [ WebPack.Hot.Events, WebPack.Hot.EventManager, - WebPack.Hot.Compiler, + WebPack.Hot.Compiler ] end def hot_processes(_) do - raise "[Reaxt] Hot reload is not supported for bundle #{inspect Reaxt.Utils.bundler()}" + raise "[Reaxt] Hot reload is not supported for the bundler #{inspect(Reaxt.Utils.bundler())}" end end diff --git a/lib/esbuild/esbuild.ex b/lib/esbuild/esbuild.ex index c3c212c..0a955a3 100644 --- a/lib/esbuild/esbuild.ex +++ b/lib/esbuild/esbuild.ex @@ -1,7 +1,9 @@ defmodule Reaxt.Esbuild do + @moduledoc """ + Utilities functions to fetch specific esbuild configs + """ def esbuild_config do Application.get_env(:reaxt, :esbuild_config, "build.js") end - end diff --git a/lib/esbuild/tasks.esbuild.ex b/lib/esbuild/tasks.esbuild.ex index 7e8f937..6725be6 100644 --- a/lib/esbuild/tasks.esbuild.ex +++ b/lib/esbuild/tasks.esbuild.ex @@ -10,6 +10,7 @@ defmodule Mix.Tasks.Esbuild.Compile do def compile() do config = "./" <> Reaxt.Esbuild.esbuild_config() + System.cmd( "node", [config], @@ -27,13 +28,20 @@ defmodule Mix.Tasks.Compile.ReaxtEsbuild do IO.puts("[Reaxt] Running Esbuild compiler...") Mix.Task.run("reaxt.validate", args ++ ["--reaxt-skip-compiler-check"]) - if !File.exists?(Path.join(Reaxt.Utils.web_app, "node_modules")) do + if !File.exists?(Path.join(Reaxt.Utils.web_app(), "node_modules")) do Mix.Task.run("npm.install", args) else - installed_version = Poison.decode!(File.read!("#{Reaxt.Utils.web_app}/node_modules/reaxt/package.json"))["version"] - current_version = Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))["version"] - if installed_version !== current_version, do: - Mix.Task.run("npm.install", args) + installed_version = + Poison.decode!(File.read!("#{Reaxt.Utils.web_app()}/node_modules/reaxt/package.json"))[ + "version" + ] + + current_version = + Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))[ + "version" + ] + + if installed_version !== current_version, do: Mix.Task.run("npm.install", args) end if !Reaxt.Utils.is_hot?() do diff --git a/lib/pool.ex b/lib/pool.ex index 7bf1f2f..02fabc3 100644 --- a/lib/pool.ex +++ b/lib/pool.ex @@ -1,4 +1,7 @@ defmodule Reaxt.PoolsSup do + @moduledoc """ + Supervision of multiple :poolboy instances for the Server Side Rendering + """ alias :poolboy, as: Pool use Supervisor @@ -9,7 +12,7 @@ defmodule Reaxt.PoolsSup do end def start_link(arg) do - Supervisor.start_link(__MODULE__,arg, name: __MODULE__) + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end def init(_) do @@ -19,21 +22,28 @@ defmodule Reaxt.PoolsSup do server_files = Path.wildcard("#{server_dir}/*.js") if server_files == [] do - Logger.error("#server JS not yet compiled in #{server_dir}, compile it before with `mix webpack.compile`") - throw {:error, :serverjs_not_compiled} + Logger.error( + "#server JS not yet compiled in #{server_dir}, compile it before with `mix webpack.compile`" + ) + + throw({:error, :serverjs_not_compiled}) else - children = Enum.map(server_files, fn server -> - parsed_js_name = server |> Path.basename(".js") |> String.replace(~r/[0-9][a-z][A-Z]/,"_") - pool_name = :"react_#{parsed_js_name}_pool" - - args = [ - worker_module: Reaxt.Render, - size: pool_size, - max_overflow: pool_overflow, - name: {:local, pool_name} - ] - Pool.child_spec(pool_name, args, server) - end) + children = + Enum.map(server_files, fn server -> + parsed_js_name = + server |> Path.basename(".js") |> String.replace(~r/[0-9][a-z][A-Z]/, "_") + + pool_name = :"react_#{parsed_js_name}_pool" + + args = [ + worker_module: Reaxt.Render, + size: pool_size, + max_overflow: pool_overflow, + name: {:local, pool_name} + ] + + Pool.child_spec(pool_name, args, server) + end) Supervisor.init(children, strategy: :one_for_one) end diff --git a/lib/reaxt_error.ex b/lib/reaxt_error.ex index a9a3944..b78459e 100644 --- a/lib/reaxt_error.ex +++ b/lib/reaxt_error.ex @@ -1,31 +1,50 @@ defmodule Reaxt.Error do - defexception [:message,:args,:js_render,:js_stack] + @moduledoc """ + Exception representing a Reaxt Error + """ + defexception [:message, :args, :js_render, :js_stack] - def exception({:handler_error,module,submodule,args,error,stack}) do + def exception({:handler_error, module, submodule, args, error, stack}) do params = %{ module: module, submodule: submodule, args: args } - %Reaxt.Error{message: "JS Handler Exception for #{inspect params}: #{error}", args: params, js_stack: (stack && parse_stack(stack))} + + %Reaxt.Error{ + message: "JS Handler Exception for #{inspect(params)}: #{error}", + args: params, + js_stack: stack && parse_stack(stack) + } end - def exception({:render_error,params,error,stack,js_render}) do - %Reaxt.Error{message: "JS Render Exception : #{error}", args: params, js_render: js_render, js_stack: (stack && parse_stack(stack))} + def exception({:render_error, params, error, stack, js_render}) do + %Reaxt.Error{ + message: "JS Render Exception : #{error}", + args: params, + js_render: js_render, + js_stack: stack && parse_stack(stack) + } end + def exception(rest) do - %Reaxt.Error{message: "JS Render Exception : #{inspect(rest)}", args: "", js_render: "", js_stack: ""} + %Reaxt.Error{ + message: "JS Render Exception : #{inspect(rest)}", + args: "", + js_render: "", + js_stack: "" + } end defp parse_stack(stack) do Regex.scan(~r/at (.*) \((.*):([0-9]*):[0-9]*\)/, stack) |> Enum.filter(fn [_, function, url, _line] -> - String.contains?(url, "/priv") and not (function in ["Port.next_term","Socket.read_term"]) + String.contains?(url, "/priv") and function not in ["Port.next_term", "Socket.read_term"] end) |> Enum.map(fn [_, function, url, line] -> {line, _} = Integer.parse(line) - [_, after_priv] = String.split(url,"/priv/", parts: 2) - {JS, :"#{function}", 0, file: '#{Reaxt.Utils.web_priv}/#{after_priv}', line: line} + [_, after_priv] = String.split(url, "/priv/", parts: 2) + {JS, :"#{function}", 0, file: ~c"#{Reaxt.Utils.web_priv()}/#{after_priv}", line: line} end) end end diff --git a/lib/reaxt_index.ex b/lib/reaxt_index.ex index b3967c4..f58ac7c 100644 --- a/lib/reaxt_index.ex +++ b/lib/reaxt_index.ex @@ -1,45 +1,62 @@ defmodule Reaxt.Index.Generator do - def webpack_config do - Application.get_env(:reaxt,:webpack_config,"webpack.config.js") - end + @moduledoc """ + Utils functions to overlaod the Reaxt.Index module for the WebPack configuration + """ def build_webpack_stats do if File.exists?("#{Reaxt.Utils.web_priv()}/webpack.stats.json") do all_stats = Poison.decode!(File.read!("#{Reaxt.Utils.web_priv()}/webpack.stats.json")) stats_array = all_stats["children"] - stats = Enum.map(stats_array, fn stats-> - %{assetsByChunkName: stats["assetsByChunkName"], - errors: stats["errors"], - warnings: stats["warnings"]} - end) + + stats = + Enum.map(stats_array, fn stats -> + %{ + assetsByChunkName: stats["assetsByChunkName"], + errors: stats["errors"], + warnings: stats["warnings"] + } + end) _ = Code.compiler_options(ignore_module_conflict: true) + defmodule Elixir.Reaxt.Index do @stats stats def stats, do: @stats + def file_of(name) do - r = Enum.find_value(WebPack.stats, fn %{assetsByChunkName: assets}-> assets["#{name}"] end) + r = + Enum.find_value(WebPack.stats(), fn %{assetsByChunkName: assets} -> + assets["#{name}"] + end) + case r do - [f|_]->f + [f | _] -> f f -> f end end - @header_script if(Reaxt.Utils.is_hot?(), do: ~s()) + @header_script if(Reaxt.Utils.is_hot?(), + do: ~s() + ) @header_global Poison.encode!(Reaxt.Render.get_global_config()) def header do "\n#{@header_script}" end end + _ = Code.compiler_options(ignore_module_conflict: false) end end end defmodule Elixir.Reaxt.Index do + @moduledoc """ + Functions to help construct the index.html to serve + """ def stats, do: %{assetsByChunkName: %{}} def file_of(_), do: nil + def header do global_config = Poison.encode!(Reaxt.Render.get_global_config()) "" diff --git a/lib/render.ex b/lib/render.ex index c406f40..7f408db 100644 --- a/lib/render.ex +++ b/lib/render.ex @@ -14,7 +14,7 @@ defmodule Reaxt.Render do end def render_result(chunk, {module, submodule}, data, timeout) do - Reaxt.PoolsSup.transaction(:"react_#{chunk}_pool", fn worker-> + Reaxt.PoolsSup.transaction(:"react_#{chunk}_pool", fn worker -> GenServer.call(worker, {:render, module, submodule, data, timeout}, timeout + 100) end) end @@ -23,26 +23,29 @@ defmodule Reaxt.Render do case render_result(chunk, module, data, timeout) do {:ok, res} -> res + {:error, err} -> try do raise(Reaxt.Error, err) - rescue ex -> - [_ | stack] = __STACKTRACE__ - # stack = List.wrap(ex[:js_stack]) |> Enum.concat(stack) - reraise ex, stack + rescue + ex -> + [_ | stack] = __STACKTRACE__ + # stack = List.wrap(ex[:js_stack]) |> Enum.concat(stack) + reraise ex, stack end end end - def render(module,data, timeout \\ 5_000) do + def render(module, data, timeout \\ 5_000) do try do - render!(module,data,timeout) + render!(module, data, timeout) rescue ex -> case ex do %{js_render: js_render} when is_binary(js_render) -> Logger.error(Exception.message(ex)) - %{css: "",html: "", js_render: js_render} + %{css: "", html: "", js_render: js_render} + _ -> reraise ex, __STACKTRACE__ end @@ -57,6 +60,6 @@ defmodule Reaxt.Render do def start_link(server_path) do init = Poison.encode!(Application.get_env(:reaxt, :global_config, nil)) - Exos.Proc.start_link("node #{server_path}", init, [cd: '#{Reaxt.Utils.web_priv}']) + Exos.Proc.start_link("node #{server_path}", init, cd: ~c"#{Reaxt.Utils.web_priv()}") end end diff --git a/lib/tasks.reaxt.ex b/lib/tasks.reaxt.ex index 2034016..2960083 100644 --- a/lib/tasks.reaxt.ex +++ b/lib/tasks.reaxt.ex @@ -1,13 +1,25 @@ defmodule Mix.Tasks.Npm.Install do + @moduledoc """ + Mix task to install npm dependencies and the reaxt js server + """ use Mix.Task @shortdoc "`npm install` in web_dir + npm install server side dependencies" def run(_args) do - System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: Reaxt.Utils.web_app) + System.cmd("npm", ["install"], into: IO.stream(:stdio, :line), cd: Reaxt.Utils.web_app()) + # TOIMPROVE- did not found a better hack to avoid npm install symlink : first make a tar gz package, then npm install it - reaxt_tgz = "#{System.tmp_dir}/reaxt.tgz" - System.cmd("tar", ["zcf", reaxt_tgz, "commonjs_reaxt"],into: IO.stream(:stdio, :line), cd: "#{:code.priv_dir(:reaxt)}") - System.cmd("npm",["install", "--no-save", reaxt_tgz], into: IO.stream(:stdio, :line), cd: Reaxt.Utils.web_app) + reaxt_tgz = "#{System.tmp_dir()}/reaxt.tgz" + + System.cmd("tar", ["zcf", reaxt_tgz, "commonjs_reaxt"], + into: IO.stream(:stdio, :line), + cd: "#{:code.priv_dir(:reaxt)}" + ) + + System.cmd("npm", ["install", "--no-save", reaxt_tgz], + into: IO.stream(:stdio, :line), + cd: Reaxt.Utils.web_app() + ) end end @@ -22,62 +34,70 @@ defmodule Mix.Tasks.Reaxt.Validate do end def validate(args) do - if Reaxt.Utils.bundler() not in [:webpack, :esbuild], do: - Mix.raise """ - Reaxt :bundler is not configured. - Add following to config.exs + if Reaxt.Utils.bundler() not in [:webpack, :esbuild], + do: + Mix.raise(""" + Reaxt :bundler is not configured. + Add following to config.exs - config :reaxt, :bundler, :webpack - OR - config :reaxt, :bundler, :esbuild - """ + config :reaxt, :bundler, :webpack + OR + config :reaxt, :bundler, :esbuild + """) - if Reaxt.Utils.web_priv == :no_app_specified, do: - Mix.raise """ - Reaxt :otp_app is not configured. - Add following to config.exs + if Reaxt.Utils.web_priv() == :no_app_specified, + do: + Mix.raise(""" + Reaxt :otp_app is not configured. + Add following to config.exs - config :reaxt, :otp_app, :your_app + config :reaxt, :otp_app, :your_app - """ + """) packageJsonPath = Path.join(Reaxt.Utils.web_app(), "package.json") - if not File.exists?(packageJsonPath), do: - Mix.raise """ - Reaxt could not find a package.json in #{Reaxt.Utils.web_app}. - Add package.json to #{Reaxt.Utils.web_app} or configure a new - web_app directory in config.exs: - config :reaxt, :web_app, "webapp" + if not File.exists?(packageJsonPath), + do: + Mix.raise(""" + Reaxt could not find a package.json in #{Reaxt.Utils.web_app()}. + Add package.json to #{Reaxt.Utils.web_app()} or configure a new + web_app directory in config.exs: + + config :reaxt, :web_app, "webapp" - """ + """) packageJson = Poison.decode!(File.read!(packageJsonPath)) - if packageJson["devDependencies"]["webpack"] == nil, do: - Mix.raise """ - Reaxt requires webpack as a devDependency in #{packageJsonPath}. - Add a dependency to 'webpack' like: - - { - devDependencies: { - "webpack": "^1.4.13" - } - } - """ + + if packageJson["devDependencies"]["webpack"] == nil, + do: + Mix.raise(""" + Reaxt requires webpack as a devDependency in #{packageJsonPath}. + Add a dependency to 'webpack' like: + + { + devDependencies: { + "webpack": "^1.4.13" + } + } + """) good_compilers = [:reaxt_webpack, :reaxt_esbuild] - if (Enum.all?(args, &(&1 != "--reaxt-skip-compiler-check")) - and not Enum.any?((Mix.Project.get!).project[:compilers], &(&1 in good_compilers))), do: - Mix.raise """ - Reaxt has a built in compiler that compiles the web app. - Remember to add it to the list of compilers in mix.exs: - - def project do - [... - app: :your_app, - compilers: [:reaxt_webpack] ++ Mix.compilers, - ...] - end - """ + + if Enum.all?(args, &(&1 != "--reaxt-skip-compiler-check")) and + not Enum.any?(Mix.Project.get!().project[:compilers], &(&1 in good_compilers)), + do: + Mix.raise(""" + Reaxt has a built in compiler that compiles the web app. + Remember to add it to the list of compilers in mix.exs: + + def project do + [... + app: :your_app, + compilers: [:reaxt_webpack] ++ Mix.compilers, + ...] + end + """) end end diff --git a/lib/utils.ex b/lib/utils.ex index d03efd0..46beeb9 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -1,29 +1,45 @@ defmodule Reaxt.Utils do + @moduledoc """ + + """ + + @doc "Return the priv directory of the otp_app using Reaxt" def web_priv do - case Application.get_env :reaxt, :otp_app, :no_app_specified do + case Application.get_env(:reaxt, :otp_app, :no_app_specified) do :no_app_specified -> :no_app_specified + web_app -> :code.priv_dir(web_app) end end + @doc "Return the name of the directory containing the Web application" + def web_app do + Application.get_env(:reaxt, :web_app, "web") + end + + @doc "Return the bundler used by Reaxt. Default is :webpack" def bundler() do - Application.get_env(:reaxt, :bundler) + Application.get_env(:reaxt, :bundler, :webpack) end + @doc "Return true if the configured bundler is webpack" def is_webpack?() do bundler() == :webpack end + @doc "Return true if the Hot reload capacity is enabled" def is_hot?() do Application.get_env(:reaxt, :hot, false) end + @doc "Return the path to the react_servers directory" def server_dir() do Application.get_env(:reaxt, :server_dir, "react_servers") end + @doc "Return the size of the Poolboy pool for SSR" def pool_size() do Application.get_env(:reaxt, :pool_size, 1) end @@ -31,8 +47,4 @@ defmodule Reaxt.Utils do def max_pool_overflow() do Application.get_env(:reaxt, :pool_max_overflow, 5) end - - def web_app do - Application.get_env(:reaxt, :web_app, "web") - end end diff --git a/lib/webpack/tasks.webpack.ex b/lib/webpack/tasks.webpack.ex index 3286f67..5612c29 100644 --- a/lib/webpack/tasks.webpack.ex +++ b/lib/webpack/tasks.webpack.ex @@ -4,9 +4,14 @@ defmodule Mix.Tasks.Webpack.Analyseapp do @shortdoc "Generate webpack stats analysing application, resulting priv/static is meant to be versionned" def run(_args) do File.rm_rf!("priv/static") - {_,0} = System.cmd("git",["clone","-b","ajax-sse-loading","https://github.com/awetzel/analyse"], into: IO.stream(:stdio, :line)) - {_,0} = System.cmd("npm",["install"], into: IO.stream(:stdio, :line), cd: "analyse") - {_,0} = System.cmd("grunt",[], into: IO.stream(:stdio, :line), cd: "analyse") + + {_, 0} = + System.cmd("git", ["clone", "-b", "ajax-sse-loading", "https://github.com/awetzel/analyse"], + into: IO.stream(:stdio, :line) + ) + + {_, 0} = System.cmd("npm", ["install"], into: IO.stream(:stdio, :line), cd: "analyse") + {_, 0} = System.cmd("grunt", [], into: IO.stream(:stdio, :line), cd: "analyse") File.cp_r!("analyse/dist", "priv/static") File.rm_rf!("analyse") end @@ -24,8 +29,9 @@ defmodule Mix.Tasks.Webpack.Compile do File.write!("priv/webpack.stats.json", json) {:ok, []} - {ret, x} when x in [1,2] -> + {ret, x} when x in [1, 2] -> require Logger + ret |> Poison.decode!() |> Map.fetch!("errors") @@ -33,13 +39,15 @@ defmodule Mix.Tasks.Webpack.Compile do bin when is_binary(bin) -> Logger.error(bin) %{"message" => bin} when is_binary(bin) -> Logger.error(bin) end) - {:error,[]} + + {:error, []} end end def compile() do - config = "./"<>Reaxt.Webpack.webpack_config() + config = "./" <> Reaxt.Webpack.webpack_config() webpack = @webpack + System.cmd( "node", [webpack, "--config", config, "--json"], @@ -57,16 +65,23 @@ defmodule Mix.Tasks.Compile.ReaxtWebpack do IO.puts("[Reaxt] Running Webpack compiler...") Mix.Task.run("reaxt.validate", args ++ ["--reaxt-skip-compiler-check"]) - if !File.exists?(Path.join(Reaxt.Utils.web_app, "node_modules")) do + if !File.exists?(Path.join(Reaxt.Utils.web_app(), "node_modules")) do Mix.Task.run("npm.install", args) else - installed_version = Poison.decode!(File.read!("#{Reaxt.Utils.web_app}/node_modules/reaxt/package.json"))["version"] - current_version = Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))["version"] - if installed_version !== current_version, do: - Mix.Task.run("npm.install", args) + installed_version = + Poison.decode!(File.read!("#{Reaxt.Utils.web_app()}/node_modules/reaxt/package.json"))[ + "version" + ] + + current_version = + Poison.decode!(File.read!("#{:code.priv_dir(:reaxt)}/commonjs_reaxt/package.json"))[ + "version" + ] + + if installed_version !== current_version, do: Mix.Task.run("npm.install", args) end - if !Application.get_env(:reaxt,:hot) do + if !Application.get_env(:reaxt, :hot) do Mix.Task.run("webpack.compile", args) else {:ok, []} diff --git a/lib/webpack/webpack.ex b/lib/webpack/webpack.ex index 4f0096f..e513195 100644 --- a/lib/webpack/webpack.ex +++ b/lib/webpack/webpack.ex @@ -1,6 +1,10 @@ defmodule Reaxt.Webpack do + @moduledoc """ + Utilities functions to fetch specific Webpack configs + """ + def webpack_config do - Application.get_env(:reaxt,:webpack_config,"webpack.config.js") + Application.get_env(:reaxt, :webpack_config, "webpack.config.js") end end @@ -11,22 +15,35 @@ defmodule WebPack.Hot.Events do @dispatch_key :events def register! do - {:ok, _} = Registry.register(__MODULE__,@dispatch_key,nil) + {:ok, _} = Registry.register(__MODULE__, @dispatch_key, nil) end + def dispatch(event) do Registry.dispatch(__MODULE__, @dispatch_key, fn entries -> - for {pid, nil} <- entries, do: send(pid,{:event,event}) + for {pid, nil} <- entries, do: send(pid, {:event, event}) end) end import Plug.Conn + def stream_chunks(conn) do register!() - conn = Stream.repeatedly(fn-> receive do {:event,event}-> event end end) + + conn = + Stream.repeatedly(fn -> + receive do + {:event, event} -> event + end + end) |> Enum.reduce_while(conn, fn event, conn -> io = "event: #{event.event}\ndata: #{Poison.encode!(event)}\n\n" - case chunk(conn,io) do {:ok,conn}->{:cont,conn};{:error,:closed}->{:halt,conn} end + + case chunk(conn, io) do + {:ok, conn} -> {:cont, conn} + {:error, :closed} -> {:halt, conn} + end end) + halt(conn) end end @@ -46,49 +63,61 @@ defmodule WebPack.Plug.Static do plug :wait_compilation def init(static_opts), do: Plug.Static.init(static_opts) + def call(conn, opts) do conn = plug_builder_call(conn, opts) - if !conn.halted, do: static_plug(conn,opts), else: conn + if !conn.halted, do: static_plug(conn, opts), else: conn end - def wait_compilation(conn,_) do - if Application.get_env(:reaxt,:hot) do + def wait_compilation(conn, _) do + if Application.get_env(:reaxt, :hot) do try do - :ok = GenServer.call(WebPack.Hot.EventManager,:wait?,30_000) + :ok = GenServer.call(WebPack.Hot.EventManager, :wait?, 30_000) catch - :exit,{:timeout,_} -> :ok + :exit, {:timeout, _} -> :ok end end + conn end - def static_plug(conn,static_opts) do - Plug.Static.call(conn,static_opts) + def static_plug(conn, static_opts) do + Plug.Static.call(conn, static_opts) end get "/webpack/stats.json" do conn |> put_resp_content_type("application/json") - |> send_file(200,"#{Reaxt.Utils.web_priv}/webpack.stats.json") + |> send_file(200, "#{Reaxt.Utils.web_priv()}/webpack.stats.json") |> halt end - get "/webpack", do: %{conn|path_info: ["webpack","static","index.html"]} + + get("/webpack", do: %{conn | path_info: ["webpack", "static", "index.html"]}) + get "/webpack/events" do - conn=conn - |> put_resp_header("content-type", "text/event-stream") - |> send_chunked(200) + conn = + conn + |> put_resp_header("content-type", "text/event-stream") + |> send_chunked(200) + hot? = Reaxt.Utils.is_hot?() if hot? == :client, do: chunk(conn, "event: hot\ndata: nothing\n\n") - if hot? do WebPack.Hot.Events.stream_chunks(conn) else conn end + + if hot? do + WebPack.Hot.Events.stream_chunks(conn) + else + conn + end end + get "/webpack/client.js" do conn |> put_resp_content_type("application/javascript") - |> send_file(200,"#{Reaxt.Utils.web_app}/node_modules/reaxt/webpack_client.js") + |> send_file(200, "#{Reaxt.Utils.web_app()}/node_modules/reaxt/webpack_client.js") |> halt end - match _, do: conn + match(_, do: conn) end defmodule WebPack.StartBlocker do @@ -99,82 +128,111 @@ defmodule WebPack.StartBlocker do def child_spec(arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}, restart: :temporary} end - def start_link(timeout) do :proc_lib.start_link(__MODULE__,:wait, [timeout]) end + + def start_link(timeout) do + :proc_lib.start_link(__MODULE__, :wait, [timeout]) + end + def wait(timeout) do - :ok = GenServer.call(WebPack.EventManager,:wait?,timeout) - :proc_lib.init_ack({:ok,self()}) + :ok = GenServer.call(WebPack.EventManager, :wait?, timeout) + :proc_lib.init_ack({:ok, self()}) end end defmodule WebPack.Hot.EventManager do use GenServer require Logger - def start_link(_) do GenServer.start_link(__MODULE__,[], name: __MODULE__) end + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end def init([]) do WebPack.Hot.Events.register!() - {:ok,%{init: true,pending: [], compiling: false, compiled: false}} + {:ok, %{init: true, pending: [], compiling: false, compiled: false}} end - def handle_call(:wait?,_from,%{compiling: false}=state), do: - {:reply,:ok,state} - def handle_call(:wait?,from,state), do: - {:noreply,%{state|pending: [from|state.pending]}} + def handle_call(:wait?, _from, %{compiling: false} = state), do: {:reply, :ok, state} + def handle_call(:wait?, from, state), do: {:noreply, %{state | pending: [from | state.pending]}} - def handle_info({:event,%{event: "client_done"}=ev},state) do + def handle_info({:event, %{event: "client_done"} = ev}, state) do Logger.info("[reaxt-webpack] client done, build_stats") _ = Reaxt.Index.Generator.build_webpack_stats() + if(!state.init) do Logger.info("[reaxt-webpack] client done, restart servers") :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.PoolsSup) - {:ok,_} = Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) + {:ok, _} = Supervisor.restart_child(Reaxt.App, Reaxt.PoolsSup) end - _ = case ev do - %{error: "soft fail", error_details: details} -> - _ = Logger.error("[reaxt-webpack] soft fail compiling server_side JS") - _ = Enum.each(details.errors, fn - bin when is_binary(bin) -> Logger.error(bin) - %{message: bin} when is_binary(bin) -> Logger.error(bin) - end) - - %{error: other} -> - _ = Logger.error("[reaxt-webpack] error compiling server_side JS : #{other}") - _ = System.halt(1) - - _ -> :ok - end - for {_idx, build} <- Reaxt.Index.stats, error<-build.errors, do: Logger.warning(error) - for {_idx, build} <- Reaxt.Index.stats, warning<-build.warnings, do: Logger.warning(warning) - {:noreply,done(state)} + + _ = + case ev do + %{error: "soft fail", error_details: details} -> + _ = Logger.error("[reaxt-webpack] soft fail compiling server_side JS") + + _ = + Enum.each(details.errors, fn + bin when is_binary(bin) -> Logger.error(bin) + %{message: bin} when is_binary(bin) -> Logger.error(bin) + end) + + %{error: other} -> + _ = Logger.error("[reaxt-webpack] error compiling server_side JS : #{other}") + _ = System.halt(1) + + _ -> + :ok + end + + for {_idx, build} <- Reaxt.Index.stats(), error <- build.errors, do: Logger.warning(error) + + for {_idx, build} <- Reaxt.Index.stats(), + warning <- build.warnings, + do: Logger.warning(warning) + + {:noreply, done(state)} end - def handle_info({:event,%{event: "client_invalid"}},%{compiling: false}=state) do + def handle_info({:event, %{event: "client_invalid"}}, %{compiling: false} = state) do Logger.info("[reaxt-webpack] detect client file change") - {:noreply,%{state|compiling: true, compiled: false}} + {:noreply, %{state | compiling: true, compiled: false}} end - def handle_info({:event,%{event: "done"}},state) do + + def handle_info({:event, %{event: "done"}}, state) do Logger.info("[reaxt-webpack] both done !") - {:noreply,state} + {:noreply, state} end - def handle_info({:event,ev},state) do + + def handle_info({:event, ev}, state) do Logger.info("[reaxt-webpack] event : #{ev[:event]}") - {:noreply,state} + {:noreply, state} end def done(state) do - for from<-state.pending do GenServer.reply(from,:ok) end + for from <- state.pending do + GenServer.reply(from, :ok) + end + WebPack.Hot.Events.dispatch(%{event: "done"}) - %{state| pending: [], init: false, compiling: false, compiled: true} + %{state | pending: [], init: false, compiling: false, compiled: true} end end defmodule WebPack.Hot.Compiler do def child_spec(arg) do - %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } + %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}} end + def start_link(_) do cmd = "node ./node_modules/reaxt/webpack_hot_server #{Reaxt.Webpack.webpack_config()}" - hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" - Exos.Proc.start_link(cmd<>hot_arg,[],[cd: Reaxt.Utils.web_app],[name: __MODULE__],&WebPack.Hot.Events.dispatch/1) + hot_arg = if Application.get_env(:reaxt, :hot) == :client, do: " hot", else: "" + + Exos.Proc.start_link( + cmd <> hot_arg, + [], + [cd: Reaxt.Utils.web_app()], + [name: __MODULE__], + &WebPack.Hot.Events.dispatch/1 + ) end end diff --git a/mix.exs b/mix.exs index 8bfabfa..9636eec 100644 --- a/mix.exs +++ b/mix.exs @@ -18,23 +18,21 @@ defmodule Reaxt.Mixfile do elixir: "~> 1.12", deps: deps(), docs: docs(), - source_url: git_repository(), + source_url: git_repository() ] end def application do - [applications: [:logger, :poolboy, :exos, :plug, :poison], - mod: {Reaxt.App, []} - ] + [applications: [:logger, :poolboy, :exos, :plug, :poison], mod: {Reaxt.App, []}] end defp deps do [ - {:exos, "~> 2.0"}, - {:poolboy, "~> 1.5"}, - {:plug, "~> 1.15"}, - {:poison,"~> 5.0"}, - {:ex_doc, "~> 0.31", only: :dev, runtime: false} + {:exos, "~> 2.0"}, + {:poolboy, "~> 1.5"}, + {:plug, "~> 1.15"}, + {:poison, "~> 5.0"}, + {:ex_doc, "~> 0.31", only: :dev, runtime: false} ] end @@ -43,9 +41,9 @@ defmodule Reaxt.Mixfile do licenses: ["The MIT License (MIT)"], links: %{ "GitHub" => git_repository(), - "Changelog" => "https://hexdocs.pm/reaxt/changelog.html", + "Changelog" => "https://hexdocs.pm/reaxt/changelog.html" }, - maintainers: ["Arnaud Wetzel"], + maintainers: ["Arnaud Wetzel"] ] end @@ -53,12 +51,12 @@ defmodule Reaxt.Mixfile do [ extras: [ "CHANGELOG.md": [title: "Changelog"], - "README.md": [title: "Overview"], + "README.md": [title: "Overview"] ], api_reference: false, main: "readme", source_url: git_repository(), - source_ref: "v#{version()}", + source_ref: "v#{version()}" ] end