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; // Mapping handlerHash to file handlers: Record; } const asyncLocalStorage = new AsyncLocalStorage(); const renderChildren = async (elements: JSX.Children): Promise => { 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 => { 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) : ""}`; } 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 = ` `; 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, ) => { return Object.entries(data).reduce((acc, [key, value]) => { return acc.replace(``, 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 = {wrapped}; } return wrapped; };