aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-02-18 10:45:00 +0000
committerHayleigh Thompson <me@hayleigh.dev>2024-02-18 10:45:00 +0000
commit69b36cf003212e98800d5f27af74bb59f3bbca5d (patch)
tree11a198e64017a5ac4207c1311b1d9ac3b0ee92de /src
parent8128c25b6d8e597e5c652007f483c22047425c5c (diff)
downloadlustre-69b36cf003212e98800d5f27af74bb59f3bbca5d.tar.gz
lustre-69b36cf003212e98800d5f27af74bb59f3bbca5d.zip
:recycle: Big refactor of CLI-related things.
Diffstat (limited to 'src')
-rw-r--r--src/cli_ffi.erl (renamed from src/lustre_add_ffi.erl)10
-rw-r--r--src/lustre/cli/add.gleam94
-rw-r--r--src/lustre/cli/build.gleam420
-rw-r--r--src/lustre/cli/dev.gleam211
-rw-r--r--src/lustre/cli/esbuild.gleam238
-rw-r--r--src/lustre/cli/project.gleam185
-rw-r--r--src/lustre/cli/try.gleam108
-rw-r--r--src/lustre/cli/utils.gleam53
-rw-r--r--src/lustre_build_ffi.erl9
-rw-r--r--src/lustre_try_ffi.erl115
10 files changed, 864 insertions, 579 deletions
diff --git a/src/lustre_add_ffi.erl b/src/cli_ffi.erl
index d7c5030..1c1b6f0 100644
--- a/src/lustre_add_ffi.erl
+++ b/src/cli_ffi.erl
@@ -1,4 +1,4 @@
--module(lustre_add_ffi).
+-module(cli_ffi).
-export([
get_cpu/0,
get_esbuild/1,
@@ -32,8 +32,8 @@ get_esbuild(Url) ->
case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
{ok, {{_, 200, _}, _, Zip}} -> {ok, Zip};
- {ok, Res} -> {error, {network_error, Res}};
- {error, Err} -> {error, {network_error, Err}}
+ {ok, Res} -> {error, Res};
+ {error, Err} -> {error, Err}
end.
unzip_esbuild(Zip) ->
@@ -44,6 +44,6 @@ unzip_esbuild(Zip) ->
case Result of
{ok, [{_, Esbuild}]} -> {ok, Esbuild};
- {ok, Res} -> {error, {unzip_error, Res}};
- {error, Err} -> {error, {unzip_error, Err}}
+ {ok, Res} -> {error, Res};
+ {error, Err} -> {error, Err}
end.
diff --git a/src/lustre/cli/add.gleam b/src/lustre/cli/add.gleam
index 1a6ebe4..5323e8d 100644
--- a/src/lustre/cli/add.gleam
+++ b/src/lustre/cli/add.gleam
@@ -1,71 +1,53 @@
// IMPORTS ---------------------------------------------------------------------
-import gleam/io
-import gleam/option
-import gleam/result
-import gleam/string
import glint.{type Command, CommandInput}
import glint/flag
import lustre/cli/esbuild
-// MAIN ------------------------------------------------------------------------
+// COMMANDS --------------------------------------------------------------------
pub fn esbuild() -> Command(Nil) {
- glint.command(fn(input) {
- let CommandInput(args: _, flags: flags, named_args: _) = input
- let os = option.from_result(flag.get_string(flags, os_flag_name))
- let cpu = option.from_result(flag.get_string(flags, cpu_flag_name))
+ let description =
+ "
+Download a platform-appropriate version of the esbuild binary. Lustre uses this
+to bundle applications and act as a development server.
+ "
- esbuild.download(os, cpu)
- |> result.map_error(explain)
- |> result.replace(Nil)
- |> result.unwrap_both
+ 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 result = esbuild.download(os, cpu)
+
+ case result {
+ Ok(_) -> Nil
+ Error(error) -> esbuild.explain(error)
+ }
})
- |> glint.flag(os_flag_name, os_flag())
- |> glint.flag(cpu_flag_name, cpu_flag())
-}
-
-// GLINT FLAGS -----------------------------------------------------------------
-
-const os_flag_name = "os"
+ |> glint.description(description)
+ |> glint.count_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()
-fn os_flag() {
- flag.string()
- |> flag.description("The host to run the server on")
+ flag.string()
+ |> flag.default(default)
+ |> flag.description(description)
+ })
}
-const cpu_flag_name = "cpu"
+// EXTERNALS -------------------------------------------------------------------
-fn cpu_flag() {
- flag.string()
- |> flag.description("The port to run the server on")
-}
+@external(erlang, "cli_ffi", "get_os")
+fn get_os() -> String
-// UTILS -----------------------------------------------------------------------
-
-fn explain(error: esbuild.Error) -> Nil {
- case error {
- esbuild.NetworkError(_) ->
- "🚨 A network error occured. Check your connection and try again "
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- esbuild.SimplifileError(_error, path) ->
- "🚨 An unknown error occured while writing the executable to `{path}` "
- |> string.replace(each: "{path}", with: path)
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- esbuild.UnknownPlatform(os, cpu) ->
- { "🚨 Could not locate download url for " <> os <> "/" <> cpu <> " " }
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- esbuild.UnzipError(_error) ->
- "🚨 An unknown error occured while extracting the archive "
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- esbuild.BundleError(message) ->
- { "🚨 " <> message }
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- }
- |> io.println
-}
+@external(erlang, "cli_ffi", "get_cpu")
+fn get_cpu() -> String
diff --git a/src/lustre/cli/build.gleam b/src/lustre/cli/build.gleam
index e5a45c4..2b939f3 100644
--- a/src/lustre/cli/build.gleam
+++ b/src/lustre/cli/build.gleam
@@ -1,226 +1,298 @@
// IMPORTS ---------------------------------------------------------------------
-import gleam/dict.{type Dict}
+import filepath
+import gleam/dict
import gleam/io
-import gleam/option.{type Option, None}
-import gleam/result
import gleam/list
+import gleam/result
import gleam/string
import glint.{type Command, CommandInput}
-import glint/flag.{type Flag}
-import filepath
-import simplifile
-import lustre/cli/project
+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 simplifile
-// TYPES -----------------------------------------------------------------------
+// COMMANDS --------------------------------------------------------------------
-type Error {
- MissingComponentPath
- MissingModule(module: String)
- /// In case a module is provided that doesn't expose a register function.
- MissingRegisterFunction(module: String)
- /// In case a module exposes a register function, but with an unexpected type.
- /// TODO: for now the type is just a string, later it's going to be a type
- /// provided by the package interface decoder.
- RegisterFunctionWithWrongType(type_: String)
- CompileError
- CannotDownloadEsbuild(error: esbuild.Error)
- CannotBundleComponents(error: esbuild.Error)
- CannotPerformCleanup(temp_file: String)
- CannotWriteTempFile(reason: simplifile.FileError, temp_file: String)
-}
+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.
-// MAIN ------------------------------------------------------------------------
+This is different from using `gleam build` directly because it produces a single
+JavaScript module for you to host or distribute.
+ "
-pub fn component() -> Command(Nil) {
glint.command(fn(input) {
- let CommandInput(args: args, flags: flags, named_args: _) = input
- let assert Ok(minify) = flag.get_bool(flags, minify_flag_name)
+ let CommandInput(flags: flags, ..) = input
+ let assert Ok(minify) = flag.get_bool(flags, "minify")
+
+ let result = {
+ use _ <- result.try(prepare_esbuild())
+
+ 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")
+ 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 =
+ "import { main } from '../dev/javascript/${project_name}/${project_name}.mjs';
+
+ main();
+ "
+ |> string.replace("${project_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 _ <- result.try(bundle(entry, tempdir, outfile, minify))
+
+ Ok(Nil)
+ }
- build_component(args, minify)
- |> result.map_error(explain)
- |> result.unwrap(Nil)
+ case result {
+ Ok(_) -> Nil
+ Error(error) -> explain(error)
+ }
+ })
+ |> glint.description(description)
+ |> glint.count_args(glint.EqArgs(0))
+ |> glint.flag("minify", {
+ let description = "Minify the output"
+ let default = False
+
+ flag.bool()
+ |> flag.default(default)
+ |> flag.description(description)
})
- |> glint.flag(minify_flag_name, minify_flag())
- |> glint.count_args(glint.MinArgs(1))
}
-pub fn app() -> Command(Nil) {
+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(args: _, flags: flags, named_args: named_args) = input
- let assert Ok(minify) = flag.get_bool(flags, minify_flag_name)
- let module = option.from_result(dict.get(named_args, module_named_arg))
+ 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 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))
+
+ io.println(" ├ generating 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())
+ 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)
+ |> string.replace("${project_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 _ <- result.try(bundle(entry, tempdir, outfile, minify))
+
+ Ok(Nil)
+ }
- build_app(module, minify)
- |> result.map_error(explain)
- |> result.unwrap(Nil)
+ case result {
+ Ok(_) -> Nil
+ Error(error) -> explain(error)
+ }
+ })
+ |> glint.description(description)
+ |> glint.named_args(["module_path"])
+ |> glint.count_args(glint.EqArgs(1))
+ |> glint.flag("minify", {
+ let description = "Minify the output"
+ let default = False
+
+ flag.bool()
+ |> flag.default(default)
+ |> flag.description(description)
})
- |> glint.flag(minify_flag_name, minify_flag())
- |> glint.named_args([module_named_arg])
}
-// GLINT FLAGS -----------------------------------------------------------------
-
-const module_named_arg = "module"
+// ERROR HANDLING --------------------------------------------------------------
-const minify_flag_name = "minify"
-
-fn minify_flag() -> flag.FlagBuilder(Bool) {
- flag.bool()
- |> flag.default(False)
- |> flag.description(string.join(
- [
- "A minified bundle renames variables to shorter names and obfuscates the code.",
- "Minified bundles are always emitted with the `.min.mjs` extension.",
- ],
- " ",
- ))
+type Error {
+ BuildError
+ BundleError(esbuild.Error)
+ ComponentMissing(module: String)
+ MainMissing(module: String)
+ ModuleMissing(module: String)
+ NameIncorrectType(module: String, got: project.Type)
+ NameMissing(module: String)
}
-// BUILD COMPONENT -------------------------------------------------------------
+fn explain(error: Error) -> Nil {
+ error
+ |> string.inspect
+ |> io.println
+}
-fn build_component(modules: List(String), minify: Bool) -> Result(Nil, Error) {
- // Build the project to make sure it doesn't have any compile errors.
- let compile = result.replace_error(project.build(), CompileError)
- use compiled <- result.try(compile)
- let configuration = project.read_configuration(compiled)
+// STEPS -----------------------------------------------------------------------
- // Ensure that all modules we're going to bundle actually expose the correct
- // `register` function needed to register a component.
- use _ <- result.try(list.try_each(modules, check_can_be_bundled))
+fn prepare_esbuild() -> Result(Nil, Error) {
+ esbuild.download(get_os(), get_cpu())
+ |> result.replace_error(BuildError)
+}
- // Figure out the outfile name based on the number of modules to bundle.
- let output_file_name = case modules {
- [module] -> {
- let component_name = string.replace(module, each: "/", with: "_")
- configuration.name <> "-" <> component_name
- }
- _ -> configuration.name <> "-components"
- }
+fn get_project_name() -> Result(String, Error) {
+ use config <- try(project.config(), replace(with: BuildError))
+ Ok(config.name)
+}
- let priv = filepath.join(project.root_folder(), "priv")
+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)),
+ )
- components_script(configuration.name, modules)
- |> bundle_script(minify, in: priv, named: output_file_name)
+ Ok(module)
}
-fn check_can_be_bundled(_module_name: String) -> Result(Nil, Error) {
- // let package_interface = todo
- // let find_module =
- // dict.get(package_interface.modules, module_name)
- // |> result.replace_error(MissingModule(module_name))
- // use module <- result.try(find_module)
- // let find_register_function =
- // dict.get(module.functions, "register")
- // |> result.replace_error(MissingRegisterFunction(module_name))
- // use register_function <- result.try(find_register_function)
- // check_returns_component(register_function)
-
- // ^^^ We will be able to do all the above once the compiler exposes the
- // package interface, for now we just assume everything's ok.
- Ok(Nil)
+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))
+ }
}
-/// Generates the script that will be bundled, exposing the `register` function
-/// of each one of the provided modules.
-///
-fn components_script(project_name: String, modules: List(String)) -> String {
- use script, module <- list.fold(over: modules, from: "")
- let module_path =
- "../build/dev/javascript/" <> project_name <> "/" <> module <> ".mjs"
-
- let alias = "register-" <> string.replace(each: "\\", with: "-", in: module)
- let export =
- "export { register as " <> alias <> " } from \"" <> module_path <> "\""
- let register = alias <> "();"
- script <> "\n" <> export <> "\n" <> register
-}
+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)),
+ )
-// BUILD APP -------------------------------------------------------------------
+ case is_string_type(component_name) {
+ True -> Ok(Nil)
+ False -> Error(NameIncorrectType(module_path, component_name))
+ }
+}
-fn build_app(module: Option(String), minify: Bool) -> Result(Nil, Error) {
- // Build the project to make sure it doesn't have any compile errors.
- let compile = result.replace_error(project.build(), CompileError)
- use compiled <- result.try(compile)
- let configuration = project.read_configuration(compiled)
+fn find_component(module_path: String, module: Module) -> Result(String, Error) {
+ let functions = dict.to_list(module.functions)
+ let error = Error(ComponentMissing(module_path))
- // If the module is missing we use the module with the same name as the
- // project as a fallback.
- let module = option.lazy_unwrap(module, fn() { configuration.name })
- use _ <- result.try(check_app_can_be_bundled(module))
+ 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)
+ }
+}
- let priv = filepath.join(project.root_folder(), "priv")
+fn bundle(
+ entry: String,
+ tempdir: String,
+ outfile: String,
+ minify: Bool,
+) -> Result(Nil, Error) {
+ let entryfile = filepath.join(tempdir, "entry.mjs")
+ let assert Ok(_) = simplifile.write(entryfile, entry)
- app_script(configuration.name, module)
- |> bundle_script(minify, in: priv, named: module <> "-app")
-}
+ use _ <- try(
+ esbuild.bundle(entryfile, outfile, minify),
+ map(with: BundleError),
+ )
-fn check_app_can_be_bundled(_module: String) -> Result(Nil, Error) {
- // Once we have the package interface we'll be able to check if the module
- // actually exposes a function with the appropriate type etc.
- // For now we just assume the user knows what they're doing.
Ok(Nil)
}
-fn app_script(project_name: String, module: String) -> String {
- let module_path =
- "../build/dev/javascript/" <> project_name <> "/" <> module <> ".mjs"
- let import_main = "import { main } from \"" <> module_path <> "\""
- let invoke_main = "main();"
+// UTILS -----------------------------------------------------------------------
- import_main <> "\n" <> invoke_main
+fn is_string_type(t: Type) -> Bool {
+ case t {
+ Named(name: "String", package: "", module: "gleam", parameters: []) -> True
+ _ -> False
+ }
}
-// UTILS -----------------------------------------------------------------------
+fn is_nil_type(t: Type) -> Bool {
+ case t {
+ Named(name: "Nil", package: "", module: "gleam", parameters: []) -> True
+ _ -> False
+ }
+}
-fn explain(error: Error) -> Nil {
- case error {
- _ -> {
- io.debug(error)
- todo as "explain the error"
- }
+fn is_type_variable(t: Type) -> Bool {
+ case t {
+ Variable(..) -> True
+ _ -> False
}
- |> string.pad_right(78, ".")
- |> string.append(" ❌")
- |> io.println
}
-fn bundle_script(
- script: String,
- minify: Bool,
- in folder: String,
- named output_file: String,
-) -> Result(Nil, Error) {
- // First, let's make sure there's the esbuild executable that can be used.
- use esbuild <- result.try(
- esbuild.download(None, None)
- |> result.map_error(CannotDownloadEsbuild),
- )
+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
+ }
+}
- let output_file = filepath.join(folder, output_file)
+// EXTERNALS -------------------------------------------------------------------
- // Write the script to bundle to a temporary file that is going to be bundled
- // by escript.
- let temp_file = filepath.join(folder, "temp-bundle-input")
- use _ <- result.try(
- simplifile.write(script, to: temp_file)
- |> result.map_error(CannotWriteTempFile(_, temp_file)),
- )
+@external(erlang, "cli_ffi", "get_os")
+fn get_os() -> String
- let bundle_result =
- esbuild.bundle(esbuild, temp_file, output_file, minify)
- |> result.map_error(CannotBundleComponents)
-
- // Regardless of the result of the bundling process we delete the temporary
- // input file.
- case bundle_result {
- Ok(Nil) ->
- simplifile.delete(temp_file)
- |> result.replace_error(CannotPerformCleanup(temp_file))
- Error(error) -> {
- let _ = simplifile.delete(temp_file)
- Error(error)
- }
- }
-}
+@external(erlang, "cli_ffi", "get_cpu")
+fn get_cpu() -> String
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)
+}
diff --git a/src/lustre/cli/esbuild.gleam b/src/lustre/cli/esbuild.gleam
index b0e9fd2..c98f5f3 100644
--- a/src/lustre/cli/esbuild.gleam
+++ b/src/lustre/cli/esbuild.gleam
@@ -1,48 +1,114 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import filepath
+import gleam/bool
import gleam/dynamic.{type Dynamic}
-import gleam/option.{type Option}
+import gleam/function
+import gleam/io
+import gleam/list
+import gleam/pair
import gleam/result
import gleam/set
import gleam/string
-import filepath
+import lustre/cli/project
+import lustre/cli/utils.{keep, map, replace, try}
import shellout
import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
-import lustre/cli/project
-// CONSTANTS -------------------------------------------------------------------
+// COMMANDS --------------------------------------------------------------------
-pub const executable_name = "esbuild"
+pub fn download(os: String, cpu: String) -> Result(Nil, Error) {
+ let outdir = filepath.join(project.root(), "build/.lustre/bin")
+ let outfile = filepath.join(outdir, "esbuild")
-// TYPES -----------------------------------------------------------------------
+ use <- bool.guard(check_esbuild_exists(outfile), Ok(Nil))
-pub type Error {
- NetworkError(Dynamic)
- SimplifileError(reason: simplifile.FileError, path: String)
- UnknownPlatform(os: String, cpu: String)
- UnzipError(Dynamic)
- BundleError(message: String)
+ io.println("\nInstalling esbuild...")
+ io.println(" ├ detecting platform")
+ use url <- result.try(get_download_url(os, cpu))
+
+ io.println(" ├ downloading from " <> url)
+ use tarball <- 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))
+
+ io.println(" ├ installed!")
+ Ok(Nil)
}
-pub opaque type Executable {
- Executable
+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))
+
+ io.println("\nBundling with esbuild...")
+ io.println(" ├ configuring tree shaking")
+ let root = project.root()
+ use _ <- try(configure_node_tree_shaking(root), map(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 _ <- try(
+ shellout.command(
+ run: "./build/.lustre/bin/esbuild",
+ in: root,
+ with: options,
+ opt: [],
+ ),
+ on_error: map(function.compose(pair.second, BundleError)),
+ )
+
+ io.println(" ├ bundle produced at " <> output_file)
+ Ok(Nil)
}
-// DOWNLOAD ESBUILD ------------------------------------------------------------
-
-/// Download the esbuild executable for the given os and cpu. If those options
-/// are not provided it tries detecting the system's os and cpu.
-///
-/// The executable will be at the project's root in the `priv/bin/esbuild`
-/// folder.
-///
-/// Returns a proof that esbuild was successfully downloaded that can be used
-/// to run all sort of things.
-///
-pub fn download(
- os: Option(String),
- cpu: Option(String),
-) -> Result(Executable, Error) {
- let os = option.unwrap(os, get_os())
- let cpu = option.unwrap(cpu, get_cpu())
+pub fn serve(host: String, port: String) -> Result(Nil, Error) {
+ use _ <- try(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(
+ shellout.command(
+ run: "./build/.lustre/bin/esbuild",
+ in: root,
+ with: flags,
+ opt: [],
+ ),
+ on_error: map(function.compose(pair.second, BundleError)),
+ )
+
+ Ok(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")
@@ -74,82 +140,78 @@ pub fn download(
_, _ -> Error(UnknownPlatform(os, cpu))
}
- use url <- result.try(result.map(path, string.append(base, _)))
- use tarball <- result.try(get_esbuild(url))
-
- let destination_folder =
- project.root_folder()
- |> filepath.join("priv")
- |> filepath.join("bin")
- let esbuild_path = filepath.join(destination_folder, executable_name)
-
- let _ = simplifile.create_directory_all(destination_folder)
- use esbuild <- result.try(unzip_esbuild(tarball))
- use _ <- result.try(
- esbuild_path
- |> simplifile.write_bits(esbuild)
- |> result.map_error(SimplifileError(_, esbuild_path)),
- )
+ 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]),
)
- use _ <- result.try(
- esbuild_path
- |> simplifile.set_permissions(permissions)
- |> result.map_error(SimplifileError(_, esbuild_path)),
- )
- Ok(Executable)
+ simplifile.set_permissions(file, permissions)
}
-// BUNDLE ----------------------------------------------------------------------
+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 `shellout` that import 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", "shellout"]
+
+ list.try_each(pure_deps, fn(dep) {
+ root
+ |> filepath.join("build/dev/javascript/" <> dep)
+ |> filepath.join("package.json")
+ |> simplifile.write(force_tree_shaking)
+ })
+}
-/// Bundles the given contents. The command will run in the project's root.
-///
-pub fn bundle(
- _esbuild: Executable,
- input_file: String,
- output_file: String,
- minify: Bool,
-) -> Result(Nil, Error) {
- let flags = [
- "--bundle",
- // The format is always "esm", we're not encouraging "cjs".
- "--format=esm",
- "--outfile=" <> output_file,
- ]
+// ERROR HANDLING --------------------------------------------------------------
- let options = case minify {
- True -> [input_file, "--minify", ..flags]
- False -> [input_file, ..flags]
- }
+pub type Error {
+ BuildError
+ BundleError(message: String)
+ NetworkError(Dynamic)
+ SimplifileError(reason: simplifile.FileError, path: String)
+ UnknownPlatform(os: String, cpu: String)
+ UnzipError(Dynamic)
+}
- shellout.command(
- run: filepath.join(".", "priv")
- |> filepath.join("bin")
- |> filepath.join(executable_name),
- in: project.root_folder(),
- with: options,
- opt: [],
- )
- |> result.replace(Nil)
- |> result.map_error(fn(error) { BundleError(error.1) })
+pub fn explain(error: Error) -> Nil {
+ error
+ |> string.inspect
+ |> io.println
}
// EXTERNALS -------------------------------------------------------------------
-@external(erlang, "lustre_add_ffi", "get_os")
+@external(erlang, "cli_ffi", "get_os")
fn get_os() -> String
-@external(erlang, "lustre_add_ffi", "get_cpu")
+@external(erlang, "cli_ffi", "get_cpu")
fn get_cpu() -> String
-@external(erlang, "lustre_add_ffi", "get_esbuild")
-fn get_esbuild(url: String) -> Result(BitArray, Error)
+@external(erlang, "cli_ffi", "get_esbuild")
+fn get_esbuild(url: String) -> Result(BitArray, Dynamic)
-@external(erlang, "lustre_add_ffi", "unzip_esbuild")
-fn unzip_esbuild(tarball: BitArray) -> Result(BitArray, Error)
+@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
index 0b7287e..ad6adb5 100644
--- a/src/lustre/cli/project.gleam
+++ b/src/lustre/cli/project.gleam
@@ -1,63 +1,200 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import filepath
import gleam/dict.{type Dict}
+import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError}
+import gleam/io
+import gleam/json
+import gleam/pair
import gleam/result
-import filepath
+import gleam/string
+import lustre/cli/utils.{map, try}
import shellout
import simplifile
import tom.{type Toml}
-/// The configuration of a Gleam project
-pub type Configuration {
- Configuration(name: String, version: String, toml: Dict(String, Toml))
+// TYPES -----------------------------------------------------------------------
+
+pub type Config {
+ Config(name: String, version: String, toml: Dict(String, Toml))
}
-/// A proof that the project was compiled successfully.
-///
-pub opaque type Compiled {
- Compiled
+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)
}
+pub type Type {
+ Named(name: String, package: String, module: String, parameters: List(Type))
+ Variable(id: Int)
+ Fn(parameters: List(Type), return: Type)
+}
+
+// COMMANDS --------------------------------------------------------------------
+
/// Compile the current project running the `gleam build` command.
///
-pub fn build() -> Result(Compiled, Nil) {
- shellout.command(
- run: "gleam",
- in: ".",
- with: ["build", "--target=js"],
- opt: [],
+pub fn build() -> Result(Nil, String) {
+ use _ <- try(
+ shellout.command(
+ run: "gleam",
+ in: ".",
+ with: ["build", "--target=js"],
+ opt: [],
+ ),
+ on_error: map(with: pair.second),
)
- |> result.nil_error()
- |> result.replace(Compiled)
+
+ 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(
+ shellout.command(
+ run: "gleam",
+ in: ".",
+ with: ["export", "package-interface", "--out", out],
+ opt: [],
+ ),
+ on_error: map(with: pair.second),
+ )
+
+ let assert Ok(json) = simplifile.read(out)
+ let assert Ok(interface) = json.decode(json, decode_interface)
+
+ Ok(interface)
}
/// Read the project configuration in the `gleam.toml` file.
/// To call this function it's necessary to provide a proof that the project
/// was successfully built. To do so you can use the `build` function.
///
-pub fn read_configuration(_compiled: Compiled) -> Configuration {
+pub fn config() -> Result(Config, String) {
+ use _ <- result.try(build())
+
// Since we have proof 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 `recursive_lookup` without
// it looping indefinitely.
- let configuration_path = filepath.join(root_folder(), "gleam.toml")
+ 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"])
- Configuration(name: name, version: version, toml: toml)
+
+ Ok(Config(name: name, version: version, toml: toml))
+}
+
+// ERROR HANDLING --------------------------------------------------------------
+
+///
+///
+pub type Error {
+ BuildError
+}
+
+pub fn explain(error: Error) -> Nil {
+ error
+ |> string.inspect
+ |> io.println
}
-/// Finds the path leading to the project's root folder.
+// 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_folder() -> String {
- do_root_folder(".")
+pub fn root() -> String {
+ find_root(".")
}
-fn do_root_folder(path: String) -> String {
+fn find_root(path: String) -> String {
let toml = filepath.join(path, "gleam.toml")
+
case simplifile.verify_is_file(toml) {
- Ok(False) | Error(_) -> do_root_folder(filepath.join("..", path))
+ Ok(False) | Error(_) -> find_root(filepath.join("..", path))
Ok(True) -> path
}
}
+
+// DECODERS --------------------------------------------------------------------
+
+fn decode_interface(dyn: Dynamic) -> Result(Interface, List(DecodeError)) {
+ dynamic.decode3(
+ Interface,
+ dynamic.field("name", dynamic.string),
+ dynamic.field("version", dynamic.string),
+ dynamic.field("modules", dynamic.dict(dynamic.string, decode_module)),
+ )(dyn)
+}
+
+fn decode_module(dyn: Dynamic) -> Result(Module, List(DecodeError)) {
+ dynamic.decode2(
+ Module,
+ dynamic.field(
+ "constants",
+ dynamic.dict(dynamic.string, dynamic.field("type", decode_type)),
+ ),
+ dynamic.field("functions", dynamic.dict(dynamic.string, decode_function)),
+ )(dyn)
+}
+
+fn decode_function(dyn: Dynamic) -> Result(Function, List(DecodeError)) {
+ dynamic.decode2(
+ Function,
+ dynamic.field("parameters", dynamic.list(decode_type)),
+ dynamic.field("return", decode_type),
+ )(dyn)
+}
+
+fn decode_type(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
+ use kind <- result.try(dynamic.field("kind", dynamic.string)(dyn))
+
+ case kind {
+ "named" -> decode_named_type(dyn)
+ "variable" -> decode_variable_type(dyn)
+ "fn" -> decode_fn_type(dyn)
+
+ _ ->
+ Error([
+ DecodeError(found: kind, expected: "'named' | 'variable' | 'fn'", path: [
+ "kind",
+ ]),
+ ])
+ }
+}
+
+fn decode_named_type(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
+ dynamic.decode4(
+ Named,
+ dynamic.field("name", dynamic.string),
+ dynamic.field("package", dynamic.string),
+ dynamic.field("module", dynamic.string),
+ dynamic.field("parameters", dynamic.list(decode_type)),
+ )(dyn)
+}
+
+fn decode_variable_type(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
+ dynamic.decode1(Variable, dynamic.field("id", dynamic.int))(dyn)
+}
+
+fn decode_fn_type(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
+ dynamic.decode2(
+ Fn,
+ dynamic.field("parameters", dynamic.list(decode_type)),
+ dynamic.field("return", decode_type),
+ )(dyn)
+}
diff --git a/src/lustre/cli/try.gleam b/src/lustre/cli/try.gleam
deleted file mode 100644
index 42f3854..0000000
--- a/src/lustre/cli/try.gleam
+++ /dev/null
@@ -1,108 +0,0 @@
-// IMPORTS ---------------------------------------------------------------------
-
-import gleam_community/ansi
-import gleam/int
-import gleam/io
-import gleam/result
-import glint.{type Command, CommandInput}
-import glint/flag
-import lustre/cli/project
-
-// TYPES -----------------------------------------------------------------------
-
-type Options {
- /// It's important to remember that for Erlang, Gleam records have their field
- /// names erased and they degenerate to tuples. This means that the order of
- /// the fields is important!
- Options(name: String, host: String, port: Int, no_styles: Bool)
-}
-
-type Error {
- CompileError
-}
-
-// MAIN ------------------------------------------------------------------------
-
-pub fn run() -> Command(Nil) {
- glint.command(fn(input) {
- let CommandInput(_, flags, _) = input
- let assert Ok(port) = flag.get_int(flags, port_flag_name)
- let assert Ok(host) = flag.get_string(flags, host_flag_name)
- let assert Ok(no_styles) = flag.get_bool(flags, no_styles_flag_name)
-
- let result = {
- let compile = result.replace_error(project.build(), CompileError)
- use compiled <- result.try(compile)
- let configuration = project.read_configuration(compiled)
- let options = Options(configuration.name, host, port, no_styles)
- serve(options, on_start(host, _), on_port_taken)
- Ok(Nil)
- }
-
- case result {
- Ok(_) -> Nil
- Error(error) -> explain(error)
- }
- })
- |> glint.flag(host_flag_name, host_flag())
- |> glint.flag(port_flag_name, port_flag())
- |> glint.flag(no_styles_flag_name, no_styles_flag())
-}
-
-// UTILS -----------------------------------------------------------------------
-
-fn explain(error: Error) -> Nil {
- case error {
- CompileError -> Nil
- }
-}
-
-// GLINT FLAGS -----------------------------------------------------------------
-
-const host_flag_name = "host"
-
-fn host_flag() {
- flag.string()
- |> flag.default("localhost")
- |> flag.description("The host to run the server on.")
-}
-
-const port_flag_name = "port"
-
-fn port_flag() {
- flag.int()
- |> flag.default(1234)
- |> flag.description("The port to run the server on.")
-}
-
-const no_styles_flag_name = "no-styles"
-
-fn no_styles_flag() {
- flag.bool()
- |> flag.default(False)
- |> flag.description("When false, lustre/ui's styles will not be included.")
-}
-
-// UTILS -----------------------------------------------------------------------
-
-fn on_start(host: String, port: Int) -> Nil {
- let address = "http://" <> host <> ":" <> int.to_string(port)
- io.println("✨ Server has been started at " <> ansi.bold(address))
-}
-
-fn on_port_taken(port) -> Nil {
- io.println(
- "🚨 Port "
- <> ansi.bold(int.to_string(port))
- <> " already in use, using next available port",
- )
-}
-
-// EXTERNALS -------------------------------------------------------------------
-
-@external(erlang, "lustre_try_ffi", "serve")
-fn serve(
- options: Options,
- on_start: fn(Int) -> Nil,
- on_port_taken: fn(Int) -> Nil,
-) -> Nil
diff --git a/src/lustre/cli/utils.gleam b/src/lustre/cli/utils.gleam
new file mode 100644
index 0000000..ddf62e3
--- /dev/null
+++ b/src/lustre/cli/utils.gleam
@@ -0,0 +1,53 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/function
+
+// 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 const keep: ErrorStrategy(e, e) = function.identity
+
+// 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/src/lustre_build_ffi.erl b/src/lustre_build_ffi.erl
deleted file mode 100644
index 9a7e280..0000000
--- a/src/lustre_build_ffi.erl
+++ /dev/null
@@ -1,9 +0,0 @@
--module(lustre_build_ffi).
--export([
- exec/1
-]).
-
-exec(Cmd) ->
- Stdout = os:cmd(unicode:characters_to_list(Cmd)),
-
- unicode:characters_to_binary(Stdout).
diff --git a/src/lustre_try_ffi.erl b/src/lustre_try_ffi.erl
deleted file mode 100644
index 0209c14..0000000
--- a/src/lustre_try_ffi.erl
+++ /dev/null
@@ -1,115 +0,0 @@
--module(lustre_try_ffi).
--export([serve/3, response_default_headers/0, exec/1]).
-
-serve({options, Name, Host, Port, NoStyles}, OnStart, OnPortTaken) ->
- Html =
- <<
- "<!DOCTYPE html>\n"
- "<html lang=\"en\">\n"
- "<head>\n"
- " <meta charset=\"UTF-8\">\n"
- " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
- " <title>Lustre preview server</title>\n",
- case NoStyles of
- true ->
- <<"">>;
- false ->
- <<" <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css\">\n">>
- end/binary,
- " <script type=\"module\">\n"
- " import { main } from './",
- Name/binary,
- "/",
- Name/binary,
- ".mjs'\n"
- "\n"
- " document.addEventListener(\"DOMContentLoaded\", () => {\n"
- " main();\n"
- " });\n"
- " </script>\n"
- "</head>\n"
- "<body>\n"
- " <div id=\"app\"></div>\n"
- "</body>\n"
- "</html>"
- >>,
-
- file:write_file("build/dev/javascript/index.html", Html),
-
- AbsPath =
- string:trim(
- filename:absname("build/dev/javascript"), trailing, "/."
- ),
-
- inets:start(),
- Address = {127, 0, 0, 1},
-
- ActualPort =
- case port_available(Port) of
- true ->
- Port;
- false ->
- OnPortTaken(Port),
- first_available_port(Port + 1)
- end,
-
- {ok, Pid} =
- httpd:start_service([
- {bind_address, Address},
- {document_root, AbsPath},
- {server_root, AbsPath},
- {directory_index, ["index.html"]},
- {server_name, binary_to_list(Host)},
- {port, ActualPort},
- {default_type, "text/html"},
- {mime_types, mime_types()},
- {customize, ?MODULE},
- {modules, [mod_alias, mod_dir, mod_get]}
- ]),
-
- OnStart(ActualPort),
-
- receive
- {From, shutdown} ->
- ok = httpd:stop_service(Pid),
- From ! done
- end.
-
-port_available(Port) ->
- case gen_tcp:listen(Port, []) of
- {ok, Sock} ->
- ok = gen_tcp:close(Sock),
- true;
- _ ->
- false
- end.
-
-first_available_port(Port) ->
- case port_available(Port) of
- true -> Port;
- false -> first_available_port(Port + 1)
- end.
-
-mime_types() ->
- [
- {"html", "text/html"},
- {"htm", "text/html"},
- {"js", "text/javascript"},
- {"mjs", "text/javascript"},
- {"css", "text/css"},
- {"gif", "image/gif"},
- {"jpg", "image/jpeg"},
- {"jpeg", "image/jpeg"},
- {"png", "image/png"}
- ].
-
-response_default_headers() ->
- [
- {"cache-control", "no-store, no-cache, must-revalidate, private"},
- {"pragma", "no-cache"}
- ].
-
-exec(Cmd) ->
- Stdout = os:cmd(unicode:characters_to_list(Cmd)),
-
- unicode:characters_to_binary(Stdout).