Skip to content

Commit c45205e

Browse files
authored
chore: Store all macro data on the client (adobe#9771)
* Store all macro debug data in the app * encode the data for css storage * fix encodings * fix memory leak, streamline look up, disable globals in tests
1 parent 6f758d8 commit c45205e

File tree

5 files changed

+118
-276
lines changed

5 files changed

+118
-276
lines changed

packages/@react-spectrum/s2/style/style-macro.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,23 @@ export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemePrope
407407
if (process.env.NODE_ENV !== 'production') {
408408
js += `let targetRules = rules + ${JSON.stringify(loc)};\n`;
409409
js += 'let hash = 5381;for (let i = 0; i < targetRules.length; i++) { hash = ((hash << 5) + hash) + targetRules.charCodeAt(i) >>> 0; }\n';
410-
js += 'rules += " -macro-dynamic-" + hash.toString(36);\n';
411-
js += `typeof window !== 'undefined' && window?.postMessage?.({action: 'stylemacro-update-macros', hash: hash.toString(36), loc: ${JSON.stringify(loc)}, style: currentRules}, "*");\n`;
410+
js += 'let hashStr = hash.toString(36);\n';
411+
js += 'rules += " -macro-dynamic-" + hashStr;\n';
412+
// Skip global __styleMacroDynamic__ in Jest so we dont' pollute the test environment and don't cause issues with timer advancement.
413+
if (!process.env.JEST_WORKER_ID) {
414+
js += 'if (typeof window !== "undefined") {\n';
415+
js += ' let g = window.__styleMacroDynamic__;\n';
416+
js += ' if (!g) {\n';
417+
js += ' g = window.__styleMacroDynamic__ = { map: {}, _timer: null };\n';
418+
js += ' g._timer = setInterval(function() {\n';
419+
js += ' for (let k in g.map) {\n';
420+
js += ' try { if (!document.querySelector("." + CSS.escape(k))) delete g.map[k]; } catch (e) {}\n';
421+
js += ' }\n';
422+
js += ' }, 300000);\n';
423+
js += ' }\n';
424+
js += ` g.map["-macro-dynamic-" + hashStr] = { style: currentRules, loc: ${JSON.stringify(loc)} };\n`;
425+
js += '}\n';
426+
}
412427
}
413428
js += 'return rules;';
414429
if (allowedOverrides) {

packages/dev/style-macro-chrome-plugin/README.md

Lines changed: 39 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,21 @@ This extension uses Chrome's standard extension architecture with three main com
6161
- **Location**: Runs in the actual page's JavaScript context
6262
- **Responsibility**:
6363
- Generates macro metadata (hash, location, styles) when style macro is evaluated
64-
- Hosts MutationObserver that watches selected element for className changes
65-
- **Storage**: None - static macros embed data in CSS, dynamic macros send messages
66-
- **Communication**:
67-
- For static macros: Embeds data in CSS custom property `--macro-data-{hash}` (unique per macro)
68-
- For dynamic macros: Sends `window.postMessage({ action: 'stylemacro-update-macros', hash, loc, style })` to content script
69-
- For className changes: Sends `window.postMessage({ action: 'stylemacro-class-changed', elementId })` to content script
64+
- Hosts MutationObserver (created by dev tool panel) that watches selected element for className changes
65+
- **Storage**:
66+
- Static macros: Data embedded in CSS rules with custom property `--macro-data-{hash}`
67+
- Dynamic macros: Data stored in global `window.__styleMacroDynamic__` (object with `map` of class name → `{ style, loc }` and an internal interval timer for cleanup)
7068

7169
#### 2. **Content Script** (`content-script.js`)
7270
- **Location**: Isolated sandboxed environment injected into the page
7371
- **Scope**: Acts as a message forwarder between page and extension
7472
- **Responsibility**:
75-
- Listens for `window.postMessage({ action: 'stylemacro-update-macros' })` from the page and forwards to background script
76-
- Forwards `window.postMessage({ action: 'stylemacro-class-changed' })` from page to background script
77-
- **Storage**: None - all macro data is stored in DevTools
73+
- Forwards `window.postMessage({ action: 'stylemacro-class-changed' })` from Mutation Observer on page to background script
74+
- **Storage**: None - all macro data is stored in the DOM via CSS custom properties
7875
- **Communication**:
7976
- Receives:
80-
- `window.postMessage({ action: 'stylemacro-update-macros', hash, loc, style })` from page
8177
- `window.postMessage({ action: 'stylemacro-class-changed', elementId })` from page
8278
- Sends:
83-
- `chrome.runtime.sendMessage({ action: 'stylemacro-update-macros', hash, loc, style })` to background
8479
- `chrome.runtime.sendMessage({ action: 'stylemacro-class-changed', elementId })` to background
8580

8681
#### 3. **Background Script** (`background.js`)
@@ -91,23 +86,19 @@ This extension uses Chrome's standard extension architecture with three main com
9186
- Receives:
9287
- `chrome.runtime.onConnect({ name: 'devtools-page' })` from DevTools
9388
- `port.onMessage({ type: 'stylemacro-init' })` from DevTools
94-
- `chrome.runtime.onMessage({ action: 'stylemacro-update-macros', hash, loc, style })` from content script
9589
- `chrome.runtime.onMessage({ action: 'stylemacro-class-changed', elementId })` from content script
9690
- Sends:
97-
- `port.postMessage({ action: 'stylemacro-update-macros', hash, loc, style })` to DevTools
9891
- `port.postMessage({ action: 'stylemacro-class-changed', elementId })` to DevTools
9992

10093
#### 4. **DevTools Panel** (`devtool.js`)
10194
- **Location**: DevTools sidebar panel context
10295
- **Responsibility**:
103-
- Stores all dynamic macro data in a local Map: `macroData[hash] = { loc, style }`
10496
- Extracts macro class names from selected element:
105-
- Static macros: `-macro-static-{hash}` → reads `--macro-data-{hash}` custom property via `getComputedStyle()`
106-
- Dynamic macros: `-macro-dynamic-{hash}`looks up data from local storage
97+
- Static macros: `-macro-static-{hash}` → reads `--macro-data-{hash}` custom property via `getComputedStyle()` on the element
98+
- Dynamic macros: `-macro-dynamic-{hash}`reads from `window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"]` (plain JSON)
10799
- Displays style information in sidebar
108100
- **Automatic Updates**: Sets up a MutationObserver on the selected element to detect className changes and automatically refreshes the panel
109-
- **Cleanup**: Every 5 minutes, checks the DOM for each stored hash and removes data for macros that no longer exist
110-
- **Storage**: `Map<hash, {loc: string, style: object}>` - stores all dynamic macro data
101+
- **Storage**: None - all data is read from CSS custom properties on demand
111102
- **Mutation Observer**:
112103
- Created when an element is selected via `chrome.devtools.panels.elements.onSelectionChanged`
113104
- Watches the selected element's `class` attribute for changes
@@ -117,7 +108,6 @@ This extension uses Chrome's standard extension architecture with three main com
117108
- Triggers automatic panel refresh when className changes
118109
- **Communication**:
119110
- Receives:
120-
- `port.onMessage({ action: 'stylemacro-update-macros', hash, loc, style })` from background (stores data and refreshes)
121111
- `port.onMessage({ action: 'stylemacro-class-changed', elementId })` from background (triggers refresh)
122112
- Sends:
123113
- `chrome.runtime.connect({ name: 'devtools-page' })` to establish connection
@@ -149,156 +139,51 @@ Static macros are generated when style macro conditions don't change at runtime.
149139

150140
**Key Design**: Each static macro has its own uniquely-named custom property (`--macro-data-{hash}`), which avoids CSS cascade issues when reading multiple macro data from the same element.
151141

152-
#### Flow 1b: Dynamic Macro Updates (Page → DevTools)
142+
#### Flow 1b: Dynamic Macro Updates (Page → Global Variable)
153143

154-
Dynamic macros are generated when style macro conditions can change at runtime. Updates are sent via message passing and stored directly in DevTools.
144+
Dynamic macros are generated when style macro conditions can change at runtime. Data is written to a global JavaScript variable; a timer on the same object cleans up entries whose class is no longer present in the DOM.
155145

156146
```
157147
┌─────────────────┐
158148
│ Page Context │
159-
│ (style-macro) │
160-
└────────┬────────┘
161-
│ window.postMessage({ action: 'stylemacro-update-macros', hash, loc, style })
162-
163-
┌─────────────────┐
164-
│ Content Script │ Forwards message (no storage)
165-
└────────┬────────┘
166-
│ chrome.runtime.sendMessage({ action: 'stylemacro-update-macros', hash, loc, style })
167-
168-
┌─────────────────┐
169-
│ Background │ Looks up DevTools connection for tabId
149+
│ (style-macro) │ Runtime evaluation with dynamic conditions
170150
└────────┬────────┘
171-
│ port.postMessage({ action: 'stylemacro-update-macros', hash, loc, style })
151+
│ 1. Ensure window.__styleMacroDynamic__ exists ({ map: {}, _timer })
152+
│ 2. Set map["-macro-dynamic-{hash}"] = { style, loc } (plain JSON)
153+
│ 3. Timer (e.g. every 5 min) removes entries with no matching element in DOM
172154
173155
┌─────────────────┐
174-
DevTools Panel │ Stores in macroData Map and triggers sidebar refresh
156+
Page (global) │ window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] = { style, loc }
175157
└─────────────────┘
176158
```
177159

178-
#### Flow 2: Display Macro Data (Synchronous Lookup)
160+
#### Flow 2: Display Macro Data (Synchronous CSS Lookup)
179161

180-
When the user selects an element or the panel refreshes, DevTools looks up macro data synchronously from its local storage.
162+
When the user selects an element or the panel refreshes, DevTools reads macro data as follows.
181163

182164
```
183165
┌─────────────────┐
184-
│ DevTools Panel │ User selects element with -macro-dynamic-{hash} class
166+
│ DevTools Panel │ User selects element with -macro-static-{hash} or -macro-dynamic-{hash} class
185167
└────────┬────────┘
186168
│ Extract hash from className
187169
188170
┌─────────────────┐
189-
│ DevTools Panel │ Look up macroData.get(hash)
190-
Local Storage │ Returns { loc, style } if available
171+
│ DevTools Panel │ Static: getComputedStyle($0).getPropertyValue('--macro-data-{hash}') → JSON.parse
172+
│ Dynamic: window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] → already { style, loc }
191173
└────────┬────────┘
192-
{ loc: "...", style: {...} } or null
174+
193175
194176
┌─────────────────┐
195-
│ DevTools Panel │ Display in sidebar (or show nothing if null)
177+
│ DevTools Panel │ Parses and displays in sidebar
196178
└─────────────────┘
197179
```
198180

199-
**Note**: If macro data hasn't been received yet for a hash, it will appear empty until the next `stylemacro-update-macros` message arrives and triggers a refresh.
181+
**Note**: Static macros are read from CSS custom properties on the inspected element. Dynamic macros are read from the page’s global `window.__styleMacroDynamic__.map` (no CSS involved).
200182

201-
#### Flow 3: Macro Data Cleanup (Automated)
202-
203-
Every 5 minutes, DevTools checks if stored macro hashes are still in use on the page and removes stale data.
204-
205-
```
206-
┌─────────────────┐
207-
│ DevTools Panel │ Every 5 minutes
208-
└────────┬────────┘
209-
│ For each hash in macroData Map:
210-
│ chrome.devtools.inspectedWindow.eval(
211-
│ `!!document.querySelector('.-macro-dynamic-${hash}')`
212-
│ )
213-
214-
┌─────────────────┐
215-
│ Page DOM │ Checks if elements with macro classes exist
216-
└────────┬────────┘
217-
│ Returns true/false for each hash
218-
219-
┌─────────────────┐
220-
│ DevTools Panel │ Removes stale entries from macroData Map
221-
│ │ macroData.delete(hash) for non-existent elements
222-
└─────────────────┘
223-
```
224-
225-
#### Flow 4: Automatic Updates on className Changes (MutationObserver)
183+
#### Flow 3: Automatic Updates on className Changes (MutationObserver)
226184

227185
When you select an element, the DevTools panel automatically watches for className changes and refreshes the panel.
228186

229-
```
230-
┌─────────────────┐
231-
│ DevTools Panel │ User selects element in Elements panel
232-
└────────┬────────┘
233-
│ chrome.devtools.panels.elements.onSelectionChanged
234-
235-
│ chrome.devtools.inspectedWindow.eval(`
236-
│ // Disconnect old observer (if any)
237-
│ if (window.__styleMacroObserver) {
238-
│ window.__styleMacroObserver.disconnect();
239-
│ }
240-
241-
│ // Create new MutationObserver on $0
242-
│ window.__styleMacroObserver = new MutationObserver(() => {
243-
│ window.postMessage({
244-
│ action: 'stylemacro-class-changed',
245-
│ elementId: $0.__devtoolsId
246-
│ }, '*');
247-
│ });
248-
249-
│ window.__styleMacroObserver.observe($0, {
250-
│ attributes: true,
251-
│ attributeFilter: ['class']
252-
│ });
253-
│ `)
254-
255-
┌─────────────────┐
256-
│ Page DOM │ MutationObserver active on selected element
257-
└────────┬────────┘
258-
259-
│ ... User interacts with page, element's className changes ...
260-
261-
│ MutationObserver detects class attribute change
262-
│ window.postMessage({ action: 'stylemacro-class-changed', elementId }, '*')
263-
264-
┌─────────────────┐
265-
│ Content Script │ Receives window message, forwards to extension
266-
└────────┬────────┘
267-
│ chrome.runtime.sendMessage({ action: 'stylemacro-class-changed', elementId })
268-
269-
┌─────────────────┐
270-
│ Background │ Looks up DevTools connection for tabId
271-
└────────┬────────┘
272-
│ port.postMessage({ action: 'stylemacro-class-changed', elementId })
273-
274-
┌─────────────────┐
275-
│ DevTools Panel │ Verifies elementId matches currently selected element
276-
│ │ Triggers full panel refresh (re-reads classes, re-queries macros)
277-
└─────────────────┘
278-
279-
When selection changes or panel closes:
280-
281-
┌─────────────────┐
282-
│ DevTools Panel │ Calls disconnectObserver()
283-
└────────┬────────┘
284-
│ chrome.devtools.inspectedWindow.eval(`
285-
│ if (window.__styleMacroObserver) {
286-
│ window.__styleMacroObserver.disconnect();
287-
│ window.__styleMacroObserver = null;
288-
│ }
289-
│ `)
290-
291-
┌─────────────────┐
292-
│ Page DOM │ Old observer disconnected, new observer created for new selection
293-
└─────────────────┘
294-
```
295-
296-
**Key Benefits:**
297-
- Panel automatically refreshes when element classes change (e.g., hover states, conditional styles)
298-
- No manual refresh needed
299-
- Observer is cleaned up properly to prevent memory leaks
300-
- Each element has its own unique tracking ID to prevent cross-contamination
301-
302187
### Key Technical Details
303188

304189
#### Why Background Script is Needed
@@ -310,64 +195,47 @@ The style macro generates different class name patterns based on whether the sty
310195

311196
**Static Macros** (`-macro-static-{hash}`):
312197
- Used when all style conditions are static (e.g., `style({ color: 'red' })`)
313-
- Macro data is embedded in CSS as a uniquely-named custom property: `--macro-data-{hash}: '{...JSON...}'`
198+
- Macro data is embedded in CSS rules as a uniquely-named custom property: `--macro-data-{hash}: '{...JSON...}'`
314199
- DevTools reads the specific custom property via `getComputedStyle($0).getPropertyValue('--macro-data-{hash}')`
315-
- Unique naming avoids CSS cascade issues when multiple macros are applied to the same element
316200

317201
**Dynamic Macros** (`-macro-dynamic-{hash}`):
318202
- Used when style conditions can change (e.g., `style({color: {default: 'blue', isActive: 'red'}})`)
319-
- Macro data is sent via `window.postMessage({ action: 'stylemacro-update-macros', ... })` whenever conditions change
320-
- Content script forwards data to DevTools, which stores it in a local Map
203+
- Macro data is stored in `window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"]` as plain JSON `{ style, loc }`
204+
- A timer on `window.__styleMacroDynamic__` periodically removes map entries whose class is no longer used in the DOM
205+
- DevTools reads via `window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"]`
321206
- Enables real-time updates when props/state change
322207

323-
#### Data Storage
324-
- **Static Macros**: Data embedded in CSS as uniquely-named custom properties `--macro-data-{hash}`, read via `getComputedStyle($0).getPropertyValue('--macro-data-{hash}')`
325-
- Each macro has its own custom property name to prevent cascade conflicts
326-
- Example: `.-macro-static-abc123 { --macro-data-abc123: '{"style": {...}, "loc": "..."}'; }`
327-
- **Dynamic Macros**: Data stored in DevTools panel's `macroData` Map
328-
- **No Content Script Storage**: Content script only forwards messages, doesn't store macro data
329-
- **Lifetime**: Macro data persists in DevTools for the duration of the DevTools session
330-
- **Cleanup**: Stale macro data (for elements no longer in DOM) is removed every 5 minutes
331-
332208
#### Connection Management
333209
- **DevTools → Background**: Uses persistent `chrome.runtime.connect()` with port-based messaging
334210
- **Content Script → Background**: Uses one-time `chrome.runtime.sendMessage()` calls
335211
- **Background tracks**: Map of `tabId → DevTools port` for routing messages
336212

337213
#### Data Structure
338214

339-
**Static Macros (in CSS):**
215+
**Static Macros (in main CSS):**
340216
```css
341217
.-macro-static-zsZ9Dc {
342218
--macro-data-zsZ9Dc: '{"style":{"paddingX":"4"},"loc":"packages/@react-spectrum/s2/src/Button.tsx:67"}';
343219
}
344220
```
345221

346-
**Dynamic Macros (in DevTools panel's macroData Map):**
222+
**Dynamic Macros (in page global):**
347223
```javascript
348-
Map {
349-
"zsZ9Dc" => {
350-
loc: "packages/@react-spectrum/s2/src/Button.tsx:67",
351-
style: {
352-
"paddingX": "4",
353-
// ... more CSS properties
354-
}
355-
}
356-
}
224+
window.__styleMacroDynamic__ = {
225+
map: {
226+
"-macro-dynamic-zsZ9Dc": { style: { paddingX: "4" }, loc: "packages/@react-spectrum/s2/src/Button.tsx:67" },
227+
"-macro-dynamic-abc123": { style: {...}, loc: "..." }
228+
},
229+
_timer: 123 // setInterval for cleanup of unused entries
230+
};
357231
```
358232

359-
**Note**:
360-
- Static macro data is stored in CSS with uniquely-named custom properties
361-
- Dynamic macro data is stored directly in the DevTools panel context
362-
- The content script acts purely as a message forwarder and doesn't store any data
363-
364233
#### Message Types
365234

366235
| Message Type | Direction | Purpose |
367236
|-------------|-----------|---------|
368-
| `stylemacro-update-macros` | Page → Content → Background → DevTools | Send macro data (hash, loc, style) to be stored in DevTools |
369237
| `stylemacro-init` | DevTools → Background | Establish connection with tabId |
370-
| `stylemacro-class-changed` | Page → Content → Background → DevTools | Notify that selected element's className changed |
238+
| `stylemacro-class-changed` | Page → Content → Background → DevTools | Notify that selected element's className changed, triggering panel refresh |
371239

372240
### Debugging
373241

packages/dev/style-macro-chrome-plugin/src/background.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3636
}
3737

3838
// Forward messages from content script to DevTools
39-
if (message.action === 'stylemacro-update-macros' || message.action === 'stylemacro-class-changed') {
39+
if (message.action === 'stylemacro-class-changed') {
4040
console.log(`[Background] Forwarding ${message.action} from content script to DevTools, tabId: ${tabId}`);
4141
const devtoolsPort = devtoolsConnections.get(tabId);
4242
if (devtoolsPort) {

0 commit comments

Comments
 (0)