// Minimal helper to create elements from generated lexicon modules // // Usage: // import { Stack } from "@/generated/org/design"; // import { Post } from "@/generated/com/example"; // import { $, serializeTree, deserializeTree } from "@inlay/core"; // // $(Stack, { gap: "medium" }, // $(Post, { uri: 'at://...' }), // $(Post, { uri: 'at://...' }) // ) // // Serialization: // serializeTree(tree) // deserializeTree(tree) // // Element types are NSIDs (e.g., "com.example.Post") import type { l } from "@atproto/lex"; import { ensureValidNsid } from "@atproto/syntax"; import { BRAND, type Element, type LexiconComponent, createElement, keyStaticChildren, } from "./element.ts"; export { type Element, isValidElement } from "./element.ts"; export { BRAND } from "./element.ts"; // --- Base types --- type LexiconModule = LexiconComponent & { readonly main: l.Procedure; }; type Child = Element | string | number | boolean | null | undefined | Child[]; // --- JSX support --- type Props = Omit< l.InferMethodInputBody, "children" > & { children?: Child; }; export type Component = M & ((props: Props) => Element); export function jslex(namespace: T): { [K in keyof T]: T[K] extends LexiconModule ? Component : T[K]; } { return namespace as any; } // --- $ function --- type Config = Omit< l.InferMethodInputBody, "children" > & { key?: string }; // Overload: typed lexicon module export function $( mod: M, config?: Config, ...children: Child[] ): Element; // Overload: string NSID (untyped) export function $( nsid: string, config?: l.LexMap & { key?: string }, ...children: Child[] ): Element; // Implementation export function $( modOrNsid: LexiconModule | string, config?: l.LexMap & { key?: string }, ...children: Child[] ): Element { const nsid = typeof modOrNsid === "string" ? (ensureValidNsid(modOrNsid), modOrNsid) : modOrNsid.$nsid; const { key, ...rest } = config ?? {}; const props = children.length > 0 ? { ...rest, children: keyStaticChildren(children) } : rest; return createElement(nsid, key, props); } export function walkTree( tree: T, mapObject: ( obj: Record, walk: (v: unknown) => unknown ) => unknown ): T { return walk(tree) as T; function walk(value: unknown): unknown { if (value == null || typeof value !== "object") { return value; } if (Array.isArray(value)) { return value.map(walk); } return mapObject(value as Record, walk); } } export type ElementVisitor = (element: Element) => unknown; export function serializeTree(tree: T, visitor?: ElementVisitor): T { return walkTree(tree, (obj, walk) => { if (visitor && obj.$ === BRAND) { const result = visitor(obj as Element); if (result !== obj) { return walk(result); } } const out: Record = {}; for (const [k, v] of Object.entries(obj)) { if (v === BRAND) { out[k] = "$"; } else if (typeof v === "string" && v.startsWith("$")) { out[k] = "$" + v; } else { out[k] = walk(v); } } return out; }); } export function deserializeTree(tree: T, visitor?: ElementVisitor): T { return walkTree(tree, (obj, walk) => { const out: Record = {}; for (const [k, v] of Object.entries(obj)) { if (v === "$") { out[k] = BRAND; } else if (typeof v === "string" && v.startsWith("$$")) { out[k] = v.slice(1); } else { out[k] = walk(v); } } if (visitor && out.$ === BRAND) { const result = visitor(out as Element); if (result !== out) { return result; } } return out; }); } /** * Walk a value tree resolving Binding elements via a caller-provided resolver. * Recurses into plain objects and arrays. Non-Binding elements are opaque — * they are NOT recursed into (their props are rendered separately by renderNode). */ export function resolveBindings( tree: T, resolve: (path: string[]) => unknown ): T { return walkTree(tree, (obj, walk) => { if (obj.$ === BRAND) { const el = obj as Element; if (el.type === "at.inlay.Binding") { const path = (el.props as Record | undefined)?.path; if (!Array.isArray(path)) throw new Error("Binding missing path"); return resolve(path as string[]); } return obj; // non-Binding element: opaque } const out: Record = {}; for (const [k, v] of Object.entries(obj)) out[k] = walk(v); return out; }); }