@base44-preview/vite-plugin 0.2.26-pr.42.7e00d38 → 0.2.26-pr.43.554d4c7

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 (67) hide show
  1. package/dist/consts.d.ts +12 -0
  2. package/dist/consts.d.ts.map +1 -0
  3. package/dist/consts.js +12 -0
  4. package/dist/consts.js.map +1 -0
  5. package/dist/injections/visual-edit-agent.d.ts.map +1 -1
  6. package/dist/injections/visual-edit-agent.js +10 -303
  7. package/dist/injections/visual-edit-agent.js.map +1 -1
  8. package/dist/jsx-processor.d.ts +4 -1
  9. package/dist/jsx-processor.d.ts.map +1 -1
  10. package/dist/jsx-processor.js +33 -6
  11. package/dist/jsx-processor.js.map +1 -1
  12. package/dist/jsx-utils.d.ts +9 -0
  13. package/dist/jsx-utils.d.ts.map +1 -1
  14. package/dist/jsx-utils.js +86 -0
  15. package/dist/jsx-utils.js.map +1 -1
  16. package/dist/processors/collection-id-processor.d.ts +20 -0
  17. package/dist/processors/collection-id-processor.d.ts.map +1 -0
  18. package/dist/processors/collection-id-processor.js +182 -0
  19. package/dist/processors/collection-id-processor.js.map +1 -0
  20. package/dist/processors/collection-item-field-processor.d.ts +22 -0
  21. package/dist/processors/collection-item-field-processor.d.ts.map +1 -0
  22. package/dist/processors/collection-item-field-processor.js +206 -0
  23. package/dist/processors/collection-item-field-processor.js.map +1 -0
  24. package/dist/processors/collection-item-id-processor.d.ts +12 -0
  25. package/dist/processors/collection-item-id-processor.d.ts.map +1 -0
  26. package/dist/processors/collection-item-id-processor.js +50 -0
  27. package/dist/processors/collection-item-id-processor.js.map +1 -0
  28. package/dist/processors/collection-reference-field-processor.d.ts +31 -0
  29. package/dist/processors/collection-reference-field-processor.d.ts.map +1 -0
  30. package/dist/processors/collection-reference-field-processor.js +174 -0
  31. package/dist/processors/collection-reference-field-processor.js.map +1 -0
  32. package/dist/processors/collection-tracing-utils.d.ts +31 -0
  33. package/dist/processors/collection-tracing-utils.d.ts.map +1 -0
  34. package/dist/processors/collection-tracing-utils.js +326 -0
  35. package/dist/processors/collection-tracing-utils.js.map +1 -0
  36. package/dist/processors/shared-utils.d.ts +64 -0
  37. package/dist/processors/shared-utils.d.ts.map +1 -1
  38. package/dist/processors/shared-utils.js +464 -0
  39. package/dist/processors/shared-utils.js.map +1 -1
  40. package/dist/processors/static-array-processor.d.ts +2 -3
  41. package/dist/processors/static-array-processor.d.ts.map +1 -1
  42. package/dist/processors/static-array-processor.js +2 -3
  43. package/dist/processors/static-array-processor.js.map +1 -1
  44. package/dist/statics/index.mjs +1 -5
  45. package/dist/statics/index.mjs.map +1 -1
  46. package/dist/types.d.ts +5 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/visual-edit-plugin.d.ts +0 -1
  51. package/dist/visual-edit-plugin.d.ts.map +1 -1
  52. package/dist/visual-edit-plugin.js +29 -178
  53. package/dist/visual-edit-plugin.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/consts.ts +12 -0
  56. package/src/injections/visual-edit-agent.ts +10 -369
  57. package/src/jsx-processor.ts +41 -14
  58. package/src/jsx-utils.ts +116 -0
  59. package/src/processors/collection-id-processor.ts +261 -0
  60. package/src/processors/collection-item-field-processor.ts +309 -0
  61. package/src/processors/collection-item-id-processor.ts +69 -0
  62. package/src/processors/collection-reference-field-processor.ts +225 -0
  63. package/src/processors/collection-tracing-utils.ts +436 -0
  64. package/src/processors/shared-utils.ts +595 -0
  65. package/src/processors/static-array-processor.ts +6 -3
  66. package/src/types.ts +4 -0
  67. package/src/visual-edit-plugin.ts +34 -215
