WIP: Architecture Refactor, add stub files and working blog post wrapped by AppShell
This commit is contained in:
parent
c34f11de00
commit
3abd97702d
31
Dockerfile
31
Dockerfile
@ -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" ]
|
||||
77
index.tsx
77
index.tsx
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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();
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
import React from 'react';
|
||||
|
||||
export function Blog() {
|
||||
return (
|
||||
|
||||
0
src/frontend/components/post-archive.tsx
Normal file
0
src/frontend/components/post-archive.tsx
Normal file
0
src/frontend/components/profile-badge.tsx
Normal file
0
src/frontend/components/profile-badge.tsx
Normal file
0
src/frontend/components/tag-picker.tsx
Normal file
0
src/frontend/components/tag-picker.tsx
Normal file
13
src/frontend/components/theme-picker.tsx
Normal file
13
src/frontend/components/theme-picker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
import React from 'react';
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
import React from 'react';
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
|
||||
12
src/frontend/pages/blog.tsx
Normal file
12
src/frontend/pages/blog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
10
src/frontend/pages/home.tsx
Normal file
10
src/frontend/pages/home.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Home!</h1>
|
||||
<a href="/blog">Blog</a>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
9
src/frontend/pages/not-found.tsx
Normal file
9
src/frontend/pages/not-found.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<h1>404 Not Found</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
1
src/public/htmx.min.js
vendored
1
src/public/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user