@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.
@@ -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