traverse.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import type { Spec, UIElement } from "@json-render/core";
  2. /**
  3. * Visitor function for spec traversal
  4. */
  5. export interface TreeVisitor {
  6. (
  7. element: UIElement,
  8. key: string,
  9. depth: number,
  10. parent: UIElement | null,
  11. ): void;
  12. }
  13. /**
  14. * Traverse a UI spec depth-first
  15. */
  16. export function traverseSpec(
  17. spec: Spec,
  18. visitor: TreeVisitor,
  19. startKey?: string,
  20. ): void {
  21. if (!spec || !spec.root) return;
  22. const rootKey = startKey ?? spec.root;
  23. const rootElement = spec.elements[rootKey];
  24. if (!rootElement) return;
  25. function visit(key: string, depth: number, parent: UIElement | null): void {
  26. const element = spec.elements[key];
  27. if (!element) return;
  28. visitor(element, key, depth, parent);
  29. if (element.children) {
  30. for (const childKey of element.children) {
  31. visit(childKey, depth + 1, element);
  32. }
  33. }
  34. }
  35. visit(rootKey, 0, null);
  36. }
  37. /**
  38. * Collect all unique component types used in a spec
  39. */
  40. export function collectUsedComponents(spec: Spec): Set<string> {
  41. const components = new Set<string>();
  42. traverseSpec(spec, (element, _key) => {
  43. components.add(element.type);
  44. });
  45. return components;
  46. }
  47. /**
  48. * Collect all state paths referenced in a spec
  49. */
  50. export function collectStatePaths(spec: Spec): Set<string> {
  51. const paths = new Set<string>();
  52. traverseSpec(spec, (element, _key) => {
  53. // Check props for data paths
  54. for (const [propName, propValue] of Object.entries(element.props)) {
  55. // Check for path props (e.g., statePath, dataPath, bindPath)
  56. if (typeof propValue === "string") {
  57. if (
  58. propName.endsWith("Path") ||
  59. propName === "bindPath" ||
  60. propName === "statePath"
  61. ) {
  62. paths.add(propValue);
  63. }
  64. }
  65. // Check for dynamic value objects with $state
  66. if (
  67. propValue &&
  68. typeof propValue === "object" &&
  69. "$state" in propValue &&
  70. typeof (propValue as { $state: unknown }).$state === "string"
  71. ) {
  72. paths.add((propValue as { $state: string }).$state);
  73. }
  74. }
  75. // Check visibility conditions for $state paths
  76. if (element.visible != null && typeof element.visible !== "boolean") {
  77. collectPathsFromCondition(element.visible, paths);
  78. }
  79. });
  80. return paths;
  81. }
  82. function collectPathFromItem(
  83. item: Record<string, unknown>,
  84. paths: Set<string>,
  85. ): void {
  86. if (typeof item.$state === "string") {
  87. paths.add(item.$state);
  88. }
  89. // Also collect $state references in comparison values (eq, neq, etc.)
  90. for (const op of ["eq", "neq", "gt", "gte", "lt", "lte"]) {
  91. const val = item[op];
  92. if (
  93. val &&
  94. typeof val === "object" &&
  95. "$state" in (val as Record<string, unknown>) &&
  96. typeof (val as Record<string, unknown>).$state === "string"
  97. ) {
  98. paths.add((val as { $state: string }).$state);
  99. }
  100. }
  101. }
  102. function collectPathsFromCondition(
  103. condition: unknown,
  104. paths: Set<string>,
  105. ): void {
  106. if (!condition || typeof condition !== "object") return;
  107. // Array = implicit AND
  108. if (Array.isArray(condition)) {
  109. for (const item of condition) {
  110. if (item && typeof item === "object") {
  111. collectPathFromItem(item as Record<string, unknown>, paths);
  112. }
  113. }
  114. return;
  115. }
  116. const cond = condition as Record<string, unknown>;
  117. // $or: recurse into each child condition
  118. if ("$or" in cond && Array.isArray(cond.$or)) {
  119. for (const child of cond.$or) {
  120. collectPathsFromCondition(child, paths);
  121. }
  122. return;
  123. }
  124. // Single StateCondition
  125. collectPathFromItem(cond, paths);
  126. }
  127. /**
  128. * Collect all action names used in a spec
  129. */
  130. export function collectActions(spec: Spec): Set<string> {
  131. const actions = new Set<string>();
  132. traverseSpec(spec, (element, _key) => {
  133. for (const propValue of Object.values(element.props)) {
  134. // Check for action prop (string action name)
  135. if (typeof propValue === "string" && propValue.startsWith("action:")) {
  136. actions.add(propValue.slice(7));
  137. }
  138. // Check for action objects
  139. if (
  140. propValue &&
  141. typeof propValue === "object" &&
  142. "name" in propValue &&
  143. typeof (propValue as { name: unknown }).name === "string"
  144. ) {
  145. actions.add((propValue as { name: string }).name);
  146. }
  147. }
  148. // Also check direct action prop
  149. const actionProp = element.props.action;
  150. if (typeof actionProp === "string") {
  151. actions.add(actionProp);
  152. }
  153. // Collect actions from on event bindings
  154. const onBindings = element.on;
  155. if (onBindings) {
  156. for (const binding of Object.values(onBindings)) {
  157. const bindings = Array.isArray(binding) ? binding : [binding];
  158. for (const b of bindings) {
  159. if (
  160. b &&
  161. typeof b === "object" &&
  162. "action" in b &&
  163. typeof (b as { action: unknown }).action === "string"
  164. ) {
  165. actions.add((b as { action: string }).action);
  166. }
  167. }
  168. }
  169. }
  170. });
  171. return actions;
  172. }