@@ -11,15 +11,6 @@ export function setupVisualEditAgent() {
11
11
  let selectedOverlays: HTMLDivElement[] = [];
12
12
  let currentHighlightedElements: Element[] = [];
13
13
  let selectedElementId: string | null = null;
14
- let currentEditingElement: HTMLElement | null = null;
15
- let debouncedSendTimeout: ReturnType<typeof setTimeout> | null = null;
16
- let isInlineEditExperimentEnabled = false;
17
-
18
- const INLINE_EDIT_DEBOUNCE_MS = 500;
19
- const REPOSITION_DELAY_MS = 50;
20
-
21
- // WeakMap to track AbortControllers for each element's input listener
22
- const listenerAbortControllers = new WeakMap<HTMLElement, AbortController>();
23
14
 
24
15
  // Create overlay element
25
16
  const createOverlay = (isSelected = false): HTMLDivElement => {
@@ -78,258 +69,6 @@ export function setupVisualEditAgent() {
78
69
  }
79
70
  };
80
71
 
81
- // --- Inline editing utilities ---
82
-
83
- const injectFocusOutlineCSS = () => {
84
- const existingStyle = document.getElementById("visual-edit-focus-styles");
85
- if (existingStyle) return;
86
-
87
- const style = document.createElement("style");
88
- style.id = "visual-edit-focus-styles";
89
- style.textContent = `
90
- [data-selected="true"][contenteditable="true"]:focus {
91
- outline: none !important;
92
- }
93
- `;
94
- document.head.appendChild(style);
95
- };
96
-
97
- const removeFocusOutlineCSS = () => {
98
- const style = document.getElementById("visual-edit-focus-styles");
99
- if (style) {
100
- style.remove();
101
- }
102
- };
103
-
104
- const selectText = (element: HTMLElement) => {
105
- const range = document.createRange();
106
- range.selectNodeContents(element);
107
- const selection = window.getSelection();
108
- selection?.removeAllRanges();
109
- selection?.addRange(range);
110
- };
111
-
112
- const isEditableTextElement = (element: Element): boolean => {
113
- if (!(element instanceof HTMLElement)) {
114
- return false;
115
- }
116
-
117
- const allowedTags = [
118
- "div", "p", "h1", "h2", "h3", "h4", "h5", "h6",
119
- "span", "li", "td", "a", "button", "label",
120
- ];
121
- if (!allowedTags.includes(element.tagName.toLowerCase())) {
122
- return false;
123
- }
124
-
125
- const textContent = element.textContent?.trim() || "";
126
- if (textContent.length === 0) {
127
- return false;
128
- }
129
-
130
- if (element.querySelector("img, video, canvas, svg") !== null) {
131
- return false;
132
- }
133
-
134
- if (element.children.length > 0) {
135
- return false;
136
- }
137
-
138
- if (element.dataset.dynamicContent === "true") {
139
- return false;
140
- }
141
-
142
- return true;
143
- };
144
-
145
- const shouldEnterInlineEditingMode = (element: Element): boolean => {
146
- if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
147
- return false;
148
- }
149
- return isEditableTextElement(element);
150
- };
151
-
152
- const handleInputEvent = function (this: HTMLElement) {
153
- onTextInputChange(this);
154
- };
155
-
156
- const enterInlineEditingMode = (element: HTMLElement) => {
157
- injectFocusOutlineCSS();
158
-
159
- element.dataset.originalTextContent = element.textContent || "";
160
- element.dataset.originalCursor = element.style.cursor;
161
-
162
- element.contentEditable = "true";
163
-
164
- const abortController = new AbortController();
165
- listenerAbortControllers.set(element, abortController);
166
- element.addEventListener("input", handleInputEvent, { signal: abortController.signal });
167
-
168
- element.style.cursor = "text";
169
- selectText(element);
170
-
171
- setTimeout(() => {
172
- element.focus();
173
- }, 0);
174
- };
175
-
176
- const clearInlineEditingMode = (element: HTMLElement) => {
177
- const abortController = listenerAbortControllers.get(element);
178
- if (abortController) {
179
- abortController.abort();
180
- listenerAbortControllers.delete(element);
181
- }
182
-
183
- if (!element.isConnected) {
184
- return;
185
- }
186
-
187
- removeFocusOutlineCSS();
188
- element.contentEditable = "false";
189
- delete element.dataset.originalTextContent;
190
-
191
- if (element.dataset.originalCursor !== undefined) {
192
- element.style.cursor = element.dataset.originalCursor;
193
- delete element.dataset.originalCursor;
194
- }
195
- };
196
-
197
- const repositionSelectedOverlays = () => {
198
- if (selectedElementId) {
199
- const elements = findElementsById(selectedElementId);
200
- selectedOverlays.forEach((overlay, index) => {
201
- if (index < elements.length) {
202
- positionOverlay(overlay, elements[index]!);
203
- }
204
- });
205
- }
206
- };
207
-
208
- const handleEnterInlineEditingMode = (element: HTMLElement) => {
209
- currentEditingElement = element;
210
-
211
- selectedOverlays.forEach((overlay) => {
212
- overlay.style.display = "none";
213
- });
214
-
215
- enterInlineEditingMode(element);
216
-
217
- window.parent.postMessage(
218
- {
219
- type: "content-editing-started",
220
- visualSelectorId: selectedElementId,
221
- },
222
- "*"
223
- );
224
- };
225
-
226
- const handleClearInlineEditingMode = () => {
227
- if (!currentEditingElement) return;
228
-
229
- if (debouncedSendTimeout) {
230
- clearTimeout(debouncedSendTimeout);
231
- debouncedSendTimeout = null;
232
- }
233
-
234
- const element = currentEditingElement;
235
- clearInlineEditingMode(element);
236
-
237
- selectedOverlays.forEach((overlay) => {
238
- overlay.style.display = "";
239
- });
240
-
241
- repositionSelectedOverlays();
242
-
243
- window.parent.postMessage(
244
- {
245
- type: "content-editing-ended",
246
- visualSelectorId: selectedElementId,
247
- },
248
- "*"
249
- );
250
-
251
- currentEditingElement = null;
252
- };
253
-
254
- const reportInlineEdit = (element: HTMLElement) => {
255
- const originalContent = element.dataset.originalTextContent;
256
- const newContent = element.textContent;
257
-
258
- const svgElement = element as unknown as SVGElement;
259
- const elementInfo = {
260
- tagName: element.tagName,
261
- classes:
262
- (svgElement.className as unknown as SVGAnimatedString)?.baseVal ||
263
- element.className ||
264
- "",
265
- visualSelectorId: selectedElementId,
266
- content: newContent,
267
- dataSourceLocation: element.dataset.sourceLocation,
268
- isDynamicContent: element.dataset.dynamicContent === "true",
269
- linenumber: element.dataset.linenumber,
270
- filename: element.dataset.filename,
271
- position: (() => {
272
- const rect = element.getBoundingClientRect();
273
- return {
274
- top: rect.top,
275
- left: rect.left,
276
- right: rect.right,
277
- bottom: rect.bottom,
278
- width: rect.width,
279
- height: rect.height,
280
- centerX: rect.left + rect.width / 2,
281
- centerY: rect.top + rect.height / 2,
282
- };
283
- })(),
284
- };
285
-
286
- window.parent.postMessage(
287
- {
288
- type: "inline-edit",
289
- elementInfo,
290
- originalContent,
291
- newContent,
292
- },
293
- "*"
294
- );
295
-
296
- element.dataset.originalTextContent = newContent || "";
297
- };
298
-
299
- const debouncedSendInlineEditMessage = (element: HTMLElement) => {
300
- if (debouncedSendTimeout) {
301
- clearTimeout(debouncedSendTimeout);
302
- }
303
-
304
- debouncedSendTimeout = setTimeout(() => {
305
- reportInlineEdit(element);
306
- }, INLINE_EDIT_DEBOUNCE_MS);
307
- };
308
-
309
- const onTextInputChange = (element: HTMLElement) => {
310
- repositionSelectedOverlays();
311
- debouncedSendInlineEditMessage(element);
312
- };
313
-
314
- const clearSelection = () => {
315
- if (selectedElementId) {
316
- const elements = findElementsById(selectedElementId);
317
- elements.forEach((el) => {
318
- if (el instanceof HTMLElement) {
319
- delete el.dataset.selected;
320
- }
321
- });
322
- }
323
-
324
- selectedOverlays.forEach((overlay) => {
325
- if (overlay && overlay.parentNode) {
326
- overlay.remove();
327
- }
328
- });
329
- selectedOverlays = [];
330
- selectedElementId = null;
331
- };
332
-
333
72
  // Clear hover overlays
