diff options
author | Giacomo Cavalieri <giacomo.cavalieri@icloud.com> | 2024-03-21 17:04:06 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-21 16:04:06 +0000 |
commit | e9b765a757b610ea970f80e7c0092bf4899acfa6 (patch) | |
tree | b4494a38e83aa52dd814024d604ae99af2775f53 /src | |
parent | e6dbabcbe4c0a87909e5c05f68cba4d748debdfd (diff) | |
download | lustre-e9b765a757b610ea970f80e7c0092bf4899acfa6.tar.gz lustre-e9b765a757b610ea970f80e7c0092bf4899acfa6.zip |
🔀 Add Tailwind support as an external tool. (#71)
* :sparkles: Add `lustre add tailwind` command
* :sparkles: Add Tailwind bundling to `lustre build` command
* ♻️ Use template for tailwind entry.css
Diffstat (limited to 'src')
-rw-r--r-- | src/cli_ffi.erl | 12 | ||||
-rw-r--r-- | src/lustre.gleam | 1 | ||||
-rw-r--r-- | src/lustre/cli/add.gleam | 38 | ||||
-rw-r--r-- | src/lustre/cli/build.gleam | 68 | ||||
-rw-r--r-- | src/lustre/cli/tailwind.gleam | 185 |
5 files changed, 303 insertions, 1 deletions
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) |