H
Hono•2w ago
sebovzeoueb

Is there a simple way to bundle the directory served by serveStatic into a single file executable?

So, I'm not 100% sure if I should be asking this here or on bun support, as it may be more of a bun issue, but I figured that you guys would be the most likely to need to do this. I've tried the recommendation from the bun documentation to embed directories (https://bun.sh/docs/bundler/executables#embed-directories) but this gives me an error about not being able to find an entrypoint there. So far I've only been able to include my static files by manually adding them to the directory containing the executable, but I'm very interested in the alleged ability of bun to include all the files inside the executable.
Bun
Single-file executable – Runtime | Bun Docs
Compile a TypeScript or JavaScript file to a standalone executable
55 Replies
sebovzeoueb
sebovzeouebOP•2w ago
Update: turns out the entrypoint issue was related to me running the command in PowerShell, using a bun package script has successfully embedded the files, but serveStatic doesn't seem to be configured to use the embedded files. Any ideas how I could do this?
sebovzeoueb
sebovzeouebOP•2w ago
here's the project
No description
sebovzeoueb
sebovzeouebOP•2w ago
and you can see that the files are successfully embedded
No description
sebovzeoueb
sebovzeouebOP•2w ago
I did this but I don't like it so much
if (embeddedFiles.length) {
const webFiles = embeddedFiles.filter(file => file.name.startsWith("web_app/"))
webFiles.forEach(file => {
const relativePath = path.posix.relative("web_app", file.name)
app.get(`/${relativePath}`, c => file.text().then(text => c.body(text, 200, {
"Content-Type": file.type
})))
if (relativePath == "index.html") app.get("/", c => file.text().then(text => c.html(text)))
})
}
else app.use('/*', serveStatic({ root: "./web_app" }))
if (embeddedFiles.length) {
const webFiles = embeddedFiles.filter(file => file.name.startsWith("web_app/"))
webFiles.forEach(file => {
const relativePath = path.posix.relative("web_app", file.name)
app.get(`/${relativePath}`, c => file.text().then(text => c.body(text, 200, {
"Content-Type": file.type
})))
if (relativePath == "index.html") app.get("/", c => file.text().then(text => c.html(text)))
})
}
else app.use('/*', serveStatic({ root: "./web_app" }))
with --asset-naming=\"[dir]/[name].[ext]\" in the compile options
ambergristle
ambergristle•2w ago
hey @sebovzeoueb! could you clarify what your goals are?
sebovzeoueb
sebovzeouebOP•2w ago
I want to ship an all in one web server with a pre-defined site bundled into it basically. Bun allows you to include files in the compiled executable (see link in the OP), so in an ideal world (but I really don't know what the implementation would look like) serveStatic would be able to read files from the bundled directory basically have a 1:1 behaviour between how the app runs when I bun run index.ts and how it runs when I build it using --compile and including the web_app directory this would maybe have to be an option because I can imagine that in some cases people actually want to serve files from their filesystem rather than the bundle
ambergristle
ambergristle•2w ago
so you're looking for static site generation? or are you actually trying to bundle an exe? a-la electron?
sebovzeoueb
sebovzeouebOP•2w ago
yeah, the goal is more like Electron, but instead of using Chromium it serves the pages to be accessible in the browser this way you can install the app on a headless server and still interact with it and also is slightly more lightweight from not bundling Chromium
ambergristle
ambergristle•2w ago
ah, no idea, sorry. that's not a use-case i've ever come across
sebovzeoueb
sebovzeouebOP•2w ago
yeah, I'm realizing it may be a bit niche
ambergristle
ambergristle•2w ago
like, why make it an exe at all?
sebovzeoueb
sebovzeouebOP•2w ago
because people don't want to install bun and download a repo from GitHub it's an app to bootstrap a bunch of stuff in Docker basically based on the user's needs it sets some env variables and launches the required Docker Compose but we're trying to make a process that's as easy as possible, so it has to be simpler than messing with a fairly hefty compose config our current installer is Python CLI but a) It's getting to be a bit of an unwieldy text based adventure b) Sometimes people have an ancient version of Python installed and it doesn't work as intended
ambergristle
ambergristle•2w ago
i'm still pretty lost, ngl. i think it may just be a bit over my head but i feel like this might be helpful: https://hono.dev/docs/helpers/ssg
sebovzeoueb
sebovzeouebOP•2w ago
hmm, maybe SSG could be an angle on this question I hadn't considered, idk it's not directly what I'm trying to do, but it might work I just got excited when I saw that bun lets you throw a directory of files into the exe, but got a bit unexcited when I realized I couldn't just serve them but yeah, the gist of it is an app with a single file executable that contains the whole client and server so you just double click it and then point your browser at it to interact with it
ambergristle
ambergristle•2w ago
don't let me discourage you, lol. i think this is a q that folks on the bun discord might have an easier time with
sebovzeoueb
sebovzeouebOP•2w ago
yeah, I've got a thread on it over there too, waiting to see what they say
ambergristle
ambergristle•2w ago
fwiw, it seems like both of these lines are necessary (from bun example)
import icon from "./public/assets/icon.png" with { type: "file" };
import { file } from "bun";
import icon from "./public/assets/icon.png" with { type: "file" };
import { file } from "bun";
no?
sebovzeoueb
sebovzeouebOP•2w ago
it doesn't seem to be the case from my tests but I think that might be the key to accessing the bundled assets in a more proper way but I've found that that import method only seems to work for a few file types idk, I feel like the whole bundling an executable part is maybe a little used feature but server side rendering seems like a possible workaround so you may have steered me in an alternative direction to explore
ambergristle
ambergristle•2w ago
or just too complicated/advanced for the average dev discord member. though maybe that's just me, lol like, wym by "point your browser at it"? like, your exe would start up a local server or port w/e, and you'd visit that page in the browser?
sebovzeoueb
sebovzeouebOP•2w ago
correct
ambergristle
ambergristle•2w ago
oh shit, that's pretty sick ok, i think i see where you're trying to go w this now
sebovzeoueb
sebovzeouebOP•2w ago
I've got it kind of working but my solution is janky I don't think it's as complex as I'm maybe making it sound 😄
ambergristle
ambergristle•2w ago
lol, nah. i just don't have any experience w exes/related patterns, so it all seems complex what's the webapp built on?
sebovzeoueb
sebovzeouebOP•2w ago
Really just the built-in stuff that bun comes with for the moment, but I am doing a test app before getting too deep into the real deal Bun shell is super nice, I envision a lot of the app using the shell commands
ambergristle
ambergristle•2w ago
can you expand on this a bit? are you serving raw html or using a frontend framework? sounds like you're not using hono/jsx ok, looking back over the code you've shared, i think i'm starting to get caught up
const app = new Hono()

if (embeddedFiles.length) {
// embeddedFiles is being used here as a way to grab every local file available
const webFiles = embeddedFiles.filter(file => file.name.startsWith("web_app/"))
webFiles.forEach(file => {
const relativePath = path.posix.relative("web_app", file.name)
// adding route route
app.get(`/${relativePath}`, c => file.text().then(text => c.body(text, 200, {
"Content-Type": file.type
})))
// if index, return html specifically from root
if (relativePath == "index.html") {
app.get("/", c => file.text().then(text => c.html(text)))
}
})
} else {
// embedding didn't work, serve static
app.use('/*', serveStatic({ root: "./web_app" }))
}
const app = new Hono()

if (embeddedFiles.length) {
// embeddedFiles is being used here as a way to grab every local file available
const webFiles = embeddedFiles.filter(file => file.name.startsWith("web_app/"))
webFiles.forEach(file => {
const relativePath = path.posix.relative("web_app", file.name)
// adding route route
app.get(`/${relativePath}`, c => file.text().then(text => c.body(text, 200, {
"Content-Type": file.type
})))
// if index, return html specifically from root
if (relativePath == "index.html") {
app.get("/", c => file.text().then(text => c.html(text)))
}
})
} else {
// embedding didn't work, serve static
app.use('/*', serveStatic({ root: "./web_app" }))
}
ngl, i use a lot more line breaks + braces, so i missed some of what was going on initially anyways, it seems like you've got an index.html, and a number of other files (JS? HTML? assets?) that you want to serve from a hono app, running on a local server, managed within an exe wait, is it actually just the html + css?
sebovzeoueb
sebovzeouebOP•2w ago
yeah, for this demo app it's just plain old HTML and CSS, I was imagining in the real app it would be bundled from source code but the JSX path seems interesting to me SSR might do the trick for this
ambergristle
ambergristle•2w ago
what's the docker compose doing then?
sebovzeoueb
sebovzeouebOP•2w ago
well, all the docker stuff is the actual app, the thing I'm trying to build with bun and potentially Hono is a configurator
sebovzeoueb
sebovzeouebOP•2w ago
GitHub
GitHub - InfoSecInnovations/concierge: Repo for Concierge AI dev work
Repo for Concierge AI dev work. Contribute to InfoSecInnovations/concierge development by creating an account on GitHub.
ambergristle
ambergristle•2w ago
gotcha. so the frontend the hono app serves would be for app config, and then /install would get called
sebovzeoueb
sebovzeouebOP•2w ago
that's right
ambergristle
ambergristle•2w ago
ok, word
sebovzeoueb
sebovzeouebOP•2w ago
because the project has a lot of permutations based on if you use the GPU to accelerate the LLM, if you use RBAC or not, if you need to serve it over some kind of proxy or you're just doing localhost etc so we couldn't really do a simple docker-compose for people to run
ambergristle
ambergristle•2w ago
that makes sense so i know shit-all about bundling, much less dev ops but i'd say that embedding + hono serving static is not the play
sebovzeoueb
sebovzeouebOP•2w ago
the bundling is one of the things that had me hyped about bun in the first place, but I think people aren't using it that much yeah, I was hoping the exe builder would do a bit more magic, but I think I'm asking a lot of it
ambergristle
ambergristle•2w ago
you can lean on hono a bit more heavily
sebovzeoueb
sebovzeouebOP•2w ago
dammit, it took me longer than I'd like to admit to realize I had to change the file extension to .tsx to serve stuff in JSX format
ambergristle
ambergristle•2w ago
been there, lol
sebovzeoueb
sebovzeouebOP•2w ago
thanks for the chat, this is looking like it has stronger potential than my original idea and it's more all in one from the development perspective too
ambergristle
ambergristle•2w ago
idk if i'd say that. it makes sense to me to use the exe to spin up a local server that can host a more/less complex frontend, that can then be used to run docker but the app that's getting bundled to the exe is the entire hono app in that case, not just the frontend
sebovzeoueb
sebovzeouebOP•2w ago
I thought that just maybe it would bundle the files and do something under the hood to hook into calls to fileSystem reads
ambergristle
ambergristle•2w ago
that seems like a legit path to take, but then what's hono really doing?
sebovzeoueb
sebovzeouebOP•2w ago
but that would probably be an undesired effect in other applications
ambergristle
ambergristle•2w ago
at that point, why not do everything with fs
sebovzeoueb
sebovzeouebOP•2w ago
well I meant more that I thought maybe the bun builder would do it automatically so that when the serveStatic route tries to read a file, it loads it from the bundled ones instead of the actual filesystem but I realize that would be confusing to people who want to build an actual web server
ambergristle
ambergristle•2w ago
hmmmmmmmmmmm. i think i see where you're getting at really, what you want is something like this
import { indexPage } from './web_app';

const app = new Hono()
.get('/', (c) => c.html(indexPage)
.post('/install', /** */)
import { indexPage } from './web_app';

const app = new Hono()
.get('/', (c) => c.html(indexPage)
.post('/install', /** */)
where bun builds indexPage directly into the app dist, right? have you seen this? https://bun.sh/docs/bundler/html idk if that will work out-of-box, you might have to try wrapping with hono's raw helper but i'm like 85% sure you can do something to that effect
sebovzeoueb
sebovzeouebOP•2w ago
the html import feature doesn't work with the executable feature yet unfortunately the bun devs have confirmed that to me already it was my first port of call one of the quite annoying issues that I haven't yet figured out a good fix for is that there doesn't seem to be a way to include any client side JavaScript in the executable because bun thinks all .js files are supposed to be part of the exe best workaround so far is using the raw html feature in hono JSX
app.get('/', (c) => {
return c.html(
<html>
<h1>Hello Server Side</h1>
{html`<button onClick="fetch('/install', {method: 'POST'})">
Install Hello World
</button>`}
</html>)
})
app.get('/', (c) => {
return c.html(
<html>
<h1>Hello Server Side</h1>
{html`<button onClick="fetch('/install', {method: 'POST'})">
Install Hello World
</button>`}
</html>)
})
I'm not a super fan of this but it does work I was looking at HonoX too, but the build process for that also outputs some .js files that need to be loaded by the client so, this actually works:
import { Hono } from 'hono'
import { html } from 'hono/html'
import { $ } from 'bun'
import style from "./style.css" with { type: "file" }
import { file } from "bun"

const app = new Hono()

app.get('/', (c) => {
return c.html(
<html>
<head>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<h1>Hello Server Side</h1>
{html`<button onClick="fetch('/install', {method: 'POST'})">
Install Hello World
</button>`}
</body>
</html>)
})
app.get('/style.css', c => file(style).text().then(text => c.text(text)))
app.post('/install', async c => {
try {
await $`docker compose up -d`
return c.json({success: true})
}
catch {
return c.json({success: false})
}
})

export default app
import { Hono } from 'hono'
import { html } from 'hono/html'
import { $ } from 'bun'
import style from "./style.css" with { type: "file" }
import { file } from "bun"

const app = new Hono()

app.get('/', (c) => {
return c.html(
<html>
<head>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<h1>Hello Server Side</h1>
{html`<button onClick="fetch('/install', {method: 'POST'})">
Install Hello World
</button>`}
</body>
</html>)
})
app.get('/style.css', c => file(style).text().then(text => c.text(text)))
app.post('/install', async c => {
try {
await $`docker compose up -d`
return c.json({success: true})
}
catch {
return c.json({success: false})
}
})

export default app
VSCode is not happy about me importing that style.css but it is actually working and including it in the bundle
sebovzeoueb
sebovzeouebOP•2w ago
getting red squiggles but it actually runs yay
No description
ambergristle
ambergristle•2w ago
leggo
sebovzeoueb
sebovzeouebOP•2w ago
well I be damned, you can actually also add .js files like I did with the .css VSCode pretends it won't work but it does bamboozled by the red lines
ambergristle
ambergristle•2w ago
lol. that's why they call it the bleeding edge
sebovzeoueb
sebovzeouebOP•2w ago
yup so I can actually get pretty close to my original goal here I think
ambergristle
ambergristle•2w ago
i wonder if anyone's made a vscode extension that makes the red squigglies drip like blood
sebovzeoueb
sebovzeouebOP•2w ago
in theory I can make a client javascript directory and build that to a single file and then only do the awful raw HTML to include that script in fact it doesn't even need the raw HTML, it'll just let me do a normal script tag
ambergristle
ambergristle•2w ago
nice! take a look at jsxRenderer if you haven't already also reminded me of this: https://github.com/lucia-auth/examples/blob/main/hono/username-and-password/routes/index.ts so sick that you got it working!

Did you find this page helpful?