Current section

Files

Jump to
novdom_dev_tools src lustre_dev_tools esbuild.gleam
Raw

src/lustre_dev_tools/esbuild.gleam

// IMPORTS ---------------------------------------------------------------------
import filepath
import gleam/bit_array
import gleam/crypto
import gleam/dynamic.{type Dynamic}
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/set
import lustre_dev_tools/cli.{type Cli}
import lustre_dev_tools/cmd
import lustre_dev_tools/error.{
type Error, BundleError, CannotSetPermissions, CannotWriteFile, NetworkError,
PackageFaulty, PackageNotFound, UnknownPlatform, UnzipError,
}
import lustre_dev_tools/esbuild/preprocess
import lustre_dev_tools/project
import lustre_dev_tools/utils
import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
// COMMANDS --------------------------------------------------------------------
pub fn download(os: String, cpu: String) -> Cli(Nil) {
use <- cli.log("Downloading esbuild")
let outdir = filepath.join(project.root(), "build/.lustre/bin")
let outfile = filepath.join(outdir, "esbuild")
case check_esbuild_exists(outfile) {
True -> cli.success("Esbuild already installed!", fn() { cli.return(Nil) })
False -> {
use <- cli.log("Detecting platform")
use #(url, hash) <- cli.try(get_download_url_and_hash(os, cpu))
// 17 is the size of "Downloading from " and 2 is the space taken by the
// spinner.
let max_url_size = utils.term_width() - 17 - 2
let shortened_url = utils.shorten_url(url, to: max_url_size)
use <- cli.log("Downloading from " <> shortened_url)
use tarball <- cli.try(get_esbuild(url))
use <- cli.log("Checking the downloaded tarball")
use _ <- cli.try(check_esbuild_integrity(tarball, hash))
use <- cli.log("Unzipping esbuild")
use bin <- cli.try(unzip_esbuild(tarball))
use _ <- cli.try(write_esbuild(bin, outdir, outfile))
use _ <- cli.try(set_file_permissions(outfile))
use <- cli.success("Esbuild installed!")
cli.return(Nil)
}
}
}
pub fn bundle(
input_file: String,
output_file: String,
is_prod: Bool,
) -> Cli(Nil) {
use _ <- cli.do(download(get_os(), get_cpu()))
let root = project.root()
let flags = [
"--bundle",
"--external:node:*",
"--format=esm",
"--outfile=" <> output_file,
]
let options = case is_prod {
True -> [input_file, "--minify", ..flags]
False -> [input_file, ..flags]
}
use <- cli.log("Bundling with esbuild")
use _ <- cli.do(preprocess.copy_deep_ffi())
use _ <- cli.try(exec_esbuild(root, options))
use <- cli.success("Bundle produced at `" <> output_file <> "`")
cli.return(Nil)
}
pub fn bundle_node_modules(modules: List(String), build_dir: String) -> Cli(Nil) {
use <- cli.log("Bundling all used node modules")
use _ <- cli.do(download(get_os(), get_cpu()))
let dir = filepath.join(build_dir, "modules")
use _ <- cli.try({
use module_name <- list.try_each(modules)
use module_main <- result.try(get_module_main(module_name))
let output_file = filepath.join(dir, module_name <> ".mjs")
let options = [
module_main,
"--bundle",
"--external:node:*",
"--format=esm",
"--tree-shaking=false",
"--outfile=" <> output_file,
]
exec_esbuild(project.root(), options)
})
use <- cli.success("Bundled all node modules at `" <> dir <> "`")
cli.return(Nil)
}
fn get_module_main(name: String) -> Result(String, Error) {
let root = project.root()
let module = filepath.join(root, "node_modules/" <> name)
let module_json = filepath.join(module, "package.json")
case simplifile.is_directory(module) {
Ok(False) | Error(_) -> Error(PackageNotFound(name))
Ok(True) -> {
use json <- result.try(
simplifile.read(module_json)
|> result.replace_error(PackageFaulty(name)),
)
use main_file <- result.try(
module_main_file(json) |> result.replace_error(PackageFaulty(name)),
)
let assert Ok(main) = filepath.expand(filepath.join(module, main_file))
Ok(main)
}
}
}
type PackageJson {
PackageJson(main: String, exports: Option(String))
}
fn module_main_file(json: String) -> Result(String, json.DecodeError) {
let dot_encoder = dynamic.field("import", dynamic.string)
let exports_decoder = dynamic.field(".", dot_encoder)
let decoder =
dynamic.decode2(
PackageJson,
dynamic.field("main", dynamic.string),
dynamic.optional_field("exports", exports_decoder),
)
use package_json <- result.map(json.decode(json, decoder))
case package_json.exports {
Some(exports) -> exports
None -> package_json.main
}
}
// STEPS -----------------------------------------------------------------------
fn check_esbuild_exists(path) {
case simplifile.is_file(path) {
Ok(True) -> True
Ok(False) | Error(_) -> False
}
}
/// Returns the appropriate url to download the esbuild binary and the expected
/// base16-encoded sha256 hash to check the downloaded binary's integrity.
///
fn get_download_url_and_hash(os, cpu) {
let base = "https://registry.npmjs.org/@esbuild/"
case os, cpu {
"android", "arm" ->
Ok(#(
base <> "android-arm/-/android-arm-0.19.10.tgz",
"545CF157B0E42E407AC1412F73876119414314D9E31982EBD1E9073336DA5365",
))
"android", "arm64" ->
Ok(#(
base <> "android-arm64/-/android-arm64-0.19.10.tgz",
"DFB0A873B1BB9698EF42561B9513FC4A7D8392CE84FBD44FC276883B82AB087E",
))
"android", "x64" ->
Ok(#(
base <> "android-x64/-/android-x64-0.19.10.tgz",
"B5D9D170F469BE483F3E16DA6033DFD064ED3DF788C6DC238BA6FE3232BF5653",
))
"darwin", "aarch64" ->
Ok(#(
base <> "darwin-arm64/-/darwin-arm64-0.19.10.tgz",
"8CEC451CBD47E228D2D0B740B62870471C22813348398061C06D829BC6610EA9",
))
"darwin", "amd64" ->
Ok(#(
base <> "darwin-arm64/-/darwin-arm64-0.19.10.tgz",
"8CEC451CBD47E228D2D0B740B62870471C22813348398061C06D829BC6610EA9",
))
"darwin", "arm64" ->
Ok(#(
base <> "darwin-arm64/-/darwin-arm64-0.19.10.tgz",
"8CEC451CBD47E228D2D0B740B62870471C22813348398061C06D829BC6610EA9",
))
"darwin", "x86_64" ->
Ok(#(
base <> "darwin-x64/-/darwin-x64-0.19.10.tgz",
"4AEC252E72CD56FD31341D960D8DCF0BEBFB858587061FE36B9047FD591C68A3",
))
"freebsd", "aarch64" ->
Ok(#(
base <> "freebsd-arm64/-/freebsd-arm64-0.19.10.tgz",
"739E9F6DD3121DB0CD03B70B14C4D17A1854970272B0F988BD035AA876BE254B",
))
"freebsd", "amd64" ->
Ok(#(
base <> "freebsd-x64/-/freebsd-x64-0.19.10.tgz",
"3E937C849B21B89244A8A62B473E36EEFE793F5BDF602BEDBB314DD33DDBE7EE",
))
"linux", "aarch64" ->
Ok(#(
base <> "linux-arm64/-/linux-arm64-0.19.10.tgz",
"D3523B8F7B2540BA5A15C4EE4C747B31DFDC496C7A8A3F3FB0ECCB3008647DB7",
))
"linux", "arm" ->
Ok(#(
base <> "linux-arm/-/linux-arm-0.19.10.tgz",
"99EEB37F5C1AB8750D9CAB6AB04469EF5CA32847B25E1461215276920AFB01B2",
))
"linux", "arm64" ->
Ok(#(
base <> "linux-arm64/-/linux-arm64-0.19.10.tgz",
"D3523B8F7B2540BA5A15C4EE4C747B31DFDC496C7A8A3F3FB0ECCB3008647DB7",
))
"linux", "ia32" ->
Ok(#(
base <> "linux-ia32/-/linux-ia32-0.19.10.tgz",
"3D69F7B90C62E6D94140355A92EDB15B8BFB934096C6E518BE41DAD6249BF38E",
))
"linux", "x64" ->
Ok(#(
base <> "linux-x64/-/linux-x64-0.19.10.tgz",
"73CA82A3C9049315027E60A50AF53C2ABFDE678BF66562B407FACA7FD3FAD6F4",
))
"linux", "x86_64" ->
Ok(#(
base <> "linux-x64/-/linux-x64-0.19.10.tgz",
"73CA82A3C9049315027E60A50AF53C2ABFDE678BF66562B407FACA7FD3FAD6F4",
))
"win32", "arm64" ->
Ok(#(
base <> "win32-arm64/-/win32-arm64-0.19.10.tgz",
"2D0EC6ED7C5BA6F2D99CBB1428C1FAABFA7D42E7435BC40474C5787DCD1FF37C",
))
"win32", "ia32" ->
Ok(#(
base <> "win32-ia32/-/win32-ia32-0.19.10.tgz",
"5BFBF08A8EDC16D53FE2103C68705DC3B4ABDFA6C44919B9602495ABA523BA46",
))
"win32", "x64" ->
Ok(#(
base <> "win32-x64/-/win32-x64-0.19.10.tgz",
"03EFF9A74ED7C72C8E4ACE85F6BFD2D097169D8D6E7D691AE1D7959F2912B785",
))
"win32", "x86_64" ->
Ok(#(
base <> "win32-x64/-/win32-x64-0.19.10.tgz",
"03EFF9A74ED7C72C8E4ACE85F6BFD2D097169D8D6E7D691AE1D7959F2912B785",
))
"netbsd", "x64" ->
Ok(#(
base <> "netbsd-x64/-/netbsd-x64-0.19.10.tgz",
"C8F6E2CB79B1DDC2AD42C0AE25FB2A769A989E36B917B231CF9847B683D6DD8D",
))
"openbsd", "x64" ->
Ok(#(
base <> "openbsd-x64/-/openbsd-x64-0.19.10.tgz",
"AFEBEAD35BB5A1B921C126E70E0D76CF04DB64FA53C60E0779816CFA9E1F9A11",
))
"sunos", "x64" ->
Ok(#(
base <> "sunos-x64/-/sunos-x64-0.19.10.tgz",
"B1E9F969433574BD43A293FA3A3C71C88B8C4CF841957DAAA2CF83A90ADAAB7E",
))
_, _ -> Error(UnknownPlatform("esbuild", os, cpu))
}
}
fn get_esbuild(url: String) -> Result(BitArray, Error) {
do_get_esbuild(url)
|> result.map_error(NetworkError)
}
fn unzip_esbuild(gzip: BitArray) -> Result(BitArray, Error) {
do_unzip_esbuild(gzip)
|> result.map_error(UnzipError)
}
fn check_esbuild_integrity(bin: BitArray, expected_hash: String) {
let hash = crypto.hash(crypto.Sha256, bin)
let hash_string = bit_array.base16_encode(hash)
case hash_string == expected_hash {
True -> Ok(Nil)
False -> Error(error.InvalidEsbuildBinary)
}
}
fn write_esbuild(
bin: BitArray,
outdir: String,
outfile: String,
) -> Result(Nil, Error) {
let _ = simplifile.create_directory_all(outdir)
simplifile.write_bits(outfile, bin)
|> result.map_error(CannotWriteFile(_, filepath.join(outdir, outfile)))
}
fn set_file_permissions(file: String) -> Result(Nil, Error) {
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)
|> result.map_error(CannotSetPermissions(_, file))
}
fn exec_esbuild(root: String, options: List(String)) -> Result(String, Error) {
cmd.exec("./build/.lustre/bin/esbuild", in: root, with: options)
|> result.map_error(fn(pair) { BundleError(pair.1) })
}
// EXTERNALS -------------------------------------------------------------------
@external(erlang, "lustre_dev_tools_ffi", "get_os")
fn get_os() -> String
@external(erlang, "lustre_dev_tools_ffi", "get_cpu")
fn get_cpu() -> String
@external(erlang, "lustre_dev_tools_ffi", "get_esbuild")
fn do_get_esbuild(url: String) -> Result(BitArray, Dynamic)
@external(erlang, "lustre_dev_tools_ffi", "unzip_esbuild")
fn do_unzip_esbuild(tarball: BitArray) -> Result(BitArray, Dynamic)