334
73
  const clearHoverOverlays = () => {
335
74
  hoverOverlays.forEach((overlay) => {
@@ -415,9 +154,6 @@ export function setupVisualEditAgent() {
415
154
  const handleMouseOver = (e: MouseEvent) => {
416
155
  if (!isVisualEditMode || isPopoverDragging) return;
417
156
 
418
- // Skip hover effects when inline editing
419
- if (currentEditingElement) return;
420
-
421
157
  const target = e.target as Element;
422
158
 
423
159
  // Prevent hover effects when a dropdown is open
@@ -485,20 +221,6 @@ export function setupVisualEditAgent() {
485
221
  // Let layer dropdown clicks pass through without interference
486
222
  if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`)) return;
487
223
 
488
- // Allow normal editing when clicking inside a contentEditable element
489
- if (isInlineEditExperimentEnabled && target instanceof HTMLElement && target.contentEditable === "true") {
490
- return;
491
- }
492
-
493
- // If currently editing, clicking outside should exit editing mode
494
- if (currentEditingElement) {
495
- e.preventDefault();
496
- e.stopPropagation();
497
- e.stopImmediatePropagation();
498
- handleClearInlineEditingMode();
499
- return;
500
- }
501
-
502
224
  // Close dropdowns when clicking anywhere in iframe if a dropdown is open
503
225
  if (isDropdownOpen) {
504
226
  e.preventDefault();
@@ -527,45 +249,14 @@ export function setupVisualEditAgent() {
527
249
  return;
528
250
  }
529
251
 
530
- const htmlElement = element as HTMLElement;
531
- const visualSelectorId = getElementSelectorId(element);
532
-
533
- // Check if this element is already selected (second click scenario)
534
- const isAlreadySelected =
535
- selectedElementId === visualSelectorId &&
536
- htmlElement.dataset.selected === "true";
537
-
538
- if (isAlreadySelected) {
539
- if (isInlineEditExperimentEnabled && shouldEnterInlineEditingMode(htmlElement)) {
540
- handleEnterInlineEditingMode(htmlElement);
541
- return;
542
- }
543
- }
544
-
545
- // Select the element, mark for inline editing, and attach layer dropdown
546
- if (currentEditingElement) {
547
- handleClearInlineEditingMode();
548
- }
549
-
550
- if (isInlineEditExperimentEnabled) {
551
- const elements = findElementsById(visualSelectorId);
552
- elements.forEach((el) => {
553
- if (el instanceof HTMLElement) {
554
- el.dataset.selected = "true";
555
- }
556
- });
557
- }
558
-
559
252
  const selectedOverlay = selectElement(element);
560
253
  layerController.attachToOverlay(selectedOverlay, element);
561
254
  };
562
255
 
563
- // Unselect the current element
564
- const unselectElement = () => {
565
- if (currentEditingElement) {
566
- handleClearInlineEditingMode();
567
- }
568
- clearSelection();
256
+ // Clear the current selection
257
+ const clearSelection = () => {
258
+ clearSelectedOverlays();
259
+ selectedElementId = null;
569
260
  };
570
261
 
571
262
  const updateElementClassesAndReposition = (visualSelectorId: string, classes: string) => {
@@ -597,7 +288,7 @@ export function setupVisualEditAgent() {
597
288
  });
598
289
  }
599
290
  }
600
- }, REPOSITION_DELAY_MS);
291
+ }, 50);
601
292
  };
602
293
 
603
294
  // Update element attribute by visual selector ID
@@ -620,7 +311,7 @@ export function setupVisualEditAgent() {
620
311
  }
621
312
  });
622
313
  }
623
- }, REPOSITION_DELAY_MS);
314
+ }, 50);
624
315
  };
625
316
 
626
317
  // Update element content by visual selector ID
@@ -643,7 +334,7 @@ export function setupVisualEditAgent() {
643
334
  }
644
335
  });
645
336
  }
646
- }, REPOSITION_DELAY_MS);
337
+ }, 50);
647
338
  };
648
339
 
649
340
  // --- Layer dropdown controller ---
@@ -665,15 +356,12 @@ export function setupVisualEditAgent() {
665
356
  isVisualEditMode = isEnabled;
666
357
 
667
358
  if (!isEnabled) {
668
- if (currentEditingElement) {
669
- handleClearInlineEditingMode();
670
- }
671
- clearSelection();
672
359
  layerController.cleanup();
673
360
  clearHoverOverlays();
674
361
  clearSelectedOverlays();
675
362
 
676
363
  currentHighlightedElements = [];
364
+ selectedElementId = null;
677
365
  document.body.style.cursor = "default";
678
366
 
679
367
  document.removeEventListener("mouseover", handleMouseOver);
@@ -734,9 +422,6 @@ export function setupVisualEditAgent() {
734
422
  switch (message.type) {
735
423
  case "toggle-visual-edit-mode":
736
424
  toggleVisualEditMode(message.data.enabled);
737
- if (message.data.specs?.newInlineEditEnabled !== undefined) {
738
- isInlineEditExperimentEnabled = message.data.specs.newInlineEditEnabled;
739
- }
740
425
  break;
741
426
 
742
427
  case "update-classes":
@@ -774,7 +459,7 @@ export function setupVisualEditAgent() {
774
459
  break;
775
460
 
776
461
  case "unselect-element":
777
- unselectElement();
462
+ clearSelection();
778
463
  break;
779
464
 
780
465
  case "refresh-page":
@@ -852,50 +537,6 @@ export function setupVisualEditAgent() {
852
537
  }
853
538
  break;
854
539
 
855
- case "toggle-inline-edit-mode":
856
- if (!isInlineEditExperimentEnabled) break;
857
- if (!message.data || !message.data.dataSourceLocation) break;
858
-
859
- {
860
- const elements = findElementsById(message.data.dataSourceLocation);
861
- if (elements.length === 0 || !(elements[0] instanceof HTMLElement)) break;
862
-
863
- const element = elements[0];
864
-
865
- if (message.data.inlineEditingMode) {
866
- if (shouldEnterInlineEditingMode(element)) {
867
- // Select the element first if not already selected
868
- if (selectedElementId !== message.data.dataSourceLocation) {
869
- if (currentEditingElement) {
870
- handleClearInlineEditingMode();
871
- }
872
- clearSelection();
873
-
874
- elements.forEach((el) => {
875
- if (el instanceof HTMLElement) {
876
- el.dataset.selected = "true";
877
- }
878
- });
879
-
880
- elements.forEach((el) => {
881
- const overlay = createOverlay(true);
882
- document.body.appendChild(overlay);
883
- selectedOverlays.push(overlay);
884
- positionOverlay(overlay, el, true);
885
- });
886
-
887
- selectedElementId = message.data.dataSourceLocation;
888
- }
889
- handleEnterInlineEditingMode(element);
890
- }
891
- } else {
892
- if (currentEditingElement === element) {
893
- handleClearInlineEditingMode();
894
- }
895
- }
896
- }
897
- break;
898
-
899
540
  default:
900
541
  break;
901
542
  }
@@ -960,7 +601,7 @@ export function setupVisualEditAgent() {
960
601
  });
961
602
 
962
603
  if (needsUpdate) {
963
- setTimeout(handleResize, REPOSITION_DELAY_MS);
604
+ setTimeout(handleResize, 50);
964
605
  }
965
606
  });
966
607
 
@@ -1,14 +1,26 @@
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 { ReferenceFieldProcessor } from "./processors/collection-reference-field-processor.js";
5
+ import { DataItemIdProcessor } from "./processors/collection-item-id-processor.js";
6
+ import { DataItemFieldProcessor } from "./processors/collection-item-field-processor.js";
3
7
  import { StaticArrayProcessor } from "./processors/static-array-processor.js";
4
8
 
5
9
  export class JSXProcessor {
10
+ private collectionIdProcessor: CollectionIdProcessor;
11
+ private referenceFieldProcessor: ReferenceFieldProcessor;
12
+ private dataItemIdProcessor: DataItemIdProcessor;
13
+ private dataItemFieldProcessor: DataItemFieldProcessor;
6
14
  private staticArrayProcessor: StaticArrayProcessor;
7
15
 
8
16
  constructor(
9
17
  private types: typeof t,
10
18
  private filename: string
11
19
  ) {
20
+ this.collectionIdProcessor = new CollectionIdProcessor(types);
21
+ this.referenceFieldProcessor = new ReferenceFieldProcessor(types);
22
+ this.dataItemIdProcessor = new DataItemIdProcessor(types);
23
+ this.dataItemFieldProcessor = new DataItemFieldProcessor(types);
12
24
  this.staticArrayProcessor = new StaticArrayProcessor(types);
13
25
  }
14
26
 
@@ -17,8 +29,13 @@ export class JSXProcessor {
17
29
 
18
30
  this.addSourceLocationAttribute(path);
19
31
  this.addDynamicContentAttribute(path);
20
- this.addContentEditableAttribute(path);
32
+
33
+ this.collectionIdProcessor.process(path);
34
+ this.referenceFieldProcessor.process(path);
35
+ this.dataItemIdProcessor.process(path);
36
+ this.dataItemFieldProcessor.process(path);
21
37
  this.staticArrayProcessor.process(path);
38
+
22
39
  }
23
40
 
24
41
  private addSourceLocationAttribute(
@@ -27,7 +44,7 @@ export class JSXProcessor {
27
44
  const { line, column } = path.node.loc?.start || { line: 1, column: 0 };
28
45
  const value = `${this.filename}:${line}:${column}`;
29
46
 
30
- path.node.attributes.push(
47
+ path.node.attributes.unshift(
31
48
  this.types.jsxAttribute(
32
49
  this.types.jsxIdentifier("data-source-location"),
33
50
  this.types.stringLiteral(value)
@@ -45,21 +62,19 @@ export class JSXProcessor {
45
62
  parentElement.node as t.JSXElement
46
63
  );
47
64
 
48
- path.node.attributes.push(
49
- this.types.jsxAttribute(
50
- this.types.jsxIdentifier("data-dynamic-content"),
51
- this.types.stringLiteral(isDynamic ? "true" : "false")
52
- )
65
+ const sourceLocIdx = path.node.attributes.findIndex(
66
+ (attr) =>
67
+ this.types.isJSXAttribute(attr) &&
68
+ this.types.isJSXIdentifier(attr.name) &&
69
+ attr.name.name === "data-source-location"
53
70
  );
54
- }
55
71
 
56
- private addContentEditableAttribute(
57
- path: NodePath<t.JSXOpeningElement>
58
- ): void {
59
- path.node.attributes.push(
72
+ path.node.attributes.splice(
73
+ sourceLocIdx + 1,
74
+ 0,
60
75
  this.types.jsxAttribute(
61
- this.types.jsxIdentifier("content-editable"),
62
- this.types.stringLiteral("true")
76
+ this.types.jsxIdentifier("data-dynamic-content"),
77
+ this.types.stringLiteral(isDynamic ? "true" : "false")
63
78
  )
64
79
  );
65
80
  }
@@ -137,6 +152,18 @@ export class JSXProcessor {
137
152
  }
138
153
  };
139
154
 
155
+ const attributes = jsxElement.openingElement?.attributes || [];
156
+ for (const attr of attributes) {
157
+ if (hasDynamicContent) break;
158
+ if (this.types.isJSXSpreadAttribute(attr)) {
159
+ hasDynamicContent = true;
160
+ break;
161
+ }
162
+ if (this.types.isJSXAttribute(attr) && attr.value) {
163
+ traverseNode(attr.value as unknown as Record<string, unknown>);
164
+ }
165
+ }
166
+
140
167
  for (const child of jsxElement.children) {
141
168
  if (hasDynamicContent) break;
142
169
  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
  }