150 lines
4.1 KiB
TypeScript
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;
|
|
};
|