Current section
Files
Jump to
Current section
Files
src/plushie@dev_server.erl
-module(plushie@dev_server).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/plushie/dev_server.gleam").
-export([stop/1, start/1, start_supervised/1]).
-export_type([dev_message/0, dev_state/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Dev server: file watcher and recompiler for live reload.\n"
"\n"
" Watches source directories for `.gleam` file changes, runs\n"
" `gleam build`, detects changed BEAM modules, reloads them, and\n"
" tells the runtime to re-render. The UI updates without losing\n"
" application state.\n"
"\n"
" Started automatically when `dev: True` is set in StartOpts.\n"
"\n"
" Requires the `file_system` Hex package (an Elixir library) as a\n"
" project dependency, and Elixir installed in your environment.\n"
" Without it, the dev server starts but file watching is disabled\n"
" and a warning is logged. See the Getting Started guide for setup.\n"
).
-opaque dev_message() :: recompile |
{raw_file_event, gleam@dynamic:dynamic_()} |
shutdown.
-type dev_state() :: {dev_state,
gleam@erlang@process:subject(plushie@runtime:runtime_message()),
gleam@option:option(gleam@dynamic:dynamic_()),
boolean(),
list({gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()}),
gleam@erlang@process:subject(dev_message())}.
-file("src/plushie/dev_server.gleam", 143).
?DOC(
" Send a shutdown message to the dev server actor.\n"
"\n"
" Stops the file watcher process and exits the actor cleanly.\n"
" Call this explicitly during runtime shutdown to clean up the\n"
" file watcher; OTP supervisor shutdown signals do not trigger\n"
" custom message handlers.\n"
).
-spec stop(gleam@erlang@process:subject(dev_message())) -> nil.
stop(Subject) ->
gleam@erlang@process:send(Subject, shutdown).
-file("src/plushie/dev_server.gleam", 246).
?DOC(
" Check if a path is inside a top-level build output directory.\n"
" Matches \"/build/dev/\" or \"build/dev/\" but not \"/my_build/\".\n"
).
-spec is_build_output_dir(binary()) -> boolean().
is_build_output_dir(Path) ->
gleam_stdlib:contains_string(Path, <<"/build/dev/"/utf8>>) orelse gleam_stdlib:string_starts_with(
Path,
<<"build/dev/"/utf8>>
).
-file("src/plushie/dev_server.gleam", 236).
?DOC(
" Check if a path is a Gleam source file worth watching.\n"
" Excludes _build/ and build/ only when they appear as directory\n"
" boundaries, not as substrings of other directory names.\n"
).
-spec is_gleam_file(binary()) -> boolean().
is_gleam_file(Path) ->
((gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>) andalso not gleam_stdlib:contains_string(
Path,
<<"/_build/"/utf8>>
))
andalso not gleam_stdlib:string_starts_with(Path, <<"_build/"/utf8>>))
andalso not is_build_output_dir(Path).
-file("src/plushie/dev_server.gleam", 150).
-spec handle_message(dev_state(), dev_message()) -> gleam@otp@actor:next(dev_state(), dev_message()).
handle_message(State, Msg) ->
case Msg of
{raw_file_event, Raw} ->
case plushie_dev_server_ffi:extract_file_path(Raw) of
{ok, Path} ->
case is_gleam_file(Path) of
true ->
case erlang:element(4, State) of
true ->
gleam@otp@actor:continue(State);
false ->
gleam@erlang@process:send_after(
erlang:element(6, State),
100,
recompile
),
gleam@otp@actor:continue(
{dev_state,
erlang:element(2, State),
erlang:element(3, State),
true,
erlang:element(5, State),
erlang:element(6, State)}
)
end;
false ->
gleam@otp@actor:continue(State)
end;
{error, _} ->
gleam@otp@actor:continue(State)
end;
recompile ->
State@1 = {dev_state,
erlang:element(2, State),
erlang:element(3, State),
false,
erlang:element(5, State),
erlang:element(6, State)},
plushie_ffi:log_info(<<"plushie dev: recompiling..."/utf8>>),
gleam@erlang@process:send(
erlang:element(2, State@1),
{set_dev_overlay, {some, <<"Rebuilding..."/utf8>>}}
),
Output = plushie_ffi:gleam_build(),
case gleam_stdlib:contains_string(Output, <<"error"/utf8>>) of
true ->
plushie_ffi:log_error(
<<"plushie dev: build failed:\n"/utf8, Output/binary>>
),
gleam@erlang@process:send(
erlang:element(2, State@1),
{set_dev_overlay, {some, <<"Build failed"/utf8>>}}
),
gleam@otp@actor:continue(State@1);
false ->
New_mtimes = plushie_ffi:list_beam_files(
<<"build/dev/erlang"/utf8>>
),
Changed = plushie_dev_server_ffi:find_changed_modules(
erlang:element(5, State@1),
New_mtimes
),
case Changed of
[] ->
plushie_ffi:log_info(
<<"plushie dev: no modules changed"/utf8>>
),
gleam@erlang@process:send(
erlang:element(2, State@1),
{set_dev_overlay, none}
),
gleam@otp@actor:continue(
{dev_state,
erlang:element(2, State@1),
erlang:element(3, State@1),
erlang:element(4, State@1),
New_mtimes,
erlang:element(6, State@1)}
);
Modules ->
plushie_ffi:reload_modules(Modules),
gleam@erlang@process:send(
erlang:element(2, State@1),
force_rerender
),
plushie_ffi:log_info(
<<"plushie dev: reload complete"/utf8>>
),
gleam@otp@actor:continue(
{dev_state,
erlang:element(2, State@1),
erlang:element(3, State@1),
erlang:element(4, State@1),
New_mtimes,
erlang:element(6, State@1)}
)
end
end;
shutdown ->
case erlang:element(3, State) of
{some, Pid} ->
plushie_ffi:stop_file_watcher(Pid);
none ->
nil
end,
plushie_ffi:log_info(<<"plushie dev: stopped"/utf8>>),
gleam@otp@actor:stop()
end.
-file("src/plushie/dev_server.gleam", 87).
-spec start_actor(
gleam@erlang@process:subject(plushie@runtime:runtime_message())
) -> {ok, gleam@otp@actor:started(gleam@erlang@process:subject(dev_message()))} |
{error, gleam@otp@actor:start_error()}.
start_actor(Runtime) ->
Initial_mtimes = plushie_ffi:list_beam_files(<<"build/dev/erlang"/utf8>>),
Watcher = case plushie_ffi:start_file_watcher([<<"src/"/utf8>>]) of
{ok, Pid} ->
plushie_ffi:file_watcher_subscribe(Pid),
plushie_ffi:log_info(
<<"plushie dev: watching "/utf8,
(gleam@string:join([<<"src/"/utf8>>], <<", "/utf8>>))/binary>>
),
{some, Pid};
{error, Reason} ->
plushie_ffi:log_warning(<<"plushie dev: "/utf8, Reason/binary>>),
none
end,
_pipe@5 = gleam@otp@actor:new_with_initialiser(
5000,
fun(Self) ->
Initial_state = {dev_state,
Runtime,
Watcher,
false,
Initial_mtimes,
Self},
Selector = begin
_pipe = gleam_erlang_ffi:new_selector(),
gleam@erlang@process:select(_pipe, Self)
end,
Selector@1 = case Watcher of
{some, _} ->
_pipe@1 = Selector,
gleam@erlang@process:select_other(
_pipe@1,
fun(Msg) -> {raw_file_event, Msg} end
);
none ->
Selector
end,
_pipe@2 = gleam@otp@actor:initialised(Initial_state),
_pipe@3 = gleam@otp@actor:selecting(_pipe@2, Selector@1),
_pipe@4 = gleam@otp@actor:returning(_pipe@3, Self),
{ok, _pipe@4}
end
),
_pipe@6 = gleam@otp@actor:on_message(_pipe@5, fun handle_message/2),
gleam@otp@actor:start(_pipe@6).
-file("src/plushie/dev_server.gleam", 71).
?DOC(" Start the dev server actor, watching src/ for changes.\n").
-spec start(gleam@erlang@process:subject(plushie@runtime:runtime_message())) -> nil.
start(Runtime) ->
_ = start_actor(Runtime),
nil.
-file("src/plushie/dev_server.gleam", 80).
?DOC(
" Start the dev server under a supervisor.\n"
"\n"
" Returns `Started` for use as a supervisor child spec.\n"
).
-spec start_supervised(
gleam@erlang@process:subject(plushie@runtime:runtime_message())
) -> {ok, gleam@otp@actor:started(gleam@erlang@process:subject(dev_message()))} |
{error, gleam@otp@actor:start_error()}.
start_supervised(Runtime) ->
start_actor(Runtime).