@base44/vite-plugin 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/capabilities/inline-edit/controller.d.ts.map +1 -1
  2. package/dist/capabilities/inline-edit/controller.js +3 -0
  3. package/dist/capabilities/inline-edit/controller.js.map +1 -1
  4. package/dist/capabilities/inline-edit/index.d.ts +1 -1
  5. package/dist/capabilities/inline-edit/index.d.ts.map +1 -1
  6. package/dist/capabilities/inline-edit/types.d.ts +4 -0
  7. package/dist/capabilities/inline-edit/types.d.ts.map +1 -1
  8. package/dist/consts.d.ts +11 -0
  9. package/dist/consts.d.ts.map +1 -0
  10. package/dist/consts.js +11 -0
  11. package/dist/consts.js.map +1 -0
  12. package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -1
  13. package/dist/injections/layer-dropdown/dropdown-ui.js +3 -0
  14. package/dist/injections/layer-dropdown/dropdown-ui.js.map +1 -1
  15. package/dist/injections/utils.d.ts +31 -0
  16. package/dist/injections/utils.d.ts.map +1 -1
  17. package/dist/injections/utils.js +97 -0
  18. package/dist/injections/utils.js.map +1 -1
  19. package/dist/injections/visual-edit-agent.d.ts.map +1 -1
  20. package/dist/injections/visual-edit-agent.js +58 -51
  21. package/dist/injections/visual-edit-agent.js.map +1 -1
  22. package/dist/jsx-processor.d.ts +3 -1
  23. package/dist/jsx-processor.d.ts.map +1 -1
  24. package/dist/jsx-processor.js +29 -6
  25. package/dist/jsx-processor.js.map +1 -1
  26. package/dist/jsx-utils.d.ts +9 -0
  27. package/dist/jsx-utils.d.ts.map +1 -1
  28. package/dist/jsx-utils.js +86 -0
  29. package/dist/jsx-utils.js.map +1 -1
  30. package/dist/processors/collection-id-processor.d.ts +20 -0
  31. package/dist/processors/collection-id-processor.d.ts.map +1 -0
  32. package/dist/processors/collection-id-processor.js +182 -0
  33. package/dist/processors/collection-id-processor.js.map +1 -0
  34. package/dist/processors/collection-item-field-processor.d.ts +39 -0
  35. package/dist/processors/collection-item-field-processor.d.ts.map +1 -0
  36. package/dist/processors/collection-item-field-processor.js +281 -0
  37. package/dist/processors/collection-item-field-processor.js.map +1 -0
  38. package/dist/processors/collection-item-id-processor.d.ts +12 -0
  39. package/dist/processors/collection-item-id-processor.d.ts.map +1 -0
  40. package/dist/processors/collection-item-id-processor.js +50 -0
  41. package/dist/processors/collection-item-id-processor.js.map +1 -0
  42. package/dist/processors/static-array-processor.d.ts +0 -3
  43. package/dist/processors/static-array-processor.d.ts.map +1 -1
  44. package/dist/processors/static-array-processor.js +2 -4
  45. package/dist/processors/static-array-processor.js.map +1 -1
  46. package/dist/processors/utils/collection-tracing-utils.d.ts +36 -0
  47. package/dist/processors/utils/collection-tracing-utils.d.ts.map +1 -0
  48. package/dist/processors/utils/collection-tracing-utils.js +390 -0
  49. package/dist/processors/utils/collection-tracing-utils.js.map +1 -0
  50. package/dist/processors/utils/shared-utils.d.ts +96 -0
  51. package/dist/processors/utils/shared-utils.d.ts.map +1 -0
  52. package/dist/processors/utils/shared-utils.js +600 -0
  53. package/dist/processors/utils/shared-utils.js.map +1 -0
  54. package/dist/statics/index.mjs +12 -2
  55. package/dist/statics/index.mjs.map +1 -1
  56. package/dist/visual-edit-plugin.d.ts +0 -1
  57. package/dist/visual-edit-plugin.d.ts.map +1 -1
  58. package/dist/visual-edit-plugin.js +29 -178
  59. package/dist/visual-edit-plugin.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/capabilities/inline-edit/controller.ts +3 -0
  62. package/src/capabilities/inline-edit/index.ts +1 -1
  63. package/src/capabilities/inline-edit/types.ts +5 -0
  64. package/src/consts.ts +11 -0
  65. package/src/injections/layer-dropdown/dropdown-ui.ts +3 -0
  66. package/src/injections/utils.ts +105 -0
  67. package/src/injections/visual-edit-agent.ts +56 -64
  68. package/src/jsx-processor.ts +36 -14
  69. package/src/jsx-utils.ts +116 -0
  70. package/src/processors/collection-id-processor.ts +261 -0
  71. package/src/processors/collection-item-field-processor.ts +433 -0
  72. package/src/processors/collection-item-id-processor.ts +69 -0
  73. package/src/processors/static-array-processor.ts +7 -4
  74. package/src/processors/utils/collection-tracing-utils.ts +507 -0
  75. package/src/processors/utils/shared-utils.ts +785 -0
  76. package/src/visual-edit-plugin.ts +34 -215
  77. package/dist/processors/shared-utils.d.ts +0 -19
  78. package/dist/processors/shared-utils.d.ts.map +0 -1
  79. package/dist/processors/shared-utils.js +0 -77
  80. package/dist/processors/shared-utils.js.map +0 -1
  81. package/src/processors/shared-utils.ts +0 -116
