| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- import type { Spec, UIElement } from "@json-render/core";
- /**
- * Visitor function for spec traversal
- */
- export interface TreeVisitor {
- (
- element: UIElement,
- key: string,
- depth: number,
- parent: UIElement | null,
- ): void;
- }
- /**
- * Traverse a UI spec depth-first
- */
- export function traverseSpec(
- spec: Spec,
- visitor: TreeVisitor,
- startKey?: string,
- ): void {
- if (!spec || !spec.root) return;
- const rootKey = startKey ?? spec.root;
- const rootElement = spec.elements[rootKey];
- if (!rootElement) return;
- function visit(key: string, depth: number, parent: UIElement | null): void {
- const element = spec.elements[key];
- if (!element) return;
- visitor(element, key, depth, parent);
- if (element.children) {
- for (const childKey of element.children) {
- visit(childKey, depth + 1, element);
- }
- }
- }
- visit(rootKey, 0, null);
- }
- /**
- * Collect all unique component types used in a spec
- */
- export function collectUsedComponents(spec: Spec): Set<string> {
- const components = new Set<string>();
- traverseSpec(spec, (element, _key) => {
- components.add(element.type);
- });
- return components;
- }
- /**
- * Collect all state paths referenced in a spec
- */
- export function collectStatePaths(spec: Spec): Set<string> {
- const paths = new Set<string>();
- traverseSpec(spec, (element, _key) => {
- // Check props for data paths
- for (const [propName, propValue] of Object.entries(element.props)) {
- // Check for path props (e.g., statePath, dataPath, bindPath)
- if (typeof propValue === "string") {
- if (
- propName.endsWith("Path") ||
- propName === "bindPath" ||
- propName === "statePath"
- ) {
- paths.add(propValue);
- }
- }
- // Check for dynamic value objects with $state
- if (
- propValue &&
- typeof propValue === "object" &&
- "$state" in propValue &&
- typeof (propValue as { $state: unknown }).$state === "string"
- ) {
- paths.add((propValue as { $state: string }).$state);
- }
- }
- // Check visibility conditions for $state paths
- if (element.visible != null && typeof element.visible !== "boolean") {
- collectPathsFromCondition(element.visible, paths);
- }
- });
- return paths;
- }
- function collectPathFromItem(
- item: Record<string, unknown>,
- paths: Set<string>,
- ): void {
- if (typeof item.$state === "string") {
- paths.add(item.$state);
- }
- // Also collect $state references in comparison values (eq, neq, etc.)
- for (const op of ["eq", "neq", "gt", "gte", "lt", "lte"]) {
- const val = item[op];
- if (
- val &&
- typeof val === "object" &&
- "$state" in (val as Record<string, unknown>) &&
- typeof (val as Record<string, unknown>).$state === "string"
- ) {
- paths.add((val as { $state: string }).$state);
- }
- }
- }
- function collectPathsFromCondition(
- condition: unknown,
- paths: Set<string>,
- ): void {
- if (!condition || typeof condition !== "object") return;
- // Array = implicit AND
- if (Array.isArray(condition)) {
- for (const item of condition) {
- if (item && typeof item === "object") {
- collectPathFromItem(item as Record<string, unknown>, paths);
- }
- }
- return;
- }
- const cond = condition as Record<string, unknown>;
- // $or: recurse into each child condition
- if ("$or" in cond && Array.isArray(cond.$or)) {
- for (const child of cond.$or) {
- collectPathsFromCondition(child, paths);
- }
- return;
- }
- // Single StateCondition
- collectPathFromItem(cond, paths);
- }
- /**
- * Collect all action names used in a spec
- */
- export function collectActions(spec: Spec): Set<string> {
- const actions = new Set<string>();
- traverseSpec(spec, (element, _key) => {
- for (const propValue of Object.values(element.props)) {
- // Check for action prop (string action name)
- if (typeof propValue === "string" && propValue.startsWith("action:")) {
- actions.add(propValue.slice(7));
- }
- // Check for action objects
- if (
- propValue &&
- typeof propValue === "object" &&
- "name" in propValue &&
- typeof (propValue as { name: unknown }).name === "string"
- ) {
- actions.add((propValue as { name: string }).name);
- }
- }
- // Also check direct action prop
- const actionProp = element.props.action;
- if (typeof actionProp === "string") {
- actions.add(actionProp);
- }
- // Collect actions from on event bindings
- const onBindings = element.on;
- if (onBindings) {
- for (const binding of Object.values(onBindings)) {
- const bindings = Array.isArray(binding) ? binding : [binding];
- for (const b of bindings) {
- if (
- b &&
- typeof b === "object" &&
- "action" in b &&
- typeof (b as { action: unknown }).action === "string"
- ) {
- actions.add((b as { action: string }).action);
- }
- }
- }
- }
- });
- return actions;
- }
|