Single file apps with Gleam and Bun

alistaiir1 pts0 comments

Single file apps with Gleam and Bunh1:first-child]:hidden prose-headings:font-serif prose-headings:font-semibold prose-img:w-full prose-pre:bg-transparent prose-pre:p-0 prose-pre:text-stone-700 dark:prose-pre:text-stone-300 [&_table]:block [&_table]:overflow-x-auto [&_video]:w-full prose-a:text-orange-800 prose-a:decoration-orange-800/40 prose-a:underline-offset-2 dark:prose-a:text-orange-300 dark:prose-a:decoration-orange-300/40 [&_a:hover]:decoration-orange-800 dark:[&_a:hover]:decoration-orange-300">Gleam is a friendly language primarily used for writing fault-tolerant and often distributed systems. It implements a Hindley-Milner type system, which is a type system that allows for maximum type-safety without sacrificing productivity. Most users of Gleam choose to run it in the BEAM, which is the same fantastic Virtual Machine that Elixir and Erlang run in. Basically, Gleam is really very good.<br>Bun, which I work on, has support for Single File Executables. This feature of Bun's bundler enables bundling your code with the runtime itself, into a single binary as output. These binaries are completely standalone and do not require your end user to have any extra packages installed on their system. They can be cross-compiled and you can target both glibc and musl on Linux. Basically, Bun single file executables are really very good.<br>So how can we combine these two really very good things?<br>Writing our program<br>Conveniently, Gleam has a JavaScript backend. That means that, as well as targeting the BEAM, Gleam can also output JavaScript. Frameworks like Lustre use this to let you write full-stack apps with Gleam that run in browsers, while libraries like gossamer provide idiomatic Gleam bindings to JavaScript APIs that exist in our runtime(s) such as fetch(), crypto, Worker, URL, etc.<br>So, if we want to use Bun's single file executable feature but author our program in Gleam, it is precisely writing idiomatic Gleam bindings for APIs that exist in our JavaScript runtime that we are interested in.<br>Here's a quick and dirty example that exposes two of Bun's APIs to Gleam:<br>src/bun_ffi.mjs<br>// Bun.file(path).text() -> Promise<br>export function readText(path) {<br>return Bun.file(path).text();

// Bun.semver.satisfies(version, range) -> boolean<br>export function semverSatisfies(version, range) {<br>return Bun.semver.satisfies(version, range);

src/app.gleam<br>import gleam/io<br>import gleam/javascript/promise.{type Promise}

@external(javascript, "./bun_ffi.mjs", "readText")<br>fn read_text(path: String) -> Promise(String)

@external(javascript, "./bun_ffi.mjs", "semverSatisfies")<br>fn semver_satisfies(version: String, range: String) -> Bool

pub fn main() {<br>use version promise.map(read_text("./VERSION"))<br>case semver_satisfies(version, "^2.0.0") {<br>True -> io.println("v" <> version <> " is compatible")<br>False -> io.println("v" <> version <> " needs migrating")

Don't worry if you are unfamiliar with the Gleam code above. The most important lines are those with @external attributes. This is syntax for telling the Gleam compiler that X function will exist at runtime, accepting Y arguments, returning Z value, but the implementation lives somewhere else. These types are not checked, so it's on the developer to make sure they are correct. In our case, the implementation lives in a separate JavaScript file on the disk, which relative to the Gleam file is ./bun_ffi.mjs.<br>We can run this code with the Gleam CLI itself, with two extra flags.<br>gleam run --target=javascript --runtime=bun<br>Downloading packages<br>Downloaded 2 packages in 0.01s<br>Compiling gleam_stdlib<br>Compiling gleam_javascript<br>Compiling app<br>Compiled in 0.03s<br>Running app.main<br>v2.1.0 is compatible<br>Nice! We just called some Bun APIs from our Gleam code, and even dealt with promises in the process! This is great, but there are two problems:<br>To run our program...<br>...we need both Gleam and Bun installed...<br>...and we need the original source code.<br>This, of course, is a very unfriendly user experience and makes it virtually impossible to distribute our application in any way that a normal user (or even developer) would expect.<br>Building our program<br>We've been using gleam run, which is fine while we're exploring, but it's strange to run our app just to generate build artifacts. Instead, Gleam provides us with a gleam build command.<br>gleam build --target=javascript<br>Compiled in 0.00s<br>Note that --runtime=bun is gone, since it's a gleam run flag only. It only ever told Gleam which runtime it should shell out to, and the JavaScript it emits is identical anyway.<br>Gleam just created a build/dev/javascript/ directory. The contents are nothing exotic - it's simply JavaScript. In fact, Gleam's JavaScript backend generates extremely human-readable and modern JavaScript, even using ECMAScript Modules (ESM). This is great!<br>Our src/app.gleam compiled to build/[..]/app.mjs, which exports our main function but doesn't call it. So, we can write a small entrypoint that does:<br>entry.mjs<br>import {main} from...

gleam javascript prose file version runtime

Related Articles