Current section

Files

Jump to
libero src libero@codegen_stubs.erl
Raw

src/libero@codegen_stubs.erl

-module(libero@codegen_stubs).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/codegen_stubs.gleam").
-export([write_endpoint_client_stubs/3, write_config/1, write_ssr_flags/1]).
-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(
" Per-client RPC stub, config, and SSR-flag generators.\n"
"\n"
" Emits the files that ship inside each consumer client package:\n"
" `messages.gleam` (typed RPC stubs), `rpc_config.gleam` + `_ffi.mjs`\n"
" (WebSocket URL resolution), and `ssr.gleam` + `ssr_ffi.mjs` (SSR flag\n"
" reader).\n"
).
-file("src/libero/codegen_stubs.gleam", 20).
?DOC(
" Generate typed client stubs from handler endpoints.\n"
" Each endpoint becomes a function that sends a ClientMsg variant\n"
" and decodes the response to RpcData(payload, domain).\n"
).
-spec write_endpoint_client_stubs(
list(libero@scanner:handler_endpoint()),
binary(),
binary()
) -> {ok, nil} | {error, libero@gen_error:gen_error()}.
write_endpoint_client_stubs(Endpoints, Client_generated, Wire_module_tag) ->
Shared_type_imports = libero@codegen:collect_endpoint_type_imports(
Endpoints,
true
),
Client_msg_variants = libero@codegen:emit_client_msg_variants(Endpoints),
Endpoint_blocks = gleam@list:map(
Endpoints,
fun(E) ->
Fn_name = erlang:element(3, E),
Variant_name = libero@codegen:to_pascal_case(erlang:element(3, E)),
Payload_type = libero@field_type:to_gleam_source(
erlang:element(4, E)
),
Error_type = libero@field_type:to_gleam_source(erlang:element(5, E)),
Fn_params = case erlang:element(6, E) of
[] ->
<<<<<<<<" on_response on_response: fn(RpcData("/utf8,
Payload_type/binary>>/binary,
", "/utf8>>/binary,
Error_type/binary>>/binary,
")) -> msg,"/utf8>>;
Params ->
Param_lines = gleam@list:map(
Params,
fun(P) ->
<<<<<<<<<<<<" "/utf8,
(erlang:element(1, P))/binary>>/binary,
" "/utf8>>/binary,
(erlang:element(1, P))/binary>>/binary,
": "/utf8>>/binary,
(libero@field_type:to_gleam_source(
erlang:element(2, P)
))/binary>>/binary,
","/utf8>>
end
),
<<<<<<<<<<(gleam@string:join(Param_lines, <<"\n"/utf8>>))/binary,
"\n on_response on_response: fn(RpcData("/utf8>>/binary,
Payload_type/binary>>/binary,
", "/utf8>>/binary,
Error_type/binary>>/binary,
")) -> msg,"/utf8>>
end,
Msg_construct = libero@codegen:variant_pattern(
Variant_name,
erlang:element(6, E)
),
<<<<<<<<<<<<<<<<<<<<<<<<<<<<"@external(javascript, \"./rpc_decoders_ffi.mjs\", \"decode_response_"/utf8,
Fn_name/binary>>/binary,
"\")\n"/utf8>>/binary,
"fn decode_response_"/utf8>>/binary,
Fn_name/binary>>/binary,
"(raw: Dynamic) -> Dynamic\n\n"/utf8>>/binary,
"pub fn "/utf8>>/binary,
Fn_name/binary>>/binary,
"(\n"/utf8>>/binary,
Fn_params/binary>>/binary,
"\n) -> Effect(msg) {\n send("/utf8>>/binary,
Msg_construct/binary>>/binary,
", fn(raw) { on_response(wire.coerce(decode_response_"/utf8>>/binary,
Fn_name/binary>>/binary,
"(raw))) })\n}"/utf8>>
end
),
Option_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_option/1,
<<"import gleam/option.{type Option}"/utf8>>
),
Dict_import = libero@codegen:import_if(
Endpoints,
fun libero@codegen:is_dict/1,
<<"import gleam/dict.{type Dict}"/utf8>>
),
Content = <<<<<<<<<<<<<<<<<<<<<<"//// Code generated by libero. DO NOT EDIT.
import generated/rpc_config
import generated/rpc_decoders
import gleam/dynamic.{type Dynamic}
import libero/remote_data.{type RpcData}
import libero/rpc
import libero/wire
import lustre/effect.{type Effect}"/utf8,
Option_import/binary>>/binary,
Dict_import/binary>>/binary,
"
"/utf8>>/binary,
(gleam@string:join(
Shared_type_imports,
<<"\n"/utf8>>
))/binary>>/binary,
"
pub type ClientMsg {
"/utf8>>/binary,
(gleam@string:join(
Client_msg_variants,
<<"\n"/utf8>>
))/binary>>/binary,
"
}
"/utf8>>/binary,
(gleam@string:join(Endpoint_blocks, <<"\n\n"/utf8>>))/binary>>/binary,
"
fn send(msg: ClientMsg, on_response: fn(Dynamic) -> msg) -> Effect(msg) {
let _ = rpc_decoders.ensure_decoders
rpc.send(
url: rpc_config.ws_url(),
module: \""/utf8>>/binary,
Wire_module_tag/binary>>/binary,
"\",
msg: msg,
on_response: on_response,
)
}
"/utf8>>,
Output = <<Client_generated/binary, "/messages.gleam"/utf8>>,
libero@codegen:ensure_parent_dir(Output),
libero@codegen:write_file(Output, Content).
-file("src/libero/codegen_stubs.gleam", 161).
?DOC(
" Write the rpc_config_ffi.mjs resolver invoked by `resolve_ws_url` in\n"
" the generated Gleam config module.\n"
).
-spec write_config_ffi(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_config_ffi(Config) ->
Ffi_path = gleam@string:replace(
erlang:element(5, Config),
<<".gleam"/utf8>>,
<<"_ffi.mjs"/utf8>>
),
Ffi_content = <<"// Code generated by libero. DO NOT EDIT.
//
// Resolves a WebSocket URL from the browser's current location + path.
// Used by the generated rpc_config.gleam.
export function resolveWsUrl(path) {
const protocol = globalThis.location?.protocol === \"https:\" ? \"wss:\" : \"ws:\";
const host = globalThis.location?.host ?? \"localhost\";
return protocol + \"//\" + host + path;
}
"/utf8>>,
libero@codegen:write_file(Ffi_path, Ffi_content).
-file("src/libero/codegen_stubs.gleam", 137).
?DOC(
" Generate the client-side `rpc_config.gleam` plus its `_ffi.mjs`\n"
" resolver.\n"
).
-spec write_config(libero@config:config()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_config(Config) ->
Content = <<<<"//// Code generated by libero. DO NOT EDIT.
////
//// WebSocket endpoint resolved at runtime from the browser's location.
//// The scheme (ws/wss) and host are inferred from window.location so
//// one compiled bundle works across all subdomains.
pub fn ws_url() -> String {
resolve_ws_url(\""/utf8,
(erlang:element(2, Config))/binary>>/binary,
"\")
}
@external(javascript, \"./rpc_config_ffi.mjs\", \"resolveWsUrl\")
fn resolve_ws_url(_path: String) -> String {
panic as \"resolve_ws_url requires a browser environment (window.location)\"
}
"/utf8>>,
Output = erlang:element(5, Config),
libero@codegen:ensure_parent_dir(Output),
gleam@result:'try'(
libero@codegen:write_file(Output, Content),
fun(_) -> write_config_ffi(Config) end
).
-file("src/libero/codegen_stubs.gleam", 181).
?DOC(
" Generate the client-side SSR flags reader (gleam + mjs).\n"
" Produces a read_flags() function that reads window.__LIBERO_FLAGS__\n"
" and returns it as a Dynamic value for ssr.decode_flags().\n"
).
-spec write_ssr_flags(binary()) -> {ok, nil} |
{error, libero@gen_error:gen_error()}.
write_ssr_flags(Client_generated) ->
Gleam_path = <<Client_generated/binary, "/ssr.gleam"/utf8>>,
Ffi_path = <<Client_generated/binary, "/ssr_ffi.mjs"/utf8>>,
Gleam_content = <<"//// Code generated by libero. DO NOT EDIT.
////
//// SSR flags reader for hydration. Reads the base64-encoded ETF
//// flags embedded by the server in window.__LIBERO_FLAGS__.
import gleam/dynamic.{type Dynamic}
/// Read the SSR flags embedded in the page by the server.
/// Returns a Dynamic value suitable for passing to ssr.decode_flags().
@external(javascript, \"./ssr_ffi.mjs\", \"readFlags\")
pub fn read_flags() -> Dynamic {
// Unreachable on the Erlang target - read_flags is client-only.
panic as \"ssr.read_flags requires a browser environment\"
}
"/utf8>>,
Ffi_content = <<"// Code generated by libero. DO NOT EDIT.
//
// Reads SSR flags embedded by the server for client hydration.
export function readFlags() {
return globalThis.__LIBERO_FLAGS__ ?? \"\";
}
"/utf8>>,
libero@codegen:ensure_parent_dir(Gleam_path),
gleam@result:'try'(
libero@codegen:write_file(Gleam_path, Gleam_content),
fun(_) -> libero@codegen:write_file(Ffi_path, Ffi_content) end
).