fast-web/examples/blog/src/lib/fast-web.tsx

150 lines
4.1 KiB
TypeScript

import { camelToKebab, renderStyle } from "~/system/style-rules";
import { transform } from "lightningcss";
import reset from "./reset.css" with { type: "text" };
import { AsyncLocalStorage } from "async_hooks";
import path from "path";
interface RenderContext {
styles: string[];
url: URL;
states: Record<string, { state: any; handlerId: string }>;
// Mapping handlerHash to file
handlers: Record<string, string>;
}
const asyncLocalStorage = new AsyncLocalStorage<RenderContext>();
const renderChildren = async (elements: JSX.Children): Promise<string> => {
if (Array.isArray(elements)) {
return (
await Promise.all(elements.map((element) => renderChildren(element)))
).join("");
}
if (typeof elements === "string") {
return elements;
}
return renderElement(elements);
};
const getStyles = () => asyncLocalStorage.getStore()!.styles;
export const getUrl = () => asyncLocalStorage.getStore()!.url;
export const getHandlers = () => asyncLocalStorage.getStore()!.handlers;
export const getStates = () => asyncLocalStorage.getStore()!.states;
export const pushStyle = (...styles: string[]) => {
getStyles().push(...styles);
};
const renderElement = async (
element: JSX.Element | string | null | undefined,
): Promise<string> => {
if (!element) {
return "";
}
if (typeof element === "string") {
return element;
}
if (element.$$typeof === Symbol.for("fast.fragment")) {
return renderChildren(element.children);
}
if (typeof element.type === "string") {
let { className, ...otherProps } = element.props;
if ("style" in element.props) {
const res = renderStyle(element.props.style);
pushStyle(res.css);
className = className ? `${className} ${res.className}` : res.className;
}
const props = { ...otherProps, class: className };
const attrs = Object.entries(props).map(([key, value]) => {
if (key === "children") {
return "";
}
if (key === "style") {
return "";
}
if (!value) {
return "";
}
return ` ${camelToKebab(key)}="${value}"`;
});
return `<${element.type}${attrs.join("")}>${element.children ? await renderChildren(element.children) : ""}</${element.type}>`;
}
if (typeof element.type === "function") {
return renderElement(
await element.type({ ...element.props, children: element.children }),
);
}
return "";
};
const render = async (
element: JSX.Element | string | null,
extraCss: string[] = [],
) => {
pushStyle(reset, ...extraCss);
const html = await renderElement(element);
const encoder = new TextEncoder();
const css = transform({
code: encoder.encode(getStyles().join("\n")),
minify: true,
filename: "render-style.ts",
}).code;
const head = `<style>${css.toString()}</style>
<script>
window.stateManager ??= {
init: false,
states: {},
stateRegistry: new Map()
}
window.stateManager.states = {...window.stateManager.states,...${JSON.stringify(getStates())}}
</script>
`;
return { html, head };
};
const runRender = (
element: JSX.Element | string | null,
extraCss: string[] = [],
url: URL,
) => {
return asyncLocalStorage.run(
{ styles: [], url, states: {}, handlers: {} },
() => render(element, extraCss),
);
};
export { runRender as render };
export const fillTemplate = (
template: string,
data: Record<string, string>,
) => {
return Object.entries(data).reduce((acc, [key, value]) => {
return acc.replace(`<!-- ${key} -->`, value);
}, 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;
};