-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).