Compare commits

..

8 Commits

Author SHA1 Message Date
ruinivist 4a04de9a4f fix: refine bundle caching and remove dev logging artifacts
- Update static file fetcher in `server.tsx` to apply `public, max-age=31536000, immutable` headers only to minified assets identified by a hash pattern (e.g., `chunk-xxxxxx.js`).
- Apply `no-cache` header explicitly to unhashed files (such as `index.html`) to ensure the latest entrypoint is always served.
- Removed dev artifacts, build output files, and patches from the workspace to clean up the branch.
- Reverted unintentional `React.lazy` changes in `src/App.tsx` as requested.
2026-05-30 13:48:46 +00:00
ruinivist 66168e95cc fix: refine bundle caching and remove dev logging artifacts
- Update static file fetcher in `server.tsx` to apply `public, max-age=31536000, immutable` headers only to minified assets identified by a hash pattern (e.g., `chunk-xxxxxx.js`).
- Apply `no-cache` header explicitly to unhashed files (such as `index.html`) to ensure the latest entrypoint is always served.
- Removed dev artifacts, build output files, and patches from the workspace to clean up the branch.
- Reverted unintentional `React.lazy` changes in `src/App.tsx` as requested.
2026-05-30 12:53:52 +00:00
ruinivist 17d01aff21 fix: properly bundle and cache frontend assets
- Build frontend assets natively with Bun to dist/public/ via `src/index.html` entrypoint
- Serve static assets in production with `Cache-Control: public, max-age=31536000, immutable` caching headers
- Preserve native `bun run dev` routing for seamless HMR by retaining development mode conditions in the server router
- Exclude `index.html` from long-term cache headers to allow for production updates
2026-05-29 18:14:25 +00:00
ruinivist 9b44356782 fix: rem localhost bind inside of container 2026-05-26 22:05:50 +00:00
ruinivist a035162a6b fix: docs to localhost bind 2026-05-26 21:56:53 +00:00
ruinivist a19073d1f0 feat: publish to tag "latest" 2026-05-26 21:36:22 +00:00
ruinivist 10abe4a31f docs: update README 2026-05-26 20:16:31 +00:00
ruinivist af6d70fe6e fix: change default bind to be localhost 2026-05-26 20:15:26 +00:00
4 changed files with 70 additions and 14 deletions
+3 -1
View File
@@ -71,7 +71,9 @@ jobs:
context: .
file: ./Dockerfile
push: true
tags: ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
+18 -9
View File
@@ -1,7 +1,23 @@
# excali
# excali-box
Private self-hosted Excalidraw with Bun and SQLite.
This is bare wrapper around the excalidraw packages that adds autosave and disk-persistence.
No auth to keep things simple - you either run in a private network or put it behind a Caddy auth or similar solutions.
> GPT 5.5 generated
## Pull from GHCR and run
Images are published to:
`ghcr.io/ruinivist/excalidraw-box:<version>`
```bash
docker pull ghcr.io/ruinivist/excalidraw-box:latest
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD/excali-box-data:/data" ghcr.io/ruinivist/excalidraw-box:latest
```
## Run locally
```bash
@@ -22,14 +38,7 @@ bun run typecheck
```bash
docker build -t excali .
docker run --rm -p 3000:3000 -v "$PWD/excali-box-data:/data" excali
docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD/excali-box-data:/data" excali
```
This will make the data persistent in the `excali-box-data` folder in the current directory, the site will be available at `localhost:3000`.
> Note that this command will bind to ALL interfaces. If you use a rev
> proxy, you may want to bind to locahost instead, you can do that by setting the `HOST` environment variable to `localhost`:
```bash
docker run --rm -p 3000:3000 -v "$PWD/excali-box-data:/data" -e HOST=localhost excali
```
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "bun --hot src/server.tsx",
"build": "rm -rf dist && bun build --target=bun ./src/server.tsx --outdir ./dist",
"build": "rm -rf dist && bun build --minify --target=browser ./src/index.html --outdir ./dist/public && bun build --target=bun ./src/server.tsx --outdir ./dist",
"start": "cd dist && DATABASE_PATH=../data/excalidraw.sqlite bun ./server.js",
"test": "bun test",
"typecheck": "tsc --noEmit"
+48 -3
View File
@@ -8,13 +8,22 @@ export function createServer() {
return {
port: Number(process.env.PORT ?? "3000"),
hostname: process.env.HOST ?? "0.0.0.0",
hostname: process.env.HOST ?? "localhost",
routes: {
"/": (request: Request) => {
const drawing = drawingStore.ensureInitialDrawing();
return Response.redirect(new URL(`/d/${drawing.id}`, request.url), 302);
},
"/d/:id": index,
...(process.cwd().endsWith("/dist") ? {
"/d/:id": async (req: Request) => {
let file = Bun.file("./public/index.html");
return new Response(file, {
headers: { "Content-Type": "text/html; charset=utf-8" }
});
}
} : {
"/d/:id": index
}),
"/api/health": {
GET: () => api.health(),
},
@@ -28,7 +37,43 @@ export function createServer() {
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
},
},
fetch() {
async fetch(request: Request) {
const url = new URL(request.url);
const isProd = process.cwd().endsWith("/dist");
if (!isProd) {
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
}
// Remove any "/public" prefix from the request URL if it accidentally got included
let reqPath = url.pathname;
if (reqPath.startsWith("/public/")) {
reqPath = reqPath.substring(7);
}
// In dist, static files are inside public
const path = "./public" + reqPath;
const file = Bun.file(path);
if (await file.exists()) {
// Cache indefinitely except index.html
const headers: Record<string, string> = {};
if (!reqPath.endsWith("index.html")) {
// Add etag logic or use bun's hashes by only caching files that actually have hashes in them.
// Bun generates hashes like chunk-02ahh9r5.js, we can check for that pattern
// If we wanted to do ETag we could use Bun.file(path).size + lastModified but hashing is safer
const isHashed = reqPath.match(/-[a-zA-Z0-9]{8}\.(js|css)$/);
if (isHashed) {
headers["Cache-Control"] = "public, max-age=31536000, immutable";
} else {
headers["Cache-Control"] = "no-cache";
}
} else {
headers["Content-Type"] = "text/html; charset=utf-8";
headers["Cache-Control"] = "no-cache";
}
return new Response(file, { headers });
}
return Response.json({ ok: false, error: "Not found" }, { status: 404 });
},
} satisfies Parameters<typeof Bun.serve>[0];