improved middlewares + added _layout support

This commit is contained in:
MasterGordon 2025-07-14 23:40:26 +02:00
parent b8107c8f04
commit 4cc9669468
8 changed files with 134 additions and 68 deletions

View File

@ -15,10 +15,6 @@
}, },
"dependencies": { "dependencies": {
"lightningcss": "^1.29.1", "lightningcss": "^1.29.1",
"pokedex-promise-v2": "^4.2.1", "serve-static-bun": "^0.5.3"
"serve-static-bun": "^0.5.3",
"tree-sitter-cli": "^0.25.2",
"tree-sitter-typescript": "^0.23.2",
"web-tree-sitter": "^0.25.2"
} }
} }

View File

@ -1,13 +1,23 @@
import { myPlugin } from "./myPlugin"; import { myPlugin } from "./myPlugin";
import { fillTemplate, render } from "./lib/fast-web"; import {
fillTemplate,
render,
resolveLayoutPaths,
wrapLayouts,
} from "./lib/fast-web";
import { requestLog } from "./middlewares/request-log"; import { requestLog } from "./middlewares/request-log";
import template from "./template.html" with { type: "text" }; import template from "./template.html" with { type: "text" };
import theme from "./theme.css" with { type: "text" }; import theme from "./theme.css" with { type: "text" };
import staticDir from "serve-static-bun"; import staticDir from "serve-static-bun";
import path from "path";
import { withMiddlewares } from "./middlewares/with-middlewares";
import { gzip } from "./middlewares/gzip";
const pagesDir = __dirname + "/pages";
const router = new Bun.FileSystemRouter({ const router = new Bun.FileSystemRouter({
style: "nextjs", style: "nextjs",
dir: __dirname + "/pages", dir: pagesDir,
}); });
const staticPublic = staticDir("./public"); const staticPublic = staticDir("./public");
@ -16,16 +26,24 @@ const scriptsGlob = new Bun.Glob(__dirname + "/scripts/*");
const server = Bun.serve({ const server = Bun.serve({
port: 8080, port: 8080,
fetch: requestLog(async (req): Promise<Response> => { fetch: withMiddlewares(
async (req): Promise<Response> => {
if (req.url.endsWith("ws")) { if (req.url.endsWith("ws")) {
if (server.upgrade(req)) return new Response("ok"); if (server.upgrade(req)) return new Response("ok");
} }
const match = router.match(req); const match = router.match(req);
if (!match) return staticPublic(req); if (!match) return staticPublic(req);
const Comp = (await import(__dirname + "/pages/" + match?.pathname)) const layouts = await resolveLayoutPaths(
.default; path.join(pagesDir, match.src),
const renderResult = await render(<Comp />, [theme], new URL(req.url)); pagesDir,
);
const Comp = (await import(path.join(pagesDir, match.src))).default;
const renderResult = await render(
await wrapLayouts(<Comp />, layouts),
[theme],
new URL(req.url),
);
const systemScripts = await Bun.build({ const systemScripts = await Bun.build({
entrypoints: [ entrypoints: [
@ -47,15 +65,16 @@ const server = Bun.serve({
body: renderResult.html, body: renderResult.html,
}); });
const gzipResponseData = Bun.gzipSync(responseData); return new Response(responseData, {
return new Response(gzipResponseData, {
headers: { headers: {
"content-type": "text/html", "content-type": "text/html",
"Content-Encoding": "gzip",
}, },
}); });
}), },
requestLog(),
gzip,
requestLog("gzip"),
),
websocket: { websocket: {
message: () => {}, message: () => {},
open: async (ws) => { open: async (ws) => {

View File

@ -2,6 +2,7 @@ import { camelToKebab, renderStyle } from "~/system/style-rules";
import { transform } from "lightningcss"; import { transform } from "lightningcss";
import reset from "./reset.css" with { type: "text" }; import reset from "./reset.css" with { type: "text" };
import { AsyncLocalStorage } from "async_hooks"; import { AsyncLocalStorage } from "async_hooks";
import path from "path";
interface RenderContext { interface RenderContext {
styles: string[]; styles: string[];
@ -122,3 +123,27 @@ export const fillTemplate = (
return acc.replace(`<!-- ${key} -->`, value); return acc.replace(`<!-- ${key} -->`, value);
}, template); }, template);
}; };
export const resolveLayoutPaths = async (file: string, rootDir: string) => {
const layouts = [];
let dir = file;
console.log(path.normalize(dir));
console.log(path.normalize(rootDir));
do {
dir = path.join(dir, "..");
const layoutFile = path.join(dir, "_layout.tsx");
if (await Bun.file(layoutFile).exists()) {
layouts.push(layoutFile);
}
} while (path.normalize(dir) != path.normalize(rootDir));
return layouts;
};
export const wrapLayouts = async (element: JSX.Element, layouts: string[]) => {
let wrapped = element;
for (const part of layouts) {
const Layout = (await import(part)).default;
wrapped = <Layout>{wrapped}</Layout>;
}
return wrapped;
};

12
src/middlewares/gzip.ts Normal file
View File

@ -0,0 +1,12 @@
export const gzip = (handler: (req: Request) => Promise<Response>) => {
return async (req: Request) => {
const response = await handler(req);
const bytes = await response.clone().bytes();
const gzipped = Bun.gzipSync(bytes);
return new Response(gzipped, {
headers: { ...response.headers.toJSON(), "Content-Encoding": "gzip" },
status: response.status,
statusText: response.statusText,
});
};
};

View File

@ -11,15 +11,16 @@ const formatBytes = (bytes: number) => {
return bytes + "B"; return bytes + "B";
}; };
export const requestLog = (handler: (req: Request) => Promise<Response>) => { export const requestLog =
(name?: string) => (handler: (req: Request) => Promise<Response>) => {
return async (req: Request) => { return async (req: Request) => {
const start = performance.now(); const start = performance.now();
const response = await handler(req); const response = await handler(req);
const bytes = await response.clone().bytes(); const bytes = await response.clone().bytes();
const delta = performance.now() - start; const delta = performance.now() - start;
console.log( console.log(
`[${req.method}] ${req.url} -> ${response.headers.get("Content-Type")} ${formatBytes(bytes.length)} in ${delta}ms`, `[${req.method}] ${req.url}${name ? " " + name : ""} -> ${response.headers.get("Content-Type")} ${formatBytes(bytes.length)} in ${delta}ms`,
); );
return response; return response;
}; };
}; };

View File

@ -0,0 +1,12 @@
type Handler = (req: Request) => Promise<Response>;
export const withMiddlewares = (
fetchFn: Handler,
...handlers: ((handler: Handler) => Handler)[]
) => {
return async (req: Request) => {
return handlers.reduce((acc, c) => {
return c(acc);
}, fetchFn)(req);
};
};

3
src/pages/_layout.tsx Normal file
View File

@ -0,0 +1,3 @@
import Layout from "../components/Layout";
export default Layout;

View File

@ -12,7 +12,6 @@ export default async function Index() {
offset: page * 20, offset: page * 20,
}); });
return ( return (
<Layout>
<> <>
<div>Hello World</div> <div>Hello World</div>
<Counter /> <Counter />
@ -25,6 +24,5 @@ export default async function Index() {
<Link href={`/?page=${page - 1}`}>Prev</Link> <Link href={`/?page=${page - 1}`}>Prev</Link>
<Link href={`/?page=${page + 1}`}>Next</Link> <Link href={`/?page=${page + 1}`}>Next</Link>
</> </>
</Layout>
); );
} }