WIP: Architecture Refactor, add stub files and working blog post wrapped by AppShell

This commit is contained in:
Caleb Braaten 2025-12-28 00:21:13 -08:00
parent c34f11de00
commit 3abd97702d
15 changed files with 96 additions and 134 deletions

View File

@ -1,31 +0,0 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1.0.25-alpine as base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS release
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# [optional] tests & build
ENV NODE_ENV=production
RUN bun test
# run the app
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "./src/index.ts" ]

View File

@ -1,49 +1,38 @@
// import { Elysia } from "elysia";
// import { html } from "@elysiajs/html";
// import { staticPlugin } from "@elysiajs/static";
import AppShellDemo from "./temp/appshell.html";
import { AppShell } from "./src/frontend/AppShell";
// import { AppShell } from "./src/frontend/AppShell";
// import { app } from "./src/backend";
// const index = new Elysia()
// .use(html())
// .onRequest(({ request }) => {
// console.log(`Request ${request.method} ${request.url}`);
// })
// .onAfterHandle(({ request, responseValue }) => {
// if (request.headers.get("shell-loaded") === "true") {
// return responseValue; // Return the <main> element if the AppShell has already been loaded
// }
// return AppShell(responseValue); // Return the <main> element wrapped by the AppShell
// })
// .use(staticPlugin({
// assets: './src/public',
// prefix: '/public'
// }))
// .use(app)
// .listen(3000);
// console.log(
// `🦊 Elysia is running at ${index.server?.hostname}:${index.server?.port}`
// );
import { serve } from "bun";
import AppShell from "./temp/appshell.html"
// Dynamically import all markdown files
const glob = new Bun.Glob("**/*.md");
const routes: Record<string, any> = {
'/': AppShell,
};
for await (const file of glob.scan("./content")) {
async function blogPosts() {
const glob = new Bun.Glob("**/*.md");
const blogPosts: Record<string, any> = {}
for await (const file of glob.scan("./content")) {
const post = await import(`./content/${file}`, { with: { type: "html" } });
const route = `/${file.replace(/\.md$/, '')}`;
routes[route] = post.default;
const route = `/${file.replace(/\.md$/, "")}`;
blogPosts[route] = post.default;
}
Object.keys(blogPosts).map((route) => {
console.info(route);
});
return blogPosts;
}
serve({
routes,
development: true,
Bun.serve({
development: {
hmr: true,
console: true
},
routes: {
"/": AppShellDemo,
... await blogPosts(),
"/content/*": {
async GET(req: Request) {
// Having trouble using Bun Bundler alongside a custom route handler to send
// different content depending on the request headers, will use /content subpath instead
// (unless I can figure it out)
return new Response("This will send the blog post content without the app shell")
}
}
}
})

View File

@ -1,22 +0,0 @@
import { Html } from "@elysiajs/html";
import { Elysia, NotFoundError } from "elysia";
import { Home } from "../frontend/home";
import { Blog } from "../frontend/blog";
import { NotFound } from "../frontend/not-found";
export const app = new Elysia()
.onError(({ error }) => {
if(error instanceof NotFoundError) {
return <NotFound />;
}
return error;
})
.get("/", () => { return <Home /> })
.get("/:path", ({ path }) => {
if(path === "/blog") {
return <Blog />;
}
throw new NotFoundError();
})

View File

@ -1,7 +1,10 @@
import { Html } from "@elysiajs/html";
import React from 'react';
import styles from '../public/styles.css' with { type: "text" };
import headScript from '../public/head.js' with { type: "text" };
import onLoadScript from '../public/onLoad.js' with { type: "text" };
// Helper: Minify CSS using simple but effective regex
async function minifyCSS(css: string): Promise<string> {
function minifyCSS(css: string): string {
return css
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
.replace(/\s+/g, ' ') // Collapse whitespace
@ -10,7 +13,7 @@ async function minifyCSS(css: string): Promise<string> {
}
// Helper: Minify JS/TS using Bun.Transpiler
async function minifyJS(code: string): Promise<string> {
function minifyJS(code: string): string {
const transpiler = new Bun.Transpiler({
loader: 'ts',
minifyWhitespace: true,
@ -19,41 +22,21 @@ async function minifyJS(code: string): Promise<string> {
return transpiler.transformSync(code);
}
// Read and minify files at module load time (runs once)
console.log('[AppShell] Loading and minifying assets...');
const rawStyles = await Bun.file("./src/public/styles.css").text();
const rawHeadScript = await Bun.file("./src/public/head.ts").text();
const rawOnLoadScript = await Bun.file("./src/public/onLoad.ts").text();
// Minify all assets
const styles = await minifyCSS(rawStyles);
const headScript = await minifyJS(rawHeadScript);
const onLoadScript = await minifyJS(rawOnLoadScript);
console.log('[AppShell] Assets minified and cached in memory');
console.log(` CSS: ${rawStyles.length}${styles.length} bytes (-${Math.round((1 - styles.length / rawStyles.length) * 100)}%)`);
console.log(` head.ts: ${rawHeadScript.length}${headScript.length} bytes (-${Math.round((1 - headScript.length / rawHeadScript.length) * 100)}%)`);
console.log(` onLoad.ts: ${rawOnLoadScript.length}${onLoadScript.length} bytes (-${Math.round((1 - onLoadScript.length / rawOnLoadScript.length) * 100)}%)`);
export function AppShell(responseValue: any) {
export function AppShell(props: { post: any }) {
return (
<html>
<html className={styles.root}>
<head>
<title>Caleb's Blog</title>
<style>
{styles}
</style>
<style>{minifyCSS(styles)}</style>
<script>
{headScript}
{minifyJS(headScript)}
</script>
<script defer>
{onLoadScript}
{minifyJS(onLoadScript)}
</script>
</head>
<body>
{responseValue}
<div dangerouslySetInnerHTML={{ __html: props.post }} />
<aside>
<h3>About Me</h3>
<p>I'm a software engineer</p>

View File

@ -1,4 +1,4 @@
import { Html } from "@elysiajs/html";
import React from 'react';
export function Blog() {
return (

View File

View File

View File

@ -0,0 +1,13 @@
import React from 'react';
export function ThemePicker() {
const randomNumber = Math.random();
return (
<div>
<h1>Sub Component!</h1>
<p>Some text here... maybe? with hot reloading! {randomNumber}</p>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { Html } from "@elysiajs/html";
import React from 'react';
export function Home() {
return (

View File

@ -1,4 +1,4 @@
import { Html } from "@elysiajs/html";
import React from 'react';
export function NotFound() {
return (

View File

@ -0,0 +1,12 @@
import React from 'react';
import { ThemePicker } from "../components/theme-picker";
export function Blog() {
return (
<main>
<h1>Blog</h1>
<a href="/">Home</a>
<ThemePicker />
</main>
)
}

View File

@ -0,0 +1,10 @@
import React from 'react';
export function Home() {
return (
<main>
<h1>Home!</h1>
<a href="/blog">Blog</a>
</main>
)
}

View File

@ -0,0 +1,9 @@
import React from 'react';
export function NotFound() {
return (
<main>
<h1>404 Not Found</h1>
</main>
)
}

File diff suppressed because one or more lines are too long