aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gleam.toml1
-rw-r--r--manifest.toml4
-rw-r--r--src/lustre/cli/add.gleam6
-rw-r--r--src/lustre/cli/build.gleam122
-rw-r--r--src/lustre/cli/dev.gleam60
-rw-r--r--src/lustre/cli/esbuild.gleam104
-rw-r--r--src/lustre/cli/project.gleam36
-rw-r--r--src/lustre/cli/step.gleam100
8 files changed, 323 insertions, 110 deletions
diff --git a/gleam.toml b/gleam.toml
index 8fc70f7..8acddac 100644
--- a/gleam.toml
+++ b/gleam.toml
@@ -52,6 +52,7 @@ justin = "~> 1.0"
shellout = "~> 1.5"
simplifile = "~> 1.4"
tom = "~> 0.3"
+spinner = "~> 1.1"
[dev-dependencies]
birdie = "~> 1.0"
diff --git a/manifest.toml b/manifest.toml
index a6d82df..cec0024 100644
--- a/manifest.toml
+++ b/manifest.toml
@@ -12,13 +12,16 @@ packages = [
{ name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" },
{ name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" },
{ name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" },
+ { name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" },
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
{ name = "glint", version = "0.16.0-rc1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "AF9A7CCAD9FC8CDFF2606F97BC52C2D98559F27EB28CD3B8A11A35641740AE2F" },
{ name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
{ name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
+ { name = "repeatedly", version = "2.1.0", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "AF58F2AF775BAAD1C4B4C74F9B5D963E50B71736C97A7323DBA40F809CF93E5A" },
{ name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" },
{ name = "simplifile", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "AAFCF154F69B237D269FF2764890F61ABC4A7EF2A592D44D67627B99694539D9" },
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
+ { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
]
@@ -37,4 +40,5 @@ glint = { version = "0.16.0-rc1" }
justin = { version = "~> 1.0" }
shellout = { version = "~> 1.5" }
simplifile = { version = "~> 1.4" }
+spinner = { version = "~> 1.1"}
tom = { version = "~> 0.3" }
diff --git a/src/lustre/cli/add.gleam b/src/lustre/cli/add.gleam
index 5fc53da..7e44334 100644
--- a/src/lustre/cli/add.gleam
+++ b/src/lustre/cli/add.gleam
@@ -3,6 +3,7 @@
import glint.{type Command, CommandInput}
import glint/flag
import lustre/cli/esbuild
+import lustre/cli/step
// COMMANDS --------------------------------------------------------------------
@@ -17,9 +18,8 @@ to bundle applications and act as a development server.
let CommandInput(flags: flags, ..) = input
let assert Ok(os) = flag.get_string(flags, "os")
let assert Ok(cpu) = flag.get_string(flags, "cpu")
- let result = esbuild.download(os, cpu)
-
- case result {
+ let script = esbuild.download(os, cpu)
+ case step.execute(script) {
Ok(_) -> Nil
Error(error) -> esbuild.explain(error)
}
diff --git a/src/lustre/cli/build.gleam b/src/lustre/cli/build.gleam
index 1314f9e..8d02b97 100644
--- a/src/lustre/cli/build.gleam
+++ b/src/lustre/cli/build.gleam
@@ -10,7 +10,8 @@ import glint.{type Command, CommandInput}
import glint/flag
import lustre/cli/esbuild
import lustre/cli/project.{type Module, type Type, Named, Variable}
-import lustre/cli/utils.{map, replace, try}
+import lustre/cli/utils.{keep, replace, try}
+import lustre/cli/step.{type Step}
import simplifile
// COMMANDS --------------------------------------------------------------------
@@ -30,20 +31,18 @@ JavaScript module for you to host or distribute.
let CommandInput(flags: flags, ..) = input
let assert Ok(minify) = flag.get_bool(flags, "minify")
- let result = {
- use _ <- result.try(prepare_esbuild())
+ let script = {
+ use <- step.new("Building your project")
+ use project_name <- step.try(get_project_name(), keep)
+ use <- step.done("✅ Project compiled successfully")
+ use <- step.new("Checking if I can bundle your application")
+ use module <- step.try(get_module_interface(project_name), keep)
+ use _ <- step.try(check_main_function(project_name, module), keep)
- io.println("\nPreparing build...")
- io.println(" ├ reading project config")
- use project_name <- result.try(get_project_name())
- use module <- result.try(get_module_interface(project_name))
- use _ <- result.try(check_main_function(project_name, module))
-
- io.println(" ├ generating entry file")
+ use <- step.new("Creating the bundle entry file")
let root = project.root()
let tempdir = filepath.join(root, "build/.lustre")
let outdir = filepath.join(root, "priv/static")
-
let _ = simplifile.create_directory_all(tempdir)
let _ = simplifile.create_directory_all(outdir)
let entry =
@@ -65,12 +64,12 @@ JavaScript module for you to host or distribute.
|> filepath.join(outdir, _)
let assert Ok(_) = simplifile.write(entryfile, entry)
- use _ <- result.try(bundle(entry, tempdir, outfile, minify))
- Ok(Nil)
+ use _ <- step.run(bundle(entry, tempdir, outfile, minify), keep)
+ step.return(Nil)
}
- case result {
+ case step.execute(script) {
Ok(_) -> Nil
Error(error) -> explain(error)
}
@@ -100,26 +99,26 @@ present.
let assert Ok(module_path) = dict.get(args, "module_path")
let assert Ok(minify) = flag.get_bool(flags, "minify")
- let result = {
- io.println("\nPreparing build...")
- io.println(" ├ reading project config")
- use module <- result.try(get_module_interface(module_path))
- use _ <- result.try(check_component_name(module_path, module))
- use component <- result.try(find_component(module_path, module))
+ let script = {
+ use <- step.new("Building your project")
+ use module <- step.try(get_module_interface(module_path), keep)
+ use <- step.done("✅ Project compiled successfully")
+ use <- step.new("Checking if I can bundle your component")
+ use _ <- step.try(check_component_name(module_path, module), keep)
+ use component <- step.try(find_component(module_path, module), keep)
- io.println(" ├ generating entry file")
+ use <- step.new("Creating the bundle entry file")
let root = project.root()
let tempdir = filepath.join(root, "build/.lustre")
let outdir = filepath.join(root, "priv/static")
-
let _ = simplifile.create_directory_all(tempdir)
let _ = simplifile.create_directory_all(outdir)
- use project_name <- result.try(get_project_name())
+ use project_name <- step.try(get_project_name(), keep)
let entry =
"import { register } from '../dev/javascript/lustre/client-component.ffi.mjs';
import { name, ${component} as component } from '../dev/javascript/${project_name}/${module_path}.mjs';
-
+
register(component(), name);
"
|> string.replace("${component}", component)
@@ -138,19 +137,18 @@ present.
|> result.map(filepath.join(outdir, _))
let assert Ok(_) = simplifile.write(entryfile, entry)
- use _ <- result.try(bundle(entry, tempdir, outfile, minify))
-
- Ok(Nil)
+ use _ <- step.run(bundle(entry, tempdir, outfile, minify), keep)
+ step.return(Nil)
}
- case result {
+ case step.execute(script) {
Ok(_) -> Nil
Error(error) -> explain(error)
}
})
|> glint.description(description)
|> glint.named_args(["module_path"])
- |> glint.unnamed_args(glint.EqArgs(1))
+ |> glint.unnamed_args(glint.EqArgs(0))
|> glint.flag("minify", {
let description = "Minify the output"
let default = False
@@ -174,18 +172,49 @@ type Error {
}
fn explain(error: Error) -> Nil {
- error
- |> string.inspect
- |> io.println
-}
+ case error {
+ BuildError -> project.explain(project.BuildError)
-// STEPS -----------------------------------------------------------------------
+ BundleError(error) -> esbuild.explain(error)
+
+ ComponentMissing(module) -> io.println("
+Module `" <> module <> "` doesn't have any public function I can use to bundle
+a component.
+
+To bundle a component your module should have a public function that returns a
+Lustre `App`:
+
+ import lustre.{type App}
+ pub fn my_component() -> App(flags, model, msg) {
+ todo as \"your Lustre component to bundle\"
+ }
+")
+
+ MainMissing(module) -> io.println("
+Module `" <> module <> "` doesn't have a public `main` function I can use as
+the bundle entry point.")
+
+ ModuleMissing(module) -> io.println("
+I couldn't find a public module called `" <> module <> "` in your project.")
-fn prepare_esbuild() -> Result(Nil, Error) {
- esbuild.download(get_os(), get_cpu())
- |> result.replace_error(BuildError)
+ NameIncorrectType(module, type_) -> io.println("
+I can't use the `name` constant exposed by module `" <> module <> "`
+to give a name to the component I'm bundling.
+I was expecting `name` to be a `String`,
+but it has type `" <> project.type_to_string(type_) <> "`.")
+
+ NameMissing(module) -> io.println("
+Module `" <> module <> "` doesn't have a public `name` constant.
+That is required so that I can give a proper name to the component I'm bundling.
+
+Try adding a `name` constant to your module like this:
+
+ const name: String = \"component-name\"")
+ }
}
+// STEPS -----------------------------------------------------------------------
+
fn get_project_name() -> Result(String, Error) {
use config <- try(project.config(), replace(with: BuildError))
Ok(config.name)
@@ -242,16 +271,11 @@ fn bundle(
tempdir: String,
outfile: String,
minify: Bool,
-) -> Result(Nil, Error) {
+) -> Step(Nil, Error) {
let entryfile = filepath.join(tempdir, "entry.mjs")
let assert Ok(_) = simplifile.write(entryfile, entry)
-
- use _ <- try(
- esbuild.bundle(entryfile, outfile, minify),
- map(with: BundleError),
- )
-
- Ok(Nil)
+ use _ <- step.run(esbuild.bundle(entryfile, outfile, minify), BundleError)
+ step.return(Nil)
}
// UTILS -----------------------------------------------------------------------
@@ -288,11 +312,3 @@ fn is_compatible_app_type(t: Type) -> Bool {
_ -> False
}
}
-
-// EXTERNALS -------------------------------------------------------------------
-
-@external(erlang, "cli_ffi", "get_os")
-fn get_os() -> String
-
-@external(erlang, "cli_ffi", "get_cpu")
-fn get_cpu() -> String
diff --git a/src/lustre/cli/dev.gleam b/src/lustre/cli/dev.gleam
index f561b39..0cbec11 100644
--- a/src/lustre/cli/dev.gleam
+++ b/src/lustre/cli/dev.gleam
@@ -3,14 +3,14 @@
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/cli/step
+import lustre/cli/utils.{guard, keep, map, replace, try}
import lustre/element
import lustre/element/html.{html}
import simplifile
@@ -28,14 +28,17 @@ pub fn run() -> Command(Nil) {
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(
+ let script = {
+ use <- step.new("Building your project")
+ use interface <- step.try(project.interface(), replace(BuildError))
+ use module <- step.try(
dict.get(interface.modules, interface.name),
replace(ModuleMissing(interface.name)),
)
- use is_app <- result.try(check_is_lustre_app(interface.name, module))
+ use is_app <- step.try(check_is_lustre_app(interface.name, module), keep)
+ use <- step.done("✅ Project compiled successfully")
+ use <- step.new("Creating the application entry point")
let root = project.root()
let tempdir = filepath.join(root, "build/.lustre")
let _ = simplifile.create_directory_all(tempdir)
@@ -50,7 +53,7 @@ pub fn run() -> Command(Nil) {
"
False ->
" import { main } from '../dev/javascript/${app_name}/${app_name}.mjs';
-
+
main();
"
}
@@ -62,7 +65,7 @@ pub fn run() -> Command(Nil) {
let assert Ok(_) = simplifile.write(tempdir <> "/entry.mjs", entry)
let assert Ok(_) = simplifile.write(tempdir <> "/index.html", html)
- use _ <- try(
+ use _ <- step.run(
esbuild.bundle(
filepath.join(tempdir, "entry.mjs"),
filepath.join(tempdir, "index.mjs"),
@@ -70,12 +73,11 @@ pub fn run() -> Command(Nil) {
),
map(BundleError),
)
- use _ <- try(esbuild.serve(host, port), map(BundleError))
-
- Ok(Nil)
+ use _ <- step.run(esbuild.serve(host, port), map(BundleError))
+ step.return(Nil)
}
- case result {
+ case step.execute(script) {
Ok(_) -> Nil
Error(error) -> explain(error)
}
@@ -120,9 +122,37 @@ type Error {
}
fn explain(error: Error) -> Nil {
- error
- |> string.inspect
- |> io.println
+ case error {
+ BuildError -> project.explain(project.BuildError)
+
+ BundleError(error) -> esbuild.explain(error)
+
+ MainMissing(module) -> io.println("
+Module `" <> module <> "` doesn't have a public `main` function I can preview.")
+
+ MainIncorrectType(module, type_) -> io.println("
+I cannot preview the `main` function exposed by module `" <> module <> "`.
+To start a preview server I need it to take no arguments and return a Lustre
+`App`.
+The one I found has type `" <> project.type_to_string(type_) <> "`.")
+
+ // TODO: maybe this could have useful links to `App`/flags...
+ MainBadAppType(module, type_) -> io.println("
+I cannot preview the `main` function exposed by module `" <> module <> "`.
+To start a preview server I need it to return a Lustre `App` that doesn't need
+any flags.
+The one I found has type `" <> project.type_to_string(type_) <> "`.
+
+Its return type should look something like this:
+
+ import lustre.{type App}
+ pub fn main() -> App(flags, model, msg) {
+ todo as \"your Lustre application to preview\"
+ }")
+
+ ModuleMissing(module) -> io.println("
+I couldn't find a public module called `" <> module <> "` in your project.")
+ }
}
// STEPS -----------------------------------------------------------------------
diff --git a/src/lustre/cli/esbuild.gleam b/src/lustre/cli/esbuild.gleam
index c98f5f3..bf824b2 100644
--- a/src/lustre/cli/esbuild.gleam
+++ b/src/lustre/cli/esbuild.gleam
@@ -3,54 +3,59 @@
import filepath
import gleam/bool
import gleam/dynamic.{type Dynamic}
-import gleam/function
import gleam/io
import gleam/list
-import gleam/pair
import gleam/result
import gleam/set
import gleam/string
import lustre/cli/project
-import lustre/cli/utils.{keep, map, replace, try}
+import lustre/cli/step.{type Step}
+import lustre/cli/utils.{keep, replace}
import shellout
import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
// COMMANDS --------------------------------------------------------------------
-pub fn download(os: String, cpu: String) -> Result(Nil, Error) {
+pub fn download(os: String, cpu: String) -> Step(Nil, Error) {
+ use <- step.new("Downloading esbuild")
+
let outdir = filepath.join(project.root(), "build/.lustre/bin")
let outfile = filepath.join(outdir, "esbuild")
- use <- bool.guard(check_esbuild_exists(outfile), Ok(Nil))
+ use <- bool.guard(check_esbuild_exists(outfile), {
+ use <- step.done("✅ Esbuild already installed!")
+ step.return(Nil)
+ })
- io.println("\nInstalling esbuild...")
- io.println(" ├ detecting platform")
- use url <- result.try(get_download_url(os, cpu))
+ use <- step.new("Detecting platform")
+ use url <- step.try(get_download_url(os, cpu), keep)
- io.println(" ├ downloading from " <> url)
- use tarball <- try(get_esbuild(url), NetworkError)
+ use <- step.new("Downloading from " <> url)
+ use tarball <- step.try(get_esbuild(url), NetworkError)
- io.println(" ├ unpacking")
- use bin <- try(unzip_esbuild(tarball), UnzipError)
- use _ <- try(write_esbuild(bin, outdir, outfile), SimplifileError(_, outfile))
- use _ <- try(set_filepermissions(outfile), SimplifileError(_, outfile))
+ use <- step.new("Unzipping esbuild")
+ use bin <- step.try(unzip_esbuild(tarball), UnzipError)
+ let write_esbuild =
+ write_esbuild(bin, outdir, outfile)
+ |> result.map_error(SimplifileError(_, outfile))
+ use _ <- step.try(write_esbuild, keep)
+ use _ <- step.try(set_filepermissions(outfile), SimplifileError(_, outfile))
- io.println(" ├ installed!")
- Ok(Nil)
+ use <- step.done("✅ Esbuild installed!")
+ step.return(Nil)
}
pub fn bundle(
input_file: String,
output_file: String,
minify: Bool,
-) -> Result(Nil, Error) {
- use _ <- try(download(get_os(), get_cpu()), keep)
- use _ <- try(project.build(), replace(BuildError))
+) -> Step(Nil, Error) {
+ use _ <- step.run(download(get_os(), get_cpu()), keep)
+ use _ <- step.try(project.build(), replace(BuildError))
+ use <- step.new("Getting everything ready for tree shaking")
- io.println("\nBundling with esbuild...")
- io.println(" ├ configuring tree shaking")
let root = project.root()
- use _ <- try(configure_node_tree_shaking(root), map(SimplifileError(_, root)))
+ use _ <- step.try(configure_node_tree_shaking(root), SimplifileError(_, root))
let flags = [
"--bundle",
@@ -63,40 +68,41 @@ pub fn bundle(
False -> [input_file, ..flags]
}
- use _ <- try(
+ use <- step.new("Bundling with esbuild")
+ use _ <- step.try(
shellout.command(
run: "./build/.lustre/bin/esbuild",
in: root,
with: options,
opt: [],
),
- on_error: map(function.compose(pair.second, BundleError)),
+ on_error: fn(pair) { BundleError(pair.1) },
)
- io.println(" ├ bundle produced at " <> output_file)
- Ok(Nil)
+ use <- step.done("✅ Bundle produced at `" <> output_file <> "`")
+ step.return(Nil)
}
-pub fn serve(host: String, port: String) -> Result(Nil, Error) {
- use _ <- try(download(get_os(), get_cpu()), keep)
+pub fn serve(host: String, port: String) -> Step(Nil, Error) {
+ use _ <- step.run(download(get_os(), get_cpu()), keep)
let root = project.root()
let flags = [
"--serve=" <> host <> ":" <> port,
"--servedir=" <> filepath.join(root, "build/.lustre"),
]
- io.println("\nStarting dev server at " <> host <> ":" <> port <> "...")
- use _ <- try(
+ use <- step.done("\nStarting dev server at " <> host <> ":" <> port <> "...")
+ use _ <- step.try(
shellout.command(
run: "./build/.lustre/bin/esbuild",
in: root,
with: flags,
opt: [],
),
- on_error: map(function.compose(pair.second, BundleError)),
+ on_error: fn(pair) { BundleError(pair.1) },
)
- Ok(Nil)
+ step.return(Nil)
}
// STEPS -----------------------------------------------------------------------
@@ -197,9 +203,37 @@ pub type Error {
}
pub fn explain(error: Error) -> Nil {
- error
- |> string.inspect
- |> io.println
+ case error {
+ BuildError -> project.explain(project.BuildError)
+
+ BundleError(message) -> io.println("
+I ran into an error while trying to create a bundle with esbuild:
+" <> message)
+
+ // TODO: Is there a better way to deal with this dynamic error?
+ NetworkError(_dynamic) ->
+ io.println(
+ "
+There was a network error!",
+ )
+
+ // TODO: this could give a better error for some common reason like Enoent.
+ SimplifileError(reason, path) -> io.println("
+I ran into the following error at path `" <> path <> "`:" <> string.inspect(
+ reason,
+ ) <> ".")
+
+ UnknownPlatform(os, cpu) -> io.println("
+I couldn't figure out the correct esbuild version for your
+os (" <> os <> ") and cpu (" <> cpu <> ").")
+
+ // TODO: Is there a better way to deal with this dynamic error?
+ UnzipError(_dynamic) ->
+ io.println(
+ "
+I couldn't unzip the esbuild executable!",
+ )
+ }
}
// EXTERNALS -------------------------------------------------------------------
diff --git a/src/lustre/cli/project.gleam b/src/lustre/cli/project.gleam
index 68b5801..8d04244 100644
--- a/src/lustre/cli/project.gleam
+++ b/src/lustre/cli/project.gleam
@@ -3,8 +3,10 @@
import filepath
import gleam/dict.{type Dict}
import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError}
+import gleam/int
import gleam/io
import gleam/json
+import gleam/list
import gleam/pair
import gleam/result
import gleam/string
@@ -72,7 +74,6 @@ pub fn interface() -> Result(Interface, String) {
let assert Ok(json) = simplifile.read(out)
let assert Ok(interface) = json.decode(json, decode_interface)
-
Ok(interface)
}
@@ -106,9 +107,13 @@ pub type Error {
}
pub fn explain(error: Error) -> Nil {
- error
- |> string.inspect
- |> io.println
+ case error {
+ BuildError ->
+ "
+It looks like your project has some compilation errors that need to be addressed
+before I can do anything."
+ |> io.println
+ }
}
// UTILS -----------------------------------------------------------------------
@@ -129,6 +134,29 @@ fn find_root(path: String) -> String {
}
}
+pub fn type_to_string(type_: Type) -> String {
+ case type_ {
+ Tuple(elements) -> {
+ let elements = list.map(elements, type_to_string)
+ "#(" <> string.join(elements, with: ", ") <> ")"
+ }
+
+ Fn(params, return) -> {
+ let params = list.map(params, type_to_string)
+ let return = type_to_string(return)
+ "fn(" <> string.join(params, with: ", ") <> ") -> " <> return
+ }
+
+ Named(name, _package, _module, []) -> name
+ Named(name, _package, _module, params) -> {
+ let params = list.map(params, type_to_string)
+ name <> "(" <> string.join(params, with: ", ") <> ")"
+ }
+
+ Variable(id) -> "a_" <> int.to_string(id)
+ }
+}
+
// DECODERS --------------------------------------------------------------------
fn decode_interface(dyn: Dynamic) -> Result(Interface, List(DecodeError)) {
diff --git a/src/lustre/cli/step.gleam b/src/lustre/cli/step.gleam
new file mode 100644
index 0000000..d3f8c8a
--- /dev/null
+++ b/src/lustre/cli/step.gleam
@@ -0,0 +1,100 @@
+import gleam/io
+import gleam_community/ansi
+import spinner.{type Spinner}
+
+type SpinnerStatus {
+ Running(message: String)
+ Stopped
+}
+
+type Env {
+ Env(spinner: Spinner, spinner_status: SpinnerStatus)
+}
+
+pub opaque type Step(a, e) {
+ Step(run: fn(Env) -> #(Env, Result(a, e)))
+}
+
+/// Replace the current spinner label with a new one.
+///
+pub fn new(message: String, then continue: fn() -> Step(a, e)) -> Step(a, e) {
+ use Env(spinner, spinner_status) <- Step
+ case spinner_status {
+ Running(_) -> {
+ spinner.set_text(spinner, message)
+ continue().run(Env(spinner, Running(message)))
+ }
+ Stopped -> {
+ let new_spinner =
+ spinner.new(message)
+ |> spinner.with_frames(spinner.snake_frames)
+ |> spinner.start
+ continue().run(Env(new_spinner, Running(message)))
+ }
+ }
+}
+
+/// Stops the current spinner and prints out the given message.
+///
+pub fn done(message: String, then continue: fn() -> Step(b, e)) -> Step(b, e) {
+ use Env(spinner, spinner_status) <- Step
+ case spinner_status {
+ Running(_) -> spinner.stop(spinner)
+ Stopped -> Nil
+ }
+ io.println(ansi.green(message))
+ continue().run(Env(spinner, Stopped))
+}
+
+/// Runs another step as part of this one. The step will use the same spinner
+/// as the previous one overriding its content.
+///
+pub fn run(
+ step: Step(a, e),
+ on_error map_error: fn(e) -> e1,
+ then continue: fn(a) -> Step(b, e1),
+) -> Step(b, e1) {
+ use env <- Step
+ case step.run(env) {
+ #(new_env, Ok(res)) -> continue(res).run(new_env)
+ #(new_env, Error(e)) -> #(new_env, Error(map_error(e)))
+ }
+}
+
+/// If the result is `Ok` will continue by passing its wrapped value to the
+/// `continue` function; otherwise will result in an error stopping the stepwise
+/// execution.
+///
+pub fn try(
+ result: Result(a, e),
+ on_error map_error: fn(e) -> e1,
+ then continue: fn(a) -> Step(b, e1),
+) -> Step(b, e1) {
+ Step(fn(env) { #(env, result) })
+ |> run(map_error, continue)
+}
+
+/// Returns a value without changing the state of any spinner.
+/// Any running spinner will still be running.
+///
+pub fn return(value: a) -> Step(a, e) {
+ use env <- Step
+ #(env, Ok(value))
+}
+
+pub fn execute(step: Step(a, e)) -> Result(a, e) {
+ let initial_spinner =
+ spinner.new("")
+ |> spinner.with_frames(spinner.snake_frames)
+ |> spinner.start
+
+ let #(Env(spinner, status), res) = step.run(Env(initial_spinner, Running("")))
+ case status {
+ Running(message) -> {
+ spinner.stop(spinner)
+ io.println("❌ " <> ansi.red(message))
+ }
+ Stopped -> Nil
+ }
+ res
+}