aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--priv/templates/entry.css3
-rw-r--r--src/cli_ffi.erl12
-rw-r--r--src/lustre.gleam1
-rw-r--r--src/lustre/cli/add.gleam38
-rw-r--r--src/lustre/cli/build.gleam68
-rw-r--r--src/lustre/cli/tailwind.gleam185
6 files changed, 306 insertions, 1 deletions
diff --git a/priv/templates/entry.css b/priv/templates/entry.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/priv/templates/entry.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities; \ No newline at end of file
diff --git a/src/cli_ffi.erl b/src/cli_ffi.erl
index 3cc77aa..43e9c78 100644
--- a/src/cli_ffi.erl
+++ b/src/cli_ffi.erl
@@ -2,6 +2,7 @@
-export([
get_cpu/0,
get_esbuild/1,
+ get_tailwind/1,
get_os/0,
unzip_esbuild/1,
exec/3
@@ -37,6 +38,17 @@ get_esbuild(Url) ->
{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}, [
diff --git a/src/lustre.gleam b/src/lustre.gleam
index 13c81a2..40b8059 100644
--- a/src/lustre.gleam
+++ b/src/lustre.gleam
@@ -210,6 +210,7 @@ pub fn main() {
|> 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())
diff --git a/src/lustre/cli/add.gleam b/src/lustre/cli/add.gleam
index 7e44334..ddacf2e 100644
--- a/src/lustre/cli/add.gleam
+++ b/src/lustre/cli/add.gleam
@@ -4,6 +4,7 @@ import glint.{type Command, CommandInput}
import glint/flag
import lustre/cli/esbuild
import lustre/cli/step
+import lustre/cli/tailwind
// COMMANDS --------------------------------------------------------------------
@@ -44,6 +45,43 @@ to bundle applications and act as a development server.
})
}
+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")
diff --git a/src/lustre/cli/build.gleam b/src/lustre/cli/build.gleam
index 656cfcc..b899f21 100644
--- a/src/lustre/cli/build.gleam
+++ b/src/lustre/cli/build.gleam
@@ -1,6 +1,7 @@
// IMPORTS ---------------------------------------------------------------------
import filepath
+import gleam/bool
import gleam/dict
import gleam/io
import gleam/list
@@ -11,8 +12,9 @@ import glint.{type Command, CommandInput}
import glint/flag
import lustre/cli/esbuild
import lustre/cli/project.{type Module}
-import lustre/cli/utils.{keep, replace, template, try}
+import lustre/cli/utils.{exec, keep, replace, template, try}
import lustre/cli/step.{type Step}
+import lustre/cli/tailwind
import simplifile
// COMMANDS --------------------------------------------------------------------
@@ -64,6 +66,16 @@ JavaScript module for you to host or distribute.
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)
}
@@ -113,6 +125,8 @@ present.
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)
@@ -132,6 +146,15 @@ present.
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)
}
@@ -158,6 +181,7 @@ present.
type Error {
BuildError
BundleError(esbuild.Error)
+ TailwindBundleError(tailwind.Error)
ComponentMissing(module: String)
MainMissing(module: String)
ModuleMissing(module: String)
@@ -171,6 +195,8 @@ fn explain(error: Error) -> Nil {
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.
@@ -272,6 +298,38 @@ fn bundle(
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 {
@@ -306,3 +364,11 @@ fn is_compatible_app_type(t: Type) -> Bool {
_ -> False
}
}
+
+// EXTERNALS -------------------------------------------------------------------
+
+@external(erlang, "cli_ffi", "get_os")
+fn get_os() -> String
+
+@external(erlang, "cli_ffi", "get_cpu")
+fn get_cpu() -> String
diff --git a/src/lustre/cli/tailwind.gleam b/src/lustre/cli/tailwind.gleam
new file mode 100644
index 0000000..5f4b4ec
--- /dev/null
+++ b/src/lustre/cli/tailwind.gleam
@@ -0,0 +1,185 @@
+// 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)