Replace Go HTML template rendering with a Bun + TSX build-time static site generator. Go backend becomes API-only for auth. Frontend: - Custom JSX-to-HTML-string factory (zero dependencies) - TSX components for Header, Footer, index page, login page - Client-side login.ts handles tab switching and fetch()-based auth - Bun bundler compiles client JS, build.ts renders pages to dist/ Backend: - Auth handlers return JSON (POST /api/login, POST /api/register) - Login page served as static HTML from dist/ - Static assets served from /assets/ (public) - Auth middleware unchanged (cookie check, redirect to /login) - Removed template parsing and renderPage Dockerfile: - 3-stage build: Bun frontend -> Go backend -> alpine runtime - Frontend dist copied to /app/dist in final image Removed: templates/, static/css/ (replaced by frontend/)
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
const VOID_ELEMENTS = new Set([
|
|
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
|
"link", "meta", "param", "source", "track", "wbr",
|
|
]);
|
|
|
|
const ATTR_MAP: Record<string, string> = {
|
|
className: "class",
|
|
htmlFor: "for",
|
|
};
|
|
|
|
function escapeAttr(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
}
|
|
|
|
function flatten(children: any[]): string {
|
|
return children
|
|
.flat(Infinity)
|
|
.filter((c) => c != null && c !== false && c !== true)
|
|
.map((c) => String(c))
|
|
.join("");
|
|
}
|
|
|
|
export function h(
|
|
tag: string | ((props: any) => string),
|
|
props: Record<string, any> | null,
|
|
...children: any[]
|
|
): string {
|
|
if (typeof tag === "function") {
|
|
return tag({ ...props, children: children.length === 1 ? children[0] : children });
|
|
}
|
|
|
|
let attrs = "";
|
|
let innerHTML = "";
|
|
|
|
if (props) {
|
|
for (const [key, value] of Object.entries(props)) {
|
|
if (key === "children") continue;
|
|
if (key === "dangerouslySetInnerHTML") {
|
|
innerHTML = value.__html;
|
|
continue;
|
|
}
|
|
if (value == null || value === false) continue;
|
|
const name = ATTR_MAP[key] || key;
|
|
if (value === true) {
|
|
attrs += ` ${name}`;
|
|
} else {
|
|
attrs += ` ${name}="${escapeAttr(String(value))}"`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (VOID_ELEMENTS.has(tag)) {
|
|
return `<${tag}${attrs}>`;
|
|
}
|
|
|
|
const content = innerHTML || flatten(children);
|
|
return `<${tag}${attrs}>${content}</${tag}>`;
|
|
}
|
|
|
|
export function Fragment({ children }: { children: any }): string {
|
|
return Array.isArray(children) ? flatten(children) : String(children ?? "");
|
|
}
|