@base44/vite-plugin 0.2.20 → 0.2.22
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.
- package/README.md +96 -0
- package/dist/bridge.d.ts +8 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +8 -0
- package/dist/bridge.js.map +1 -0
- package/dist/html-injections-plugin.d.ts +2 -1
- package/dist/html-injections-plugin.d.ts.map +1 -1
- package/dist/html-injections-plugin.js +123 -55
- package/dist/html-injections-plugin.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/injections/visual-edit-agent.d.ts +1 -1
- package/dist/injections/visual-edit-agent.d.ts.map +1 -1
- package/dist/injections/visual-edit-agent.js +1 -1
- package/dist/injections/visual-edit-agent.js.map +1 -1
- package/dist/statics/index.mjs +2 -0
- package/dist/statics/index.mjs.map +1 -0
- package/dist/visual-edit-plugin.d.ts +1 -0
- package/dist/visual-edit-plugin.d.ts.map +1 -1
- package/dist/visual-edit-plugin.js +16 -1
- package/dist/visual-edit-plugin.js.map +1 -1
- package/package.json +8 -2
- package/src/bridge.ts +8 -0
- package/src/html-injections-plugin.ts +148 -55
- package/src/index.ts +4 -1
- package/src/injections/visual-edit-agent.ts +2 -2
- package/src/visual-edit-plugin.md +358 -0
- package/src/visual-edit-plugin.ts +18 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# visual-edit-plugin.ts - Deep Dive
|
|
2
|
+
|
|
3
|
+
## What Is This?
|
|
4
|
+
|
|
5
|
+
A **Vite plugin** that instruments every JSX element in React components with metadata attributes at build time. This enables a **live visual editing** experience for React apps running inside sandboxed iframes.
|
|
6
|
+
|
|
7
|
+
It adds two HTML `data-*` attributes to every JSX element:
|
|
8
|
+
- `data-source-location` - maps the element back to its source file, line, and column
|
|
9
|
+
- `data-dynamic-content` - flags whether the element contains runtime-generated content
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
// BEFORE (source code):
|
|
13
|
+
<Button className="px-4">Click me</Button>
|
|
14
|
+
|
|
15
|
+
// AFTER (served to browser):
|
|
16
|
+
<Button
|
|
17
|
+
data-source-location="components/Button:42:8"
|
|
18
|
+
data-dynamic-content="false"
|
|
19
|
+
className="px-4"
|
|
20
|
+
>Click me</Button>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## When Does It Run?
|
|
26
|
+
|
|
27
|
+
### Activation Conditions (ALL must be true)
|
|
28
|
+
|
|
29
|
+
| Condition | Where | Why |
|
|
30
|
+
|-----------|-------|-----|
|
|
31
|
+
| `MODAL_SANDBOX_ID` env var is set | `index.ts:8` | Only runs inside Modal sandbox containers |
|
|
32
|
+
| Vite mode is `"development"` | `apply: (config) => config.mode === "development"` | No overhead in production builds |
|
|
33
|
+
| File is `.js`, `.jsx`, `.ts`, or `.tsx` | `id.match(/\.(jsx?\|tsx?)$/)` | Only JSX-capable files |
|
|
34
|
+
| File is NOT in `node_modules` | `id.includes("node_modules")` | Skip third-party code |
|
|
35
|
+
| File is NOT `visual-edit-agent` | `id.includes("visual-edit-agent")` | Don't instrument the agent itself |
|
|
36
|
+
|
|
37
|
+
### Execution Timeline
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
1. npm run dev
|
|
41
|
+
2. Vite starts, loads plugin config
|
|
42
|
+
3. index.ts checks: isRunningInSandbox = !!process.env.MODAL_SANDBOX_ID
|
|
43
|
+
4. If true → registers visualEditPlugin() in the plugin pipeline
|
|
44
|
+
5. Vite dev server starts
|
|
45
|
+
├── transformIndexHtml runs ONCE → injects Tailwind CDN into <head>
|
|
46
|
+
└── transform runs PER FILE when browser requests it
|
|
47
|
+
├── Babel parses code → AST
|
|
48
|
+
├── Traverse finds all JSXElement nodes
|
|
49
|
+
├── Adds data-source-location + data-dynamic-content attributes
|
|
50
|
+
├── Babel generates modified code
|
|
51
|
+
└── Serves transformed code to browser
|
|
52
|
+
6. Browser renders components with data-* attributes in the DOM
|
|
53
|
+
7. sandbox-mount-observer.js detects instrumented elements → notifies parent
|
|
54
|
+
8. visual-edit-agent loads → enables hover/click/edit interactions
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Plugin Ordering
|
|
58
|
+
|
|
59
|
+
- `enforce: "pre"` + `order: "pre"` ensures this runs **before** all other Vite transforms
|
|
60
|
+
- This is critical because the plugin needs to process raw JSX before any other plugin modifies it
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## How It Works - The Transform Logic
|
|
65
|
+
|
|
66
|
+
### Step 1: Filename Extraction (lines 123-173)
|
|
67
|
+
|
|
68
|
+
Builds a human-readable source path from the full file path:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
/Users/dev/project/src/pages/About/index.tsx → "pages/About/index"
|
|
72
|
+
/Users/dev/project/src/components/ui/Button.tsx → "components/ui/Button"
|
|
73
|
+
/Users/dev/project/src/Layout.tsx → "Layout"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Rules:**
|
|
77
|
+
- Files under `/pages/` → preserves `pages/...` prefix with nested structure
|
|
78
|
+
- Files under `/components/` → preserves `components/...` prefix with nested structure
|
|
79
|
+
- All other files → just the filename (no directory prefix)
|
|
80
|
+
- File extensions are always stripped
|
|
81
|
+
|
|
82
|
+
### Step 2: AST Parsing (lines 177-196)
|
|
83
|
+
|
|
84
|
+
Uses `@babel/parser` with 15 syntax plugins to handle any modern TypeScript/React code:
|
|
85
|
+
|
|
86
|
+
| Plugin | Handles |
|
|
87
|
+
|--------|---------|
|
|
88
|
+
| `jsx` | JSX syntax (`<Component />`) |
|
|
89
|
+
| `typescript` | TypeScript type annotations |
|
|
90
|
+
| `decorators-legacy` | `@decorator` syntax |
|
|
91
|
+
| `classProperties` | Class field declarations |
|
|
92
|
+
| `objectRestSpread` | `...spread` syntax |
|
|
93
|
+
| `optionalChaining` | `?.` operator |
|
|
94
|
+
| `nullishCoalescingOperator` | `??` operator |
|
|
95
|
+
| `dynamicImport` | `import()` expressions |
|
|
96
|
+
| `asyncGenerators` | `async function*` |
|
|
97
|
+
| `bigInt` | `123n` literals |
|
|
98
|
+
| `optionalCatchBinding` | `catch {}` without param |
|
|
99
|
+
| `throwExpressions` | `throw` as expression |
|
|
100
|
+
| `functionBind` | `::` bind operator |
|
|
101
|
+
| `exportDefaultFrom` | `export v from "mod"` |
|
|
102
|
+
| `exportNamespaceFrom` | `export * as ns from "mod"` |
|
|
103
|
+
|
|
104
|
+
**Why Babel over regex?** AST parsing is accurate and won't break on edge cases like JSX-in-strings or commented-out JSX.
|
|
105
|
+
|
|
106
|
+
### Step 3: AST Traversal (lines 199-246)
|
|
107
|
+
|
|
108
|
+
Visits every `JSXElement` node. For each one:
|
|
109
|
+
|
|
110
|
+
1. **Skip JSX Fragments** (`<>...</>`) - they have no opening tag to attach attributes to
|
|
111
|
+
2. **Skip already-instrumented elements** - checks if `data-source-location` already exists (idempotent)
|
|
112
|
+
3. **Extract location** from AST node: `openingElement.loc.start` gives `{ line, column }`
|
|
113
|
+
4. **Detect dynamic content** via `checkIfElementHasDynamicContent()` (see below)
|
|
114
|
+
5. **Insert BOTH attributes** at position 0 of the attributes array (appear first in output)
|
|
115
|
+
|
|
116
|
+
**`data-dynamic-content` is added to EVERY JSX element unconditionally** — it is always present alongside `data-source-location`. The only thing that changes is the value: `"true"` if the element's own attributes OR children contain any dynamic patterns (expressions, props, function calls, spread attributes, etc.), or `"false"` if everything is purely static. This means every element in the DOM carries both attributes, so the visual editor can always check any element's dynamic status without extra logic.
|
|
117
|
+
|
|
118
|
+
### Step 4: Dynamic Content Detection (lines 8-98)
|
|
119
|
+
|
|
120
|
+
`checkIfElementHasDynamicContent(jsxElement)` determines whether to set `data-dynamic-content` to `"true"` or `"false"`.
|
|
121
|
+
|
|
122
|
+
#### Two-phase detection: attributes first, then children
|
|
123
|
+
|
|
124
|
+
The function checks in two phases:
|
|
125
|
+
|
|
126
|
+
1. **Own attributes** (lines 91-106): Iterates `jsxElement.openingElement.attributes`. Spread attributes (`{...props}`) are always dynamic. For regular attributes, the attribute value is checked via `traverseNode` — so `src={photo.url}` is detected as dynamic (JSXExpressionContainer with a non-literal expression), while `src="static.png"` is not (StringLiteral).
|
|
127
|
+
|
|
128
|
+
2. **Children** (lines 108-112): Iterates `jsxElement.children` and recursively traverses all descendants.
|
|
129
|
+
|
|
130
|
+
Both phases use early exit — if the attribute check already found dynamic content, the children check is skipped entirely.
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
// "true" — src={url} is a dynamic OWN attribute
|
|
134
|
+
<img src={url} />
|
|
135
|
+
|
|
136
|
+
// "true" — {name} is a dynamic CHILD
|
|
137
|
+
<div>{name}</div>
|
|
138
|
+
|
|
139
|
+
// "true" — spread attribute is always dynamic
|
|
140
|
+
<Component {...props} />
|
|
141
|
+
|
|
142
|
+
// "false" — static attribute + static child
|
|
143
|
+
<div className="box">Hello</div>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### What counts as dynamic (the `checkNodeForDynamicContent` function)
|
|
147
|
+
|
|
148
|
+
Each attribute value and child node (and recursively all descendants) is checked against these patterns in order:
|
|
149
|
+
|
|
150
|
+
| # | Check | Matches | Example |
|
|
151
|
+
|---|-------|---------|---------|
|
|
152
|
+
| 1 | `isJSXExpressionContainer` + expression is NOT a literal | Any `{expression}` where the expression isn't a string/number/boolean literal | `{variable}`, `{obj.prop}`, `{fn()}` |
|
|
153
|
+
| 2 | `isTemplateLiteral` with `expressions.length > 0` | Template literals with interpolation | `` `Hello ${name}` `` |
|
|
154
|
+
| 3 | `isMemberExpression` | Property access | `props.title`, `state.value` |
|
|
155
|
+
| 4 | `isCallExpression` | Function/method calls | `getData()`, `arr.map()` |
|
|
156
|
+
| 5 | `isConditionalExpression` | Ternary operators | `x ? "a" : "b"` |
|
|
157
|
+
| 6 | `isIdentifier` + name contains keyword | Identifiers whose name includes (case-sensitive substring match): `props`, `state`, `data`, `item`, `value`, `text`, `content` | `dataItems` matches, but `userData` does NOT (capital D) |
|
|
158
|
+
|
|
159
|
+
Check #1 is the main workhorse — it catches most dynamic patterns because in JSX, dynamic values are always wrapped in `{...}`.
|
|
160
|
+
|
|
161
|
+
Checks #3-#6 catch patterns found during **deep recursion** into nested AST structures.
|
|
162
|
+
|
|
163
|
+
#### The recursive traversal
|
|
164
|
+
|
|
165
|
+
`traverseNode` (line 69) recursively visits ALL properties of each AST node (`Object.keys(node).forEach`). This means once a node is passed to `traverseNode`, it checks every nested structure — child elements, their attributes, their children, etc.
|
|
166
|
+
|
|
167
|
+
Both the element's own attributes AND its children tree are checked consistently:
|
|
168
|
+
|
|
169
|
+
```jsx
|
|
170
|
+
// "true" — div's OWN dynamic attribute is checked
|
|
171
|
+
<div className={styles.foo}>Hello</div>
|
|
172
|
+
|
|
173
|
+
// "true" — recursion also reaches nested span's attributes
|
|
174
|
+
<div><span className={styles.foo}>text</span></div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Concrete examples
|
|
178
|
+
|
|
179
|
+
```jsx
|
|
180
|
+
// "true" — {name} is a non-literal expression in children
|
|
181
|
+
<h1>{name}</h1>
|
|
182
|
+
|
|
183
|
+
// "false" — {"Hello"} is a literal expression (string literal)
|
|
184
|
+
<h1>{"Hello"}</h1>
|
|
185
|
+
|
|
186
|
+
// "false" — {} is an empty expression (JSXEmptyExpression)
|
|
187
|
+
<div>{}</div>
|
|
188
|
+
|
|
189
|
+
// "false" — {42} is a literal expression (numeric literal)
|
|
190
|
+
<span>{42}</span>
|
|
191
|
+
|
|
192
|
+
// "true" — {user.name} is a MemberExpression (non-literal)
|
|
193
|
+
<p>{user.name}</p>
|
|
194
|
+
|
|
195
|
+
// "true" — {getLabel()} is a CallExpression (non-literal)
|
|
196
|
+
<button>{getLabel()}</button>
|
|
197
|
+
|
|
198
|
+
// "true" — ternary is a ConditionalExpression (non-literal)
|
|
199
|
+
<span>{active ? "On" : "Off"}</span>
|
|
200
|
+
|
|
201
|
+
// "true" — `Hello ${name}` is a TemplateLiteral with expressions
|
|
202
|
+
<div>{`Hello ${name}`}</div>
|
|
203
|
+
|
|
204
|
+
// "true" — src={imageUrl} is a dynamic own attribute
|
|
205
|
+
<img src={imageUrl} />
|
|
206
|
+
|
|
207
|
+
// "true" — spread attribute is always dynamic
|
|
208
|
+
<Component {...props} />
|
|
209
|
+
|
|
210
|
+
// "true" — recursion also reaches nested img's src attribute
|
|
211
|
+
<div><img src={imageUrl} /></div>
|
|
212
|
+
|
|
213
|
+
// "false" — no children, no dynamic attributes
|
|
214
|
+
<br />
|
|
215
|
+
|
|
216
|
+
// "true" — deeply nested dynamic content found
|
|
217
|
+
<div><ul><li>{item.name}</li></ul></div>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Early exit optimization
|
|
221
|
+
|
|
222
|
+
The function stops as soon as any dynamic pattern is found (line 71: `hasDynamicContent = true; return;` and line 93: `if (hasDynamicContent) return;`).
|
|
223
|
+
|
|
224
|
+
### Step 5: Code Generation (lines 249-258)
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
generate.default(ast, {
|
|
228
|
+
compact: false, // Don't minify
|
|
229
|
+
concise: false, // Keep readable formatting
|
|
230
|
+
retainLines: true, // Preserve original line numbers
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`retainLines: true` is critical - it ensures the generated code has the same line numbers as the original, so the `data-source-location` values remain accurate and debugger breakpoints still work.
|
|
235
|
+
|
|
236
|
+
### Error Handling (lines 259-265)
|
|
237
|
+
|
|
238
|
+
If parsing or transformation fails, the plugin **returns the original code unchanged**. Errors are logged but never break the build.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Why Do We Need This?
|
|
243
|
+
|
|
244
|
+
### Problem 1: Source Code Mapping in Iframes
|
|
245
|
+
|
|
246
|
+
In a sandboxed iframe, clicking a rendered element doesn't tell you which React component file created it. The `data-source-location` attribute creates a direct link from the rendered DOM back to the source code.
|
|
247
|
+
|
|
248
|
+
### Problem 2: Dynamic vs. Static Content Awareness
|
|
249
|
+
|
|
250
|
+
A visual editor needs to know:
|
|
251
|
+
- **Static elements** (`data-dynamic-content="false"`) → safe to edit directly, changes will persist
|
|
252
|
+
- **Dynamic elements** (`data-dynamic-content="true"`) → content comes from variables/props/state, editing the DOM will be overwritten on next render
|
|
253
|
+
|
|
254
|
+
This prevents users from wasting time editing elements that will reset.
|
|
255
|
+
|
|
256
|
+
### Problem 3: Cross-Window Communication
|
|
257
|
+
|
|
258
|
+
The parent window (editor UI) and the sandbox iframe can't share a DOM. The `data-*` attributes serve as a **stable identifier system** that both sides understand:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
Parent Window (Editor UI) iframe Sandbox (React App)
|
|
262
|
+
───────────────────────── ──────────────────────────
|
|
263
|
+
"Select element components/Button:42:8" → querySelector('[data-source-location="components/Button:42:8"]')
|
|
264
|
+
← "Element found: classes='px-4', isDynamic=false"
|
|
265
|
+
"Update classes to 'px-6 py-3'" → element.setAttribute("class", "px-6 py-3")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Problem 4: Multi-Instance Elements
|
|
269
|
+
|
|
270
|
+
React can render the same component multiple times (e.g., list items). The `findElementsById()` utility finds ALL elements with the same `data-source-location`, enabling bulk updates across all instances.
|
|
271
|
+
|
|
272
|
+
### Problem 5: Tailwind CSS Support
|
|
273
|
+
|
|
274
|
+
The `transformIndexHtml` hook injects the Tailwind CSS CDN, enabling visual editing tools to apply Tailwind utility classes to elements in real time.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Full System Architecture
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
┌──────────────────────────────┐
|
|
282
|
+
│ Parent Window (Editor) │
|
|
283
|
+
│ │
|
|
284
|
+
│ - Shows element inspector │
|
|
285
|
+
│ - Provides class editor │
|
|
286
|
+
│ - Shows source location │
|
|
287
|
+
│ - Content editing UI │
|
|
288
|
+
│ - Attribute editing UI │
|
|
289
|
+
└──────────┬───────────────────┘
|
|
290
|
+
│ postMessage (both ways)
|
|
291
|
+
│
|
|
292
|
+
┌────────────────────┴───────────────────────┐
|
|
293
|
+
│ iframe Sandbox │
|
|
294
|
+
│ │
|
|
295
|
+
│ ┌───────────────────────────────────────┐ │
|
|
296
|
+
│ │ visual-edit-agent.ts (runtime) │ │
|
|
297
|
+
│ │ - Hover/click event handlers │ │
|
|
298
|
+
│ │ - Overlay positioning │ │
|
|
299
|
+
│ │ - DOM query & updates │ │
|
|
300
|
+
│ │ - postMessage bridge │ │
|
|
301
|
+
│ └───────────────────────────────────────┘ │
|
|
302
|
+
│ ▲ │
|
|
303
|
+
│ │ reads data-* attributes │
|
|
304
|
+
│ ┌───────────────────────────────────────┐ │
|
|
305
|
+
│ │ React App (DOM) │ │
|
|
306
|
+
│ │ │ │
|
|
307
|
+
│ │ <div data-source-location= │ │
|
|
308
|
+
│ │ "pages/Home:10:4" │ │
|
|
309
|
+
│ │ data-dynamic-content="true"> │ │
|
|
310
|
+
│ │ {greeting} │ │
|
|
311
|
+
│ │ </div> │ │
|
|
312
|
+
│ └───────────────────────────────────────┘ │
|
|
313
|
+
│ ▲ │
|
|
314
|
+
│ │ build-time transform │
|
|
315
|
+
│ ┌───────────────────────────────────────┐ │
|
|
316
|
+
│ │ visual-edit-plugin.ts (Vite plugin) │ │
|
|
317
|
+
│ │ - Parses JSX with Babel │ │
|
|
318
|
+
│ │ - Injects data-* attributes │ │
|
|
319
|
+
│ │ - Detects dynamic content │ │
|
|
320
|
+
│ │ - Injects Tailwind CDN │ │
|
|
321
|
+
│ └───────────────────────────────────────┘ │
|
|
322
|
+
└─────────────────────────────────────────────┘
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Messages: Parent → iframe
|
|
326
|
+
|
|
327
|
+
| Message Type | Purpose |
|
|
328
|
+
|-------------|---------|
|
|
329
|
+
| `toggle-visual-edit-mode` | Enable/disable visual editing cursor |
|
|
330
|
+
| `update-classes` | Change CSS classes on selected element |
|
|
331
|
+
| `update-content` | Change text content of selected element |
|
|
332
|
+
| `update-attribute` | Change an attribute (e.g., `src`) on selected element |
|
|
333
|
+
| `unselect-element` | Remove selection overlay |
|
|
334
|
+
| `refresh-page` | Reload the iframe |
|
|
335
|
+
| `request-element-position` | Ask for current element coordinates |
|
|
336
|
+
|
|
337
|
+
### Messages: iframe → Parent
|
|
338
|
+
|
|
339
|
+
| Message Type | Purpose |
|
|
340
|
+
|-------------|---------|
|
|
341
|
+
| `element-selected` | User clicked an element - sends full element info |
|
|
342
|
+
| `element-position-update` | Element moved (scroll/resize) - updated coordinates |
|
|
343
|
+
| `visual-edit-agent-ready` | Agent loaded and listening |
|
|
344
|
+
| `sandbox:onMounted` | Instrumented elements appeared in DOM |
|
|
345
|
+
| `sandbox:onUnmounted` | No instrumented elements found |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Related Files
|
|
350
|
+
|
|
351
|
+
| File | Role |
|
|
352
|
+
|------|------|
|
|
353
|
+
| `src/index.ts` | Registers the plugin (conditionally, in sandbox mode) |
|
|
354
|
+
| `src/html-injections-plugin.ts` | Injects agent scripts and observer scripts into HTML |
|
|
355
|
+
| `src/injections/visual-edit-agent.ts` | Runtime agent in the iframe (hover, click, overlay, postMessage) |
|
|
356
|
+
| `src/injections/sandbox-mount-observer.ts` | Detects when instrumented elements mount in DOM |
|
|
357
|
+
| `src/injections/utils.ts` | `findElementsById()`, `updateElementClasses()` utilities |
|
|
358
|
+
| `tests/visual-edit-agent.test.ts` | Unit tests for element finding and class updates |
|
|
@@ -5,7 +5,7 @@ import * as t from "@babel/types";
|
|
|
5
5
|
import type { Plugin } from "vite";
|
|
6
6
|
|
|
7
7
|
// Helper function to check if JSX element contains dynamic content
|
|
8
|
-
function checkIfElementHasDynamicContent(jsxElement: any) {
|
|
8
|
+
export function checkIfElementHasDynamicContent(jsxElement: any) {
|
|
9
9
|
let hasDynamicContent = false;
|
|
10
10
|
|
|
11
11
|
// Helper function to check if any node contains dynamic patterns
|
|
@@ -88,6 +88,23 @@ function checkIfElementHasDynamicContent(jsxElement: any) {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Check the element's own attributes for dynamic content
|
|
92
|
+
const attributes = jsxElement.openingElement?.attributes || [];
|
|
93
|
+
attributes.forEach((attr: any) => {
|
|
94
|
+
if (hasDynamicContent) return; // Early exit if already found dynamic content
|
|
95
|
+
|
|
96
|
+
// Spread attributes like {...props} are always dynamic
|
|
97
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
98
|
+
hasDynamicContent = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check attribute values for dynamic expressions
|
|
103
|
+
if (t.isJSXAttribute(attr) && attr.value) {
|
|
104
|
+
traverseNode(attr.value);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
91
108
|
// Check all children of the JSX element
|
|
92
109
|
jsxElement.children.forEach((child: any) => {
|
|
93
110
|
if (hasDynamicContent) return; // Early exit if already found dynamic content
|