aboutsummaryrefslogtreecommitdiff
path: root/src/lustre/cli/dev.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'src/lustre/cli/dev.gleam')
-rw-r--r--src/lustre/cli/dev.gleam211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/lustre/cli/dev.gleam b/src/lustre/cli/dev.gleam
new file mode 100644
index 0000000..75711bc
--- /dev/null
+++ b/src/lustre/cli/dev.gleam
@@ -0,0 +1,211 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import filepath
+import gleam/dict
+import gleam/io
+import gleam/result
+import gleam/string
+import glint.{type Command, CommandInput}
+import glint/flag
+import lustre/attribute.{attribute}
+import lustre/cli/esbuild
+import lustre/cli/project.{type Module, type Type, Fn, Named, Variable}
+import lustre/cli/utils.{guard, map, replace, try}
+import lustre/element
+import lustre/element/html.{html}
+import simplifile
+
+// COMMANDS --------------------------------------------------------------------
+
+pub fn run() -> Command(Nil) {
+ let description =
+ "
+
+ "
+
+ glint.command(fn(input) {
+ let CommandInput(flags: flags, ..) = input
+ let assert Ok(host) = flag.get_string(flags, "host")
+ let assert Ok(port) = flag.get_string(flags, "port")
+ let assert Ok(include_styles) = flag.get_bool(flags, "include-styles")
+
+ let result = {
+ use interface <- try(project.interface(), replace(BuildError))
+ use module <- try(
+ dict.get(interface.modules, interface.name),
+ replace(ModuleMissing(interface.name)),
+ )
+ use is_app <- result.try(check_is_lustre_app(interface.name, module))
+
+ let root = project.root()
+ let tempdir = filepath.join(root, "build/.lustre")
+ let _ = simplifile.create_directory_all(tempdir)
+
+ let entry =
+ case is_app {
+ True ->
+ " import { start } from '../dev/javascript/lustre/lustre.mjs';
+ import { main } from '../dev/javascript/${app_name}/${app_name}.mjs';
+
+ start(main(), ${container_id});
+ "
+ False ->
+ " import { main } from '../dev/javascript/${app_name}/${app_name}.mjs';
+
+ main();
+ "
+ }
+ |> string.replace("${app_name}", interface.name)
+ |> string.replace("${container_id}", "app")
+
+ let html = index_html(interface.name, "app", include_styles)
+
+ let assert Ok(_) = simplifile.write(tempdir <> "/entry.mjs", entry)
+ let assert Ok(_) = simplifile.write(tempdir <> "/index.html", html)
+
+ use _ <- try(
+ esbuild.bundle(
+ filepath.join(tempdir, "entry.mjs"),
+ filepath.join(tempdir, "index.mjs"),
+ False,
+ ),
+ map(BundleError),
+ )
+ use _ <- try(esbuild.serve(host, port), map(BundleError))
+
+ Ok(Nil)
+ }
+
+ case result {
+ Ok(_) -> Nil
+ Error(error) -> explain(error)
+ }
+ })
+ |> glint.description(description)
+ |> glint.count_args(glint.EqArgs(0))
+ |> glint.flag("host", {
+ let description = ""
+ let default = "0.0.0.0"
+
+ flag.string()
+ |> flag.default(default)
+ |> flag.description(description)
+ })
+ |> glint.flag("port", {
+ let description = ""
+ let default = "1234"
+
+ flag.string()
+ |> flag.default(default)
+ |> flag.description(description)
+ })
+ |> glint.flag("include-styles", {
+ let description = ""
+ let default = False
+
+ flag.bool()
+ |> flag.default(default)
+ |> flag.description(description)
+ })
+}
+
+// ERROR HANDLING --------------------------------------------------------------
+
+type Error {
+ BuildError
+ BundleError(esbuild.Error)
+ MainMissing(module: String)
+ MainIncorrectType(module: String, got: project.Type)
+ MainBadAppType(module: String, got: project.Type)
+ ModuleMissing(module: String)
+}
+
+fn explain(error: Error) -> Nil {
+ error
+ |> string.inspect
+ |> io.println
+}
+
+// STEPS -----------------------------------------------------------------------
+
+fn check_is_lustre_app(
+ module_path: String,
+ module: Module,
+) -> Result(Bool, Error) {
+ use main <- try(
+ dict.get(module.functions, "main"),
+ replace(MainMissing(module_path)),
+ )
+ use <- guard(
+ main.parameters != [],
+ MainIncorrectType(module_path, Fn(main.parameters, main.return)),
+ )
+
+ case main.return {
+ Named(
+ name: "App",
+ package: "lustre",
+ module: "lustre",
+ parameters: [flags, ..],
+ ) ->
+ case is_compatible_flags_type(flags) {
+ True -> Ok(True)
+ False -> Error(MainBadAppType(module_path, main.return))
+ }
+
+ _ -> Ok(False)
+ }
+}
+
+// UTILS -----------------------------------------------------------------------
+
+fn index_html(
+ app_name: String,
+ container_id: String,
+ include_styles: Bool,
+) -> String {
+ let styles = case include_styles {
+ True ->
+ html.link([
+ attribute.rel("stylesheet"),
+ attribute.href(
+ "https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css",
+ ),
+ ])
+ False -> element.none()
+ }
+
+ html([], [
+ html.head([], [
+ html.meta([attribute("charset", "utf-8")]),
+ html.meta([
+ attribute("name", "viewport"),
+ attribute("content", "width=device-width, initial-scale=1"),
+ ]),
+ html.title([], app_name),
+ html.script([attribute.type_("module"), attribute.src("./index.mjs")], ""),
+ styles,
+ ]),
+ html.body([], [html.div([attribute.id(container_id)], [])]),
+ ])
+ |> element.to_string
+ |> string.append("<!DOCTYPE html>", _)
+}
+
+fn is_nil_type(t: Type) -> Bool {
+ case t {
+ Named(name: "Nil", package: "", module: "gleam", parameters: []) -> True
+ _ -> False
+ }
+}
+
+fn is_type_variable(t: Type) -> Bool {
+ case t {
+ Variable(..) -> True
+ _ -> False
+ }
+}
+
+fn is_compatible_flags_type(t: Type) -> Bool {
+ is_nil_type(t) || is_type_variable(t)
+}