@@ -1,4 +1,4 @@
1
- import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId } from "./utils.js";
1
+ import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId, stopAnimations, resumeAnimations, findInstrumentedElement, resolveHoverTarget } from "./utils.js";
2
2
  import { createLayerController } from "./layer-dropdown/controller.js";
3
3
  import { LAYER_DROPDOWN_ATTR } from "./layer-dropdown/consts.js";
4
4
  import { createInlineEditController } from "../capabilities/inline-edit/index.js";
@@ -191,53 +191,19 @@ export function setupVisualEditAgent() {
191
191
  window.parent.postMessage({ type: "unselect-element" }, "*");
192
192
  };
193
193
 
194
- // Handle mouse over event
195
- const handleMouseOver = (e: MouseEvent) => {
196
- if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
197
-
198
-
199
- const target = e.target as Element;
200
-
201
- // Prevent hover effects when a dropdown is open
202
- if (isDropdownOpen) {
203
- clearHoverOverlays();
204
- return;
205
- }
206
-
207
- // Prevent hover effects on SVG path elements
208
- if (target.tagName.toLowerCase() === "path") {
209
- clearHoverOverlays();
210
- return;
211
- }
194
+ // Hover detection via mousemove + elementFromPoint (since app elements have pointer-events: none)
195
+ let lastHoveredSelectorId: string | null = null;
196
+ let pendingMouseMoveRaf: number | null = null;
212
197
 
213
- // Support both data-source-location and data-visual-selector-id
214
- const element = target.closest(
215
- "[data-source-location], [data-visual-selector-id]"
216
- );
217
- if (!element) {
218
- clearHoverOverlays();
219
- return;
220
- }
221
-
222
- // Prefer data-source-location, fallback to data-visual-selector-id
223
- const htmlElement = element as HTMLElement;
224
- const selectorId =
225
- htmlElement.dataset.sourceLocation ||
226
- htmlElement.dataset.visualSelectorId;
227
-
228
- // Skip if this element is already selected
229
- if (selectedElementId === selectorId) {
230
- clearHoverOverlays();
231
- return;
232
- }
233
-
234
- // Find all elements with the same ID
235
- const elements = findElementsById(selectorId || null);
198
+ const clearHoverState = () => {
199
+ clearHoverOverlays();
200
+ lastHoveredSelectorId = null;
201
+ };
236
202
 
237
- // Clear previous hover overlays
203
+ const applyHoverOverlays = (selectorId: string) => {
204
+ const elements = findElementsById(selectorId);
238
205
  clearHoverOverlays();
239
206
 
240
- // Create overlays for all matching elements
241
207
  elements.forEach((el) => {
242
208
  const overlay = createOverlay(false);
243
209
  document.body.appendChild(overlay);
@@ -246,12 +212,33 @@ export function setupVisualEditAgent() {
246
212
  });
247
213
 
248
214
  currentHighlightedElements = elements;
215
+ lastHoveredSelectorId = selectorId;
249
216
  };
250
217
 
251
- // Handle mouse out event
252
- const handleMouseOut = () => {
253
- if (isPopoverDragging) return;
254
- clearHoverOverlays();
218
+ const handleMouseMove = (e: MouseEvent) => {
219
+ if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing()) return;
220
+
221
+ if (pendingMouseMoveRaf !== null) return;
222
+ pendingMouseMoveRaf = requestAnimationFrame(() => {
223
+ pendingMouseMoveRaf = null;
224
+
225
+ if (isDropdownOpen) { clearHoverState(); return; }
226
+
227
+ const selectorId = resolveHoverTarget(e.clientX, e.clientY, selectedElementId);
228
+ if (!selectorId) { clearHoverState(); return; }
229
+ if (lastHoveredSelectorId === selectorId) return;
230
+
231
+ applyHoverOverlays(selectorId);
232
+ });
233
+ };
234
+
235
+ // Clear hover overlays when mouse leaves the viewport
236
+ const handleMouseLeave = () => {
237
+ if (pendingMouseMoveRaf !== null) {
238
+ cancelAnimationFrame(pendingMouseMoveRaf);
239
+ pendingMouseMoveRaf = null;
240
+ }
241
+ clearHoverState();
255
242
  };
256
243
 
257
244
  // Handle element click
@@ -288,20 +275,12 @@ export function setupVisualEditAgent() {
288
275
  return;
289
276
  }
290
277
 
291
- // Prevent clicking on SVG path elements
292
- if (target.tagName.toLowerCase() === "path") {
293
- return;
294
- }
295
-
296
278
  // Prevent default behavior immediately when in visual edit mode
297
279
  e.preventDefault();
298
280
  e.stopPropagation();
299
281
  e.stopImmediatePropagation();
300
282
 
301
- // Support both data-source-location and data-visual-selector-id
302
- const element = target.closest(
303
- "[data-source-location], [data-visual-selector-id]"
304
- );
283
+ const element = findInstrumentedElement(e.clientX, e.clientY);
305
284
  if (!element) {
306
285
  return;
307
286
  }
@@ -435,21 +414,21 @@ export function setupVisualEditAgent() {
435
414
  isVisualEditMode = isEnabled;
436
415
 
437
416
  if (!isEnabled) {
417
+ resumeAnimations();
438
418
  inlineEdit.stopEditing();
439
419
  clearSelection();
440
420
  layerController.cleanup();
441
- clearHoverOverlays();
442
-
443
- currentHighlightedElements = [];
421
+ handleMouseLeave();
444
422
  document.body.style.cursor = "default";
445
423
 
446
- document.removeEventListener("mouseover", handleMouseOver);
447
- document.removeEventListener("mouseout", handleMouseOut);
424
+ document.removeEventListener("mousemove", handleMouseMove);
425
+ document.removeEventListener("mouseleave", handleMouseLeave);
448
426
  document.removeEventListener("click", handleElementClick, true);
449
427
  } else {
450
428
  document.body.style.cursor = "crosshair";
451
- document.addEventListener("mouseover", handleMouseOver);
452
- document.addEventListener("mouseout", handleMouseOut);
429
+ stopAnimations();
430
+ document.addEventListener("mousemove", handleMouseMove);
431
+ document.addEventListener("mouseleave", handleMouseLeave);
453
432
  document.addEventListener("click", handleElementClick, true);
454
433
  }
455
434
  };
@@ -620,6 +599,19 @@ export function setupVisualEditAgent() {
620
599
  }
621
600
  break;
622
601
 
602
+ case "update-theme-variables":
603
+ if (message.data?.variables) {
604
+ const target = message.data.mode === 'dark'
605
+ ? document.querySelector('.dark') as HTMLElement
606
+ : document.documentElement;
607
+ if (target) {
608
+ for (const [name, value] of Object.entries(message.data.variables)) {
609
+ target.style.setProperty(name, value as string);
610
+ }
611
+ }
612
+ }
613
+ break;
614
+
623
615
  case "toggle-inline-edit-mode":
624
616
  if (message.data) {
625
617
  inlineEdit.handleToggleMessage(message.data);
@@ -1,14 +1,23 @@
1
1
  import type { NodePath } from "@babel/traverse";
2
2
  import type * as t from "@babel/types";
3
+ import { CollectionIdProcessor } from "./processors/collection-id-processor.js";
4
+ import { DataItemIdProcessor } from "./processors/collection-item-id-processor.js";
5
+ import { DataItemFieldProcessor } from "./processors/collection-item-field-processor.js";
3
6
  import { StaticArrayProcessor } from "./processors/static-array-processor.js";
4
7
 
5
8
  export class JSXProcessor {
9
+ private collectionIdProcessor: CollectionIdProcessor;
10
+ private dataItemIdProcessor: DataItemIdProcessor;
11
+ private dataItemFieldProcessor: DataItemFieldProcessor;
6
12
  private staticArrayProcessor: StaticArrayProcessor;
7
13
 
8
14
  constructor(
9
15
  private types: typeof t,
10
16
  private filename: string
11
17
  ) {
18
+ this.collectionIdProcessor = new CollectionIdProcessor(types);
19
+ this.dataItemIdProcessor = new DataItemIdProcessor(types);
20
+ this.dataItemFieldProcessor = new DataItemFieldProcessor(types);
12
21
  this.staticArrayProcessor = new StaticArrayProcessor(types);
13
22
  }
14
23
 
@@ -17,7 +26,10 @@ export class JSXProcessor {
17
26
 
18
27
  this.addSourceLocationAttribute(path);
19
28
  this.addDynamicContentAttribute(path);
20
- this.addContentEditableAttribute(path);
29
+
30
+ this.collectionIdProcessor.process(path);
31
+ this.dataItemIdProcessor.process(path);
32
+ this.dataItemFieldProcessor.process(path);
21
33
  this.staticArrayProcessor.process(path);
22
34
  }
23
35
 
@@ -27,7 +39,7 @@ export class JSXProcessor {
27
39
  const { line, column } = path.node.loc?.start || { line: 1, column: 0 };
28
40
  const value = `${this.filename}:${line}:${column}`;
29
41
 
30
- path.node.attributes.push(
42
+ path.node.attributes.unshift(
31
43
  this.types.jsxAttribute(
32
44
  this.types.jsxIdentifier("data-source-location"),
33
45
  this.types.stringLiteral(value)
@@ -45,21 +57,19 @@ export class JSXProcessor {
45
57
  parentElement.node as t.JSXElement
46
58
  );
47
59
 
48
- path.node.attributes.push(
49
- this.types.jsxAttribute(
50
- this.types.jsxIdentifier("data-dynamic-content"),
51
- this.types.stringLiteral(isDynamic ? "true" : "false")
52
- )
60
+ const sourceLocIdx = path.node.attributes.findIndex(
61
+ (attr) =>
62
+ this.types.isJSXAttribute(attr) &&
63
+ this.types.isJSXIdentifier(attr.name) &&
64
+ attr.name.name === "data-source-location"
53
65
  );
54
- }
55
66
 
56
- private addContentEditableAttribute(
57
- path: NodePath<t.JSXOpeningElement>
58
- ): void {
59
- path.node.attributes.push(
67
+ path.node.attributes.splice(
68
+ sourceLocIdx + 1,
69
+ 0,
60
70
  this.types.jsxAttribute(
61
- this.types.jsxIdentifier("content-editable"),
62
- this.types.stringLiteral("true")
71
+ this.types.jsxIdentifier("data-dynamic-content"),
72
+ this.types.stringLiteral(isDynamic ? "true" : "false")
63
73
  )
64
74
  );
65
75
  }
@@ -137,6 +147,18 @@ export class JSXProcessor {
137
147
  }
138
148
  };
139
149
 
150
+ const attributes = jsxElement.openingElement?.attributes || [];
151
+ for (const attr of attributes) {
152
+ if (hasDynamicContent) break;
153
+ if (this.types.isJSXSpreadAttribute(attr)) {
154
+ hasDynamicContent = true;
155
+ break;
156
+ }
157
+ if (this.types.isJSXAttribute(attr) && attr.value) {
158
+ traverseNode(attr.value as unknown as Record<string, unknown>);
159
+ }
160
+ }
161
+
140
162
  for (const child of jsxElement.children) {
141
163
  if (hasDynamicContent) break;
142
164
  traverseNode(child as unknown as Record<string, unknown>);
package/src/jsx-utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type * as t from "@babel/types";
2
+ import { ALLOWED_CUSTOM_COMPONENTS } from "./consts.js";
2
3
 
3
4
  export class JSXUtils {
4
5
  private static types: typeof t;
@@ -12,4 +13,119 @@ export class JSXUtils {
12
13
  ? `${attr.name.namespace.name}:${attr.name.name.name}`
13
14
  : attr.name.name;
14
15
  }
16
+
17
+ static getElementName(node: t.JSXOpeningElement): string | null {
18
+ const name = node.name;
19
+
20
+ if (this.types.isJSXIdentifier(name)) {
21
+ return name.name;
22
+ }
23
+
24
+ if (this.types.isJSXNamespacedName(name)) {
25
+ return `${name.namespace.name}:${name.name.name}`;
26
+ }
27
+
28
+ if (this.types.isJSXMemberExpression(name)) {
29
+ return this.collectJSXMemberName(name);
30
+ }
31
+
32
+ return null;
33
+ }
34
+
35
+ private static collectJSXMemberName(
36
+ node: t.JSXMemberExpression
37
+ ): string {
38
+ const parts: string[] = [node.property.name];
39
+ let current: t.JSXMemberExpression["object"] = node.object;
40
+
41
+ while (this.types.isJSXMemberExpression(current)) {
42
+ parts.unshift(current.property.name);
43
+ current = current.object;
44
+ }
45
+
46
+ if (this.types.isJSXIdentifier(current)) {
47
+ parts.unshift(current.name);
48
+ }
49
+
50
+ return parts.join(".");
51
+ }
52
+
53
+ static isCustomComponent(name: string): boolean {
54
+ return name.length > 0 && name.charAt(0) === name.charAt(0).toUpperCase();
55
+ }
56
+
57
+ static isAllowedCustomComponent(name: string): boolean {
58
+ return ALLOWED_CUSTOM_COMPONENTS.includes(name);
59
+ }
60
+
61
+ static isDOMElement(name: string): boolean {
62
+ return name.length > 0 && name.charAt(0) === name.charAt(0).toLowerCase();
63
+ }
64
+
65
+ static isDOMOrAllowed(name: string): boolean {
66
+ return this.isDOMElement(name) || this.isAllowedCustomComponent(name);
67
+ }
68
+
69
+ static producesJSX(node: t.Expression | t.Node): boolean {
70
+ const types = this.types;
71
+
72
+ if (types.isJSXElement(node) || types.isJSXFragment(node)) return true;
73
+
74
+ if (types.isConditionalExpression(node)) {
75
+ return (
76
+ this.producesJSX(node.consequent) || this.producesJSX(node.alternate)
77
+ );
78
+ }
79
+
80
+ if (types.isLogicalExpression(node)) {
81
+ return this.producesJSX(node.left) || this.producesJSX(node.right);
82
+ }
83
+
84
+ if (types.isCallExpression(node)) {
85
+ if (types.isMemberExpression(node.callee)) {
86
+ const prop = node.callee.property;
87
+ if (
88
+ types.isIdentifier(prop) &&
89
+ (prop.name === "map" || prop.name === "flatMap")
90
+ ) {
91
+ const callback = node.arguments[0];
92
+ if (callback) return this.callbackProducesJSX(callback);
93
+ }
94
+ }
95
+ }
96
+
97
+ if (types.isParenthesizedExpression(node)) {
98
+ return this.producesJSX(node.expression);
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ private static callbackProducesJSX(
105
+ callback: t.Expression | t.SpreadElement | t.ArgumentPlaceholder
106
+ ): boolean {
107
+ const types = this.types;
108
+
109
+ if (types.isArrowFunctionExpression(callback)) {
110
+ if (types.isBlockStatement(callback.body)) {
111
+ return this.doesBlockReturnJSX(callback.body);
112
+ }
113
+ return this.producesJSX(callback.body);
114
+ }
115
+
116
+ if (types.isFunctionExpression(callback)) {
117
+ return this.doesBlockReturnJSX(callback.body);
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ static doesBlockReturnJSX(block: t.BlockStatement): boolean {
124
+ for (const stmt of block.body) {
125
+ if (this.types.isReturnStatement(stmt) && stmt.argument) {
126
+ if (this.producesJSX(stmt.argument)) return true;
127
+ }
128
+ }
129
+ return false;
130
+ }
15
131
  }
@@ -0,0 +1,261 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type * as t from "@babel/types";
3
+ import type { CollectionInfo } from "../capabilities/inline-edit/types.js";
4
+ import {
5
+ DATA_COLLECTION_ID,
6
+ DATA_COLLECTION_REFERENCE,
7
+ MAX_JSX_DEPTH,
8
+ } from "../consts.js";
9
+ import { JSXUtils } from "../jsx-utils.js";
10
+ import {
11
+ JSXAttributeUtils,
12
+ PathNavigationUtils,
13
+ ExpressionAnalysisUtils,
14
+ } from "./utils/shared-utils.js";
15
+ import { CollectionTracingUtils } from "./utils/collection-tracing-utils.js";
16
+
17
+ export class CollectionIdProcessor {
18
+ private attributeUtils: JSXAttributeUtils;
19
+ private pathUtils: PathNavigationUtils;
20
+ private expressionUtils: ExpressionAnalysisUtils;
21
+ private tracingUtils: CollectionTracingUtils;
22
+
23
+ constructor(private types: typeof t) {
24
+ this.attributeUtils = new JSXAttributeUtils(types);
25
+ this.pathUtils = new PathNavigationUtils(types);
26
+ this.expressionUtils = new ExpressionAnalysisUtils(types);
27
+ this.tracingUtils = new CollectionTracingUtils(types);
28
+ }
29
+
30
+ process(path: NodePath<t.JSXOpeningElement>): void {
31
+ if (this.attributeUtils.hasAttribute(path, DATA_COLLECTION_ID)) return;
32
+
33
+ const info =
34
+ this.tryDirectMapInChildren(path) ??
35
+ this.tryGetByIdInChildren(path) ??
36
+ this.tryComponentRootWithCollectionProp(path);
37
+
38
+ if (!info) return;
39
+
40
+ const target = this.pathUtils.findDOMElementTarget(path) ?? path;
41
+
42
+ this.attributeUtils.addStringAttribute(target, DATA_COLLECTION_ID, info.id);
43
+
44
+ if (info.references.length > 0) {
45
+ this.attributeUtils.addStringAttribute(
46
+ target,
47
+ DATA_COLLECTION_REFERENCE,
48
+ info.references.join(",")
49
+ );
50
+ }
51
+ }
52
+
53
+ private tryDirectMapInChildren(
54
+ path: NodePath<t.JSXOpeningElement>
55
+ ): CollectionInfo | null {
56
+ const jsxElement = path.parentPath;
57
+ if (!jsxElement?.isJSXElement()) return null;
58
+
59
+ const children = jsxElement.get("children");
60
+ for (const child of children) {
61
+ if (!child.isJSXExpressionContainer()) continue;
62
+
63
+ const expression = child.get("expression");
64
+ if (expression.isJSXEmptyExpression()) continue;
65
+
66
+ const mapSource = this.extractMapSource(
67
+ expression as NodePath<t.Expression>
68
+ );
69
+ if (mapSource) return mapSource;
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ private extractMapSource(
76
+ expr: NodePath<t.Expression>
77
+ ): CollectionInfo | null {
78
+ if (expr.isCallExpression()) {
79
+ return this.traceMapCall(expr);
80
+ }
81
+
82
+ if (expr.isLogicalExpression()) {
83
+ if (expr.node.operator === "&&") {
84
+ const right = expr.get("right") as NodePath<t.Expression>;
85
+ return this.extractMapSource(right);
86
+ }
87
+ }
88
+
89
+ if (expr.isConditionalExpression()) {
90
+ const consequent = expr.get("consequent") as NodePath<t.Expression>;
91
+ const alternate = expr.get("alternate") as NodePath<t.Expression>;
92
+ return (
93
+ this.extractMapSource(consequent) ?? this.extractMapSource(alternate)
94
+ );
95
+ }
96
+
97
+ if (expr.isOptionalCallExpression()) {
98
+ return this.traceOptionalMapCall(expr);
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ private traceMapCall(
105
+ callPath: NodePath<t.CallExpression>
106
+ ): CollectionInfo | null {
107
+ const callee = callPath.get("callee");
108
+ if (!callee.isMemberExpression()) return null;
109
+
110
+ const property = callee.get("property") as NodePath;
111
+ if (
112
+ !property.isIdentifier() ||
113
+ (property.node.name !== "map" && property.node.name !== "flatMap")
114
+ ) {
115
+ return null;
116
+ }
117
+
118
+ if (!this.callbackProducesJSX(callPath)) return null;
119
+
120
+ const arrayObj = callee.get("object") as NodePath<t.Expression>;
121
+ return this.tracingUtils.traceCollectionSource(arrayObj);
122
+ }
123
+
124
+ private traceOptionalMapCall(
125
+ callPath: NodePath<t.OptionalCallExpression>
126
+ ): CollectionInfo | null {
127
+ const callee = callPath.get("callee");
128
+ if (
129
+ !callee.isMemberExpression() &&
130
+ !callee.isOptionalMemberExpression()
131
+ ) {
132
+ return null;
133
+ }
134
+
135
+ const property = (callee as NodePath<t.MemberExpression>).get(
136
+ "property"
137
+ ) as NodePath;
138
+ if (
139
+ !property.isIdentifier() ||
140
+ (property.node.name !== "map" && property.node.name !== "flatMap")
141
+ ) {
142
+ return null;
143
+ }
144
+
145
+ const arrayObj = (callee as NodePath<t.MemberExpression>).get(
146
+ "object"
147
+ ) as NodePath<t.Expression>;
148
+ return this.tracingUtils.traceCollectionSource(arrayObj);
149
+ }
150
+
151
+ private callbackProducesJSX(
152
+ callPath: NodePath<t.CallExpression>
153
+ ): boolean {
154
+ const args = callPath.get("arguments");
155
+ const callback = args[0];
156
+ if (!callback) return false;
157
+
158
+ if (callback.isArrowFunctionExpression()) {
159
+ const body = callback.get("body");
160
+ if (body.isBlockStatement()) {
161
+ return JSXUtils.doesBlockReturnJSX(body.node);
162
+ }
163
+ return JSXUtils.producesJSX(body.node);
164
+ }
165
+
166
+ if (callback.isFunctionExpression()) {
167
+ return JSXUtils.doesBlockReturnJSX(callback.node.body);
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ private tryGetByIdInChildren(
174
+ path: NodePath<t.JSXOpeningElement>,
175
+ depth: number = 0
176
+ ): CollectionInfo | null {
177
+ if (depth > MAX_JSX_DEPTH) return null;
178
+
179
+ const jsxElement = path.parentPath;
180
+ if (!jsxElement?.isJSXElement()) return null;
181
+
182
+ const children = jsxElement.get("children");
183
+ for (const child of children) {
184
+ const info = this.checkChildForGetById(child, depth);
185
+ if (info) return info;
186
+ }
187
+
188
+ for (const attr of path.node.attributes) {
189
+ if (
190
+ this.types.isJSXAttribute(attr) &&
191
+ attr.value &&
192
+ this.types.isJSXExpressionContainer(attr.value)
193
+ ) {
194
+ const expr = attr.value.expression;
195
+ if (this.types.isMemberExpression(expr)) {
196
+ const info = this.tracingUtils.traceGetByIdSource(path);
197
+ if (info) return info;
198
+ }
199
+ }
200
+ }
201
+
202
+ return null;
203
+ }
204
+
205
+ private checkChildForGetById(
206
+ child: NodePath,
207
+ depth: number
208
+ ): CollectionInfo | null {
209
+ if (child.isJSXExpressionContainer()) {
210
+ const expr = child.get("expression");
211
+ if (
212
+ expr.isMemberExpression() ||
213
+ expr.isOptionalMemberExpression()
214
+ ) {
215
+ return this.tracingUtils.traceGetByIdSource(child);
216
+ }
217
+ }
218
+
219
+ if (child.isJSXElement()) {
220
+ const childOpening = child.get("openingElement");
221
+ return this.tryGetByIdInChildren(childOpening, depth + 1);
222
+ }
223
+
224
+ return null;
225
+ }
226
+
227
+ private tryComponentRootWithCollectionProp(
228
+ path: NodePath<t.JSXOpeningElement>
229
+ ): CollectionInfo | null {
230
+ if (!this.pathUtils.isRootReturnElement(path)) return null;
231
+
232
+ const fn = this.pathUtils.findEnclosingFunction(path);
233
+ if (!fn) return null;
234
+
235
+ let info: CollectionInfo | null = null;
236
+
237
+ fn.traverse({
238
+ CallExpression: (callPath: NodePath<t.CallExpression>) => {
239
+ if (info) return;
240
+
241
+ const callee = callPath.get("callee");
242
+ if (!callee.isMemberExpression()) return;
243
+
244
+ const prop = callee.get("property") as NodePath;
245
+ if (
246
+ !prop.isIdentifier() ||
247
+ (prop.node.name !== "map" && prop.node.name !== "flatMap")
248
+ ) {
249
+ return;
250
+ }
251
+
252
+ if (!this.callbackProducesJSX(callPath)) return;
253
+
254
+ const arrayObj = callee.get("object") as NodePath<t.Expression>;
255
+ info = this.tracingUtils.traceCollectionSource(arrayObj);
256
+ },
257
+ });
258
+
259
+ return info;
260
+ }
261
+ }