Current section
Files
Jump to
Current section
Files
src/libero@cli@templates.erl
-module(libero@cli@templates).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/cli/templates.gleam").
-export([starter_messages/0, starter_handler/0, starter_shared_state/0, starter_app_error/0, starter_test/0, starter_spa/1, starter_cli/0, starter_readme/2, gleam_toml/3, shared_gleam_toml/0, client_gleam_toml/3]).
-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(
" Template strings for `libero new` scaffolding.\n"
"\n"
" Each function returns a file's content as a String. The generated\n"
" files give a new project a minimal skeleton to build from.\n"
).
-file("src/libero/cli/templates.gleam", 63).
?DOC(
" Returns a skeleton messages module.\n"
"\n"
" Defines the typed RPC boundary between client and server.\n"
" Add your message types here — libero scans for MsgFromClient\n"
" and MsgFromServer to generate dispatch and client stubs.\n"
).
-spec starter_messages() -> binary().
starter_messages() ->
<<"/// Define your message types here.
/// Libero scans for MsgFromClient and MsgFromServer to generate
/// dispatch and client stubs.
///
/// Each MsgFromServer variant should have exactly one field.
/// Wrap it in Result(payload, error) so the client can use
/// remote_data.to_remote to handle responses.
pub type MsgFromClient {
Ping
}
pub type MsgFromServer {
Pong(Result(String, Nil))
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 83).
?DOC(" Returns a skeleton handler module.\n").
-spec starter_handler() -> binary().
starter_handler() ->
<<"import server/app_error.{type AppError}
import server/shared_state.{type SharedState}
import shared/messages.{type MsgFromClient, type MsgFromServer, Ping, Pong}
pub fn update_from_client(
msg msg: MsgFromClient,
state state: SharedState,
) -> Result(#(MsgFromServer, SharedState), AppError) {
case msg {
Ping -> Ok(#(Pong(Ok(\"pong\")), state))
}
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 100).
?DOC(" Returns a skeleton SharedState module.\n").
-spec starter_shared_state() -> binary().
starter_shared_state() ->
<<"pub type SharedState {
SharedState
}
pub fn new() -> SharedState {
SharedState
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 112).
?DOC(" Returns a skeleton AppError module.\n").
-spec starter_app_error() -> binary().
starter_app_error() ->
<<"pub type AppError {
AppError(reason: String)
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 120).
?DOC(" Returns a skeleton test that verifies the handler works.\n").
-spec starter_test() -> binary().
starter_test() ->
<<"import server/handler
import server/shared_state
import shared/messages.{Ping, Pong}
import gleeunit
pub fn main() {
gleeunit.main()
}
pub fn ping_test() {
let state = shared_state.new()
let assert Ok(#(Pong(Ok(\"pong\")), _)) =
handler.update_from_client(msg: Ping, state:)
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 139).
?DOC(" Returns a starter Lustre SPA app module with a working RPC example.\n").
-spec starter_spa(binary()) -> binary().
starter_spa(Name) ->
<<<<"import generated/messages as rpc
import libero/remote_data.{type RemoteData}
import lustre
import lustre/element.{type Element}
import lustre/element/html
import lustre/effect.{type Effect}
import lustre/event
import shared/messages.{Ping, Pong}
// -- Model --
pub type Model {
Model(response: RemoteData(String, remote_data.RpcFailure))
}
// -- Messages --
pub type Msg {
UserClickedPing
GotPong(RemoteData(String, remote_data.RpcFailure))
}
// -- Init --
fn init(_flags) -> #(Model, Effect(Msg)) {
#(Model(response: remote_data.NotAsked), effect.none())
}
// -- Update --
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
UserClickedPing -> #(
Model(response: remote_data.Loading),
send_ping(),
)
GotPong(rd) -> #(Model(response: rd), effect.none())
}
}
fn send_ping() -> Effect(Msg) {
// rpc.send_to_server sends a typed message to the server over WebSocket.
// The on_response callback receives the raw wire response. Use
// remote_data.to_remote to unwrap it into a RemoteData value that
// handles both RPC errors and domain errors.
rpc.send_to_server(msg: Ping, on_response: fn(raw) {
GotPong(remote_data.to_remote(raw:, format_domain: fn(_) { \"error\" }))
})
}
// -- View --
fn view(model: Model) -> Element(Msg) {
html.div([], [
html.h1([], [html.text(\""/utf8,
Name/binary>>/binary,
"\")]),
html.button([event.on_click(UserClickedPing)], [html.text(\"Ping\")]),
html.p([], [html.text(
case model.response {
remote_data.NotAsked -> \"Click the button to ping the server.\"
remote_data.Loading -> \"Loading...\"
remote_data.Success(msg) -> \"Server says: \" <> msg
remote_data.Failure(err) -> \"Error: \" <> err.message
},
)]),
])
}
// -- Main --
pub fn main() {
let app = lustre.application(init, update, view)
let assert Ok(_) = lustre.start(app, \"#app\", Nil)
Nil
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 239).
?DOC(" Returns a starter CLI main module.\n").
-spec starter_cli() -> binary().
starter_cli() ->
<<"import gleam/io
pub fn main() -> Nil {
io.println(\"Hello from your Libero app!\")
}
"/utf8>>.
-file("src/libero/cli/templates.gleam", 250).
?DOC(
" Returns a README.md for a new project.\n"
" `db_section` is optional database-specific content appended to the end.\n"
).
-spec starter_readme(binary(), binary()) -> binary().
starter_readme(Name, Db_section) ->
<<<<<<"# "/utf8, Name/binary>>/binary,
"
A Libero project.
## Getting started
Build the generated client and server code, then start the server:
```sh
gleam run -m libero -- build
gleam run
```
The server starts on the port configured in `gleam.toml` under
`[tools.libero]` (default 8080).
## Project structure
```
src/server/ Server-side Gleam code (handlers, shared state, etc.)
shared/ Types shared between server and clients (messages, models)
clients/ Client packages (SPA, CLI, etc.)
test/ Tests
```
## Commands
- `gleam run -m libero -- build` generates dispatch, client stubs, and
other derived code from your message types.
- `gleam run -m libero -- gen` regenerates only the derived code without
rebuilding everything.
- `gleam run -m libero -- add <name> --target <javascript|erlang>` adds
a new client package.
- `gleam test` runs the test suite.
## How it works
Define your message types in `shared/src/shared/messages.gleam`. The
`MsgFromClient` type lists every request the client can send; the
`MsgFromServer` type lists every response the server can return.
When you run `libero build`, it reads those types and generates:
- A dispatch module that routes incoming messages to your handler.
- Client stub functions so each client package can call the server
with typed arguments.
Your handler in `src/server/handler.gleam` pattern-matches on
`MsgFromClient` variants and returns `MsgFromServer` values. Shared
state (database connections, caches, etc.) is threaded through
`SharedState` so every handler call has access to it.
"/utf8>>/binary,
Db_section/binary>>.
-file("src/libero/cli/templates.gleam", 14).
?DOC(
" Returns gleam.toml content for a new project (the server package).\n"
" Libero config lives under the [tools.libero] section.\n"
" `db_deps` is inserted after the existing deps (e.g. pog, sqlight lines).\n"
" `extra_toml` is appended after the [tools.libero] section (e.g. [tools.marmot]).\n"
).
-spec gleam_toml(binary(), binary(), binary()) -> binary().
gleam_toml(Name, Db_deps, Extra_toml) ->
<<<<<<<<<<<<<<"name = \""/utf8, Name/binary>>/binary,
"\"
version = \"0.1.0\"
target = \"erlang\"
[dependencies]
gleam_stdlib = \">= 0.69.0 and < 1.0.0\"
gleam_erlang = \"~> 1.0\"
gleam_http = \"~> 4.0\"
mist = \"~> 6.0\"
lustre = \"~> 5.6\"
shared = { path = \"shared\" }
libero = \""/utf8>>/binary,
"~> 4.2"/utf8>>/binary,
"\"
"/utf8>>/binary,
Db_deps/binary>>/binary,
"
[dev-dependencies]
gleeunit = \"~> 1.0\"
[tools.libero]
port = 8080
"/utf8>>/binary,
Extra_toml/binary>>.
-file("src/libero/cli/templates.gleam", 43).
?DOC(
" Returns gleam.toml content for the shared package.\n"
" Target-agnostic so both the Erlang server and JS clients can import\n"
" messages and types from it.\n"
).
-spec shared_gleam_toml() -> binary().
shared_gleam_toml() ->
<<<<"name = \"shared\"
version = \"0.1.0\"
# No target specified - compiles to both Erlang and JavaScript.
[dependencies]
gleam_stdlib = \">= 0.69.0 and < 1.0.0\"
libero = \""/utf8,
"~> 4.2"/utf8>>/binary,
"\"
[dev-dependencies]
gleeunit = \"~> 1.0\"
"/utf8>>.
-file("src/libero/cli/templates.gleam", 218).
?DOC(" Returns a gleam.toml for a client package.\n").
-spec client_gleam_toml(binary(), binary(), binary()) -> binary().
client_gleam_toml(Name, Target, Root_package) ->
<<<<<<<<<<<<<<<<<<"name = \""/utf8, Name/binary>>/binary,
"\"
version = \"0.1.0\"
target = \""/utf8>>/binary,
Target/binary>>/binary,
"\"
[dependencies]
gleam_stdlib = \">= 0.69.0 and < 1.0.0\"
shared = { path = \"../../shared\" }
"/utf8>>/binary,
Root_package/binary>>/binary,
" = { path = \"../../\" }
libero = \""/utf8>>/binary,
"~> 4.2"/utf8>>/binary,
"\"
"/utf8>>/binary,
(case Target of
<<"javascript"/utf8>> ->
<<"lustre = \"~> 5.6\"\n"/utf8>>;
_ ->
<<""/utf8>>
end)/binary>>.