aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gleam.toml10
-rw-r--r--manifest.toml15
-rw-r--r--src/cli_ffi.erl87
-rw-r--r--src/lustre.gleam42
-rw-r--r--src/lustre/cli/add.gleam91
-rw-r--r--src/lustre/cli/build.gleam374
-rw-r--r--src/lustre/cli/dev.gleam245
-rw-r--r--src/lustre/cli/esbuild.gleam249
-rw-r--r--src/lustre/cli/project.gleam186
-rw-r--r--src/lustre/cli/step.gleam100
-rw-r--r--src/lustre/cli/tailwind.gleam185
-rw-r--r--src/lustre/cli/utils.gleam68
-rw-r--r--test/lustre_test.gleam16
13 files changed, 9 insertions, 1659 deletions
diff --git a/gleam.toml b/gleam.toml
index 533c4bc..f96c2e2 100644
--- a/gleam.toml
+++ b/gleam.toml
@@ -40,21 +40,13 @@ pages = [
]
[dependencies]
-argv = "~> 1.0"
-filepath = "~> 0.1"
-gleam_community_ansi = "~> 1.4"
gleam_erlang = "~> 0.24"
gleam_json = "~> 1.0"
gleam_otp = "~> 0.9"
gleam_stdlib = "~> 0.34 or ~> 1.0"
-glint = "0.16.0"
-justin = "~> 1.0"
-simplifile = "~> 1.4"
-spinner = "~> 1.1"
-tom = "~> 0.3"
-gleam_package_interface = "~> 1.0"
[dev-dependencies]
birdie = "~> 1.0"
gleeunit = "~> 1.0"
shellout = "~> 1.6"
+simplifile = "~> 1.4"
diff --git a/manifest.toml b/manifest.toml
index 31e4e21..6f29957 100644
--- a/manifest.toml
+++ b/manifest.toml
@@ -11,36 +11,21 @@ packages = [
{ name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" },
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
{ 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_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
{ 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", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "61B7E85CBB0CCD2FD8A9C7AE06CA97A80BF6537716F34362A39DF9C74967BBBC" },
{ 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" },
]
[requirements]
-argv = { version = "~> 1.0" }
birdie = { version = "~> 1.0" }
-filepath = { version = "~> 0.1" }
-gleam_community_ansi = { version = "~> 1.4" }
gleam_erlang = { version = "~> 0.24" }
gleam_json = { version = "~> 1.0" }
gleam_otp = { version = "~> 0.9" }
-gleam_package_interface = { version = "~> 1.0"}
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleeunit = { version = "~> 1.0" }
-glint = { version = "0.16.0" }
-justin = { version = "~> 1.0" }
shellout = { version = "~> 1.6" }
simplifile = { version = "~> 1.4" }
-spinner = { version = "~> 1.1" }
-tom = { version = "~> 0.3" }
diff --git a/src/cli_ffi.erl b/src/cli_ffi.erl
deleted file mode 100644
index 43e9c78..0000000
--- a/src/cli_ffi.erl
+++ /dev/null
@@ -1,87 +0,0 @@
--module(cli_ffi).
--export([
- get_cpu/0,
- get_esbuild/1,
- get_tailwind/1,
- get_os/0,
- unzip_esbuild/1,
- exec/3
-]).
-
-get_os() ->
- case os:type() of
- {win32, _} -> <<"win32">>;
- {unix, darwin} -> <<"darwin">>;
- {unix, linux} -> <<"linux">>;
- {_, Unknown} -> atom_to_binary(Unknown, utf8)
- end.
-
-get_cpu() ->
- case erlang:system_info(os_type) of
- {unix, _} ->
- [Arch, _] = string:split(erlang:system_info(system_architecture), "-"),
- list_to_binary(Arch);
- {win32, _} ->
- case erlang:system_info(wordsize) of
- 4 -> {ok, <<"ia32">>};
- 8 -> {ok, <<"x64">>}
- end
- end.
-
-get_esbuild(Url) ->
- inets:start(),
- ssl:start(),
-
- case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
- {ok, {{_, 200, _}, _, Zip}} -> {ok, Zip};
- {ok, Res} -> {error, Res};
- {error, Err} -> {error, Err}
- end.
-
-get_tailwind(Url) ->
- inets:start(),
- ssl:start(),
-
- case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
- {ok, {{_, 200, _}, _, Bin}} -> {ok, Bin};
- {ok, Res} -> {error, Res};
- {error, Err} -> {error, Err}
- end.
-
-
-unzip_esbuild(Zip) ->
- Result =
- erl_tar:extract({binary, Zip}, [
- memory, compressed, {files, ["package/bin/esbuild"]}
- ]),
-
- case Result of
- {ok, [{_, Esbuild}]} -> {ok, Esbuild};
- {ok, Res} -> {error, Res};
- {error, Err} -> {error, Err}
- end.
-
-exec(Command, Args, Cwd) ->
- Command_ = binary_to_list(Command),
- Args_ = lists:map(fun(Arg) -> binary_to_list(Arg) end, Args),
- Cwd_ = binary_to_list(Cwd),
-
- Name = case Command_ of
- "./" ++ _ -> {spawn_executable, Command_};
- "/" ++ _ -> {spawn_executable, Command_};
- _ -> {spawn_executable, os:find_executable(Command_)}
- end,
-
- Port = open_port(Name, [exit_status, binary, hide, stream, eof,
- {args, Args_},
- {cd, Cwd_}
- ]),
-
- do_exec(Port, []).
-
-do_exec(Port, Acc) ->
- receive
- {Port, {data, Data}} -> do_exec(Port, [Data | Acc]);
- {Port, {exit_status, 0}} -> {ok, list_to_binary(lists:reverse(Acc))};
- {Port, {exit_status, Code}} -> {error, {Code, list_to_binary(lists:reverse(Acc))}}
- end.
diff --git a/src/lustre.gleam b/src/lustre.gleam
index 40b8059..ff4fcdd 100644
--- a/src/lustre.gleam
+++ b/src/lustre.gleam
@@ -163,7 +163,6 @@
// IMPORTS ---------------------------------------------------------------------
-import argv
import gleam/bool
import gleam/dict.{type Dict}
import gleam/dynamic.{type Decoder}
@@ -171,52 +170,11 @@ import gleam/erlang/process.{type Subject}
import gleam/option.{type Option, None, Some}
import gleam/otp/actor.{type StartError}
import gleam/result
-import glint
-import lustre/cli/add
-import lustre/cli/build
-import lustre/cli/dev
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/internals/patch
import lustre/internals/runtime
-// MAIN ------------------------------------------------------------------------
-
-/// This function exists so you can run helpful Lustre utilities from the command
-/// line using `gleam run -m lustre`. Here's a brief overview of the different
-/// subcommands you can run:
-///
-/// - `add` can be used to add useful binaries like `esbuild` to your project.
-///
-/// - `build` has additional subcommands for building Lustre applications or
-/// packaging Web Components.
-///
-/// - `dev` starts a development server that automatically loads your Lustre app.
-///
-/// For a proper help message run:
-///
-/// ```sh
-/// gleam run -m lustre -- --help
-/// ```
-///
-/// **Note**: If you're just using Lustre as a library, *you can ignore this
-/// function*.
-///
-pub fn main() {
- let args = argv.load().arguments
-
- glint.new()
- |> glint.as_gleam_module
- |> glint.with_name("lustre")
- |> glint.with_pretty_help(glint.default_pretty_help())
- |> glint.add(at: ["add", "esbuild"], do: add.esbuild())
- |> glint.add(at: ["add", "tailwind"], do: add.tailwind())
- |> glint.add(at: ["build", "app"], do: build.app())
- |> glint.add(at: ["build", "component"], do: build.component())
- |> glint.add(at: ["dev"], do: dev.run())
- |> glint.run(args)
-}
-
// TYPES -----------------------------------------------------------------------
/// Represents a constructed Lustre application that is ready to be started.
diff --git a/src/lustre/cli/add.gleam b/src/lustre/cli/add.gleam
deleted file mode 100644
index ddacf2e..0000000
--- a/src/lustre/cli/add.gleam
+++ /dev/null
@@ -1,91 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import glint.{type Command, CommandInput}
-import glint/flag
-import lustre/cli/esbuild
-import lustre/cli/step
-import lustre/cli/tailwind
-
-// COMMANDS --------------------------------------------------------------------
-
-pub fn esbuild() -> Command(Nil) {
- let description =
- "
-Download a platform-appropriate version of the esbuild binary. Lustre uses this
-to bundle applications and act as a development server.
- "
-
- glint.command(fn(input) {
- let CommandInput(flags: flags, ..) = input
- let assert Ok(os) = flag.get_string(flags, "os")
- let assert Ok(cpu) = flag.get_string(flags, "cpu")
- let script = esbuild.download(os, cpu)
- case step.execute(script) {
- Ok(_) -> Nil
- Error(error) -> esbuild.explain(error)
- }
- })
- |> glint.description(description)
- |> glint.unnamed_args(glint.EqArgs(0))
- |> glint.flag("os", {
- let description = ""
- let default = get_os()
-
- flag.string()
- |> flag.default(default)
- |> flag.description(description)
- })
- |> glint.flag("cpu", {
- let description = ""
- let default = get_cpu()
-
- flag.string()
- |> flag.default(default)
- |> flag.description(description)
- })
-}
-
-pub fn tailwind() -> Command(Nil) {
- let description =
- "
-Download a platform-appropriate version of the Tailwind binary.
- "
-
- glint.command(fn(input) {
- let CommandInput(flags: flags, ..) = input
- let assert Ok(os) = flag.get_string(flags, "os")
- let assert Ok(cpu) = flag.get_string(flags, "cpu")
-
- let script = tailwind.setup(os, cpu)
- case step.execute(script) {
- Ok(_) -> Nil
- Error(error) -> tailwind.explain(error)
- }
- })
- |> glint.description(description)
- |> glint.unnamed_args(glint.EqArgs(0))
- |> glint.flag("os", {
- let description = ""
- let default = get_os()
-
- flag.string()
- |> flag.default(default)
- |> flag.description(description)
- })
- |> glint.flag("cpu", {
- let description = ""
- let default = get_cpu()
-
- flag.string()
- |> flag.default(default)
- |> flag.description(description)
- })
-}
-
-// 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/build.gleam b/src/lustre/cli/build.gleam
deleted file mode 100644
index b899f21..0000000
--- a/src/lustre/cli/build.gleam
+++ /dev/null
@@ -1,374 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import filepath
-import gleam/bool
-import gleam/dict
-import gleam/io
-import gleam/list
-import gleam/package_interface.{type Type, Named, Variable}
-import gleam/result
-import gleam/string
-import glint.{type Command, CommandInput}
-import glint/flag
-import lustre/cli/esbuild
-import lustre/cli/project.{type Module}
-import lustre/cli/utils.{exec, keep, replace, template, try}
-import lustre/cli/step.{type Step}
-import lustre/cli/tailwind
-import simplifile
-
-// COMMANDS --------------------------------------------------------------------
-
-pub fn app() -> Command(Nil) {
- let description =
- "
-Build and bundle an entire Lustre application. The generated JavaScript module
-calls your app's `main` function on page load and can be included in any Web
-page without Gleam or Lustre being present.
-
-This is different from using `gleam build` directly because it produces a single
-JavaScript module for you to host or distribute.
- "
-
- glint.command(fn(input) {
- let CommandInput(flags: flags, ..) = input
- let assert Ok(minify) = flag.get_bool(flags, "minify")
-
- 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)
-
- 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 =
- template("entry-with-main.mjs")
- |> string.replace("{app_name}", project_name)
-
- let entryfile = filepath.join(tempdir, "entry.mjs")
- let ext = case minify {
- True -> ".min.mjs"
- False -> ".mjs"
- }
-
- let outfile =
- project_name
- |> string.append(ext)
- |> filepath.join(outdir, _)
-
- let assert Ok(_) = simplifile.write(entryfile, entry)
-
- use _ <- step.run(bundle(entry, tempdir, outfile, minify), keep)
-
- // Tailwind bundling
- let entry = template("entry.css")
- let outfile =
- filepath.strip_extension(outfile)
- |> string.append(".css")
-
- let bundle = bundle_tailwind(entry, tempdir, outfile, minify)
- use _ <- step.run(bundle, on_error: TailwindBundleError)
-
- step.return(Nil)
- }
-
- case step.execute(script) {
- Ok(_) -> Nil
- Error(error) -> explain(error)
- }
- })
- |> glint.description(description)
- |> glint.unnamed_args(glint.EqArgs(0))
- |> glint.flag("minify", {
- let description = "Minify the output"
- let default = False
-
- flag.bool()
- |> flag.default(default)
- |> flag.description(description)
- })
-}
-
-pub fn component() -> Command(Nil) {
- let description =
- "
-Build a Lustre component as a portable Web Component. The generated JavaScript
-module can be included in any Web page and used without Gleam or Lustre being
-present.
- "
-
- glint.command(fn(input) {
- let CommandInput(flags: flags, named_args: args, ..) = input
- let assert Ok(module_path) = dict.get(args, "module_path")
- let assert Ok(minify) = flag.get_bool(flags, "minify")
-
- 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)
-
- 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 <- step.try(get_project_name(), keep)
-
- // Esbuild bundling
- let entry =
- template("component-entry.mjs")
- |> string.replace("{component_name}", component)
- |> string.replace("{app_name}", project_name)
- |> string.replace("{module_path}", module_path)
-
- let entryfile = filepath.join(tempdir, "entry.mjs")
- let ext = case minify {
- True -> ".min.mjs"
- False -> ".mjs"
- }
- let assert Ok(outfile) =
- string.split(module_path, "/")
- |> list.last
- |> result.map(string.append(_, ext))
- |> result.map(filepath.join(outdir, _))
-
- let assert Ok(_) = simplifile.write(entryfile, entry)
- use _ <- step.run(bundle(entry, tempdir, outfile, minify), keep)
-
- // Tailwind bundling
- let entry = template("entry.css")
- let outfile =
- filepath.strip_extension(outfile)
- |> string.append(".css")
-
- let bundle = bundle_tailwind(entry, tempdir, outfile, minify)
- use _ <- step.run(bundle, on_error: TailwindBundleError)
- step.return(Nil)
- }
-
- case step.execute(script) {
- Ok(_) -> Nil
- Error(error) -> explain(error)
- }
- })
- |> glint.description(description)
- |> glint.named_args(["module_path"])
- |> glint.unnamed_args(glint.EqArgs(0))
- |> glint.flag("minify", {
- let description = "Minify the output"
- let default = False
-
- flag.bool()
- |> flag.default(default)
- |> flag.description(description)
- })
-}
-
-// ERROR HANDLING --------------------------------------------------------------
-
-type Error {
- BuildError
- BundleError(esbuild.Error)
- TailwindBundleError(tailwind.Error)
- ComponentMissing(module: String)
- MainMissing(module: String)
- ModuleMissing(module: String)
- NameIncorrectType(module: String, got: Type)
- NameMissing(module: String)
-}
-
-fn explain(error: Error) -> Nil {
- case error {
- BuildError -> project.explain(project.BuildError)
-
- BundleError(error) -> esbuild.explain(error)
-
- TailwindBundleError(error) -> tailwind.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.")
-
- 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)
-}
-
-fn get_module_interface(module_path: String) -> Result(Module, Error) {
- use interface <- try(project.interface(), replace(with: BuildError))
- use module <- try(
- dict.get(interface.modules, module_path),
- replace(with: ModuleMissing(module_path)),
- )
-
- Ok(module)
-}
-
-fn check_main_function(
- module_path: String,
- module: Module,
-) -> Result(Nil, Error) {
- case dict.has_key(module.functions, "main") {
- True -> Ok(Nil)
- False -> Error(MainMissing(module_path))
- }
-}
-
-fn check_component_name(
- module_path: String,
- module: Module,
-) -> Result(Nil, Error) {
- use component_name <- try(
- dict.get(module.constants, "name"),
- replace(with: NameMissing(module_path)),
- )
-
- case is_string_type(component_name) {
- True -> Ok(Nil)
- False -> Error(NameIncorrectType(module_path, component_name))
- }
-}
-
-fn find_component(module_path: String, module: Module) -> Result(String, Error) {
- let functions = dict.to_list(module.functions)
- let error = Error(ComponentMissing(module_path))
-
- use _, #(name, t) <- list.fold_until(functions, error)
- case t.parameters, is_compatible_app_type(t.return) {
- [], True -> list.Stop(Ok(name))
- _, _ -> list.Continue(error)
- }
-}
-
-fn bundle(
- entry: String,
- tempdir: String,
- outfile: String,
- minify: Bool,
-) -> Step(Nil, Error) {
- let entryfile = filepath.join(tempdir, "entry.mjs")
- let assert Ok(_) = simplifile.write(entryfile, entry)
- use _ <- step.run(esbuild.bundle(entryfile, outfile, minify), BundleError)
- step.return(Nil)
-}
-
-fn bundle_tailwind(
- entry: String,
- tempdir: String,
- outfile: String,
- minify: Bool,
-) -> Step(Nil, tailwind.Error) {
- // We first check if there's a `tailwind.config.js` at the project's root.
- // If not present we do nothing; otherwise we go on with bundling.
- let root = project.root()
- let tailwind_config_file = filepath.join(root, "tailwind.config.js")
- let has_tailwind_config =
- simplifile.verify_is_file(tailwind_config_file)
- |> result.unwrap(False)
- use <- bool.guard(when: !has_tailwind_config, return: step.return(Nil))
-
- use _ <- step.run(tailwind.setup(get_os(), get_cpu()), keep)
-
- use <- step.new("Bundling with Tailwind")
- let entryfile = filepath.join(tempdir, "entry.css")
- let assert Ok(_) = simplifile.write(entryfile, entry)
-
- let flags = ["--input=" <> entryfile, "--output=" <> outfile]
- let options = case minify {
- True -> ["--minify", ..flags]
- False -> flags
- }
- let bundle = exec("./build/.lustre/bin/tailwind", in: root, with: options)
- use _ <- step.try(bundle, on_error: fn(pair) { tailwind.BundleError(pair.1) })
- use <- step.done("✅ Bundle produced at `" <> outfile <> "`")
- step.return(Nil)
-}
-
-// UTILS -----------------------------------------------------------------------
-
-fn is_string_type(t: Type) -> Bool {
- case t {
- Named(name: "String", package: "", module: "gleam", parameters: []) -> True
- _ -> False
- }
-}
-
-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_app_type(t: Type) -> Bool {
- case t {
- Named(
- name: "App",
- package: "lustre",
- module: "lustre",
- parameters: [flags, ..],
- ) -> is_nil_type(flags) || is_type_variable(flags)
- _ -> 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
deleted file mode 100644
index f7e12c2..0000000
--- a/src/lustre/cli/dev.gleam
+++ /dev/null
@@ -1,245 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import filepath
-import gleam/dict
-import gleam/io
-import gleam/package_interface.{type Type, Fn, Named, Variable}
-import gleam/result
-import gleam/string
-import glint.{type Command, CommandInput}
-import glint/flag
-import lustre/cli/esbuild
-import lustre/cli/project.{type Module}
-import lustre/cli/step
-import lustre/cli/utils.{guard, keep, map, replace, template, try}
-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(use_lustre_ui) = flag.get_bool(flags, "use-lustre-ui")
- let assert Ok(spa) = flag.get_bool(flags, "spa")
- let custom_html = flag.get_string(flags, "html")
-
- 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 <- 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)
-
- let entry =
- template(case is_app {
- True -> "entry-with-start.mjs"
- False -> "entry-with-main.mjs"
- })
- |> string.replace("{app_name}", interface.name)
-
- use html <- step.try(
- case custom_html {
- Ok(custom_html_path) ->
- custom_html_path
- |> simplifile.read
- |> result.map_error(CouldntOpenCustomHtml(_, custom_html_path))
- |> result.map(string.replace(
- _,
- "<script type=\"application/lustre\">",
- "<script type=\"module\" src=\"./index.mjs\">",
- ))
-
- Error(_) if use_lustre_ui ->
- template("index-with-lustre-ui.html")
- |> string.replace("{app_name}", interface.name)
- |> Ok
-
- _ ->
- template("index.html")
- |> string.replace("{app_name}", interface.name)
- |> Ok
- },
- keep,
- )
-
- let assert Ok(_) = simplifile.write(tempdir <> "/entry.mjs", entry)
- let assert Ok(_) = simplifile.write(tempdir <> "/index.html", html)
-
- use _ <- step.run(
- esbuild.bundle(
- filepath.join(tempdir, "entry.mjs"),
- filepath.join(tempdir, "index.mjs"),
- False,
- ),
- map(BundleError),
- )
- use _ <- step.run(esbuild.serve(host, port, spa), map(BundleError))
- step.return(Nil)
- }
-
- case step.execute(script) {
- Ok(_) -> Nil
- Error(error) -> explain(error)
- }
- })
- |> glint.description(description)
- |> glint.unnamed_args(glint.EqArgs(0))
- |> glint.flag("host", {
- let description = ""
- let default = "localhost"
-
- 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("use-lustre-ui", {
- let description = "Inject lustre/ui's stylesheet. Ignored if --html is set."
- let default = False
-
- flag.bool()
- |> flag.default(default)
- |> flag.description(description)
- })
- |> glint.flag("spa", {
- let description =
- "Serve your app on any route. Useful for apps that do client-side routing."
- let default = False
-
- flag.bool()
- |> flag.default(default)
- |> flag.description(description)
- })
- |> glint.flag("html", {
- let description =
- "Supply a custom HTML file to use as the entry point.
-To inject the Lustre bundle, make sure it includes the following empty script:
-<script type=\"application/lustre\"></script>
- "
- |> string.trim_right
-
- flag.string()
- |> flag.description(description)
- })
-}
-
-// ERROR HANDLING --------------------------------------------------------------
-
-type Error {
- BuildError
- BundleError(esbuild.Error)
- CouldntOpenCustomHtml(error: simplifile.FileError, path: String)
- MainMissing(module: String)
- MainIncorrectType(module: String, got: Type)
- MainBadAppType(module: String, got: Type)
- ModuleMissing(module: String)
-}
-
-fn explain(error: Error) -> Nil {
- case error {
- BuildError -> project.explain(project.BuildError)
-
- BundleError(error) -> esbuild.explain(error)
-
- CouldntOpenCustomHtml(_, path) -> io.println("
-I couldn't open the custom HTML file at `" <> path <> "`.")
-
- 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 -----------------------------------------------------------------------
-
-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 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)
-}
diff --git a/src/lustre/cli/esbuild.gleam b/src/lustre/cli/esbuild.gleam
deleted file mode 100644
index 2161a2f..0000000
--- a/src/lustre/cli/esbuild.gleam
+++ /dev/null
@@ -1,249 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import filepath
-import gleam/bool
-import gleam/dynamic.{type Dynamic}
-import gleam/io
-import gleam/list
-import gleam/result
-import gleam/set
-import gleam/string
-import lustre/cli/project
-import lustre/cli/step.{type Step}
-import lustre/cli/utils.{exec, keep, replace}
-import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
-
-// COMMANDS --------------------------------------------------------------------
-
-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), {
- use <- step.done("✅ Esbuild already installed!")
- step.return(Nil)
- })
-
- use <- step.new("Detecting platform")
- use url <- step.try(get_download_url(os, cpu), keep)
-
- use <- step.new("Downloading from " <> url)
- use tarball <- step.try(get_esbuild(url), NetworkError)
-
- 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))
-
- use <- step.done("✅ Esbuild installed!")
- step.return(Nil)
-}
-
-pub fn bundle(
- input_file: String,
- output_file: String,
- minify: Bool,
-) -> 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")
-
- let root = project.root()
- use _ <- step.try(configure_node_tree_shaking(root), SimplifileError(_, root))
-
- let flags = [
- "--bundle",
- "--external:node:*",
- "--format=esm",
- "--outfile=" <> output_file,
- ]
- let options = case minify {
- True -> [input_file, "--minify", ..flags]
- False -> [input_file, ..flags]
- }
-
- use <- step.new("Bundling with esbuild")
- use _ <- step.try(
- exec(run: "./build/.lustre/bin/esbuild", in: root, with: options),
- on_error: fn(pair) { BundleError(pair.1) },
- )
-
- use <- step.done("✅ Bundle produced at `" <> output_file <> "`")
- step.return(Nil)
-}
-
-pub fn serve(host: String, port: String, spa: Bool) -> 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"),
- ]
-
- let options = case spa {
- True -> [
- "--serve-fallback=" <> filepath.join(root, "build/.lustre/index.html"),
- ..flags
- ]
- False -> flags
- }
-
- use <- step.done("\nStarting dev server at " <> host <> ":" <> port <> "...")
- use _ <- step.try(
- exec(run: "./build/.lustre/bin/esbuild", in: root, with: options),
- on_error: fn(pair) { BundleError(pair.1) },
- )
-
- step.return(Nil)
-}
-
-// STEPS -----------------------------------------------------------------------
-
-fn check_esbuild_exists(path) {
- case simplifile.verify_is_file(path) {
- Ok(True) -> True
- Ok(False) | Error(_) -> False
- }
-}
-
-fn get_download_url(os, cpu) {
- let base = "https://registry.npmjs.org/@esbuild/"
- let path = case os, cpu {
- "android", "arm" -> Ok("android-arm/-/android-arm-0.19.10.tgz")
- "android", "arm64" -> Ok("android-arm64/-/android-arm64-0.19.10.tgz")
- "android", "x64" -> Ok("android-x64/-/android-x64-0.19.10.tgz")
-
- "darwin", "aarch64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
- "darwin", "amd64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
- "darwin", "arm64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
- "darwin", "x86_64" -> Ok("darwin-x64/-/darwin-x64-0.19.10.tgz")
-
- "freebsd", "arm64" -> Ok("freebsd-arm64/-/freebsd-arm64-0.19.10.tgz")
- "freebsd", "x64" -> Ok("freebsd-x64/-/freebsd-x64-0.19.10.tgz")
-
- "linux", "aarch64" -> Ok("linux-arm64/-/linux-arm64-0.19.10.tgz")
- "linux", "arm" -> Ok("linux-arm/-/linux-arm-0.19.10.tgz")
- "linux", "arm64" -> Ok("linux-arm64/-/linux-arm64-0.19.10.tgz")
- "linux", "ia32" -> Ok("linux-ia32/-/linux-ia32-0.19.10.tgz")
- "linux", "x64" -> Ok("linux-x64/-/linux-x64-0.19.10.tgz")
- "linux", "x86_64" -> Ok("linux-x64/-/linux-x64-0.19.10.tgz")
-
- "win32", "arm64" -> Ok("win32-arm64/-/win32-arm64-0.19.10.tgz")
- "win32", "ia32" -> Ok("win32-ia32/-/win32-ia32-0.19.10.tgz")
- "win32", "x64" -> Ok("win32-x64/-/win32-x64-0.19.10.tgz")
- "win32", "x86_64" -> Ok("win32-x64/-/win32-x64-0.19.10.tgz")
-
- "netbsd", "x64" -> Ok("netbsd-x64/-/netbsd-x64-0.19.10.tgz")
- "openbsd", "x64" -> Ok("openbsd-x64/-/openbsd-x64-0.19.10.tgz")
- "sunos", "x64" -> Ok("sunos-x64/-/sunos-x64-0.19.10.tgz")
-
- _, _ -> Error(UnknownPlatform(os, cpu))
- }
-
- result.map(path, string.append(base, _))
-}
-
-fn write_esbuild(bin, outdir, outfile) {
- let _ = simplifile.create_directory_all(outdir)
-
- simplifile.write_bits(outfile, bin)
-}
-
-fn set_filepermissions(file) {
- let permissions =
- FilePermissions(
- user: set.from_list([Read, Write, Execute]),
- group: set.from_list([Read, Execute]),
- other: set.from_list([Read, Execute]),
- )
-
- simplifile.set_permissions(file, permissions)
-}
-
-fn configure_node_tree_shaking(root) {
- // This whole chunk of code is to force tree shaking on dependencies that esbuild
- // has a habit of including because it thinks their imports might have side
- // effects.
- //
- // This is a really grim hack but it's the only way I've found to get esbuild to
- // ignore unused deps like `glint` that imports node stuff but aren't used in
- // app code.
- let force_tree_shaking = "{ \"sideEffects\": false }"
- let assert Ok(_) =
- simplifile.write(
- filepath.join(root, "build/dev/javascript/package.json"),
- force_tree_shaking,
- )
- let pure_deps = ["lustre", "glint", "simplifile"]
-
- list.try_each(pure_deps, fn(dep) {
- root
- |> filepath.join("build/dev/javascript/" <> dep)
- |> filepath.join("package.json")
- |> simplifile.write(force_tree_shaking)
- })
-}
-
-// ERROR HANDLING --------------------------------------------------------------
-
-pub type Error {
- BuildError
- BundleError(message: String)
- NetworkError(Dynamic)
- SimplifileError(reason: simplifile.FileError, path: String)
- UnknownPlatform(os: String, cpu: String)
- UnzipError(Dynamic)
-}
-
-pub fn explain(error: Error) -> Nil {
- 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 -------------------------------------------------------------------
-
-@external(erlang, "cli_ffi", "get_os")
-fn get_os() -> String
-
-@external(erlang, "cli_ffi", "get_cpu")
-fn get_cpu() -> String
-
-@external(erlang, "cli_ffi", "get_esbuild")
-fn get_esbuild(url: String) -> Result(BitArray, Dynamic)
-
-@external(erlang, "cli_ffi", "unzip_esbuild")
-fn unzip_esbuild(tarball: BitArray) -> Result(BitArray, Dynamic)
diff --git a/src/lustre/cli/project.gleam b/src/lustre/cli/project.gleam
deleted file mode 100644
index f52a847..0000000
--- a/src/lustre/cli/project.gleam
+++ /dev/null
@@ -1,186 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import filepath
-import gleam/dict.{type Dict}
-import gleam/dynamic.{type DecodeError, type Decoder, type Dynamic, DecodeError}
-import gleam/int
-import gleam/io
-import gleam/json
-import gleam/list
-import gleam/package_interface.{type Type, Fn, Named, Tuple, Variable}
-import gleam/pair
-import gleam/result
-import gleam/string
-import lustre/cli/utils.{exec, map, try}
-import simplifile
-import tom.{type Toml}
-
-// TYPES -----------------------------------------------------------------------
-
-pub type Config {
- Config(name: String, version: String, toml: Dict(String, Toml))
-}
-
-pub type Interface {
- Interface(name: String, version: String, modules: Dict(String, Module))
-}
-
-pub type Module {
- Module(constants: Dict(String, Type), functions: Dict(String, Function))
-}
-
-pub type Function {
- Function(parameters: List(Type), return: Type)
-}
-
-// COMMANDS --------------------------------------------------------------------
-
-/// Compile the current project running the `gleam build` command.
-///
-pub fn build() -> Result(Nil, String) {
- use _ <- try(
- exec(run: "gleam", in: ".", with: ["build", "--target=js"]),
- on_error: map(with: pair.second),
- )
-
- Ok(Nil)
-}
-
-pub fn interface() -> Result(Interface, String) {
- let dir = filepath.join(root(), "build/.lustre")
- let out = filepath.join(dir, "package-interface.json")
-
- use _ <- try(
- exec(run: "gleam", in: ".", with: [
- "export",
- "package-interface",
- "--out",
- out,
- ]),
- on_error: map(with: pair.second),
- )
-
- let assert Ok(json) = simplifile.read(out)
- let assert Ok(interface) = json.decode(json, interface_decoder)
- Ok(interface)
-}
-
-/// Read the project configuration in the `gleam.toml` file.
-///
-pub fn config() -> Result(Config, String) {
- use _ <- result.try(build())
-
- // Since we made sure that the project could compile we're sure that there is
- // bound to be a `gleam.toml` file somewhere in the current directory (or in
- // its parent directories). So we can safely call `root()` without
- // it looping indefinitely.
- let configuration_path = filepath.join(root(), "gleam.toml")
-
- // All these operations are safe to assert because the Gleam project wouldn't
- // compile if any of this stuff was invalid.
- let assert Ok(configuration) = simplifile.read(configuration_path)
- let assert Ok(toml) = tom.parse(configuration)
- let assert Ok(name) = tom.get_string(toml, ["name"])
- let assert Ok(version) = tom.get_string(toml, ["version"])
-
- Ok(Config(name: name, version: version, toml: toml))
-}
-
-// ERROR HANDLING --------------------------------------------------------------
-
-///
-///
-pub type Error {
- BuildError
-}
-
-pub fn explain(error: Error) -> Nil {
- 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 -----------------------------------------------------------------------
-
-/// Finds the path leading to the project's root folder. This recursively walks
-/// up from the current directory until it finds a `gleam.toml`.
-///
-pub fn root() -> String {
- find_root(".")
-}
-
-fn find_root(path: String) -> String {
- let toml = filepath.join(path, "gleam.toml")
-
- case simplifile.verify_is_file(toml) {
- Ok(False) | Error(_) -> find_root(filepath.join("..", path))
- Ok(True) -> path
- }
-}
-
-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 interface_decoder(dyn: Dynamic) -> Result(Interface, List(DecodeError)) {
- dynamic.decode3(
- Interface,
- dynamic.field("name", dynamic.string),
- dynamic.field("version", dynamic.string),
- dynamic.field("modules", string_dict(module_decoder)),
- )(dyn)
-}
-
-fn module_decoder(dyn: Dynamic) -> Result(Module, List(DecodeError)) {
- dynamic.decode2(
- Module,
- dynamic.field(
- "constants",
- string_dict(dynamic.field("type", package_interface.type_decoder)),
- ),
- dynamic.field("functions", string_dict(function_decoder)),
- )(dyn)
-}
-
-fn function_decoder(dyn: Dynamic) -> Result(Function, List(DecodeError)) {
- dynamic.decode2(
- Function,
- dynamic.field("parameters", dynamic.list(labelled_argument_decoder)),
- dynamic.field("return", package_interface.type_decoder),
- )(dyn)
-}
-
-fn labelled_argument_decoder(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
- // In this case we don't really care about the label, so we're just ignoring
- // it and returning the argument's type.
- dynamic.field("type", package_interface.type_decoder)(dyn)
-}
-
-fn string_dict(values: Decoder(a)) -> Decoder(Dict(String, a)) {
- dynamic.dict(dynamic.string, values)
-}
diff --git a/src/lustre/cli/step.gleam b/src/lustre/cli/step.gleam
deleted file mode 100644
index d3f8c8a..0000000
--- a/src/lustre/cli/step.gleam
+++ /dev/null
@@ -1,100 +0,0 @@
-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
-}
diff --git a/src/lustre/cli/tailwind.gleam b/src/lustre/cli/tailwind.gleam
deleted file mode 100644
index 5f4b4ec..0000000
--- a/src/lustre/cli/tailwind.gleam
+++ /dev/null
@@ -1,185 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import filepath
-import gleam/bool
-import gleam/dynamic.{type Dynamic}
-import gleam/io
-import gleam/result
-import gleam/set
-import gleam/string
-import lustre/cli/project
-import lustre/cli/step.{type Step}
-import lustre/cli/utils.{keep}
-import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
-
-const tailwind_version = "v3.4.1"
-
-// COMMANDS --------------------------------------------------------------------
-
-pub fn setup(os: String, cpu: String) -> Step(Nil, Error) {
- use _ <- step.run(download(os, cpu, tailwind_version), on_error: keep)
- use _ <- step.run(write_tailwind_config(), on_error: keep)
- step.return(Nil)
-}
-
-fn download(os: String, cpu: String, version: String) -> Step(Nil, Error) {
- use <- step.new("Downloading Tailwind")
-
- let root = project.root()
- let outdir = filepath.join(root, "build/.lustre/bin")
- let outfile = filepath.join(outdir, "tailwind")
-
- //todo as "do something with the version and see if its different from the one we already have"
-
- use <- bool.guard(check_tailwind_exists(outfile), {
- use <- step.done("✅ Tailwind already installed!")
- step.return(Nil)
- })
-
- use <- step.new("Detecting platform")
- use url <- step.try(get_download_url(os, cpu, version), keep)
-
- use <- step.new("Downloading from " <> url)
- use bin <- step.try(get_tailwind(url), NetworkError)
-
- let write_tailwind =
- write_tailwind(bin, outdir, outfile)
- |> result.map_error(CannotWriteTailwind(_, outfile))
- use _ <- step.try(write_tailwind, keep)
- use _ <- step.try(set_filepermissions(outfile), fn(reason) {
- CannotSetPermissions(reason, outfile)
- })
-
- use <- step.done("✅ Tailwind installed!")
-
- step.return(Nil)
-}
-
-fn write_tailwind_config() -> Step(Nil, Error) {
- let config_filename = "tailwind.config.js"
- let config_outfile = filepath.join(project.root(), config_filename)
- let config_already_exists =
- simplifile.verify_is_file(config_outfile)
- |> result.unwrap(False)
-
- // If there already is a configuration file, we make sure not to override it.
- use <- bool.guard(when: config_already_exists, return: step.return(Nil))
- use <- step.new("Writing `" <> config_filename <> "`")
- let write_config =
- simplifile.write(
- to: config_outfile,
- contents: "/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [\"./src/**/*.{gleam,mjs}\"],
- theme: {
- extend: {},
- },
- plugins: [],
-}
-",
- )
-
- use _ <- step.try(write_config, CannotWriteConfig(_, config_outfile))
- use <- step.done("✅ Written `" <> config_outfile <> "`")
- step.return(Nil)
-}
-
-// STEPS -----------------------------------------------------------------------
-
-fn check_tailwind_exists(path) {
- case simplifile.verify_is_file(path) {
- Ok(True) -> True
- Ok(False) | Error(_) -> False
- }
-}
-
-fn get_download_url(os, cpu, version) {
- let base =
- "https://github.com/tailwindlabs/tailwindcss/releases/download/"
- <> version
- <> "/tailwindcss-"
-
- let path = case os, cpu {
- "linux", "armv7" -> Ok("linux-armv7")
- "linux", "arm64" -> Ok("linux-arm64")
- "linux", "x64" | "linux", "x86_64" -> Ok("linux-x64")
-
- "win32", "arm64" -> Ok("windows-arm64.exe")
- "win32", "x64" | "win32", "x86_64" -> Ok("windows-x64.exe")
-
- "darwin", "arm64" | "darwin", "aarch64" -> Ok("macos-arm64")
- "darwin", "x64" | "darwin", "x86_64" -> Ok("macos-x64")
-
- _, _ -> Error(UnknownPlatform(os, cpu))
- }
-
- result.map(path, string.append(base, _))
-}
-
-fn write_tailwind(bin, outdir, outfile) {
- let _ = simplifile.create_directory_all(outdir)
-
- simplifile.write_bits(outfile, bin)
-}
-
-fn set_filepermissions(file) {
- let permissions =
- FilePermissions(
- user: set.from_list([Read, Write, Execute]),
- group: set.from_list([Read, Execute]),
- other: set.from_list([Read, Execute]),
- )
-
- simplifile.set_permissions(file, permissions)
-}
-
-// ERROR HANDLING --------------------------------------------------------------
-
-pub type Error {
- NetworkError(Dynamic)
- CannotWriteTailwind(reason: simplifile.FileError, path: String)
- CannotSetPermissions(reason: simplifile.FileError, path: String)
- CannotWriteConfig(reason: simplifile.FileError, path: String)
- UnknownPlatform(os: String, cpu: String)
- BundleError(reason: String)
-}
-
-pub fn explain(error: Error) -> Nil {
- case error {
- // TODO: Is there a better way to deal with this dynamic error?
- NetworkError(_dynamic) ->
- io.println(
- "
-There was a network error!",
- )
-
- UnknownPlatform(os, cpu) -> io.println("
-I couldn't figure out the correct Tailwind version for your
-os (" <> os <> ") and cpu (" <> cpu <> ").")
-
- CannotSetPermissions(reason, _) -> io.println("
-I ran into an error (" <> string.inspect(reason) <> ") when trying
-to set permissions for the Tailwind executable.
-")
-
- CannotWriteConfig(reason, _) -> io.println("
-I ran into an error (" <> string.inspect(reason) <> ") when trying
-to write the `tailwind.config.js` file to the project's root.
-")
-
- CannotWriteTailwind(reason, path) -> io.println("
-I ran into an error (" <> string.inspect(reason) <> ") when trying
-to write the Tailwind binary to
- `" <> path <> "`.
-")
-
- BundleError(reason) -> io.println("
-I ran into an error while trying to create a bundle with Tailwind:
-" <> reason)
- }
-}
-
-// EXTERNALS -------------------------------------------------------------------
-
-@external(erlang, "cli_ffi", "get_esbuild")
-fn get_tailwind(url: String) -> Result(BitArray, Dynamic)
diff --git a/src/lustre/cli/utils.gleam b/src/lustre/cli/utils.gleam
deleted file mode 100644
index a677e70..0000000
--- a/src/lustre/cli/utils.gleam
+++ /dev/null
@@ -1,68 +0,0 @@
-import simplifile
-import gleam/erlang
-
-pub fn template(name: String) -> String {
- let assert Ok(priv) = erlang.priv_directory("lustre")
- let assert Ok(file) = simplifile.read(priv <> "/templates/" <> name)
-
- file
-}
-
-@external(erlang, "cli_ffi", "exec")
-pub fn exec(
- run command: String,
- with args: List(String),
- in in: String,
-) -> Result(String, #(Int, String))
-
-// CHAINING RESULTS ------------------------------------------------------------
-
-pub fn try(
- result: Result(a, x),
- on_error strategy: ErrorStrategy(x, e),
- then f: fn(a) -> Result(b, e),
-) -> Result(b, e) {
- case result {
- Ok(value) -> f(value)
- Error(x) -> Error(strategy(x))
- }
-}
-
-pub type ErrorStrategy(x, e) =
- fn(x) -> e
-
-pub fn replace(with error: e) -> ErrorStrategy(x, e) {
- fn(_) { error }
-}
-
-pub fn map(with f: fn(x) -> e) -> ErrorStrategy(x, e) {
- f
-}
-
-pub fn keep(err: e) -> e {
- err
-}
-
-// BOOLEAN GUARDS --------------------------------------------------------------
-
-pub fn guard(
- condition: Bool,
- consequence: e,
- then: fn() -> Result(a, e),
-) -> Result(a, e) {
- case condition {
- True -> Error(consequence)
- False -> then()
- }
-}
-
-pub fn when(
- condition: Bool,
- consequence: a,
- then: fn() -> Result(a, e),
-) -> Result(a, e) {
- case condition {
- True -> Ok(consequence)
- False -> then()
- }
-}
diff --git a/test/lustre_test.gleam b/test/lustre_test.gleam
index bae6c0f..9e47a01 100644
--- a/test/lustre_test.gleam
+++ b/test/lustre_test.gleam
@@ -35,8 +35,8 @@ pub fn counter_init_test() {
process.call(
runtime,
function.curry2(process.send)
- |> function.compose(View)
- |> function.compose(Debug),
+ |> function.compose(View)
+ |> function.compose(Debug),
100,
)
@@ -57,8 +57,8 @@ pub fn counter_update_test() {
process.call(
runtime,
function.curry2(process.send)
- |> function.compose(View)
- |> function.compose(Debug),
+ |> function.compose(View)
+ |> function.compose(Debug),
100,
)
@@ -75,8 +75,8 @@ pub fn counter_diff_test() {
process.call(
runtime,
function.curry2(process.send)
- |> function.compose(View)
- |> function.compose(Debug),
+ |> function.compose(View)
+ |> function.compose(Debug),
100,
)
@@ -88,8 +88,8 @@ pub fn counter_diff_test() {
process.call(
runtime,
function.curry2(process.send)
- |> function.compose(View)
- |> function.compose(Debug),
+ |> function.compose(View)
+ |> function.compose(Debug),
100,
)